Veritabanı Yönetim Sistemleri

Veri Yazmak: INSERT, DELETE, UPDATE

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

2026

Öğrenme Kazanımları

Bu dersin sonunda:

  • INSERT INTO ile tek ve çok satırlı ekleme yapabilir, kısıt hatalarını okuyabilirsiniz.
  • Bir CSV dosyasını .import ile tabloya aktarabilir, kimlik sütunu olmayan veriyi geçici tablo üzerinden ekleyebilirsiniz.
  • DELETE ile satır silebilir, yabancı anahtarın silmeye etkisini ve ON DELETE seçeneklerini açıklayabilirsiniz.
  • UPDATE ile değer düzeltebilir, trim, upper ve LIKE ile dağınık veriyi temizleyebilirsiniz.
  • Bir tetikleyici yazabilir ve kalıcı silme ile yumuşak silme arasında seçim yapabilirsiniz.

Geçen Dersten Bu Derse

Üçüncü haftada üç tabloyu tasarladık: cards, stations, swipes. Şema hazır, tablolar boş.

cardsid
swipesid, card_id, station_id,
type, datetime, amount_kurus
stationsid, name, district

Bugün bu tablolara veri yazacağız: durak ekleyeceğiz, kart işlemleri kaydedeceğiz, hatalı kayıtları düzeltip sileceğiz.

Veriyle Yapılan Dört İş

OkumaSELECTİlk iki haftada yaptık: sorgu, filtre, JOIN.
EklemeINSERTTabloya yeni satır yazar.
SilmeDELETEVar olan satırı kaldırır.
GüncellemeUPDATEVar olan satırın değerini değiştirir.

Okumak için önce veri gerekir. Bu yüzden bugün eklemeyle başlıyoruz.

Bugünün Dosyaları

Boş bir çalışma klasörü açın ve şu dosyaları içine indirin:

Ardından terminalde o klasöre geçin ve yeni bir veritabanı oluşturun:

sqlite3 ulasim.db

Şemayı Yüklemek

.read schema.sql
.tables
cards     stations  swipes

Her oturumda bir satır daha gerekir:

PRAGMA foreign_keys = ON;

SQLite yabancı anahtarları ancak bu ayar açıkken denetler. Ayar bağlantıya özeldir; .quit deyip geri gelince yeniden yazılır.

INSERT: Tabloya Yeni Satır Eklemek

Şimdiye kadar tablolardan okuduk. Şimdi tabloya bir şey yazacağız: yeni bir durak.

nameYurtlardistrictAtakum
stations
id name district
1 OMÜ Rektörlük Atakum
2 Yurtlar Atakum

Bir ekleme üç şeyi bilmek ister: hangi tablo, hangi sütunlar, hangi değerler.

INSERT Sözdizimi

INSERT INTO "stations" ("id", "name", "district")
VALUES (1, 'OMÜ Rektörlük', 'Atakum');
INSERT INTO tabloHangi tabloya?
(sütunlar)Hangi sütunlara?
VALUES (değerler)Hangi değerler?

Sütunlar ve değerler sırayla eşleşir: birinci sütun birinci değeri, ikinci sütun ikinci değeri alır.

İlk Durağı Ekleyelim

INSERT INTO "stations" ("id", "name", "district")
VALUES (1, 'OMÜ Rektörlük', 'Atakum');

Ekran boş kalır; SQLite başarılı yazmada mesaj vermez. Kontrol için okuyalım:

SELECT * FROM "stations";
id name district
1 OMÜ Rektörlük Atakum

Kimliği Kim Versin?

İkinci durağı id = 2 ile ekledik. Üçüncüde kimliği hiç yazmayalım:

INSERT INTO "stations" ("name", "district")
VALUES ('Türkiş', 'Atakum');
id name district
1 OMÜ Rektörlük Atakum
2 Yurtlar Atakum
3 Türkiş Atakum

id sütunu INTEGER PRIMARY KEY olduğu için SQLite en büyük kimliğe bir ekler. Kimliği elle vermek, var olanı tekrar yazma riskidir.

Kısıtlar Kapıda Bekler

Üçüncü haftada name için NOT NULL UNIQUE yazmıştık. Aynı durağı bir kez daha ekleyelim:

