Key'ler ve Primary Key Seçimi: Candidate Key, Natural ve Surrogate Key, Composite Key
Surrogate key natural key'in yerine geçmez, onun yükünü taşır: kimlik IDENTITY ya da GUID'de, iş kuralındaki benzersizlik UNIQUE constraint'te durur. Superkey'den candidate key'e zinciri, iyi bir primary key'in beş özelliğini, ISBN, e-posta ve kimlik numarasının key olarak tuzaklarını, composite foreign key'in yakaladığı kuralı ve maliyetini, fiziksel tip kararını ve PostgreSQL 18'in uuidv7() fonksiyonunu gösteriyoruz.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0, Cumulative Update 10) üzerinde, PostgreSQL notundaki örnek PostgreSQL 18.6 üzerinde çalıştırılmıştır.
Önce Tahmin Et: Primary Key Varken Aynı Kitap İki Kez Girer mi?
Serinin ilk yazısındaki requirement listesinde bu kural açıktı: iki kitap aynı ISBN'i (International Standard Book Number) taşıyamaz. Ekip books tablosunu "her tablonun bir id'si olur" alışkanlığıyla kuruyor ve primary key'i IDENTITY kolonuna veriyor. Katalog ekibi Dune'u kaydediyor; bir saat sonra başka bir ekran aynı kitabı tekrar kaydediyor. Tahmin et: İkinci INSERT hata verir mi?
CREATE TABLE dbo.books (
book_id INT IDENTITY(1,1) NOT NULL,
isbn CHAR(13) NOT NULL,
title NVARCHAR(200) NOT NULL,
list_price DECIMAL(10,2) NOT NULL,
CONSTRAINT PK_books PRIMARY KEY (book_id)
);
INSERT INTO dbo.books (isbn, title, list_price) VALUES ('9798900000268', N'Dune', 390.50);
INSERT INTO dbo.books (isbn, title, list_price) VALUES ('9798900000268', N'Dune', 390.50);
SELECT book_id, isbn, title FROM dbo.books;
(1 rows affected)
(1 rows affected)
book_id isbn title
------- ------------- -----
1 9798900000268 Dune
2 9798900000268 Dune
(2 rows affected)
Vermedi. Tabloda bir primary key var ve görevini yapıyor: 1 ile 2 farklı değerler. Ama primary key'in koruduğu şey, veritabanının kendi ürettiği bir sayı. İş kuralının söylediği "bir ISBN bir kez" kuralını hiçbir şey korumuyor. Bundan sonra stok iki satıra bölünecek, yorumların bir kısmı 1'e bir kısmı 2'ye yazılacak, satış raporunda Dune iki kez görünecek. Hatayı fark edip kuralı sonradan eklemeye çalıştığında ise:
ALTER TABLE dbo.books ADD CONSTRAINT UQ_books_isbn UNIQUE (isbn);
Msg 1505, Level 16, State 1, Server b8be943222fa, Line 1
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.books' and the index name 'UQ_books_isbn'. The duplicate key value is (9798900000268).
Msg 1750, Level 16, State 1, Server b8be943222fa, Line 1
Could not create constraint or index. See previous errors.
The statement has been terminated.
Constraint eklenemiyor, çünkü veri zaten kuralı ihlal ediyor. Önce tekrarları bulman, hangisini tutacağına karar vermen, bağlı satırları (sipariş kalemleri, yorumlar, stok) tutulan id'ye taşıman ve ancak ondan sonra constraint'i eklemen gerekiyor:
SELECT isbn, COUNT(*) AS copies, MIN(book_id) AS keep_book_id
FROM dbo.books
GROUP BY isbn
HAVING COUNT(*) > 1;
isbn copies keep_book_id
------------- ------ ------------
9798900000268 2 1
(1 rows affected)
Bu yazı, key'leri konuşuyor: superkey'den candidate key'e, natural key'den surrogate key'e ve composite key'e uzanan zincir, "doğal" görünen key adaylarının tuzakları ve fiziksel tip kararı.
Superkey, Candidate Key, Primary Key ve Alternate Key
Bir tablodaki satırları tek başına ayırt edebilen her attribute kümesi bir superkey'dir. customers tablosunda {customer_id} bir superkey, {email} bir superkey, {customer_id, email} de bir superkey, çünkü bir superkey'e attribute eklemek onu ayırt edici olmaktan çıkarmaz. Ama son küme gereğinden büyük: içinden email'i atınca hâlâ ayırt edici kalıyor.
Candidate key, minimal superkey'dir: içinden herhangi bir attribute'u çıkardığında artık ayırt edici olmaz. customers için iki candidate key var: {customer_id} ve {email}. Bunlardan birini primary key olarak seçersin; seçilmeyen candidate key'lere alternate key denir. Alternate key'in ilişkisel modeldeki yeri primary key'den aşağı değildir, yalnızca foreign key'lerin referans verdiği key o değildir. Fiziksel modelde alternate key bir UNIQUE constraint'e dönüşür ve dönüşmek zorundadır.
Bir şey daha: candidate key'i veriye bakarak değil iş kuralına bakarak bulursun. Bugün tabloda aynı ada sahip iki müşteri yok diye {first_name, last_name} bir candidate key olmaz. Bu ayrım serinin 5. yazısında functional dependency konusunda tekrar karşımıza çıkacak.
İyi Bir Primary Key'in Özellikleri
Primary key seçerken beş özelliğe bakılır. Unique: her satır için farklı. Never null: her satır için var; değeri sonradan öğrenilecek bir attribute key olamaz. Minimal: fazladan kolon taşımıyor. Stable: bir kez atandıktan sonra değişmiyor, çünkü değiştiği an ona referans veren her foreign key'in de değişmesi gerekiyor. Ve tercihen anlamsız: içinde iş bilgisi taşımıyor, çünkü iş bilgisi değişir.
Son iki özellik, natural key ile surrogate key tartışmasının özüdür. Natural key, iş dünyasında zaten var olan bir tanımlayıcıdır: ISBN, e-posta, vergi numarası. Surrogate key ise veritabanının ürettiği, dış dünyada anlamı olmayan bir değerdir: IDENTITY ile gelen bir sayı ya da bir GUID (Globally Unique Identifier). Natural key ilk dört özelliği sağlıyor gibi görünür, ama "stable" ve "never null" testlerinde sık sık kalır.
Natural Key Adayları ve Tuzakları
ISBN, kitap için en doğal key gibi duruyor. Ama birkaç sorun var. Birincisi, her kitabın ISBN'i yok. Sistem 1960'ların sonunda, İngiltere'deki 9 haneli SBN (Standard Book Number) üzerinden doğdu ve 1970'te ISO (International Organization for Standardization) 2108 standardı olarak yayımlandı; bundan önce basılmış bir sahaf kitabının, kitapçının kendi ürettiği bir defterin ya da ISBN almamış bir yayının bu numarası yok. İkincisi, format değişti: 1 Ocak 2007'den beri ISBN'ler 13 hanelidir, öncesinde 10 haneliydi. Eski sistemden gelen verinin bir kısmı 10, bir kısmı 13 haneli olabilir ve aynı kitap iki farklı değerle görünebilir. Üçüncüsü ve modelleme açısından en önemlisi: ISBN bir eseri değil, bir baskıyı ve formatı tanımlar. Aynı kitabın ciltli, karton kapak ve e-kitap hâllerinin ISBN'leri farklıdır. ISBN'i key yaptığında aslında "books tablosunun bir satırı neyi temsil ediyor?" sorusunu cevaplamış olursun: bir eseri mi, satılan bir ürünü mü? Natural key seçmeye çalışmak, entity'nin grain'ini netleştirmenin en hızlı yoludur.
ISBN'i olmayan kitapları da kabul edeceksek kolon nullable olur ve kural "ISBN varsa benzersizdir" hâline gelir. SQL Server'da nullable bir kolondaki UNIQUE constraint tek bir NULL'a izin verir; çözüm filtered unique index. Bu davranışın ayrıntısı SQL Temelleri serisinin "Constraint'ler ve Şema Değişikliği" yazısında, burada sadece modeldeki karşılığını kuruyoruz:
ALTER TABLE dbo.books DROP CONSTRAINT UQ_books_isbn;
ALTER TABLE dbo.books ALTER COLUMN isbn CHAR(13) NULL;
CREATE UNIQUE INDEX UX_books_isbn ON dbo.books (isbn) WHERE isbn IS NOT NULL;
INSERT INTO dbo.books (isbn, title, list_price) VALUES (NULL, N'Meditations (1890 print)', 2400.00);
INSERT INTO dbo.books (isbn, title, list_price) VALUES (NULL, N'Bookstore Gift Notebook', 150.00);
INSERT INTO dbo.books (isbn, title, list_price) VALUES ('9798900000268', N'Dune', 390.50);
(1 rows affected)
(1 rows affected)
Msg 2601, Level 14, State 1, Server b8be943222fa, Line 7
Cannot insert duplicate key row in object 'dbo.books' with unique index 'UX_books_isbn'. The duplicate key value is (9798900000268).
The statement has been terminated.
İki ISBN'siz ürün girdi, tekrar eden ISBN girmedi. Model artık "ISBN'i olmayan ürün olabilir ama ISBN'i olan iki ürün aynı numarayı taşıyamaz" diyor.
E-posta, müşteri için doğal bir key adayı. İlk yazıdaki requirement listesi ise açık: müşteri e-postasını değiştirebilir. E-postayı primary key yaptığında, adres değişikliği orders, reviews, payments ve müşteriye bağlanan her tabloda bir key güncellemesi demektir. Bir de karşılaştırma sorunu var:
CREATE TABLE dbo.customers (
customer_id INT IDENTITY(1,1) NOT NULL,
email VARCHAR(254) NOT NULL,
first_name NVARCHAR(100) NOT NULL,
CONSTRAINT PK_customers PRIMARY KEY (customer_id),
CONSTRAINT UQ_customers_email UNIQUE (email)
);
INSERT INTO dbo.customers (email, first_name) VALUES ('ada@example.com', N'Ada');
INSERT INTO dbo.customers (email, first_name) VALUES ('Ada@Example.com', N'Ada');
SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS db_collation;
(1 rows affected)
Msg 2627, Level 14, State 1, Server b8be943222fa, Line 9
Violation of UNIQUE KEY constraint 'UQ_customers_email'. Cannot insert duplicate key in object 'dbo.customers'. The duplicate key value is (Ada@Example.com).
The statement has been terminated.
db_collation
----------------------------
SQL_Latin1_General_CP1_CI_AS
(1 rows affected)
Veritabanının collation'ı case-insensitive olduğu için Ada@Example.com ile ada@example.com aynı değer sayıldı. Bu örnekte istediğimiz davranış bu; ama aynı veriyi case-sensitive karşılaştıran bir sistemle (başka bir veritabanı, bir cache key'i, bir dictionary) eşleştirdiğin anda iki farklı müşteri ortaya çıkar. Doğru yer: customer_id surrogate primary key, email ise UNIQUE constraint ile korunan bir alternate key. Kimlik sabit kalır, e-posta değişebilir ve tekrar edemez. Collation'ın karşılaştırmaya etkisi SQL Temelleri serisinin 3. yazısında.
T.C. kimlik numarası bir başka cazip aday: 11 hane, herkeste farklı, değişmiyor. Ama üç sorunu var. Yabancı müşterilerin bu numarası yok, yani "never null" testini geçmiyor. Kimlik numarası, 7 Nisan 2016'da Resmî Gazete'de yayımlanan 6698 sayılı KVKK (Kişisel Verilerin Korunması Kanunu) kapsamında kişisel veri; primary key yaptığın anda her foreign key kolonuna, her log satırına, belki URL'lere (Uniform Resource Locator) de kopyalanır ve "bu veriyi kim, nerede tutuyor?" sorusunun cevabı tüm şema olur. Üçüncüsü, kişisel veriyi işleme amacı ortadan kalkınca silme ya da anonimleştirme yükümlülüğü doğabilir; key olan bir değeri silmek, ona bağlı her satırı etkiler. Kimlik numarası gerekiyorsa ayrı, erişimi kısıtlı bir tabloda, surrogate key'e bağlı bir attribute olarak durmalı.
Surrogate Key Kullanırken Natural Key'i Yine Korumak
Bu yazının ana mesajı şu: surrogate key, natural key'in yerine geçmez; onun yükünü taşır. Surrogate key foreign key'lerin referans verdiği stabil kimliktir. Natural key ise iş kuralının söylediği tekliktir ve UNIQUE constraint ile korunmaya devam eder. Yazının başındaki hata, surrogate key eklenince iş kuralının unutulmasıydı.
Bir tablonun natural uniqueness'ı yoksa bu da bir uyarı işaretidir. Her satırı yalnızca IDENTITY ile ayırt edilen bir tabloda, aynı gerçek dünyadaki şeyin iki kez kaydedilmesini hiçbir şey engellemez. Serinin son yazısındaki şema review checklist'inin ilk sorusu bu olacak: "Bu tablonun IDENTITY dışında anlamlı bir benzersizliği var mı?"
Composite Key: Doğal Seçim ve Maliyeti
Associative entity'de composite key çoğu zaman doğal seçimdir. book_authors için (book_id, author_id) hem iş kuralını (aynı yazar aynı kitaba bir kez bağlanır) hem kimliği aynı anda ifade eder; ayrı bir book_author_id kolonu eklemek, bu kuralı korumak için yine aynı iki kolona UNIQUE koymanı gerektirir. Ama bu tabloya başka bir tablo referans vermeye başladığında maliyet görünür hâle gelir. Yazarların telif paylarını tutan bir tablo ekleyelim:
CREATE TABLE dbo.authors (
author_id INT IDENTITY(1,1) NOT NULL,
full_name NVARCHAR(200) NOT NULL,
CONSTRAINT PK_authors PRIMARY KEY (author_id)
);
CREATE TABLE dbo.book_authors (
book_id INT NOT NULL,
author_id INT NOT NULL,
author_order TINYINT NOT NULL,
CONSTRAINT PK_book_authors PRIMARY KEY (book_id, author_id),
CONSTRAINT FK_book_authors_books FOREIGN KEY (book_id) REFERENCES dbo.books (book_id),
CONSTRAINT FK_book_authors_authors FOREIGN KEY (author_id) REFERENCES dbo.authors (author_id)
);
CREATE TABLE dbo.royalty_shares (
book_id INT NOT NULL,
author_id INT NOT NULL,
valid_from DATE NOT NULL,
share_percent DECIMAL(5,2) NOT NULL,
CONSTRAINT PK_royalty_shares PRIMARY KEY (book_id, author_id, valid_from),
CONSTRAINT FK_royalty_shares_book_authors FOREIGN KEY (book_id, author_id)
REFERENCES dbo.book_authors (book_id, author_id)
);
INSERT INTO dbo.authors (full_name) VALUES (N'Frank Herbert'), (N'Terry Pratchett');
INSERT INTO dbo.book_authors (book_id, author_id, author_order) VALUES (1, 1, 1);
INSERT INTO dbo.royalty_shares VALUES (1, 1, '2026-01-01', 10.00);
INSERT INTO dbo.royalty_shares VALUES (1, 2, '2026-01-01', 5.00);
(2 rows affected)
(1 rows affected)
(1 rows affected)
Msg 547, Level 16, State 1, Server b8be943222fa, Line 26
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_royalty_shares_book_authors". The conflict occurred in database "dm_04", table "dbo.book_authors".
The statement has been terminated.
Composite foreign key tam olarak istediğimizi yaptı: Terry Pratchett, Dune'un yazarları arasında olmadığı için ona Dune'dan telif payı yazılamadı. İki ayrı foreign key (biri books'a, biri authors'a) bunu yakalayamazdı; ikisi de ayrı ayrı geçerli değerler. Maliyeti ise şu: royalty_shares iki kolonu birden taşıyor, ona bağlanacak her tablo da taşıyacak, her join iki koşulla yazılacak ve parent key'in bir parçası değişirse güncelleme zinciri uzayacak. Kural: composite key'e referans veren tek bir tablo varsa ve referans iş kuralı taşıyorsa (yukarıdaki gibi) composite key iyi bir seçim. Birçok tablo referans verecekse surrogate key ekleyip composite'i UNIQUE olarak korumak çoğu zaman daha rahat.
Fiziksel Tip Kararı
Surrogate key'e karar verdikten sonra tipini ve üretim yerini seçmen gerekiyor. Kısa bir karar tablosu:
| Seçenek | Boyut | Değeri kim üretir | Ne zaman |
|---|---|---|---|
INT IDENTITY | 4 byte | Veritabanı, insert sırasında | Tek veritabanı, 2,1 milyarın altında satır beklentisi |
BIGINT IDENTITY | 8 byte | Veritabanı, insert sırasında | Çok büyüyecek tablolar (sipariş kalemi, log, event) |
SEQUENCE | Tipe göre | Veritabanı, insert'ten önce de alınabilir | Birden çok tablonun ortak sayacı, id'yi önceden bilmek |
UNIQUEIDENTIFIER + NEWID() | 16 byte | Veritabanı ya da uygulama, rastgele (v4) | Dağıtık üretim; clustered key için kötü |
UNIQUEIDENTIFIER + NEWSEQUENTIALID() | 16 byte | Veritabanı, yalnızca DEFAULT ile | Sıralı GUID; sonraki değer tahmin edilebilir |
| Uygulamada UUIDv7 | 16 byte | Uygulama, zaman önekli | Dağıtık üretim, insert'ten önce id |
Bu seçeneklerin clustered index'e etkisi (page split, fragmentation) SQL Server Derinlemesine serisinin "Page'ler, Heap ve Clustered Index" yazısında; dışarıya açılan id ile iç key'in ayrılması ve SQL Server'ın uniqueidentifier'ı son 6 byte'tan başlayarak sıralaması yüzünden UUIDv7'nin zaman sırasının index'e yansımaması Çözüm Desenleri serisinin "Id'yi Dışarıya Açmak" yazısında ölçüldü; UUIDv7 ve Snowflake gibi üretim algoritmaları ise System Design serisinin "Unique ID Üretimi" yazısında. Microsoft'un dokümantasyonu NEWSEQUENTIALID() için ayrıca uyarıyor: yalnızca DEFAULT constraint içinde kullanılabilir, sunucu yeniden başladıktan sonra daha düşük bir aralıktan devam edebilir ve sonraki değer tahmin edilebildiği için gizliliğin önemli olduğu yerde kullanılmamalıdır.
PostgreSQL 18 notu: PostgreSQL 18 ile uuidv7() fonksiyonu çekirdeğe geldi ve DEFAULT olarak doğrudan kullanılabiliyor. uuid tipi baştan sona byte sırasıyla karşılaştırıldığı için zaman öneki PostgreSQL'de index sırasına yansıyor:
CREATE TABLE gift_cards (
gift_card_id uuid PRIMARY KEY DEFAULT uuidv7(),
code varchar(20) NOT NULL UNIQUE
);
INSERT INTO gift_cards (code) VALUES ('GIFT-0001'), ('GIFT-0002');
SELECT gift_card_id, uuid_extract_version(gift_card_id) AS version,
uuid_extract_timestamp(gift_card_id) AS created_at, code
FROM gift_cards
ORDER BY gift_card_id;
CREATE TABLE
INSERT 0 2
gift_card_id | version | created_at | code
--------------------------------------+---------+----------------------------+-----------
01a1d6d1-58a5-7bd5-9e65-0e76445f4659 | 7 | 2026-11-13 21:30:06.373+00 | GIFT-0001
01a1d6d1-58a5-7d84-b99e-c46151562ee1 | 7 | 2026-11-13 21:30:06.373+00 | GIFT-0002
(2 rows)
Aynı milisaniyede üretilen iki değerin de zaman damgası aynı, ama sıralamaları üretim sırasıyla tutarlı. Bu davranışın PostgreSQL tarafındaki ayrıntısı PostgreSQL serisinde.
Üretimde Dikkat
Constraint'i baştan koy. Yazının başındaki senaryo gösteriyor ki tekrar eden veri bir kez içeri girdikten sonra constraint eklemek bir temizlik projesine dönüşür: tekrarları bul, birini seç, bağlı satırları taşı, sonra constraint'i ekle. Bu temizliği canlıda yapmak, aynı anda yeni tekrarların girmemesini de garanti etmeyi gerektirir.
INT sınırını izle. INT IDENTITY 2.147.483.647'de durur ve bir sonraki insert hata verir. Hızlı büyüyen tablolarda identity'nin ne kadar kullanıldığını izlemek (IDENT_CURRENT) ve sınıra aylar kala BIGINT'e geçişi planlamak, bir gece aniden durmaktan çok daha ucuzdur. Geçiş, o key'e referans veren her foreign key kolonunun da tipini değiştirmek demek.
Surrogate key'i dışarıya açma kararını ayrı ver. İç key'in sıralı bir sayı olması, URL'de /orders/1003 görünmesini ve rakiplerin sipariş hacmini tahmin etmesini de beraberinde getirir. Dışarıya açılan id'yi iç key'den ayırmanın yolları Çözüm Desenleri serisinin "Id'yi Dışarıya Açmak" yazısında.
Natural key'in formatını normalleştir. ISBN'ler tireli ve tiresiz, 10 ve 13 haneli gelebilir; e-postalar farklı büyük-küçük harfle. UNIQUE constraint ancak değerler aynı biçimde saklanıyorsa işe yarar. Format kuralını CHECK constraint ile ya da yazma yolunda tek bir normalizasyon fonksiyonuyla koru.
Sık Düşülen Tuzaklar
Surrogate key ekleyip iş kuralındaki benzersizliği tamamen unutmak. Yazının başındaki aynı ISBN'li iki kitap bunun en sık görülen hâli; aynı e-postalı iki müşteri, aynı koda sahip iki kupon da aynı hatadan doğar.
Değişebilen bir değeri primary key yapmak. E-posta, telefon, kullanıcı adı, ürün kodu: hepsi bir gün değişir ve değiştiği gün tüm foreign key'ler güncellenmek zorunda kalır.
Kişisel veriyi key yapmak. Kimlik numarası ya da e-posta key olunca şemanın her köşesine kopyalanır; silme ve anonimleştirme talebi bütün şemayı etkiler.
Her tabloya refleksle surrogate key eklemek. Associative entity'de (book_id, author_id) zaten doğal ve yeterli bir key'dir; üstüne IDENTITY eklemek bir kolon ve bir index daha demektir, kuralı korumak için yine composite UNIQUE gerekir.
Key'i veriye bakarak seçmek. Bugün tekrar etmiyor diye bir kolonun key olduğuna karar vermek, yarınki ilk tekrar eden kayıtta çöker. Key'i iş kuralı belirler.
Küçük Bir Deneme
★☆☆ customers tablosuna nullable bir phone kolonu ekle ve "telefon varsa benzersizdir" kuralını filtered unique index ile kur. Aynı telefonu iki müşteriye yazmayı ve iki müşteriyi telefonsuz bırakmayı dene.
★★☆ books tablosunun bir eseri mi yoksa satılan bir ürünü mü temsil ettiğine karar ver. Ürün ise, aynı eserin ciltli ve e-kitap hâllerini bağlayan bir works tablosu tasarla. Hangi tablonun natural key'i ISBN olur, diğerinin natural key'i ne olabilir?
★★★ royalty_shares tablosunu book_authors'a surrogate key ile bağlanacak şekilde yeniden tasarla: book_authors'a book_author_id ekle, foreign key'i ona çevir. Terry Pratchett'a Dune'dan pay yazılmasını engelleyen kural hâlâ korunuyor mu? Korunmuyorsa onu nasıl geri getirirsin?
Key'leri seçtik ama bir tablodaki bir attribute'un hangi key'e bağlı olduğunu henüz sistemli olarak sormadık. Bir sonraki yazıda serinin ikinci bölümüne, normalizasyona geçiyoruz: sipariş Excel'ini SQL Server'a olduğu gibi yükleyip insert, update ve delete anomaly'lerini canlı görecek, functional dependency kavramıyla bu anomalilerin nereden geldiğini açıklayacağız.