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.

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?

Bugünün Veritabanı

Boş bir klasöre indirin:

sqlite3 ulasim.db
sqlite> .headers on
sqlite> .mode column
sqlite> .tables
cards     stations  swipes

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 idPK holder balance_kurus swipes idPK card_idFK → cards station_idFK → stations type datetime amount_kurus yapar stations idPK nameUNIQUE district closed gerçekleşir kart ↔ durak: çoktan çoğa ilişki, köprü tablo: swipes

Kartlar ve Duraklar

SELECT * FROM "cards";
id holder balance_kurus
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";
id name district closed
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

swipes: Üç Tür Olay

SELECT * FROM "swipes" WHERE "id" IN (1, 5, 6, 7, 8, 22);
id card_id station_id type datetime amount_kurus
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.

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 · cardsid = 17
2 · swipesstation_id ∈ {2, 1, 3}
3 · stationsCumhuriyet Meydanı, Gar, Büyük Cami

Sorgu doğru, ancak üç SELECT ve iç içe sorgular okumayı zorlaştırıyor.

Üç Tablo, Tek Bakış

Önce sorguyu yazmadan, tabloları yan yana koyup eşleşen satırları inceleyelim.

üç ayrı tablocards ⋈ swipes ⋈ stationsride_history (görünüm)
cards
idholder 17Ayşe Kaya 19Elif Yıldız 18Mehmet Demir 20Can Öztürk
swipes
card_iddatetimestation_id 1708:15:032 1908:31:445 1808:47:194 2012:20:511
stations
idname 2Gar 5Yurtlar 4Atakent 1Cumhuriyet Meydanı

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';
holder datetime station
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.

CREATE VIEW: Sorguya Ad Vermek

Bu sorguyu her seferinde yeniden yazmak istemiyoruz. Başına bir satır eklemek yeter:

CREATE VIEW "ride_history" ASSELECT "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

cardstable
swipestable
stationstable
ride_historyview
ulasim.db: 20480 bayt → 20480 bayt

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';
station
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.

Görünüm Nerede Duruyor?

Görünüm şemada bir nesnedir. Tabloların yanında listelenir:

.tables
cards         ride_history  stations      swipes
SELECT "type", "name" FROM "sqlite_schema"
WHERE "type" = 'view';
type name
view ride_history

SQLite görünümün yalnızca sorgu metnini saklar; her sorguda alttaki sorgu yeniden çalışır.

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;
holder datetime station
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.

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.

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österAz önce yaptığımız: ride_history
2 · TOPLAMAÖzet sorgusunu yeniden kullanToplam, sayı ve ortalamayı her sorguda yeniden yazma.
3 · BÖLÜMLEMETabloyu mantıksal parçalara ayırHer uygulama kendi dilimini görsün.
4 · GÜVENLİKHassas sütunları gizleBaşkasına verilen veriden kişisel bilgiyi çıkar.

Bonus: Kapalı durakları görünümün sonucundan çıkarmak. Dersin sonunda.

Aynı Veri, Farklı Bakış

Aynı veritabanını kullanan üç birimin farklı bilgilere ihtiyacı var:

MuhasebeDurak başına gelir ve biniş sayısı.station_summary
AnalistKullanım örüntüleri; kimin bindiği değil.swipes_analysis
Durak yönetimiYalnızca açık duraklar.open_stations
Hepsinin altında aynı üç tablo: cards · swipes · stations

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:

nametypeamount_kurus
nameboardingsrevenue_kurusavg_fare_kurus
Cumhuriyet Meydanıboarding-875
Garboarding-1750
Cumhuriyet Meydanıboarding-1750
Garboarding-1750
Cumhuriyet Meydanıboarding-875
Gar235001750.0
Cumhuriyet Meydanı335001166.67

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";
station_id avg_fare
1 1166.66666666667
2 1750.0
… …

ROUND(değer, 2) iki ondalığa yuvarlar:

station_id avg_fare
1 1166.67
2 1750.0
… …

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";

Durak Başına Özet

Sorgunun sonucu: her durak için bir satır.

id name district boardings revenue_kurus avg_fare_kurus
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.

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";

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;
name boardings
Gar 7
Cumhuriyet Meydanı 6
Atakent 5

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)
id datetime amount_kurus
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
name boardings revenue_kurus
Gar 78 1225014000

INSERT INTO "swipes" ("card_id", "station_id", "type",
                      "datetime", "amount_kurus")
VALUES (17, 2, 'boarding', '2026-11-02 08:10:00', -1750);

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");
name district
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.

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";
name boardings
… …
Tekkeköy 0
Belediye Evleri 0

Filtreyi ON içine yazdık; WHERE’e yazsaydık eşleşmeyen satırlar yine elenirdi.

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.dbbağlantı 2 · yeniden açıldı
VIEWşemada kalıcı
CREATE VIEW station_summary
TEMP VIEWbağlantı boyunca
CREATE TEMPORARY VIEW✕
CTEtek sorgu boyunca
WITH … SELECT

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";
district boardings revenue_kurus avg_fare_kurus
Atakum 13 18375 1413.46
İlkadım 16 24500 1531.25

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.

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ğru1413.46
toplam gelir ÷ toplam biniş
İlçe: İlkadım1555.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ş).

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ç sorguAdın neyi temsil ettiğini söyler.
dış sorguAdı bir tablo gibi çağırır.

