Ana içeriğe geç

Veritabanlarında Denormalizasyon: Ne Zaman ve Nasıl Kullanılır

Denormalizasyonun, join’leri azaltıp sorguları basitleştirerek okuma performansını nasıl artırdığını öğrenin. Güçlü bir araç hâline getiren takasları, teknikleri ve kullanım alanlarını anlayın.
· 15 dk. oku

Yapay Zeka ile Keşfedin

I’ve, normalleştirme ve bunun veri bütünlüğü için neden bu kadar güvenilir bir temel olduğuna dair çok şey yazdım. Kendimle çelişmiyorum. İyi normalleştirilmiş bir şemanın çoğu işlemsel sistem için doğru başlangıç noktası olduğuna hâlâ inanıyorum. Bu makale, okuma performansının katı normal forma göre daha önemli olduğu çok belirli durumlarda, bu saflığa bilinçli olarak karşı çıkma seçimini ele alıyor.

Denormalizasyon, normalleştirmeyi atlamak değildir. Gerçek sorgular, gerçek kullanıcılar ve gerçek SLA’ler size join’lerin ve anlık hesaplamaların çok yavaş veya çok maliyetli olduğunu söylediğinde, normalleştirilmiş bir modele uyguladığınız bir performans optimizasyonudur. Pratikte daha hızlı okumalar ve daha basit sorgular için daha fazla depolama, daha karmaşık yazmalar ve ek tutarlılık çalışmasıyla takas yaparsınız.

Bu makalede denormalizasyonun nerede faydalı olduğunu, nerede olmadığını ve SQL veritabanlarında nasıl güvenle uygulanacağını göstereceğim. Amaç, iyi tasarımı terk etmek değil; iş yükü haklı çıkardığında onun üzerine kestirme yolları katmanlamak.

Veritabanlarında Denormalizasyon Nedir?

Normalleştirme ve veritabanı tasarımına aşinaysanız, kısa cevap şöyle: 

Denormalizasyon, daha hızlı okumalar ve daha basit sorgular için, önceden normalleştirilmiş bir şemaya kasıtlı olarak yinelenen veri ekleme eylemidir. Bu, iyi modellemeyi atlamak için bir bahane değil, hedefli bir performans optimizasyonudur!

Veritabanı tasarımında nispeten yeniyseniz, “normalleştirilmiş”, “denormalize” ve “normalleştirilmemiş” kavramlarını açmak faydalı olabilir. Bu üç terimi çevrimiçi ortamda sık duyarsınız ve birbirine karıştırmamak önemlidir.

Normalleştirilmiş: Veriler, yinelenmeyi en aza indiren ve bütünlüğü koruyan iyi yapılandırılmış tablolara bölünür (bkz. 3NF/BCNF).

Denormalize: Normalleştirilmiş modelin üzerine, yaygın okumaları hızlandırmak için ekstra sütunlar, önceden hesaplanmış değerler veya önceden birleştirilmiş tablolarla seçici yinelenmeyi yeniden eklersiniz. Denormalizasyonda tek bir gerçek kaynağını (normalleştirilmiş tablolar) korur, ardından gösterge tabloları, ürün listeleri, arama vb. gibi sıcak yollar için daha hızlı bir veya daha fazla temsili sürdürürsünüz. Depolama + yazma karmaşıklığını, okuma hızı + daha basit sorgularla takas edersiniz.

Normalleştirilmemiş: Yapısı ve kısıtları düzgün tasarlanmamış ham, ad hoc veya dağınık veri. Burada yaptığımız bu değil.

SQL’de örnek

Normalleştirilmiş ve denormalize modelleme arasındaki farkı canlandırmanıza yardımcı olacak küçük bir SQL örneği:

Normalleştirilmiş modelleme

-- Source of truth
CREATE TABLE customers (
  customer_id   BIGINT PRIMARY KEY,
  name          TEXT NOT NULL,
  tier          TEXT NOT NULL
);

CREATE TABLE orders (
  order_id      BIGINT PRIMARY KEY,
  customer_id   BIGINT NOT NULL REFERENCES customers(customer_id),
  order_total   NUMERIC(12,2) NOT NULL,
  created_at    TIMESTAMP NOT NULL DEFAULT now()
);

-- Typical report needs a join
SELECT c.name, c.tier, SUM(o.order_total) AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUP BY c.name, c.tier;

Bir raporlama yolu için denormalize

-- Add redundant fields for faster reads
ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Now most reports avoid the join
SELECT customer_name, customer_tier, SUM(order_total) AS revenue
FROM orders
GROUP BY customer_name, customer_tier;

Bu örnekte, customers tablosunu gerçek kaynağı olarak tutar ve orders.customer_name / orders.customer_tier alanlarının senkron kalmasını sağlarsınız (örneğin, tetikleyici, CDC işi veya zamanlanmış bir backfill ile). Aynı gerçek, iki temsil; her biri farklı bir iş için optimize edilmiştir.

