İçeriğe geç
academia.sh

Ders 01 / 20

Alt Sorgular

Skaler, satır ve tablo alt sorgularının yazıldığı yerler, türetilmiş tablolar, IN ile EXISTS ayrımı ve boş değerlerin alt sorgu sonucunu sessizce boşaltması.

İçindekiler

SQL Temelleri kursu, tek bir sonuç kümesi üreten deyimleri kurdu: satır seçmek, tabloları birleştirmek, gruplayıp özetlemek. Bu deyimlerin ortak yanı, hesabın tek adımda bitmesidir. Oysa gerçek sorular çoğu zaman iki adımlıdır: “ortalamanın üstünde ödünç alan üyeler” sorusu, önce ortalamayı, sonra ondan büyük olanları ister.

Bu tür soruları iki ayrı sorguyla çözüp sonucu elde birleştirmek mümkündür, ancak veri sorgular arasında değişebilir ve ara sonucun taşınması gereksiz iştir. SQL’in yanıtı, bir sorgunun içine başka bir sorgu yerleştirmektir. Önceki kursta bu yerleştirme yalnız koşul yazarken kullanıldı; burada kendi başına bir yapı olarak ele alınır. Bu ders, yerleştirmenin biçimlerini ve her birinin nereye yazılabildiğini kurar.

Alt Sorgu ve Döndürdüğü Şekil

Alt sorgu (subquery), başka bir deyimin içine parantezle gömülmüş bir SELECT deyimidir. Nereye yazılabileceğini belirleyen tek şey, döndürdüğü şekildir:

Şekil Ne döndürür Nerede kullanılır
Skaler alt sorgu Tek satır, tek sütun Bir değerin geçebildiği her yer
Satır alt sorgusu Tek satır, çok sütun Satır karşılaştırmalarında
Tablo alt sorgusu Çok satır, çok sütun FROM, IN, EXISTS

Bu üçlü, dersin iskeletidir. Şekil uymadığında motor hata verir; şekil uyduğu hâlde beklenmedik sonuç çıkıyorsa neredeyse her zaman sorumlu boş değerlerdir — dersin son bölümü bunu ele alır.

Kurs boyunca aynı örnek üzerinde çalışılacak: bir kütüphanenin ödünç kayıtları. Dört tablo vardır — sube (şubeler), uye (üyeler), kitap (kitaplar) ve odunc (ödünç işlemleri). Her kod bloğu ihtiyaç duyduğu tabloları kendi içinde kurar; blok olduğu gibi kopyalanıp çalıştırıldığında yazılan çıktıyı üretir.

Skaler Alt Sorgu

Skaler alt sorgu tek bir değer üretir; bu yüzden bir sabitin ya da sütun adının yazılabildiği her yere yazılabilir. En sık kullanıldığı yer, karşılaştırmanın sağ tarafıdır.

“Üye başına düşen ortalama ödünç sayısından fazla kitap alan üyeler” sorusu, HAVING içinde bir skaler alt sorguyla ifade edilir:

sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5),
                       (5,'Emre',6),(6,'Fatma',6),(7,'Gokhan',4),(8,'Hale',5);
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.ad, COUNT(*) AS adet
FROM odunc o JOIN uye u ON u.id = o.uye_id
GROUP BY u.id, u.ad
HAVING COUNT(*) > (SELECT COUNT(*) * 1.0 / COUNT(DISTINCT uye_id) FROM odunc)
ORDER BY adet DESC, u.ad;
SQL
┌───────┬──────┐
│  ad   │ adet │
├───────┼──────┤
│ Ayse  │ 5    │
│ Burak │ 4    │
│ Ceren │ 4    │
└───────┴──────┘

Alt sorgu, dış sorgunun hiçbir sütununa başvurmaz. Bu bağımsızlık iki sonuç doğurur: motor onu bir kez hesaplar ve sonucu sabit gibi kullanır; ayrıca alt sorgu tek başına çalıştırılıp doğrulanabilir. Alt sorgu ile dış sorgu arasında bağ kurulduğunda tablo değişir — sonraki dersin konusu budur.

Skaler alt sorgunun sözleşmesi katıdır: en fazla bir satır döndürmelidir. Sıfır satır döndürürse sonuç boş değerdir; birden çok satır döndürürse standart bir hata gerektirir. Bu yüzden skaler alt sorgular ya bir toplama işlevi içerir ya da anahtara göre süzülür.

Satır Alt Sorgusu

Karşılaştırma yalnız tek değerler arasında yapılmak zorunda değildir. Standart SQL, satır değerlerini de karşılaştırır: soldaki parantezli sütun listesiyle sağdaki alt sorgunun döndürdüğü satır, sütun sütun eşlenir.

sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO odunc VALUES
  (2,3,1,'2024-03-01','2024-03-20'),(11,3,3,'2024-03-06','2024-03-27'),
  (22,3,6,'2024-03-20',NULL),(3,5,1,'2024-03-04','2024-03-18');
SELECT id, kitap_id, uye_id, alis
FROM odunc
WHERE (kitap_id, alis) = (SELECT kitap_id, MIN(alis) FROM odunc WHERE kitap_id = 3);
SQL
┌────┬──────────┬────────┬────────────┐
│ id │ kitap_id │ uye_id │    alis    │
├────┼──────────┼────────┼────────────┤
│ 2  │ 3        │ 1      │ 2024-03-01 │
└────┴──────────┴────────┴────────────┘

Satır karşılaştırması, “şu kitabın ilk ödünç kaydı” gibi iki koşulu birlikte tutan soruları tek ifadeyle yazar. Aynı sonuç iki ayrı skaler alt sorguyla da elde edilebilir, ancak o yazımda iki alt sorgunun aynı satırdan geldiği görsel olarak kaybolur.

Satır değeri karşılaştırması standarttadır, fakat desteklenme derecesi motora göre değişir; sıralama işleçleriyle (<, >) birleştirildiğinde daha da değişir. Taşınabilir kod yazılıyorsa hedef motorun desteğini doğrulamak gerekir.

Tablo Alt Sorgusu ve Türetilmiş Tablolar

Çok satır döndüren bir alt sorgu, bir tablo gibi davranır. İki kullanımı vardır.

Birincisi, FROM içinde türetilmiş tablo (derived table) olarak yer almasıdır. Bu, gruplamanın sonucunu yeniden gruplamak gibi iki katmanlı hesapların standart yoludur:

sqlite3 -box -header <<'SQL'
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
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 MIN(adet) AS en_az, MAX(adet) AS en_cok, ROUND(AVG(adet), 2) AS ortalama
FROM (SELECT uye_id, COUNT(*) AS adet FROM odunc GROUP BY uye_id) AS sayim;
SQL
┌───────┬────────┬──────────┐
│ en_az │ en_cok │ ortalama │
├───────┼────────┼──────────┤
│ 1     │ 5      │ 3.13     │
└───────┴────────┴──────────┘

Toplama işlevi doğrudan iç içe yazılamaz; AVG(COUNT(*)) geçersizdir. Türetilmiş tablo, iç hesabı bir tabloya dönüştürerek dış hesabın önünü açar. Türetilmiş tabloya takma ad verilmesi zorunludur (burada sayim), çünkü dış sorgu ona bir adla başvurur.

İkinci kullanım, tablo alt sorgusunun bir liste olarak süzmede yer almasıdır: WHERE id IN (SELECT …). Bu yazım okunaklıdır; ancak listeye boş değer karıştığında sessizce yanlış davranır.

Boş Değerin Listeyi Boşaltması

Kütüphanede kayıp bildirimleri ayrı bir tabloda tutulsun. Bir bildirim, hangi kitaba ait olduğu henüz belirlenememişse kitap_id sütunu boş bırakılarak kaydedilir. “Kayıp bildirimi olan kitaplar” ve “olmayan kitaplar” sorgularının ikisi de IN ile yazılabilir:

sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
                         (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim');
CREATE TABLE kayip(id INTEGER PRIMARY KEY, kitap_id INT, bildirim TEXT);
INSERT INTO kayip VALUES (1,2,'2024-03-08'),(2,NULL,'2024-03-19');

SELECT baslik FROM kitap WHERE id IN (SELECT kitap_id FROM kayip);
SELECT baslik FROM kitap WHERE id NOT IN (SELECT kitap_id FROM kayip);
SQL
┌───────────┐
│  baslik   │
├───────────┤
│ Sessiz Ev │
└───────────┘

İlk sorgu beklendiği gibi bir satır verdi. İkinci sorgu hiçbir satır vermedi; bu yüzden çıktıda yalnız bir tablo görünüyor. Oysa üç kitabın kayıp bildirimi yok.

Nedeni, İlişkisel Kuram kursunda tanıtılan üç değerli mantıktır. x NOT IN (a, b) ifadesi x <> a AND x <> b demektir. Listede boş değer varsa o karşılaştırma doğru ya da yanlış değil, bilinmeyen olur; bir çarpımın bilinmeyen çarpanı sonucu bilinmeyene çeker ve bilinmeyen satır süzgeçten geçmez:

sqlite3 -box -header <<'SQL'
CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, tur TEXT);
INSERT INTO kitap VALUES (1,'Kayip Zaman','roman'),(2,'Sessiz Ev','roman'),
                         (3,'Sayilar Kurami','bilim'),(4,'Evrenin Yapisi','bilim');
CREATE TABLE kayip(id INTEGER PRIMARY KEY, kitap_id INT, bildirim TEXT);
INSERT INTO kayip VALUES (1,2,'2024-03-08'),(2,NULL,'2024-03-19');

SELECT 3 IN (2, NULL) AS "3 IN (2,NULL)", 3 NOT IN (2, NULL) AS "3 NOT IN (2,NULL)";
SELECT baslik FROM kitap k
WHERE NOT EXISTS (SELECT 1 FROM kayip y WHERE y.kitap_id = k.id);
SQL
┌───────────────┬───────────────────┐
│ 3 IN (2,NULL) │ 3 NOT IN (2,NULL) │
├───────────────┼───────────────────┤
│               │                   │
└───────────────┴───────────────────┘
┌────────────────┐
│     baslik     │
├────────────────┤
│ Kayip Zaman    │
│ Sayilar Kurami │
│ Evrenin Yapisi │
└────────────────┘

