SEN2104 · 6. hafta · Structured Query Language (SQL)

SQL temeli: DDL, kısıtlar, SELECT ve join

Tablo kurmak (CREATE TABLE ve kısıtlar), tek tablodan sorgu yazmak ve tabloları birleştirmek (inner, natural, outer, self join). Vizedeki "write the SQL" sorularının temeli.

DDLDMLCREATE TABLEPRIMARY KEYFOREIGN KEYNOT NULLUNIQUECHECKDEFAULTON DELETE CASCADEALTER TABLEDROP TABLESELECT-FROM-WHEREDISTINCTalias (AS)LIKEBETWEENINORDER BYinner joinnatural joinJOIN USINGouter joinself joinCartesian product

SQL (Structured Query Language), ilişkisel veritabanıyla konuşmanın standart dili. Dördüncü ve beşinci haftada ilişkisel cebirle (relational algebra) ne istediğini matematiksel olarak yazdın. SQL aynı fikri okunabilir bir dille yazar. Sınavda en çok puanı “write an SQL query that…” soruları getirir. Bu sayfada iki şeyi öğreneceksin: tabloyu kurmak (DDL) ve tablolardan veri çekmek (SELECT ve join).

Altın kurallar:

  1. SELECT-FROM-WHERE okuma sırası FROM → WHERE → SELECT. Önce tabloyu kur, sonra satırları süz, en son sütunları seç.
  2. SQL tekrarları silmez. İlişkisel cebirde π\pi tekrarları atar, SQL’de bunun için DISTINCT yazman gerekir.
  3. Her join’in bir koşulu olmalı. Koşulsuz FROM r, s Kartezyen çarpımdır.
  4. NULL hiçbir şeye eşit değil, kendine bile. NULL arıyorsan IS NULL yaz.
  5. “Eşi olmayanlar” sorusu = outer join + IS NULL (ya da 7. haftadaki NOT EXISTS).

Örnek veritabanı

Bütün ders boyunca aynı küçük üniversite veritabanını kullanıyoruz. Altı tablo:

TabloSütunlar (ilk sütun anahtar; parantez = bileşik anahtar)Satırlar
departmentdept_name, building, budgetCS Galata 500000, EE Galata 350000, MATH Rumeli 200000, BIO Kadikoy 150000
instructorinst_id, name, dept_name, salaryI1 Aksoy CS 95000, I2 Demir CS 80000, I3 Kaya EE 85000, I4 Sahin MATH 70000, I5 Celik MATH 72000, I6 Yildiz BIO 60000
studentstu_id, name, dept_name, tot_credS1 Ada CS 60, S2 Bora CS 30, S3 Cem EE 45, S4 Defne MATH 90, S5 Ece CS 15, S6 Firat EE 0, S7 Gizem NULL 10
coursecourse_id, title, dept_name, creditsCS101 Intro to Programming 4, CS201 Data Structures 4, CS305 Databases 3, EE210 Circuits 3, MA101 Calculus 4, MA202 Linear Algebra 3, BI100 Biology 3
teaches(inst_id, course_id)I1-CS101, I1-CS305, I2-CS201, I3-EE210, I4-MA101, I5-MA202
takes(stu_id, course_id), gradeS1: CS101 A, CS201 B, CS305 A, MA101 B · S2: CS101 B, CS201 C · S3: EE210 A, MA101 C, CS101 NULL · S4: MA101 A, MA202 A, CS305 B · S5: CS101 F · S7: MA202 B

Dikkat et: Firat (S6) hiç ders almıyor, Gizem’in bölümü NULL, Yildiz ders vermiyor, Biology’yi kimse vermiyor ve almıyor, Cem’in CS101 notu henüz NULL. Bu boşluklar outer join ve NULL sorularının malzemesi.

DDL: tabloyu kurmak

DDL (Data Definition Language), şemayı tanımlar. Satır değil, yapı yaratır.

CREATE TABLE course (
  course_id TEXT PRIMARY KEY,
  title     TEXT NOT NULL,
  dept_name TEXT REFERENCES department(dept_name),
  credits   INTEGER CHECK (credits BETWEEN 1 AND 6)
);

