MariaDB ve MySQL’de UNIQUE Kısıtlaması ile Benzersiz Alan Tanımlama

Veritabanı tasarımında en çok göz ardı edilen ama en çok pişmanlık duyulan konulardan biri benzersizlik kısıtlamalarıdır. “Uygulama tarafında hallederiz” diye düşünülür, aylar sonra production veritabanında binlerce duplicate kayıt bulunur ve temizleme operasyonu başlar. MySQL ve MariaDB’de UNIQUE kısıtlaması tam da bu noktada devreye girer: veri bütünlüğünü veritabanı seviyesinde garanti altına alır, uygulama katmanındaki hatalardan bağımsız olarak çalışır.

UNIQUE Kısıtlaması Nedir?

UNIQUE kısıtlaması, bir sütun ya da sütun kombinasyonunun tablodaki tüm satırlar arasında benzersiz değerler içermesini zorunlu kılar. Birincil anahtar (PRIMARY KEY) ile benzer bir işlev görür, ancak önemli bir fark vardır: PRIMARY KEY NULL değer kabul etmezken UNIQUE kısıtlaması NULL değerlere izin verir. Hatta MySQL ve MariaDB’de birden fazla satır NULL değeri taşıyabilir çünkü NULL, NULL’a eşit değildir mantığı bu motorlarda geçerlidir.

Bir tabloda yalnızca bir PRIMARY KEY tanımlayabilirsiniz, ancak istediğiniz kadar UNIQUE kısıtlaması ekleyebilirsiniz. Bu esneklik, e-posta adresi, kullanıcı adı, TC kimlik numarası, sipariş numarası gibi birden fazla “doğal anahtar” içeren tablolarda son derece işe yarar.

Neden Uygulama Katmanında Kontrol Yetmez?

Pek çok geliştirici “zaten INSERT yapmadan önce SELECT ile kontrol ediyorum, ne gerek var UNIQUE’e?” der. Bu yaklaşımın tehlikesi race condition sorunudur. İki istek eş zamanlı geldiğinde, her ikisi de SELECT sorgusunu çalıştırır, ikisi de kaydın olmadığını görür ve ikisi de INSERT yapar. Sonuç: duplicate kayıt.

Yüksek trafikli bir sistemde bu senaryo nadir değil, oldukça yaygındır. UNIQUE kısıtlaması ise bu kontrolü transaction seviyesinde, atomik olarak yapar. Veritabanı motoru kilit mekanizmasını devreye alır ve yarış koşulunu engeller.

Temel Kullanım: Tablo Oluştururken UNIQUE Tanımlamak

En yaygın kullanım şekli tablo oluştururken UNIQUE kısıtlamasını tanımlamaktır.

CREATE TABLE kullanicilar (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    kullanici_adi VARCHAR(50)  NOT NULL,
    email       VARCHAR(100) NOT NULL,
    telefon     VARCHAR(20),
    created_at  DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_kullanici_adi (kullanici_adi),
    UNIQUE KEY uk_email (email)
);

Burada iki ayrı UNIQUE kısıtlaması tanımladık. uk_kullanici_adi ve uk_email isimlendirmeleri tamamen bizim tercihimiz; MySQL/MariaDB isimsiz bırakırsanız sütun adını kullanır. Kısıtlamalara anlamlı isimler vermek ilerleyen süreçte yönetimi kolaylaştırır.

Sütun tanımının hemen yanına da koyabilirsiniz:

CREATE TABLE urunler (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    barkod      VARCHAR(30) NOT NULL UNIQUE,
    sku         VARCHAR(50) NOT NULL UNIQUE,
    urun_adi    VARCHAR(200) NOT NULL,
    stok        INT DEFAULT 0
);

Bu yöntem daha kısa görünür ama kısıtlamaya özel bir isim veremezsiniz. Büyük projelerde kısıtlama isimlerinin tutarlı olması hata mesajlarını yorumlamayı kolaylaştırdığından ilk yöntemi tercih etmenizi öneririm.