nameTürkişdistrictAtakum
UNIQUE constraint failed: stations.name
stations
id name district
1 OMÜ Rektörlük Atakum
2 Yurtlar Atakum
3 Türkiş Atakum

INSERT INTO "stations" ("name", "district") VALUES ('Türkiş', 'Atakum');
Runtime error: UNIQUE constraint failed: stations.name (19)

NOT NULL ve CHECK de Aynı Kapıda

INSERT INTO "stations" ("name", "district") VALUES (NULL, 'Atakum');
Runtime error: NOT NULL constraint failed: stations.name (19)

swipes tablosundaki tutar kuralı da eklemeyi durdurur. Biniş tutarı eksi olmalıydı:

INSERT INTO "swipes" ("card_id", "station_id", "type", "datetime", "amount_kurus")
VALUES (17, 1, 'boarding', '2026-09-02 08:00:00', 3900);
Runtime error: CHECK constraint failed: ...

Hata alan INSERT hiçbir şey yazmaz; tablo önceki hâlinde kalır.

Uygulama 1: Kartları Ekleyin

Bir dakika: cards tablosunun tek sütunu var: id. 17 ve 18 numaralı iki kartı ekleyin, sonra tabloyu okuyun.

INSERT INTO "cards" ("id") VALUES (17);
INSERT INTO "cards" ("id") VALUES (18);
SELECT * FROM "cards";

Kartlar ileride swipes satırlarının bağlanacağı kayıtlar; önce onlar var olmalı.

Birçok Satırı Tek Seferde

Üç durak birden geldi. Üç ayrı INSERT yerine değer listelerini virgülle ayırırız:

nameMimarsinandistrictAtakum
nameAtakentdistrictAtakum
nameCumhuriyet Meydanıdistrictİlkadım
stations
id name district
3 Türkiş Atakum
4 Mimarsinan Atakum
5 Atakent Atakum
6 Cumhuriyet Meydanı İlkadım

INSERT INTO "stations" ("name", "district") VALUES
    ('Mimarsinan', 'Atakum'),
    ('Atakent', 'Atakum'),
    ('Cumhuriyet Meydanı', 'İlkadım');

Ya Satırlardan Biri Hatalıysa?

INSERT INTO "stations" ("name", "district") VALUES
    ('Deneme A', 'Atakum'),
    ('Yurtlar', 'Atakum');
Runtime error: UNIQUE constraint failed: stations.name (19)

Deneme A da eklenmedi. Çok satırlı INSERT ya tamamen yazılır ya hiç yazılmaz.

Kart İşlemlerini Kaydedelim

swipes tablosuna 17 ve 18 numaralı kartların hareketlerini yazalım. Yükleme işleminde durak yoktur; station_id için NULL yazarız:

INSERT INTO "swipes" ("card_id", "station_id", "type", "datetime", "amount_kurus") VALUES
    (17, NULL, 'topup',    '2026-09-01 07:50:00',  10000),
    (17, 1,    'boarding', '2026-09-01 08:15:00',  -3900),
    (17, 3,    'refund',   '2026-09-01 08:45:00',    900),
    (18, 2,    'boarding', '2025-12-20 17:05:00',  -3900),
    (18, 1,    'boarding', '2025-12-21 09:30:00',  -3900);

NULL, tırnaksız yazılır; “bu değer yok” demektir. datetime sütununu boş bırakırsak DEFAULT CURRENT_TIMESTAMP çalışır ve o anki zamanı yazar.

CSV: Veri Çoğu Zaman Hazır Gelir

Dört yeni durağın listesi elimize bir dosya olarak ulaştı. Dosyada ne var?

id,name,district
7,Denizevleri,Atakum
8,Gar,İlkadım
9,Büyük Cami,İlkadım
10,Tekkeköy,Tekkeköy
duraklar.csv
id name district
7 Denizevleri Atakum
8 Gar İlkadım
9 Büyük Cami İlkadım
10 Tekkeköy Tekkeköy

.import: Dosyadan Tabloya

SQLite isteminin kendi komutu CSV’yi satır satır tabloya ekler:

.import --csv --skip 1 duraklar.csv stations
Parça Anlamı
--csv Değerler virgülle ayrılmış
--skip 1 İlk satırı atla: başlık satırı, veri değil
duraklar.csv Okunacak dosya
stations Hedef tablo

