SQL Server 2025 ile T-SQL'e Gelenler: REGEXP Function'ları, Native JSON Tipi, || Operatörü, PRODUCT ve CURRENT_DATE
SQL Server 2025, RE2 tabanlı REGEXP_LIKE/REPLACE/SUBSTR/INSTR/COUNT/MATCHES/SPLIT_TO_TABLE, native json tipi, JSON_ARRAYAGG/JSON_OBJECTAGG, || operatörü, PRODUCT, CURRENT_DATE, UNISTR ve BASE64 getirdi. Regex'in collation'ı yok saymasını, sabit önekli pattern'in index seek'ini, preview fuzzy function'ları ve REGEXP_LIKE'ın compatibility level 170 gereksinimini gerçek çıktılarla gösteriyoruz.
Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0, CU10 (Cumulative Update 10)) üzerinde serinin Bookstore veritabanıyla, compatibility level 170 ve 160 ile çalıştırılmıştır.
Önce Tahmin Et
Kayıt formundan gelen e-posta adresleri bir staging tablosunda bekliyor. Uzun yıllar SQL Server'da e-posta biçimini LIKE ile kontrol ettik, çünkü başka araç yoktu. Altı adresten kaçı şu kontrolden geçer?
DROP TABLE IF EXISTS dbo.signups;
CREATE TABLE dbo.signups (
signup_id INT IDENTITY(1,1) PRIMARY KEY,
email VARCHAR(254) NOT NULL
);
INSERT INTO dbo.signups (email) VALUES
('margaret.hamilton@example.com'),
('ada@@example.com'),
('grace@example'),
('linus@exa mple.com'),
('.alan@example.com'),
('katherine.johnson@example.co.uk');
SELECT email,
CASE WHEN email LIKE '%_@_%._%' THEN 'pass' ELSE 'fail' END AS like_check
FROM dbo.signups
ORDER BY signup_id;
email like_check
------------------------------- ----------
margaret.hamilton@example.com pass
ada@@example.com pass
grace@example fail
linus@exa mple.com pass
.alan@example.com pass
katherine.johnson@example.co.uk pass
Beşi. Çift @'li, boşluklu ve noktayla başlayan adresler de geçti; yalnız alan adında nokta olmayan grace@example takıldı. LIKE'ın joker karakterleri (%, _, [...]) "herhangi bir şey" demekten öteye gidemez. SQL Server 2025 nihayet regular expression (regex) function'larını getirdi. Bu yazı onları, native json tipini, || operator'ünü ve 2025'in diğer dil eklemelerini, yine compatibility level gereksinimleriyle birlikte deniyor.
REGEXP_LIKE ve CHECK Constraint
Aynı altı adres, makul bir e-posta regex'iyle:
SELECT email,
CASE WHEN REGEXP_LIKE(email, '^[A-Za-z0-9_%+-]+(\.[A-Za-z0-9_%+-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$')
THEN 'pass' ELSE 'fail' END AS regex_check
FROM dbo.signups
ORDER BY signup_id;
email regex_check
------------------------------- -----------
margaret.hamilton@example.com pass
ada@@example.com fail
grace@example fail
linus@exa mple.com fail
.alan@example.com fail
katherine.johnson@example.co.uk pass
Bu kez yalnız gerçekten geçerli görünen iki adres geçti. Regex'i yazarken amacım "RFC (Request for Comments) 5322'nin tamamı" değil, "bariz hataları yakala" idi: yerel kısımda harf, rakam ve birkaç işaret, noktalar arasında boş parça yok, @'ten sonra en az bir nokta ve en az iki harfli bir uzantı. E-posta doğrulamanın son sözü her zaman doğrulama e-postasıdır.
REGEXP_LIKE bir boolean predicate olduğu için doğrudan WHERE'de ve CHECK constraint'te kullanılabilir. Constraint'i mevcut tabloya eklemeye çalışınca:
ALTER TABLE dbo.signups ADD CONSTRAINT CK_signups_email
CHECK (REGEXP_LIKE(email, '^[A-Za-z0-9_%+-]+(\.[A-Za-z0-9_%+-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$'));
Msg 547, Level 16, State 1, Server b8be943222fa, Line 1
The ALTER TABLE statement conflicted with the CHECK constraint "CK_signups_email". The conflict occurred in database "sqlt_18", table "dbo.signups", column 'email'.
Serinin 04. yazısında gördüğümüz gibi SQL Server yeni bir constraint'i eklerken mevcut veriyi de kontrol eder. Geçersiz row'ları temizleyip constraint'i ekledim ve yeni bir geçersiz adres göndermeyi denedim:
DELETE FROM dbo.signups WHERE signup_id IN (2, 3, 4, 5);
ALTER TABLE dbo.signups ADD CONSTRAINT CK_signups_email
CHECK (REGEXP_LIKE(email, '^[A-Za-z0-9_%+-]+(\.[A-Za-z0-9_%+-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$'));
INSERT INTO dbo.signups (email) VALUES ('grace@example');
Msg 547, Level 16, State 1, Server b8be943222fa, Line 4
The INSERT statement conflicted with the CHECK constraint "CK_signups_email". The conflict occurred in database "sqlt_18", table "dbo.signups", column 'email'.
The statement has been terminated.
Artık kural veritabanında. Uygulama tarafındaki doğrulamayı (ASP.NET Core Web API (Application Programming Interface) serisinin Model Validation yazısı) gereksiz kılmaz, ama farklı bir yoldan gelen veriyi (toplu import, elle yazılan script) de yakalar.
Bilmen gereken önemli bir fark: regex function'ları collation'ı dikkate almaz. Microsoft Learn bunu açıkça belirtir; eşleşme collation kurallarına değil pattern'e göre yapılır. Bookstore veritabanı büyük-küçük harf duyarsız bir collation kullanıyor ve Alan.Turing@Example.com adresi büyük harfle başlıyor:
SELECT COUNT(*) AS like_matches FROM dbo.customers WHERE email LIKE 'alan.%';
SELECT COUNT(*) AS regex_default FROM dbo.customers WHERE REGEXP_LIKE(email, '^alan\.');
SELECT COUNT(*) AS regex_i_flag FROM dbo.customers WHERE REGEXP_LIKE(email, '^alan\.', 'i');
like_matches
------------
1
regex_default
-------------
0
regex_i_flag
------------
1
LIKE collation sayesinde buldu, regex varsayılan olarak büyük-küçük harf duyarlı olduğu için bulmadı, 'i' flag'i ile buldu. Aynı sebeple Türkçe İ/ı gibi dile özgü kurallar regex'te geçerli değildir. SQL Server'ın regex engine'i RE2 (Google'ın açık kaynak regular expression kütüphanesi); RE2 geri izleme (backtracking) yapmadığı için kötü yazılmış bir pattern'in üstel sürede çalışması (ReDoS (Regular expression Denial of Service)) gibi riskler taşımaz, ama karşılığında backreference ve lookaround desteklemez.
REGEXP_REPLACE, REGEXP_SUBSTR, REGEXP_INSTR ve REGEXP_COUNT
Telefon numaralarını normalize etmek, yani rakam ve + dışındaki her şeyi silmek, 2025 öncesinde iç içe REPLACE zinciri demekti:
SELECT customer_id, phone,
REGEXP_REPLACE(phone, '[^0-9+]', '') AS phone_normalized
FROM dbo.customers
WHERE phone IS NOT NULL AND customer_id <= 12
ORDER BY customer_id;
customer_id phone phone_normalized
----------- ----------------- ----------------
4 +90 555 017 95 53 +905550179553
5 +90 555 012 91 77 +905550129177
9 +90 555 011 15 94 +905550111594
11 +90 555 011 68 88 +905550116888
12 +90 555 011 62 73 +905550116273
REGEXP_SUBSTR eşleşen parçayı, REGEXP_INSTR konumunu, REGEXP_COUNT ise eşleşme sayısını döndürür. E-postadan alan adını çıkarmak:
SELECT TOP (4) email,
REGEXP_SUBSTR(email, '@(.+)$', 1, 1, '', 1) AS domain,
REGEXP_INSTR(email, '@') AS at_position,
REGEXP_COUNT(email, '[0-9]') AS digits
FROM dbo.customers
ORDER BY customer_id;
email domain at_position digits
------------------------------ ----------- ----------- ------
ada.lovelace@example.com example.com 13 0
Alan.Turing@Example.com Example.com 12 0
grace.hopper+books@example.org example.org 19 0
linus.torvalds@example.com example.com 15 0
REGEXP_SUBSTR'ın son argümanı capture group numarası: @(.+)$ pattern'indeki parantezli grubu, yani @'ten sonrasını aldım. Ortadaki argümanlar başlangıç konumu (1), kaçıncı eşleşme (1) ve flag'ler (boş).
REGEXP_MATCHES ve REGEXP_SPLIT_TO_TABLE
İki regex function'ı table-valued: bir string'den row'lar üretirler. Design Patterns'ın alt başlığını boşluklara göre kelimelere bölmek:
SELECT b.title, s.value AS word, s.ordinal
FROM dbo.books AS b
CROSS APPLY REGEXP_SPLIT_TO_TABLE(b.subtitle, '\s+') AS s
WHERE b.book_id = 60
ORDER BY s.ordinal;
title word ordinal
--------------- --------------- -------
Design Patterns Elements 1
Design Patterns of 2
Design Patterns Reusable 3
Design Patterns Object-Oriented 4
Design Patterns Software 5
STRING_SPLIT tek karakterlik ayraçla çalışır; REGEXP_SPLIT_TO_TABLE ise "bir ya da daha fazla boşluk" gibi bir pattern'le böler ve sıra numarasını her zaman verir. REGEXP_MATCHES her eşleşmeyi bir row olarak döndürür; serbest metinden sayıları çıkarmak:
SELECT match_id, match_value
FROM REGEXP_MATCHES('Order 380 shipped with 2 items for 940.80 TRY', '[0-9]+(\.[0-9]+)?');
match_id match_value
-------- -----------
1 380
2 2
3 940.80
Regex ve Performans
Regex'in maliyeti iki yerden gelir: pattern'i her row için çalıştırmak ve index kullanamamak. İkincisini planda görebiliriz. customers.email üzerinde UQ_customers_email unique index'i var. LIKE 'alan.%':
SET SHOWPLAN_TEXT ON;
GO
SELECT customer_id FROM dbo.customers WHERE email LIKE 'alan.%';
GO
SET SHOWPLAN_TEXT OFF;
|--Index Seek(OBJECT:([sqlt_18].[dbo].[customers].[UQ_customers_email]), SEEK:([sqlt_18].[dbo].[customers].[email] >= 'alan-þ' AND [sqlt_18].[dbo].[customers].[email] < 'alan/'), WHERE:([sqlt_18].[dbo].[customers].[email] like 'alan.%') ORDERED FORWARD)
Başı sabit bir LIKE index seek'e dönüşür; serinin 07. yazısındaki sargability kuralı. Beni şaşırtan, başa sabitlenmiş (^ ile başlayan) bir regex'in de seek'e dönüşmesi oldu:
SET SHOWPLAN_TEXT ON;
GO
SELECT customer_id FROM dbo.customers WHERE REGEXP_LIKE(email, '^alan\.', 'i');
GO
SET SHOWPLAN_TEXT OFF;
|--Index Seek(OBJECT:([sqlt_18].[dbo].[customers].[UQ_customers_email]), SEEK:([sqlt_18].[dbo].[customers].[email] >= 'alan-þþ' AND [sqlt_18].[dbo].[customers].[email] < 'alan/'), WHERE:(regexp_like([sqlt_18].[dbo].[customers].[email],'^alan\.','i','alan-þþ','alan/',(30))) ORDERED FORWARD)
Optimizer pattern'in sabit önekinden (alan.) bir aralık türetip seek yaptı, regex'i yalnız o aralıktaki row'lara uyguladı. Başa sabitlenmemiş bir pattern'de ise bütün index taranıyor:
SET SHOWPLAN_TEXT ON;
GO
SELECT customer_id FROM dbo.customers WHERE REGEXP_LIKE(email, 'turing', 'i');
GO
SET SHOWPLAN_TEXT OFF;
|--Index Scan(OBJECT:([sqlt_18].[dbo].[customers].[UQ_customers_email]), WHERE:(regexp_like([sqlt_18].[dbo].[customers].[email],'turing','i',NULL,NULL,(1))))
Kural LIKE'takiyle aynı: önü sabitse index işe yarayabilir, değilse her row okunur ve her row için regex engine'i çalışır. Büyük tablolarda regex'i filtrenin tek koşulu yapmak yerine önce ucuz bir koşulla (tarih aralığı, status) row sayısını daralt. Bir sınır daha: varchar(max)/nvarchar(max) girdiler destekleniyor ama 2 MB ile sınırlı.
Native json Tipi ve JSON Aggregate'leri
2025 öncesinde JSON (JavaScript Object Notation) nvarchar(max) içinde, bir ISJSON CHECK constraint'iyle korunarak saklanırdı. 2025'in json tipi veriyi ikili (binary) biçimde saklar ve geçerliliği tipin kendisi garanti eder:
DROP TABLE IF EXISTS dbo.book_events;
CREATE TABLE dbo.book_events (
event_id INT IDENTITY(1,1) PRIMARY KEY,
payload JSON NOT NULL
);
INSERT INTO dbo.book_events (payload) VALUES
('{"type":"price_changed","bookId":26,"old":390.50,"new":420.00}'),
('{"type":"stock_low","bookId":18,"quantity":0}');
SELECT event_id,
JSON_VALUE(payload, '$.type') AS event_type,
JSON_VALUE(payload, '$.bookId') AS book_id
FROM dbo.book_events
ORDER BY event_id;
event_id event_type book_id
-------- ------------- -------
1 price_changed 26
2 stock_low 18
Bozuk bir JSON'u (sondaki virgüle dikkat) eklemeye çalışınca:
INSERT INTO dbo.book_events (payload) VALUES ('{"type":"stock_low","bookId":18,}');
Msg 13609, Level 16, State 9, Server b8be943222fa, Line 1
JSON text is not properly formatted. Unexpected character '}' is found at position 32.
nvarchar bir değişkende aynı metin sessizce kabul edilir; geçerliliği ancak sen sorarsan öğrenirsin:
DECLARE @text NVARCHAR(MAX) = N'{"type":"stock_low","bookId":18,}';
SELECT ISJSON(@text) AS is_valid_text;
is_valid_text
-------------
0
Native JSON tipi standarda SQL:2023 (T801) ile girdi. Bir önceki yazının son alıştırmasındaki "değişken sayıda yazarı array'e koy" sorununun 2025'teki cevabı JSON_ARRAYAGG:
SELECT b.book_id,
JSON_OBJECT('title': b.title,
'authors': JSON_ARRAYAGG(a.last_name ORDER BY ba.author_order)) AS book_json
FROM dbo.books AS b
JOIN dbo.book_authors AS ba ON ba.book_id = b.book_id
JOIN dbo.authors AS a ON a.author_id = ba.author_id
WHERE b.book_id IN (42, 60)
GROUP BY b.book_id, b.title
ORDER BY b.book_id;
book_id book_json
------- ----------------------------------------------------------------------------
42 {"title":"Good Omens","authors":["Pratchett","Gaiman"]}
60 {"title":"Design Patterns","authors":["Gamma","Helm","Johnson","Vlissides"]}
JSON_ARRAYAGG bir aggregate; GROUP BY ile her kitabın yazarlarını sıralı bir array'e topladı. JSON_OBJECTAGG ise key/value çiftlerinden bir object kurar:
SELECT JSON_OBJECTAGG(status: order_count) AS orders_by_status
FROM (SELECT status, COUNT(*) AS order_count FROM dbo.orders GROUP BY status) AS s;
orders_by_status
-----------------------------------------------------------------
{"Cancelled":24,"Delivered":345,"Paid":1,"Pending":1,"Shipped":8}
İki aggregate de standardın SQL:2016'daki SQL/JSON bölümünde tanımlı ve PostgreSQL 16'dan beri var. 2025 öncesinde aynı sonuç için FOR JSON PATH ve string birleştirme hileleri gerekirdi.
|| Operator'ü, SUBSTRING, PRODUCT ve CURRENT_DATE
16. yazıda gördüğümüz standart string birleştirme operator'ü || artık T-SQL'de (Transact-SQL) var ve + gibi NULL ile birleşince NULL üretir:
SELECT TOP (3) first_name || ' ' || last_name AS full_name,
first_name || ' ' || city AS with_city
FROM dbo.customers
WHERE customer_id IN (1, 6, 7)
ORDER BY customer_id;
full_name with_city
-------------- --------------
Ada Lovelace Ada Manchester
Donald Knuth Donald Berlin
Barbara Liskov NULL
Barbara Liskov'un şehri NULL olduğu için ikinci column NULL. ||= bileşik atama operator'ü de geldi:
DECLARE @label NVARCHAR(100) = N'Bookstore';
SET @label ||= N' / 2025';
SELECT @label AS label;
label
----------------
Bookstore / 2025
SUBSTRING'in üçüncü argümanı artık opsiyonel; verilmezse string'in sonuna kadar alır. Standart zaten böyle tanımlıyordu:
SELECT SUBSTRING(isbn, 4) AS without_prefix,
SUBSTRING(isbn, 4, 10) AS old_style
FROM dbo.books
WHERE book_id = 26;
without_prefix old_style
-------------- ----------
8900000268 8900000268
PRODUCT çarpım aggregate'i. Üst üste uygulanan indirimlerin toplam etkisini hesaplamak için:
SELECT PRODUCT(factor) AS combined_factor,
CAST(100 * (1 - PRODUCT(factor)) AS DECIMAL(5,2)) AS total_discount_pct
FROM (VALUES (0.90), (0.95), (0.98)) AS d(factor);
combined_factor total_discount_pct
--------------- ------------------
.837900 16.21
%10, %5 ve %2'lik indirimler toplam %17 değil, %16,21 ediyor. Büyüme oranlarında da işe yarar: aylık büyüme çarpanlarının çarpımı dönemin toplam büyümesidir.
WITH monthly AS (
SELECT DATETRUNC(month, o.ordered_at) AS month_start,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM dbo.orders AS o
JOIN dbo.order_items AS oi ON oi.order_id = o.order_id
WHERE o.status <> 'Cancelled' AND o.ordered_at >= '2025-09-01' AND o.ordered_at < '2026-01-01'
GROUP BY DATETRUNC(month, o.ordered_at)
),
growth AS (
SELECT month_start, revenue / LAG(revenue) OVER (ORDER BY month_start) AS factor
FROM monthly
)
SELECT CAST(PRODUCT(factor) AS DECIMAL(10,4)) AS sep_to_dec_factor
FROM growth
WHERE factor IS NOT NULL;
sep_to_dec_factor
-----------------
1.2554
Eylül'den Aralık'a üç aylık çarpanların çarpımı 1,2554; yani Aralık cirosu Eylül'ünkünün 1,2554 katı. Bu sonucu EXP(SUM(LOG(x))) hilesiyle de alırdın, ama o hile sıfır ve negatif değerlerde bozulur. PRODUCT standart değil.
CURRENT_DATE ise standardın tarih function'ı; 2025'e kadar T-SQL'de yoktu ve CAST(GETDATE() AS date) yazılırdı:
SELECT CURRENT_DATE AS current_date_value,
CAST(SYSUTCDATETIME() AS date) AS utc_date_old_style,
SQL_VARIANT_PROPERTY(CURRENT_DATE, 'BaseType') AS base_type;
current_date_value utc_date_old_style base_type
------------------ ------------------ ---------
2026-11-08 2026-11-08 date
Tipi date. Dikkat: CURRENT_DATE sunucunun yerel saat dilimine göre hesaplanır; container UTC (Coordinated Universal Time) olduğu için burada iki değer aynı. Türkiye'de çalışan bir sunucuda gece yarısından sonraki üç saat boyunca UTC tarihinden bir gün ileride olur.
UNISTR, BASE64 ve DATEADD'de bigint
UNISTR Unicode kod noktalarıyla string yazmanı sağlar. Kaynak dosyanın encoding'inden emin olmadığın script'lerde Türkçe karakterleri güvenle yazmak için:
SELECT UNISTR(N'\0130stanbul') AS city_name,
UNISTR(N'K\00FCrk Mantolu Madonna \2014 Sabahattin Ali') AS with_dash;
city_name with_dash
--------- -------------------------------------
İstanbul Kürk Mantolu Madonna — Sabahattin Ali
BASE64_ENCODE ve BASE64_DECODE binary veriyi metne ve geri çevirir; 2025 öncesinde bunun için XML (Extensible Markup Language) hileleri gerekirdi:
SELECT BASE64_ENCODE(CAST('Dune:26' AS VARBINARY(20))) AS encoded,
CAST(BASE64_DECODE('RHVuZToyNg==') AS VARCHAR(20)) AS decoded;
encoded decoded
------------ -------
RHVuZToyNg== Dune:26
DATEADD'in sayı argümanı artık bigint kabul ediyor. JavaScript'in Date.now() gibi milisaniye cinsinden Unix zaman damgaları int aralığını çok aşar; 2025 öncesinde bunu saniye ve milisaniye diye ikiye bölüp iki DATEADD ile çözerdin. Artık tek adım:
DECLARE @js_timestamp_ms BIGINT = 1767225600000;
SELECT DATEADD(millisecond, @js_timestamp_ms, CAST('1970-01-01' AS datetime2(3))) AS utc_time;
utc_time
-----------------------
2026-01-01 00:00:00.000
Üretimde Dikkat: Preview Function'lar ve Compatibility Level
2025 bir de bulanık metin eşleştirme function'ları getirdi: EDIT_DISTANCE, EDIT_DISTANCE_SIMILARITY, JARO_WINKLER_DISTANCE ve JARO_WINKLER_SIMILARITY. Microsoft Learn bunları hâlâ preview olarak işaretliyor ve "What's new" sayfası PREVIEW_FEATURES database scoped configuration'ını gerektirdiklerini yazıyor. Benim CU10 build'imde PREVIEW_FEATURES kapalıyken de çalıştılar, ama ilk denemede başka bir engelle karşılaştım:
SELECT title, EDIT_DISTANCE(title, N'Dnue') AS distance
FROM dbo.books
WHERE book_id = 26;
Msg 9847, Level 16, State 1, Server b8be943222fa, Line 1
The fuzzy string matching function does not support SQL_* collations. Use a Windows collation instead.
Bookstore veritabanı SQL_Latin1_General_CP1_CI_AS kullanıyor ve bu function'lar SQL_ ile başlayan collation'ları desteklemiyor. Column'u bir Windows collation'ına çevirince:
SELECT TOP (3) title,
EDIT_DISTANCE(title COLLATE Latin1_General_100_CI_AS, N'Dnue Mesiah') AS edit_distance,
JARO_WINKLER_SIMILARITY(title COLLATE Latin1_General_100_CI_AS, N'Dnue Mesiah') AS jw_similarity
FROM dbo.books
ORDER BY EDIT_DISTANCE(title COLLATE Latin1_General_100_CI_AS, N'Dnue Mesiah');
title edit_distance jw_similarity
------------ ------------- -------------
Dune Messiah 2 95
İnce Memed 6 65
Emma 8 56
"Dnue Mesiah" yazım hatasına en yakın kitap Dune Messiah, iki düzenleme mesafesinde. Arama kutusunda "bunu mu demek istediniz?" önerisi için çekici, ama preview etiketi şu demek: davranış ve syntax genel kullanıma geçmeden değişebilir. Production'da kullanma; denemek için bile sys.database_scoped_configurations'ta ne açık olduğunu kontrol et:
SELECT name, value FROM sys.database_scoped_configurations WHERE name = 'PREVIEW_FEATURES';
name value
---------------- -----
PREVIEW_FEATURES 0
Compatibility level tarafında 2025'in kuralı 2022'den farklı. Veritabanımı 160'a çekip denedim:
ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 160;
SELECT COUNT(*) FROM dbo.customers WHERE REGEXP_LIKE(email, '^alan');
Msg 195, Level 15, State 10, Server b8be943222fa, Line 1
'REGEXP_LIKE' is not a recognized built-in function name.
SELECT REGEXP_REPLACE('+90 555 017 95 53', '[^0-9+]', '') AS works_at_160;
works_at_160
-------------
+905550179553
SELECT 'a' || 'b' AS pipes, PRODUCT(x) AS product_value, SUBSTRING('abcdef', 4) AS substring_value
FROM (VALUES (2), (3)) AS t(x);
pipes product_value substring_value
----- ------------- ---------------
ab 6 def
Denemelerimde 160 seviyesinde REGEXP_LIKE, REGEXP_MATCHES ve REGEXP_SPLIT_TO_TABLE çalışmadı; REGEXP_REPLACE, REGEXP_SUBSTR, REGEXP_INSTR ve REGEXP_COUNT ise çalıştı. json tipi, JSON aggregate'leri, ||, PRODUCT, CURRENT_DATE, UNISTR, BASE64_* ve opsiyonel SUBSTRING uzunluğu 150 seviyesinde bile çalıştı. Yani aynı ailenin function'ları farklı kurallara tabi; "regex kullanıyorum, 160 yeter" diye düşünme. SQL Server 2025'e yükselttiğin bir veritabanı eski seviyesini korur ve REGEXP_LIKE içeren bir CHECK constraint'i deploy sırasında "not a recognized built-in function name" hatasıyla düşer. 2025'in diğer büyük eklemesi olan vector tipi ve AI (Artificial Intelligence) function'ları bu serinin değil, Yapay Zeka serilerinin konusu.
Sık Düşülen Tuzaklar
Regex'in collation'a uyduğunu sanmak. Büyük-küçük harf duyarsız bir veritabanında bile regex varsayılan olarak duyarlıdır; 'i' flag'ini kullan.
E-posta gibi karmaşık biçimler için "mükemmel" regex aramak. Bariz hataları yakala, gerisini doğrulama akışına bırak.
Başa sabitlenmemiş bir regex'i büyük bir tabloda tek filtre olarak kullanmak. Her row okunur ve her row için regex çalışır.
REGEXP_LIKE'ın compatibility level 170 istediğini bilmeden, yükseltilmiş bir veritabanına CHECK constraint deploy etmek.
JSON'u hâlâ nvarchar(max) içinde ve doğrulamasız saklamak. 2025'te json tipi geçerliliği garanti eder.
Preview function'ları production koduna koymak.
Küçük Bir Deneme
★☆☆ customers tablosundaki bütün telefonları REGEXP_REPLACE ile normalize et ve REGEXP_LIKE ile ^\+[0-9]{10,15}$ biçimine uymayanları bul.
★★☆ Her kategori için kitapları JSON_ARRAYAGG ile [{"id": ..., "title": ...}, ...] biçiminde bir array'e topla (JSON_OBJECT ile JSON_ARRAYAGG'i birleştir). Sonucu json tipinde bir column'a sahip yeni bir tabloya yaz.
★★★ REGEXP_LIKE ile LIKE'ı aynı sorguda yarıştır: customers tablosunu GENERATE_SERIES ile 100.000 row'a büyüttüğün bir kopyada başa sabitlenmiş ve sabitlenmemiş pattern'ler için SET STATISTICS IO ON ile logical read'leri karşılaştır. Süre ölçme; container emülasyonla çalıştığı için süreler yanıltıcı olur, logical read ve plan operator'leri değil.
Bu yazıyla SQL Temelleri serisini tamamladık: ilişkisel modelden başlayıp SELECT, join'ler, gruplama, subquery, CTE (Common Table Expression), window function, veri değiştirme, lehçe farkları ve sürüm yeniliklerine kadar sorgu dilinin kendisini gördük. Aynı Bookstore veritabanıyla devam edecek olan SQL Server Derinlemesine serisinde bu sorguların arkasına geçiyoruz: index'ler, execution plan'lar, statistics, transaction isolation ve lock'lar. Seri, SQL Server'ın yeni sürümleri çıktıkça güncellenmeye devam edecek.