Elegir el índice de PostgreSQL adecuado

PostgreSQL incluye seis métodos de acceso para índices, y CREATE INDEX sin una cláusula USING siempre te da un B-tree. Ese valor por defecto es el correcto la mayor parte del tiempo: esta guía trata de reconocer los casos en que no lo es, y de las tres características de índice (parcial, de expresión, de cobertura) que importan más que el método de acceso en el trabajo del día a día.

B-tree: el valor por defecto por una razón

Un índice B-tree admite comparaciones de igualdad y de rango (=, <, >, BETWEEN, IN), IS NULL, LIKE 'abc%' por prefijo (con la collation adecuada o text_pattern_ops) y —a menudo olvidado— la ordenación: un índice sobre (created_at) puede satisfacer ORDER BY created_at DESC LIMIT 20 sin ordenar nada.

En los B-tree multicolumna, el orden de las columnas lo es todo. Un índice sobre (customer_id, created_at) es excelente para WHERE customer_id = ? ORDER BY created_at, pero casi inútil para una consulta que filtra solo por created_at. Regla general: primero las columnas de igualdad, luego la columna de rango o de ordenación.

Cuándo B-tree es la herramienta equivocada

Tipo de índiceRecurre a él cuando…Ten cuidado con
GIN El valor es un contenedor y buscas dentro de él: jsonb @> '{"status":"active"}', solapamiento de arrays &&, búsqueda de texto completo tsvector @@ tsquery, búsqueda por trigramas con pg_trgm para LIKE '%term%'. Más lento de actualizar que un B-tree; el tamaño del índice puede ser grande. Va bien para datos con muchas lecturas.
GiST Datos geométricos, tipos de rango (solapamientos de tstzrange), vecino más cercano ORDER BY location <-> point y restricciones de exclusión (p. ej. «ninguna reserva solapada para la misma sala»). Con pérdida (lossy) en algunas clases de operadores; suele ser más grande y lento que un B-tree para la igualdad simple.
BRIN Tablas enormes de solo anexado (logs, eventos) donde la columna se correlaciona con el orden físico de las filas, típicamente marcas de tiempo. Diminuto: megabytes donde un B-tree ocuparía gigabytes. Inútil si los datos no están ordenados físicamente por esa columna; solo acota el escaneo a rangos de bloques.
Hash Igualdad pura sobre valores largos donde un B-tree engorda. Rara vez necesario; es seguro ante caídas y se replica desde PostgreSQL 10. Solo igualdad: sin rangos, sin ordenación, sin forzar unicidad.
SP-GiST Estructuras particionadas por el espacio: búsquedas por prefijo sobre texto, rangos de IP (inet), quadtrees para puntos. De nicho; compara mediante benchmark contra GiST/B-tree con tus datos reales antes de comprometerte.

Índices parciales: indexa solo lo que consultas

CREATE INDEX orders_pending_idx
    ON orders (created_at)
    WHERE status = 'pending';

Si el 99 % de tus pedidos están completados pero cada sondeo pregunta por los pendientes, un índice parcial se mantiene pequeño, se mantiene caliente en caché y es barato de mantener: las filas completadas nunca lo tocan. La consulta debe incluir la condición WHERE (o algo que el planificador pueda demostrar que la implica) para que el índice se tenga en cuenta.

Índices de expresión: coincide con lo que la consulta calcula

CREATE INDEX users_email_lower_idx ON users (lower(email));

Una consulta que filtra por lower(email) = lower($1) no puede usar un índice simple sobre email: el índice almacena los valores en bruto, no en minúsculas. La expresión del índice debe coincidir con la expresión de la consulta. La misma técnica funciona con date_trunc('day', created_at), la extracción de campos JSON y cualquier función inmutable.

Índices de cobertura y escaneos solo de índice

CREATE INDEX orders_cust_idx
    ON orders (customer_id) INCLUDE (total, created_at);

Si el índice contiene todas las columnas que la consulta necesita, PostgreSQL puede saltarse la tabla por completo: un index-only scan. INCLUDE añade columnas de carga útil a las hojas del índice sin hacerlas parte de la clave. Dos advertencias: el índice se vuelve más grande y los escaneos solo de índice también requieren que el mapa de visibilidad esté actualizado; en una tabla muy actualizada y rara vez pasada por vacuum verás cómo crecen los Heap Fetches y desaparece gran parte del beneficio.

Reglas prácticas

El EXPLAIN Visualizer señala los dos indicios clásicos de un índice ausente o incorrecto: escaneos secuenciales que leen mucho más de lo que devuelven, y recuentos altos de Rows Removed by Filter.
🧯 Errores relacionados: duplicate key value violates unique constraint, no constraint matching ON CONFLICT y violaciones de clave foránea; los borrados lentos en la tabla padre suelen significar una columna FK sin indexar.