Kısım II · İnşaat · Bölüm 38

Veritabanları ve SQL

Kalıcı veriyi ilişkileriyle düşünmek (Databases and SQL)

Neden burada · Neye dayanıyor

Neden burada: Go sunucusunun belleği süreç kapanınca kayboldu. Şimdi dosyaların üzerine kurulan veri yönetimini, ilişkileri ve SQL'in ne istediğimizi söyleyen dilini öğreniyoruz.

Neye dayanıyor: Bölüm 16: dosya ve kalıcılık; Bölüm 11: veriyi düzenlemek; Bölüm 21.5: bildirimsel yaklaşım. Bölüm 37.5 veri deposu sınırını kurdu.

  1. Kat 10Bizim uygulamamız: Go arka uç, Postgres ve istemciBu bölüm: Veritabanları ve SQL. Bölüm 1'in kat planı üzerindeyiz.
  2. Kat 9 ve 8Web ve ağKullanıcı arayüzünden gelen istekler HTTP ile taşınır.
  3. Kat 7 ve 5İşletim sistemi, diller ve derleyiciProgram süreç olur; derleyici Go metnini çalıştırılabilir dosyaya çevirir.
  4. Kat 0–4 ve 6Fiziksel temel, makine dili ve açılışBellek, depolama ve işlemci alttaki işi sürdürür; burada yeni donanım yok.

Bölüm sonunda kullanıcı, bölüm, soru ve cevap verilerini tablolara yerleştirebilecek; bir SQL sorgusunun neyi seçtiğini ve ilişkilerin hangi kurallarla korunduğunu okuyabileceksin. İndeks, işlem ve gerçek Go–Postgres bağlantısı sonraki bölümde; burada onların üzerinde çalışacağı veri modelini kuruyoruz.

38.1 Veritabanı ve veritabanı yönetim sistemi

Bölüm 37’nin sunucusu kapanınca bellekteki puanlar kaybolur. Bölüm 16.5 dosyayla kalıcılığı hazırlamıştı. Şimdi veriyi yalnız byte dizisi değil, ilişkileri ve kurallarıyla saklayan düzeni kuruyoruz.

Veritabanı (database) düzenlenmiş veriler bütünüdür; veritabanı yönetim sistemi (DBMS, database management system) onu tanımlayan, saklayan, sorgulayan ve değiştiren yazılımdır. PostgreSQL DBMS, hukuk uygulamamızın veritabanı onun yönettiği belirli veridir. Günlük konuşmada ikisine de veritabanı denebilir; burada yazılım ile kaydı ayırıyoruz.12

CSV ve JSON dosyaları da kalıcı veri saklayabilir. DBMS’ye geçmek yalnız uzantı değiştirmek değildir: bağlı kayıtları bulma, olmayan kullanıcıya cevap yazılmasını önleme ve eşzamanlı değişikliği düzenleme sorumluluğunu da taşırız. Bunları dosyalar üzerinde kendin yazarsan veri yönetim yazılımının işini üstlenirsin. CS50’nin düz dosyadan ilişkisel veriye geçişi bu ayrımı görünür kılar.3

Postgres ayrı sunucu programı, ona bağlanan Go API ise istemcidir. Expo veritabanı parolası taşımaz: HTTP ile Go’ya, Go da veritabanı bağlantısıyla Postgres’e ulaşır. Bölüm 34.1’deki “sunucunun başka sunucuya istemci olması” burada somutlaşır.2

Expo / web istemcisi
  │ HTTP + JSON
  ▼
Go API: işleyici → servis → depo
  │ veritabanı bağlantısı üzerinden SQL
  ▼
Postgres: tablo, ilişkiler ve kalıcı kayıtlar
  │ işletim sisteminin dosya ve depolama hizmetleri
  ▼
Disk / kalıcı depolama

HTTP yanıtı ile veritabanına yazmanın tamamlanması ayrı olaylardır; Go bunları koordine eder. Eşzamanlı güncelleme, tamamlanma güvencesi ve yedekleme Bölüm 39’un konusudur. Veritabanı bunları hangi koşullarda sağladığı açıklanmadan “sihirle halleden” kutu değildir.

