Artículo
Has creado el índice y Postgres pasa de él
Cinco motivos por los que el planificador prefiere leer la tabla entera, en el orden en que conviene comprobarlos.
Es una situación bastante frustrante. Creas el índice, lanzas la consulta con EXPLAIN y ahí sigue
el Seq Scan, como si no hubieras hecho nada. Antes de echarle la culpa al planificador, que casi
siempre tiene razón, merece la pena repasar estas cinco causas. Las pongo en el orden en que yo las
miro.
Antes de nada, mira el plan de verdad
EXPLAIN (ANALYZE, BUFFERS) SELECT ...;
Sin ANALYZE solo ves lo que el planificador cree que va a pasar. Con él ves también lo que ha
pasado. Cuando la estimación dice rows=12 y la realidad dice actual rows=48000, ya tienes casi
resuelto el problema: el planificador está tomando decisiones con información equivocada.
1. La tabla es pequeña
Con unos pocos miles de filas, leer la tabla entera de forma secuencial es más rápido que ir saltando entre el índice y la tabla. Aquí no hay nada que arreglar. El planificador ha hecho bien y el índice se usará cuando la tabla crezca.
2. La consulta devuelve demasiadas filas
Si el filtro se queda con, digamos, un 10 % de la tabla o más, el acceso aleatorio del índice deja de compensar frente a una lectura secuencial. El umbral exacto depende del disco y de la configuración, pero la idea es esa.
3. Estás aplicando una función a la columna
Este es el más habitual, con diferencia.
-- No puede usar el índice sobre email
WHERE lower(email) = 'ana@ejemplo.com';
-- Con un índice sobre la expresión, sí
CREATE INDEX idx_email_lower ON usuarios (lower(email));
Lo mismo ocurre con date(creado_en) = '2026-08-01'. En ese caso la solución es reescribirlo como
un rango, creado_en >= '2026-08-01' AND creado_en < '2026-08-02', que sí puede aprovechar el índice
normal.
4. Los tipos no cuadran
Comparar una columna bigint con un parámetro que el driver envía como numeric puede dejar el
índice fuera de juego, y es de esos fallos que no se ven a simple vista porque la consulta funciona.
Comprueba los tipos con \d tabla y, si hay que castear, hazlo en el lado del parámetro. Nunca en el
de la columna, porque entonces vuelves al caso anterior.
5. Las estadísticas están viejas
Después de una carga masiva, el planificador sigue pensando que la tabla está casi vacía y actúa en consecuencia.
ANALYZE pedidos;
SELECT last_analyze, last_autoanalyze FROM pg_stat_user_tables WHERE relname = 'pedidos';
Si la segunda consulta te devuelve fechas de hace semanas, ya sabes por dónde van los tiros.
Un extra: el orden de las columnas
En un índice compuesto sobre (a, b), Postgres puede filtrar por a o por a AND b, pero no por
b sola. Es como una guía telefónica ordenada por apellido y nombre: sirve para buscar por apellido,
pero no para buscar solo por nombre. Por eso conviene poner primero la columna por la que filtras con
igualdad y después la del rango:
CREATE INDEX ON eventos (usuario_id, creado_en); -- usuario_id = ? AND creado_en > ?