Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Optymalizacja zapytań i N+1 problem

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 — cichy zabójca wydajności

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.

Przykład problemu N+1

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ą.

Rozwiązanie: Prisma include i select

Include — ładowanie relacji w jednym zapytaniu

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});

Select — pobieraj tylko potrzebne pola

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ć.

Paginacja — nie pobieraj wszystkiego naraz

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);

Cursor-based pagination

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});

Indeksy bazodanowe — przyspieszanie wyszukiwania

Indeks w bazie danych działa jak spis treści w encyklopedii Metropolii Quantum — zamiast przeszukiwać każdą stronę, od razu wiesz, gdzie szukać.

Definiowanie indeksów w Prisma Schema

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?

  • Na kolumnach używanych w
    where
    (filtry)
  • Na kolumnach używanych w
    orderBy
    (sortowanie)
  • Na kluczach obcych (foreign keys) — Prisma często robi to automatycznie
  • Na kolumnach używanych w
    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.

Batching — grupowanie operacji

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]);

Monitorowanie wolnych zapytań

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ć:

  • Czas odpowiedzi poszczególnych zapytań
  • Liczbę zapytań na żądanie HTTP (wykrycie N+1)
  • Wolne zapytania powyżej ustalonego progu

Podsumowanie

Optymalizacja zapytań bazodanowych to kluczowy aspekt budowania wydajnych aplikacji w Next.js:

  1. Problem N+1 — unikaj pętli z pojedynczymi zapytaniami. Używaj
    include
    do ładowania relacji.
  2. Select — pobieraj tylko potrzebne pola, nie całe rekordy.
  3. Paginacja — używaj
    skip/take
    lub cursor-based pagination zamiast pobierania wszystkiego.
  4. Indeksy — dodawaj na kolumnach filtrowanych i sortowanych w schemacie Prisma.
  5. Batching — grupuj operacje w
    createMany
    lub
    $transaction
    .
  6. Monitoring — loguj wolne zapytania i analizuj liczbę zapytań na request.

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.

Przejdź do CodeWorlds