MySQL’de Foreign Key Kısıtlaması Hataları ve Çözümleri

Veritabanı yönetiminde en sinir bozucu anlardan biri, bir INSERT veya DELETE işlemi yaparken ekrana “Cannot add or update a child row: a foreign key constraint fails” hatasının düşmesidir. Özellikle production ortamında bu hatayı gördüğünde, ne yapacağını bilmeden panikleyebilirsin. Bu yazıda MySQL’de foreign key kısıtlama hatalarını derinlemesine inceleyeceğiz, neden oluştuklarını anlayacağız ve gerçek dünya senaryolarıyla nasıl çözüleceğini göreceğiz.

Foreign Key Kısıtlaması Nedir ve Neden Önemlidir?

Foreign key (yabancı anahtar), iki tablo arasındaki ilişkiyi tanımlayan bir veritabanı kısıtlamasıdır. Bir tablodaki sütunun değerinin, başka bir tablodaki birincil anahtara (primary key) referans vermesini zorunlu kılar. Bu mekanizma, veri bütünlüğünü korur. Yani orphan (öksüz) kayıtların oluşmasını engeller.

Örneğin bir e-ticaret sisteminde orders tablosundaki her siparişin geçerli bir customer_id içermesi gerekir. Eğer o müşteri yoksa sipariş kaydının da olmaması lazım. İşte foreign key tam olarak bunu denetler.

Ancak bu koruyucu mekanizma, bazen senin düşmanın da olabilir. Özellikle veri migration, toplu silme işlemleri veya legacy sistemlerle entegrasyon sırasında bu kısıtlamalar seni durdurabilir.

Hata Türleri ve Anlamları

MySQL’de foreign key ile ilgili karşılaşacağın temel hata mesajları şunlardır:

  • Error 1451: “Cannot delete or update a parent row: a foreign key constraint fails” – Parent tablodan silme veya güncelleme yapmaya çalışırken child kayıtlar mevcut.
  • Error 1452: “Cannot add or update a child row: a foreign key constraint fails” – Child tabloya ekleme yaparken parent’ta karşılık gelen kayıt yok.
  • Error 1215: “Cannot add foreign key constraint” – Foreign key tanımı oluştururken syntax veya tip uyumsuzluğu var.
  • Error 1005: “Can’t create table (errno: 150)” – Tablo oluşturma sırasında foreign key kısıtlaması eklenemedi.

Hataları Reproducing Etmek: Örnek Senaryo

Önce basit bir senaryo üzerinden hataların nasıl oluştuğunu görelim. Bir sipariş yönetim sistemi kuralım:

mysql -u root -p

CREATE DATABASE order_system;
USE order_system;

CREATE TABLE customers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE NOT NULL
);

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    total_amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE
);

Şimdi bu yapıyla hataları tetikleyelim:

-- Önce müşteri ekleyelim
INSERT INTO customers (name, email) VALUES ('Ahmet Yilmaz', '[email protected]');
INSERT INTO customers (name, email) VALUES ('Fatma Kaya', '[email protected]');

-- Var olmayan müşteriye sipariş eklemeye çalışalım (Error 1452)
INSERT INTO orders (customer_id, total_amount) VALUES (999, 150.00);
-- ERROR 1452: Cannot add or update a child row

-- Önce sipariş ekleyelim, sonra müşteriyi silmeye çalışalım (Error 1451)
INSERT INTO orders (customer_id, total_amount) VALUES (1, 250.00);
DELETE FROM customers WHERE id = 1;
-- ERROR 1451: Cannot delete or update a parent row

Bu iki hata, günlük operasyonlarda en sık karşılaşılan senaryolardır.

Hata Ayıklama Adımları

Mevcut Foreign Key İlişkilerini Keşfetmek

Bir hatayla karşılaştığında ilk yapman gereken şey, tablolar arasındaki ilişkileri anlamaktır. information_schema bu noktada en iyi arkadaşın olur:

-- Belirli bir tablonun tüm foreign key'lerini listele
SELECT
    CONSTRAINT_NAME,
    TABLE_NAME,
    COLUMN_NAME,
    REFERENCED_TABLE_NAME,
    REFERENCED_COLUMN_NAME
FROM
    information_schema.KEY_COLUMN_USAGE
WHERE
    REFERENCED_TABLE_SCHEMA = 'order_system'
    AND TABLE_NAME = 'orders';

