İleri SQL: GROUP BY, NULL, alt sorgular, DML ve view
Aggregate fonksiyonlar, GROUP BY/HAVING, NULL'un üç değerli mantığı, IN/EXISTS/korelasyonlu alt sorgular, küme işlemleri, INSERT/UPDATE/DELETE ve view. Vizenin en zor SQL soruları buradan.
Altıncı haftada tek tek satırları seçip birleştirdin. Bu hafta satırları özetliyoruz (kaç tane, ortalama ne), sorgunun içine sorgu koyuyoruz ve veriyi değiştiriyoruz. Vize SQL bölümündeki zor sorular neredeyse hep şu üç şeyden birine dayanır: GROUP BY/HAVING ayrımı, NULL’un garip davranışı ve “her X’i yapanlar” (bölme) sorusu.
Altın kurallar:
- WHERE satırı, HAVING grubu süzer. Aggregate fonksiyon
WHEREiçine giremez. - NULL ile karşılaştırma TRUE ya da FALSE değil, UNKNOWN verir.
WHEREsadece TRUE olan satırları tutar. NOT INve NULL birlikte zehirlidir. Alt sorguda bir NULL varsaNOT INhiçbir satır döndürmez.- “Her/tüm (every/all)” geçen soru bölme sorusudur. Çift
NOT EXISTSile yaz. COUNT(*)satır sayar,COUNT(sütun)NULL olmayan değer sayar.
Örnek veritabanı 6. haftadakiyle aynı (department, instructor, student, course, teaches, takes).
Aggregate fonksiyonlar
Beş temel aggregate: COUNT, SUM, AVG, MIN, MAX. Bir sütundaki çok değeri tek değere indirirler.
SELECT COUNT(*), COUNT(grade), COUNT(DISTINCT stu_id) FROM takes;
| COUNT(*) | COUNT(grade) | COUNT(DISTINCT stu_id) |
|---|---|---|
| 14 | 13 | 6 |
14 kayıt var. Biri NULL notlu (Cem, CS101), o yüzden COUNT(grade) 13. Ders alan farklı öğrenci 6 (Firat yok).
SELECT SUM(salary), AVG(salary), MIN(salary), MAX(salary) FROM instructor;
| SUM(salary) | AVG(salary) | MIN(salary) | MAX(salary) |
|---|---|---|---|
| 462000 | 77000 | 60000 | 95000 |
SQLite’ta AVG her zaman REAL döndürür (typeof = real), SUM tam sayı sütunda INTEGER kalır. Bu yüzden AVG(credits) 3.4285714285714284 verir. Ama 7/2 hâlâ 3: sadece AVG kendiliğinden ondalığa geçer.
Boş bir kümede COUNT 0, diğerleri NULL döner: WHERE salary > 100000 ile COUNT(*), SUM(salary), AVG(salary) sonucu 0, NULL, NULL.
GROUP BY ve HAVING
GROUP BY satırları ortak değere göre gruplara ayırır, aggregate her gruba ayrı uygulanır.
SELECT dept_name, COUNT(*) AS n, AVG(salary) AS avg_sal
FROM instructor GROUP BY dept_name;
| dept_name | n | avg_sal |
|---|---|---|
| BIO | 1 | 60000 |
| CS | 2 | 87500 |
| EE | 1 | 85000 |
| MATH | 2 | 71000 |
Gruplara koşul koymak için HAVING:
SELECT dept_name, AVG(salary) AS avg_sal
FROM instructor GROUP BY dept_name HAVING AVG(salary) > 80000;
| dept_name | avg_sal |
|---|---|
| CS | 87500 |
| EE | 85000 |
WHERE ile HAVING birlikte
“20’den fazla kredisi olan öğrencilerden en az 2 tanesi bulunan bölümler”:
SELECT dept_name, COUNT(*) AS n
FROM student
WHERE tot_cred > 20
GROUP BY dept_name
HAVING COUNT(*) >= 2;
| dept_name | n |
|---|---|
| CS | 2 |
Adım adım: WHERE önce Ece (15), Firat (0) ve Gizem’i (10) atar. Kalanlar gruplanır: CS 2 (Ada, Bora), EE 1 (Cem), MATH 1 (Defne). HAVING sadece CS’yi bırakır.
FROM student 7 satır
WHERE tot_cred > 20 4 satır: Ada, Bora, Cem, Defne
GROUP BY dept_name CS{Ada,Bora} EE{Cem} MATH{Defne}
HAVING COUNT(*) >= 2 CS
SELECT CS, 2
TUZAK · gruplanmamış sütun. Standart SQL’de
SELECTlistesinde aggregate dışındaki her sütunGROUP BYda olmalı.SELECT dept_name, name, MAX(salary) FROM instructor GROUP BY dept_namePostgreSQL’de hata verir. SQLite hata vermez venameiçin gruptan bir satır seçer (MAX ile kullanılınca en yüksek maaşlının adı: Aksoy, Kaya, Celik, Yildiz). Sınavda bu sorguyu hatalı say.
Sıfırları saymak
Her dersin öğrenci sayısı, hiç öğrencisi olmayanlar dahil:
SELECT c.course_id, COUNT(t.stu_id) AS enrolled
FROM course c LEFT JOIN takes t ON c.course_id = t.course_id
GROUP BY c.course_id;
| course_id | enrolled |
|---|---|
| BI100 | 0 |
| CS101 | 4 |
| CS201 | 2 |
| CS305 | 2 |
| EE210 | 1 |
| MA101 | 3 |
| MA202 | 2 |
COUNT(*) yazsaydın BI100 için 1 çıkardı, çünkü LEFT JOIN onu (BI100, NULL, NULL) satırıyla getirir.
NULL ve üç değerli mantık
NULL “bilinmiyor” ya da “yok” demek. Aritmetikte bulaşıcıdır: 5 + NULL = NULL. Karşılaştırmada ise sonuç UNKNOWN olur: NULL = NULL bile UNKNOWN.
| AND | TRUE | UNKNOWN | FALSE |
|---|---|---|---|
| TRUE | TRUE | UNKNOWN | FALSE |
| UNKNOWN | UNKNOWN | UNKNOWN | FALSE |
| FALSE | FALSE | FALSE | FALSE |
OR için: TRUE OR UNKNOWN = TRUE, FALSE OR UNKNOWN = UNKNOWN. NOT UNKNOWN = UNKNOWN. SQLite’ta SELECT NULL = NULL, NULL AND 0, NULL OR 1, NULL AND 1, NOT NULL sonucu NULL, 0, 1, NULL, NULL (0 = FALSE, 1 = TRUE).
Sonuçlar:
WHERE dept_name = NULLhiçbir satır döndürmez. DoğrusuWHERE dept_name IS NULL→ Gizem.WHERE dept_name <> 'CS'3 satır döndürür (Cem, Defne, Firat). Gizem gelmez:NULL <> 'CS'UNKNOWN.WHERE grade = 'A' OR grade <> 'A'14 değil 13 satır verir. “Her şey” gibi görünen koşul NULL’u kaçırır.GROUP BYise NULL’ları bir grup sayar:GROUP BY dept_nameöğrencilerde NULL 1, CS 3, EE 2, MATH 1 verir.
Alt sorgular (subqueries)
IN
I1’in (Aksoy) verdiği bir dersi alan öğrenciler:
SELECT name FROM student
WHERE stu_id IN (SELECT t.stu_id FROM takes t JOIN teaches te ON t.course_id = te.course_id
WHERE te.inst_id = 'I1');
Sonuç: Ada, Bora, Cem, Defne, Ece. Aksoy CS101 ve CS305 veriyor; CS101’i S1, S2, S3, S5, CS305’i S1, S4 almış. IN tekrar üretmez, Ada bir kez gelir.
NOT IN tuzağı ve NOT EXISTS
Öğrencisi olmayan bölümleri arayalım:
SELECT dept_name FROM department
WHERE dept_name NOT IN (SELECT dept_name FROM student);
Sonuç boş. Alt sorgu Gizem’in NULL’unu da döndürür. 'BIO' NOT IN (CS, EE, MATH, NULL) aslında 'BIO' <> 'CS' AND ... AND 'BIO' <> NULL demek ve son terim UNKNOWN. Doğrusu:
SELECT d.dept_name FROM department d
WHERE NOT EXISTS (SELECT * FROM student s WHERE s.dept_name = d.dept_name);
| dept_name |
|---|
| BIO |
TUZAK · NOT IN + NULL. Alt sorgu tek bir NULL bile döndürürse
NOT INhiçbir zaman TRUE olmaz. YaNOT EXISTSkullan ya da alt sorguyaWHERE ... IS NOT NULLekle.INiçin böyle bir sorun yok.
EXISTS ve korelasyonlu alt sorgu
EXISTS (alt sorgu) alt sorgu en az bir satır döndürürse TRUE. Alt sorgu dıştaki satıra referans veriyorsa korelasyonlu (correlated) denir; dıştaki her satır için yeniden değerlendirilir.
SELECT c.course_id FROM course c
WHERE EXISTS (SELECT * FROM takes t WHERE t.course_id = c.course_id AND t.grade = 'A');
Sonuç: CS101, CS305, EE210, MA101, MA202 (en az bir A notu verilmiş dersler).
Kendi bölüm ortalamasından fazla kazananlar:
SELECT i.name, i.salary FROM instructor i
WHERE i.salary > (SELECT AVG(i2.salary) FROM instructor i2 WHERE i2.dept_name = i.dept_name);
| name | salary |
|---|---|
| Aksoy | 95000 |
| Celik | 72000 |
Skaler alt sorgu
Tek satır, tek sütun döndüren alt sorgu bir değer gibi kullanılabilir, SELECT listesinde bile:
SELECT d.dept_name,
(SELECT COUNT(*) FROM instructor i WHERE i.dept_name = d.dept_name) AS n_inst
FROM department d;
Sonuç: BIO 1, CS 2, EE 1, MATH 2. FROM içinde de alt sorgu olur: SELECT ... FROM (SELECT dept_name, AVG(salary) AS avg_sal FROM instructor GROUP BY dept_name) WHERE avg_sal > 80000 HAVING örneğiyle aynı sonucu verir.
ALL ve SOME
Standart SQL’de x > ALL (küme) “kümedeki her elemandan büyük”, x > SOME (küme) (ya da ANY) “en az birinden büyük” demek.
| Standart SQL | Eşdeğeri (SQLite’ta çalışan) | MATH maaşlarına göre sonuç |
|---|---|---|
salary > ALL (MATH maaşları) | salary > (SELECT MAX(...)) | Aksoy, Demir, Kaya |
salary > SOME (MATH maaşları) | salary > (SELECT MIN(...)) | Aksoy, Demir, Kaya, Celik |
= SOME | IN | |
<> ALL | NOT IN |
TUZAK · SQLite’ta ALL/SOME yok.
> ALL (...)SQLite’tasyntax errorverir. Sınavda standart yazımı bil, SQLite’ta MAX/MIN ile yaz. Celik 72000, MATH’teki 70000’den büyük ama 72000’den (kendisinden) büyük değil: SOME’da var, ALL’da yok.
Bölme: “hepsini alan” öğrenciler
İlişkisel cebirdeki işleminin SQL’de doğrudan karşılığı yok. “Bütün CS derslerini alan öğrenciler” sorusunu “alınmamış CS dersi olmayan öğrenciler” diye çevir:
SELECT s.name FROM student s
WHERE NOT EXISTS (
SELECT c.course_id FROM course c WHERE c.dept_name = 'CS'
EXCEPT
SELECT t.course_id FROM takes t WHERE t.stu_id = s.stu_id);
| name |
|---|
| Ada |
Her öğrenci için “CS dersleri eksi onun aldıkları” hesaplanır. Küme boşsa öğrenci hepsini almış. Aynı sorgu çift NOT EXISTS ile de yazılır (sorular bölümünde MATH örneği var).
Küme işlemleri
| İşlem | Anlamı | Tekrar |
|---|---|---|
UNION | siler | |
INTERSECT | siler | |
EXCEPT (Oracle: MINUS) | siler | |
UNION ALL | çoklu küme birleşimi | tutar |
SELECT course_id FROM takes WHERE stu_id = 'S1'
INTERSECT
SELECT course_id FROM takes WHERE stu_id = 'S2';
Sonuç: CS101, CS201. EXCEPT ile çevirirsen (S1 eksi S2) CS305, MA101 gelir. SELECT course_id FROM course EXCEPT SELECT course_id FROM takes hiç alınmamış dersi verir: BI100.
SELECT dept_name FROM instructor UNION SELECT dept_name FROM student 5 satır döndürür (NULL, BIO, CS, EE, MATH); UNION ALL ile 6 + 7 = 13. İki tarafın sütun sayısı ve tipleri uyumlu olmalı. Standart SQL’de INTERSECT ALL ve EXCEPT ALL da var, SQLite’ta yok.
Veri değiştirme: INSERT, UPDATE, DELETE
Her örnek taze bir veritabanı kopyasında çalıştırıldı.
INSERT INTO course (course_id, title, dept_name, credits)
VALUES ('CS410', 'Machine Learning', 'CS', 3);
-- 30+ kredili CS öğrencilerini yeni derse kaydet
INSERT INTO takes (stu_id, course_id, grade)
SELECT stu_id, 'CS410', NULL FROM student WHERE dept_name = 'CS' AND tot_cred >= 30;
İkinci ifade S1 ve S2 için iki satır ekler.
UPDATE instructor SET salary = salary * 1.05 WHERE salary < 75000;
Sahin 73500, Celik 75600, Yildiz 63000 olur, diğerleri değişmez. İki farklı kural varsa tek ifadede CASE kullan; iki ayrı UPDATE yazarsan birincinin yükselttiği satır ikincinin koşuluna girebilir:
UPDATE instructor
SET salary = CASE WHEN salary < 80000 THEN salary + 5000 ELSE salary + 2000 END;
Sonuç: Aksoy 97000, Demir 82000, Kaya 87000, Sahin 75000, Celik 77000, Yildiz 65000.
DELETE FROM takes WHERE grade = 'F'; -- 14 -> 13 satır
DELETE FROM takes
WHERE stu_id IN (SELECT stu_id FROM student WHERE tot_cred < 20); -- 13 -> 12
DELETE FROM instructor WHERE salary < (SELECT AVG(salary) FROM instructor) ilginç: silme sırasında ortalama değişir mi? Hayır. SQL alt sorguyu silmeden önce bir kez hesaplar (77000) ve sonra siler. FK kontrolü kapalı bir kopyada Sahin, Celik, Yildiz silinir. FK açıkken ise ifade reddedilir, çünkü Sahin ve Celik teaches tablosunda ders veriyor.
View
View, adı olan kayıtlı bir sorgudur. Veri kopyalamaz; her kullanıldığında tanımı yeniden çalışır.
CREATE VIEW cs_roster AS
SELECT s.name, t.course_id, t.grade
FROM student s JOIN takes t ON s.stu_id = t.stu_id
WHERE s.dept_name = 'CS';
SELECT course_id, COUNT(*) AS n FROM cs_roster GROUP BY course_id;
| course_id | n |
|---|---|
| CS101 | 3 |
| CS201 | 2 |
| CS305 | 1 |
| MA101 | 1 |
takes tablosuna yeni bir CS öğrencisi kaydı eklenince view kendiliğinden 7’den 8 satıra çıkar. View’lar güvenlik (kullanıcıya sadece bazı sütunları göstermek) ve karmaşık sorguyu basitleştirmek için kullanılır. View üzerinden INSERT her sistemde mümkün değil; SQLite cannot modify cs_roster because it is a view der.
Sık yapılan hatalar
| Yanlış | Doğrusu |
|---|---|
WHERE AVG(salary) > 75000 | Aggregate koşulu HAVINGe |
SELECT dept_name, name, MAX(salary) ... GROUP BY dept_name | name gruplanmamış; standart SQL’de hata |
Sıfırlı gruplar için inner join + COUNT(*) | LEFT JOIN + COUNT(sağ.sütun) |
WHERE x = NULL | WHERE x IS NULL |
NOT IN (alt sorgu) alt sorgu NULL içerirken | NOT EXISTS ya da IS NOT NULL filtresi |
COUNT(grade) ile satır saymak | NULL notlar sayılmaz; satır için COUNT(*) |
“Hepsini alan” için IN | IN “en az birini” verir; bölme için çift NOT EXISTS |
> ALL yerine > SOME | ALL = en büyükten büyük, SOME = en küçükten büyük |
UNION tekrar tutar sanmak | Tutan UNION ALL |
İki kuralı iki ayrı UPDATE ile yapmak | Etkileşebilir; tek UPDATE + CASE |
| View’ı tablo kopyası sanmak | Saklanan sorgu; veri her seferinde yeniden hesaplanır |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
What does the query below return?
SELECT COUNT(*), COUNT(grade)
FROM takes
WHERE course_id = 'CS101';Cevabı göster
CS101'i alanlar: S1 (A), S2 (B), S3 (NULL), S5 (F). Dört satır, COUNT(*) = 4.COUNT(grade) NULL olmayan değerleri sayar. Cem'in notu NULL olduğu için sayılmaz: 3.
Write an SQL query that lists each department name with the average salary of its instructors, but only for departments whose average salary is above 75000.
Cevabı göster
Grup bazında koşul olduğu için WHERE değil HAVING.
Grup ortalamaları: BIO 60000, CS (95000 + 80000)/2 = 87500, EE 85000, MATH (70000 + 72000)/2 = 71000.
75000'in üstü: CS 87500, EE 85000.
Write an SQL query that lists every course_id with the number of students enrolled in it. Courses with no students must appear with 0.
Cevabı göster
İki tuzak var. Inner join kullanırsan BI100 hiç gelmez, bu yüzden LEFT JOIN.COUNT(*) yazarsan BI100 için 1 çıkar, çünkü LEFT JOIN onu NULL'larla dolu bir satırla getirir. COUNT(t.stu_id) NULL'u saymaz, 0 verir.
Sonuç: BI100 0, CS101 4, CS201 2, CS305 2, EE210 1, MA101 3, MA202 2.
The query below is meant to list departments that have no students. What does it return on our database, why, and how do you fix it?
SELECT dept_name FROM department
WHERE dept_name NOT IN (SELECT dept_name FROM student);Cevabı göster
Alt sorgu şunu döndürür: CS, CS, EE, MATH, CS, EE, NULL.
BIO için 'BIO' NOT IN (..., NULL) demek 'BIO' <> 'CS' AND ... AND 'BIO' <> NULL. Son terim unknown. TRUE AND unknown = unknown, satır seçilmez. Hiçbir bölüm için TRUE çıkmaz: sonuç boş.
Düzeltme 1: NOT EXISTS (SELECT * FROM student s WHERE s.dept_name = d.dept_name).
Düzeltme 2: alt sorguya WHERE dept_name IS NOT NULL ekle. İkisi de BIO döndürür.
Write an SQL query using NOT EXISTS that lists the names of students who have not taken any course.
Cevabı göster
Korelasyonlu alt sorgu: dıştaki her s satırı için içteki sorgu o öğrencinin kayıtlarını arar. Hiç satır yoksa NOT EXISTS TRUE olur.
Kaydı olmayan tek öğrenci: Firat (S6).
Write an SQL query that lists the names of students who have taken every course offered by the MATH department.
Cevabı göster
"Every" sorusu bölme (division) sorusudur. SQL'de çift NOT EXISTS ile yazılır: "almadığı bir MATH dersi olmayan öğrenciler".
MATH dersleri: MA101, MA202. MA101'i alanlar S1, S3, S4; MA202'yi alanlar S4, S7. İkisini de alan sadece S4: Defne.
Aynı soru NOT EXISTS (MATH dersleri EXCEPT öğrencinin dersleri) biçiminde de yazılabilir. İlişkisel cebirde de S4 verdi.
In standard SQL, the query below lists instructors earning more than every MATH instructor. SQLite rejects it. Rewrite it so it runs in SQLite and give the result.
SELECT name FROM instructor
WHERE salary > ALL (SELECT salary FROM instructor
WHERE dept_name = 'MATH');Cevabı göster
> ALL (küme), kümedeki en büyük değerden büyük olmak demek. Bu yüzden > (SELECT MAX(...)) ile aynı. (> SOME ise > MIN olur.)
MATH maaşları 70000 ve 72000, en büyüğü 72000. Üstünde olanlar: Aksoy 95000, Demir 80000, Kaya 85000.
SQLite ALL/SOME/ANY karşılaştırmalarını desteklemez (syntax error). PostgreSQL ve Oracle destekler.
Not: MATH'te hiç hoca olmasaydı > ALL her satır için TRUE verirdi, > MAX (NULL ile karşılaştırma) hiçbir satır vermezdi.
teaches has 6 rows and takes has 14 rows. How many rows does each query return? (a) SELECT course_id FROM teaches UNION ALL SELECT course_id FROM takes; (b) the same with UNION.
Cevabı göster
UNION ALL tekrarları korur: 6 + 14 = 20.UNION tekrarları siler. İki tablodaki farklı course_id değerleri: CS101, CS201, CS305, EE210, MA101, MA202, yani 6. BI100 ikisinde de yok.
Write an SQL query that lists the name and salary of instructors who earn more than the average salary of their own department.
Cevabı göster
"Kendi bölümünün" dediği için alt sorgu dıştaki satıra bağlı: korelasyonlu (correlated) alt sorgu. İçteki i2.dept_name = i.dept_name bu bağı kurar.
Bölüm ortalamaları: CS 87500, EE 85000, MATH 71000, BIO 60000.
Ortalamadan büyük olanlar: Aksoy (95000 > 87500), Celik (72000 > 71000). Kaya ve Yildiz bölümlerinin tek hocası, ortalamaya eşit, büyük değil.
After running the statement below, what is Ada's (S1) tot_cred? Which other students change?
UPDATE student
SET tot_cred = tot_cred + 5
WHERE stu_id IN (SELECT stu_id FROM takes WHERE grade = 'A');Cevabı göster
Alt sorgu A alanları bulur: S1 (iki tane A), S3, S4. IN üyelik sorar, S1'in iki A'sı olması onu iki kez güncellemez.
Ada 60 + 5 = 65, Cem 45 + 5 = 50, Defne 90 + 5 = 95. Diğerleri değişmez.
Find the bug and fix it.
SELECT dept_name
FROM instructor
WHERE AVG(salary) > 75000
GROUP BY dept_name;Cevabı göster
WHERE gruplamadan önce, tek tek satırlara uygulanır. O anda henüz grup yok, AVG hesaplanamaz. SQLite mesajı: misuse of aggregate function AVG(). (Ayrıca WHERE, GROUP BYdan sonra yazılamaz.)
Grup koşulu HAVING ile yazılır. Düzeltilmiş sorgu CS ve EE döndürür.
Give the result table of SELECT grade, COUNT(*) FROM takes GROUP BY grade ORDER BY grade;
Cevabı göster
GROUP BY, NULL değerlerini tek bir grup olarak toplar (WHERE'deki = mantığından farklı). Burada COUNT(*) kullanıldığı için NULL grubu 1 sayılır (Cem'in CS101'i). COUNT(grade) olsaydı o satır 0 olurdu.
A: S1-CS101, S1-CS305, S3-EE210, S4-MA101, S4-MA202 = 5. B: S1-CS201, S1-MA101, S2-CS101, S4-CS305, S7-MA202 = 5. C: S2-CS201, S3-MA101 = 2. F: S5-CS101 = 1.
SQLite artan sıralamada NULL'u en başa koyar. Toplam 1 + 5 + 5 + 2 + 1 = 14 = takes satır sayısı.
Doğrulama: Sayfadaki her SQL ifadesi ve her soru cevabı node:sqlite (SQLite 3.53.4) ile örnek üniversite veritabanında çalıştırıldı; DML örnekleri her biri taze bir veritabanı kopyasında. ALL/SOME SQLite'ta olmadığı için hata verdiği ve MAX/MIN eşdeğerlerinin aynı sonucu verdiği hem SQL hem JS ile doğrulandı; bölme sorgusu JS ilişkisel cebir bölmesiyle karşılaştırıldı (verify/SEN2104/07.mjs).