SELECT COUNT(*) FROM "stations"; artık 10 döndürür.

Kimliği Dosya mı Vermeli?

duraklar.csv kimlikleri hazır getirdi: 7, 8, 9, 10. .import bunları olduğu gibi yazdı; kimlikleri dosya belirledi, veritabanı değil. Bu derste kuralımız tersiydi: INSERT yazarken id vermedik, SQLite verdi.

Çakışma riskiDosyada 3 yazsaydı tabloda 3 zaten var: o satır UNIQUE constraint failed ile reddedilir, kalanlar girer. Elimizde eksik bir aktarım kalır.
Gerçek dosyalarElimize ulaşan listelerde çoğu zaman kimlik yoktur; olsa da bizim tablomuzun değil, başka bir sistemin numarasıdır.

Olması gereken dosya kimliksiz olandır: yeni-duraklar.csv yalnızca ad ve ilçe taşır. Kimliği yine SQLite versin.

Kimlik Sütunu Olmayan CSV

yeni-duraklar.csv olması gerektiği gibi: yalnızca ad ve ilçe. Ama stations üç sütun bekler, dosya iki sütun verir; doğrudan .import sütunları eşleyemez. Çözüm üç adım:

yeni-duraklar.csv
name,district
Belediye Evleri,Tekkeköy
Yeni Mahalle,Atakum
→.import
temp
name district
Belediye Evleri Tekkeköy
Yeni Mahalle Atakum
→INSERT … SELECT
stations
id name district
10 Tekkeköy Tekkeköy
11 Belediye Evleri Tekkeköy
12 Yeni Mahalle Atakum

Adım 1: Dosyayı Kendi Tablosuna Al

.import --csv yeni-duraklar.csv temp

İlk aktarım komutuyla iki fark var:

Fark Neden
--skip 1 yok Başlık satırı artık veri değil, sütun adı olacak
Hedef temp db’de yok Olmayan tabloyu SQLite dosyaya bakarak kendisi kurar
sqlite> .schema temp
CREATE TABLE IF NOT EXISTS "temp"(
"name" TEXT, "district" TEXT);

--skip 1 Yazsaydık?

Başlığı atlayınca SQLite ilk veri satırını sütun adı sanır:

sqlite> .import --csv --skip 1 yeni-duraklar.csv yanlis
sqlite> .schema yanlis
CREATE TABLE IF NOT EXISTS "yanlis"(
"Belediye Evleri" TEXT, "Tekkeköy" TEXT);
sqlite> SELECT * FROM "yanlis";
Yeni Mahalle|Atakum

Bir durak sütun adına dönüştü, tabloda tek satır kaldı. Kural: var olan tabloya aktarırken başlığı atla; yeni tablo kurdururken başlığı bırak.

temp İçinde Ne Var?

SELECT * FROM "temp";
┌─────────────────┬──────────┐
│      name       │ district │
├─────────────────┼──────────┤
│ Belediye Evleri │ Tekkeköy │
│ Yeni Mahalle    │ Atakum   │
└─────────────────┴──────────┘

Satırlar geldi ama kimlik yok ve typeof("name") her satırda text. Kimliği stations tablosu verecek.

Adım 2: temp’ten stations’a Taşı

INSERT INTO "stations" ("name", "district")
SELECT "name", "district" FROM "temp";

VALUES yerine bir sorgunun sonucu eklenir. Sol taraftaki sütun listesiyle SELECT listesi sırayla eşleşir; id iki tarafta da yok, SQLite verir.

sqlite> SELECT * FROM "stations" WHERE "id" >= 10;
┌────┬─────────────────┬──────────┐
│ id │      name       │ district │
├────┼─────────────────┼──────────┤
│ 10 │ Tekkeköy        │ Tekkeköy │
│ 11 │ Belediye Evleri │ Tekkeköy │
│ 12 │ Yeni Mahalle    │ Atakum   │
└────┴─────────────────┴──────────┘

Adım 3: Geçici Tabloyu Kaldır

DROP TABLE "temp";
sqlite> .tables
cards     stations  swipes

