İçeriğe geç
Educora
Üniversite30 dk20 / 22

Sorgu optimizasyonu ve indeksler

B-ağacı indeksinin nasıl çalıştığını, sorgu planının nasıl okunduğunu, indeksi kullanabilen (sargable) koşulların nasıl yazıldığını, bileşik ve kapsayan indeksleri ve indeksin ne zaman zarar verdiğini öğren.

Kendini test et
Bu derste öğreneceklerin
  • B-ağacı yüksekliğini ve seçiciliği hesaplayarak bir indeksin yararını tahmin etmek
  • EXPLAIN QUERY PLAN ve EXPLAIN ANALYZE çıktısını okumak, sargable olmayan koşulları yeniden yazmak
  • Bileşik ve kapsayan indeksler tasarlamak ve indeks oluşturmamak gereken durumları tanımak

On milyon siparişlik bir tabloda tek bir müşterinin siparişlerini bulan sorgu 8 saniye sürüyor; doğru indeksten sonra aynı sorgu milisaniyeler içinde çalışıyor. SQL bildirimsel bir dildir: ne istediğimizi yazarız, nasıl bulunacağını ise VTYS'nin iyileştiricisi (optimizer) seçer. Birkaç olası planı karşılaştırır ve tablo istatistiklerine dayanarak en ucuzunu alır. Bu derste iyileştiricinin nasıl “düşündüğünü” öğreneceğiz: indeksin iç yapısı, planın okunması ve onun işini kolaylaştıran sorgular. İyi haber: bunun için özel araçlar gerekmez; her şey EXPLAIN komutu ve birkaç basit kuralla başlar.

B-ağacı indeksinin içi

Çoğu VTYS'de varsayılan indeks bir B-ağacıdır (tam söylemek gerekirse B+ ağacı): anahtarlar disk sayfalarında sıralı biçimde tutulur, her iç sayfada yüzlerce anahtar ve alt düzeye işaretçiler bulunur, yapraklar ise satırlara başvurur ve birbirine zincirlenir. Ağaç her zaman dengelidir: kökten herhangi bir yaprağa giden yol aynı uzunluktadır. Bu yüzden bir arama yalnızca birkaç sayfa okumasıyla biter; yaprak zinciri de aralık sorgularını (BETWEEN, >, ORDER BY) ucuzlatır.

h = ⌈ log N ÷ log f ⌉
burada:
  • hağacın yüksekliği — bir anahtarı bulmak için okunan sayfa sayısı
  • Nindeksteki anahtar (satır) sayısı
  • fdallanma katsayısı — bir sayfaya sığan anahtar sayısı (genellikle yüzlerce)

Yükseklik satır sayısıyla logaritmik olarak artar: tablo 500 kat büyüdüğünde ağaca yalnızca bir düzey eklenir.

Örnek 1: indeks mi, tam tarama mı?

orders tablosunda N = 10⁸ satır var, bir sayfaya 100 satır sığıyor, indeksin dallanma katsayısı f = 500. Bir müşterinin 12 siparişini bulmak için indeksli ve indekssiz kaç sayfa okunur?

Çözümü göster
1) Tam tarama: tablonun tamamı okunur — 10⁸ ÷ 100 = 10⁶ sayfa.
2) Ağaç yüksekliği: log 10⁸ ÷ log 500 = 8 ÷ 2,70 ≈ 2,96; yukarı yuvarlarsak h = 3 sayfa.
3) En kötü durumda bulunan 12 satır 12 farklı sayfadadır: toplam ≈ 3 + 12 = 15 sayfa.
4) Fark: 10⁶ ÷ 15 ≈ 67.000 kat daha az okuma. Mantık denetimi: satırların %30'u gerekseydi dağınık sayfaları tek tek okumak sıralı taramadan pahalıya gelirdi; o zaman iyileştirici tam taramayı seçer.
s = n ÷ N
burada:
  • skoşulun seçiciliği (0 ile 1 arasında)
  • nkoşula uyan satır sayısı
  • Ntablodaki tüm satırlar

Küçük s (az satır döndüren bir koşul) indeks için iyidir; s büyükse tam tarama daha ucuz olabilir.

Sargable koşullar

