İçeriğe geç
academia.sh

Ders 10 / 14

Denormalizasyon

Normalleştirilmiş şemanın okuma bedelinin ölçülmesi, denormalizasyonun yazma maliyeti, türetilmiş sütunların her yazma yolunu kapsama zorunluluğu ve denormalizasyon kararının ölçütleri.

İçindekiler

Normalizasyon basamaklarının hepsi aynı yönde ilerledi: tekrarı kaldır, bağıntıyı böl, kuralı şemaya yazdır. Önceki dersin sonunda söylenen bedel burada ölçülecek — bölünen her bağıntı, okuma sırasında yeniden birleştirilir. Bu dersin sorusu şudur: bu bedel nasıl sayılır, ve tekrarı bilerek geri koymak hangi koşulda savunulabilir?

Denormalizasyon (denormalization), normalleştirilmiş bir şemaya okuma başarımı için bilinçli olarak tekrar eklemektir. Tanımdaki iki sözcük belirleyicidir: bilinçli (hatayla değil, ölçüme dayanarak) ve normalleştirilmiş bir şemaya (normalleştirmeyi hiç yapmamak denormalizasyon değil, eksik tasarımdır).

Okumanın Bedeli

Ödünç listesini ekranda göstermek için üye adı, kitap başlığı ve şube adı gerekir. Bu bilgiler normalleştirilmiş şemada dört ayrı bağıntıda durur. Motorun sorguyu yürütürken kaç bağıntıya eriştiği, plan çıktısından sayılabilir:

sqlite3 :memory: <<'SQL'
CREATE TABLE sube  (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE uye   (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL);
CREATE TABLE kitap (isbn TEXT PRIMARY KEY NOT NULL, baslik TEXT NOT NULL);
CREATE TABLE odunc (
  odunc_no    INTEGER PRIMARY KEY,
  uye_no      INTEGER NOT NULL REFERENCES uye (uye_no),
  isbn        TEXT    NOT NULL REFERENCES kitap (isbn),
  sube_kodu   TEXT    NOT NULL REFERENCES sube (sube_kodu),
  alis_tarihi TEXT    NOT NULL
);
CREATE TABLE odunc_ozet (
  odunc_no     INTEGER PRIMARY KEY,
  uye_ad       TEXT NOT NULL,
  kitap_baslik TEXT NOT NULL,
  sube_ad      TEXT NOT NULL,
  alis_tarihi  TEXT NOT NULL
);
EXPLAIN QUERY PLAN
SELECT u.ad, k.baslik, s.ad, o.alis_tarihi
FROM odunc o
JOIN uye   u ON u.uye_no    = o.uye_no
JOIN kitap k ON k.isbn      = o.isbn
JOIN sube  s ON s.sube_kodu = o.sube_kodu
WHERE o.alis_tarihi >= '2025-03-01';
EXPLAIN QUERY PLAN
SELECT uye_ad, kitap_baslik, sube_ad, alis_tarihi
FROM odunc_ozet WHERE alis_tarihi >= '2025-03-01';
SQL
QUERY PLAN
|--SCAN o
|--SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
|--SEARCH k USING INDEX sqlite_autoindex_kitap_1 (isbn=?)
`--SEARCH s USING INDEX sqlite_autoindex_sube_1 (sube_kodu=?)
QUERY PLAN
`--SCAN odunc_ozet

Birinci planda dört erişim adımı, ikincisinde bir tane vardır. Plan çıktısının biçimi ve plan isteme yolu motora göre değişir; sayılan şey biçimden bağımsızdır — sorgunun kaç bağıntıya dokunduğu ve her birine hangi yoldan eriştiği. Ödünç tablosu taranıyor, kalan üçüne anahtar üzerinden tek satırlık erişim yapılıyor. Yani tarama başına üç ek arama; milyon satırlık bir taramada üç milyon arama.

Birleştirmelerin bedeli her zaman bu kadar belirgin değildir. Anahtar üzerinden yapılan erişimler ucuzdur ve küçük bağıntılar bellekte kalır. Ölçmeden denormalizasyona geçmek, bu dersin savunmadığı karardır.

Yazmanın Bedeli

Tekrar geri konduğunda maliyet yer değiştirir. Şubenin adı değiştiğinde normalleştirilmiş şemada bir satır, özet tabloda o şubeye ait her satır güncellenir:

sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE sube (sube_kodu TEXT PRIMARY KEY NOT NULL, ad TEXT NOT NULL);
CREATE TABLE odunc_ozet (odunc_no INTEGER PRIMARY KEY, sube_kodu TEXT NOT NULL,
                         sube_ad TEXT NOT NULL);
INSERT INTO sube VALUES ('MRK', 'Merkez'), ('BHC', 'Bahcelievler');
INSERT INTO odunc_ozet VALUES (1001, 'MRK', 'Merkez'), (1002, 'MRK', 'Merkez'),
                              (1003, 'BHC', 'Bahcelievler'), (1004, 'MRK', 'Merkez'),
                              (1005, 'MRK', 'Merkez');

UPDATE sube SET ad = 'Merkez Sube' WHERE sube_kodu = 'MRK';
SELECT changes() AS kaynak_satiri;

UPDATE odunc_ozet SET sube_ad = 'Merkez Sube' WHERE sube_kodu = 'MRK';
SELECT changes() AS ozet_satiri;

INSERT INTO odunc_ozet VALUES (1006, 'MRK', 'Merkez');
SELECT sube_kodu, COUNT(DISTINCT sube_ad) AS farkli_ad FROM odunc_ozet GROUP BY sube_kodu;
SQL
┌───────────────┐
│ kaynak_satiri │
├───────────────┤
│ 1             │
└───────────────┘
┌─────────────┐
│ ozet_satiri │
├─────────────┤
│ 4           │
└─────────────┘
┌───────────┬───────────┐
│ sube_kodu │ farkli_ad │
├───────────┼───────────┤
│ BHC       │ 1         │
│ MRK       │ 2         │
└───────────┴───────────┘

Bir satır yerine dört satır güncellendi — bu, ölçülebilir ve kabul edilebilir bir maliyettir. Asıl tehlike son deyimdedir: özet tabloya eski adla yeni bir satır eklendi ve şube yine iki adla göründü. Denormalizasyonun bedeli, güncelleme maliyeti değil, her yazma yolunun kopyayı doğru doldurma zorunluluğudur.

Türetilmiş Sütun ve Her Yazma Yolu

Aynı sorun türetilmiş değerlerde daha keskin görünür. Üyenin açık ödünç sayısını her sorguda saymak yerine bir sütunda tutmak, sayımın maliyetini yazmaya taşır. Tutarlılık tetikleyicilerle sağlanabilir — yeter ki bütün yazma yolları kapsansın:

sqlite3 :memory: <<'SQL'
.headers on
.mode box
CREATE TABLE uye   (uye_no INTEGER PRIMARY KEY, ad TEXT NOT NULL,
                    acik_odunc INTEGER NOT NULL DEFAULT 0);
CREATE TABLE odunc (odunc_no INTEGER PRIMARY KEY, uye_no INTEGER NOT NULL,
                    iade_tarihi TEXT);
INSERT INTO uye VALUES (41, 'Ayse Demir', 0), (52, 'Mert Kaya', 0);

CREATE TRIGGER odunc_eklendi AFTER INSERT ON odunc
BEGIN
  UPDATE uye SET acik_odunc = acik_odunc + 1 WHERE uye_no = NEW.uye_no;
END;
CREATE TRIGGER odunc_iade AFTER UPDATE OF iade_tarihi ON odunc
WHEN OLD.iade_tarihi IS NULL AND NEW.iade_tarihi IS NOT NULL
BEGIN
  UPDATE uye SET acik_odunc = acik_odunc - 1 WHERE uye_no = NEW.uye_no;
END;

INSERT INTO odunc VALUES (1001, 41, NULL), (1002, 41, NULL), (1003, 52, NULL);
UPDATE odunc SET iade_tarihi = '2025-03-16' WHERE odunc_no = 1002;
SELECT u.uye_no, u.acik_odunc AS sayac,
       (SELECT COUNT(*) FROM odunc o
         WHERE o.uye_no = u.uye_no AND o.iade_tarihi IS NULL) AS gercek
FROM uye u;

DELETE FROM odunc WHERE odunc_no = 1001;
SELECT u.uye_no, u.acik_odunc AS sayac,
       (SELECT COUNT(*) FROM odunc o
         WHERE o.uye_no = u.uye_no AND o.iade_tarihi IS NULL) AS gercek
FROM uye u;
SQL
┌────────┬───────┬────────┐
│ uye_no │ sayac │ gercek │
├────────┼───────┼────────┤
│ 41     │ 1     │ 1      │
│ 52     │ 1     │ 1      │
└────────┴───────┴────────┘
┌────────┬───────┬────────┐
│ uye_no │ sayac │ gercek │
├────────┼───────┼────────┤
│ 41     │ 1     │ 0      │
│ 52     │ 1     │ 1      │
└────────┴───────┴────────┘

Ekleme ve iade yolları kapsandığı için ilk sonuç doğrudur. Silme yolu için tetikleyici yazılmamıştır; bir ödünç kaydı silindiğinde sayaç olduğu yerde kalır ve gerçekten sayılan değerle ayrışır. Türetilmiş sütunun kuralı budur: kaynağı değiştiren her yol sayılmalıdır — ekleme, silme, güncelleme, toplu göç ve elle çalıştırılan bakım deyimleri dâhil. Bir yol atlanırsa sütun sessizce yanlışa döner ve yanlışlığın ne zaman başladığı kayıtlarda görünmez.

Bu yüzden türetilmiş değerler için ikinci bir gereklilik daha vardır: kaynaktan yeniden hesaplayan ve farkı bildiren bir denetim işi. Yukarıdaki ikinci sorgu tam olarak budur.

Denormalizasyon Biçimleri

Uygulamada beş kalıp görülür ve maliyetleri farklıdır.

  • Kopyalanan sütun. Sık okunan bir öznitelik, başvuran bağıntıya kopyalanır. Kaynak nadiren değişiyorsa ucuzdur.
  • Türetilmiş toplam. Sayım, tutar toplamı ya da en son tarih bir sütunda tutulur. Yukarıdaki örnektir; her yazma yolunu kapsamak gerekir.
  • Özet bağıntısı. Raporlama için önceden hesaplanmış bir bağıntı tutulur ve belirli aralıklarla yenilenir. Tazelik ile maliyet arasında açık bir ödünleşim kurar.
  • Önceden birleştirilmiş bağıntı. Sık kullanılan bir birleştirmenin sonucu saklanır.
  • Görünüm. Birleştirme bir görünümün arkasına alınır; sonuç saklanmadığı için tekrar eklenmez, yalnız sorgu yazımı yalınlaşır. Sonucun saklandığı biçimi bulunup bulunmadığı ve nasıl tazelendiği motora göre değişir.

Son madde önemlidir: görünüm tek başına denormalizasyon değildir. Okuma maliyeti aynı kalır, yalnız tekrarlanan sorgu metni ortadan kalkar.

Karar Ölçütleri

Denormalizasyon son sıradaki araçtır. Ondan önce denenecekler bellidir: sorgunun kendisini düzeltmek, gerekli dizinleri eklemek, gereksiz sütun okumasını kesmek, sayfalama kullanmak. Bunlar veriyi tek yerde bırakır; denormalizasyon bırakmaz.

Karar için dört soru sorulur. Okuma ile yazma oranı nedir — okuma baskın değilse kazanç küçük, maliyet büyüktür. Kopyalanan veri ne sıklıkta değişiyor — sık değişen veri kopyayı sürekli geçersiz kılar. Bayat veri kabul edilebilir mi — özet bağıntısı için verilecek en kritik yanıttır. Ve kopyayı dolduran bütün yollar biliniyor mu — bilinmiyorsa karar ertelenmelidir.

Kararın kendisi de yazılı kalmalıdır. Şemayı sonradan okuyan biri, tekrarı bir tasarım hatası sanıp “düzeltmeye” kalkabilir; hangi ölçümün bu tekrarı gerekli kıldığı kayıtlıysa bu olmaz.

Özet

  • Denormalizasyon, normalleştirilmiş bir şemaya ölçüme dayanarak tekrar eklemektir; normalleştirmeyi hiç yapmamak denormalizasyon değildir.
  • Okuma bedeli, sorgunun kaç bağıntıya ve hangi yolla eriştiğinin plan çıktısından sayılmasıyla ölçülür.
  • Tekrar eklendiğinde maliyet yazmaya geçer: güncellenen satır sayısı artar ve her yazma yolu kopyayı doğru doldurmak zorunda kalır.
  • Türetilmiş sütunun tutarlılığı, kaynağı değiştiren yollardan biri atlandığında sessizce bozulur; kaynaktan yeniden hesaplayan bir denetim işi zorunludur.
  • Denormalizasyondan önce sorgu düzeltme, dizin ekleme ve sayfalama denenir; karar gerekçesiyle birlikte yazılı bırakılır.

Sonraki Adım

Normalizasyon konusunun son dersi, tek tek kuralların ötesine geçip tekrar eden tasarım durumlarına bakar: bire çok ve çoka çok ilişkiler nasıl kurulur, ağaç biçimindeki veri ilişkisel şemada nasıl tutulur, geçmişi saklamak gerektiğinde satır nasıl sürümlenir, ve hangi yaygın kalıplar aslında karşı kalıptır — her satırın anahtar-değer çifti olarak tutulduğu tablolar bunların başında gelir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat