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

SQL Server 2022 ile T-SQL'e Gelenler: IS DISTINCT FROM, GREATEST/LEAST, WINDOW, GENERATE_SERIES, DATE_BUCKET ve JSON Function'ları

Mert Özen 8 Kas 2026 12 dk 4 görüntülenme
SQL Server 2022 ile T-SQL'e Gelenler: IS DISTINCT FROM, GREATEST/LEAST, WINDOW, GENERATE_SERIES, DATE_BUCKET ve JSON Function'ları

SQL Server 2022, NULL-güvenli karşılaştırma için IS DISTINCT FROM, GREATEST/LEAST, GENERATE_SERIES, DATETRUNC, DATE_BUCKET, karakterli TRIM, sıralı STRING_SPLIT, JSON_OBJECT/JSON_ARRAY ve APPROX_PERCENTILE_CONT getirdi. Her birini önce/sonra koduyla deniyor ve 150 seviyesinde GENERATE_SERIES, WINDOW ve TRIM LEADING'in neden çalışmadığını gerçek hata mesajlarıyla 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 150, 160 ve 170 ile çalıştırılmıştır; PostgreSQL ve MySQL karşılaştırmaları PostgreSQL 18.6 ve MySQL 8.4 ile denenmiştir.

Önce Tahmin Et

CRM (Customer Relationship Management) sisteminden gelen telefon güncellemeleri customer_updates tablosuna yüklendi. Hangi müşterilerin telefonu gerçekten değişti, onu bulmak istiyoruz. Hazırlık ve mevcut durum:

DROP TABLE IF EXISTS dbo.customer_updates;
CREATE TABLE dbo.customer_updates (
    customer_id INT         NOT NULL PRIMARY KEY,
    phone       VARCHAR(20) NULL
);
INSERT INTO dbo.customer_updates (customer_id, phone) VALUES
    (1, '+44 7700 900123'),
    (2, NULL),
    (4, NULL),
    (5, '+90 555 012 91 77'),
    (6, '+49 151 2345678');
SELECT u.customer_id, c.phone AS current_phone, u.phone AS new_phone
FROM dbo.customer_updates AS u
JOIN dbo.customers AS c ON c.customer_id = u.customer_id
ORDER BY u.customer_id;
customer_id current_phone     new_phone
----------- ----------------- -----------------
          1 NULL              +44 7700 900123
          2 NULL              NULL
          4 +90 555 017 95 53 NULL
          5 +90 555 012 91 77 +90 555 012 91 77
          6 NULL              +49 151 2345678

Müşteri 1 ve 6'nın telefonu yoktu, artık var. Müşteri 4'ün telefonu silinmiş. Müşteri 2'de ikisi de NULL, müşteri 5'te ikisi de aynı. Yani üç değişiklik var. Şu sorgu kaç sayar?

SELECT COUNT(*) AS changed
FROM dbo.customer_updates AS u
JOIN dbo.customers AS c ON c.customer_id = u.customer_id
WHERE u.phone <> c.phone;
changed
-------
      0

Sıfır. Üç değişikliğin üçünde de bir taraf NULL ve serinin 06. yazısındaki üç değerli mantık gereği NULL <> 'value' UNKNOWN döndürür; WHERE onu geçirmez. Senkronizasyon job'larında, audit tablolarında ve "değişen row'ları bul" sorgularında bu hata yıllarca fark edilmeden yaşar. SQL Server 2022 bunun standart çözümünü getirdi: IS DISTINCT FROM. Bu yazı SQL Server 2022'nin T-SQL'e (Transact-SQL) eklediği dil özelliklerini önce/sonra koduyla ve hangisinin compatibility level istediğiyle birlikte ele alıyor.

IS [NOT] DISTINCT FROM

SQL Server 2022'den önce doğru sorgu şöyle yazılırdı:

SELECT u.customer_id
FROM dbo.customer_updates AS u
JOIN dbo.customers AS c ON c.customer_id = u.customer_id
WHERE u.phone <> c.phone
   OR (u.phone IS NULL AND c.phone IS NOT NULL)
   OR (u.phone IS NOT NULL AND c.phone IS NULL)
ORDER BY u.customer_id;
customer_id
-----------
          1
          4
          6

