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

T-SQL String Function'ları: LEN ve DATALENGTH, CHARINDEX, SUBSTRING, REPLACE, STUFF, TRANSLATE, CONCAT_WS, STRING_AGG ve STRING_SPLIT

Mert Özen 15 Kas 2026 15 dk 7 görüntülenme
T-SQL String Function'ları: LEN ve DATALENGTH, CHARINDEX, SUBSTRING, REPLACE, STUFF, TRANSLATE, CONCAT_WS, STRING_AGG ve STRING_SPLIT

T-SQL'in string function'ları collation'a uyar, NULL'ı farklı ele alır ve sınır durumlarında sessiz davranır: case-insensitive veritabanında REPLACE büyük harfi de değiştirir, N öneki unutulunca 'İ' 'I' olur. CHARINDEX, STUFF, TRANSLATE, QUOTENAME, CONCAT_WS ile STRING_AGG'in boş string tuzağı, 8000 byte hatası ve STRING_SPLIT'in sırası gerçek çıktılarla, PostgreSQL karşılıklarıyla.

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: REPLACE Büyük Harfi Değiştirir mi?

Bir kitap adındaki küçük "d" harfini "X" ile değiştirmek istiyoruz. "Dune Messiah"ta küçük "d" yok; tek "D" büyük harf ve kelimenin başında. Üç column'un sonucunu tahmin et:

SELECT REPLACE(N'Dune Messiah', N'd', N'X') AS replaced,
       CHARINDEX(N'MESSIAH', N'Dune Messiah') AS position_ci,
       REPLACE(N'Dune Messiah' COLLATE Latin1_General_100_CS_AS, N'd', N'X') AS replaced_cs;
replaced      position_ci  replaced_cs
------------  -----------  ------------
Xune Messiah  6            Dune Messiah

(1 rows affected)

C#'taki string.Replace alışkanlığıyla "değişmez" dersin, ama SQL Server büyük "D"yi de değiştirdi. CHARINDEX de büyük harfle yazdığımız MESSIAH'ı buldu. Sebep collation: sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) veritabanı SQL_Latin1_General_CP1_CI_AS ile kurulu ve CI büyük-küçük harf duyarsız demek. T-SQL'in string function'ları karşılaştırma yaparken girdinin collation'ını kullanır; üçüncü column'da collation'ı case-sensitive (CS) bir collation'a çevirince "d" hiçbir yerde eşleşmedi. Serinin 03. yazısında collation'ın sıralamayı ve karşılaştırmayı nasıl etkilediğini görmüştük; aynı kural REPLACE, CHARINDEX ve TRANSLATE için de geçerli. PostgreSQL'de ise replace('Dune Messiah', 'd', 'X') "Dune Messiah" döndürüyor, çünkü orada string function'ları her zaman case-sensitive.

07. yazıda scalar function'lara genel bir giriş yapmış, CONCAT, LEN/DATALENGTH ve sargability konusunu görmüştük. Bu yazı ve sonraki üç yazı T-SQL'in her gün kullanılan function'larını tek tek ele alıyor: tanımı, NULL davranışı, sınır durumları ve PostgreSQL karşılığı. Bu yazının konusu string function'ları.

Konum Bulmak ve Parça Almak: CHARINDEX, PATINDEX, LEFT, RIGHT ve SUBSTRING

CHARINDEX(search, text [, start]) aranan ifadenin ilk geçtiği konumu 1'den sayarak döndürür. Bulamazsa 0, argümanlardan biri NULL ise NULL döner. Üçüncü argüman aramanın kaçıncı karakterden başlayacağını söyler. PATINDEX ise LIKE'taki joker karakterleri kabul eder, ama pattern'in başına ve sonuna % koymak gerekir:

SELECT CHARINDEX(N'e', N'Middlemarch: A Study of Provincial Life') AS first_e,
       CHARINDEX(N'e', N'Middlemarch: A Study of Provincial Life', 7) AS next_e,
       CHARINDEX(N'x', N'Middlemarch') AS not_found,
       CHARINDEX(NULL, N'Middlemarch') AS null_search,
       PATINDEX(N'%[0-9]%', N'Fahrenheit 451') AS first_digit,
       SUBSTRING(N'Fahrenheit 451', PATINDEX(N'%[0-9]%', N'Fahrenheit 451'), 10) AS digits;