Var Olan Tabloya UNIQUE Kısıtlaması Eklemek

Production ortamında çalışan bir tabloya UNIQUE eklemek biraz daha dikkat gerektirir. Öncelikle mevcut veride duplicate kayıt var mı kontrol etmelisiniz, yoksa ALTER TABLE komutu hata verir.

Önce duplicate kontrolü:

-- email sutununda duplicate var mi kontrol et
SELECT email, COUNT(*) as adet
FROM kullanicilar
GROUP BY email
HAVING COUNT(*) > 1;

-- Sonuc bos gelirse duplicate yok demektir
-- Eger kayit gelirse once bunlari temizlemeniz gerekir

Temiz olduğunu doğruladıktan sonra kısıtlamayı ekleyin:

-- ALTER TABLE ile UNIQUE INDEX ekle
ALTER TABLE kullanicilar
ADD UNIQUE INDEX uk_email (email);

-- Birden fazla kisitlamayi tek seferde de ekleyebilirsiniz
ALTER TABLE siparisler
ADD UNIQUE INDEX uk_siparis_no (siparis_no),
ADD UNIQUE INDEX uk_fatura_no (fatura_no);

Büyük tablolarda ALTER TABLE işlemi tablo kilitlemesine yol açabilir. MariaDB’de ALGORITHM=INPLACE ve LOCK=NONE seçeneklerini kullanarak bunu minimize edebilirsiniz:

ALTER TABLE kullanicilar
ADD UNIQUE INDEX uk_email (email),
ALGORITHM=INPLACE,
LOCK=NONE;

Bileşik UNIQUE Kısıtlaması: Composite Unique

Gerçek dünya senaryolarında tek sütun değil, sütun kombinasyonunun benzersiz olması gerektiği durumlar çok daha yaygındır. Örneğin bir kullanıcı aynı ürünü favorilerine yalnızca bir kez ekleyebilir. Burada user_id ve product_id’nin kombinasyonu benzersiz olmalıdır, ayrı ayrı değil.

CREATE TABLE favoriler (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    kullanici_id INT NOT NULL,
    urun_id     INT NOT NULL,
    ekleme_tarihi DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_kullanici_urun (kullanici_id, urun_id),
    FOREIGN KEY (kullanici_id) REFERENCES kullanicilar(id) ON DELETE CASCADE,
    FOREIGN KEY (urun_id) REFERENCES urunler(id) ON DELETE CASCADE
);

Bu sayede aynı kullanıcı aynı ürünü birden fazla kez eklemeye çalıştığında veritabanı hata verir. Aynı kullanıcı farklı ürünleri, farklı kullanıcılar aynı ürünü ekleyebilir; kısıtlama yalnızca kombinasyonu kontrol eder.

Şöyle bir senaryo daha düşünelim: bir şirkette her çalışan yalnızca bir departmanda birincil görevde bulunabilir, ancak aynı departmanda birden fazla çalışan çalışabilir.

CREATE TABLE departman_atamalari (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    calisan_id      INT NOT NULL,
    departman_id    INT NOT NULL,
    baslangic_tarihi DATE NOT NULL,
    aktif           TINYINT(1) DEFAULT 1,
    UNIQUE KEY uk_calisan_departman_aktif (calisan_id, departman_id, aktif)
);

INSERT Sırasında Duplicate Yönetimi

UNIQUE kısıtlaması olan bir tabloya duplicate değer eklemeye çalıştığınızda MySQL/MariaDB Error 1062: Duplicate entry hatası döner. Uygulamanız bu hatayı yakalayabilir. Ama bazen “kayıt yoksa ekle, varsa güncelle” mantığı gerekir. Bunun için iki yol vardır.

INSERT IGNORE: Duplicate durumunda sessizce atlama yapar, hata vermez.