Denormalizasyon kötü tasarım mıdır?

Denormalizasyonun sadece kötü tasarımın bir ürünü olduğuna dair yaygın bir yanlış kanı var. Ölçülü, kapsamı belirli ve bakımı yapılırsa durum böyle değildir. Evet, denormalizasyon sıklıkla daha yüksek normal formları ihlal eder (zaten amacı budur), ancak bu kasıtlıdır ve tutarlılığı korumaya yönelik bir planla desteklenir. Kötü tasarım, normalleştirmeyi tamamen atlamak ya da bir senkronizasyon stratejisi olmadan gelişi güzel yinelenme serpiştirmektir.

Veritabanı tasarımı hakkında daha fazla bilgi edinmek isterseniz, konuyla ilgili başlangıç kursumuz iyi bir başlangıç noktasıdır!

Normalleştirme ve Denormalizasyon

İşte yan yana bir karşılaştırma. Kapsamlı değildir ancak her yaklaşımın takaslarını anlamanıza yardımcı olur.

Boyut

Normalleştirme

Denormalizasyon

Birincil amaç

Bütünlük, minimum yinelenme, kolay & doğru güncellemeler

Daha hızlı okumalar, sıcak yollarda daha basit sorgular

En uygun

Sık yazma/güncelleme yapılan OLTP sistemleri

Okuma ağırlıklı gösterge tabloları, arama/listeleme sayfaları, raporlama/analitik

Okuma performansı

Sıklıkla join gerektirir, doğru indekslerle iyidir

Daha az join, çok daha hızlı ve öngörülebilir olabilir

Yazma karmaşıklığı

Basit: tek gerçek kaynağı

Daha yüksek: yinelenen kopyalar veya özetlerin güncellenmesi/senkronu gerekir

Depolama

Sade

Daha büyük (ek sütunlar/tablolar, önceden hesaplanmış görünümler)

Veri bütünlüğü

Tasarım gereği güçlü (3NF/BCNF kısıtları)

Sapmayı önlemek için mekanizmalar gerektirir (tetikleyiciler, CDC, işler)

Değişim hızı

Sütun yeniden adlandırma/güncellemeleri yerelleştirilmiştir

Değişiklikler çoğaltılmış veriler boyunca yayılabilir

Operasyonel ek yük

Daha düşük: daha az hareketli parça

Daha yüksek: yenileme politikaları, backfill’ler, izleme

Arıza biçimleri

N+1 sorgular, yavaş join’ler, eksik indeksler

Bayat veri, tutarsızlık, yazma amplifikasyonu

Şema evrimi

Öngörülebilir, refactor-dostu

Yinelenmiş temsiller için geçiş planları gerekir

Tipik örnekler

Siparişler, müşteriler, işlemler

Önceden join’lenmiş ürün listeleri; toplu satış tabloları; materialized/indeksli görünümler

Neden ve Ne Zaman Denormalize Edilir

Gerçek kullanıcılar ve gerçek sorgular, join’ler, toplulaştırmalar veya tekrarlanan aramalar nedeniyle tıkandığında denormalizasyon gerçekten faydalı hâle gelir. İndeksleme, sorgu ayarı ve önbelleklemenin yeterli olmadığını doğruladıktan sonra, öngörülebilir erişim desenlerinde hızlı okumalar için denormalizasyona yönelebilirsiniz.

Denormalizasyonun yardımcı olabileceği durumlar

  • Okuma ağırlıklı iş yükleri: p95/p99 gecikmeleri join’ler veya toplulaştırmalar tarafından domine ediliyor, CPU hash/merge join’lerde harcanıyor, buffer cache devri yüksek.
  • Stabil sorgu şekilleri: Aynı gösterge tabloları/uç noktalar gün boyu benzer filtrelerle çalışıyor (ör. dünkü satışlar kategori bazında).
  • Fan-out join’ler: Tek bir sıcak tablo, bir kart ya da liste görünümü oluşturmak için 3–5 başka tabloyla join oluyor.
  • Toplulaştırma sıcak noktaları: Sürekli olarak geniş aralıklarda toplamlar, sayımlar veya en son değerleri hesaplıyorsunuz.
  • SLA baskısı: Mevcut planların diske döktüğü veya çok tarama yaptığı yerde ürün, 200 ms altı yanıtlar istiyor.

Klasik kullanım alanları

  • Gösterge tabloları & BI raporlama (OLAP/analitik): Günlük/aylık toplulaştırmaları önceden hesaplayın, pahalı group-by’ların materialized görünümlerini tutun veya yaygın dilimler için denormalize edilmiş olgu tablolarını saklayın.
  • E-ticaret/katalog & arama/listeleme sayfaları: category_name, brand_name, price_with_tax gibi alanları veya hızlı listeler ve filtreler için önceden join’lenmiş bir ürün projeksiyonunu çoğaltın.
  • CMS/blog/haber akışları: Makale/gönderi tablosunda author_name, primary_topic veya rendered_excerpt alanlarını saklayarak join’leri veya çalışma zamanı dönüşümlerini önleyin.
  • Aktivite akışları & sayaçlar: like_count, follower_count, veya latest_comment_at gibi türetilmiş öznitelikleri, yeniden hesaplamak yerine saklayın.
  • Büyük ölçekte olay/kayıt analitiği (OLAP/NoSQL): Sütunlu depolar için iç içe verileri düzleştirin ve taramaları öngörülebilir kılmak için bölüm dostu geniş satırlar tutun.

Ne zaman denormalize etmemeli ve önce başka bir şey kullanmalı

  • Sıkı tutarlılık gerektiren yazma ağırlıklı OLTP (siparişler, ödemeler, stok ayarlamaları).
  • Darboğaz eksik/zayıf indeksler, N+1 sorgular veya geveze ORM ise. Önce bunları düzeltin.
  • Yüksek oynaklıktaki alanlar (ör. her saniye değişen ürün bulunabilirliği) — çoğaltma devri artırır.
  • Açık bir sahiplik ve senkron planı olmayan ekipler (tetikleyiciler, CDC/işler, yenileme politikaları, sapma izleme). Bu yoksa, iyiden çok zarar verme riski alırsınız!
  • Veri kümesi o kadar küçük ki bir kaplayan indeks veya önbellek hâlihazırda hızlı yapıyor.

Denormalizasyonu, size en az ek operasyonel yükle en fazla okuma hızını sağladığı yerlerde ve yalnızca daha ucuz çözümleri elediğinizde kullanmak istersiniz.

Denormalizasyonun şu an için tam olarak ihtiyacınız olan şey olmadığını fark ettiyseniz, bekleyin! Bir sonraki bölümde denormalizasyona alternatifleri keşfedeceğiz.

Denormalizasyona Alternatifler

Denormalizasyon her sorununuzun cevabı değildir. Yinelenme eklemeden önce, motor ve uygulamanızdan alabileceğiniz her şeyi zorlamalısınız. Bu düzeltmelerin bakımı daha ucuzdur ve sıklıkla aynı kazanımları sağlar.

1) İndeksleme

  • Bileşik/kaplayan indeksler: SQL sorgularınızda, filtre sütunlarını önce, ardından GROUP BY/ORDER BY sütunlarını koyun. Motorun sorguyu yalnızca indeksten sunabilmesi için seçili liste sütunlarını dahil ediyorum.

  • Filtreli/kısmi indeksler: Yalnızca sıcak dilimi indeksleyin (ör. status = 'ACTIVE'); indeksi küçük ve hızlı tutar.

  • İfade/fonksiyonel indeksler: LOWER(email) veya date_trunc('day', created_at) üzerinde indeks oluşturup hesaplanmış taramalardan kaçının.

2) Sorgu ayarı & sayfalama

  • SELECT * kullanmaktan kaçının. Yalnızca görüntülediğinizi alın.

  • Yalnızca varlık kontrolü gerektiğinde gereksiz join’leri EXISTS/SEMI ile değiştirin.

  • Koşulları aşağı itin: erken filtreleyin, geç toplulaştırın.

  • Stabil, hızlı kaydırma için anahtar kümesi sayfalama (WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) kullanın.

3) Önbellekleme

  • Uygulama önbelleği (ör. Redis) kullanarak sıcak sorguları ve işlenmiş parçaları önbelleğe alın.
  • HTTP önbellekleme (ETag/Last-Modified) ile herkese açık sayfalar ve gösterge tabloları için faydalanın.
  • Kısa ömürlü önbellekler (30–120 sn) sıklıkla şema değişikliği ihtiyacını ortadan kaldırır.

4) Okuma replikaları

  • Ağır okumaları replikalara aktarın. Gösterge tabloları ve dışa aktarmalar için harikadır.

5) Bölümlendirme & budama

  • Büyük tabloları aralık/karma bölümlendirin ki taramalar yalnızca ilgili bölümlere dokunsun (ör. son 30 gün). Bu arada bu denormalizasyon değildir; yalnızca taranan veri miktarını azaltmaktır.

6) Sütunlu/OLAP depoları

  • Ağır analitiği Snowflake/BigQuery/ClickHouse’a (ELT/dbt ile) taşıyın. OLTP’yi normalleştirilmiş tutun ve depo, geniş ve tarama dostu şekilleri yönetsin.

7) ORM hijyeni

  • N+1 sorguları öldürün (hemen yükleme veya toplu), makul seçim listeleri belirleyin ve sayfa boyutlarını sınırlayın. Temiz bir ORM katmanı denormalizasyon ihtiyacını ortadan kaldırabilir.

