Ders 07 / 20
Döndürme İşlemleri
Uzun biçimden geniş biçime koşullu toplamayla döndürme, FILTER yan tümcesi, ters döndürmenin iki yazımı ve sütun listesinin sorgu yazılırken bilinme zorunluluğu.
İçindekiler
Buraya kadarki her sorgunun sonucu, sütunları sorgu yazılırken belli olan bir tabloydu. Raporlarda ise sık sık tersi istenir: satırlardaki değerler sütun başlığına çıksın, her kitap türü ayrı bir sütun olsun, şubeler satırlarda kalsın.
Bu dönüşüme döndürme (pivot) denir. Kaynak biçim uzun biçimdir: her ölçüm bir satır, ayrım sütunları değer taşır. Hedef biçim geniş biçimdir: ayrım sütunundaki her değer bir sütuna dönüşür. Dönüşüm, SQL’in temel bir kuralıyla gerilim içindedir — sonucun sütun listesi, sorgu derlenirken bilinmek zorundadır.
Uzun Biçim
Başlangıç noktası, gruplamayla üretilen olağan özettir: şube ve tür ikilisi başına ödünç sayısı.
sqlite3 -box -header <<'SQL' CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT); CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT); CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'), (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'), (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar'); INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'), (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'), (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'), (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe'); INSERT INTO odunc VALUES (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'), (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL), (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'), (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'), (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'), (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'), (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'), (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL), (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'), (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'), (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL), (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'), (25,4,8,'2024-03-13','2024-03-24'); SELECT u.sube, k.tur, COUNT(*) AS adet FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id GROUP BY u.sube, k.tur ORDER BY u.sube, k.tur; SQL
┌──────────┬─────────┬──────┐ │ sube │ tur │ adet │ ├──────────┼─────────┼──────┤ │ Besiktas │ bilim │ 1 │ │ Besiktas │ deneme │ 1 │ │ Besiktas │ felsefe │ 1 │ │ Besiktas │ roman │ 2 │ │ Besiktas │ tarih │ 1 │ │ Kadikoy │ bilim │ 2 │ │ Kadikoy │ deneme │ 1 │ │ Kadikoy │ roman │ 4 │ │ Kadikoy │ siir │ 1 │ │ Kadikoy │ tarih │ 3 │ │ Uskudar │ bilim │ 3 │ │ Uskudar │ felsefe │ 1 │ │ Uskudar │ roman │ 2 │ │ Uskudar │ siir │ 1 │ │ Uskudar │ tarih │ 1 │ └──────────┴─────────┴──────┘
Bu biçim veri işleme için elverişlidir: yeni bir tür eklendiğinde sorgu değişmez, yalnız satır sayısı artar. Okumak içinse elverişsizdir. Kadıköy ile Üsküdar’ın roman sayısını karşılaştırmak için gözün on beş satır arasında yukarı aşağı gezinmesi gerekir; ayrıca sıfır olan hücreler — Beşiktaş’ta şiir yok — hiç görünmez.
Koşullu Toplamayla Döndürme
Döndürmenin taşınabilir yazımı, her hedef sütun için bir koşullu toplama ifadesi
yazmaktır. CASE ifadesi, satırın o sütuna ait olup olmadığına göre 1 ya da 0 üretir;
SUM bunları toplar.
sqlite3 -box -header <<'SQL' CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT); CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT); CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'), (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'), (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar'); INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'), (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'), (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'), (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe'); INSERT INTO odunc VALUES (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'), (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL), (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'), (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'), (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'), (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'), (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'), (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL), (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'), (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'), (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL), (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'), (25,4,8,'2024-03-13','2024-03-24'); SELECT u.sube, SUM(CASE WHEN k.tur = 'roman' THEN 1 ELSE 0 END) AS roman, SUM(CASE WHEN k.tur = 'bilim' THEN 1 ELSE 0 END) AS bilim, SUM(CASE WHEN k.tur = 'tarih' THEN 1 ELSE 0 END) AS tarih, SUM(CASE WHEN k.tur = 'siir' THEN 1 ELSE 0 END) AS siir, SUM(CASE WHEN k.tur = 'deneme' THEN 1 ELSE 0 END) AS deneme, SUM(CASE WHEN k.tur = 'felsefe' THEN 1 ELSE 0 END) AS felsefe, COUNT(*) AS toplam FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id GROUP BY u.sube ORDER BY u.sube; SQL
┌──────────┬───────┬───────┬───────┬──────┬────────┬─────────┬────────┐ │ sube │ roman │ bilim │ tarih │ siir │ deneme │ felsefe │ toplam │ ├──────────┼───────┼───────┼───────┼──────┼────────┼─────────┼────────┤ │ Besiktas │ 2 │ 1 │ 1 │ 0 │ 1 │ 1 │ 6 │ │ Kadikoy │ 4 │ 2 │ 3 │ 1 │ 1 │ 0 │ 11 │ │ Uskudar │ 2 │ 3 │ 1 │ 1 │ 0 │ 1 │ 8 │ └──────────┴───────┴───────┴───────┴──────┴────────┴─────────┴────────┘
On beş satır üçe indi ve boş hücreler sıfır olarak göründü. Bu, uzun biçimin veremediği bilgidir: Beşiktaş’ta şiir ödüncü yok ile şiir ödüncü bilinmiyor arasındaki fark artık okunur.
ELSE 0 bölümü ihmal edilirse eşleşmeyen satırlar boş değer üretir. SUM boş değerleri
atladığı için sonuç çoğu durumda değişmez; ancak bir grubun tüm satırları eşleşmezse
sütun sıfır yerine boş kalır. Sıfırın anlamlı olduğu raporlarda ELSE 0 yazılması ya da
sonucun COALESCE ile sarılması gerekir.
Bu yazımın maliyeti tek bir taramadır: her satır bir kez okunur, altı ifade aynı satır üzerinde değerlendirilir. Aynı sonucu altı ayrı sorguyu birleştirerek üretmek, tabloyu altı kez taramak demektir.
FILTER Yan Tümcesi
Standart SQL, koşullu toplama için ayrı bir yazım tanımlar: toplama işlevinin ardına
gelen FILTER (WHERE …) yan tümcesi, o işlevin yalnız koşulu sağlayan satırları görmesini
sağlar. Amaç CASE ile aynıdır, okunurluk daha iyidir — koşul, sayılan şeyin yanında durur:
sqlite3 -box -header <<'SQL' CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube TEXT); CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT); CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); INSERT INTO uye VALUES (1,'Ayse','Kadikoy'),(2,'Burak','Kadikoy'),(3,'Ceren','Uskudar'), (4,'Deniz','Uskudar'),(5,'Emre','Besiktas'),(6,'Fatma','Besiktas'), (7,'Gokhan','Kadikoy'),(8,'Hale','Uskudar'); INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'), (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim'),(5,'Kisa Tarih','tarih'), (6,'Anadolu Notlari','tarih'),(7,'Siirler','siir'),(8,'Denemeler','deneme'), (9,'Yol Haritasi','roman'),(10,'Bilgi Kurami','felsefe'); INSERT INTO odunc VALUES (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'), (3,5,1,'2024-03-04','2024-03-18'),(4,7,1,'2024-03-11',NULL), (5,9,1,'2024-03-18','2024-03-29'),(6,2,2,'2024-03-01','2024-03-12'), (7,4,2,'2024-03-06','2024-03-25'),(8,6,2,'2024-03-11','2024-03-19'), (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10'), (11,3,3,'2024-03-06','2024-03-27'),(12,10,3,'2024-03-13','2024-03-22'), (13,2,3,'2024-03-25',NULL),(14,5,4,'2024-03-04','2024-03-09'), (15,7,4,'2024-03-13','2024-03-26'),(16,4,4,'2024-03-20',NULL), (17,6,5,'2024-03-06','2024-03-14'),(18,9,5,'2024-03-11','2024-03-23'), (19,1,5,'2024-03-25','2024-03-28'),(20,8,6,'2024-03-04','2024-03-17'), (21,10,6,'2024-03-13','2024-03-21'),(22,3,6,'2024-03-20',NULL), (23,2,7,'2024-03-11','2024-03-16'),(24,5,7,'2024-03-18','2024-03-24'), (25,4,8,'2024-03-13','2024-03-24'); SELECT u.sube, COUNT(*) FILTER (WHERE k.tur = 'roman') AS roman, COUNT(*) FILTER (WHERE k.tur = 'bilim') AS bilim, COUNT(*) FILTER (WHERE k.tur = 'tarih') AS tarih, COUNT(*) FILTER (WHERE o.iade IS NULL) AS iade_edilmemis, COUNT(*) AS toplam FROM odunc o JOIN uye u ON u.id = o.uye_id JOIN kitap k ON k.id = o.kitap_id GROUP BY u.sube ORDER BY u.sube; SQL
┌──────────┬───────┬───────┬───────┬────────────────┬────────┐ │ sube │ roman │ bilim │ tarih │ iade_edilmemis │ toplam │ ├──────────┼───────┼───────┼───────┼────────────────┼────────┤ │ Besiktas │ 2 │ 1 │ 1 │ 1 │ 6 │ │ Kadikoy │ 4 │ 2 │ 3 │ 2 │ 11 │ │ Uskudar │ 2 │ 3 │ 1 │ 2 │ 8 │ └──────────┴───────┴───────┴───────┴────────────────┴────────┘
Roman, bilim ve tarih sütunları önceki sonucun aynısıdır. Dördüncü sütun, döndürmenin
yalnız bir ayrım sütunuyla sınırlı olmadığını gösterir: iade_edilmemis, tür ile ilgisi
olmayan bağımsız bir koşuldur ve aynı taramada hesaplanmıştır. Farklı ölçütlerin aynı
satırda yan yana getirilmesi, koşullu toplamanın döndürmeden bağımsız asıl yararıdır.
FILTER yan tümcesi standarttadır ancak her motorda bulunmaz; desteklenmediği yerde
CASE yazımı her zaman çalışır.
Ters Yön: Sütundan Satıra
Ters işlem, geniş biçimdeki bir tabloyu uzun biçime çevirir. İhtiyaç, dış bir kaynaktan gelen geniş özetin normalleştirilmiş bir tabloya aktarılmasında ya da tür sayısı değiştikçe bozulmayan bir sorgu yazılmasında doğar.
Taşınabilir yazım, sütun adlarını bir değer listesine dönüştürüp kaynak tabloyla çapraz birleştirmektir:
sqlite3 -box -header <<'SQL' CREATE TABLE ozet(sube TEXT PRIMARY KEY, roman INT, bilim INT, tarih INT); INSERT INTO ozet VALUES ('Besiktas',2,1,1),('Kadikoy',4,2,3),('Uskudar',2,3,1); WITH turler(tur) AS (VALUES ('roman'),('bilim'),('tarih')) SELECT o.sube, t.tur, CASE t.tur WHEN 'roman' THEN o.roman WHEN 'bilim' THEN o.bilim WHEN 'tarih' THEN o.tarih END AS adet FROM ozet o CROSS JOIN turler t ORDER BY o.sube, t.tur; SQL
┌──────────┬───────┬──────┐ │ sube │ tur │ adet │ ├──────────┼───────┼──────┤ │ Besiktas │ bilim │ 1 │ │ Besiktas │ roman │ 2 │ │ Besiktas │ tarih │ 1 │ │ Kadikoy │ bilim │ 2 │ │ Kadikoy │ roman │ 4 │ │ Kadikoy │ tarih │ 3 │ │ Uskudar │ bilim │ 3 │ │ Uskudar │ roman │ 2 │ │ Uskudar │ tarih │ 1 │ └──────────┴───────┴──────┘
Çapraz birleştirme üç şubeyi üç türle eşleştirerek dokuz satır üretti; CASE her satırda
doğru sütunu seçti. Kaynak tablo bir kez okunur.
Aynı sonuç, her sütun için bir sorgu yazıp UNION ALL ile birleştirerek de üretilir:
sqlite3 -box -header <<'SQL' CREATE TABLE ozet(sube TEXT PRIMARY KEY, roman INT, bilim INT, tarih INT); INSERT INTO ozet VALUES ('Besiktas',2,1,1),('Kadikoy',4,2,3),('Uskudar',2,3,1); SELECT sube, 'roman' AS tur, roman AS adet FROM ozet UNION ALL SELECT sube, 'bilim', bilim FROM ozet UNION ALL SELECT sube, 'tarih', tarih FROM ozet ORDER BY sube, tur; SQL
┌──────────┬───────┬──────┐ │ sube │ tur │ adet │ ├──────────┼───────┼──────┤ │ Besiktas │ bilim │ 1 │ │ Besiktas │ roman │ 2 │ │ Besiktas │ tarih │ 1 │ │ Kadikoy │ bilim │ 2 │ │ Kadikoy │ roman │ 4 │ │ Kadikoy │ tarih │ 3 │ │ Uskudar │ bilim │ 3 │ │ Uskudar │ roman │ 2 │ │ Uskudar │ tarih │ 1 │ └──────────┴───────┴──────┘
Sonuçlar aynı, okunma ve maliyet farklı. Birleşimli yazım her sütun için tabloyu bir kez tarar: üç sütun, üç tarama. Küçük özet tablolarında bu önemsizdir; kaynak büyükse çapraz birleştirmeli yazım tercih edilir.
Sütun Listesi Neden Sabittir
Her iki yönde de tür adları sorgunun metnine elle yazıldı. Bunun nedeni bir eksiklik değil, ilişkisel modelin bir sonucudur: bir sorgunun sonucu bir bağıntıdır ve bağıntının başlığı — sütun adları ve tipleri — sorgu çalıştırılmadan bellidir. Veriye bakarak sütun üretmek bu tanımı bozar.
Bunun pratik karşılığı şudur: yeni bir kitap türü eklendiğinde geniş biçimli sorgu kendiliğinden bir sütun kazanmaz; sorgunun güncellenmesi gerekir. Sütun listesini veriden üretmek isteyen, sorgu metnini uygulama tarafında birleştirip motora göndermek zorundadır. Bu yol açıktır ama iki bedeli vardır: metin birleştirmeyle üretilen sorgular enjeksiyon yüzeyi açar ve her farklı sütun listesi ayrı bir sorgu metni ürettiği için plan önbelleğinden yararlanmaz. Bu iki konu, kursun Sorgu Başarımı konusundaki Dinamik SQL Riskleri dersinde ele alınacaktır.
Bazı motorlar PIVOT ya da benzeri bir anahtar sözcük sunar. Bu yazımlar standart SQL’in
parçası değildir ve sütun listesini yine sorgu yazılırken ister; yaptıkları, koşullu
toplamayı kısaltmaktır.
Özet
- Uzun biçim veri işlemeye, geniş biçim okumaya elverişlidir; döndürme bu iki biçim arasındaki dönüşümdür.
- Taşınabilir döndürme yazımı, hedef sütun başına bir koşullu toplama ifadesidir ve tek
taramada tamamlanır;
ELSE 0yazılmazsa hiç eşleşmeyen gruplarda sütun boş kalır. FILTER (WHERE …)yan tümcesi aynı işi daha okunur yazar ve birbirinden bağımsız ölçütlerin aynı satırda toplanmasına izin verir.- Ters döndürme, sütun adlarını değer listesine çevirip çapraz birleştirerek tek taramada
ya da
UNION ALLile sütun başına bir tarama yaparak yazılır. - Sonuç sütunlarının sorgu yazılırken bilinmesi zorunludur; sütun listesini veriden üretmek sorgu metninin uygulama tarafında birleştirilmesini gerektirir.
Sonraki Adım
Bu konu boyunca yazılan her sorgu tek bir deyimdi ve tek bir soruya yanıt verdi. Veri değiştiren işler ise çoğu zaman tek deyime sığmaz: bir kitabın ödünç verilmesi, ödünç kaydının açılmasını ve kitabın durumunun güncellenmesini birlikte gerektirir. İkisinden biri yapılıp diğeri yapılmazsa veri tutarsız kalır. Sonraki konu, birden çok deyimi bölünmez tek bir birim hâline getiren işlemleri ele alacak; ilk ders işlemin başlatılması, kesinleştirilmesi ve geri alınmasıyla başlayacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.