import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function measureQueries(action: () => Promise<any>) {
const before = await prisma.$metrics.json();
const beforeQueries = before.counters.find(c => c.name === 'prisma_client_queries_total')?.value || 0;
const result = await action();
const after = await prisma.$metrics.json();
const afterQueries = after.counters.find(c => c.name === 'prisma_client_queries_total')?.value || 0;
const diff = Number(afterQueries) - Number(beforeQueries);
console.log(`[Metrics] Action executed ${diff} SQL queries.`);
return result;
}Salutare! Prisma e genială până când nu mai e. Chestia asta cu "scapi de SQL scris de mână" vine la pachet cu o problemă veche de când lumea: interogările N+1. Am pățit-o acum vreo 6 luni pe un proiect cu 12k de useri activi zilnic, unde un singur endpoint de dashboard începuse să îngenuncheze baza de date. CPU-ul pe RDS se ducea în 90% din senin.
Cum am dat de dracu' cu 12k request-uri
Problema la Prisma e că nu are lazy loading clasic ca în Hibernate, dar te lasă să faci prostii la fel de mari. Cel mai des văd cod de genul: iei o listă de utilizatori și apoi, într-un map cu Promise.all, apelezi prisma.post.findMany pentru fiecare user în parte ca să îi aduci postările.
Dacă ai 100 de useri, faci 1 interogare inițială și încă 100 de interogări separate în baza de date. Asta e definiția de manual pentru N+1. Prisma are un motor de batching intern (DataLoader pattern), dar dacă schimbi puțin structura sau pui logica în servicii diferite, batching-ul ăla moare silențios.
Detecția: Cum prinzi N+1 înainte să plângă DevOps-ul
În loc să stai cu ochii pe logurile din PostgreSQL, poți folosi clientul de Prisma ca să îți spună exact câte query-uri rulează pe bune sub capotă. Pentru asta ne folosim de $metrics.
Am scris o metodă simplă pe care o rulăm în mediul de staging. Aceasta verifică numărul de query-uri înainte și după execuția unui serviciu sau controller. Dacă un singur request HTTP face mai mult de 10 query-uri în DB, testele noastre de integrare crapă direct. Am economisit cam 30% la build time și resurse pe server doar eliminând aceste apeluri redundante.
Rezolvarea: Include inteligent și select
Cea mai simplă metodă de rezolvare e să folosești include pentru a aduce relațiile într-un singur query (sau într-un query optimizat de tip batch pe care Prisma îl face automat în spate prin sub-select-uri).
// Greșit: generează N+1 dacă e apelat în loop
const posts = await prisma.post.findMany({ where: { userId } });
// Corect: aduce totul batch-uit
const usersWithPosts = await prisma.user.findMany({
include: {
posts: true,
},
});
Trade-off-ul sincer: include te poate lăsa fără memorie
Sună simplu, nu? Pui include peste tot și gata, ai rezolvat performanța. Ei bine, nu.
Aici e capcana: include aduce absolut toate coloanele din tabela relaționată. Dacă tabela posts are un câmp de tip content de tip TEXT de câțiva megabytes, o să umpli memoria Node.js imediat. Am avut un caz în care un simplu include pe o relație de loguri a dublat consumul de RAM al containerului de backend în 10 minute.
Soluția? Folosește întotdeauna select în interiorul lui include ca să aduci doar id-ul, titlul sau ce ai strict nevoie pentru UI.
Voi cum monitorizați query-urile astea în producție? Vă bazați pe APM-uri externe sau aveți chestii custom în cod?