CREATE TABLE takes (
  stu_id    TEXT REFERENCES student(stu_id),
  course_id TEXT REFERENCES course(course_id),
  grade     TEXT,
  PRIMARY KEY (stu_id, course_id)
);

Sık kullanılan veri tipleri (data types): INT/INTEGER, NUMERIC(p,d) (p basamak, d’si ondalık), VARCHAR(n) (en çok n karakter), CHAR(n) (sabit n), DATE, REAL. SQLite tiplere gevşek bakar (TEXT, INTEGER, REAL), standart SQL ve diğer sistemler daha katıdır.

Bütünlük kısıtları (integrity constraints)

KısıtNe yaparİhlal örneği (SQLite mesajı)
PRIMARY KEYtekil ve NULL olamazINSERT INTO student VALUES ('S1','Ali','CS',0) → UNIQUE constraint failed: student.stu_id
NOT NULLboş bırakılamazinstructor name NULL → NOT NULL constraint failed
UNIQUEtekrar yok (aday anahtar)iki kulüp aynı isim → UNIQUE constraint failed
CHECK (P)her satır P’yi sağlamalıcredits = 8 → CHECK constraint failed
DEFAULT vdeğer verilmezse vtot_cred verilmeden ekleme → 0
FOREIGN KEY / REFERENCESdeğer, hedef tabloda olmalıtakes içine S99 → FOREIGN KEY constraint failed

Yabancı anahtar (foreign key) iki yönden korur. Var olmayan öğrenciye kayıt ekleyemezsin. Ders alan bir öğrenciyi de silemezsin: DELETE FROM student WHERE stu_id='S1' hata verir, çünkü takes hâlâ S1’e işaret ediyor. Silmeye izin vermek istiyorsan davranışı sen seçersin:

CREATE TABLE advisor (
  stu_id  TEXT PRIMARY KEY REFERENCES student(stu_id) ON DELETE CASCADE,
  inst_id TEXT NOT NULL REFERENCES instructor(inst_id)
);

ON DELETE CASCADE ile öğrenci silinince advisor satırı da kendiliğinden silinir. Diğer seçenekler ON DELETE SET NULL ve varsayılan davranış olan reddetmek.

TUZAK · SQLite’ta FK kontrolü varsayılan olarak kapalı. SQLite yabancı anahtarları ancak PRAGMA foreign_keys = ON; çalıştırılınca uygular. Doğrulama script’imiz bunu açıyor. Standart SQL’de ve PostgreSQL’de bu kontrol hep açıktır.

ALTER ve DROP

ALTER TABLE student ADD COLUMN email TEXT;   -- var olan satırlarda email = NULL
DROP TABLE club;                             -- tablo ve şeması tamamen gider
DELETE FROM club;                            -- satırlar gider, boş tablo kalır

Temel sorgu: SELECT-FROM-WHERE

SELECT name, salary FROM instructor WHERE dept_name = 'CS' AND salary > 85000;
namesalary
Aksoy95000

Bu sorgu ilişkisel cebirde πname, salary(σdept_name=′CS′∧salary>85000(instructor))\pi_{name,\,salary}(\sigma_{dept\_name='CS' \land salary>85000}(instructor)) demek. Eşleme şöyle:

SQLİlişkisel cebir
FROM r, sr×sr \times s
WHERE PσP\sigma_P
SELECT DISTINCT AπA\pi_A
ASρ\rho

DISTINCT ve tekrarlar

SELECT dept_name FROM instructor 6 satır döndürür: CS, CS, EE, MATH, MATH, BIO. SQL bir çoklu küme (multiset, bag) döndürür. DISTINCT ekleyince:

dept_name
CS
EE
MATH
BIO

AS ve aritmetik

SELECT name, salary / 12 AS monthly FROM instructor WHERE dept_name = 'CS';
namemonthly
Aksoy7916
Demir6666

TUZAK · tam sayı bölmesi. SQLite’ta (ve PostgreSQL’de) iki INTEGER’ın bölümü tam sayıdır, ondalık atılır. salary / 12.0 yazınca 7916.666666666667 ve 6666.666666666667 gelir.

