Optimizacion de Queries e Indexing en PostgreSQL
Analiza y optimiza queries lentas en PostgreSQL usando EXPLAIN, indexing apropiado, partial indexes y reescritura de queries para reducir tiempo de ejecucion
Identifica y corrige queries lentas en PostgreSQL usando analisis de planes de ejecucion, indexing estrategico y reestructuracion de queries. Esta recipe cubre EXPLAIN ANALYZE, indexes B-tree y partial, covering indexes y anti-patterns comunes que degradan rendimiento.
Cuando Usar Esto
- Las queries tardan mas de 100ms y se ejecutan frecuentemente. Consulta Database Views para resultados precomputados.
- Aparecen sequential scans en planes de queries donde deberian usarse index scans. Consulta SQL Joins para optimización de joins.
- CPU o I/O de la base de datos estan saturados bajo carga normal. Consulta Redis Caching para reducir carga.
Solucion
1. Analizar Planes con EXPLAIN
-- Plan basico
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.name
ORDER BY order_count DESC
LIMIT 10;
Busca:
Seq Scanen tablas grandes → falta indexHash Joincon alto uso de memoria → considerar nested loop con indexSortcon alto costo → agregar index en columnas de sort
2. Crear Indexes Estrategicos
-- Index compuesto para queries de rango + igualdad
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at);
-- Index partial para registros activos solamente
CREATE INDEX idx_orders_pending
ON orders(created_at)
WHERE status = 'pending';
-- Covering index para evitar heap lookups
CREATE INDEX idx_orders_covering
ON orders(user_id, status, total)
INCLUDE (created_at);
3. Reescribir Queries para Usar Indexes
-- Antes: funcion en columna previene uso de index
SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024;
-- Despues: condicion de rango permite index scan
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01';
4. Optimizar Joins
-- Antes: cross join implicito
SELECT * FROM users, orders WHERE users.id = orders.user_id;
-- Despues: JOIN explicito con condiciones apropiadas
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
AND o.created_at > NOW() - INTERVAL '30 days';
5. Particionar Tablas Grandes
-- Particion por rango por mes
CREATE TABLE events (
id BIGSERIAL,
event_type VARCHAR(50),
created_at TIMESTAMP NOT NULL,
payload JSONB
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2024_01 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE events_2024_02 PARTITION OF events
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
Como Funciona
- EXPLAIN ANALYZE muestra el plan de ejecucion actual y tiempos
- B-tree indexes aceleran lookups de igualdad y rango
- Partial indexes son mas pequenos y rapidos para subconjuntos filtrados
- Covering indexes incluyen todas las columnas necesarias, evitando acceso al heap
- Partitioning prunea datos irrelevantes, reduciendo el scope del scan
Variacion: Encontrar Indexes Faltantes
-- Identificar tablas frecuentemente scaneadas
SELECT
schemaname,
tablename,
seq_scan,
idx_scan,
seq_tup_read,
idx_tup_fetch
FROM pg_stat_user_tables
WHERE seq_scan > 1000
AND idx_scan < seq_scan * 0.1
ORDER BY seq_scan DESC
LIMIT 20;
Consideraciones de Produccion
- Corre
ANALYZEdespues de bulk loads o cambios importantes de datos para actualizar estadisticas - Usa
pg_stat_statementspara identificar las queries mas lentas por tiempo total. Consulta Logging para observabilidad de queries. - Monitorea bloat de indexes con
pgstattupley rebuild conREINDEX
Errores Comunes
- Agregar indexes en cada columna sin considerar patrones de query
- Usar
SELECT *cuando solo se necesitan pocas columnas - No actualizar estadisticas de tabla despues de migraciones grandes de datos. Consulta Database Migrations para cambios de schema seguros.
Buenas prácticas adicionales
- Usa
EXPLAIN (ANALYZE, BUFFERS)para stats de I/O detalladas. La opciónBUFFERSmuestra cuántos bloques fueron hit desde caché vs leídos desde disco:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 42;
- Elimina indexes sin uso. Los indexes ralentizan writes. Identifica y elimina indexes con cero scans:
SELECT
schemaname,
relname,
indexrelname,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
- Usa
CLUSTERpara ordenar físicamente los datos. Para tablas frecuentemente consultadas por una columna específica, clustering mejora la localidad de caché:
CLUSTER orders USING idx_orders_user_id;
- Configura
effective_cache_size. Dile al planner cuánta memoria tiene el caché del SO. Esto afecta si el planner elige index scans vs seq scans:
ALTER SYSTEM SET effective_cache_size = '4GB';
- Usa
pg_prewarmpara cachear tablas críticas. Pre-carga tablas frecuentemente accedidas en shared buffers después de reinicios:
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT pg_prewarm('users');
SELECT pg_prewarm('orders', 'main', 'read');
Tips de Rendimiento
-
Usa
pg_stat_statements.track = allpara capturar queries anidadas. Esto rastrea queries dentro de funciones y triggers, no solo queries top-level. -
Monitorea el buffer hit ratio. Un ratio below 90% significa que la base de datos lee desde disco demasiado. Aumenta
shared_bufferso añade RAM:
SELECT
sum(blks_hit) AS hits,
sum(blks_read) AS reads,
100.0 * sum(blks_hit) / NULLIF(sum(blks_hit) + sum(blks_read), 0) AS hit_ratio
FROM pg_stat_database;
- Usa
pgbenchpara load testing. Benchmark cambios antes de desplegar:
pgbench -i -s 10 mydb # Inicializar con scale factor 10
pgbench -c 20 -j 4 -T 60 mydb # 20 clientes, 4 threads, 60 segundos
- Verifica bloat de indexes regularmente. Usa
pgstattuplepara medir bloat:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('orders');
- Usa tuning de
parallel_setup_costyparallel_tuple_cost. Para workloads analíticos, baja estos para favorecer planes de query paralelos:
SET parallel_setup_cost = 100;
SET parallel_tuple_cost = 0.03;
SET max_parallel_workers_per_gather = 4; Preguntas frecuentes
Cuantos indexes son demasiados?
Mas de 5-7 indexes por tabla ralentiza writes. Cada index agrega overhead a INSERT, UPDATE y DELETE.
Cuando deberia usar BRIN en lugar de B-tree?
Los BRIN indexes son ideales para tablas muy grandes y naturalmente ordenadas (time-series, datos de log) donde un B-tree completo seria demasiado grande.
Recursos Relacionados
Transacciones de Base de Datos en Python, JS y Java
Cómo usar transacciones ACID para garantizar integridad de datos en Python, JavaScript y Java con ejemplos SQL.
RecipePatrones de Cache de Redis para Aplicaciones de Alto
Como implementar patrones cache-aside, write-through y write-behind con Redis para reducir carga de base de datos y mejorar tiempos de respuesta
PatternPatrón Repository
Abstrae la lógica de acceso a datos detrás de una interfaz limpia. Patrón de diseño arquitectural para capas de datos testeables y mantenibles.
RecipeGeneracion de UUID: v4, v7 y Comparacion con ULID
Compara UUID v4, v7, ULID y nanoid para generar identificadores unicos con diferentes tradeoffs en aleatoriedad, ordenamiento, rendimiento y localidad de indices de base de datos
RecipePool de Conexiones a Base de Datos
Configura y ajusta pools de conexiones para maximizar throughput y prevenir el agotamiento de conexiones.
RecipeReplicación de Bases de Datos
Configura y gestiona replicación de bases de datos para alta disponibilidad, escalado de lecturas y disaster recovery con arquitecturas primaria-réplica.