-- Duplicate olursa hata yerine warning verir, satir eklenmez
INSERT IGNORE INTO favoriler (kullanici_id, urun_id)
VALUES (42, 187);

-- Birden fazla satir eklerken bazilari duplicate olsa bile devam eder
INSERT IGNORE INTO etiketler (icerik_id, etiket)
VALUES 
    (1, 'mysql'),
    (1, 'veritabani'),
    (1, 'mysql');  -- bu satir sessizce atlandi

ON DUPLICATE KEY UPDATE: Duplicate durumunda güncelleme yapar.

-- Kayit yoksa ekle, varsa updated_at guncelle
INSERT INTO kullanici_tercihleri (kullanici_id, tercih_adi, deger)
VALUES (42, 'tema', 'karanlik')
ON DUPLICATE KEY UPDATE 
    deger = VALUES(deger),
    updated_at = CURRENT_TIMESTAMP;

-- Sayac tutma ornegi: ziyaret sayisini artir
INSERT INTO sayfa_istatistikleri (sayfa_url, ziyaret_sayisi, ilk_ziyaret)
VALUES ('/hakkimizda', 1, NOW())
ON DUPLICATE KEY UPDATE 
    ziyaret_sayisi = ziyaret_sayisi + 1,
    son_ziyaret = NOW();

REPLACE INTO: Önce siler, sonra yeni kaydı ekler. PRIMARY KEY veya UNIQUE ihlali varsa eski kaydı tamamen silip yerine yenisini yazar. Auto-increment id değişeceğinden ve foreign key cascade etkileri olacağından dikkatli kullanın.

-- Eski kaydi siler, yenisini ekler (id degisir!)
REPLACE INTO kullanici_oturum_bilgileri (kullanici_id, token, son_aktif)
VALUES (42, 'abc123xyz', NOW());

UNIQUE Kısıtlamasını Kaldırmak

Kısıtlamayı isimsiz oluştursanız bile MySQL bir index adı atar. Önce adını bulmanız gerekir.

-- Tablodaki tum index bilgilerini goruntule
SHOW INDEX FROM kullanicilar;

-- ya da information_schema uzerinden
SELECT 
    INDEX_NAME,
    COLUMN_NAME,
    NON_UNIQUE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'veritabanim'
  AND TABLE_NAME = 'kullanicilar'
  AND NON_UNIQUE = 0;

Index adını öğrendikten sonra:

-- UNIQUE kisitlama aslinda bir index oldugu icin DROP INDEX ile kaldirilir
ALTER TABLE kullanicilar
DROP INDEX uk_email;

-- ya da INDEX yerine KEY de yazabilirsiniz, ikisi de ayni
ALTER TABLE kullanicilar
DROP KEY uk_kullanici_adi;

NULL Değerler ve UNIQUE Kısıtlaması

MySQL ve MariaDB’de NULL, NULL’a eşit değildir. Bu nedenle UNIQUE sütununda birden fazla satır NULL değeri taşıyabilir. Bu özellik bazen istenen, bazen beklenmeyen bir davranıştır.

CREATE TABLE teklifler (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    musteri_id  INT NOT NULL,
    proje_kodu  VARCHAR(50) UNIQUE,  -- NULL birden fazla satirda olabilir
    teklif_tutari DECIMAL(10,2)
);

-- Bu iki insert basarili olur, hata vermez
INSERT INTO teklifler (musteri_id, proje_kodu, teklif_tutari)
VALUES (1, NULL, 5000.00);

INSERT INTO teklifler (musteri_id, proje_kodu, teklif_tutari)
VALUES (2, NULL, 7500.00);

-- Ama ayni proje_kodu iki kez girilemez
INSERT INTO teklifler (musteri_id, proje_kodu, teklif_tutari)
VALUES (3, 'PRJ-2024-001', 3000.00);

INSERT INTO teklifler (musteri_id, proje_kodu, teklif_tutari)
VALUES (4, 'PRJ-2024-001', 4500.00);  -- ERROR 1062: Duplicate entry