LIKE, BETWEEN, IN

LIKE kalıp eşleştirir: % herhangi uzunlukta dizi (boş da olabilir), _ tam bir karakter.

SELECT course_id, title FROM course WHERE title LIKE '%a%s';
course_idtitle
CS201Data Structures
CS305Databases
MA101Calculus

Kalıp “içinde bir yerde a olsun ve s ile bitsin” diyor. Circuits s ile biter ama içinde a yok. course_id LIKE 'CS_01' ise CS101 ve CS201’i bulur.

TUZAK · LIKE ve büyük harf. Standart SQL’de LIKE büyük/küçük harfe duyarlıdır. SQLite ASCII harflerde duyarsızdır: title LIKE 'data%' SQLite’ta Data Structures ve Databases’ı bulur, PostgreSQL’de hiçbir şey bulmaz.

BETWEEN iki ucu dahil eder:

SELECT name, salary FROM instructor WHERE salary BETWEEN 70000 AND 85000;
namesalary
Demir80000
Kaya85000
Sahin70000
Celik72000

IN bir listeye üyeliği sorar: WHERE dept_name IN ('EE', 'MATH') → Cem, Defne, Firat. Bu, dept_name = 'EE' OR dept_name = 'MATH' ile aynıdır.

ORDER BY

SELECT name, dept_name, salary FROM instructor ORDER BY dept_name ASC, salary DESC;
namedept_namesalary
YildizBIO60000
AksoyCS95000
DemirCS80000
KayaEE85000
CelikMATH72000
SahinMATH70000

Önce bölüme göre artan, aynı bölümde maaşa göre azalan. ORDER BY olmadan satırların sırası garanti değildir. İlk n satır için SQLite ve MySQL LIMIT n, standart SQL FETCH FIRST n ROWS ONLY kullanır (SQLite ikincisini tanımaz).

Join: tabloları birleştirmek

Inner join ve üç yazım biçimi

EE öğrencilerinin aldığı dersler:

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 = 'EE';
namecourse_idgrade
CemCS101NULL
CemEE210A
CemMA101C

Aynı sonucu veren dört yazım (hepsi script’te çalıştırıldı):

FROM student s JOIN takes t ON s.stu_id = t.stu_id      -- ON: koşulu sen yazarsın
FROM student JOIN takes USING (stu_id)                  -- USING: bu sütunlarda eşitle
FROM student NATURAL JOIN takes                         -- aynı isimli TÜM sütunlar
FROM student, takes WHERE student.stu_id = takes.stu_id -- eski stil

Firat bu sonuçta yok. Inner join sadece eşi olan satırları tutar.

NATURAL JOIN tuzağı

student JOIN takes USING (stu_id) JOIN course USING (course_id) 14 satır verir, her kayıt bir kez. Ama:

SELECT name, course_id, dept_name FROM student NATURAL JOIN takes NATURAL JOIN course;

sadece 9 satır döndürür. Sebep: student ve course ikisinde de dept_name var. NATURAL JOIN onları da eşitler, sorgu sessizce “öğrencinin kendi bölümünden aldığı dersler” sorusuna dönüşür. Ada’nın MA101’i, Cem’in MA101 ve CS101’i, Defne’nin CS305’i ve Gizem’in MA202’si kaybolur.

Outer join

Inner join eşi olmayan satırları atar. Outer join onları tutar ve eksik tarafı NULL ile doldurur.

  • LEFT JOIN: soldaki tüm satırlar.
  • RIGHT JOIN: sağdaki tüm satırlar.
  • FULL OUTER JOIN: iki taraftaki tüm satırlar.

Hiç ders almamış öğrenciler:

SELECT s.stu_id, s.name, t.course_id
FROM student s LEFT JOIN takes t ON s.stu_id = t.stu_id
WHERE t.course_id IS NULL;
stu_idnamecourse_id
S6FiratNULL

Öğrenciler ve bölümleri, iki taraftaki yetimler dahil:

SELECT s.name, d.dept_name, d.building
FROM student s FULL OUTER JOIN department d ON s.dept_name = d.dept_name;
namedept_namebuilding
AdaCSGalata
BoraCSGalata
CemEEGalata
DefneMATHRumeli
EceCSGalata
FiratEEGalata
GizemNULLNULL
NULLBIOKadikoy

Gizem’in bölümü NULL olduğu için hiçbir bölümle eşleşmez. BIO’nun öğrencisi yok.

TUZAK · FULL OUTER JOIN her yerde yok. SQLite 3.39’dan (2022) önce RIGHT ve FULL join desteklemiyordu; bu sayfadaki sorgular 3.53’te çalıştırıldı. MySQL hâlâ FULL OUTER JOIN desteklemez. Orada LEFT JOIN sonucunu, sağdaki eşsiz satırlarla UNION yaparak aynı sonucu kurarsın.

Self join

Aynı tabloyu iki farklı takma adla kullanırsın. Aynı bölümde, diğerinden fazla kazanan hocalar:

SELECT a.name AS higher, b.name AS lower
FROM instructor a JOIN instructor b
  ON a.dept_name = b.dept_name AND a.salary > b.salary;
higherlower
AksoyDemir
CelikSahin

Takma ad (alias) zorunlu: instructor.salary > instructor.salary hangisinin hangisi olduğunu söyleyemez.

Sık yapılan hatalar

YanlışDoğrusu
FROM student, department ve join koşulu yokKartezyen çarpım: 7 × 4 = 28 satır. Koşulu ekle.
WHERE dept_name = NULLHiçbir satır gelmez. WHERE dept_name IS NULL
SQL sonucu küme sanmakTekrarlar kalır; tekil isteniyorsa DISTINCT
3 tabloyu NATURAL JOIN ile bağlamakGizli ortak sütunlar (dept_name) da eşitlenir; USING ya da ON kullan
LEFT JOIN ... ON ... AND t.x IS NULLFiltre olmaz, tüm sol satırlar gelir. IS NULL koşulu WHEREe
BETWEEN 70000 AND 85000 uçları hariç sanmakİki uç da dahil
salary / 12 ondalık verir sanmakINTEGER / INTEGER tam sayıdır; 12.0 ile böl
Self join’de a.id <> b.idHer çift iki kez gelir; a.id < b.id
ORDER BY yazmadan sıra beklemekSıra tanımsızdır
DROP TABLE ile satır silmekTablo tamamen gider; satırlar için DELETE FROM

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

Using the university database, write an SQL query that lists the names of CS students who have at least 30 total credits.

Cevabı göster
SELECT name FROM student WHERE dept_name = 'CS' AND tot_cred >= 30;

Tek tablo yeterli: student. İki koşul AND ile bağlanır. "At least" demek >= demek.
CS öğrencileri: Ada (60), Bora (30), Ece (15). 30 ve üstü: Ada, Bora. Bora tam sınırda, > yazsaydın kaybolurdu.

Soru 2kolay

How many rows does SELECT DISTINCT building FROM department; return? How many does it return without DISTINCT?

Cevabı göster
3 rows (Galata, Rumeli, Kadikoy); 4 without DISTINCT

department tablosunda 4 satır var, binalar: Galata, Galata, Rumeli, Kadikoy.
SQL varsayılan olarak tekrarları silmez (multiset). DISTINCT olmadan 4 satır gelir, Galata iki kez. DISTINCT ile 3.

Soru 3orta

Write an SQL query that lists the titles of all courses taken by the student named 'Ada'.

Cevabı göster
SELECT c.title FROM student s JOIN takes t ON s.stu_id = t.stu_id JOIN course c ON t.course_id = c.course_id WHERE s.name = 'Ada';

İsim studentta, ders başlığı courseta, ikisini bağlayan takes. Üç tablo, iki join koşulu.
Ada = S1. S1'in takes satırları: CS101, CS201, CS305, MA101. Başlıklar: Intro to Programming, Data Structures, Databases, Calculus.

Soru 4kolay

Write an SQL query that lists the names of instructors whose salary is between 72000 and 85000 (inclusive), highest salary first.