-- Veya tüm veritabanındaki foreign key ilişkilerini gör
SELECT
    TABLE_NAME,
    COLUMN_NAME,
    CONSTRAINT_NAME,
    REFERENCED_TABLE_NAME,
    REFERENCED_COLUMN_NAME
FROM
    information_schema.KEY_COLUMN_USAGE
WHERE
    REFERENCED_TABLE_SCHEMA = DATABASE()
    AND REFERENCED_TABLE_NAME IS NOT NULL
ORDER BY TABLE_NAME;

Bu sorgu sana hangi tablonun neyi referans aldığını net biçimde gösterir. Production’da karmaşık bir şema varsa bu sorgu olmadan hata ayıklamak gerçekten zor olur.

Orphan Kayıtları Tespit Etmek

Error 1452 alıyorsan, child tabloda parent’ta karşılığı olmayan kayıtlar var demektir. Bunları bulmak için LEFT JOIN kullanabilirsin:

-- Parent'ta karşılığı olmayan child kayıtları bul
SELECT o.id, o.customer_id, o.total_amount
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

-- Daha kapsamlı bir kontrol için
SELECT
    COUNT(*) as orphan_count,
    GROUP_CONCAT(o.customer_id) as missing_customer_ids
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

Bu sorgu sana kaç orphan kaydın olduğunu ve hangi customer_id değerlerinin eksik olduğunu gösterir.

Gerçek Dünya Senaryosu: Veri Migration Sorunu

Diyelim ki eski bir sistemden yeni bir MySQL veritabanına veri aktarıyorsun. CSV dosyalarından toplu import yapıyorsun ve sürekli Error 1452 alıyorsun. Bu durumda yapman gerekenler:

-- Geçici olarak foreign key kontrollerini kapat (DİKKATLİ KULLAN!)
SET FOREIGN_KEY_CHECKS = 0;

-- Önce parent tabloyu doldur
LOAD DATA INFILE '/tmp/customers.csv'
INTO TABLE customers
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY 'n'
IGNORE 1 ROWS
(id, name, email);

-- Sonra child tabloyu doldur
LOAD DATA INFILE '/tmp/orders.csv'
INTO TABLE orders
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY 'n'
IGNORE 1 ROWS
(id, customer_id, total_amount, created_at);

-- Foreign key kontrollerini tekrar aç
SET FOREIGN_KEY_CHECKS = 1;

-- Orphan kayıt kontrolü yap
SELECT COUNT(*) as orphan_orders
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;

Önemli uyarı: FOREIGN_KEY_CHECKS = 0 ayarı çok güçlü bir silahtır. Bunu kullandıktan sonra mutlaka veri bütünlüğünü manuel olarak kontrol etmelisin. Aksi takdirde veritabanında tutarsız veriler kalabilir.

ON DELETE ve ON UPDATE Aksiyonlarını Anlamak

Foreign key tanımlarındaki aksiyonlar çok kritiktir ve yanlış seçildiğinde hem hatalara hem de beklenmedik veri kayıplarına yol açabilir:

  • RESTRICT: Parent kayıt silinemez veya güncellenemez, eğer child kayıt varsa. Bu varsayılan davranıştır.
  • CASCADE: Parent silinince child kayıtlar da otomatik silinir. Güncelleme de cascade edilir.
  • SET NULL: Parent silinince child’daki foreign key sütunu NULL olur. Sütunun NULL’a izin vermesi gerekir.
  • NO ACTION: RESTRICT ile benzerdir, ancak kontrol transaction sonunda yapılır.
  • SET DEFAULT: Pek kullanılmaz, child sütun varsayılan değere ayarlanır.

Yanlış senaryo örneği:

-- CASCADE kullanıyorsun ama yanlış yerde
-- Bu şema bir müşteri silinince tüm siparişleri de siler!
CREATE TABLE orders_dangerous (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT NOT NULL,
    total_amount DECIMAL(10,2),
    FOREIGN KEY (customer_id) REFERENCES customers(id)
        ON DELETE CASCADE  -- DİKKAT: Müşteri silinince siparişler de gider!
);

-- Daha güvenli yaklaşım: SET NULL kullan
ALTER TABLE orders_dangerous
DROP FOREIGN KEY orders_dangerous_ibfk_1;

ALTER TABLE orders_dangerous
MODIFY COLUMN customer_id INT NULL,
ADD CONSTRAINT fk_order_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE SET NULL
ON UPDATE CASCADE;

Foreign Key Kısıtlamasını Geçici Olarak Devre Dışı Bırakma