CTE ile Aynı Sonuç, Şemada İz Yok

Önceki slayttaki sorgu, geçici görünümle aynı sonucu verir:

district boardings revenue_kurus
Atakum 13 18375
İlkadım 16 24500

Sorgu bitince per_station yok olur:

SELECT COUNT(*) FROM "sqlite_schema"
WHERE "name" = 'per_station';
0

Hangisini Ne Zaman?

VIEW TEMPORARY VIEW CTE
Ö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.

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
id type amount_kurus
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ı
5678…
topups 9 satır · yükleme gişesi
12…
refunds 3 satır · iade birimi
223241

Üç 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';

Görünümleri Sorgulamak

Her görünüm artık kendi tablosu gibi sorgulanır:

SELECT * FROM "refunds";
id card_id station_id datetime amount_kurus
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ı.

Bölümlemenin Sınırı ve Adlandırma

Mantıksal bölümleme: görünümVeri 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ğiPostgreSQL 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.

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)
id card_id amount_kurus
5 17 -1750
6 19 -875
7 18 -1750
Analistkullanım örüntülerini inceler
swipes_analysis (görünüm)
id amount_kurus card
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;

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;
id station_id datetime amount_kurus card
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.

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;
id datetime holder
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.

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:%';
datetime station_id amount_kurus
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.

Erişim Denetimi: SQLite’ta Yok, Başka Sistemlerde Var

SQLiteVeritabanı 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, OracleKullanı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.

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;

open_stations’ı Sorgulamak

SELECT * FROM "open_stations";
id name district
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.

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
id name closed
4 Atakent 0
5 Yurtlar 01
6 Mimarsinan 0
8 Belediye Evleri 1
→closed = 0
open_stations
id name
4 Atakent
5 Yurtlar
6 Mimarsinan

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.

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';
open_stations (görünüm)
4 Atakent
6 Mimarsinan
INSTEAD OF DELETE
UPDATE "stations"
SET "closed" = 1
WHERE "id" = OLD."id";
5 Yurtlar 1
6 Mimarsinan 01

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;
Parça Anlamı
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

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;
id name district
1 Cumhuriyet Meydanı İlkadım
2 Gar İlkadım
3 Büyük Cami İlkadım
4 Atakent Atakum
7 Tekkeköy Tekkeköy
id name closed
5 Yurtlar 1
6 Mimarsinan 1
8 Belediye Evleri 1

Mimarsinan görünümden çıktı; tabloda satır yerinde, yalnızca closed = 1.

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.

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ırINSERT INTO "stations" ("name", "district") VALUES (NEW."name", NEW."district");

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.

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.

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";
id name district
… … …
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.

Yumuşak Silmenin İki Yüzü

open_stations: bugünün listesiUygulama 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ş raporuKapalı 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';
name boardings
Yurtlar 5

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.

Hepsi Bir Arada

Aynı üç tablonun üzerinde yedi görünüm, beş ayrı ihtiyaç:

SADELEŞTİRMEride_history
cardsswipesstations
TOPLAMAstation_summary
swipesstations
BÖLÜMLEMEboardings · topups · refunds
swipes
GÜVENLİKswipes_analysis
swipes
YUMUŞAK SİLMEopen_stations
stationsINSTEAD OF
geçici katmanTEMPORARY VIEW district_summary · CTE per_station
cardsid · holder · balance_kurus
swipesid · card_id · station_id · type · …
stationsid · name · district · closed

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” datetime station
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.

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%';
type name
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.

Görünümün Sınırları

Sonucu saklamazHer sorguda alttaki sorgu yeniden çalışır; büyük tabloda yavaş olabilir. MATERIALIZED VIEW (PostgreSQL, Oracle) sonucu saklar; SQLite’ta yoktur.
Doğrudan yazılamazGö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ırTablonun 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.

Alıştırmalar

Beş soru dersin dört bölümünü tarar. Her sorunun cevabı bir sonraki slayttadır.

  1. Günlük gelir: özet görünümü
  2. Ortalamanın üstündeki duraklar: CTE
  3. Sızıntıyı bulun: güvenlik
  4. Görünümden silmek: hata ve çıkış yolları
  5. INSTEAD OF DELETE: tetikleyici

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");
day revenue_kurus
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

Amaç SQL
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:

  1. Bir görünüm ile bir tablo arasındaki en önemli fark nedir?
  2. CREATE TEMPORARY VIEW ile WITH (CTE) arasında ömür bakımından ne fark var?
  3. 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;

Dersi Toparlayalım

  1. 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.
  2. TEMPORARY VIEW bağlantı boyunca, CTE yalnızca tanımlandığı sorguda geçerlidir.
  3. 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.