Bu bölümdeki SQL'in hedefi Postgres'tir. Küçük örneklerin tablo, ilişki, sıralama ve toplama sonuçları ayrıca Python'un sqlite3 modülüyle gerçekten sınandı. SQLite Postgres'in küçük bir sürümü değildir; ayrı DBMS'dir. Ortak sözdiziminden yararlanıyoruz, PostgreSQL sunucusu çalıştırdığımızı iddia etmiyoruz. Kimlikleri örnekte açıkça verdiğimiz için otomatik kimlik üretimi, Postgres'e özgü tipler ve bağlantı ayarları henüz devrede değil.

38.2 İlişkisel model: tablo, satır, sütun ve anahtarlar

İlişkisel model (relational model), verileri adlandırılmış ilişkiler, pratik SQL dünyasında tablolar halinde düzenler. Tablo (table), belirli sütunları paylaşan kayıtlar bütünüdür. Satır (row) tek kayıt; sütun (column) o kayıtta adlandırılmış veri alanıdır. “İlişkisel” yalnız tabloların birbirine bağlanabilmesi demek değildir; kökü verinin ilişki/tablo düzeninde ifade edilmesidir.1

idusername
1Kerem
2Emre
3Ayşe

Yukarıdaki users tablosunda her satır bir kullanıcı, id ve username iki sütundur. Görsel olarak elektronik tabloya benzer; ama SQL tablosunun satır sırası kendiliğinden belirli değildir. “İlk eklenen en üstte durur” sözleşmesi yoktur. Belirli sıra gerekiyorsa sorguda ORDER BY yazacağız. Aynı şekilde satırın fiziksel yeri, uygulamanın güveneceği kullanıcı kimliği değildir.14

Birincil anahtar (primary key) satırı benzersiz, boş olmayan değerlerle tanımlar; bir veya birkaç sütun olabilir. Kullanıcı adı değişebildiği için bağları değişmeyen id ile kurarız. Tam sayı veya id adı zorunlu değildir; eğitim tercihimizdir. Postgres anahtarsız tabloya izin verse de burada her tablonun kimliği açıktır.5

Yabancı anahtar (foreign key), değerin uygun mevcut anahtara karşılık gelmesini zorlar. answers.user_id = 1, adı kopyalamadan kullanıcıya bağlanır. NOT NULL ve REFERENCES birlikte bu bağın mevcut kullanıcıyı göstermesini sağlar. Yalnız sütuna user_id adını vermek güvence değildir.5

İlk şemamız: dört kavram, dört tablo

Şema (schema) burada tablo, alan, tip ve kısıtların tasarımıdır; Postgres’teki ad alanı anlamını henüz yönetmiyoruz. Kullanıcı adı, bölüm başlığı, soru ve cevabı ayrı tutacağız. Bu öğretici şema gerçek üretim projesinden alınmadı.65

CREATE TABLE users (
  id INTEGER PRIMARY KEY NOT NULL,
  username TEXT NOT NULL UNIQUE
);
CREATE TABLE lessons (
  id INTEGER PRIMARY KEY NOT NULL,
  title TEXT NOT NULL
);
CREATE TABLE questions (
  id INTEGER PRIMARY KEY NOT NULL,
  lesson_id INTEGER NOT NULL REFERENCES lessons(id),
  body TEXT NOT NULL,
  correct_choice INTEGER NOT NULL CHECK (correct_choice BETWEEN 1 AND 3)
);
CREATE TABLE answers (
  id INTEGER PRIMARY KEY NOT NULL,
  user_id INTEGER NOT NULL REFERENCES users(id),
  question_id INTEGER NOT NULL REFERENCES questions(id),
  choice INTEGER NOT NULL CHECK (choice BETWEEN 1 AND 3),
  points_awarded INTEGER NOT NULL CHECK (points_awarded >= 0)
);

CREATE TABLE tabloyu; parantez içi sütunları ve kuralları tanımlar; noktalı virgül komutu bitirir. INTEGER tam sayı, TEXT metindir. PRIMARY KEY kimliği seçer; açık NOT NULL, Postgres’te birincil anahtarın zaten sağladığı boş olmama kuralını görünür kılar. UNIQUE kullanıcı adının tekrarını reddeder; öteki metinler otomatik benzersiz olmaz.56

questions.lesson_id bölüm kimliğine, answers.user_id kullanıcıya, answers.question_id soruya bağlanır. CHECK (... BETWEEN 1 AND 3) şıkkı bu eğitim örneğinde izin verdiğimiz aralıkta tutar. CHECK (points_awarded >= 0) eksi ödül kabul etmez. NOT NULL ile CHECK farklı soruları yanıtlar: biri alan boş olabilir mi, diğeri verilen değer koşula uyuyor mu? Postgres'te CHECK'in tek başına null'u reddettiğini sanma; boşluğu ayrıca tanımlıyoruz.5

