🎨 Sysprovider Code
Sysprovider LogoWiki
🇪🇸Hosting español para ecommerce

Optimización de índices en PostgreSQL para alto rendimiento

Actualizado el 21 de junio de 2026

Cuando una base de datos PostgreSQL empieza a ralentizarse, el primer instinto suele ser lanzar más hardware al problema. Sin embargo, la verdadera clave del alto rendimiento en entornos de producción no reside únicamente en la memoria RAM o en los discos NVMe, sino en la optimización índices. Un índice mal diseñado consume espacio y ralentiza las escrituras; uno bien diseñado puede reducir el tiempo de una consulta de minutos a milisegundos.

En este artículo, exploraremos las estrategias más avanzadas para la optimización índices en PostgreSQL. Desde la elección del tipo de índice correcto hasta el uso de índices parciales y BRIN, pasando por técnicas de mantenimiento y monitorización. Si buscas llevar tu base de datos al siguiente nivel de rendimiento, este es tu punto de partida.


Comprendiendo el coste de un índice en PostgreSQL

Antes de optimizar, debemos entender qué sucede internamente. Un índice en PostgreSQL es una estructura de datos (normalmente un árbol B+ o una bitmap) que acelera la búsqueda de filas a costa de:

  • Espacio en disco: Los índices pueden ocupar más espacio que la propia tabla.
  • Rendimiento de escritura: Cada INSERT, UPDATE o DELETE debe actualizar el índice asociado.
  • Memoria compartida: El índice se cachea en shared_buffers; índices muy grandes pueden expulsar datos útiles.

[INFO] PostgreSQL no crea índices por defecto en columnas foráneas o de JOIN. El desarrollador debe ser explícito. Esto es una ventaja, pero también una trampa si no se planifica.

La regla de oro: medir antes de indexar

Usa EXPLAIN (ANALYZE, BUFFERS) para entender si una consulta realiza un Seq Scan (escaneo secuencial) o un Index Scan. Si ves Seq Scan en tablas con más de 10,000 filas, probablemente necesitas un índice.

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM pedidos WHERE fecha >= '2024-01-01';

Tipos de índices: cuándo usar cada uno

PostgreSQL ofrece varios tipos de índices, cada uno optimizado para un patrón de consulta específico. Elegir el incorrecto es el error más común en optimización índices.

Índice B-tree (por defecto)

El más versátil. Sirve para:

  • Comparaciones de igualdad (=)
  • Rangos (<, >, BETWEEN)
  • Ordenaciones (ORDER BY)
  • Búsquedas con LIKE (solo si el patrón no empieza por %)
CREATE INDEX idx_pedidos_fecha ON pedidos (fecha);

Índice Hash

Solo para igualdad (=). No soporta rangos ni ordenaciones. Ocupa menos espacio que un B-tree en columnas con muchos valores repetidos.

CREATE INDEX idx_usuarios_email_hash ON usuarios USING hash (email);

[WARNING] Los índices Hash no se recomiendan en tablas pequeñas o con pocos valores distintos. PostgreSQL los elige automáticamente solo si el planificador lo considera óptimo.

Índice GiST y GIN

Para datos complejos como geometrías (PostGIS), arrays, texto completo o JSONB. GIN es excelente para búsquedas dentro de arrays o documentos JSON.

CREATE INDEX idx_documentos_texto ON documentos USING gin (contenido to_tsvector('spanish', contenido));

Índice BRIN (Block Range Index)

Aquí llegamos a una de las joyas de la optimización índices moderna. BRIN es ideal para tablas muy grandes (millones de filas) cuyos datos están ordenados físicamente o tienen una correlación temporal alta (ej: logs, series temporales).

  • Ventaja: Ocupa una fracción del espacio de un B-tree (hasta 100 veces menos).
  • Desventaja: Es menos preciso; puede devolver falsos positivos que luego la tabla debe filtrar.
