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

Scalar Function'lar ve CASE: String, Tarih/Saat, Tip Dönüşümü ve Sargability

Mert Özen 8 Kas 2026 13 dk 3 görüntülenme
Scalar Function'lar ve CASE: String, Tarih/Saat, Tip Dönüşümü ve Sargability

Scalar function'lar kullanışlıdır ama WHERE'de column'un üzerinde kullanılınca sorguyu sargable olmaktan çıkarır: 200.000 row'da YEAR()/MONTH() filtresi 402, aynı ayı yarı açık aralıkla soran sorgu 21 logical read yaptı. CONCAT/CONCAT_WS, LEN ile DATALENGTH, CAST/CONVERT/TRY_CAST, nvarchar parametrenin CONVERT_IMPLICIT ile index'i kaybettirmesi, DATEDIFF'in sınır sayması ve CASE'in tip ve ELSE tuzaklarını gösteriyoruz.

Sürüm notu: Bu yazıdaki SQL (Structured Query Language) sorguları SQL Server 2025 (17.0, Cumulative Update 10) üzerinde, PostgreSQL karşılaştırmaları PostgreSQL 18.6 üzerinde çalıştırılmıştır.

Önce Tahmin Et: Aynı Sonuç, Aynı Maliyet mi?

2025 yılında kaç sipariş alındığını iki farklı şekilde soruyoruz. orders.ordered_at üzerinde bir index var:

CREATE INDEX IX_orders_ordered_at ON dbo.orders (ordered_at);

SELECT
    (SELECT COUNT(*) FROM dbo.orders WHERE YEAR(ordered_at) = 2025) AS with_year,
    (SELECT COUNT(*) FROM dbo.orders WHERE ordered_at >= '2025-01-01' AND ordered_at < '2026-01-01') AS with_range;
with_year  with_range
---------  ----------
263        263

(1 rows affected)

Sonuçlar aynı. Birincisi daha kısa ve daha okunur; ikincisi uzun ve "tarih aralığı" mantığıyla uğraştırıyor. Hangisini yazmalısın? Planlara bakalım:

SET SHOWPLAN_TEXT ON;
GO
SELECT COUNT(*) FROM dbo.orders WHERE YEAR(ordered_at) = 2025;
SELECT COUNT(*) FROM dbo.orders WHERE ordered_at >= '2025-01-01' AND ordered_at < '2026-01-01';
GO
SET SHOWPLAN_TEXT OFF;
  |--Index Scan(OBJECT:([sqlt_07].[dbo].[orders].[IX_orders_ordered_at]),  WHERE:(datepart(year,[sqlt_07].[dbo].[orders].[ordered_at])=(2025)))

  |--Index Seek(OBJECT:([sqlt_07].[dbo].[orders].[IX_orders_ordered_at]), SEEK:([sqlt_07].[dbo].[orders].[ordered_at] > [Expr1011] AND [sqlt_07].[dbo].[orders].[ordered_at] < [Expr1012]) ORDERED FORWARD)

Planların ilgili satırlarını aldım. İlk sorgu index'in tamamını tarıyor ve her row için datepart(year, ...) hesaplıyor; ikincisi index'te doğrudan 2025'in başına gidip sonunda duruyor. 379 siparişte bu fark görünmez, ama bu yazının sonunda 200.000 row'luk bir table'da logical read sayısıyla ölçeceğiz. Bu yazıda scalar function'ları, yani her row için bir değer üreten function'ları ele alacağız: string, tip dönüşümü, tarih ve CASE. Hepsi kullanışlı; ama yanlış yerde kullanıldıklarında, yani WHERE'de column'un üzerinde, sorguyu sargable olmaktan çıkarıyorlar.

String Function'ları

Müşteri listesi için bir ad, bir konum ve e-posta domain'i üretelim:

SELECT TOP (3)
       CONCAT(first_name, N' ', last_name) AS full_name,
       CONCAT_WS(N', ', city, country_code) AS location,
       UPPER(LEFT(last_name, 3)) AS short_code,
       SUBSTRING(email, CHARINDEX('@', email) + 1, 100) AS email_domain
