Ders 15 / 20
Sorgu Planı Okuma
Plan ağacının yapısı ve okunma yönü, tarama ile arama düğümlerinin ayrımı, kapsayan dizinin plandaki izi, birleştirme düğümlerinin sırası, sıralama düğümünün ölçülen maliyeti ve bileşik dizinde sütun sırasının plana etkisi.
İçindekiler
Önceki ders plan çıktısına iki kez baktı ve iki kelime ayırt etti: SCAN ve SEARCH.
Tek tablolu, tek koşullu bir sorguda plan tek satırdır ve okunması kolaydır.
Gerçek sorgular böyle değildir. Üç tablo birleştiren, süzen, gruplayan ve sıralayan bir sorgunun planı çok düğümlü bir ağaçtır; bu ağaçtaki her düğüm bir karar taşır. Bu ders o ağacı okumayı ele alır: düğümlerin ne anlattığını, hangi tablonun diğerlerini sürdüğünü ve sıralamanın plana nasıl yansıdığını.
Veri Kümesi
Ölçümler önceki dersteki kütüphane veri kümesi üzerinde yapılır. Aşağıdaki blok bu kümeyi sıfırdan kurar; başka hiçbir dosya gerektirmez.
rm -f kutuphane.db cat > kurulum.sql <<'SQL' 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; SQL sqlite3 kutuphane.db < kurulum.sql
Plan Bir Ağaçtır
Belli bir yazarın kitaplarının kimler tarafından ödünç alındığını soran sorgu üç tabloya dokunur. Planı da üç satırdır.
sqlite3 kutuphane.db <<'SQL' CREATE INDEX odunc_uye ON odunc(uye_id); CREATE INDEX odunc_kitap ON odunc(kitap_id); CREATE INDEX kitap_yazar ON kitap(yazar); EXPLAIN QUERY PLAN SELECT u.ad, k.baslik, o.alis_tarihi FROM odunc o JOIN uye u ON u.uye_id = o.uye_id JOIN kitap k ON k.kitap_id = o.kitap_id WHERE k.yazar = 'Yazar 7'; SQL
QUERY PLAN |--SEARCH k USING INDEX kitap_yazar (yazar=?) |--SEARCH o USING INDEX odunc_kitap (kitap_id=?) `--SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
Üç satır, üç tablo erişimidir ve sıraları anlamlıdır. En üstteki düğüm sürücü tablodur
(driving table): veritabanı işe oradan başlar. Alttaki düğümler, sürücüden gelen her
satır için bir kez çalışır. Burada iş kitap tablosundan başlıyor, çünkü tek süzme
koşulu orada; yazar = 'Yazar 7' elli satır bırakıyor. Bu elli kitabın her biri için
odunc içinde kitap_id araması yapılıyor, bulunan her ödünç kaydı için de uye
tablosundan birincil anahtarla tek satır çekiliyor.
Sorgu metninde odunc önce yazılmıştı. Plan bunu dikkate almadı: FROM yan tümcesindeki
sıra bir yürütme sırası değil, bir ad listesidir. Erişim sırasına planlayıcı karar verir.
Tarama ve Arama Düğümleri
Plan düğümlerinin ilk ayrımı, tablonun baştan sona mı okunduğu yoksa bir anahtardan mı girildiğidir.
SCAN, tablodaki ya da dizindeki her kaydın sırayla okunduğunu söyler. SEARCH, bir
anahtar üzerinden girildiğini ve yalnız eşleşen bölümün okunduğunu söyler; parantez
içindeki (uye_id=?) hangi koşulun anahtar olarak kullanıldığını gösterir. Üçüncü bir
ayrım, aranan yapının kendisidir: USING INTEGER PRIMARY KEY (rowid=?) ayrı bir dizin
değil, tablonun kendi birincil anahtarıdır.
Dördüncü kelime, tabloya hiç dokunulmadığını söyler.
sqlite3 kutuphane.db <<'SQL' EXPLAIN QUERY PLAN SELECT count(*) FROM odunc WHERE uye_id = 4242; EXPLAIN QUERY PLAN SELECT count(iade_tarihi) FROM odunc WHERE uye_id = 4242; .stats vmstep SELECT count(*) FROM odunc WHERE uye_id = 4242; SELECT count(iade_tarihi) FROM odunc WHERE uye_id = 4242; SQL
QUERY PLAN `--SEARCH odunc USING COVERING INDEX odunc_uye (uye_id=?) QUERY PLAN `--SEARCH odunc USING INDEX odunc_uye (uye_id=?) 17 VM-steps: 62 17 VM-steps: 97
İki sorgu aynı dizini kullanıyor, ancak birincisinin planında COVERING sözcüğü var.
Kapsayan dizin (covering index), sorgunun istediği bütün sütunları kendi içinde
barındıran dizindir: uye_id dizinde zaten var, sayım için başka sütun gerekmiyor, bu
yüzden tabloya hiç gidilmiyor. İkinci sorgu iade_tarihi sütununu istiyor; o sütun
dizinde olmadığı için her eşleşen dizin girdisinden tabloya bir dönüş yapılıyor. On yedi
satır için fark otuz beş adım; bu fark eşleşen satır sayısıyla doğru orantılı büyür.
Sıralama Düğümü
Bazı düğümler tabloya değil, veri üzerinde yapılan bir işe karşılık gelir. Sıralama bunların en pahalısıdır.
sqlite3 kutuphane.db <<'SQL' EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5; .timer on .stats vmstep SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5; .timer off .stats off CREATE INDEX odunc_alis ON odunc(alis_tarihi); EXPLAIN QUERY PLAN SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5; .timer on .stats vmstep SELECT odunc_id, alis_tarihi FROM odunc ORDER BY alis_tarihi LIMIT 5; SQL
QUERY PLAN |--SCAN odunc `--USE TEMP B-TREE FOR ORDER BY 2437|2018-01-01 4874|2018-01-01 7311|2018-01-01 9748|2018-01-01 12185|2018-01-01 VM-steps: 12000130 Run Time: real 0.066 user 0.057176 sys 0.008530 QUERY PLAN `--SCAN odunc USING COVERING INDEX odunc_alis 2437|2018-01-01 4874|2018-01-01 7311|2018-01-01 9748|2018-01-01 12185|2018-01-01 VM-steps: 32 Run Time: real 0.000 user 0.000014 sys 0.000014
USE TEMP B-TREE FOR ORDER BY düğümü, sıralamanın geçici bir yapı kurularak yapıldığını
söyler. Beş satır isteniyor olması bunu ucuzlatmıyor: en küçük beş tarihi bulmak için iki
milyon satırın hepsinin görülmesi gerekiyor, çünkü sıra bilinmiyor.
Dizin oluşturulduğunda sıralama düğümü plandan kayboluyor. Dizin zaten tarihe göre sıralıdır; veritabanı baştan beş girdi okuyup duruyor. On iki milyon adım otuz iki adıma iniyor — dizinin ikinci işlevi budur: aramayı ucuzlatmanın yanı sıra hazır bir sıra sunar. Süre ise yine ölçüm sınırının altına düşüyor; ortama bağlı olan süre değil, oranların yönü kararlıdır.
Bileşik Dizinde Sütun Sırası
Bileşik dizin (composite index), birden çok sütun üzerinde tanımlanan dizindir ve girdileri önce birinci sütuna, eşitlik durumunda ikinciye göre sıralar. Sıra bir ayrıntı değil, dizinin ne işe yarayacağını belirleyen karardır. Aşağıdaki ölçüm aynı sorguyu iki farklı sütun sırasıyla çalıştırır.
for sira in "alis_tarihi, uye_id" "uye_id, alis_tarihi"; do rm -f sira.db sqlite3 sira.db < kurulum.sql sqlite3 sira.db "CREATE INDEX odunc_bilesik ON odunc($sira);" printf 'dizin: (%s)\n' "$sira" sqlite3 sira.db <<'SQL' EXPLAIN QUERY PLAN SELECT odunc_id FROM odunc WHERE uye_id = 4242 ORDER BY alis_tarihi; .stats vmstep SELECT count(*) FROM (SELECT odunc_id FROM odunc WHERE uye_id = 4242 ORDER BY alis_tarihi); SQL done
dizin: (alis_tarihi, uye_id) QUERY PLAN `--SCAN odunc USING COVERING INDEX odunc_bilesik 17 VM-steps: 6000101 dizin: (uye_id, alis_tarihi) QUERY PLAN `--SEARCH odunc USING COVERING INDEX odunc_bilesik (uye_id=?) 17 VM-steps: 135
İki dizin aynı iki sütunu içeriyor, aynı yeri kaplıyor ve aynı sorgu çalışıyor. Fark kırk
dört bin kat. Nedeni sıralama düzenidir: telefon rehberi soyada göre sıralıysa ada göre
arama yapılamaz. (alis_tarihi, uye_id) dizininde uye_id değerleri tarih tarih
dağılmıştır; bir üyeyi bulmak için dizinin tamamının okunması gerekir — plan bunu SCAN
diyerek söyler. (uye_id, alis_tarihi) dizininde ise aynı üyenin bütün kayıtları yan
yanadır ve tarih sırasındadır; plan SEARCH diyor ve sıralama düğümü hiç görünmüyor.
Kural şudur: eşitlikle süzülen sütunlar dizinin başında, aralık koşulu ve sıralama için kullanılan sütunlar sonrasında yer alır.
Okunacak Olan Biçim Değil Karar
Buradaki çıktılar tek bir motorun biçimidir. Başka motorlarda plan bir ağaç yerine girintili bir liste, bir tablo ya da düğüm başına tahmini satır sayısı ve maliyet taşıyan satırlar olarak gelir; düğüm adları da değişir — sıralı tarama, dizin taraması, karma birleştirme gibi adlar aynı kavramların başka isimleridir.
Değişmeyen şey, plandan okunan kararlardır: tabloya baştan sona mı bakıldı yoksa bir anahtardan mı girildi, hangi tablo diğerlerini sürdü, tabloya dönmek gerekti mi, ayrıca bir sıralama yapıldı mı. Bir plan çıktısını okumak, bu dört sorunun cevabını çıkarmaktır.
Özet
- Plan bir ağaçtır; üstteki düğüm sürücü tablodur ve alttaki düğümler sürücüden gelen her
satır için çalışır.
FROMyan tümcesindeki sıra yürütme sırasını belirlemez. SCANbaştan sona okumayı,SEARCHbir anahtardan girmeyi gösterir; parantez içindeki koşul hangi sütunun anahtar olarak kullanıldığını söyler.COVERINGsözcüğü sorgunun yalnız dizinden yanıtlandığını, tabloya hiç dönülmediğini bildirir.- Sıralama düğümü ayrı bir maliyettir; uygun bir dizin sıralamayı plandan tamamen kaldırdı ve aynı sorgu 12.000.130 adımdan 32 adıma indi.
- Bileşik dizinde sütun sırası, dizinin hangi sorguya yarayacağını belirler; aynı iki sütunun ters sırası aynı sorguyu 135 adımdan 6.000.101 adıma çıkardı.
Sonraki Adım
Bu dersteki bütün ölçümlerde dizin vardı ve kullanıldı. Uygulamada sık karşılaşılan durum
başkadır: dizin vardır, sorgu o sütunu süzer, plan yine de SCAN der. Sonraki ders bu
durumu ele alır — sütunun üzerine uygulanan bir işlev, sol ucu açık bir kalıp eşleme ya da
tip uyuşmazlığı dizini nasıl devre dışı bırakır ve aynı sonucu veren hangi yeniden yazım
dizini geri getirir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.