first_e  next_e  not_found  null_search  first_digit  digits
-------  ------  ---------  -----------  -----------  ------
6        39      0          NULL         12           451

"Bulunamadı" sonucunun 0 olması önemli: SUBSTRING(x, CHARINDEX(...) + 1, ...) gibi bir ifadede bulunamayan ayraç 1. konumdan başlayan bir parça üretir, hata vermez. Bu tür bir ifadeyi yazarken "ayraç yoksa ne olacak" sorusunu ayrıca cevaplaman gerekir.

LEFT, RIGHT ve SUBSTRING sınır dışı değerlere hoşgörülüdür. String'den uzun bir uzunluk istersen olduğu kadarını alırsın. SUBSTRING'e 0 ya da negatif başlangıç verirsen string'in öncesindeki "hayali" konumlar da sayılır ve sonuç kısalır:

SELECT LEFT(N'Dune', 10) AS left_too_long, SUBSTRING(N'Dune', 3, 100) AS sub_too_long,
       SUBSTRING(N'Dune', 0, 3) AS sub_from_zero, SUBSTRING(N'Dune', -1, 3) AS sub_negative;
left_too_long  sub_too_long  sub_from_zero  sub_negative
-------------  ------------  -------------  ------------
Dune           ne            Du             D

SUBSTRING(N'Dune', 0, 3) 0, 1 ve 2. konumları kapsıyor; 0. konumda karakter olmadığı için yalnız iki karakter geldi. Hoşgörünün bittiği yer negatif uzunluk:

SELECT LEFT(N'Dune', -1) AS left_negative;
Msg 536, Level 16, State 6, Line 1
Invalid length parameter passed to the left function.

Bu hata çoğunlukla LEFT(x, CHARINDEX(' ', x) - 1) gibi bir ifadede, ayracı içermeyen ilk row'a gelindiğinde ortaya çıkar: CHARINDEX 0 döndürür, uzunluk -1 olur ve sorgu o row'da durur.

Gerçek bir örnek: bir tedarikçi yazar listesini "Ad Soyad" biçiminde tek bir column'da gönderiyor ve biz katalogda "Soyad, Ad" göstermek istiyoruz. Soyadı son boşluktan sonraki kısım; T-SQL'de "son geçtiği yer" function'ı olmadığı için string'i REVERSE ile ters çevirip ilk boşluğu arıyoruz:

DROP TABLE IF EXISTS dbo.supplier_authors;
CREATE TABLE dbo.supplier_authors (
    line_no   INT           NOT NULL PRIMARY KEY,
    full_name NVARCHAR(100) NOT NULL
);
INSERT INTO dbo.supplier_authors (line_no, full_name)
SELECT author_id, CONCAT_WS(N' ', first_name, last_name)
FROM dbo.authors
WHERE author_id IN (1, 10, 20, 28, 67);

SELECT full_name,
       CONCAT(RIGHT(full_name, CHARINDEX(N' ', REVERSE(full_name)) - 1), N', ',
              LEFT(full_name, LEN(full_name) - CHARINDEX(N' ', REVERSE(full_name)))) AS sort_name
FROM dbo.supplier_authors
ORDER BY line_no;
full_name             sort_name
--------------------  ---------------------
Jane Austen           Austen, Jane
Miguel de Cervantes   Cervantes, Miguel de
Ursula K. Le Guin     Guin, Ursula K. Le
J. R. R. Tolkien      Tolkien, J. R. R.
Ahmet Hamdi Tanpınar  Tanpınar, Ahmet Hamdi

Beş isimden üçü doğru. "Cervantes, Miguel de" ve "Guin, Ursula K. Le" yanlış, çünkü "de" ve "Le" soyadın parçası. String function'larıyla bunu kurala bağlayamazsın; ad ve soyadı baştan ayrı column'larda tutmak tek güvenilir çözüm. Bookstore'un authors table'ı da bu yüzden first_name ve last_name'i ayrı tutuyor. Tedarikçi verisini içeri alırken bu tür parçalama yalnız bir öneri üretir; sonucu bir insanın kontrol etmesi gerekir.

Değiştirmek: REPLACE, STUFF, TRANSLATE, REPLICATE ve REVERSE

REPLACE bir ifadenin bütün geçişlerini değiştirir. STUFF(text, start, length_to_delete, insert) ise konuma göre çalışır: belirtilen konumdan itibaren bir parçayı silip yerine başka bir şey koyar. Silinecek uzunluk 0 ise yalnız ekleme yapar. E-posta adresini maskelemek için ilk karakterden sonrasını @'e kadar yıldızla değiştirelim:

SELECT TOP (3) email,
       STUFF(email, 2, CHARINDEX('@', email) - 2, REPLICATE('*', CHARINDEX('@', email) - 2)) AS masked
FROM dbo.customers
ORDER BY customer_id;
email                           masked
------------------------------  ------------------------------
ada.lovelace@example.com        a***********@example.com
Alan.Turing@Example.com         A**********@Example.com
grace.hopper+books@example.org  g*****************@example.org

REPLICATE(text, n) metni n kez tekrar eder; n 0 ise boş string, negatif ise NULL döndürür. Aynı function ISBN'i (kitaplara verilen uluslararası standart numara) ekranda okunur göstermek için de işe yarar: STUFF(isbn, 4, 0, '-') "9798900000268"i "979-8900000268" yapar. Gerçek ISBN tirelemesi ülke ve yayıncı aralıklarına göre değiştiği için doğru tirelemeyi bir tablo olmadan string function'larıyla üretemezsin.

TRANSLATE(text, characters, replacements) SQL Server 2017 ile geldi ve karakter karakter çalışır: ikinci argümandaki her karakteri üçüncüdeki aynı sıradaki karakterle değiştirir. Türkçe kitap adlarından ASCII (yalnız İngilizce harf, rakam ve temel işaretlerden oluşan karakter seti) bir slug üretmek için iç içe altı REPLACE yerine tek bir çağrı yeterli:

SELECT title,
       LOWER(REPLACE(TRANSLATE(title, N'çğıöşüÇĞİÖŞÜ', N'cgiosuCGIOSU'), N' ', N'-')) AS slug
FROM dbo.books
WHERE book_id BETWEEN 74 AND 77
ORDER BY book_id;
title                        slug
---------------------------  ---------------------------
Kürk Mantolu Madonna         kurk-mantolu-madonna
İnce Memed                   ince-memed
Tutunamayanlar               tutunamayanlar
Saatleri Ayarlama Enstitüsü  saatleri-ayarlama-enstitusu

İki liste aynı uzunlukta olmak zorunda; PostgreSQL'deki translate fazla karakterleri siler, T-SQL ise hata verir:

SELECT TRANSLATE(N'abc', N'ab', N'xyz') AS bad_translate;
Msg 9828, Level 16, State 3, Line 1
The second and third arguments of the TRANSLATE built-in function must contain an equal number of characters.

TRANSLATE bir karakteri yalnız tek bir karaktere çevirebilir; "ß"yi "ss"e çevirmek gibi işler için yine REPLACE gerekir. Ayrıca REPLACE gibi collation'a uyar: bu veritabanında TRANSLATE(N'Dune', N'd', N'X') da "Xune" döndürüyor. REVERSE ise yukarıda gördüğümüz gibi en çok "son geçtiği yeri bul" hilesinde kullanılır.

Büyük-Küçük Harf, Boşluk ve N Öneki

UPPER ve LOWER dönüşümü de collation'a göre yapar. Türkçe "i" ve "ı" bu yüzden ayrı bir dikkat ister:

SELECT UPPER(N'istanbul') AS upper_db,
       UPPER(N'istanbul' COLLATE Turkish_100_CI_AS) AS upper_turkish,
       LOWER(N'IŞIK') AS lower_db,
       LOWER(N'IŞIK' COLLATE Turkish_100_CI_AS) AS lower_turkish;
upper_db  upper_turkish  lower_db  lower_turkish
--------  -------------  --------  -------------
ISTANBUL  İSTANBUL       işik      ışık

Veritabanının varsayılan collation'ı "IŞIK"ı "işik" yaptı: "Ş"yi doğru küçülttü ama "I"yı Türkçe kurala göre "ı" değil "i" yaptı. Türkçe metni büyütüp küçültüyorsan collation'ı ifadede açıkça yaz. Bu konuyu 03. yazıda Türkçe İ tuzağıyla ayrıntılı görmüştük.

Daha sinsi bir sorun string literal'ında N önekini unutmak. N'...' bir NVARCHAR (Unicode) literal'ı, öneksiz '...' ise veritabanının code page'indeki bir VARCHAR literal'ıdır. Bu veritabanının code page'i 1252 ve orada "İ", "ı" ve "ş" yok:

DECLARE @t TABLE (id INT, title_v VARCHAR(50), title_n NVARCHAR(50));
INSERT INTO @t VALUES
    (1, 'İnce Memed', 'İnce Memed'),
    (2, N'İnce Memed', N'İnce Memed'),
    (3, 'Tutunamayanlar ışık', N'Tutunamayanlar ışık');
SELECT id, title_v, title_n FROM @t;
id  title_v              title_n
--  -------------------  -------------------
1   Ince Memed           Ince Memed
2   Ince Memed           İnce Memed
3   Tutunamayanlar isik  Tutunamayanlar ışık

Birinci row'da column NVARCHAR olmasına rağmen veri bozuldu: literal öneksiz yazıldığı için "İ" daha column'a gelmeden "I"ya çevrildi. Hata da uyarı da yok. İkinci ve üçüncü row'larda VARCHAR column'u da bu karakterleri saklayamadı. Kural basit: NVARCHAR column'a giden her literal N önekiyle yazılır. 07. yazıda bunun tersini, VARCHAR column'u NVARCHAR parametreyle aramanın index'i kaybettirmesini görmüştük; iki kuralı birlikte düşün: literal'ın tipi column'un tipiyle aynı olmalı.

LTRIM, RTRIM ve TRIM varsayılan olarak yalnız boşluk karakterini (32) siler; tab ve satır sonu kalır. TRIM'e silinecek karakterleri FROM ile verebilirsin (2017'den beri; LTRIM/RTRIM'in ikinci argümanı 17. yazıda gördüğümüz gibi 2022 ile geldi):

SELECT LEN(TRIM(CHAR(9) + N'Emma' + CHAR(13) + CHAR(10))) AS default_trim,
       LEN(TRIM(CHAR(9) + CHAR(13) + CHAR(10) + N' ' FROM CHAR(9) + N'Emma' + CHAR(13) + CHAR(10))) AS whitespace_trim;
default_trim  whitespace_trim
------------  ---------------
7             4

Excel'den ya da bir formdan kopyalanan değerlerde sondaki satır sonu sık görülür ve WHERE title = N'Emma' eşleşmesini sessizce bozar.

Son olarak QUOTENAME: bir nesne adını köşeli parantez içine alır ve içindeki ] karakterini iki katına çıkarır. Dynamic SQL'de table ya da column adını string'e eklemenin güvenli yolu budur:

SELECT QUOTENAME(N'order items') AS bracketed,
       QUOTENAME(N'odd]name') AS escaped,
       QUOTENAME(N'O''Neil', '''') AS single_quoted,
       QUOTENAME(REPLICATE(N'x', 129)) AS too_long;
bracketed      escaped      single_quoted  too_long
-------------  -----------  -------------  --------
[order items]  [odd]]name]  'O''Neil'      NULL

Girdi 128 karakterden uzunsa QUOTENAME NULL döndürür; çünkü SQL Server'da bir identifier en fazla 128 karakter olabilir. Dynamic SQL ve parametre kullanımı SQL Server Derinlemesine serisinin "Stored Procedure'ler: Parametreler, Hata Yönetimi, Yetki Zinciri ve Güvenli Dynamic SQL" yazısında.

Birleştirmek: +, CONCAT, CONCAT_WS ve STRING_AGG

Üç birleştirme yolunun NULL davranışı farklı. Barbara Liskov'un şehri NULL:

SELECT customer_id, first_name, city,
       first_name + N' (' + city + N')' AS plus_op,
       CONCAT(first_name, N' (', city, N')') AS concat_fn,
       CONCAT_WS(N' / ', first_name, city, country_code) AS concat_ws_fn
FROM dbo.customers
WHERE customer_id IN (6, 7)
ORDER BY customer_id;
customer_id  first_name  city    plus_op          concat_fn        concat_ws_fn
-----------  ----------  ------  ---------------  ---------------  --------------------
6            Donald      Berlin  Donald (Berlin)  Donald (Berlin)  Donald / Berlin / DE
7            Barbara     NULL    NULL             Barbara ()       Barbara / DE

+ tek bir NULL'da bütün sonucu NULL yapar (standart || ve SQL Server 2025'in ||'ı da öyle; 18. yazı). CONCAT NULL'ı boş string sayar ve "Barbara ()" gibi yarım bir metin üretir. CONCAT_WS NULL argümanı ayıracıyla birlikte atlar. Boş string ise NULL değildir: CONCAT_WS(N', ', N'Emma', N'', N'Dune') "Emma, , Dune" döndürür. Bütün argümanlar NULL olduğunda da CONCAT ve CONCAT_WS NULL değil boş string döndürür.

Bu son ayrıntı STRING_AGG ile birleşince bir tuzak kurar. 10. yazıda STRING_AGG'i ve WITHIN GROUP ile sıralamayı görmüştük. STRING_AGG NULL değerleri atlar; ama girdiyi CONCAT_WS ile hazırlarsan NULL boş string'e dönüşür ve atlanmaz. Beowulf'un yazar kaydı yok:

SELECT b.book_id,
       STRING_AGG(CONCAT_WS(N' ', a.first_name, a.last_name), N'; ') AS via_concat_ws,
       STRING_AGG(a.last_name, N'; ') AS via_column
FROM dbo.books AS b
LEFT JOIN dbo.book_authors AS ba ON ba.book_id = b.book_id
LEFT JOIN dbo.authors AS a ON a.author_id = ba.author_id
WHERE b.book_id IN (24, 42)
GROUP BY b.book_id
ORDER BY b.book_id;
book_id  via_concat_ws                 via_column
-------  ----------------------------  -----------------
24                                     NULL
42       Terry Pratchett; Neil Gaiman  Pratchett; Gaiman

Birinci column Beowulf için boş string, ikincisi NULL. API bu iki durumu farklı serileştirir ("" ve null) ve Angular tarafındaki "yazar bilinmiyor" kontrolü birinde çalışıp diğerinde çalışmaz. Hangisini istediğine karar ver ve gerekiyorsa NULLIF(..., N'') ile boş string'i NULL'a geri çevir.

STRING_AGG'in ikinci tuzağı dönüş tipi. Girdi VARCHAR(1..8000) ise sonuç VARCHAR(8000), NVARCHAR(1..4000) ise NVARCHAR(4000) olur. Sonuç bu sınırı aşarsa kırpılmaz, sorgu hata verir:

SELECT LEN(STRING_AGG(CAST(CONCAT(o.order_id, ':', c.email) AS VARCHAR(300)), ',')) AS too_long
FROM dbo.orders AS o
JOIN dbo.customers AS c ON c.customer_id = o.customer_id;
Msg 9829, Level 16, State 1, Line 1
STRING_AGG aggregation result exceeded the limit of 8000 bytes. Use LOB types to avoid result truncation.

Girdiyi VARCHAR(MAX)'a çevirince aynı sorgu 11.295 karakterlik sonucu döndürdü. Test verisinde 40 müşteri varken çalışan bir rapor, veri büyüdüğünde bu hatayla durur; listenin ne kadar uzayabileceğini bilmiyorsan girdiyi baştan MAX tipine çevir.

Parçalamak: STRING_SPLIT

Ters yön: virgülle ayrılmış bir id listesini satırlara çevirmek. Angular'daki çoklu seçimli bir filtre ?ids=26,42,60 gibi bir parametre gönderdiğinde ilk akla gelen yol bu. STRING_SPLIT 2016'dan beri var, ama üç ayrıntısı her zaman unutulur:

SELECT '[' + value + ']' AS value, ordinal
FROM STRING_SPLIT('12,7,,42, 26', ',', 1)
ORDER BY ordinal;
value  ordinal
-----  -------
[12]   1
[7]    2
[]     3
[42]   4
[ 26]  5

Birincisi, iki ayraç arasındaki boşluk boş string bir row üretir. İkincisi, değerler kırpılmaz: " 26" baştaki boşlukla geldi. Üçüncüsü, sıra. Microsoft Learn'e göre ordinal olmadan dönen row'ların sırası girdideki sıraya uymak zorunda değil. enable_ordinal argümanı (üçüncü argüman, 1) SQL Server 2022 ile geldi ve parçanın girdideki konumunu bigint olarak verir; sıra önemliyse onu kullan ve ORDER BY ordinal yaz.

value column'u string'dir. Onu doğrudan INT bir column'la karşılaştırırsan SQL Server her değeri INT'e çevirmeye çalışır ve ilk bozuk değerde sorgu durur:

DECLARE @ids VARCHAR(100) = '26,42,abc';
SELECT b.book_id, b.title
FROM dbo.books AS b
JOIN STRING_SPLIT(@ids, ',') AS s ON s.value = b.book_id;
Msg 245, Level 16, State 1, Line 2
Conversion failed when converting the varchar value 'abc' to data type int.

TRY_CAST ile çevirip LEFT JOIN yaparsan hem bozuk değeri hem bulunamayan id'yi görürsün:

DECLARE @ids VARCHAR(100) = '26,42,abc,60, 39';
SELECT s.ordinal, s.value, b.book_id, b.title
FROM STRING_SPLIT(@ids, ',', 1) AS s
LEFT JOIN dbo.books AS b ON b.book_id = TRY_CAST(s.value AS INT)
ORDER BY s.ordinal;
ordinal  value  book_id  title
-------  -----  -------  ---------------
1        26     26       Dune
2        42     42       Good Omens
3        abc    NULL     NULL
4        60     60       Design Patterns
5         39    39       The Hobbit

TRY_CAST baştaki boşluğa takılmadı, " 39"u 39'a çevirdi. Ayraç tek karakter olmak zorunda; STRING_SPLIT('Dune -- Emma', '--') "Procedure expects parameter 'separator' of type 'nchar(1)/nvarchar(1)'" hatasıyla (Msg 214) reddedilir. Birden çok karakterli ya da pattern'li ayraç için 18. yazıdaki REGEXP_SPLIT_TO_TABLE var. Uygulamadan çok sayıda id gönderiyorsan virgüllü string yerine table-valued parameter ya da JSON parametresi daha sağlam; table-valued parameter'ı (TVP; tablo biçiminde gönderilen parametre) Dapper serisinin SQL Server'a özgü parametreleri anlatan yazısında, JSON parametresini bu serinin ilerideki JSON yazısında görebilirsin.

Function Özeti ve PostgreSQL Karşılıkları

Bu yazıdaki function'ların NULL davranışı ve PostgreSQL 18'deki karşılıkları. PostgreSQL sütunundaki her ifadeyi kendi container'ımda çalıştırdım:

T-SQLNULL ve sınır davranışıPostgreSQL
LEN(s)Sondaki boşluğu saymaz; NULL → NULLlength(s), sondaki boşluğu sayar (text)
CHARINDEX(a, s [, start])Bulamazsa 0; collation'a uyarposition(a IN s), strpos; case-sensitive
SUBSTRING(s, start, len)Negatif uzunlukta hata; 2025'te len opsiyonelsubstring(s FROM start FOR len)
STUFF(s, start, len, new)Başlangıç sınır dışıysa NULLoverlay(s PLACING new FROM start FOR len)
TRANSLATE(s, from, to)Listeler eşit uzunlukta olmalı, yoksa hatatranslate, fazla karakterleri siler
CONCAT_WS(sep, ...)NULL'ı atlar, hepsi NULL ise boş stringconcat_ws
STRING_AGG(x, sep)NULL'ı atlar; MAX olmayan girdide 8000 byte sınırıstring_agg(x, sep ORDER BY ...)
STRING_SPLIT(s, sep, 1)NULL girdi → boş table; ayraç tek karakterunnest(string_to_array(s, sep)) WITH ORDINALITY
QUOTENAME(s)128 karakterden uzunsa NULLquote_ident(s)

Tablodaki iki fark günlük işte sık karşına çıkar. PostgreSQL'de length('Dune ') 7 döndürüyor, T-SQL'deki LEN 4. Son kelimeyi almak PostgreSQL'de tek adım: split_part('Ursula K. Le Guin', ' ', -1) "Guin" döndürüyor (negatif konum PostgreSQL 14 ile geldi); T-SQL'de aynı iş için yukarıdaki REVERSE hilesi gerekiyor.

Üretimde Dikkat

String function'larının asıl maliyeti, 07. yazıda gördüğümüz gibi WHERE'de column'un üzerine konduklarında ortaya çıkar. LEFT ile LIKE aynı soruyu sorar ama plan farklıdır:

SET SHOWPLAN_TEXT ON;
GO
SELECT customer_id FROM dbo.customers WHERE LEFT(email, 5) = 'grace';
SELECT customer_id FROM dbo.customers WHERE email LIKE 'grace%';
GO
SET SHOWPLAN_TEXT OFF;
  |--Index Scan(OBJECT:([sqlseries_ext_19].[dbo].[customers].[UQ_customers_email]),  WHERE:(substring([sqlseries_ext_19].[dbo].[customers].[email],(1),(5))='grace'))

  |--Index Seek(OBJECT:([sqlseries_ext_19].[dbo].[customers].[UQ_customers_email]), SEEK:([sqlseries_ext_19].[dbo].[customers].[email] >= 'gracDþ' AND [sqlseries_ext_19].[dbo].[customers].[email] < 'gracF'),  WHERE:([sqlseries_ext_19].[dbo].[customers].[email] like 'grace%') ORDERED FORWARD)

Optimizer LEFT'i substring'e çevirdi ve index'in tamamını taradı; LIKE 'grace%' ise bir aralık aramasına dönüştü. Her sorguda aynı string ifadesini hesaplıyorsan (ör. e-postanın domain'i) o ifadeyi bir computed column'a koyup index'lemek ayrı bir tasarım kararı; SQL Server Derinlemesine serisinde index'lerle birlikte ele alınıyor.

İkinci konu tip. VARCHAR ile NVARCHAR karıştığında hem veri kaybı (öneksiz literal) hem index kaybı (implicit conversion) yaşanır. REPLACE, STUFF ve CONCAT gibi function'ların sonuç tipi girdinin tipinden gelir; bir NVARCHAR girdi bütün ifadeyi NVARCHAR yapar. Üçüncüsü, büyük metinler: MAX olmayan tiplerde iki function da 8000 byte sınırına bağlı, ama farklı davranıyor. STRING_AGG yukarıdaki gibi hata veriyor; REPLACE ise sonucu sessizce kesiyor. 8000 karakterlik bir VARCHAR'daki her "a"yı "bb" yapınca sonuç 16.000 değil 8.000 karakter oldu; girdi VARCHAR(MAX) olunca 16.000. Raporu küçük test verisiyle değil, production'daki en uzun değerle dene.

Sık Düşülen Tuzaklar

  • REPLACE ve CHARINDEX'in collation'a uyduğunu unutmak; case-insensitive bir veritabanında "d" araması "D"yi de bulur.
  • NVARCHAR column'a giden literal'da N önekini unutmak; "İ", "ı" ve "ş" hiçbir uyarı olmadan bozulur.
  • CHARINDEX'in 0 döndürdüğü row'u düşünmeden LEFT(x, CHARINDEX(...) - 1) yazmak (Msg 536).
  • STRING_SPLIT çıktısının sırasına güvenmek; sıra gerekiyorsa enable_ordinal ve ORDER BY ordinal.
  • STRING_SPLIT'in value'sunu doğrudan INT column'la karşılaştırmak; ilk bozuk değerde sorgu durur.
  • STRING_AGG'i VARCHAR(n) girdiyle büyük gruplarda çalıştırmak (Msg 9829) ve CONCAT_WS'in NULL yerine boş string üretmesi.
  • WHERE LEFT(column, n) = ... ile index'i kullanılamaz hâle getirmek; LIKE 'prefix%' yaz.

Küçük Bir Deneme

★☆☆ customers table'ındaki e-posta adreslerinden domain'i SUBSTRING ve CHARINDEX ile çıkar, LOWER ile normalize et ve domain başına müşteri sayısını bul. Sonra @ içermeyen bir test adresi ekleyip sorgunun ne döndürdüğüne bak.

★★☆ Her kategori için kitap adlarını alfabetik sırayla tek bir column'da topla (STRING_AGG ... WITHIN GROUP). Kitabı olmayan Poetry kategorisi sonuçta NULL mı, boş string mi görünüyor? İkisinden hangisini istediğine karar verip sorguyu ona göre düzelt.

★★★ "Ad Soyad" parçalama örneğini, soyad önekleri listesini (de, Le, van, von) bir table'da tutan ve son iki kelimeyi önek listesine göre birleştiren bir sorguya çevir. Miguel de Cervantes ve Ursula K. Le Guin doğru çıkıyor mu? Hangi isim hâlâ yanlış kalıyor?

Bir sonraki yazıda tarih ve saat function'larına geçiyoruz: SYSDATETIME ile SYSUTCDATETIME farkı, DATEADD, DATEDIFF'in sınır sayma mantığı, EOMONTH, DATEFROMPARTS ve UTC (Coordinated Universal Time; koordineli evrensel zaman) ile İstanbul saati arasında AT TIME ZONE ile dönüşüm.