Yabancı anahtar sorunun varlığını, CHECK şıkkın aralığını denetler; seçilen şıkkın doğru olduğunu bilmez. Eksi olmayan puan da mutlaka hak edilmiş puan değildir. Bu yüzden Go servisindeki karar sürer. Kullanıcının veritabanında mevcut olması, isteği onun gönderdiğini de kanıtlamaz.

Bir bölümün birçok sorusu, bir sorunun ve kullanıcının birçok cevabı olabilir; her cevap bir kullanıcıya ve soruya bağlıdır. Kullanıcı–soru çoktan çoğa bağını cevap olayı üzerinden kurarız. Tekrar denemeyi kabul ettiğimiz için bu çifti benzersiz yapmadık. Tek ödül ayrı tasarlanır; deneme kimliğiyle aynı kural değildir.

38.3 SQL: okumak, eklemek, güncellemek, silmek, birleştirmek

SQL (Structured Query Language), ilişkisel verileri tanımlamak, sorgulamak ve değiştirmek için kullanılan dildir. Bölüm 21.5'teki bildirimsel düşünceyi hatırla: çoğu sorguda “satır satır nasıl gezileceğini” değil, istediğin sonucu söylersin. DBMS uygun yürütme yolunu seçer. Bu, sorgunun her zaman hızlı olacağı anlamına gelmez; nasıl çalıştığını görmenin aracı olan sorgu planını Bölüm 39'da açacağız.34

Sorgu (query) burada veritabanından sonuç istemektir. Bölüm 33’ün TanStack sorgusu istemci kopyasını yönetirken SQL’i DBMS yürütür. Bir HTTP isteği birden çok SQL sorgusu doğurabilir; Go sonuçları API yanıtına dönüştürür.

INSERT: önce üzerinde çalışacağımız veriyi kuralım

Şemadan sonra bu veriyi ayrı deneme veritabanına ekleyelim. Sorular öğretici yer tutuculardır. Önceki Go örneğinin başlangıç puanlarını taşımıyoruz; bu bağımsız kümede yalnız aşağıdaki cevap ödüllerini toplayacağız.

INSERT INTO users (id, username) VALUES
  (1, 'Kerem'), (2, 'Emre'), (3, 'Ayşe');
INSERT INTO lessons (id, title) VALUES (1, 'Hukuka Giriş');
INSERT INTO questions (id, lesson_id, body, correct_choice) VALUES
  (1, 1, 'Örnek soru A', 2),
  (2, 1, 'Örnek soru B', 1);
INSERT INTO answers (id, user_id, question_id, choice, points_awarded) VALUES
  (1, 1, 1, 2, 10),
  (2, 1, 2, 2, 0),
  (3, 2, 1, 2, 10),
  (4, 2, 2, 1, 10);

INSERT INTO hedef tabloyu; sonraki parantez, yazılacak sütunları; VALUES gerçek değerleri verir. Çoklu parantez grupları birden çok satır ekler. Sayılar tırnaksız, metinler SQL'de tek tırnaklıdır. Bu JSON'un çift tırnak kuralı değil; farklı dilin farklı sözdizimidir. Her satırda sütun sırasını açıkça yazmak, tabloya ileride alan eklenince anlamı korumaya yardım eder. Buradaki kimlikler elle verildi; otomatik üretildikleri varsayılmıyor.7

Kerem’in iki cevabı 10 ve 0; Emre’nin iki cevabı 10 ve 10 ödül alır. Ayşe’nin cevabı yoktur. Toplam 10 + 0 + 10 + 10 = 30; kişi toplamları 10, 20, 0’dır. Aritmetik Python, sorgular sqlite3 ile sınandı. Kaydedilen ödül istemcinin beyanı değil, sunucunun doğrulanmış kuralının sonucu olmalıdır.

SELECT ve WHERE: istediğin alanları, istediğin kayıtlardan al

SELECT id, username
FROM users
WHERE id = 1;

SELECT id, username hangi sütunları istediğimizi; FROM users nereden; WHERE id = 1 hangi satırları söyler. Çıktı 1 | Kerem'dir. Buradaki = koşuldaki eşitliktir; JavaScript'teki atama işaretiyle aynı bağlamda değiliz. WHERE, Boole koşulu doğru olan satırları seçer. AND, OR, NOT, Bölüm 3.3'ün mantığını bu katta yeniden kullanır.4

