FOREIGN KEY ile Tablolar Arası İlişki Tanımlama

Veritabanı tasarımının belki de en kritik kavramlarından biri olan yabancı anahtar ilişkileri, veriler arasındaki bütünlüğü sağlamak için vazgeçilmez bir araçtır. Bir e-ticaret sitesinde siparişlerin müşterilere bağlanması, bir blog sisteminde yorumların yazılara eklenmesi ya da bir şirket veritabanında çalışanların departmanlara atanması… Tüm bu senaryolar arkada güçlü bir FOREIGN KEY altyapısı gerektirir. Bu yazıda MariaDB ve MySQL üzerinde FOREIGN KEY kullanımını gerçek dünya senaryolarıyla birlikte ele alacağız.

FOREIGN KEY Nedir ve Neden Kullanılır?

FOREIGN KEY (Yabancı Anahtar), bir tablodaki sütunun başka bir tablonun birincil anahtarına (PRIMARY KEY) veya benzersiz anahtarına (UNIQUE KEY) referans vermesini sağlayan bir kısıtlamadır. Bu kısıtlama sayesinde veritabanı motoru, tablolar arasındaki ilişkinin tutarlılığını otomatik olarak denetler.

Mesela bir sipariş tablosunda müşteri ID’si tutuyorsunuz. FOREIGN KEY olmadan, var olmayan bir müşteri ID’si girebilir, müşteriyi silerken siparişler ortada kalabilir ya da yanlış ID yazarak veri bütünlüğünü bozabilirsiniz. FOREIGN KEY tüm bu sorunları engeller.

Temel faydaları şöyle sıralayabiliriz:

  • Referans bütünlüğü: Var olmayan bir kayda referans verilmesini engeller
  • Kademeli işlemler: Silme veya güncelleme işlemlerini ilişkili tablolara yayabilirsiniz
  • Otomatik doğrulama: Uygulama katmanında yapılması gereken kontrolleri veritabanı seviyesine taşır
  • Belgeleme: Şema üzerinden tablolar arası ilişkileri açıkça görünür kılar

Önemli bir not: MySQL ve MariaDB’de FOREIGN KEY kısıtlamaları yalnızca InnoDB depolama motoru ile çalışır. MyISAM tabloları FOREIGN KEY sözdizimini kabul eder ama denetim yapmaz. Tablolarınızın InnoDB kullandığından emin olun.

Temel FOREIGN KEY Sözdizimi

Önce en basit haliyle bir FOREIGN KEY nasıl tanımlanır, ona bakalım.

-- Temel FOREIGN KEY sözdizimi
CREATE TABLE tablo_adi (
    sutun_adi veri_tipi,
    referans_sutun veri_tipi,
    CONSTRAINT kisitlama_adi FOREIGN KEY (referans_sutun)
        REFERENCES hedef_tablo (hedef_sutun)
        ON DELETE aksiyon
        ON UPDATE aksiyon
);

Şimdi gerçek bir örneğe geçelim. Bir e-ticaret sisteminde müşteriler ve siparişler tablosu oluşturalım:

