Bilgi Bankası
PostgreSQLPerformans ve kapasite 18 dk 14.06.2026

PostgreSQL ve MySQL’de normalizasyon: foreign key, join ve indeks dengesini nasıl kurarsınız?

İlişkisel veritabanlarında normalizasyon güçlü bir başlangıçtır; ama “her şeyi en küçük tabloya bölelim” demek de çözüm değildir. Bu yazı foreign key, join ve indeks dengesini gerçek uygulama akışları üzerinden anlatır.

Bu yazı sana şu durumda yardımcı olur
  • aynı müşteri veya ürün bilgisi birçok tabloda tekrar ediyor
  • foreign key yok diye silinen kayıtlar yetim satır bırakıyor
  • join sayısı artınca sorgular yavaşlıyor
  • denormalize alanlar güncel kalmıyor
  • raporlama sorguları OLTP tablolarını yoruyor
Yazıyı bitirince

PostgreSQL/MySQL tarafında normalizasyonu veri tutarlılığı için nasıl kullanacağınızı, join maliyetini indeksle nasıl yöneteceğinizi ve hangi durumda bilinçli denormalizasyon düşüneceğinizi anlayacaksınız.

TürkDB açısından ürünleşme notu

Bu bölüm, TürkDB’nin ilgili problemi hangi ürün ilkeleri ve kontrol noktalarıyla ele alacağını anlatır. Kapsam, gerçek iş yükü ve ihtiyaçla birlikte netleşir.

TürkDB bu kararı sizin yerinize vermez; fakat pilotta yavaş sorgu, bağlantı, yedek ve bakım etkilerini aynı bağlamda gözlemlemek normalizasyon kararının gerçek maliyetini görmeye yardım eder.

OLTP sistemde makul normalizasyon iyi başlangıçtır

Sipariş alan bir uygulamada müşteri, sipariş, sipariş satırı ve ödeme ayrı kavramlardır. Bunları tek dev tabloya koymak ilk gün kolay görünür; ama müşteri bilgisi değiştiğinde, sipariş geçmişi okunurken veya ödeme durumu güncellenirken tablo hızla karmaşıklaşır.

Makul normalizasyon, her kavramın kendi tablosunda yaşamasını sağlar. Bu sayede veri güncelleme noktası nettir, foreign key ile ilişki korunur ve uygulama kodu hangi verinin gerçeğine bakacağını bilir.

Basit normalleştirilmiş sipariş modeli

orders-normalized.sql
CREATE TABLE customers (
  id BIGSERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL
);

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES customers(id),
  status TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
  id BIGSERIAL PRIMARY KEY,
  order_id BIGINT NOT NULL REFERENCES orders(id),
  product_id BIGINT NOT NULL,
  quantity INT NOT NULL,
  unit_price_cents INT NOT NULL
);

Foreign key tek başına yetmez, indeks de gerekir

Foreign key ilişkiyi korur ama her sorguyu otomatik hızlandırmaz. Siparişleri müşteriyle, sipariş satırlarını siparişle okuyan sorgularınız varsa ilgili foreign key kolonlarında indeks gerekir.

İndeks tasarımı sorgu desenine göre yapılmalıdır. Sadece customer_id indekslemek bazı sorgulara yeter; ama müşterinin son siparişlerini okuyorsanız customer_id, created_at kombinasyonu daha anlamlı olabilir.

Join ve listeleme için indeksler

orders-indexes.sql
CREATE INDEX idx_orders_customer_created
ON orders (customer_id, created_at DESC);

CREATE INDEX idx_order_items_order
ON order_items (order_id);

EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, o.status, o.created_at, sum(oi.quantity * oi.unit_price_cents) AS total_cents
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.customer_id = 42
GROUP BY o.id
ORDER BY o.created_at DESC
LIMIT 20;

Aşırı normalizasyon da okunabilirliği bozabilir

Her küçük alanı ayrı tabloya bölmek bazen modeli daha doğru değil, daha kırılgan yapar. Örneğin çok az değişen ve sadece ana kayıtla birlikte kullanılan ayarları ayrı ayrı tablolara taşımak gereksiz join ve kod karmaşıklığı yaratabilir.

Normalizasyonu amaç değil araç olarak düşünün. Veri değişim noktalarını netleştiriyor, tutarsızlığı azaltıyor ve sorguları anlaşılır tutuyorsa işe yarıyordur. Sadece şemayı akademik olarak daha saf gösteriyorsa dikkat etmek gerekir.

  • Sık birlikte okunan ve aynı yaşam döngüsüne sahip alanlar aynı tabloda kalabilir.
  • Tarihsel kayıt için bazı değerler bilinçli kopyalanabilir.
  • Raporlama için ayrı özet tablo veya analitik sistem daha doğru olabilir.
  • Denormalize alanın güncelleme sahibi yazılı olmalıdır.

Denormalize edecekseniz doğrulama sorgusu yazın

Örneğin orders tablosuna customer_email_snapshot alanı eklemek tarihsel sipariş görünümü için doğru olabilir. Ama customer_current_email gibi güncel kalması gereken bir alan kopyalanıyorsa tutarsızlık riski doğar.

Denormalizasyon yaptığınız her yerde küçük bir doğrulama sorgusu bulunsun. Bu sorgu düzenli çalışmak zorunda değildir; ama sorun anında “hangi kayıtlar ayrışmış?” sorusuna cevap vermelidir.

Tutarsız kopyaları bulma

denormalized-email-check.sql
SELECT o.id, o.customer_id, o.customer_email_copy, c.email AS current_email
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.customer_email_copy IS DISTINCT FROM c.email
LIMIT 100;
Devam etmek istersen

Sorunu motor bazında derinleştirin

Benzer belirtiler farklı motorlarda farklı nedenlere dayanır; yayınlanan tüm rehberleri ana sayfadan takip edebilirsiniz.

Tüm makaleler