İki ek koşul, her nullable column için. Beş column'u karşılaştıran bir senkronizasyon sorgusunda bu on beş satır demek ve birini unutmak çok kolay. 2022 ile:

SELECT u.customer_id
FROM dbo.customer_updates AS u
JOIN dbo.customers AS c ON c.customer_id = u.customer_id
WHERE u.phone IS DISTINCT FROM c.phone
ORDER BY u.customer_id;
customer_id
-----------
          1
          4
          6

IS DISTINCT FROM iki NULL'ı eşit, NULL ile bir değeri farklı sayar ve sonucu her zaman TRUE ya da FALSE'tur, asla UNKNOWN değil. IS NOT DISTINCT FROM ise NULL-güvenli eşitliktir; iki nullable column'u join'lerken işe yarar. Bu predicate standarda SQL:1999 ile girmişti; PostgreSQL uzun zamandır destekliyor (denemede 1 IS DISTINCT FROM NULL true döndü). MySQL 8.4'te ise yok; onun yerine kendi NULL-güvenli eşitlik operator'ü <=> var, NOT (a <=> b) aynı işi görür.

GREATEST ve LEAST

Bir müşterinin "son aktivite" tarihi üç kaynaktan gelir: kayıt tarihi, son sipariş ve son yorum. 2022 öncesinde birden çok column'un en büyüğünü almak için ya iç içe CASE yazılırdı ya da şu hile kullanılırdı:

WITH activity AS (
    SELECT c.customer_id, c.created_at,
           (SELECT MAX(o.ordered_at)  FROM dbo.orders  AS o WHERE o.customer_id = c.customer_id) AS last_order,
           (SELECT MAX(r.reviewed_at) FROM dbo.reviews AS r WHERE r.customer_id = c.customer_id) AS last_review
    FROM dbo.customers AS c
    WHERE c.customer_id IN (3, 7, 38)
)
SELECT customer_id,
       (SELECT MAX(v) FROM (VALUES (created_at), (last_order), (last_review)) AS x(v)) AS last_activity
FROM activity
ORDER BY customer_id;
customer_id last_activity
----------- -------------------
          3 2026-06-20 16:58:05
          7 2026-02-19 15:36:21
         38 2026-06-29 07:58:00

VALUES ile column'ları row'a çevirip MAX almak çalışır ama niyeti saklar. 2022 ile:

WITH activity AS (
    SELECT c.customer_id, c.created_at,
           (SELECT MAX(o.ordered_at)  FROM dbo.orders  AS o WHERE o.customer_id = c.customer_id) AS last_order,
           (SELECT MAX(r.reviewed_at) FROM dbo.reviews AS r WHERE r.customer_id = c.customer_id) AS last_review
    FROM dbo.customers AS c
    WHERE c.customer_id IN (3, 7, 38)
)
SELECT customer_id, created_at, last_order, last_review,
       GREATEST(created_at, last_order, last_review) AS last_activity
FROM activity
ORDER BY customer_id;
customer_id created_at          last_order          last_review         last_activity
----------- ------------------- ------------------- ------------------- -------------------
          3 2024-06-23 13:50:00 2026-06-20 16:58:05 2026-05-05 10:33:45 2026-06-20 16:58:05
          7 2024-07-22 10:34:00 2026-02-19 15:36:21 2026-02-02 06:42:52 2026-02-19 15:36:21
         38 2026-06-29 07:58:00 NULL                NULL                2026-06-29 07:58:00

Müşteri 38'in hiç siparişi ve yorumu yok; GREATEST NULL'ları yok saydı ve kayıt tarihini döndürdü. SQL Server'da GREATEST ve LEAST NULL'ı atlar, yalnız bütün argümanlar NULL ise NULL döner. PostgreSQL de aynı davranır (GREATEST(1, NULL, 3) 3 döndü). MySQL'de ise tek bir NULL argüman sonucu NULL yapar (aynı ifade NULL döndü). İki function standarda SQL:2023 ile (T054) girdi, ama NULL davranışı lehçeden lehçeye değişiyor; taşınabilir kod yazıyorsan COALESCE ile koru.

LEAST'in en sık kullanımı bir değeri tavanla sınırlamak:

SELECT book_id, title, list_price,
       LEAST(list_price, 500.00) AS capped_price
FROM dbo.books
WHERE book_id IN (26, 57, 65);
book_id title                           list_price capped_price
------- ------------------------------- ---------- ------------
     26 Dune                                390.50       390.50
     57 The Pragmatic Programmer           1150.00       500.00
     65 The Art of Computer Programming    1450.00       500.00

GENERATE_SERIES

Serinin 12. yazısında tarih serisini recursive CTE (Common Table Expression) ile üretmiştik:

WITH n AS (
    SELECT 0 AS i
    UNION ALL
    SELECT i + 1 FROM n WHERE i < 6
)
SELECT DATEADD(day, i, CAST('2025-12-20' AS date)) AS d FROM n;
d
----------
2025-12-20
2025-12-21
2025-12-22
2025-12-23
2025-12-24
2025-12-25
2025-12-26

2022 ile aynı iş tek satır:

SELECT DATEADD(day, value, CAST('2025-12-20' AS date)) AS d
FROM GENERATE_SERIES(0, 6);
d
----------
2025-12-20
2025-12-21
2025-12-22
2025-12-23
2025-12-24
2025-12-25
2025-12-26

GENERATE_SERIES(start, stop [, step]) bir table-valued function; value adında tek column döndürür. Adım negatif olabilir:

SELECT value FROM GENERATE_SERIES(100, 0, -25);
value
-----
  100
   75
   50
   25
    0

Recursive CTE'nin 100 iterasyon sınırı burada yok ve optimizer seriyi tek bir operator olarak üretir. Takvim, saat dilimleri ya da test verisi üretmek için doğal araç; SQL Server Derinlemesine serisinde Bookstore'u milyonlarca row'a büyütmek için bunu kullanacağız. GENERATE_SERIES standartta yok; adı ve fikri PostgreSQL'den geliyor. SQL Server'da compatibility level 160 gerektiriyor (aşağıda deneyeceğiz).

DATETRUNC ve DATE_BUCKET

Bir tarihi ayın başına kesmenin üç kuşak yolu:

SELECT DATEADD(month, DATEDIFF(month, 0, ordered_at), 0) AS month_start_old,
       DATEFROMPARTS(YEAR(ordered_at), MONTH(ordered_at), 1) AS month_start_parts,
       DATETRUNC(month, ordered_at) AS month_start_new
FROM dbo.orders
WHERE order_id = 250;
month_start_old         month_start_parts month_start_new
----------------------- ----------------- -------------------
2025-12-01 00:00:00.000 2025-12-01        2025-12-01 00:00:00

DATEADD(month, DATEDIFF(month, 0, x), 0) hilesi eski kod tabanlarında her yerde ve sonucu her zaman datetime (dikkat: milisaniye hassasiyeti .000). DATEFROMPARTS date döndürür. 2022'nin DATETRUNC'ı ise girdinin tipini korur; burada datetime2(0) girdi, datetime2(0) çıktı. Yıldan mikrosaniyeye kadar (yıl, çeyrek, ay, hafta, gün, saat, dakika, saniye...) keser.

DATE_BUCKET bir adım ileri gider: zamanı sabit genişlikte kovalara böler. Aralık 2025'teki siparişler haftalık kovalarda:

SELECT DATE_BUCKET(week, 1, CAST(ordered_at AS date)) AS week_start,
       COUNT(*) AS orders
FROM dbo.orders
WHERE ordered_at >= '2025-12-01' AND ordered_at < '2026-01-01'
GROUP BY DATE_BUCKET(week, 1, CAST(ordered_at AS date))
ORDER BY week_start;
week_start orders
---------- ------
2025-12-01     12
2025-12-08      8
2025-12-15      7
2025-12-22     11
2025-12-29      4

Varsayılan başlangıç noktası (origin) 1900-01-01, ve o gün bir pazartesi; bu yüzden haftalar pazartesi başlıyor. Dördüncü argümanla origin'i değiştirebilirsin; haftayı pazar günü başlatmak için bir pazar tarihi ver:

SELECT DATE_BUCKET(week, 1, CAST(ordered_at AS date), CAST('2025-11-30' AS date)) AS week_start,
       DATENAME(weekday, DATE_BUCKET(week, 1, CAST(ordered_at AS date), CAST('2025-11-30' AS date))) AS day_name,
       COUNT(*) AS orders
FROM dbo.orders
WHERE ordered_at >= '2025-12-01' AND ordered_at < '2026-01-01'
GROUP BY DATE_BUCKET(week, 1, CAST(ordered_at AS date), CAST('2025-11-30' AS date))
ORDER BY week_start;
week_start day_name orders
---------- -------- ------
2025-11-30 Sunday       11
2025-12-07 Sunday        9
2025-12-14 Sunday        5
2025-12-21 Sunday       12
2025-12-28 Sunday        5

DATE_BUCKET(week, 2, ...) iki haftalık, DATE_BUCKET(minute, 15, ...) 15 dakikalık kovalar üretir; zaman serisi raporlarında DATEPART hesaplarıyla uğraşmaktan kurtarır. DATETRUNC ve DATE_BUCKET standart değil; PostgreSQL'deki karşılıkları date_trunc ve date_bin.

Karakterli TRIM ve Sıralı STRING_SPLIT

2022 öncesinde LTRIM ve RTRIM yalnız boşluk silerdi. Artık silinecek karakterleri verebilirsin:

SELECT LTRIM('000123', '0')              AS ltrim_zeros,
       RTRIM('Dune...', '.')             AS rtrim_dots,
       TRIM('-' FROM '--978-1-23--')     AS trim_dashes,
       TRIM(LEADING '+' FROM '+90 555')  AS trim_leading;
ltrim_zeros rtrim_dots trim_dashes trim_leading
----------- ---------- ----------- ------------
        123 Dune       978-1-23    90 555

TRIM(LEADING ... FROM ...) biçimi standardın syntax'ı; LTRIM(x, chars) biçimi ise SQL:2023'ün çok karakterli trim function'larıyla (T056) aynı fikir. Dikkat: LEADING/TRAILING/BOTH syntax'ı compatibility level 160 istiyor, iki argümanlı LTRIM/RTRIM ise istemiyor.

STRING_SPLIT 2016'dan beri var ama parçaların sırasını garanti etmezdi; "ilk etiket" ya da "üçüncü parça" gerektiğinde güvenilmezdi. 2022 ile üçüncü argüman 1 verilirse ordinal column'u da döner:

SELECT value, ordinal
FROM STRING_SPLIT('dune,arrakis,spice,fremen', ',', 1)
ORDER BY ordinal;
value   ordinal
------- -------
dune          1
arrakis       2
spice         3
fremen        4

JSON_OBJECT, JSON_ARRAY ve JSON_PATH_EXISTS

SQL Server 2016'dan beri JSON'u (JavaScript Object Notation) FOR JSON ile üretebiliyordun, ama bu sorgunun bütün sonucunu JSON'a çevirir. 2022'nin constructor'ları tek bir değer içinde JSON kurmanı sağlar:

SELECT JSON_OBJECT('id': b.book_id,
                   'title': b.title,
                   'price': b.list_price,
                   'authors': JSON_ARRAY(a1.last_name, a2.last_name),
                   'subtitle': b.subtitle NULL ON NULL) AS book_json
FROM dbo.books AS b
JOIN dbo.book_authors AS ba1 ON ba1.book_id = b.book_id AND ba1.author_order = 1
JOIN dbo.authors AS a1       ON a1.author_id = ba1.author_id
JOIN dbo.book_authors AS ba2 ON ba2.book_id = b.book_id AND ba2.author_order = 2
JOIN dbo.authors AS a2       ON a2.author_id = ba2.author_id
WHERE b.book_id = 42;
book_json
----------------------------------------------------------------------------------------------
{"id":42,"title":"Good Omens","price":340.90,"authors":["Pratchett","Gaiman"],"subtitle":null}

JSON_ARRAY iki yazarı bir array'e koydu, NULL ON NULL ise NULL alt başlığı null olarak yazdı. Varsayılan da bu; ABSENT ON NULL ise key'i hiç yazmaz:

SELECT JSON_OBJECT('title': title, 'subtitle': subtitle ABSENT ON NULL) AS absent_on_null,
       JSON_OBJECT('title': title, 'subtitle': subtitle)                AS default_behaviour
FROM dbo.books
WHERE book_id = 26;
absent_on_null   default_behaviour
---------------- --------------------------------
{"title":"Dune"} {"title":"Dune","subtitle":null}