İş bitti, iz kalmadı. Üç adımın özeti:

  1. .import dosyayı kendi tablosuna alır (başlık sütun adı olur).
  2. INSERT ... SELECT satırları asıl tabloya taşır, kimlikleri SQLite verir.
  3. DROP TABLE geçici tabloyu kaldırır.

CSV’den Gelen Verinin İki Tuzağı

Her şey metin olarak gelirYeni oluşturulan tabloda tüm sütunlar TEXT. Var olan tabloya aktarırken sütun türü dönüşümü yapar.
Boş hücre NULL değildirDosyada virgülden sonra hiçbir şey yoksa SQLite oraya boş metin ’’ yazar. IS NULL bunu bulmaz.
SELECT "name", typeof("district"), "district" IS NULL, "district" = ''
FROM "temp";
name typeof IS NULL = ’’
Eczaneler text 0 1

Boş hücreleri gerçek NULL yapmak için az sonra göreceğimiz UPDATE gerekir.

DELETE: Satırı Kaldırmak

Bir deneme durağı eklemiştik; artık gerekmiyor. Silme, bir satırı tablodan tamamen kaldırır.

stations
id name district
10 Tekkeköy Tekkeköy
11 Belediye Evleri Tekkeköy
12 Yeni Mahalle Atakum
13 Deneme Durağı Atakum
DELETE FROM "stations"
WHERE "name" = 'Deneme Durağı';

DELETE FROM tablo WHERE koşul; WHERE, ilk haftadan tanıdık: hangi satırlar?

WHERE Olmazsa Ne Olur?

stations
id name district
1 OMÜ Rektörlük Atakum
2 Yurtlar Atakum
3 Türkiş Atakum
4 Mimarsinan Atakum
DELETE FROM "stations";
WHERE yok: 12 satırın hepsi silinir

DROP TABLE tabloyu yapısıyla siler; koşulsuz DELETE yapıyı bırakıp bütün satırları siler.

Birden Çok Satırı Silmek

Kart hareketlerinin 2026 öncesi kayıtları arşive gitti; tablodan kaldıralım. Önce seçelim:

SELECT "id", "card_id", "datetime" FROM "swipes"
WHERE "datetime" < '2026-01-01';
id card_id datetime
4 18 2025-12-20 17:05:00
5 18 2025-12-21 09:30:00
DELETE FROM "swipes" WHERE "datetime" < '2026-01-01';

İki satır gitti. Tarihler YYYY-MM-DD biçiminde metin olduğu için karşılaştırma doğru çalışır.

Silinen Kimlik Geri Gelir mi?

Durum Sonraki INSERT hangi kimliği alır?
En büyük kimlik silindi: 13 gitti Yine 13. SQLite en büyük mevcut kimliğe bir ekler.
Aradaki kimlik silindi: 12 gitti, 13 duruyor 14. Boşluk dolmaz.

Silinen kimliğin yeniden verilmesi istenmiyorsa INTEGER PRIMARY KEY AUTOINCREMENT kullanılır; o zaman sayaç geri dönmez.

Uygulama 2: Hangi Koşul?

İki dakika: 17 numaralı kartın yalnızca yükleme (topup) işlemlerini silmek istiyoruz. Önce seçen SELECT’i, sonra DELETE’i yazın. Henüz çalıştırmayın.

SELECT * FROM "swipes" WHERE "card_id" = 17 AND "type" = 'topup';

DELETE FROM "swipes" WHERE "card_id" = 17 AND "type" = 'topup';

Bir satır seçilir: 1 numaralı yükleme.

Silmek Her Zaman Serbest mi?

Deneme Durağında bir biniş kaydedilmiş olsun. Şimdi durağı silmeye çalışalım:

stations
id name
12 Yeni Mahalle
13 Deneme Durağı
swipes
id card_id station_id
3 17 3
7 18 13
FOREIGN KEY constraint failedİki adımda silindi

İki Adımda Silmek

Önce bağlı satırları, sonra durağı sileriz. Durağın kimliğini ezberlemek yerine alt sorguyla buluruz:

DELETE FROM "swipes"
WHERE "station_id" = (
    SELECT "id" FROM "stations" WHERE "name" = 'Deneme Durağı'
);

DELETE FROM "stations" WHERE "name" = 'Deneme Durağı';

Sıra önemli: ters sırada ilk ifade yine hata verir.

ON DELETE: Silmeye Verilecek Tepki

Yabancı anahtar tanımına, işaret edilen satır silinince ne olacağı eklenebilir:

FOREIGN KEY("station_id") REFERENCES "stations"("id") ON DELETE SET NULL
Seçenek Üst satır silinince alt satıra ne olur?
RESTRICT Silme reddedilir. Az önce gördüğümüz davranış.
NO ACTION Varsayılan; SQLite’ta pratikte RESTRICT gibi davranır.
SET NULL Yabancı anahtar NULL olur; alt satır kalır.
SET DEFAULT Yabancı anahtar sütunun varsayılan değerini alır.
CASCADE Alt satırlar da silinir.

SET NULL: Kayıt Kalır, Bağ Kopar

Durak kapandı ama biniş kayıtları muhasebe için gerekli. Durağı silince bağlı satırların station_id değeri NULL olur:

stations
id name
1 OMÜ Rektörlük
13 Deneme Durağı
swipes
id card_id station_id
1 17 1
2 17 13NULL
3 18 13NULL

station_id sütunu NULL kabul ettiği için bu seçenek mümkün; yükleme işlemlerinde zaten NULL kullanıyorduk.

CASCADE: Zincirleme Silme

Bir kart iptal edildi ve hareketleri de gitmeli. card_id için ON DELETE CASCADE seçersek kartı silmek hareketlerini de siler:

cards
id
17
18
swipes
id card_id station_id
1 17 1
4 18 2
5 18 1

Tek DELETE, iki tabloda silme. Kolay ama tehlikeli: bir satır silmek yüzlerce satırı götürebilir.

Hangisini Seçmeli?

Bağ Seçenek Gerekçe
swipes.station_id → stations SET NULL Durak kapanabilir; parasal kayıt kalmalı.
swipes.card_id → cards RESTRICT Hareketi olan kart silinmemeli; iptal için başka yol gerekir.

İki kararı da veren şey teknik değil, iş kuralı. Üçüncü haftadaki kısıtlar gibi ON DELETE de şemanın bir parçasıdır.

UPDATE: Değeri Değiştirmek

Üçüncü haftada “Türkiş” adını “Türk-İş” yapmak için kaç satıra dokunmamız gerektiğini sormuştuk. Artık durak adı tek yerde:

stations
id name district
2 Yurtlar Atakum
3 TürkişTürk-İş Atakum
4 Mimarsinan Atakum
UPDATE "stations"
SET "name" = 'Türk-İş'
WHERE "name" = 'Türkiş';

UPDATE tablo SET sütun = değer WHERE koşul; Birden fazla sütun virgülle ayrılarak aynı ifadede değiştirilebilir.

WHERE Olmadan UPDATE

stations
id name district
6 Cumhuriyet Meydanı İlkadımAtakum
8 Gar İlkadımAtakum
10 Tekkeköy TekkeköyAtakum
UPDATE "stations"
SET "district" = 'Atakum';
WHERE yok: her satır değişir

Silmede olduğu gibi: önce aynı koşulla SELECT, satır sayısına bak, sonra UPDATE.

Alt Sorguyla Güncelleme

3 numaralı iade işlemi yanlış durağa yazılmış; aslında Mimarsinan’da yapıldı. Durağın kimliğini ezberlemeden düzeltelim:

UPDATE "swipes"
SET "station_id" = (
    SELECT "id" FROM "stations" WHERE "name" = 'Mimarsinan'
)
WHERE "id" = 3;
id station_id
3 4

SET tarafında da WHERE tarafında da alt sorgu kullanılabilir. İkinci haftanın aracı üçüncü işte.

Uygulama 3: Adı Düzeltin

Bir dakika: Büyük Cami durağının ilçesi yanlışlıkla İlkadım yerine Ilkadim girilmiş olsun. Düzelten UPDATE’i yazın; önce seçen SELECT’i.

SELECT * FROM "stations" WHERE "district" = 'Ilkadim';

UPDATE "stations"
SET "district" = 'İlkadım'
WHERE "district" = 'Ilkadim';

Koşul name üzerinden de kurulabilir; iki yol da tek satırı seçer.

Elle Girilmiş Veri Dağınık Gelir

