T-SQL Matematik, Sistem ve Metadata Function'ları: ROUND, NEWID, SCOPE_IDENTITY, @@ROWCOUNT, OBJECT_ID ve Catalog View'lar
Yeni eklenen row'un id'si için SCOPE_IDENTITY ya da OUTPUT INTO kullanılır; trigger'lı bir table'da @@IDENTITY trigger'ın eklediği row'un id'sini döndürür ve INTO'suz OUTPUT Msg 334 verir. ROUND'un taşması, POWER(2, 0.5)'in 1 dönmesi, AVG(int), decimal precision kuralları, RAND ve ORDER BY NEWID(), @@ROWCOUNT'un sıfırlanması ve index'siz foreign key'leri bulan catalog view sorgusu gerçek çıktılarla.
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: 9,99'u Bir Basamağa Yuvarlamak
DECIMAL(3,2) tipinde 9,99 TL'lik bir indirim tutarını tek ondalık basamağa yuvarlıyoruz. Sonuç 10,0 mı?
SELECT ROUND(CAST(9.99 AS DECIMAL(3,2)), 1) AS overflow;
Msg 8115, Level 16, State 2, Line 1
Arithmetic overflow error converting expression to data type numeric.
Hata. ROUND'un sonucu girdiyle aynı tipte, yani DECIMAL(3,2): virgülden önce tek basamak. 10,00 bu tipe sığmıyor. Fiyat column'ları genelde geniş tanımlandığı için bunu üretimde nadiren görürsün; ama dar bir ara hesapta, bir staging column'unda ya da bir CHECK'te karşına çıkabilir. Bu yazı T-SQL function kataloğunun son parçası: matematik function'ları ve tamsayı bölmesi, rastgele değer üretmek, yeni eklenen row'un id'sini doğru almak, @@ROWCOUNT gibi sistem function'ları ve şemayı sorguyla keşfetmek. Örnekler sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) veritabanında.
ROUND, CEILING, FLOOR ve Diğer Matematik Function'ları
SELECT ROUND(2.345, 2) AS r2, ROUND(2.345, 2, 1) AS truncated, ROUND(-2.5, 0) AS neg_half, ROUND(2.5, 0) AS pos_half,
ROUND(1234.5, -2) AS to_hundreds, ROUND(CAST(2.5 AS FLOAT), 0) AS float_half;
r2 truncated neg_half pos_half to_hundreds float_half
----- --------- -------- -------- ----------- ----------
2.350 2.340 -3.0 3.0 1200.0 3.0
ROUND(x, n) yarımları sıfırdan uzağa yuvarlar: 2,5 → 3, -2,5 → -3. Üçüncü argüman 0 dışında bir değer olursa yuvarlamaz, keser: 2,345 → 2,340. Negatif n virgülün soluna yuvarlar; 1234,5'i yüzlere yuvarlamak 1200. Sonucun ölçeği (scale) değişmez: 2,345 yuvarlanınca 2,35 değil 2,350 görünüyor. PostgreSQL'de round(2.5::float8) 2 döndürüyor; float'ta yarımları çifte yuvarlıyor (banker's rounding, yani yarımı en yakın çift sayıya yuvarlama), numeric'te ise SQL Server gibi sıfırdan uzağa. Para hesabını iki sistemde de DECIMAL/numeric ile yapmanın bir sebebi daha.
SELECT CEILING(2.1) AS ceil_pos, CEILING(-2.1) AS ceil_neg, FLOOR(-2.1) AS floor_neg, ABS(-7) AS abs_val,
POWER(2, 0.5) AS pow_int_base, POWER(2.0, 0.5) AS pow_dec_base, SQRT(2) AS sqrt_val, SIGN(-3.5) AS sign_val, 17 % 5 AS modulo;
ceil_pos ceil_neg floor_neg abs_val pow_int_base pow_dec_base sqrt_val sign_val modulo
-------- -------- --------- ------- ------------ ------------ ------------------ -------- ------
3 -2 -3 7 1 1.4 1.4142135623730951 -1.0 2
CEILING yukarı, FLOOR aşağı yuvarlar; negatif sayılarda "yukarı" sıfıra doğru demek: CEILING(-2.1) -2. "Kaç koli gerekir" hesabı (CEILING(adet / 1.0 / koli_kapasitesi)) tipik kullanımı. POWER'ın tuzağı sonuç tipinin birinci argümandan gelmesi: POWER(2, 0.5) 1 döndürdü, çünkü 2 bir INT ve √2 = 1,414 INT'e kesildi. POWER(2.0, 0.5) ise 1,4 döndürdü, çünkü 2.0 DECIMAL(2,1): tek ondalık basamak. Doğru sonucu istiyorsan tabanı FLOAT ya da yeterince geniş bir DECIMAL yap. SQRT, LOG, EXP gibi function'lar her zaman FLOAT döndürür.
Tamsayı Bölmesi ve Decimal Precision
SELECT 7 / 2 AS int_div, 7 / 2.0 AS dec_div, 7 % 2 AS remainder, CAST(7 AS DECIMAL(10,2)) / 3 AS dec_by_int;
int_div dec_div remainder dec_by_int
------- -------- --------- ----------
3 3.500000 1 2.333333
İki INT'in bölümü INT'tir ve kesilir: 7 / 2 = 3. Bookstore'da en sık rastlanan hali yorum puanının ortalaması, çünkü reviews.rating bir TINYINT ve AVG girdinin tipinde sonuç döndürür:
SELECT AVG(rating) AS avg_int, AVG(CAST(rating AS DECIMAL(5,2))) AS avg_dec, AVG(rating * 1.0) AS avg_times_one
FROM dbo.reviews
WHERE book_id = 26;
avg_int avg_dec avg_times_one
------- -------- -------------
4 4.400000 4.400000
Dune'un ortalaması 4,4 ama AVG(rating) 4 dedi; kitap kartında "4 yıldız" ile "4,4 yıldız" arasındaki fark bu. PostgreSQL'de avg tamsayı girdide numeric döndürür, bu tuzak orada yok. DECIMAL işlemlerinde ise sonucun precision ve scale'ini SQL Server kurallarla hesaplar:
DECLARE @a DECIMAL(10,2) = 1234.56, @b DECIMAL(10,2) = 3;
SELECT @a * @b AS product, @a / @b AS quotient,
SQL_VARIANT_PROPERTY(@a * @b, 'Precision') AS p_prec, SQL_VARIANT_PROPERTY(@a * @b, 'Scale') AS p_scale,
SQL_VARIANT_PROPERTY(@a / @b, 'Precision') AS q_prec, SQL_VARIANT_PROPERTY(@a / @b, 'Scale') AS q_scale;
product quotient p_prec p_scale q_prec q_scale
--------- ----------------- ------ ------- ------ -------
3703.6800 411.5200000000000 21 4 23 13
Çarpımda precision'lar ve scale'ler toplanır: DECIMAL(10,2) * DECIMAL(10,2) → DECIMAL(21,4). Bölmede scale büyür: DECIMAL(23,13). Sonuç 38 basamağı aşacaksa SQL Server scale'den keser ve en az 6 basamak bırakır:
DECLARE @x DECIMAL(38,10) = 1, @y DECIMAL(38,10) = 3;
SELECT @x / @y AS big_div, SQL_VARIANT_PROPERTY(@x / @y, 'Scale') AS scale;
big_div scale
------- -----
.333333 6
10 ondalık basamaklı iki sayının bölümü 6 basamakta kaldı. Döviz kuru ya da birim fiyat gibi hassas hesaplarda "ne kadar geniş o kadar iyi" diye her şeyi DECIMAL(38, x) yapmak bu yüzden ters teper; ihtiyacın kadar precision seç ve ara sonuçları açıkça CAST et.
RAND, NEWID ve Rastgele Seçim
RAND() sorgu başına bir kez hesaplanır; bütün row'lar aynı değeri alır. Row başına rastgele sayı için her row'da değişen NEWID()'den bir seed türetmek gerekir:
SELECT TOP (3) book_id, RAND() AS rand_value, RAND(CHECKSUM(NEWID())) AS rand_per_row
FROM dbo.books
ORDER BY book_id;
book_id rand_value rand_per_row
------- ------------------- -------------------
1 0.57200777273679648 0.70284360746822694
2 0.57200777273679648 0.33864401226013674
3 0.57200777273679648 0.77666590896452226
Aynı seed aynı değeri verir: RAND(42) iki kez çağrıldığında iki kez 0,71435594503450972 döndürdü. Testlerde tekrarlanabilir veri üretmek için kullanışlı.
"Bir rastgele kitap" için en çok önerilen yol ORDER BY NEWID(). Plan ne yaptığını gösteriyor:
SET SHOWPLAN_TEXT ON;
GO
SELECT TOP (1) book_id, title FROM dbo.books WHERE is_active = 1 ORDER BY NEWID();
GO
SET SHOWPLAN_TEXT OFF;
|--Sort(TOP 1, ORDER BY:([Expr1002] ASC))
|--Compute Scalar(DEFINE:([Expr1002]=newid()))
|--Clustered Index Scan(OBJECT:([sqlseries_ext_22].[dbo].[books].[PK_books]), WHERE:([sqlseries_ext_22].[dbo].[books].[is_active]=(1)))
Bütün table taranıyor, her row için bir GUID (globally unique identifier; her üretildiğinde farklı olan 128 bit'lik kimlik değeri) üretiliyor ve sıralanıyor; tek bir row için. 77 kitapta sorun değil, birkaç milyon row'da her çağrıda tam tarama demek. Bookstore'un ana sayfasındaki "günün kitabı" için ayrıca bir iş kuralı var: gün boyunca aynı kitap görünmeli. Rastgelelik yerine tarihten deterministik bir sıra numarası türetiyoruz:
DECLARE @day DATE = '2026-06-20';
SELECT b.book_id, b.title
FROM dbo.books AS b
WHERE b.is_active = 1
ORDER BY b.book_id
OFFSET ABS(CHECKSUM(@day)) % (SELECT COUNT(*) FROM dbo.books WHERE is_active = 1) ROWS FETCH NEXT 1 ROWS ONLY;
book_id title
------- ---------
69 Peter Pan
20 Haziran'da Peter Pan; aynı sorgu '2026-06-21' ile Charlotte's Web'i (70) döndürdü. Sonuç gün boyunca sabit, cache'lenebilir ve sorgu bütün table'ı sıralamıyor. OFFSET büyük tablolarda yine row'ları atlayarak ilerler; çok büyük bir katalogda id aralığından rastgele bir değer seçip WHERE book_id >= @x ile ilk row'u almak daha ucuz, ama id'lerdeki boşluklar dağılımı bozar.
GUID üretmenin iki yolu var. NEWID() tamamen rastgele bir GUID üretir. NEWSEQUENTIALID() ise aynı makinede artan değerler üretir ve yalnız bir column'un DEFAULT'unda kullanılabilir:
DROP TABLE IF EXISTS dbo.guid_test;
CREATE TABLE dbo.guid_test (
id_random UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
id_sequential UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID(),
n INT NOT NULL
);
INSERT INTO dbo.guid_test (n) VALUES (1), (2), (3);
SELECT n, LEFT(CAST(id_random AS CHAR(36)), 18) AS random_prefix, LEFT(CAST(id_sequential AS CHAR(36)), 18) AS sequential_prefix
FROM dbo.guid_test
ORDER BY n;
n random_prefix sequential_prefix
- ------------------ ------------------
1 18FFEA6F-2B21-4400 A7554E3E-F36B-1410
2 058C7260-5D93-48C9 A8554E3E-F36B-1410
3 AACBA6DD-1960-4374 A9554E3E-F36B-1410
Rastgele GUID'lerin arasında bir düzen yok; sıralı olanlar ilk byte'ta birer birer artıyor (A7, A8, A9). Clustered index'in anahtarı GUID olacaksa bu fark page split (index sayfasının dolup ikiye bölünmesi) ve parçalanma açısından önemli; ayrıntısı SQL Server Derinlemesine serisinde. Microsoft Learn iki uyarı yapıyor: sıradaki değer tahmin edilebildiği için gizlilik önemliyse (ör. URL'de görünen bir id) NEWSEQUENTIALID kullanılmamalı, ve Windows yeniden başladıktan sonra değerler daha düşük bir aralıktan başlayabilir. Function'ı sorguda doğrudan çağırmaya çalışırsan "The newsequentialid() built-in function can only be used in a DEFAULT expression ..." (Msg 302) hatası alırsın. PostgreSQL 18 bu iş için uuidv7()'yi getirdi: zaman damgasıyla başlayan, sıralanabilir bir UUID (GUID'in standartlardaki adı); uuid_extract_version(uuidv7()) 7 döndürüyor.
SCOPE_IDENTITY, @@IDENTITY, IDENT_CURRENT ve OUTPUT
15. yazıda yeni eklenen row'ların id'lerini OUTPUT ile almıştık. Tek row eklerken çok kullanılan üç function daha var ve farkları bir trigger ekleyince ortaya çıkıyor. orders'a her insert'te bir audit kaydı yazan bir trigger kuralım; audit table'ının identity'si 5000'den başlıyor:
DROP TABLE IF EXISTS dbo.order_audit;
CREATE TABLE dbo.order_audit (
audit_id INT IDENTITY(5000, 1) PRIMARY KEY,
order_id INT NOT NULL,
action VARCHAR(10) NOT NULL,
audited_at DATETIME2(0) NOT NULL DEFAULT SYSUTCDATETIME()
);
GO
CREATE OR ALTER TRIGGER dbo.tr_orders_audit ON dbo.orders AFTER INSERT AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.order_audit (order_id, action) SELECT order_id, 'insert' FROM inserted;
END;
GO
BEGIN TRAN;
INSERT INTO dbo.orders (customer_id, ordered_at, status) VALUES (1, '2026-06-30 10:00', 'Pending');
SELECT SCOPE_IDENTITY() AS scope_identity, @@IDENTITY AS at_at_identity, IDENT_CURRENT('dbo.orders') AS ident_current_orders;
ROLLBACK;
scope_identity at_at_identity ident_current_orders
-------------- -------------- --------------------
380 5000 380
SCOPE_IDENTITY() aynı scope'taki (aynı batch, procedure ya da trigger) son identity değerini verir: siparişin id'si, 380. @@IDENTITY oturumdaki son identity değerini scope'a bakmadan verir: trigger'ın audit table'ına yazdığı 5000. Trigger'ı eklemeden önce iki function aynı değeri döndürüyordu; yani @@IDENTITY kullanan kod, biri tabloya trigger eklediği gün sessizce yanlış id döndürmeye başlar. IDENT_CURRENT('dbo.orders') ise oturumdan ve scope'tan bağımsız, table'ın son ürettiği değeri verir; aynı anda başka bir oturum da sipariş ekliyorsa onun id'sini görebilirsin. Uygulama kodunda tek row için doğru seçim SCOPE_IDENTITY(), birden çok row için OUTPUT.
OUTPUT'un da trigger'la ilgili bir kısıtı var. İstemciye doğrudan döndürülen OUTPUT trigger'lı bir table'da çalışmaz:
BEGIN TRAN;
INSERT INTO dbo.orders (customer_id, ordered_at, status)
OUTPUT inserted.order_id, inserted.status
VALUES (1, '2026-06-30 10:00', 'Pending'), (3, '2026-06-30 10:05', 'Pending');
ROLLBACK;
Msg 334, Level 16, State 1, Line 2
The target table 'dbo.orders' of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause.
Çözüm OUTPUT ... INTO ile sonucu bir table variable'a yazıp oradan okumak:
DECLARE @new TABLE (order_id INT, customer_id INT);
BEGIN TRAN;
INSERT INTO dbo.orders (customer_id, ordered_at, status)
OUTPUT inserted.order_id, inserted.customer_id INTO @new (order_id, customer_id)
VALUES (1, '2026-06-30 10:00', 'Pending'), (3, '2026-06-30 10:05', 'Pending');
SELECT order_id, customer_id FROM @new ORDER BY order_id;
SELECT @@TRANCOUNT AS open_transactions;
ROLLBACK;
SELECT @@TRANCOUNT AS after_rollback, IDENT_CURRENT('dbo.orders') AS ident_after_rollback;
order_id customer_id
-------- -----------
381 1
382 3
(2 rows affected)
open_transactions
-----------------
1
(1 rows affected)
after_rollback ident_after_rollback
-------------- --------------------
0 382
Entity Framework Core (EF Core) serisinin EF Core 7 yeniliklerini anlatan yazısında gördüğümüz HasTrigger ayarının sebebi bu hata: EF Core kaydederken OUTPUT kullanıyor ve trigger'lı table'ı ona bildirmen gerekiyor. Son satıra da dikkat: iki transaction'ı da geri aldık, ama IDENT_CURRENT 382'de kaldı. Identity değerleri rollback ile geri verilmez; sonraki sipariş 383 alacak ve 380–382 hiç kullanılmamış olacak. Identity'deki boşlukların normal olması ve SEQUENCE ile farkı bu serinin ilerideki günlük DDL (Data Definition Language; şemayı tanımlayan CREATE, ALTER, DROP komutları) yazısının konusu.
@@ROWCOUNT, @@TRANCOUNT, ERROR_ Function'ları ve Oturum Bilgisi
@@ROWCOUNT son statement'ın etkilediği row sayısını verir; "son statement" kelimesi önemli:
UPDATE dbo.books SET list_price = list_price WHERE category_id = 4;
IF @@ROWCOUNT = 0 PRINT 'nothing updated';
SELECT @@ROWCOUNT AS rowcount_after_if;
(4 rows affected)
rowcount_after_if
-----------------
0
UPDATE 4 row etkiledi, IF bunu doğru okudu, ama IF'in kendisi de bir statement ve @@ROWCOUNT'u sıfırladı. PRINT da aynı şeyi yapar: PRINT'ten sonra okunan @@ROWCOUNT 0 çıktı. Değeri birden fazla yerde kullanacaksan statement'ın hemen ardından bir değişkene al: DECLARE @updated INT = @@ROWCOUNT; 4 değerini korudu. 15. yazıdaki "önce SELECT, sonra @@ROWCOUNT kontrolü" pattern'i bu kurala dayanıyor. @@TRANCOUNT ise açık transaction sayısını verir; yukarıda BEGIN TRAN'dan sonra 1, ROLLBACK'ten sonra 0. Bir script'in sonunda @@TRANCOUNT 0 değilse bir transaction açık kalmış demektir.
ERROR_ ailesi yalnız CATCH bloğunun içinde değer döndürür:
BEGIN TRY
SELECT 1 / 0;
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS err_no, ERROR_SEVERITY() AS severity, ERROR_LINE() AS err_line, ERROR_MESSAGE() AS err_message;
END CATCH;
SELECT ERROR_NUMBER() AS outside_catch;
err_no severity err_line err_message
------ -------- -------- ---------------------------------
8134 16 2 Divide by zero error encountered.
(1 rows affected)
outside_catch
-------------
NULL
TRY...CATCH, THROW ve transaction'lı hata yönetimini serinin ilerideki script yazma yazısında ayrıntılı göreceğiz. Oturumun kim ve nerede olduğunu soran function'lar da log ve audit kayıtlarında işe yarar:
SELECT DB_NAME() AS db_name, SUSER_SNAME() AS login_name, USER_NAME() AS user_name, APP_NAME() AS app_name;
db_name login_name user_name app_name
---------------- ---------- --------- --------
sqlseries_ext_22 sa dbo SQLCMD
SUSER_SNAME() login'i, USER_NAME() veritabanındaki kullanıcıyı verir; sa login'i her veritabanında dbo'dur. APP_NAME() connection string'deki Application Name'den gelir; uygulamanın connection string'ine "Bookstore.Api" gibi bir ad koyarsan yavaş sorguları ve kilitlenmeleri hangi uygulamanın yaptığını DMV'lerden (dynamic management view; sunucunun anlık durumunu gösteren sistem view'ları) okuyabilirsin. Bu değer istemcinin beyanıdır; güvenlik kararı için kullanma.
Metadata: OBJECT_ID, COL_LENGTH, Catalog View'lar ve INFORMATION_SCHEMA
Şemayı tanımanın en hızlı yolu onu sorgulamak. OBJECT_ID bir nesnenin id'sini, yoksa NULL döndürür ve ikinci argümanla tip kontrolü yapar; COL_LENGTH bir column'un byte cinsinden uzunluğunu verir:
SELECT OBJECT_NAME(OBJECT_ID('dbo.books', 'U')) AS as_table, OBJECT_ID('dbo.books', 'V') AS as_view,
OBJECT_ID('dbo.no_such_table') AS missing, COL_LENGTH('dbo.books', 'title') AS title_bytes,
COL_LENGTH('dbo.books', 'isbn') AS isbn_bytes, COL_LENGTH('dbo.books', 'no_such_column') AS missing_column;
as_table as_view missing title_bytes isbn_bytes missing_column
-------- ------- ------- ----------- ---------- --------------
books NULL NULL 400 13 NULL
title bir NVARCHAR(200); COL_LENGTH karakter değil byte saydığı için 400. IF OBJECT_ID('dbo.order_audit', 'U') IS NULL ve IF COL_LENGTH('dbo.books', 'subtitle') IS NULL tekrar çalıştırılabilir DDL script'lerinin temel kontrolleri; o konu da ilerideki DDL yazısında.
Ayrıntı için iki kaynak var: SQL Server'a özgü catalog view'lar (sys.tables, sys.columns, sys.indexes, sys.foreign_keys…) ve standarttaki INFORMATION_SCHEMA view'ları. books'un üç column'unu INFORMATION_SCHEMA'dan okuyalım:
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'books' AND COLUMN_NAME IN ('title', 'isbn', 'list_price')
ORDER BY ORDINAL_POSITION;
TABLE_NAME COLUMN_NAME DATA_TYPE CHARACTER_MAXIMUM_LENGTH IS_NULLABLE
---------- ----------- --------- ------------------------ -----------
books isbn char 13 NO
books title nvarchar 200 NO
books list_price decimal NULL NO
INFORMATION_SCHEMA uzunluğu karakter olarak (200), sys.columns.max_length ise byte olarak (400) veriyor. INFORMATION_SCHEMA PostgreSQL ve MySQL'de de var ve taşınabilir script'ler için iyi bir başlangıç; ama index, partition, compression gibi ürüne özgü bilgileri içermez. Onlar için catalog view'lar gerekir. order_items'ın index'leri ve key column'ları:
SELECT i.name AS index_name, i.type_desc, i.is_unique, i.is_primary_key,
STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS key_columns
FROM sys.indexes AS i
JOIN sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE i.object_id = OBJECT_ID('dbo.order_items') AND ic.is_included_column = 0
GROUP BY i.name, i.type_desc, i.is_unique, i.is_primary_key
ORDER BY i.name;
index_name type_desc is_unique is_primary_key key_columns
------------------------- ------------ --------- -------------- -----------------
PK_order_items CLUSTERED 1 1 order_item_id
UQ_order_items_order_book NONCLUSTERED 1 0 order_id, book_id
sp_help 'dbo.books' aynı bilgilerin bir kısmını tek komutta, ama sekiz ayrı result set olarak döndürür; elle bakmak için pratik, script'te işlemek için değil.
Bookstore için işe yarar bir sorgu: index'i olmayan foreign key'ler. SQL Server primary key ve unique constraint için otomatik index oluşturur, foreign key için oluşturmaz. Index'siz bir foreign key'de parent row silinirken ya da join yapılırken child table baştan sona taranır:
SELECT OBJECT_NAME(fk.parent_object_id) AS table_name, fk.name AS fk_name, c.name AS fk_column,
OBJECT_NAME(fk.referenced_object_id) AS referenced_table
FROM sys.foreign_keys AS fk
JOIN sys.foreign_key_columns AS fkc ON fkc.constraint_object_id = fk.object_id
JOIN sys.columns AS c ON c.object_id = fkc.parent_object_id AND c.column_id = fkc.parent_column_id
WHERE NOT EXISTS (
SELECT 1
FROM sys.index_columns AS ic
WHERE ic.object_id = fkc.parent_object_id
AND ic.column_id = fkc.parent_column_id
AND ic.key_ordinal = 1
)
ORDER BY table_name, fk_name;
table_name fk_name fk_column referenced_table
------------ ----------------------- ------------------ ----------------
book_authors FK_book_authors_authors author_id authors
books FK_books_categories category_id categories
categories FK_categories_parent parent_category_id categories
order_items FK_order_items_books book_id books
orders FK_orders_customers customer_id customers
payments FK_payments_orders order_id orders
reviews FK_reviews_customers customer_id customers
Veritabanındaki 11 foreign key'den 7'sinin column'u hiçbir index'in ilk key column'u değil. order_items.order_id listede yok, çünkü UQ_order_items_order_book index'i onunla başlıyor; book_authors.book_id de primary key'in ilk column'u olduğu için yok. Sorgu tek column'lu foreign key'ler için yazıldı; çok column'lu foreign key'lerde column'ların sırasını da karşılaştırmak gerekir. Hangi index'lerin gerçekten eklenmesi gerektiği bir tasarım kararı ve SQL Server Derinlemesine serisinin index yazılarında ele alınıyor; bu sorgu yalnız aday listesini çıkarıyor.
Function Özeti ve PostgreSQL Karşılıkları
PostgreSQL sütunundaki ifadeleri kendi PostgreSQL 18 container'ımda çalıştırdım:
| T-SQL | NULL ve sınır davranışı | PostgreSQL |
|---|---|---|
ROUND(x, n [, 1]) | Tip girdinin; sığmazsa Msg 8115 | round, trunc; float'ta çifte yuvarlar |
POWER(x, y) | Tip x'in; INT tabanda kesilir | power, numeric ya da float |
INT / INT, AVG(int) | Tamsayı sonucu | / tamsayı; avg numeric döndürür |
RAND() | Sorgu başına tek değer | random(), row başına |
NEWID(), NEWSEQUENTIALID() | Sıralı olan yalnız DEFAULT'ta | gen_random_uuid(), uuidv7() (18) |
SCOPE_IDENTITY() | Scope'ta insert yoksa NULL | RETURNING, lastval() |
@@ROWCOUNT | Her statement'ta sıfırlanır | GET DIAGNOSTICS ... ROW_COUNT (PL/pgSQL) |
OBJECT_ID(name, type) | Yoksa NULL | to_regclass(name) |
sys.*, INFORMATION_SCHEMA | Uzunluk byte / karakter farkı | pg_catalog, information_schema |
Üretimde Dikkat
Para hesaplarında tip seçimi function seçiminden önce gelir: DECIMAL kullan, INT bölmesini ve AVG(int)'i tanı, ara sonuçların precision'ını SQL_VARIANT_PROPERTY ile kontrol et. Yuvarlamayı tek bir yerde ve iş kuralına göre yap; satır bazında yuvarlayıp toplamak ile toplayıp yuvarlamak farklı sonuç verir ve faturada kuruş farkı olarak görünür.
Yeni id'yi her zaman SCOPE_IDENTITY() ya da OUTPUT ... INTO ile al. @@IDENTITY ve IDENT_CURRENT bugün doğru çalışsa da yarın eklenecek bir trigger ya da eş zamanlı bir oturumla yanlış değer döndürür. Rastgele seçimi büyük table'larda ORDER BY NEWID() ile yapma; deterministik bir seçim kuralı ya da önceden hesaplanmış bir aday listesi daha ucuz. Uygulamanın connection string'ine bir Application Name koy; sorun anında hangi uygulamanın hangi oturumu açtığını görmek zaman kazandırır.
Sık Düşülen Tuzaklar
- Trigger'lı bir table'da
@@IDENTITYile yeni id'yi almak; trigger'ın eklediği row'un id'si gelir. - Trigger'lı table'da
INTO'suzOUTPUTkullanmak (Msg 334). @@ROWCOUNT'uIF,PRINTya da başka bir statement'tan sonra okumak; değeri hemen bir değişkene al.AVG(rating)ve7 / 2gibi tamsayı işlemlerinde kesilen ondalığı fark etmemek;POWER(2, 0.5)'in 1 döndürmesi.ROUND'un sonucunun girdinin tipinde kalması ve sınırda taşması (Msg 8115).- Büyük bir table'da
ORDER BY NEWID(); tek row için bütün table taranır ve sıralanır. - Row başına rastgele değer için
RAND()kullanmak; sorgu başına bir kez hesaplanır.
Küçük Bir Deneme
★☆☆ Her kitabın ortalama puanını hem AVG(rating) hem AVG(rating * 1.0) ile hesapla ve tam sayıya yuvarlandığında iki sonucun farklı yıldız sayısı gösterdiği kitapları listele.
★★☆ customers table'ına bir audit trigger'ı ekle ve yeni bir müşteri ekleyen bir script'te SCOPE_IDENTITY(), @@IDENTITY ve OUTPUT ... INTO ile alınan id'leri karşılaştır. Script'i transaction içinde çalıştırıp sonunda geri al ve IDENT_CURRENT'in ne gösterdiğine bak.
★★★ Index'siz foreign key sorgusunu çok column'lu foreign key'leri de doğru değerlendirecek şekilde genişlet: foreign key'in bütün column'ları, bir index'in ilk n key column'unda aynı sırayla bulunmalı. Test için iki column'lu bir foreign key'i olan küçük bir table çifti oluştur.
Bu yazıyla T-SQL'in günlük function kataloğunu tamamladık: string, tarih ve saat, dönüşüm ve NULL, matematik, sistem ve metadata. Bir sonraki yazıda bu function'ların JSON'la buluştuğu yere geçiyoruz: FOR JSON ile sorgu sonucunu JSON'a çevirmek, OPENJSON ile JSON'u row'lara açmak, JSON_VALUE, JSON_QUERY ve JSON_MODIFY.