İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 5 · Ders 33

Veritabanı nedir? PostgreSQL'e giriş

Tablo, satır, sütun, veri tipleri ve birincil anahtar; ücretsiz bir PostgreSQL veritabanı açıp ilk tabloyu oluşturuyorsun.

Bu dersin sonunda

  • Bir JSON dosyasını çok kullanıcılı bir API'de veri deposu yapmanın neden tehlikeli olduğunu çalışan bir örnekle gösterebilirsin
  • Tablo, satır, sütun ve veri tipi kavramlarını bir Excel sayfasıyla karşılaştırarak anlatabilirsin
  • Neon'da ücretsiz bir PostgreSQL veritabanı açıp SQL Editor'de sorgu çalıştırabilirsin
  • CREATE TABLE ile doğru veri tipleri ve otomatik artan birincil anahtarı olan bir tablo kurabilirsin
  • INSERT ile tek ya da çok satır ekleyip SELECT * ile tabloya bakabilirsin
  • Tırnak, virgül, tip ve anahtar hatalarını PostgreSQL'in mesajını okuyarak düzeltebilirsin
Bu sayfada
  1. Dosya neden yetmiyor?
  2. Veritabanı: veriyi yöneten program
  3. Tablo, satır, sütun
  4. PostgreSQL ve SQL
  5. Neon'da ücretsiz bir Postgres aç
  6. İlk sorgu
  7. Veri tipleri
  8. İlk tablo: CREATE TABLE
  9. INSERT: satır eklemek
  10. Birincil anahtar
  11. Şemanı dosyaya kaydet
  12. Sık hatalar
  13. Görevler
  14. Kendini sına
  15. Kayıt noktası

Blok 4'te API'lerinin verisini bir JSON dosyasında tuttun ve Ders 29'da bu dosyanın zayıf yerlerini kendi gözünle gördün. Bu derste o sorunlara adlarını koyacak, sonra verini gerçek bir veritabanına taşımaya başlayacaksın: bulutta ücretsiz bir PostgreSQL veritabanı açacak, ilk tablonu kuracak ve içine satırlar ekleyeceksin. Bunun için yeni bir dil öğreniyorsun: SQL. Kuralları JavaScript'inkinden azdır ve yazdığın her komutun sonucunu hemen bir tablo olarak görürsün.

Dosya neden yetmiyor?

Ders 32'deki takım API'si verisini bir JSON dosyasında tutuyordu: bir değişiklik geldiğinde dosya okunur, değiştirilir ve geri yazılır. Sen tek başına denerken bu sorunsuz çalışır. Ama API internette; aynı anda onlarca istek gelebilir.