8) Hesaplanan/üretilen sütunlar (VT tarafından yönetilen)

  • Türetilmiş değerleri (ör. price_with_tax) üretilen/hesaplanan sütunlar veya ifade indeksleri olarak veritabanına yönettirin. Böylece uygulama seviyesinde senkron mantığı yazmadan hızlı okumalar elde edersiniz.

Denormalizasyon Teknikleri

Normalleştirilmiş bir modele kontrollü yinelenme eklemenin en yaygın yolları burada. Her biri için ne yaptığını, ne zaman kullanılacağını ve nasıl senkron tutulacağını göstereceğim.

Kurulum: çekirdekte customers, orders, order_items, products bulunan normalleştirilmiş bir yapı varsayın.

1) Düzleştirilmiş / önceden join’lenmiş “projeksiyon” tabloları

Ne: Hızlı okumalar için ihtiyaç duyduğunuz sütunları önceden join’leyen bir tablo oluşturun (ör. ürün listesi veya siparişler panosu).

Ne zaman: Sıcak yolunuz 3 ila 5 tabloyu öngörülebilir biçimlerde join’liyorsa.

Nasıl (PostgreSQL):

-- Read-optimised projection for a typical orders list
CREATE TABLE orders_projection (
  order_id        BIGINT PRIMARY KEY,
  created_day     DATE NOT NULL,
  customer_id     BIGINT NOT NULL,
  customer_name   TEXT  NOT NULL,
  total_amount    NUMERIC(12,2) NOT NULL
);

-- Initial backfill
INSERT INTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)
SELECT o.order_id,
       o.created_at::date AS created_day,
       c.customer_id,
       c.name AS customer_name,
       SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, created_day, c.customer_id, c.name;

-- Index for fast filtering by day/customer
CREATE INDEX ON orders_projection (created_day, customer_id);

Senkron seçenekleri:

  • Yazma sonrası iş ("order.updated" kuyruğa al → satırı yeniden hesapla)
  • Gecelik/15 dakikalık toplu iş (dbt/cron)

2) Yinelenen sütunlar (birkaç özniteliği kopyala)

Ne: Sık okunan birkaç özniteliği, join’lerden kaçınmak için başka bir tabloya çoğaltın (ör. orders.customer_name).

Ne zaman: Yalnızca 1 veya 2 değere ihtiyacınız varsa ve tam bir projeksiyon istemiyorsanız.

Nasıl (PostgreSQL tetikleyici örneği):

ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Keep the redundant fields correct on insert/update
CREATE OR REPLACE FUNCTION sync_order_customer_fields()
RETURNS TRIGGER AS $
BEGIN
  SELECT name, tier INTO NEW.customer_name, NEW.customer_tier
  FROM customers WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_sync_customer
BEFORE INSERT OR UPDATE OF customer_id ON orders
FOR EACH ROW EXECUTE FUNCTION sync_order_customer_fields();

-- Propagate customer name/tier changes to existing orders
CREATE OR REPLACE FUNCTION propagate_customer_changes()
RETURNS TRIGGER AS $
BEGIN
  UPDATE orders
  SET customer_name = NEW.name,
      customer_tier = NEW.tier
  WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_customer_propagate
AFTER UPDATE OF name, tier ON customers
FOR EACH ROW EXECUTE FUNCTION propagate_customer_changes();

Takas: Okumalar basittir, ancak bir müşteri güncellendiğinde yazmalar yayılarak artar.

3) Türetilmiş öznitelikler (hesaplanan değerleri sakla)

Ne: Normalde anlık hesapladığınız değerleri kalıcılaştırın (ör. price_with_tax, latest_comment_at, item_count).

Ne zaman: Hesaplama sürekli yapılıyor ve determinizm var.

İki yaygın kalıp:

  • Üretilen sütun (VT yazmada yeniden hesaplar: kontrolünüz daha az, tablo dışı referans yok)
  • Saklanan sütun + tetikleyici/iş (ne zaman/nasıl değişeceğini siz kontrol edersiniz)
-- Generated column example (Postgres 12+; same-table expressions)
ALTER TABLE products
  ADD COLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS (price * 1.20) STORED;

-- Counter maintained by triggers (likes per post)
ALTER TABLE posts ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;

CREATE OR REPLACE FUNCTION inc_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = like_count + 1 WHERE post_id = NEW.post_id;
  RETURN NEW;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_insert
AFTER INSERT ON post_likes
FOR EACH ROW EXECUTE FUNCTION inc_like_count();

CREATE OR REPLACE FUNCTION dec_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = GREATEST(like_count - 1, 0) WHERE post_id = OLD.post_id;
  RETURN OLD;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_delete
