Tek tablodaki veri tekrarını ve olası tutarsızlığı bir örnekte gösterebilirsiniz.
Bir tasarım fikrini tablolara, sütunlara ve ilişkilere dönüştürebilirsiniz.
SQLite’ta storage class ile type affinity arasındaki farkı açıklayabilirsiniz.
CREATE TABLE içinde anahtarları ve veri bütünlüğü kurallarını tanımlayabilirsiniz.
.schema, .read, DROP TABLE ve ALTER TABLE komutlarını ne zaman kullanacağınızı seçebilirsiniz.
Geçen Dersten Bu Derse
Chinook’ta Track, Album ve Artist tablolarını kullandık. O tablolar hazırdı.
Şimdi bir veritabanını kendimiz tasarlayacağız:
Hangi bilgiyi hangi tabloda tutacağız? Hangi değerleri zorunlu kılacağız?
Şema Neyi Tanımlar?
Şema, veritabanındaki tabloların, sütunların, ilişkilerin ve kuralların tanımıdır.
Örneğin Album için şu kararlar şemanın parçasıdır:
Title metin olarak saklanır.
AlbumId her albümü ayırt eder.
ArtistId bir sanatçı kaydına bağlanır.
Satırların kendisi şema değildir. Greatest Hits I bir veri; onu tutacak sütunun tanımı şemadır.
Hazır Şemayı Okuyalım
Geçen dersin chinook.db dosyasını açın:
sqlite3 -readonly chinook.db.schema Album
CREATETABLE Album ( AlbumId INTEGERNOTNULL, Title NVARCHAR(160) NOTNULL, ArtistId INTEGERNOTNULL,PRIMARYKEY (AlbumId),FOREIGNKEY (ArtistId) REFERENCES Artist(ArtistId));
Bu, Chinook şemasının okunması kolaylaştırılmış bir özetidir. Birazdan kendi CREATE TABLE ifadelerimizi yazacağız.
Tasarım Problemi: SAMULAŞ Binişleri
Başlangıç bakiyesi 100,00 TL olan kartların hareketlerini tek tabloda topladığımızı düşünelim:
olay
yolcu
durak
işlem
tutar (TL)
kalan (TL)
101
Deniz
OMÜ Rektörlük
biniş
−39,00
61,00
102
Deniz
Türkiş
iniş (iade)
+9,00
70,00
103
Deniz
Türkiş
biniş
−39,00
31,00
104
Ayşe
OMÜ Rektörlük
biniş
−39,00
61,00
105
Ayşe
Türkiş
iniş (iade)
+9,00
70,00
1–24 istasyonluk yolculukta, inişten sonra iade cihazına kart okutulunca 9,00 TL geri yüklenir.
Aynı Bilgi Kaç Kez Tutuluyor?
Tabloda Deniz ve Türkiş tekrar ediyor. Yeni binişlerle tekrar sayısı büyür.
Türkiş → Türk-İş
Adı tek satırda değiştirirsek aynı durak iki farklı adla görünür. Buna güncelleme anomalisi deriz.
Yeni açılan bir durağı henüz kimse kullanmadıysa bu tabloda onu nereye yazacağız? Bu da ekleme anomalisidir.
Önce Varlıkları Ayıralım
Bu kayıtlarda yolcuyu, durağı ve işlemi birbirinden ayırabiliriz. Önce binişleri modelleyelim. İade hareketine dersin ilerleyen bölümünde döneceğiz.
Varlık / olay
Kendi bilgisi
Yolcu
Yolcunun kimliği ve adı
Durak
Durağın kimliği ve adı
Biniş
Hangi yolcu, hangi durak?
Biniş ile yolcu ve durak arasında nasıl bağlantı kuracağımıza bakalım.
Yolcu ile Durağın İlişkisi
Üç biniş kaydını yolcu ve durak arasında bağlantı olarak gösterelim.
101: Deniz, OMÜ Rektörlük'ten bindi.
103: Deniz, başka bir gün Türkiş'ten de bindi. Bir yolcu birden fazla durak kullanabiliyor.
104: Ayşe de OMÜ Rektörlük'ten bindi. Aynı durakta birden fazla yolcu var.
Yolcu ile durak arasındaki ilişki çoka çok. Her bağlantıyı ayrı bir biniş olarak kaydedeceğiz.
Yolcu–Durak ER Diyagramı
Yolcuyu ilk binişinde kaydediyoruz: Bir yolcu 1..N duraktan binebilir. Bir duraktan ise 0..N yolcu binebilir.
Tek Tablodan Üç Tabloya
İlk taslaktaki binişler
olay
yolcu
durak
101
Deniz
OMÜ Rektörlük
103
Deniz
Türkiş
104
Ayşe
OMÜ Rektörlük
riders
id
name
1
Deniz
2
Ayşe
stations
id
name
2
OMÜ Rektörlük
3
Türkiş
boardings
id
rider_id
station_id
101
1
2
103
1
3
104
2
2
Normalizasyon
Normalizasyon, gereksiz tekrarı ve bundan doğan tutarsızlıkları azaltmak için veriyi uygun tablolara ayırma sürecidir.
Burada yolcunun adı riders tablosunda, durağın adı stations tablosunda durur. boardings yalnızca binişin kendisini ve ilişkili kimlikleri tutar.
Tek bir “her tablo tek varlık” kuralı tüm tasarım sorunlarını çözmez. Hangi bilginin hangi kayda ait olduğunu ve tekrarın neye mal olduğunu sorgulamak gerekir.
Ara Tablo: boardings
Az önceki üç bağlantı, boardings tablosunda üç satır olur:
id
rider_id
station_id
101
1
2
103
1
3
104
2
2
rider_id yolcuyu, station_id durağı gösterir. Ayrı id, aynı yolcunun aynı duraktan sonraki binişlerini ayırt eder.
Boş Bir Veritabanıyla Başlayalım
Terminalde bu ders için ayrı bir çalışma klasörü açın ve o klasörde:
sqlite3 ulasim.db.headers on.mode box.tables
Henüz tablo oluşturmadık; .tables çıktı vermez. Dosya yoksa SQLite bu adla yeni bir veritabanı açar.
CREATE TABLE
CREATE TABLE, tabloyu ve sütunlarını tanımlar:
CREATETABLE riders (id, name);
Parantez içindeki tanımlar virgülle ayrılır. Son tanımdan sonra virgül yoktur; noktalı virgül SQL ifadesini bitirir.
riders Tablosunu Oluşturmak
CREATETABLE riders (id, name);
riders tablo adı; id ve name sütun adlarıdır. Veri türleri ve kısıtları biraz sonra ekleyeceğiz.
.schema riders
CREATE TABLE çıktı vermez. .schema riders ile oluşan tanımı görürüz.
stations Tablosunu Oluşturmak
CREATETABLE stations (id, name, district);
Bu tablo:
durak kimliği,
durak adı,
bulunduğu ilçe
bilgilerini tutar.
boardings Tablosunu Oluşturmak
CREATETABLE boardings (id, rider_id, station_id);
Bir satır bir binişi temsil eder. id olayın kendisini ayırt eder.
rider_id → riders.idstation_id → stations.id
Bu aşamada sayıların başka tablolara ait olduğunu henüz şemaya söylemedik. Yabancı anahtarları birazdan tanımlayacağız.
SQL Biçimlendirme
Aşağıdaki iki ifade SQL açısından eşdeğerdir:
CREATETABLE riders (id, name);
ve:
CREATETABLE riders (id, name);
İkinci biçim daha okunaklıdır.
Girintileme SQL tarafından zorunlu tutulmaz; kod okunabilirliği için kullanılır.
Veri Türleri ve Storage Classes
SQLite’ta her değer şu beş saklama sınıfından (storage class) birine sahiptir:
Storage Class
Kullanım
NULL
Değer yok
INTEGER
Tam sayılar
REAL
Ondalıklı sayılar
TEXT
Metin
BLOB
Binary Large Object
Sütuna yazılan tür adıyla değerin gerçekten nasıl saklandığı aynı şey değildir.
NULL
NULL, bir değerin bulunmadığını ya da bilinmediğini ifade eder.
0 : bilinen sayı'' : boş metinNULL : değer yok / bilinmiyor
İlçesi henüz bilinmeyen durakları ararken = NULL değil, IS NULL kullanılır.
SELECT*FROM stations WHERE district ISNULL;
INTEGER
Tam sayılar için kullanılır.
1422026-15
Örnek alanlar:
veritabanı içindeki kimlik
ürün adedi
yaş
kuruş cinsinden fiyat
REAL
Ondalıklı sayılar için kullanılır.
1.53.141592.40
İkili kayan nokta gösterimi 0.1 gibi birçok ondalık değeri tam olarak temsil edemez.
Bu nedenle özellikle para değerlerinde tasarım dikkatli yapılmalıdır.
Para Değerlerini Nasıl Saklamalıyız?
Bir ücret değerini farklı şekillerde saklayabiliriz:
Değer
Tür
Sonuç
10 kuruş
INTEGER
Kesin tam sayı hesabı
"0,10 TL"
TEXT
Okunur; doğrudan hesaplanmaz
0.10 TL
REAL
Hesaplanır; yuvarlama gerekir
Önce birimi belirleyin: 10 neyin 10’u?
Para İçin INTEGER Yaklaşımı
Bu dersteki kart hareketlerini kuruş cinsinden INTEGER olarak saklayacağız.
Örneğin:
39,00 TL → 3900 kuruş9,00 TL → 900 kuruş
Binişte 3900 kuruş düşüp kısa yolculukta 900 kuruş iade edilirse net ücret 3000 kuruştur (30,00 TL).
Bu tercih kadar, birimlerin açıkça belgelenmesi de önemlidir.
TEXT
Karakter dizilerini saklamak için kullanılır.
Örneğin:
'Atakum''OMÜ Rektörlük''Cumhuriyet Meydanı'
SQL’de metin değerleri tek tırnakla yazılır: 'Atakum'. Çift tırnak ise gerektiğinde sütun ve tablo adlarını sınırlar.
BLOB
Binary Large Object anlamına gelir.
Binary veriyi değiştirmeden saklamak için kullanılabilir.
Örneğin:
resim
ses
video
binary dosyalar
Ancak büyük dosyaların doğrudan veritabanında tutulup tutulmaması ayrıca bir sistem tasarımı kararıdır.
Storage Class ve Data Type
SQLite’ta storage class, belirli bir değerin nasıl saklandığını ifade eder.
Örneğin:
42 → INTEGER3.14 → REAL"OMÜ Rektörlük" → TEXT
SQLite bir INTEGER değerini ihtiyaca göre farklı byte uzunluklarında saklayabilir.
Bu fiziksel ayrıntıyı SQLite kendisi yönetir.
Type Affinity
SQLite’ta normal tablolardaki sütunların bir type affinity değeri vardır: eklenecek değeri hangi sınıfa dönüştürmeyi tercih ettikleri.
Başlıca affinity türleri:
TEXT
NUMERIC
INTEGER
REAL
BLOB
Sütun türü yazılmazsa affinity BLOB olur. Bu, diğer sınıflara dönüştürmeyi zorlamaz. NUMERIC varsayılan değildir.
Type Affinity Örneği
Bir TEXT sütununa 10 sayısı yazıldığında SQLite bunu metne çevirir:
İlişkiyi tanımlamak yetmez. SQLite bağlantısında denetimi açıp durumunu kontrol edin:
PRAGMA foreign_keys =ON;PRAGMA foreign_keys;
İkinci komut 1 döndürmelidir. Bundan sonra olmayan bir rider_id ile boardings satırı eklemek reddedilir.
Olmayan Yolcuya Biniş
PRAGMA foreign_keys = ON açık. Aşağıdaki yeni boardings satırı kaydedilebilir mi?
riders
id
name
1
Deniz
2
Ayşe
boardings için yeni satır
id
rider_id
station_id
106
4
2
riders tablosunda id = 4 yok.
FOREIGN KEY denetimi satırı reddeder.
Table Constraint
Sütun tanımlarından sonra yazılan, bir veya birkaç sütunu ilgilendiren kural table constraint biçimindedir.
Örnekler:
PRIMARYKEY(id)
FOREIGNKEY(station_id)REFERENCES stations(id)
PRIMARY KEY(rider_id, station_id) gibi çok sütunlu bir kural bu biçimde yazılır.
Column Constraint
Sütun tanımının yanına yazılan kural column constraint biçimindedir.
SQLite’ta sık kullanılanlar:
NOT NULL
UNIQUE
CHECK
DEFAULT
id INTEGER PRIMARY KEY de sütun yanında yazılmış bir anahtar tanımıdır. Fark çoğu zaman nereye yazıldığıdır.
NOT NULL: Gerekli Alan
NOT NULL, sütunun NULL olmasını engeller.
name TEXT NOTNULL
Örneğin:
CREATETABLE stations (idINTEGERPRIMARYKEY, name TEXT NOTNULL, district TEXT NOTNULL);
Durak adı ve ilçesi girilmelidir. Ancak '' boş metni NULL değildir; NOT NULL tek başına boş metni engellemez.
UNIQUE
Bir sütundaki değerlerin tekrarlanmamasını sağlar.
name TEXT NOTNULLUNIQUE
Örneğin:
CREATETABLE stations (idINTEGERPRIMARYKEY, name TEXT NOTNULLUNIQUE, district TEXT NOTNULL);
Aynı durak adı ikinci kez eklenemez. UNIQUE tek başına NULL değerlerini yasaklamaz; bu yüzden yanında NOT NULL var.
Primary Key ve SQLite Ayrıntısı
Bu dersteki id INTEGER PRIMARY KEY tanımı:
aynı id değerinin tekrarını engeller,
SQLite’ın örtük rowid değerini bu adla erişilebilir kılar.
Ancak SQLite’ta diğer bazı primary key biçimleri tarihsel bir istisna nedeniyle NULL kabul edebilir. Genel bir “primary key her durumda NOT NULL yapar” kuralı kurmayın.
Bizim şemamızda INTEGER PRIMARY KEY ve diğer gerekli alanlar için açık NOT NULL kullanacağız.
Modeli Yeniden Düşünelim
İlk tasarım yolcuyu tanıyordu. SAMKART okutulduğunda turnike kişinin adını değil kartı okur. İsimsiz kartlar da kullanılabilir.
riders→boardings→stations
cards→swipes→stations
Bundan sonra her swipe bir kartın işlemidir. Şemayı kart hareketlerine göre kuracağız.
Kart–İşlem–Durak ER Diyagramı
Bir kart 0..N işlem yapar; her işlem tam olarak bir karta aittir. Bir durakta 0..N işlem olur; bir işlem en çok bir durakta gerçekleşir.
Çizgiler tabloya card_id ve station_id olarak girecek; bu yüzden kutularda ayrıca yazmadık.
Bu ilk sürüm, tabloları bağlar ama type, zaman ve tutar kurallarını henüz koymaz.
DEFAULT
DEFAULT, yeni satırda sütun atlanırsa kullanılacak değeri tanımlar.
Örneğin:
datetime TEXT DEFAULTCURRENT_TIMESTAMP
datetime açıkça NULL verilirse DEFAULT devreye girmez. Bunu NOT NULL ile birlikte düşünmeliyiz.
CURRENT_TIMESTAMP
CURRENT_TIMESTAMP, SQLite’ın saat bilgisinden ürettiği özel bir varsayılan ifadedir.
DEFAULTCURRENT_TIMESTAMP
UTC zamanında şu biçimde bir TEXT değer üretir:
2026-09-01 19:42:18
Ekrandaki tarih bir örnektir; sorguyu çalıştırdığınız anın saati değildir. Yerel saat gerektiğinde dönüşüm ayrıca yapılır.
CHECK
CHECK, yeni veya güncellenen değerin bir koşulu sağlamasını ister.
CHECK (expression)
Yalnızca binişleri düşünürsek aklımıza şu kural gelebilir:
amount_kurus INTEGERCHECK(amount_kurus <0)
Ancak karta para yüklediğimizde veya seyahat sonunda ücret iadesi aldığımızda bakiye artar. Bu kural geçerli iki işlemi de reddeder. Tutarı işlem türüyle birlikte kontrol etmeliyiz.
CHECK ile Değer Kümesini Sınırlamak
Bu örnekte kart işlemi üç türden biri olabilir:
type TEXT CHECK (typeIN ('boarding', 'refund', 'topup'))
dışında bir metin kabul edilmez. type alanı boş bırakılamasın istiyorsak NOT NULL da ekleriz.
Tutar Kuralı İşleme Bağlı
İşlem
Kart bakiyesi
amount_kurus
boarding (biniş)
Azalır
Negatif
refund (iade)
Artar
Pozitif
topup (yükleme)
Artar
Pozitif
CHECK ( (type='boarding'AND amount_kurus <0) OR (type='refund'AND amount_kurus >0) OR (type='topup'AND amount_kurus >0))
Bu kural, kart bakiyesindeki değişimi temsil eder. Gerçek iade miktarını hesaplamaz; kart türü ve yolculuk bilgisi ayrıca gerekir.
swipes: Sütun Kuralları
type TEXT NOTNULLCHECK(typeIN ('boarding', 'refund', 'topup')),datetime TEXT NOTNULLDEFAULTCURRENT_TIMESTAMP,amount_kurus INTEGERNOTNULLCHECK(typeof(amount_kurus) ='integer')
İşlem türü üç seçenekten biri olmalı; zaman verilmezse varsayılan değer kullanılmalı; tutar gerçekten tam sayı olarak saklanmalı.
swipes: Tablonun Bütünüyle İlgili Kurallar
CHECK ( (type='boarding'AND amount_kurus <0) OR (type='refund'AND amount_kurus >0) OR (type='topup'AND amount_kurus >0)),FOREIGNKEY(card_id) REFERENCES cards(id),FOREIGNKEY(station_id) REFERENCES stations(id)
İlk kural iki sütunu birlikte değerlendirir; diğer ikisi kart ve durak bağlantısını korur.
station_id isteğe bağlıdır: online yükleme duraktan bağımsız olabilir. Biniş ve iade için durak istemek ayrı bir kural gerektirir.
Hangi Satır Kabul Edilir?
Deneme
Sonuç
Hangi kural?
type = 'boarding', amount_kurus = -3900
Kabul
Kurallara uygun
type = 'flying'
Ret
CHECK(type IN (...))
type = 'boarding', amount_kurus = 0
Ret
Tür–tutar kuralı
type = 'refund', amount_kurus = 900
Kabul
Tür–tutar kuralı
card_id = 999, kart 999 yok
Ret*
FOREIGN KEY
*Son satır yalnızca bu bağlantıda PRAGMA foreign_keys = ON ise reddedilir.
Veri Bütünlüğü
Veri bütünlüğü, kaydedilen bilgilerin tanımladığımız kurallara uymasını sağlamaktır.
NOT NULL eksik alanı, UNIQUE tekrarı, CHECK geçersiz değeri, etkin FOREIGN KEY olmayan bağlantıyı yakalar.
Hiçbir kısıt kendi başına tüm iş kurallarını çözmez. Önce hangi durumları kabul edeceğimizi açıkça tarif etmeliyiz.
Şemanın Son Hali
Adım adım kurduğumuz üç tablo, tek dosyada. Slayt aşağı kaydırılabilir.
-- Ders 3, SAMULAŞ örneği: yeni ve boş bir SQLite veritabanında çalıştırın.-- Bu komut yalnızca mevcut SQLite bağlantısı için geçerlidir.PRAGMA foreign_keys =ON;CREATETABLE cards (idINTEGERPRIMARYKEY);CREATETABLE stations (idINTEGERPRIMARYKEY, name TEXT NOTNULLUNIQUE, district TEXT NOTNULL);CREATETABLE swipes (idINTEGERPRIMARYKEY, card_id INTEGERNOTNULL, station_id INTEGER,type TEXT NOTNULLCHECK(typeIN ('boarding', 'refund', 'topup')), datetime TEXT NOTNULLDEFAULTCURRENT_TIMESTAMP, amount_kurus INTEGERNOTNULLCHECK(typeof(amount_kurus) ='integer'),CHECK ( (type='boarding'AND amount_kurus <0) OR (type='refund'AND amount_kurus >0) OR (type='topup'AND amount_kurus >0) ),FOREIGNKEY(card_id) REFERENCES cards(id),FOREIGNKEY(station_id) REFERENCES stations(id));
Bu örnekte sırayla tabloyu yeniden adlandırıp sütun ekleyecek, adını düzeltecek ve sileceğiz.
Tabloyu Yeniden Adlandırmak
ALTERTABLE boardings_demoRENAMETO swipes_demo;
Tablo adı swipes_demo olur; sütunlar ve mevcut satırlar değişmez.
Yeni Sütun Eklemek
ALTERTABLE swipes_demoADDCOLUMN ttpe TEXT;
Yeni sütunun adı yanlışlıkla ttpe yazıldı. .schema swipes_demo ile kontrol edin.
Sütunu Yeniden Adlandırmak
Yanlışlıkla:
ttpe
adı verilmişse:
ALTERTABLE swipes_demoRENAMECOLUMN ttpe TOtype;
kullanılabilir.
Sütun Silmek
ALTERTABLE swipes_demoDROPCOLUMNtype;
Bu işlem sütunu içindeki verilerle birlikte kaldırır.
SQLite, sütun bir anahtar, indeks veya başka bir şema nesnesinde kullanılıyorsa silmeyi reddedebilir.
ALTER TABLE mı, Şemayı Yeniden Oluşturmak mı?
Geliştirme sırasında iki ayrı ihtiyaç vardır:
Mevcut veriyi koruyarak küçük değişiklik:ALTER TABLE ve gerekirse veri göçü.
ALTERTABLE...
Boş deneme veritabanını yeniden kurma:schema.sql dosyasını düzenleyip yeni bir *.db dosyasında çalıştırma.
DROP TABLE ve yeniden kurma, veri içeren bir veritabanında doğrudan “kolay yol” değildir.
SQLite ve Diğer DBMS’ler
PostgreSQL, MySQL ve MariaDB’de de tablo, anahtar ve kısıt kavramlarını kullanırız.
Fakat aynı SQL kodu her sistemde olduğu gibi çalışmayabilir. Özellikle şunları kontrol etmek gerekir:
veri türleri ve otomatik kimlik sözdizimi,
ALTER TABLE ile yapılabilen değişiklikler,
kısıtların uygulanma biçimi.
Alıştırma 1: İkinci Biniş
Deniz pazartesi ve salı günü OMÜ Rektörlük durağından bindi.
PRIMARYKEY(rider_id, station_id)
Bu anahtar iki binişi de saklamaya izin verir mi? Nasıl değiştirirsiniz?
Hayır. İki satırda aynı (rider_id, station_id) ikilisi oluşur. Her binişe ayrı id INTEGER PRIMARY KEY veririz.
Alıştırma 2: Hangi Kural Eksik?
stations.name boş metin '' olarak eklenebiliyor:
name TEXT NOTNULLUNIQUE
NOT NULL varken bu neden oluyor?
'' bir TEXT değeridir; NULL değildir. Boş metin yasaksa ayrıca CHECK(length(trim(name)) > 0) eklenebilir.
Alıştırma 3: Yeni Sütun
Deneme tablosunda swipes_demo için işlem açıklaması tutmak istiyoruz. Eski satırlarda açıklama olmayabilir.
Hangi komut yeterli?
ALTERTABLE swipes_demoADDCOLUMN note TEXT;
Yeni sütun eski satırlarda NULL olur. Şimdilik NOT NULL istemiyoruz.
Tasarımda Temel İlkeler
İyi bir şema tasarımında:
Gerçek dünyadaki varlıkları belirle.
Her varlığı uygun bir tabloya dönüştür.
Sütunlara uygun veri türleri / affinity ata.
Her tablo için uygun primary key belirle.
Tabloları foreign key ile ilişkilendir.
NOT NULL, UNIQUE, CHECK, DEFAULT ile veri bütünlüğünü güçlendir.
Gereksiz veri tekrarını azalt.
Sonuç
Artık hazır bir şemayı okumakla kalmıyor, temelini kendimiz kurabiliyoruz:
CREATE TABLE yapıyı kurar. Veri türü, anahtar ve kısıtlar hangi kayıtların kabul edileceğini belirler. ALTER TABLE mevcut yapıyı değiştirir; veri varsa etkisini ayrıca düşünürüz.
Bir sonraki derste bu tablolara satır ekleyip kayıtları güncelleyecek ve sileceğiz.