Ders 10 / 25
Kapsayan Dizinler
Dizinden tabloya dönüşün gerçek bedeli, kapsayan dizinin bu dönüşü ortadan kaldırması, kazancın eşleşen satır sayısıyla büyümesi ve sütun eklendikçe dizinin tablonun kopyasına yaklaşması.
İçindekiler
Önceki dersin son ölçümünde aynı dizin, aynı üye ve aynı sorgu biçimi iki ayrı sayı
verdi: 62 adım ve 103 adım. Aradaki tek fark, planın birinde COVERING sözcüğünün
bulunması, diğerinde bulunmamasıydı.
O sözcük tek bir işi anlatır. Dizin, sorgunun istediği sütunların hepsini taşıyorsa veritabanı yanıtı doğrudan dizinden üretir. Taşımıyorsa, dizinde bulunan her girdi için tabloya dönüp eksik sütunu okumak zorundadır. Bu ders o dönüşün ne kadar pahalı olduğunu ölçer — ve ölçünün hangi araçla yapılacağının kendisi de bir bulgu üretir.
Dönüşün İzi
Ölçümler kütüphane ödünç kayıtları üzerinde yapılır. Aylık bir rapor sorusu seçilmiştir:
bir ay içinde ödünç alınan kaç ayrı kitap var? Sorgu iki sütuna dokunur — alis_tarihi
süzme için, kitap_id sayım için.
rm -f temel.db dar.db kapsayan.db cat > kurulum.sql <<'SQL' CREATE TABLE uye ( uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, sehir TEXT NOT NULL, kayit_tarihi 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 ); 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 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; SQL sqlite3 temel.db < kurulum.sql cp temel.db dar.db cp temel.db kapsayan.db sqlite3 dar.db 'CREATE INDEX odunc_alis ON odunc(alis_tarihi);' sqlite3 kapsayan.db 'CREATE INDEX odunc_alis_kitap ON odunc(alis_tarihi, kitap_id);' echo "-- sayfa sayisi --" for db in temel.db dar.db kapsayan.db; do printf '%-13s %s\n' "$db" "$(sqlite3 "$db" 'PRAGMA page_count;')" done for db in dar.db kapsayan.db; do printf '\n=== %s ===\n' "$db" sqlite3 "$db" <<'SQL' EXPLAIN QUERY PLAN SELECT count(DISTINCT kitap_id) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31'; .stats vmstep SELECT count(DISTINCT kitap_id) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31'; SQL done
-- sayfa sayisi -- temel.db 19078 dar.db 28424 kapsayan.db 30311 === dar.db === QUERY PLAN |--USE TEMP B-TREE FOR count(DISTINCT) `--SEARCH odunc USING INDEX odunc_alis (alis_tarihi>? AND alis_tarihi<?) 25441 VM-steps: 203543 === kapsayan.db === QUERY PLAN |--USE TEMP B-TREE FOR count(DISTINCT) `--SEARCH odunc USING COVERING INDEX odunc_alis_kitap (alis_tarihi>? AND alis_tarihi<?) 25441 VM-steps: 178101
Plan farkı beklendiği gibi: ikinci dizin kitap_id sütununu da taşıdığı için plan
COVERING INDEX diyor. Boyut farkı da beklendiği gibi: kapsayan dizin bir sütun fazla
taşıdığı için veritabanına 1.887 sayfa daha ekledi.
Adım sayısı ise beklentiyi boşa çıkarıyor. 203.543’ten 178.101’e, yalnızca yüzde on iki. Yirmi beş bin satır için yirmi beş bin tablo dönüşünün maliyeti bu mudur?
Doğru Ölçü
Adım sayısı, sorgunun yürüttüğü sanal makine komutlarını sayar. Tablodan bir satır okumak tek bir komuttur — o komutun arkasında bir disk sayfasının bulunup okunması yatar ama sayaç bunu görmez. Bir başka deyişle: adım sayısı yapılan işlemleri sayar, dokunulan sayfaları değil.
İşlemci Önbelleği dersinde kurulan ayrım burada geri gelir. Aynı sayıda komut, verinin nerede olduğuna göre yüz kat farklı sürebilir. Ölçünün değiştirilmesi gerekir: sayfa ıskası sayısı. Aşağıdaki koşum aynı sorguyu üç ayrı genişlikte çalıştırır ve her koşum için hem sayfa ıskasını hem geçen süreyi yazar. Her sorgu ayrı bir süreçte çalışır, bu yüzden sayaçlar soğuk önbellekten başlar.
echo "-- eslesen satir sayilari --" sqlite3 temel.db "SELECT '1 gun', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-15' AND '2021-03-15' UNION ALL SELECT '1 ay', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-03-01' AND '2021-03-31' UNION ALL SELECT '1 yil', count(*) FROM odunc WHERE alis_tarihi BETWEEN '2021-01-01' AND '2021-12-31';" for aralik in "2021-03-15 2021-03-15" "2021-03-01 2021-03-31" "2021-01-01 2021-12-31"; do bas=${aralik% *}; bit=${aralik#* } printf '\n### %s .. %s\n' "$bas" "$bit" for db in dar.db kapsayan.db; do printf '%-13s ' "$db" sqlite3 "$db" <<SQL 2>&1 | grep -E 'Page cache misses|Run Time' | tr '\n' ' ' .stats on .timer on SELECT count(DISTINCT kitap_id) FROM odunc WHERE alis_tarihi BETWEEN '$bas' AND '$bit'; SQL echo done done
-- eslesen satir sayilari -- 1 gun|820 1 ay|25441 1 yil|299547 ### 2021-03-15 .. 2021-03-15 dar.db Page cache misses: 874 Run Time: real 0.001 user 0.000626 sys 0.000896 kapsayan.db Page cache misses: 8 Run Time: real 0.000 user 0.000136 sys 0.000042 ### 2021-03-01 .. 2021-03-31 dar.db Page cache misses: 26241 Run Time: real 0.034 user 0.017126 sys 0.016881 kapsayan.db Page cache misses: 147 Run Time: real 0.005 user 0.005101 sys 0.000187 ### 2021-01-01 .. 2021-12-31 dar.db Page cache misses: 309414 Run Time: real 0.372 user 0.204850 sys 0.166556 kapsayan.db Page cache misses: 1686 Run Time: real 0.060 user 0.057790 sys 0.002153
Sayılar tek bir örüntüyü gösteriyor. Dar dizinde sayfa ıskası sayısı, eşleşen satır sayısını yaklaşık birebir izliyor: 820 satır için 874 sayfa, 25.441 satır için 26.241 sayfa, 299.547 satır için 309.414 sayfa. Kapsayan dizinde aynı üç sayı 8, 147 ve 1.686.
Oranın kaynağı fiziksel depolama düzenidir. Dizin, tarihe göre sıralı olduğu için
eşleşen girdiler yan yanadır ve bir sayfa okuması yüzlerce girdi getirir. Tabloya dönüş
ise odunc_id sırasına göre dizilmiş bir yapıda rastgele noktalara gitmektir; art
arda gelen iki dizin girdisinin satırları tabloda birbirinden uzaktır. Her satır için
ayrı bir sayfa okunur, aynı sayfa birden çok kez okunur ve tampon havuzu bu erişimi
tutamaz.
Süre bunu doğruluyor: yıllık sorguda 0,372 saniyeye karşı 0,060 saniye, altı kat. Süreler ortama bağlıdır ve başka bir makinede değişir; kararlı olan, sayfa ıskası oranıdır — yıllık sorguda 309.414 / 1.686 ≈ 184.
Buradan ölçüm disiplininin bir kuralı çıkar: bir plan değişikliğinin etkisi değerlendirilirken, ölçünün değişen işi görüyor olması gerekir. Adım sayısı sorgunun mantığını iyi ölçer, disk erişimini ölçmez.
Kapsamanın Sınırı
Kapsayan dizin bedelsiz değildir. Dizine konan her sütun, iki milyon girdinin her birinde tekrarlanır. Aşağıdaki ölçüm aynı dizini dört ayrı genişlikte kurar ve her birinin kapladığı sayfa sayısını tablonun kendisiyle karşılaştırır.
rm -f genislik.db sqlite3 genislik.db < kurulum.sql sqlite3 genislik.db <<'SQL' .mode column .headers on CREATE INDEX d1 ON odunc(alis_tarihi); CREATE INDEX d2 ON odunc(alis_tarihi, kitap_id); CREATE INDEX d3 ON odunc(alis_tarihi, kitap_id, uye_id); CREATE INDEX d4 ON odunc(alis_tarihi, kitap_id, uye_id, iade_tarihi); SELECT name AS nesne, count(*) AS sayfa FROM dbstat WHERE name IN ('odunc','d1','d2','d3','d4') GROUP BY name ORDER BY sayfa; SQL
nesne sayfa ----- ----- d1 9346 d2 11233 d3 13089 d4 17968 odunc 18013
Dört sütunlu dizin 17.968 sayfa; tablonun kendisi 18.013 sayfa. Sorgunun istediği her sütun dizine eklendiğinde dizin, tablonun sıralanmış bir kopyası hâline gelir. O noktada kapsama artık bir eniyileme değil, verinin iki kez saklanmasıdır: yer iki katına çıkar, her yazma iki yapıya birden gider ve yedeğin boyutu büyür.
Karar ölçütü buradan çıkar. Kapsamaya değen sütunlar, sorgunun sonucunda geçen ama tablonun geri kalanına göre dar olan sütunlardır. Bir sayım için gereken tek bir kimlik sütununu eklemek 1.887 sayfa ödeyip 184 kat sayfa ıskası kazandırdı; uzun bir metin sütununu eklemek aynı hesabı ters çevirir.
İkinci ölçüt sorgunun genişliğidir. Bir gün süzen sorguda kazanç 874 sayfadan 8 sayfaya — mutlak değeri küçük bir tasarruf. Aynı dizin yıllık raporda üç yüz bin sayfa okumayı engelledi. Kapsayan dizin, çok satır eşleştiren ve az sütun isteyen sorgular için kurulur; tek satır getiren aramalarda kurulmasına gerek yoktur, çünkü orada dönüş zaten tek sayfadır.
Kapsama ve Sıra Birlikte
Kapsayan dizinin sütun listesi iki işi birden yapar ve bu iki iş çakışabilir. Önceki derste kurulan sol ön ek kuralı süzme ve sıralama için sütun sırasını belirliyordu; kapsama ise yalnız sütunun var olmasını ister, sırasını değil.
İki gereksinim bir listede toplanır: süzme ve sıralama sütunları sırası önemli olacak
biçimde başa, yalnız sonuçta geçtiği için eklenen sütunlar sona. (alis_tarihi, kitap_id)
dizini tam olarak budur — tarih giriş noktasını verir, kitap kimliği yalnız taşınır.
Ters sıra, (kitap_id, alis_tarihi), aynı sütunları taşır ve aynı yeri kaplar ama tarih
aralığına giriş noktası vermez.
Bazı motorlar bu ayrımı sözdizimine taşır ve yalnız taşınacak sütunları dizin anahtarının dışında tutan bir yazım sunar; böyle bir sütun benzersizlik denetimine katılmaz ve sıralamada yer tutmaz. Bu yazımın adı ve varlığı motora özgüdür; kavram aynıdır — anahtar olan sütunlarla yalnız taşınan sütunların ayrılması.
Özet
- Kapsayan dizin, sorgunun istediği bütün sütunları taşıyan dizindir; plan çıktısında
COVERINGsözcüğüyle görünür ve tabloya hiç dönülmediğini bildirir. - Tabloya dönüşün maliyeti adım sayısında görünmez: aylık sorguda adım sayısı yüzde on iki düşerken sayfa ıskası 26.241’den 147’ye indi.
- Sayfa ıskası dar dizinde eşleşen satır sayısını birebir izler, çünkü tabloya dönüş rastgele erişimdir; kapsayan dizinde okuma dizin içinde sıralı ilerler.
- Kazanç eşleşen satır sayısıyla büyür: bir günlük sorguda 874 sayfaya karşı 8 sayfa, yıllık sorguda 309.414 sayfaya karşı 1.686 sayfa.
- Dizine sütun eklendikçe dizin tablonun kopyasına yaklaşır; dört sütunlu dizin 17.968 sayfa, tablonun kendisi 18.013 sayfa oldu.
Sonraki Adım
Buraya kadarki üç ders dizini bir kazanç olarak ele aldı ve bedelini yalnız kurulduğu andaki yerle ölçtü. Dizin kurulduğu anda kalmaz: satırlar silindikçe ve güncellendikçe dizin ağacında boşluklar birikir, dosya büyür, her yeni dizin yazma yolunu bir kat daha uzatır. Sonraki ders bu tarafı ölçer — silme sonrası dizinin ne kadar şiştiği, yeniden oluşturmanın neyi geri kazandırdığı ve tabloya eklenen her dizinin ekleme süresini ne kadar uzattığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.