AFTER DELETE ON post_likes
FOR EACH ROW EXECUTE FUNCTION dec_like_count();

4) Toplam/özet tabloları

Ne: Günlük gelir, günlük aktif kullanıcılar, kategori başına siparişler gibi rollup’ları önceden hesaplayın.

Ne zaman: Gösterge tabloları aynı group-by’ları tekrar ediyor ve ham taramalar pahalı.

CREATE TABLE daily_sales (
  sales_day    DATE PRIMARY KEY,
  gross_amount NUMERIC(14,2) NOT NULL,
  order_count  INTEGER NOT NULL
);

-- Incremental upsert for "yesterday" (run hourly/15-min)
INSERT INTO daily_sales (sales_day, gross_amount, order_count)
SELECT (o.created_at AT TIME ZONE 'UTC')::date AS sales_day,
       SUM(oi.quantity * oi.unit_price)       AS gross_amount,
       COUNT(DISTINCT o.order_id)             AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day', now() - interval '1 day')
  AND o.created_at <  date_trunc('day', now())
GROUP BY sales_day
ON CONFLICT (sales_day) DO UPDATE
  SET gross_amount = EXCLUDED.gross_amount,
      order_count  = EXCLUDED.order_count;

Senkron seçenekleri: zamanlanmış iş (cron/dbt), akış güncellemeleri (CDC) veya olay güdümlü toplayıcılar.

5) Materialized view’lar (kalıcı sorgu sonuçları)

Ne: Pahalı bir sorgunun çıktısını indeksleyebileceğiniz fiziksel bir tablo olarak saklayın.

Ne zaman: Sonuç kümesi pahalı, nispeten stabil ve yenileme pencerelerini tolere edebiliyorsanız.

-- Expensive report
CREATE MATERIALIZED VIEW mv_category_sales AS
SELECT p.category_id,
       date_trunc('day', o.created_at) AS day,
       SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.category_id, date_trunc('day', o.created_at);

-- Make it fast to query
CREATE INDEX ON mv_category_sales (category_id, day);

-- Refresh patterns
-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;

-- Non-blocking reads (requires unique index on the MV):
-- 1) ensure a unique index exists (e.g., (category_id, day))
-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;

Takas: Postgres’te artımlı yenileme yerel değildir; bu nedenle yenileme temposunu yönetirsiniz. Yine de, dbt veya özel “yalnızca ekle + pencereli yeniden kur” kalıpları kullandığınızda yeterince kolaydır.

6) Önceden hesaplanmış JSON “belge” projeksiyonları

Ne: API/UI ihtiyaçlarınıza uyan denormalize bir JSON yığını (ör. ürün kartı) saklayın ve yanında hedefli JSON indeksleri bulundurun.

Ne zaman: API’niz aynı şekli tekrarlı sunuyorsa ve tek okumayla iş yapmak istiyorsanız.

CREATE TABLE product_cards (
  product_id BIGINT PRIMARY KEY,
  card_json  JSONB NOT NULL
);

-- Build or rebuild card documents
INSERT INTO product_cards (product_id, card_json)
SELECT p.product_id,
       jsonb_build_object(
         'id', p.product_id,
         'name', p.name,
         'brand', b.name,
         'price_with_tax', p.price * 1.20,
         'category', c.name
       )
FROM products p
JOIN brands b    ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id) DO UPDATE SET card_json = EXCLUDED.card_json;

-- GIN index for JSON queries if needed
CREATE INDEX idx_product_cards_gin ON product_cards USING GIN (card_json);

Senkron seçenekleri: ürün/marka/kategori değişikliklerinde olay güdümlü yeniden kurma veya yakın zamanda güncellenen ürünler için sık toplu yenileme.

Bir senkron stratejisi seçme (hızlı rehber)

  • Tetikleyiciler: Anlıktırlar ve işlemsel tutarlılık sağlarlar. Genel olarak küçük yayılım ve düşük yazma hacmi için iyidirler; ancak aşırı kullanılırsa yazma gecikmesini artırabilirler.
  • Uygulama çift yazımı: Uygulama hem kaynağa hem denormalizasyona yazar. Basittir ama daha risklidir. Bunu idempotent yeniden denemeler ve outbox/CDC kalıplarıyla hafifletebilirsiniz.
  • CDC → işçi: Bu senk stratejisi, değişiklikleri eşzamansız olarak yaydığı için güvenilir ve ölçeklenebilirdir. Sonunda tutarlılığın kabul edilebilir olduğu durumlar için idealdir.
  • Zamanlanmış işler: Toplamalar ve materialized view’lar için en basit seçenektir. UX toleransınıza uygun yenileme pencereleri seçin.

Çözümünüze mutlaka gözlemlenebilirlik katmanını dahil etmenizi öneririm. Değişikliklerin doğru şekilde işlenip çoğaltıldığından emin olamamak kadar kötü bir şey yoktur. Bu aynı zamanda sapma kontrollerinizi ve backfill betiklerinizi hesaplamanıza da yardımcı olur.