SELECT * bütün sütunları ister; keşifte yararlıdır, kalıcı API’de alanları açık seçmek daha nettir. correct_choice gibi iç alanlar istemciye sızmamalıdır. SQL’in seçtiği alan ile Go’nun JSON’a koyduğu alan iki ayrı sınırdır. WHERE yoksa bütün uygun satırlar istenir; sıra ayrıca ORDER BY gerektirir.4

JOIN: ayrı tutulan bilgileri ihtiyaç anında buluştur

Birleştirme (JOIN), koşula göre farklı tablolardan satır çiftlerini aynı sorgu sonucunda buluşturur. Ayrı tabloları dosyada kalıcı olarak tek tabloya yapıştırmaz. Cevap kaydında yalnız kullanıcı kimliği saklarken ekranda kullanıcı adını görebilmemizin yolu budur. Yabancı anahtar ve JOIN aynı şey de değildir: ilki veri kısıtı, ikincisi sonuç üretme işlemidir; JOIN yazmak yabancı anahtar kurmaz.8

SELECT u.username, q.body, a.choice, a.points_awarded
FROM answers AS a
JOIN users AS u ON u.id = a.user_id
JOIN questions AS q ON q.id = a.question_id
WHERE u.id = 1
ORDER BY a.id;

answers AS a, tabloya bu sorguda takma ad (alias) verir; a.choice cevabın şıkkıdır. İlk JOIN, cevabın kullanıcı kimliğine uygun kullanıcıyı; ikinci JOIN, soru kimliğine uygun soruyu getirir. WHERE u.id = 1 Kerem'in kayıtlarını bırakır. ORDER BY a.id denemeleri kimliğe göre sıralar. Seçilen alanlar, SQL satırları ve JSON API alanları arasında açık bir dönüştürme gerekeceğini de gösterir.

Kerem | Örnek soru A | 2 | 10
Kerem | Örnek soru B | 2 |  0

İki cevap varsa JOIN’de kullanıcı adı iki kez görünür; kullanıcı tablosunda kopya olduğu anlamına gelmez. Sonuç satırının birimi cevaptır. Yanlış eşleşme koşulu sayımı ve toplamı şişirebilir; körlemesine DISTINCT eklemeden önce her satırın neyi temsil ettiğini sor.

GROUP BY: olay satırlarından kişi toplamına

Gruplama (GROUP BY), seçilmiş satırları aynı anahtar değerlerini paylaşan kümelere ayırır. Toplulaştırma fonksiyonu (aggregate function), bu kümedeki birçok girdiden tek sonuç hesaplar: COUNT sayar, SUM toplar; AVG ortalama, MIN ve MAX sınır değerleri verir. Uygulamamızda bir cevap satırından bir kullanıcı toplamına geçmek için bu düşünceyi kullanırız.9

SELECT u.id AS user_id, u.username,
       COUNT(a.id) AS answer_count,
       COALESCE(SUM(a.points_awarded), 0) AS points
FROM users AS u
LEFT JOIN answers AS a ON a.user_id = u.id
GROUP BY u.id, u.username
ORDER BY points DESC, u.id ASC;

FROM users ile başlamak “cevabı olmasa da kullanıcıyı görmek istiyorum” niyetidir. LEFT JOIN bu yüzden vardır: sağda eşleşen cevap yoksa soldaki kullanıcıyı yine sonuçta tutar, sağ alanlar NULL (null) olur. NULL sıfır değildir; burada eşleşen değerin bulunmamasını temsil eder. COUNT(a.id) NULL olmayan cevap kimliklerini sayar; Ayşe için 0 verir. COUNT(*) yazsaydık dış birleştirmenin bıraktığı satırı da sayıp cevapsız kişide 1 görürdük. İkisinin farkını sonucun anlamına göre seçmelisin.814

SUM(a.points_awarded) kişinin ödüllerini toplar; hiç girdi yoksa sonuç NULL olabilir. COALESCE(..., 0), NULL yerine burada istediğimiz sıfırı koyar. GROUP BY u.id, u.username, kişi başına sonuç üretir. ORDER BY points DESC, u.id ASC, önce toplamı büyükten küçüğe, eşitlikte kimliği küçükten büyüğe sıralar. Buradaki points, SELECT'te AS points ile verdiğimiz sonuç takma adıdır. Kullanıcı adı aynı görünse bile kimliğe göre gruplama, iki kişiyi yanlışlıkla birleştirmemeyi sağlar.1415