Node bir isteğin dosya okumasını beklerken (await) boş durmaz, öteki isteğe geçer (Ders 15'teki olay döngüsü). Ders 29'da kalabalik.js ile on isteği aynı anda gönderdiğinde ne olduğunu gördün: "eklendi" cevabı alan şampiyonların çoğu listede yoktu.

Sorunu en küçük hâline indirelim: sunucu yok, istek yok; yalnız aynı dosyaya aynı anda yazan iki fonksiyon var.

Takımın ortak bir kasası olsun. İki oyuncu aynı anda kasaya altın ekliyor: biri 100, öteki 50. Kasada başta 0 altın var, sonuç 150 olmalı. C:\kod\veritabani adında yeni bir klasör aç, VS Code'da bu klasörü aç ve kayip.mjs adında bir dosyaya şunu yaz:

kayip.mjs — veritabani — Visual Studio Code
kayip.mjs
import { readFile, writeFile } from "node:fs/promises" async function altinEkle(miktar) {    const kasa = JSON.parse(await readFile("kasa.json", "utf8"))    kasa.altin = kasa.altin + miktar    await writeFile("kasa.json", JSON.stringify(kasa))} await writeFile("kasa.json", JSON.stringify({ altin: 0 }))await Promise.all([altinEkle(100), altinEkle(50)])const son = JSON.parse(await readFile("kasa.json", "utf8"))console.log("Kasadaki altın:", son.altin)
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\veritabani> node kayip.mjsKasadaki altın: 100PS C:\kod\veritabani> node kayip.mjsKasadaki altın: 50PS C:\kod\veritabani> node kayip.mjsKasadaki altın: 50
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Üç çalıştırma, hiçbirinde 150 yok. Promise.all iki altinEkle çağrısını aynı anda başlatır; iki oyuncunun isteği aynı anda gelmiş gibi.

Birkaç kez daha çalıştırırsan program bazen sonucu yazamadan çöker:

Terminal — PowerShell
PS C:\kod\veritabani> node kayip.mjs<anonymous_script>:1  SyntaxError: Unexpected end of JSON input    at JSON.parse (<anonymous>)    at altinEkle (file:///C:/kod/veritabani/kayip.mjs:4:23)    at async Promise.all (index 0)    at async file:///C:/kod/veritabani/kayip.mjs:10:1 Node.js v24.20.0

Programı sen de beş on kez çalıştır; sonuç her seferinde değişebilir, çünkü hangi işin önce biteceğine işletim sistemi karar verir. Bu dersi hazırlarken programı 200 kez çalıştırdık: doğru sonuç (150) yalnız 2 kez çıktı. 172 kez altının bir kısmı kayboldu (50 ya da 100), 26 kez de program çöktü. Sende sayılar farklı çıkar; bilgisayarın o an ne kadar meşgul olduğuna bile bağlıdır.

Her sonuç ayrı bir sorunu gösteriyor:

  • 50 ya da 100: İki çağrı da dosyayı neredeyse aynı anda okudu ve ikisi de altin: 0 gördü. Biri 0 + 100 yazdı, öteki 0 + 50 yazıp onun üstüne bindi. Hata mesajı yok; veri sessizce yanlış.
  • SyntaxError: writeFile önce dosyayı boşaltır, sonra yazar (Ders 29'da gördün). altinEkle'nin içindeki okuma (mesajdaki kayip.mjs:4) dosyayı tam o boş anda yakaladı; boş metin geçerli bir JSON değildir: Unexpected end of JSON input, yani "JSON beklenmedik yerde bitti". İki yazma iç içe geçerse dosyada iki kaydın karışımı da kalabilir ({"altin":50}} gibi); o zaman mesaj Unexpected non-whitespace character after JSON olur ve bozuk dosyayı okuyan her istek aynı hatayla çöker.
  • 150: Ara sıra doğru sonuç da çıkar. Asıl tehlike şu: istekleri tek tek denediğin sürece kod hep doğru çalışır; sorun ancak canlıda, istekler aynı anda gelince ortaya çıkar.
İstek A 100 altın ekler kasa.json dosyanın içi İstek B 50 altın ekler 1. okur: 0 altin: 0 1. okur: 0 2. yazar: 0 + 100 altin: 100 3. yazar: 0 + 50 altin: 50 Beklenen 150. B, A'nın yazdığını görmeden üstüne yazdı.
Şekil 33.1 İki istek dosyayı aynı anda okuyor; sonra yazanın değeri öncekini eziyor. Kasada 150 olması gerekirken 50 kalıyor.

Ders 29'da bu iki sorunun çaresini yazmıştın: yazmaları degistir ile sıraya koymak ve geçici dosyaya yazıp rename ile değiştirmek (atomik işlem). Bu çareler işe yarar, ama yalnız tek bir programın içinde.

Kalabalık sitelerde aynı API'nin birden çok kopyası çalışır (Ders 32'de sunucusuz barındırıcıların trafiğe göre kopya açtığını gördün) ve her kopyanın kendi sırası olur. Kopyalar aynı dosyayı paylaşıyorsa yine aynı anda yazarlar; paylaşmıyorsa her biri verinin ayrı bir hâlini görür, bu daha da kötü. Veritabanı ise sırayı, ona bağlanan bütün programlar için tek bir yerde kendisi tutar.

Aynı anda yazma, dosyanın tek zayıflığı değil:

SorunJSON dosyasındaVeritabanında
Aynı anda yazmaKayıp güncelleme, bozuk dosyaİstekleri düzenler; bir değişiklik ya tam yapılır ya hiç yapılmaz
Arama hızıTek kaydı bulmak için bütün dosya okunurAradığını indeksle doğrudan bulur
Bütünlük"hp": "yüz", eksik alan, aynı id'den iki tane: hepsi girerTipleri ve kuralları kendisi denetler, uymayanı reddeder
İlişkiŞampiyonun adı her takımın içinde kopya; ad değişince Ders 32'deki gibi bütün takımları dolaşırsınTablolar birbirine kimlik numarasıyla bağlanır, ad tek yerde durur

Arama hızını da ölçtük. Bir milyon maçlık bir JSON dosyası (her maçta oyuncu, şampiyon, sonuç, skor ve tarih) 142 MB tuttu. İçinden tek bir maçı bulmak, dosyayı okuyup JSON.parse ile çözmek dahil, her seferinde yaklaşık 650 milisaniye sürdü. Aynı bir milyon maçı aynı bilgisayardaki bir PostgreSQL tablosuna koyup aynı maçı numarasıyla aradığımızda cevap bir milisaniyeden kısa sürede geldi: yüzlerce kat hızlı.

Postgres bunu indeks ile yapar: kitabın sonundaki dizin gibi, bir değerin hangi satırda olduğunu tutan yardımcı bir yapı. Bu derste kuracağın birincil anahtarın indeksi kendiliğinden oluşur; başka indeksleri ve tablolar arası bağları Ders 36'da kuracaksın.

Bir sorun daha var; ısınmada hatırladın: barındırıcıda dosyaya yazdığın veri kalıcı değil. Ders 32'de API'ni koyduğun Render'ın ücretsiz servisinde disk geçicidir; servis uyuyup uyanınca ya da yeni sürüm yüklenince veri.json ilk hâline döner. Veritabanı ise kendi sunucusunda, kalıcı diskte durur; API'n kaç kez yeniden başlarsa başlasın veri yerinde kalır.

Veritabanı: veriyi yöneten program

Veritabanı (database), düzenli tutulan veri deposudur. Ama asıl işi, bu depoyu yöneten program yapar: veritabanı sunucusu. Ona "şu satırı ekle" ya da "Ahri'nin maçlarını getir" dersin; o da veriyi diske güvenle yazar, aynı anda gelen istekleri düzene koyar, kurallara uymayanı geri çevirir ve aradığını hızla bulur.

Ders 27'de bir portu dinleyip istek bekleyen kendi sunucunu yazmıştın. PostgreSQL de böyle bir sunucudur; varsayılan portu 5432'dir. Ders 37'de Node.js'ten ona bağlanacak, Ders 38'de takım API'ni ona taşıyacaksın. Bu derste ona tarayıcıdan, Neon'un SQL Editor'ü üzerinden bağlanıyorsun.

SQL Editor tarayıcıda, Neon'da senin API'n Node.js, Ders 37–38 SQL komutu sonuç tablosu SQL komutu sonuç tablosu PostgreSQL sunucusu port 5432 • veriyi kalıcı diske yazar • istekleri düzene koyar • kuralları denetler neondb sampiyon_havuzu
Şekil 33.2 Veritabanı ayrı bir sunucudur. SQL Editor da senin API'n da ona SQL ile soru sorar, cevabı tablo olarak alır.

Tablo, satır, sütun

Veritabanlarının birkaç türü var; bu kursta en yaygınını, ilişkisel veritabanını (relational database) kullanacaksın. MongoDB gibi belge veritabanlarının adını da duyacaksın; onlar veriyi JSON'a benzer belgeler olarak saklar. İlişkisel veritabanında ise veri tablolarda durur. Her tablonun bir adı ve baştan belli sütunları vardır; her satır bir kayıttır.

sampiyon_havuzu ← tablonun adı sampiyon text rol text mac_sayisi integer favori boolean AhriOrta48true LuxDestek31false JinxNişancı22false sütun başlığı: ad ve tip satır: bir kayıt sütun: bütün değerleri aynı tipte
Şekil 33.3 sampiyon_havuzu tablosu. Her sütunun adı ve tipi var; her satır bir şampiyon.

"İlişkisel" adı aslında tablolardan gelir: matematikte böyle tablolara "ilişki" (relation) denir. Postgres de tabloya relation der; hata mesajlarında bu sözcüğü sık göreceksin. Yine de modelin en güçlü yanı, adının çağrıştırdığı şey: tabloların birbirine bağlanabilmesi. Bir maç satırı, oynayan oyuncuyu ve şampiyonu kimlik numaralarıyla gösterir. Bunu Ders 36'da kuracaksın.

Excel sayfasına benzer, ama kuralları var

Tablo ilk bakışta bir Excel sayfasına benzer: üstte sütun başlıkları, altta satır satır veri. Benzetme işe yarar, ama farklar önemli:

Excel sayfası A B 1 2 3 4 şampiyonmaç Ahri48 Luxkırk sekiz Jinx22 Maç sütununa yazı da girer. Excel itiraz etmez; sorunu ancak bu sütunla hesap yapınca görürsün. Postgres tablosu sampiyon text mac_sayisi integer Ahri48 Luxkırk sekiz ✗ invalid input syntax for type integer: "kırk sekiz" Tipi tutmayan satır tabloya hiç girmez.
Şekil 33.4 Excel her hücreye her şeyi yazdırır. Postgres'te her sütunun bir tipi vardır; tipe uymayan satır tabloya hiç girmez.
  • Tip zorunlu. Excel'de maç sütununa kırk sekiz yazabilirsin. Postgres'te mac_sayisi sütunu integer ise o satır reddedilir.
  • Kural koyabilirsin. "Bu sütun boş olamaz", "bu değer iki kez olamaz" gibi kurallara kısıt (constraint) denir. Bu derste en önemlisini, birincil anahtarı kuracaksın; ötekiler Ders 35'te.
  • Satır numarası yok. Excel'deki 5. satırın yeri sabittir. Veritabanında satırların sabit bir sırası yoktur; bir satırı "5. satır" diye değil, kimliğiyle bulursun (birazdan göreceksin).
  • Formül yok. Hesabı hücreye değil, sorguya yazarsın. "Toplam kaç maç oynadım?" sorusunu Ders 34'te soracaksın.
  • Çok kullanıcı, çok veri. Excel dosyasını genelde önünde oturan bir kişi kullanır. Veritabanı ise aynı anda yüzlerce programın okuyup yazması ve milyonlarca satır için yapıldı.

PostgreSQL ve SQL

Veritabanıyla SQL ile konuşursun (Structured Query Language, yapılandırılmış sorgu dili). SQL 1970'lerde IBM'de doğdu; bugün neredeyse bütün ilişkisel veritabanları onu anlar. Okunuşu iki türlü: harf harf "es-kü-el" ya da eski adı SEQUEL'den kalma "sikuıl". İkisi de doğru.

SQL'in JavaScript'ten büyük bir farkı var: nasıl yapılacağını değil, ne istediğini yazarsın. Ders 5'te bir dizide arama yaparken adımları sen kurmuştun: baştan başla, her elemana tek tek bak, aradığını bulunca dur. SQL'de bu adımları yazmazsın. "Kazanma oranı 50'nin üstündeki şampiyonları getir" dersin; nereden ve hangi sırayla bakılacağına veritabanı karar verir ve en hızlı yolu kendisi seçer.

PostgreSQL bu kursta kullanacağın veritabanı sunucusu. 1986'da California Üniversitesi Berkeley'de başlayan POSTGRES projesinden doğdu ve yaklaşık 40 yıldır geliştiriliyor. Okunuşu "post-gres-kü-el"; neredeyse herkes kısaca Postgres der. Neden Postgres?

  • Ücretsiz ve açık kaynak. Lisansı onu istediğin işte kullanmana izin verir.
  • En yaygını. 2025 Stack Overflow geliştirici anketinde katılımcıların %55,6'sı Postgres kullandığını söyledi; veritabanları arasında birinci. Profesyonel geliştiricilerde oran %58,2.
  • Sıkı ve güçlü. Tipleri ve kuralları ciddiye alır; gerektiğinde JSON verisini de saklayıp sorgulayabilir.
  • Her yerde. Neon ve Supabase gibi bulut servisleri doğrudan Postgres sunar; birinden ötekine geçmek kolaydır.

Bu kursta SQL'i şu kurallarla yazıyoruz:

  • SQL'in kendi sözcüklerini (SELECT, CREATE TABLE, INSERT INTO, VALUES…) büyük, senin verdiğin adları (tablo, sütun) ve veri tiplerini küçük harfle. Postgres için fark etmez, select da çalışır; büyük harf yalnız okumayı kolaylaştırır.
  • Her komut noktalı virgülle (;) biter. JavaScript'te noktalı virgül koymuyoruz, ama SQL'de editörde birden çok komut olduğunda Postgres nerede bittiklerini buradan anlar.
  • Metin tek tırnakla yazılır: 'Ahri'. JavaScript alışkanlığından en çok ayrılacağın yer burası.
  • -- ile başlayan kısım yorumdur, JavaScript'teki // gibi.

Neon'da ücretsiz bir Postgres aç

Postgres'i kendi bilgisayarına da kurabilirsin, ama bu derste kurulum yok: bulutta hazır çalışan bir Postgres kullanacaksın. Neon, Postgres'i bulutta çalıştıran bir servis. Ücretsiz planı bir deneme sürümü değil, kalıcıdır ve kredi kartı istemez.

Ücretsiz planın sınırları (Ekim 2026'daki resmî belgeye göre): 100 proje; her projede 1 GB depolama; her proje için ayda 100 CU-saat işlem süresi. CU-saat, veritabanının çalıştığı süreyi ölçer; 100 CU-saat, en küçük boyda bir veritabanını ayda 400 saat çalıştırmaya yeter. Veritabanı 5 dakika istek almazsa uyur ve süre harcamaz; sıradaki sorguda birkaç yüz milisaniyede uyanır. Ay içinde süre biterse veritabanı yeni aya kadar durur, ama verin silinmez.

  1. neon.com adresine git ve Sign up (ya da Get started) düğmesine bas. GitHub hesabınla (Ders 7'de açtın), Google hesabınla ya da e-postanla kaydolabilirsin.
  2. Neon senden bir proje oluşturmanı ister; istemezse New Project'e bas. Project name kutusuna ogren yaz. Region olarak sana en yakın bölgeyi seç; Türkiye'den AWS Europe (Frankfurt) iyi bir seçim. Bölge sonradan değiştirilemez.
  3. Services bölümünde Postgres database açık kalsın, sürümünü (Postgres version) olduğu gibi bırak. Öteki servisler bu kursta gerekmiyor. Create project'e bas.
  4. Birkaç saniye içinde veritabanın hazır; adı neondb. Sol menüde Postgres database altındaki SQL Editor'ü aç. Üstteki seçicilerde dal (branch) olarak production, veritabanı olarak neondb seçili olmalı.
  5. Editörde Neon'un hazırladığı örnek bir kod durabilir. New Query ile boş bir sayfa aç.
SQL Editor Saved History eski sorguların production ▾ neondb ▾ 1 1 SELECT 'Merhaba' AS mesaj; 2 2 Run 3 Explain Analyze 1: SELECT 1 4 # mesaj 1 Merhaba
Şekil 33.5 SQL Editor'ün bölümleri. Sadeleştirilmiş çizim; gerçek ekran farklı görünebilir.
  1. Dal ve veritabanı: sorgunun hangi veritabanında çalışacağı. Bu derste hep production ve neondb.
  2. Editör: SQL'i buraya yazarsın.
  3. Run: editördeki komutları çalıştırır. Yanındaki Explain ve Analyze'a şimdilik dokunma.
  4. Sonuç bölümü: editörde birden çok komut varsa her birinin sonucu, çalışma sırasıyla numaralanmış ayrı bir sekmede çıkar. Soldaki History daha önce çalıştırdığın komutları saklar.

Başka yollar. Postgres'i bilgisayarına kurmak istersen Windows için postgresql.org'daki EDB yükleyicisi sunucuyu ve pgAdmin adlı görsel yönetim aracını birlikte kurar. Docker kullanıyorsan resmî postgres imajıyla tek komutla bir sunucu açılır. Bulutta Neon'a benzer başka servisler de var (Supabase gibi). Hangisini seçersen seç SQL aynıdır; bu kurstaki komutlar hepsinde çalışır. Neon'u seçmemizin sebebi kurulum istememesi ve API'ni yayına aldığında internetten hazır ulaşılabilir olması.

İlk sorgu

Editöre şunu yaz ve Run'a bas:

SQL
SELECT 'Merhaba Sihirdar Vadisi' AS mesaj, 2 + 3 AS toplam;
mesajtoplam
Merhaba Sihirdar Vadisi5

Sonuç bölümünde iki sütunlu, tek satırlı bir tablo görürsün. Postgres sorguların cevabını tablo olarak verir; bu derste sonuçları hep böyle tablolarla göstereceğiz.

  • SELECT "şunu göster" demek. Burada hiçbir tabloya bakmıyoruz, yalnız bir metni ve bir hesabı gösteriyoruz. Asıl işini, tablodan veri seçmeyi, Ders 34'te öğreneceksin.
  • 'Merhaba Sihirdar Vadisi' bir metin; tek tırnak içinde.
  • AS mesaj sonuç sütununa ad verir. Yazmazsan Postgres sütunun adını ?column? koyar.
  • ; komutu bitirir.
Sık hatacolumn "Ahri" does not exist

Ne zaman çıkar? Metni JavaScript alışkanlığıyla çift tırnakla yazınca. SQL'de çift tırnak metin değil, ad (tablo ya da sütun adı) demektir; Postgres Ahri adında bir sütun arar, bulamaz.

SQL
SELECT "Ahri" AS sampiyon;
Hata çıktısı
ERROR:  column "Ahri" does not exist

Nasıl düzelir? Metni tek tırnağa al: SELECT 'Ahri' AS sampiyon;. Kuralı ezberle: tek tırnak değer, çift tırnak ad.

Sık hatasyntax error at or near "SELECT"

Ne zaman çıkar? Editörde iki komut var ve aralarındaki noktalı virgülü unuttun. Postgres ikisini tek komut sanır ve ikinci SELECT'e gelince takılır.

SQL
SELECT 'Ahri' AS sampiyonSELECT 'Lux' AS sampiyon;
Hata çıktısı
ERROR:  syntax error at or near "SELECT"

Nasıl düzelir? Her komutun sonuna ; koy. syntax error at or near "X", "X'te ya da hemen öncesinde bir yazım hatası var" demektir; hatayı çoğu zaman X'in hemen önünde bulursun. Yanlış yazılmış bir sözcük de aynı mesajı verir: SELEC yazarsan syntax error at or near "SELEC" alırsın.

Veri tipleri

Excel'den en büyük fark buydu: her sütunun bir veri tipi var ve Postgres o tipe uymayan değeri sütuna sokmaz. Sık kullanacakların:

TipNe içinÖrnek değer
integerTam sayı, yaklaşık ±2,1 milyara kadar48
bigintÇok büyük tam sayı9000000000
numeric(5, 2)Kesin ondalıklı sayı: toplam 5 basamak, 2'si virgülden sonra54.17
textHer uzunlukta metin'Ahri'
varchar(16)En çok 16 karakterlik metin'Faker'
booleanDoğru ya da yanlıştrue, false
timestamptzBir an: tarih, saat ve saat dilimi'2026-10-09 21:40:00+03'
dateYalnız tarih'2026-10-09'
jsonbJSON verisi'{"rol": "Orta"}'

Birkaç not:

  • integer mi bigint mi? Maç sayısı, altın, seviye gibi değerlere integer yeter. 2,1 milyarı geçebilecek sayılar (bütün oyuncuların bütün maçlarının numarası gibi) için bigint seçilir.
  • text mi varchar mı? Postgres'te ikisi aynı hızda çalışır. Gerçek bir uzunluk sınırın yoksa text yaz. Riot ID'deki oyun adı gibi, sınırı kesin bilinen bir değer için varchar(16) kullanabilirsin; daha uzun metin hata verir.
  • timestamptz bir anı saklar. Yazarken saat dilimini ekle: Türkiye için +03. Postgres anı saat dilimiyle birlikte anlar, gösterirken bağlantının saat dilimine çevirir. Neon'da bu saat dilimi UTC'dir; bu yüzden 21:40:00+03 diye yazdığın an 18:40:00+00 diye görünür. İkisi aynı an. Saat dilimsiz bir timestamp tipi de var; kullanma, hangi saate göre olduğu belirsiz kalır.
  • jsonb esnek veri tutar. Ama her şeyi jsonb sütununa koyarsan JSON dosyasına geri dönmüş olursun; önce sütunları düşün.

numeric kesin hesaplar. Ders 9'da JavaScript'in 0.1 + 0.2 için 0.30000000000000004 verdiğini görmüştün. Postgres'te ondalıklı sayılar varsayılan olarak numeric'tir:

SQL
SELECT 0.1 + 0.2 AS toplam;
toplam
0.3

Para, oran, puan gibi yuvarlama hatası istemediğin her şey için numeric kullan. numeric(5, 2) en çok 999.99 tutar; virgülden sonraki fazla basamak yuvarlanır, sığmayan sayı hata verir. İkisini de görevlerde deneyeceksin.

İlk tablo: CREATE TABLE

Şimdi kendi tablonu kur. Oynadığın şampiyonları ve her biriyle ilgili istatistiklerini tutan bir şampiyon havuzu yapacağız. Bu ilk sürüm bir taslak: eksiğini birazdan kendin bulacaksın.

SQL
CREATE TABLE sampiyon_havuzu (    sampiyon text,    rol text,    mac_sayisi integer,    kazanma_orani numeric(5, 2),    favori boolean,    son_mac timestamptz);
ParçaÖrnekAnlamı
komutCREATE TABLE"yeni tablo kur"
tablo adısampiyon_havuzutablonun adı
parantez( … )sütunların listesi
sütunmac_sayisi integerönce ad, sonra tip
virgül,sütunları ayırır; sonuncudan sonra yok
noktalı virgül;komutun sonu

Komutu birkaç satıra böldük, ama Postgres için satır sonları ve girinti önemsizdir; aynı komutu tek satıra da yazabilirdin. Böyle yazmak, her sütunu ayrı satırda görmeyi sağlar.

Run'a bas. Bu komut tablo kurar, satır döndürmez; sonuç bölümünde komutun başarıyla çalıştığını görürsün. Tablon hazır, ama içi boş.

Adlara dikkat et: sampiyon_havuzu, mac_sayisi, son_mac. JavaScript'teki camelCase yerine sözcükler alt çizgiyle bağlanıyor ve hepsi küçük harf.

Sebebi şu: Postgres tırnaksız yazılan adları küçük harfe çevirir. macSayisi adında bir sütun açarsan Postgres onu macsayisi diye kaydeder; büyük harfi korumak için o adı her yerde çift tırnakla yazman gerekir. SQL'de bu yüzden adlar küçük harf ve alt çizgiyle yazılır. Türkçe harf kullanmama kuralı JavaScript'teki gibi burada da geçerli.

Sık hatasyntax error at or near ")"

Ne zaman çıkar? Son sütundan sonra virgül bırakınca. JavaScript'te dizinin son elemanından sonra virgül sorun olmuyordu; SQL'de oluyor.

SQL
CREATE TABLE sampiyon_havuzu (    sampiyon text,    rol text,);
Hata çıktısı
ERROR:  syntax error at or near ")"

Nasıl düzelir? Mesaj kapanış parantezini gösteriyor, ama hata hemen önünde: rol text, satırının sonundaki virgülü sil. Ters durumda, iki sütun arasındaki virgülü unutursan da hata bir sonraki sütunun adında çıkar: sampiyon text ile rol text arasında virgül yoksa mesaj syntax error at or near "rol" olur.

Sık hatarelation "sampiyon_havuzu" already exists

Ne zaman çıkar? Aynı CREATE TABLE'ı ikinci kez çalıştırınca. Çoğu zaman bilerek değil: editörde eski CREATE TABLE duruyorken yeni bir komut ekleyip Run'a basınca.

SQL
CREATE TABLE sampiyon_havuzu (    sampiyon text,    rol text);
Hata çıktısı
ERROR:  relation "sampiyon_havuzu" already exists

Nasıl düzelir? Tablo zaten kurulu; editörden CREATE TABLE komutunu sil ve yalnız yeni komutunu çalıştır. Relation (ilişki), Postgres'in tabloya verdiği teknik ad.

INSERT: satır eklemek

Tabloya satır eklemek için INSERT INTO kullanırsın. Editörü temizle ve Ahri'yi ekle:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES ('Ahri', 'Orta', 48, 54.17, true, '2026-10-09 21:40:00+03');

Komut iki parçadan oluşuyor. INSERT INTO'dan sonra tablonun adı ve parantez içinde değer vereceğin sütunlar gelir (satır uzamasın diye sütun listesini alt satıra aldık); VALUES'tan sonra da aynı sırayla değerler. İlk değer ilk sütuna, ikinci değer ikinci sütuna gider. Metinler ve tarih tek tırnakta; sayılar ve true/false tırnaksız. Run'a basınca Postgres eklediği satır sayısını (1) bildirir.

Tabloya bakmak için şimdilik tek bir kalıp yeter: SELECT * FROM tablo_adi;. * "bütün sütunlar" demek. Editörü temizle ve çalıştır:

SQL
SELECT * FROM sampiyon_havuzu;
sampiyonrolmac_sayisikazanma_oranifavorison_mac
AhriOrta4854.17true2026-10-09 18:40:00+00

İlk satırın tabloda. son_mac sütununda 21:40 yerine 18:40 görmen normal: Neon saati UTC olarak gösteriyor, an aynı. Bazı araçlar true yerine t, false yerine f gösterir; anlamı aynı. SELECT'i bütün yetenekleriyle Ders 34'te öğreneceksin.

Tek komutla birden çok satır da ekleyebilirsin; değer gruplarını virgülle ayır:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES    ('Lux', 'Destek', 31, 51.61, false, '2026-10-08 19:05:00+03'),    ('Jinx', 'Nişancı', 22, 45.45, false, '2026-10-10 22:15:00+03');
SQL
SELECT * FROM sampiyon_havuzu;
sampiyonrolmac_sayisikazanma_oranifavorison_mac
AhriOrta4854.17true2026-10-09 18:40:00+00
LuxDestek3151.61false2026-10-08 16:05:00+00
JinxNişancı2245.45false2026-10-10 19:15:00+00

Çift tırnak hatasını ilk sorguda gördün; INSERT'te de aynısı olur: VALUES ("Ahri", …) yazarsan Postgres yine column "Ahri" does not exist der. Şimdi INSERT'e özgü tuzaklara bakalım.

Metnin içinde kesme işareti

Sıradaki şampiyon Kai'Sa. Adında kesme işareti var ve SQL'de metni bitiren işaret de tam o:

SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol)VALUES ('Kai'Sa', 'Nişancı');
Hata çıktısı
ERROR:  syntax error at or near "Sa"

Postgres 'Kai' metnini okudu ve bitti sandı; ardından gelen Sa'yı anlamlandıramadı. Metnin içindeki tek tırnağı iki tek tırnak yazarak belirtirsin. Ekranda yine tek tırnak görünür:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES ('Kai''Sa', 'Nişancı', 9, 66.67, false, '2026-10-10 23:30:00+03');

Bu komut hatasız çalışır ve tabloya Kai'Sa adıyla bir satır ekler. Cho'Gath, Kog'Maw ve Bel'Veth için de aynı kural geçerli.

Sık hataINSERT has more expressions than target columns

Ne zaman çıkar? Ondalık sayıyı Türkçe alışkanlıkla virgülle yazınca. 51,61 Postgres için iki ayrı değerdir: 51 ve 61. Değer sayısı sütun sayısını geçer.

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES ('Lux', 'Destek', 31, 51,61, false, '2026-10-08 19:05:00+03');
Hata çıktısı
ERROR:  INSERT has more expressions than target columns

Nasıl düzelir? Ondalık ayırıcı noktadır: 51.61. Mesajın tersi de var: bir değeri unutursan INSERT has more target columns than expressions (sütun çok, değer az) alırsın. Çok satırlı bir INSERT'te satırlardan birinin değer sayısı farklıysa mesaj VALUES lists must all be the same length olur. Üçünde de sütunları ve değerleri parmağınla tek tek say.

Sık hatainvalid input syntax for type integer

Ne zaman çıkar? Bir sütuna tipine uymayan değer yazınca. Excel'in izin verdiği şeyi Postgres reddeder.

SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol, mac_sayisi)VALUES ('Garen', 'Üst', 'kırk sekiz');
Hata çıktısı
ERROR:  invalid input syntax for type integer: "kırk sekiz"

Nasıl düzelir? Mesaj sütunun tipini (integer) ve kabul edilmeyen değeri birlikte söylüyor: sayıyı rakamla, tırnaksız yaz. boolean sütuna 'evet' yazarsan da invalid input syntax for type boolean: "evet" alırsın; doğrusu true ya da false.

Tarih yazmanın tek güvenli yolu

Garen'i ekleyelim. Maçı 9 Ekim akşamı oynadın ve tarihi alıştığın gibi gün.ay.yıl yazdın:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES ('Garen', 'Üst', 15, 46.67, false, '09.10.2026 21:40+03');

Komut hatasız çalıştı. Sonra tabloya bakmak için altına SELECT yazdın ve Run'a bastın, ama INSERT'i silmeyi unuttun. Editörde şu duruyordu:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES ('Garen', 'Üst', 15, 46.67, false, '09.10.2026 21:40+03');SELECT * FROM sampiyon_havuzu;
sampiyonrolmac_sayisikazanma_oranifavorison_mac
AhriOrta4854.17true2026-10-09 18:40:00+00
LuxDestek3151.61false2026-10-08 16:05:00+00
JinxNişancı2245.45false2026-10-10 19:15:00+00
Kai'SaNişancı966.67false2026-10-10 20:30:00+00
GarenÜst1546.67false2026-09-10 18:40:00+00
GarenÜst1546.67false2026-09-10 18:40:00+00

Tabloda iki sorun var. Birincisi: Garen'in maçı 10 Eylül görünüyor. Postgres 09.10.2026'yı varsayılan olarak ay.gün.yıl sırasıyla okudu: 9. ayın 10'u. Hata mesajı yok, veri sessizce yanlış.

İkincisi: Garen iki kez eklenmiş, çünkü Run editördeki INSERT'i de yeniden çalıştırdı.

Sık hataGün ile ay yer değiştirdi (hata mesajı yok)

Ne zaman çıkar? Tarihi 09.10.2026 gibi gün.ay.yıl sırasıyla yazınca. Postgres bu biçimi varsayılan olarak ay.gün.yıl diye okur. Gün 12'den büyükse hata verir ('25.10.2026' için date/time field value out of range), değilse sessizce başka bir tarih kaydeder.

Nasıl düzelir? Tarihi her zaman yıl-ay-gün sırasıyla, tire ile yaz: '2026-10-09 21:40:00+03'. Bu biçim (ISO 8601) her ayarda aynı okunur. Saat dilimini (+03) de ekle; eklemezsen saat, bağlantının saat dilimine göre yorumlanır.

İkinci sorun daha derin: iki Garen satırı tıpatıp aynı. Birini silmek ya da tarihini düzeltmek istesen (Ders 35'te öğreneceksin) Postgres'e "şu satır" diyemezsin; bütün değerleri aynı. Excel'deki gibi "6. satır" da diyemezsin, çünkü satır numarası yok. Tablonun eksiği buydu.

Birincil anahtar

Her satırın, onu öteki bütün satırlardan ayıran bir kimliği olmalı. Bu işi yapan sütuna birincil anahtar (primary key) denir. İki kuralı vardır: değeri iki satırda aynı olamaz ve boş olamaz.

İlk deneme: adı anahtar yapmak

İlk akla gelen şu: bir şampiyon havuzda bir kez olur, öyleyse şampiyonun adı anahtar olsun. Bunu denemek için tabloyu silip yeniden kuracağız. DROP TABLE bir tabloyu içindeki bütün satırlarla birlikte siler:

SQL
DROP TABLE sampiyon_havuzu;

Deneme için iki sütun yeter. Ad sütununun yanına PRIMARY KEY yaz:

SQL
CREATE TABLE sampiyon_havuzu (    sampiyon text PRIMARY KEY,    rol text);
SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol)VALUES ('Ahri', 'Orta'), ('Lux', 'Destek');

Şimdi Ahri'yi bir daha eklemeyi dene:

SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol)VALUES ('Ahri', 'Orta');
Hata çıktısı
ERROR:  duplicate key value violates unique constraint "sampiyon_havuzu_pkey"DETAIL:  Key (sampiyon)=(Ahri) already exists.

Bu sefer reddedildi. Mesajı parçalayalım: duplicate key value "tekrarlanan anahtar değeri", violates "çiğniyor", unique constraint "benzersizlik kuralı". sampiyon_havuzu_pkey, Postgres'in birincil anahtar kuralına verdiği ad: tablo adı ve pkey (primary key). DETAIL satırı hangi değerin zaten olduğunu söylüyor.

İkinci kural: anahtar boş olamaz. INSERT'te yazmadığın sütuna NULL konur. NULL SQL'de "değer yok" demektir, JavaScript'teki null gibi:

SQL
INSERT INTO sampiyon_havuzu (rol) VALUES ('Orman');
Hata çıktısı
ERROR:  null value in column "sampiyon" of relation "sampiyon_havuzu" violates not-null constraintDETAIL:  Failing row contains (null, Orman).

"sampiyon sütununa boş değer yazmak, boş olamaz kuralını çiğniyor." Failing row başarısız olan satırın kendisi: adı boş, rolü Orman.

İki hata da istediğimiz şeydi: tablo aynı şampiyonu iki kez almıyor ve adsız satırı kabul etmiyor. Peki neden burada durmuyoruz?

Adlar değişir

Ad değişince ne olduğunu Ders 32'de yaşadın: takım API'sinde şampiyonun adını değiştiren route, adı havuzda ve onu seçen her takımda tek tek değiştirmek zorundaydı. Riot da aynı sorunla karşılaşır; çözümünü kendi verisinde görebilirsin.

Wukong geliştirilirken MonkeyKing diye anılıyordu (oyuncu topluluğunun derlediği bilgi). Oyuna Wukong adıyla çıktı, ama Riot'un sistemlerindeki kimliği MonkeyKing olarak kaldı. Data Dragon'da bugün de böyle görünür:

Data Dragon tr_TR/champion.json, Wukong kaydından (kısaltıldı)
{    "id": "MonkeyKing",    "key": "62",    "name": "Wukong",    "title": "Maymun Kral"}

Nunu'nun adı 2018'de yeniden tasarlandıktan sonra "Nunu ve Willump" oldu; kimliği hâlâ Nunu. Oyuncular için de aynısı geçerli: Riot ID'ni değiştirebilirsin, ama Riot API'si seni puuid denen, değişmeyen, uzun ve anlamsız bir kimlikle tanır. Riot neden böyle yapıyor? Adı anahtar yapsaydı, ad değişince o ada bağlı her kaydı (maçlar, takımlar, istatistikler) tek tek değiştirmesi gerekirdi.

Anahtar = ad şampiyonlar MonkeyKing → Wukong maçlar maç 1 MonkeyKing maç 2 MonkeyKing maç 3 MonkeyKing Ad değişince hepsi düzeltilmeli. Anahtar = sayı şampiyonlar 62 Wukong maçlar maç 1 62 maç 2 62 maç 3 62 Ad tek yerde değişir; bağlar sağlam.
Şekil 33.6 Ad anahtar olursa, ad değişince ona bağlı bütün kayıtlar değişmeli. Sayı anahtar olursa ad tek yerde değişir, bağlar bozulmaz.

Anlamsız bir numara, Postgres versin

Çözüm: her satıra anlamı olmayan bir numara ver ve onu birincil anahtar yap. Numarayı sen seçersen bir gün iki satıra aynı numarayı verirsin ya da sırayı karıştırırsın. Numarayı Postgres versin. Deneme tablosunu sil:

SQL
DROP TABLE sampiyon_havuzu;

Ve şampiyon havuzunu son hâliyle kur:

SQL
CREATE TABLE sampiyon_havuzu (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    sampiyon text,    rol text,    mac_sayisi integer,    kazanma_orani numeric(5, 2),    favori boolean,    son_mac timestamptz);

İlk satır yeni: id adında bir integer sütun. GENERATED ALWAYS AS IDENTITY "bu sütunun değerini her zaman Postgres üretsin: 1, 2, 3…" demek; PRIMARY KEY onu birincil anahtar yapar. INSERT'te id'yi hiç yazmazsın:

SQL
INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES    ('Ahri', 'Orta', 48, 54.17, true, '2026-10-09 21:40:00+03'),    ('Lux', 'Destek', 31, 51.61, false, '2026-10-08 19:05:00+03'),    ('Jinx', 'Nişancı', 22, 45.45, false, '2026-10-10 22:15:00+03'),    ('Kai''Sa', 'Nişancı', 9, 66.67, false, '2026-10-10 23:30:00+03');
SQL
SELECT * FROM sampiyon_havuzu;
idsampiyonrolmac_sayisikazanma_oranifavorison_mac
1AhriOrta4854.17true2026-10-09 18:40:00+00
2LuxDestek3151.61false2026-10-08 16:05:00+00
3JinxNişancı2245.45false2026-10-10 19:15:00+00
4Kai'SaNişancı966.67false2026-10-10 20:30:00+00

Numaraları sen vermedin; Postgres sırayla verdi. Artık her satırın değişmeyen bir kimliği var: Ahri'nin adı bir gün değişse de satırı 1 numarasıyla bulursun. Bu numaralar sıra numarası değildir: bir satırı sildiğinde (Ders 35) onun numarası bir daha kullanılmaz ve araya boşluk girer. Bu normaldir.

ALWAYS sözcüğü "her zaman Postgres üretir" demek. Numarayı elle vermeye kalkarsan itiraz eder:

SQL
INSERT INTO sampiyon_havuzu (id, sampiyon, rol)VALUES (5, 'Garen', 'Üst');
Hata çıktısı
ERROR:  cannot insert a non-DEFAULT value into column "id"DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.HINT:  Use OVERRIDING SYSTEM VALUE to override.

DETAIL sebebi söylüyor: id her zaman Postgres'in ürettiği bir kimlik sütunu. HINT bu kuralı delmenin yolunu gösteriyor, ama onu kullanma; yalnız başka bir veritabanından kayıt taşırken gerekir. Satırı id olmadan ekle.

Bir şeye dikkat: birincil anahtar artık id, ad değil. Aynı INSERT'i iki kez çalıştırırsan Ahri yine iki kez eklenir; ama bu sefer birinin id'si 1, öbürününki 5 olur ve hangisini kastettiğini söyleyebilirsin. Aynı adın iki kez girmesini de engellemeyi Ders 35'te, UNIQUE kısıtıyla öğreneceksin.

Şemanı dosyaya kaydet

Tablon Neon'da duruyor. Ama nasıl kurduğunu da bir dosyada saklamalısın: veritabanını baştan kurman gerektiğinde (yeni bir proje, başka bir bilgisayar, Ders 38'deki proje) aynı komutları çalıştırman yeter. Tabloların, sütunların, tiplerin ve kuralların tanımına şema (schema) denir; bu dosyaya da genelde schema.sql adı verilir.

VS Code'da C:\kod\veritabani klasörüne schema.sql adında bir dosya aç ve tablonun son hâlini başlangıç verisiyle birlikte yaz. VS Code .sql uzantılı dosyaları kendiliğinden renklendirir:

schema.sql
-- Ders 33: ilk tablom-- Şampiyon havuzum: oynadığım şampiyonlar ve istatistiklerim CREATE TABLE sampiyon_havuzu (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    sampiyon text,    rol text,    mac_sayisi integer,    kazanma_orani numeric(5, 2),    favori boolean,    son_mac timestamptz); INSERT INTO sampiyon_havuzu    (sampiyon, rol, mac_sayisi, kazanma_orani, favori, son_mac)VALUES    ('Ahri', 'Orta', 48, 54.17, true, '2026-10-09 21:40:00+03'),    ('Lux', 'Destek', 31, 51.61, false, '2026-10-08 19:05:00+03'),    ('Jinx', 'Nişancı', 22, 45.45, false, '2026-10-10 22:15:00+03'),    ('Kai''Sa', 'Nişancı', 9, 66.67, false, '2026-10-10 23:30:00+03');

Bu dosya bir kayıt: boş bir veritabanında her şeyi baştan kurmak içindir. Tablo Neon'da zaten var; dosyayı orada yeniden çalıştırırsan relation "sampiyon_havuzu" already exists alırsın.

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.