API'ye (Application Programming Interface) gidecek bir payload'da bu fark önemlidir: istemci "alan yok" ile "alan null" arasında farklı davranabilir. JSON_PATH_EXISTS bir path'in var olup olmadığını söyler, ISJSON'a ise tip kontrolü eklendi:

SELECT JSON_PATH_EXISTS('{"title":"Dune","price":390.5}', '$.price')  AS has_price,
       JSON_PATH_EXISTS('{"title":"Dune","price":390.5}', '$.isbn')   AS has_isbn,
       ISJSON('42')          AS any_type_default,
       ISJSON('42', SCALAR)  AS is_scalar,
       ISJSON('42', OBJECT)  AS is_object;
has_price has_isbn any_type_default is_scalar is_object
--------- -------- ---------------- --------- ---------
        1        0                0         1         0

ISJSON('42') varsayılan olarak 0 döndü, çünkü eski davranışla uyumlu kalmak için yalnız object ve array'i geçerli sayar; SCALAR vererek tek değerlerin de geçerli JSON olduğunu kabul ettirebilirsin. JSON_OBJECT ve JSON_ARRAY standardın SQL:2016'daki SQL/JSON bölümünden geliyor. SQL Server 2025'in native json tipi ve JSON_OBJECTAGG/JSON_ARRAYAGG aggregate'leri bir sonraki yazının konusu.

APPROX_PERCENTILE_CONT ve Diğerleri

Serinin 14. yazısında medyanı PERCENTILE_CONT ile hesaplamıştık; o function bütün değerleri sıralar ve yalnız window function olarak çalışır. 2022'nin APPROX_PERCENTILE_CONT'u ise normal bir aggregate ve KLL (Karnin, Lang ve Liberty'nin 2016'da yayımladığı quantile sketch algoritması) adlı bir sketch ile çalışır:

WITH order_totals AS (
    SELECT o.order_id, SUM(oi.quantity * oi.unit_price) AS total
    FROM dbo.orders AS o
    JOIN dbo.order_items AS oi ON oi.order_id = o.order_id
    GROUP BY o.order_id
)
SELECT APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) AS approx_median,
       (SELECT DISTINCT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) OVER () FROM order_totals) AS exact_median
FROM order_totals;
approx_median exact_median
------------- ------------
       802.62       802.62

380 siparişte sonuç birebir aynı. Microsoft Learn'e göre bu function rank bazlı hata garantisi verir: %99 güvenle en fazla %1,33 hata. Algoritma rastgele örnekleme kullandığı için büyük veride sonuç çalıştırmadan çalıştırmaya küçük farklar gösterebilir. Dashboard'da milyonlarca row'un p95 (95. persentil) süresini göstermek için iyi; fatura kesmek için değil.

2022'nin dil eklemeleri bunlarla bitmiyor. 14. yazıda kullandığımız WINDOW clause'u, FIRST_VALUE/LAST_VALUE'daki IGNORE NULLS, ve LEFT_SHIFT, RIGHT_SHIFT, BIT_COUNT, GET_BIT, SET_BIT gibi bit manipulation function'ları da aynı sürümle geldi.

Üretimde Dikkat: Compatibility Level

Her veritabanının bir compatibility level'ı var ve bazı yeni syntax'lar yalnız belirli bir seviyenin üstünde açılıyor. SQL Server 2022'de yeni veritabanları 160 ile, 2025'te 170 ile oluşturulur; ama eski bir sunucudan restore edilen ya da yükseltilen veritabanı eski seviyesini korur. Kendi veritabanımı 150'ye (SQL Server 2019 seviyesi) çekip denedim:

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 150;
SELECT value FROM GENERATE_SERIES(1, 3);
Msg 208, Level 16, State 1, Server b8be943222fa, Line 1
Invalid object name 'GENERATE_SERIES'.
SELECT title, MAX(list_price) OVER w AS max_price
FROM dbo.books
WINDOW w AS (PARTITION BY category_id);
Msg 102, Level 15, State 1, Server b8be943222fa, Line 3
Incorrect syntax near 'w'.
SELECT TRIM(LEADING '0' FROM '007');
Msg 102, Level 15, State 1, Server b8be943222fa, Line 1
Incorrect syntax near '0'.