SAMULAŞ bir anket yapmış: “En çok kullandığınız durak hangisi?” Yanıtlar serbest metin olarak yazılmış ve anket.csv dosyasında geldi.

.import --csv anket.csv anket
SELECT "durak", COUNT(*) AS "oy" FROM "anket"
GROUP BY "durak" ORDER BY "oy" DESC;
durak oy
Yurtlar 2
Mimarsinan 2
Gar 2
yurtlar 1
mimar sinan 1
… …

20 yanıt, 17 farklı grup. Beş durak için beş grup bekliyorduk.

Temizlik Dört Adımda

ham veri17farklı grup
trim()15baştaki ve sondaki boşluklar gitti
upper()8büyük küçük harf farkı gitti
LIKE ile düzeltme5yazım hataları gitti

Her adım bir UPDATE. Hedef: beş durak, beş grup.

Adım 1: trim ile Boşlukları Atmak

" gar" ve "Gar " farklı değerler; görünmeyen boşluk gruplandırmayı bozar. trim() baştaki ve sondaki boşlukları atar:

UPDATE "anket" SET "durak" = trim("durak");

Koşul yok, çünkü her satırı temizlemek istiyoruz. Bu kez koşulsuz UPDATE tam yerinde.

SELECT COUNT(DISTINCT "durak") FROM "anket"; → 17’den 15’e.

Adım 2: upper ile Harfleri Eşitlemek

"Gar", "gar" ve "GAR" aynı durak. Hepsini büyük harfe çevirirsek fark kalmaz:

UPDATE "anket" SET "durak" = upper("durak");
durak oy
GAR 5
YURTLAR 4
MIMARSINAN 3
ATAKENT 3
DENIZEVLERI 2
MIMARSıNAN 1
MIMAR SINAN 1
DENIZ EVLERI 1

8 grup kaldı. lower() aynı işi küçük harfle yapar.

Türkçe Uyarısı: upper Yalnızca ASCII Bilir

SELECT upper('omü rektörlük'), lower('İLKADIM');
upper lower
OMü REKTöRLüK İlkadim

SQLite’ın upper ve lower fonksiyonları yalnızca İngiliz alfabesindeki harfleri çevirir. ö, ı, İ, ş olduğu gibi kalır.

Türkçe metinde harf eşitlemek için ya uygulama tarafında dönüştürün ya da eşleştirmeyi LIKE ve replace() ile kurun.

Adım 3: Yazım Hatalarını LIKE ile Yakalamak

Kalan farklar yazım hatası: MIMAR SINAN, MIMARSıNAN, DENIZ EVLERI. Tek tek düzeltmek yerine örüntüyle yakalarız:

UPDATE "anket" SET "durak" = 'MIMARSINAN' WHERE "durak" LIKE 'mimar%';
UPDATE "anket" SET "durak" = 'DENIZEVLERI' WHERE "durak" LIKE 'deniz%';
durak oy
MIMARSINAN 5
GAR 5
YURTLAR 4
DENIZEVLERI 3
ATAKENT 3

Beş grup, 20 oy. LIKE büyük küçük harfe bakmadığı için MIMARSıNAN da yakalandı.

Uygulama 4: Örüntüyü Siz Seçin

Bir dakika: Anket tablosunda ATAKENT için "atakent " ve "Atakent" gibi yanıtlar olsaydı ve trim, upper yapılmamış olsaydı, tek UPDATE ile hepsini ATAKENT yapan ifadeyi yazın.

UPDATE "anket" SET "durak" = 'ATAKENT' WHERE "durak" LIKE '%atakent%';

Başta ve sonda % olduğu için boşluklu yanıtlar da eşleşir; LIKE harf büyüklüğüne bakmaz.

Mola

On dakika ara.

Dönüşte: bir yazma işleminin başka bir yazma işlemini otomatik tetiklemesi ve silmeden silmek.

Tetikleyici: Bir Yazma Diğerini Başlatır

Kartın bakiyesini her işlemden sonra elle güncellemek zorunda mıyız? Tetikleyici, bir tabloya yazıldığında başka bir ifadeyi kendiliğinden çalıştırır:

swipes
id card_id type amount_kurus
3 17 refund 900
8 17 topup 10000
⚡AFTER INSERTON swipes
cards
id balance_kurus
17 010000
18 0

Önce Bakiye Sütunu