Adım Adım Denormalizasyon Nasıl Uygulanır

Her zamanki gibi, veritabanınızla oynamak oldukça risklidir. Veritabanınızı çoğaltmanızı ve önce denormalizasyonu replika üzerinde uygulamayı denemenizi şiddetle tavsiye ederim; böylece her şeyin beklediğiniz gibi davrandığından emin olursunuz. Bir geliştirme ortamınız varsa, daha da iyi!

Süreç oldukça basit: ölç → mümkün olduğunca az değiştir → doğru tut → doğrula → izle → yinele.

1) Profilleyin ve başarı hedefi koyun

  • Sorgunun tam metnini (parametrelerle) ve sıklığını yakalayın.
  • Bir temel değer kaydedin: EXPLAIN (ANALYZE, BUFFERS) planı, p95 gecikmesi, CPU/IO, taranan satırlar.
  • Bir başarı kriteri üzerinde anlaşın (örneğin “p95 < 120 ms ve yazma maliyeti ≤1,2×”).
  • Tutarlılık toleransını teyit edin (ör. “analitik 5 dk bayat olabilir”).

2) İşe yarayan en küçük denormalizasyonu seçin

  • Tercihen büyük bir projeksiyon yerine tek bir yinelenen sütun veya tek bir özet tablo deneyin.
  • Yeni şekli taslaklayın:
    • Yinelenen sütunlar? (hangileri, neden)
    • Toplama tablosu veya materialized view? (tane, anahtarlar, yenileme penceresi)
  • Tutarlılık modeline karar verin:
    • Güçlü (tetikleyici/işlemsel) vs sonunda (CDC/iş/materialized view yenilemesi).
  • Yazma amplifikasyonu bütçesini yazın (olay başına kabul edilebilir ek yazma sayısı).

3) Senkron yolunu tasarlayın

  • Tetikleyiciler (güçlü tutarlılık, küçük yayılım).
  • CDC/outbox → işçi (iyi ölçeklenir, sonunda tutarlılık):
    • Uygulama, aynı işlemde kaynak tablolara ve outbox’a yazar.
    • Bir işçi, outbox’ı okur ve denormalize hedefi idempotent olarak günceller.
  • Zamanlanmış işler / MV yenilemesi (toplamalar için harika):
    • Yenileme temposunu, pencerelerini ve backfill stratejisini tanımlayın.

İdempotentlik pazarlıksızdır. Güncellemeler güvenle tekrarlanabilir olmalıdır (ör. belirleyici yeniden hesaplamayla UPSERT). Buna güvenin; sonra kendinize teşekkür edeceksiniz.

4) Oluşturun, backfill yapın ve doğrulayın

  1. Yapıları oluşturun (tablolar, sütunlar, indeksler, tetikleyiciler/işler) ancak uygulamayı değiştirmeyin.
  2. Gerçek kaynaktan backfill yapın.
  3. Eşdeğerliği değişmezlerle doğrulayın:
-- Example parity check: orders_projection vs live join
SELECT COUNT(*) AS mismatches
FROM orders_projection p
JOIN orders o   ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
   OR p.total_amount <> (
        SELECT SUM(oi.quantity * oi.unit_price)
        FROM order_items oi WHERE oi.order_id = o.order_id
      );

4. Uyumsuzlukları düzeltin; sıfıra inene (veya üzerinde anlaşılan hata bütçesine) kadar yeniden çalıştırın.

5) Güvenle devreye alın

  • Uygulama değişikliğini bir özellik bayrağı veya kanarya ile yayınlayın (ör. trafiğin %10’u denormalize yoldan okur).
  • Gölge okumalar çalıştırın: eski sonucu arka planda hesaplayın ve isteklerin bir alt kümesi için hash/toplamları karşılaştırın.
  • Hızlı bir geri alma mekanizması tutun (anında normalleştirilmiş yola geri dönün).

6) Doğru şeyleri izleyin

Seçtiğiniz gözlemlenebilirlik aracında küçük bir pano oluşturun:

  • Okuma: p50/p95 gecikmesi, okunan satırlar, arabellek isabetleri, sorgu planı istikrarı.
  • Yazma: ek yazma süresi, tetikleyici/iş hataları, kuyruk gecikmesi (CDC), MV son yenilenme zaman damgası.
  • Veri kalitesi: sapma sayaçları (günlük eşdeğerlik kontrolleri), backfill edilen satır sayısı, uyumsuzluk yüzdesi.
  • Maliyet/ayak izi: tablo boyutları, indeks şişmesi, MV boyut artışı.