Eğer NULL’ların da benzersiz olmasını istiyorsanız veya hiç NULL kabul etmek istemiyorsanız sütunu NOT NULL olarak tanımlayın.

Gerçek Dünya Senaryosu: E-Ticaret Veritabanı

Pratikte nasıl kullanıldığını görmek için küçük bir e-ticaret şeması inceleyelim.

CREATE TABLE musteriler (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    tc_kimlik       CHAR(11),
    email           VARCHAR(100) NOT NULL,
    telefon         VARCHAR(20),
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    -- TC kimlik bos olabilir ama doluysa benzersiz olmali
    UNIQUE KEY uk_tc_kimlik (tc_kimlik),
    UNIQUE KEY uk_email (email),
    UNIQUE KEY uk_telefon (telefon)
);

CREATE TABLE siparisler (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    siparis_no      VARCHAR(20) NOT NULL,
    musteri_id      INT NOT NULL,
    toplam_tutar    DECIMAL(10,2) NOT NULL,
    durum           ENUM('beklemede','onaylandi','kargolandi','teslim','iptal') DEFAULT 'beklemede',
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_siparis_no (siparis_no),
    FOREIGN KEY (musteri_id) REFERENCES musteriler(id)
);

CREATE TABLE siparis_urunler (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    siparis_id      INT NOT NULL,
    urun_id         INT NOT NULL,
    varyasyon_id    INT,
    adet            INT NOT NULL DEFAULT 1,
    birim_fiyat     DECIMAL(10,2) NOT NULL,
    -- Ayni siparis icinde ayni urun/varyasyon kombinasyonu bir kez olabilir
    UNIQUE KEY uk_siparis_urun_varyasyon (siparis_id, urun_id, varyasyon_id),
    FOREIGN KEY (siparis_id) REFERENCES siparisler(id) ON DELETE CASCADE
);

Bu şemada varyasyon_id NULL olabilir (varyasyonsuz ürün). Composite UNIQUE’deki NULL davranışı nedeniyle varyasyonsuz iki farklı ürün aynı siparişte sorunsuz yer alır. Ama aynı ürünün aynı varyasyonu aynı siparişe iki kez eklenemez.

UNIQUE Kısıtlaması ve Performans

UNIQUE kısıtlaması arka planda bir index oluşturur. Bu durumun iki yönü vardır:

Olumlu etki: O sütuna yapılan SELECT sorguları index sayesinde hızlanır. Email ile kullanıcı arama gibi işlemler çok daha hızlı sonuç döner.

Olumsuz etki: Her INSERT ve UPDATE işleminde index güncellenir. Çok fazla UNIQUE kısıtlaması olan tablolarda toplu veri yükleme yavaşlayabilir.

Büyük veri yüklemelerinde UNIQUE kontrollerini geçici olarak devre dışı bırakmak mümkündür, ancak bunu yalnızca verinin zaten temiz olduğundan emin olduğunuzda yapın:

-- Buyuk import oncesi unique check'i kapat (dikkatli kullanin!)
SET unique_checks = 0;
SET foreign_key_checks = 0;

-- Toplu INSERT islemleri buraya
LOAD DATA INFILE '/tmp/veri.csv' INTO TABLE urunler ...;

-- Bitince geri ac
SET unique_checks = 1;
SET foreign_key_checks = 1;

Hata Yönetimi ve Uygulama Entegrasyonu

UNIQUE ihlali hatası olan 1062 kodunu uygulamanızda özel olarak yakalayıp kullanıcıya anlamlı mesaj gösterebilirsiniz. Hata mesajında kısıtlamanın adı geçer, bu yüzden anlamlı isimler vermenin önemi burada da ortaya çıkar.

Örnek hata mesajı: Duplicate entry '[email protected]' for key 'uk_email'