user_id | username | answer_count | points
2       | Emre     | 2            | 20
1       | Kerem    | 2            | 10
3       | Ayşe     | 0            |  0

Bu sorgu GET /leaderboard için veri sağlayabilir. SQL’deki username alanını API’deki name alanına çevirmek Go’nun işidir. Eşit puanda kimlik sırası eğitim tercihimizdir. Sonuna LIMIT 10 eklemek ilk on sonucu ister; anlamlı sıra için önce ORDER BY gerekir.4

WHERE ile HAVING: filtreleme zamanı farklı

SELECT user_id, COUNT(*) AS answer_count
FROM answers
WHERE points_awarded > 0
GROUP BY user_id
HAVING COUNT(*) >= 2
ORDER BY user_id;

WHERE points_awarded > 0 önce ödül alan cevap satırlarını bırakır. GROUP BY user_id bunları kullanıcı bazında toplar. HAVING COUNT(*) >= 2 ise grup hesabından sonra en az iki ödüllü cevabı olanları bırakır. Sonuç 2 | 2, yani Emre ve iki ödüllü cevaptır. Sıralama sonucu kimliğe göre verir. WHERE'e doğrudan COUNT koşulu yazamamanın sezgisi budur: satır seçilirken henüz grubun toplamını kullanmaya çalışıyorsun. DBMS'nin fiziksel yürütme planıyla bu mantıksal açıklamayı aynı şey sayma.9

UPDATE: kaydı değiştir, bütün tabloyu değil

UPDATE users
SET username = 'Kerem_O'
WHERE id = 1;
SELECT id, username FROM users WHERE id = 1;

UPDATE users hedefi seçer; SET username = 'Kerem_O' yeni alan değerini bildirir; WHERE id = 1 hangi kaydın değişeceğini sınırlar. Sonraki SELECT artık 1 | Kerem_O verir. Cevaplardaki user_id hâlâ 1'dir; kullanıcı adını her cevapta saklamadığımız için onları tek tek düzeltmedik. WHERE'i unutmak, uygun satırların tümünü güncellemeyi istemek anlamına gelir; araç senin niyetini tahmin etmez. Bu örnekte UNIQUE kısıtı ayrıca bazı toplu değişiklikleri reddedebilir, ama ona güvenip koşulu atlamayız.10

DELETE: satır silmek ile tabloyu kaldırmak farklı

DELETE FROM answers
WHERE id = 2;
SELECT COUNT(*) AS answer_count FROM answers WHERE user_id = 1;

DELETE FROM answers cevap satırlarını hedefler; WHERE yalnız 2 numaralı denemeyi seçer. İkinci satırdaki sorgu Kerem'in kalan cevap sayısını 1 bulur. Sildiğimiz kayıt sıfır ödüllüydü; bu örnekte toplam ödül değişmez. DELETE tablo tanımını kaldırmaz, satırları kaldırır. Koşulsuz DELETE bütün satırları hedefler ve DBMS mutlaka “emin misin?” diye sormaz. Bunlar yalnız deneme verisinde çalıştırılacak komutlardır; gerçek kullanıcı verisi üzerinde alıştırma yapılmaz.11

Bir kullanıcıyı silmek istediğinde ona bağlı cevaplar varsa yabancı anahtarın davranışı önemlidir: silmeyi reddetmek, bağlı kayıtları silmek veya başka politika seçmek farklı kararlardır. Bu şemada açık bir zincirleme silme politikası tanımlamadık; kısıtlar mevcut bağları korur. “Kullanıcıyı sildim, bütün ilişkileri sistem doğru biçimde halleder” diye varsayma. Saklama ve ürün kararlarının kesiştiği yeri görmek, tek komut öğrenmekten daha önemlidir.5

38.4 Normalizasyon ve şema tasarımı: kullanıcılar, bölümler, sorular, cevaplar

Normalizasyon (normalization), veriyi bağımlılıklarına göre düzenleyip gereksiz tekrar ve tutarsız güncelleme riskini azaltma yaklaşımıdır. “Her şeyi olabildiğince çok tabloya böl” demek değildir. Sorulacak soru şudur: bu bilgi hangi varlık veya olaya ait; aynı gerçeği kaç yerde değiştiriyorum? Kullanıcının güncel adı kullanıcıya, bölüm başlığı bölüme, cevabın verildiği anda kazanılan ödül ise o cevap olayına aittir.12

Kötü başlangıçNe bozulur?Bu bölümdeki ayrım
Her cevapta kullanıcı adını yeniden yazmakAd değişince eski kopyalar farklı kalabilirusers tablosu + user_id
question1, question2, question3 sütunlarıYeni soru eklemek tablo yapısı değişikliği isterquestions içinde her soru bir satır
“1,2,7” gibi metinde cevap kimlikleriTek tek anahtar kısıtı ve birleştirme zorlaşıranswers içinde her deneme bir satır
Her soruda bölüm başlığını tekrarlamakBaşlık değişince birçok kayıt düzeltilirlessons + lesson_id

Adı tek yerde değiştirince sonraki JOIN yeni adı gösterir. Her cevapta kopya tutsaydık bazıları eski kalabilirdi: güncelleme anomalisi (update anomaly) budur. Ekleme/silme de risk taşır: bölüm bilgisi yalnız cevapta dursa henüz cevapsız bölümü nereye yazacaksın; son cevap silinince bölüm de mi kaybolacak? Ayrı tablolar farklı yaşamları ayırır.12

Normal biçimlerin sezgisi: tekrar eden sütun gruplarını satırlara ayır; birleşik anahtarda alanın anahtarın tamamına bağlı olup olmadığını sor; başka varlığa ait güncel bilgiyi kopyalama. Matematiksel ispat yapmıyoruz. Birincil anahtar eklemek tek başına normalizasyon değildir; öteki alanların bağımlılıkları ayrıca incelenir.

Her tekrar yanlış değildir: verilen ödül geçmiş olayı anlatır, güncel soru ödülünden farklıdır. Kural değişse de eskiden gerçekten verilmiş ödül korunabilir. Bu, bilerek tutulan anlık görüntü (snapshot) tasarımımızdır; yanlışlıkla çoğalan güncel kopyayla aynı sorun değildir.

Peki toplam puan? Bu bölümde SUM(points_awarded) ile kayıtlardan türettik; böylece ayrıca güncellenecek ikinci bir toplam tutmadık. Gerçek ürün, daha hızlı okuma için kullanıcı tablosunda total_xp gibi bir toplam saklayabilir. O zaman cevap kaydıyla toplamı birlikte doğru tutmak yeni sorumluluk olur. Bölüm 39.3 işlem ve eşzamanlı yazmayı, Bölüm 40 türetilmiş önbelleği anlatır. Önce doğru kaynağı bil, sonra gerekçeyle kopya ekle.

Şema yalnız tablo listesi değildir. Tekrar deneme ve ikinci ödül izinleri; soru değişince eski cevabın anlamı; serinin zaman dilimi ürün kararıdır. Bu dört tablo hepsini çözmez. Kullanıcı–soru çiftini benzersiz yapmamak tekrar denemeye izin verir, ama tek ödülü kalıcı biçimde sağlamaz. Go’nun bellek kilidi veritabanı güvencesi değildir.

Uygulamamızda: şema dosyası bir ürün kararıdır

Prototipinin vanilla HTML/CSS/JS verilerini görmedik. Buradaki tablo adları öğretici örnektir, mevcut projeye ilişkin tespit değildir. Expo + TypeScript kullanıcı arayüzünü; TanStack Query sonuçların istemci kopyasını; Go kuralları ve veri erişimini; Postgres asıl kayıtları yönetir. Agent “answers tablosuna UNIQUE(user_id, question_id) ekledim” derse yalnız SQL'in geçerli olup olmadığını değil, “tekrar denemeyi bilerek mi yasakladık?” sorusunu da sor. Bir kısıt sözdizimsel ayrıntı değil, kalıcı davranış kararıdır.

Bu şema parola, seri, can, zaman damgası ve izinleri tamamlamaz; kavram sayısını sınırlıyoruz. Bölüm 39’un total_xp ve zaman damgalı örnekleri ayrı çalışma şemalarıdır; buraya değişmeden yapıştırılacak göç dosyaları değildir. Şema göçünü orada öğreneceksin.

38.5 İlişkisel olmayan veritabanları: ne zaman hangisi?

İlişkisel olmayan veritabanı (NoSQL database) tek ürün değil, belge ve anahtar–değer gibi farklı modelleri kapsayan başlıktır. Sorgusuz veya kuralsız demek değildir; esneklik uygulamanın veri beklentilerini yok etmez. Bu beklentinin nerede korunacağını yine tasarlarız.