CREATE INDEX idx_logs_fecha_brin ON logs USING brin (fecha) WITH (pages_per_range = 32);

pages_per_range controla el tamaño del bloque de páginas que resume el índice. Un valor más pequeño (ej: 4) ofrece mayor precisión pero ocupa más espacio; uno más grande (ej: 128) comprime más pero puede perder selectividad.


Índices parciales: menos es más

Un índice parcial solo indexa un subconjunto de filas que cumplen una condición. Esto reduce drásticamente el tamaño del índice y acelera las escrituras.

Caso de uso típico: pedidos activos vs históricos

Imagina una tabla pedidos con 50 millones de filas, donde el 95% son pedidos cerrados (estado = 'completado') y solo el 5% están activos (estado = 'pendiente' o 'procesando'). Las consultas críticas solo buscan pedidos activos.

CREATE INDEX idx_pedidos_activos ON pedidos (fecha_creacion)
WHERE estado IN ('pendiente', 'procesando');

Este índice solo contendrá 2.5 millones de filas en lugar de 50 millones. Las búsquedas serán mucho más rápidas y el índice ocupará una fracción del espacio.

[TIP] Combina índices parciales con columnas de alta cardinalidad (como fechas) para obtener el máximo rendimiento.

Otro ejemplo: usuarios no eliminados

CREATE INDEX idx_usuarios_activos_email ON usuarios (email)
WHERE deleted_at IS NULL;

Las consultas de login solo buscan usuarios activos. Este índice evita indexar los usuarios eliminados, que nunca se consultan.


Índices compuestos: el orden importa

Un índice compuesto (sobre varias columnas) es útil cuando las consultas filtran por múltiples campos. Sin embargo, el orden de las columnas es crítico.

La regla de la igualdad primero

Coloca primero las columnas que usan = y después las de rango (<, >, BETWEEN).

-- Mal: orden incorrecto
CREATE INDEX idx_pedidos_cliente_fecha ON pedidos (fecha, cliente_id);

-- Bien: igualdad primero, rango después
CREATE INDEX idx_pedidos_cliente_fecha ON pedidos (cliente_id, fecha);

La consulta WHERE cliente_id = 123 AND fecha >= '2024-01-01' usará eficientemente el segundo índice. El primero solo sería útil si filtráramos por fecha sin cliente.

Índices de solo escaneo (Index Only Scan)

Si el índice contiene todas las columnas necesarias para la consulta, PostgreSQL puede evitar acceder a la tabla principal. Esto se llama Index Only Scan y es extremadamente rápido.

CREATE INDEX idx_pedidos_cliente_fecha_estado ON pedidos (cliente_id, fecha, estado);

Consulta: SELECT estado FROM pedidos WHERE cliente_id = 456 AND fecha = '2024-06-15';

PostgreSQL leerá directamente del índice, sin tocar la tabla.


Mantenimiento de índices: VACUUM, REINDEX y estadísticas

Un índice puede degradarse con el tiempo debido a actualizaciones y eliminaciones. PostgreSQL usa MVCC, lo que significa que las filas muertas (muertas por UPDATE o DELETE) siguen ocupando espacio hasta que VACUUM las limpia.

VACUUM y autovacuum

Asegúrate de que autovacuum esté activo y configurado correctamente. Para tablas críticas, ajusta los umbrales:

ALTER TABLE pedidos SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.005);

REINDEX

Cuando un índice se vuelve hinchado (bloques muertos internos), REINDEX lo reconstruye desde cero.

REINDEX INDEX idx_pedidos_fecha;

[WARNING] REINDEX bloquea la tabla con un ACCESS EXCLUSIVE LOCK. En producción, usa REINDEX CONCURRENTLY (disponible desde PostgreSQL 12) para evitar downtime.

REINDEX INDEX CONCURRENTLY idx_pedidos_fecha;

Estadísticas del planificador

PostgreSQL usa estadísticas para decidir si usar un índice. Si las estadísticas están desactualizadas, el planificador puede ignorar índices perfectamente válidos.