-- Önce ana tablo oluşturuluyor (referans alınan tablo)
CREATE TABLE musteriler (
    musteri_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ad VARCHAR(100) NOT NULL,
    soyad VARCHAR(100) NOT NULL,
    email VARCHAR(200) UNIQUE NOT NULL,
    telefon VARCHAR(20),
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Şimdi bağımlı tablo oluşturuluyor
CREATE TABLE siparisler (
    siparis_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    musteri_id INT UNSIGNED NOT NULL,
    siparis_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    toplam_tutar DECIMAL(10,2) NOT NULL,
    durum ENUM('beklemede','onaylandi','gonderildi','teslim_edildi','iptal') DEFAULT 'beklemede',
    CONSTRAINT fk_siparis_musteri FOREIGN KEY (musteri_id)
        REFERENCES musteriler (musteri_id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Burada dikkat edilmesi gereken birkaç nokta var:

  • musteri_id sütununun veri tipi her iki tabloda da aynı olmalı (INT UNSIGNED)
  • CONSTRAINT ismi benzersiz olmalı ve açıklayıcı olmasına özen gösterilmeli
  • Ana tablo (musteriler) her zaman önce oluşturulmalı

ON DELETE ve ON UPDATE Aksiyonları

FOREIGN KEY’in en güçlü özelliği, referans alınan kayıt silindiğinde veya güncellendiğinde ne olacağını belirleyebilmemizdir. MariaDB ve MySQL’de kullanabileceğiniz aksiyonlar şunlardır:

  • RESTRICT: Referans alınan kayıt silinmesini veya güncellenmesini engeller (varsayılan davranış)
  • CASCADE: Ana tablodaki değişiklik bağımlı tabloya yansıtılır
  • SET NULL: Ana kayıt silindiğinde veya güncellendiğinde FOREIGN KEY sütunu NULL yapılır
  • SET DEFAULT: FOREIGN KEY sütunu tanımlı varsayılan değerine döner (MariaDB’de desteklenir, MySQL’de sınırlı destek)
  • NO ACTION: RESTRICT ile benzer davranış gösterir, fakat kontrol gecikmeli yapılabilir

Gerçek Dünya Senaryo 1: Blog Sistemi

Bir blog sistemini modelleyelim. Yazarlar, yazılar, kategoriler ve yorumlar olsun:

-- Yazarlar tablosu
CREATE TABLE yazarlar (
    yazar_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    kullanici_adi VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(200) UNIQUE NOT NULL,
    tam_ad VARCHAR(150) NOT NULL,
    aktif TINYINT(1) DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Kategoriler tablosu (kendi kendine referans veren hiyerarşik yapı)
CREATE TABLE kategoriler (
    kategori_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    ust_kategori_id INT UNSIGNED NULL,
    kategori_adi VARCHAR(100) NOT NULL,
    slug VARCHAR(120) UNIQUE NOT NULL,
    CONSTRAINT fk_kategori_ust FOREIGN KEY (ust_kategori_id)
        REFERENCES kategoriler (kategori_id)
        ON DELETE SET NULL
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Yazılar tablosu (birden fazla FOREIGN KEY)
CREATE TABLE yazilar (
    yazi_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    yazar_id INT UNSIGNED NOT NULL,
    kategori_id INT UNSIGNED NULL,
    baslik VARCHAR(255) NOT NULL,
    icerik LONGTEXT,
    yayin_tarihi DATETIME,
    durum ENUM('taslak','yayinda','arsiv') DEFAULT 'taslak',
    CONSTRAINT fk_yazi_yazar FOREIGN KEY (yazar_id)
        REFERENCES yazarlar (yazar_id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE,
    CONSTRAINT fk_yazi_kategori FOREIGN KEY (kategori_id)
        REFERENCES kategoriler (kategori_id)
        ON DELETE SET NULL
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Yorumlar tablosu
CREATE TABLE yorumlar (
    yorum_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    yazi_id INT UNSIGNED NOT NULL,
    ust_yorum_id INT UNSIGNED NULL,
    yorum_yapan VARCHAR(100) NOT NULL,
    email VARCHAR(200) NOT NULL,
    yorum_metni TEXT NOT NULL,
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    onaylandi TINYINT(1) DEFAULT 0,
    CONSTRAINT fk_yorum_yazi FOREIGN KEY (yazi_id)
        REFERENCES yazilar (yazi_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT fk_yorum_ust FOREIGN KEY (ust_yorum_id)
        REFERENCES yorumlar (yorum_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Bu yapıda dikkat çekici noktalar var. Kategoriler tablosu kendi kendine referans veriyor, böylece ana kategori ve alt kategori hiyerarşisi oluşturabiliyoruz. Yorumlar tablosu da aynı şekilde kendi kendine referans vererek iç içe yorum (thread) yapısı destekliyor. Yorum silindiğinde ON DELETE CASCADE sayesinde alt yorumlar da otomatik siliniyor.

Mevcut Tabloya FOREIGN KEY Ekleme

Çoğu zaman sıfırdan tablo oluşturmak yerine, var olan tablolara sonradan FOREIGN KEY eklemek gerekir. Bunu ALTER TABLE ile yapıyoruz:

-- Var olan bir tabloya FOREIGN KEY ekleme
ALTER TABLE siparisler
    ADD CONSTRAINT fk_siparis_musteri
    FOREIGN KEY (musteri_id)
    REFERENCES musteriler (musteri_id)
    ON DELETE RESTRICT
    ON UPDATE CASCADE;

-- Birden fazla FOREIGN KEY aynı anda eklenebilir
ALTER TABLE siparis_kalemleri
    ADD CONSTRAINT fk_kalem_siparis FOREIGN KEY (siparis_id)
        REFERENCES siparisler (siparis_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    ADD CONSTRAINT fk_kalem_urun FOREIGN KEY (urun_id)
        REFERENCES urunler (urun_id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE;

-- Var olan FOREIGN KEY kaldırma
ALTER TABLE siparisler
    DROP FOREIGN KEY fk_siparis_musteri;

-- FOREIGN KEY kaldırıp yeniden eklemek (değiştirmek için)
ALTER TABLE siparisler
    DROP FOREIGN KEY fk_siparis_musteri,
    ADD CONSTRAINT fk_siparis_musteri FOREIGN KEY (musteri_id)
        REFERENCES musteriler (musteri_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE;

FOREIGN KEY Bilgilerini Sorgulama

Hangi tablolarda hangi FOREIGN KEY’lerin tanımlı olduğunu görmek için birkaç yöntem kullanabilirsiniz:

-- Belirli bir tablonun FOREIGN KEY'lerini gösterme
SHOW CREATE TABLE siparislerG

-- information_schema üzerinden FOREIGN KEY sorgusu
SELECT
    kcu.CONSTRAINT_NAME,
    kcu.TABLE_NAME,
    kcu.COLUMN_NAME,
    kcu.REFERENCED_TABLE_NAME,
    kcu.REFERENCED_COLUMN_NAME,
    rc.UPDATE_RULE,
    rc.DELETE_RULE
FROM
    information_schema.KEY_COLUMN_USAGE kcu
    JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
        ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
        AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
WHERE
    kcu.TABLE_SCHEMA = 'veritabanim'
    AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY
    kcu.TABLE_NAME, kcu.CONSTRAINT_NAME;

-- Belirli bir tabloya referans veren tüm FOREIGN KEY'leri bulma
SELECT
    TABLE_NAME,
    CONSTRAINT_NAME,
    COLUMN_NAME
FROM
    information_schema.KEY_COLUMN_USAGE
WHERE
    CONSTRAINT_SCHEMA = 'veritabanim'
    AND REFERENCED_TABLE_NAME = 'musteriler';

Bu sorgular özellikle büyük projelerde veritabanı dokümantasyonu oluştururken veya bir tablonun neden silinip silinemediğini anlamaya çalışırken inanılmaz faydalı olur.

FOREIGN KEY Kontrolleri Devre Dışı Bırakma

Bazen toplu veri yükleme, yedek geri yükleme veya büyük migration işlemleri sırasında FOREIGN KEY kontrollerini geçici olarak kapatmak gerekir. Bu işlemi dikkatli yapmanız gerektiğini vurgulayalım, verilerinizin tutarlılığını bozmadan yapın:

-- FOREIGN KEY kontrollerini devre dışı bırak
SET FOREIGN_KEY_CHECKS = 0;

-- Yedek dosyasını içe aktar veya büyük veri yükle
LOAD DATA INFILE '/tmp/musteriler.csv'
INTO TABLE musteriler
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY 'n'
IGNORE 1 ROWS;

-- Kontrolleri yeniden etkinleştir
SET FOREIGN_KEY_CHECKS = 1;

-- Etkinleştirdikten sonra tutarsızlık kontrolü yapılabilir
SELECT s.siparis_id, s.musteri_id
FROM siparisler s
LEFT JOIN musteriler m ON s.musteri_id = m.musteri_id
WHERE m.musteri_id IS NULL;

Önemli uyarı: SET FOREIGN_KEY_CHECKS = 0 yaptıktan sonra mutlaka tutarsız veri olup olmadığını kontrol edin. Aksi takdirde fark etmeden referanssal bütünlüğü bozmuş olabilirsiniz.

Gerçek Dünya Senaryo 2: Çok-Çoğa İlişki (Many-to-Many)

Ürünler ve etiketler arasındaki ilişki klasik bir çok-çoğa ilişki örneğidir. Bir ürünün birden fazla etiketi olabilir, bir etiket de birden fazla üründe kullanılabilir. Bunu bağlantı tablosu (junction table) ile modelleriz:

-- Ürünler tablosu
CREATE TABLE urunler (
    urun_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    urun_kodu VARCHAR(50) UNIQUE NOT NULL,
    urun_adi VARCHAR(200) NOT NULL,
    fiyat DECIMAL(10,2) NOT NULL,
    stok_adedi INT UNSIGNED DEFAULT 0,
    aktif TINYINT(1) DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Etiketler tablosu
CREATE TABLE etiketler (
    etiket_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    etiket_adi VARCHAR(80) UNIQUE NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Bağlantı tablosu (junction table)
CREATE TABLE urun_etiketler (
    urun_id INT UNSIGNED NOT NULL,
    etiket_id INT UNSIGNED NOT NULL,
    ekleme_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (urun_id, etiket_id),
    CONSTRAINT fk_urunetiket_urun FOREIGN KEY (urun_id)
        REFERENCES urunler (urun_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    CONSTRAINT fk_urunetiket_etiket FOREIGN KEY (etiket_id)
        REFERENCES etiketler (etiket_id)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Bu yapıyı kullanarak örnek sorgu
-- Belirli bir ürünün tüm etiketleri
SELECT u.urun_adi, e.etiket_adi
FROM urunler u
JOIN urun_etiketler ue ON u.urun_id = ue.urun_id
JOIN etiketler e ON ue.etiket_id = e.etiket_id
WHERE u.urun_id = 1;

-- Belirli bir etiketle işaretlenmiş tüm ürünler
SELECT u.urun_adi, u.fiyat
FROM etiketler e
JOIN urun_etiketler ue ON e.etiket_id = ue.etiket_id
JOIN urunler u ON ue.urun_id = u.urun_id
WHERE e.slug = 'elektronik'
AND u.aktif = 1;

Sık Karşılaşılan Hatalar ve Çözümleri

FOREIGN KEY tanımlarken en sık karşılaşılan hataları ve çözümlerini şöyle özetleyebilirim:

Hata 1150: Table engine does not support foreign keys

Bu hata MyISAM tablosu kullandığınızda çıkar. Tabloyu InnoDB’ye dönüştürmeniz gerekir:

-- Tabloyu InnoDB'ye dönüştürme
ALTER TABLE tablo_adi ENGINE=InnoDB;

-- Tüm tablolarınızı kontrol etmek için
SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'veritabanim'
AND ENGINE != 'InnoDB';

Hata 1452: Cannot add or update a child row: a foreign key constraint fails

Bu hata, FOREIGN KEY sütununa koyduğunuz değer ana tabloda bulunmuyorsa alınır:

-- Tutarsız verileri tespit etme
SELECT DISTINCT s.musteri_id
FROM siparisler s
WHERE NOT EXISTS (
    SELECT 1 FROM musteriler m
    WHERE m.musteri_id = s.musteri_id
);

-- Düzeltme: Önce ana tabloya kayıt ekle, sonra bağımlı tabloya ekle
INSERT INTO musteriler (ad, soyad, email) VALUES ('Ahmet', 'Yilmaz', '[email protected]');
INSERT INTO siparisler (musteri_id, toplam_tutar) VALUES (LAST_INSERT_ID(), 250.00);

Hata 1215: Cannot add foreign key constraint

Bu genellikle veri tipi uyumsuzluğundan kaynaklanır:

-- Veri tiplerini kontrol et
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'veritabanim'
AND TABLE_NAME = 'siparisler'
AND COLUMN_NAME = 'musteri_id';

-- Her iki sütun da aynı tipte, işaretli/işaretsiz olması dahil olmalı
-- INT ve INT UNSIGNED farklı tiplerdir, dikkat!

FOREIGN KEY Performans Etkileri

FOREIGN KEY’lerin performansına dair birkaç önemli noktayı sysadmin gözüyle ele alalım:

  • İndeks zorunluluğu: MariaDB ve MySQL, FOREIGN KEY sütunlarına otomatik olarak indeks ekler. Bu JOIN sorgularını hızlandırır.
  • Yazma performansı: Her INSERT ve UPDATE işleminde veritabanı ek kontrol yapar. Yoğun yazma işlemlerinde bu fark edilebilir.
  • Silme performansı: CASCADE silme işlemleri, binlerce kaydı etkiliyor olabilir. Büyük tablolarda dikkatli olun.
  • Toplu yükleme: Büyük veri yüklemelerinden önce SET FOREIGN_KEY_CHECKS = 0 yaparak performans kazanabilirsiniz.
-- FOREIGN KEY için manuel indeks ekleme (otomatik eklenmiyorsa)
ALTER TABLE siparisler
    ADD INDEX idx_musteri_id (musteri_id);

-- EXPLAIN ile sorgu planını kontrol etme
EXPLAIN SELECT s.siparis_id, m.ad, m.soyad, s.toplam_tutar
FROM siparisler s
JOIN musteriler m ON s.musteri_id = m.musteri_id
WHERE s.durum = 'beklemede';

FOREIGN KEY ile Transaction Kullanımı

FOREIGN KEY kısıtlamalarıyla çalışırken transaction kullanımı kritik öneme sahiptir. İlişkili kayıtları atomik olarak eklemek veya silmek için mutlaka transaction kullanın:

-- Transaction ile güvenli kayıt ekleme
START TRANSACTION;

INSERT INTO musteriler (ad, soyad, email, telefon)
VALUES ('Zeynep', 'Kaya', '[email protected]', '0532-111-2233');

SET @yeni_musteri_id = LAST_INSERT_ID();

INSERT INTO siparisler (musteri_id, toplam_tutar, durum)
VALUES (@yeni_musteri_id, 1500.00, 'onaylandi');

SET @yeni_siparis_id = LAST_INSERT_ID();

INSERT INTO siparis_kalemleri (siparis_id, urun_id, adet, birim_fiyat)
VALUES
    (@yeni_siparis_id, 101, 2, 500.00),
    (@yeni_siparis_id, 205, 1, 500.00);

-- Her şey tamam, işlemi onayla
COMMIT;

-- Hata durumunda geri al
-- ROLLBACK;

Bu yaklaşım özellikle web uygulamalarında sipariş, ödeme, stok güncelleme gibi birden fazla tabloya aynı anda yazmayı gerektiren senaryolarda hayat kurtarır.

MariaDB ile MySQL Arasındaki Farklar

FOREIGN KEY konusunda iki veritabanı arasındaki önemli farklara değinelim:

  • SET DEFAULT aksiyonu: MariaDB’de desteklenir, MySQL 8.0’da da resmi destek geldi ancak bazı depolama motorlarıyla sınırlı çalışabilir.
  • Sanal sütunlar üzerinde FOREIGN KEY: MariaDB daha esnek bir destek sunar.
  • FOREIGN KEY metadata: MariaDB’nin information_schema implementasyonu bazı durumlarda daha fazla bilgi sunar.
  • Deferred constraint checking: MariaDB bazı versiyonlarında gecikmeli kısıtlama denetimi için seçenekler sunabilir.

Genel olarak söz dizimi ve davranış büyük ölçüde aynıdır, migration yaparken sorun yaşamak pek mümkün değil.

Sonuç

FOREIGN KEY ilişkileri, veritabanı tasarımının temel taşlarından biridir. Doğru kullanıldığında uygulamanızın veri katmanını çok daha sağlam hale getirir. Yanlış kullanıldığında ise başınızı ağrıtabilir.

Pratik önerileri şöyle özetleyebilirim: Her FOREIGN KEY için anlamlı bir CONSTRAINT ismi koyun, böylece hata mesajlarını okumak kolaylaşır. ON DELETE ve ON UPDATE aksiyonlarını tasarım aşamasında dikkatlice düşünün, sonradan değiştirmek zahmetli olabilir. Büyük veri migrasyonlarında FOREIGN KEY kontrollerini geçici kapatın ama sonrasında mutlaka tutarsızlık taraması yapın. Transaction kullanımını alışkanlık haline getirin, özellikle çok tablolu yazma operasyonlarında. Son olarak, information_schema sorgularını araç kutunuzda hazır tutun; bir tablonun neden silinmediğini anlamak için bunları sık kullanırsınız.

Veritabanı tasarımında kestirme yoldan gidip FOREIGN KEY’leri atlamak kısa vadede cazip görünebilir, ama uzun vadede veri tutarsızlıklarıyla boğuşmak çok daha maliyetlidir. InnoDB ile gelen bu güçlü özelliği, başlangıçtan itibaren tasarımınıza dahil edin.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir