İlişkisel veritabanlarında şema tasarımı, projenin ilk günlerinde alınan ve sonradan değiştirilmesi pahalı olan kararların başında gelir. Normalizasyon ile denormalizasyon arasındaki tercih, çoğu zaman "doğru olan" ile "hızlı olan" arasında bir seçim gibi sunulur. Gerçekte durum bundan daha nüanslıdır: ikisi de belirli problemleri çözmek için tasarlanmış araçlardır ve hangisinin uygun olduğu, verinin nasıl yazıldığına ve nasıl okunduğuna bağlıdır.
Bu rehber, orta seviye geliştiricilerin bu kararı bilinçli verebilmesi için normalizasyonun ne sağladığını, denormalizasyonun hangi maliyetleri getirdiğini ve ikisinin nasıl birlikte kullanılabileceğini ele alır.
Önkoşullar
- İlişkisel veritabanı temelleri (tablolar, birincil ve yabancı anahtarlar, temel SQL).
- En az bir normal formun (1NF, 2NF, 3NF) ne anlama geldiğine dair genel fikir.
- Uygulama tarafında veri erişim katmanının (ORM veya ham SQL) nasıl çalıştığına dair deneyim.
Sürüm kapsamı: Buradaki kavramlar belirli bir veritabanı motoruna veya sürüme bağlı değildir. Örnekler standart SQL'e yakın tutulmuştur; üretim ortamına uyarlarken kullandığınız motorun dokümantasyonunu esas alın.
Normalizasyon Ne Çözer
Normalizasyon, veriyi tekrarı en aza indirecek ve güncelleme tutarsızlıklarını önleyecek şekilde parçalara ayırmaktır. Amaç estetik değil, bütünlüktür.
Klasik bir örnekle düşünün: siparişleri ve müşteri bilgilerini tek tabloda tutmak.
-- Denormalize edilmiş tek tablo (tasarım kokusu)
CREATE TABLE orders_flat (
order_id BIGINT PRIMARY KEY,
customer_name VARCHAR(200),
customer_city VARCHAR(100),
product_name VARCHAR(200),
quantity INT
);
Bu tasarımda aynı müşteri birden fazla sipariş verdiğinde customer_name ve customer_city her satırda tekrarlanır. Müşteri şehir değiştirdiğinde tüm satırları güncellemek gerekir; bir satır atlanırsa aynı müşteri için iki farklı şehir bilgisi oluşur. Bu, güncelleme anomalisi olarak bilinen ve normalizasyonun doğrudan hedef aldığı problemdir.
Normalize edilmiş hali veriyi sorumluluklarına ayırır:
CREATE TABLE customers (
customer_id BIGINT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
city VARCHAR(100)
);
CREATE TABLE products (
product_id BIGINT PRIMARY KEY,
name VARCHAR(200) NOT NULL
);
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(customer_id)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(order_id),
product_id BIGINT NOT NULL REFERENCES products(product_id),
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Burada müşteri şehri tek bir yerde tutulur; güncelleme tek satırı etkiler. Yazma işlemleri tutarlı ve öngörülebilir hale gelir.
Denormalizasyon Ne Zaman Gündeme Gelir
Normalize şema, veriyi yazmak için idealdir; ancak okuma sırasında sık sık birden fazla tabloyu birleştirmeyi (JOIN) gerektirir. Okuma yükü arttığında veya sorgular çok sayıda tabloyu birleştirdiğinde, bu maliyet ölçülebilir hale gelebilir.
Denormalizasyon, bilinçli olarak veri tekrarı eklemek veya hazır hesaplanmış değerleri saklamak anlamına gelir. Amaç, okuma sırasında yapılacak işi yazma sırasına taşımaktır.
Tipik denormalizasyon biçimleri:
- Türetilmiş değerleri saklamak: Örneğin
order_itemssatırlarının toplamınıorders.total_amountalanında tutmak. Okuma sırasındaSUMhesaplamak yerine tek alan okunur. - Sık kullanılan alanları kopyalamak: Sipariş listesinde müşteri adını göstermek için her sorguda
customerstablosuna JOIN yapmak yerine, sipariş satırındacustomer_name_snapshottutmak. - Önceden birleştirilmiş görünümler veya özet tablolar: Raporlama için ayrı bir tablo veya materialized view.
Örnek olarak toplam tutarı sipariş tablosunda tutalım:
ALTER TABLE orders ADD COLUMN total_amount NUMERIC(12, 2) NOT NULL DEFAULT 0;
Bu alanı güncel tutmak artık uygulamanın sorumluluğundadır. Bir order_items satırı eklendiğinde, güncellendiğinde veya silindiğinde total_amount yeniden hesaplanmalıdır. Bunu tek bir transaction içinde yapmak, tutarsızlığı önlemenin temel yoludur:
BEGIN;
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (42, 7, 3);
UPDATE orders o
SET total_amount = (
SELECT COALESCE(SUM(oi.quantity * p.unit_price), 0)
FROM order_items oi
JOIN products p ON p.product_id = oi.product_id
WHERE oi.order_id = o.order_id
)
WHERE o.order_id = 42;
COMMIT;
Buradaki kritik nokta, iki ifadenin aynı transaction içinde olmasıdır. Aksi halde araya giren bir hata, total_amount ile gerçek satır toplamının ayrışmasına yol açar.
Karar Verirken Sorulacak Sorular
Normalizasyon ve denormalizasyon arasında seçim yaparken şu sorular yol gösterir:
- Yazma mı okuma mı baskın? Yazma yoğun sistemlerde tekrar eden veriyi senkron tutmak maliyetlidir; okuma yoğun sistemlerde JOIN maliyeti öne çıkar.
- Veri ne sıklıkla değişiyor? Nadiren değişen referans verisi (şehir, kategori adı) kopyalanmaya daha uygundur. Sürekli değişen bir değeri kopyalamak, senkronizasyon yükünü artırır.
- Tutarsızlık tolere edilebilir mi? Finansal toplamlar için tolerans yoktur; bir ürünün görüntülenme sayısı için küçük sapmalar kabul edilebilir.
- Ölçüm yapıldı mı? Denormalizasyon, ölçülmüş bir darboğaza cevap olmalıdır, öngörüye değil.
Yaygın Hatalar
- Erken denormalizasyon: Henüz bir performans problemi ölçülmeden veri tekrarı eklemek, ileride bakımı zor bir şema bırakır. Önce normalize tasarımla başlayın, darboğazı ölçün, sonra hedefli denormalize edin.
- Senkronizasyonu uygulamaya bırakmak: Türetilmiş alanı güncelleyen tek bir kod yolu olmalı. Farklı yerlerden
total_amountgüncellenirse tutarsızlık kaçınılmazdır. Mümkünse tetikleyici (trigger) veya tek bir servis metodu üzerinden yönetin. - Transaction sınırlarını göz ardı etmek: Kaynak veri ile kopyası arasındaki güncelleme atomik değilse, kısmi hata durumunda veri ayrışır.
- Kopyalanan veriyi "gerçek" sanmak: Kopya alanlar türetilmiş veridir; tek doğruluk kaynağı asıl tablodur. Bu ayrımı dokümante edin.
Güvenlik ve Bütünlük Etkileri
Denormalizasyon, veri bütünlüğü risklerini artırır. Kopyalanan alanlar, asıl kaynaktaki bir silme veya güncelleme işleminden haberdar olmayabilir. Örneğin bir müşteri silindiğinde, sipariş satırlarındaki customer_name_snapshot hâlâ eski ismi taşır. Bu bazen istenen davranıştır (tarihsel kayıt), bazen de gizlilik açısından problemdir: silinmesi gereken kişisel veri, kopya olarak başka tablolarda kalmış olabilir.
Bu nedenle veri saklama ve silme politikalarınızı denormalize alanları da kapsayacak şekilde tasarlayın. "Silme" işleminin hangi tablolardaki kopyaları etkilemesi gerektiğini baştan belirleyin.
Ayrıca normalizasyon, yabancı anahtar kısıtları sayesinde geçersiz referansları veritabanı seviyesinde engeller. Denormalize tasarımlarda bu tür kısıtlar zayıflar; tutarlılığı uygulama katmanına taşımış olursunuz ve bu katmanın doğruluğundan siz sorumlu olursunuz.
İkisini Birlikte Kullanmak
Bu bir ya/ya da tercihi değildir. Yaygın ve sağlam yaklaşım, çekirdek veriyi normalize tutmak, okuma performansı kritik olan belirli sorgular için hedefli denormalizasyon uygulamaktır.
Örneğin:
- İşlemsel tablolar (
customers,orders,order_items) normalize kalır. - Sık çalışan bir rapor sorgusu için ayrı bir özet tablo veya materialized view tutulur ve periyodik olarak yenilenir.
- Toplam tutar gibi sık okunan türetilmiş değerler, transaction içinde güncellenen alanlar olarak saklanır.
Bu yaklaşımda normalizasyon doğruluk kaynağı, denormalizasyon ise performans katmanı olarak konumlanır.
Sonuç
Normalizasyon, veri bütünlüğünü ve yazma tutarlılığını önceliklendirir; denormalizasyon, okuma performansını önceliklendirir ve karşılığında senkronizasyon yükü ile bütünlük riski getirir. Doğru başlangıç noktası genellikle normalize bir tasarımdır. Denormalizasyon, ölçülmüş bir ihtiyaca cevap olarak, sınırları net çizilmiş ve senkronizasyonu tek bir yerde yönetilen biçimde eklenmelidir. Hangi yolu seçerseniz seçin, türetilmiş verinin tek doğruluk kaynağını ve silme politikalarını açıkça belirleyin.