Abdulaziz Akyol

SQL Server'da tablo tasarımı: CREATE, ALTER, DROP ve kısıtlar

Microsoft SQL Server · Yazılım geliştirme
25 Temmuz 2020 · 7 dk okuma · Abdulaziz Akyol

SQL Server’da tablo tasarımı, verinin hangi sütunlarda, hangi veri tipleriyle ve hangi kurallarla (kısıtlarla) saklanacağını T-SQL’in CREATE TABLE, ALTER TABLE ve DROP TABLE komutlarıyla tanımlamaktır. İyi tasarlanmış bir tablo yanlış veriyi daha girişte reddeder; kötü tasarlanmış bir tabloyu ise sonradan hiçbir indeks kurtaramaz.

Bu yazı, 2020’de ayrı ayrı yayımladığım “SQL de Kodla Tablo Oluşturma” ve “SQL de Alter Tablo” notlarının birleştirilmiş ve düzeltilmiş hâlidir. Eski örneklerde SQL Server’da çalışmayan sözdizimleri vardı; aşağıda hem doğrusunu hem nedenini yazdım.

SQL komut grupları: DDL, DML, DCL ve TCL

Eski notta DDL ve DML ayrımı vardı. Tabloyu dört grupla düşünmek daha kullanışlı:

Grup Açılımı Komutlar Ne yapar
DDL Data Definition Language CREATE, ALTER, DROP, TRUNCATE TABLE Nesnelerin yapısını tanımlar
DML Data Manipulation Language SELECT, INSERT, UPDATE, DELETE, MERGE Verinin kendisini okur ve değiştirir
DCL Data Control Language GRANT, REVOKE, DENY Yetkileri yönetir
TCL Transaction Control Language BEGIN TRAN, COMMIT, ROLLBACK İşlemleri (transaction) yönetir

SQL Server’da DDL komutları da işlem içinde çalışır; BEGIN TRAN içinde yapılan bir DROP TABLE henüz COMMIT edilmediyse ROLLBACK ile geri alınabilir.

Eski örnekteki hatalar

2020 notundaki örneklerde şu sorunlar vardı:

  • CREATE <tablo adı> yazılmıştı; doğrusu CREATE TABLE <şema>.<tablo>.
  • Sütun tanımları arasında virgüller eksikti ve parantez yanlış yerde kapanıyordu.
  • FLOAT(x;y), LOGICAL ve GRAPHIC(n) SQL Server veri tipi değildir. float(n) tek parametre alır, mantıksal değer için bit kullanılır.
  • TIMESTAMP SQL Server’da tarih değil, rowversion tipinin eski adıdır; satır her güncellendiğinde değişen bir sürüm numarası tutar.
  • ALTER TABLE Musteri ADD tel varchar(20) NOT NULL komutu tablo doluysa hata verir; aşağıda nedenini anlatıyorum.

Veri tipi nasıl seçilir?

Veri tipi, hem doğruluğu hem de depolama ve performansı belirler. Pratikte kullandığım tablo:

İhtiyaç Önerilen tip Kaçının Neden
Kimlik (ID) int, çok büyük tablolarda bigint numeric(18,0), uniqueidentifier (kümelenmiş anahtar olarak) Dar, sıralı anahtar
Para, fiyat, miktar decimal(p,s) float, real float yaklaşık değer saklar, kuruş kaydırır
Tarih ve saat datetime2(n) datetime, timestamp Daha geniş aralık ve ayarlanabilir hassasiyet
Saat dilimiyle zaman datetimeoffset Metin olarak saklamak Ofset bilgisini korur
Yalnızca tarih date datetime 3 bayt, saat karmaşası yok
Türkçe ya da çok dilli metin nvarchar(n) text, ntext text/ntext eski tiplerdir
ASCII kod, e-posta, barkod varchar(n) nvarchar(max) Gereksiz alan ve bellek
Evet/Hayır bit char(1) ile ‘E’/‘H’ Tek anlamlı, küçük

nvarchar mı varchar mı?

char ve varchar içindeki n, Microsoft belgesinin özellikle vurguladığı gibi karakter sayısı değil bayt sayısıdır. Türkçe harmanlamada (ör. Turkish_100_CI_AS) varchar Türkçe karakterleri saklar ama Arapça ya da Yunanca bir müşteri adı gelirse karakterler ? olur. SQL Server 2019 ile gelen UTF-8 harmanlamalarında varchar tüm Unicode karakterlerini saklayabilir. Daha eski sürümlerde ya da karma ortamlarda nvarchar en güvenli seçim. Unicode sabitlerini N'İstanbul' biçiminde, N önekiyle yazmayı unutmayın.

