---
title: 'Sorgu Arayüzü ve N+1 Sorunu'
source: 'https://academia.sh/tr/kurslar/mvc-cercevesi/sorgu-arayuzu-ve-n-arti-bir'
course: 'Model–Görünüm–Denetleyici Çerçevesi'
language: tr
updated: '2026-10-21T05:03:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Sorgu önbelleğinin kalışının bu istek olduğunu dört istekte ölçer'
    - 'Aynı kimliğin ikinci kez sorgulanmadığını sorgu sayacıyla gösterir'
    - 'N+1 sorununu tembel ve istekli yüklemenin sorgu sayılarıyla karşılaştırır; olmayan kimliğin listede nil bıraktığını gösterir'
    - 'Sorgu önbelleğinin boş sonucu da sakladığını ve bunun sonucunu ölçer'
---

# Sorgu Arayüzü ve N+1 Sorunu

Veritabanına giden sorguların sayısını ölçer: sorgu önbelleğinin bu istekte kaldığını, N+1 sorununu ve istekli yüklemeyle çözümünü, önbelleğin boş sonucu da sakladığını gösterir.

Önceki ders, doğrulama hatasının yalnız yazıldığı istekte okunduğunu ve ilişkinin sipariş kaydındaki bir kimlik alanından ibaret olduğunu gösterdi. Bir ürünün siparişlerini okumak için sipariş tablosu süzülüyordu. Peki bir istek, bir sayfayı doldurmak için veritabanına kaç kez gidiyor? Tek bir ürünü okumak bir sorgudur; yüz siparişin ürününü ayrı ayrı okumak yüz sorgudur. Sorgu sayısı, bir sayfanın ne kadar sürede çizileceğini belirleyen sayılardan biridir.

**Sorgu arayüzü** (query interface), kayıtları koşulla, kimlikle ya da ilişkiyle okuyan yöntemlerin toplamıdır. Bu ders arayüzün kendisini değil, arayüzün arkasında kaç sorgunun koştuğunu ölçer. Ölçü kursun hep aynı sorusudur: **bu değer hangi isteğe kadar kalıyor?** Burada ölçülen değer, aynı sorgunun ikinci kez sorulmasını önleyen **sorgu önbelleği**dir (query cache). Adı önbellek olduğu için kalıcı sanılabilir. Ders bunu ölçer.

## Ölçüm Çekirdeği

Birinci dosya, kursun her dersinde aynı olan istek döngüsü ve kalış sınıflandırıcıdır. Bu blok kaynaktır; dosya diske yazılır ve `require_relative` ile çekilir.

```ruby
# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci

# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
  def initialize(gelen)
    @gelen = gelen
    @yeni = {}
  end

  def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }

  def []=(anahtar, deger)
    @yeni[anahtar] = deger
  end

  def birakilan = @yeni
end

# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
  attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu

  def initialize(uygulama, oturum)
    @uygulama = uygulama
    @params = {}
    @oturum = oturum
    @flash = Flash.new(oturum.delete(:flash) || {})
    @istek_deposu = {}
  end
end

# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
  attr_reader :depo

  def initialize
    @oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
    @depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
              kuyruk: [], onbellek: {}, sayac: 0 }
  end

  def istek(istemci)
    baglam = Baglam.new(self, @oturumlar[istemci])
    sonuc = yield baglam
    baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
    sonuc
  end
end

# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
  uygulama = Uygulama.new
  ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
  raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?

  gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
    uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
  end
  tur = if gorulen[2] != "--" then "kalici"
        elsif gorulen[1] != "--" then "oturum"
        elsif gorulen[0] != "--" then "sonraki"
        else "bu_istek"
        end
  [tur, ["A1", *gorulen].join(" "), ilk]
end
```

Çekirdek bir çerçeve değil, bir çerçevenin modelidir. İkinci dosya, ürün modelini ve kimlikle okuyan `bul` yöntemini taşır. `bul`, önce istek deposundaki sorgu önbelleğine bakar; kimlik orada yoksa sayacı bir artırır, tabloyu okur ve sonucu önbelleğe yazar.

