BCNF, 4NF, 5NF ve Ötesi: 3NF Ne Zaman Yetmez?
3NF yalnızca key olmayan attribute'lara bakar; bu yüzden tüm kolonları key'in parçası olan bir tablo 3NF'te olup yine de anomaly üretebilir. BCNF, 4NF ve 5NF bu boşlukları kapatır. SQL Server'da imza günü tablosuyla BCNF'i ve kaybolan dependency'yi indexed view ile geri getirmeyi, dil ve tür örneğiyle 4NF'i, dağıtımcı tablosuyla 5NF'teki join dependency'yi ve 6NF'in temporal veriyle ilişkisini gösteriyoruz.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0, Cumulative Update 10) üzerinde çalıştırılmıştır.
Önce Tahmin Et: 3NF'teki Bir Tablo Hâlâ Bozulabilir mi?
Kitapçının üç mağazası var ve yazar imza günleri düzenliyor. Her mağaza, gelen her yazara o gün ona eşlik edecek bir görevli atıyor; bir görevli yalnızca kendi mağazasında çalışıyor. Bu bilgiyi tutan tablo şöyle:
CREATE TABLE dbo.signing_hosts (
author_name NVARCHAR(100) NOT NULL,
store NVARCHAR(50) NOT NULL,
host NVARCHAR(50) NOT NULL,
CONSTRAINT PK_signing_hosts PRIMARY KEY (author_name, store),
CONSTRAINT UQ_signing_hosts_author_host UNIQUE (author_name, host)
);
INSERT INTO dbo.signing_hosts (author_name, store, host) VALUES
(N'Frank Herbert', N'Kadikoy', N'Grace'),
(N'Ursula K. Le Guin', N'Kadikoy', N'Grace'),
(N'Neil Gaiman', N'Kadikoy', N'Linus'),
(N'Frank Herbert', N'Cankaya', N'Margaret'),
(N'Neil Gaiman', N'Cankaya', N'Margaret');
SELECT author_name, store, host
FROM dbo.signing_hosts
ORDER BY store, host, author_name;
(5 rows affected)
author_name store host
----------------- ------- --------
Frank Herbert Cankaya Margaret
Neil Gaiman Cankaya Margaret
Frank Herbert Kadikoy Grace
Ursula K. Le Guin Kadikoy Grace
Neil Gaiman Kadikoy Linus
(5 rows affected)
Primary key (author_name, store), çünkü bir yazarın bir mağazada tek görevlisi olur. Görevli bir yazara aynı anda iki mağazada eşlik edemeyeceği için (author_name, host) da benzersiz. Tabloda key olmayan tek bir attribute bile yok: her kolon bir candidate key'in parçası. Bir önceki yazının ölçüsüyle bu tablo 3NF'te (Third Normal Form), çünkü 3NF yalnızca key olmayan attribute'lar hakkında konuşuyor. Tahmin et: Grace başka bir mağazaya geçerse bu tabloda bir update anomaly yaşanabilir mi?
Yaşanır. Bu yazıda 3NF'in neden yetmediği bu tür üç durumu inceleyeceğiz: BCNF (Boyce-Codd Normal Form), 4NF (Fourth Normal Form) ve 5NF (Fifth Normal Form). Sonra 6NF'e (Sixth Normal Form) kısaca bakıp çoğu OLTP (Online Transaction Processing) şeması için hedefin neden 3NF ya da BCNF olduğunu konuşacağız.
BCNF: Her Determinant Bir Candidate Key Olmalı
Tablonun iki dependency'si var: {author_name, store} → host ve host → store. İkinci dependency'nin determinant'ı olan host tek başına bir key değil; bir görevli birden çok yazara eşlik ediyor. Bunu serinin 5. yazısındaki dbo.attribute_closure procedure'üyle doğrulayalım. Procedure aynı, yalnızca dependency tablosunun içeriği değişti:
CREATE TABLE dbo.fds (
fd_id INT NOT NULL,
side CHAR(3) NOT NULL CHECK (side IN ('lhs', 'rhs')),
attr VARCHAR(30) NOT NULL
);
INSERT INTO dbo.fds (fd_id, side, attr) VALUES
(1, 'lhs', 'author_name'), (1, 'lhs', 'store'), (1, 'rhs', 'host'),
(2, 'lhs', 'host'), (2, 'rhs', 'store');
EXEC dbo.attribute_closure 'author_name, store';
EXEC dbo.attribute_closure 'author_name, host';
EXEC dbo.attribute_closure 'host';
step attributes
---- ------------------
0 author_name, store
1 host
step attributes
---- -----------------
0 author_name, host
1 store
step attributes
---- ----------
0 host
1 store
{author_name, store} ve {author_name, host}'un closure'ları üç attribute'un hepsine ulaşıyor: ikisi de candidate key. host'un closure'ı ise yalnızca {host, store}; author_name dışarıda kalıyor. Yani host bir şeyi belirliyor ama satırı belirlemiyor.
BCNF'in kuralı tam olarak bunu yasaklar: tabloda trivial olmayan her dependency'nin sol tarafı bir superkey olmalı. 3NF'in tanımında bir kaçış kapısı vardı: dependency'nin sağ tarafı bir candidate key'in parçasıysa, sol tarafın key olması gerekmiyordu. host → store bu kapıdan geçiyor, çünkü store ilk candidate key'in parçası. BCNF bu kapıyı kapatır. Kural, Raymond F. Boyce ve E. F. Codd'un 1974'teki çalışmasına dayanır; bu yüzden adında ikisi var.
Kapının açık kalmasının bedeli tahmin sorusunun cevabı. Grace Çankaya mağazasına geçiyor ve birisi Le Guin'in satırını güncelliyor:
UPDATE dbo.signing_hosts
SET store = N'Cankaya'
WHERE author_name = N'Ursula K. Le Guin' AND host = N'Grace';
SELECT host, COUNT(DISTINCT store) AS store_count,
STRING_AGG(store, ', ') AS stores
FROM dbo.signing_hosts
GROUP BY host
HAVING COUNT(DISTINCT store) > 1;
(1 rows affected)
host store_count stores
----- ----------- ----------------
Grace 2 Kadikoy, Cankaya
(1 rows affected)
Grace artık iki mağazada çalışıyor gibi görünüyor. "Görevlinin mağazası" bilgisi her yazar satırında tekrarlandığı için, bir önceki yazıda müşteri telefonunda gördüğümüz anomaly'nin aynısı burada da var. Insert anomaly de var: yeni işe başlayan bir görevliyi, ona bir yazar atanana kadar hangi mağazada çalıştığıyla birlikte kaydedemezsin.
BCNF'e Geçmenin Bedeli: Dependency Preservation
Çözüm, ihlal eden dependency'yi kendi tablosuna almak; 3NF'te yaptığımız işin aynısı. host → store ayrı bir tabloya çıkıyor, geriye yazar–görevli eşleşmesi kalıyor. Güncellemeyi geri alıp bölelim ve bir önceki yazıdaki iki yönlü EXCEPT kontrolünü tekrarlayalım:
CREATE TABLE dbo.hosts (
host NVARCHAR(50) NOT NULL,
store NVARCHAR(50) NOT NULL,
CONSTRAINT PK_hosts PRIMARY KEY (host)
);
CREATE TABLE dbo.author_hosts (
author_name NVARCHAR(100) NOT NULL,
host NVARCHAR(50) NOT NULL,
CONSTRAINT PK_author_hosts PRIMARY KEY (author_name, host),
CONSTRAINT FK_author_hosts_hosts FOREIGN KEY (host) REFERENCES dbo.hosts (host)
);
INSERT INTO dbo.hosts (host, store)
SELECT DISTINCT host, store FROM dbo.signing_hosts;
INSERT INTO dbo.author_hosts (author_name, host)
SELECT author_name, host FROM dbo.signing_hosts;
WITH rejoined AS (
SELECT ah.author_name, h.store, ah.host
FROM dbo.author_hosts AS ah
JOIN dbo.hosts AS h ON h.host = ah.host
)
SELECT 'missing' AS direction, COUNT(*) AS row_count
FROM (SELECT * FROM dbo.signing_hosts EXCEPT SELECT * FROM rejoined) AS m
UNION ALL
SELECT 'spurious', COUNT(*)
FROM (SELECT * FROM rejoined EXCEPT SELECT * FROM dbo.signing_hosts) AS s;
(3 rows affected)
(5 rows affected)
direction row_count
--------- ---------
missing 0
spurious 0
(2 rows affected)
Bölme kayıpsız, çünkü ortak kolon host, hosts tablosunun key'i. Grace'in mağazası artık tek bir satırda duruyor. Ama bir şey kaybettik. {author_name, store} → host kuralı, yani "bir yazarın bir mağazada tek görevlisi olur", artık hiçbir tablonun key'i değil: store bir tabloda, author_name diğerinde. Her tablo kendi başına geçerliyken bu kural sessizce bozulabilir:
INSERT INTO dbo.author_hosts (author_name, host) VALUES (N'Frank Herbert', N'Linus');
SELECT ah.author_name, h.store, COUNT(*) AS host_count
FROM dbo.author_hosts AS ah
JOIN dbo.hosts AS h ON h.host = ah.host
GROUP BY ah.author_name, h.store
HAVING COUNT(*) > 1;
(1 rows affected)
author_name store host_count
------------- ------- ----------
Frank Herbert Kadikoy 2
(1 rows affected)
Herbert'in Kadıköy'de artık iki görevlisi var ve iki tablo da bu satırı itirazsız kabul etti. Literatürde bu duruma dependency preservation kaybı denir: bölünmüş tablolar dependency'lerin hepsini tek tek tablolarda kontrol edilebilir hâlde tutamıyor. Bu, BCNF'in bilinen bir bedeli; 3NF'e her zaman hem kayıpsız hem dependency'leri koruyan bir bölmeyle ulaşılabilir, BCNF'e ise her zaman ulaşılamaz.
Pratikte üç seçeneğin var. Birincisi tabloyu 3NF'te bırakıp tekrarlanan bilgiyi kabul etmek. İkincisi BCNF'e geçip kuralı uygulama koduna emanet etmek. Üçüncüsü, SQL Server'da kaybolan kuralı bir indexed view ile geri getirmek. İki tabloyu birleştiren view'a (author_name, store) üzerinde unique clustered index koyarsan, kural yine veritabanının içinde yaşar:
CREATE VIEW dbo.author_store_hosts
WITH SCHEMABINDING
AS
SELECT ah.author_name, h.store, ah.host
FROM dbo.author_hosts AS ah
JOIN dbo.hosts AS h ON h.host = ah.host;
GO
CREATE UNIQUE CLUSTERED INDEX UX_author_store_hosts
ON dbo.author_store_hosts (author_name, store);
GO
INSERT INTO dbo.author_hosts (author_name, host) VALUES (N'Frank Herbert', N'Linus');
Msg 2601, Level 14, State 1, Server b8be943222fa, Line 1
Cannot insert duplicate key row in object 'dbo.author_store_hosts' with unique index 'UX_author_store_hosts'. The duplicate key value is (Frank Herbert, Kadikoy).
The statement has been terminated.
Hatalı satırı geri alıp aynı INSERT'i tekrar denedim; bu kez index, birleştirilmiş görünümdeki ikinci Kadıköy satırını reddetti. Bedel, author_hosts ve hosts'a her yazışta view index'inin de güncellenmesi. Indexed view'ın kuralları ve maliyeti bir sonraki yazının konusu; burada önemli olan, BCNF ile dependency preservation arasındaki seçimin bir yol ayrımı değil, bir mühendislik kararı olması.
4NF: Bağımsız Multivalued Fact'ler
Kitap sitesinde yazar sayfasında iki filtre var: yazarın mağazada hangi dillerde baskısı var ve hangi türlerde yazmış. Birisi ikisini aynı tabloya koyuyor:
CREATE TABLE dbo.author_profiles (
author_name NVARCHAR(100) NOT NULL,
language CHAR(2) NOT NULL,
genre NVARCHAR(50) NOT NULL,
CONSTRAINT PK_author_profiles PRIMARY KEY (author_name, language, genre)
);
INSERT INTO dbo.author_profiles (author_name, language, genre) VALUES
(N'Ursula K. Le Guin', 'en', N'Fantasy'),
(N'Ursula K. Le Guin', 'en', N'Science Fiction'),
(N'Ursula K. Le Guin', 'en', N'Essay'),
(N'Ursula K. Le Guin', 'tr', N'Fantasy'),
(N'Ursula K. Le Guin', 'tr', N'Science Fiction'),
(N'Ursula K. Le Guin', 'tr', N'Essay'),
(N'Frank Herbert', 'en', N'Science Fiction'),
(N'Frank Herbert', 'tr', N'Science Fiction');
-- German editions arrive; only one row is added
INSERT INTO dbo.author_profiles (author_name, language, genre)
VALUES (N'Ursula K. Le Guin', 'de', N'Fantasy');
SELECT author_name,
COUNT(*) AS row_count,
COUNT(DISTINCT language) * COUNT(DISTINCT genre) AS cross_product
FROM dbo.author_profiles
GROUP BY author_name;
(8 rows affected)
(1 rows affected)
author_name row_count cross_product
----------------- --------- -------------
Frank Herbert 2 2
Ursula K. Le Guin 7 9
(2 rows affected)
Tablonun key'i üç kolonun hepsi ve hiçbir dependency yok; tablo BCNF'te. Ama Le Guin'in iki dili ve üç türü için altı satır gerekti, çünkü dil ve tür birbirinden bağımsız: her dil her türle eşleşmek zorunda. Almanca baskılar gelince doğru davranış üç satır eklemekti; birisi tek satır ekledi ve tablo artık "Le Guin'in Almanca baskısı yalnızca fantastik türde var" diyor gibi. Sorgu bunu ortaya çıkarıyor: Le Guin için satır sayısı yedi, dil ve tür sayılarının çarpımı dokuz. Bağımsız iki olgu aynı tabloda durduğunda, tablonun tutarlı kalması için her yazar için tam bir kartezyen çarpım taşıması gerekir.
Bu yapının adı multivalued dependency (MVD): yazar, dillerinin bir kümesini ve türlerinin bir kümesini belirler ve bu iki küme birbirinden bağımsızdır. Gösterimi author_name ↠ language ve author_name ↠ genre. 4NF'i Ronald Fagin 1977'de tanımladı ve kuralı şu: trivial olmayan her multivalued dependency'nin sol tarafı bir superkey olmalı. William Kent'in 1983 tarihli "A Simple Guide to Five Normal Forms in Relational Database Theory" makalesi bunu daha gündelik bir dille anlatır; kendi kelimelerimizle: bir kayıt, bir entity hakkında birbirinden bağımsız iki ya da daha fazla çok değerli olgu taşımamalı. Bölünce ne olduğuna bakalım:
SELECT DISTINCT author_name, language INTO dbo.author_languages FROM dbo.author_profiles;
SELECT DISTINCT author_name, genre INTO dbo.author_genres FROM dbo.author_profiles;
SELECT l.author_name, l.language, g.genre
FROM dbo.author_languages AS l
JOIN dbo.author_genres AS g ON g.author_name = l.author_name
EXCEPT
SELECT author_name, language, genre
FROM dbo.author_profiles;
(5 rows affected)
(4 rows affected)
author_name language genre
----------------- -------- ---------------
Ursula K. Le Guin de Essay
Ursula K. Le Guin de Science Fiction
(2 rows affected)
Bölünmüş iki tablo birleştirildiğinde, eksik eklenen iki satır kendiliğinden ortaya çıktı: Le Guin'in Almanca deneme ve bilim kurgu baskıları. Dil ve tür gerçekten bağımsızsa bu "sahte" değil, tam olarak eksik girilen bilgi; 4NF'teki iki tabloyla bu hata hiç oluşamazdı, çünkü yeni dili eklemek tek satırlık bir iş olurdu. Kent'in makalesindeki büyüme argümanı da buradan çıkıyor: dört dili ve on türü olan bir yazar için birleşik tablo kırk satır, ayrılmış tablolar on dört satır taşır. Normalize edilmiş tasarım toplamsal, birleşik tasarım çarpımsal büyür.
Aynı çıktı madalyonun öbür yüzünü de gösteriyor. Kitapçı "Almanca baskı yalnızca fantastik türde var" diyorsa dil ve tür bağımsız değildir, aralarında bir ilişki vardır ve üç kolonlu tablo doğru modeldir. Bu durumda bölme iki yanlış satır üretir. Hangisinin doğru olduğuna veri değil, iş kuralı karar verir; 5. yazıdaki "dependency'ler veriden değil iş kuralından gelir" cümlesi multivalued dependency için de geçerli.
5NF: Join Dependency ve Üç Parçaya Bölmek
Serinin 3. yazısında yayın hakkı tablosunu iki binary ilişkiye bölmüş, birleştirince sahte satırlar görmüş ve ternary ilişkinin hangi koşulda bölünebildiğini bu yazıya bırakmıştık. Benzer bir ternary ilişki: kitapçı, kitapları dağıtımcılar üzerinden alıyor ve bir satır "bu dağıtımcı, bu yayınevinin kitaplarını, bu mağazaya getirir" diyor. Dağıtımcı adları örnek için uydurulmuş adlar. Aşağıdaki view, tabloyu iki parçaya ve üç parçaya bölüp geri birleştiriyor ve orijinalde olmayan satırları listeliyor:
CREATE TABLE dbo.supplies (
distributor NVARCHAR(50) NOT NULL,
publisher NVARCHAR(50) NOT NULL,
store NVARCHAR(50) NOT NULL,
CONSTRAINT PK_supplies PRIMARY KEY (distributor, publisher, store)
);
INSERT INTO dbo.supplies (distributor, publisher, store) VALUES
(N'Atlas', N'Northwind Books', N'Kadikoy'),
(N'Atlas', N'Harbor Press', N'Kadikoy'),
(N'Atlas', N'Northwind Books', N'Cankaya'),
(N'Meridian', N'Harbor Press', N'Konak');
GO
CREATE OR ALTER VIEW dbo.supply_split_check
AS
WITH dp AS (SELECT DISTINCT distributor, publisher FROM dbo.supplies),
ds AS (SELECT DISTINCT distributor, store FROM dbo.supplies),
ps AS (SELECT DISTINCT publisher, store FROM dbo.supplies),
two_way AS (
SELECT dp.distributor, dp.publisher, ds.store
FROM dp JOIN ds ON ds.distributor = dp.distributor),
three_way AS (
SELECT t.distributor, t.publisher, t.store
FROM two_way AS t
JOIN ps ON ps.publisher = t.publisher AND ps.store = t.store)
SELECT 'two-way' AS split, distributor, publisher, store
FROM (SELECT * FROM two_way EXCEPT SELECT * FROM dbo.supplies) AS x
UNION ALL
SELECT 'three-way', distributor, publisher, store
FROM (SELECT * FROM three_way EXCEPT SELECT * FROM dbo.supplies) AS y;
GO
SELECT split, distributor, publisher, store
FROM dbo.supply_split_check
ORDER BY split, distributor, publisher, store;
(4 rows affected)
split distributor publisher store
------- ----------- ------------ -------
two-way Atlas Harbor Press Cankaya
(1 rows affected)
İki parçaya bölmek (dağıtımcı–yayınevi ve dağıtımcı–mağaza) yine sahte bir satır üretti: Atlas, Harbor Press kitaplarını Çankaya'ya getirmiyor. Üçüncü ikiliyi (yayınevi–mağaza) de ekleyince sahte satır kayboldu ve üç tablo orijinali birebir geri verdi. Bu tesadüf değil; kitapçının sözleşmelerinde simetrik bir kural var: bir dağıtımcı bir yayınevini taşıyorsa, bir mağazaya hizmet veriyorsa ve o mağaza o yayınevini satıyorsa, dağıtımcı o yayınevinin kitaplarını o mağazaya getirir. Bu kural geçerliyse üçlü tablo, üç ikilinin birleşiminden başka bir şey söylemiyor.
Bu duruma join dependency (JD) denir: tablo, belirli parçalarına bölünüp birleştirildiğinde kayıpsız geri kurulabiliyor. 5NF'i Fagin 1979'da tanımladı; tablonun her join dependency'si yalnızca candidate key'lerinden kaynaklanıyorsa tablo 5NF'tedir. Simetrik kural geçerliyken supplies tablosu 5NF'te değil ve üç ikiliye bölünmeli. Kent'e göre 4NF ile 5NF ancak böyle simetrik bir kısıt olduğunda birbirinden ayrılır. Bölmenin faydası 4NF'tekiyle aynı: Atlas yeni bir mağazaya hizmet vermeye başlayınca, o mağazanın sattığı her yayınevi için ayrı satır eklemek yerine tek bir ikili satır eklersin.
Şimdi kuralı bozalım. Northwind Books, Kadıköy mağazası için yalnızca Meridian'la çalışmaya karar veriyor:
DELETE FROM dbo.supplies
WHERE distributor = N'Atlas' AND publisher = N'Northwind Books' AND store = N'Kadikoy';
INSERT INTO dbo.supplies (distributor, publisher, store)
VALUES (N'Meridian', N'Northwind Books', N'Kadikoy');
SELECT split, distributor, publisher, store
FROM dbo.supply_split_check
ORDER BY split, distributor, publisher, store;
(1 rows affected)
(1 rows affected)
split distributor publisher store
--------- ----------- --------------- -------
three-way Atlas Northwind Books Kadikoy
three-way Meridian Harbor Press Kadikoy
two-way Atlas Harbor Press Cankaya
two-way Atlas Northwind Books Kadikoy
two-way Meridian Harbor Press Kadikoy
two-way Meridian Northwind Books Konak
(6 rows affected)
Üç parçalı bölme artık iki sahte satır üretiyor. Atlas hâlâ Northwind taşıyor, hâlâ Kadıköy'e hizmet veriyor ve Kadıköy hâlâ Northwind satıyor; ama bu üç ikili artık "Atlas Northwind'i Kadıköy'e getirir" anlamına gelmiyor. Kural ortadan kalkınca join dependency de kalkar ve üçlü tablo, bölünemeyen tek bir olgu olarak 5NF'tedir. 3. yazıdaki yayın hakkı tablosu da bu durumdaydı. Pratikte 5NF ihlali nadirdir, çünkü böyle simetrik kurallar nadirdir; ama ternary bir ilişkiyle karşılaştığında sorman gereken soru şu: "Bu üçlü, ikililerinden türetilebilir mi, yoksa kendi başına bir karar mı?"
6NF'e Kısa Bir Bakış: Temporal Veri
Merdivenin son basamağı farklı bir ihtiyaçtan doğdu. C. J. Date, Hugh Darwen ve Nikos Lorentzos'un 2003 tarihli "Temporal Data and the Relational Model" kitabında tanımlanan 6NF, tabloyu trivial olmayan hiçbir join dependency kalmayana kadar böler; pratikte her tablo bir key ve en fazla bir attribute taşır. Bu, OLTP için aşırı görünür ve çoğunlukla öyledir. Ama geçerlilik tarihli veride anlamlı hâle gelir: kitabın fiyatı haftada bir, kategorisi yılda bir değişiyorsa ikisini tek bir geçmiş tablosunda tutmak, her fiyat değişikliğinde kategoriyi de kopyalamak demektir. Her biri kendi değişim ritmiyle kendi tablosunda durur:
CREATE TABLE dbo.book_price_history (
book_id INT NOT NULL,
valid_from DATE NOT NULL,
list_price DECIMAL(10,2) NOT NULL,
CONSTRAINT PK_book_price_history PRIMARY KEY (book_id, valid_from)
);
CREATE TABLE dbo.book_category_history (
book_id INT NOT NULL,
valid_from DATE NOT NULL,
category_id INT NOT NULL,
CONSTRAINT PK_book_category_history PRIMARY KEY (book_id, valid_from)
);
INSERT INTO dbo.book_price_history VALUES
(26, '2026-01-01', 390.50), (26, '2026-02-01', 351.45), (26, '2026-03-01', 390.50);
INSERT INTO dbo.book_category_history VALUES (26, '2025-06-01', 3);
DECLARE @on DATE = '2026-02-15';
SELECT p.book_id, p.list_price, c.category_id
FROM dbo.book_price_history AS p
JOIN dbo.book_category_history AS c ON c.book_id = p.book_id
WHERE p.valid_from = (SELECT MAX(valid_from) FROM dbo.book_price_history
WHERE book_id = p.book_id AND valid_from <= @on)
AND c.valid_from = (SELECT MAX(valid_from) FROM dbo.book_category_history
WHERE book_id = c.book_id AND valid_from <= @on);
(3 rows affected)
(1 rows affected)
book_id list_price category_id
------- ---------- -----------
26 351.45 3
(1 rows affected)
Bedeli de görünüyor: "15 Şubat'ta kitabın durumu neydi?" sorusu her attribute için ayrı bir "o tarihteki son satır" araması gerektiriyor. Geçerlilik tarihli kayıtların, fiyat geçmişinin ve bitemporal modelin pratik tasarımı Çözüm Desenleri serisinin "Temporal Data: Geçerlilik Tarihli Kayıtlar, Fiyat Geçmişi ve Bitemporal Model" yazısında. Buradaki teorik bağ şu: 6NF, temporal veride "neyin ne zaman değiştiğini" kopyasız tutmanın formal hâli.
Normal Form Merdiveninin Tamamı