Üçüncü haftanın ALTER TABLE komutuyla cards tablosuna bakiye ekleyelim:

ALTER TABLE "cards" ADD COLUMN "balance_kurus" INTEGER NOT NULL DEFAULT 0;
SELECT * FROM "cards";
id balance_kurus
17 0
18 0

Var olan satırlar varsayılan değeri aldı. Bakiye kuruş cinsinden tam sayı; para için üçüncü haftada verdiğimiz karar.

CREATE TRIGGER Sözdizimi

CREATE TRIGGER "balance_update"
AFTER INSERT ON "swipes"
FOR EACH ROW
BEGIN
    UPDATE "cards"
    SET "balance_kurus" = "balance_kurus" + NEW."amount_kurus"
    WHERE "id" = NEW."card_id";
END;
Parça Anlamı
AFTER INSERT ON "swipes" Ne zaman: swipes tablosuna ekleme yapıldıktan sonra
FOR EACH ROW Eklenen her satır için bir kez
BEGIN ... END Çalışacak ifadeler; birden fazla olabilir
NEW."amount_kurus" Az önce eklenen satırın değeri

Tetikleyiciyi Çalıştırmak

Bir yükleme, bir biniş ekleyelim ve cards tablosuna bakalım:

INSERT INTO "swipes" ("card_id", "station_id", "type", "datetime", "amount_kurus")
VALUES (17, NULL, 'topup', '2026-09-05 07:30:00', 10000);
id balance_kurus
17 10000
INSERT INTO "swipes" ("card_id", "station_id", "type", "datetime", "amount_kurus")
VALUES (17, 2, 'boarding', '2026-09-05 08:10:00', -3900);
id balance_kurus
17 6100

İkinci Tetikleyici: Silmeden Önce Arşivle

Kapanan durakların adını bir arşiv tablosunda saklayalım. BEFORE DELETE ile silinen satıra OLD üzerinden ulaşırız:

CREATE TABLE "closed_stations" ("id" INTEGER PRIMARY KEY, "name" TEXT NOT NULL,
    "closed_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);

CREATE TRIGGER "archive_station"
BEFORE DELETE ON "stations"
FOR EACH ROW
BEGIN
    INSERT INTO "closed_stations" ("name") VALUES (OLD."name");
END;

DELETE FROM "stations" WHERE "name" = 'Yeni Mahalle'; sonrasında arşivde bir satır: Yeni Mahalle, silinme zamanıyla.

Yumuşak Silme: Satırı Silmeden İşaretlemek

Durak kapandı ama kaydı silmek istemiyoruz: geçmiş binişler ona bağlı, raporlarda adı geçmeli. Satırı silmek yerine işaretleriz:

stations
id name closed
9 Büyük Cami 0
10 Tekkeköy 0
11 Belediye Evleri 01
UPDATE "stations" SET "closed" = 1
WHERE "name" = 'Belediye Evleri';
SELECT * FROM "stations"
WHERE "closed" = 0;

Silmek mi, İşaretlemek mi?

Kalıcı silme: DELETEVeri gerçekten gider. Yer açılır, geçmiş kaybolur. Kişisel verinin silinmesi istendiğinde çoğu zaman gereken budur.
Yumuşak silme: işaret sütunuVeri kalır, görünmez olur. Geri alınabilir, geçmiş korunur. Her sorguda filtre gerekir.

Durak için işaretlemek doğru. Bir yolcunun kişisel bilgisi için karar hukuki: KVKK’da veri sahibi silinmesini isteyebilir ve “görünmez” yapmak silmek değildir.

Alıştırmalar

Soru 1: Yeni Kart, İlk Yükleme

19 numaralı yeni bir kart kaydedin ve 5.000 kuruşluk ilk yüklemesini tek INSERT ile swipes tablosuna yazın. Yükleme durağa bağlı değildir.

INSERT INTO "cards" ("id") VALUES (19);

INSERT INTO "swipes" ("card_id", "station_id", "type", "amount_kurus")
VALUES (19, NULL, 'topup', 5000);

datetime yazılmadı; varsayılan zaman damgası girer. Tetikleyici kurulduysa kartın bakiyesi 5000 olur.

Soru 2: Bu İfadede Ne Yanlış?

Ekip yalnızca 18 numaralı kartın işlemlerini silmek istiyor:

DELETE FROM "swipes"
WHERE "card_id" = 18
   OR "type" = 'boarding';

Kaç satır gider? Düzeltin.

OR yüzünden bütün binişler de silinir. Koşul AND olmalıydı ya da yalnızca "card_id" = 18. Yazmadan önce aynı koşulla SELECT COUNT(*) çalıştırın.

Soru 3: Sıra Önemli

Türk-İş durağını kalıcı olarak silmek istiyoruz; durağa bağlı biniş kayıtları var ve şemada ON DELETE tanımı yok. Gereken ifadeleri doğru sırayla yazın.

DELETE FROM "swipes"
WHERE "station_id" = (SELECT "id" FROM "stations" WHERE "name" = 'Türk-İş');

DELETE FROM "stations" WHERE "name" = 'Türk-İş';

Önce bağlı satırlar, sonra durak. Şemada ON DELETE SET NULL olsaydı tek ifade yeterdi ve biniş kayıtları kalırdı.

Soru 4: Ankette Son Düzeltme

anket tablosunda "YURTLAR" için bir de "YURTLAR DURAGI" yanıtı olsaydı, bunu diğerleriyle aynı gruba sokan tek UPDATE nedir? Örüntü neden tehlikeli olabilir?

UPDATE "anket" SET "durak" = 'YURTLAR' WHERE "durak" LIKE 'yurtlar%';

Tehlike: yurtlar ile başlayan başka bir durak adı varsa o da değişir. Önce aynı LIKE ile SELECT.

Soru 5: Tetikleyiciyi Tamamlayın

Bir işlem silinince kart bakiyesi de düzelmeli. Boşlukları doldurun:

CREATE TRIGGER "balance_revert"
AFTER DELETE ON "swipes"
FOR EACH ROW
BEGIN
    UPDATE "cards"
    SET "balance_kurus" = "balance_kurus" ___ ___."amount_kurus"
    WHERE "id" = ___."card_id";
END;
    SET "balance_kurus" = "balance_kurus" - OLD."amount_kurus"
    WHERE "id" = OLD."card_id";

Silinen satıra OLD ile ulaşılır; eklenen tutar geri çıkarılır.

Bu Dersin Anahtar Kelimeleri

Amaç SQL
Satır ekleme INSERT INTO ... VALUES, INSERT INTO ... SELECT
Dosyadan aktarma .import --csv --skip 1
Satır silme DELETE FROM ... WHERE
Silmeye tepki ON DELETE RESTRICT / SET NULL / CASCADE
Değer değiştirme UPDATE ... SET ... WHERE
Temizlik trim(), upper(), lower(), LIKE
Otomatik ifade CREATE TRIGGER ... BEGIN ... END, NEW, OLD
Yumuşak silme İşaret sütunu + UPDATE + filtreli SELECT

Çıkış Soruları

Üç kısa yanıt yazın:

  1. INSERT yazarken kimliği ne zaman biz veririz, ne zaman SQLite’a bırakırız?
  2. DELETE FROM "stations"; ile DROP TABLE "stations"; arasındaki fark nedir?
  3. Aşağıdaki ifade kaç satırı değiştirir ve bu bir sorun mudur?
UPDATE "anket" SET "durak" = trim("durak");

Kataloğa Dönüş

  1. Dış dünyada basılı bir numara varsa biz veririz: kart numarası. Satırı yalnızca ayırt edecek bir sayı gerekiyorsa SQLite’a bırakırız: durak kimliği.
  2. DELETE satırları siler, tablo ve şema kalır. DROP TABLE tabloyu yapısıyla kaldırır.
  3. Bütün satırları değiştirir ve bu niyetlidir: her yanıtın boşluklarını temizlemek istiyoruz.

Bugün şemayı veriyle doldurduk, yanlışları düzelttik, sildik ve bir yazmanın başka bir yazmayı tetiklemesini sağladık. Gelecek hafta filtreli sorguları görünüm olarak saklayacağız.

Kaynaklar ve Dosyalar

Durak adları SAMULAŞ hattından; ilçe bilgileri, kart hareketleri ve anket yanıtları ders için kurgulandı. Tüm çıktılar SQLite 3.51 ile doğrulandı.