Bazen özellikle backup restore işlemlerinde veya büyük toplu güncellemelerde kısıtlamaları geçici olarak kapatman gerekebilir:

-- Session bazlı kapatma (sadece bu bağlantı için geçerli)
SET SESSION FOREIGN_KEY_CHECKS = 0;

-- Toplu silme işlemi yap
DELETE FROM orders WHERE created_at < '2020-01-01';
DELETE FROM customers WHERE id NOT IN (SELECT DISTINCT customer_id FROM orders);

-- Tekrar aç
SET SESSION FOREIGN_KEY_CHECKS = 1;

-- Global olarak kapatma (tüm bağlantılar için - production'da çok dikkatli!)
-- SET GLOBAL FOREIGN_KEY_CHECKS = 0;

Bu işlemi script olarak kullanıyorsan, bir transaction içine almak daha güvenli olur:

START TRANSACTION;
SET FOREIGN_KEY_CHECKS = 0;

-- İşlemlerini yap
UPDATE orders SET customer_id = 2 WHERE customer_id = 1;
DELETE FROM customers WHERE id = 1;

SET FOREIGN_KEY_CHECKS = 1;
COMMIT;

Error 1215: Foreign Key Oluşturulurken Karşılaşılan Hatalar

Bu hata genellikle tablo oluştururken veya ALTER TABLE yaparken gelir. En yaygın nedenleri:

  • Veri tipi uyumsuzluğu: Parent’ta INT, child’da BIGINT kullanmak
  • İşaretsiz (UNSIGNED) uyumsuzluğu: Birinde INT UNSIGNED, diğerinde INT
  • Karakter seti uyumsuzluğu: Parent utf8mb4, child utf8
  • Referans edilen sütunun INDEX’i yok
-- Hata veren örnek: tip uyumsuzluğu
CREATE TABLE departments (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);

-- Bu hata verecek! employees.dept_id SIGNED ama departments.id UNSIGNED
CREATE TABLE employees (
    id INT AUTO_INCREMENT PRIMARY KEY,
    dept_id INT NOT NULL,  -- UNSIGNED eksik!
    name VARCHAR(100),
    FOREIGN KEY (dept_id) REFERENCES departments(id)
);

-- Doğru kullanım
CREATE TABLE employees (
    id INT AUTO_INCREMENT PRIMARY KEY,
    dept_id INT UNSIGNED NOT NULL,  -- departments.id ile eşleşiyor
    name VARCHAR(100),
    FOREIGN KEY (dept_id) REFERENCES departments(id)
);

-- Mevcut tabloda tip kontrolü
SHOW CREATE TABLE departments;
SHOW CREATE TABLE employees;

Karakter seti uyumsuzluğunu tespit etmek ve düzeltmek için:

-- Tablo karakter setlerini kontrol et
SELECT
    TABLE_NAME,
    TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE();

-- Sütun karakter setlerini kontrol et
SELECT
    TABLE_NAME,
    COLUMN_NAME,
    CHARACTER_SET_NAME,
    COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND DATA_TYPE IN ('varchar', 'char', 'text');

-- Uyumsuzluğu düzelt
ALTER TABLE employees CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE departments CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Performans Etkisi ve İndexleme

Foreign key kısıtlamaları her yazma işleminde arka planda bir SELECT yaparak veri bütünlüğünü kontrol eder. Bu, yüksek trafikte ciddi performans sorunlarına yol açabilir. Özellikle child tabloda foreign key sütunu index’lenmemişse her parent silme/güncelleme işleminde full table scan yapılır.

-- Foreign key sütunlarının index'lenip index'lenmediğini kontrol et
SELECT
    TABLE_NAME,
    COLUMN_NAME,
    INDEX_NAME
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, COLUMN_NAME;

-- Eksik index'i ekle
ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);

-- Index varlığını doğrula
EXPLAIN SELECT * FROM orders WHERE customer_id = 1;

-- Performans testini EXPLAIN ile görmek
EXPLAIN DELETE FROM customers WHERE id = 1;

MySQL, foreign key olan sütunları otomatik index’ler ama bazen bu davranış beklenmedik sonuçlar doğurabilir. Özellikle composite foreign key’lerde index sırası önemlidir.

Büyük Tablolarda Foreign Key Yönetimi

Production’da milyonlarca kayıt içeren tablolarda foreign key operasyonları çok dikkat ister. Yanlış bir işlem hem tabloyu kilitleyebilir hem de servis kesintisine yol açabilir:

-- Büyük tablodan güvenli silme (batch işlem)
DELIMITER //
CREATE PROCEDURE safe_delete_customers(IN batch_size INT)
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE rows_affected INT;

    REPEAT
        -- Önce silinecek customer'ların siparişlerini sil
        DELETE FROM orders
        WHERE customer_id IN (
            SELECT id FROM customers
            WHERE status = 'inactive'
            LIMIT batch_size
        );

        -- Sonra customer'ları sil
        DELETE FROM customers
        WHERE status = 'inactive'
        AND id NOT IN (SELECT DISTINCT customer_id FROM orders)
        LIMIT batch_size;

        SET rows_affected = ROW_COUNT();

        -- Her batch sonrası kısa bekle (diğer query'lere nefes al)
        DO SLEEP(0.1);

    UNTIL rows_affected = 0 END REPEAT;
END //
DELIMITER ;

-- Prosedürü çalıştır
CALL safe_delete_customers(1000);

MySQL 8.0’da Foreign Key Metadata İyileştirmeleri

MySQL 8.0 ile birlikte information_schema‘dan foreign key bilgisi almak daha kolay hale geldi:

-- MySQL 8.0+ için geliştirilmiş sorgular
SELECT
    kcu.CONSTRAINT_NAME,
    kcu.TABLE_NAME as child_table,
    kcu.COLUMN_NAME as child_column,
    kcu.REFERENCED_TABLE_NAME as parent_table,
    kcu.REFERENCED_COLUMN_NAME as parent_column,
    rc.DELETE_RULE,
    rc.UPDATE_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_CATALOG
WHERE kcu.REFERENCED_TABLE_SCHEMA = DATABASE()
ORDER BY kcu.TABLE_NAME;

-- Constraint detaylarını göster
SELECT * FROM information_schema.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE();

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

Production deneyimimden derlediğim yaygın tuzaklar:

  • MyISAM tabloları: Foreign key sadece InnoDB’de çalışır. Tablonun engine’ini kontrol et: SHOW TABLE STATUS WHERE Name = 'orders';
  • NULL değerler: Foreign key sütunu NULL içeriyorsa MySQL bu kontrolü geçer. Bu bazen beklenmedik davranışlara yol açar.
  • Circular referanslar: İki tablonun birbirini referans etmesi deadlock’a neden olabilir.
  • Backup/restore sırası: mysqldump ile dump alırken --single-transaction ve --routines kullan, restore sırasında SET FOREIGN_KEY_CHECKS=0 ekle.
# Güvenli mysqldump
mysqldump -u root -p 
    --single-transaction 
    --add-drop-table 
    --disable-keys 
    order_system > order_system_backup.sql

# Restore sırasında foreign key sorunlarını önlemek için
# dump dosyasının başında şu satırı kontrol et:
head -20 order_system_backup.sql
# mysqldump otomatik olarak SET FOREIGN_KEY_CHECKS=0 ekler

# Manuel restore
mysql -u root -p order_system < order_system_backup.sql

Sonuç

Foreign key kısıtlama hataları ilk bakışta korkutucu görünse de aslında veritabanının sana “dur, bir şeyler yanlış” dediği anlardır. Bu sinyalleri doğru okumak, verinin tutarlılığını korumakla doğrudan ilişkilidir.

Özetlemek gerekirse:

  • Hata mesajını gördüğünde önce information_schema ile ilişki haritasını çıkar.
  • Orphan kayıtları LEFT JOIN sorgusuyla tespit et.
  • FOREIGN_KEY_CHECKS = 0 güçlü ama tehlikeli bir araçtır, kullandıktan sonra mutlaka bütünlük kontrolü yap.
  • Foreign key sütunlarının her zaman index’lendiğinden emin ol, aksi halde performans sorunları kaçınılmaz olur.
  • Büyük tablolarda toplu işlemleri batch’ler halinde yap ve lock sürelerini minimize et.
  • ON DELETE ve ON UPDATE aksiyonlarını uygulamanın ihtiyaçlarına göre dikkatli seç, CASCADE yanlış yerde veri kaybına neden olabilir.

Veritabanı yönetiminde “hızlı çözüm” olarak foreign key kontrollerini kapatmak yerine, sorunun köküne inmek her zaman daha sağlıklı bir yaklaşımdır. Bugün kapatıp geçtiğin bir kısıtlama, yarın sana çok daha büyük bir baş ağrısı olarak geri dönebilir.

Bir yanıt yazın

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