İçeriğe geç
academia.sh

Ders 14 / 20

Dizin Kavramı

Tam tablo taramasının maliyeti, dizinin bu maliyeti neye çevirdiği, aynı sorgunun dizinsiz ve dizinli ölçümü, dizinin yer ve yazma tarafındaki bedeli ve seçiciliğin kazancı belirlemesi.

İçindekiler

İşlemler konusu, veritabanının doğru cevabı vermesi için gereken düzeni tamamladı: kesinleştirme sınırları, yalıtım düzeyleri, kilitler ve tetikleyicilerin örtük yan etkileri. O konunun sonunda elde kalan şey, doğru sonucu veren bir sorgudur.

Bu ders aynı sorguyu ikinci kez sormaz. Sorduğu şey şudur: veritabanı o doğru sonucu verirken kaç satıra baktı? İki milyon satırlık bir tabloda tek bir üyenin kayıtlarını isteyen sorgu, doğru cevabı iki milyon satırı okuyarak da verebilir, altmış kadar adımda da verebilir. İkisi arasındaki fark, sorgunun yazımında değil, verinin nasıl düzenlendiğindedir.

Ölçülebilir Bir Veri Kümesi

Başarım iddiaları küçük tablolarda görünmez: yüz satırlık bir tabloda her yol hızlıdır. Ölçümün gürültünün üstüne çıkması için tablo yeterince büyük olmalıdır. Aşağıdaki betik, kütüphane ödünç kayıtlarından oluşan bir veri kümesi kurar. Satırlar özyinelemeli bir ortak tablo ifadesiyle üretilir; dışarıdan hiçbir dosya gerekmez.

-- kurulum.sql — kutuphane veri kumesi
CREATE TABLE sube (
  sube_id INTEGER PRIMARY KEY,
  ad      TEXT NOT NULL,
  sehir   TEXT NOT NULL
);
CREATE TABLE uye (
  uye_id       INTEGER PRIMARY KEY,
  ad           TEXT NOT NULL,
  sehir        TEXT NOT NULL,
  kayit_tarihi TEXT NOT NULL
);
CREATE TABLE kitap (
  kitap_id INTEGER PRIMARY KEY,
  baslik   TEXT NOT NULL,
  yazar    TEXT NOT NULL,
  yil      INTEGER NOT NULL,
  sube_id  INTEGER 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
);

INSERT INTO sube (sube_id, ad, sehir) VALUES
  (1,'Merkez','Ankara'),(2,'Bahcelievler','Ankara'),(3,'Kadikoy','Istanbul'),
  (4,'Beyoglu','Istanbul'),(5,'Konak','Izmir'),(6,'Nilufer','Bursa'),
  (7,'Selcuklu','Konya'),(8,'Cankaya','Ankara');

INSERT INTO uye (uye_id, ad, sehir, kayit_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 120000)
SELECT n, 'Uye ' || n,
       CASE n % 5 WHEN 0 THEN 'Ankara' WHEN 1 THEN 'Istanbul' WHEN 2 THEN 'Izmir'
                  WHEN 3 THEN 'Bursa' ELSE 'Konya' END,
       date('2015-01-01', '+' || (n % 3200) || ' days')
FROM sayac;

INSERT INTO kitap (kitap_id, baslik, yazar, yil, sube_id)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 200000)
SELECT n, 'Kitap ' || n, 'Yazar ' || (n % 4000), 1950 + (n % 75), 1 + (n % 8)
FROM sayac;

INSERT INTO odunc (odunc_id, kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE sayac(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM sayac WHERE n < 2000000)
SELECT n, 1 + ((n * 7) % 200000), 1 + ((n * 13) % 120000),
       date('2018-01-01', '+' || ((n * 37) % 2437) || ' days'),
       CASE WHEN n % 9 = 0 THEN NULL
            ELSE date('2018-01-01', '+' || (((n * 37) % 2437) + 14) || ' days') END
FROM sayac;

Betik kurulum.sql adıyla kaydedilip çalıştırılır. Bu konudaki bütün ölçümler bu veri kümesi üzerinde yapılır.

rm -f kutuphane.db
sqlite3 kutuphane.db < kurulum.sql
sqlite3 kutuphane.db "SELECT 'uye', count(*) FROM uye
  UNION ALL SELECT 'kitap', count(*) FROM kitap
  UNION ALL SELECT 'odunc', count(*) FROM odunc;"
uye|120000
kitap|200000
odunc|2000000

Aramanın Varsayılan Maliyeti

Tek bir üyenin ödünç kayıtları soruluyor. Veritabanı bu sorguyu nasıl karşılıyor? Sorgu planı (query plan), sorgunun hangi adımlarla yürütüleceğini gösteren ağaçtır; komut satırından EXPLAIN QUERY PLAN ön ekiyle istenir. Ölçüm için iki araç daha kullanılır: .timer on geçen süreyi, .stats vmstep sorgunun harcadığı sanal makine adımı sayısını yazar.

sqlite3 kutuphane.db <<'SQL'
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc WHERE uye_id = 4242;
.timer on
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SQL
QUERY PLAN
`--SCAN odunc
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028072 sys 0.007521
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028525 sys 0.007273
17
VM-steps: 6000028
Run Time: real 0.036 user 0.028689 sys 0.007418

Planın tek düğümü var: SCAN odunc. Bu, tam tablo taraması (full table scan) demektir — tablodaki her satır okunur, koşul her satır için ayrı ayrı sınanır. On yedi satır döndürmek için iki milyon satır ziyaret edilmiştir.

İki ölçü birlikte okunmalıdır. Süre ortama bağlıdır: aynı sorgu daha hızlı bir diskte ya da daha yüklü bir makinede farklı çıkar; bu yüzden süreye mutlak bir değer olarak değil, aynı ortamdaki başka bir süreyle oranı üzerinden bakılır. Adım sayısı ise ortamdan bağımsızdır: aynı veri ve aynı plan her makinede aynı adım sayısını verir. Altı milyon adımın kaynağı da bellidir — iki milyon satırın her biri için ortalama üç sanal makine komutu.

Bu maliyet tablo büyüklüğüyle doğru orantılıdır. Ödünç sayısı ikiye katlanırsa tarama da ikiye katlanır: O(n)O(n). Algoritmalar kursundaki Doğrusal Arama dersinin sonucudur bu; tek fark, dizinin diskte olmasıdır.

Dizin Nedir

Dizin (index), bir sütunun değerlerini sıralı tutan ve her değerin hangi satıra ait olduğunu kaydeden ayrı bir yapıdır. Tablo verisi yerinde kalır; dizin, o veriye ikinci bir erişim yolu açar.

Kütüphane benzetmesi doğrudan çalışır: raflardaki kitaplar bir düzene göre dizilidir, ancak kataloğu olmayan bir kütüphanede belli bir yazarın kitaplarını bulmanın yolu bütün rafları gezmektir. Katalog fişleri yazar adına göre sıralanmış ayrı bir kutuda durur ve her fiş bir raf numarası taşır. Fiş kutusunda arama, kutunun ortasını açıp yönü belirleyerek ilerler.

İlişkisel veritabanlarında bu kutunun karşılığı çoğunlukla bir B-ağacıdır (B-tree). Veri Yapıları kursunda B-Ağaçları dersinde kurulan yapı budur: her düğüm bir disk sayfasına sığacak kadar geniştir, ağaç dengelidir ve yüksekliği kayıt sayısının logaritmasıyla artar. Bir anahtarı bulmak için kökten yaprağa inmek gerekir; iki milyon kayıtlı bir dizide bu, birkaç sayfanın okunması demektir. Tablo on kat büyüdüğünde tarama on kat pahalılaşır, ağaçta inilen düzey sayısı ise bir artar.

Dizinin Ölçülen Etkisi

Aynı sorgu, uye_id üzerinde dizin varken tekrarlanır.

sqlite3 kutuphane.db <<'SQL'
.timer on
CREATE INDEX odunc_uye ON odunc(uye_id);
.timer off
EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc WHERE uye_id = 4242;
.timer on
.stats vmstep
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SELECT count(*) FROM odunc WHERE uye_id = 4242;
SQL
Run Time: real 0.263 user 0.219940 sys 0.036873
QUERY PLAN
`--SEARCH odunc USING INDEX odunc_uye (uye_id=?)
17
VM-steps: 62
Run Time: real 0.000 user 0.000016 sys 0.000012
17
VM-steps: 62
Run Time: real 0.000 user 0.000007 sys 0.000004
17
VM-steps: 62
Run Time: real 0.000 user 0.000005 sys 0.000004

Plan düğümü SCAN iken SEARCH ... USING INDEX oldu. Okunacak olan budur: veritabanı artık tabloyu baştan sona okumuyor, dizinden uye_id = 4242 anahtarına iniyor ve yalnız oradaki satırlara bakıyor.

Adım sayısı 6.000.028’den 62’ye indi — yaklaşık yüz bin kat. Süre ise ölçülemez oldu: 0.000 değeri “sıfır süre” demek değil, zamanlayıcının çözünürlüğünün altında kalmak demektir. Bu, ölçüm disiplininin ilk kuralını hatırlatır: bir yol yeterince ucuzladığında süre bilgi vermeyi bırakır, ölçülmesi gereken şey yapılan iştir. Dizinin oluşturulması da bedava değildir; iki milyon satırın sıralanması bu ortamda çeyrek saniye sürmüştür.

Dizinin Bedeli

Dizin, okumayı ucuzlatırken iki yerde pahalılaştırır: yer ve yazma. Aşağıdaki ölçüm aynı veri kümesinin iki kopyasına aynı iki yüz bin satırı ekler; kopyaların biri dizinsiz, diğeri odunc üzerinde üç dizinlidir.

rm -f dizinsiz.db dizinli.db
sqlite3 dizinsiz.db < kurulum.sql
cp dizinsiz.db dizinli.db
sqlite3 dizinli.db 'CREATE INDEX o_uye ON odunc(uye_id);
CREATE INDEX o_kitap ON odunc(kitap_id);
CREATE INDEX o_alis ON odunc(alis_tarihi);'

for db in dizinsiz.db dizinli.db; do
  printf '%s ' "$db"
  sqlite3 "$db" <<'SQL'
.timer on
INSERT INTO odunc (kitap_id, uye_id, alis_tarihi, iade_tarihi)
WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n < 200000)
SELECT 1 + ((n*7) % 200000), 1 + ((n*13) % 120000),
       date('2024-01-01','+'||(n%300)||' days'), NULL
