← Kursa Dön
📄 Text · 30 min

Views — Sanal Tablolar

Giriş — Neden Her Seferinde Aynı Sorguyu Yazıyorsun?

Bir e-ticaret sisteminde "aktif müşterilerin son 3 aydaki siparişlerini, ürün detaylarıyla birlikte getir" sorgusunu düşün. Bu sorgu 4-5 tablo JOIN'liyor, WHERE koşulları var, belki window function'lar da içeriyor. Ve bu sorguyu raporlama panelinde, müşteri servisinde, e-posta kampanya sisteminde — en az 5 farklı yerde kullanıyorsun.

Her yerde aynı karmaşık sorguyu kopyalamak hem tehlikeli (bir yerde günceller, diğerini unutursun) hem de bakım kabusu. İşte View tam bu sorunu çözer: karmaşık bir sorguyu bir kez tanımlar, sonra onu basit bir tablo gibi kullanırsın.

🎯 Analoji: View'ı bir pencere gibi düşün (window function'daki pencere değil, gerçek bir pencere). Pencereden dışarı baktığında bir manzara görürsün — ama o manzara pencerede "saklanmıyor", pencere sadece dışarıdaki gerçekliğe bir bakış açısı sunuyor. View de aynı: veri tablolarda saklanır, view sadece o veriye belirli bir açıdan bakmanı sağlar.


View Nedir?

View, kaydedilmiş bir SELECT sorgusudur. Fiziksel olarak veri saklamaz — her çağrıldığında arkasındaki sorgu çalışır. Ama dışarıdan bakıldığında tablo gibi görünür ve tablo gibi kullanılır.

-- View oluştur
CREATE VIEW active_customer_orders AS
SELECT 
    c.customer_id,
    c.first_name,
    c.last_name,
    c.email,
    o.order_id,
    o.order_date,
    o.total_amount,
    o.status
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.status = 'active'
  AND o.order_date >= DATE_SUB(CURDATE(), INTERVAL 3 MONTH);

Artık bu view'ı tablo gibi kullanabilirsin:

-- View'dan okuma — tablo gibi
SELECT * FROM active_customer_orders 
WHERE total_amount > 5000
ORDER BY order_date DESC;

-- Aggregate yapabilirsin
SELECT first_name, COUNT(*) AS order_count 
FROM active_customer_orders 
GROUP BY customer_id, first_name;

-- JOIN yapabilirsin
SELECT aco.*, oi.product_id
FROM active_customer_orders aco
JOIN order_items oi ON aco.order_id = oi.order_id;

CREATE VIEW Syntax

Temel Kullanım

CREATE VIEW view_adı AS
SELECT sorgu;

-- Sütun isimlerini değiştir
CREATE VIEW monthly_summary (month, revenue, order_count) AS
SELECT 
    DATE_FORMAT(order_date, '%Y-%m'),
    SUM(total_amount),
    COUNT(*)
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m');

CREATE OR REPLACE — Varsa Güncelle

-- Varsa güncelle, yoksa oluştur
CREATE OR REPLACE VIEW active_customer_orders AS
SELECT 
    c.customer_id,
    c.first_name,
    c.last_name,
    c.email,
    c.phone,        -- ← Yeni sütun eklendi
    o.order_id,
    o.order_date,
    o.total_amount,
    o.status
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE c.status = 'active'
  AND o.order_date >= DATE_SUB(CURDATE(), INTERVAL 3 MONTH);

View'ı Silme ve İnceleme

-- View'ı sil
DROP VIEW IF EXISTS active_customer_orders;

-- View tanımını göster
SHOW CREATE VIEW active_customer_orders;

-- Mevcut view'ları listele
SHOW FULL TABLES WHERE Table_type = 'VIEW';

-- View hakkında bilgi
SELECT * FROM information_schema.VIEWS 
WHERE TABLE_SCHEMA = 'ecommerce';

View Kullanım Senaryoları

1. Karmaşık Sorguları Basitleştirme

