---
title: 'N+1 Sorunu ve Toplu Yükleme'
source: 'https://academia.sh/tr/kurslar/api-tasarimi/n-arti-1-sorunu-ve-toplu-yukleme'
course: 'Web API Tasarımı'
language: tr
updated: '2026-08-17T18:06:45+00:00'
license: 'CC BY-SA 4.0'
---

# N+1 Sorunu ve Toplu Yükleme

Liste uzunluğuyla birlikte büyüyen veri kaynağı turu sayısı, aynı iş parçacığındaki çağrıları tek turda birleştiren yükleyici katmanı ve 101 turdan 3 tura düşen ölçüm.

Çözücüler dersi bir sayıyı açıkta bırakmıştı: üç ödünç kaydı için üç üye çağrısı yapılmıştı,
oysa iki ayrı üye vardı. Parçalar ve değişkenler bu sayıya dokunmadı; alan birleştirme
yalnız aynı yanıt anahtarındaki çakışmaları giderir, farklı kayıtların aynı üyeyi
istemesini gidermez.

Bu ders sorunu ölçekte gösterir ve çözer. Veri kaynağı elli ödünç kaydı, sekiz üye ve on
iki kitap içerir.

```js
// veri.mjs — olcek icin uretilmis odunc verisi; her veri kaynagi turu sayilir
export const tur = { kitap: 0, uye: 0, odunc: 0 };
export const getirilenKayit = { kitap: 0, uye: 0 };
export const sifirla = () => { tur.kitap = tur.uye = tur.odunc = 0; getirilenKayit.kitap = getirilenKayit.uye = 0; };

const YAZAR = ["Cormen", "Knuth", "Kernighan", "Dijkstra", "Hoare", "Lamport"];
export const KITAPLAR = new Map(Array.from({ length: 12 }, (_, i) => {
  const isbn = `978-000000${String(i).padStart(2, "0")}`;
  return [isbn, { isbn, baslik: `Kitap ${i}`, yazar: YAZAR[i % YAZAR.length], kopya: 1 + (i % 3) }];
}));
export const UYELER = new Map(Array.from({ length: 8 }, (_, i) => {
  const id = `U-${1001 + i}`;
  return [id, { id, olusturma: "2025-01-01", ad: `Üye ${i}` }];
}));
export const ODUNCLER = new Map(Array.from({ length: 50 }, (_, i) => {
  const id = `O-${i + 1}`;
  return [id, { id, olusturma: "2026-01-01", uyeId: `U-${1001 + (i % 8)}`,
    durum: i % 3 === 0 ? "KAPALI" : "ACIK", iadeTarihi: "2026-04-01",
    kalemler: [{ isbn: `978-000000${String(i % 12).padStart(2, "0")}`, sube: "merkez" }] }];
}));

// Tekil erisimler: her cagri bir tur
export const kitapGetir = (isbn) => { tur.kitap++; getirilenKayit.kitap++; return KITAPLAR.get(isbn); };
export const uyeGetir = (id) => { tur.uye++; getirilenKayit.uye++; return UYELER.get(id); };
export const oduncListesi = () => { tur.odunc++; return [...ODUNCLER.values()]; };

// Toplu erisimler: tek turda birden cok anahtar
export const kitaplariGetir = (isbnler) => { tur.kitap++; getirilenKayit.kitap += isbnler.length; return isbnler.map((i) => KITAPLAR.get(i)); };
export const uyeleriGetir = (idler) => { tur.uye++; getirilenKayit.uye += idler.length; return idler.map((i) => UYELER.get(i)); };
```

Ayrıştırıcı, şema, düzleyici ve çalıştırıcı önceki derslerde yazıldıkları hâliyle
kullanılır; yeni olan veri kaynağı ile yükleyici katmanıdır.

## Sorunun Adı

Elli ödünç kaydını üyeleriyle birlikte isteyen bir sorgu, veri kaynağına elli bir kez
gider: bir kez listeyi almak için, elli kez her kaydın üyesini almak için. Kitap alanı
eklendiğinde elli tur daha. Buna **N+1 sorunu** denir: bir liste sorgusu artı liste
uzunluğu kadar tekil sorgu.

Sorunun kaynağı çözücü modelidir. `Odunc.uye` çözücüsü tek bir ödünç kaydı görür ve o kaydın
üyesini getirir; kendisinin kırk dokuz kardeşi olduğunu bilmez. Her çözücünün yalnız kendi
kaynağını görmesi, çözücü modelinin gücüdür — ve bu sorunun da nedenidir.

Çözüm, çözücüleri değiştirmek değil, aralarına bir katman koymaktır. **Yükleyici**, kendisine
gelen anahtarları hemen getirmez; aynı iş parçacığında istenen anahtarları biriktirir ve
tek bir toplu çağrıya indirir. İkinci görevi tekilleştirmedir: aynı anahtar iki kez
istenirse bir kez getirilir.