Sık hatarelation "sampiyonlar" does not exist

Ne zaman çıkar? Tablo adını yanlış yazınca ya da tabloyu kurmadan kullanınca. Burada Ders 27–32'deki API'den kalma alışkanlıkla sampiyonlar yazılmış.

SQL
INSERT INTO sampiyonlar (sampiyon, rol)VALUES ('Garen', 'Üst');
Hata çıktısı
ERROR:  relation "sampiyonlar" does not exist

Nasıl düzelir? Tablonun adını kurduğun gibi yaz: sampiyon_havuzu. Ad doğruysa doğru veritabanında olduğuna bak: SQL Editor'ün üstündeki seçicide neondb seçili mi? Tabloyu hiç kurmadıysan önce CREATE TABLE'ı çalıştır.

Sık hatacolumn "macsayisi" of relation "sampiyon_havuzu" does not exist

Ne zaman çıkar? Sütun adını JavaScript alışkanlığıyla camelCase yazınca. Mesajdaki adın küçük harfe dönmüş olduğuna dikkat et: Postgres tırnaksız adı macsayisi'ye çevirdi ve öyle bir sütun bulamadı.

SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol, macSayisi)VALUES ('Garen', 'Üst', 15);
Hata çıktısı
ERROR:  column "macsayisi" of relation "sampiyon_havuzu" does not exist

Nasıl düzelir? Sütunu tabloda nasıl adlandırdıysan öyle yaz: mac_sayisi. Adı hatırlamıyorsan SELECT * FROM sampiyon_havuzu; ile sonuç tablosunun başlıklarına bak.

Sık hataduplicate key value violates unique constraint "…_pkey"

Ne zaman çıkar? Birincil anahtar sütununa tabloda zaten olan bir değeri yazınca. Birincil anahtar bölümünde şampiyon adını anahtar yaptığında gördün. DETAIL satırı hangi değerin zaten olduğunu söyler: Key (sampiyon)=(Ahri) already exists.

Nasıl düzelir? Aynı kaydı ikinci kez ekliyorsan (editörde kalan bir INSERT'i yeniden çalıştırmak gibi) eklemeyi bırak; kayıt zaten tabloda. Gerçekten farklı bir kayıtsa anahtarı da farklı olmalı. Anahtarı elle uydurma; Postgres üretsin (GENERATED ALWAYS AS IDENTITY).

Sık hatanull value in column "…" violates not-null constraint

Ne zaman çıkar? Boş olamayan bir sütunu (birincil anahtar gibi) INSERT'te yazmayınca ya da ona NULL verince. Yazmadığın her sütuna NULL konur. DETAIL satırı reddedilen satırın tamamını gösterir: Failing row contains (null, Orman).

Nasıl düzelir? Mesajdaki sütunu INSERT'in sütun listesine ekle ve ona bir değer ver.

Sık hatacannot insert a non-DEFAULT value into column "id"

Ne zaman çıkar? GENERATED ALWAYS AS IDENTITY olan bir sütuna elle değer yazınca. Anlamsız bir numara, Postgres versin bölümünde gördün.

Nasıl düzelir? id'yi hem sütun listesinden hem değerlerden çıkar; numarayı Postgres verir. HINT satırındaki OVERRIDING SYSTEM VALUE'yu kullanma.

Görevler

Görevleri Neon'un SQL Editor'ünde yap; her adımda editörde yalnız o adımın komutu olsun. İstersen yazdığın SQL'i C:\kod\veritabani\gorevler.sql dosyasında da sakla. Önce kendin dene, takılırsan ipuçlarını sırayla aç.

Görev 1

Tipi seç

Bir oyuncu profili tablosu tasarlıyorsun. Aşağıdaki her bilgi için en uygun veri tipini seç (tabloyu kurman gerekmiyor):

  1. Riot ID'deki oyun adı (Riot'un kuralı: en çok 16 karakter)
  2. Hesap seviyesi
  3. Mağazada bugüne kadar harcanan para (TL, kuruşlu)
  4. Oyuncu şu an çevrimiçi mi?
  5. Hesabın açıldığı an
  6. Doğum günü
  7. Telefon numarası
  8. Oyun ayarları (her oyuncuda farklı alanları olabilen bir JSON)
İpucu 1

Sorular sor: hesap yapılacak mı, kesin mi olmalı, saat de var mı, uzunluğun kesin bir sınırı var mı?

İpucu 2

Telefon numarasıyla toplama, çıkarma yapar mısın? Başındaki 0'a ne olur?

Çözümü göster
  1. varchar(16) (ya da text). Sınırı veritabanı da bilsin istersen varchar(16).
  2. integer.
  3. numeric(10, 2): para kesin olmalı. 10 basamağın 2'si kuruş, yani 99.999.999,99 TL'ye kadar.
  4. boolean.
  5. timestamptz: bir an, saat dilimiyle.
  6. date: doğum gününün saati yok.
  7. text: telefon numarası bir sayı değil, rakamlardan oluşan bir metin. Onunla hesap yapılmaz.
  8. jsonb.

Telefon numarasını sayı olarak yazarsan baştaki 0 kaybolur:

SQL
SELECT 05321234567 AS telefon;
telefon
5321234567

Sınır denemesi: varchar(16) sınırının iki yanını dene. Tam 16 karakterlik ad girer, 17'lik ad reddedilir:

SQL
CREATE TABLE profil_deneme (oyun_adi varchar(16));
SQL
INSERT INTO profil_deneme (oyun_adi)VALUES ('SihirdarAhri1234');
SQL
INSERT INTO profil_deneme (oyun_adi)VALUES ('SihirdarAhri12345');
Hata çıktısı
ERROR:  value too long for type character varying(16)