Belge veritabanı (document database) örneği MongoDB, aynı koleksiyondaki belgelerde farklı alanlara izin verir; iç içe belgeler ve dizilerle birlikte erişilen veriyi birlikte tutmayı hedefleyebilir. İlişkiler, belge içine gömme veya başka belgeye başvurma ile modellenebilir. MongoDB'nin kendi belgeleri ayrıca şema doğrulamasıyla sabit yapının zorlanabildiğini söyler. Dolayısıyla “şema yok, düşünmeye gerek yok” sonucunu buradan çıkaramayız.13

Anahtar–değer (key–value) düzeninde ise temel erişim bir anahtar ve ona bağlı değerdir; Bölüm 40'ta Redis'i bu ailede göreceğiz. Bu bölümde Redis'i Postgres'in yerine yerleştirmiyoruz. Bizim kullanıcı–bölüm–soru–cevap ilişkilerimiz, anahtar kısıtları ve farklı toplama sorularımız ilişkisel başlangıcı anlaşılır kılıyor. Bu, bütün belgeli ürünlerin veya anahtar–değer sistemlerinin aynı yetenek ve bedellere sahip olduğu iddiası değildir.

Önce sorİlişkisel başlangıç neden uygun olabilir?Belge modeli neden düşünülebilir?
Veriler birbirine nasıl bağlanıyor?Birçok tür arasında açık anahtar bağları varBirlikte okunan içerik doğal bir belge bütünüdür
Hangi sorguları soruyoruz?Farklı tablolardan JOIN ve toplama gerekiyorBelgeyi bir bütün olarak okumak baskın olabilir
Biçim ne kadar değişiyor?Belirli alan ve kısıtlar önemliİçerik türleri farklı alanlar taşıyabilir
Kim hangi kuralı koruyacak?DBMS kısıtlarını açıkça kullanırızBelge doğrulaması ve erişim modeli ayrıca tasarlanır

Bu tablo tasarım rehberidir, performans ölçümü değildir. “NoSQL” etiketi tek başına işlem, tutarlılık, dizin veya dağıtım özelliklerini açıklamaz. Postgres de JSONB gibi belge benzeri alanlar sunar; Bölüm 39.1 açacak. Buradaki temel kayıt tercihi Postgres’tir; başka sistemler gerekçeyle eklenir.

Bölüm 11 ve 16 ile bağlantı

Bölüm 11’in bellek yapıları ile Bölüm 16’nın dosyaları burada birleşir: satırı kimlikle bulur, kuralla bağlar, sorguyla isteriz. Bölüm 39’un dizinleri bu aramanın depolama ölçeğindeki devamıdır.

Bu bölümde neler kuruldu

DBMS yalnız dosya uzantısı değil, veri yönetimi sorumluluğunu üstlenen yazılımdır. İlişkisel modelde tablo, satır ve sütun; anahtarlarda kimlik ve ilişki vardır. SQL ile veri ister, ekler, değiştirir, sileriz; JOIN ilişkili bilgiyi buluşturur, GROUP BY toplar. Normalizasyon bilginin neye ait olduğunu düşünür; her tekrarı yasaklamaz. Bizim başlangıcımız Postgres'tir; NoSQL etiketi tek başına hız veya kuralsızlık açıklaması değildir.

Terimler sözlükte, Bölüm 38 başlığı altında.

Birincil okuma

İlk okuma: kaynak 1, PostgreSQL Tutorial 2.2 Concepts; ardından aynı öğreticinin kaynak 4'teki 2.5 Querying a Table bölümü. Derinleşme: kaynak 8, 2.6 Joins Between Tables; kaynak 3, CS50 Hafta 7 Relational Databases ve JOINs.

Takıldığın yeri yapay zekâ öğretmenine sor. Alıştırma: “Ayşe'nin cevabı yokken LEFT JOIN ve COUNT(*) neden yanıltıcı olabilir?” Önce satırları kâğıtta çiz, sonra COUNT(a.id) ile farkı anlat.

Bölüm sonu soruları

Kitaba bakmadan cevapla. Geri bildirim yalnız sonucu değil, gerekçeyi de gösterir.

