W podziemnych serwerowniach Metropolii Quantum, inżynierowie danych nieustannie monitorują przepływ informacji między bazami danych a aplikacjami. Każde nieoptymalne zapytanie to jak zator w tunelu transportowym — spowalnia cały system i marnuje zasoby. Jednym z najczęstszych problemów wydajnościowych jest tzw. problem N+1, który potrafi zamienić błyskawiczną aplikację w powolną maszynę wykonującą setki niepotrzebnych zapytań.
Problem N+1 występuje, gdy aplikacja wykonuje 1 zapytanie aby pobrać listę rekordów, a następnie N dodatkowych zapytań (jedno dla każdego rekordu) aby pobrać powiązane dane. To jak wysyłanie osobnego kuriera po każdy dokument zamiast pobrania wszystkich naraz.
Wyobraź sobie, że chcesz wyświetlić listę postów z autorami:
1// BAD: Problem N+1!
2// 1 zapytanie: pobierz wszystkie posty
3const posts = await prisma.post.findMany(); // 1 zapytanie
4
5// N zapytań: dla każdego posta pobierz autora
6for (const post of posts) {
7 const author = await prisma.user.findUnique({
8 where: { id: post.authorId }
9 }); // N zapytań!
10 post.author = author;
11}
12// Razem: 1 + N zapytań (jeśli 100 postów = 101 zapytań!)Przy 100 postach to 101 zapytań do bazy danych! Każde zapytanie to osobna podróż przez sieć — opóźnienia się kumulują.
Prisma pozwala na eager loading powiązanych danych za pomocą
include:1// GOOD: Jedno zapytanie z include!
2const posts = await prisma.post.findMany({
3 include: {
4 author: true, // Dołącz dane autora
5 comments: true, // Dołącz komentarze
6 categories: true, // Dołącz kategorie
7 }
8});
9// Prisma generuje wydajne JOINy — zamiast 101 zapytań, mamy kilka!Możesz zagnieżdżać include na wielu poziomach:
1const posts = await prisma.post.findMany({
2 include: {
3 author: {
4 include: {
5 profile: true, // Autor -> jego profil
6 }
7 },
8 comments: {
9 include: {
10 author: true, // Komentarz -> autor komentarza
11 },
12 orderBy: {
13 createdAt: 'desc' // Sortuj komentarze od najnowszych
14 }
15 }
16 }
17});Zamiast pobierać wszystkie kolumny rekordu, możesz wybrać tylko te, których potrzebujesz:
1// Pobierz tylko tytuł posta i imię autora
2const posts = await prisma.post.findMany({
3 select: {
4 id: true,
5 title: true,
6 createdAt: true,
7 author: {
8 select: {
9 name: true,
10 avatar: true,
11 }
12 }
13 }
14});
15// Mniejszy transfer danych = szybsza odpowiedź!Ważne:
select i include nie mogą być używane jednocześnie na tym samym poziomie. Jeśli używasz select, musisz jawnie wybrać wszystkie pola, które chcesz pobrać.Pobieranie tysięcy rekordów naraz to jak próba przetransportowania całej populacji Metropolii Quantum jednym pojazdem. Zamiast tego, używamy paginacji z
skip i take:1// Pobierz stronę 3 (20 rekordów na stronę)
2const page = 3;
3const pageSize = 20;
4
5const posts = await prisma.post.findMany({
6 skip: (page - 1) * pageSize, // Pomiń 40 rekordów
7 take: pageSize, // Pobierz 20 rekordów
8 orderBy: {
9 createdAt: 'desc'
10 },
11 select: {
12 id: true,
13 title: true,
14 author: {
15 select: { name: true }
16 }
17 }
18});
19
20// Policz łączną liczbę rekordów dla informacji o stronach
21const totalPosts = await prisma.post.count();
22const totalPages = Math.ceil(totalPosts / pageSize);Dla dużych zbiorów danych, paginacja oparta na kursorze jest wydajniejsza niż
skip/take:1// Pobierz następne 20 postów po danym ID
2const posts = await prisma.post.findMany({
3 take: 20,
4 cursor: {
5 id: lastPostId // Zacznij od tego rekordu
6 },
7 skip: 1, // Pomiń sam kursor
8 orderBy: {
9 id: 'asc'
10 }
11});Indeks w bazie danych działa jak spis treści w encyklopedii Metropolii Quantum — zamiast przeszukiwać każdą stronę, od razu wiesz, gdzie szukać.
1model Post {
2 id Int @id @default(autoincrement())
3 title String
4 content String
5 published Boolean @default(false)
6 authorId Int
7 createdAt DateTime @default(now())
8 author User @relation(fields: [authorId], references: [id])
9
10 // Indeks na kolumnie authorId — przyspiesza zapytania filtrujące po autorze
11 @@index([authorId])
12
13 // Indeks złożony — przyspiesza zapytania filtrujące po published + createdAt
14 @@index([published, createdAt])
15
16 // Unikalny indeks — gwarantuje unikalność i przyspiesza wyszukiwanie
17 @@unique([title, authorId])
18}Kiedy dodawać indeksy?
where (filtry)orderBy (sortowanie)JOIN (relacje)Uwaga: Indeksy przyspieszają odczyt, ale spowalniają zapis (insert/update), bo indeks musi być aktualizowany. Nie dodawaj indeksów na każdej kolumnie — tylko tam, gdzie faktycznie filtrujesz lub sortujesz.
Zamiast wykonywać wiele pojedynczych operacji, grupuj je w jedną transakcję:
1// BAD: Wiele osobnych zapytań
2for (const tag of tags) {
3 await prisma.tag.create({ data: { name: tag } });
4}
5
6// GOOD: Jedna operacja batch
7await prisma.tag.createMany({
8 data: tags.map(name => ({ name })),
9 skipDuplicates: true,
10});
11
12// GOOD: Transakcja dla powiązanych operacji
13const [post, notification] = await prisma.$transaction([
14 prisma.post.create({
15 data: { title: 'Nowy post', authorId: userId }
16 }),
17 prisma.notification.create({
18 data: { message: 'Nowy post!', userId: adminId }
19 }),
20]);Prisma pozwala logować zapytania i mierzyć ich czas wykonania:
1// prisma/client.ts
2import { PrismaClient } from '@prisma/client';
3
4const prisma = new PrismaClient({
5 log: [
6 { level: 'query', emit: 'event' },
7 { level: 'warn', emit: 'stdout' },
8 { level: 'error', emit: 'stdout' },
9 ],
10});
11
12// Loguj zapytania trwające dłużej niż 200ms
13prisma.$on('query', (e) => {
14 if (e.duration > 200) {
15 console.warn(`Slow query (${e.duration}ms): ${e.query}`);
16 }
17});
18
19export default prisma;W środowisku produkcyjnym warto monitorować:
Optymalizacja zapytań bazodanowych to kluczowy aspekt budowania wydajnych aplikacji w Next.js:
include do ładowania relacji.skip/take lub cursor-based pagination zamiast pobierania wszystkiego.createMany lub $transaction.Te techniki, stosowane przez inżynierów danych Metropolii Quantum, pozwalają utrzymać błyskawiczną responsywność aplikacji nawet przy rosnącej ilości danych i ruchu użytkowników.