-- Karmaşık rapor sorgusunu view yap
CREATE VIEW product_sales_report AS
SELECT 
    p.product_id,
    p.product_name,
    c.category_name,
    COUNT(DISTINCT oi.order_id) AS times_ordered,
    SUM(oi.quantity) AS total_units_sold,
    SUM(oi.quantity * oi.unit_price) AS total_revenue,
    ROUND(AVG(oi.unit_price), 2) AS avg_selling_price,
    MIN(o.order_date) AS first_sold,
    MAX(o.order_date) AS last_sold
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
LEFT JOIN orders o ON oi.order_id = o.order_id
LEFT JOIN categories c ON p.category_id = c.category_id
GROUP BY p.product_id, p.product_name, c.category_name;

-- Artık basitçe kullan
SELECT * FROM product_sales_report 
WHERE total_revenue > 100000
ORDER BY total_revenue DESC;

SELECT category_name, SUM(total_revenue) AS category_revenue
FROM product_sales_report
GROUP BY category_name;

2. Güvenlik — Hassas Verileri Gizleme

-- Çağrı merkezi çalışanları için: kredi kartı ve şifre bilgisi olmayan view
CREATE VIEW customer_service_view AS
SELECT 
    customer_id,
    first_name,
    last_name,
    email,
    phone,
    city,
    registration_date,
    status
    -- password_hash, credit_card_number YOK!
FROM customers;

-- Çağrı merkezi kullanıcısına sadece view erişimi ver
GRANT SELECT ON ecommerce.customer_service_view TO 'callcenter_user'@'%';
-- Asıl customers tablosuna erişim verme

Bu, veri maskeleme (data masking) için view kullanmanın basit ama etkili bir yolu.

3. Geriye Dönük Uyumluluk

-- Tablo yapısı değişti: first_name ve last_name ayrıldı
-- Ama eski kod hâlâ "name" sütunu bekliyor
CREATE VIEW customers_legacy AS
SELECT 
    customer_id,
    CONCAT(first_name, ' ', last_name) AS name,  -- Eski "name" sütunu
    email,
    phone,
    city
FROM customers;
-- Eski kod değiştirilmeden çalışmaya devam eder

4. Rapor Katmanı

-- Dashboard için hazır view'lar
CREATE VIEW dashboard_kpi AS
SELECT 
    (SELECT COUNT(*) FROM orders WHERE DATE(order_date) = CURDATE()) AS today_orders,
    (SELECT SUM(total_amount) FROM orders WHERE DATE(order_date) = CURDATE()) AS today_revenue,
    (SELECT COUNT(*) FROM customers WHERE DATE(registration_date) = CURDATE()) AS new_customers_today,
    (SELECT COUNT(*) FROM orders WHERE status = 'pending') AS pending_orders;

-- Dashboard'dan tek sorgu:
SELECT * FROM dashboard_kpi;

Updatable Views — View Üzerinden Veri Güncelleme

Bazı view'lar sadece okunmaz — üzerinden INSERT, UPDATE, DELETE de yapılabilir. Ama bunun koşulları var.

Updatable View Koşulları

Bir view updatable olabilmesi için:

  • Tek tablodan oluşmalı (JOIN yok)

  • Aggregate fonksiyon yok (SUM, COUNT, AVG...)

  • GROUP BY, HAVING, DISTINCT yok

  • UNION yok

  • Subquery yok (SELECT listesinde)

-- ✅ Updatable view
CREATE VIEW istanbul_customers AS
SELECT customer_id, first_name, last_name, email, city
FROM customers
WHERE city = 'İstanbul';

-- View üzerinden güncelleme yapılabilir
UPDATE istanbul_customers SET email = 'yeni@email.com' WHERE customer_id = 101;
-- Aslında customers tablosu güncellenir

-- View üzerinden ekleme yapılabilir
INSERT INTO istanbul_customers (first_name, last_name, email, city) 
VALUES ('Yeni', 'Müşteri', 'yeni@test.com', 'İstanbul');
-- customers tablosuna eklenir

-- View üzerinden silme
DELETE FROM istanbul_customers WHERE customer_id = 999;

WITH CHECK OPTION — Kapsam Dışı Güncellemeyi Engelle

CREATE VIEW istanbul_customers AS
SELECT customer_id, first_name, last_name, email, city
FROM customers
WHERE city = 'İstanbul'
WITH CHECK OPTION;

-- ✅ Bu çalışır (İstanbul müşterisi)
INSERT INTO istanbul_customers (first_name, last_name, email, city) 
VALUES ('Ahmet', 'Yılmaz', 'ahmet@test.com', 'İstanbul');

