Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
Plan de ejecución, índices avanzados, concurrencia, particionamiento, replicación, backup, seguridad y diagnóstico: 200 comandos para producción.
Aquí el SQL se encuentra con la infraestructura. Estos 200 elementos son lo que se usa cuando la base ya está en producción, con volumen, concurrencia y alguien al teléfono preguntando por qué el informe está lento.
Plan de ejecución, índices avanzados, aislamiento, particionado, replicación, backup, seguridad y diagnóstico — con la sintaxis de cada base donde diverge.
Aviso: varios comandos de aquí alteran el comportamiento del servidor. Pruébalos antes en preproducción, y entiende qué hace cada uno antes de ejecutarlo en producción.
Frames, distribución y el patrón gaps-and-islands — análisis serio sin salir del SQL.
Define la ventana por un número de filas físicas antes/después de la actual.
SELECT dia, valor,
AVG(valor) OVER (ORDER BY dia ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS media_7d
FROM metricas;Define la ventana por valor, no por posición: los empates entran juntos.
SELECT valor, SUM(valor) OVER (ORDER BY valor RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
FROM vendas;Cuenta los grupos de valores iguales como una unidad (PostgreSQL 11+).
SELECT dia, SUM(valor) OVER (ORDER BY dia GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW)
FROM metricas;Quita la fila actual (o su grupo) del cálculo — la media de los demás, no la propia.
SELECT id, AVG(nota) OVER (PARTITION BY turma ORDER BY id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW) AS media_dos_outros
FROM provas;La posición relativa de 0 a 1 dentro de la partición.
SELECT nome, total, ROUND(PERCENT_RANK() OVER (ORDER BY total)::numeric, 3) AS percentil
FROM vendedores;La distribución acumulada: la proporción de filas con un valor menor o igual.
SELECT nome, CUME_DIST() OVER (ORDER BY salario) AS acumulada FROM funcionarios;Un percentil interpolado — la mediana verdadera, insensible a los valores atípicos.
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) AS mediana FROM pedidos;Un percentil discreto: devuelve un valor que existe de hecho en el conjunto.
SELECT PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY duracao_ms) AS p90 FROM requisicoes;El valor más frecuente del grupo.
SELECT MODE() WITHIN GROUP (ORDER BY categoria) AS mais_vendida FROM pedidos;Coge el enésimo valor de la ventana.
SELECT cliente_id, NTH_VALUE(total, 2) OVER (PARTITION BY cliente_id ORDER BY criado_em) AS segundo_pedido
FROM pedidos;Detecta secuencias continuas: la diferencia entre la fecha y el ROW_NUMBER es constante dentro de una isla.
SELECT usuario_id, MIN(dia) AS inicio, MAX(dia) AS fim, COUNT(*) AS dias_seguidos
FROM (
SELECT usuario_id, dia,
dia - (ROW_NUMBER() OVER (PARTITION BY usuario_id ORDER BY dia))::int AS grupo
FROM acessos
) ace_num
GROUP BY usuario_id, grupo;Agrupa eventos en sesiones cuando el intervalo entre ellos supera un umbral.
SELECT *, SUM(nova_sessao) OVER (PARTITION BY usuario_id ORDER BY quando) AS sessao
FROM (
SELECT *, CASE WHEN quando - LAG(quando) OVER (PARTITION BY usuario_id ORDER BY quando)
> INTERVAL '30 minutes' THEN 1 ELSE 0 END AS nova_sessao
FROM eventos
) eve_marcado;Compara el conjunto de usuarios entre periodos con una window + join.
WITH usu_mes AS (
SELECT DISTINCT usuario_id, DATE_TRUNC('month', quando) AS mes
FROM eventos
)
SELECT atual.mes,
COUNT(*) FILTER (WHERE seg.usuario_id IS NOT NULL) AS retidos
FROM usu_mes atual
LEFT JOIN usu_mes seg
ON seg.usuario_id = atual.usuario_id
AND seg.mes = atual.mes + INTERVAL '1 month'
GROUP BY atual.mes
ORDER BY atual.mes;RANK en lugar de ROW_NUMBER cuando los empates deben entrar juntos.
SELECT *
FROM (
SELECT prod.*,
RANK() OVER (PARTITION BY prod.categoria ORDER BY prod.vendas DESC) AS posicao
FROM produtos prod
) prod_rank
WHERE posicao <= 3;Compara cada fila con el mejor de su partición.
SELECT nome, categoria, vendas,
MAX(vendas) OVER (PARTITION BY categoria) - vendas AS distancia_do_topo
FROM produtos;RANGE con INTERVAL crea ventanas por tiempo real, no por número de filas.
SELECT quando, valor,
SUM(valor) OVER (ORDER BY quando RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW) AS ultimos_7d
FROM transacoes;Repite el último valor conocido (last value carried forward).
SELECT dia, cotacao,
COALESCE(cotacao, (array_agg(cotacao) FILTER (WHERE cotacao IS NOT NULL)
OVER (ORDER BY dia ROWS UNBOUNDED PRECEDING))[COUNT(cotacao) OVER (ORDER BY dia ROWS UNBOUNDED PRECEDING)]) AS preenchida
FROM cotacoes;Cada ventana distinta puede generar un sort. Reaprovecha la misma cláusula OVER siempre que puedas.
-- Un solo sort: la misma ventana con nombre
SELECT SUM(v) OVER w, AVG(v) OVER w, COUNT(*) OVER w
FROM t WINDOW w AS (PARTITION BY g ORDER BY d);Agregaciones multidimensionales y recorrido de grafos directamente en la base.
Varios GROUP BY en una sola consulta, sin UNION ALL.
SELECT regiao, produto, SUM(valor)
FROM vendas
GROUP BY GROUPING SETS ((regiao, produto), (regiao), ());Subtotales jerárquicos + total general.
SELECT ano, mes, SUM(valor) FROM vendas GROUP BY ROLLUP (ano, mes);Todas las combinaciones posibles de subtotales.
SELECT regiao, canal, SUM(valor) FROM vendas GROUP BY CUBE (regiao, canal);Dice si la fila es un subtotal — evita confundirlo con un NULL del dato.
SELECT regiao, GROUPING(regiao) AS eh_total, SUM(valor)
FROM vendas GROUP BY ROLLUP (regiao);Las filas se convierten en columnas. Nativo en SQL Server y Oracle; en PostgreSQL usa CASE o crosstab.
SELECT *
FROM vendas ven
PIVOT (SUM(ven.valor) FOR ven.mes IN ([1],[2],[3])) AS ven_pivot; -- SQL ServerLas columnas se convierten en filas — normaliza una hoja de cálculo importada.
SELECT produto, mes, valor
FROM vendas_larga ven_larga
UNPIVOT (valor FOR mes IN (jan, fev, mar)) AS ven_normalizada; -- SQL Server / OracleUn pivote dinámico vía la extensión tablefunc.
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT * FROM crosstab('SELECT produto, mes, valor FROM vendas ORDER BY 1,2')
AS t(produto TEXT, jan NUMERIC, fev NUMERIC);Una CTE recursiva recorre relaciones a una profundidad arbitraria.
WITH RECURSIVE caminho AS (
SELECT origem, destino, 1 AS saltos FROM rotas WHERE origem = 'GRU'
UNION ALL
SELECT cam.origem, rot.destino, cam.saltos + 1
FROM caminho cam
INNER JOIN rotas rot
ON rot.origem = cam.destino
WHERE cam.saltos < 4
)
SELECT DISTINCT destino,
MIN(saltos)
FROM caminho
GROUP BY destino;Lleva el camino recorrido y párate cuando se repita un nodo.
WITH RECURSIVE arvore AS (
SELECT id, pai_id, ARRAY[id] AS caminho
FROM nos
WHERE pai_id IS NULL
UNION ALL
SELECT nos.id, nos.pai_id, arv.caminho || nos.id
FROM nos
INNER JOIN arvore arv
ON nos.pai_id = arv.id
WHERE NOT nos.id = ANY(arv.caminho)
)
SELECT *
FROM arvore;PostgreSQL 14+ marca los ciclos sin que montes el array a mano.
WITH RECURSIVE arvore AS (
SELECT id, pai_id
FROM nos
WHERE pai_id IS NULL
UNION ALL
SELECT nos.id, nos.pai_id
FROM nos
INNER JOIN arvore arv
ON nos.pai_id = arv.id
) CYCLE id SET tem_ciclo USING caminho
SELECT *
FROM arvore;Controla si la recursión es en anchura o en profundidad.
WITH RECURSIVE t AS (...) SEARCH DEPTH FIRST BY id SET ordem
SELECT * FROM t ORDER BY ordem;Lista de materiales: componentes de componentes, con la cantidad acumulada.
WITH RECURSIVE bom AS (
SELECT peca_id, componente_id, qtd FROM estrutura WHERE peca_id = 100
UNION ALL
SELECT bom.peca_id, estr.componente_id, bom.qtd * estr.qtd
FROM bom
INNER JOIN estrutura estr
ON estr.peca_id = bom.componente_id
)
SELECT componente_id,
SUM(qtd)
FROM bom
GROUP BY componente_id;Dejar de adivinar. El plan dice exactamente por dónde se va el tiempo.
Muestra el plan que el optimizador pretende usar, sin ejecutarlo.
EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 42;Lo ejecuta de verdad y compara la estimación con la realidad. Es el comando que resuelve la mayoría de los casos.
EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 42;Muestra la lectura de caché y de disco — separa un problema de E/S de uno de CPU.
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM pedidos WHERE total > 1000;El formato ideal para las herramientas de visualización de planes.
EXPLAIN (ANALYZE, FORMAT JSON) SELECT * FROM pedidos;Un recorrido completo. Malo para pocas filas; excelente cuando lees casi todo de todas formas.
-- Si aparece un Seq Scan en un filtro selectivo, falta un índice (o la estadística está vieja).Un Index Only Scan ni siquiera toca la tabla: todas las columnas vinieron del índice.
CREATE INDEX idx_cob ON pedidos (cliente_id) INCLUDE (total);
EXPLAIN SELECT cliente_id, total FROM pedidos WHERE cliente_id = 1;Combina varios índices o lee muchas filas dispersas en orden de página.
EXPLAIN SELECT * FROM pedidos WHERE status = 'pago' AND cliente_id < 500;Bueno cuando un lado es pequeño y el otro tiene índice. Pésimo cuando los dos son grandes.
-- Un Nested Loop con millones de filas en ambos lados = una consulta atascadaMonta una tabla hash del lado menor. Rápido para volúmenes grandes, consume memoria.
SET work_mem = '128MB'; -- un hash que cabe en memoria evita ir al discoUne dos conjuntos ya ordenados. Excelente cuando los índices ya entregan el orden.
EXPLAIN
SELECT *
FROM pedidos ped
INNER JOIN itens ite
ON ite.pedido_id = ped.id; -- busca Merge Join en el planUn rows=1000 estimado frente a actual rows=2000000 es el síntoma clásico de una estadística desactualizada.
ANALYZE pedidos; -- y ejecuta el EXPLAIN ANALYZE de nuevoEl coste es una unidad relativa del optimizador, no segundos. Sirve para comparar planes entre sí.
EXPLAIN SELECT * FROM pedidos; -- cost=0.00..1234.56external merge Disk en el plan significa que faltó work_mem.
SET work_mem = '64MB';
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM pedidos ORDER BY total;Un filtro que preserva el índice. Una función aplicada a la columna lo rompe.
-- No usa el índice
WHERE UPPER(nome) = 'ANA'
-- Sí lo usa (con un índice por expresión)
CREATE INDEX ON clientes (UPPER(nome));Las columnas de más impiden el Index Only Scan y mueven datos inútiles.
SELECT id, total FROM pedidos WHERE cliente_id = 1; -- y no SELECT *Un OR entre columnas distintas suele convertirse en un Seq Scan. UNION ALL lo resuelve.
SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2 AND a <> 1;OFFSET 100000 lee y descarta 100 mil filas. Pagina por clave (keyset pagination).
-- Lento
SELECT * FROM pedidos ORDER BY id LIMIT 20 OFFSET 100000;
-- Rápido
SELECT * FROM pedidos WHERE id > 100000 ORDER BY id LIMIT 20;Para saber si algo existe, párate en la primera fila.
-- Mal
SELECT COUNT(*) FROM pedidos WHERE cliente_id = 1;
-- Bien
SELECT EXISTS (SELECT 1 FROM pedidos WHERE cliente_id = 1);Mil consultas de una fila cuestan mucho más que una consulta de mil filas.
SELECT * FROM pedidos WHERE cliente_id = ANY($1); -- un solo round-tripUn cálculo pesado reutilizado varias veces merece una tabla temporal o una vista materializada.
CREATE TEMP TABLE base AS SELECT ... ;
ANALYZE base;Para diagnosticar: desactiva un método y mira si el plan alternativo es mejor.
SET enable_seqscan = off;
EXPLAIN ANALYZE SELECT ...;
RESET enable_seqscan;Algunas bases permiten instruir al optimizador directamente. Último recurso.
SELECT /*+ INDEX(p idx_pedidos_cliente) */ * FROM pedidos ped WHERE cliente_id = 1;Impide que una consulta mala monopolice el servidor.
SET statement_timeout = '30s';Volúmenes y estadísticas distintos generan planes distintos. Valida siempre con datos realistas.
-- Compara el EXPLAIN de los dos entornos antes de concluir que 'aquí va rápido'.Más allá del B-tree: la estructura correcta para cada tipo de búsqueda.
El estándar: igualdad, intervalo y ordenación. Resuelve el 90% de los casos.
CREATE INDEX idx_padrao ON pedidos (criado_em);Solo igualdad, sin ordenación. Un nicho estrecho en el PostgreSQL moderno.
CREATE INDEX idx_hash ON sessoes USING HASH (token);Para valores compuestos: arrays, JSONB y búsqueda textual.
CREATE INDEX idx_gin ON eventos USING GIN (payload);Estructuras geométricas, intervalos y vecindad.
CREATE INDEX idx_gist ON reservas USING GIST (periodo);Minúsculo, para tablas gigantes con datos naturalmente ordenados (series temporales).
CREATE INDEX idx_brin ON logs USING BRIN (criado_em);Estructuras particionadas: datos no balanceados, prefijos, quadtrees.
CREATE INDEX idx_spgist ON pontos USING SPGIST (coord);Búsqueda por palabras con ranking, no con LIKE.
CREATE INDEX idx_busca ON artigos USING GIN (to_tsvector('portuguese', titulo || ' ' || corpo));
SELECT * FROM artigos WHERE to_tsvector('portuguese', corpo) @@ plainto_tsquery('portuguese', 'banco de dados');Acelera el LIKE '%medio%' y la búsqueda por similitud.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_trgm ON clientes USING GIN (nome gin_trgm_ops);
SELECT * FROM clientes WHERE nome ILIKE '%silva%';Ordena por proximidad textual — una búsqueda tolerante a las erratas.
SELECT cli.nome,
similarity(cli.nome, 'jonatan') AS similaridade
FROM clientes cli
ORDER BY similaridade DESC
LIMIT 10;En SQL Server y MySQL/InnoDB, la tabla está físicamente ordenada por la clave primaria.
CREATE CLUSTERED INDEX ix_pedidos ON pedidos (criado_em); -- SQL ServerReordena físicamente la tabla según un índice. Mejora la lectura por rango; hay que rehacerlo periódicamente.
CLUSTER pedidos USING idx_pedidos_data;Deja espacio libre en la página para las actualizaciones, reduciendo la fragmentación.
CREATE INDEX idx_x ON t (col) WITH (fillfactor = 80);Una columna con 2 valores rara vez compensa — salvo como índice parcial sobre el valor raro.
CREATE INDEX idx_erro ON logs (criado_em) WHERE nivel = 'ERROR';Cero recorridos en producción = candidato a eliminación.
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY relname;La misma columna inicial en varios índices suele indicar redundancia.
SELECT indrelid::regclass, array_agg(indexrelid::regclass)
FROM pg_index GROUP BY indrelid, indkey HAVING COUNT(*) > 1;Un índice muy actualizado se hincha y pierde eficiencia. REINDEX CONCURRENTLY lo resuelve sin downtime.
REINDEX INDEX CONCURRENTLY idx_pedidos_cliente;Si el índice ya entrega el orden, la base lee solo las primeras filas.
CREATE INDEX idx_top ON pedidos (criado_em DESC);
SELECT * FROM pedidos ORDER BY criado_em DESC LIMIT 10;Primero la igualdad, después el intervalo. (status, criado_em) sirve para status = X AND criado_em > Y.
CREATE INDEX idx_ordem ON pedidos (status, criado_em);Si filtras por IS NULL con frecuencia, un índice parcial queda mucho más pequeño.
CREATE INDEX idx_sem_processar ON pedidos (id) WHERE processado_em IS NULL;Mide el impacto en la escritura: un índice que acelera un informe y retrasa 10 mil INSERT por minuto puede no compensar.
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes ORDER BY pg_relation_size(indexrelid) DESC;Lo que ocurre cuando mil sesiones quieren la misma fila.
Cada transacción ve una foto consistente de la base; leer no bloquea escribir.
-- PostgreSQL y Oracle usan MVCC por defecto:
-- los lectores no bloquean a los escritores y viceversa.La misma consulta devuelve valores distintos dentro de la transacción. Desaparece en REPEATABLE READ.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;Aparecen filas nuevas en medio de la transacción. Desaparece en SERIALIZABLE.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;Dos transacciones leen, validan y graban — cada una correcta por sí sola, juntas violan la regla. Solo SERIALIZABLE lo evita.
-- Dos médicos de guardia saliendo a la vez: cada transacción ve al otro de guardia.FOR UPDATE bloquea las filas hasta el final de la transacción.
SELECT * FROM contas WHERE id = 1 FOR UPDATE;Un lock más débil: permite otras operaciones que no alteran la clave.
SELECT * FROM pedidos WHERE id = 1 FOR NO KEY UPDATE;Impide la modificación, pero permite otras lecturas bloqueadas.
SELECT * FROM produtos WHERE id = 1 FOR SHARE;Bloquea la tabla entera. Úsalo con mucha parsimonia y con una transacción cortísima.
BEGIN;
LOCK TABLE inventario IN EXCLUSIVE MODE;
-- operación crítica
COMMIT;Un lock con nombre puesto por la aplicación, sin tabla de por medio. Garantiza que solo una instancia ejecuta la rutina.
SELECT pg_try_advisory_lock(12345);
-- rutina exclusiva
SELECT pg_advisory_unlock(12345);Diagnóstico de bloqueos: quién retiene qué.
SELECT loc.pid,
loc.mode,
loc.granted,
cla.relname
FROM pg_locks loc
LEFT JOIN pg_class cla
ON cla.oid = loc.relation
WHERE NOT loc.granted;Muestra la cadena de bloqueo directo.
SELECT pid, pg_blocking_pids(pid) AS bloqueado_por, query
FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;Desiste de esperar el lock en lugar de acumular cola.
SET lock_timeout = '5s';Mata una transacción abierta y olvidada — la mayor causa de locks y de hinchazón.
SET idle_in_transaction_session_timeout = '60s';SKIP LOCKED deja que varios workers consuman la misma tabla sin colisionar.
UPDATE fila SET status = 'processando'
WHERE id = (SELECT id FROM fila WHERE status = 'pendente'
ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1)
RETURNING *;En SERIALIZABLE, la aplicación necesita repetir la transacción abortada. No es un bug, es el contrato.
-- Código de la aplicación: captura el SQLSTATE 40001 e inténtalo de nuevo (con backoff).Un deadlock casi siempre viene de órdenes distintos. Estandariza el orden de las tablas y de las claves.
-- Siempre: contas (el id menor primero) -> lancamentosCuando la tabla se vuelve demasiado grande para tratarla como una sola cosa.
El caso más común: una partición por mes de datos temporales.
CREATE TABLE eventos (id BIGSERIAL, criado_em DATE NOT NULL, dados JSONB)
PARTITION BY RANGE (criado_em);Cada rango se convierte en una tabla física propia.
CREATE TABLE eventos_2026_01 PARTITION OF eventos
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');Una partición por valor discreto: región, país, tenant.
CREATE TABLE clientes (id BIGINT, regiao TEXT) PARTITION BY LIST (regiao);
CREATE TABLE clientes_sul PARTITION OF clientes FOR VALUES IN ('PR','SC','RS');Distribuye uniformemente cuando no hay un criterio natural.
CREATE TABLE sessoes (id BIGINT) PARTITION BY HASH (id);
CREATE TABLE sessoes_0 PARTITION OF sessoes FOR VALUES WITH (MODULUS 4, REMAINDER 0);Recibe lo que no encaja con ningún rango — evita el error de inserción.
CREATE TABLE eventos_outros PARTITION OF eventos DEFAULT;La ganancia real: la base lee solo las particiones que el filtro alcanza. Confírmalo en el EXPLAIN.
EXPLAIN SELECT * FROM eventos WHERE criado_em >= DATE '2026-01-01';Creado en el padre, se propaga a todas las particiones.
CREATE INDEX idx_eventos_data ON eventos (criado_em);Desvincula la partición antigua sin borrarla: se convierte en una tabla normal para exportar.
ALTER TABLE eventos DETACH PARTITION eventos_2025_01;Trae una tabla existente dentro del conjunto particionado (valida el rango).
ALTER TABLE eventos ATTACH PARTITION eventos_2026_02
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');Borrar datos antiguos se convierte en una operación instantánea, sin un DELETE de millones de filas.
DROP TABLE eventos_2024_01;Programa la creación con antelación: una partición que falta tumba la inserción.
-- Una rutina mensual que crea los 3 meses siguientes.
-- O usa pg_partman.La clave de partición tiene que formar parte de la clave primaria y de las restricciones únicas.
CREATE TABLE eventos (id BIGINT, criado_em DATE, PRIMARY KEY (id, criado_em))
PARTITION BY RANGE (criado_em);Por debajo de decenas de millones de filas, un buen índice suele resolverlo mejor y sin complejidad.
-- Particiona por una necesidad de mantenimiento (purga, archivado), no por moda.Particionado entre servidores distintos. Gana escala de escritura, pierde los JOIN y la transacción global.
-- Elige la clave de shard con mucho cuidado: cambiarla después es reescribir el sistema.Con postgres_fdw, una partición puede vivir en otro servidor (sharding declarativo).
CREATE EXTENSION postgres_fdw;
CREATE SERVER shard2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'db2', dbname 'loja');Cómo sobrevive la base a una máquina que muere — y cómo distribuir la lectura.
Toda modificación se convierte en un registro del WAL antes de ir a la tabla. Es la base de la durabilidad, la replicación y el PITR.
SHOW wal_level; -- 'replica' o 'logical' para replicarLa réplica aplica el WAL byte a byte: una copia idéntica del clúster entero.
-- En la réplica
pg_basebackup -h primario -U replicador -D /var/lib/postgresql/data -R -PReplica por tabla, entre versiones distintas y con transformación. La base de una migración sin downtime.
-- Origen
CREATE PUBLICATION pub_vendas FOR TABLE pedidos, itens;
-- Destino
CREATE SUBSCRIPTION sub_vendas
CONNECTION 'host=origem dbname=loja user=repl'
PUBLICATION pub_vendas;El COMMIT solo vuelve después de que la réplica haya confirmado. Cero pérdida, más latencia.
ALTER SYSTEM SET synchronous_standby_names = 'replica1';
SELECT pg_reload_conf();Commit rápido, con riesgo de perder las últimas transacciones en un failover.
ALTER SYSTEM SET synchronous_commit = 'off'; -- valora el riesgo antesUn lag alto significa lecturas desactualizadas y un failover más arriesgado.
SELECT now() - pg_last_xact_replay_timestamp() AS atraso;Dirige los informes a la réplica y alivia el primario.
-- La aplicación usa dos conexiones: escritura en el primario, lectura en la réplica.
SELECT pg_is_in_recovery(); -- true = es una réplicaFailover: la réplica se convierte en primario.
SELECT pg_promote();Garantiza que el primario no descarte WAL que la réplica aún no ha consumido — y llena el disco si la réplica desaparece.
SELECT * FROM pg_replication_slots;
SELECT pg_drop_replication_slot('slot_orfao');Herramientas como Patroni, repmgr y pg_auto_failover se ocupan de la elección y del redireccionamiento.
-- Sin orquestador, el failover es manual: alguien tiene que promover y reapuntar la aplicación.Dos primarios aceptando escritura a la vez. El fencing y el quórum existen para evitarlo.
-- Nunca promuevas manualmente sin garantizar que el antiguo primario está aislado.Los equivalentes en SQL Server (Availability Groups) y Oracle (Data Guard).
-- SQL Server
ALTER AVAILABILITY GROUP ag1 FAILOVER;Basada en el binlog, con GTID para un posicionamiento fiable.
CHANGE REPLICATION SOURCE TO SOURCE_HOST='primario', SOURCE_AUTO_POSITION=1;
START REPLICA;
SHOW REPLICA STATUS\GA una base no le gustan miles de conexiones. PgBouncer ahorra memoria y estabiliza la latencia.
-- pgbouncer.ini
-- pool_mode = transaction
-- max_client_conn = 1000
-- default_pool_size = 20Un backup que nunca se ha restaurado no es un backup — es una esperanza.
Exporta la base como sentencias SQL. Portátil entre versiones y arquitecturas.
pg_dump -h localhost -U app -Fc loja > loja.dumpRestaura el archivo generado, con paralelismo cuando el formato lo permite.
pg_restore -h localhost -U app -d loja -j 4 loja.dumpIncluye usuarios, roles y permisos — lo que el pg_dump de una sola base no trae.
pg_dumpall -h localhost -U postgres > cluster.sqlÚtil para comparar estructuras entre entornos.
pg_dump --schema-only -U app loja > schema.sqlUna copia de los archivos del clúster; mucho más rápido para restaurar bases grandes.
pg_basebackup -h localhost -U replicador -D /backup/base -Fp -Xs -PGuardar los segmentos del WAL es lo que permite recuperar en cualquier punto en el tiempo.
ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM SET archive_command = 'cp %p /backup/wal/%f';Restaura el backup base y reaplica el WAL hasta el segundo anterior al incidente.
-- postgresql.conf de la restauración
restore_command = 'cp /backup/wal/%f %p'
recovery_target_time = '2026-08-10 14:29:00'mysqldump para el lógico; Percona XtraBackup para el físico en caliente.
mysqldump --single-transaction --routines --triggers loja > loja.sqlFull, diferencial y de log forman la cadena de recuperación.
BACKUP DATABASE loja TO DISK = 'D:\bkp\loja.bak' WITH COMPRESSION;
BACKUP LOG loja TO DISK = 'D:\bkp\loja_log.trn';RMAN gestiona el backup, el catálogo y la recuperación.
RMAN> BACKUP DATABASE PLUS ARCHIVELOG;gbak hace el backup lógico; nbackup hace el incremental físico.
gbak -b -user SYSDBA -password senha /dados/loja.fdb /backup/loja.fbkPrograma una restauración periódica en una máquina aparte. Es la única prueba que cuenta.
# cron mensual: restaura el último backup y ejecuta una consulta de sanidad
pg_restore -d loja_teste ultimo.dump && psql -d loja_teste -c 'SELECT COUNT(*) FROM pedidos;'Tres copias, en dos tipos de medio, una fuera del sitio. Vale también para las bases de datos.
-- Diario local (7 días) + semanal en object storage (8 semanas) + mensual offsite (12 meses)Toda migración estructural empieza con un backup verificado y un plan de rollback escrito.
pg_dump -Fc loja > pre-migracao-$(date +%F).dumpLa base necesita limpieza. Ignorarlo es acumular deuda hasta la parada.
Marca como reutilizable el espacio de las versiones antiguas de fila (MVCC).
VACUUM pedidos;Reescribe la tabla y devuelve el espacio al sistema — con un lock exclusivo. Solo en ventana de mantenimiento.
VACUUM FULL pedidos;Se ejecuta solo. El error común es dejarlo demasiado conservador en tablas de mucha escritura.
ALTER TABLE eventos SET (autovacuum_vacuum_scale_factor = 0.02);Espacio muerto acumulado. El síntoma: la tabla crece y la consulta se ralentiza sin aumento de dato útil.
SELECT relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;Un riesgo real de PostgreSQL: sin vacuum, el contador de transacciones se desborda y la base entra en modo protegido.
SELECT datname, age(datfrozenxid) FROM pg_database ORDER BY 2 DESC;Recalcula las estadísticas de distribución que usa el optimizador.
ANALYZE VERBOSE pedidos;Le enseña al optimizador que dos columnas están correlacionadas (ciudad y provincia, por ejemplo).
CREATE STATISTICS st_cidade_estado (dependencies) ON cidade, estado FROM clientes;
ANALYZE clientes;Una columna con una distribución irregular merece un histograma más detallado.
ALTER TABLE pedidos ALTER COLUMN status SET STATISTICS 1000;
ANALYZE pedidos;Reconstruye los índices hinchados sin bloquear la escritura.
REINDEX TABLE CONCURRENTLY pedidos;OPTIMIZE TABLE reorganiza; ANALYZE actualiza las estadísticas.
OPTIMIZE TABLE pedidos;
ANALYZE TABLE pedidos;Rebuild/reorganize de índices y actualización de estadísticas.
ALTER INDEX ALL ON pedidos REBUILD;
UPDATE STATISTICS pedidos;El sweep quita las versiones antiguas; el backup/restore compacta el archivo.
gfix -sweep -user SYSDBA -password senha /dados/loja.fdbPrográmala para el valle de uso, vigila el tiempo de ejecución y ten un plan de aborto.
SET statement_timeout = '2h'; -- un límite explícito para la rutina pesadaUn dato sensible exige más que un usuario y una contraseña.
Filtra las filas por política, dentro de la propia base.
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
CREATE POLICY p_tenant ON pedidos USING (tenant_id = current_setting('app.tenant')::int);Reglas distintas para la lectura y la escritura.
CREATE POLICY p_insert ON pedidos FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant')::int);Aplica la política incluso al dueño de la tabla.
ALTER TABLE pedidos FORCE ROW LEVEL SECURITY;TDE en SQL Server/Oracle, LUKS/dm-crypt en el disco, o cifrado por campo en la aplicación.
-- SQL Server
ALTER DATABASE loja SET ENCRYPTION ON;Solo quien tiene la clave lee el dato — protege incluso de un dump filtrado.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO clientes (cpf_cifrado) VALUES (pgp_sym_encrypt('123.456.789-00', 'chave'));
SELECT pgp_sym_decrypt(cpf_cifrado, 'chave') FROM clientes;Una contraseña nunca es cifrado reversible: es un hash con sal y un coste alto (bcrypt, argon2).
SELECT crypt('senha-do-usuario', gen_salt('bf', 12));Sin TLS, las credenciales y los datos viajan legibles por la red.
-- pg_hba.conf
hostssl all all 0.0.0.0/0 scram-sha-256El método de autenticación moderno de PostgreSQL.
ALTER SYSTEM SET password_encryption = 'scram-sha-256';Registrar quién leyó y modificó un dato sensible — una exigencia legal en muchos escenarios.
-- pgaudit
ALTER SYSTEM SET pgaudit.log = 'write, ddl';Un entorno de pruebas no debe tener PII real. Enmascárala en la copia.
UPDATE clientes SET email = 'user' || id || '@exemplo.com', cpf = NULL;La defensa es la consulta parametrizada. Concatenar una cadena con la entrada del usuario es el origen de casi toda filtración.
-- Vulnerable
-- 'SELECT * FROM users WHERE email = ''' + entrada + ''''
-- Seguro
SELECT * FROM users WHERE email = $1;Una función con SQL dinámico también es vulnerable. Usa quote_ident y quote_literal.
EXECUTE format('SELECT * FROM %I WHERE nome = %L', tabela, valor);La aplicación no crea tablas, no borra nada y no lee lo que no necesita.
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO app_web;Lo que la base ha ganado en los últimos años y mucha gente todavía no usa.
La extensión más valiosa para el rendimiento: agrega el tiempo por consulta normalizada.
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, total_exec_time FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;Datos geográficos con índice espacial y cálculo de distancia real.
CREATE EXTENSION postgis;
SELECT nome FROM lojas
ORDER BY geom <-> ST_MakePoint(-47.06, -22.90)::geography LIMIT 5;Series temporales a escala: hipertablas, compresión y agregación continua.
SELECT create_hypertable('metricas', 'coletado_em');Búsqueda por similitud vectorial — la base del RAG y de la recomendación.
CREATE EXTENSION vector;
CREATE TABLE docs (id BIGSERIAL, embedding vector(1536));
SELECT * FROM docs ORDER BY embedding <-> $1 LIMIT 5;Consulta otra base (o un CSV, o una API) como si fuera una tabla local.
CREATE EXTENSION postgres_fdw;
IMPORT FOREIGN SCHEMA public FROM SERVER outro INTO externo;Una columna calculada y almacenada por la base, siempre coherente.
ALTER TABLE itens ADD COLUMN total NUMERIC GENERATED ALWAYS AS (qtd * preco) STORED;El sustituto moderno del SERIAL, conforme al estándar SQL.
CREATE TABLE t (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);El comando estándar para insertar/actualizar/borrar en una pasada (PostgreSQL 15+, Oracle, SQL Server).
MERGE INTO estoque est
USING recebimento rec ON est.produto_id = rec.produto_id
WHEN MATCHED THEN UPDATE SET qtd = est.qtd + rec.qtd
WHEN NOT MATCHED THEN INSERT (produto_id, qtd) VALUES (rec.produto_id, rec.qtd);Convierte JSON en una tabla relacional dentro de la consulta (SQL:2016).
SELECT * FROM JSON_TABLE(
'[{"id":1,"nome":"Ana"}]', '$[*]'
COLUMNS (id INT PATH '$.id', nome TEXT PATH '$.nome')
) AS cli_json;La base usa varios núcleos en una sola consulta — ajusta los límites según la máquina.
SET max_parallel_workers_per_gather = 4;
EXPLAIN ANALYZE SELECT COUNT(*) FROM eventos;TOAST comprime los valores grandes; LZ4 es más rápido que el valor por defecto en muchos casos.
ALTER TABLE documentos ALTER COLUMN corpo SET COMPRESSION lz4;La base está lenta. Estos son los comandos que responden 'por qué' en minutos.
El primer comando de cualquier incidente.
SELECT pid, now() - query_start AS duracao, state, query
FROM pg_stat_activity WHERE state <> 'idle' ORDER BY duracao DESC;Aísla a los sospechosos inmediatos.
SELECT pid, now() - query_start AS duracao, query FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > INTERVAL '1 minute';El 'idle in transaction' retiene locks e impide el vacuum. Una gran causante de incidentes.
SELECT pid, now() - state_change AS parado, query FROM pg_stat_activity
WHERE state = 'idle in transaction' ORDER BY parado DESC;Cancel intenta terminar la consulta; terminate tira la conexión.
SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE pid = 12345;
SELECT pg_terminate_backend(12345);Muestra quién está bloqueando a quién, con la consulta de cada lado.
SELECT bloqueada.pid AS vitima,
bloqueadora.pid AS culpada,
bloqueadora.query
FROM pg_stat_activity bloqueada
INNER JOIN pg_stat_activity bloqueadora
ON bloqueadora.pid = ANY(pg_blocking_pids(bloqueada.pid));Optimizar la consulta de 5 ms llamada un millón de veces rinde más que la de 2 s llamada tres veces.
SELECT query, calls, round(total_exec_time::numeric, 0) AS ms_total
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;Por debajo de ~99% en OLTP suele indicar memoria insuficiente.
SELECT round(100.0 * sum(blks_hit) / nullif(sum(blks_hit + blks_read), 0), 2) AS cache_pct
FROM pg_stat_database;Señala dónde la memoria no está dando abasto.
SELECT relname, heap_blks_read, heap_blks_hit
FROM pg_statio_user_tables ORDER BY heap_blks_read DESC LIMIT 10;Una tabla grande con muchos seq scans está pidiendo un índice a gritos.
SELECT relname, seq_scan, seq_tup_read, idx_scan
FROM pg_stat_user_tables WHERE seq_scan > 0 ORDER BY seq_tup_read DESC LIMIT 10;Descubre si el problema es un exceso de conexiones en lugar de una consulta lenta.
SELECT state, COUNT(*) FROM pg_stat_activity GROUP BY state;Un crecimiento inesperado suele ser bloat, un índice nuevo o un log olvidado.
SELECT pg_size_pretty(pg_database_size(current_database()));
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;WAL acumulado significa que el archivado está fallando o que hay un slot de replicación huérfano — y un disco lleno por delante.
SELECT COUNT(*) * 16 AS mb_wal FROM pg_ls_waldir();Un contador creciente indica un patrón de acceso conflictivo en la aplicación.
SELECT datname, deadlocks, conflicts FROM pg_stat_database;Un volumen alto de archivos temporales significa que el work_mem es bajo para las consultas actuales.
SELECT datname, temp_files, pg_size_pretty(temp_bytes) FROM pg_stat_database ORDER BY temp_bytes DESC;La recolección continua vale más que una investigación puntual.
ALTER SYSTEM SET log_min_duration_statement = '1000'; -- 1 s
SELECT pg_reload_conf();Registra el plan de las consultas lentas automáticamente, sin reproducirlas a mano.
LOAD 'auto_explain';
SET auto_explain.log_min_duration = '3s';
SET auto_explain.log_analyze = on;Comprueba el valor efectivo antes de teorizar sobre la causa.
SHOW work_mem;
SELECT name, setting, unit FROM pg_settings WHERE name LIKE '%mem%';Muchos parámetros se aplican sin reiniciar el servidor.
ALTER SYSTEM SET work_mem = '32MB';
SELECT pg_reload_conf();Los equivalentes: la lista de procesos y el performance schema.
SHOW FULL PROCESSLIST;
SELECT * FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10;Las DMV entregan las consultas más caras y las esperas dominantes.
SELECT TOP 10 total_worker_time/execution_count AS media_cpu,
text
FROM sys.dm_exec_query_stats
CROSS APPLY sys.dm_exec_sql_text(sql_handle)
ORDER BY media_cpu DESC;Lo que evita el incidente antes de que exista.
La estructura cambia mediante un script versionado en el repositorio, nunca con una modificación manual en el servidor.
-- migrations/0042_add_index_pedidos.sql
CREATE INDEX CONCURRENTLY idx_pedidos_status ON pedidos (status);Todo script tiene su camino de vuelta escrito antes de subir.
-- up
ALTER TABLE pedidos ADD COLUMN canal VARCHAR(20);
-- down
ALTER TABLE pedidos DROP COLUMN canal;Añade una columna nullable, puéblala por lotes y solo entonces hazla obligatoria.
ALTER TABLE pedidos ADD COLUMN canal VARCHAR(20); -- 1
UPDATE pedidos SET canal = 'web' WHERE canal IS NULL; -- 2 (por lotes)
ALTER TABLE pedidos ALTER COLUMN canal SET NOT NULL; -- 3En una tabla grande, un ALTER descuidado bloquea la aplicación entera. Comprueba el comportamiento de tu versión.
SET lock_timeout = '3s'; -- falla rápido en lugar de encolar la producción
ALTER TABLE pedidos ADD COLUMN x INT;Un UPDATE y un DELETE empiezan siendo un SELECT, y se ejecutan primero en preproducción con un volumen parecido.
BEGIN;
UPDATE pedidos SET status = 'x' WHERE ...;
-- comprueba el número de filas afectadas
ROLLBACK; -- o COMMITVigila las conexiones, el cache hit, la replicación, el tamaño y las consultas lentas — con alertas, no con un panel que nadie mira.
-- Alertas mínimas: lag de réplica, disco > 80%, conexiones > 80% del límite,
-- transacción abierta > 5 min, fallo de backup.Dimensiona el pool: más conexiones que núcleos suele empeorar, no mejorar.
SHOW max_connections;
SELECT COUNT(*) FROM pg_stat_activity;La consulta, la transacción y la conexión necesitan un límite. Sin eso, una consulta mala lo tumba todo.
SET statement_timeout = '30s';
SET idle_in_transaction_session_timeout = '60s';
SET lock_timeout = '5s';COMMENT vive junto al esquema y aparece en las herramientas — mejor que un wiki desactualizado.
COMMENT ON TABLE pedidos IS 'Pedidos de venta; una fila por checkout completado.';
COMMENT ON COLUMN pedidos.total IS 'Importe final en BRL, con el descuento ya aplicado.';Un script de base pasa por revisión como cualquier otro código — con el EXPLAIN de lo que cambia y un plan de rollback.
-- Checklist: ¿backup ok? ¿rollback escrito? ¿lock estimado? ¿ventana definida? ¿monitorización activa?