Blok 5 · Ders 36
İlişkiler ve JOIN
Tabloları birbirine bağlamak: yabancı anahtar, bire-çok ve çoka-çok ilişkiler, INNER/LEFT JOIN ve tekrarı önleyen tasarım.
Bu dersin sonunda
- Tek tabloda tekrar eden verinin yol açtığı güncelleme, ekleme ve silme sorunlarını gösterip veriyi ilişkili tablolara bölebilirsin
REFERENCESile yabancı anahtar kurupON DELETEdavranışını (CASCADE,RESTRICT,SET NULL) bilerek seçebilirsin- Bire-çok ve çoka-çok ilişkileri ayırt edip çoka-çok için bileşik anahtarlı bir ara tablo tasarlayabilirsin
INNER JOINveLEFT JOINile iki ya da üç tabloyu takma adlarla birleştirip doğru sayıda satır aldığını denetleyebilirsin- JOIN'i
GROUP BYile birleştirip boş grupları da doğru sayan istatistikler çıkarabilirsin - İndeksin ne işe yaradığını anlatıp
EXPLAINçıktısında tablonun baştan sona mı tarandığını, indeksten mi okunduğunu ayırt edebilirsin
Bu sayfada
- Başlamadan: aynı adlı eski tablolar
- Tek tablonun üç derdi
- Veriyi ikiye bölmek: yabancı anahtar
- Bire-çok ilişki
- INNER JOIN: tabloları sorguda birleştirmek
- Takma adlar
- LEFT JOIN: eşi olmayanları da göstermek
- JOIN ile istatistik
- Çoka-çok ilişki ve ara tablo
- ON DELETE: bağlı satır silinince ne olacak?
- Normalizasyon sezgisi
- İndeks: aramayı hızlandırmak
- Sık hatalar
- Görevler
- Kendini sına
- Kayıt noktası
Şimdiye kadar her şeyi tek bir tabloda tuttun. Gerçek uygulamalarda veriler birbirine bağlıdır: her maçın bir oyuncusu, her takımın birkaç şampiyonu vardır. Bu derste tabloları birbirine bağlamayı, bağlı tabloları tek bir sorguda yeniden birleştirmeyi ve bunu yaparken veriyi gereksiz yere tekrar etmeyen bir tasarım kurmayı öğreneceksin. Dersin sonunda beş tablodan oluşan küçük bir lig veritabanın ve onunla istatistik çıkaran sorguların olacak.
Başlamadan: aynı adlı eski tablolar
Bu derste beş yeni tablo kuracaksın: oyuncular, maclar, sampiyonlar, takimlar ve takim_sampiyonlari. Önceki derslerden bu adlarla bir tablo kaldıysa (Ders 34'teki maç tablosu gibi) yeni CREATE TABLE komutun Ders 33 ve 35'ten tanıdığın relation "maclar" already exists hatasını verir. O tablolardaki deneme verisine artık ihtiyacın yok. SQL Editor'de şu satırları çalıştır; tablo varsa siler, yoksa yalnız bir bilgi notu (NOTICE) yazar:
DROP TABLE IF EXISTS takim_sampiyonlari;DROP TABLE IF EXISTS maclar;DROP TABLE IF EXISTS takimlar;DROP TABLE IF EXISTS sampiyonlar;DROP TABLE IF EXISTS oyuncular;DROP TABLE IF EXISTS mac_kayitlari;DROP TABLE IF EXISTS mac_gecmisi;Dersi bir gün baştan yapmak istersen de aynı satırları çalıştırman yeter. Satırların sırası rastgele değil; nedenini dersin sonunda, sık hatalar bölümünde göreceksin. Eski bir tabloyu saklamak istiyorsan silmek yerine Ders 35'teki gibi adını değiştir: ALTER TABLE maclar RENAME TO maclar_ders34;
Bu dersin SQL'i uzun. Ders 34 ve 35'teki gibi, kalıcı tabloları kuran ve veri ekleyen komutları C:\kod\veritabani klasöründe yeni bir dosyada biriktir: ders36.sql. Komutu VS Code'da dosyaya yaz, seçip kopyala, SQL Editor'e yapıştır ve Run'a bas. Dersin sonunda dosyayı Git'e kaydedeceksin. İlk bölümdeki mac_kayitlari yalnız bir deneme; onu doğrudan SQL Editor'e yazman yeter, dosya oyuncular tablosuyla başlayacak.
Tek tablonun üç derdi
Ders 34'te maç geçmişini tek bir tabloda tuttun; oyuncunun adı her maç satırında yeniden yazılıyordu. Bunun neden sorun olduğunu küçük bir tabloyla görelim. Bu kez oyuncunun bölgesini de tutuyoruz:
CREATE TABLE mac_kayitlari ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, oyuncu text NOT NULL, bolge text NOT NULL, sampiyon text NOT NULL, sonuc text NOT NULL); INSERT INTO mac_kayitlari (oyuncu, bolge, sampiyon, sonuc) VALUES ('Deniz', 'TR', 'Ahri', 'zafer'), ('Deniz', 'TR', 'Lux', 'yenilgi'), ('Ece', 'TR', 'Jinx', 'zafer'), ('Deniz', 'TR', 'Ahri', 'zafer');SELECT * FROM mac_kayitlari ORDER BY id;| id | oyuncu | bolge | sampiyon | sonuc |
|---|---|---|---|---|
| 1 | Deniz | TR | Ahri | zafer |
| 2 | Deniz | TR | Lux | yenilgi |
| 3 | Ece | TR | Jinx | zafer |
| 4 | Deniz | TR | Ahri | zafer |
Deniz'in adı ve bölgesi üç satırda tekrar ediyor. Şimdi Deniz hesabını EUW sunucusuna taşıdı. Kaydı düzelten arkadaşın yalnız son maçı güncelledi:
UPDATE mac_kayitlari SET bolge = 'EUW' WHERE id = 4;Deniz hangi bölgede? Sor bakalım:
SELECT oyuncu, bolge, COUNT(*) AS macFROM mac_kayitlariWHERE oyuncu = 'Deniz'GROUP BY oyuncu, bolgeORDER BY bolge;| oyuncu | bolge | mac |
|---|---|---|
| Deniz | EUW | 1 |
| Deniz | TR | 2 |
Veritabanı aynı soruya iki cevap veriyor. Doğrusu WHERE oyuncu = 'Deniz' ile üç satırı birden güncellemekti, ama bunu her seferinde hatırlamak zorundasın ve tek bir unutkanlık veriyi çelişkili bırakır. Sorun güncellemenin kendisi değil: aynı bilginin birden çok yerde durması.
Tek tablonun iki derdi daha var. Birincisi ekleme: hiç maç oynamamış yeni oyuncu Kaan'ı kaydedemezsin, çünkü bu tablo bir maç tablosu ve her satır bir maç ister:
INSERT INTO mac_kayitlari (oyuncu, bolge) VALUES ('Kaan', 'EUW');ERROR: null value in column "sampiyon" of relation "mac_kayitlari" violates not-null constraintDETAIL: Failing row contains (5, Kaan, EUW, null, null).NOT NULL kısıtlarını kaldırsan da kurtulamazsın; bu kez maç gibi görünen ama maç olmayan, içi boş bir satırın olur. İkincisi silme: Ece'nin tek maçını silersen Ece'nin TR'de oynadığı bilgisi de onunla birlikte gider. Oyuncu bilgisi maç bilgisine yapışık olduğu için biri gidince öbürü de gidiyor.
Veriyi ikiye bölmek: yabancı anahtar
Çözüm, her bilgiyi tek bir yerde tutmak. Oyuncunun adı ve bölgesi oyuncular tablosunda bir kez durur. maclar tablosu oyuncunun adını değil, oyuncular tablosundaki id'sini yazar. Deniz taşınırsa tek bir satırı güncellersin; maçlar ona id ile bağlı olduğu için hepsi doğru bölgeyi gösterir.
İki tabloyu ders36.sql dosyana yaz, kopyala ve SQL Editor'de çalıştır:
CREATE TABLE oyuncular ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE, bolge text NOT NULL); CREATE TABLE maclar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, oyuncu_id integer NOT NULL REFERENCES oyuncular (id), sampiyon text NOT NULL, sonuc text NOT NULL CHECK (sonuc IN ('zafer', 'yenilgi')), kill integer NOT NULL, olum integer NOT NULL, asist integer NOT NULL);VS Code .sql dosyasını renklendirir; eksik bir parantez ya da tırnak gözüne hemen çarpar. 9. satır bu dersin yeni parçası.
Yeni olan tek şey 9. satır. Parça parça okuyalım:
oyuncu_id integer: maçı oynayan oyuncunun id'sini tutacak sütun. Tipi, göstereceğioyuncular.idile aynı:integer.NOT NULL: her maçın bir oyuncusu olmak zorunda.REFERENCES oyuncular (id): bu sütuna yazılan her değer,oyunculartablosununidsütununda gerçekten bulunmalı.
İki CREATE TABLE'ın sırası önemli. maclar tablosu oyuncular'ı gösteriyor; gösterilen tablonun önce var olması gerekir. Sırayı ters çevirirsen Postgres relation "oyuncular" does not exist der.
Şimdi oyuncuları ekle. RETURNING (Ders 35) Postgres'in verdiği numaraları gösterecek:
INSERT INTO oyuncular (ad, bolge) VALUES ('Deniz', 'TR'), ('Ece', 'TR'), ('Mert', 'EUW'), ('Selin', 'TR'), ('Kaan', 'EUW')RETURNING id, ad;| id | ad |
|---|---|
| 1 | Deniz |
| 2 | Ece |
| 3 | Mert |
| 4 | Selin |
| 5 | Kaan |
Maçları eklerken oyuncunun adı yerine bu numaraları yazacaksın. Kaan'ın hiç maçı yok; artık bu bir sorun değil, Kaan oyuncular tablosunda kendi başına durabiliyor.
INSERT INTO maclar (oyuncu_id, sampiyon, sonuc, kill, olum, asist)VALUES (1, 'Ahri', 'zafer', 8, 2, 6), (1, 'Lux', 'yenilgi', 3, 5, 10), (2, 'Jinx', 'zafer', 12, 4, 5), (1, 'Ahri', 'zafer', 10, 1, 7), (3, 'Garen', 'yenilgi', 2, 7, 3), (2, 'Lux', 'zafer', 6, 2, 14), (4, 'Teemo', 'yenilgi', 4, 9, 2), (3, 'Yasuo', 'zafer', 9, 6, 4), (2, 'Annie', 'yenilgi', 5, 6, 8), (1, 'Jinx', 'zafer', 7, 3, 9);SELECT id, oyuncu_id, sampiyon, sonuc FROM maclar ORDER BY id;| id | oyuncu_id | sampiyon | sonuc |
|---|---|---|---|
| 1 | 1 | Ahri | zafer |
| 2 | 1 | Lux | yenilgi |
| 3 | 2 | Jinx | zafer |
| 4 | 1 | Ahri | zafer |
| 5 | 3 | Garen | yenilgi |
| 6 | 2 | Lux | zafer |
| 7 | 4 | Teemo | yenilgi |
| 8 | 3 | Yasuo | zafer |
| 9 | 2 | Annie | yenilgi |
| 10 | 1 | Jinx | zafer |
Tablo doğru ama okuması zor: adların yerinde sayılar var. Adları birazdan JOIN ile geri getireceksin. Önce yabancı anahtarın seni nelerden koruduğuna bak.
Hataları Ders 33'teki gibi psql biçimiyle gösteriyoruz: ERROR: asıl mesaj, DETAIL: ayrıntı, HINT: öneri. Neon'da düzen biraz farklı görünebilir; cümleler aynıdır.
insert or update on table "maclar" violates foreign key constraint "maclar_oyuncu_id_fkey"Ne zaman çıkar? Yabancı anahtar sütununa, gösterdiği tabloda olmayan bir değer yazınca. 99 numaralı bir oyuncu yok:
INSERT INTO maclar (oyuncu_id, sampiyon, sonuc, kill, olum, asist)VALUES (99, 'Ahri', 'zafer', 5, 1, 3);ERROR: insert or update on table "maclar" violates foreign key constraint "maclar_oyuncu_id_fkey"DETAIL: Key (oyuncu_id)=(99) is not present in table "oyuncular".Nasıl düzelir? DETAIL satırı neyin nerede bulunamadığını söylüyor: oyuncu_id değeri 99, oyuncular tablosunda yok. Önce oyuncuyu ekle ya da var olan bir id yaz. maclar_oyuncu_id_fkey, Postgres'in bu kısıta verdiği addır; Ders 35'teki dukkan_ad_key gibi tablo, sütun ve kısıtın türünden (fkey, foreign key) oluşur. Var olan bir maçın oyuncu_id'sini UPDATE ile olmayan bir sayıya çevirmeye çalışınca da aynı hatayı alırsın.
update or delete on table "oyuncular" violates foreign key constraint "maclar_oyuncu_id_fkey" on table "maclar"Ne zaman çıkar? Başka bir tablodaki satırların gösterdiği bir satırı silmeye çalışınca. Deniz'in dört maçı var; Deniz silinseydi o maçlar var olmayan bir oyuncuyu gösterirdi.
DELETE FROM oyuncular WHERE ad = 'Deniz';ERROR: update or delete on table "oyuncular" violates foreign key constraint "maclar_oyuncu_id_fkey" on table "maclar"DETAIL: Key (id)=(1) is still referenced from table "maclar".Nasıl düzelir? Postgres seni sahipsiz kalacak maç satırlarından koruyor; bu yabancı anahtarın varsayılan davranışı. Gerçekten silmen gerekiyorsa önce o oyuncunun maçlarını silersin ya da tabloyu kurarken "gösterilen satır silinirse ne olsun" kuralını ON DELETE ile değiştirirsin. Seçenekleri ON DELETE bölümünde göreceksin.
Bire-çok ilişki
Kurduğun bağın bir adı var. Bir oyuncunun çok maçı olabilir; ama bu tablodaki her maç satırı tek bir oyuncuya aittir. Buna bire-çok ilişki denir. Veritabanlarında en sık göreceğin ilişki budur.
oyuncu_id ile tek bir oyuncuyu gösterir. Aynı renk, aynı oyuncu demek. Bir oyuncunun (Deniz) çok maçı olabilir, bir oyuncunun (Kaan) hiç maçı olmayabilir.Kural: yabancı anahtar her zaman çok tarafına konur. maclar tablosunun her satırı tek bir oyuncuyu gösterir, sütun bu yüzden orada durur. Tersini düşün: maçları oyuncular tablosuna yazmaya kalksaydın Deniz'in satırındaki bir hücreye 1, 2, 4, 10 gibi bir liste sığdırman gerekirdi. Bir hücre tek değer tutar; bunun neden önemli olduğunu normalizasyon bölümünde göreceksin.
Oyunlardan başka bire-çok örnekleri: bir klanın çok üyesi olur, her üye tek bir klandadır. Bir maçta çok olay (kill, ejderha, kule) yaşanır, her olay tek bir maça aittir. Bir hesabın çok satın alması olur, her satın alma tek bir hesaba yazılır.
INNER JOIN: tabloları sorguda birleştirmek
maclar tablosu artık adları bilmiyor. "Hangi maçı kim oynadı?" sorusunun cevabı iki tabloya dağıldı: oyuncu_id maçlarda, ad oyuncularda. JOIN, iki tablonun satırlarını bir koşula göre yan yana koyup tek bir sonuç tablosu yapar:
SELECT maclar.id, oyuncular.ad, maclar.sampiyon, maclar.sonucFROM maclarINNER JOIN oyuncular ON maclar.oyuncu_id = oyuncular.idORDER BY maclar.id;| id | ad | sampiyon | sonuc |
|---|---|---|---|
| 1 | Deniz | Ahri | zafer |
| 2 | Deniz | Lux | yenilgi |
| 3 | Ece | Jinx | zafer |
| 4 | Deniz | Ahri | zafer |
| 5 | Mert | Garen | yenilgi |
| 6 | Ece | Lux | zafer |
| 7 | Selin | Teemo | yenilgi |
| 8 | Mert | Yasuo | zafer |
| 9 | Ece | Annie | yenilgi |
| 10 | Deniz | Jinx | zafer |
Sorgunun yeni parçaları:
FROM maclar: başlangıç tablosu.INNER JOIN oyuncular: yanına eklenecek tablo.ON maclar.oyuncu_id = oyuncular.id: eşleşme koşulu. "Maç satırındakioyuncu_id, oyuncu satırındakiid'ye eşitse bu iki satırı birleştir."maclar.id,oyuncular.ad:tablo.sütunyazımı, sütunun hangi tablodan geldiğini söyler. İki tabloda daidvar; tablo adını yazmasan Postgres hangisini istediğini bilemezdi.
JOIN'in ne yaptığını görmek için bir kez yıldızla bak. Ders 34'te *'ı programlarda değil, bir tabloyu tanırken kullanmayı öğrenmiştin; burada da birleşen satırı tanımak için kullanıyoruz:
SELECT *FROM maclarJOIN oyuncular ON maclar.oyuncu_id = oyuncular.idWHERE maclar.id <= 3ORDER BY maclar.id;| id | oyuncu_id | sampiyon | sonuc | kill | olum | asist | id | ad | bolge |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 1 | Ahri | zafer | 8 | 2 | 6 | 1 | Deniz | TR |
| 2 | 1 | Lux | yenilgi | 3 | 5 | 10 | 1 | Deniz | TR |
| 3 | 2 | Jinx | zafer | 12 | 4 | 5 | 2 | Ece | TR |
Her sonuç satırı, bir maç satırıyla bir oyuncu satırının yan yana yapıştırılmış hâli: önce maclar'ın yedi sütunu, sonra oyuncular'ın üç sütunu. Sonuçta iki tane id sütunu var: biri maçın, biri oyuncunun. Bu sorguda yalnız JOIN yazdığına dikkat et: INNER JOIN ile JOIN aynı şeydir. Dersin geri kalanında kısa olanı kullanacağız.
Ne zaman çıkar? ON kısmını yazmayı unutunca. JOIN yazdıysan Postgres seni durdurur:
SELECT maclar.id, oyuncular.adFROM maclarJOIN oyuncular;ERROR: syntax error at or near ";"Asıl tehlike sessiz olanı. JOIN'in eski bir yazımı daha var ve internetteki örneklerde sık görürsün: tablolar FROM'da virgülle sıralanır, eşleşme koşulu WHERE'e yazılır (FROM maclar, oyuncular WHERE maclar.oyuncu_id = oyuncular.id). Bu yazımda WHERE'i unutursan hata almazsın:
SELECT COUNT(*) AS satirFROM maclar, oyuncular;| satir |
|---|
| 50 |
10 maç ve 5 oyuncu var. Postgres her maçı her oyuncuyla eşleştirdi ve 50 satır çıktı: Deniz'in maçı Ece'nin, Mert'in, Kaan'ın maçıymış gibi de göründü. Hata mesajı olmadığı için fark etmesi zordur; tek ipucu satır sayısıdır.
Nasıl düzelir? Her JOIN'in yanına ON koşulunu yaz ve sonucun satır sayısını beklediğinle karşılaştır: maçları listeliyorsan 10'dan fazla satır görmemelisin. ON maclar.oyuncu_id = maclar.oyuncu_id gibi iki tarafı aynı tabloyu gösteren bir yazım hatası da her satırı her satırla eşler ve aynı 50'yi verir.
Ne zaman çıkar? ON'a yabancı anahtar yerine iki tablonun da id'sini yazınca.
SELECT maclar.id, oyuncular.ad, maclar.sampiyonFROM maclarJOIN oyuncular ON maclar.id = oyuncular.idORDER BY maclar.id;| id | ad | sampiyon |
|---|---|---|
| 1 | Deniz | Ahri |
| 2 | Ece | Lux |
| 3 | Mert | Jinx |
| 4 | Selin | Ahri |
| 5 | Kaan | Garen |
Sonuç inandırıcı görünüyor ama yanlış: 3 numaralı maçta Jinx'i Ece oynadı, Mert değil; Kaan hiç maç oynamadı. "Maçın id'si oyuncunun id'sine eşit olsun" dediğin için Postgres 3 numaralı maçı 3 numaralı oyuncuyla eşledi. Üstelik yalnız 5 satır geldi: 6–10 numaralı maçlarla eşleşecek oyuncu yok.
Nasıl düzelir? ON'da yabancı anahtarı, gösterdiği sütunla eşleştir: maclar.oyuncu_id = oyuncular.id. İşine yarayacak bir kural: ON'un bir tarafında hemen her zaman _id ile biten yabancı anahtar sütunu durur.
Takma adlar
Tablo adlarını her sütunun önüne yazmak sorguyu uzatıyor. SQL'de bir tabloya, yalnız o sorgu içinde geçerli kısa bir ad verebilirsin:
SELECT m.id, o.ad AS oyuncu, m.sampiyon, m.killFROM maclar AS mJOIN oyuncular AS o ON m.oyuncu_id = o.idWHERE o.ad = 'Deniz'ORDER BY m.id;| id | oyuncu | sampiyon | kill |
|---|---|---|---|
| 1 | Deniz | Ahri | 8 |
| 2 | Deniz | Lux | 3 |
| 4 | Deniz | Ahri | 10 |
| 10 | Deniz | Jinx | 7 |
maclar AS m, "bu sorguda maclar'a m diyeceğim" demek. Takma adı Ders 34'te sütunlar için kullanmıştın (kill + asist AS katki); aynı fikir tablolar için de geçerli ve yine yalnız o sorgu içinde yaşar.
Tablo takma adlarında AS'yi atlamak çok yaygındır: FROM maclar m. Sütun takma adlarında ise Ders 34'teki gibi AS'yi yazmaya devam et; o.ad AS oyuncu, sonuç tablosunun başlığını ad yerine oyuncu yapar. Tablo takma adını kısa ve tahmin edilebilir seç: genelde tablo adının ilk harfi (m, o) ya da baş harfleri (takim_sampiyonlari için ts).
column reference "id" is ambiguousNe zaman çıkar? İki tabloda da bulunan bir sütunun adını, hangi tablodan geldiğini söylemeden yazınca. id hem maclar'da hem oyuncular'da var:
SELECT id, ad, sampiyonFROM maclarJOIN oyuncular ON maclar.oyuncu_id = oyuncular.id;ERROR: column reference "id" is ambiguousNasıl düzelir? Ambiguous "belirsiz" demek. Sütunun önüne tablo adını ya da takma adını yaz: maclar.id ya da m.id. Yalnız bir tabloda bulunan sütunlarda (sampiyon) zorunlu değil, ama JOIN'li sorgularda her sütunun önüne takma ad yazmak iyi bir alışkanlıktır: okuyan kişi de sütunun nereden geldiğini görür.
invalid reference to FROM-clause entry for table "maclar"Ne zaman çıkar? Bir tabloya takma ad verdikten sonra, aynı sorguda tablonun asıl adını kullanınca.
SELECT m.id, o.adFROM maclar mJOIN oyuncular o ON m.oyuncu_id = o.idWHERE maclar.sonuc = 'zafer';ERROR: invalid reference to FROM-clause entry for table "maclar"HINT: Perhaps you meant to reference the table alias "m".Nasıl düzelir? HINT satırı cevabı veriyor: "takma ad olan m'yi mi kastettin?" Takma ad verdiğin anda o sorgu içinde tablonun adı takma addır. WHERE m.sonuc = 'zafer' yaz.
LEFT JOIN: eşi olmayanları da göstermek
Oyuncuları maçlarıyla listelediğinde Kaan nerede? INNER JOIN yalnız eşi bulunan satırları getirir. Kaan'ın hiç maçı yok; eşleşecek bir maç satırı olmadığı için sonuçtan düştü. Bazen istediğin tam da budur. "Bütün oyuncular ve maçları, maçı olmayanlar da" istiyorsan LEFT JOIN gerekir.
INNER JOIN yalnız eşleşen satırları getirir; LEFT JOIN soldaki tablonun her satırını tutar, eşi yoksa sağ taraftaki sütunlara NULL yazar.SELECT o.ad, m.sampiyon, m.sonucFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idORDER BY o.id, m.id;| ad | sampiyon | sonuc |
|---|---|---|
| Deniz | Ahri | zafer |
| Deniz | Lux | yenilgi |
| Deniz | Ahri | zafer |
| Deniz | Jinx | zafer |
| Ece | Jinx | zafer |
| Ece | Lux | zafer |
| Ece | Annie | yenilgi |
| Mert | Garen | yenilgi |
| Mert | Yasuo | zafer |
| Selin | Teemo | yenilgi |
| Kaan | NULL | NULL |
LEFT JOIN, soldaki tablonun (FROM'dan sonra yazılan, burada oyuncular) her satırını sonuca alır. Sağdaki tabloda (maclar) eşi bulunursa onunla birleştirir; bulunmazsa sağdaki sütunları NULL ile doldurur. Bu yüzden tabloların yeri önemli: FROM oyuncular LEFT JOIN maclar bütün oyuncuları, FROM maclar LEFT JOIN oyuncular bütün maçları tutar. Satır sayısına bak: INNER JOIN 10, LEFT JOIN 11 satır getiriyor; fark Kaan.
NULL'u işine yarayacak biçimde de kullanabilirsin. Hiç maç oynamamış oyuncuları bulmak için LEFT JOIN'den sonra eşi olmayanları seç:
SELECT o.adFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idWHERE m.id IS NULL;| ad |
|---|
| Kaan |
m.id maç tablosunun birincil anahtarı; gerçek bir maç satırında asla NULL olmaz. Sonuçta NULL ise o oyuncunun eşi bulunamamış demektir. = NULL yazarsan hiçbir satır gelmez; Ders 34'teki gibi IS NULL kullan.
WHERE ile farkında olmadan INNER JOIN'e çevirmekNe zaman çıkar? LEFT JOIN'den sonra, sağdaki tablonun bir sütununa WHERE ile koşul koyunca. Amaç "her oyuncunun zaferleri, zaferi olmayanlar da görünsün":
SELECT o.ad, m.sampiyonFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idWHERE m.sonuc = 'zafer'ORDER BY o.id, m.id;| ad | sampiyon |
|---|---|
| Deniz | Ahri |
| Deniz | Ahri |
| Deniz | Jinx |
| Ece | Jinx |
| Ece | Lux |
| Mert | Yasuo |
Selin ve Kaan kayboldu; hata mesajı yok. Sebep sıralamada: önce JOIN çalışır ve Kaan'ın satırı m.sonuc sütunu NULL olarak gelir. Sonra WHERE satırları süzer: NULL ile yapılan karşılaştırma Ders 34'teki gibi doğru çıkmaz, Kaan'ın satırı atılır. Selin'in tek maçı yenilgi olduğu için o da gider. Sonuç, INNER JOIN yazmışsın gibi oldu.
Nasıl düzelir? Sağdaki tabloyla ilgili koşulu WHERE'e değil, ON'a yaz. Böylece koşul "hangi maçlar eşleşsin" kuralının parçası olur ve eşi kalmayan oyuncu yine de tutulur:
SELECT o.ad, m.sampiyonFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.id AND m.sonuc = 'zafer'ORDER BY o.id, m.id;| ad | sampiyon |
|---|---|
| Deniz | Ahri |
| Deniz | Ahri |
| Deniz | Jinx |
| Ece | Jinx |
| Ece | Lux |
| Mert | Yasuo |
| Selin | NULL |
| Kaan | NULL |
JOIN ile istatistik
Asıl güç, JOIN'i Ders 34'teki toplama fonksiyonlarıyla birleştirince ortaya çıkar. Her oyuncunun maç sayısı, toplam ve ortalama kill'i:
SELECT o.ad, COUNT(*) AS mac, SUM(m.kill) AS toplam_kill, ROUND(AVG(m.kill), 1) AS ort_killFROM oyuncular oJOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.adORDER BY mac DESC;| ad | mac | toplam_kill | ort_kill |
|---|---|---|---|
| Deniz | 4 | 28 | 7.0 |
| Ece | 3 | 23 | 7.7 |
| Mert | 2 | 11 | 5.5 |
| Selin | 1 | 4 | 4.0 |
Ders 34'te bir sorgunun yazıldığı sırayla çalıştığı sıranın farklı olduğunu görmüştün. JOIN o sıranın en başına, FROM'un yanına oturur: önce tablolar birleşir, sonra WHERE satırları süzer, sonra GROUP BY grupları kurar, en son SELECT'teki hesaplar yapılır ve ORDER BY sıralar. Yani GROUP BY birleşmiş tablonun üzerinde çalışır; her grup bir oyuncunun maç satırlarıdır.
GROUP BY o.ad burada doğru sonuç veriyor, çünkü ad sütunu UNIQUE: iki oyuncunun adı aynı olamaz, her grup tek bir oyuncudur. Gruplanan sütun ile toplanan sütun farklı tablolarda da olabilir; tek bir tabloyla soramayacağın sorular böyle sorulur. Bölgelere göre maç ve kill sayıları:
SELECT o.bolge, COUNT(*) AS mac, SUM(m.kill) AS toplam_killFROM oyuncular oJOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.bolgeORDER BY o.bolge;| bolge | mac | toplam_kill |
|---|---|---|
| EUW | 2 | 11 |
| TR | 8 | 55 |
bolge oyuncularda, kill maçlarda duruyor; JOIN ikisini aynı satırda buluşturdu, GROUP BY da bölgelere ayırdı.
COUNT(*) maçsız oyuncuya 1 yazıyorNe zaman çıkar? Maçı olmayanları da göstermek için LEFT JOIN yapıp satırları COUNT(*) ile sayınca:
SELECT o.ad, COUNT(*) AS macFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.adORDER BY mac DESC, o.ad;| ad | mac |
|---|---|
| Deniz | 4 |
| Ece | 3 |
| Mert | 2 |
| Kaan | 1 |
| Selin | 1 |
Kaan hiç maç oynamadı ama 1 görünüyor. LEFT JOIN Kaan için bir satır üretti (maç sütunları NULL) ve COUNT(*) satırın içine bakmadan onu saydı.
Nasıl düzelir? Satırları değil, sağdaki tablonun bir sütununu say. Ders 34'ten hatırla: COUNT(sütun) yalnız NULL olmayan değerleri sayar. m.id gibi gerçek maç satırında hiç boş kalmayan bir sütun seç:
SELECT o.ad, COUNT(m.id) AS macFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.adORDER BY mac DESC, o.ad;| ad | mac |
|---|---|
| Deniz | 4 |
| Ece | 3 |
| Mert | 2 |
| Selin | 1 |
| Kaan | 0 |
Adın yanında başka sütunlar: id'ye göre gruplamak
Oyuncunun adının yanında bölgesini de görmek istedin ve o.bolge'yi SELECT'e ekledin. Ders 34'ten tanıdığın hata geliyor:
SELECT o.ad, o.bolge, COUNT(m.id) AS macFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.ad;ERROR: column "o.bolge" must appear in the GROUP BY clause or be used in an aggregate functionPostgres her grubun tek bir satıra inmesini ister; bolge'yi gruplamadığın için "hangi değeri yazayım?" diye soruyor. GROUP BY o.ad, o.bolge yazabilirsin. JOIN'li sorgularda daha iyi bir yol var: birincil anahtara göre gruplamak. GROUP BY o.id yazınca Postgres her grubun tek bir oyuncu satırından geldiğini bilir ve o satırın öbür sütunlarını (ad, bolge) seçmene izin verir:
SELECT o.ad, o.bolge, COUNT(m.id) AS macFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idGROUP BY o.idORDER BY mac DESC, o.ad;| ad | bolge | mac |
|---|---|---|
| Deniz | TR | 4 |
| Ece | TR | 3 |
| Mert | EUW | 2 |
| Selin | TR | 1 |
| Kaan | EUW | 0 |
Id'ye göre gruplamanın bir yararı daha var: adı tekrar edebilen şeylerde (iki oyuncunun adı aynı olabilseydi) iki farklı oyuncu aynı gruba karışmaz. Bu dersin geri kalanında bir tablonun satırlarına göre (her takım, her kategori için) gruplarken birincil anahtarı kullanacağız.
Çoka-çok ilişki ve ara tablo
Sıra takımlarda. Bir takımın kadrosunda birden çok şampiyon var; bir şampiyon da birden çok takımın kadrosunda olabilir: Jinx hem Gece Avcıları'nda hem Demir Duvar'da. Bu ilişkiye çoka-çok denir.
Yabancı anahtarı nereye koyacağız? takimlar tablosuna bir sampiyon_id sütunu koyarsan her takımın tek bir şampiyonu olabilir. sampiyonlar tablosuna bir takim_id koyarsan her şampiyon tek bir takımda olabilir. İkisi de yetmez. Çözüm üçüncü bir tablo: her satırı "şu takımda şu şampiyon var" diyen bir ara tablo.
Önce şampiyonlar:
CREATE TABLE sampiyonlar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE, rol text NOT NULL); INSERT INTO sampiyonlar (ad, rol) VALUES ('Ahri', 'büyücü'), ('Lux', 'büyücü'), ('Garen', 'savaşçı'), ('Jinx', 'nişancı'), ('Yasuo', 'savaşçı'), ('Annie', 'büyücü'), ('Teemo', 'nişancı');Sonra takımlar. Takımı kuran oyuncuyu da tutalım: kurucu_id bir yabancı anahtar daha. Sonundaki ON DELETE SET NULL'u ve ara tablodaki ON DELETE satırlarını bir sonraki bölümde göreceksin; şimdilik olduğu gibi yaz.
CREATE TABLE takimlar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE, kurucu_id integer REFERENCES oyuncular (id) ON DELETE SET NULL); INSERT INTO takimlar (ad, kurucu_id) VALUES ('Gece Avcıları', 2), ('Demir Duvar', 1), ('Yıldız Tozu', 5), ('Boş Kadro', 4);kurucu_id'de NOT NULL yok: kurucusu bilinmeyen bir takım da olabilir. Şimdi ara tablo:
CREATE TABLE takim_sampiyonlari ( takim_id integer NOT NULL REFERENCES takimlar (id) ON DELETE CASCADE, sampiyon_id integer NOT NULL REFERENCES sampiyonlar (id) ON DELETE RESTRICT, PRIMARY KEY (takim_id, sampiyon_id)); INSERT INTO takim_sampiyonlari (takim_id, sampiyon_id) VALUES (1, 1), (1, 2), (1, 4), (1, 6), (2, 3), (2, 5), (2, 4), (3, 1), (3, 2);takim_sampiyonlari tablosunun kendi id'si yok. Her satırını iki sayı tanımlıyor: hangi takım, hangi şampiyon. PRIMARY KEY (takim_id, sampiyon_id) bu ikiliyi birincil anahtar yapar: aynı takım–şampiyon çifti iki kez yazılamaz, ama takım ya da şampiyon tek başına istediği kadar tekrar edebilir. (1, 4) satırı "1 numaralı takımda (Gece Avcıları) 4 numaralı şampiyon (Jinx) var" demek. Boş Kadro'nun (4) hiç satırı yok; Teemo (7) hiçbir takımda değil.
Beş tablonun hepsini birlikte çizelim:
1 ucu birincil anahtarın, N ucu yabancı anahtarın olduğu taraftır. Takımlar ile şampiyonlar arasındaki çoka-çok ilişki, ara tabloda buluşan iki bire-çok ilişkiden oluşur.Bileşik anahtarın işe yaradığını görmek için Jinx'i Gece Avcıları'na ikinci kez eklemeyi dene:
INSERT INTO takim_sampiyonlari (takim_id, sampiyon_id) VALUES (1, 4);ERROR: duplicate key value violates unique constraint "takim_sampiyonlari_pkey"DETAIL: Key (takim_id, sampiyon_id)=(1, 4) already exists.Mesajı Ders 33'ten tanıyorsun; bu kez DETAIL iki sütunu birlikte gösteriyor: (takim_id, sampiyon_id)=(1, 4) çifti zaten var. Ara tabloya bileşik anahtar koymasaydın bu satır sessizce eklenir, Gece Avcıları'nın şampiyon sayısı bir fazla görünürdü.
Ara tablo tek başına okunmaz: (1, 4) satırının anlamını ancak iki JOIN ile görürsün. Takımların kadroları:
SELECT t.ad AS takim, s.ad AS sampiyonFROM takimlar tJOIN takim_sampiyonlari ts ON ts.takim_id = t.idJOIN sampiyonlar s ON s.id = ts.sampiyon_idORDER BY t.ad, s.ad;| takim | sampiyon |
|---|---|
| Demir Duvar | Garen |
| Demir Duvar | Jinx |
| Demir Duvar | Yasuo |
| Gece Avcıları | Ahri |
| Gece Avcıları | Annie |
| Gece Avcıları | Jinx |
| Gece Avcıları | Lux |
| Yıldız Tozu | Ahri |
| Yıldız Tozu | Lux |
Üç tablo, iki JOIN. Her JOIN'in kendi ON'u var: önce takımdan ara tabloya, sonra ara tablodan şampiyona. İki tabloda da ad sütunu olduğu için sonuç sütunlarına takma ad (takim, sampiyon) verdik. Postgres bunu zorunlu tutmaz, ama vermeseydin sonuçta iki tane ad başlığı olurdu ve hangisinin ne olduğu anlaşılmazdı.
Aynı tablolar öbür yönden de okunur. Jinx hangi takımlarda?
SELECT t.ad AS takimFROM sampiyonlar sJOIN takim_sampiyonlari ts ON ts.sampiyon_id = s.idJOIN takimlar t ON t.id = ts.takim_idWHERE s.ad = 'Jinx'ORDER BY t.ad;| takim |
|---|
| Demir Duvar |
| Gece Avcıları |
Takım başına şampiyon sayısı; kadrosu boş takım da görünsün:
SELECT t.ad AS takim, COUNT(ts.sampiyon_id) AS sampiyon_sayisiFROM takimlar tLEFT JOIN takim_sampiyonlari ts ON ts.takim_id = t.idGROUP BY t.idORDER BY sampiyon_sayisi DESC, t.ad;| takim | sampiyon_sayisi |
|---|---|
| Gece Avcıları | 4 |
| Demir Duvar | 3 |
| Yıldız Tozu | 2 |
| Boş Kadro | 0 |
Boş Kadro'nun 0 ile görünmesi için iki şey birlikte gerekti: LEFT JOIN (takımı tutmak için) ve COUNT(ts.sampiyon_id) (NULL'u saymamak için). Şampiyon adına ihtiyaç olmadığı için sampiyonlar tablosuna hiç gitmedik; sayı için ara tablo yetiyor.
ON DELETE: bağlı satır silinince ne olacak?
Yabancı anahtar, gösterdiği satır silinmek istendiğinde ne olacağına da karar verir. Bu kararı tabloyu kurarken ON DELETE ile yazarsın:
| Yazım | Gösterilen satır silinince | Ne zaman seç |
|---|---|---|
hiçbir şey yazmamak (NO ACTION) | Silmeyi reddeder | Varsayılan. Emin değilsen bu. |
RESTRICT | Silmeyi reddeder | Kullanılan bir şeyin silinmesi asla istenmiyorsa |
CASCADE | Bağlı satırları da siler | Bağlı satır tek başına anlamsızsa (kadro satırı) |
SET NULL | Bağlı satırdaki sütunu NULL yapar | İlişki isteğe bağlıysa (takımın kurucusu) |
NO ACTION ile RESTRICT arasındaki asıl fark denetimin zamanlamasındadır; bu kursta ikisi aynı işi görür, yalnız hata mesajları biraz farklıdır. Ara tabloda takim_id için CASCADE, sampiyon_id için RESTRICT seçtik; takimlar.kurucu_id için SET NULL. Üçünü de dene.
update or delete on table "sampiyonlar" violates RESTRICT setting of foreign key constraint "takim_sampiyonlari_sampiyon_id_fkey" on table "takim_sampiyonlari"Ne zaman çıkar? ON DELETE RESTRICT ile korunan bir satırı silmeye çalışınca. Jinx iki kadroda kullanılıyor:
DELETE FROM sampiyonlar WHERE ad = 'Jinx';ERROR: update or delete on table "sampiyonlar" violates RESTRICT setting of foreign key constraint "takim_sampiyonlari_sampiyon_id_fkey" on table "takim_sampiyonlari"DETAIL: Key (id)=(4) is referenced from table "takim_sampiyonlari".Nasıl düzelir? Bu bir arıza değil, kurduğun kural: kullanılan şampiyon silinemez. Gerçekten kaldırman gerekiyorsa önce ara tablodaki satırlarını sil (DELETE FROM takim_sampiyonlari WHERE sampiyon_id = 4;), sonra şampiyonu. Hiçbir kadroda olmayan Teemo'yu silmek hata vermezdi.
Mesajın bu hâli PostgreSQL 18'e aittir; Neon yeni projeleri Haziran 2026'dan beri bu sürümle açıyor. Projen daha eskiyse (PostgreSQL 17 ya da öncesi) RESTRICT de varsayılan davranışın mesajını verir: violates foreign key constraint ... on table ... ve DETAIL satırında is still referenced from table. Anlamı aynı.
Şimdi CASCADE. Demir Duvar dağıldı:
DELETE FROM takimlar WHERE ad = 'Demir Duvar';Hata yok. Kadrolara yeniden bak:
SELECT t.ad AS takim, s.ad AS sampiyonFROM takimlar tJOIN takim_sampiyonlari ts ON ts.takim_id = t.idJOIN sampiyonlar s ON s.id = ts.sampiyon_idORDER BY t.ad, s.ad;| takim | sampiyon |
|---|---|
| Gece Avcıları | Ahri |
| Gece Avcıları | Annie |
| Gece Avcıları | Jinx |
| Gece Avcıları | Lux |
| Yıldız Tozu | Ahri |
| Yıldız Tozu | Lux |
Demir Duvar'ın üç kadro satırı da gitti; sahipsiz satır kalmadı. Garen ve Yasuo ise sampiyonlar tablosunda duruyor: CASCADE yalnız silinen takıma bağlı satırları (ara tablodakileri) siler, şampiyonların kendisine dokunmaz.
Son olarak SET NULL. Kaan, Yıldız Tozu'nu kurmuştu; hiç maç oynamadan hesabını sildi:
DELETE FROM oyuncular WHERE ad = 'Kaan';Takımları kurucularıyla listele. Kurucusu olmayan takım da görünsün diye LEFT JOIN:
SELECT t.ad AS takim, o.ad AS kurucuFROM takimlar tLEFT JOIN oyuncular o ON o.id = t.kurucu_idORDER BY t.id;| takim | kurucu |
|---|---|
| Gece Avcıları | Ece |
| Yıldız Tozu | NULL |
| Boş Kadro | Selin |
Takım duruyor, kurucusu NULL oldu. Kaan'ın maçı olsaydı bu DELETE yine reddedilirdi: maclar.oyuncu_id'nin ON DELETE'i yok, yani orada varsayılan (reddet) geçerli. Her yabancı anahtar kendi kuralını taşır; bir satırı silerken ona bağlı bütün tabloların kuralları birlikte işler. SET NULL'un çalışması için sütunun NULL kabul etmesi gerekir; kurucu_id'yi bu yüzden NOT NULL yapmadık.
Normalizasyon sezgisi
Bu derste yaptığın şeyin bir adı var: normalizasyon. Formülü yok; üç sezgiyle yürür:
- Bir hücreye tek değer.
'Ahri, Lux, Jinx'gibi listeler hücreye yazılmaz; liste bir ara tabloya gider. - Her bilgi tek yerde. Deniz'in bölgesi yalnız
oyunculartablosunda yazar; başka tablolar ona id ile işaret eder. - Her tablo tek bir şeyi anlatır. Oyuncu bilgisi oyuncularda, maç bilgisi maçlarda, kadro bilgisi ara tabloda.
İlk kuralı çiğneyen tasarım çok sık görülür: kadroyu takımın satırına metin olarak sıkıştırmak.
CREATE TABLE takimlar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL, kadro text);| id | ad | kadro |
|---|---|---|
| 1 | Gece Avcıları | Ahri, Lux, Jinx, Annie |
| 2 | Demir Duvar | Garen, Yasuo, Jinx |
Böyle bir tabloda:
- "Jinx hangi takımlarda?" sorusunu ancak
LIKE '%Jinx%'ile sorabilirsin. - Hücrenin içine yabancı anahtar konamaz;
'Jinks'diye yanlış yazılan bir ad da kabul edilir. - Bir şampiyonun adı değişince her takımın metnini tek tek düzeltirsin.
- Aynı şampiyonun bir kadroya iki kez yazılmasını engelleyen bir kısıt yoktur.
sampiyon1, sampiyon2, sampiyon3 diye numaralı sütunlar açmak aynı hatanın başka bir kılığıdır: dördüncü şampiyon için tabloyu değiştirmen, "Jinx hangi takımlarda" için üç sütunu ayrı ayrı sorman gerekir.
Bu dersteki takim_sampiyonlari aynı bilgiyi her satıra tek bir bağlantı yazarak tutar. Yukarıdaki sorunların hiçbiri onda yok: Jinx'in takımları basit bir JOIN'le gelir, yanlış yazılmış bir şampiyonu yabancı anahtar içeri almaz, aynı bağlantıyı bileşik anahtar ikinci kez yazdırmaz.
Ne zaman durmalı? Her şeyi bölmek de hata olur. Kendine şunu sor: "Bu bilgi değişirse kaç satırı düzeltmem gerekir?" Cevap "birden çok" ise o bilgiyi kendi tablosuna taşı. bolge gibi kısa ve neredeyse hiç değişmeyen bir değeri (TR, EUW) ayrı tabloya almak zorunda değilsin; izin verilen değerleri bir CHECK ile sınırlamak çoğu zaman yeter.
Kendi tasarımımıza bu gözle bakınca bir kusur görünüyor: maclar.sampiyon hâlâ metin. "Ahri" iki maç satırında yazılı, oysa sampiyonlar tablosunda Ahri'nin bir id'si var. İkinci kuralı çiğniyor. Bunu Görev 5'te düzelteceksin.
İndeks: aramayı hızlandırmak
Şimdiye kadarki tabloların hepsi birkaç satırdı. Gerçek bir oyunun maç tablosunda milyonlarca satır olur. WHERE mac_kodu = 'TR1_77777' dediğinde Postgres o tek maçı nasıl bulur? Elinde başka bir yol yoksa bütün satırları baştan sona tek tek okur ve her birinde koşula bakar.
İngilizce adı Ders 5'teki dizilerin sıra numarasıyla aynı (index), ama ikisi ayrı şeyler. Dizide index bir elemanın yeridir; veritabanında indeks, aramayı hızlandıran ek bir yapıdır. Hangisinden söz edildiğini cümlenin geri kalanından anlarsın.
Aslında indekslerin zaten var: Postgres her PRIMARY KEY ve her UNIQUE için kendiliğinden bir indeks açar. Görmek için pg_indexes adlı sistem tablosuna sor:
SELECT tablename, indexnameFROM pg_indexesWHERE tablename IN ('oyuncular', 'maclar')ORDER BY tablename, indexname;| tablename | indexname |
|---|---|
| maclar | maclar_pkey |
| oyuncular | oyuncular_ad_key |
| oyuncular | oyuncular_pkey |
oyuncular_pkey birincil anahtar için, oyuncular_ad_key adlardaki UNIQUE için. maclar.oyuncu_id için bir indeks yok: Postgres yabancı anahtar sütununa kendiliğinden indeks açmaz.
Farkı görmek için büyük bir tablo lazım. Aşağıdaki komut 100.000 sahte maçlık bir tablo kurar. Ezberlemen gerekmiyor; kopyalayıp çalıştır:
CREATE TABLE mac_gecmisi ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, mac_kodu text NOT NULL, kill integer NOT NULL); INSERT INTO mac_gecmisi (mac_kodu, kill)SELECT 'TR1_' || sayi, sayi % 21FROM generate_series(1, 100000) AS sayi; ANALYZE mac_gecmisi;Yeni parçalar kısaca: generate_series(1, 100000) 1'den 100.000'e kadar sayı üretir; INSERT ... SELECT her sayı için bir satır ekler. 'TR1_' || sayi metinle sayıyı yan yana yapıştırır (JavaScript'teki + gibi), sayi % 21 0 ile 20 arasında bir kill sayısı verir. ANALYZE ise Postgres'e tabloyu inceletip tahminlerini güncelletir.
||'ye dikkat: JavaScript'te "ya da" demekti, SQL'de ise metin yapıştırır. SQL'de "ya da" için Ders 34'teki gibi OR yazarsın. Tablonun ilk satırları:
SELECT * FROM mac_gecmisi ORDER BY id LIMIT 3;| id | mac_kodu | kill |
|---|---|---|
| 1 | TR1_1 | 1 |
| 2 | TR1_2 | 2 |
| 3 | TR1_3 | 3 |
Şimdi tek bir maçı ara, ama sorgunun başına EXPLAIN yaz:
EXPLAIN SELECT * FROM mac_gecmisi WHERE mac_kodu = 'TR1_77777';Seq Scan on mac_gecmisi (cost=0.00..1886.00 rows=1 width=17) Filter: (mac_kodu = 'TR1_77777'::text)EXPLAIN sorguyu çalıştırmaz; Postgres'in onu nasıl çalıştıracağını, yani sorgu planını gösterir. SQL Editor'de sonuç QUERY PLAN adlı tek sütunlu bir tablo olarak gelir; burada satırlarını düz metin olarak gösteriyoruz. Okuması:
Seq Scan on mac_gecmisi: tabloyu baştan sona tara (sequential scan, sıralı tarama).Filter: her satırda bakılan koşul. 100.000 satırın hepsine bakılacak.cost=0.00..1886.00: tahmini maliyet. Birimi yoktur; yalnız iki planı karşılaştırmak için kullanılır.rows=1: Postgres'in kaç satır bulmayı beklediği.
İndeksi aç ve aynı EXPLAIN'i yeniden çalıştır:
CREATE INDEX mac_gecmisi_mac_kodu_idx ON mac_gecmisi (mac_kodu);EXPLAIN SELECT * FROM mac_gecmisi WHERE mac_kodu = 'TR1_77777';Index Scan using mac_gecmisi_mac_kodu_idx on mac_gecmisi (cost=0.42..8.44 rows=1 width=17) Index Cond: (mac_kodu = 'TR1_77777'::text)Plan değişti. Index Scan using mac_gecmisi_mac_kodu_idx: indekste bak, doğrudan o satıra git. Tahmini maliyet 1886'dan 8,44'e indi; Index Cond da koşulun artık indekste arandığını söylüyor.
İndeksin adını sen verdin: CREATE INDEX ad ON tablo (sütun). Adını yazmazsan Postgres benzer bir ad üretir. Senin çıktındaki sayılar biraz farklı olabilir; bakacağın yer ilk satırın ilk sözcükleri: Seq Scan mı, Index Scan mı?
JOIN'ler ve yabancı anahtar indeksleri
JOIN'in ON koşulunda da arama var: her oyuncu için maclar.oyuncu_id sütununda eşleşen satırlar aranır. Büyük tablolarda yabancı anahtar sütununa indeks açmak bu yüzden iyi bir alışkanlıktır:
CREATE INDEX maclar_oyuncu_id_idx ON maclar (oyuncu_id);Şimdi bu küçük tabloda EXPLAIN'e bak:
EXPLAIN SELECT * FROM maclar WHERE oyuncu_id = 1;Seq Scan on maclar (cost=0.00..1.12 rows=1 width=84) Filter: (oyuncu_id = 1)İndeks var ama Postgres kullanmadı. Burada bir yanlışlık yok: maclar 10 satır ve diskte tek bir sayfaya (Postgres'in diskten bir seferde okuduğu 8 KB'lık parça) sığıyor. Bu kadar küçük bir tabloyu baştan okumak, önce indekse bakıp sonra tabloya gitmekten ucuz.
Postgres her sorguda tahmini maliyetlere bakıp en ucuz yolu seçer; tablo büyüdüğünde aynı sorgu için kendiliğinden indekse geçer. Küçük bir tabloda Seq Scan görmek bu yüzden sorun değildir.
Ne zaman indeks açmalı?
İndeks açmaya değer sütunlar:
- Büyük bir tabloda
WHEREile sık aranan sütunlar (maç kodu, e-posta). - JOIN'lerde kullanılan yabancı anahtar sütunları.
- Büyük bir tabloda sık
ORDER BYyapılan sütunlar.
Bedeli de var:
- Her
INSERT,UPDATEveDELETEindeksleri de günceller; yazma işleri yavaşlar. - İndeks diskte yer kaplar.
LIKE '%777'gibi başında joker olan aramalar sıradan bir indeksi kullanamaz; yine baştan sona tarama yapılır.
Kısacası her sütuna indeks açma. Yavaş bir sorgu gördüğünde önce EXPLAIN'e bak; büyük bir tabloda Seq Scan görüyorsan aradığın sütuna indeks aç ve planı yeniden karşılaştır.
Sık hatalar
Bu derste karşılaşabileceğin öteki hatalar. Bir hata aldığında buraya ya da bütün derslerin hatalarının toplandığı hata rehberine bak.
cannot drop table oyuncular because other objects depend on itNe zaman çıkar? Başka tabloların yabancı anahtarlarla gösterdiği bir tabloyu silmeye çalışınca.
DROP TABLE oyuncular;ERROR: cannot drop table oyuncular because other objects depend on itDETAIL: constraint maclar_oyuncu_id_fkey on table maclar depends on table oyuncularconstraint takimlar_kurucu_id_fkey on table takimlar depends on table oyuncularHINT: Use DROP ... CASCADE to drop the dependent objects too.Nasıl düzelir? DETAIL hangi kısıtların bu tabloya bağlı olduğunu tek tek sayıyor. Tabloları gösterenden gösterilene doğru, yani ters sırayla sil: önce takim_sampiyonlari, maclar ve takimlar, sonra sampiyonlar ve oyuncular. Dersin başındaki DROP satırları bu yüzden o sıradaydı.
HINT'teki DROP TABLE oyuncular CASCADE başka bir şeydir: tabloyu siler ve ona bağlı yabancı anahtar kısıtlarını kaldırır; maclar tablosu yerinde kalır ama artık hiçbir şeye bağlı değildir. Ne yaptığından emin değilsen kullanma.
foreign key constraint "yorumlar_oyuncu_id_fkey" cannot be implementedNe zaman çıkar? Yabancı anahtar sütununun tipi, gösterdiği sütunun tipinden farklıysa.
CREATE TABLE yorumlar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, oyuncu_id text NOT NULL REFERENCES oyuncular (id), metin text NOT NULL);ERROR: foreign key constraint "yorumlar_oyuncu_id_fkey" cannot be implementedDETAIL: Key columns "oyuncu_id" of the referencing table and "id" of the referenced table are of incompatible types: text and integer.Nasıl düzelir? DETAIL iki tipi söylüyor: text ile integer uyuşmuyor. Yabancı anahtar sütununu, gösterdiği sütunla aynı tipte aç: oyuncu_id integer.
there is no unique constraint matching given keys for referenced table "oyuncular"Ne zaman çıkar? REFERENCES ile birincil anahtar ya da UNIQUE olmayan bir sütunu göstermeye çalışınca. bolge sütununda aynı değer birçok satırda var:
CREATE TABLE yorumlar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, bolge text NOT NULL REFERENCES oyuncular (bolge), metin text NOT NULL);ERROR: there is no unique constraint matching given keys for referenced table "oyuncular"Nasıl düzelir? Yabancı anahtar tek bir satırı göstermelidir; 'TR' değeri üç oyuncuyu birden gösterirdi. Neredeyse her zaman doğru hedef, öbür tablonun birincil anahtarıdır: REFERENCES oyuncular (id).
Görevler
Görevleri SQL Editor'de yap ve yazdıklarını ders36.sql dosyana ekle. İlk üç görev kendi tablolarını kurar; Görev 4 ve 5 dersteki tablolarla çalışır. Bir görevi baştan kurmak istersen önce o görevin tablolarını DROP TABLE ile sil (gösteren tablo önce). Beklenen sonuçlar görevin içinde; önce kendin dene, takılırsan ipuçlarını sırayla aç, çözüme en son bak.
Eşya kategorileri
Dükkândaki eşyaları kategorilere ayır. kategoriler (id, ad) ve dukkan_esyalari (id, ad, fiyat, kategori_id) adlı iki tablo kur. Kurallar:
- Her eşya en fazla bir kategoride olsun; olmayan bir kategoriye eşya eklenemesin.
- Bir kategori kaldırılırsa eşyaları dükkândan silinmesin, kategorisiz kalsın. (Bu kuralı Görev 2'nin sonunda deneyeceksin.)
Şu verileri ekle:
- Kategoriler: Saldırı, Büyü, Savunma, Botlar
- Eşyalar: Uzun Kılıç (350, Saldırı), Savaş Baltası (1300, Saldırı), Büyü Kitabı (435, Büyü), Kadim Asa (1250, Büyü), Bez Zırh (300, Savunma), Zincir Yelek (800, Savunma)
Sonra eşyaları kategori adlarıyla listele; önce kategoriye (id sırası), sonra fiyata göre sırala. Beklenen sonuç:
| kategori | esya | fiyat |
|---|---|---|
| Saldırı | Uzun Kılıç | 350 |
| Saldırı | Savaş Baltası | 1300 |
| Büyü | Büyü Kitabı | 435 |
| Büyü | Kadim Asa | 1250 |
| Savunma | Bez Zırh | 300 |
| Savunma | Zincir Yelek | 800 |
Sınır denemesi: 9 numaralı kategoriye bir "Hız Botu" (300 altın) eklemeyi dene. Böyle bir kategori yok; yabancı anahtar hatası almalısın.
İpucu 1
Hangi tablo "çok" tarafı? Bir kategoride çok eşya var, her eşya tek kategoride. Yabancı anahtar eşya tablosuna gider: kategori_id integer REFERENCES kategoriler (id).
İpucu 2
"Kategori silinince eşya kalsın, kategorisi boşalsın" kuralı ON DELETE SET NULL. Bu kural sütuna NULL yazacağı için kategori_id'ye NOT NULL koyma. Önce kategoriler'i kur ve doldur; eşyaları eklerken kategorinin adını değil id'sini yaz: Saldırı 1, Büyü 2, Savunma 3, Botlar 4.
İpucu 3
Liste için FROM dukkan_esyalari e JOIN kategoriler k ON k.id = e.kategori_id ve ORDER BY k.id, e.fiyat. Sütun başlıkları için k.ad AS kategori, e.ad AS esya.
Çözümü göster
CREATE TABLE kategoriler ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE); CREATE TABLE dukkan_esyalari ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE, fiyat integer NOT NULL CHECK (fiyat > 0), kategori_id integer REFERENCES kategoriler (id) ON DELETE SET NULL); INSERT INTO kategoriler (ad) VALUES ('Saldırı'), ('Büyü'), ('Savunma'), ('Botlar'); INSERT INTO dukkan_esyalari (ad, fiyat, kategori_id) VALUES ('Uzun Kılıç', 350, 1), ('Savaş Baltası', 1300, 1), ('Büyü Kitabı', 435, 2), ('Kadim Asa', 1250, 2), ('Bez Zırh', 300, 3), ('Zincir Yelek', 800, 3);SELECT k.ad AS kategori, e.ad AS esya, e.fiyatFROM dukkan_esyalari eJOIN kategoriler k ON k.id = e.kategori_idORDER BY k.id, e.fiyat;| kategori | esya | fiyat |
|---|---|---|
| Saldırı | Uzun Kılıç | 350 |
| Saldırı | Savaş Baltası | 1300 |
| Büyü | Büyü Kitabı | 435 |
| Büyü | Kadim Asa | 1250 |
| Savunma | Bez Zırh | 300 |
| Savunma | Zincir Yelek | 800 |
Sınır denemesi:
INSERT INTO dukkan_esyalari (ad, fiyat, kategori_id)VALUES ('Hız Botu', 300, 9);ERROR: insert or update on table "dukkan_esyalari" violates foreign key constraint "dukkan_esyalari_kategori_id_fkey"DETAIL: Key (kategori_id)=(9) is not present in table "kategoriler".Botlar listede yok: hiç eşyası olmadığı için INNER JOIN'de eşi bulunamadı. Sonraki görevde onu da göstereceksin. kategori_id'de NOT NULL yok, çünkü SET NULL kategorisi silinen eşyaya NULL yazacak. CHECK (fiyat > 0) ve eşya adındaki UNIQUE görevde istenmedi, ama Ders 35'teki kısıtları burada da kullanmak iyi bir alışkanlık.
Kategori raporu
Görev 1'in tablolarıyla her kategori için eşya sayısını ve en pahalı eşyanın fiyatını bul. Botlar da listede olsun ve eşya sayısı 0 görünsün; en pahalı eşyası olmadığı için o sütun NULL kalabilir. Kategorileri id sırasıyla listele:
| kategori | esya_sayisi | en_pahali |
|---|---|---|
| Saldırı | 2 | 1300 |
| Büyü | 2 | 1250 |
| Savunma | 2 | 800 |
| Botlar | 0 | NULL |
Sınır denemesi, Görev 1'deki "kategorisiz kalsın" kuralı için: Savunma kategorisini sil. Eşyaları dükkândan gitmemeli. Bunu görmek için bütün eşyaları kategori adlarıyla listele; kategorisi olmayan eşyalar da görünsün, eşyalar id sırasıyla gelsin:
| esya | kategori |
|---|---|
| Uzun Kılıç | Saldırı |
| Savaş Baltası | Saldırı |
| Büyü Kitabı | Büyü |
| Kadim Asa | Büyü |
| Bez Zırh | NULL |
| Zincir Yelek | NULL |
İpucu 1
Bütün kategorileri tutmak için hangi JOIN gerekiyor? Hangi tablo solda, yani FROM'dan sonra olmalı?
İpucu 2
COUNT(*) boş kategoriye 1 der. Yalnız NULL olmayan değerleri sayan COUNT(e.id) kullan. En pahalı için MAX(e.fiyat).
İpucu 3
GROUP BY k.id yazarsan k.ad'ı da seçebilirsin; sıralama için ORDER BY k.id. Sınır denemesindeki listede ise kategorisi olmayan eşyaları tutmak istiyorsun: bu kez eşya tablosu solda olmalı, FROM dukkan_esyalari e LEFT JOIN kategoriler k ....
Çözümü göster
SELECT k.ad AS kategori, COUNT(e.id) AS esya_sayisi, MAX(e.fiyat) AS en_pahaliFROM kategoriler kLEFT JOIN dukkan_esyalari e ON e.kategori_id = k.idGROUP BY k.idORDER BY k.id;| kategori | esya_sayisi | en_pahali |
|---|---|---|
| Saldırı | 2 | 1300 |
| Büyü | 2 | 1250 |
| Savunma | 2 | 800 |
| Botlar | 0 | NULL |
Sınır kontrolü: COUNT(e.id) yerine COUNT(*) yazsaydın Botlar 1 görünürdü; kendin dene ve farkı gör. MAX'ın NULL vermesi doğru: eşyası olmayan kategorinin en pahalı eşyası da yoktur. NULL yerine 0 görmek istersen COALESCE(MAX(e.fiyat), 0) yazabilirsin, ama "0 altınlık eşya" yanlış bir izlenim verir.
Sınır denemesi:
DELETE FROM kategoriler WHERE ad = 'Savunma';SELECT e.ad AS esya, k.ad AS kategoriFROM dukkan_esyalari eLEFT JOIN kategoriler k ON k.id = e.kategori_idORDER BY e.id;| esya | kategori |
|---|---|
| Uzun Kılıç | Saldırı |
| Savaş Baltası | Saldırı |
| Büyü Kitabı | Büyü |
| Kadim Asa | Büyü |
| Bez Zırh | NULL |
| Zincir Yelek | NULL |
Bez Zırh ve Zincir Yelek dükkânda duruyor; SET NULL onların kategori_id'sini NULL yaptı. Kategorisi olmayan eşyaları tutmak için eşya tablosunu LEFT JOIN'in soluna koydun. Görev 1'deki INNER JOIN listesini şimdi yeniden çalıştırırsan bu iki eşya görünmez: eşleşecek kategorileri yok.
TFT birimleri ve özellikleri
TFT'de bir birimin birden çok özelliği olabilir (Ahri hem Büyücü hem Suikastçı), bir özellik de birden çok birimde bulunur. Üç tablo kur: birimler (id, ad, maliyet), ozellikler (id, ad) ve ara tablo birim_ozellikleri. Kurallar:
- Aynı birim–özellik çifti iki kez eklenemesin.
- Bir birim silinince (oyundan kaldırılınca) özellik bağları da silinsin.
- Bir birimde kullanılan özellik silinemesin.
Veriler:
- Birimler (maliyetiyle): Ahri (4), Lux (3), Garen (1), Jinx (4), Teemo (2)
- Özellikler: Büyücü, Koruyucu, Nişancı, Suikastçı, Ejderha
- Bağlar: Ahri: Büyücü ve Suikastçı. Lux: Büyücü. Garen: Koruyucu. Jinx: Nişancı. Teemo: Nişancı ve Suikastçı.
İki sorgu yaz. (a) Büyücü birimleri, maliyeti yüksekten düşüğe:
| birim | maliyet |
|---|---|
| Ahri | 4 |
| Lux | 3 |
(b) Her özellikte kaç birim var, Ejderha 0 ile, özellikler id sırasıyla:
| ozellik | birim_sayisi |
|---|---|
| Büyücü | 2 |
| Koruyucu | 1 |
| Nişancı | 2 |
| Suikastçı | 2 |
| Ejderha | 0 |
Sınır denemeleri, sırayla:
- Lux–Büyücü bağını ikinci kez ekle: hata vermeli.
- Teemo'yu sil ve (b)'yi yeniden çalıştır: Nişancı ve Suikastçı birer azalıp 1 olmalı.
- Büyücü'yü silmeyi dene: hata vermeli.
- Ejderha'yı sil: hatasız silinmeli.
İpucu 1
Ara tablonun iki sütunu var, ikisi de yabancı anahtar: birim_id ve ozellik_id. Çiftin tekrarını PRIMARY KEY (birim_id, ozellik_id) engeller.
İpucu 2
Birim silinince bağlar da gitsin: birim_id için ON DELETE CASCADE. Kullanılan özellik silinmesin: ozellik_id için ON DELETE RESTRICT. Bağları eklerken id'leri kullan: Ahri 1, Lux 2, Garen 3, Jinx 4, Teemo 5; Büyücü 1, Koruyucu 2, Nişancı 3, Suikastçı 4, Ejderha 5.
İpucu 3
(a) üç tablo ister: ozellikler → birim_ozellikleri → birimler, WHERE o.ad = 'Büyücü'. (b) yalnız iki tablo: ozellikler solda, LEFT JOIN birim_ozellikleri, COUNT(bo.birim_id), GROUP BY o.id.
Çözümü göster
CREATE TABLE birimler ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE, maliyet integer NOT NULL CHECK (maliyet BETWEEN 1 AND 5)); CREATE TABLE ozellikler ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ad text NOT NULL UNIQUE); CREATE TABLE birim_ozellikleri ( birim_id integer NOT NULL REFERENCES birimler (id) ON DELETE CASCADE, ozellik_id integer NOT NULL REFERENCES ozellikler (id) ON DELETE RESTRICT, PRIMARY KEY (birim_id, ozellik_id)); INSERT INTO birimler (ad, maliyet) VALUES ('Ahri', 4), ('Lux', 3), ('Garen', 1), ('Jinx', 4), ('Teemo', 2); INSERT INTO ozellikler (ad) VALUES ('Büyücü'), ('Koruyucu'), ('Nişancı'), ('Suikastçı'), ('Ejderha'); INSERT INTO birim_ozellikleri (birim_id, ozellik_id) VALUES (1, 1), (1, 4), (2, 1), (3, 2), (4, 3), (5, 3), (5, 4);(a):
SELECT b.ad AS birim, b.maliyetFROM ozellikler oJOIN birim_ozellikleri bo ON bo.ozellik_id = o.idJOIN birimler b ON b.id = bo.birim_idWHERE o.ad = 'Büyücü'ORDER BY b.maliyet DESC;| birim | maliyet |
|---|---|
| Ahri | 4 |
| Lux | 3 |
(b):
SELECT o.ad AS ozellik, COUNT(bo.birim_id) AS birim_sayisiFROM ozellikler oLEFT JOIN birim_ozellikleri bo ON bo.ozellik_id = o.idGROUP BY o.idORDER BY o.id;| ozellik | birim_sayisi |
|---|---|
| Büyücü | 2 |
| Koruyucu | 1 |
| Nişancı | 2 |
| Suikastçı | 2 |
| Ejderha | 0 |
Sınır denemeleri:
INSERT INTO birim_ozellikleri (birim_id, ozellik_id)VALUES (2, 1);ERROR: duplicate key value violates unique constraint "birim_ozellikleri_pkey"DETAIL: Key (birim_id, ozellik_id)=(2, 1) already exists.DELETE FROM birimler WHERE ad = 'Teemo';SELECT o.ad AS ozellik, COUNT(bo.birim_id) AS birim_sayisiFROM ozellikler oLEFT JOIN birim_ozellikleri bo ON bo.ozellik_id = o.idGROUP BY o.idORDER BY o.id;| ozellik | birim_sayisi |
|---|---|
| Büyücü | 2 |
| Koruyucu | 1 |
| Nişancı | 1 |
| Suikastçı | 1 |
| Ejderha | 0 |
DELETE FROM ozellikler WHERE ad = 'Büyücü';ERROR: update or delete on table "ozellikler" violates RESTRICT setting of foreign key constraint "birim_ozellikleri_ozellik_id_fkey" on table "birim_ozellikleri"DETAIL: Key (id)=(1) is referenced from table "birim_ozellikleri".DELETE FROM ozellikler WHERE ad = 'Ejderha';Teemo silinince iki bağı da CASCADE ile gitti. Büyücü iki birimde kullanıldığı için RESTRICT silmeyi reddetti; hiçbir birimde olmayan Ejderha sorunsuz silindi.
İndeks yerine UNIQUE
mac_gecmisi tablosunda her maç kodunun zaten benzersiz olması gerekiyor. Dersteki mac_gecmisi_mac_kodu_idx indeksini sil ve EXPLAIN ile planın Seq Scan'e döndüğünü gör. Sonra mac_kodu sütununa bir UNIQUE kısıtı ekle ve EXPLAIN'e yeniden bak: Postgres yine indeks kullanmalı. Aramada 'TR1_4242' kodunu kullan. Beklenen son plan:
Index Scan using mac_gecmisi_mac_kodu_key on mac_gecmisi (cost=0.42..8.44 rows=1 width=17) Index Cond: (mac_kodu = 'TR1_4242'::text)Sınır denemesi: aynı kodu ('TR1_4242') tabloya ikinci kez eklemeyi dene; hata almalısın.
İpucu 1
İndeks silmek tek satır: DROP INDEX indeksin_adi;
İpucu 2
Var olan bir tabloya kısıt eklemek Ders 35'teki ALTER TABLE ile: ALTER TABLE mac_gecmisi ADD UNIQUE (mac_kodu);
İpucu 3
Postgres'in UNIQUE için açtığı indeksin adını görmek için pg_indexes'e WHERE tablename = 'mac_gecmisi' ile sor.
Çözümü göster
DROP INDEX mac_gecmisi_mac_kodu_idx;EXPLAIN SELECT * FROM mac_gecmisi WHERE mac_kodu = 'TR1_4242';Seq Scan on mac_gecmisi (cost=0.00..1886.00 rows=1 width=17) Filter: (mac_kodu = 'TR1_4242'::text)ALTER TABLE mac_gecmisi ADD UNIQUE (mac_kodu);EXPLAIN SELECT * FROM mac_gecmisi WHERE mac_kodu = 'TR1_4242';Index Scan using mac_gecmisi_mac_kodu_key on mac_gecmisi (cost=0.42..8.44 rows=1 width=17) Index Cond: (mac_kodu = 'TR1_4242'::text)Sınır denemesi:
INSERT INTO mac_gecmisi (mac_kodu, kill)VALUES ('TR1_4242', 3);ERROR: duplicate key value violates unique constraint "mac_gecmisi_mac_kodu_key"DETAIL: Key (mac_kodu)=(TR1_4242) already exists.SELECT indexnameFROM pg_indexesWHERE tablename = 'mac_gecmisi'ORDER BY indexname;| indexname |
|---|
| mac_gecmisi_mac_kodu_key |
| mac_gecmisi_pkey |
UNIQUE iki işi birden yaptı: tekrarı engelledi ve arama için bir indeks (mac_gecmisi_mac_kodu_key) açtı. Benzersiz olması gereken bir sütuna ayrıca indeks açmana gerek yoktur; ikisini birden açarsan aynı işi gören iki indeksi boşuna güncellersin.
Maçları şampiyonlara bağla
Normalizasyon bölümündeki kusuru düzelt: maclar.sampiyon sütunu şampiyonun adını hâlâ metin olarak tutuyor. maclar tablosuna sampiyonlar'ı gösteren bir sampiyon_id yabancı anahtar sütunu ekle, her maçı doğru şampiyonun id'siyle doldur, hiçbir maçın boş kalmadığını denetle, sütunu NOT NULL yap ve eski sampiyon sütununu sil. Sonra şampiyon rollerine göre maç sayısını ve ortalama kill'i çıkar:
| rol | mac | ort_kill |
|---|---|---|
| büyücü | 5 | 6.4 |
| nişancı | 3 | 7.7 |
| savaşçı | 2 | 5.5 |
Sınır denemesi: işin bitince Teemo'yu sampiyonlar'dan silmeyi dene. Artık bir maçta kullanıldığı için reddedilmeli.
İpucu 1
Sütun eklemek: ALTER TABLE maclar ADD COLUMN sampiyon_id integer REFERENCES sampiyonlar (id);. Yeni sütun başta her satırda NULL olacağı için NOT NULL'u en sona bırak. Erken eklersen Postgres Ders 35'ten tanıdığın column "sampiyon_id" of relation "maclar" contains null values hatasını verir.
İpucu 2
Şampiyonların id'lerini SELECT id, ad FROM sampiyonlar ORDER BY id; ile öğren. Her şampiyon için bir UPDATE yeter: UPDATE maclar SET sampiyon_id = 1 WHERE sampiyon = 'Ahri';
İpucu 3
Denetim: sampiyon_id IS NULL olan satırları say; 0 çıkmalı. Sonra ALTER TABLE maclar ALTER COLUMN sampiyon_id SET NOT NULL; ve ALTER TABLE maclar DROP COLUMN sampiyon;. Rapor için maclar'ı sampiyonlar ile JOIN'le ve s.rol'e göre grupla.
Çözümü göster
SELECT id, ad FROM sampiyonlar ORDER BY id;| id | ad |
|---|---|
| 1 | Ahri |
| 2 | Lux |
| 3 | Garen |
| 4 | Jinx |
| 5 | Yasuo |
| 6 | Annie |
| 7 | Teemo |
ALTER TABLE maclar ADD COLUMN sampiyon_id integer REFERENCES sampiyonlar (id); UPDATE maclar SET sampiyon_id = 1 WHERE sampiyon = 'Ahri';UPDATE maclar SET sampiyon_id = 2 WHERE sampiyon = 'Lux';UPDATE maclar SET sampiyon_id = 3 WHERE sampiyon = 'Garen';UPDATE maclar SET sampiyon_id = 4 WHERE sampiyon = 'Jinx';UPDATE maclar SET sampiyon_id = 5 WHERE sampiyon = 'Yasuo';UPDATE maclar SET sampiyon_id = 6 WHERE sampiyon = 'Annie';UPDATE maclar SET sampiyon_id = 7 WHERE sampiyon = 'Teemo';SELECT COUNT(*) AS bos_kalanFROM maclarWHERE sampiyon_id IS NULL;| bos_kalan |
|---|
| 0 |
ALTER TABLE maclar ALTER COLUMN sampiyon_id SET NOT NULL;ALTER TABLE maclar DROP COLUMN sampiyon;SELECT s.rol, COUNT(*) AS mac, ROUND(AVG(m.kill), 1) AS ort_killFROM maclar mJOIN sampiyonlar s ON s.id = m.sampiyon_idGROUP BY s.rolORDER BY mac DESC;| rol | mac | ort_kill |
|---|---|---|
| büyücü | 5 | 6.4 |
| nişancı | 3 | 7.7 |
| savaşçı | 2 | 5.5 |
Sınır denemesi:
DELETE FROM sampiyonlar WHERE ad = 'Teemo';ERROR: update or delete on table "sampiyonlar" violates foreign key constraint "maclar_sampiyon_id_fkey" on table "maclar"DETAIL: Key (id)=(7) is still referenced from table "maclar".Denetim sorgusu boşuna değil: maç tablosunda bir şampiyon adı yanlış yazılmış olsaydı ('Jinks') hiçbir UPDATE onu bulamaz, sampiyon_id NULL kalırdı. NOT NULL'u o hâlde eklemeye çalışsaydın Postgres izin vermezdi. Teemo'nun silinememesi de bağın kurulduğunun kanıtı: metin sütunu varken veritabanı Teemo'nun bir maçta kullanıldığını bilmiyordu.
Kendini sına
Soru 1
Dersin başındaki veride 5 oyuncu ve 10 maç vardı; Kaan'ın hiç maçı yoktu. Bu iki sorgu kaçar satır döndürür?
SELECT o.ad, m.sampiyonFROM oyuncular oJOIN maclar m ON m.oyuncu_id = o.id; SELECT o.ad, m.sampiyonFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.id;Cevabı göster
İlki 10, ikincisi 11 satır. JOIN (yani INNER JOIN) her maç için oyuncusunu bulur; eşi olmayan Kaan düşer. LEFT JOIN soldaki her oyuncuyu tutar; Kaan, maç sütunları NULL olan bir satırla gelir.
Soru 2
Bu sorgu neden hata verir? Hangi mesajı görürsün, nasıl düzeltirsin?
SELECT ad, rolFROM takimlar tJOIN takim_sampiyonlari ts ON ts.takim_id = t.idJOIN sampiyonlar s ON s.id = ts.sampiyon_id;Cevabı göster
ERROR: column reference "ad" is ambiguous. ad sütunu hem takimlar'da hem sampiyonlar'da var; Postgres hangisini istediğini bilemiyor. rol yalnız sampiyonlar'da olduğu için sorun çıkarmıyor. Düzeltmesi: SELECT t.ad AS takim, s.ad AS sampiyon, s.rol. Takma ad vermezsen sonuçta iki tane ad başlığı olur.
Soru 3
Bir takımın birden çok oyuncusu olacak ve her oyuncu en fazla bir takımda oynayacak. Takımla oyuncuyu bağlayan takim_id sütununu hangi tabloya koyarsın? Ara tablo gerekir mi?
Cevabı göster
oyuncular tablosuna. Bu bir bire-çok ilişki: bir takımın çok oyuncusu var, her oyuncunun tek takımı. Yabancı anahtar "çok" tarafına, yani oyunculara konur: takim_id integer REFERENCES takimlar (id). Ara tablo gerekmez; ara tablo ancak bir oyuncu aynı anda birden çok takımda oynayabilseydi (çoka-çok) gerekirdi.
Soru 4
Her durum için ON DELETE CASCADE, RESTRICT ve SET NULL'dan birini seç. (a) Bir hesap silinince ona gelen arkadaşlık istekleri. (b) Satın alma kayıtlarında geçen bir eşya silinmek isteniyor. (c) Klanın lideri hesabını siliyor ama klan dağılmamalı.
Cevabı göster
(a) CASCADE: silinen hesaba gelen istekler tek başına anlamsız. (b) RESTRICT: geçmiş satın almalar, silinmiş bir eşyayı gösteremez; eşya önce satıştan kaldırılmalı, kayıtlar korunmalı. (c) SET NULL: klan durur, lider sütunu NULL olur. Bunun için lider sütununda NOT NULL olmamalı.
Soru 5
oyuncular tablosu dersteki gibi, maclar tablosunu ise aşağıdaki gibi kurdun. Tek maçı olan Deniz'i silersen ne olur?
CREATE TABLE maclar ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, oyuncu_id integer NOT NULL REFERENCES oyuncular (id) ON DELETE SET NULL, sampiyon text NOT NULL); DELETE FROM oyuncular WHERE ad = 'Deniz';Cevabı göster
Silme reddedilir: ERROR: null value in column "oyuncu_id" of relation "maclar" violates not-null constraint. SET NULL, Deniz'in maçındaki oyuncu_id'yi NULL yapmaya çalışır; ama sütun NOT NULL. Tablo kurulurken Postgres bu çelişkiyi fark etmez, hata ancak silme anında çıkar. SET NULL seçtiysen sütunun NOT NULL'unu kaldır; maçların oyuncusuz kalması istenmiyorsa başka bir kural seç.
Soru 6
4 takım ve 7 şampiyon varken bu sorgu ne döndürür? Neden?
SELECT COUNT(*) FROM takimlar, sampiyonlar;Cevabı göster
28. Aralarında eşleşme koşulu olmayan iki tabloda Postgres her takımı her şampiyonla eşler: 4 takımın her biri için 7 satır, toplam 28. Hata vermez; JOIN koşulunu unuttuğunda satırların katlanması budur. Satır sayısının beklediğinden büyük çıkması bu hatanın tek ipucudur.
Soru 7
Dersin başındaki veriyle (5 oyuncu, 10 maç) bu sorgu "her oyuncunun Lux maçları, Lux oynamamış oyuncular da görünsün" diye yazıldı. Yalnız iki satır dönüyor. Neden? Nasıl düzeltirsin?
SELECT o.ad, m.killFROM oyuncular oLEFT JOIN maclar m ON m.oyuncu_id = o.idWHERE m.sampiyon = 'Lux';Cevabı göster
WHERE, JOIN'den sonra çalışır. Lux oynamamış oyuncuların satırlarında m.sampiyon ya başka bir şampiyondur ya da (Kaan gibi maçı olmayanlarda) NULL; NULL = 'Lux' doğru çıkmadığı için o satırlar da elenir ve sorgu INNER JOIN gibi davranır. Yalnız Deniz ile Ece'nin Lux maçları kalır. Koşulu ON'a taşı: LEFT JOIN maclar m ON m.oyuncu_id = o.id AND m.sampiyon = 'Lux'. Böylece Deniz ve Ece kill sayılarıyla, Mert, Selin ve Kaan NULL ile gelir: 5 satır.
Soru 8
Hangilerine indeks açarsın? (a) 2 milyon satırlık maç tablosunda, her JOIN'de kullanılan oyuncu_id. (b) Aynı tablodaki, UNIQUE olan mac_kodu. (c) 20 satırlık bolgeler tablosundaki ad. (d) Maç tablosunun bütün sütunları, hız için.
Cevabı göster
Yalnız (a). Yabancı anahtara Postgres kendiliğinden indeks açmaz ve büyük bir tabloda JOIN'ler ondan yararlanır. (b)'nin indeksi zaten var: UNIQUE bir indeks açar. (c) 20 satırlık bir tabloyu baştan okumak zaten ucuz; Postgres indeksi kullanmayabilir bile. (d) yanlış: her indeks yer kaplar ve her INSERT, UPDATE, DELETE'te güncellenir. Önce EXPLAIN ile yavaş sorguyu bul, sonra gereken sütuna indeks aç.
Kayıt noktası
ders36.sql dosyanı Git'e kaydet. Git'i C:\kod\veritabani klasöründe Ders 33'te başlattın; şimdi yalnız yeni bir kayıt alacaksın. VS Code terminalinde, bu klasördeyken:
Bir sonraki derste veritabanını Node.js'ten kullanacaksın: pg paketiyle Neon'a bağlanacak, parametreli sorgular yazacak ve sonuçları API'nden JSON olarak döndüreceksin.