```ruby
# sorgu.rb — urun modeli: bul ayni istekte ayni sorguyu bir kez calistirir
require_relative "kalis"

module Urun
  def self.olustur(b, nitelikler)
    hatalar = []
    hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
    hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
    return { hatalar: } unless hatalar.empty?

    tablo = b.uygulama.depo[:urunler]
    id = tablo.size + 1
    tablo[id] = nitelikler.merge(id:)
    { id:, hatalar: }
  end

  def self.bul(b, id)
    sorgular = (b.istek_deposu[:sorgular] ||= {})
    # Ayni istekte ayni sorgu ikinci kez veritabanina gitmez.
    sorgular.fetch([:urunler, id]) do
      b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
      sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
    end
  end
end
```

Bu blok da kaynaktır. Önbellek anahtarı `[:urunler, id]` çiftidir: hangi tablo ve hangi kimlik. Önbellek ve sayaç `istek_deposu`nda durur; yani her istek için yeniden başlar.

## Önbelleğin Kalışı

Dersin gözlemi sorgu önbelleğidir. Yazan lambda bir ürün oluşturup kimliğiyle okur; okuyan lambda önbellekteki kaydın adına bakar.

```ruby
# olcum.rb — sorgu onbellegi gozleminin kalisi
require_relative "sorgu"

kalem = ->(b) { Urun.olustur(b, ad: "kalem", fiyat: 12) }
yaz = ->(b) { kalem.call(b); Urun.bul(b, 1) }
oku = ->(b) { b.istek_deposu.dig(:sorgular, [:urunler, 1], :ad) }
p kalis(yaz, oku)
```
```
["bu_istek", "A1 -- -- --", "kalem"]
```

Kalış **bu istek**. Kanıt satırı `A1 -- -- --`: önbellekteki kayıt yalnız yazıldığı istekte okundu; A'nın sonraki isteklerinde ve B'nin isteğinde yok. Önbelleğin adı değerin uzun yaşadığı izlenimini verir, ama ölçülen değer istek deposundadır ve istek bitince gider. Kalıcı olan, ürünün kendisidir; önbellekteki kopya değil. **VK13.** Bu ölçümde önbellek `istek_deposu`ndadır. Önbelleği uygulama deposunda tutan bir düzenek başka bir kalış verirdi; bu dersin iddiası yalnız istek deposundaki önbelleğin kalışıyla ilgilidir.

## Sorgu Sayısı

Önbelleğin işe yaradığını görmek için sayaca bakılır. Üç ürün bir istekte oluşturulur; sonraki istekte üç farklı kimlikle `bul` çağrılır, sonra aynı üç kimlikle yeniden.

```ruby
# sayac.rb — ayni kimlik ikinci kez sorgulanmaz
require_relative "sorgu"

uygulama = Uygulama.new
uygulama.istek("A") do |b|
  %w[kalem defter silgi].each_with_index { |ad, i| Urun.olustur(b, ad:, fiyat: 10 + i) }
end

sayilar = uygulama.istek("A") do |b|
  [1, 2, 3].each { |id| Urun.bul(b, id) }
  ilk = b.istek_deposu[:sorgu_sayisi]
  [1, 2, 3].each { |id| Urun.bul(b, id) }
  [ilk, b.istek_deposu[:sorgu_sayisi]]
end
p sayilar
p uygulama.istek("A") { |b| b.istek_deposu[:sorgu_sayisi] }
```
```
[3, 3]
nil
```

Üç farklı kimlik üç sorgu saydı. Aynı kimliklerle ikinci tur sayacı oynatmadı: `3` kaldı. Sonraki istekte sayaç `nil`: istek deposu yeni istekle birlikte boş başladığı için sayaç da, önbellek de sıfırdan başlar. Aynı sorgu iki ayrı istekte iki kez çalışır.

Kursun önceki bir dersinde "kaç kez koştu" sorusu ölçü olmuştu; bu derste de bir sayı basılıyor. Fark şudur: burada sayı kendi başına ölçülen şey değil, önbelleğin işe yaradığının kanıtıdır. Dersin sınıflandırdığı değer yine kalıştır: sorgu önbelleği bu istekte kalır.

Önbelleğin aynı sorguyu tanıması anahtara bağlıdır. Anahtar `[:urunler, id]` çiftidir ve çift, kimliğin türüyle birlikte karşılaştırılır. Formdan ya da adres satırından gelen bir kimlik çoğu zaman metin olarak gelir; aşağıdaki ölçüm aynı kimliği tam sayı ve metin olarak sorar.