İlk satırdaki iki boş hücre, iki ifadenin de boş değer ürettiğini gösterir: ne doğru ne yanlış. EXISTS ise farklı bir soruyu sorar — “eşleşen satır var mı” — ve yanıtı her zaman doğru ya da yanlıştır; boş değer üretmez. Bu yüzden NOT EXISTS üç kitabı da döndürdü.

Kural olarak: alt sorgunun döndürdüğü sütunda boş değer olabiliyorsa NOT IN yerine NOT EXISTS yazılır ya da alt sorguya WHERE kitap_id IS NOT NULL koşulu eklenir. Sütunun boş değer alamayacağı şemada güvenceye alınmışsa NOT IN güvenlidir.

Alt Sorgu Nereye Yazılabilir

Alt sorgunun yazılabildiği dört konum ve her birinin tipik işi şudur:

  • SELECT listesinde: satır başına bir ek değer üretmek.
  • FROM içinde: ara sonucu tablo hâline getirmek.
  • WHERE içinde: satırları başka bir sorgunun sonucuna göre süzmek.
  • HAVING içinde: grupları başka bir sorgunun sonucuna göre süzmek.

SELECT listesindeki alt sorgu okunaklı olsa da maliyeti gizler:

sqlite3 -box -header <<'SQL'
CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT, sube_id INT);
CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT);
INSERT INTO uye VALUES (1,'Ayse',4),(2,'Burak',4),(3,'Ceren',5),(4,'Deniz',5);
INSERT INTO odunc VALUES
  (1,1,1,'2024-03-01','2024-03-15'),(2,3,1,'2024-03-01','2024-03-20'),
  (4,7,1,'2024-03-11',NULL),(6,2,2,'2024-03-01','2024-03-12'),
  (9,8,2,'2024-03-21',NULL),(10,1,3,'2024-03-04','2024-03-10');

SELECT u.ad,
       (SELECT COUNT(*) FROM odunc o WHERE o.uye_id = u.id) AS toplam,
       (SELECT MAX(alis) FROM odunc) AS son_islem_tarihi
FROM uye u
ORDER BY u.id;
SQL
┌───────┬────────┬──────────────────┐
│  ad   │ toplam │ son_islem_tarihi │
├───────┼────────┼──────────────────┤
│ Ayse  │ 3      │ 2024-03-21       │
│ Burak │ 2      │ 2024-03-21       │
│ Ceren │ 1      │ 2024-03-21       │
│ Deniz │ 0      │ 2024-03-21       │
└───────┴────────┴──────────────────┘

Çıktıdaki iki sütun aynı görünse de yapıları farklıdır. Üçüncü sütundaki alt sorgu dış sorguya bağlı değildir; tek kez hesaplanıp her satıra kopyalanır. İkinci sütundaki alt sorgu ise u.id üzerinden dış satıra bağlıdır: her üye için ayrıca değerlendirilmesi gerekir. Bu bağ, alt sorgunun maliyetini satır sayısıyla çarpar.

Ayrıca bu yazımın bir yan yararı görünür: hiç ödünç kaydı olmayan Deniz için sıfır çıktı. İç birleştirmeyle yazılsaydı bu üye sonuçtan tümüyle düşerdi ve onu geri getirmek için dış birleştirme gerekirdi; alt sorgu eşleşme yokken sıfır üretti.

Özet

  • Alt sorgu, döndürdüğü şekle göre skaler, satır ve tablo alt sorgusu olarak ayrılır; şekil, nereye yazılabileceğini belirler.
  • Skaler alt sorgu en fazla bir satır döndürmelidir; sıfır satır boş değer üretir.
  • FROM içindeki türetilmiş tablo, toplama işlevlerini katmanlamanın standart yoludur ve takma ad almak zorundadır.
  • Alt sorgunun sütununda boş değer olabiliyorsa NOT IN sessizce boş sonuç verir; NOT EXISTS üç değerli mantıktan etkilenmediği için doğru yanıtı verir.
  • Dış sorgunun sütununa başvurmayan alt sorgu bir kez hesaplanır; başvuran alt sorgu satır başına değerlendirilir.

Sonraki Adım

Bu dersin son örneğinde iki alt sorgu yan yana duruyordu: biri tek kez, diğeri satır başına hesaplanan. Aradaki fark yazımda tek bir sütun başvurusundan ibaretti, ancak iş miktarındaki fark katsayıyla ölçülür. Sonraki ders, dış satıra bağlı olan bu ikinci biçimi — ilişkili alt sorguyu — tanımlayacak, kaç kez değerlendirildiğini sayarak ölçecek ve aynı sonucu üreten birleştirmeli yazımla karşılaştıracak.

İ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