# Referencia Completa — PostgreSQL Experto

## Arrays Nativos de PostgreSQL

```sql
-- Columna de array
CREATE TABLE articulos (
    id SERIAL PRIMARY KEY,
    titulo TEXT NOT NULL,
    etiquetas TEXT[] NOT NULL DEFAULT '{}'
);

-- Insertar con array
INSERT INTO articulos (titulo, etiquetas)
VALUES ('Intro a SQL', ARRAY['sql', 'basico', 'tutorial']);

-- Operadores de array
SELECT * FROM articulos WHERE 'sql' = ANY(etiquetas);       -- Contiene elemento
SELECT * FROM articulos WHERE etiquetas @> ARRAY['sql'];    -- Contiene subconjunto
SELECT * FROM articulos WHERE etiquetas && ARRAY['sql', 'python'];  -- Intersección

-- Índice GIN para arrays
CREATE INDEX idx_articulos_etiquetas ON articulos USING GIN(etiquetas);

-- Unnest de array a filas
SELECT titulo, unnest(etiquetas) AS etiqueta FROM articulos;

-- Funciones de array
SELECT array_length(etiquetas, 1) AS num_etiquetas FROM articulos;
SELECT array_append(etiquetas, 'nuevo') FROM articulos WHERE id = 1;
SELECT array_remove(etiquetas, 'obsoleto') FROM articulos WHERE id = 1;
```

---

## Tipos Personalizados

### ENUM

```sql
-- Crear tipo enum
CREATE TYPE estatus_factura AS ENUM ('borrador', 'emitida', 'cancelada', 'pagada');

-- Usar en tabla
ALTER TABLE facturas
ADD COLUMN estatus estatus_factura NOT NULL DEFAULT 'borrador';

-- Agregar valor a un enum existente (sin drop/recreate en PostgreSQL 9.1+)
ALTER TYPE estatus_factura ADD VALUE 'en_revision' AFTER 'borrador';
```

### Tipos Compuestos

```sql
-- Tipo compuesto para dirección
CREATE TYPE direccion AS (
    calle TEXT,
    numero TEXT,
    colonia TEXT,
    ciudad TEXT,
    cp CHAR(5)
);

-- Usar en tabla
ALTER TABLE clientes ADD COLUMN domicilio_fiscal direccion;

-- Insertar
UPDATE clientes
SET domicilio_fiscal = ROW('Insurgentes Sur', '1234', 'Del Valle', 'CDMX', '03100')::direccion
WHERE id = ?;

-- Acceder a campos
SELECT (domicilio_fiscal).ciudad FROM clientes WHERE id = ?;
```

### Dominio (tipo con constraint)

```sql
CREATE DOMAIN rfc_mx AS TEXT
    CHECK (VALUE ~ '^[A-Z]{3,4}[0-9]{6}[A-Z0-9]{3}$');

CREATE DOMAIN email_valido AS TEXT
    CHECK (VALUE ~ '^[^@]+@[^@]+\.[^@]+$');

-- Usar el dominio
ALTER TABLE clientes
    ALTER COLUMN rfc TYPE rfc_mx USING rfc::rfc_mx,
    ALTER COLUMN email TYPE email_valido USING email::email_valido;
```

---

## Funciones Almacenadas (PL/pgSQL)

```sql
-- Función para calcular total de factura
CREATE OR REPLACE FUNCTION calcular_total_factura(p_factura_id UUID)
RETURNS DECIMAL(12, 2)
LANGUAGE plpgsql
STABLE  -- No modifica BD, mismo input = mismo output en la transacción
AS $$
DECLARE
    v_subtotal DECIMAL(12, 2);
    v_tasa_iva DECIMAL(4, 2);
BEGIN
    SELECT
        SUM(cantidad * precio_unitario),
        MAX(tasa_iva)
    INTO v_subtotal, v_tasa_iva
    FROM items_factura
    WHERE factura_id = p_factura_id;

    IF v_subtotal IS NULL THEN
        RAISE EXCEPTION 'Factura % no encontrada o sin ítems', p_factura_id;
    END IF;

    RETURN v_subtotal * (1 + v_tasa_iva);
END;
$$;

-- Trigger para auditoría automática
CREATE OR REPLACE FUNCTION trigger_auditoria()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO auditoria (
        tabla, operacion, fila_anterior, fila_nueva,
        usuario_db, timestamp
    ) VALUES (
        TG_TABLE_NAME,
        TG_OP,
        CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
        CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
        current_user,
        NOW()
    );
    RETURN COALESCE(NEW, OLD);
END;
$$;

CREATE TRIGGER auditoria_facturas
AFTER INSERT OR UPDATE OR DELETE ON facturas
FOR EACH ROW EXECUTE FUNCTION trigger_auditoria();
```

---

## Configuración de Performance

```sql
-- Ver configuración actual
SHOW shared_buffers;        -- Cache de PostgreSQL (recomendado: 25% de RAM)
SHOW effective_cache_size;  -- Estimación de cache total del SO (75% de RAM)
SHOW work_mem;              -- Memoria por operación de sort/hash
SHOW max_connections;

-- Queries lentas en pg_stat_statements
SELECT
    query,
    calls,
    total_exec_time / 1000 AS total_seg,
    mean_exec_time AS avg_ms,
    rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- Tablas con más seq scans (candidatos para índices)
SELECT
    schemaname,
    tablename,
    seq_scan,
    idx_scan,
    n_live_tup AS filas
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
  AND n_live_tup > 10000
ORDER BY seq_scan DESC;

-- Índices no usados (candidatos para eliminar)
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan AS veces_usado,
    pg_size_pretty(pg_relation_size(indexrelid)) AS tamano
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND indexname NOT LIKE '%_pkey'
ORDER BY pg_relation_size(indexrelid) DESC;
```

---

## Extensiones Útiles

```sql
-- uuid-ossp o gen_random_uuid() (nativa en PostgreSQL 13+)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
SELECT uuid_generate_v4();     -- uuid-ossp
SELECT gen_random_uuid();      -- nativa (preferir esta)

-- pg_trgm: búsqueda difusa con LIKE/ILIKE eficiente
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX ON clientes USING GIN(nombre gin_trgm_ops);
SELECT * FROM clientes WHERE nombre % 'empresa acme';  -- similitud

-- pgcrypto: hashing y cifrado
CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT crypt('contraseña', gen_salt('bf'));  -- Bcrypt

-- pg_stat_statements: monitoreo de queries
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
```
