Veritabanı Yönetim Sistemleri

Veritabanı Şeması Tasarlama

Dr. Öğr. Üyesi Oğuz Emre Kural

2026

Öğrenme Kazanımları

Bu dersin sonunda:

  • 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
CREATE TABLE Album (
    AlbumId INTEGER NOT NULL,
    Title NVARCHAR(160) NOT NULL,
    ArtistId INTEGER NOT NULL,
    PRIMARY KEY (AlbumId),
    FOREIGN KEY (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ı

Yolcu ve durak arasındaki çoka çok ilişki Rider üstte, Station altta. Bir duraktan sıfır veya çok yolcu binebilir; bir yolcu bir veya çok duraktan binebilir. Rider Station biner

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:

CREATE TABLE 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

CREATE TABLE 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

CREATE TABLE stations (
    id,
    name,
    district
);

Bu tablo:

  • durak kimliği,
  • durak adı,
  • bulunduğu ilçe

bilgilerini tutar.

boardings Tablosunu Oluşturmak

CREATE TABLE boardings (
    id,
    rider_id,
    station_id
);

Bir satır bir binişi temsil eder. id olayın kendisini ayırt eder.

rider_id  → riders.id
station_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:

CREATE TABLE riders (id, name);

ve:

CREATE TABLE 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ş metin
NULL : değer yok / bilinmiyor

İlçesi henüz bilinmeyen durakları ararken = NULL değil, IS NULL kullanılır.

SELECT * FROM stations WHERE district IS NULL;

INTEGER

Tam sayılar için kullanılır.

1
42
2026
-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.5
3.14159
2.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        → INTEGER
3.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:

CREATE TABLE demo (amount TEXT);
INSERT INTO demo VALUES (10);
SELECT amount, typeof(amount) FROM demo;

10gönderilen: integer amount TEXTsütunun tercihi 10saklanan: text

Ters Dönüşüm

Bir INTEGER sütununa sayıya çevrilebilen metin yazarsak:

CREATE TABLE demo_integer (amount INTEGER);
INSERT INTO demo_integer VALUES ('25');
SELECT amount, typeof(amount) FROM demo_integer;
25 | integer

'yirmi beş' ise kendiliğinden sayıya dönüşmez. Affinity, tek başına katı doğrulama değildir.

Şemaya Veri Türleri Eklemek

Daha anlamlı bir riders tablosu:

CREATE TABLE riders (
    id INTEGER,
    name TEXT
);

stations Şeması

CREATE TABLE stations (
    id INTEGER,
    name TEXT,
    district TEXT
);

Burada:

  • id → tam sayı
  • name → metin
  • district → ilçenin adı

olarak tasarlanmıştır.

boardings Şeması

CREATE TABLE boardings (
    id INTEGER,
    rider_id INTEGER,
    station_id INTEGER
);

id biniş olayını, diğer iki sütun bağlı kayıtları gösterecek. Henüz anahtar kısıtları eklenmedi.

DROP TABLE

Bir tabloyu yapısıyla ve içindeki satırlarla birlikte silmek için:

DROP TABLE boardings;
DROP TABLE stations;
DROP TABLE riders;

DROP TABLE geri alma planı olmadan ders dışındaki bir veritabanında denenmemelidir. Burada yalnızca kendi boş ulasim.db taslağımızı temizliyoruz.

Şemayı Dosyada Tutmak

Az önce yazdığımız üç CREATE TABLE ifadesini bir metin dosyasına kaydedelim:

schema.sql
├─ CREATE TABLE riders (...);
├─ CREATE TABLE stations (...);
└─ CREATE TABLE boardings (...);

Bu bir dosya planı; çalıştırılabilir SQL kodu önceki slaytlardaki tam tablo tanımlarıdır. Şema değiştiğinde bütün tanımları tek yerde görürüz.

schema.sql Dosyasını Çalıştırmak

SQLite içinde:

.read schema.sql

.read dosyadaki SQL ifadelerini bulunduğunuz veritabanında çalıştırır. Dosya aynı klasörde olmalı; tablolar zaten varsa CREATE TABLE hata verir.

Ardından .schema ile üç tabloyu kontrol edin. schema.sql ekranda belge, ulasim.db ise oluşturulan veritabanıdır.

Tablo Kısıtları (Constraints)

Şimdiye kadar sütunları ve veri türlerini tanımladık. Şema, hangi kayıtların kabul edileceğini de söylemeli:

  • Aynı riders.id iki yolcuya verilmemeli.
  • Bir biniş, olmayan bir yolcuya bağlanmamalı.
  • stations.name değeri NULL olmamalı.

Bu kurallara kısıt (constraint) denir. Sütunun yanındaki yazım column constraint, sütunlardan sonraki yazım table constraint olarak adlandırılır.

Primary Key: Yolcu Kimliği

İki yolcunun adı aynı olabilir. Her kayda değişmeyen bir kimlik vermek gerekir. Primary key bunu şemada tanımlar.

Örneğin:

CREATE TABLE riders (
    id INTEGER PRIMARY KEY,
    name TEXT
);

id, bu tablodaki bir yolcuyu tekil olarak bulur. Aynı sayı stations.id içinde de geçebilir; her tablo kendi kimlik alanına sahiptir.

stations Primary Key

CREATE TABLE stations (
    id INTEGER PRIMARY KEY,
    name TEXT,
    district TEXT
);

Aynı id değeri iki farklı durakta kullanılamaz.

Birleşik Primary Key

Bir primary key birden fazla sütundan oluşabilir.

CREATE TABLE boardings (
    rider_id INTEGER NOT NULL,
    station_id INTEGER NOT NULL,
    PRIMARY KEY(rider_id, station_id)
);

Bu durumda:

(rider_id, station_id)

kombinasyonu benzersiz olmak zorundadır. Bu tasarımda aynı yolcunun aynı duraktaki ikinci binişi kaydedilemez.

Aynı Duraktan İkinci Biniş

Deniz pazartesi ve salı günü OMÜ Rektörlük durağından binebilir.

biniş rider_id station_id
Pazartesi 4 2
Salı 4 2

Her iki biniş de ayrı bir olay. Şu anahtar ikisini de kabul eder mi?

PRIMARY KEY(rider_id, station_id)

İlişki tekrarlanabiliyor. Her binişe ayrı kimlik vermeliyiz.

Binişin Kendi Kimliği

Alternatif olarak boardings tablosuna ayrı bir id eklenebilir.

CREATE TABLE boardings (
    id INTEGER PRIMARY KEY,
    rider_id INTEGER NOT NULL,
    station_id INTEGER NOT NULL
);

id, biniş olayını benzersiz biçimde tanımlar. (rider_id, station_id) ikilisi farklı günlerde tekrarlanabilir.

SQLite rowid

SQLite çoğu normal tabloda her satıra örtük bir rowid verir.

id INTEGER PRIMARY KEY tanımladığımızda id, bu değerin açık adlı karşılığı olur.

Şemada görünen id, tabloyu okuyanlar ve uygulama kodu için daha belirgindir. Örtük rowid ise SQLite’a özgü bir ayrıntıdır.

Foreign Key: Biniş Kime Ait?

boardings.rider_id = 4 yazdığımızda riders tablosunda 4 numaralı bir yolcu bulunmalıdır. Foreign key, bu bağlantıyı şemada tanımlar.

FOREIGN KEY(rider_id)
REFERENCES riders(id)

Foreign Key ile boardings

CREATE TABLE boardings (
    id INTEGER PRIMARY KEY,
    rider_id INTEGER NOT NULL,
    station_id INTEGER NOT NULL,

    FOREIGN KEY(rider_id) REFERENCES riders(id),
    FOREIGN KEY(station_id) REFERENCES stations(id)
);

Bu tasarım:

boardings.rider_id  → riders.id
boardings.station_id → stations.id

ilişkilerini tanımlar.

SQLite’ta Foreign Key Denetimi

İ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

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:

PRIMARY KEY(id)
FOREIGN KEY(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 NOT NULL

Örneğin:

CREATE TABLE stations (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    district TEXT NOT NULL
);

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 NOT NULL UNIQUE

Örneğin:

CREATE TABLE stations (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    district TEXT NOT NULL
);

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.

ridersboardingsstations
cardsswipesstations

Bundan sonra her swipe bir kartın işlemidir. Şemayı kart hareketlerine göre kuracağız.

Kart–İşlem–Durak ER Diyagramı

Kart, işlem ve durak varlıkları Bir kart sıfır veya çok işlem yapar; her işlem tam olarak bir karta aittir. Bir durakta sıfır veya çok işlem olur; bir işlem en çok bir durakta gerçekleşir. Card Swipe Station id id type datetime amount_kurus id name district yapar gerçekleşir

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.

Yeni Varlıklar

Tablo Bir satır neyi temsil eder? Temel alanlar
cards Bir ulaşım kartı id
stations Bir durak id, name, district
swipes Bir kart işlemi id, card_id, station_id, type, datetime, amount_kurus

swipes.card_id kartı, swipes.station_id durağı gösterir.

swipes Tablosu

Bir kartın iki işlem kaydı:

id card_id station_id type datetime amount_kurus
901 17 2 boarding 2026-09-01 08:15:00 -3900
902 17 3 refund 2026-09-01 08:45:00 900

100,00 TL bakiye binişte 61,00 TL’ye düşer, 9,00 TL iadeyle 70,00 TL’ye çıkar. Net ücret: 30,00 TL.

Yeni Şema

CREATE TABLE cards (
    id INTEGER PRIMARY KEY
);

CREATE TABLE stations (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    district TEXT NOT NULL
);

swipes Şeması

CREATE TABLE swipes (
    id INTEGER PRIMARY KEY,
    card_id INTEGER NOT NULL,
    station_id INTEGER,
    type TEXT,
    datetime TEXT,
    amount_kurus INTEGER,

    FOREIGN KEY(card_id) REFERENCES cards(id),
    FOREIGN KEY(station_id) REFERENCES stations(id)
);

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 DEFAULT CURRENT_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.

DEFAULT CURRENT_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 INTEGER CHECK(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 (
    type IN ('boarding', 'refund', 'topup')
)

Bu sayede:

boarding  → biniş
refund    → ücret iadesi
topup     → bakiye yükleme

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 NOT NULL
    CHECK(type IN ('boarding', 'refund', 'topup')),
datetime TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
amount_kurus INTEGER NOT NULL
    CHECK(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)
),
FOREIGN KEY(card_id) REFERENCES cards(id),
FOREIGN KEY(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;

CREATE TABLE cards (
    id INTEGER PRIMARY KEY
);

CREATE TABLE stations (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    district TEXT NOT NULL
);

CREATE TABLE swipes (
    id INTEGER PRIMARY KEY,
    card_id INTEGER NOT NULL,
    station_id INTEGER,
    type TEXT NOT NULL
        CHECK(type IN ('boarding', 'refund', 'topup')),
    datetime TEXT NOT NULL
        DEFAULT CURRENT_TIMESTAMP,
    amount_kurus INTEGER NOT NULL
        CHECK(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)
    ),
    FOREIGN KEY(card_id) REFERENCES cards(id),
    FOREIGN KEY(station_id) REFERENCES stations(id)
);

Çalışan Şema Dosyası

Dersin son şemasını indirin. Dosyayı boş bir çalışma klasörüne koyun.

sqlite3 ulasim-final.db
.read lecture3-schema.sql
PRAGMA foreign_keys;
.schema swipes

Dosya foreign key denetimini açar; PRAGMA foreign_keys; çıktısı 1 olmalıdır. ulasim-final.db yenidir; önceki ulasim.db değişmez.

ALTER TABLE

Mevcut bir tablonun şemasını değiştirmek için ALTER TABLE kullanılır. Son oluşturduğumuz swipes tablosuna dokunmadan, boş bir deneme tablosu açalım:

CREATE TABLE boardings_demo (
    id INTEGER PRIMARY KEY,
    rider_id INTEGER
);

Bu örnekte sırayla tabloyu yeniden adlandırıp sütun ekleyecek, adını düzeltecek ve sileceğiz.

Tabloyu Yeniden Adlandırmak

ALTER TABLE boardings_demo
RENAME TO swipes_demo;

Tablo adı swipes_demo olur; sütunlar ve mevcut satırlar değişmez.

Yeni Sütun Eklemek

ALTER TABLE swipes_demo
ADD COLUMN 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:

ALTER TABLE swipes_demo
RENAME COLUMN ttpe TO type;

kullanılabilir.

Sütun Silmek

ALTER TABLE swipes_demo
DROP COLUMN type;

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öçü.

ALTER TABLE ...

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.

PRIMARY KEY(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 NOT NULL UNIQUE

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?

ALTER TABLE swipes_demo
ADD COLUMN note TEXT;

Yeni sütun eski satırlarda NULL olur. Şimdilik NOT NULL istemiyoruz.

Tasarımda Temel İlkeler

İyi bir şema tasarımında:

  1. Gerçek dünyadaki varlıkları belirle.
  2. Her varlığı uygun bir tabloya dönüştür.
  3. Sütunlara uygun veri türleri / affinity ata.
  4. Her tablo için uygun primary key belirle.
  5. Tabloları foreign key ile ilişkilendir.
  6. NOT NULL, UNIQUE, CHECK, DEFAULT ile veri bütünlüğünü güçlendir.
  7. 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.