```ruby
# anahtar.rb — ayni kimlik, iki tur
require_relative "sorgu"

uygulama = Uygulama.new
uygulama.istek("A") { |b| Urun.olustur(b, ad: "kalem", fiyat: 12) }

uygulama.istek("A") do |b|
  p Urun.bul(b, 1)[:ad]
  p Urun.bul(b, "1")
  p Urun.bul(b, "1".to_i)[:ad]
  p b.istek_deposu[:sorgu_sayisi]
  p b.istek_deposu[:sorgular].keys
end
```
```
"kalem"
nil
"kalem"
2
[[:urunler, 1], [:urunler, "1"]]
```

`1` ve `"1"` iki ayrı anahtar oldu: sayaç `2` gösterdi, yani önbellek ikinci çağrıyı aynı sorgu saymadı. Metin kimlikle yapılan sorgu ayrıca `nil` döndürdü, çünkü tablonun anahtarları tam sayıdır. `"1".to_i` ise ilk anahtarla örtüştü: üçüncü çağrı önbellekten döndü ve sayaç artmadı. Bu çekirdek modelin bir özelliğidir: kimliği tam sayıya çevirmek çağıranın işidir. Gerçek eşleyicilerin metin kimliği kendiliğinden çevirip çevirmediği burada ölçülmemiştir; ölçülen, önbelleğin anahtar eşitliğine bağlı olduğudur ve eşitlik türü de kapsar. Sorgu sayısını düşük tutmak isteyen kod, kimliği önbelleğe girmeden önce tek bir türe çevirmelidir.

## N+1 Sorunu

Bir sayfa, siparişleri ve her siparişin ürününü göstersin. Önce siparişler okunur: bir sorgu. Sonra her sipariş için ürünü ayrı bir sorguyla okumak, sipariş sayısı kadar sorgu daha demektir. Sipariş sayısı N ise toplam N+1 sorgudur. Buna **N+1 sorunu** (N+1 problem), ilişkili kaydı ancak erişildiğinde çeken yaklaşıma da **tembel yükleme** (lazy loading) denir; kayıt başına bir kez erişildiğinde bu, her kayıt için ayrı bir sorgu demektir. Çözümü, ilişkili kayıtların tek sorguyla topluca çekildiği **istekli yükleme**dir (eager loading).

Bu derste önbellekten bağımsız, yalnız sayaçla ölçülen küçük bir sorgu yardımcısı kurulur. Her yöntem, bir sorgu çalıştırdığında sayacı artırır.

```ruby
# siparis_sorgu.rb — sorgu sayan yardimcilar
require_relative "sorgu"

module SiparisSorgu
  def self.say(b)
    b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
  end

  def self.siparisler(b)
    say(b)
    b.uygulama.depo[:siparisler].values
  end

  def self.urun(b, id)
    say(b)
    b.uygulama.depo[:urunler][id]
  end

  def self.urunler(b, idler)
    say(b)
    b.uygulama.depo[:urunler].values_at(*idler)
  end
end
```

Bu blok da kaynaktır. `siparisler` tüm siparişleri tek sorguyla, `urun` bir ürünü tek sorguyla, `urunler` birden çok ürünü yine tek sorguyla okur. Aşağıdaki ölçüm beş siparişi üç yolla okur: tembel, istekli ve `Urun.bul` önbelleğiyle.