FROM dbo.customers
ORDER BY customer_id;
full_name     location        short_code  email_domain
------------  --------------  ----------  ------------
Ada Lovelace  Manchester, GB  LOV         example.com
Alan Turing   Berlin, DE      TUR         Example.com
Grace Hopper  Istanbul, TR    HOP         example.org

(3 rows affected)

CONCAT, önceki yazıda gördüğümüz gibi NULL'ları boş string sayar; + ise tek bir NULL'da bütün sonucu NULL yapar. CONCAT_WS (with separator) ayıracı bir kez alır ve NULL argümanları atlar; şehri olmayan bir müşteride ", TR" yerine yalnız "TR" yazar. SUBSTRING ve CHARINDEX 1'den sayar; C#'taki 0 tabanlı index alışkanlığı burada tek karakterlik hatalar üretir. Alan Turing'in domain'i "Example.com" olarak geldi: veri nasıl kaydedildiyse öyle. Karşılaştırmada sorun yok, çünkü collation case-insensitive; ama gruplama ve raporlamada iki farklı yazım görürsün.

Uzunluk için iki function var ve farklı şeyler ölçüyorlar:

SELECT LEN(N'Dune   ') AS len_trailing, DATALENGTH(N'Dune   ') AS datalength_trailing,
       LEN(N'   Dune') AS len_leading,
       LEN(TRIM(N'   Dune   ')) AS len_trimmed;
len_trailing  datalength_trailing  len_leading  len_trimmed
------------  -------------------  -----------  -----------
4             14                   7            4

(1 rows affected)

LEN karakter sayar ama sondaki boşlukları saymaz; baştakileri sayar. DATALENGTH byte sayar ve boşlukları da dahil eder; NVARCHAR'da karakter başına 2 byte olduğu için 7 karakter 14 byte. "Kullanıcı adı en fazla 20 karakter" kuralını LEN ile kontrol ediyorsan sondaki boşluklar kontrolden kaçar. TRIM SQL Server 2017'den beri var; öncesinde LTRIM(RTRIM(...)) yazılırdı.

REPLACE ve LOWER ile basit bir slug üretelim:

SELECT TOP (3) title, REPLACE(LOWER(title), N' ', N'-') AS slug
FROM dbo.books
WHERE category_id = 4
ORDER BY book_id;
title                 slug
--------------------  --------------------
Nineteen Eighty-Four  nineteen-eighty-four
Brave New World       brave-new-world
Fahrenheit 451        fahrenheit-451

(3 rows affected)

Gerçek bir slug için kesme işaretleri, Türkçe karakterler ve ardışık tireler de ele alınmalı; bu tür bir dönüşümü veritabanında değil uygulamada, test edilebilir bir function'da yapmak daha doğru. Biçimlendirme için de aynı kural geçerli. T-SQL'in FORMAT'ı kültüre göre sayı ve tarih biçimlendirir:

SELECT TOP (2) title,
       FORMAT(list_price, 'N2', 'tr-TR') AS tr_price,
       FORMAT(list_price, 'N2', 'en-US') AS us_price,
       CONVERT(VARCHAR(20), list_price) AS plain
FROM dbo.books
WHERE list_price > 1000
ORDER BY book_id;
title                     tr_price  us_price  plain
------------------------  --------  --------  -------
The Pragmatic Programmer  1.150,00  1,150.00  1150.00
Refactoring               1.150,00  1,150.00  1150.00

(2 rows affected)