7) İşletin & yineleyin

  • Sahiplik (ekip/nöbet sistemi) ve bir çalışma kitabı atayın (nasıl yeniden kurulur, backfill yapılır, onarılır). Bu sıklıkla gözden kaçar; ancak bir olay olduğunda olabildiğince hızlı yanıt vermek isteyeceksiniz. 
  • Üç ayda bir yeniden değerlendirin: hâlâ gerekli mi? Hâlâ doğru şekil mi? Basitleştirebilir miyiz? Kararı belgeleyin ve panolara bağlayın.

Denormalizasyonun Artıları ve Eksileri

Ayrıntıları önceki bölümdeki Normalleştirme ve Denormalizasyon tablosunda ele aldık; burada hızlı bir özet var.

Kazançlarınız

  • Daha hızlı okumalar ve daha öngörülebilir p95/p99 gecikmeleri
  • Sıcak yollarda daha basit sorgular (daha az join/toplulaştırma)
  • Okuma başına daha düşük CPU/IO ve daha ucuz analitik sorguları

Maliyetleri

  • Yazma amplifikasyonu (ek güncellemeler/eklemeler)
  • Tutarlılık riski (iyi senkronize edilmezse bayat/sapan veri)
  • Depolama büyümesi (yinelenen sütunlar/tablolar/görünümler)
  • Operasyonel ek yük (yenilemeler, backfill’ler, izleme)
  • Değişim yayılımı (şema/mantık güncellemeleri yansıtılmalıdır)

Denormalizasyonu Destekleyen Araçlar ve Teknolojiler

Her veritabanı denormalizasyona aynı şekilde yardımcı olmaz. Bazıları pahalı sorgu sonuçlarını kalıcılaştırmak için birinci sınıf özellikler sunarken, diğerleri işleri, tetikleyicileri veya boru hatlarını kullanarak sizin kendi çözümünüzü geliştirmenizi bekler.

PostgreSQL

Postgres, okuma için optimize edilmiş şekilleri somutlaştırmanın birkaç yolunu sunar. 

Materialized view’lar, bir sorgunun sonucunu kalıcılaştırır; böylece okumalar anlıktır. Ne zaman yenileneceğini (zamanlanmış veya isteğe bağlı) siz seçersiniz ve okuyucuların engellenmemesi için eşzamanlı yenileme de yapabilirsiniz.

Hafif kopyalamalar için, üretilen sütunlar ve ifade indeksleri, veritabanının türetilmiş değerleri korumasına ve yaygın filtreleri hızlandırmasına olanak tanır; ek senkron mantığı yazmadan. 

Kaynak ile denormalize bir kopya arasında güçlü tutarlılığa ihtiyaç duyduğunuzda, senkronu korumak için tetikleyiciler kullanabilirsiniz (ancak unutmayın, bunlar yazma yolunuza iş ekler!). Olay güdümlü veya toplu yenilemeler için Postgres’i bir zamanlayıcıyla (ör. pg_cron veya harici bir iş çalıştırıcısı) ya da mantıksal çoğaltma/CDC ile eşleştirip değişiklikleri, denormalize tablolarınızı eşzamansız güncelleyen bir işçiye akıtabilirsiniz.

Kendi veritabanlarınızı oluşturup denormalizasyonu test etmek istiyorsanız, PostgreSQL kursumuza göz atın.

SQL Server

SQL Server, motorun görünümü her ekleme/güncelleme/silmede senkron tuttuğu, dolayısıyla okumaların çok hızlı olduğu, adeta sürekli açık materialized view’lar olan indeksli görünümlere ağırlık verir. Öte yandan, artık hem temel tablolar hem görünüm bakımı yazmalara dâhildir; dolayısıyla ağır OLTP iş yükleri yavaşlayabilir. 

Bunun dışında, tek satırlık türetilmiş öznitelikler için kalıcı hesaplanan sütunlardan yararlanabilirsiniz. 

Büyük ölçekli raporlama için, columnstore indeksleri denormalizasyona bir alternatiftir: büyük veri kümelerini çok verimli şekilde sıkıştırır ve tarar; bu da baştan veri çoğaltma ihtiyacını ortadan kaldırabilir.

Oracle

Oracle’ın Materialized View’ları, yalnızca değişeni güncelleyen değişim günlüklerini kullanan FAST yenileme veya tümünü yeniden inşa etmenin sorun olmadığı COMPLETE yenileme ile olgun bir seçenektir. 

Query Rewrite ile, birisi temel tabloları sorguladığında, iyileştirici materialized view’ınızı şeffaf biçimde kullanabilir; böylece ekipler uygulama SQL’ini değiştirmeden hız kazanır. Her zamanki gibi, daha hızlı okumalar bir yerde ekstra iş demektir: günlükleri korumak ve görünüm yenilemek, yazma ve operasyonel ek yükü artırır.

MySQL / MariaDB