```ruby
# n_arti_bir.rb — ayni sayfa, uc yol, uc sorgu sayisi
require_relative "siparis_sorgu"

uygulama = Uygulama.new
uygulama.istek("A") do |b|
  %w[kalem defter silgi].each_with_index { |ad, i| Urun.olustur(b, ad:, fiyat: 10 + i) }
  kayitlar = [1, 1, 2, 3, 2].each_with_index.to_h do |urun_id, i|
    [i + 1, { id: i + 1, urun_id:, adet: i + 1 }]
  end
  b.uygulama.depo[:siparisler] = kayitlar
end

tembel = uygulama.istek("A") do |b|
  adlar = SiparisSorgu.siparisler(b).map { |s| SiparisSorgu.urun(b, s[:urun_id])[:ad] }
  [adlar, b.istek_deposu[:sorgu_sayisi]]
end

istekli = uygulama.istek("A") do |b|
  kayitlar = SiparisSorgu.siparisler(b)
  urunler = SiparisSorgu.urunler(b, kayitlar.map { |s| s[:urun_id] }.uniq).to_h { |u| [u[:id], u] }
  adlar = kayitlar.map { |s| urunler[s[:urun_id]][:ad] }
  [adlar, b.istek_deposu[:sorgu_sayisi]]
end

onbellekli = uygulama.istek("A") do |b|
  adlar = SiparisSorgu.siparisler(b).map { |s| Urun.bul(b, s[:urun_id])[:ad] }
  [adlar, b.istek_deposu[:sorgu_sayisi]]
end

p tembel
p istekli
p onbellekli
p tembel[0] == istekli[0] && istekli[0] == onbellekli[0]
```
```
[["kalem", "kalem", "defter", "silgi", "defter"], 6]
[["kalem", "kalem", "defter", "silgi", "defter"], 2]
[["kalem", "kalem", "defter", "silgi", "defter"], 4]
true
```

Üç yol aynı beş adı üretti, ama sorgu sayıları farklı. Tembel yol `6` sorgu çalıştırdı: siparişler için bir, her sipariş için bir daha, yani 1+5. İstekli yol `2` sorgu çalıştırdı: siparişler için bir, tekil ürünler için bir. Sipariş sayısı büyüse de istekli yolun sayısı iki kalır; tembel yolun sayısı siparişle birlikte büyür. Üçüncü yol ikisinin arasındadır: önbellek tekrar eden kimlikleri bir kez sorgular, bu yüzden `4` sorgu çalıştı, yani 1 artı üç farklı ürün. Önbellek N+1'i kaldırmaz, farklı kimlik sayısına indirir; farklı kimlik de sipariş kadar çoksa sayı yine büyür.

**VK14.** Bu ölçüm sorgu sayısını sayar, süreyi ölçmez. Sorgu başına maliyet bu bellek içi modelde sıfıra yakındır; gerçek bir veritabanında her sorgunun bir gidiş geliş maliyeti olduğu bu derste ölçülmemiştir, dersin iddiası yalnız sayıların farkıdır.

## İstekli Yüklemede Olmayan Kimlik

İstekli yükleme ilişkili kayıtlar için sorguyu bire indirir, ama sonucun biçimini de değiştirir. Bu modelde topluca okunan kimliklerden biri tabloda yoksa sonuç listesinde o yer `nil` kalır; gerçek eşleyicilerin eksik kayıtla ne yaptığı burada ölçülmemiştir. Ölçüm, bir siparişin ürünü silinmiş olduğunda istekli yolun ne yaptığına bakar.

```ruby
# eksik_urun.rb — istekli yuklemede olmayan kimlik
require_relative "siparis_sorgu"

uygulama = Uygulama.new
uygulama.istek("A") do |b|
  Urun.olustur(b, ad: "kalem", fiyat: 12)
  b.uygulama.depo[:siparisler] = {
    1 => { id: 1, urun_id: 1, adet: 2 },
    2 => { id: 2, urun_id: 99, adet: 1 }
  }
end

uygulama.istek("A") do |b|
  kayitlar = SiparisSorgu.siparisler(b)
  urunler = SiparisSorgu.urunler(b, kayitlar.map { |s| s[:urun_id] }.uniq)
  p urunler.map { |u| u&.fetch(:ad) }
  p b.istek_deposu[:sorgu_sayisi]
  begin
    urunler.to_h { |u| [u[:id], u] }
  rescue => e
    p e.class
  end
end
```
```
["kalem", nil]
2
NoMethodError
```

Siparişler için bir, ürünler için bir sorgu çalıştı ve sayaç `2` gösterdi; ama sonuç listesinde olmayan kimliğin yeri `nil`. Bu `nil` üzerinde `u[:id]` çağırmak `NoMethodError` verdi. Topluca kurulan karma yapı, tek bir eksik kayıt yüzünden hiç kurulamadı. İstekli yükleme sorgu sayısını sabit tutar, ama eksik ilişkili kaydı işleme sorumluluğunu çağıran koda bırakır. Bir önceki dersteki yetim sipariş burada `NoMethodError` olarak görünür.

## Sınırlayıcı Ölçüm

