---
title: 'N+1 Sorgu Sorunu'
source: 'https://academia.sh/tr/kurslar/veri-erisim-katmani/n-arti-1-sorgu-sorunu'
course: 'Veri Erişim Katmanı ve İş Mantığı'
language: tr
updated: '2026-08-17T18:06:51+00:00'
license: 'CC BY-SA 4.0'
---

# N+1 Sorgu Sorunu

Sorgu sayısının kayıt sayısıyla büyümesi: sayan bir sarmalayıcıyla ölçüm, tek tek yükleme ile toplu getirme ve birleştirmenin karşılaştırılması, ölçeklenme tablosu, gidiş-dönüş maliyetinin etkisi ve sorgu bütçesinin bir sınama olarak yazılması.

İşlemler konusu yazmanın doğruluğunu kurdu: sınır nereye çizilir, okuma neyi görür,
eşzamanlı güncelleme nasıl çözülür, yeniden deneme neden güvenlidir. Hiçbiri hızla ilgili
değildi.

Doğru çalışan bir veri erişim katmanı yavaş olabilir ve yavaşlığın kaynağı çoğunlukla
sorgunun kendisi değil, sorgu **sayısıdır**. Bu kursun ilk dersinde küçük bir eşleyici üç
ödünç kaydı için on sorgu çalıştırmıştı. Bu ders o sayıyı ölçer, kayıt sayısıyla nasıl
büyüdüğünü gösterir ve sabitler.

## Ölçüm Verisi

Ölçüm için gerçek büyüklükte bir veri kümesi gerekir.

```js
// uret.mjs — olcum icin kutuphane verisini uretir: 200 kitap, 60 uye, 2000 odunc kaydi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";

for (const d of ["kutuphane.db", "kutuphane.db-wal", "kutuphane.db-shm"]) {
  rmSync(d, { force: true });
}
const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL);
CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL,
                    uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT);`);

