Nota revisada y reescrita el 18 de octubre de 2026: la versión original tenía errores y datos sin fuente.
Antes de crear índices a ciegas, merece la pena saber qué consulta es la lenta y por qué. PostgreSQL trae las dos herramientas para eso: pg_stat_statements y EXPLAIN.
1. Qué consulta duele
pg_stat_statements agrupa las consultas por forma (sin los valores concretos) y acumula tiempos. Hay que cargarla en postgresql.conf y reiniciar:
shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT calls,
round(total_exec_time::numeric, 0) AS total_ms,
round(mean_exec_time::numeric, 1) AS media_ms,
query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Ordena por tiempo total, no por media: una consulta de 20 ms que se ejecuta 50.000 veces al día pesa más que una de 2 segundos que corre una vez.
2. Por qué es lenta
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total
FROM orders
WHERE customer_id = 4821 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
ANALYZE ejecuta la consulta y muestra tiempos reales. Con un UPDATE o un DELETE, hazlo dentro de BEGIN; ... ROLLBACK;. Lo que hay que mirar:
Seq Scansobre una tabla grande con un filtro que descarta casi todo (Rows Removed by Filterenorme): falta un índice.rows=estimadas frente aactual rowsmuy distintas: el planificador trabaja con estadísticas viejas.ANALYZE orders;y vuelve a mirar.Sortconexternal merge Disk: la ordenación no cabe enwork_memy se va a disco.Buffers: shared readalto: está leyendo de disco, no de caché.
3. El índice que encaja
Para la consulta de arriba, un índice compuesto con las columnas de igualdad primero y la de orden al final permite filtrar y devolver ya ordenado, sin Sort:
CREATE INDEX CONCURRENTLY orders_customer_status_created_idx
ON orders (customer_id, status, created_at DESC);
CONCURRENTLY evita bloquear las escrituras mientras se construye, a cambio de tardar más. No puede ir dentro de una transacción, y si falla deja un índice INVALID que hay que borrar y volver a crear.
Otros dos tipos que resuelven casos frecuentes:
-- Parcial: si casi siempre filtras por pedidos pendientes, indexa solo esos
CREATE INDEX CONCURRENTLY orders_pending_idx
ON orders (created_at) WHERE status = 'pending';
-- De expresión: búsquedas sin distinguir mayúsculas
CREATE INDEX CONCURRENTLY customers_email_lower_idx
ON customers (lower(email));
-- solo se usa si la consulta escribe exactamente lower(email) = ...
4. Comprobar y limpiar
Vuelve a lanzar el EXPLAIN (ANALYZE, BUFFERS) y confirma que aparece Index Scan o Index Only Scan y que el tiempo baja. Los índices no son gratis: ocupan disco y hacen más lento cada INSERT y UPDATE. Para ver los que nunca se usan:
SELECT relname AS tabla, indexrelname AS indice, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
idx_scan cuenta desde el último reinicio de estadísticas. Antes de borrar nada, asegúrate de que el contador lleva tiempo suficiente acumulando, y de que el índice no respalda una restricción UNIQUE o una clave primaria.