Sargable (search argument able), indeksle aramaya uygun bir koşul demektir: sütun, karşılaştırmanın bir tarafında “çıplak” durur. Sütuna bir fonksiyon ya da aritmetik uygulandığında (strftime('%m', order_date) = '03', price * 1.18 > 100) indeksteki sıralı değerler işe yaramaz ve VTYS her satırı denetlemek zorunda kalır. Aynı koşulu bir aralık olarak yeniden yazmak yeterlidir.

SQL
CREATE INDEX idx_orders_date ON orders (order_date);

-- not sargable: a function on the column
EXPLAIN QUERY PLAN
SELECT id FROM orders
WHERE strftime('%m', order_date) = '03';
▸ Beklenen çıktı
id | parent | notused | detail
2 | 0 | 214 | SCAN orders USING COVERING INDEX idx_orders_date
SCAN — indeksin her girdisi denetlenir; indeks yalnızca tablodan küçük olduğu için seçilmiştir.
SQL
CREATE INDEX idx_orders_date ON orders (order_date);

-- sargable: a range on the bare column
EXPLAIN QUERY PLAN
SELECT id FROM orders
WHERE order_date >= '2025-03-01' AND order_date < '2025-04-01';
▸ Beklenen çıktı
id | parent | notused | detail
2 | 0 | 154 | SEARCH orders USING COVERING INDEX idx_orders_date (order_date>? AND order_date<?)
SEARCH — ağaçta aralığın başına inilir ve yalnızca gereken yapraklar okunur. SQLite her indekste id değerini saklar; bu yüzden indeks “kapsayıcıdır”. İlk üç sayı sürüme bağlıdır; önemli olan detail sütunudur.

Bileşik ve kapsayan indeksler

Bileşik bir indeks birkaç sütuna göre sıralanır: (customer_id, order_date) önce müşteriye, aynı müşteri içinde ise tarihe göre. Bir telefon rehberi gibi çalışır: soyadla ya da soyad ve adla arama yapılabilir, ama yalnızca adla yapılamaz; buna sol önek kuralı denir. İndeks bir sorgunun gerektirdiği tüm sütunları içeriyorsa VTYS tabloya hiç başvurmaz; böyle bir indekse kapsayan (covering) indeks denir. Ayrıca indeksin sırası ORDER BY ile örtüştüğünde ayrı bir sıralama adımı da ortadan kalkar.

Örnek 2: yavaş bir sorguyu düzeltmek

Müşteri hesabı sayfasından bir sorgu: SELECT id, order_date FROM orders WHERE customer_id = 1 AND strftime('%Y', order_date) = '2025' ORDER BY order_date. Planında SCAN orders ve USE TEMP B-TREE FOR ORDER BY var. Nasıl hızlandırılır?

Çözümü göster
1) Tarih koşulunu sargable yap: order_date >= '2025-01-01' AND order_date < '2026-01-01'.
2) Eşitlik sütunu önce, aralık ve sıralama sütunu sonra gelecek şekilde bir bileşik indeks oluştur: (customer_id, order_date).
3) İndeks customer_id değerine göre tam, order_date değerine göre aralıkla arar ve satırları zaten tarih sırasıyla verir; geçici sıralama ortadan kalkar.
4) id de indekste olduğu için tabloya erişim gerekmez: plan SEARCH ... USING COVERING INDEX gösterir.
SQL
-- before: function on the column, no suitable index
EXPLAIN QUERY PLAN
SELECT id, order_date FROM orders
WHERE customer_id = 1 AND strftime('%Y', order_date) = '2025'
ORDER BY order_date;
▸ Beklenen çıktı
id | parent | notused | detail
3 | 0 | 216 | SCAN orders
15 | 0 | 0 | USE TEMP B-TREE FOR ORDER BY
SQL
-- after: sargable range + composite index
CREATE INDEX idx_orders_cust_date ON orders (customer_id, order_date);

EXPLAIN QUERY PLAN
SELECT id, order_date FROM orders
WHERE customer_id = 1
  AND order_date >= '2025-01-01' AND order_date < '2026-01-01'
ORDER BY order_date;
▸ Beklenen çıktı
id | parent | notused | detail
3 | 0 | 46 | SEARCH orders USING COVERING INDEX idx_orders_cust_date (customer_id=? AND order_date>? AND order_date<?)