character varying, varchar'ın uzun adı. Denemen bitince tabloyu DROP TABLE profil_deneme; ile silebilirsin.

Görev 2

Eşya dükkânı

Dükkândaki eşyalar için esyalar adında bir tablo kur. Sütunlar: Postgres'in ürettiği bir id (birincil anahtar), eşyanın adı (ad), altın fiyatı (fiyat) ve kullanınca tükenip tükenmediği (tuketilir). Sonra şu üç eşyayı tek bir INSERT ile ekle ve tabloya bak:

adfiyattuketilir
Doran'ın Kılıcı450hayır
Can İksiri50evet
Görünmez Totem0hayır

SELECT * sonucunda id'ler 1, 2, 3 olmalı.

İpucu 1

id satırı ders anlatımındakiyle aynı kalıp: id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY. "Evet/hayır" bilgisi için hangi tip?

İpucu 2

Doran'ın adında kesme işareti var. Metnin içindeki tek tırnak nasıl yazılıyordu?

Çözümü göster
SQL
CREATE TABLE esyalar (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    ad text,    fiyat integer,    tuketilir boolean);
SQL
INSERT INTO esyalar (ad, fiyat, tuketilir)VALUES    ('Doran''ın Kılıcı', 450, false),    ('Can İksiri', 50, true),    ('Görünmez Totem', 0, false);
SQL
SELECT * FROM esyalar;
idadfiyattuketilir
1Doran'ın Kılıcı450false
2Can İksiri50true
3Görünmez Totem0false

Sınır denemesi: Görünmez Totem'in fiyatı 0. Sıfır da bir değerdir; NULL (değer yok) ile karıştırma. Fiyatı bilinmeyen bir eşyayı eklerken fiyat sütununu hiç yazmazsan o hücreye NULL konur, 0 değil.

Görev 3

Hata avı

Bir arkadaşın TFT birimleri için bir tablo kurmak istemiş, ama iki komutunda toplam üç hata var. Gözle aramadan, yalnız mesajları okuyarak düzelt. Önce CREATE TABLE'ı tek başına çalıştır; tablo kurulunca INSERT'i ayrıca çalıştır.

SQL✗ Yanlış
CREATE TABLE tft_birimleri (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    ad text    maliyet integer,    yildiz integer);
Hata çıktısı
ERROR:  syntax error at or near "maliyet"
SQL✗ Yanlış
INSERT INTO tft_birimleri (ad, maliyet, yildiz)VALUES ("Jinx", 4, 2), ('Ahri', 'dört', 1);

Hepsini düzeltince SELECT * FROM tft_birimleri; iki satır göstermeli: Jinx (4 altın, 2 yıldız) ve Ahri (4 altın, 1 yıldız).

İpucu 1

İlk mesaj maliyet sözcüğünü gösteriyor, ama hata çoğu zaman gösterilen yerin hemen önündedir. Bir üst satırın sonuna bak.

İpucu 2

INSERT'in ilk mesajı column "Jinx" does not exist olacak. Postgres Jinx'i neden bir sütun adı sandı?

İpucu 3

Son mesaj invalid input syntax for type integer: "dört". maliyet sütunu hangi tipte?

Çözümü göster
SQL
CREATE TABLE tft_birimleri (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    ad text,    maliyet integer,    yildiz integer);
SQL
INSERT INTO tft_birimleri (ad, maliyet, yildiz)VALUES ('Jinx', 4, 2), ('Ahri', 4, 1);SELECT * FROM tft_birimleri;
idadmaliyetyildiz
1Jinx42
2Ahri41

