Denormalizasyon, bir veriyi bilerek birden fazla yerde saklamayı kabul etmektir. Kulağa kötü bir fikir gibi gelir çünkü normalizasyonun tüm amacı bunun tersiydi; ama üç milyon satırlık bir sipariş tablosunda "her müşterinin toplam harcaması" sorgusu her ana sayfa açılışında beş tabloyu tarıyorsa, o toplamı bir sütuna yazmak akıllıca bir alışverişe dönüşür. Karşılığında ödediğiniz bedel bellidir: artık iki yeri güncel tutmak zorundasınız.
Bu rehberde denormalizasyonu bir tercih değil, ölçülmüş bir karar olarak ele alacağım. Önce denormalize etmeden önce mutlaka denemeniz gereken adımları göreceğiz; sonra gerçekten mantıklı olduğu senaryoları, uygulamada işe yarayan beş kalıbı ve her kalıpta tutarlılığı nasıl koruyacağınızı somut SQL örnekleriyle inceleyeceğiz. Son bölümde de bu kararın nasıl geri alınacağını konuşacağız, çünkü geri alamayacağınız bir optimizasyon zaten optimizasyon değildir.
Denormalizasyon Nedir, Normalizasyonun Tersi mi#
Tam olarak tersi değildir; daha doğru tanım, "normalize edilmiş bir tasarımdan bilinçli sapma"dır. Bu ayrım önemlidir çünkü baştan düzensiz kurulmuş bir şema denormalize değil, sadece hatalıdır. Denormalizasyon, doğru bir 3NF şemanın üzerine, ölçüme dayanarak eklenen bir katmandır. Şemanın referans hâli hâlâ normalize olan yapıdır; kopyalanan değer ise türetilmiş, yani her an yeniden hesaplanabilir bir veridir.
İkisi arasındaki takas şu tabloda özetlenir:
| Ölçüt | Normalize şema | Denormalize şema |
|---|---|---|
| Okuma maliyeti | Birden çok birleştirme | Tek tablodan okuma |
| Yazma maliyeti | Tek satır güncellenir | Birkaç yer güncellenir |
| Disk kullanımı | Düşük | Daha yüksek |
| Tutarlılık riski | Şema garanti eder | Uygulamanın sorumluluğunda |
| Şema değişikliği | Yereldir | Kopyaların hepsini etkiler |
Denormalizasyonun temel kuralı bu tablodan çıkar: okuma/yazma oranınız ne kadar yüksekse, yani veri ne kadar çok okunup ne kadar az yazılıyorsa denormalizasyon o kadar mantıklıdır. Günde bir kez güncellenip yüz bin kez okunan bir alanı kopyalamak açık bir kazançtır; saniyede güncellenen bir alanı kopyalamak ise sadece yeni bir hata kaynağıdır. Normalize tasarımın kurallarını tazelemek isterseniz veritabanı normalizasyonu rehberi başlangıç noktanız olsun.
Denormalizasyona Geçmeden Önce Denenecekler#
Yavaş bir sorguyla karşılaşınca ilk refleks denormalizasyon olmamalı. Deneyimime göre "denormalize etmemiz gerek" denen vakaların çoğu, aslında eksik bir indeks ya da kötü yazılmış bir sorgudur. Şu sırayı izleyin:
- Sorgunun gerçek planına bakın.
EXPLAINile hangi tablonun tam tarandığını, hangi indeksin kullanılmadığını görün. - Eksik indeksleri ekleyin. Özellikle
WHERE,JOINveORDER BYsütunları için. - Kapsayıcı (covering) indeks deneyin. Sorgunun ihtiyaç duyduğu tüm sütunlar indekste varsa motor tabloya hiç gitmez.
- Sorguyu yeniden yazın. Gereksiz alt sorguları,
SELECT *alışkanlığını ve satır satır çalışan döngüleri kaldırın. - Uygulama katmanında önbelleğe alın. Sonuç birkaç dakika bayat kalabilir mi? O zaman bir bellek önbelleği en ucuz çözümdür.
- Ancak bunlardan sonra denormalizasyonu tartışın.
-- Önce planı okuyun: hangi adım pahalı?
EXPLAIN
SELECT m.id, m.ad_soyad, SUM(k.adet * k.birim_fiyat) AS toplam
FROM musteriler m
JOIN siparisler sp ON sp.musteri_id = m.id
JOIN siparis_kalemleri k ON k.siparis_no = sp.siparis_no
WHERE m.id = 4211
GROUP BY m.id, m.ad_soyad;
-- Kapsayıcı indeks: motorun tabloya hiç gitmemesini sağlar
CREATE INDEX idx_kalem_kapsayici
ON siparis_kalemleri (siparis_no, adet, birim_fiyat);
Yavaş sorguyu tespit etme yöntemlerini MySQL yavaş sorgu bulma yazısında adım adım anlattım; oradaki yavaş sorgu kaydını açmadan denormalizasyon kararı vermek, hangi hastalığı tedavi ettiğinizi bilmemek demektir. Önbellek katmanı düşünüyorsanız Memcached nedir yazısı da hızlı bir başlangıç sunar.
Hangi Durumlarda Gerçekten Mantıklı#
Yukarıdaki adımları tükettikten sonra denormalizasyonun gerçekten doğru cevap olduğu birkaç net senaryo vardır.
Toplama (aggregate) sorguları listede gösteriliyorsa. Ürün listesinde her ürünün ortalama puanı ve yorum sayısı gösteriliyorsa, bu değerleri her sayfa yüklemesinde milyonlarca yorum satırından hesaplamak savunulamaz. Sayıyı ürün satırında tutmak doğru karardır.
Rapor ve panolar canlı tablolardan besleniyorsa. Yönetim panosundaki "bu ayın cirosu" kutucuğu her açılışta işlem tablosunu taramamalıdır. Saatlik güncellenen bir özet tablosu hem panoyu hızlandırır hem de canlı tabloların üzerindeki yükü kaldırır.
Tarihsel doğruluk gerekiyorsa. Fatura satırındaki ürün adı ve birim fiyatı, o anki değerin kopyasıdır. Ürün adı sonradan değişse bile fatura değişmemelidir. Bu teknik olarak denormalizasyondur ama aslında farklı bir gerçeğin kaydıdır ve tartışmasız doğrudur.
Birleştirme sayısı okunabilirliği aşmışsa. Yedi tabloyu birleştiren bir sorgu, indeksler mükemmel olsa bile bakımı zor bir yapıdır. Sık kullanılan bir alanı bir tablo yukarı kopyalamak burada hem hız hem sadelik kazandırır.
Okuma kopyası (read replica) üzerinde raporlama yapılıyorsa. Replika üzerinde ağır özet tablolar tutmak, ana sunucuyu hiç etkilemeden raporu hızlandırır.
Buna karşılık şu durumlarda denormalizasyondan uzak durun: veri saniyede birden fazla değişiyorsa, kopyalanacak alan finansal bir bakiye ise ve doğruluğu kritikse, ya da ekipte kopyayı güncel tutacak mekanizmayı kimse üstlenmiyorsa.
Uygulamada İşe Yarayan Beş Kalıp#
1. Sayaç sütunu. En yaygın ve en kolay kalıptır. İlişkili satır sayısını ana tabloya yazarsınız.
ALTER TABLE urunler
ADD COLUMN yorum_sayisi INT UNSIGNED NOT NULL DEFAULT 0,
ADD COLUMN puan_ortalama DECIMAL(3,2) NOT NULL DEFAULT 0.00;
2. Türetilmiş sütun (generated column). Değer aynı satırdaki diğer sütunlardan hesaplanıyorsa veritabanı bunu sizin yerinize yapar ve tutarsızlık ihtimali sıfırdır.
ALTER TABLE siparis_kalemleri
ADD COLUMN satir_toplam DECIMAL(14,2)
AS (adet * birim_fiyat) STORED;
-- STORED olduğu için indekslenebilir
CREATE INDEX idx_satir_toplam ON siparis_kalemleri (satir_toplam);
3. Kopyalanan görüntüleme alanı. Listelerde gösterilen ad, başlık gibi alanların bir üst tabloya kopyalanmasıdır. Birleştirmeyi tamamen ortadan kaldırır ama kaynak değiştiğinde kopyayı güncellemek gerekir.
4. Özet tablo. Zaman aralığına göre önceden hesaplanmış toplamları tutar. MySQL'de materialized view olmadığı için bu iş elle yapılır:
CREATE TABLE gunluk_satis_ozet (
gun DATE NOT NULL,
urun_id INT UNSIGNED NOT NULL,
adet_toplam INT UNSIGNED NOT NULL,
ciro DECIMAL(14,2) NOT NULL,
PRIMARY KEY (gun, urun_id)
) ENGINE=InnoDB;
-- Her gece çalışacak yeniden hesaplama
REPLACE INTO gunluk_satis_ozet (gun, urun_id, adet_toplam, ciro)
SELECT DATE(sp.olusturuldu), k.urun_id,
SUM(k.adet), SUM(k.adet * k.birim_fiyat)
FROM siparisler sp
JOIN siparis_kalemleri k ON k.siparis_no = sp.siparis_no
WHERE sp.olusturuldu >= CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(sp.olusturuldu), k.urun_id;
5. Materialized view (PostgreSQL). PostgreSQL kullanıyorsanız bu iş için hazır bir mekanizma vardır:
CREATE MATERIALIZED VIEW gunluk_satis AS
SELECT date_trunc('day', sp.olusturuldu) AS gun,
k.urun_id,
sum(k.adet) AS adet_toplam,
sum(k.adet * k.birim_fiyat) AS ciro
FROM siparisler sp
JOIN siparis_kalemleri k ON k.siparis_no = sp.siparis_no
GROUP BY 1, 2;
-- CONCURRENTLY için benzersiz indeks şart
CREATE UNIQUE INDEX ON gunluk_satis (gun, urun_id);
-- Okumaları kilitlemeden tazele
REFRESH MATERIALIZED VIEW CONCURRENTLY gunluk_satis;
Özet tabloları hesaplarken sıralama, kümülatif toplam veya son N kayıt gibi ihtiyaçlarınız varsa SQL window fonksiyonları bu sorguları çok daha okunabilir hâle getirir.
Tutarlılığı Nasıl Korursunuz#
Denormalizasyonun asıl işi kopyayı oluşturmak değil, kopyayı güncel tutmaktır. Bunun dört yolu vardır ve her birinin farklı bir karakteri vardır.
| Yöntem | Tutarlılık | Gecikme | Riski |
|---|---|---|---|
| Tetikleyici (trigger) | Anlık | Yok | Yazma yavaşlar, gizli mantık |
| Uygulama katmanı | Anlık | Yok | Bir kod yolu unutulursa bozulur |
| Kuyruk / arka plan işi | Nihai | Saniyeler | Kuyruk birikirse gecikir |
| Zamanlanmış yeniden hesap | Periyodik | Dakika/saat | Arada bayat veri |
Tetikleyici, kopyanın kaynağıyla aynı işlem içinde güncellenmesini garanti eder:
DELIMITER //
CREATE TRIGGER trg_yorum_ekle AFTER INSERT ON yorumlar
FOR EACH ROW
BEGIN
UPDATE urunler
SET yorum_sayisi = yorum_sayisi + 1,
puan_ortalama = (SELECT AVG(puan) FROM yorumlar WHERE urun_id = NEW.urun_id)
WHERE urun_id = NEW.urun_id;
END//
DELIMITER ;
Tetikleyicinin avantajı, hangi koddan yazılırsa yazılsın devreye girmesidir; dezavantajı, mantığın uygulamanın göremediği bir yerde saklanması ve yazma işlemlerini yavaşlatmasıdır. Yüksek hacimli tablolarda tetikleyici yerine kuyruk tabanlı bir yaklaşım genellikle daha iyi ölçeklenir.
Hangi yöntemi seçerseniz seçin, bir uzlaştırma (reconciliation) işi yazmayı ihmal etmeyin. Kopya er ya da geç kayar: bir hata, bir elle müdahale, yarıda kalan bir toplu iş. Gece çalışan basit bir kontrol sorgusu bu kaymayı erken yakalar:
-- Sayaç ile gerçek arasındaki farkı bul
SELECT u.urun_id, u.yorum_sayisi AS kopya, COUNT(y.id) AS gercek
FROM urunler u
LEFT JOIN yorumlar y ON y.urun_id = u.urun_id
GROUP BY u.urun_id, u.yorum_sayisi
HAVING kopya <> gercek;
Bu sorgu boş dönmüyorsa denormalizasyonunuz sessizce yalan söylüyor demektir. Sonucu bir izleme sistemine bağlayın, çünkü fark eden ilk kişi müşteriniz olmamalı.
Ölçüm ve Geri Alma#
Denormalizasyonun bir optimizasyon olduğunu söyledik; her optimizasyon gibi öncesi ve sonrası ölçülmelidir. Değişiklikten önce hedef sorgunun süresini ve okuduğu satır sayısını kaydedin, sonra aynı ölçümü tekrarlayın. İki kat hız için tutarlılık riskini üstlenmek genellikle kötü bir alışveriştir; elli kat hız içinse konuşulabilir.
-- MySQL 8: gerçek çalışma süresini ve satır sayısını gösterir
EXPLAIN ANALYZE
SELECT toplam_harcama FROM musteriler WHERE musteri_id = 4211;
Geri alma yolunu da baştan tasarlayın. Kopyaladığınız her alanın kaynağı hâlâ normalize tabloda durmalı; kopyayı silmek tek bir ALTER TABLE ... DROP COLUMN olmalı ve uygulama kodunda o alanı okuyan yerler kolayca bulunabilmeli. Kopyayı tek gerçek kaynak hâline getirir, orijinal veriyi silerseniz artık geri dönüşü yoktur ve o noktada yaptığınız şey optimizasyon değil, veri modelini bozmaktır.
Sık Yapılan Hatalar#
Ölçmeden denormalize etmek. "Bu sorgu ileride yavaşlar" tahmini genellikle yanlış çıkar. Ölçün, gerçekten yavaşsa ve indeksle çözülmüyorsa hareket edin.
Kaynağı silmek. Toplamı özet tabloya yazıp ham satırları silmek geri dönülemez bir adımdır. Ham veri kalsın; disk, yanlış rapordan daha ucuzdur.
Güncelleme yolunu tek bir yerde varsaymak. Kopyayı yalnızca uygulamanın "kaydet" akışında güncellerseniz, toplu içe aktarma veya elle çalıştırılan bir UPDATE kopyayı bozar. Ya tetikleyici kullanın ya da uzlaştırma işi yazın.
Her alanı kopyalamak. Denormalizasyon hedefli olmalıdır. "Nasılsa hızlı olsun" diye on beş sütunu kopyalayan tablo, yazma yükünü ve tutarsızlık yüzeyini gereksiz büyütür.
Sayaç güncellemesini yarış koşullarına açık bırakmak. SELECT ... ; UPDATE ... SET sayac = okunan + 1 biçimindeki iki adımlı güncelleme, eşzamanlı isteklerde sayacı kaybettirir. Tek adımda SET sayac = sayac + 1 yazın; bu işlem atomiktir.
Kopyayı sorgu içinde kaynakla karıştırmak. Aynı raporun bir sütunu özet tablodan, diğeri canlı tablodan geliyorsa rakamlar birbirini tutmaz. Bir raporun tüm satırları aynı kaynaktan beslensin.
Sıkça Sorulan Sorular#
Denormalizasyon her zaman kötü bir uygulama mı#
Hayır, kötü olan denormalizasyonun kendisi değil, gerekçesiz yapılanıdır. Ölçüme dayanan, hangi sorgunun ne kadar hızlandığı bilinen ve kopyanın güncel tutulması için bir mekanizması olan denormalizasyon tamamen meşru bir mühendislik kararıdır. Sorun, "ne olur ne olmaz" mantığıyla baştan denormalize kurulan ve kimsenin bakımını üstlenmediği şemalardır.
Önce indeks mi denormalizasyon mu denemeliyim#
Kesinlikle önce indeks. Doğru indeks çoğu yavaş sorguyu birkaç kat hızlandırır, hiçbir tutarlılık riski getirmez ve gerektiğinde tek komutla silinir. Denormalizasyon ise kalıcı bir bakım yükü doğurur. Sorgu planına bakmadan, kapsayıcı indeks denemeden denormalizasyona geçmek neredeyse her zaman erken bir karardır.
Özet tabloyu ne sıklıkla güncellemeliyim#
Bunu verinin kabul edilebilir bayatlığı belirler. Yönetim panosundaki ciro kutucuğu için saatlik güncelleme genellikle yeterlidir; stok gösterimi gibi kullanıcı kararını etkileyen alanlarda ise anlık güncelleme gerekir. Pratik yöntem, ekranın yanına "son güncelleme" zamanını yazmaktır; böylece kullanıcı verinin ne kadar taze olduğunu bilir ve sizin de daha uzun aralık kullanma özgürlüğünüz olur.
Tetikleyici kullanmak güvenli mi#
Tetikleyici, kopyanın kaynakla aynı işlemde güncellenmesini garanti ettiği için tutarlılık açısından en güçlü seçenektir. Riski iki tanedir: yazma işlemlerini yavaşlatır ve mantığı uygulama kodunda görünmeyen bir yere saklar, bu da yeni gelen bir geliştiricinin işini zorlaştırır. Yüksek yazma hacminde kuyruk tabanlı güncellemeyi, düşük hacimde tetikleyiciyi tercih edin ve her hâlükârda tetikleyicileri belgeleyin.
Materialized view MySQL'de var mı#
MySQL'de yerleşik materialized view yoktur; PostgreSQL'de vardır ve REFRESH MATERIALIZED VIEW CONCURRENTLY ile okumaları kilitlemeden tazelenebilir. MySQL tarafında aynı işi bir özet tablo ve onu dolduran zamanlanmış bir görev veya olay (event) ile elle kurarsınız. İşlevsel olarak sonuç aynıdır, sadece bakımı size aittir.
Denormalize ettiğim alanın bozulduğunu nasıl anlarım#
Düzenli çalışan bir uzlaştırma sorgusu yazın: kopyalanan değeri kaynağından yeniden hesaplayıp farkları listeleyin. Bu sorguyu gece çalıştırıp sonucu izleme sisteminize bağlayın, fark bulunduğunda uyarı üretsin. Uzlaştırma olmadan yapılan denormalizasyon, ne zaman yalan söylemeye başladığını kimsenin bilmediği bir veri kaynağıdır.
Denormalizasyondan geri dönmek zor mu#
Baştan doğru kurulmuşsa zor değildir: normalize kaynak tablolar yerinde durduğu için kopya sütunu düşürmek ve uygulamadaki okuma noktalarını kaynağa çevirmek yeterlidir. Zorlaşan durum, kopyanın zamanla tek gerçek kaynak hâline gelmesi ve orijinal verinin silinmiş olmasıdır. Bu yüzden kural nettir: kopya her zaman türetilmiş kalsın, asla tek nüsha olmasın.
Kapanış#
Denormalizasyon, doğru kullanıldığında güçlü bir hızlandırma aracı, yanlış kullanıldığında sessizce yalan söyleyen bir veri kaynağıdır. Aklınızda kalması gereken dört alışkanlık şunlar: normalize başlayın ve önce indeksleri tüketin, kararı ölçümle verin, kopyayı güncel tutacak mekanizmayı aynı gün kurun ve düzenli bir uzlaştırma sorgusuyla kaymayı erken yakalayın. Kopyanın her zaman türetilmiş kalması, geri dönüş kapısını açık tutmanızı sağlar.
Özet tabloları, materialized view tazelemelerini ve gece çalışan uzlaştırma işlerini rahatça kurmak için tam yetkili bir sunucu işinizi kolaylaştırır; VDS ve bulut sunucu paketlerimiz zamanlanmış görevler ve özel MySQL ayarları için uygun bir zemin sunar. Kurulum, izleme ve yedekleme tarafını bize bırakmak isterseniz sunucu yönetimi ve yedekleme hizmetlerimiz devreye girer.