Yönetim panelindeki rapor ekranı açılırken 12 saniye bekliyorsanız ve arka planda altı tabloyu birleştiren bir sorgu varsa, iki seçeneğiniz olduğunu duymuşsunuzdur: view ya da materialized view. İkisinin de söz dizimi neredeyse aynıdır, ikisi de bir sorguyu isimlendirir, ama aralarındaki fark performans açısından gece ile gündüz kadar açıktır. View sorguyu her çağrıda baştan çalıştırır; materialized view sonucu bir kez hesaplayıp diske yazar.
Bu farkı bilmemek iki yönde de zarar verir. Bir tarafta, karmaşık bir raporu view'e sarıp "hızlandırdım" sanan ekipler var — oysa hiçbir şey değişmemiştir. Diğer tarafta, anlık doğruluk gerektiren bir bakiye ekranını materialized view'e taşıyıp müşterilere bir saat önceki rakamı gösteren sistemler. Bu rehberde ikisinin nasıl çalıştığını, PostgreSQL'de nasıl oluşturulup yenilendiğini, indekslenme ve kilitlenme davranışlarını ve hangi durumda hangisini seçmeniz gerektiğini anlatacağım.
View Nedir ve Ne Değildir#
View, kaydedilmiş bir SELECT sorgusudur. Veri tutmaz, disk yeri kaplamaz, kendi indeksi yoktur. Siz bir view'i sorguladığınızda veritabanı, view'in tanımını ana sorgunuzun içine açar (buna kural yeniden yazımı, rule rewriting denir) ve tek bir birleşik sorgu planlar.
CREATE VIEW aktif_musteri_ozeti AS
SELECT
m.id,
m.ad,
count(s.id) AS siparis_adedi,
coalesce(sum(s.adet * s.birim_fiyat), 0) AS toplam_ciro,
max(s.olusturulma) AS son_siparis
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
WHERE m.durum = 'aktif'
GROUP BY m.id, m.ad;
Artık SELECT * FROM aktif_musteri_ozeti WHERE toplam_ciro > 10000; yazabilirsiniz. Ama şunu net anlayın: bu sorgu çalıştığında veritabanı musteriler ve siparisler tablolarını yeniden tarar, yeniden gruplar. View size okunabilirlik ve yeniden kullanılabilirlik verir, hız vermez.
View'in asıl değeri üç yerdedir. Birincisi karmaşıklığı gizlemek: on satırlık bir birleşim mantığını tek isimle çağırmak, aynı mantığın on farklı yerde kopyalanmasını engeller. İkincisi yetki sınırlama: bir tabloyu doğrudan açmadan, yalnızca belirli sütunlarını ve satırlarını gösteren bir view'e erişim verebilirsiniz. Üçüncüsü şema kararlılığı: alttaki tablo yapısı değişse bile view'in çıktısını sabit tutarak uygulamayı kırılmaktan koruyabilirsiniz.
-- Yetki sınırlama örneği: personel tablosundaki maaş sütunu gizleniyor
CREATE VIEW personel_liste AS
SELECT id, ad, soyad, departman, ise_giris
FROM personel;
REVOKE ALL ON personel FROM raporlama_kullanici;
GRANT SELECT ON personel_liste TO raporlama_kullanici;
Materialized View Nedir#
Materialized view, aynı sorgunun sonucunu diske yazan bir yapıdır. Fiziksel olarak bir tablodur; yer kaplar, indekslenebilir, çok hızlı okunur. Karşılığında ödediğiniz bedel şudur: içindeki veri, en son yenilendiği andaki hâlidir. Kaynak tablolar değiştiğinde materialized view kendiliğinden güncellenmez.
CREATE MATERIALIZED VIEW mv_gunluk_ciro AS
SELECT
date_trunc('day', s.olusturulma)::date AS gun,
s.kategori_id,
count(*) AS siparis_adedi,
sum(s.adet * s.birim_fiyat) AS ciro
FROM siparisler s
WHERE s.durum = 'tamamlandi'
GROUP BY 1, 2
WITH DATA;
WITH DATA sonucu hemen hesaplar; WITH NO DATA derseniz boş bir kabuk oluşturur ve ilk REFRESH komutuna kadar sorgulanamaz. Materialized view'in gerçek gücü, üzerine indeks koyabilmenizden gelir:
-- Eşsiz indeks, eşzamanlı yenileme için ZORUNLUDUR
CREATE UNIQUE INDEX idx_mv_gunluk_ciro_pk ON mv_gunluk_ciro (gun, kategori_id);
CREATE INDEX idx_mv_gunluk_ciro_gun ON mv_gunluk_ciro (gun DESC);
Bu iki indeksle birlikte, aylardır biriken milyonlarca sipariş satırı üzerinde her seferinde toplama yapan 12 saniyelik rapor, birkaç milisaniyeye iner. İndeks türlerinin hangi sorgu desenine uyduğunu merak ediyorsanız PostgreSQL index türleri yazısı seçim mantığını açıklıyor.
İki Yapının Yan Yana Karşılaştırması#
| Özellik | View | Materialized View |
|---|---|---|
| Veri saklar mı | Hayır | Evet, diskte |
| Sorgu anında maliyet | Alttaki sorgunun tam maliyeti | Tablo okuma maliyeti |
| Güncellik | Her zaman anlık | Son yenileme anındaki hâl |
| İndeks koyulabilir mi | Hayır | Evet |
| Disk kullanımı | Sıfır | Sonuç kümesi kadar |
| Yenileme gerekir mi | Hayır | Evet, elle veya zamanlanmış |
| Yazma işlemi (INSERT/UPDATE) | Basit view'lerde mümkün | Doğrudan mümkün değil |
| Kaynak şema değişince | Bozulabilir | Bozulabilir, yeniden oluşturmak gerekir |
Karar ölçütü aslında tek bir soruya iner: verinin bir süre eski olması kabul edilebilir mi? Kabul edilebiliyorsa materialized view devreye girer. Bir raporda dünün rakamlarını göstermek sorun değildir; bir ödeme ekranında bakiyenin beş dakika eski olması felakettir.
İkinci ölçüt maliyet oranıdır. Materialized view mantıklıdır çünkü sorgu çok okunur, az değişir. Günde bir kez hesaplanıp binlerce kez okunan bir rapor idealdir. Günde bin kez değişip on kez okunan bir veri için materialized view yalnızca yük getirir.
Yenileme Stratejileri#
En basit yenileme komutu şudur:
REFRESH MATERIALIZED VIEW mv_gunluk_ciro;
Ancak bu komut view üzerinde ACCESS EXCLUSIVE kilidi alır; yani yenileme süresince o materialized view'i kimse okuyamaz. Beş dakika süren bir yenileme, raporlama ekranınızı beş dakika kapatır. Çözüm eşzamanlı yenilemedir:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_gunluk_ciro;
CONCURRENTLY seçeneği eski veriyi okunur tutarak yeni sonucu arka planda hesaplar ve farkları uygular. İki koşulu vardır: materialized view üzerinde en az bir eşsiz (UNIQUE) indeks bulunmalıdır ve view daha önce en az bir kez veriyle doldurulmuş olmalıdır. Eşsiz indeks yoksa PostgreSQL şu hatayı verir ve komutu reddeder: cannot refresh materialized view concurrently — bu yüzden yukarıdaki örnekte eşsiz indeksi ilk sıraya koydum.
CONCURRENTLY bedavaya gelmez. Fark hesaplaması yaptığı için normal yenilemeden belirgin biçimde yavaştır ve geçici olarak iki kat disk alanı kullanır. Küçük view'lerde normal yenileme, büyük ve sürekli okunanlarda eşzamanlı yenileme doğru seçimdir.
Yenilemeyi zamanlamak için üç yaygın yöntem var:
- Sistem cron'u. En basit ve en şeffaf yöntem.
psqlile tek satırlık bir komut çağırır. - pg_cron eklentisi. Zamanlamayı veritabanının içinde tutar, sunucu dışında bir bileşene bağımlılığı kaldırır.
- Uygulama içi zamanlayıcı. Zaten bir iş kuyruğunuz varsa mantıklıdır, ama uygulama ölçeklendiğinde aynı yenilemenin iki kez çalışmamasına dikkat edin.
Cron örneği:
# /etc/cron.d/pg-mv-refresh
# Her gece 03:15'te günlük ciro özetini yenile
15 3 * * * postgres psql -d uygulama -c "REFRESH MATERIALIZED VIEW CONCURRENTLY mv_gunluk_ciro;" >> /var/log/mv-refresh.log 2>&1
Sık yenilemek isteyip de kaynak tablonun tamamını taramak istemiyorsanız, "artımlı" bir desen kurabilirsiniz: materialized view yerine gerçek bir özet tablosu tutup, yalnızca değişen günleri güncelleyen bir procedure yazarsınız. PostgreSQL çekirdeği artımlı yenilemeyi desteklemez; bu işi kendiniz kurgularsınız. Bu tür bir toplu güncelleme mantığını stored procedure rehberi yazısındaki parça parça işleme desenine göre yazabilirsiniz.
Sık Yapılan Hatalar#
View'i hızlandırma aracı sanmak. En yaygın yanılgı budur. Bir sorguyu view'e sarmak planı değiştirmez, sadece adını değiştirir. Yavaş bir sorgunun çözümü indeks, sorgu yeniden yazımı veya materialized view'dir; view'e sarmak değil.
İç içe view yığmak. View içinde view, onun içinde başka bir view... Üç seviye sonra planlayıcı kaybolur, gereksiz birleşimleri eleyemez ve basit bir filtre bile tüm zinciri tarar. İki seviyeden fazlasına çıkıyorsanız yapıyı düzleştirin.
Eşsiz indeks koymadan CONCURRENTLY denemek. Yukarıda anlattığım hata mesajını üretim gecesinde görmek can sıkıcıdır. Materialized view oluşturur oluşturmaz eşsiz indeksi de ekleyin.
Yenileme başarısızlığını sessiz geçmek. Cron satırınız hata verirse ve çıktıyı bir yere yazmıyorsanız, rapor ekranınız günlerce eski veriyi gösterir ve kimse fark etmez. Yenileme zamanını bir tabloya yazın ve panelde gösterin:
CREATE TABLE mv_yenileme_kaydi (
view_adi text PRIMARY KEY,
son_calisma timestamptz NOT NULL,
sure_ms integer
);
Rapor ekranının üstünde "Son güncelleme: 03:15" yazması, hem kullanıcıya dürüst bir bilgi verir hem de bir aksaklığı ilk gören kişinin siz olmanızı sağlar.
Bakım işlerini unutmak. Materialized view fiziksel bir tablodur; ölü satır biriktirir ve istatistikleri eskir. Normal yenileme tabloyu baştan yazdığı için sorun çıkarmaz, ancak CONCURRENTLY fark uyguladığı için ölü satır bırakır. Autovacuum'un bu nesneleri de kapsadığından emin olun; ayrıntı için PostgreSQL VACUUM ve autovacuum yazısına bakın.
Şema değişikliğini unutmak. Kaynak tabloya sütun eklemek view'i etkilemez ama sütun silmek ya da tür değiştirmek view'i bozar. CREATE OR REPLACE VIEW yalnızca sütun listesi aynı kaldığında çalışır; sütun eklemek için sıralamayı bozmamanız gerekir. Materialized view'de ise REPLACE seçeneği yoktur, DROP ve yeniden CREATE yaparsınız — bu da indekslerinizi ve yetkilerinizi yeniden kurmanız gerektiği anlamına gelir. Bu adımları bir göç dosyasında birlikte tutun.
Hangisini Seçmelisiniz#
Karar akışını pratik bir sırayla kurun:
- Sorgu zaten hızlıysa (birkaç yüz milisaniye) view kullanın; sadeleştirme ve yetki için yeterlidir.
- Sorgu yavaşsa önce
EXPLAIN ANALYZEile bakın. Eksik bir indeks veya kötü bir birleşim sırası varsa önce onu düzeltin — materialized view kötü bir sorguyu gizler, iyileştirmez. - Sorgu doğası gereği pahalıysa (büyük toplama, geniş tarih aralığı) ve veri birkaç dakika/saat eski olabilirse materialized view kurun.
- Veri her zaman anlık olmak zorundaysa ve sorgu pahalıysa, özet tabloyu trigger ile artımlı güncelleyin. Bu, materialized view'in artımlı sürümüdür ve yazma maliyetini okuma maliyetinden çalar.
Dördüncü seçenek için veritabanı trigger'ları yazısındaki türetilmiş sayaç desenine bakabilirsiniz. Hangi yolu seçerseniz seçin, kararınızı ölçüm üzerine kurun. Bir raporun ne kadar sürdüğünü ve ne sıklıkla açıldığını bilmeden yapılan optimizasyon, çoğu zaman yanlış yere harcanan emektir.
-- Materialized view'lerin diskte kapladığı alanı görmek
SELECT
schemaname,
matviewname,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || matviewname)) AS boyut,
ispopulated AS dolu_mu
FROM pg_matviews
ORDER BY pg_total_relation_size(schemaname || '.' || matviewname) DESC;
Bu sorgu, hem hangi materialized view'lerin ne kadar yer kapladığını hem de ispopulated sütunuyla hangilerinin hâlâ boş olduğunu gösterir. false gördüğünüz bir satır, WITH NO DATA ile oluşturulup hiç yenilenmemiş bir view demektir ve o view'i sorgulayan her istek hata alır.
Sıkça Sorulan Sorular#
View performansı artırır mı#
Hayır, view tek başına performans artırmaz. View bir sorgunun kaydedilmiş hâlidir; siz onu sorguladığınızda alttaki sorgu baştan çalışır. View'in kazancı okunabilirlik, tekrar kullanılabilirlik ve yetki sınırlamadır. Performans için indeks, sorgu iyileştirme veya materialized view gerekir.
Materialized view ne sıklıkla yenilenmeli#
Bu tamamen verinin ne kadar eski olabileceğine bağlıdır. Günlük yönetim raporları için gecelik bir yenileme yeterlidir; saatlik takip edilen bir gösterge için 15 dakikada bir mantıklı olabilir. Yenileme süresini de hesaba katın: yenilemesi 4 dakika süren bir view'i 5 dakikada bir yenilemek, sunucuyu sürekli meşgul tutar. Kural olarak yenileme süresi, yenileme aralığının onda birinden az olmalıdır.
CONCURRENTLY neden hata veriyor#
En yaygın iki sebep var. Birincisi materialized view üzerinde eşsiz (UNIQUE) indeks bulunmamasıdır; CONCURRENTLY farkı bu indeks üzerinden hesapladığı için zorunludur. İkincisi view'in daha önce hiç doldurulmamış olmasıdır; WITH NO DATA ile oluşturulmuş bir view'i önce normal REFRESH ile bir kez doldurmanız gerekir.
Materialized view yerine özet tablo kullanmalı mıyım#
Yenileme sıklığınız çok yüksekse ve kaynak tablonun tamamını taramak pahalıysa, elle yönettiğiniz bir özet tablosu daha verimlidir; yalnızca değişen kısmı güncellersiniz. Buna karşılık özet tabloyu siz tutarlı tutmak zorundasınız, materialized view ise tanımdan otomatik olarak yeniden hesaplanır. Basitlik istiyorsanız materialized view, azami verim istiyorsanız özet tablo doğru seçimdir.
View üzerinden veri güncelleyebilir miyim#
Basit view'lerde evet. Tek tabloya dayanan, gruplama, DISTINCT veya pencere fonksiyonu içermeyen bir view otomatik olarak güncellenebilir kabul edilir. Karmaşık view'lerde ise INSTEAD OF trigger'ı yazarak yazma davranışını kendiniz tanımlarsınız. Materialized view'e doğrudan yazamazsınız; o bir sorgu sonucudur, kaynak tabloyu güncelleyip yenilemeniz gerekir.
Materialized view ne kadar disk yer kaplar#
Sonuç kümesi kadar, artı indekslerin boyutu. Milyonlarca satırı günlere ve kategorilere indirgeyen bir özet genelde kaynak tablonun binde biri kadardır. pg_matviews görünümünü pg_total_relation_size ile birlikte sorgulayarak gerçek boyutu ölçebilirsiniz. CONCURRENTLY yenileme sırasında geçici olarak fazladan alan gerektirir; disk planlamanızda buna pay bırakın.
Kapanış#
View ile materialized view arasındaki seçim, aslında "hız mı, güncellik mi" sorusunun cevabıdır. View sorguyu adlandırır ve her zaman güncel sonucu verir; materialized view sonucu dondurur ve karşılığında hız verir. Aklınızda kalması gereken dört alışkanlık: view'i hızlandırma aracı sanmayın, materialized view oluşturur oluşturmaz eşsiz indeksi de ekleyin, yenileme işini loglayıp son güncelleme zamanını kullanıcıya gösterin ve materialized view kurmadan önce sorgunun gerçekten iyileştirilemez olduğunu EXPLAIN ANALYZE ile doğrulayın.
Raporlama yükü büyüdükçe disk ve bellek davranışı belirleyici hâle gelir; özellikle gece yenilemeleri, ana iş yüküyle aynı kaynağı paylaşır. Kendi PostgreSQL sunucunuzu ayarlarıyla birlikte yönetmek isterseniz VDS ve bulut sunucu paketlerimiz iyi bir başlangıç noktası; ayar ve izleme işini üstlenmemizi isterseniz sunucu yönetimi hizmetimiz bunun için var. Rapor tablolarınızın da düzenli kopyalanması için yedekleme sayfamıza bakabilirsiniz.