Veritabanı Yönetim Sistemleri
Görünümler
Dr. Öğr. Üyesi Oğuz Emre Kural
2026
Öğrenme Kazanımları
Bu dersin sonunda:
Birden çok tabloyu birleştiren sorguya CREATE VIEW ile ad verip görünümü bir tablo gibi sorgulayabilirsiniz.
GROUP BY içeren bir özet sorgusunu görünüm olarak tanımlayabilir; TEMPORARY VIEW ve CTE ile geçici ara sonuçlar oluşturabilirsiniz.
Görünümü bir tabloyu mantıksal parçalara ayırmak ve hassas sütunları gizlemek için kullanabilir, bu korumanın SQLite’taki sınırını açıklayabilirsiniz.
Görünüme neden doğrudan DELETE ve INSERT uygulanamadığını açıklayabilir, INSTEAD OF tetikleyicisiyle yumuşak silmeyi görünüm üzerinden yürütebilirsiniz.
Dört kazanım dersin dört bölümüne denk geliyor: sadeleştirme ve toplama, geçici görünümler, bölümleme ve güvenlik, yumuşak silme. Dersin ana fikri tek cümle: görünüm, bir sorguya verilen addır . Bu cümleyi ders boyunca tekrar edeceğiz; öğrenci bunu yakalarsa gerisi uygulama.
Geçen Dersten Bu Derse
Dördüncü haftada tabloları veriyle doldurduk. Kapanan bir durağı silmek yerine closed sütunuyla işaretlemeyi öğrendik. Ders şu cümleyle bitti: “Bu koşulu gelecek hafta bir görünümde tanımlayacağız.”
Kapalı durakları sonuçlardan çıkarmak için her sorguya aynı koşulu yazıyorduk:
SELECT * FROM "stations" WHERE "closed" = 0 ;
Bir sorguda koşulu unutursak kapalı durak listeye geri döner. Üstelik bu yalnızca bir örnek: bir sorgu uzadıkça onu her yerde yeniden yazmak hata kaynağı olur.
Bugünün sorusu: Bir kez yazdığımız sorguya ad verip sonradan bir tabloymuş gibi çağırabilir miyiz?
Geçen derse bağlayın: yumuşak silmenin bedeli her okuma sorgusuna closed = 0 yazmaktı. Öğrencilere “Bu koşulu bir yere yazıp unutabilsek ne olurdu?” diye sorun. Ders sonunda bu soruya cevap vereceğiz; ders boyunca da aynı fikrin başka yüzlerini göreceğiz.
Bugünün Veritabanı
Boş bir klasöre indirin:
sqlite3 ulasim.db
sqlite> .headers on
sqlite> .mode column
sqlite> .tables
cards stations swipes
ulasim.db dördüncü haftanın şemasıdır; üç fark var: tablolar doldurulmuş, cards tablosuna kart sahibini tutan holder sütunu eklenmiş (kişiselleştirilmiş kartlar), bakiye tetikleyicileri kurulu. Kart sahipleri ve işlemler ders için kurgulandı; gerçek kişilerle ilgisi yok. .headers on ve .mode column çıktıyı slayttaki gibi hizalar. Öğrencilerden .tables komutunu çalıştırmalarını isteyin: üç tablo görmeliler. Bozarsanız dosyayı silip sqlite3 yeni.db ".read ulasim.sql" ile yeniden kurabilirler.
Veritabanının Haritası
Üç tablo ve iki yabancı anahtar bağı:
SAMULAŞ veritabanının ilişki şeması
cards, swipes ve stations tabloları. Her işlemin tam bir kartı, sıfır veya bir durağı vardır. Bir kartın sıfır veya çok işlemi, bir durağın sıfır veya çok işlemi olabilir. swipes tablosu kartlarla durakları bağlayan köprü tablodur.
cards
id PK
holder
balance_kurus
swipes
id PK
card_id FK → cards
station_id FK → stations
type
datetime
amount_kurus
yapar
stations
id PK
name UNIQUE
district
closed
gerçekleşir
kart ↔ durak: çoktan çoğa ilişki, köprü tablo: swipes
tam bir
sıfır veya bir (isteğe bağlı)
sıfır veya çok
Notasyonu bir kez, yavaşça anlatın; haftalar sonra aynı çizgilerle karşılaşacaklar. Çizginin ucundaki simge o ucun kaç kayıt taşıdığını söyler: iki dik çizgi “tam bir”, daire artı dik çizgi “sıfır veya bir”, daire artı çatal “sıfır veya çok”. swipes.station_id boş olabilir çünkü yükleme işlemi bir durakta olmaz; bu yüzden durak ucunda daire var. Sonunda şunu sorun: “Bu şemada ‘hangi kart hangi durakta bindi’ sorusu kaç tabloyu gezmeyi gerektirir?” Cevap üç; bugünün konusu bu zinciri tek bakışa indirmek.
Kartlar ve Duraklar
17
Ayşe Kaya
7750
18
Mehmet Demir
1250
19
Elif Yıldız
1500
20
Can Öztürk
6500
21
Zeynep Arslan
9750
22
Burak Şahin
4750
SELECT * FROM "stations" ;
1
Cumhuriyet Meydanı
İlkadım
0
2
Gar
İlkadım
0
3
Büyük Cami
İlkadım
0
4
Atakent
Atakum
0
5
Yurtlar
Atakum
0
6
Mimarsinan
Atakum
0
7
Tekkeköy
Tekkeköy
0
8
Belediye Evleri
Tekkeköy
1
holder sütunu bu derste eklendi: kişiselleştirilmiş kartlarda sahibin adı tutuluyor. Bu sütun hem JOIN örneğinin okunur olmasını sağlıyor hem de güvenlik bölümünde kişisel veri örneğimiz olacak. Bakiyeler geçen haftanın tetikleyicisiyle hesaplanmış durumda; her kartın bakiyesi işlemlerinin toplamına eşit.
swipes: Üç Tür Olay
SELECT * FROM "swipes" WHERE "id" IN (1 , 5 , 6 , 7 , 8 , 22 );
1
17
NULL
topup
2026-10-26 07:41:12
15000
5
17
2
boarding
2026-10-26 08:15:03
-1750
6
19
5
boarding
2026-10-26 08:31:44
-875
7
18
4
boarding
2026-10-26 08:47:19
-1750
8
20
1
boarding
2026-10-26 12:20:51
-875
22
18
4
refund
2026-10-28 08:52:10
1750
Tabloda 41 işlem var: 29 biniş (boarding), 9 yükleme (topup), 3 iade (refund). Yüklemenin durağı yoktur; biniş ve iadenin vardır.
Her türden örnek görünsün diye altı satır kimliğiyle seçildi; tablonun ilk satırları dört yüklemeyle başlar. Öğrenciler SELECT * FROM "swipes"; yazarsa daha fazla satır görür. Kuruş cinsinden tutarlar üçüncü haftadaki “para için tam sayı” kararından geliyor: 1750 kuruş 17,50 TL. Bu tablodaki üç tür olay bugünkü bölümleme konusunun ham maddesi.
Ayşe Kaya Hangi Duraklardan Bindi?
Bu sorunun yanıtını bulmak için üç tabloya bakıyoruz:
SELECT "name" FROM "stations" WHERE "id" IN ( SELECT "station_id" FROM "swipes" WHERE "type" = 'boarding' AND "card_id" = ( SELECT "id" FROM "cards" WHERE "holder" = 'Ayşe Kaya' ) );
1 · cards id = 17
2 · swipes station_id ∈ {2, 1, 3}
3 · stations Cumhuriyet Meydanı, Gar, Büyük Cami
Sorgu doğru, ancak üç SELECT ve iç içe sorgular okumayı zorlaştırıyor.
Sorguyu öğrenciye okutun, içten dışa sırayı kendileri söylesin. Sunumda her tıklamada ilgili katman vurgulanır ve sağda o adımın ara sonucu belirir. Vurgulanacak nokta: sorunun cevabı basit, yolu karmaşık. Bu bir yazım sorunu değil, tasarım sonucu: bilgi üç tabloya dağıtıldı. Aynı birleştirme sorgusunu her seferinde yeniden yazmak yerine ona bir kez ad vermek istiyoruz.
Üç Tablo, Tek Bakış
Önce sorguyu yazmadan, tabloları yan yana koyup eşleşen satırları inceleyelim.
üç ayrı tablo cards ⋈ swipes ⋈ stations ride_history (görünüm)
cards
id holder
17 Ayşe Kaya
19 Elif Yıldız
18 Mehmet Demir
20 Can Öztürk
swipes
card_id datetime station_id
17 08:15:03 2
19 08:31:44 5
18 08:47:19 4
20 12:20:51 1
stations
id name
2 Gar
5 Yurtlar
4 Atakent
1 Cumhuriyet Meydanı
Bu görselde adımlara zaman ayırın. Adım 1: aynı renk aynı kaydı gösteriyor; Ayşe’nin satırı üç tabloda aynı yükseklikte duruyor. Adım 2: cards tablosunda id sütununu sağa alıyoruz; böylece cards.id ile swipes.card_id yan yana görünüyor. Adım 3: tablolar birbirine yaklaşıp tek bir görünüm oluşturuyor. Adım 4: yalnızca eşleştirme için kullanılan kimlik sütunları gizleniyor. Adım 5: sonuç, “kim, ne zaman, nerede”. Slayttaki tablolar sadeleştirildi: datetime yalnızca saatle gösteriliyor; gerçek swipes altı sütunlu. Gerçekte satırlar aynı sırada olmayabilir; JOIN eşleşmeyi satır sırasına göre değil, kimliklere göre yapar.
JOIN’i Yazalım
Her JOIN, bir birincil anahtarı bir yabancı anahtara bağlar:
SELECT "holder" , "datetime" , "name" AS "station" FROM "cards"
JOIN "swipes" ON "cards" ."id" = "swipes" ."card_id"
JOIN "stations" ON "stations" ."id" = "swipes" ."station_id"
WHERE "swipes" ."type" = 'boarding' ;
Ayşe Kaya
2026-10-26 08:15:03
Gar
Elif Yıldız
2026-10-26 08:31:44
Yurtlar
Mehmet Demir
2026-10-26 08:47:19
Atakent
…
…
…
ON: hangi iki sütun aynı değeri taşıyor? WHERE: iadeleri dışarıda bırakır.
Çıktı 29 satırdır; slaytta ilk beşi gösteriliyor. JOIN yazarken en sık hata ON koşulunu unutmak ya da yanlış sütunları eşitlemektir; koşul yoksa satırlar çarpılır (her satır her satırla eşleşir). Yüklemelerin station_id değeri NULL olduğu için stations ile eşleşemez ve zaten sonuçta yok olurlar. İadelerin durağı olduğundan onları ayrıca type = 'boarding' ile eliyoruz. "name" AS "station" yeni sütun adı veriyor: görünümün sütun adlarını biz belirliyoruz.
CREATE VIEW: Sorguya Ad Vermek
Bu sorguyu her seferinde yeniden yazmak istemiyoruz. Başına bir satır eklemek yeter:
CREATE VIEW "ride_history" AS SELECT "holder", "datetime", "name" AS "station" FROM "cards"
JOIN "swipes" ON "cards"."id" = "swipes"."card_id"
JOIN "stations" ON "stations"."id" = "swipes"."station_id"
WHERE "swipes"."type" = 'boarding';
→
sqlite_schema
cards table
swipes table
stations table
ride_history view
ulasim.db: 20480 bayt → 20480 bayt
Sözdizimi: CREATE VIEW ad AS sorgu;. Başka hiçbir şey eklemiyoruz. Komut çalışınca ekrana bir şey yazılmaz; sorguyu bir kez çalıştırıp sonucu saklamıyoruz, sorgunun kendisini saklıyoruz. Dosya boyutu bu veritabanında ls -l ulasim.db ile bakıldığında 20480 bayt olarak kalır: SQLite sayfalarında hâlâ yer vardı ve eklenen şey birkaç yüz karakterlik sorgu metni. Büyük bir tabloda aynı durum geçerli: görünüm satırları çoğaltmaz. Öğrenciler kendi bilgisayarında ls -l ulasim.db ya da dosya özelliklerinden bakabilir.
Görünümü Sorgulamak
ride_history artık bir tablo gibi sorgulanabilir. Az önceki üç iç içe SELECT yerine tek sorgu:
SELECT DISTINCT "station" FROM "ride_history"
WHERE "holder" = 'Ayşe Kaya' ;
Gar
Cumhuriyet Meydanı
Büyük Cami
Aynı yanıt; üstelik üç tabloyu bilmeye gerek yok. Görünüme WHERE, ORDER BY, GROUP BY ve JOIN bile uygulanabilir.
DISTINCT ekledik çünkü Ayşe Gar’dan dört kez bindi; görünümde dört satır var, ama biz hangi durakları sorduk. Burada dikkat çekilecek nokta soyutlama: görünümü kullanan kişi altındaki üç tabloyu, kimlik bağlarını ya da type = 'boarding' filtresini bilmeden doğru sonucu alıyor. Bu, veritabanında mantıksal veri bağımsızlığı diye anılır: altta tablo yapısı değişse bile görünümün tanımı güncellendiği sürece sorgular çalışmaya devam eder. Silberschatz’ın görünümler bölümünde bu fikir resmî olarak anlatılır.
Görünüm Nerede Duruyor?
Görünüm şemada bir nesnedir. Tabloların yanında listelenir:
cards ride_history stations swipes
SELECT "type" , "name" FROM "sqlite_schema"
WHERE "type" = 'view' ;
SQLite görünümün yalnızca sorgu metnini saklar; her sorguda alttaki sorgu yeniden çalışır.
.tables çıktısında ride_history’nin tablolarla aynı listede görünmesi önemlidir: bir uygulama için görünüm ile tablo ayırt edilemez. .schema ride_history yazılırsa saklanan metin görünür; SQLite sonuna sütun listesini bir yorum olarak ekler. Bu yüzden görünüm sonucu “saklamaz”: tablolar değişince görünüm de değişir. Bunun artısı güncellik, eksisi her sorguda yeniden hesaplamadır; bu bedeli “Görünümün Sınırları” slaytında konuşacağız.
Görünümü Sıralamak
Görünümler de bir tablo gibi sıralanabilir:
SELECT * FROM "ride_history" ORDER BY "datetime" DESC LIMIT 4 ;
Burak Şahin
2026-11-01 15:10:05
Mimarsinan
Zeynep Arslan
2026-11-01 13:25:18
Atakent
Can Öztürk
2026-11-01 11:40:33
Cumhuriyet Meydanı
Mehmet Demir
2026-10-31 10:02:11
Büyük Cami
ORDER BY görünümün tanımına da yazılabilir; ama sıralamayı sorgulayanın kararına bırakmak daha sağlıklıdır.
Öğrencilerin sık sorduğu soru: “Görünümün kendisi sıralı olabilir mi?” Evet, tanıma ORDER BY yazılabilir; ama görünümü kullanan sorgu başka bir sıra isterse tanımdaki sıra kaybolur. Tanımın içinde sıralama genellikle gereksizdir. En yeni dört kaydı seçtik: bu, görünümün sorgulamaya açık gerçek bir tablo gibi davrandığının kanıtı.
Görünüme Doğrudan Yazılamaz
Görünüm veri taşımaz; satırlar altındaki tablolarda durur. Bu yüzden değiştirmeye çalışırsak:
UPDATE "ride_history" SET "station" = 'Gar'
WHERE "holder" = 'Ayşe Kaya' ;
Parse error: cannot modify ride_history because it is a view
DELETE ve INSERT aynı hatayı verir: değiştirilecek veri swipes ve stations tablolarındadır.
Bu kuralın bir çıkış yolu var: tetikleyici . Dersin sonunda göreceğiz.
Neden izin verilmediğini sorun: station sütunu stations.name’den geliyor; değiştirirsek stations tablosunun adını mı, swipes satırının durak bağını mı değiştirmeliyiz? Belirsiz. Görünüm üç tablodan üretildiği için bir değişikliğin hangi tabloya yazılacağı bilinemez. Bu bölümün sonunda “ya görünüme yazmak istiyorsak?” sorusuna geri döneceğiz; yumuşak silme bölümünde cevabı INSTEAD OF tetikleyicisi.
Görünüm Neye Yarar?
Görünüm tek bir fikirdir, ama dört farklı işte karşımıza çıkar:
1 · SADELEŞTİRME Çok tabloyu tek tablo gibi göster Az önce yaptığımız: ride_history
2 · TOPLAMA Özet sorgusunu yeniden kullan Toplam, sayı ve ortalamayı her sorguda yeniden yazma.
3 · BÖLÜMLEME Tabloyu mantıksal parçalara ayır Her uygulama kendi dilimini görsün.
4 · GÜVENLİK Hassas sütunları gizle Başkasına verilen veriden kişisel bilgiyi çıkar.
Bonus: Kapalı durakları görünümün sonucundan çıkarmak. Dersin sonunda.
Dört amacı kısaca açın, ayrıntıya şimdi girmeyin. Her biri için ders boyunca bir örnek göreceğiz. Sadeleştirmeyi zaten yaptık. Dört amaç da aynı mekanizmadan çıkıyor: bir sorguya ad vermek. Fark, sorgunun içine ne yazdığımızdır.
Aynı Veri, Farklı Bakış
Aynı veritabanını kullanan üç birimin farklı bilgilere ihtiyacı var:
Muhasebe Durak başına gelir ve biniş sayısı.station_summary
Analist Kullanım örüntüleri; kimin bindiği değil.swipes_analysis
Durak yönetimi Yalnızca açık duraklar.open_stations
Hepsinin altında aynı üç tablo: cards · swipes · stations
Bugünün yol haritası bu slayt. Üç görünümün adını şimdiden söyleyin; dersin ikinci yarısında her birini kuracağız. Mesaj şu: veritabanını bölmüyoruz, çoğaltmıyoruz; aynı veriye farklı bakış açıları tanımlıyoruz. Her bakış açısı bir görünüm.
Satırları Tek Satıra İndirmek
Muhasebe işlem satırlarını değil, durak başına özeti görmek istiyor. GROUP BY aynı değeri taşıyan satırları gruplar; toplama fonksiyonları her grubu tek satıra indirir:
name type amount_kurus
name boardings revenue_kurus avg_fare_kurus
Cumhuriyet Meydanı boarding -875
Gar boarding -1750
Cumhuriyet Meydanı boarding -1750
Gar boarding -1750
Cumhuriyet Meydanı boarding -875
Gar 2 3500 1750.0
Cumhuriyet Meydanı 3 3500 1166.67
Slayttaki beş satır gerçek tablodan seçilmiş örnek; gerçek tabloda 29 biniş var. Aynı renk aynı durağı gösteriyor. İkinci tıklamada satırlar yer değiştirip gruplarına toplanıyor: GROUP BY’ın yaptığı budur. Üçüncü tıklamada her grup tek satıra iniyor. Gar’da iki biniş var (3500 kuruş toplam, ortalama 1750); Cumhuriyet Meydanı’nda üç (toplam 3500, ortalama 1166,67). Fonksiyonun ne yaptığını görsel olarak anlayan öğrenci COUNT, SUM, AVG farkını ezberlemeden kavrar.
AVG ve ROUND
Biniş tutarları eksi değerlerle tutuluyor. Durak başına ortalama ücreti göstermek için işareti tersine çeviriyoruz:
SELECT "station_id" , - AVG ("amount_kurus" ) AS "avg_fare"
FROM "swipes" WHERE "type" = 'boarding'
GROUP BY "station_id" ;
1
1166.66666666667
2
1750.0
…
…
ROUND(değer, 2) iki ondalığa yuvarlar:
Cumhuriyet Meydanı’nda öğrenci ve tam tarifeli binişler (875 ve 1750 kuruş) birlikte bulunuyor; ortalama 1166,666… Bu yüzden uzun bir ondalık sayı görüyoruz. Diğer duraklarda tek tarife olduğu için sonuç tam sayı. Yuvarlama sonucu okumayı kolaylaştırır, ancak sonraki hesaplarda yuvarlanmış değeri kullanmak hata biriktirebilir. Görünüm tanımında ortalamayı yuvarlarsak onu kullanan sorgular da bu değeri alır. Bunu birkaç slayt sonra “ortalamaların ortalaması” örneğinde göreceğiz.
Durak Adını da Getirelim
swipes yalnızca durak kimliğini taşır. Adı ve ilçeyi görmek için stations ile birleştiriyoruz. Sırayı unutmayın: önce JOIN, sonra GROUP BY çalışır.
SELECT "stations" ."id" , "name" , "district" ,
COUNT (* ) AS "boardings" ,
- SUM ("amount_kurus" ) AS "revenue_kurus" ,
ROUND (- AVG ("amount_kurus" ), 2 ) AS "avg_fare_kurus"
FROM "swipes"
JOIN "stations" ON "stations" ."id" = "swipes" ."station_id"
WHERE "type" = 'boarding'
GROUP BY "stations" ."id" ;
GROUP BY JOIN’den sonra çalıştığı için gruplama yapılırken durak adı da elimizde. GROUP BY "stations"."id" yazdık çünkü name ve district her durak için tek değer; SQLite buna izin verir (bazı sistemler name’i de gruplamayı ister). -SUM(...) işareti çevirir; iadeler WHERE ile dışarıda olduğu için toplam yalnızca binişlerdir. Sonucu bir sonraki slaytta görüyoruz.
Durak Başına Özet
Sorgunun sonucu: her durak için bir satır.
1
Cumhuriyet Meydanı
İlkadım
6
7000
1166.67
2
Gar
İlkadım
7
12250
1750.0
3
Büyük Cami
İlkadım
3
5250
1750.0
4
Atakent
Atakum
5
8750
1750.0
5
Yurtlar
Atakum
5
4375
875.0
6
Mimarsinan
Atakum
3
5250
1750.0
Altı satırın gelir toplamı 42875 kuruş: tablodaki bütün binişlerin geliri.
Altı satırın toplamı 42875 kuruş. Bu sayıyı SELECT -SUM("amount_kurus") FROM "swipes" WHERE "type" = 'boarding'; ile de doğrulayabilirler. Bir sonraki adım bu sorguya ad vermek.
station_summary: Özet Sorgusuna Ad Vermek
Bu sorguyu her gün yazmak yerine görünüm yapalım:
CREATE VIEW "station_summary" AS
SELECT "stations" ."id" , "name" , "district" ,
COUNT (* ) AS "boardings" ,
- SUM ("amount_kurus" ) AS "revenue_kurus" ,
ROUND (- AVG ("amount_kurus" ), 2 ) AS "avg_fare_kurus"
FROM "swipes"
JOIN "stations" ON "stations" ."id" = "swipes" ."station_id"
WHERE "type" = 'boarding'
GROUP BY "stations" ."id" ;
Sorgunun başına yalnızca CREATE VIEW "station_summary" AS ekledik; gerisi aynı. Görünümde sütun adlarını (boardings, revenue_kurus) biz seçtik; görünümü sorgulayan kişi bu adları kullanır.
station_summary’yi Sorgulamak
En çok biniş yapılan üç durağı artık kısa bir sorguyla bulabiliriz:
SELECT "name" , "boardings" FROM "station_summary"
ORDER BY "boardings" DESC LIMIT 3 ;
Gar
7
Cumhuriyet Meydanı
6
Atakent
5
Görünümü, tabloda SELECT yazıyormuş gibi sorguluyoruz: ORDER BY, LIMIT hepsi çalışır. Önemli olan, görünümün içindeki toplama sorgusunu bir daha hiç yazmamamız. Muhasebeden gelen “en yoğun üç durak” sorusu artık iki satır.
Görünüm Güncel Sonucu Verir
Görünüm veri saklamaz; her sorgulandığında tablodaki güncel verilerle sonuç üretir:
swipes (Gar binişleri)
24
2026-10-28 17:35:44
-1750
30
2026-10-30 08:16:29
-1750
33
2026-10-30 17:55:48
-1750
42
2026-11-02 08:10:00
-1750
→ SELECT
station_summary
Gar
7 8
12250 14000
INSERT INTO "swipes" ("card_id" , "station_id" , "type" ,
"datetime" , "amount_kurus" )
VALUES (17 , 2 , 'boarding' , '2026-11-02 08:10:00' , - 1750 );
Öğrenci aynı komutları çalıştırırsa station_summary sorgusunda Gar’ın sayısının 8’e çıktığını görür. Denemeyi geri almak için DELETE FROM "swipes" WHERE "datetime" = '2026-11-02 08:10:00'; yazılır; geçen haftaki balance_revert tetikleyicisi kartın bakiyesini de eski hâline döndürür. İki noktayı vurgulayın: Görünüme hiçbir şey yazmadık, yine de sonuç değişti, çünkü görünüm her sorguda altındaki sorguyu yeniden çalıştırıyor. Bunun bedeli: büyük bir tabloda her çağrıda toplamayı yeniden hesaplamak yavaş olabilir. PostgreSQL ve Oracle’da bu yüzden sonucu saklayan materialized view vardır; SQLite’ta yoktur.
Tuzak: Binişi Olmayan Duraklar Nereye Gitti?
stations tablosunda sekiz durak var, ama station_summary altı satır döndürüyor. Eksik olanlar:
SELECT "name" , "district" FROM "stations"
WHERE "id" NOT IN (SELECT "id" FROM "station_summary" );
Tekkeköy
Tekkeköy
Belediye Evleri
Tekkeköy
Neden? JOIN yalnızca iki tabloda da eşleşen satırları tutar. Binişi olmayan durak swipes ile eşleşmez ve sonuçtan düşer; özet raporda o durak hiç yokmuş gibi görünür.
Bu, görünümde sık yapılan bir hatanın erken uyarısı: görünümün sonucunu tam sanmak. Raporda “hiç kullanılmayan duraklar” önemliyse JOIN yerine LEFT JOIN gerekir; bir sonraki slayt bunu gösteriyor.
LEFT JOIN: Eşleşmeyeni de Tut
LEFT JOIN soldaki tablonun tüm satırlarını korur; eşleşme yoksa sağ taraf NULL kalır:
SELECT "stations" ."name" , COUNT ("swipes" ."id" ) AS "boardings"
FROM "stations"
LEFT JOIN "swipes" ON "stations" ."id" = "swipes" ."station_id"
AND "swipes" ."type" = 'boarding'
GROUP BY "stations" ."id" ;
…
…
Tekkeköy
0
Belediye Evleri
0
Filtreyi ON içine yazdık; WHERE’e yazsaydık eşleşmeyen satırlar yine elenirdi.
Çıktı sekiz satırdır; slaytta son ikisi gösteriliyor. Tekkeköy ve Belediye Evleri için boardings 0 çıkar. COUNT(*) yerine COUNT("swipes"."id") yazdık: COUNT(*) eşleşmeyen satırı da 1 sayardı. ON koşuluna type = 'boarding' yazdığımıza dikkat edin; WHERE’e yazsaydık LEFT JOIN yine eşleşmeyenleri eleyip INNER JOIN’e dönerdi. Kural: soldaki tablonun tüm satırlarını korumak istiyorsak sağ tabloya ait filtreyi ON içine yazarız.
VIEW, TEMP VIEW ve CTE’nin Geçerlilik Süresi
Bazen bir ara sorguyu şemada kalıcı olarak tutmak istemeyiz. Üç yöntem farklı süreler boyunca kullanılabilir:
bağlantı 1 · sqlite3 ulasim.db bağlantı 2 · yeniden açıldı
VIEWşemada kalıcı
CREATE VIEW station_summary
TEMP VIEWbağlantı boyunca
Zaman çizelgesi olarak okutun: soldan sağa iki bağlantı. Kesikli çizgi .quit anı, yani oturumu kapatıp yeniden açtığımız yer. Kalıcı görünüm iki bağlantıda da kullanılabilir; geçici görünüm yalnızca ilk bağlantıda, CTE ise tanımlandığı sorguda geçerlidir. Dördüncü tıklamada geçici görünümün yeni bağlantıda neden bulunamadığını görüyoruz. Kapanışta bu ayrımı yeniden özetleyeceğiz.
CREATE TEMPORARY VIEW
Analist bir ilçe özeti denemek istiyor ama bunu şemaya yazıp herkese göstermek istemiyor. Görünümün üstüne bir görünüm kuruyoruz:
CREATE TEMPORARY VIEW "district_summary" AS
SELECT "district" ,
SUM ("boardings" ) AS "boardings" ,
SUM ("revenue_kurus" ) AS "revenue_kurus" ,
ROUND (1.0 * SUM ("revenue_kurus" ) / SUM ("boardings" ), 2 )
AS "avg_fare_kurus"
FROM "station_summary"
GROUP BY "district" ;
SELECT * FROM "district_summary" ;
Atakum
13
18375
1413.46
İlkadım
16
24500
1531.25
Bir görünümün üstüne başka bir görünüm kurulabilir. Geçici görünüm sqlite_schema’da görünmez; sqlite_temp_schema tablosunda durur. Geçici görünüm denemeler için birebir: bir sorgunun doğru olup olmadığını denemek, kendi analizim için ara adım kurmak, ama başkalarının görmesini ya da şemayı kirletmesini istemediğim durumlar. Ağırlıklı ortalamayı bilerek kullandık; iki slayt sonra neden diye soruyor.
Bağlantı Kapanınca Geçici Görünüm Kaybolur
Geçici görünüm yalnızca bu bağlantıda geçerlidir. sqlite3 oturumunu kapatıp yeniden açalım:
sqlite> .quit
$ sqlite3 ulasim.db
sqlite> SELECT * FROM "district_summary";
Parse error: no such table: district_summary
Üstüne kurulduğu station_summary kalıcı olduğu için yeni bağlantıda hâlâ çalışır; kaybolan yalnızca üstündeki geçici katmandır.
Bu, geçerlilik süresi çizelgesinin dördüncü adımının terminaldeki karşılığı. Kalıcı görünüm şemada durduğu için iki bağlantıda da kullanılabilir; geçici görünüm .quit ile kaybolur. Öğrenciler kendi terminallerinde deneyebilir.
Tuzak: Ortalamaların Ortalaması
İlçe ortalamasını yazmanın kolay yolu, durak ortalamalarının ortalamasını almaktır. Sonuç yanlıştır:
SELECT "district" ,
ROUND (AVG ("avg_fare_kurus" ), 2 ) AS "yanlis"
FROM "station_summary"
GROUP BY "district" ;
Atakum: yanlış 1458.33 durak ortalamalarının ortalaması
Atakum: doğru 1413.46 toplam gelir ÷ toplam biniş
İlçe: İlkadım 1555.56 ile 1531.25 aynı fark, aynı neden
Neden? Duraklar farklı sayıda binişe sahip: az binişli durak, çok binişli durakla eşit ağırlık almamalı. Doğru yöntem: SUM(gelir) / SUM(biniş).
Bu tuzak yalnızca SQL’e özgü değil, istatistik. Görünümün üstüne görünüm kurmak bu hatayı kolaylaştırır: alt görünümün avg_fare_kurus sütunu hazır olduğu için üstteki sorguda AVG yazmak çok doğal gelir. Kural: özet sütunlarını (ortalama, oran) bir üst düzeyde yeniden özetlemeyin; ham toplamları (sayı, toplam) taşıyın ve oranı en son hesaplayın. Atakum’da Yurtlar 875 tarifeli beş biniş taşıyor, diğer duraklar 1750; yanlış yöntem Yurtlar’ın etkisini abartıyor.
CTE: Tek Sorgunun Görünümü
Ara sonuç yalnızca bu sorguya özelse: CTE .
WITH "per_station" AS (
SELECT "stations"."id", "district",
COUNT(*) AS "boardings",
-SUM("amount_kurus") AS "revenue_kurus"
FROM "swipes"
JOIN "stations" ON "stations"."id" = "swipes"."station_id"
WHERE "type" = 'boarding'
GROUP BY "stations"."id"
)
SELECT "district", SUM("boardings") AS "boardings",
SUM("revenue_kurus") AS "revenue_kurus"
FROM "per_station"
GROUP BY "district";
WITH ad AS (…) Ad yalnızca bu sorguda geçerlidir.
iç sorgu Adın neyi temsil ettiğini söyler.
dış sorgu Adı bir tablo gibi çağırır.
CTE, Common Table Expression (ortak tablo ifadesi) demektir. Sözdizimini üç parça olarak okutun: WITH ad AS ( ile tanım başlar, parantez içindeki sorgu adın içeriğidir, parantezden sonra gelen asıl sorgu adı kullanır. Virgülle birden çok CTE tanımlanabilir (slayttaki şemada tek tane var). Burada ilçe özetini station_summary görünümüne dokunmadan, aynı mantığı sorgunun içine gömerek kurduk. Önemli ayrım: görünüm yeniden kullanım içindir; CTE aynı sorguda bir alt sorguyu tekrar yazmamak içindir.
CTE ile Aynı Sonuç, Şemada İz Yok
Önceki slayttaki sorgu, geçici görünümle aynı sonucu verir:
Atakum
13
18375
İlkadım
16
24500
Sorgu bitince per_station yok olur:
SELECT COUNT (* ) FROM "sqlite_schema"
WHERE "name" = 'per_station' ;
Bu sorgu geçici görünümle aynı sonucu verdi (13 ve 16 biniş), ama hiçbir nesne oluşturmadı; dosyaya, şemaya, bağlantıya iz bırakmadı. Hangisinin daha iyi olduğu sorusu yanlış; ihtiyaç farklı: tekrar kullanılacaksa görünüm, deneme için geçici görünüm, tek sorguluk ara adım için CTE.
Hangisini Ne Zaman?
Ömrü
Silinene kadar
Bağlantı boyunca
Tek sorgu
Şemada görünür mü?
Evet
Hayır (sqlite_temp_schema)
Hayır
Başkası kullanabilir mi?
Evet
Hayır
Hayır
Ne zaman?
Birçok kişi ya da sorgu kullanacak
Kendi denememi hızlandırmak
Tek sorguda ara adım
Silmek
DROP VIEW "ad";
DROP VIEW "ad";
Gerekmez
Kural: başkası kullanacaksa VIEW, kendim deneyeceksem TEMPORARY VIEW, bir sorguluksa CTE.
Tablo ömür çizelgesinin metin hâli. DROP VIEW hem kalıcı hem geçici görünüm için aynıdır; olmayan bir görünümü silmeye çalışırsak no such view hatası alırız, DROP VIEW IF EXISTS hata vermez. Mola öncesi son slayt: ilk yarıda görünümün sorguya ad vermek olduğunu, ikinci yarıda aynı fikirle bir tabloyu parçalamayı, gizlemeyi ve silmeyi yönetmeyi göreceğiz.
Bölümleme: Bir Tablodan Üç Görünüm
swipes içinde üç tür olay karışık; her birim yalnızca kendi türünü görmek ister. Satırları kopyalamadan bölmek için görünüm kullanırız:
swipes
1
topup
15000
2
topup
10000
5
boarding
-1750
6
boarding
-875
7
boarding
-1750
8
boarding
-875
22
refund
1750
32
refund
875
boardings 29 satır · tahsilat ekranı 5 6 7 8 …
topups 9 satır · yükleme gişesi 1 2 …
refunds 3 satır · iade birimi 22 32 41
Bölümleme, bir tabloyu mantıksal parçalara ayırmaktır. Parça ölçütü, her görünümün WHERE koşulu. Burada ölçüt type sütunu; tarih (her ay için ayrı görünüm), ilçe ya da yıl da ölçüt olabilirdi. Bu bölümleme mantıksaldır : veri tek tabloda duruyor, hiçbir satır kopyalanmadı; görünümler aynı satırlara farklı pencerelerden bakıyor. Slayttaki sekiz satır swipes’tan seçilmiş örneklerdir; sağdaki sayılar gerçek satır sayıları (29, 9, 3).
Üç Görünüm Yazalım
Her görünüm yalnızca kendi işine yarayan sütunları taşır; yüklemenin durağı olmadığı için topups görünümünde station_id yok:
CREATE VIEW "boardings" AS
SELECT "id" , "card_id" , "station_id" , "datetime" , "amount_kurus"
FROM "swipes" WHERE "type" = 'boarding' ;
CREATE VIEW "topups" AS
SELECT "id" , "card_id" , "datetime" , "amount_kurus"
FROM "swipes" WHERE "type" = 'topup' ;
CREATE VIEW "refunds" AS
SELECT "id" , "card_id" , "station_id" , "datetime" , "amount_kurus"
FROM "swipes" WHERE "type" = 'refund' ;
Sütun seçimi de bölümlemenin parçasıdır: yüklemelerde station_id hep boş olduğu için görünümden çıkarıldı. Üç görünümün her birinin tanımı yalnızca WHERE koşulunda ve sütun listesinde farklı.
Görünümleri Sorgulamak
Her görünüm artık kendi tablosu gibi sorgulanır:
22
18
4
2026-10-28 08:52:10
1750
32
19
5
2026-10-30 08:42:00
875
41
21
2
2026-11-01 15:30:00
1750
Satır sayıları: boardings 29, topups 9, refunds 3. Toplam 41: swipes’ın tamamı.
Üç görünümün satır sayıları 29, 9 ve 3: toplamı 41, yani swipes’ın tamamı. Bu bir doğrulama yöntemidir: parçaların toplamı bütüne eşit mi? Eşit değilse bir tür type değeri kapsam dışında kalmış demektir.
Bölümlemenin Sınırı ve Adlandırma
Mantıksal bölümleme: görünüm Veri tek tabloda kalır. Satırlar kopyalanmaz. Her uygulama kendi dilimini sorgular. Disk ya da hız kazancı yoktur.
Fiziksel bölümleme: DBMS özelliği PostgreSQL gibi sistemler tabloyu gerçekten parçalara böler. Büyük tabloda yalnızca ilgili parça taranır. SQLite’ta yoktur.
Ad seçerken bir denge var: boardings açıklayıcıdır; v1 kısadır ama anlamsızdır. Görünümün adını, onu ilk kez gören kişi içeriğini tahmin edebilsin diye seçin.
“Bölümleme” kelimesi iki farklı şey için kullanılır; ikisini ayırın. Görünümle yaptığımız, uygulamaya kolaylık sağlayan mantıksal bölümleme. Gerçek veritabanı sistemlerinde, yüz milyonlarca satırlık bir tabloyu aylara böldüğünüzde sorgu yalnızca ilgili ayın dosyasını okur; bu performans içindir ve SQLite’ın kapsamı dışındadır. Adlandırmada çift tırnak notu: "2026" gibi rakamla başlayan bir ad yazılabilir ama her yerde çift tırnak gerektirir; sade bir ad daha az hata verir.
Herkes Her Şeyi Görmemeli
Analist en çok kullanılan duraklara bakacak; kimin bindiğini bilmesine gerek yok. Kişiselleştirilmiş kartta card_id bir kişiyi belirler: kişisel veri .
swipes (ham tablo)
5
17
-1750
6
19
-875
7
18
-1750
←
Analist kullanım örüntülerini inceler
→
swipes_analysis (görünüm)
5
-1750
Anonim
6
-875
Anonim
7
-1750
Anonim
ham tabloya da erişebilir REVOKE ALL ON swipes FROM analyst;
GRANT SELECT ON swipes_analysis TO analyst;
Kişisel veri tanımı için KVKK’yı hatırlatın: belirli ya da belirlenebilir gerçek kişiye ilişkin her bilgi. Kişiselleştirilmiş kartın numarasından sahibine ulaşılabilir; bu nedenle numarayı analistle paylaşmak istemeyiz. Görünüm bu bilgiyi gizler. Üçüncü tıklamada kritik nokta: SQLite’ın erişim denetimi yok. Dosyayı açan herkes bütün tabloları okuyabilir; bu yüzden görünüm burada gizleme aracıdır, güvenlik sınırı değildir. Dördüncü tıklamada PostgreSQL gibi sistemlerin yaptığı gösteriliyor: ham tabloya erişimi kapatıp görünüme okuma izni verirler. Görünüm ancak erişim denetimiyle birlikte koruma sağlar.
swipes_analysis: Sütunu Gizlemek
CREATE VIEW "swipes_analysis" AS
SELECT "id" , "station_id" , "datetime" , "amount_kurus" ,
'Anonim' AS "card"
FROM "swipes"
WHERE "type" = 'boarding' ;
SELECT * FROM "swipes_analysis" LIMIT 3 ;
5
2
2026-10-26 08:15:03
-1750
Anonim
6
5
2026-10-26 08:31:44
-875
Anonim
7
4
2026-10-26 08:47:19
-1750
Anonim
Görünümdeki card sütununda gerçek kart kimliği yerine her satırda Anonim yazıyor. Analist kartın kimliğini göremez.
card_id’yi tamamen çıkarmak yerine card adlı sabit bir sütun koyduk; bu analiste “burada bir kart bilgisi var ama size gösterilmiyor” mesajı verir. Bu bir tasarım tercihi: bazı ekipler sütunu tamamen kaldırır. Görünüm yalnızca binişleri taşıyor; topup ve refund satırları analiz kapsamı dışında bırakıldı. İlk bakışta temiz görünen bu görünümün iki zayıflığı var; sıradaki iki slayt onları açıyor.
Sızıntı 1: id ile Geri Birleştirme
Görünümde id sütunu duruyor. SQLite ham tabloyu da açık bıraktığı için analist bu kimlikle kartı bulabilir:
SELECT "a" ."id" , "a" ."datetime" , "holder"
FROM "swipes_analysis" AS "a"
JOIN "swipes" ON "swipes" ."id" = "a" ."id"
JOIN "cards" ON "cards" ."id" = "swipes" ."card_id"
LIMIT 3 ;
5
2026-10-26 08:15:03
Ayşe Kaya
6
2026-10-26 08:31:44
Elif Yıldız
7
2026-10-26 08:47:19
Mehmet Demir
Gizlediğimiz bilgi, bir JOIN ile geri geldi.
Öğrencilerden önce tahmin etmelerini isteyin: “Analist elindeki görünümle kimin bindiğini bulabilir mi?” Cevap evet; çünkü görünümde satırın kimliği var ve ham tabloya erişim kapatılmadı. Satır kimliğini görünümden çıkarmak ilk adımdır; asıl çözüm ham tabloya erişimi kapatmaktır. SQLite bunu yapamaz.
Sızıntı 2: SELECT * ve Tekil Biniş
İki zayıflık daha:
1 · SELECT * her sütunu taşır. Görünümü SELECT *, 'Anonim' AS "card" diye yazarsak card_id de görünümün içindedir; 29 satırın 29’unda kart numarası görünür.
2 · Anonim görünüm bile kişiyi tek başına ele verebilir. Sabah 07:48’de Mimarsinan’da kaç biniş var?
SELECT "datetime" , "station_id" , "amount_kurus"
FROM "swipes_analysis"
WHERE "datetime" LIKE '2026-10-28 07:%' ;
2026-10-28 07:48:02
6
-1750
O saatte o durakta tek kişi bindiyse, o kişiyi tanıyan biri satırı eşleştirebilir.
İkinci zayıflık “bağlantılı saldırı” (linkage attack) diye bilinir: tek başına zararsız görünen alanlar (zaman, yer, tutar), dışarıdan bilinen bir bilgiyle birleşince kişiyi ele verir. Kişisel Verilerin Korunması Kanunu’nda anonim hâle getirme, verinin başka verilerle eşleştirilse bile kişiyle ilişkilendirilememesi anlamına gelir. Bizim görünümümüz yalnızca maskeleme , anonimleştirme değil. Gerçekten anonim paylaşım için zamanı saate yuvarlamak, az sayıdaki satırları birleştirmek gibi başka önlemler gerekir. Görünüm bu işin yalnızca ilk adımıdır.
Erişim Denetimi: SQLite’ta Yok, Başka Sistemlerde Var
SQLite Veritabanı tek bir dosyadır. Dosyayı açan herkes bütün tabloları okuyabilir; “yalnızca bu görünümü ver” diyemezsiniz. Görünüm burada gizleme aracıdır, güvenlik sınırı değildir.
PostgreSQL, MySQL, Oracle Kullanıcılara tek tek yetki verilir. Ham tabloyu kapatıp yalnızca görünümü açabilirsiniz:REVOKE ALL ON swipes FROM analyst;GRANT SELECT ON swipes_analysis TO analyst;
Güvenli paylaşım iki parçadır: hassas veriyi görünümle gizlemek ve ham veriye erişimi yetkiyle kapatmak . SQLite yalnızca birincisini sağlar.
Beklenti yönetimi: bu slayttan sonra öğrenci SQLite’ta görünümle güvenlik sağladığını düşünmemeli. SQLite gömülü bir veritabanıdır; telefon uygulamaları, tarayıcılar, küçük araçlar için tasarlandı; kullanıcı yönetimi yok. Sunucu tabanlı sistemlerde (PostgreSQL gibi) kullanıcılar ve roller vardır; GRANT ve REVOKE komutları ders programının ilerleyen bölümlerinde ayrıca ele alınabilir. Buradaki sözdizimi yalnızca örnektir, bugün çalıştırmayacağız.
Bugüne Kadar: Her Sorguda closed = 0
Dördüncü haftadaki yumuşak silmeyi hatırlayın: kapanan durak silinmedi, closed = 1 yapıldı. Ama her okuma sorgusunun kapalı durakları süzmesi gerekti:
SELECT * FROM "stations" WHERE "closed" = 0 ;
Bu koşulu unutan her sorgu kapalı durağı gösterir. Görünüm, koşulu her sorguda tekrarlamamızı önler:
CREATE VIEW "open_stations" AS
SELECT "id" , "name" , "district" FROM "stations"
WHERE "closed" = 0 ;
Görünümün closed sütununu taşımadığına dikkat edin: uygulama bu görünümü kullanırken bir durağın kapalı olabileceğini hiç bilmek zorunda değil. Bu, dördüncü hafta sonunda verdiğimiz sözün karşılığı: filtreyi bir kez yazdık, bir daha unutamayız.
open_stations’ı Sorgulamak
SELECT * FROM "open_stations" ;
1
Cumhuriyet Meydanı
İlkadım
2
Gar
İlkadım
3
Büyük Cami
İlkadım
4
Atakent
Atakum
5
Yurtlar
Atakum
6
Mimarsinan
Atakum
7
Tekkeköy
Tekkeköy
Belediye Evleri closed = 1 olduğu için listede yok.
Bu, dördüncü hafta sonunda verdiğimiz sözün karşılığı. Belediye Evleri’ni kimse silmedi; yalnızca görünüm onu göstermiyor.
Kapat ve Gör
Yumuşak silme, stations tablosundaki closed değerini değiştirir. Görünümü yeniden sorguladığımızda sonuç da değişir:
UPDATE "stations" SET "closed" = 1 WHERE "name" = 'Yurtlar' ;
stations
4
Atakent
0
5
Yurtlar
0 1
6
Mimarsinan
0
8
Belediye Evleri
1
→ closed = 0
open_stations
4
Atakent
5
Yurtlar
6
Mimarsinan
Sağdaki tablo tek bir sorgunun sonucu; soldaki tabloyu değiştirince sağdaki yeniden sorgulanınca değişiyor. Sunumdaki ilk iki tıklamada tablo ve görünüm eş zamanlı gösterilir. Üçüncü tıklamada bir sorun belirir: uygulama durakları yalnızca open_stations üzerinden görüyorsa durak kapatmak için de stations tablosunu bilmek zorunda. Bu bağımlılık istenmez. Görünüm üzerinden de silebilsek ne iyi olurdu; ama görünüme DELETE yazılamıyor.
Görünüme Doğrudan DELETE Yazamayız
DELETE FROM "open_stations" WHERE "name" = 'Mimarsinan' ;
Parse error: cannot modify open_stations because it is a view
Uygulama yalnızca open_stations’ı biliyor olsun. Durak kapatmak için bu görünüme DELETE yazmasını istiyoruz; ama SQLite hangi tabloyu değiştireceğini bilemiyor.
Çözüm: SQLite’a bu ifade yerine ne yapacağını biz söyleyeceğiz.
Aynı hatayı bu dersin başında UPDATE ile görmüştük. Burada soru yeni bir şey değil, bir ihtiyaç: görünümü okumak için değil, yazmak için de kullanmak istiyoruz. Çözüm tetikleyiciler; geçen hafta AFTER INSERT ve AFTER DELETE tetikleyicilerini görmüştük. Görünümlerde yalnızca bir tür tetikleyici çalışır: INSTEAD OF.
INSTEAD OF: Bunun Yerine Şunu Yap
INSTEAD OF DELETE tetikleyicisi, görünüme gelen DELETE ifadesini yakalar ve onun yerine kendi ifadesini çalıştırır:
gelen ifade DELETE FROM "open_stations"
WHERE "name" = 'Mimarsinan';
→
⚡
INSTEAD OF DELETE UPDATE "stations"
SET "closed" = 1
WHERE "id" = OLD."id";
5
Yurtlar
1
6
Mimarsinan
0 1
Akışı soldan sağa izletin. Birinci tıklama: ifade görünüme geliyor, normalde burada hata alırdık. İkinci tıklama: tetikleyici araya girip DELETE’in yerine geçiyor; üzeri çizilen ifade hiç çalışmayacak. Üçüncü tıklama: tetikleyicinin kendi ifadesi stations tablosunda closed’u 1 yapıyor ve görünüm satırı artık göstermiyor. OLD burada silinmek istenen satır, yani Mimarsinan. Dikkat: hiçbir satır gerçekten silinmedi; uygulama DELETE yazdığını sanıyor, veritabanı bunu yumuşak silmeye çeviriyor.
CREATE TRIGGER … INSTEAD OF
CREATE TRIGGER "open_stations_delete"
INSTEAD OF DELETE ON "open_stations"
FOR EACH ROW
BEGIN
UPDATE "stations" SET "closed" = 1
WHERE "id" = OLD ."id" ;
END ;
INSTEAD OF DELETE ON "open_stations"
Ne zaman: görünüme DELETE yazıldığında, onun yerine
FOR EACH ROW
Silinmek istenen her satır için bir kez
UPDATE "stations" ...
Bunun yerine çalışacak ifade
OLD."id"
Silinmek istenen satırın kimliği
Geçen haftaki CREATE TRIGGER sözdizimine çok benziyor; tek fark BEFORE/AFTER yerine INSTEAD OF ve tablo yerine görünüm adı. SQLite’ta INSTEAD OF yalnızca görünümlerde, BEFORE/AFTER ise yalnızca tablolarda çalışır. OLD silinmek istenen satırı, NEW (ekleme ve güncellemede) yazılmak istenen satırı temsil eder.
Tetikleyiciyi Çalıştıralım
DELETE FROM "open_stations" WHERE "name" = 'Mimarsinan' ;
Hata yok. Görünüme, sonra asıl tabloya bakalım:
SELECT * FROM "open_stations" ;
SELECT "id" , "name" , "closed" FROM "stations"
WHERE "closed" = 1 ;
1
Cumhuriyet Meydanı
İlkadım
2
Gar
İlkadım
3
Büyük Cami
İlkadım
4
Atakent
Atakum
7
Tekkeköy
Tekkeköy
5
Yurtlar
1
6
Mimarsinan
1
8
Belediye Evleri
1
Mimarsinan görünümden çıktı; tabloda satır yerinde, yalnızca closed = 1.
Bu tablolar, bu dersteki dizinin devamı: önce UPDATE ile Yurtlar kapandı, sonra DELETE ile Mimarsinan. İki yoldan da sonuç aynı: closed = 1. Öğrencinin kendi dosyasında bu sırayı izlemesini bekleyin. Aksi hâlde çıktıları farklı çıkar. Tüm stations tablosunu görmek isteyen WHERE koşulunu kaldırabilir: sekiz satırın üçü kapalıdır.
Ekleme de Aynı Kapıdan Geçsin
Uygulama kapalı bir durağı yeniden açmak için de görünüme INSERT yazar. Tetikleyici yoksa:
INSERT INTO "open_stations" ("name" , "district" )
VALUES ('Belediye Evleri' , 'Tekkeköy' );
Parse error: cannot modify open_stations because it is a view
Burada iki farklı durum var:
Durak zaten stations tablosunda var, yalnızca kapalı: yeni satır eklemeyip closed’u 0 yapmalıyız.
Durak hiç yok : gerçekten yeni bir satır yazmalıyız.
stations.name sütunu UNIQUE olduğundan aynı adlı ikinci satır eklenemez; kapalı bir durağı yeniden açmak için yeni satır eklemek hataya yol açardı. Bu iki durumu ayıracak bir koşul gerekiyor. Tetikleyiciler koşullu da çalışabilir: WHEN anahtar sözcüğü.
Yeniden Aç mı, Yeni Satır mı?
INSERT INTO open_stations (name, district)
'Belediye Evleri', 'Tekkeköy'
'Samsun Üniversitesi', 'Atakum'
name stations'ta zaten var mı?WHEN … IN
Evet → yeniden aç UPDATE "stations" SET "closed" = 0 WHERE "name" = NEW."name";
Hayır → yeni satır INSERT INTO "stations" ("name", "district") VALUES (NEW."name", NEW."district");
Akışı bir karar ağacı olarak anlatın: ekleme görünüme gelir, tetikleyici adın stations’ta olup olmadığına bakar; varsa durak yeniden açılır, yoksa yeni satır yazılır. İkinci tıklama Belediye Evleri, üçüncü tıklama Samsun Üniversitesi ile iki yolu da gösteriyor. Bu aslında iki ayrı tetikleyicidir; her biri kendi WHEN koşuluyla çalışır. Bir sonraki slaytta kodları var.
INSERT Tetikleyicisi 1: Durak Zaten Var
Aynı olaya iki tetikleyici bağlayacağız; hangisinin çalışacağını WHEN koşulu seçer. Birincisi kapalı durağı yeniden açar:
CREATE TRIGGER "open_stations_insert_existing"
INSTEAD OF INSERT ON "open_stations"
FOR EACH ROW
WHEN NEW ."name" IN (SELECT "name" FROM "stations" )
BEGIN
UPDATE "stations" SET "closed" = 0
WHERE "name" = NEW ."name" ;
END ;
WHEN koşulu doğruysa tetikleyici çalışır, yanlışsa atlanır.
NEW eklenmek istenen satırdır. WHEN içindeki alt sorgu stations tablosundaki mevcut adlara bakıyor: ad zaten varsa yeni satır yazmak yerine yalnızca closed bayrağı 0 yapılır. stations.name sütunu UNIQUE olduğundan aynı adlı ikinci satır zaten eklenemezdi; bu tetikleyici o durumu doğru yola çevirir.
INSERT Tetikleyicisi 2: Durak Hiç Yok
İkincisi, adı stations içinde olmayan duraklar için yeni satır yazar:
CREATE TRIGGER "open_stations_insert_new"
INSTEAD OF INSERT ON "open_stations"
FOR EACH ROW
WHEN NEW ."name" NOT IN (SELECT "name" FROM "stations" )
BEGIN
INSERT INTO "stations" ("name" , "district" )
VALUES (NEW ."name" , NEW ."district" );
END ;
İki koşul birbirinin tersidir (IN, NOT IN): her INSERT için tam bir tetikleyici çalışır.
İki koşulun birbirini tamamlaması önemli: biri IN, diğeri NOT IN; ikisi birlikte her durumu kapsıyor. Koşul hatalıysa aynı ekleme iki kez işlenebilir ya da hiç işlenmeyebilir; bu yüzden tetikleyici yazarken “her girdi için kaç tetikleyici çalışır?” diye sormak iyi bir kontrol.
Denemek
INSERT INTO "open_stations" ("name" , "district" )
VALUES ('Belediye Evleri' , 'Tekkeköy' );
INSERT INTO "open_stations" ("name" , "district" )
VALUES ('Samsun Üniversitesi' , 'Atakum' );
SELECT * FROM "open_stations" ;
…
…
…
7
Tekkeköy
Tekkeköy
8
Belediye Evleri
Tekkeköy
9
Samsun Üniversitesi
Atakum
Belediye Evleri aynı kimlikle (8) döndü: yalnızca closed = 0 oldu. Samsun Üniversitesi yeni satırdır; kimliği SQLite verdi.
Bu çıktıda Belediye Evleri’nin kimliği hâlâ 8: tetikleyici yeni satır yazmadı, yalnızca bayrağı çevirdi. Kimlik korunduğu için swipes’taki eski kayıtlar bu durağa bağlı kalmaya devam eder: tüm geçmiş korunur. Samsun Üniversitesi ise kimlik 9 ile yeni durak. Tetikleyiciyi iki kez deneyen öğrenci, ikinci denemede Belediye Evleri’nin zaten açık olduğunu görür; WHEN koşulu yine doğrudur, UPDATE bir şeyi değiştirmez ama hata da vermez.
Yumuşak Silmenin İki Yüzü
open_stations: bugünün listesi Uygulama yalnızca açık durakları görür. Yeni durak ekler, kapatır, yeniden açar; closed sütununu hiç bilmez.
station_summary: geçmiş raporu Kapalı bir durağın eski binişleri raporlarda durur. Yurtlar kapalı olsa da geçmişteki 5 biniş raporda görünür.
SELECT "name" , "boardings" FROM "station_summary"
WHERE "name" = 'Yurtlar' ;
Hangi görünümün hangi soruya cevap verdiğini bilmek, tasarımın parçasıdır: güncel liste için open_stations, tarihsel rapor için station_summary.
Yumuşak silmenin kazancı bu: geçmiş bozulmadan duruyor. Yurtlar kapalı olduğu hâlde station_summary görünümü onu hâlâ gösteriyor çünkü bu görünüm stations tablosuna closed filtresi uygulamadan bakıyor. Bu bir hata değil, bir karar: geçen ay Yurtlar’dan binen yolcuların gelirini raporlarda kaybetmek istemeyiz. İki görünüm aynı tabloya farklı sorular soruyor. Geçen haftanın uyarısını hatırlatın: kişisel veride yumuşak silme yeterli değildir; durak gibi kişisel olmayan kayıtlar için uygundur.
Hepsi Bir Arada
Aynı üç tablonun üzerinde yedi görünüm, beş ayrı ihtiyaç:
SADELEŞTİRME ride_historycards swipes stations
TOPLAMA station_summaryswipes stations
BÖLÜMLEME boardings · topups · refundsswipes
GÜVENLİK swipes_analysisswipes
YUMUŞAK SİLME open_stationsstations INSTEAD OF
geçici katman TEMPORARY VIEW district_summary · CTE per_station
cards id · holder · balance_kurus
swipes id · card_id · station_id · type · …
stations id · name · district · closed
Bu slayt dersin özeti. Dikkat çekin: altta yalnızca üç tablo var, üstte yedi görünüm; bölümleme tek amaç ama üç görünüm. Hiçbir görünüm veri taşımıyor; hepsi aynı satırlara farklı pencerelerden bakıyor. Renkler dersin dört amacını gösteriyor. Geçici katman, kalıcı şemaya girmeyen ara adımlar. Öğrencilerden .tables komutuyla kendi veritabanlarında bu haritayı doğrulamalarını isteyin: üç tablo, yedi görünüm.
Tuzak: Alttaki Tablo Değişirse
Görünüm tabloya bağlıdır. Bir kopya üzerinde cards tablosundan holder sütununu silelim:
ALTER TABLE "cards" DROP COLUMN "holder" ;
SELECT * FROM "ride_history" LIMIT 3 ;
holder
2026-10-26 08:15:03
Gar
holder
2026-10-26 08:31:44
Yurtlar
holder
2026-10-26 08:47:19
Atakent
Hata yok; ama kart sahiplerinin adı yerine holder kelimesi geliyor. SQLite, çift tırnakla yazılmış ve bulunamayan bir adı metin sabiti sayar: görünüm sessizce bozuldu.
Bu tuzak dersin en sinsi olanı: hata mesajı yok. Nedeni SQLite’ın eski bir uyumluluk davranışı: çift tırnaklı bir ad çözümlenemezse onu metin olarak yorumlar. Biz bu derste tanımlayıcıları hep çift tırnakla yazdığımız için görünümler de bu davranışa açık. Tek tırnakla yazılan metin ile çift tırnakla yazılan ad arasındaki ayrım geçen haftaki kuralımızdı; burada sınır çiziliyor. Bu örneği yalnızca kopya veritabanında deneyin. Resmî açıklama: https://www.sqlite.org/quirks.html
Sütunu Silmeden Önce Bağımlılıkları Kontrol Edin
Bir sütunu silmeden önce şemada o adı kimin kullandığını arayın:
SELECT "type" , "name" FROM "sqlite_schema"
WHERE "sql" LIKE '%holder%' ;
table
cards
view
ride_history
holder yalnızca cards tablosunda ve ride_history görünümünde geçiyor: silersek bozulacak nesne belli. Değişiklikten sonra görünümlerin sonuçlarını gözle denetlemek de iyi bir alışkanlıktır.
Koruma: sütun silmeden önce sqlite_schema içinde ad araması yapmak ve değişiklik sonrası görünümlerin sonuçlarını gözle denetlemek. Arama basit bir metin eşleşmesidir; ad başka bir nesnenin adının parçasıysa fazladan satır döndürebilir, ama kaçırmaz. Sonuç tablosu bu dersteki tüm görünümler kurulduktan sonraki durumdur.
Görünümün Sınırları
Sonucu saklamaz Her sorguda alttaki sorgu yeniden çalışır; büyük tabloda yavaş olabilir. MATERIALIZED VIEW (PostgreSQL, Oracle) sonucu saklar; SQLite’ta yoktur.
Doğrudan yazılamaz Görünüme INSERT, UPDATE, DELETE hata verir. Çözüm: INSTEAD OF tetikleyicisi.
Erişim denetimi yok (SQLite) Görünüm hassas sütunu gizler, ama ham tabloya erişimi kapatmaz. Güvenlik için yetkilendirme gerekir.
Alttaki tabloya bağlıdır Tablonun yapısı değişirse görünüm bozulabilir; bazen hata vermeden.
Görünüm bir bakış açısı tanımlar; veri, yetki ve hız sorunlarını tek başına çözmez.
Dört sınırı kapanıştan önce toplu söyleyin; her biri derste bir yerde geçti. Dersin başındaki cümleye dönün: “görünüm bir sorguya verilen addır”. Ad vermek güçlü bir araç, ama adın arkasında hâlâ sorgu çalışıyor, hâlâ aynı tablolar var. Performans konusu birkaç hafta sonra indeks ve sorgu planı konularıyla birlikte tekrar gündeme gelebilir.
Alıştırmalar
Beş soru dersin dört bölümünü tarar. Her sorunun cevabı bir sonraki slayttadır.
Günlük gelir: özet görünümü
Ortalamanın üstündeki duraklar: CTE
Sızıntıyı bulun: güvenlik
Görünümden silmek: hata ve çıkış yolları
INSTEAD OF DELETE: tetikleyici
Zaman sınırlıysa 1, 3 ve 5 numaralı soruları sınıfta yapın; 2 ve 4 ders sonrasına kalabilir. Her soruda önce “bu bir görünüm mü, CTE mi, yoksa doğrudan sorgu mu?” diye sorun. Her sorunun cevabı bir sonraki slayttadır.
Soru 1: Günlük Gelir
Muhasebe, her günün toplam biniş gelirini görmek istiyor. Tarih bölümünü almak için date("datetime") kullanabilirsiniz. daily_revenue adlı bir görünüm yazın.
Soru 1: Cevap
CREATE VIEW "daily_revenue" AS
SELECT date ("datetime" ) AS "day" ,
- SUM ("amount_kurus" ) AS "revenue_kurus"
FROM "swipes"
WHERE "type" = 'boarding'
GROUP BY date ("datetime" );
2026-10-26
8750
2026-10-27
7875
2026-10-28
7875
…
…
date() zaman damgasından yalnızca günü alır; GROUP BY aynı günün satırlarını toplar. Toplam yedi gün çıkar.
Soru 2: Ortalamanın Üstündeki Duraklar
Durak başına ortalama biniş sayısının üzerinde kalan durakları tek bir sorguyla bulun. Her durağın biniş sayısını sorgunun iki yerinde kullanmanız gerekecek; kalıcı bir nesne oluşturmayın. Hangi aracı kullanırdınız?
Soru 2: Cevap
WITH "per_station" AS (
SELECT "station_id" , COUNT (* ) AS "boardings"
FROM "swipes" WHERE "type" = 'boarding'
GROUP BY "station_id"
)
SELECT "name" , "boardings" FROM "per_station"
JOIN "stations" ON "stations" ."id" = "station_id"
WHERE "boardings" >
(SELECT AVG ("boardings" ) FROM "per_station" );
Sonuç: Cumhuriyet Meydanı (6), Gar (7), Atakent (5), Yurtlar (5); ortalama 4,83. Ara sonuç iki yerde kullanıldığı için CTE tam burada işe yarar.
Soru 3: Sızıntıyı Bulun
Bir meslektaşınız analist için şu görünümü yazdı:
CREATE VIEW "analysis" AS
SELECT * , 'Anonim' AS "card"
FROM "swipes" WHERE "type" = 'boarding' ;
Hangi bilgi hâlâ görünür? Görünümü düzeltin. Düzeltmeden sonra kalan başka bir risk var mı?
Soru 3: Cevap
SELECT * yüzünden card_id görünümün içinde; 'Anonim' yalnızca yeni bir sütun ekledi, eskisini silmedi.
CREATE VIEW "analysis" AS
SELECT "id" , "station_id" , "datetime" , "amount_kurus" ,
'Anonim' AS "card"
FROM "swipes" WHERE "type" = 'boarding' ;
Kalan risk: id ham tablodaki satırın kimliği. Ham tabloya erişim kapalı değilse id ile swipes ve cards birleştirilip kişi bulunur. Görünümden id’yi de çıkarmak ve erişimi yetkiyle kapatmak gerekir.
Soru 4: Görünümden Silmek
İade birimi, hatalı bir iadeyi geri almak için görünümden satır silmeye çalışıyor:
DELETE FROM "refunds" WHERE "id" = 41 ;
Ne olur? Bu işi yapabilecek iki yol söyleyin.
Soru 4: Cevap
Parse error: cannot modify refunds because it is a view
Yol 1: Doğrudan asıl tablodan silmek: DELETE FROM "swipes" WHERE "id" = 41;
Yol 2: Görünüme INSTEAD OF DELETE tetikleyicisi yazmak (sıradaki soru).
Her iki yolda da swipes’tan satır silindiği için geçen haftaki balance_revert tetikleyicisi çalışır; 21 numaralı kartın bakiyesi 9750’den 8000’e düşer.
Soru 5: INSTEAD OF DELETE Tetikleyicisi
refunds görünümüne yazılan DELETE, asıl tablodan ilgili satırı silsin. Boşlukları doldurun:
CREATE TRIGGER "refunds_delete"
INSTEAD OF ___ ON "refunds"
FOR EACH ROW
BEGIN
DELETE FROM "swipes" WHERE "id" = ___."id" ;
END ;
Soru 5: Cevap
CREATE TRIGGER "refunds_delete"
INSTEAD OF DELETE ON "refunds"
FOR EACH ROW
BEGIN
DELETE FROM "swipes" WHERE "id" = OLD ."id" ;
END ;
Silinmek istenen satıra OLD ile ulaşılır. Deneyin: DELETE FROM "refunds" WHERE "id" = 41; artık hata vermez, refunds iki satıra düşer ve bakiye düzelir.
Kısa SQL Özeti
Sorguya ad vermek
CREATE VIEW "ad" AS SELECT ...
Görünümü kaldırmak
DROP VIEW "ad"; · DROP VIEW IF EXISTS "ad";
Geçici görünüm
CREATE TEMPORARY VIEW "ad" AS ...
Tek sorguluk ara tablo
WITH "ad" AS (...) SELECT ...
Özet sorgusunu yeniden kullanmak
GROUP BY, COUNT, SUM, AVG, ROUND
Eşleşmeyenleri korumak
LEFT JOIN ... ON ...
Bölümleme, gizleme
görünümün WHERE koşulu, seçilen sütunlar, sabit değer
Görünüme yazmak
CREATE TRIGGER ... INSTEAD OF DELETE / INSERT ON "görünüm", OLD, NEW, WHEN
Şemada bakmak
.tables, .schema ad, sqlite_schema, sqlite_temp_schema
Çıkış Soruları
Üç kısa yanıt yazın:
Bir görünüm ile bir tablo arasındaki en önemli fark nedir?
CREATE TEMPORARY VIEW ile WITH (CTE) arasında ömür bakımından ne fark var?
Aşağıdaki görünüme INSERT INTO "open_stations" ... yazılınca neden hata alırız ve nasıl çözülür?
CREATE VIEW "open_stations" AS
SELECT "id" , "name" , "district" FROM "stations"
WHERE "closed" = 0 ;
Bir dakika bireysel süre. Üçüncü sorunun cevabı iki parçalı olmalı: görünüm veri taşımaz, yazılacak tabloyu belirleyemez; çözüm INSTEAD OF INSERT tetikleyicisi. Dikkat: öğrenci “yeni satır eklemek için stations’a yazarız” derse bu da doğru bir çözüm; ama uygulama görünümü bilip tabloyu bilmiyorsa tetikleyici gerekir.
Dersi Toparlayalım
Tablo veri taşır; görünüm yalnızca bir sorgunun adıdır ve her çağrıldığında altındaki sorgu yeniden çalışır.
TEMPORARY VIEW bağlantı boyunca, CTE yalnızca tanımlandığı sorguda geçerlidir.
Görünüme doğrudan yazılamaz; INSTEAD OF tetikleyicisi ifadeyi yakalayıp asıl tabloya yönlendirir.
Bugün bir sorguya ad vermeyi, özet sorgusunu yeniden kullanmayı, tabloyu mantıksal olarak bölmeyi ve hassas sütunu gizlemeyi gördük. Yumuşak silme işlemini de görünüm üzerinden yürüttük. Gelecek hafta aynı sorguların daha hızlı çalışmasını konuşacağız.
Üç cümle dersin üç ana fikri. Birincisi dersin ilk cümlesine dönüş: görünüm bir sorguya verilen addır. İkincisi ömür ayrımı: şemada kalıcı, bağlantıda geçici, sorguda anlık. Üçüncüsü görünümün yazma sınırı ve çözümü. Kapanışta “Görünümün Sınırları” slaytındaki dört maddeyi bir kez daha hatırlatabilirsiniz.