ANALYZE pedidos;

Configura default_statistics_target a un valor más alto (ej: 500) para columnas con distribuciones complejas.

ALTER TABLE pedidos ALTER COLUMN fecha SET STATISTICS 500;

Monitorización y detección de índices ineficientes

No todos los índices son útiles. Algunos pueden estar duplicados o simplemente no usarse nunca. Para identificarlos, PostgreSQL ofrece varias vistas del sistema.

Índices no usados

SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY idx_tup_read DESC;

Si un índice tiene idx_scan = 0 y lleva días sin usarse, probablemente puedes eliminarlo.

Duplicados

Dos índices que comienzan con las mismas columnas suelen ser redundantes.

SELECT
    a.indexrelid::regclass AS index_name,
    a.indrelid::regclass AS table_name,
    a.indkey AS columns
FROM pg_index a
WHERE EXISTS (
    SELECT 1
    FROM pg_index b
    WHERE a.indrelid = b.indrelid
    AND a.indexrelid <> b.indexrelid
    AND a.indkey LIKE b.indkey || '%'
    AND a.indisprimary = false
);

Tamaño de índices

SELECT
    pg_size_pretty(pg_indexes_size('pedidos')) AS total_index_size,
    pg_size_pretty(pg_relation_size('pedidos')) AS table_size;

Estrategias avanzadas para alto rendimiento

Particionamiento + índices parciales

Combina el particionamiento de tablas (por rango de fechas) con índices parciales en cada partición. Esto permite que las consultas solo escaneen índices pequeños dentro de la partición relevante.

CREATE TABLE pedidos_2024_q1 PARTITION OF pedidos FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE INDEX idx_pedidos_2024_q1_activos ON pedidos_2024_q1 (cliente_id) WHERE estado = 'pendiente';

Índices funcionales

Indexa el resultado de una función o expresión para acelerar consultas que usan transformaciones.

CREATE INDEX idx_usuarios_email_lower ON usuarios (lower(email));
-- Consulta: SELECT * FROM usuarios WHERE lower(email) = 'usuario@ejemplo.com';

Uso de covering indexes con INCLUDE

Desde PostgreSQL 11, puedes incluir columnas adicionales en un índice sin que formen parte del árbol de búsqueda. Esto permite Index Only Scan sin aumentar el tamaño del índice de forma drástica.

CREATE INDEX idx_pedidos_fecha_include ON pedidos (fecha) INCLUDE (cliente_id, total);

La columna fecha se usa para la búsqueda; cliente_id y total se almacenan solo en las hojas del índice para evitar acceder a la tabla.


Conclusión: el arte de la optimización índices

La optimización índices en PostgreSQL no es una tarea única, sino un proceso continuo de medición, ajuste y mantenimiento. Las claves para lograr alto rendimiento son:

  1. Elegir el tipo de índice adecuado: B-tree para la mayoría, BRIN para series temporales, GIN para texto y JSON.
  2. Usar índices parciales para reducir el tamaño y acelerar las escrituras.
  3. Diseñar índices compuestos con la regla de igualdad primero y rango después.
  4. Mantener los índices con VACUUM y REINDEX CONCURRENTLY.
  5. Monitorizar constantemente con pg_stat_user_indexes para eliminar redundancias.

Recuerda: cada índice tiene un coste. No indexes por inercia; hazlo solo cuando el planificador lo justifique. Con estas técnicas, tu base de datos PostgreSQL estará preparada para manejar millones de transacciones sin sudar.

[TIP] Si tu tabla tiene más de 100 millones de filas y las consultas son por rangos de fecha, prueba BRIN con pages_per_range = 32. Podrías reducir el tamaño del índice en un 90% sin perder rendimiento.

¿Necesitas ayuda?Son dos de nuestros técnicos, Agustín y Mikel, y están disponibles para resolver cualquier problema.

Hablar con ellos ahora
Agustín y Mikel