Her basamak bir öncekini içerir: BCNF'teki bir tablo 3NF'tedir, 4NF'teki bir tablo BCNF'tedir. Her basamak da bir anomaly türünü daha ortadan kaldırır. Ama basamakların maliyeti eşit değil. 3NF'e kadar olan adımlar neredeyse her zaman bedavadır, çünkü bölünen tablolar gerçekten ayrı olguları taşır. BCNF bazen dependency preservation'dan, 4NF ve 5NF ise ancak ilgili multivalued ya da join dependency gerçekten varsa bir şey kazandırır. Bu yüzden pratik hedef "her tablo en yüksek normal form'da" değil, "her tablo en az 3NF'te, BCNF'ten bilinçli olarak sapılan yerler belgelenmiş, 4NF ve 5NF ihlalleri review'da aranmış" olmalı.
Üretimde Dikkat
4NF ihlali çoğunlukla bir "her şeyi bir yere koy" tablosunda saklanır: kullanıcının rolleri ve abonelikleri, ürünün renkleri ve bedenleri, yazarın dilleri ve türleri. Review'da işaret şudur: key'in tamamı olan ve hiçbir key olmayan attribute'u bulunmayan üç kolonlu bir tablo. Böyle bir tablo gördüğünde iki kolonun birbirinden bağımsız olup olmadığını sor. Mevcut veride kontrol için yukarıdaki satır sayısı ile kartezyen çarpım karşılaştırması iyi bir başlangıç; ama bu sorgu bir ihlali gösterebilir, bir kuralı kanıtlayamaz.
BCNF ihlali genellikle "çakışan candidate key'ler" olan tablolarda çıkar: iki composite candidate key'in ortak bir kolonu varsa (burada author_name) dikkatli bak. Gerçek hayatta bu, bir kaynağın (görevli, oda, makine) tek bir yere ait olduğu atama ve rezervasyon tablolarında sık görülür.
Kaybolan dependency'yi geri getirmek için indexed view kullanıyorsan, yazma yolunu ölç. View'a bağlı her tabloya yapılan her INSERT, UPDATE ve DELETE view index'ini de günceller ve aynı satırları kilitler. Az yazılan atama tablolarında bu ihmal edilebilir; yoğun yazılan tablolarda uygulama tarafında kontrol ya da 3NF'te kalmak daha ucuz olabilir.
Normal form kararlarını şemanın yanında yaz. "Bu tablo bilinçli olarak 3NF'te; host → store tekrarı kabul edildi, çünkü..." gibi tek bir not, iki yıl sonra tabloyu "düzeltmeye" kalkan birinin işini kurtarır. Açıklamaları şemanın içinde tutmanın yolları serinin son yazısında.
Sık Düşülen Tuzaklar
Akademik normal form'ları körü körüne uygulamak. Her tabloyu 5NF'e, hatta 6NF'e bölmek, bir sipariş ekranı için on tabloyu birleştirmek demek. Normal form, gerçekten var olan bir dependency'nin ürettiği anomaly'yi kaldırdığında değerlidir; olmayan bir dependency için tablo bölmek yalnızca join sayısını artırır.
Bağımlı olguları bağımsız sanıp bölmek. Dil ve tür örneğinde gördüğümüz gibi, aralarında ilişki olan iki olguyu 4NF adına ayırırsan birleştirme yanlış satırlar üretir. Bölmeden önce kuralı iş tarafına sor.
BCNF'e geçip kaybolan kuralı unutmak. Tablo bölündüğünde hangi dependency'nin artık hiçbir tablonun key'i olmadığını listele ve her biri için bir koruma seç: indexed view, uygulama kontrolü ya da bilinçli kabul.
Ternary ilişkiyi alışkanlıkla üç ikiliye bölmek. 5NF ancak simetrik bir kural varsa bölmeyi önerir; kural yoksa üçlü tablo zaten 5NF'tedir. Kuralı veride "keşfetmek" de aynı derecede tehlikeli: d1.sql'deki dört satırda kural tesadüfen tutuyordu ve tek bir sözleşme değişikliği onu bozdu.
3NF tanımını yalnızca "key olmayan attribute'lar" üzerinden ezberlemek. Tüm kolonları key'in parçası olan bir tablo 3NF'i otomatik geçer ve bu, onun anomaly'siz olduğu anlamına gelmez. İmza günü tablosu ve dil–tür tablosu bunun iki örneği.
Küçük Bir Deneme
★☆☆ author_profiles tablosuna Herbert için Almanca baskıyı doğru şekilde ekle: kaç satır gerekiyor? Aynı işi author_languages tablosunda yap ve satır sayılarını karşılaştır.
★★☆ İmza günü örneğine "bir görevli bir günde en fazla bir yazara eşlik eder" kuralını ve signing_date kolonunu ekle. Dependency'leri yeniden yaz, closure procedure'üyle candidate key'leri bul ve tablonun BCNF'te olup olmadığını kontrol et.
★★★ supplies örneğinde simetrik kural geçerliyken üç ikili tabloyu kur ve Konak'a da hizmet vermeye başlayan yeni bir dağıtımcı ekle. Üçlü tabloda kaç satır eklemen gerekirdi, ikili tablolarda kaç satır? Ardından üç ikilinin birleşiminin her zaman kurala uyduğunu gösteren bir kontrol sorgusu yaz.
Normal form merdivenini sonuna kadar çıktık ve her basamağın bir bedeli olduğunu gördük. Bir sonraki yazıda yönü tersine çeviriyoruz: denormalization ne zaman bir performans kararıdır, persisted computed column, indexed view ve özet tablo gibi teknikler nasıl çalışır ve kopyalanan verinin tutarlılığını kim korur?