İçeriğe geç
academia.sh

Ders 30 / 34

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.

İçindekiler

Çö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.

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

// 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

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

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

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat