eduardweb.
PerformanceIntermediar#nodejs#performance#prisma#database

Cum prinzi și omori query-urile N+1 în Prisma folosind $metrics

De Diana Oprea, 4 iul. 2026 · 13 vizualizări · 2 like-uri

Postat 4 iul. 2026
typescript
// Cum extragem metricile pentru a detecta excesul de query-uri
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function checkQueryAbuse() {
  const metrics = await prisma.$metrics.json();
  const queryCounter = metrics.counters.find(
    (c) => c.name === 'prisma_client_queries_total'
  );
  
  console.log(`Query-uri executate până acum: ${queryCounter?.value}`);
}

// REZOLVAREA: În loc de interogări în loop, folosim select targetat
async function getArticlesOptimized() {
  return await prisma.article.findMany({
    take: 20,
    select: {
      id: true,
      title: true,
      author: {
        select: {
          id: true,
          name: true // aducem doar numele, nu tot profilul
        }
      }
    }
  });
}

Am pățit-o acum un an pe un proiect cu vreo 12.000 de useri activi, unde o pagină de dashboard aparent simplă începuse să răspundă în mai bine de 4 secunde. Clasica problemă: un query N+1 mascat frumos de abstractizarea ORM-ului. Prisma e genială pentru Developer Experience, dar dacă nu ești atent, te lasă să scrii cod care bombardează baza de date cu sute de interogări inutile într-un singur request.

Faza cu Prisma e că încearcă să fie deșteaptă. Are un mecanism intern de batching (un fel de dataloader), dar acesta funcționează doar dacă interogările sunt trimise în exact aceeași fază de execuție a event loop-ului. Dacă ai un map asincron sau un loop în care faci query-uri unul câte unul pe baza ID-urilor primite anterior, batching-ul e complet inutil.

Cum ne-am prins de problemă folosind $metrics

Pe local e ușor să vezi logurile în consolă, dar în staging sau producție nu poți lăsa log: ['query'] activ fiindcă îți umple discul instant. Soluția curată pe care am aplicat-o a fost folosirea API-ului de $metrics oferit direct de Prisma Client.

Am scris o funcție utilitară care rulează pe staging după anumite request-uri cheie. Aceasta interoghează engine-ul de Rust al Prisma și ne spune exact câte query-uri s-au executat în baza de date. Dacă pentru un singur request HTTP către lista de postări aveam mai mult de 5 query-uri la baza de date, trimiteam un avertisment în Slack.

Folosind $metrics în format JSON, poți extrage contorul prisma_client_queries_total. Când am văzut că o singură listare de 50 de articole genera 101 query-uri (1 pentru articole, și câte 2 pentru autorul și comentariile fiecărui articol), am știut exact unde e buba.

Rezolvarea cu include smart și trade-off-ul de memorie

Soluția clasică este să folosești include sau select pentru a forța Prisma să facă join-urile necesare direct în baza de date. În loc să lași codul din controller să ceară autorul separat pentru fiecare articol, îi spui bazei de date să le aducă la pachet.

Dar aici apare un trade-off destul de periculos pe care mulți îl ignoră.

Când folosești include în Prisma, ea generează uneori query-uri cu JOIN-uri masive sau execută query-uri separate optimizate pe care apoi le combină în memorie, în Node.js. Dacă relația pe care o incluzi conține tabele mari (de exemplu, vrei să incluzi toate comentariile unui articol, iar unele articole au mii de comentarii), riști să umpli memoria RAM a containerului. Am pățit să avem crash-uri cu Out of Memory pe un container de 512MB din cauza unui include făcut fără cap.

De aceea, regula mea de aur este să nu folosesc aproape niciodată include simplu pe relații one-to-many fără să filtrez sau să limitez rezultatele. În schimb, folosesc select pentru a aduce doar câmpurile de care am strictă nevoie, evitând să încarc în memorie obiecte gigantice de tip JSON.

Cum monitorizați voi query-urile astea în producție? Folosiți APM-uri dedicate sau v-ați scris și voi wrapperi custom peste clientul de Prisma?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.