Önbellek yalnız bulunan kayıtları mı saklar? Olmayan bir kimlikle ilk sorgu `nil` döner. Sorgu önbelleği bu `nil`i de saklıyorsa, olmayan kimliğin ikinci sorgusu veritabanına gitmez. Bunun bir yan etkisi de olur: aynı istekte kayıt sonradan eklenirse, önbellek hâlâ `nil` döndürür.

```ruby
# sinir.rb — onbellek nil'i de saklar
require_relative "sorgu"

uygulama = Uygulama.new
uygulama.istek("A") do |b|
  %w[kalem defter silgi].each_with_index { |ad, i| Urun.olustur(b, ad:, fiyat: 10 + i) }
end

sonuc = uygulama.istek("A") do |b|
  [1, 2, 3].each { |id| Urun.bul(b, id) }
  Urun.bul(b, 99)
  Urun.bul(b, 99)
  sayac = b.istek_deposu[:sorgu_sayisi]
  onceki = Urun.bul(b, 4)
  Urun.olustur(b, ad: "cetvel", fiyat: 8)
  sonraki = Urun.bul(b, 4)
  [sayac, onceki, sonraki, b.uygulama.depo[:urunler][4][:ad]]
end
p sonuc
p uygulama.istek("A") { |b| Urun.bul(b, 4)[:ad] }
p uygulama.istek("A") { |b| b.istek_deposu[:sorgu_sayisi] }
```
```
[4, nil, nil, "cetvel"]
"cetvel"
nil
```

Üç farklı kimlik ve olmayan `99` kimliği dört sorgu saydı; `99`un ikinci çağrısı sayacı oynatmadı. Önbellek `nil` sonucu da sakladığı için bunu boş bir kayıt olarak hatırladı. Dördüncü kimlikle aynı şey oldu: ürün henüz yokken `nil` önbelleğe girdi; ürün oluşturulduktan sonra aynı istekte `bul(4)` yine `nil` döndü, oysa tabloda ürün `"cetvel"` olarak vardı. Önbellek bu istekte bayat kaldı. Bir sonraki istek önbelleği sıfırdan başlattığı için aynı ürünü doğru okudu, ve sayaç yeni istekte yine `nil` oldu.

Bu, önbelleğin kalışının ters yüzüdür. Önbellek yalnız bu istekte durduğu için bayatlığı da yalnız bu istekle sınırlıdır; bir sonraki istek temiz başlar. Bir istek içinde okuyup sonra yazan kod, önbelleğin yazmadan önceki hâlini okuyabilir. **VK15.** Burada önbellek yazmada geçersiz kılınmıyor. Yazmada önbelleği boşaltan bir düzenek bu bayatlığı önlerdi; böyle bir düzenek bu derste kurulmadı ve ölçülmedi.

## Özet

- Sorgu arayüzü kayıtları kimlikle, koşulla ya da ilişkiyle okur; bir istekte kaç sorgunun koştuğu sayaçla ölçülür.
- Bu dersin gözlemi olan sorgu önbelleği, kalışı bu istek olan değerdir: önbellekteki kayıt yalnız yazıldığı istekte okundu.
- Aynı istekte aynı kimlik ikinci kez sorgulanmaz; yeni istekte sayaç ve önbellek sıfırdan başlar.
- N+1 sorunu, ilişkili kaydı her kayıt için ayrı sorguyla çekmektir; istekli yükleme sorguları ikiye indirir, önbellek yalnız tekrar eden kimlikleri eler.
- Önbellek anahtarı kimliğin türünü de içerir; kimliği türe çevirmeden gelen `"1"` ayrı sorgu sayılır. İstekli yüklemede tabloda olmayan kimlik, bu modelde listede `nil` bırakır ve sonucu kuran kod bunu ayrıca ele almalıdır.
- Önbellek `nil` sonucu da saklar; aynı istekte sonradan eklenen kayıt görünmez, ama bu bayatlık yalnız o istekle sınırlıdır.

## Sonraki Adım

Veri katmanı tamam: kayıt, şema, ilişki, doğrulama ve sorgu sayısı ölçüldü. Giriş yapan bir kullanıcı sonraki sayfada nasıl tanınıyor? Bir sonraki konu, kimlik doğrulamada bu sorunun cevabını ölçer.
