Mert Özen Her Satırda Daha İleri
Veritabanı SQL Temelleri — #25

Günlük DDL ve Idempotent Script'ler: IDENTITY ve SEQUENCE, Computed Column, CREATE OR ALTER, DROP IF EXISTS ve Synonym

Mert Özen 15 Kas 2026 17 dk 7 görüntülenme
Günlük DDL ve Idempotent Script'ler: IDENTITY ve SEQUENCE, Computed Column, CREATE OR ALTER, DROP IF EXISTS ve Synonym

Tekrar çalıştırılabilir bir deploy script'i her adımı OBJECT_ID/COL_LENGTH kontrolüne bağlar, view ve procedure için CREATE OR ALTER, diğerleri için DROP ... IF EXISTS kullanır ve DDL'i XACT_ABORT ON ile transaction'a alır. Identity ve sequence boşluklarının kaynağı, IDENTITY_INSERT ve CHECKIDENT, paylaşılan sipariş numarası sequence'ı, PERSISTED computed column, sp_rename'in kırdığı view ve synonym gerçek çıktılarla; serinin son yazısı.

Sürüm notu: Bu yazıdaki SQL sorguları SQL Server 2025 (17.0, Cumulative Update 10) üzerinde serinin Bookstore veritabanıyla, PostgreSQL karşılaştırmaları PostgreSQL 18.7 üzerinde çalıştırılmıştır.

Önce Tahmin Et: Deploy Script'i İkinci Kez Çalışınca

Sadakat programı için müşterilere bir loyalty_tier column'u ekleyen ve telefon numarasını tekil yapan iki satırlık bir deploy script'i:

ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
Msg 1505, Level 16, State 1, Line 2
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.customers' and the index name 'UX_customers_phone'. The duplicate key value is (<NULL>).
The statement has been terminated.

Index oluşmadı: 21 müşterinin telefonu NULL ve SQL Server unique index'te NULL'ları birbirine eşit sayıyor. Hatayı gören ekip arkadaşın script'i bir daha çalıştırıyor. Ne olur?

ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
Msg 2705, Level 16, State 4, Line 1
Column names in each table must be unique. Column name 'loyalty_tier' in table 'dbo.customers' is specified more than once.
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes,
       INDEXPROPERTY(OBJECT_ID('dbo.customers'), 'UX_customers_phone', 'IndexId') AS ux_index_id;
loyalty_tier_bytes  ux_index_id
------------------  -----------
10                  NULL

İlk çalıştırma yarıda kaldı: column eklendi, index eklenmedi. İkinci çalıştırma artık ilk satırda takılıyor, çünkü column zaten var. Veritabanı ne eski ne yeni hâlinde; script de ne bu durumu düzeltebiliyor ne de geri alabiliyor. Bu yazı serinin son yazısı ve bu sorunun iki tarafını ele alıyor: günlük DDL (Data Definition Language; şemayı tanımlayan CREATE, ALTER, DROP komutları) araçları olan IDENTITY, SEQUENCE ve computed column'lar, ve iki kez çalıştırılınca hata vermeyen, yarıda kalmayan deploy script'leri. Örnekler sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) veritabanında.

IDENTITY: Seed, Increment ve Boşluklar

IDENTITY(seed, increment) 4. yazıdan beri bütün Bookstore table'larının primary key'inde. 22. yazıda geri alınan bir insert'ten sonra IDENT_CURRENT'in geri dönmediğini görmüştük; boşlukların nereden geldiğini toplu görelim:

SELECT IDENT_SEED('dbo.orders') AS seed, IDENT_INCR('dbo.orders') AS increment, IDENT_CURRENT('dbo.orders') AS current_value;
BEGIN TRAN;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Pending');
ROLLBACK;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Unknown');
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Pending');
SELECT TOP (2) order_id, status FROM dbo.orders ORDER BY order_id DESC;
seed  increment  current_value
----  ---------  -------------
1     1          379

(1 rows affected)

(1 rows affected)
Msg 547, Level 16, State 1, Line 5
The INSERT statement conflicted with the CHECK constraint "CK_orders_status". The conflict occurred in database "sqlseries_ext_25", table "dbo.orders", column 'status'.
The statement has been terminated.

(1 rows affected)
order_id  status
--------  -------
382       Pending
379       Pending

379'dan sonraki sipariş 382 oldu. 380'i geri alınan transaction, 381'i CK_orders_status'a takılan insert harcadı. Identity değeri insert anında ayrılır ve ne rollback ne de hata onu geri verir; böylece eş zamanlı oturumlar birbirini beklemeden numara alır. Buna bir de sunucu yeniden başlarken kaybolan değerler eklenir: SQL Server performans için identity değerlerini önceden ayırıp bellekte tutar ve beklenmedik bir kapanmada bu değerler atlanır. Veritabanı seviyesindeki IDENTITY_CACHE ayarı (sys.database_scoped_configurations'ta varsayılan değeri 1) bunu kapatır, ama insert performansından ödün vererek; kapatsan da rollback boşlukları kalır. Sonuç: identity bir kimliktir, sıra numarası değil. "Sipariş numaraları ardışık olmalı" diye bir iş kuralı varsa onu identity'ye yükleme.

Identity'nin durumu DBCC CHECKIDENT ile okunur ve gerektiğinde değiştirilir:

DBCC CHECKIDENT ('dbo.orders', NORESEED);
Checking identity information: current identity value '382', current column value '382'.
DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Bir boşluğu elle doldurmak ya da başka bir sistemden id'leriyle veri taşımak için identity column'a değer yazman gerekir. Varsayılan olarak buna izin verilmez:

INSERT INTO dbo.orders (order_id, customer_id, ordered_at, status) VALUES (381, 1, '2026-06-30 10:00', 'Pending');
Msg 544, Level 16, State 1, Line 1
Cannot insert explicit value for identity column in table 'orders' when IDENTITY_INSERT is set to OFF.
SET IDENTITY_INSERT dbo.orders ON;
INSERT INTO dbo.orders (order_id, customer_id, ordered_at, status) VALUES (381, 1, '2026-06-30 10:05', 'Pending');
SET IDENTITY_INSERT dbo.orders OFF;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:10', 'Pending');
SELECT TOP (3) order_id, ordered_at FROM dbo.orders ORDER BY order_id DESC;
order_id  ordered_at
--------  -------------------
383       2026-06-30 10:10:00
382       2026-06-30 10:00:00
381       2026-06-30 10:05:00

IDENTITY_INSERT açıkken column listesini yazmak zorunlu ve aynı anda oturum başına yalnız bir table'da açık olabilir. Yazdığın değer mevcut değerden büyükse identity oradan devam eder; küçükse (381 gibi) olduğu yerde kalır ve sıradaki insert 383 alır. Test verisini temizledikten sonra sayacı geri çekmek için RESEED:

DELETE FROM dbo.orders WHERE order_id > 379;
DBCC CHECKIDENT ('dbo.orders', RESEED, 379);
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:15', 'Pending');
SELECT TOP (1) order_id FROM dbo.orders ORDER BY order_id DESC;
(3 rows affected)
Checking identity information: current identity value '383'.
DBCC execution completed. If DBCC printed error messages, contact your system administrator.

(1 rows affected)
order_id
--------
380

RESEED'i mevcut en büyük id'nin altına çekersen sıradaki insert'ler var olan id'lere çarpar ve primary key hatası alırsın. Production'da RESEED kullanman gereken durum çok azdır; çoğu zaman test ortamında temizlik içindir.

SEQUENCE: Table'dan Bağımsız Numara

SEQUENCE bir table'a bağlı olmayan bir sayaç nesnesidir; NEXT VALUE FOR ile değer verir. Bookstore'da müşteriye gösterilen sipariş numarası için kullanacağız: id'den ayrı, 100001'den başlayan ve hem bireysel siparişlerin hem kurumsal siparişlerin (Institutional module) ortak kullandığı bir numara. Script baştan tekrar çalıştırılabilir yazıldı:

IF OBJECT_ID('dbo.order_number_seq', 'SO') IS NULL
    CREATE SEQUENCE dbo.order_number_seq AS INT START WITH 100001 INCREMENT BY 1 CACHE 50;
IF COL_LENGTH('dbo.orders', 'order_number') IS NULL
    ALTER TABLE dbo.orders ADD order_number INT NULL
        CONSTRAINT DF_orders_order_number DEFAULT (NEXT VALUE FOR dbo.order_number_seq);
DROP TABLE IF EXISTS dbo.institution_orders;
CREATE TABLE dbo.institution_orders (
    institution_order_id INT IDENTITY(1,1) CONSTRAINT PK_institution_orders PRIMARY KEY,
    institution_name     NVARCHAR(100) NOT NULL,
    order_number         INT NOT NULL CONSTRAINT DF_institution_orders_order_number DEFAULT (NEXT VALUE FOR dbo.order_number_seq)
);
GO
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 11:00', 'Pending'), (3, '2026-06-30 11:05', 'Pending');
INSERT INTO dbo.institution_orders (institution_name) VALUES (N'Hilltop School Library');
SELECT 'orders' AS source, order_id AS id, order_number FROM dbo.orders WHERE order_number IS NOT NULL
UNION ALL
SELECT 'institution_orders', institution_order_id, order_number FROM dbo.institution_orders
ORDER BY order_number;
source              id   order_number
------------------  ---  ------------
orders              381  100001
orders              382  100002
institution_orders  1    100003

İki table aynı sayaçtan numara aldı; identity ile bunu yapamazsın, çünkü identity tek bir table'ın column'una bağlı. Column'u NULL kabul eden bir column olarak ekledik ve DEFAULT yalnız yeni row'lara uygulandı; mevcut 380 sipariş NULL kaldı. Eski siparişlere de numara vermek istersen bunu ayrı ve bilinçli bir adım olarak yap (NEXT VALUE FOR ... OVER (ORDER BY ordered_at) ile sırayı da belirleyebilirsin). SEQUENCE'ın iki inceliği daha var:

SELECT NEXT VALUE FOR dbo.order_number_seq AS peek_consumes;
DECLARE @first SQL_VARIANT, @last SQL_VARIANT;
EXEC sys.sp_sequence_get_range @sequence_name = N'dbo.order_number_seq', @range_size = 100,
     @range_first_value = @first OUTPUT, @range_last_value = @last OUTPUT;
SELECT @first AS range_first, @last AS range_last;
SELECT current_value, cache_size, is_cached FROM sys.sequences WHERE name = 'order_number_seq';
peek_consumes
-------------
100004

(1 rows affected)
range_first  range_last
-----------  ----------
100005       100104

(1 rows affected)
current_value  cache_size  is_cached
-------------  ----------  ---------
100104         50          1

"Sıradaki numara ne olacak?" diye NEXT VALUE FOR çağırmak o numarayı harcar; 100004 artık hiçbir siparişe verilmeyecek. Sadece bakmak istiyorsan sys.sequences.current_value'yu oku. CACHE 50 değerleri 50'şerli bloklar hâlinde bellekte ayırır ve her değer için diske yazmaz; bedeli, beklenmedik bir kapanmada bloğun kalanının atlanması. Uygulama tarafında bir seferde birçok numara gerekiyorsa sys.sp_sequence_get_range bir aralığı tek çağrıda ayırır: 100 numaralık istek 100005–100104 aralığını verdi ve current_value aralığın sonuna atladı; o numaraları kullanmak artık uygulamanın işi. Yani sequence de identity gibi boşluklu çalışır. Fatura numarası gibi yasal olarak boşluksuz olması gereken numaralar için ayrı bir tasarım gerekir; Çözüm Desenleri serisinin "Numaralandırma: Fatura ve Sipariş Numaraları, Gapless Sequence ve Yıllık Sıfırlama" yazısı bunu ve yıllık sıfırlamayı anlatıyor.

Computed Column ve PERSISTED

Computed column bir ifadeden hesaplanan column'dur. Varsayılan olarak saklanmaz, her okumada hesaplanır; PERSISTED ile hesaplanan değer row'a yazılır ve kaynak column'lar değiştikçe güncellenir. Sipariş kaleminin tutarı:

IF COL_LENGTH('dbo.order_items', 'line_total') IS NULL
    ALTER TABLE dbo.order_items ADD line_total AS (quantity * unit_price) PERSISTED;
GO
SELECT TOP (3) order_item_id, quantity, unit_price, line_total FROM dbo.order_items WHERE quantity > 1 ORDER BY order_item_id;
SELECT c.name, c.is_computed, cc.is_persisted, TYPE_NAME(c.system_type_id) AS type_name, c.precision, c.scale
FROM sys.columns AS c
LEFT JOIN sys.computed_columns AS cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
WHERE c.object_id = OBJECT_ID('dbo.order_items') AND c.name = 'line_total';
order_item_id  quantity  unit_price  line_total
-------------  --------  ----------  ----------
1              2         290.90      581.80
5              3         440.00      1320.00
10             2         460.00      920.00

(3 rows affected)
name        is_computed  is_persisted  type_name  precision  scale
----------  -----------  ------------  ---------  ---------  -----
line_total  1            1             decimal    21         2

Tip ifadeden geliyor: INT * DECIMAL(10,2) → DECIMAL(21,2) (22. yazıdaki precision kuralları). Computed column'a doğrudan yazamazsın; UPDATE ... SET line_total = 0 Msg 271 ile reddedildi. PERSISTED olabilmesi için ifadenin deterministic olması gerekir, yani aynı girdiye her zaman aynı sonucu vermesi:

ALTER TABLE dbo.orders ADD age_days AS DATEDIFF(day, ordered_at, SYSUTCDATETIME()) PERSISTED;
Msg 4936, Level 16, State 1, Line 1
Computed column 'age_days' in table 'orders' cannot be persisted because the column is non-deterministic.

Siparişin yaşı her gün değişir; saklanması anlamsız. PERSISTED'sız hâli eklenebilir ve her okumada hesaplanır. Ne zaman hangisi: okuma sıklığı yüksek ve ifadesi pahalı olan değerleri PERSISTED yap; index'lenecek computed column'lar deterministic olmak zorunda, ama PERSISTED olmak zorunda değil (23. yazıdaki JSON_VALUE column'u persisted değildi). 20. yazıda AT TIME ZONE'un da aynı sebeple persisted computed column'a giremediğini görmüştük.

DDL'i Transaction'a Almak ve Tekrar Çalıştırılabilir Yazmak

Açılıştaki yarım kalmış script'e dönelim. SQL Server'da ALTER TABLE, CREATE INDEX gibi DDL statement'larının çoğu transaction içinde çalışır ve geri alınabilir. Önce yarım kalan column'u kaldırıp aynı script'i transaction'a alalım:

ALTER TABLE dbo.customers DROP COLUMN IF EXISTS loyalty_tier;
GO
SET XACT_ABORT ON;
BEGIN TRAN;
ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone);
COMMIT;
GO
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes, @@TRANCOUNT AS open_tran;
Msg 1505, Level 16, State 1, Line 4
The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.customers' and the index name 'UX_customers_phone'. The duplicate key value is (<NULL>).
loyalty_tier_bytes  open_tran
------------------  ---------
NULL                0

Hata aynı, ama bu kez column da geri alındı: veritabanı script'ten önceki hâlinde. Şimdi asıl düzeltmeyi yapalım. Telefonu olmayan müşteriler için filtered index (yalnız WHERE koşulunu sağlayan row'ları içeren index) kullanıyoruz ve her adımı "zaten yapılmış mı" kontrolüne bağlıyoruz. GO 2 script'i art arda iki kez çalıştırıyor:

SET XACT_ABORT ON;
BEGIN TRAN;
IF COL_LENGTH('dbo.customers', 'loyalty_tier') IS NULL
    ALTER TABLE dbo.customers ADD loyalty_tier VARCHAR(10) NULL;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.customers') AND name = 'UX_customers_phone')
    CREATE UNIQUE INDEX UX_customers_phone ON dbo.customers (phone) WHERE phone IS NOT NULL;
COMMIT;
PRINT 'migration applied';
GO 2
SELECT COL_LENGTH('dbo.customers', 'loyalty_tier') AS loyalty_tier_bytes,
       INDEXPROPERTY(OBJECT_ID('dbo.customers'), 'UX_customers_phone', 'IndexId') AS ux_index_id;
migration applied
migration applied
loyalty_tier_bytes  ux_index_id
------------------  -----------
10                  3

İki çalıştırma da hatasız bitti; ikincisinde iki IF de yapılacak iş bulamadı. Kontrollerin araçları 22. yazıda gördüğümüz metadata function'ları: tablo, view, procedure ve sequence için OBJECT_ID(name, type) ('U' table, 'V' view, 'P' procedure, 'SO' sequence), column için COL_LENGTH, index için sys.indexes, constraint için sys.objects ya da OBJECT_ID.

Bu tür script'lerde bir tuzak daha var. Bir column'u ekleyip aynı batch'te kullanmak:

IF COL_LENGTH('dbo.orders', 'order_code') IS NULL
    ALTER TABLE dbo.orders ADD order_code AS CONCAT('BK-', order_number);
SELECT TOP (2) order_id, order_number, order_code FROM dbo.orders WHERE order_number IS NOT NULL ORDER BY order_id;
Msg 207, Level 16, State 1, Line 3
Invalid column name 'order_code'.

Batch bütün olarak derlenir; derleme anında order_code yok, bu yüzden ALTER TABLE da çalışmadı. 24. yazıdaki batch kuralının DDL'deki karşılığı: yeni column'u kullanan statement'ı bir sonraki batch'e (GO'dan sonra) koy.

IF COL_LENGTH('dbo.orders', 'order_code') IS NULL
    ALTER TABLE dbo.orders ADD order_code AS CONCAT('BK-', order_number);
GO
SELECT TOP (2) order_id, order_number, order_code FROM dbo.orders WHERE order_number IS NOT NULL ORDER BY order_id;
order_id  order_number  order_code
--------  ------------  ----------
381       100001        BK-100001
382       100002        BK-100002

Batch'lere bölünmüş bir script'te transaction batch'ler arasında da açık kalabilir, ama XACT_ABORT bir batch'te transaction'ı geri alırsa sonraki batch'ler transaction dışında çalışmaya devam eder. Bu yüzden çok batch'li deploy script'ini sqlcmd ile :on error exit (24. yazı) kullanarak çalıştır ve her adımı tekrar çalıştırılabilir yaz; ikisi birlikte "yarıda kalırsa düzelt, tekrar çalıştır" akışını güvenli yapar.

CREATE OR ALTER ve DROP ... IF EXISTS

Programlanabilir nesneler (view, stored procedure, function, trigger) için kontrol yazmaya gerek yok; SQL Server 2016 SP1'den (ilk service pack, yani birinci büyük güncelleme paketi) beri CREATE OR ALTER var. Nesne yoksa oluşturur, varsa yetkilerini koruyarak tanımını değiştirir:

CREATE OR ALTER VIEW dbo.v_book_prices AS
SELECT book_id, title, list_price FROM dbo.books WHERE is_active = 1;
GO
CREATE OR ALTER VIEW dbo.v_book_prices AS
SELECT book_id, title, list_price, listed_on FROM dbo.books WHERE is_active = 1;
GO
SELECT TOP (2) * FROM dbo.v_book_prices ORDER BY book_id;
book_id  title                list_price  listed_on
-------  -------------------  ----------  ----------
1        Pride and Prejudice  230.90      2024-06-01
2        Emma                 680.50      2024-06-01

Eski kalıp olan "varsa DROP, sonra CREATE" nesneye verilmiş yetkileri de siler ve iki adım arasında nesne bir an yoktur; CREATE OR ALTER ikisini de yapmaz. Table, index ve diğer nesneler için kısa yol DROP ... IF EXISTS:

ALTER TABLE dbo.customers DROP COLUMN IF EXISTS no_such_column;
ALTER TABLE dbo.customers DROP CONSTRAINT IF EXISTS no_such_constraint;
DROP INDEX IF EXISTS no_such_index ON dbo.customers;
DROP SEQUENCE IF EXISTS dbo.no_such_seq;
DROP SYNONYM IF EXISTS dbo.no_such_synonym;
PRINT 'nothing to drop, no error';
nothing to drop, no error

Table için CREATE OR ALTER yok, olamaz da: var olan bir table'ın tanımını "değiştirmek" verinin ne olacağı sorusunu doğurur. Table'larda IF OBJECT_ID(..., 'U') IS NULL CREATE TABLE, column'larda IF COL_LENGTH(...) IS NULL ALTER TABLE ... ADD kalıbı geçerli. DROP TABLE IF EXISTS'i deploy script'inde dikkatle kullan: tekrar çalıştırılabilir ama verinin tamamını siler; yalnız geçici ya da staging table'larda yerinde.

sp_rename, Bağımlılıklar ve Synonym

Bir column'un adını değiştirmek tek komut, ama kimin o adı kullandığını önce sorman gerekir:

SELECT referencing_schema_name, referencing_entity_name FROM sys.dm_sql_referencing_entities('dbo.books', 'OBJECT');
referencing_schema_name  referencing_entity_name
-----------------------  -----------------------
dbo                      CK_books_list_price
dbo                      CK_books_page_count
dbo                      v_book_prices
EXEC sp_rename 'dbo.books.listed_on', 'listed_date', 'COLUMN';
GO
SELECT TOP (2) * FROM dbo.v_book_prices ORDER BY book_id;
Caution: Changing any part of an object name could break scripts and stored procedures.
Msg 207, Level 16, State 1, Procedure v_book_prices, Line 2
Invalid column name 'listed_on'.
Msg 4413, Level 16, State 1, Line 1
Could not use view or function 'dbo.v_book_prices' because of binding errors.

sp_rename yalnız bir uyarı verdi ve işini yaptı; kırılma view ilk kullanıldığında ortaya çıktı. View, procedure ve function'ların içindeki metin güncellenmez; uygulama kodu, raporlar ve ETL (Extract, Transform, Load; veriyi başka sistemlere taşıyan süreçler) işleri de eski adı kullanmaya devam eder. Geri alalım:

EXEC sp_rename 'dbo.books.listed_date', 'listed_on', 'COLUMN';
GO
SELECT TOP (1) book_id, listed_on FROM dbo.v_book_prices ORDER BY book_id;
Caution: Changing any part of an object name could break scripts and stored procedures.
book_id  listed_on
-------  ----------
1        2024-06-01

View'u WITH SCHEMABINDING ile tanımlarsan bu sessiz kırılma olmaz: SQL Server referans verilen column'un adını değiştirmeyi baştan reddeder. list_price'ı yeniden adlandırmayı denediğimde, üzerinde CK_books_list_price olduğu için aynı mantıkla "Msg 15336 ... cannot be renamed because the object participates in enforced dependencies." hatasını aldım:

CREATE OR ALTER VIEW dbo.v_book_prices WITH SCHEMABINDING AS
SELECT book_id, title, list_price, listed_on FROM dbo.books WHERE is_active = 1;
GO
EXEC sp_rename 'dbo.books.listed_on', 'listed_date', 'COLUMN';
Msg 15336, Level 16, State 1, Procedure sp_rename, Line 774
Object 'dbo.books.listed_on' cannot be renamed because the object participates in enforced dependencies.

Production'da yeniden adlandırma çoğu zaman tek adımda değil, geçiş dönemli yapılır: yeni column eklenir, iki column bir süre birlikte yaşar, okuyan ve yazan kod taşınır, en son eski column silinir. Table'ın yerini değiştirirken synonym işe yarar. Synonym başka bir nesneye verilmiş takma addır; kod synonym'i kullanır, sen arkadaki nesneyi değiştirirsin:

CREATE OR ALTER SYNONYM dbo.catalog_books FOR dbo.books;
Msg 102, Level 15, State 1, Line 1
Incorrect syntax near 'SYNONYM'.
DROP SYNONYM IF EXISTS dbo.catalog_books;
CREATE SYNONYM dbo.catalog_books FOR dbo.books;
SELECT COUNT(*) AS via_synonym FROM dbo.catalog_books;
SELECT name, base_object_name FROM sys.synonyms;
via_synonym
-----------
77

(1 rows affected)
name           base_object_name
-------------  ----------------
catalog_books  [dbo].[books]

Synonym için CREATE OR ALTER desteklenmiyor; tekrar çalıştırılabilir hâli DROP SYNONYM IF EXISTS + CREATE SYNONYM. Tipik kullanım: katalog verisini arka planda yeni bir table'a (ör. books_v2) hazırlayıp synonym'i tek bir kısa transaction'da yeni table'a çevirmek, ya da başka bir veritabanındaki table'a uygulamanın kodunu değiştirmeden işaret etmek. Synonym bir isim yönlendirmesidir; arkadaki table'ın column'ları değişirse kod yine kırılır.

SQL Server ve PostgreSQL Karşılıkları

PostgreSQL sütunundaki ifadeleri kendi PostgreSQL 18 container'ımda çalıştırdım:

SQL ServerDavranışPostgreSQL
IDENTITY(1,1)Rollback ve hata boşluk bırakırGENERATED ALWAYS AS IDENTITY; elle değer için OVERRIDING SYSTEM VALUE
SET IDENTITY_INSERT ONOturum başına bir tableOVERRIDING SYSTEM VALUE (statement bazında)
CREATE SEQUENCE ... CACHE, NEXT VALUE FORBoşluklu; okumak tüketirCREATE SEQUENCE, nextval()
Computed column, PERSISTEDPersisted için deterministic şartGENERATED ALWAYS AS (...); 18'de varsayılan VIRTUAL, STORED seçilebilir
CREATE OR ALTERView, procedure, function, triggerCREATE OR REPLACE (view, function, procedure)
DROP ... IF EXISTSYoksa sessiz geçerDROP ... IF EXISTS, ADD COLUMN IF NOT EXISTS, CREATE INDEX IF NOT EXISTS
Unique indexBirden çok NULL'a izin vermezNULL'lar farklı sayılır; NULLS NOT DISTINCT ile SQL Server gibi
sp_rename columnBağlı view kırılırALTER TABLE ... RENAME COLUMN; view çalışmaya devam eder
SynonymNesne için takma adYok; search_path ya da view

İki fark özellikle deploy script'lerini etkiliyor. PostgreSQL'de açılıştaki telefon index'i hatasız oluştu, çünkü unique index'te NULL'lar birbirinden farklı sayılıyor. Ve PostgreSQL'de DDL'in neredeyse tamamı transaction içinde çalışıyor: bir transaction'da column ekleyip ardından hata veren bir insert yaptığımda column da geri alındı. Column'u yeniden adlandırdığımda ise view çalışmaya devam etti; PostgreSQL view'u column adıyla değil column'un kendisiyle saklıyor ve tanımını phone_number AS phone olarak güncelliyor.

Üretimde Dikkat

Her deploy script'i iki soruya cevap vermeli: yarıda kalırsa veritabanı hangi durumda olur ve ikinci kez çalıştırılırsa ne olur? Cevap "script'ten önceki hâli" ve "hiçbir şey" olmalı. Bunun için her adımı varlık kontrolüne bağla (OBJECT_ID, COL_LENGTH, sys.indexes, CREATE OR ALTER, DROP ... IF EXISTS), adımları mümkünse tek transaction'da ve XACT_ABORT ON ile çalıştır, çok batch'li script'leri :on error exit ile çalıştır. Büyük table'lardaki CREATE INDEX ve column tipi değişikliği gibi uzun süren, kilit tutan adımları ayrı bir pencereye ve ayrı bir script'e koy; online index işlemleri ve bunların kilit davranışı SQL Server Derinlemesine serisinde.

Identity ve sequence boşluklarını normal kabul et ve iş kurallarını onlara dayandırma. sp_rename'den önce sys.dm_sql_referencing_entities ile bağımlılıklara bak, ama veritabanı dışındaki kullanıcıları (uygulama, rapor, ETL) bu sorgunun göremeyeceğini unutma. Şema değişikliklerinin versiyonlanması ve araçlarla (SQL Database Projects, DbUp, Flyway) yönetilmesi SQL Server Derinlemesine serisinin ilerideki şema değişikliği yazısının konusu; bu yazıdaki kalıplar o araçların ürettiği ya da çalıştırdığı script'lerin de temeli.

Sık Düşülen Tuzaklar

  • Identity ya da sequence'ta boşluk olmayacağını varsaymak; rollback, hata, cache ve yeniden başlatma boşluk bırakır.
  • "Sıradaki numara ne?" diye NEXT VALUE FOR çağırıp numarayı harcamak; sys.sequences.current_value'yu oku.
  • Transaction'sız ve kontrolsüz bir deploy script'ini yarıda bırakmak; ikinci çalıştırma Msg 2705 gibi hatalarla takılır.
  • Yeni column'u ekleyip aynı batch'te kullanmak (Msg 207, batch hiç çalışmaz).
  • sp_rename'in uyarısını önemsememek; bağlı view ve procedure'ler ilk kullanımda kırılır.
  • Nondeterministic bir ifadeyi PERSISTED yapmaya çalışmak (Msg 4936).
  • Unique index'in birden çok NULL'a izin vereceğini sanmak; filtered index kullan.
  • Deploy script'inde DROP TABLE IF EXISTS ile veri taşıyan bir table'ı "tekrar çalıştırılabilir" yapmak.

Küçük Bir Deneme

★☆☆ books table'ına weight_grams column'u ve CHECK (weight_grams > 0) constraint'i ekleyen, iki kez çalıştırılınca hata vermeyen bir script yaz. Constraint'in varlığını OBJECT_ID('dbo.CK_books_weight_grams', 'C') ile kontrol et.

★★☆ Mevcut 380 siparişe order_number ver: numaralar sipariş tarihine göre sıralı olsun ve script tekrar çalıştırılınca numarası olan siparişlere dokunmasın. Sonra order_number'ı NOT NULL yapıp üzerine unique constraint ekle.

★★★ dbo.books'u dbo.books_v2'ye kopyalayıp list_price'ı DECIMAL(12,2) yapan, ardından dbo.catalog_books synonym'ini tek bir transaction içinde yeni table'a çeviren bir script yaz. Synonym'i kullanan bir view varsa ne oluyor? sys.dm_sql_referencing_entities synonym'in arkasını görüyor mu?

Bu yazıyla SQL Temelleri serisini tamamladık. 25 yazıda ilişkisel modelden ve veri tiplerinden başlayıp SELECT, join'ler, gruplama, subquery, CTE (WITH ile tanımlanan, sorgu içinde isimli geçici sonuç kümesi), window function ve veri değiştirmeye; ardından lehçe farklarına, SQL Server 2022 ve 2025 yeniliklerine, T-SQL'in günlük function kataloğuna, JSON'a, script yazmaya ve deploy script'lerine kadar sorgu dilinin kendisini gördük. Aynı Bookstore veritabanıyla devam eden SQL Server Derinlemesine serisi bu sorguların arkasına geçiyor: index'ler, execution plan'lar, statistics, transaction isolation ve lock'lar, stored procedure'ler ve tempdb. Bir sorgunun ne döndürdüğünü bu seride öğrendin; neden yavaş ya da neden kilitli olduğunu orada öğreneceksin.