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 índice | Recurre 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
- Crea índices en producción con
CREATE INDEX CONCURRENTLY: no bloquea las escrituras (no puede ejecutarse dentro de una transacción y tarda más, ese es el precio). - Cada índice grava cada
INSERT/UPDATE/DELETE. Revisapg_stat_user_indexes.idx_scanperiódicamente y elimina lo que nunca se lee. - Un índice que el planificador ignora no significa necesariamente que falten estadísticas: comprueba que los tipos coincidan, que las funciones del predicado coincidan con la expresión del índice y que la fracción filtrada sea lo bastante pequeña para superar a un escaneo secuencial.
- Verifica, no supongas: ejecuta
EXPLAIN (ANALYZE, BUFFERS)antes y después. La prueba está en el plan.
Rows Removed by Filter.