```js
// yukleyici.mjs — ayni is parcaciginda istenen anahtarlari tek toplu cagriya indirir
// Ikinci gorevi tekillestirme: ayni anahtar iki kez istenirse bir kez getirilir.
export function yukleyiciKur(topluGetir) {
  let bekleyen = new Map();          // anahtar -> [coz, ...]
  let planlandi = false;

  const bosalt = async () => {
    const kume = bekleyen; bekleyen = new Map(); planlandi = false;
    const anahtarlar = [...kume.keys()];
    const degerler = await topluGetir(anahtarlar);
    anahtarlar.forEach((a, i) => kume.get(a).forEach((coz) => coz(degerler[i])));
  };

  return (anahtar) => new Promise((coz) => {
    if (!bekleyen.has(anahtar)) bekleyen.set(anahtar, []);
    bekleyen.get(anahtar).push(coz);
    if (!planlandi) { planlandi = true; queueMicrotask(bosalt); }
  });
}
```

Toplama penceresi `queueMicrotask` çağrısıyla belirlenir: o an çalışan iş parçacığı bitene
kadar gelen bütün anahtarlar aynı kümeye düşer. Çözücüler dersinde ölçülen düzey sıralı
yürütme burada karşılığını buluyor — aynı düzeydeki elli `Odunc.uye` çağrısı zaten birlikte
başlatılıyordu, dolayısıyla hepsi aynı pencereye girer.

## Ölçüm

```js
// kosu.mjs — tekil ve toplu cozuculerle ayni sorguyu calistirip tur sayisini karsilastirir
import { ayristir } from "./ayristirici.mjs";
import { calistir } from "./calistirici.mjs";
import { SEMA } from "./sema.mjs";
import { yukleyiciKur } from "./yukleyici.mjs";
import { oduncListesi, kitapGetir, uyeGetir, kitaplariGetir, uyeleriGetir, tur, getirilenKayit, sifirla } from "./veri.mjs";

const SORGU = `{ oduncler { id durum uye { ad } kalemler { sube kitap { baslik yazar } } } }`;

const TEKIL = {
  Sorgu: { oduncler: () => oduncListesi() },
  Odunc: { uye: (o) => uyeGetir(o.uyeId) },
  Kalem: { kitap: (k) => kitapGetir(k.isbn) },
};

// Toplu cozuculer, yukleyicileri istek basina baglamdan alir.
const TOPLU = {
  Sorgu: { oduncler: () => oduncListesi() },
  Odunc: { uye: (o, _, b) => b.uyeYukle(o.uyeId) },
  Kalem: { kitap: (k, _, b) => b.kitapYukle(k.isbn) },
};

async function olc(baslik, cozucular, baglamUret) {
  sifirla();
  const iz = [];
  const s = await calistir(SEMA, cozucular, ayristir(SORGU), { baglam: baglamUret(), izleyici: (b) => iz.push(b) });
  const toplam = tur.kitap + tur.uye + tur.odunc;
  console.log(`${baslik.padEnd(14)} çözücü çağrısı=${String(iz.length).padStart(3)}  ` +
    `veri kaynağı turu: ödünç=${tur.odunc} üye=${String(tur.uye).padStart(2)} kitap=${String(tur.kitap).padStart(2)} toplam=${String(toplam).padStart(3)}  ` +
    `getirilen kayıt: üye=${getirilenKayit.uye} kitap=${getirilenKayit.kitap}`);
  return s.data.oduncler.length;
}

const n = await olc("tekil", TEKIL, () => ({}));
await olc("toplu", TOPLU, () => ({
  uyeYukle: yukleyiciKur(uyeleriGetir),      // istek basina yeni yukleyici
  kitapYukle: yukleyiciKur(kitaplariGetir),
}));
console.log(`\nyanıttaki ödünç kaydı: ${n}   ayrı üye: 8   ayrı kitap: 12`);
```

```
tekil          çözücü çağrısı=451  veri kaynağı turu: ödünç=1 üye=50 kitap=50 toplam=101  getirilen kayıt: üye=50 kitap=50
toplu          çözücü çağrısı=451  veri kaynağı turu: ödünç=1 üye= 1 kitap= 1 toplam=  3  getirilen kayıt: üye=8 kitap=12

yanıttaki ödünç kaydı: 50   ayrı üye: 8   ayrı kitap: 12
```

Üç sayı okunmalıdır.

**Tur sayısı 101'den 3'e indi.** Sorgu değişmedi, çözücü sayısı değişmedi, yanıt değişmedi;
değişen yalnız çözücülerin veri kaynağına nasıl gittiğidir.

**Çözücü çağrısı sayısı 451'de sabit kaldı.** Yükleyici çözücüleri azaltmaz; her ödünç
kaydının `uye` alanı yine ayrı ayrı çözülür. Azalttığı şey, o çözücülerin veri kaynağına
yaptığı turdur. Bu ayrım önemlidir: yürütmenin biçimi sorgudan gelir ve değiştirilemez,
veri erişiminin biçimi çözücüden gelir ve değiştirilebilir.

**Getirilen kayıt sayısı 50'den 8'e indi.** Tekilleştirmenin payı budur. Elli ödünç kaydı
sekiz üyeye dağıldığı için aynı üye ortalama altı kez isteniyordu; yükleyici her birini bir
kez getirdi.

