Los índices inválidos de Postgres degradan silenciosamente el rendimiento de escritura tras fallos en la construcción
Las construcciones fallidas de índices concurrentes en Postgres dejan entradas inválidas que consumen recursos. Aprende a detectar, entender y limpiar estos problemas ocultos de la base de datos.
Traducido automáticamente del original en inglés.
Un patrón común en las migraciones modernas de bases de datos implica usar CREATE INDEX CONCURRENTLY para evitar bloquear tablas durante los cambios de esquema. Sin embargo, cuando esta operación falla debido a tiempos de espera agotados o conflictos de datos, a menudo deja atrás una entrada de índice "inválida". Estos remanentes no ayudan con las consultas pero pueden ralentizar significativamente las operaciones de escritura al obligar a la base de datos a mantenerlos innecesariamente.
Qué sucedió
Cuando un desarrollador o un agente automatizado solicita un índice único en una tabla activa, el enfoque correcto es utilizar la opción de construcción concurrente. Si el proceso falla a mitad de camino—quizás debido a una violación de clave duplicada, un bloqueo mutuo (deadlock) o un tiempo de espera agotado en la sentencia—Postgres no simplemente revierte toda la operación. En su lugar, confirma la entrada inicial del catálogo para el índice antes de que se complete la construcción. Esto resulta en un índice que existe en el sistema pero está marcado como inválido.
El problema crítico surge cuando los scripts de migración se reintentan sin verificar el estado de la base de datos. Un error común es añadir IF NOT EXISTS al comando de reintento. Esto permite que la migración pase las comprobaciones de integración continua, pero ignora el hecho de que el índice existente está roto. La base de datos continúa soportando la sobrecarga de esta estructura inválida, lo que lleva a un rendimiento degradado difícil de diagnosticar porque los planes de consulta estándar no mostrarán su uso.
Detalles clave
- Los índices inválidos se crean cuando una construcción concurrente falla después de que la transacción inicial del catálogo se ha confirmado.
- Dos banderas determinan el coste:
indisvalid = falsesignifica que el índice es inútil para las consultas, mientras queindisready = falsesignifica que las escrituras lo ignoran. - Si
indisreadyes verdadero, la base de datos aún actualiza el índice en cada inserción o actualización, desperdiciando CPU y almacenamiento. - Las restricciones únicas derivadas de construcciones fallidas pueden seguir aplicando reglas de unicidad incluso si el planificador nunca usa el índice para búsquedas.
- Herramientas de migración como Rails, Knex y golang-migrate a menudo envuelven comandos en transacciones, lo que hace que las operaciones
CONCURRENTLYfallen inmediatamente. - La limpieza requiere
DROP INDEX CONCURRENTLY, que debe ejecutarse fuera de un bloque de transacción.
Contexto
Para entender por qué sucede esto, ayuda saber que una construcción de índice concurrente no es una única transacción atómica. Ocurre en tres fases distintas a través de transacciones separadas. Primero, el índice se registra en los catálogos del sistema como inválido. Segundo, la base de datos escanea los datos existentes de la tabla para poblar el índice. Tercero, espera a que finalicen todas las transacciones antiguas antes de marcar el índice como listo para su uso.
Si el proceso se interrumpe después de la primera fase pero antes de la fase final, el índice permanece en el catálogo. Dependiendo de cuándo exactamente ocurrió el fallo, el índice podría estar en un estado donde no está listo para las consultas pero sigue siendo actualizado por nuevas escrituras. Esto es particularmente insidioso porque el índice parece existir, pero no ofrece ningún beneficio de lectura mientras penaliza activamente el rendimiento de escritura.
Por qué importa
Para equipos que gestionan su propio software, la degradación silenciosa del rendimiento es un gran riesgo operativo. Un índice inválido que todavía se mantiene consume espacio en disco, aumenta el volumen del registro previo a la escritura (WAL) y previene las actualizaciones Heap-Only Tuple (HOT). Esto conduce a mayores costes de almacenamiento y tiempos de respuesta más lentos de la aplicación durante la ingesta de datos. Dado que el índice es ignorado por el planificador de consultas, las herramientas de monitoreo estándar enfocadas en el rendimiento de lectura pueden no señalar el problema.
Además, los pipelines de despliegue automatizados pueden exacerbar el problema. Si un agente de codificación o un script de CI reintenta una migración fallida con IF NOT EXISTS, efectivamente oculta el error. El despliegue tiene éxito, pero la base de datos acumula deuda técnica en forma de índices rotos. Con el tiempo, múltiples intentos fallidos pueden dejar varios índices inválidos en una sola tabla, acumulando la sobrecarga de escritura y haciendo el mantenimiento futuro más complejo.
Qué puedes hacer
- Consulta regularmente el catálogo del sistema
pg_indexpara encontrar entradas dondeindisvalidsea falso. - Verifica la bandera
indisreadypara determinar si el índice inválido sigue consumiendo recursos de escritura. - Usa
DROP INDEX CONCURRENTLYpara eliminar índices inválidos sin bloquear la tabla para lecturas o escrituras. - Asegúrate de que las herramientas de migración estén configuradas para deshabilitar el envoltorio de transacciones para operaciones de índices concurrentes.
- Evita usar
IF NOT EXISTSen los reintentos; en su lugar, verifica la validez y elimina primero el índice roto. - Añade una comprobación posterior al despliegue que lance una excepción si quedan índices inválidos en la base de datos.