db.exec("BEGIN");
const uyeEkle = db.prepare("INSERT INTO uye VALUES (?,?,?)");
for (let i = 1; i <= 60; i++) uyeEkle.run(i, `Uye${i}`, `Soyad${i}`);
const kitapEkle = db.prepare("INSERT INTO kitap VALUES (?,?,?)");
for (let i = 1; i <= 200; i++) kitapEkle.run(i, `Kitap ${i}`, `Yazar ${(i % 40) + 1}`);
const oduncEkle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
for (let i = 1; i <= 2000; i++) {
  const gun = String((i % 28) + 1).padStart(2, "0");
  const ay = String((i % 12) + 1).padStart(2, "0");
  oduncEkle.run(i, (i % 200) + 1, (i % 60) + 1, `2025-${ay}-${gun}`, i % 4 === 0 ? null : "2025-12-31");
}
db.exec("COMMIT");
console.log("uye:", db.prepare("SELECT count(*) AS n FROM uye").get().n,
            " kitap:", db.prepare("SELECT count(*) AS n FROM kitap").get().n,
            " odunc:", db.prepare("SELECT count(*) AS n FROM odunc").get().n,
            " acik:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
```

```sh
node uret.mjs
```

```
uye: 60  kitap: 200  odunc: 2000  acik: 500
```

## Sorguyu Görünür Kılmak

Ölçülmeyen sayı düşmez. Bağlantının önüne konan ince bir sarmalayıcı, çalışan sorguları ve
dönen satırları sayar.

```js
// sayan.mjs — calisan sorgulari sayan ince bir sarmalayici
import { DatabaseSync } from "node:sqlite";

export function sayanBaglanti(dosya) {
  const db = new DatabaseSync(dosya);
  const sayac = { sorgu: 0, satir: 0 };
  return {
    sayac,
    tek(sql, ...d) { sayac.sorgu += 1; const r = db.prepare(sql).get(...d);
                     if (r !== undefined) sayac.satir += 1; return r; },
    cok(sql, ...d) { sayac.sorgu += 1; const r = db.prepare(sql).all(...d);
                     sayac.satir += r.length; return r; },
    sifirla() { sayac.sorgu = 0; sayac.satir = 0; },
  };
}
```

## Üç Yaklaşım

Görev aynı: iade edilmemiş ödünç kayıtlarını kitap başlığı ve üye adıyla listele. Üç yol
denenir. **Tek tek** yükleme her kayıt için ilişkileri ayrı ayrı okur; ilk derste kurulan
tembel yüklemenin doğrudan yazılmış hâlidir. **Toplu getirme** kimlikleri toplayıp tek
sorguda okur. **Birleştirme** işi veritabanına bırakır.

```js
// n-arti-1.mjs — uc yaklasim, ayni liste; sorgu ve satir sayilari olculur
import { sayanBaglanti } from "./sayan.mjs";
const db = sayanBaglanti("kutuphane.db");
const SINIR = Number(process.argv[2] ?? 100);

function tekTek() {
  db.sifirla();
  const t = performance.now();
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", SINIR);
  const liste = kayitlar.map((o) => ({
    id: o.odunc_id,
    baslik: db.tek("SELECT baslik FROM kitap WHERE kitap_id = ?", o.kitap_id).baslik,
    uye: db.tek("SELECT ad FROM uye WHERE uye_id = ?", o.uye_id).ad,
  }));
  return { ad: "tek tek", liste, ...db.sayac, sure: performance.now() - t };
}

function topluGetir() {
  db.sifirla();
  const t = performance.now();
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", SINIR);
  const kitapIds = [...new Set(kayitlar.map((o) => o.kitap_id))];
  const uyeIds = [...new Set(kayitlar.map((o) => o.uye_id))];
  const kitapYer = new Map(db.cok(
    `SELECT kitap_id, baslik FROM kitap WHERE kitap_id IN (${kitapIds.map(() => "?").join(",")})`,
    ...kitapIds).map((k) => [k.kitap_id, k.baslik]));
  const uyeYer = new Map(db.cok(
    `SELECT uye_id, ad FROM uye WHERE uye_id IN (${uyeIds.map(() => "?").join(",")})`,
    ...uyeIds).map((u) => [u.uye_id, u.ad]));
  const liste = kayitlar.map((o) => ({
    id: o.odunc_id, baslik: kitapYer.get(o.kitap_id), uye: uyeYer.get(o.uye_id),
  }));
  return { ad: "toplu getirme", liste, ...db.sayac, sure: performance.now() - t };
}

function birlestirme() {
  db.sifirla();
  const t = performance.now();
  const liste = db.cok(
    `SELECT o.odunc_id AS id, k.baslik, u.ad AS uye
     FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
     WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id LIMIT ?`, SINIR);
  return { ad: "birlestirme", liste, ...db.sayac, sure: performance.now() - t };
}

const sonuclar = [tekTek(), topluGetir(), birlestirme()];
const ilk = JSON.stringify(sonuclar[0].liste);
for (const s of sonuclar) {
  console.log(`${s.ad.padEnd(14)} sorgu=${String(s.sorgu).padStart(4)}  ` +
    `donen_satir=${String(s.satir).padStart(4)}  sure=${s.sure.toFixed(1).padStart(6)} ms  ` +
    `sonuc_ayni=${JSON.stringify(s.liste) === ilk}`);
}
```

```sh
node n-arti-1.mjs 100
```

```
tek tek        sorgu= 201  donen_satir= 300  sure=   1.2 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 165  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 100  sure=   0.1 ms  sonuc_ayni=true
```

Üç yol da aynı listeyi üretti; son sütun bunu doğruluyor. Sorgu sayıları 201, 3 ve 1.

İlk sayının yapısı adı veriyor: bir sorgu listeyi getirir, sonra her kayıt için ek sorgular
çalışır. Bu örnekte kayıt başına iki ilişki okunduğu için sayı $1 + 2N$ oluyor. Genel
biçimiyle **N+1 sorgu sorunu** budur: liste için bir, her öğe için bir sorgu.

## Sayının Büyümesi

Sorunun ciddiyeti tek bir ölçümde görünmez; kayıt sayısı arttıkça görünür.

```sh
for n in 10 50 100 250 500; do echo "--- kayit sayisi $n ---"; node n-arti-1.mjs $n; done
```

```
--- kayit sayisi 10 ---
tek tek        sorgu=  21  donen_satir=  30  sure=   0.3 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir=  30  sure=   0.1 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir=  10  sure=   0.0 ms  sonuc_ayni=true
--- kayit sayisi 50 ---
tek tek        sorgu= 101  donen_satir= 150  sure=   0.8 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 115  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir=  50  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 100 ---
tek tek        sorgu= 201  donen_satir= 300  sure=   1.2 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 165  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 100  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 250 ---
tek tek        sorgu= 501  donen_satir= 750  sure=   2.6 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 315  sure=   0.2 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 250  sure=   0.1 ms  sonuc_ayni=true
--- kayit sayisi 500 ---
tek tek        sorgu=1001  donen_satir=1500  sure=   5.1 ms  sonuc_ayni=true
toplu getirme  sorgu=   3  donen_satir= 565  sure=   0.4 ms  sonuc_ayni=true
birlestirme    sorgu=   1  donen_satir= 500  sure=   0.2 ms  sonuc_ayni=true
```

Süreler makineye bağlıdır; sorgu sayıları değildir. Tek tek yüklemede sayı kayıt sayısıyla
doğrusal artıyor. Toplu getirmede üçte, birleştirmede birde sabit kalıyor. **Sabitlik
aranan özelliktir**; listedeki kayıt sayısı iki katına çıktığında sorgu sayısının
değişmemesi gerekir.

Dönen satır sayıları ikinci bir ayrımı gösteriyor. Toplu getirme beş yüz kayıt için 565
satır döndürdü, birleştirme 500. İlkinde ödünç kayıtları bir kez, kitaplar ve üyeler
tekilleştirilmiş hâlde okundu; ikincisinde her satır kendi kitap ve üye sütunlarını
taşıdığı için aynı kitabın başlığı defalarca aktarıldı. Bu fark, bir sonraki dersin
konusudur.

## Yerel Ölçümün Gizlediği

Yukarıdaki sürelerde tek tek yükleme beş yüz kayıt için beş milisaniye aldı; kabul
edilebilir görünüyor. Bu, ölçümün yerel bir dosya üzerinde yapılmasından kaynaklanıyor.

Bağlantı havuzu dersinde konu edilen gidiş-dönüş burada belirleyici olur. Veritabanı ayrı
bir sunucudaysa her sorgu bir ağ turu ister. Tur başına yalnız yarım milisaniye varsayalım:
1001 sorgu 500 milisaniyeden çok, tek sorgu yarım milisaniye eder. Bu bir ölçüm değil,
ölçülen sorgu sayısıyla yapılan bir hesaptır; hesabın söylediği, sorgu sayısının yerel
ortamda gizlenip gerçek ortamda ortaya çıkan bir maliyet olduğudur.

Bu yüzden başarım ölçütü süre değil **sorgu sayısı** olarak konur. Süre ortamdan ortama
değişir; sorgu sayısı kodun bir özelliğidir.

## Toplu Getirme mi, Birleştirme mi

İki çözüm arasındaki seçim veri şekline bağlıdır.

Birleştirme tek sorgudur ve en az satırı döndürür; ilişkiler bire bir ya da bire az sayıda
olduğunda doğru seçimdir. Bire çok ilişkilerde ise sonuç kümesi çarpar: her ödünç kaydı için
üç etiket varsa satır sayısı üçe katlanır ve ödünç sütunları her satırda tekrarlanır.

Toplu getirme sabit sayıda ek sorgu ekler, ama çarpma üretmez. Her bağıntı bir kez okunur ve
birleştirme uygulamada yapılır. Çok sayıda bire çok ilişki varsa bu yol daha az veri
aktarır.

Toplu getirmenin bir uygulama ayrıntısı vardır: kimlik listesi sorgu metnine soru işareti
olarak girer. Liste büyüdükçe metin büyür ve motorların bağlı değişken sayısı sınırına
yaklaşılır. Bu yüzden kimlikler parçalara bölünerek sorulur; parça büyüklüğü sabit tutulduğu
sürece sorgu sayısı kayıt sayısıyla değil, sabit bir bölme oranıyla artar.

## Bütçeyi Sınamaya Çevirmek

Sorgu sayısı ölçülebiliyorsa sınanabilir. Aşağıdaki sınama, listenin belirli bir bütçenin
üstüne çıkmasını hata sayar.

```js
// butce.test.mjs — sorgu sayisi butcesi bir sinama olarak yazilir
import { test } from "node:test";
import assert from "node:assert/strict";
import { sayanBaglanti } from "./sayan.mjs";

const BUTCE = 5;

function acikOduncListesi(db, sinir) {
  const kayitlar = db.cok(
    "SELECT odunc_id, kitap_id, uye_id FROM odunc WHERE iade_tarihi IS NULL ORDER BY odunc_id LIMIT ?", sinir);
  const kitapIds = [...new Set(kayitlar.map((o) => o.kitap_id))];
  const uyeIds = [...new Set(kayitlar.map((o) => o.uye_id))];
  const kitapYer = new Map(db.cok(
    `SELECT kitap_id, baslik FROM kitap WHERE kitap_id IN (${kitapIds.map(() => "?").join(",")})`,
    ...kitapIds).map((k) => [k.kitap_id, k.baslik]));
  const uyeYer = new Map(db.cok(
    `SELECT uye_id, ad FROM uye WHERE uye_id IN (${uyeIds.map(() => "?").join(",")})`,
    ...uyeIds).map((u) => [u.uye_id, u.ad]));
  return kayitlar.map((o) => ({ id: o.odunc_id, baslik: kitapYer.get(o.kitap_id), uye: uyeYer.get(o.uye_id) }));
}

for (const sinir of [10, 100, 500]) {
  test(`acik odunc listesi ${sinir} kayit icin butceyi asmiyor`, () => {
    const db = sayanBaglanti("kutuphane.db");
    const liste = acikOduncListesi(db, sinir);
    assert.equal(liste.length, sinir);
    assert.ok(db.sayac.sorgu <= BUTCE, `sorgu sayisi ${db.sayac.sorgu} > ${BUTCE}`);
  });
}
```

```sh
node --test butce.test.mjs
```

```
✔ acik odunc listesi 10 kayit icin butceyi asmiyor (0.722292ms)
✔ acik odunc listesi 100 kayit icin butceyi asmiyor (0.57875ms)
✔ acik odunc listesi 500 kayit icin butceyi asmiyor (0.475583ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 36.655375
```

Sınamanın değeri, üç farklı kayıt sayısında aynı bütçeyi istemesidir. Biri tembel yüklemeye
dönen bir değişiklik yaparsa, beş yüz kayıtlı sınama bütçeyi aşar ve hata sorgu sayısıyla
birlikte bildirilir. Bu, N+1 sorununun kod incelemesinde gözden kaçmasını engelleyen tek
otomatik yoldur.

## Özet

- Sorgu sayısı, sarmalayıcıyla sayıldığında görünür oldu: aynı liste için tek tek yükleme
  201, toplu getirme 3, birleştirme 1 sorgu çalıştırdı.
- Tek tek yüklemede sorgu sayısı kayıt sayısıyla doğrusal artıyor; beş yüz kayıtta 1001'e
  çıktı. Öteki iki yolda sabit kaldı.
- Yerel ölçümde fark milisaniyelerle kalır; ağ üzerinden her sorgu bir gidiş-dönüş
  istediğinden aynı fark saniyelere çıkar. Bu yüzden ölçüt süre değil sorgu sayısıdır.
- Birleştirme en az satırı döndürür ama bire çok ilişkilerde sonucu çarpar; toplu getirme
  çarpma üretmez, karşılığında sabit sayıda ek sorgu ister.
- Sorgu bütçesi bir sınamaya çevrildiğinde, tembel yüklemeye dönüş otomatik olarak yakalanır.

## Sonraki Adım

Bu derste bir sayı düştü, başka bir sayı gözden kaçtı. Toplu getirme beş yüz kayıt için 565
satır, birleştirme 500 satır döndürdü; ama satır sayısı aktarılan verinin ölçüsü değil.
Sorgular yalnız gereken sütunları istemişti. Uygulamalarda çoğu sorgu bütün sütunları ister,
çoğu liste gereğinden çok satır getirir ve fark bayt cinsinden ölçülene kadar görünmez.
Sonraki ders aynı listeyi farklı sütun kümeleriyle çekerek aktarılan veriyi bayt olarak
ölçer ve alan ile satır düzeyinde sınırlamanın karşılığını gösterir.
