import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function monitorQueries() {
// Activăm colectarea metricilor în format JSON
const metrics = await prisma.$metrics.json();
// Căutăm contorul specific pentru query-urile rulate
const queryCounter = metrics.counters.find(
(c) => c.name === 'prisma_client_queries_total'
);
const totalQueries = queryCounter ? queryCounter.value : 0;
if (totalQueries > 15) {
console.warn(`[Warning] Posibil N+1 detectat. S-au rulat ${totalQueries} interogări!`);
}
}Am dat acum ceva timp peste o problemă urâtă de performanță pe un serviciu care deservea în jur de 14.000 de utilizatori activi în fiecare zi. Baza de date dădea semne serioase de oboseală la acțiuni banale, deși aveam indecși puși ca la carte pe tabelele mari. Vinovatul? Clasicul N+1, mascat foarte elegant de sintaxa curată din Prisma ORM.
Cum ne furăm singuri căciula cu Prisma
Prisma te face să uiți că în spate rulează SQL pur. Ai un model de User și unul de Post. Vrei să afișezi o listă de utilizatori și, pentru fiecare, să-i tragi și ultimele postări.
Dacă faci greșeala să iei utilizatorii și apoi să rulezi o buclă în Node.js în care apelezi prisma.post.findMany pentru fiecare ID în parte, ai dat de dracu'. Pentru 100 de utilizatori transmiși în interfață, codul tău va rula 101 interogări separate în baza de date. Network roundtrips, overhead de conexiuni, procesare inutilă, tot tacâmul.
Partea proastă e că pe mașina locală de dezvoltare, cu o bază de date SQLite sau un Postgres local care are zero latență, totul se mișcă instant. Problema devine vizibilă abia când ajungi în staging sau producție, unde latența rețelei dintre serverul de API și serverul de baze de date începe să-și spună cuvântul.
Detecția rapidă în producție folosind $metrics
În proiectele mari e greu să prinzi toate aceste bucle doar făcând code review manual, mai ales când logica este împrăștiată în mai multe servicii sau controllere. Aici m-a salvat API-ul de $metrics pus la dispoziție de Prisma Client.
Am scris un middleware simplu pentru Express (care poate fi adaptat ușor pentru NestJS sau Fastify) care verifică numărul de interogări rulate în timpul unui singur request HTTP. Ideea e simplă: pornești un contor la începutul request-ului și verifici diferența la final. Dacă pentru un singur endpoint depășești un prag rezonabil de, să zicem, 15 interogări, trimiți o alertă în sistemul de monitorizare.
Această metodă ne-a ajutat să depistăm trei endpoint-uri critice care făceau peste 80 de query-uri fiecare din cauza unor map-uri asincrone rulate aiurea.
Rezolvarea cu include și compromisul de care te lovești
Soluția directă este să folosești include sau select pentru a aduce datele corelate dintr-o singură mișcare. Când scrii un include, Prisma este destul de deșteaptă: ea nu face un JOIN clasic în SQL (care uneori poate fi lent dacă returnează rânduri duplicate), ci rulează două interogări optimizate. Una pentru părinți și una cu operatorul IN pentru copii, după care le combină direct în memorie, în Node.js.
Am redus timpul de răspuns cu 65% pe endpoint-ul de feed doar făcând acest switch simplu.
Totuși, există un trade-off destul de periculos aici: memoria RAM. Dacă faci include pe relații foarte adânci (de exemplu, vrei să aduci utilizatorii, postările lor, comentariile la postări și autorii acelor comentarii), Prisma va încărca un volum uriaș de date în memoria procesului Node.js pentru a le mapa. Am pățit ca un container de producție limitat la 512MB de RAM să crape cu Out of Memory din cauza unui astfel de query masiv rulate de câțiva utilizatori simultan.
Pentru relații foarte complexe sau tabele uriașe, uneori e mult mai sănătos să folosești noua opțiune relationJoins din Prisma (care forțează un JOIN real la nivel de bază de date) sau chiar să scrii un query nativ cu prisma.$queryRaw.
Voi cum monitorizați numărul de query-uri trimise către baza de date? Vă bazați exclusiv pe tool-uri de APM gen Datadog sau aveți logici custom scrise în cod?