datetime2 ve decimal

datetime2 0001–9999 yılları arasını 100 nanosaniye doğrulukla saklar; hassasiyeti datetime2(0) ile saniyeye indirip yerden kazanabilirsiniz. Microsoft yeni geliştirmelerde datetime yerine date, time, datetime2 ve datetimeoffset kullanılmasını önerir. Para için decimal(12,2) gibi kesin bir tip seçin; float ile toplanan fiyatlar raporlarda kuruş farkı üretir.

NULL politikası

Varsayılanınız NOT NULL olsun. NULL yalnızca “bilinmiyor” ya da “uygulanamaz” anlamı taşıyan alanlarda kabul edilmeli. Her sütunda NULL ya da NOT NULL ifadesini açıkça yazın: oturum ayarları varsayılan boş geçilebilirliği değiştirebilir. ALTER COLUMN ile tip değiştirirken belirtmezseniz sütun boş geçilebilir hâle gelir.

Kısıtlar açısından iki tuzak var. CHECK kısıtı yalnızca FALSE sonucu reddeder; NULL değer UNKNOWN sonucunu verdiği için kontrolden geçer. UNIQUE kısıtı ise SQL Server’da sütun başına yalnızca bir NULL kabul eder. Birden çok boş değere izin verip dolu değerlerde tekillik istiyorsanız aşağıdaki örnekteki gibi filtreli benzersiz indeks kullanın.

Adlandırma kuralları

  • Tabloları bir şema altında toplayın (satis.Musteri); dbo her şeyin çöplüğü olmasın.
  • Tanımlayıcılarda Türkçe karakter ve boşluk kullanmayın. Türkçe harmanlamada büyük/küçük harf duyarsızlığı i/I için beklendiği gibi çalışmaz; musteriid ile MusteriID eşleşmeyebilir. Tutarlı PascalCase yazın.
  • Kısıtlara açık ad verin: PK_Tablo, FK_Cocuk_Ebeveyn, UQ_Tablo_Sutun, CK_Tablo_Sutun, DF_Tablo_Sutun. Sistemin ürettiği PK__Musteri__3214EC07... gibi adlar hata mesajlarını okumayı ve kısıtı sonradan kaldırmayı zorlaştırır.

Çalışır örnek şema: Müşteri–Sipariş

Aşağıdaki betik SQL Server 2016 ve sonrasında çalışır. Müşteri, ürün, sipariş ve sipariş kalemi tablolarıyla birincil anahtar (PRIMARY KEY), IDENTITY, yabancı anahtar (FOREIGN KEY), UNIQUE, CHECK ve DEFAULT kısıtlarını birlikte gösterir.

IF DB_ID(N'MagazaDemo') IS NULL
    CREATE DATABASE MagazaDemo;
GO
USE MagazaDemo;
GO
IF SCHEMA_ID(N'satis') IS NULL
    EXEC (N'CREATE SCHEMA satis AUTHORIZATION dbo;');
GO

CREATE TABLE satis.Musteri
(
    MusteriId    int IDENTITY(1, 1) NOT NULL,
    MusteriKodu  varchar(10)        NOT NULL,
    AdSoyad      nvarchar(100)      NOT NULL,
    Eposta       varchar(254)       NULL,
    Telefon      varchar(20)        NULL,
    Il           nvarchar(50)       NULL,
    UlkeKodu     char(2)            NOT NULL CONSTRAINT DF_Musteri_UlkeKodu DEFAULT ('TR'),
    KayitTarihi  datetime2(0)       NOT NULL CONSTRAINT DF_Musteri_KayitTarihi DEFAULT (SYSDATETIME()),
    AktifMi      bit                NOT NULL CONSTRAINT DF_Musteri_AktifMi DEFAULT (1),
    CONSTRAINT PK_Musteri PRIMARY KEY CLUSTERED (MusteriId),
    CONSTRAINT UQ_Musteri_MusteriKodu UNIQUE (MusteriKodu),
    CONSTRAINT CK_Musteri_UlkeKodu CHECK (LEN(UlkeKodu) = 2)
);
GO

-- Birden çok NULL'a izin veren, dolu değerlerde tekil e-posta
CREATE UNIQUE NONCLUSTERED INDEX UX_Musteri_Eposta
    ON satis.Musteri (Eposta)
    WHERE Eposta IS NOT NULL;
GO

CREATE TABLE satis.Urun
(
    UrunId      int IDENTITY(1, 1) NOT NULL,
    Barkod      varchar(20)        NOT NULL,
    UrunAdi     nvarchar(200)      NOT NULL,
    BirimFiyat  decimal(12, 2)     NOT NULL,
    KdvOrani    decimal(5, 2)      NOT NULL,
    CONSTRAINT PK_Urun PRIMARY KEY (UrunId),
    CONSTRAINT UQ_Urun_Barkod UNIQUE (Barkod),
    CONSTRAINT CK_Urun_BirimFiyat CHECK (BirimFiyat >= 0),
    CONSTRAINT CK_Urun_KdvOrani CHECK (KdvOrani BETWEEN 0 AND 100)
);
GO

CREATE TABLE satis.Siparis
(
    SiparisId      bigint IDENTITY(1, 1) NOT NULL,
    MusteriId      int                   NOT NULL,
    SiparisTarihi  datetime2(3)          NOT NULL CONSTRAINT DF_Siparis_SiparisTarihi DEFAULT (SYSDATETIME()),
    Durum          varchar(20)           NOT NULL CONSTRAINT DF_Siparis_Durum DEFAULT ('YENI'),
    TeslimAdresi   nvarchar(400)         NULL,
    CONSTRAINT PK_Siparis PRIMARY KEY (SiparisId),
    CONSTRAINT FK_Siparis_Musteri FOREIGN KEY (MusteriId)
        REFERENCES satis.Musteri (MusteriId),
    CONSTRAINT CK_Siparis_Durum
        CHECK (Durum IN ('YENI', 'HAZIRLANIYOR', 'KARGODA', 'TESLIM', 'IPTAL'))
);
GO

-- FOREIGN KEY sütunları otomatik indekslenmez
CREATE INDEX IX_Siparis_MusteriId ON satis.Siparis (MusteriId);
GO

CREATE TABLE satis.SiparisKalemi
(
    SiparisId   bigint          NOT NULL,
    SatirNo     smallint        NOT NULL,
    UrunId      int             NOT NULL,
    Miktar      decimal(10, 3)  NOT NULL,
    BirimFiyat  decimal(12, 2)  NOT NULL,
    Tutar       AS (Miktar * BirimFiyat) PERSISTED,
    CONSTRAINT PK_SiparisKalemi PRIMARY KEY (SiparisId, SatirNo),
    CONSTRAINT FK_SiparisKalemi_Siparis FOREIGN KEY (SiparisId)
        REFERENCES satis.Siparis (SiparisId) ON DELETE CASCADE,
    CONSTRAINT FK_SiparisKalemi_Urun FOREIGN KEY (UrunId)
        REFERENCES satis.Urun (UrunId),
    CONSTRAINT CK_SiparisKalemi_Miktar CHECK (Miktar > 0)
);
GO

CREATE INDEX IX_SiparisKalemi_UrunId ON satis.SiparisKalemi (UrunId);
GO

INSERT INTO satis.Musteri (MusteriKodu, AdSoyad, Eposta, Il)
VALUES ('M0001', N'Örnek Müşteri A', 'a@example.com', N'İstanbul'),
       ('M0002', N'Örnek Müşteri B', NULL, N'Kocaeli');

INSERT INTO satis.Urun (Barkod, UrunAdi, BirimFiyat, KdvOrani)
VALUES ('8690000000001', N'Örnek Ürün', 250.00, 20.00);

INSERT INTO satis.Siparis (MusteriId) VALUES (1);

INSERT INTO satis.SiparisKalemi (SiparisId, SatirNo, UrunId, Miktar, BirimFiyat)
VALUES (1, 1, 1, 2, 250.00);

SELECT s.SiparisId, m.AdSoyad, k.UrunId, k.Miktar, k.Tutar
FROM satis.Siparis AS s
JOIN satis.Musteri AS m ON m.MusteriId = s.MusteriId
JOIN satis.SiparisKalemi AS k ON k.SiparisId = s.SiparisId;
GO

Tasarımdaki kararlar:

  • PRIMARY KEY ve IDENTITY: Kümelenmiş anahtar dar, değişmeyen ve artan bir değer olmalı; int IDENTITY bu yüzden iyi bir varsayılan. IDENTITY boşluksuz numara garanti etmez; geri alınan işlemler ve yeniden başlatmalar numara atlatabilir. Fatura numarası gibi kesintisiz seri gereken yerlerde ayrı bir mekanizma kurun.
  • İş anahtarı UNIQUE ile korunur: MusteriKodu ve Barkod birincil anahtar değil ama tekrar etmemeli. UNIQUE kısıtı bunun için arka planda benzersiz bir indeks oluşturur.
  • FOREIGN KEY indeksi: SQL Server yabancı anahtar sütununa otomatik indeks açmaz. Birleştirme (JOIN) ve silme işlemlerinin tabloyu taramaması için bu indeksleri kendiniz ekleyin. İndeks ve sorgu planı tarafını SQL Server performans ayarı: indeks, istatistik ve sorgu planı yazısında ayrıntılı anlattım.
  • ON DELETE CASCADE: Sipariş silinince kalemlerinin de silinmesi mantıklı; müşteri silinince siparişlerin silinmesi değil. Bu yüzden kademeli silmeyi yalnızca kalem tablosunda kullandım.
  • Hesaplanan sütun: Tutar saklanmaz, Miktar * BirimFiyat ifadesinden türetilir; PERSISTED ile diske yazılır ve indekslenebilir.

ALTER TABLE: mevcut tabloyu değiştirmek

ALTER TABLE, daha önce oluşturulmuş bir tablonun yapısını değiştirir. Eski notumdaki ADD tel varchar(20) NOT NULL satırı, tabloda veri varsa hata verir. Microsoft belgesindeki kural açık: yeni sütun NULL kabul etmiyorsa ve tablo boş değilse sütunla birlikte bir DEFAULT tanımı eklenmelidir.

-- 1) Boş geçilebilir sütun eklemek her zaman mümkündür
ALTER TABLE satis.Musteri
    ADD VergiNo varchar(11) NULL;
GO

-- 2) Dolu tabloya NOT NULL sütun: DEFAULT ile birlikte
ALTER TABLE satis.Musteri
    ADD MusteriTipi char(1) NOT NULL
        CONSTRAINT DF_Musteri_MusteriTipi DEFAULT ('B');
GO

-- 3) Sütun tipini değiştirmek: NULL/NOT NULL'ı açıkça yazın
ALTER TABLE satis.Musteri
    ALTER COLUMN Telefon varchar(30) NULL;
GO

-- 4) Kısıt eklemek: mevcut veriyi de doğrula
ALTER TABLE satis.Musteri WITH CHECK
    ADD CONSTRAINT CK_Musteri_MusteriTipi CHECK (MusteriTipi IN ('B', 'K'));
GO

-- 5) Sütun adını değiştirmek
EXEC sp_rename N'satis.Musteri.Telefon', N'CepTelefonu', N'COLUMN';
GO

-- 6) Kısıt ve sütun kaldırmak (SQL Server 2016+)
ALTER TABLE satis.Musteri DROP CONSTRAINT IF EXISTS CK_Musteri_MusteriTipi;
ALTER TABLE satis.Musteri DROP CONSTRAINT IF EXISTS DF_Musteri_MusteriTipi;
ALTER TABLE satis.Musteri DROP COLUMN IF EXISTS MusteriTipi;
GO

-- Güvenilmeyen (WITH NOCHECK ile eklenmiş) kısıtları bulmak
SELECT name, is_disabled, is_not_trusted
FROM sys.check_constraints
WHERE parent_object_id = OBJECT_ID(N'satis.Musteri');

Dikkat edilecek noktalar:

  • SQL Server 2012 ve sonrası Enterprise sürümünde, varsayılan değeri çalışma zamanı sabiti olan bir NOT NULL sütun eklemek çevrimiçi (online) işlemdir; diğer durumlarda büyük tablolarda tablo yeniden yazılabilir, bakım penceresinde yapın.
  • ALTER COLUMN, PRIMARY KEY, FOREIGN KEY ya da DEFAULT ile ilişkili bir sütunda tip değişikliğine izin vermez. Önce kısıtı kaldırmanız, sonra yeniden oluşturmanız gerekir.
  • Bir sütunu kaldırmadan önce ona bağlı DEFAULT ve CHECK kısıtlarını kaldırın; örnekteki sıra bu yüzden böyle.
  • WITH NOCHECK ile eklenen kısıtlar mevcut veriyi doğrulamaz ve sorgu iyileştiricisi bu kısıtlara güvenmez. Zorunlu kalmadıkça WITH CHECK kullanın.

DROP, TRUNCATE ve DELETE farkı

Eski notta “DROP TABLE tabloyu ve verisini siler” diye tek satır vardı. Üç komut arasındaki fark, üretimde yanlış komutun bedelini belirler:

Özellik DELETE TRUNCATE TABLE DROP TABLE
Ne kaldırır Seçilen satırlar (WHERE ile) Tüm satırlar Tablo, veri, indeks, kısıt, tetikleyici
Günlükleme Her satır ayrı kaydedilir Yalnızca sayfa serbest bırakma kaydedilir Nesne kaldırma
IDENTITY sayacı Korunur Başlangıç değerine döner Tabloyla birlikte gider
Tetikleyici DELETE tetikleyicisi çalışır Çalışmaz Tetikleyici de silinir
FOREIGN KEY ile başvurulan tablo Kısıt kurallarına göre Kullanılamaz Önce başvuran kısıt kaldırılmalı
İşlem içinde geri alma Evet Evet Evet
Grup DML DDL DDL
DELETE FROM satis.Siparis WHERE Durum = 'IPTAL';   -- kalemler CASCADE ile silinir
TRUNCATE TABLE satis.SiparisKalemi;                 -- başvurulmayan tablo: izinli
-- TRUNCATE TABLE satis.Siparis;                    -- hata: SiparisKalemi başvuruyor
DROP TABLE IF EXISTS satis.SiparisKalemi, satis.Siparis, satis.Urun, satis.Musteri;

Tek DROP TABLE komutunda hem başvuran hem başvurulan tabloları listeliyorsanız başvuran (çocuk) tabloyu önce yazın. Üretimde DROP ve TRUNCATE öncesinde yedeğin alındığını ve doğru veritabanında olduğunuzu (SELECT DB_NAME();) kontrol etmek, her ekipte yazılı kural olmalı.

Sonuç ve kontrol listesi

  • Her tablo bir şema altında, her kısıt açık adlı.
  • Her sütunda NULL/NOT NULL açıkça yazılı; varsayılan NOT NULL.
  • Para decimal, tarih datetime2/date, çok dilli metin nvarchar.
  • İş anahtarları UNIQUE, ilişkiler FOREIGN KEY, iş kuralları CHECK ile korunuyor.
  • Yabancı anahtar sütunları indeksli.
  • Dolu tabloya NOT NULL sütun DEFAULT ile ekleniyor.
  • TRUNCATE ve DROP yalnızca yedek ve doğru bağlam kontrolünden sonra.

Tablo tasarımı veri ambarı ve raporlama projelerinde de temel belirleyicidir; kurumsal ölçekte bunun nasıl bir iş zekâsı altyapısına dönüştüğünü Nebim V3 veri ambarı ve iş zekâsı röportajında anlatmıştım.

Sık sorulan sorular

SQL'de DROP, TRUNCATE ve DELETE arasındaki fark nedir?

DELETE seçilen satırları tek tek siler, WHERE ile süzülebilir ve tetikleyicileri çalıştırır. TRUNCATE TABLE tüm satırları sayfa düzeyinde kaldırır, IDENTITY sayacını başa alır ve FOREIGN KEY ile başvurulan tablolarda kullanılamaz. DROP TABLE ise tabloyu tanımı, indeksleri ve kısıtlarıyla birlikte veritabanından kaldırır.

SQL Server'da varchar mı nvarchar mı kullanmalıyım?

Türkçe dahil birden çok dilde metin saklanacaksa nvarchar en güvenli seçimdir. SQL Server 2019 ve sonrasında UTF-8 etkin bir harmanlama (collation) ile varchar da tüm Unicode karakterlerini saklayabilir. Yalnızca ASCII içeren kod, e-posta, barkod gibi alanlar için varchar yeterlidir.

Dolu bir tabloya NOT NULL sütun nasıl eklenir?

ALTER TABLE ... ADD ile sütunu NOT NULL olarak tanımlarken aynı komutta bir DEFAULT kısıtı verin; mevcut satırlar bu varsayılan değerle doldurulur. DEFAULT vermeden NOT NULL sütun eklemek yalnızca tablo boşsa mümkündür.

SQL Server'da TIMESTAMP veri tipi tarih saklar mı?

Hayır. SQL Server'da timestamp, rowversion veri tipinin eski eş anlamlısıdır ve satır her değiştiğinde artan ikili bir sürüm numarası tutar. Tarih ve saat için datetime2, saat dilimi bilgisi gerekiyorsa datetimeoffset kullanılmalıdır.

Kaynaklar

  1. Microsoft Learn — CREATE TABLE (Transact-SQL) learn.microsoft.com
  2. Microsoft Learn — ALTER TABLE (Transact-SQL) learn.microsoft.com
  3. Microsoft Learn — TRUNCATE TABLE (Transact-SQL) learn.microsoft.com
  4. Microsoft Learn — char and varchar (Transact-SQL) learn.microsoft.com
  5. Microsoft Learn — datetime2 (Transact-SQL) learn.microsoft.com
  6. Microsoft Learn — Unique constraints and check constraints learn.microsoft.com

SQL ServerT-SQLCREATE TABLEALTER TABLEkısıtlarveritabanı tasarımıMarkdown sürümü

İletişim

Konuşalım.