Cevabı göster
SELECT name FROM instructor WHERE salary BETWEEN 72000 AND 85000 ORDER BY salary DESC;

BETWEEN a AND b iki ucu da dahil eder, yani salary >= 72000 AND salary <= 85000.
Uyanlar: Kaya 85000, Demir 80000, Celik 72000. ORDER BY salary DESC ile sonuç: Kaya, Demir, Celik.

Soru 5orta

Write an SQL query that lists (without duplicates) the names of instructors who teach at least one 4-credit course.

Cevabı göster
SELECT DISTINCT i.name FROM instructor i JOIN teaches t ON i.inst_id = t.inst_id JOIN course c ON t.course_id = c.course_id WHERE c.credits = 4;

4 kredilik dersler: CS101, CS201, MA101. Bunları verenler: I1 Aksoy (CS101), I2 Demir (CS201), I4 Sahin (MA101).
Bir hoca birden fazla 4 kredilik ders verseydi adı iki kez gelirdi; "without duplicates" dediği için DISTINCT şart.
Sonuç: Aksoy, Demir, Sahin.

Soru 6orta

Write an SQL query that lists the titles of courses that no instructor teaches. Use an outer join.

Cevabı göster
SELECT c.title FROM course c LEFT JOIN teaches t ON c.course_id = t.course_id WHERE t.inst_id IS NULL;

LEFT JOIN, eşi olmayan course satırlarını da tutar ve teaches sütunlarını NULL ile doldurur. Sonra WHERE t.inst_id IS NULL sadece eşi olmayanları bırakır.
Sonuç: Biology (BI100).
Tuzak: IS NULL koşulunu ON içine yazarsan filtre olmaz; 7 dersin hepsi gelir. Koşul WHEREde olmalı.

Soru 7orta

A student wrote the query below to list each student's name with the building of their department, for departments with a budget over 300000. How many rows does it return, and what is the bug?

SELECT name, building
FROM student, department
WHERE budget > 300000;
Cevabı göster
14 rows; the join condition student.dept_name = department.dept_name is missing (Cartesian product)

FROM student, department Kartezyen çarpımdır: 7 × 4 = 28 satır. WHERE sadece bütçeyi süzer: CS ve EE kalır, 7 × 2 = 14 satır. Her öğrenci iki bölümle eşleşir, anlamsız.
Düzeltme: WHERE student.dept_name = department.dept_name AND budget > 300000 (ya da JOIN ... ON). Doğru sorgu 5 satır döndürür: Ada, Bora, Cem, Ece, Firat (hepsi Galata).

Soru 8orta

Write an SQL query that returns every pair of distinct students who are in the same department, each pair listed only once, as (s1, s2) student IDs.

Cevabı göster
SELECT a.stu_id AS s1, b.stu_id AS s2 FROM student a JOIN student b ON a.dept_name = b.dept_name AND a.stu_id < b.stu_id;

Aynı tabloyu iki takma adla (alias) kullanan self join.a.stu_id < b.stu_id iki işi birden yapar: kendisiyle eşleşmeyi (S1,S1) ve ters tekrarı (S2,S1) atar. <> yazsaydın her çift iki kez gelirdi (8 satır).
Sonuç: (S1,S2), (S1,S5), (S2,S5), (S3,S6). Gizem'in bölümü NULL; NULL = NULL doğru değil, kimseyle eşleşmez.

Soru 9zor

How many rows does the query below return? (Correct join on stu_id and course_id gives 14 rows.)

SELECT name, course_id
FROM student NATURAL JOIN takes NATURAL JOIN course;
Cevabı göster
9

