Recetia: el 80 % del temps se'n va a portar dades

Recetia genera receptes creuant 1.506 ingredients, els seus perfils de sabor i olor, i les tècniques de cuina que els escauen. Aquest és el motor que fa aquest creuament.

Producte propi, en desenvolupament — sense clients en producció

El teu equip té un càlcul lent o una capa de dades que ningú no vol tocar?

En parlem
Portar dades
80% del temps
Càlcul pur
5% del temps
Accessos fora de norma
3 → 0

Què mesura aquesta pàgina

El que segueix són els seus números reals, mesurats executant-lo contra la base de dades, no estimats, i el que vam trobar quan el vam auditar demanant que se cerquessin errors en lloc de confirmar-los.

Més avall apareixen dues xifres diferents i convé no confondre-les: el flux complet de generació són 40 consultes, i la simulació del panell en són 16. Són mesures de coses diferents.

Què és el que costa en un motor de càlcul?

No pensar. Anar a buscar.

Per proposar una recepta cal respondre moltes preguntes alhora: a què sap cada ingredient, quina tècnica li convé, quines receptes s'assemblen al que hi ha a la nevera, quant costa, quant triga.

Escrit de la manera directa, cada pregunta va pel seu compte a la base de dades. I moltes demanen el mateix: la llista de tècniques de cuina cal per calcular l'afinitat, per calcular la compatibilitat, per ordenar els passos i per comparar amb receptes existents. Quatre vegades la mateixa consulta.

El motor separa dues coses que normalment van barrejades. Primer reuneix totes les dades que caldran, agrupant les peticions repetides en una de sola. Després fa els comptes, sense tornar a tocar la base de dades.

On se'n va el temps, exactament?

Una execució completa es resol en unes nou centèsimes de segon. Instrumentant cada fase per separat: portar dades, 74 ms, el 80 %. Càlcul pur, 4,7 ms, el 5 %. Orquestració, la resta fins a 92 ms, el 15 %.

Si el càlcul són cinc mil·lisegons de noranta-dos, optimitzar fórmules no serveix de res: podries fer els comptes el doble de ràpid i ningú no notaria la diferència. Tot el marge és en com es demanen les dades.

Per això el motor està construït al voltant d'aquesta separació, i no al voltant dels algoritmes. És una decisió que només es pot prendre havent mesurat: la intuïció diu el contrari.

El 80 % del temps se'n va a portar dades i el 5 % a calcular. Això és el que decideix el disseny, no els algoritmes.

Com escala?

Mesurat, no extrapolat: el motor complet amb volums creixents, tres execucions cadascun, mediana. Amb 1 ingredient, 49 ms. Amb 10, 74 ms. Amb 50, 146 ms. Amb 100, 217 ms. Amb 200, 270 ms. Amb 400, 403 ms.

Multiplicar el volum per 400 multiplica el temps per deu. El nombre de consultes no creix amb els ingredients; el que creix és la feina dins de cadascuna. La corba no té genoll en el rang mesurat.

Què va passar en auditar la regla?

El motor té una norma central: les dades es demanen en un lloc, i a la part de càlcul no es toca la base de dades. Està escrita a la documentació, als comentaris i a la guia de revisió.

No es complia en tres llocs. Un d'ells havia entrat mesos abans, en una funció nova, fent exactament el que la norma prohibeix. Tots els tests continuaven en verd.

Hi havia un test de pressupost —comptava peticions i fallava si pujaven— i funcionava. Però comptava peticions agrupades, no consultes reals. Una petició amb dades associades compta com una i n'executa dues. Així, un grapat de peticions amagava quaranta consultes.

Per què un test de propietat no ho hauria evitat?

Aquesta és la part incòmoda, i és la que més ens emportem.

La conclusió fàcil seria "faltava un test". És més subtil que això. En escriure el test que comprova la regla, amb les dades de prova que ja existien només detectava dos dels tres incompliments. Amb una sola tècnica de cuina a la taula i sense encadenaments, dues branques del codi no s'executen mai.

