Veritabanı normalizasyonu, kulağa akademik gelen ama günlük hayatta çok somut bir işi olan bir tasarım disiplinidir: aynı bilgiyi birden fazla yerde saklamayı bırakıp her gerçeği tek bir yerde tutmak. Bir müşterinin adresi üç ayrı tabloda tekrarlanıyorsa, adres değiştiğinde üçünü de güncellemeyi unutan ilk kod satırı veritabanınızı tutarsız hâle getirir. Normalizasyon bu ihtimali şema düzeyinde imkânsız kılmayı hedefler.
Bu rehberde normalizasyonu tanım ezberleterek değil, tek bir "sipariş" tablosunu adım adım parçalayarak anlatacağım. Önce ham hâliyle karşılaştığımız anomalileri göreceğiz, sonra 1NF, 2NF ve 3NF kurallarını sırayla uygulayıp aynı verinin nasıl temiz bir yapıya oturduğunu izleyeceğiz. Sonunda BCNF'ye kısaca değinip, normalize bir şemayı sorgularken nelere dikkat edeceğinizi ve pratikte en sık yapılan hataları göreceksiniz.
Normalizasyon Nedir ve Hangi Sorunu Çözer#
Normalizasyon, tabloları fonksiyonel bağımlılıklarına göre parçalayarak veri tekrarını azaltma yöntemidir. "Fonksiyonel bağımlılık" korkutucu görünse de anlamı basittir: bir sütunun değerini bilmek başka bir sütunun değerini belirliyorsa, ikincisi birincisine bağımlıdır. Örneğin musteri_id biliniyorsa musteri_adi da bellidir; yani musteri_adi, musteri_id'ye fonksiyonel olarak bağımlıdır.
Bu bağımlılıklar yanlış tabloya yerleşince üç tür anomali doğar:
| Anomali | Ne zaman olur | Somut sonuç |
|---|---|---|
| Ekleme (insert) | Bir gerçeği başka bir gerçek olmadan yazamamak | Henüz siparişi olmayan müşteriyi kaydedememek |
| Güncelleme (update) | Aynı bilgi birden çok satırda durur | Adresi 40 satırın 38'inde güncelleyip 2'sini unutmak |
| Silme (delete) | Bir kaydı silince başka bilgi de yok olur | Son siparişi silince ürünün fiyat bilgisinin kaybolması |
Şu ham tabloyu ele alalım; birçok projede ilk hâli buna benzer:
CREATE TABLE siparisler_ham (
siparis_no INT,
musteri_adi VARCHAR(120),
musteri_sehir VARCHAR(60),
sehir_plaka VARCHAR(3),
urunler VARCHAR(255), -- "Klavye, Mouse, Monitör"
adetler VARCHAR(50), -- "1, 2, 1"
urun_fiyatlari VARCHAR(100) -- "450.00, 250.00, 3200.00"
);
Bu tabloda ürün adını değiştirmek, bir siparişteki tek bir kalemi silmek ya da "hangi üründen kaç adet satıldı" sorusunu cevaplamak son derece zordur. Üstelik urunler sütunundaki metni ayrıştırmak zorunda kaldığınız için hiçbir indeks işe yaramaz. Normalizasyonun tüm amacı bu tabloyu, veritabanının anlayabileceği bir yapıya çevirmektir.
Birinci Normal Form (1NF)#
Bir tablo 1NF'de sayılabilmesi için üç koşulu sağlamalıdır: her hücre tek bir atomik değer taşımalı, tekrarlayan sütun grupları bulunmamalı ve her satır benzersiz biçimde tanımlanabilmelidir. Yukarıdaki tabloda urunler sütunu üç ürün adını virgülle ayırıp tek hücreye sıkıştırdığı için bu kural açıkça ihlal edilmiştir.
Sık yapılan ama yanlış olan çözüm, urun_1, urun_2, urun_3 biçiminde sütunlar açmaktır. Bu da tekrarlayan grup olduğu için hâlâ 1NF değildir; ayrıca dördüncü ürün geldiğinde şemayı değiştirmeniz gerekir. Doğru çözüm, tekrarlayan yapıyı satırlara taşımaktır:
-- 1NF: her satır tek bir sipariş kalemini temsil eder
CREATE TABLE siparis_kalemleri_1nf (
siparis_no INT NOT NULL,
urun_adi VARCHAR(120) NOT NULL,
adet SMALLINT UNSIGNED NOT NULL,
urun_fiyati DECIMAL(12,2) NOT NULL,
musteri_adi VARCHAR(120) NOT NULL,
musteri_sehir VARCHAR(60) NOT NULL,
sehir_plaka CHAR(2) NOT NULL,
PRIMARY KEY (siparis_no, urun_adi)
) ENGINE=InnoDB;
Artık her hücrede tek bir değer var ve birincil anahtar (siparis_no, urun_adi) bileşiminden oluşuyor. Sütunların doğru tiplerle tanımlandığına dikkat edin; fiyatın DECIMAL olması gerektiğini doğru veri tipi seçimi rehberinde ayrıntılı anlatmıştım. Ancak bu tablo hâlâ ciddi biçimde tekrarlıdır: aynı siparişin üç kalemi varsa müşteri adı ve şehri üç kez yazılıyor.
İkinci Normal Form (2NF)#
2NF, 1NF'e ek olarak şunu ister: anahtar olmayan her sütun, birincil anahtarın tamamına bağımlı olmalıdır, bir parçasına değil. Bu kural yalnızca bileşik (birden çok sütunlu) anahtarı olan tablolarda anlam taşır; anahtarınız tek sütunsa 1NF'den sonra zaten 2NF'desiniz.
Yukarıdaki tabloya bakalım. Anahtar (siparis_no, urun_adi). Şimdi bağımlılıkları tek tek sorgulayalım:
adetneye bağlı? Hem siparişe hem ürüne, yani anahtarın tamamına. Sorun yok.musteri_adineye bağlı? Yalnızcasiparis_no'ya. Ürün adının bununla ilgisi yok — kısmi bağımlılık.urun_fiyatineye bağlı? Yalnızcaurun_adi'na. Yine kısmi bağımlılık.
Bu iki kısmi bağımlılık, tabloyu üçe bölmemizi gerektirir:
-- 2NF: kısmi bağımlılıklar kendi tablolarına taşındı
CREATE TABLE siparisler (
siparis_no INT UNSIGNED NOT NULL AUTO_INCREMENT,
musteri_adi VARCHAR(120) NOT NULL,
musteri_sehir VARCHAR(60) NOT NULL,
sehir_plaka CHAR(2) NOT NULL,
PRIMARY KEY (siparis_no)
) ENGINE=InnoDB;
CREATE TABLE urunler (
urun_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
urun_adi VARCHAR(120) NOT NULL,
liste_fiyat DECIMAL(12,2) NOT NULL,
PRIMARY KEY (urun_id),
UNIQUE KEY uq_urun_adi (urun_adi)
) ENGINE=InnoDB;
CREATE TABLE siparis_kalemleri (
siparis_no INT UNSIGNED NOT NULL,
urun_id INT UNSIGNED NOT NULL,
adet SMALLINT UNSIGNED NOT NULL,
birim_fiyat DECIMAL(12,2) NOT NULL, -- satış anındaki fiyat
PRIMARY KEY (siparis_no, urun_id),
CONSTRAINT fk_kalem_siparis FOREIGN KEY (siparis_no) REFERENCES siparisler(siparis_no),
CONSTRAINT fk_kalem_urun FOREIGN KEY (urun_id) REFERENCES urunler(urun_id)
) ENGINE=InnoDB;
Burada bilinçli bir istisnaya dikkat edin: siparis_kalemleri.birim_fiyat sütunu urunler.liste_fiyat ile aynı bilgiyi taşıyor gibi görünür ama değildir. Liste fiyatı zamanla değişir; siparişin verildiği andaki fiyat ise tarihsel bir gerçektir ve saklanmalıdır. Aksi hâlde ürün fiyatını yükselttiğinizde geçmiş faturalarınızın tutarı da değişir. Bu, normalizasyon kuralının değil, "farklı gerçekler farklı sütunlardır" ilkesinin uygulanmasıdır. Yabancı anahtarların doğru kurulması için primary key ve foreign key tasarımı rehberine göz atın.
Üçüncü Normal Form (3NF)#
3NF, 2NF'e ek olarak geçişli bağımlılığı yasaklar: anahtar olmayan bir sütun, anahtar olmayan başka bir sütuna bağımlı olmamalıdır. siparisler tablosuna tekrar bakalım. sehir_plaka neye bağlı? siparis_no'ya değil, musteri_sehir değerine. Yani zincir şöyle işliyor: siparis_no → musteri_sehir → sehir_plaka. Ortadaki sütun anahtar olmadığı için bu geçişli bir bağımlılıktır.
Aynı şekilde musteri_adi da aslında siparişe değil müşteriye aittir. Müşteriyi ayrı bir varlık olarak modellemek gerekir:
CREATE TABLE sehirler (
plaka CHAR(2) NOT NULL,
sehir_adi VARCHAR(60) NOT NULL,
PRIMARY KEY (plaka),
UNIQUE KEY uq_sehir_adi (sehir_adi)
) ENGINE=InnoDB;
CREATE TABLE musteriler (
musteri_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
ad_soyad VARCHAR(120) NOT NULL,
eposta VARCHAR(190) NOT NULL,
sehir_plaka CHAR(2) NOT NULL,
PRIMARY KEY (musteri_id),
UNIQUE KEY uq_eposta (eposta),
CONSTRAINT fk_musteri_sehir FOREIGN KEY (sehir_plaka) REFERENCES sehirler(plaka)
) ENGINE=InnoDB;
CREATE TABLE siparisler (
siparis_no INT UNSIGNED NOT NULL AUTO_INCREMENT,
musteri_id INT UNSIGNED NOT NULL,
olusturuldu DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (siparis_no),
KEY idx_musteri (musteri_id),
CONSTRAINT fk_siparis_musteri FOREIGN KEY (musteri_id) REFERENCES musteriler(musteri_id)
) ENGINE=InnoDB;
Şema artık 3NF'de. Sonucu somut kazançlarla özetleyelim: müşterinin şehri tek bir satırda duruyor, bir şehrin adı değişirse tek bir UPDATE yetiyor, henüz siparişi olmayan müşteri sorunsuz kaydedilebiliyor ve son siparişi silmek müşteri kaydını yok etmiyor. Yani başlangıçtaki üç anomalinin üçü de ortadan kalktı.
Normal formların birbirine göre konumunu şu tablo netleştirir:
| Form | Kural | Tipik ihlal |
|---|---|---|
| 1NF | Atomik değer, tekrarlayan grup yok | Virgülle ayrılmış liste, urun_1, urun_2 sütunları |
| 2NF | Anahtarın tamamına bağımlılık | Bileşik anahtarın yarısına bağlı sütun |
| 3NF | Geçişli bağımlılık yok | Şehir adından türeyen plaka kodu |
| BCNF | Her belirleyici aday anahtardır | Örtüşen aday anahtarlar |
| 4NF | Çok değerli bağımlılık yok | Birbirinden bağımsız iki listeyi tek tabloda tutmak |
BCNF ve Daha İleri Formlar#
3NF çoğu uygulama için yeterlidir; ama nadiren de olsa 3NF'i sağladığı hâlde hâlâ anomali üreten şemalar vardır. Boyce-Codd Normal Formu (BCNF), 3NF'in biraz daha katı hâlidir ve şunu ister: bir sütunu belirleyen her ifade, aday anahtar olmalıdır. Sorun genellikle bir tabloda birden fazla aday anahtar bulunduğunda ve bunlar örtüştüğünde ortaya çıkar.
Klasik örnek randevu tablosudur: bir dersliğe belirli bir saatte tek bir öğretmen atanabilir ve bir öğretmen belirli bir saatte tek bir derslikte olabilir. (derslik, saat) bir aday anahtar, (ogretmen, saat) başka bir aday anahtardır. Bu tür örtüşmeler pratikte enderdir; iş uygulamalarının büyük kısmında 3NF ile durup daha ileri formları teorik bilgi olarak bırakmak makul bir tercihtir.
4NF ise çok değerli bağımlılıkları hedefler: birbiriyle ilgisiz iki listeyi (örneğin bir çalışanın konuştuğu diller ve sahip olduğu sertifikalar) aynı tabloda tutarsanız satır sayısı çarpım kadar şişer. Çözüm, iki bağımsız ilişkiyi iki ayrı tabloya ayırmaktır ki bu zaten sağduyulu tasarımın doğal sonucudur.
Normalize Şemayı Sorgulamak#
Normalizasyonun bedeli, bir bilgiyi toplamak için birden fazla tabloya bakmak zorunda kalmanızdır. Yukarıdaki şemada "hangi müşteri hangi üründen kaç adet aldı" sorusu artık bir birleştirme (join) gerektirir:
SELECT m.ad_soyad,
s.sehir_adi,
u.urun_adi,
k.adet,
k.birim_fiyat * k.adet AS satir_toplam
FROM siparis_kalemleri k
JOIN siparisler sp ON sp.siparis_no = k.siparis_no
JOIN musteriler m ON m.musteri_id = sp.musteri_id
JOIN sehirler s ON s.plaka = m.sehir_plaka
JOIN urunler u ON u.urun_id = k.urun_id
WHERE sp.olusturuldu >= '2026-08-01'
ORDER BY sp.olusturuldu DESC;
Bu sorgu, ilk bakışta ham tablodaki tek satırlık SELECT'ten karmaşık görünür. Ancak yabancı anahtarlar indeksliyse bu birleştirmeler InnoDB için son derece ucuzdur; her biri birincil anahtar üzerinden noktasal arama yapar. Beş tabloyu birleştirmek, milyonlarca satırda metin ayrıştırmaktan kat kat hızlıdır. Birleştirme türlerinin davranışını SQL JOIN türleri yazısında ayrıntılı bulabilirsiniz.
Yine de yüksek trafikli raporlarda beş tablolu birleştirmeler ölçekleme sorunu yaratabilir. Bu noktada devreye bilinçli denormalizasyon girer: doğru tasarlanmış 3NF şemadan başlayıp, ölçüm sonuçlarına göre belirli alanları kopyalamak. Sırayı tersine çevirmek, yani baştan denormalize başlamak neredeyse her zaman hataya götürür. Bu dengeyi denormalizasyon ne zaman mantıklı yazısında ele alıyorum.
Sık Yapılan Hatalar#
Normalizasyonu tablo sayısını azaltmakla karıştırmak. Normalizasyon tablo sayısını artırır, azaltmaz. "Az tablo = sade tasarım" sezgisi ilişkisel modelde yanlıştır; sadelik, her tablonun tek bir şeyi temsil etmesidir.
Virgüllü listeleri "pratik" bulmak. etiketler VARCHAR(255) içinde "php,mysql,linux" tutmak yazması kolaydır ama "php etiketli kaç yazı var" sorusuna indeksli cevap veremezsiniz, LIKE '%php%' her zaman tam tarama yapar ve phpstorm gibi değerleri de yanlışlıkla eşler. Doğrusu bir bağlantı tablosudur.
Yabancı anahtar kısıtlarını koymamak. Şemayı 3NF'e getirip FOREIGN KEY tanımlamazsanız, tutarlılığı yalnızca uygulama kodu korur. Bir arka plan görevi ya da elle çalıştırılan bir DELETE bu korumayı atlar ve öksüz satırlar oluşur.
Her tabloya anlamsız bir kimlik sütunu ekleyip doğal anahtarı unutmak. sehirler tablosunda plaka kodu zaten benzersiz bir doğal anahtardır. Yapay bir id ekleyecekseniz bile plaka üzerinde UNIQUE kısıtı bırakın, yoksa aynı şehir iki kez girilebilir.
Tarihsel değeri "tekrar" sanıp silmek. Fatura satırındaki birim fiyat, ürün tablosundaki güncel fiyatın kopyası değildir; o an geçerli olan değerin kaydıdır. Bunu normalizasyon adına kaldırmak, geçmiş kayıtları bozar.
Aşırı normalize etmek. Her sütunu ayrı bir arama tablosuna çıkarmak (adres için ayrı, mahalle için ayrı, sokak için ayrı tablo) şemayı okunmaz ve sorguları gereksiz ağır hâle getirir. Ölçüt şudur: bu değer bağımsız olarak değişiyor ve başka yerlerde de kullanılıyor mu? Cevap hayırsa ayrı tabloya gerek yoktur.
Sıkça Sorulan Sorular#
Hangi normal forma kadar gitmeliyim#
Pratikte 3NF hedeflemek doğru cevaptır ve iş uygulamalarının neredeyse tamamı için yeterlidir. 3NF, güncelleme ve silme anomalilerinin büyük çoğunluğunu ortadan kaldırırken şemayı hâlâ okunabilir tutar. BCNF ve üzeri formlar örtüşen aday anahtarlar veya çok değerli bağımlılıklar gibi özel durumlarda gerekir; bu durumlarla karşılaşırsanız zaten belirtileri fark edersiniz.
Normalizasyon sorguları yavaşlatır mı#
Normalizasyon birleştirme sayısını artırır ama bu otomatik olarak yavaşlık demek değildir. Yabancı anahtar sütunları indekslenmişse birleştirmeler birincil anahtar üzerinden noktasal aramalarla çözülür ve çok hızlıdır. Asıl yavaşlığa yol açan şey, normalize edilmemiş bir tabloda metin ayrıştırmak veya milyonlarca tekrarlı satırı taramaktır. Gerçek bir darboğaz görürseniz önce indeksleri ve sorgu planını inceleyin.
Normalizasyon ile denormalizasyon arasında nasıl karar veririm#
Kural olarak normalize başlayın, ölçün, sonra gerekiyorsa hedefli biçimde denormalize edin. Denormalizasyon bir optimizasyon tekniğidir ve her optimizasyon gibi ölçüme dayanmalıdır. Bir raporun gerçekten yavaş olduğunu ve indeksle çözülemediğini gördükten sonra ilgili alanı kopyalamak makuldür; tahmine dayalı erken denormalizasyon ise tutarsızlık borcunu baştan üstlenmektir.
Bileşik birincil anahtar kullanmak sorun mu#
Sorun değildir, hatta bağlantı (ara) tablolarında en doğal çözümdür: siparis_kalemleri tablosunda (siparis_no, urun_id) bileşimi hem benzersizliği garanti eder hem de fazladan bir sütun gerektirmez. Dikkat etmeniz gereken tek şey, InnoDB'de ikincil indekslerin birincil anahtarı içinde taşıması nedeniyle çok geniş bileşik anahtarların indeksleri şişirmesidir. İki dar tam sayı sütunu bu açıdan tamamen güvenlidir.
Var olan bir veritabanını nasıl normalize ederim#
Önce yeni tabloları oluşturun, sonra veriyi INSERT ... SELECT DISTINCT ile taşıyın, ardından yabancı anahtar sütunlarını doldurup kısıtları ekleyin ve en son eski sütunları düşürün. Her adımdan önce mutlaka yedek alın ve mümkünse önce bir kopya veritabanında prova yapın. Uygulama kodunu da aynı anda güncellemeniz gerekeceği için bu iş genellikle bir bakım penceresi ister.
Normalizasyon NoSQL veritabanlarında da geçerli mi#
Normal formlar ilişkisel modelin kuralları olduğu için belge tabanlı veritabanlarında birebir uygulanmaz; oralarda genellikle bilinçli olarak gömülü (embedded) yapılar tercih edilir. Yine de altta yatan soru aynıdır: bu bilgi nerede tek kopya hâlinde duruyor ve değiştiğinde kaç yeri güncellemem gerekiyor. Belge veritabanlarında bu güncellemenin sorumluluğu tamamen uygulamaya geçer, çünkü yabancı anahtar kısıtı yoktur.
1NF için neden ayrı tablo açmam gerekiyor, ek sütun yetmez mi#
urun_1, urun_2, urun_3 biçiminde sütun eklemek tekrarlayan grup oluşturur ve 1NF'i sağlamaz. Bu yaklaşımda dördüncü ürün geldiğinde şemayı değiştirmeniz, tüm sorguları güncellemeniz ve "hangi üründen kaç adet satıldı" gibi soruları üç sütunu ayrı ayrı toplayarak cevaplamanız gerekir. Satırlara taşımak ise sınırsız sayıda kaleme izin verir ve tek bir GROUP BY ile toplama yapmanızı sağlar.
Kapanış#
Normalizasyon, veriyi düzenlemekten çok "her gerçek tek bir yerde dursun" ilkesini şemaya kazımaktır. Aklınızda kalacak dört alışkanlık şunlar: hücreye liste sıkıştırmayın, bileşik anahtarın yarısına bağlı sütunları ayırın, anahtar olmayanın anahtar olmayana bağımlılığını kaldırın ve yabancı anahtar kısıtlarını gerçekten tanımlayın. Bunları yaptığınızda güncelleme anomalileri şema düzeyinde imkânsız hâle gelir.
Şemanızı büyütürken veritabanı sunucunuzun da bu yükü kaldırması gerekir. Kendi MySQL veya PostgreSQL ayarlarınızı özgürce yapmak isterseniz VDS ve bulut sunucu paketlerimiz tam yetki verir; ölçeklenen bir mağaza için hazır yapılandırmalı e-ticaret hosting paketleri işinizi kolaylaştırır. Şema tasarımı ve bakım penceresi planlamasında yardım isterseniz sunucu yönetimi hizmetimiz bu konuları da kapsıyor.