NATURAL JOIN aynı isimli bütün sütunları eşitler. student ile course ikisinde de dept_name var, yani gizlice student.dept_name = course.dept_name koşulu da eklenir.
Öğrencinin kendi bölümü dışındaki dersler düşer: Ada-MA101, Cem-MA101, Cem-CS101, Defne-CS305, Gizem-MA202 (Gizem'in bölümü NULL). 14 − 5 = 9.
Bu yüzden ikiden fazla tabloda JOIN ... USING (...) ya da ON daha güvenli.

Soru 10zor

How many rows does this full outer join return, and which rows contain NULLs?

SELECT s.name, d.dept_name
FROM student s FULL OUTER JOIN department d
  ON s.dept_name = d.dept_name;
Cevabı göster
8 rows; (Gizem, NULL) and (NULL, BIO)

Eşleşen satırlar: 6 öğrenci (Gizem hariç) kendi bölümüyle, 6 satır.
Soldan eşi olmayan: Gizem (bölümü NULL), sağ taraf NULL ile gelir.
Sağdan eşi olmayan: BIO bölümünde öğrenci yok, sol taraf NULL ile gelir.
6 + 1 + 1 = 8. Not: SQLite FULL OUTER JOINu 3.39 sürümünden beri destekler; MySQL hiç desteklemez (orada LEFT JOIN + UNION ile yazılır).

Soru 11kolay

Which statement fails, and which constraint does it violate? (a) INSERT INTO student (stu_id, name, dept_name) VALUES ('S8', 'Hakan', 'CS'); (b) INSERT INTO course VALUES ('CS999', 'Capstone', 'CS', 8);

Cevabı göster
(b) fails: CHECK (credits BETWEEN 1 AND 6)

(a) Geçerli. tot_cred verilmedi, DEFAULT 0 devreye girer. S8 yeni bir anahtar, CS var olan bir bölüm.
(b) credits sütununda CHECK (credits BETWEEN 1 AND 6) var; 8 bu aralıkta değil. SQLite mesajı: CHECK constraint failed.

Soru 12zor

Write a CREATE TABLE statement for advisor(stu_id, inst_id): each student has at most one advisor, inst_id is required and must be an existing instructor, and when a student is deleted their advisor row must be deleted automatically.

Cevabı göster
CREATE TABLE advisor (stu_id TEXT PRIMARY KEY REFERENCES student(stu_id) ON DELETE CASCADE, inst_id TEXT NOT NULL REFERENCES instructor(inst_id));

"At most one advisor per student": stu_id tek başına anahtar, yani PRIMARY KEY.
"Required": NOT NULL. "Existing instructor": REFERENCES instructor(inst_id).
"Deleted automatically": ON DELETE CASCADE, öğrenci silinince ona bağlı satır da silinir.
Script'te S6 silinince advisor satırının da gittiği, var olmayan I99 ekleyince FK hatası verdiği görüldü.

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ı; sonuç tabloları script çıktısından birebir alındı, kısıt ihlalleri hata mesajıyla doğrulandı (verify/SEN2104/06.mjs, ortak veri lib.mjs).

Deftere not kartı

DDL: CREATE / ALTER / DROP TABLE DML: SELECT / INSERT / UPDATE / DELETE Kısıtlar: PRIMARY KEY (tekil + NOT NULL), UNIQUE, NOT NULL, CHECK (...), DEFAULT v FOREIGN KEY ... REFERENCES t(col) [ON DELETE CASCADE | SET NULL] DROP TABLE = tablo yok olur; DELETE FROM t = satırlar gider, tablo kalır SELECT DISTINCT A FROM r WHERE P = pi_A(sigma_P(r)) (DISTINCT yoksa tekrar kalır) BETWEEN iki ucu dahil. IN ('a','b'). LIKE: % = herhangi dizi, _ = tek karakter ORDER BY a ASC, b DESC (varsayılan ASC) int / int SQLite'ta tam sayı: 95000/12 = 7916. Ondalık için 12.0 FROM r, s WHERE yok = Kartezyen çarpım (|r| x |s| satır) NATURAL JOIN aynı isimli TÜM sütunları eşitler (3 tabloda gizli dept_name tuzağı) LEFT JOIN + WHERE s.col IS NULL = eşi olmayanlar (koşul ON'a değil WHERE'e) FULL OUTER: SQLite 3.39+, MySQL yok (LEFT JOIN UNION ...) self join: iki alias, çiftler için a.id < b.id NULL = NULL doğru değil; join'de NULL hiçbir şeyle eşleşmez