Bu mesajda uk_email kısıtlama adı sayesinde hangi alanın duplicate olduğunu hemen anlıyorsunuz. Eğer kısıtlama adı email_2 ya da idx_1 gibi anlamsız bir şey olsaydı, büyük tablolarda hangi kısıtlamanın tetiklendiğini anlamak zaman alırdı.

Kısıtlama isimlerini standartlaştırmak için önerim:

  • uk_tablo_sutun: Tek sütun benzersizliği için (uk_users_email)
  • uk_tablo_sutun1_sutun2: Composite benzersizlik için (uk_orders_items_product_variant)

Mevcut UNIQUE Kısıtlamalarını Listelemek

Bir tablonun mevcut kısıtlamalarını görmek ve denetlemek için:

-- Tablonun yapisini ve kisitlamalarini goster
SHOW CREATE TABLE kullanicilarG

-- Tum unique index'leri listele
SELECT 
    TABLE_NAME,
    INDEX_NAME,
    GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS sutunlar
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
  AND NON_UNIQUE = 0
  AND INDEX_NAME != 'PRIMARY'
GROUP BY TABLE_NAME, INDEX_NAME
ORDER BY TABLE_NAME, INDEX_NAME;

Bu sorgu tüm veritabanındaki UNIQUE kısıtlamalarını bir bakışta görmenizi sağlar. Periyodik denetimde veya yeni bir projeyi devraldığınızda oldukça işe yarar.

Sık Yapılan Hatalar

  • Uygulama katmanına güvenmek: Daha önce belirttiğimiz race condition problemi. Veritabanı seviyesinde kısıtlama olmadan veri bütünlüğü garanti edilemez.
  • NULL davranışını göz ardı etmek: Özellikle composite UNIQUE’lerde NULL içeren satırların nasıl davrandığını test etmeden production’a almak beklenmedik sonuçlar doğurabilir.
  • Kısıtlamaları isimsiz bırakmak: Hata ayıklama sırasında ve kısıtlamayı kaldırmanız gerektiğinde isimli kısıtlamalar çok daha pratiktir.
  • Büyük tablolarda online DDL’i atlamak: ALTER TABLE işlemi sırasında tablo kilitlenebilir. Yoğun saatlerde bu işlemi yapmak servis kesintisine yol açabilir. MariaDB’de ALGORITHM=INPLACE, LOCK=NONE seçeneklerini veya Percona Online Schema Change aracını kullanın.
  • Duplicate temizliği yapmadan kısıtlama eklemeye çalışmak: Önce SELECT… GROUP BY… HAVING COUNT(*) > 1 sorgusuyla duplicate kontrol edin, temizleme yapın, sonra kısıtlamayı ekleyin.

Sonuç

UNIQUE kısıtlaması, veritabanı tasarımının en basit ama en güçlü araçlarından biridir. Doğru kullanıldığında uygulamanızdaki yüzlerce satır duplicate kontrol kodunun yerini alır, race condition problemlerini ortadan kaldırır ve veri bütünlüğünü veritabanı motorunun garantisi altına alır. Yanlış ya da eksik kullanıldığında ise duplicate verilerden kaynaklanan temizleme operasyonları, bozuk raporlar ve güvenilmez sistem davranışları kaçınılmaz olur.

Yeni bir tablo tasarlarken “bu sütunda aynı değer iki kez girilmemeli” diye düşündüğünüz her an bir UNIQUE kısıtlaması ekleyin. Var olan tablolarınızı gözden geçirin, hangi sütunlarda benzersizlik garantisi olması gerektiğini değerlendirin ve eksikleri tamamlayın. Kısıtlamalarınıza anlamlı isimler verin, NULL davranışını test edin ve büyük tablolarda online DDL seçeneklerini kullanın. Bu alışkanlıkları edindiğinizde veritabanı yönetimi çok daha öngörülebilir ve güvenilir bir hal alacaktır.

Bir yanıt yazın

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