Çıktı doğru, ama FORMAT Microsoft dokümanına göre .NET CLR'ın (Common Language Runtime) varlığına dayanıyor ve her değer için .NET'in biçimlendirme altyapısını çağırıyor; büyük result set'lerde CONVERT'ten pahalı. Daha önemlisi, biçimlendirme bir sunum işi: sayıyı veritabanından sayı olarak al, kullanıcının kültürüne göre uygulamada biçimlendir. PostgreSQL'de string birleştirmenin standart yolu || operatörü ve o da NULL ile NULL verir: 'Dune' || NULL boş (NULL), concat('Dune', NULL) ise "Dune". SQL Server 2025 da || operatörünü destekliyor; 18. yazıda bakacağız.

Tip Dönüşümü: CAST, CONVERT ve TRY_CAST

CAST standart, CONVERT T-SQL'e özgü ve bir style parametresi alıyor. Style 104 Alman biçimi, yani gün.ay.yıl:

SELECT CAST('2025-03-31' AS DATE) AS iso_date,
       CONVERT(VARCHAR(10), CAST('2025-03-31' AS DATE), 104) AS german_style,
       CONVERT(DATE, '31.03.2025', 104) AS parsed_104,
       TRY_CAST('31.03.2025' AS DATE) AS try_cast_result,
       TRY_CONVERT(DATE, '2025-02-30') AS invalid_date;
iso_date    german_style  parsed_104  try_cast_result  invalid_date
----------  ------------  ----------  ---------------  ------------
2025-03-31  31.03.2025    2025-03-31  NULL             NULL

(1 rows affected)

"31.03.2025" style'ı söyleyince doğru okundu; TRY_CAST ise style bilmediği için okuyamadı ve hata yerine NULL döndürdü. Aynı dönüşümü CAST ile yapsaydın sorgu Msg 241, Conversion failed when converting date and/or time from character string. ile dururdu. TRY_ ailesi dışarıdan gelen, kalitesi bilinmeyen veriyi temizlerken işe yarar: dönüşmeyen row'ları WHERE TRY_CAST(...) IS NULL ile bulursun. Tarih literal'larında da bir alışkanlık edin: '2025-03-31' gibi ISO (International Organization for Standardization) 8601 biçimi DATE ve DATETIME2 için oturumun dil ve tarih ayarından bağımsız okunur, '31/03/2025' gibi biçimler değil. Eski DATETIME'da tire içermeyen '20250331' biçimi güvenli olandır.

En tehlikeli dönüşüm senin yazmadığın, SQL Server'ın kendi yaptığı dönüşüm: implicit conversion. customers.email bir VARCHAR column'u ve üzerinde unique bir index var. Aynı aramayı N'li ve N'siz literal ile yapalım:

SET SHOWPLAN_TEXT ON;
GO
SELECT customer_id FROM dbo.customers WHERE email = 'ada.lovelace@example.com';
SELECT customer_id FROM dbo.customers WHERE email = N'ada.lovelace@example.com';
GO
SET SHOWPLAN_TEXT OFF;
  |--Index Seek(OBJECT:([sqlt_07].[dbo].[customers].[UQ_customers_email]), SEEK:([sqlt_07].[dbo].[customers].[email]=[@1]) ORDERED FORWARD)

  |--Index Scan(OBJECT:([sqlt_07].[dbo].[customers].[UQ_customers_email]),  WHERE:(CONVERT_IMPLICIT(nvarchar(254),[sqlt_07].[dbo].[customers].[email],0)=[@1]))

NVARCHAR'ın tip önceliği VARCHAR'dan yüksek. İki taraf karşılaştırılırken düşük öncelikli taraf, yani column, NVARCHAR'a çevriliyor ve her row için yapılan bu CONVERT_IMPLICIT index seek'i engelliyor. Bu veritabanının SQL_ ile başlayan collation'ında sonuç Index Scan. Uygulama tarafında bu hata çok sık: .NET'te string parametreler varsayılan olarak nvarchar gönderilir. ANSI (American National Standards Institute) string'leri ve TVP'leri (Table-Valued Parameter) işleyen Dapper serisinin "Dapper ve SQL Server: TVP, ANSI String'ler, {=literal} ve Parameter Sniffing" yazısında parametreyi DbType.AnsiString ile göndermeyi görmüştük; bu plan, o ayarın neden önemli olduğunun kanıtı.

Tarih ve Saat Function'ları

En sık kullanılanlar bir arada:

SELECT SYSUTCDATETIME() AS utc_now_type_check,
       DATEADD(DAY, 14, CAST('2026-06-20' AS DATE)) AS due_date,
       EOMONTH('2026-02-10') AS end_of_feb,
       DATEFROMPARTS(2026, 6, 1) AS first_of_june,
       DATEADD(MONTH, 1, CAST('2026-01-31' AS DATE)) AS jan31_plus_month;
utc_now_type_check           due_date    end_of_feb  first_of_june  jan31_plus_month
---------------------------  ----------  ----------  -------------  ----------------
2026-11-08 12:10:13.4864959  2026-07-04  2026-02-28  2026-06-01     2026-02-28

(1 rows affected)

SYSUTCDATETIME() DATETIME2(7) hassasiyetinde UTC (Coordinated Universal Time) zamanı verir; GETDATE() ise sunucunun yerel saatini eski DATETIME tipinde döndürür. Bookstore UTC sakladığı için doğrusu birincisi. Son column'a dikkat: 31 Ocak'a bir ay eklemek 31 Şubat olamayacağı için ayın son gününe, 28 Şubat'a düşüyor. "Her ay aynı gün yenilenen abonelik" gibi bir hesapta bu davranışı bilmen gerekir.

DATEDIFF ise sezgiye en aykırı function. İki tarih arasındaki süreyi değil, aradaki sınır sayısını döndürür:

SELECT DATEDIFF(YEAR, '2025-12-31', '2026-01-01') AS years_1_day,
       DATEDIFF(YEAR, '2025-01-01', '2025-12-31') AS years_364_days,
       DATEDIFF(DAY, '2025-03-31 23:59', '2025-04-01 00:01') AS days_2_minutes,
       DATEDIFF(MONTH, '2025-01-31', '2025-02-01') AS months_1_day;
years_1_day  years_364_days  days_2_minutes  months_1_day
-----------  --------------  --------------  ------------
1            0               1               1

(1 rows affected)

Bir günlük fark "1 yıl", 364 günlük fark "0 yıl". İki dakika "1 gün". Bu yüzden müşterinin yaşını ya da abonelik süresini DATEDIFF(YEAR, ...) ile hesaplamak yanlış sonuç verir. Kargo süresini ölçerken farkı görelim:

SELECT TOP (5) order_id, ordered_at, shipped_at,
       DATEDIFF(HOUR, ordered_at, shipped_at) / 24.0 AS days_exact,
       DATEDIFF(DAY, ordered_at, shipped_at) AS day_boundaries
FROM dbo.orders
WHERE shipped_at IS NOT NULL
ORDER BY order_id;
order_id  ordered_at           shipped_at           days_exact  day_boundaries
--------  -------------------  -------------------  ----------  --------------
1         2025-01-02 09:19:25  2025-01-03 11:19:25  1.083333    1
2         2025-01-02 22:36:16  2025-01-05 04:36:16  2.250000    3
3         2025-01-10 10:19:19  2025-01-13 13:19:19  3.125000    3
4         2025-01-11 19:07:55  2025-01-13 02:07:55  1.291666    2
5         2025-01-12 09:03:38  2025-01-15 17:03:38  3.333333    3

(5 rows affected)

2 numaralı sipariş 2,25 günde kargolanmış ama gece verildiği için üç gün sınırı geçmiş. "Kaç günde kargolandı" sorusunun iş tanımı (geçen süre mi, takvim günü mü) sorgudan önce netleşmeli. Yerel takvim günü gerekiyorsa önce saat dilimini çevir. 3. yazıda gördüğümüz AT TIME ZONE ile UTC'ye göre 31 Aralık'ta verilen iki sipariş İstanbul'da yılbaşına düşüyor:

SELECT order_id, ordered_at,
       CAST(ordered_at AT TIME ZONE 'UTC' AT TIME ZONE 'Turkey Standard Time' AS DATE) AS istanbul_date
FROM dbo.orders
WHERE order_id IN (262, 263, 264);
order_id  ordered_at           istanbul_date
--------  -------------------  -------------
262       2025-12-31 22:30:22  2026-01-01
263       2025-12-31 23:06:27  2026-01-01
264       2026-01-01 13:26:26  2026-01-01

(3 rows affected)

PostgreSQL'de bu function'ların çoğunun adı farklı: DATEADD yerine interval aritmetiği, DATEDIFF yerine çıkarma ve date_part, ayın başı için date_trunc('month', ...). Tarih function'ları lehçeler arasında en az taşınabilir alan; 16. yazıda karşılaştırma tablosunda göreceğiz.

CASE İfadeleri

CASE standart SQL'in koşul ifadesi ve iki biçimi var. Searched CASE her WHEN'de ayrı bir koşul değerlendirir; ilk TRUE olan kazanır. Kitapları fiyat bantlarına ayıralım:

SELECT book_id, title, list_price,
       CASE
           WHEN list_price < 300 THEN 'budget'
           WHEN list_price < 700 THEN 'standard'
           WHEN list_price < 1000 THEN 'premium'
           ELSE 'collector'
       END AS price_band
FROM dbo.books
WHERE book_id IN (39, 26, 58, 60, 73)
ORDER BY list_price;
book_id  title            list_price  price_band
-------  ---------------  ----------  ----------
39       The Hobbit       230.90      budget
26       Dune             390.50      standard
73       Meditations      670.50      standard
58       Clean Code       890.00      premium
60       Design Patterns  1450.00     collector

(5 rows affected)

Koşulların sırası önemli: 230,90 TL'lik bir kitap < 700 ve < 1000'i de sağlıyor, ama ilk eşleşen WHEN kazanıyor. Simple CASE ise tek bir ifadeyi değerlerle karşılaştırır ve ORDER BY'da özel bir sıralama tanımlamak için de kullanılabilir:

SELECT status,
       CASE status
           WHEN 'Pending' THEN N'Awaiting payment'
           WHEN 'Paid' THEN N'Preparing'
           WHEN 'Shipped' THEN N'On the way'
           WHEN 'Delivered' THEN N'Delivered'
       END AS status_text,
       COUNT(*) AS order_count
FROM dbo.orders
GROUP BY status
ORDER BY CASE status WHEN 'Pending' THEN 1 WHEN 'Paid' THEN 2 WHEN 'Shipped' THEN 3 WHEN 'Delivered' THEN 4 ELSE 5 END;
status     status_text       order_count
---------  ----------------  -----------
Pending    Awaiting payment  1
Paid       Preparing         1
Shipped    On the way        8
Delivered  Delivered         345
Cancelled  NULL              24

(5 rows affected)

Cancelled için bir WHEN yazmayı unuttuk ve ELSE de yok; CASE hiçbir dalı eşleşmeyince NULL döndürür, hata vermez. Her CASE'e bilinçli bir ELSE yaz. T-SQL'in IIF(condition, a, b)'si iki dallı bir CASE'in kısaltması; IIF(is_active = 1, 'on sale', 'discontinued') The Colour of Magic ve Code Complete için "discontinued" döndürüyor. Son bir kural: CASE'in sonucu tek bir tiptir ve dallar arasında tip önceliği en yüksek olan seçilir. Bir dalda metin, diğerinde sayı döndürmeye çalışmak şuna yol açar:

SELECT CASE WHEN list_price > 1000 THEN 'expensive' ELSE list_price END AS label
FROM dbo.books;
...
470.00
460.90
950.00
Msg 8114, Level 16, State 5, Server b8be943222fa, Line 1
Error converting data type varchar to numeric.

Sonuç tipi DECIMAL seçildi ve sorgu ilk 1000 TL'nin üzerindeki kitaba kadar 56 row döndürdükten sonra "expensive"i sayıya çeviremeyip durdu. Çıktının ilk satırlarını kısalttım; asıl ders, hatanın veriye bağlı olarak sorgunun ortasında patlaması. Dalları aynı tipe kendin getir: ELSE CAST(list_price AS VARCHAR(20)).

Sargability: Column'a Function Uygulamamak

Bir predicate sargable (search argument-able) ise optimizer onu bir index'te aralık aramasına çevirebilir. Column'u bir function'ın içine koyduğunda, YEAR(ordered_at) gibi, optimizer'ın elinde index'in sıralı olduğu değer değil, her row için hesaplanması gereken başka bir değer kalır. Farkı ölçebilmek için Bookstore'un büyüdüğü bir geleceği taklit eden 200.000 row'luk bir sipariş geçmişi table'ı kuralım:

CREATE TABLE dbo.order_history (
    history_id  INT          NOT NULL PRIMARY KEY,
    ordered_at  DATETIME2(0) NOT NULL,
    customer_id INT          NOT NULL
);
INSERT INTO dbo.order_history (history_id, ordered_at, customer_id)
SELECT value, DATEADD(MINUTE, value * 5, CAST('2024-01-01' AS DATETIME2(0))), value % 40 + 1
FROM GENERATE_SERIES(1, 200000);
CREATE INDEX IX_order_history_ordered_at ON dbo.order_history (ordered_at);

GENERATE_SERIES SQL Server 2022 ile geldi ve 1'den 200.000'e kadar sayı üretiyor; her sipariş 5 dakika arayla, 2024 başından 2025 Kasım'ına uzanıyor. Mart 2025'i iki şekilde sayalım:

SET STATISTICS IO ON;
SELECT COUNT(*) AS march_2025 FROM dbo.order_history WHERE YEAR(ordered_at) = 2025 AND MONTH(ordered_at) = 3;
SELECT COUNT(*) AS march_2025 FROM dbo.order_history WHERE ordered_at >= '2025-03-01' AND ordered_at < '2025-04-01';
SET STATISTICS IO OFF;
march_2025
----------
8928

(1 rows affected)
Table 'order_history'. Scan count 1, logical reads 402, ...
march_2025
----------
8928

(1 rows affected)
Table 'order_history'. Scan count 1, logical reads 21, ...

Aynı 8.928 sipariş; birinci sorgu 402 sayfa, ikincisi 21 sayfa okudu. Oran sabit değil, table büyüdükçe açılır: function'lı versiyon her zaman index'in tamamını okur, aralıklı versiyon yalnız o ayın sayfalarını. Kural genel: hesaplamayı column'un değil sabitin tarafına taşı. "Son 7 gün içinde kargolanacak siparişler" gibi bir koşulda fark planda açıkça görünüyor:

SET SHOWPLAN_TEXT ON;
GO
SELECT order_id FROM dbo.orders WHERE DATEADD(DAY, 7, ordered_at) > '2026-06-25';
SELECT order_id FROM dbo.orders WHERE ordered_at > DATEADD(DAY, -7, CAST('2026-06-25' AS DATETIME2(0)));
GO
SET SHOWPLAN_TEXT OFF;
  |--Index Scan(OBJECT:([sqlt_07].[dbo].[orders].[IX_orders_ordered_at]),  WHERE:(dateadd(day,(7),[sqlt_07].[dbo].[orders].[ordered_at])>'2026-06-25 00:00:00.0000000'))

  |--Index Seek(OBJECT:([sqlt_07].[dbo].[orders].[IX_orders_ordered_at]), SEEK:([sqlt_07].[dbo].[orders].[ordered_at] > '2026-06-18 00:00:00') ORDERED FORWARD)

İkinci yazımda DATEADD sabite uygulanıyor, bir kez hesaplanıyor (2026-06-18 00:00:00) ve seek'e dönüşüyor. Aynı mantık WHERE list_price * 1.10 > 1000 yerine WHERE list_price > 1000 / 1.10 yazmak, WHERE LEFT(title, 4) = N'Dune' yerine WHERE title LIKE N'Dune%' yazmak, WHERE ISNULL(city, N'') = N'' yerine WHERE city IS NULL OR city = N'' yazmak için de geçerli. Bazı durumlarda SQL Server kendisi yardım ediyor: CAST(ordered_at AS DATE) = '2025-03-31' gibi bir dönüşümü özel olarak tanıyıp aralık aramasına çevirebiliyor. Ama buna güvenmek yerine predicate'i baştan sargable yazmak, hem okuyana hem optimizer'a ne istediğini açıkça söyler. Planın nasıl okunacağı ve hangi function'ların istisna olduğu "SQL Server Derinlemesine" serisinin konusu.

Üretimde Dikkat

Implicit conversion'ı plan cache'te ara. Uygulamadan gelen nvarchar parametrelerin varchar column'larla karşılaştırıldığı her sorgu, test ortamındaki küçük veride görünmez, production'daki büyük table'da Index Scan olarak ortaya çıkar. Entity Framework Core (EF Core) column tipini modelden bildiği için doğru parametre tipini gönderir; elle yazılan Dapper kodunda bunu sen ayarlarsın. Hesaplanmış bir değere göre sık filtreleme yapıyorsan (ör. sipariş yılı) function'ı her sorguda tekrarlamak yerine computed column ve üzerine index düşünebilirsin; bu bir index tasarımı kararı, "SQL Server Derinlemesine" serisinde. Son olarak tarih hesaplarında saat dilimini sorguda açık yaz: "bugünkü siparişler" İstanbul'daki bir operasyon ekibi için UTC gece yarısında değil, İstanbul gece yarısında başlar.

Sık Düşülen Tuzaklar

  • WHERE YEAR(ordered_at) = 2025 gibi column'a function uygulamak; index seek yerine scan.
  • .NET'ten gelen nvarchar parametreyi varchar column'la karşılaştırmak ve CONVERT_IMPLICIT ile index'i kaybetmek.
  • DATEDIFF(YEAR, ...) ile yaş ya da süre hesaplamak; DATEDIFF süre değil sınır sayar.
  • Tarih literal'larını '31/03/2025' gibi oturum ayarına bağlı biçimlerde yazmak; ISO 8601 kullan.
  • LEN'in sondaki boşlukları saymadığını unutmak.
  • CASE'te ELSE unutmak ve sessiz NULL'lar; dallarda farklı tipler döndürüp sorgunun veriye bağlı olarak ortada patlaması.
  • Biçimlendirmeyi (FORMAT) veritabanında yapmak; sayıyı sayı olarak taşı, sunumda biçimlendir.

Küçük Bir Deneme

★☆☆ Müşterilerin e-posta adreslerini kullanıcı adı (@ öncesi) ve domain olarak iki column'a ayır. Domain'leri küçük harfe çevirip kaç farklı domain olduğunu say.

★★☆ "Geçen ay verilen siparişler" sorgusunu, verinin "bugün"ü 2026-06-30 kabul ederek, EOMONTH ve DATEADD ile sargable olacak şekilde yaz. MONTH(ordered_at) = 5 yazımıyla planlarını karşılaştır.

★★★ order_history üzerinde "her müşterinin 2025'teki sipariş sayısı" sorgusunu önce YEAR() ile, sonra aralıkla yaz ve SET STATISTICS IO ile karşılaştır. Sonra (customer_id, ordered_at) üzerine bir index ekleyip ikisini tekrar ölç. Index hangi yazımda işe yaradı?

Şimdiye kadar hep tek bir table'dan okuduk. Bir sonraki yazıda table'ları birleştiriyoruz: INNER JOIN, LEFT/RIGHT/FULL OUTER JOIN, join koşulunu ON'a mı WHERE'e mi yazdığının sonucu nasıl değiştirdiği ve Venn diyagramlarının join'leri neden yanlış anlattığı.