1. Veritabanı ile DBMS ayrımı hangisidir?
Veritabanı düzenlenmiş veridir; DBMS bu veriyi yöneten sistemdir. Bkz. 38.1.
2. Birincil anahtarın görevi nedir?
Birincil anahtar benzersiz ve boş olmayan kimliktir; sıra veya kopyalama değildir. Bkz. 38.2.
3. Yabancı anahtar neyi denetler?
REFERENCES bağı korur; cevap doğruluğunu veya kullanıcı kimliğini kanıtlamaz. Bkz. 38.2.
4. WHERE ile HAVING arasındaki fark nedir?
WHERE grup hesabından önce girdi satırlarını, HAVING sonrasında grupları seçer. Bkz. 38.3.
5. LEFT JOIN sonrasında cevapsız kişide COUNT(a.id) neden sıfırdır?
COUNT(a.id) yalnız NULL olmayan cevap kimliklerini sayar; soldaki kullanıcı kalır. Bkz. 38.3.
6. ORDER BY olmadan hangi beklenti yanlıştır?
SQL tablosu ve sırasız sorgu belirli satır sırası garantilemez. Bkz. 38.3.
7. Normalizasyonun temel amacı hangisidir?
Amaç bilgiyi bağımlılıklarına göre düzenlemek ve tutarsız kopyaları azaltmaktır. Bkz. 38.4.
8. Esnek belge modeli ne anlama gelmez?
Esneklik kuralsızlık değildir; doğrulama ve erişim modeli yine düşünülür. Bkz. 38.5.
9. Bir kullanıcıya üç cevap bağladığında JOIN sonucunda ad neden üç kez görünür? Bu durumla kullanıcı tablosundaki kopyayı nasıl ayırırsın?
Örnek cevabı göster

JOIN sonucunun birimi burada cevap kaydıdır. Tek kullanıcı satırı üç ayrı cevapla eşleştiği için adı üç sonuçta görünür. Kullanıcı kimliği benzersiz kalır; sonucu DISTINCT ile körlemesine temizlemek cevapları kaybettirebilir. Bkz. 38.3.

10. Kullanıcı–soru çiftini answers tablosunda UNIQUE yapmak neden ürün kararıdır? Tek ödül ile tekrar deneme aynı kural mıdır?
Örnek cevabı göster

UNIQUE çift, aynı kullanıcının aynı soruya ikinci denemesini de engeller. Tek ödül ise denemeyi kabul edip yeniden ödül vermemeyi isteyebilir; deneme ve ödül kaydı ayrı modellenmelidir. Bkz. 38.4.

Kaynaklar

  1. PostgreSQL Tutorial · 2.2 Concepts: ilişkisel model, satır, sütun ve sırasız tablo.
  2. PostgreSQL Tutorial · 1.2 Architectural Fundamentals: Go istemcisi ve ayrı Postgres sunucusu fikri.
  3. CS50x · Lecture 7 ders notları: Flat-File Database; Relational Databases; SELECT; JOINs: düz dosya, SQL ve toplama/sayma sezgisi.
  4. PostgreSQL Tutorial · 2.5 Querying a Table: SELECT, WHERE, açık sütun listesi ve sıralama.
  5. PostgreSQL · Constraints: Check, Not-Null, Unique, Primary Keys, Foreign Keys: kısıtların sağladığı ve sağlamadığı güvenceler.
  6. PostgreSQL Tutorial · 2.3 Creating a New Table: CREATE TABLE, tipler ve SQL yazımı.
  7. PostgreSQL Tutorial · 2.4 Populating a Table With Rows: INSERT ve açık sütun adları.
  8. PostgreSQL Tutorial · 2.6 Joins Between Tables: eşleşme koşulları, takma adlar ve dış birleştirme.
  9. PostgreSQL Tutorial · 2.7 Aggregate Functions: GROUP BY, toplulaştırma, WHERE ve HAVING.
  10. PostgreSQL Tutorial · 2.8 Updates: UPDATE ve koşulla sınırlandırma.
  11. PostgreSQL Tutorial · 2.9 Deletions: DELETE ve koşulsuz silme riski.
  12. Microsoft · Database normalization description: Description of normalization; First/Second/Third normal form: gereksiz tekrar ve bağımlılıklar; ileri normal biçimlere ilişkin etiketleri bu bölümde kullanmıyoruz.
  13. MongoDB · Data Modeling in MongoDB: esnek belgeler, şema doğrulaması ve ilişki modelleme.
  14. PostgreSQL · Aggregate Functions: count, sum ve boş girdi notu: COUNT(*) / COUNT(alan) ayrımı ve toplamanın NULL sonucu.
  15. PostgreSQL · Conditional Expressions: 9.18.2 COALESCE: ilk NULL olmayan değerle varsayılan sonuç.