## Toplama Penceresi Yürütme Sırasına Bağlıdır

Yükleyicinin kazancı, anahtarların aynı pencereye düşmesine bağlıdır. Pencere bir iş
parçacığı kadardır ve arada bir bekleme varsa kapanır.

```js
// pencere.mjs — yukleyicinin toplama penceresi bir is parcacigi kadardir
import { yukleyiciKur } from "./yukleyici.mjs";

const izle = (ad) => (anahtarlar) => {
  console.log(`  ${ad}: bir turda ${anahtarlar.length} anahtar -> ${anahtarlar.join(", ")}`);
  return anahtarlar.map((a) => ({ id: a }));
};

console.log("aynı iş parçacığında istenen anahtarlar:");
const y1 = yukleyiciKur(izle("birlikte"));
await Promise.all(["U-1001", "U-1002", "U-1003", "U-1001"].map(y1));

console.log("sırayla beklenerek istenen anahtarlar:");
const y2 = yukleyiciKur(izle("sırayla "));
for (const a of ["U-1001", "U-1002", "U-1003"]) await y2(a);
```

```
aynı iş parçacığında istenen anahtarlar:
  birlikte: bir turda 3 anahtar -> U-1001, U-1002, U-1003
sırayla beklenerek istenen anahtarlar:
  sırayla : bir turda 1 anahtar -> U-1001
  sırayla : bir turda 1 anahtar -> U-1002
  sırayla : bir turda 1 anahtar -> U-1003
```

Dört anahtar istendi, üçü getirildi: `U-1001` iki kez istenmiş ve bir kez getirilmiştir.
Aynı anahtarlar sırayla beklenerek istendiğinde üç ayrı tur oluştu; toplama hiç işlemedi.

Buradan iki kural çıkar. Çözücü içinde gereksiz `await` kullanmak toplamayı bozar: bir
çözücü önce başka bir şeyi bekleyip sonra yükleyiciyi çağırırsa, kardeşleriyle aynı
pencereye giremez. Ve mutasyonlarda kök alanlar sırayla yürütüldüğü için kök düzeyde toplama
olmaz; toplama alt düzeylerde yine işler.

## Yükleyicinin Ömrü

Ölçüm betiğinde yükleyiciler her çalıştırmada yeniden kuruluyor. Bu bir ayrıntı değil,
zorunluluktur: **yükleyici istek başına yaratılır.**

Nedeni tekilleştirmenin aynı zamanda bir önbellek olmasıdır. Yükleyici bir isteğin ömrü
boyunca aynı anahtar için aynı değeri döndürür; bu, tek bir yanıtın kendi içinde tutarlı
olmasını sağlar. İstekler arasında paylaşılan bir yükleyici ise eski veriyi süresiz
saklayan bir önbelleğe dönüşür ve iki ayrı kullanıcının isteği aynı kayıtları paylaşmaya
başlar. İstek başına yaratılan yükleyici, ömrü bittiğinde birlikte gider.

Bu, bağlamın neden var olduğunu da açıklar. Yükleyiciler bağlama konur, çünkü bağlam istek
başına yaratılan ve bütün çözücülerin eriştiği tek yapıdır.

## Özet

- N+1 sorunu bir liste sorgusu artı liste uzunluğu kadar tekil sorgudur; kaynağı, her
  çözücünün yalnız kendi kaynağını görmesidir.
- Yükleyici, aynı iş parçacığında istenen anahtarları biriktirip tek toplu çağrıya indirir
  ve aynı anahtarı bir kez getirir.
- Elli kayıtlık sorguda veri kaynağı turu 101'den 3'e, getirilen kayıt sayısı 50'den 8'e
  iner; çözücü çağrısı sayısı 451'de sabit kalır.
- Yürütmenin biçimi sorgudan gelir ve değiştirilemez; veri erişiminin biçimi çözücüden gelir
  ve değiştirilebilir.
- Toplama penceresi bir iş parçacığı kadardır; sırayla beklenen çağrılar ayrı turlar oluşturur
  ve çözücü içindeki gereksiz beklemeler toplamayı bozar.
- Yükleyici istek başına yaratılır; paylaşılan bir yükleyici süresiz önbelleğe dönüşür ve
  ayrı isteklerin verisini karıştırır.

## Sonraki Adım

Elli ödünç kaydının tamamı tek yanıtta döndü. Kütüphanenin gerçek ödünç sayısı elli değil,
yüz binlerdir ve hiçbir istemci hepsini istemez. Kaynak tasarımı konusunda sayfalama üç
biçimde ele alınmıştı — ofset, imleç ve anahtar tabanlı — ama oradaki tasarım adres ve sorgu
parametreleri üzerine kuruluydu. Sorgu temelli yaklaşımda sayfalama bir uç noktanın değil
bir **alanın** özelliğidir ve iç içe her listede yeniden ortaya çıkar. Sonraki ders imleç
temelli bağlantı modelini kurar, sayfa bilgisinin nereye yazılacağını belirler ve iç içe
listelerde sayfalamanın nasıl davrandığını gösterir.