-- ❌ Bu HATA verir (Ankara, view'ın WHERE koşuluna uymuyor)
INSERT INTO istanbul_customers (first_name, last_name, email, city) 
VALUES ('Mehmet', 'Demir', 'mehmet@test.com', 'Ankara');
-- ERROR: CHECK OPTION failed

WITH CHECK OPTION, view'ın WHERE koşuluna uymayan veri eklenmesini veya güncellenmesini engeller. View üzerinden işlem yapılacaksa, bu seçenek veri tutarlılığı için önemli.

-- CASCADED vs LOCAL check
CREATE VIEW active_istanbul AS
SELECT * FROM istanbul_customers
WHERE status = 'active'
WITH CASCADED CHECK OPTION;
-- CASCADED: üst view'ın (istanbul_customers) koşullarını da kontrol eder
-- LOCAL: sadece kendi WHERE koşulunu kontrol eder

-- ❌ Bu hata verir (city = 'Ankara' üst view'ın koşuluna uymuyor)
UPDATE active_istanbul SET city = 'Ankara' WHERE customer_id = 101;

View Performansı

View fiziksel olarak veri saklamaz — her çağrıldığında arkasındaki sorgu çalışır. Bu performans açısından ne anlama gelir?

-- Bu view her çağrıldığında 5 tabloyu JOIN'ler
CREATE VIEW detailed_order_report AS
SELECT ... FROM orders o
JOIN customers c ON ...
JOIN order_items oi ON ...
JOIN products p ON ...
JOIN categories cat ON ...;

-- Her kullanımda full sorgu çalışır
SELECT * FROM detailed_order_report WHERE order_date > '2024-01-01';
-- = SELECT ... FROM orders o JOIN customers c ... WHERE order_date > '2024-01-01'

MySQL optimizer, view sorgusunu dış sorguyla birleştirir (merge) ve optimize eder. Ama bazı durumlarda merge edemez:

-- Merge EDİLEMEZ durumlar (ayrı temporary table oluşturulur):
-- 1. Aggregate fonksiyonlar (SUM, COUNT, AVG...)
-- 2. DISTINCT
-- 3. GROUP BY
-- 4. UNION
-- 5. Subquery (FROM'da)
-- 6. LIMIT

-- Bu durumda view önce çalıştırılır, sonra dış filtre uygulanır
-- → Potansiyel performans sorunu!

CREATE VIEW product_summary AS
SELECT product_id, SUM(quantity) AS total_sold
FROM order_items
GROUP BY product_id;

SELECT * FROM product_summary WHERE total_sold > 100;
-- MySQL önce TÜM ürünlerin toplamını hesaplar (GROUP BY),
-- sonra total_sold > 100 filtresini uygular
-- Index kullanılamaz!

💡 İpucu: Performance-critical durumlarda view yerine doğrudan sorgu yazmak veya materialized view (aşağıda) kullanmak daha iyi olabilir. View'ı daha çok okunabilirlik ve güvenlik aracı olarak düşün.


Materialized View Kavramı

Normal view her çağrıldığında sorguyu çalıştırır. Materialized view ise sorgu sonucunu fiziksel olarak saklar — bir tablo gibi. Sorgu bir kez çalışır, sonuç kaydedilir ve sonraki okumalar çok hızlı olur.

MySQL'de Materialized View Yok! Ama Simüle Edebilirsin

-- MySQL'de materialized view yok
-- Ama tablo + trigger/event ile simüle edebilirsin:

-- Adım 1: Sonuç tablosu oluştur
CREATE TABLE mv_product_sales AS
SELECT 
    p.product_id,
    p.product_name,
    SUM(oi.quantity) AS total_sold,
    SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id, p.product_name;

-- Adım 2: Index ekle (artık normal tablo, index eklenebilir!)
CREATE INDEX idx_mv_revenue ON mv_product_sales(total_revenue);

-- Adım 3: Periyodik güncelleme (Event veya cron ile)
CREATE EVENT refresh_mv_product_sales
ON SCHEDULE EVERY 1 HOUR
DO
BEGIN
    TRUNCATE TABLE mv_product_sales;
    INSERT INTO mv_product_sales
    SELECT 
        p.product_id,
        p.product_name,
        SUM(oi.quantity) AS total_sold,
        SUM(oi.quantity * oi.unit_price) AS total_revenue
    FROM products p
    LEFT JOIN order_items oi ON p.product_id = oi.product_id
    GROUP BY p.product_id, p.product_name;
END;

PostgreSQL'de Gerçek Materialized View

-- PostgreSQL: Native materialized view desteği
CREATE MATERIALIZED VIEW mv_product_sales AS
SELECT 
    p.product_id,
    p.product_name,
    SUM(oi.quantity) AS total_sold,
    SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM products p
LEFT JOIN order_items oi ON p.product_id = oi.product_id
GROUP BY p.product_id, p.product_name;

-- Index ekle
CREATE INDEX idx_mv_revenue ON mv_product_sales(total_revenue);

-- Yenile (refresh)
REFRESH MATERIALIZED VIEW mv_product_sales;
-- CONCURRENTLY: Okumayı engellemeden yenile
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_product_sales;
ÖzellikViewMaterialized View
Veri saklar mı?HayırEvet
Okuma hızıSorgu karmaşıklığına bağlıÇok hızlı (tablo gibi)
Veri güncelliğiHer zaman güncelSon refresh anına ait
IndexEklenemezEklenebilir
Disk kullanımıYokVar
MySQL desteği❌ (simülasyon)
PostgreSQL desteği

Gerçek Dünya Örnekleri

Örnek 1: E-Ticaret Rapor View'ları

-- Sipariş detay view'ı — en sık kullanılan
CREATE VIEW v_order_details AS
SELECT 
    o.order_id,
    o.order_date,
    o.status AS order_status,
    c.customer_id,
    c.first_name,
    c.last_name,
    c.email,
    c.city,
    oi.product_id,
    p.product_name,
    cat.category_name,
    oi.quantity,
    oi.unit_price,
    oi.quantity * oi.unit_price AS line_total,
    o.total_amount AS order_total
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories cat ON p.category_id = cat.category_id;

-- Kullanım
SELECT city, SUM(line_total) AS city_revenue
FROM v_order_details
WHERE order_date >= '2024-01-01'
GROUP BY city
ORDER BY city_revenue DESC;

Örnek 2: Row-Level Security Simülasyonu

-- Her satış müdürü sadece kendi bölgesinin verilerini görsün
CREATE VIEW v_my_region_orders AS
SELECT o.*
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE c.region = (
    SELECT region FROM sales_managers WHERE user = CURRENT_USER()
);

GRANT SELECT ON ecommerce.v_my_region_orders TO 'sales_manager'@'%';

Özet

  • View kaydedilmiş bir SELECT sorgusudur — fiziksel veri saklamaz, her çağrıldığında çalışır

  • CREATE OR REPLACE VIEW ile güvenle güncellenir

  • Karmaşık sorguları basitleştirir, güvenlik sağlar, geriye uyumluluk verir

  • Updatable view: tek tablo, aggregate/GROUP BY/DISTINCT yok — INSERT/UPDATE/DELETE yapılabilir

  • WITH CHECK OPTION: view koşuluna uymayan değişiklikleri engeller

  • Materialized view: sorgu sonucunu fiziksel saklar — MySQL'de yok, tablo + event ile simüle et

  • Performans: view merge edilemezse temporary table oluşturulur — dikkatli kullan

Sıkça Yapılan Hatalar

  1. View'ı performans aracı sanmak — View veri saklamaz, her çağrıda sorgu çalışır. Performans artışı sağlamaz, hatta aggregate view'larda yavaşlatabilir.

  2. Updatable view koşullarını bilmemek — JOIN, GROUP BY, DISTINCT, UNION içeren view'lar updatable değildir. Güncelleme yapmaya çalışırsan hata alırsın.

  3. WITH CHECK OPTION'ı unutmak — Updatable view'da bu seçenek yoksa, view koşuluna uymayan veri eklenebilir. İstanbul view'ından Ankara müşterisi eklenebilir.

  4. View'dan view oluşturmak — Teknik olarak mümkün ama her katman performansı düşürür ve bakımı zorlaştırır. 2-3 katmandan fazlası tehlikeli.

  5. Materialized view'ı yenilemeyi unutmak — Simüle edilmiş materialized view, otomatik güncellenmez. Event veya cron ile periyodik refresh planla.