Hata mesajları yanıltıcı: "Invalid object name 'GENERATE_SERIES'" ya da "Incorrect syntax near 'w'" sana compatibility level'dan hiç bahsetmiyor. Aynı seviyede şu üçü ise sorunsuz çalıştı:

SELECT GREATEST(1, 5, 3) AS greatest_ok,
       DATETRUNC(year, CAST('2025-12-22' AS date)) AS datetrunc_ok,
       CASE WHEN 1 IS DISTINCT FROM NULL THEN 'yes' END AS distinct_ok;
greatest_ok datetrunc_ok distinct_ok
----------- ------------ -----------
          5 2025-01-01   yes

Denemelerimde SQL Server 2025 üzerindeki 150 seviyesindeki veritabanında, bu yazıdaki özelliklerden yalnız GENERATE_SERIES, WINDOW clause'u ve TRIM(LEADING/TRAILING/BOTH ...) syntax'ı çalışmadı; IS DISTINCT FROM, GREATEST/LEAST, DATETRUNC, DATE_BUCKET, iki argümanlı LTRIM/RTRIM, STRING_SPLIT'in ordinal'ı, JSON function'ları ve APPROX_PERCENTILE_CONT çalıştı. Seviyeyi 160'a çıkarınca:

ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 160;
SELECT value FROM GENERATE_SERIES(1, 3);
value
-----
    1
    2
    3

Bir uyarı daha: compatibility level yalnız syntax'ı değil, optimizer davranışını da (cardinality estimator, intelligent query processing özellikleri) değiştirir. Seviyeyi "yeni function'ı kullanabilmek için" yükseltmek, bütün sorguların planını etkileyebilecek bir karardır; Query Store ile önce/sonra karşılaştırması yapmadan production'da değiştirme. Ayrıntılar SQL Server Derinlemesine serisinde. Ve en temel tuzak: bu function'ların hiçbiri SQL Server 2019'da, hangi compatibility level olursa olsun, yok. Kodun hem 2019 hem 2022 sunucularında çalışacaksa en düşük sürüme göre yaz.

Sık Düşülen Tuzaklar

Nullable column'ları <> ile karşılaştırıp değişiklik tespiti yapmak. NULL'lı her değişiklik kaybolur; IS DISTINCT FROM kullan.

GREATEST'in NULL davranışının her lehçede aynı olduğunu sanmak. SQL Server ve PostgreSQL atlar, MySQL NULL döndürür.

GENERATE_SERIES'in "Invalid object name" hatasını eksik bir tabloya yormak. Önce sys.databases'te compatibility level'a bak.

Compatibility level'ı yalnız yeni bir function için, plan regresyonlarını ölçmeden yükseltmek.

DATE_BUCKET'in haftaları her zaman pazartesi başlattığını unutmak; raporun pazar başlaması gerekiyorsa origin ver.

JSON_OBJECT'in varsayılan olarak NULL değerleri null yazdığını bilmemek; API sözleşmen key'in hiç olmamasını bekliyorsa ABSENT ON NULL yaz.

Küçük Bir Deneme

★☆☆ customer_updates tablosuna city column'unu ekle ve hem telefonu hem şehri değişen müşterileri IS DISTINCT FROM ile bul. Aynı sorguyu 2022 öncesi syntax ile yaz ve satır sayılarını karşılaştır.

★★☆ GENERATE_SERIES ve DATE_BUCKET ile 2025'in bütün haftalarını üret (siparişi olmayan haftalar 0 ile görünsün) ve her hafta için sipariş sayısını döndür.

★★★ Her kitap için JSON_OBJECT ile {"id": ..., "title": ..., "authors": [...]} biçiminde bir JSON üret; yazar sayısı kitaptan kitaba değiştiği için JSON_ARRAY yetmeyecek. 2022'de bunu hangi yöntemle çözebilirsin? Bir sonraki yazıdaki JSON_ARRAYAGG ile karşılaştırmak için çözümünü sakla.

SQL Server 2022 dil tarafında standarda yaklaşan bir sürümdü. 2025 bu yolda daha da ileri gitti: regular expression function'ları, native json tipi, || operator'ü ve PRODUCT gibi yeni aggregate'ler. Bir sonraki yazıda onları, yine compatibility level gereksinimleriyle birlikte deniyoruz.