Imagina el pull request. Una migración, una docena de líneas, añade un índice sobre customer_id. Quien revisa lo aprueba en unos noventa segundos, y ¿por qué no iba a hacerlo? La columna aparece en el WHERE, así que el índice parece evidentemente correcto. Lo que nadie abrió fue el método del repositorio al que ese índice debía servir. Está tres directorios más allá y ha ganado un ordenamiento desde la última vez que alguien lo leyó. Creo que la mayoría de nuestros fallos de indexación empiezan justo ahí. Un índice es un acuerdo que haces con una consulta concreta, y en un proyecto PHP típico el acuerdo se escribe en un archivo mientras la consulta vive en otro, así que nadie comprueba nunca que los dos sigan de acuerdo.
SELECT * FROM order_events
WHERE customer_id = 8842
AND created_at > now() - interval '30 days'
ORDER BY created_at DESC;El artículo reciente de Farhad Ali en Medium muestra cuánto cuesta ese hueco. Su equipo tenía en PostgreSQL una tabla llamada order_events, con unos cuatro millones de filas sobre el ciclo de vida de los pedidos, y una consulta que traía los eventos de un cliente de los últimos treinta días, del más reciente al más antiguo. Habían indexado customer_id por sí solo. EXPLAIN ANALYZE mostró que el planificador ignoraba ese índice y hacía el trabajo por la vía cara, lo que incluía ordenar todo el historial del cliente por created_at después de encontrar las filas. Cambiarlo por un índice compuesto sobre (customer_id, created_at DESC) bajó la consulta de algo más de cuatro segundos a 9 milisegundos. El detalle al que no dejo de volver es que era la tercera vez. El mismo desajuste ya les había pasado en una aplicación de citas sanitarias y en una herramienta interna de analítica para un cliente de logística.
Tres proyectos con una misma causa raíz son un patrón, y yo culparía al patrón de nuestra forma de trabajar antes que a ningún desarrollador en particular. Piensa en cómo cambia de verdad una consulta en una aplicación Laravel o Symfony. Alguien añade orderByDesc('created_at') a una cadena del query builder porque el product owner quiere lo más nuevo arriba. Unos meses después, otra persona añade un scope que hace join con una segunda tabla. En el diff, cada cambio parece un retoque mínimo de presentación. Ninguno toca una migración, así que el índice se queda exactamente como el día en que se creó, mientras la consulta para la que se construyó se va convirtiendo poco a poco en otra consulta.
El ORDER BY es la parte traicionera, porque casi nadie lo ve como una decisión de rendimiento. Parece formato. Para PostgreSQL, en cambio, es un requisito: o un índice le entrega las filas ya en ese orden, o la base de datos reúne todas las filas que coinciden y las ordena ella misma. Piensa en un cliente veterano con decenas de miles de eventos. Ese ordenamiento se ejecuta en cada carga de su panel, y cuando se juntan unas cuantas de esas peticiones, tu pool de PHP-FPM se llena de workers que solo están esperando a Postgres.
Así que esta es mi propuesta. Haz que el índice y la consulta se conozcan. El método del repositorio lleva un comentario que nombra el índice del que depende, y la migración lleva un comentario que nombra el método para el que se creó. Un cambio en la cadena del builder de ese método debería disparar la misma pregunta de revisión que un cambio de esquema: ¿sigue encajando el índice con lo que pide esta consulta? Si a tu equipo le gusta automatizar, un job de CI puede poblar una tabla y lanzar EXPLAIN sobre tus consultas más calientes. En cualquier caso, quien cambia el orden debería ser quien tenga que pensar en el índice.
El mejor argumento en contra es serio, y me lo tomo en serio. Los índices son infraestructura compartida. Si piensas en un índice por consulta, una tabla de eventos acaba con catorce, y cada INSERT tiene que actualizar los catorce. En un log con mucha escritura como order_events, eso se nota en inserciones más lentas, un autovacuum más ocupado y un planificador eligiendo entre casi duplicados. Además, las estadísticas del planificador se desvían, así que un índice perfecto en marzo puede quedar ignorado en noviembre. Si atas los índices demasiado a consultas concretas, cambias lecturas lentas por escrituras lentas y un esquema que nadie se atreve a tocar.
Aun así, me quedo con el acoplamiento, y la razón es precisamente esa acumulación. Los índices se amontonan porque nadie sabe de quién son. Un índice sin un consumidor con nombre no se puede eliminar con seguridad, así que nunca se elimina, y el siguiente desarrollador añade otro al lado por si acaso. Cuando cada índice indica qué consultas dependen de él, fusionar dos es un trabajo rápido: sabes exactamente sobre qué sentencias volver a ejecutar EXPLAIN ANALYZE, y puedes mirar pg_stat_user_indexes para ver cuáles no se escanean nunca. Saber de quién es cada índice es lo que te permite limpiarlos. La ignorancia te deja con los catorce.
Conozco equipos que lo resuelven con convenciones de nombres, otros que mantienen un archivo markdown junto a las migraciones y al menos uno que revisa los contadores de idx_scan cada trimestre y elimina lo que sigue en cero. Me interesa saber qué aguanta de verdad después de dos años y tres rotaciones de desarrolladores. En tu código, ¿cómo se entera quien cambia una consulta de qué índice depende? ¿Y alguna vez eso ha frenado un panel de cuatro segundos antes de que lo viera un cliente?




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.