Una consulta que devuelve 12 filas rastreó dos millones. El SQL es legible y aun así el plan de ejecución eligió Seq Scan. Antes de culpar al ORM o al hardware, lees el plan: EXPLAIN (ANALYZE, BUFFERS) imprime el camino que Postgres recorrió de verdad, con tiempo por nodo y páginas leídas. Los tres tipos de índice de abajo cubren la mayor parte de los problemas de lectura en PostgreSQL.
Leer el plan antes de crear un índice
EXPLAIN muestra el plan estimado. EXPLAIN ANALYZE ejecuta la consulta y muestra el tiempo real de cada nodo. La diferencia entre estadística y realidad delata un filtro que descarta la mayoría de las filas después del scan, o un buffer cache frío en el momento del test. Un Seq Scan en una tabla de 50 mil filas suele ser la elección correcta: leer todo en secuencia cuesta menos que saltar por árbol. Un Seq Scan en una tabla de 20 millones con un filtro que devuelve 10 filas delata un índice ausente o una columna fuera del índice existente.
La documentación oficial de PostgreSQL (2025) describe la familia de métodos de índice: B-tree, Hash, GiST, SP-GiST, GIN y BRIN. Cada método atiende una forma de búsqueda, y el B-tree domina igualdad y rango.
B-tree, GIN y el orden de las columnas
Un índice B-tree atiende =, operadores de rango, BETWEEN y ORDER BY en la dirección indexada. Una búsqueda de patrón en medio del texto, como ILIKE '%valor%', escapa del B-tree y exige GIN con la extensión pg_trgm. Las columnas JSONB y los arrays usan GIN: el índice invierte el documento y apunta a las filas que contienen cada clave o valor.
El orden de columnas en un índice compuesto sigue la regla del planner: columnas de igualdad primero, columnas de rango después. Un índice en (tenant_id, created_at) atiende filtro por tenant con ordenación por fecha; el índice invertido atiende filtro por fecha de cualquier tenant y fuerza un rastreo mayor. Markus Winand documenta esa jerarquía en use-the-index-luke.com, referencia que los equipos de base de datos consultan desde hace más de una década.
Índice parcial y índice cubridor
Un índice parcial cubre un subconjunto de la tabla: WHERE status = 'active' encoge el índice y reduce la escritura. Una tabla donde las consultas apuntan solo al subconjunto activo gana con eso. Un índice cubridor, con la cláusula INCLUDE, guarda columnas extra en la hoja del índice: el planner lee el índice sin volver a la tabla heap. La cuenta de hoja en hoja que sostiene esto aparece en Database Internals, de Alex Petrov (O'Reilly, 2019), con el detalle de cómo Postgres recorre las páginas.
Costo de escritura e índice olvidado
Cada INSERT, UPDATE y DELETE paga el costo de mantener los índices de la tabla. Una tabla de eventos con 12 índices duplica el trabajo de escritura. pg_stat_user_indexes lista índices sin uso por el planner; mides el rendimiento de escritura antes y después del drop y conservas solo los que atienden una consulta rara de auditoría, con la justificación registrada en el repositorio. En tablas de escritura pesada, el autovacuum necesita margen; sin él, el B-tree se fragmenta y una hoja ocupa más espacio que el dato.
¿Te gustó el contenido?
Construyo productos web y soluciones con IA de la manera correcta — arquitectura sólida, código sostenible y entrega real.
Hablemos