És a dir: encara que el test hagués existit des del principi, l'error hauria entrat igualment, en verd. No per falta de test, sinó perquè les dades amb què corria no arribaven fins allà. Aquest és l'error que comet tothom qui sí que escriu tests.

La solució va ser doble. Enriquir les dades de prova fins que les tres branques s'executin. I afegir un segon test que no depèn de dades en absolut: llegeix el codi font de cada part del motor i cerca accessos a la base de dades on no hi han de ser, incloses les parts que cap test no recorre.

L'estàtic es va verificar injectant-hi una violació a propòsit i comprovant que saltava. D'un test que no has vist fallar no saps si funciona.

Test dinàmic amb dades pobres, en veu 2 de 3. Amb dades completes, veu les 3. Test estàtic, sense dades, veu fins i tot el que ningú no executa.

Per què després de l'arranjament hi ha més consultes, no menys?

De 39 a 40. Sembla un empitjorament i és el contrari.

Aquestes consultes ja s'estaven executant. El que passava és que, en saltar-se el sistema d'agrupació, eludien el comptador: no apareixien en cap mètrica ni en cap test. En reconduir-les per on correspon es van fer visibles.

El pressupost intern va pujar de 21 a 24, i aquest canvi va quedar anotat amb el seu motiu al mateix test. Un límit que s'afluixa sense explicació deixa de ser un límit.

El balanç complet: consultes SQL per execució, de 39 a 40, les mateixes però ara comptades. Accessos fora de norma, de 3 a 0. Columnes portades per consulta, de 19 a 12. Repartiment càrrega/càlcul, de 79/11 a 80/5. Tests a la suite, de 501 a 520, sense trencar-ne cap.

El cas de criteri: avisar en lloc de denegar

El perfil de sabor es calcula ponderant cada ingredient pel seu pes en grams. Si el client no envia les quantitats, tots pesen igual: un pessic de safrà compta com un quilo de patates.

La reacció immediata va ser exigir les quantitats i rebutjar la petició sense elles. Correcte sobre el paper i trencat a la pràctica: els dos productes que consumeixen aquest motor avui no les envien. Haurien quedat sense servei.

La bona sortida era una altra. La petició s'accepta, i la resposta declara quina ponderació s'ha aplicat realment: per grams, parcial o sense ponderar. S'aconsegueix el mateix —que ningú no confongui un perfil ponderat amb un de pla— sense deixar ningú fora.

I de passada va aparèixer una cosa que l'avís hauria tapat: les quantitats arribaven del JSON amb la clau en text i es descartaven en silenci. La ponderació per grams no havia funcionat mai en aquesta ruta. Sense arreglar-ho, l'avís hauria dit "ponderat per grams" sobre un perfil pla, que és pitjor que no avisar.

Què ens emportem?

La documentació descriu intencions; el codi, fets. L'arquitectura documentada parlava de sis components i disset orígens de dades. El codi en tenia dotze i vint. Cap xifra no era mentida quan es va escriure: ningú no les va tornar a mesurar. Auditar és executar, no llegir.

Un test verd pot ser un test cec. El de pressupost existia i funcionava, però mesurava la unitat equivocada. El de propietat, amb dades pobres, en veia dos de tres. Abans de confiar en un test, comprova que falla quan ha de fallar.

El correcte que ho trenca tot a tothom no és el correcte. Exigir les quantitats era defensable en abstracte i desastrós en concret.

Nota sobre les xifres

Tots els números surten d'executar el motor contra la base de dades de desenvolupament —1.506 ingredients, 22 tècniques, 34 receptes— instrumentant cada fase. Els temps són la mediana de tres execucions.

Dues condicions que convé declarar: dues taules d'afinitat són buides, així que aquests temps són un terra i no un sostre; i el corpus de receptes és petit, de manera que la corba d'escalat descriu el comportament amb molts ingredients, no amb moltes receptes.

Tecnologies

  • Python 3.12
  • Django 5
  • PostgreSQL 16
  • pgvector
  • Celery
  • Redis