Todos los artículos

Arquitectura · Rendimiento · Refactor

El desperdicio no estaba en las consultas, sino arriba

La optimización de consultas suele plantearse como un problema de escribir mejor SQL: índices, select_related, evitar el N+1. Todo eso importa. En este caso ya estaba resuelto, y aun así una simulación del panel ejecutaba once unidades de cálculo y dieciséis consultas para devolver un solo dato. El desperdicio estaba una capa más arriba.

Contexto: esto ocurre en Recetia, un producto propio en desarrollo, sin clientes en producción. Las cifras salen de ejecutar el motor contra PostgreSQL con los contadores del orquestador, y corresponden a la simulación del panel, no al flujo completo de generación, que son cuarenta consultas y se mide aparte.

¿Cuál era el síntoma?

Un fichero que había crecido hasta 876 líneas, tres veces y media el siguiente fichero de métricas del proyecto, donde el resto vive entre 150 y 250. Dentro, un único método hacía cinco trabajos distintos: filtrar técnicas por el equipamiento del usuario, ordenarlas por sus encadenamientos, aplicar penalizaciones de balance del plato, estimar tiempos, y asignar una técnica a cada ingrediente. Ninguno de esos cinco cálculos se podía pedir por separado. Y eso ya se estaba pagando.

¿Cuál era el coste real?

El panel ofrece una simulación: qué técnica va a cada ingrediente. Para responderla, el código ejecutaba la métrica de cocción entera y después descartaba todo menos una clave. Calculaba el perfil sensorial, la transformación sensorial, la matriz de compatibilidad entre técnicas y la estimación de tiempos, para tirarlos. Siete consultas y cinco unidades de cálculo de trabajo desperdiciado en cada ejecución.

¿Qué cambia al partirlo?

Separando cada cálculo en su propia unidad, el orquestador resuelve un grafo mínimo para cada pregunta. El flujo completo sigue en once unidades y dieciséis consultas, porque es el envoltorio íntegro. Pedir los tracks por ingrediente baja a seis unidades y nueve consultas: siete consultas y cinco unidades menos. Los tiempos de cocción, el balance del plato y las técnicas candidatas antes no se podían pedir por separado, y ahora cuestan nueve, nueve y siete consultas.

¿Y no cuesta más añadir cinco piezas?

Aquí está lo interesante, porque contradice la intuición: añadir cinco unidades nuevas costó cero consultas adicionales en el flujo completo. El orquestador deduplica los cargadores de datos por clase y clave de parámetros, así que declarar el mismo cargador en cuatro unidades distintas sigue costando una sola consulta. Es lo que permite partir un método grande en piezas pequeñas sin pagar por ello. El presupuesto de consultas del proyecto, comprobado por un test que falla si sube, se mantuvo en 24.

¿Qué apareció al separar?

  • Qué herramienta falta. Antes, una técnica descartada producía un aviso de texto plano; ahora se devuelve la lista concreta de herramientas que faltan, y el panel puede decir "te falta un wok" en lugar de "técnica omitida".
  • Cuánto penaliza cada regla. Las penalizaciones se aplicaban a la puntuación y solo sobrevivía una cadena de texto; ahora se devuelve la regla, su tipo y el número.
  • Si el orden es real. Un contador nuevo distingue "estas técnicas están genuinamente secuenciadas" de "no había datos de encadenamiento y esto es simplemente el orden por puntuación".
  • Por qué se eligió esa técnica. La heurística que decide si un ingrediente va a calor seco o húmedo se calculaba y se descartaba; ahora es parte de la respuesta.

¿Cómo se hace sin romper a los consumidores?

Seis claves de salida alimentan la generación de texto, el emparejado de recetas y el panel. Romperlas habría convertido una optimización en una migración. La unidad original sobrevive como ensamblador: unas cuarenta líneas que reconstruyen exactamente el mismo diccionario desde los resultados de las cinco nuevas, preservando incluso el orden en que se concatenan los avisos. Ese detalle del orden parece trivial y no lo es: es el tipo de cosa que rompe un consumidor en silencio, sin error y sin test que lo detecte. Verificado contra la base de datos real: las seis claves intactas, la suite en verde, y ni una línea modificada en los cuatro módulos que consumen ese contrato.

¿En qué orden se hace un refactor así?

Se sostiene o se cae por el orden. Primero, una base verde de verdad: la suite completa antes de tocar nada, porque sin esa referencia no hay forma de distinguir una regresión propia de algo que ya estaba roto. Segundo, mover sin cambiar: las funciones puras salieron a módulos por responsabilidad sin alterar una línea de lógica, con la suite en verde antes de continuar. Tercero, extraer con red: cada unidad nueva comprobando el presupuesto después de cada paso. Ocho tests nuevos, y uno de ellos dedicado a afirmar lo que NO debe ejecutarse. Ese octavo es el que demuestra que el refactor sirvió: los otros siete comprueban que lo que funcionaba sigue funcionando, y solo ese comprueba que algo dejó de ocurrir. Casi nadie escribe tests de ausencia, y son los únicos que prueban una optimización.

La lección transferible

Un bloque de cálculo que solo se puede pedir entero obliga a todos sus clientes a pagar por todo, aunque necesiten una décima parte. Ninguna optimización de SQL arregla eso; hay que partir la unidad. Y el reverso, que es la parte alentadora: si la arquitectura deduplica bien sus accesos a datos, partir en piezas pequeñas no cuesta nada. Antes de reescribir una consulta, merece la pena preguntarse si el problema es cómo se pide el dato o cuánto de más se está pidiendo.

Arquitectura
Rendimiento
Django
Refactor