MySQL yerel materialized view sunmaz; bu nedenle denormalizasyon genellikle fiziksel tablolar artı bunları güncel tutmak için zamanlanmış işler veya tetikleyicilerle inşa edilir. 

Üretilen sütunlar, basit türetilmiş değerlerde yardımcı olur. Bu, öngörülebilir panolar ve özetler için iyi çalışır; ancak yine de yenileme temposu ve tetikleyici karmaşıklığı konusunda kasıtlı olmak gerekir; kritik yazma yollarını yanlışlıkla vergilendirmemek için. Birçok ekip bunu okuma replikalarıyla birleştirerek raporlama sorgularını tamamen devre dışı bırakır.

Snowflake / BigQuery (analitik katman)

Sütunlu depolar, geniş, tarama dostu, denormalize veriler için inşa edilmiştir. Genellikle içine yardımcı öznitelikler gömülmüş geniş olgu tabloları modelleyip, ardından tarananı budamak için bölümlendirme/kümelendirmeden yararlanırsınız. 

Her iki platform da, OLTP’ye dokunmadan toplamaları taze tutmak için materialized view’lar ve zamanlanmış görevleri/sorguları destekler. Ölçekte çok öngörülebilir, ucuz okumalar için depolama ve yenileme maliyetlerini takas edersiniz. Gösterge tabloları ve BI için mükemmeldir.

Bu, PostgreSQL kursumuzdan biraz daha ileri düzeydir; bu nedenle veritabanlarında zaten rahatsanız Snowflake ile veri modellemeye giriş kursumuzu deneyebilirsiniz.

dbt (modelleme ve orkestrasyon)

dbt, denormalize analitik için “kod olarak altyapı” katmanı olarak bilinir. Modelleri bir kez tanımlar, bir somutlaştırma (tablo, görünüm, artımlı) seçer ve dbt’nin kurulumları, bağımlılıkları ve testleri yönetmesine izin verirsiniz. Yalnızca yeni verilerin eklenip birleştirilmesine ihtiyaç duyan özet tablolar için artımlı modeller özellikle faydalıdır. En sevdiğim araçlardan biridir!

CDC & veri boru hatları

Uygulama değişikliklerinin denormalize yapılara güvenilir biçimde akmasını istediğinizde, Change Data Capture başvurulacak yöntemdir. Debezium (kendi barındırılan) gibi araçlar veya yönetilen bağlayıcılar (Fivetran, Airbyte), OLTP’den satır düzeyindeki değişiklikleri, projeksiyon tablolarını, sayaçları veya toplamaları güncelleyen işçilere veya depolara akıtır. Bu genellikle sonunda tutarlıdır (gösterge tabloları ve akışlar için harika) ve her şeyi tetikleyicilere sıkıştırmaktan çok daha iyi ölçeklenir. Güncellemeleri idempotent yapın, gecikmeyi izleyin ve geç ya da kaçırılan olaylar için bir backfill yolu tutun.

Temel Çıkarımlar & Son Düşünceler

Denormalizasyon, normalleştirilmiş bir tasarımın üzerine katmanlanan pragmatik bir optimizasyondur; onu reddetmek değildir. Belirli ve iyi anlaşılan yollarda daha hızlı, daha basit okumalar için depolamayı ve yazma karmaşıklığını takas edersiniz.

Unutmayın:

  • Normalleştirilmiş başlayın. Yalnızca adı konmuş, ölçülmüş bir sorgunun ihtiyaç duyduğu yerlerde denormalizasyon kullanın.
  • Önce daha ucuz çözümleri tüketin (indeksler, sorgu yeniden yazımları, önbellekler, replikalar, OLAP).
  • Karşılığını veren en küçük denormalizasyonu seçin (tek bir yinelenen sütun, küçük bir toplam veya bir materialized view).
  • Tazelik konusunda net olun (güçlü ve sonunda tutarlılık) ve idempotent bir senkron inşa edin.
  • Öncesi/sonrası ölçün, sapmayı izleyin ve bir geri alma anahtarını hazır tutun.
  • Normalleştirilmiş tabloları gerçek kaynağı olarak görün! Denormalize parçalar, yeniden inşa edebileceğiniz okuma için optimize kopyalardır.
Marie Fayard

Yazar

Marie Fayard

İlk prototipten ürün-pazar uyumuna ve ötesine kadar erken aşama girişimleri büyütme konusunda uzmanlaşmış, ürün odaklı bir teknik liderim. İnsanların teknolojiyi nasıl kullandığına dair bitmeyen bir merakım var ve kurucularla ile disiplinler arası ekiplerle yakın çalışarak cesur fikirleri hayata geçirmeyi seviyorum. Ürün inşa etmediğim zamanlarda ya dünyanın yeni köşelerinde ilham peşinde koşuyor ya da yoga stüdyosunda stres atıyorum.

Konular

İlgili