Sırasıyla gelen mesajlar: syntax error at or near "maliyet" (ad text'ten sonra virgül eksik), column "Jinx" does not exist (metin çift tırnakta), invalid input syntax for type integer: "dört" (sayı yazıyla yazılmış).

Görev 4

Sınırları dene

TFT'de bir oyuncunun ortalama sırası 1.00 ile 8.00 arasındadır. Şu tabloyu kur:

SQL
CREATE TABLE tft_istatistik (    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,    oyuncu text,    oynanan_mac integer,    ortalama_sira numeric(3, 2));

Sonra şu beş oyuncuyu ayrı ayrı INSERT ile eklemeyi dene. Önce her biri için tahminini yaz: girer mi, değişerek mi girer, hata mı verir?

oyuncuoynanan_macortalama_sira
Deniz1201
Ece858
Mert404.355
Arda6010
Selin30000000004.5

En sonda SELECT * ile tabloya bak.

İpucu 1

numeric(3, 2) toplam 3 basamak tutar, 2'si virgülden sonra. Virgülden önce kaç basamak kalıyor?

İpucu 2

integer'ın sınırı yaklaşık 2,1 milyar. Virgülden sonra fazla basamak gelirse ne olduğunu ders anlatımında okudun.

Çözümü göster

İlk üçü girer. Deniz ve Ece tam sınırda (1 ve 8) ve sorunsuz. Mert'in ortalaması yuvarlanarak girer:

SQL
INSERT INTO tft_istatistik    (oyuncu, oynanan_mac, ortalama_sira)VALUES ('Deniz', 120, 1);
SQL
INSERT INTO tft_istatistik    (oyuncu, oynanan_mac, ortalama_sira)VALUES ('Ece', 85, 8);
SQL
INSERT INTO tft_istatistik    (oyuncu, oynanan_mac, ortalama_sira)VALUES ('Mert', 40, 4.355);

Arda'nın 10'u numeric(3, 2)'ye sığmaz; virgülden önce yalnız bir basamak var:

SQL
INSERT INTO tft_istatistik    (oyuncu, oynanan_mac, ortalama_sira)VALUES ('Arda', 60, 10);
Hata çıktısı
ERROR:  numeric field overflowDETAIL:  A field with precision 3, scale 2 must round to an absolute value less than 10^1.

Selin'in maç sayısı integer'ın sınırını aşıyor:

SQL
INSERT INTO tft_istatistik    (oyuncu, oynanan_mac, ortalama_sira)VALUES ('Selin', 3000000000, 4.5);
Hata çıktısı
ERROR:  integer out of range
SQL
SELECT * FROM tft_istatistik;
idoyuncuoynanan_macortalama_sira
1Deniz1201.00
2Ece858.00
3Mert404.36

4.355 sessizce 4.36 oldu; hata yok. numeric(3, 2) 9.99'a kadar her değeri kabul eder; yani TFT'de imkânsız olan 9.50 gibi bir ortalama da girer. Değeri 1–8 arasında tutan bir kural (CHECK) Ders 35'te. Selin gibi gerçekten büyük sayılar için sütun bigint olmalıydı.

Görev 5

Riot'un maç kimliği

TFT maçlarını Riot API'sinden çekip saklayacaksın. Riot her maça TR1_1484000001 gibi bir kimlik verir; bu kimlik değişmez ve iki maçta aynı olmaz. Bu sefer birincil anahtar olarak id yerine Riot'un maç kimliğini kullan.

  1. tft_maclari tablosunu kur: mac_id (metin, birincil anahtar), sira (tam sayı), kompozisyon (metin), oynanma (an).
  2. İki maç ekle: TR1_1484000001 (3. olmuşsun, Büyücüler, 10 Ekim 2026 saat 20:15, Türkiye saati) ve TR1_1484000002 (1. olmuşsun, Nişancılar, 11 Ekim 2026 saat 00:40, Türkiye saati).
  3. API'den aynı maç ikinci kez gelmiş gibi, birinci maçı yeniden eklemeyi dene. Ne oluyor, bu iyi mi?
  4. mac_id'siz bir maç eklemeyi dene.
  5. SELECT * ile bak. İkinci maçın tarihi neden 10 Ekim görünüyor?

Bonus soru: Wukong ve Nunu örneklerinde adı anahtar yapmak kötü bir fikirdi. Burada dışarıdan gelen bir kimliği anahtar yapmak neden doğru?

İpucu 1

Türkiye saatini +03 ile yaz: '2026-10-10 20:15:00+03'. Gün.ay.yıl yazma.

İpucu 2

Üçüncü ve dördüncü adımda göreceğin iki hatayı bu derste, şampiyon adını anahtar yaptığında da gördün.

İpucu 3

Gece 00:40 Türkiye saati, UTC'de hangi gün ve saat?

Çözümü göster
SQL
CREATE TABLE tft_maclari (    mac_id text PRIMARY KEY,    sira integer,    kompozisyon text,    oynanma timestamptz);
SQL
INSERT INTO tft_maclari (mac_id, sira, kompozisyon, oynanma)VALUES    ('TR1_1484000001', 3, 'Büyücüler', '2026-10-10 20:15:00+03'),    ('TR1_1484000002', 1, 'Nişancılar', '2026-10-11 00:40:00+03');

Aynı maçı ikinci kez kaydetmek reddedilir. Bu tam istediğimiz şey: API'den aynı maç iki kez gelse de istatistiklerin şişmez.

SQL
INSERT INTO tft_maclari (mac_id, sira, kompozisyon, oynanma)VALUES    ('TR1_1484000001', 3, 'Büyücüler', '2026-10-10 20:15:00+03');
Hata çıktısı
ERROR:  duplicate key value violates unique constraint "tft_maclari_pkey"DETAIL:  Key (mac_id)=(TR1_1484000001) already exists.
SQL
INSERT INTO tft_maclari (sira, kompozisyon)VALUES (5, 'Savaşçılar');
Hata çıktısı
ERROR:  null value in column "mac_id" of relation "tft_maclari" violates not-null constraintDETAIL:  Failing row contains (null, 5, Savaşçılar, null).
SQL
SELECT * FROM tft_maclari;
mac_idsirakompozisyonoynanma
TR1_14840000013Büyücüler2026-10-10 17:15:00+00
TR1_14840000021Nişancılar2026-10-10 21:40:00+00

İkinci maçı 11 Ekim gece 00:40'ta oynadın; UTC'de saat o an 10 Ekim 21:40'tı. Veri doğru, yalnız gösterim UTC. Sınır durumu bu: gece yarısından sonraki üç saatte oynanan her maç, UTC'de bir önceki güne düşer. Maçları güne göre sayarken bunu hatırla.

Bonus: Şampiyonun adı oyunda görünen bir addır ve Nunu'da olduğu gibi değişebilir. Maç kimliği ise hiç değişmez, benzersizliğini Riot garanti eder ve zaten anlamsız bir koddur. Dışarıdan gelen, değişmeyen kimlikler iyi birincil anahtardır. Kendi ürettiğin kayıtlar için (şampiyon havuzu gibi) ise Postgres'in ürettiği id'yi kullan.

Kendini sına

Soru 1

Ders 29'daki degistir sırası kayıp güncellemeyi bitirmişti. API birden çok kopyayla çalışınca sorun neden geri gelir?

Cevabı göster

Sıra, programın belleğinde duran bir Promise zinciriydi; yalnız o programın kendi işlerini sıraya koyar. İki kopya çalışınca her birinin ayrı bir sırası olur. A kopyasındaki istek ile B kopyasındaki istek aynı dosyayı yine aynı anda okuyup yazar; biri ötekini ezer. Kopyalar diski paylaşmıyorsa her biri kendi dosyasını görür ve veri kopyalar arasında bölünür. Veritabanında ise bütün kopyalar aynı sunucuya bağlanır ve aynı anda gelen yazmaları o sunucu düzenler.

Soru 2

Bu sorgu ne döndürür?

SQL
SELECT 7 / 2 AS sonuc;
Cevabı göster

3. İki tam sayıyı böldüğünde Postgres sonucu da tam sayı yapar ve küsuratı atar; JavaScript'teki gibi 3.5 vermez:

SQL
SELECT 7 / 2 AS sonuc;
sonuc
3

Sayılardan biri ondalıklıysa sonuç numeric olur:

SQL
SELECT 7.0 / 2 AS sonuc;
sonuc
3.5000000000000000

Soru 3

Bu komut hata verir. Hangi mesajı alırsın, neden?

SQL
INSERT INTO sampiyon_havuzu (sampiyon, rol)VALUES ('Teemo', "Üst");
Cevabı göster

column "Üst" does not exist. "Üst" çift tırnakta olduğu için Postgres onu bir değer değil, sütun adı sanar. Doğrusu 'Üst'. Tek tırnak değer, çift tırnak ad.

Soru 4

kazanma_orani sütunu numeric(5, 2). Sırasıyla 51.616 ve 1000 yazmaya kalkarsan ne olur?

Cevabı göster

51.616 hata vermez, 51.62 olarak yuvarlanıp kaydedilir. 1000 ise sığmaz: numeric(5, 2) virgülden önce en çok üç basamak tutar (en büyük değer 999.99), Postgres numeric field overflow hatası verir.

Soru 5

Oyuncular tablosunda birincil anahtar olarak hangisini seçerdin: oyuncunun Riot ID'si mi, Postgres'in ürettiği id mi?

Cevabı göster

Postgres'in ürettiği id'yi. Riot ID değiştirilebilir; anahtar olsaydı, değiştiğinde o oyuncuya bağlı bütün kayıtları güncellemen gerekirdi. Riot'un kendisi de oyuncuları değişmeyen puuid ile tanır. Riot ID'yi yine bir sütunda tutarsın, ama anahtar yapmazsın.

Soru 6

sampiyon text PRIMARY KEY olan deneme tablosuna ('', 'Orman') satırını eklemek istesen ne olur? Boş metin NULL mıdır?

Cevabı göster

Satır eklenir. '' boş bir metindir, yani uzunluğu sıfır olan bir değer; NULL ise "değer yok" demektir. Birincil anahtar yalnız NULL'u ve tekrarı reddeder, boş metni kabul eder. Boş metni de engelleyen bir kuralı (CHECK) Ders 35'te göreceksin.

Soru 7

Ahri'nin maçını '2026-10-09 21:40:00+03' olarak ekledin ama Neon 2026-10-09 18:40:00+00 gösteriyor. Veri bozuldu mu?

Cevabı göster

Hayır. timestamptz bir anı saklar. 21:40 Türkiye saatiyle (+03) ve 18:40 UTC (+00) aynı andır; Neon gösterirken UTC kullanıyor. Veriyi Ders 37'de JavaScript'e aldığında yine aynı an olarak gelecek.

Kayıt noktası

C:\kod\veritabani yeni bir proje klasörü, bu yüzden önce git init gerekiyor. VS Code'un terminalinde, bu klasördeyken:

Bir sonraki derste SELECT'i gerçekten öğreneceksin: bir maç geçmişi tablosundan istediğin satırları ve sütunları seçecek, sıralayacak, sayacak ve gruplayacaksın.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.