PostgreSQL'de EXPLAIN planı ve tahmini maliyeti gösterir, EXPLAIN ANALYZE ise sorguyu gerçekten çalıştırıp gerçek süreleri gösterir. Aşağıda 1.000.000 satırlık bir tabloda indeksten önce ve sonra alınan tipik bir sonuç var (sayılar donanıma bağlıdır). cost=başlangıç..toplam keyfî birimlerdedir, rows ise iyileştiricinin tahminidir; tahmin gerçek rows değerinden çok farklıysa istatistikler eskimiştir ve ANALYZE gerekir.

SQL
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;

CREATE INDEX idx_orders_customer ON orders (customer_id);

EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;
Beklenen çıktı
Seq Scan on orders  (cost=0.00..20834.00 rows=10 width=24) (actual time=0.018..61.922 rows=12 loops=1)
  Filter: (customer_id = 42)
  Rows Removed by Filter: 999988
Planning Time: 0.081 ms
Execution Time: 61.950 ms

Index Scan using idx_orders_customer on orders  (cost=0.42..44.58 rows=10 width=24) (actual time=0.024..0.039 rows=12 loops=1)
  Index Cond: (customer_id = 42)
Planning Time: 0.210 ms
Execution Time: 0.061 ms
PostgreSQL (çalıştırılamaz, örnek çıktı). Rows Removed by Filter — boşuna okunan satırlar.

İndeks ne zaman gereksizdir

  • Küçük tablolar — birkaç sayfalık bir tabloyu taramak, ağaçta inmekten daha ucuzdur.
  • Düşük seçicilik — is_active gibi iki değerli bir sütunda koşul satırların yarısını döndürür.
  • Yoğun yazılan tablolar — her indeks her INSERT, UPDATE ve DELETE işleminde güncellenir; günlük ve sensör tablolarında bu pahalıdır.
  • Yinelenen indeksler — (customer_id, order_date) varsa ayrı bir (customer_id) indeksi gereksizdir; sol önek onun yerini tutar.
Alıştırma

order_date üzerinde bir indeks oluştur ve 2025'in ilk çeyreğindeki (ocak–mart) siparişleri sargable bir aralık koşuluyla seç: id ve order_date, tarihe ve id değerine göre sıralı. strftime kullanma.

Alıştırma · SQL
CREATE INDEX idx_orders_date ON orders (order_date);

-- slow version: WHERE strftime('%m', order_date) IN ('01', '02', '03')
SELECT id, order_date
FROM orders
-- WHERE: a range on the bare column
ORDER BY order_date, id;
▸ Beklenen çıktı
id | order_date
1 | 2025-01-15
2 | 2025-01-15
3 | 2025-02-02
4 | 2025-02-10
5 | 2025-03-05
6 | 2025-03-18
Alıştırma

products.category üzerindeki bir indeksin yararını değerlendir: her kategori için satır sayısını (rows_count) ve yüzde olarak seçiciliği (selectivity_pct, 1 ondalık basamak) göster. Satır sayısına göre azalan, sonra kategoriye göre sırala.

Alıştırma · SQL
SELECT category,
       COUNT(*) AS rows_count
       -- selectivity_pct = 100 * n / N
FROM products
GROUP BY category
ORDER BY rows_count DESC, category;
▸ Beklenen çıktı
category | rows_count | selectivity_pct
Electronics | 4 | 40
Accessories | 2 | 20
Stationery | 2 | 20
Games | 1 | 10
Home | 1 | 10

Önemli noktalar

  • B-ağacı dengelidir: yüksekliği h = ⌈log N ÷ log f⌉ olduğu için bir arama yalnızca birkaç sayfa okuması gerektirir.
  • Seçicilik s = n ÷ N küçükken indeks yararlıdır; büyükken iyileştirici tam taramayı seçebilir.
  • Sargable bir koşulda sütun çıplak kalır: fonksiyonları ve aritmetiği sabit tarafa taşı, tarihler için aralık yaz.
  • Bileşik indeks sol önek kuralına uyar: önce eşitlik, sonra aralık/sıralama sütunu; kapsayan indeks tablo erişimini ortadan kaldırır.
  • Planları EXPLAIN ile denetle; küçük, seçiciliği düşük ve yoğun yazılan tablolarda indeksten kaçın ve her değişikliğin etkisini ölç.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Bir tabloda 250.000 satır var ve dallanma katsayısı f = 500. B-ağacının yüksekliği kaçtır?