FROM s;
SQL
done
ls -l dizinsiz.db dizinli.db | awk '{print $9, $5}'
dizinsiz.db Run Time: real 0.060 user 0.055046 sys 0.003094
dizinli.db Run Time: real 0.397 user 0.289750 sys 0.095673
dizinli.db 191856640
dizinsiz.db 91119616

Aynı ekleme üç dizin varken bu ortamda altı buçuk kat uzun sürdü ve dosya iki katından fazla büyüdü. Nedeni doğrudan: eklenen her satır için üç ayrı ağaca da bir anahtar yazılır ve o ağaçların dengesi korunur. Silme ve anahtar sütunu güncelleyen UPDATE için de aynı bedel geçerlidir. Dizin bu yüzden “eklenince iyi olur” türü bir seçenek değil, okuma ile yazma arasında bilinçli bir ödünleşimdir.

Seçicilik

Her sütuna dizin açmak, bedeli çoğaltmanın yanı sıra çoğu zaman kazandırmaz da. Belirleyen ölçü seçiciliktir (selectivity): bir koşulun tablonun ne kadar küçük bir bölümünü seçtiği. kitap tablosunda sube_id sekiz farklı değer alır, yazar ise dört bin. Aşağıdaki ölçüm aynı sorgu biçimini iki sütun için, dizinsiz ve dizinli olarak çalıştırır.

rm -f secicilik.db
sqlite3 secicilik.db < kurulum.sql

echo "dizinsiz:"
sqlite3 secicilik.db <<'SQL'
.stats vmstep
SELECT sum(yil) FROM kitap WHERE sube_id = 3;
SELECT sum(yil) FROM kitap WHERE yazar = 'Yazar 7';
SQL

sqlite3 secicilik.db 'CREATE INDEX kitap_sube ON kitap(sube_id);
CREATE INDEX kitap_yazar ON kitap(yazar);'

echo "dizinli:"
sqlite3 secicilik.db <<'SQL'
.stats vmstep
SELECT sum(yil) FROM kitap WHERE sube_id = 3;
SELECT sum(yil) FROM kitap WHERE yazar = 'Yazar 7';
SQL
dizinsiz:
49674950
VM-steps: 650011
99075
VM-steps: 600111
dizinli:
49674950
VM-steps: 125012
99075
VM-steps: 262

Yazar koşulu iki bin üç yüz kat ucuzladı; şube koşulu yalnızca beş kat. Ayrım seçicilikten geliyor: yazar = 'Yazar 7' iki yüz bin kitabın ellisini, sube_id = 3 ise yirmi beş binini seçiyor. İkinci durumda veritabanı yirmi beş bin dizin girdisini okuduktan sonra yirmi beş bin satırı ayrıca tablodan almak zorunda; bu iş, tabloyu sırayla okumaktan çok da ucuz değil. Seçiciliği düşük bir sütuna açılan dizin, yazma bedelini öder ama okuma tarafında karşılığını vermez.

Özet

  • Dizinsiz bir sütun üzerindeki koşul tam tablo taramasıyla karşılanır; maliyet satır sayısıyla doğru orantılıdır.
  • Dizin, sütun değerlerini sıralı tutan ayrı bir yapıdır; B-ağacı biçimindeki bir dizinde arama maliyeti satır sayısının logaritmasıyla artar.
  • Aynı sorgu bu veri kümesinde 6.000.028 adımdan 62 adıma indi; süre zamanlayıcının çözünürlüğünün altına düştüğü için ölçü olarak adım sayısı kullanıldı.
  • Dizin yer kaplar ve her ekleme, silme ve anahtar güncellemesini pahalılaştırır; üç dizinli tabloya ekleme bu ortamda altı buçuk kat uzun sürdü.
  • Kazanç seçicilikle belirlenir: az sayıda satır seçen koşullarda dizin büyük kazandırır, tablonun büyük bölümünü seçen koşullarda kazanç kaybolur.

Sonraki Adım

Bu derste plan çıktısına iki kez bakıldı ve iki kelime ayırt edildi: SCAN ve SEARCH. Gerçek sorgular tek düğümlü değildir — birleştirmeler, sıralamalar, gruplamalar ve alt sorgular planı çok satırlı bir ağaca dönüştürür. Sonraki ders bu ağacı okumayı ele alır: düğümlerin hangi sırada çalıştığı, hangi düğümün hangi tabloyu sürdüğü ve sıralamanın plana nasıl yansıdığı. Plan çıktısının biçimi motordan motora değişir; okunacak olan biçim değil, plandaki karardır.

İ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