ترکیب مقادیر چند سطر از یک جدول در قالب یک رشته واحد، یکی از نیازهای رایج در گزارشگیری و پردازش داده در SQL Server است. برای مثال، نمایش لیست محصولات یک سفارش در یک ستون واحد یا ترکیب برچسبهای مرتبط با یک رکورد، نیازمند نوعی تجمیع رشتهای (String Aggregation) است.
پیش از SQL Server 2017، یکی از رایجترین روشهای T-SQL برای تجمیع رشتهای چند سطر، استفاده از ترکیب STUFF و FOR XML PATH بود. این روش هرچند کاربردی بود، اما نگارش پیچیدهای داشت و در برخورد با کاراکترهای خاص XML به مدیریت دقیق نیاز داشت.
تابع STRING_AGG با معرفی در SQL Server 2017، این نیاز را بهصورت مستقیم و در قالب یک تابع تجمعی استاندارد پاسخ داد. در این مقاله از لاندا، نحوه عملکرد STRING_AGG، ترکیب آن با GROUP BY و WITHIN GROUP، محدودیتهای فنی، ملاحظات Performance و مقایسه دقیق آن با FOR XML PATH بررسی میشود.
تعریف تابع STRING_AGG
STRING_AGG یک تابع تجمعی (Aggregate Function) در T-SQL است که مقادیر یک ستون یا عبارت را از چند سطر گرفته و آنها را با یک جداکننده مشخص، در قالب یک رشته واحد ترکیب میکند.
این تابع از نسخه SQL Server 2017 به بعد در دسترس است و همچنین در Azure SQL Database، Azure SQL Managed Instance و Azure Synapse Analytics پشتیبانی میشود. در نسخههای قدیمیتر مانند SQL Server 2016 یا نسخههای ماقبل آن، این تابع وجود ندارد.
نکته مهمی که در محیطهای Migration باید مورد توجه قرار گیرد این است که خود STRING_AGG محدود به یک Compatibility Level خاص نیست، اما بخش WITHIN GROUP نیازمند Compatibility Level 110 یا بالاتر است. بنابراین در Databaseهایی که بهصورت دستی روی Compatibility Levelهای پایینتر تنظیم شدهاند، حتی در نسخههای جدید SQL Server، ممکن است لازم باشد این تنظیم پیش از استفاده از WITHIN GROUP بررسی و بهروزرسانی شود.
نحوه عملکرد STRING_AGG
Syntax پایه
ساختار کلی تابع STRING_AGG به شکل زیر است.
STRING_AGG ( expression, separator )
[ WITHIN GROUP ( ORDER BY order_expression [ ASC | DESC ] ) ]
پارامترهای این تابع عبارتاند از:
- expression: ستون یا عبارتی که مقدار آن باید ترکیب شود.
- separator: کاراکتر یا رشتهای که بین مقادیر ترکیبشده قرار میگیرد.
- WITHIN GROUP: بخش اختیاری برای تعیین ترتیب مقادیر در رشته خروجی.
مثال زیر یک استفاده ساده از این تابع را نشان میدهد.
SELECT
STRING_AGG(CONVERT(NVARCHAR(MAX), ProductName), N', ') AS ProductList
FROM Products;
این کوئری نام تمام محصولات جدول Products را با جداکننده کاما و فاصله، در یک رشته واحد ترکیب میکند. استفاده از CONVERT به NVARCHAR(MAX) در این مثال به دو دلیل انجام شده است.در این مثال Expression به NVARCHAR(MAX) تبدیل شده است تا خروجی از محدودیت طول انواع nvarchar(n) خارج شود. همچنین استفاده از N', ' باعث میشود Separator نیز از نوع Unicode باشد.
محدودیت نوع داده Separator
یکی از نکات فنی مهم درباره STRING_AGG، سازگاری نوع داده میان expression و separator است. اگر expression از نوع varchar باشد، separator نیز باید از نوع varchar باشد و نمیتوان برای آن یک مقدار nvarchar مانند N', ' استفاده کرد. در پروژههایی که از دادههای یونیکد و زبان فارسی استفاده میشود، توصیه میشود هر دو مقدار expression و separator از نوع nvarchar باشند تا از بروز ناسازگاری نوع داده جلوگیری شود.
ترکیب با GROUP BY
STRING_AGG مانند سایر توابع تجمعی با GROUP BY قابل استفاده است.
SELECT
c.CategoryName,
STRING_AGG(CONVERT(NVARCHAR(MAX), p.ProductName), N', ') AS ProductList
FROM Categories c
INNER JOIN Products p
ON c.CategoryID = p.CategoryID
GROUP BY c.CategoryName;
خروجی این کوئری برای هر دستهبندی، لیست محصولات مربوط به آن را در یک ستون واحد نمایش میدهد. این الگو یکی از پرکاربردترین سناریوهای استفاده از STRING_AGG در گزارشهای مدیریتی است.
مرتبسازی خروجی با WITHIN GROUP
برخلاف بسیاری از توابع تجمعی، ترتیب مقادیر در خروجی STRING_AGG بهصورت پیشفرض تضمینشده نیست، مگر اینکه از WITHIN GROUP استفاده شود. WITHIN GROUP فقط ترتیب مقادیر داخل رشته تجمیعشده را کنترل میکند و ترتیب سطرهای Result Set را تعیین نمیکند.
SELECT
c.CategoryName,
STRING_AGG(CONVERT(NVARCHAR(MAX), p.ProductName), N', ')
WITHIN GROUP (ORDER BY p.ProductName ASC) AS ProductList
FROM Categories c
INNER JOIN Products p
ON c.CategoryID = p.CategoryID
GROUP BY c.CategoryName;
در این مثال، نام محصولات پیش از ترکیب، بر اساس حروف الفبا مرتب میشوند. استفاده از WITHIN GROUP در سناریوهایی که ترتیب نمایش دادهها اهمیت دارد، ضروری است، زیرا بدون آن، ترتیب خروجی به Execution Plan و نحوه پردازش داخلی موتور پایگاهداده بستگی دارد و نباید به آن اتکا کرد.
رفتار STRING_AGG با مقادیر NULL
یکی از نکات مهم در استفاده از STRING_AGG، نحوه برخورد آن با مقادیر NULL است. برخلاف بسیاری از عملیات رشتهای در SQL Server که وجود یک مقدار NULL میتواند کل نتیجه را NULL کند، تابع STRING_AGG مقادیر NULL را بهصورت خودکار از فرایند ترکیب حذف میکند و جداکننده متناظر نیز اضافه نمیشود.
SELECT STRING_AGG(Note, N', ') AS Notes
FROM
(
VALUES (N'یادداشت اول'), (NULL), (N'یادداشت سوم')
) AS T(Note);
خروجی این کوئری برابر «یادداشت اول, یادداشت سوم» است و مقدار NULL بدون ایجاد جداکننده اضافی از خروجی حذف میشود. این رفتار در اکثر سناریوهای گزارشگیری مطلوب است، اما در صورتی که نیاز به نمایش صریح مقادیر گمشده باشد، باید پیش از استفاده از STRING_AGG، مقادیر NULL را با ISNULL یا COALESCE به یک مقدار جایگزین تبدیل کرد.
نوع داده خروجی و محدودیت طول
نوع داده خروجی STRING_AGG بر اساس نوع داده expression ورودی تعیین میشود و از الگوی مشخصی پیروی میکند.
| نوع Expression ورودی | نوع داده خروجی |
|---|---|
| varchar(max) | varchar(max) |
| nvarchar(max) | nvarchar(max) |
| varchar(1) تا varchar(8000) | varchar(8000) |
| nvarchar(1) تا nvarchar(4000) | nvarchar(4000) |
| انواع عددی یا تاریخی | nvarchar(4000) |
بر اساس این جدول، اگر expression از نوع varchar(n) یا nvarchar(n) با طول ثابت باشد، خروجی بهترتیب در varchar(8000) یا nvarchar(4000) محدود میشود، صرفنظر از طول واقعی ستون ورودی. در صورتی که مجموع طول مقادیر ترکیبشده از این محدودیت فراتر رود، اجرای کوئری با خطا متوقف میشود.
SELECT STRING_AGG(CAST(ProductName AS NVARCHAR(50)), N', ') AS ProductList
FROM Products;
در صورتی که تعداد محصولات و طول نامها به گونهای باشد که مجموع طول خروجی از 4000 کاراکتر فراتر رود، این کوئری با خطای مشابه «STRING_AGG aggregation result exceeded the limit» متوقف میشود. برای جلوگیری از این مشکل، توصیه میشود در سناریوهایی که تعداد سطرها یا طول مقادیر قابل پیشبینی نیست، expression با استفاده از CONVERT یا CAST به نوع varchar(max) یا nvarchar(max) تبدیل شود.
تبدیل ضمنی انواع غیر رشتهای
STRING_AGG محدود به مقادیر رشتهای نیست و میتواند Expressionهای غیررشتهای مانند int، bigint، decimal یا datetime را بهصورت ضمنی به نوع رشتهای تبدیل کند.
SELECT
CustomerID,
STRING_AGG(OrderID, N', ')
WITHIN GROUP (ORDER BY OrderID) AS OrderIDList
FROM Orders
GROUP BY CustomerID;
در این مثال، ستون OrderID که از نوع عددی است، بدون نیاز به CAST صریح، بهصورت خودکار در فرایند ترکیب رشتهای شرکت میکند. با این حال، در سناریوهایی که کنترل دقیق قالب خروجی اهمیت دارد، مانند تعیین صریح تعداد ارقام اعشار یا فرمت تاریخ، استفاده از CAST یا CONVERT انتخاب مناسبتری است، زیرا تبدیل ضمنی همیشه فرمت مورد انتظار توسعهدهنده را تولید نمیکند.
اگر قالب نمایش مقدار برای کاربر اهمیت داشته باشد، بهتر است تبدیل نوع داده بهصورت صریح انجام شود.
مقایسه STRING_AGG با FOR XML PATH
پیش از معرفی STRING_AGG، روش رایج برای ترکیب رشتهای چند سطر، استفاده از ترکیب STUFF و FOR XML PATH('') بود.
SELECT
c.CategoryName,
STUFF(
(
SELECT N', ' + p.ProductName
FROM Products p
WHERE p.CategoryID = c.CategoryID
FOR XML PATH('')
), 1, 2, ''
) AS ProductList
FROM Categories c;
این کوئری همان نتیجهای را تولید میکند که نسخه معادل آن با STRING_AGG تولید میکرد، اما نگارش آن بهمراتب پیچیدهتر است. تابع STUFF برای حذف جداکننده اضافی ابتدای رشته به کار میرود و ترکیب آن با Subquery و FOR XML PATH('') خوانایی کد را کاهش میدهد.
مشکل کاراکترهای خاص در FOR XML PATH
یکی از ایرادات فنی مهم روش FOR XML PATH('') بدون استفاده از TYPE، نحوه برخورد آن با کاراکترهای خاص XML مانند &، < و > است. این روش بهصورت پیشفرض این کاراکترها را Escape میکند و آنها را به معادل XML خود مانند & تبدیل میکند.
SELECT
STUFF(
(
SELECT N', ' + Note
FROM
(
VALUES (N'A & B'), (N'C < D')
) AS T(Note)
FOR XML PATH('')
), 1, 2, ''
) AS Result;
خروجی این کوئری بهجای مقدار اصلی، رشتهای شامل & و < تولید میکند که نیازمند مدیریت اضافی برای بازگرداندن به فرمت متن ساده است.
راهحل استاندارد برای این مشکل در روش سنتی، استفاده از TYPE همراه با متد .value() است.
SELECT
STUFF(
(
SELECT N', ' + Note
FROM
(
VALUES (N'A & B'), (N'C < D')
) AS T(Note)
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),
1,
2,
N''
) AS Result;
در این نسخه، خروجی ابتدا به نوع xml تبدیل میشود و سپس متد .value() مقدار متنی صحیح و بدون Escape اضافی را استخراج میکند. این الگو یکی از شناختهشدهترین راهحلها برای رفع مشکل Escape در FOR XML PATH است، هرچند همچنان از نظر خوانایی نسبت به STRING_AGG پیچیدهتر باقی میماند.
در مقابل، STRING_AGG بدون نیاز به چنین ترفندی، مقادیر را دقیقاً همانطور که هستند ترکیب میکند.
SELECT STRING_AGG(Note, N', ') AS Result
FROM
(
VALUES (N'A & B'), (N'C < D')
) AS T(Note);
خروجی این کوئری بدون هیچ Escape اضافی، دقیقاً برابر «A & B, C < D» است.
جدول مقایسه
| ویژگی | STRING_AGG | FOR XML PATH |
|---|---|---|
| نسخه پشتیبانی | SQL Server 2017 به بعد | SQL Server 2005 به بعد |
| خوانایی نگارش | بالا | پایینتر |
| Escape کاراکترهای XML | ندارد | در حالت معمول دارد، مگر با TYPE و value |
| کنترل ترتیب خروجی | WITHIN GROUP | ORDER BY در Subquery |
| نوع خروجی | وابسته به نوع Expression ورودی |
معمولاً رشته متنی و در صورت استفاده از TYPE، مقدار XML |
| پشتیبانی مستقیم از DISTINCT | ندارد | ندارد، اما با الگوهای مناسب قابل پیادهسازی است |
| نیاز به STUFF | ندارد | معمولاً در الگوی سنتی دارد |
برای پروژههایی که روی SQL Server 2017 به بعد اجرا میشوند، STRING_AGG از نظر خوانایی و سادگی نگارش انتخاب مناسبتری است. اما برای سازگاری با نسخههای قدیمیتر یا سناریوهای Migration که باید از نسخههای پیشین SQL Server پشتیبانی کنند، همچنان باید از FOR XML PATH استفاده کرد.
ملاحظات Performance
مقایسه Performance میان STRING_AGG و FOR XML PATH موضوعی است که نباید بدون بررسی دقیق در هر سناریو، بهصورت مطلق بیان شود. عملکرد هر دو روش به عوامل متعددی وابسته است، از جمله تعداد سطرهای ورودی، طول نهایی رشته تولیدشده، نیاز یا عدم نیاز به Sort برای WITHIN GROUP یا ORDER BY، میزان Memory Grant اختصاصیافته توسط Query Optimizer و ساختار Index موجود روی ستونهای درگیر در JOIN و GROUP BY.
بهطور کلی، STRING_AGG الگوی نگارشی مستقیمتری نسبت به ترکیب Subquery و FOR XML PATH دارد و در بسیاری از سناریوها Execution Plan متفاوت و سادهتری ایجاد میکند، اما نمیتوان برتری Performance آن را بدون آزمایش روی داده واقعی قطعی دانست.
سناریوهای واقعی استفاده
در پروژههای واقعی، STRING_AGG معمولاً در موارد زیر کاربرد دارد.
- نمایش لیست محصولات یک سفارش در یک ستون واحد در گزارشهای فروش
- ترکیب برچسبها یا دستهبندیهای مرتبط با یک رکورد
- تولید لیست ایمیل یا شماره تماس اعضای یک گروه در قالب یک رشته واحد
- سادهسازی Viewها و Stored Procedureهایی که پیش از این با ترکیب پیچیده STUFF و FOR XML PATH نوشته شده بودند
مثال ترکیب چند ستون در گزارش سفارش
SELECT
o.OrderID,
STRING_AGG(
CONCAT(p.ProductName, N' (', od.Quantity, N' عدد)'),
N', '
) WITHIN GROUP (ORDER BY p.ProductName) AS OrderItems
FROM Orders o
INNER JOIN OrderDetails od
ON o.OrderID = od.OrderID
INNER JOIN Products p
ON od.ProductID = p.ProductID
GROUP BY o.OrderID;
این کوئری برای هر سفارش، لیست محصولات همراه با تعداد آنها را در قالب یک رشته واحد و مرتبشده بر اساس نام محصول تولید میکند. استفاده از CONCAT در داخل STRING_AGG امکان ترکیب چند ستون پیش از تجمیع را فراهم میکند.
مثال با DISTINCT برای حذف مقادیر تکراری
STRING_AGG بهصورت مستقیم از کلمه کلیدی DISTINCT پشتیبانی نمیکند، اما این محدودیت با استفاده از یک Subquery میانی قابل رفع است. نکته مهم در این الگو این است که در صورت نیاز به حذف مقادیر تکراری به تفکیک هر گروه، DISTINCT باید در سطح همان گروه اعمال شود، نه در سطح کل جدول.
SELECT
CustomerID,
STRING_AGG(CategoryName, N', ') AS Categories
FROM
(
SELECT DISTINCT
CustomerID,
CategoryName
FROM CustomerCategories
) AS D
GROUP BY CustomerID;
در این مثال، ابتدا ترکیب یکتای CustomerID و CategoryName با DISTINCT استخراج میشود و سپس نتیجه به تفکیک هر مشتری با STRING_AGG ترکیب میشود. این الگو تضمین میکند که تکرار دستهبندی برای هر مشتری بهصورت مستقل حذف شود.
مزایای استفاده از STRING_AGG
- نگارش ساده و خوانا در مقایسه با STUFF و FOR XML PATH
- عدم نیاز به مدیریت Escape کاراکترهای خاص XML
- پشتیبانی مستقیم از مرتبسازی از طریق WITHIN GROUP
- رفتار قابل پیشبینی و مستند با مقادیر NULL
- پشتیبانی از تبدیل ضمنی انواع غیررشتهای
- سازگاری با الگوی استاندارد توابع تجمعی و ترکیب طبیعی با GROUP BY
محدودیتهای STRING_AGG
- عدم پشتیبانی در نسخههای SQL Server قدیمیتر از 2017
- نیاز به Compatibility Level 110 یا بالاتر برای استفاده از WITHIN GROUP
- عدم پشتیبانی مستقیم از DISTINCT در آرگومان ورودی
- محدودیت طول خروجی در صورت استفاده از نوعهای داده با طول ثابت
- الزام سازگاری نوع داده میان expression و separator
- عدم تضمین ترتیب خروجی بدون استفاده از WITHIN GROUP
Best Practiceها
برای استفاده صحیح از STRING_AGG در پروژههای واقعی، رعایت نکات زیر توصیه میشود.
- استفاده از WITHIN GROUP در تمام سناریوهایی که ترتیب خروجی اهمیت دارد
- تبدیل expression به varchar(max) یا nvarchar(max) در صورتی که حجم داده یا تعداد سطرها قابل پیشبینی نباشد
- هماهنگسازی نوع داده expression و separator، بهویژه در پروژههای چندزبانه با داده یونیکد
- استفاده از CAST یا CONVERT برای مقادیر غیررشتهای در مواردی که کنترل دقیق فرمت خروجی اهمیت دارد
- استفاده از Subquery میانی با DISTINCT در سطح گروه برای پیادهسازی رفتار مشابه DISTINCT در STRING_AGG
- بررسی Compatibility Level دیتابیس پیش از استفاده از WITHIN GROUP در پروژههای Migration
- مقایسه Performance واقعی STRING_AGG و FOR XML PATH با استفاده از Execution Plan و STATISTICS IO در سناریوهای با حجم داده بالا
اشتباهات رایج
- فرض غلط درباره ترتیب پیشفرض خروجی بدون استفاده از WITHIN GROUP
- استفاده از STRING_AGG با نوع داده با طول ثابت در سناریوهایی با حجم داده بالا که منجر به خطای عبور از محدودیت طول میشود
- ترکیب expression از نوع varchar با separator از نوع nvarchar بدون توجه به ناسازگاری نوع داده
- تلاش برای استفاده از DISTINCT بهصورت مستقیم داخل STRING_AGG که از نظر Syntax مجاز نیست
- نادیده گرفتن Compatibility Level دیتابیس هنگام استفاده از WITHIN GROUP در محیطهای Migration
- استفاده از STRING_AGG در پروژههایی که باید با نسخههای قدیمیتر SQL Server سازگار باشند
جمعبندی
تابع STRING_AGG در SQL Server راهحلی ساده و خوانا برای ترکیب مقادیر چند سطر در قالب یک رشته واحد است که از نسخه 2017 به بعد در دسترس قرار گرفته است. این تابع نسبت به روش قدیمیتر STUFF و FOR XML PATH، از نظر خوانایی کد و مدیریت صحیح کاراکترهای خاص برتری قابل توجهی دارد، هرچند مقایسه Performance میان این دو روش باید بر اساس بررسی واقعی هر سناریو انجام شود. توجه به رفتار آن با مقادیر NULL، محدودیت طول خروجی بر اساس نوع داده، سازگاری نوع داده میان expression و separator، Compatibility Level مورد نیاز برای WITHIN GROUP و استفاده صحیح از آن برای مرتبسازی، از نکات کلیدی در استفاده صحیح از این تابع محسوب میشود. برای پروژههایی که نیاز به سازگاری با نسخههای قدیمیتر SQL Server دارند، همچنان باید از FOR XML PATH استفاده کرد.
پرسشهای متداول FAQ
STRING_AGG از کدام نسخه SQL Server پشتیبانی میشود؟
این تابع از SQL Server 2017 به بعد و همچنین در Azure SQL Database، Azure SQL Managed Instance و Azure Synapse Analytics در دسترس است. برای نسخههای قدیمیتر باید از STUFF همراه با FOR XML PATH استفاده شود.
آیا ترتیب خروجی STRING_AGG تضمینشده است؟
خیر. بدون استفاده از WITHIN GROUP، ترتیب مقادیر در خروجی به نحوه پردازش داخلی موتور پایگاهداده بستگی دارد و نباید به آن اتکا کرد. برای تضمین ترتیب مشخص، باید از WITHIN GROUP همراه با ORDER BY استفاده شود.
چرا STRING_AGG با خطای طول رشته مواجه میشود؟
این خطا زمانی رخ میدهد که expression از نوع داده با طول ثابت مانند varchar(n) یا nvarchar(n) باشد و مجموع طول مقادیر ترکیبشده از محدودیت 8000 بایت یا 4000 کاراکتر یونیکد فراتر رود. تبدیل expression به varchar(max) یا nvarchar(max) این مشکل را برطرف میکند.
آیا برای ترکیب مقادیر عددی در STRING_AGG حتماً باید از CAST استفاده کرد؟
خیر. STRING_AGG میتواند مقادیر عددی و تاریخی را بهصورت ضمنی به نوع رشتهای تبدیل کند. با این حال، در مواردی که کنترل دقیق فرمت خروجی، مانند تعداد ارقام اعشار یا قالب تاریخ، اهمیت دارد، استفاده از CAST یا CONVERT توصیه میشود.
آیا میتوان مقادیر تکراری را در STRING_AGG حذف کرد؟
STRING_AGG بهصورت مستقیم از DISTINCT پشتیبانی نمیکند، اما با استفاده از یک Subquery میانی که مقادیر تکراری را در سطح گروه مورد نظر با DISTINCT حذف میکند، میتوان به همین نتیجه دست یافت.
مشاوره تخصصی SQL Server و بهینهسازی پایگاه داده
تجمیع رشتهها تنها یکی از نیازهای رایج در پروژههای SQL Server است. در پروژههای سازمانی، انتخاب روش مناسب برای اجرای Query، طراحی صحیح Indexها، بهینهسازی Execution Plan و مدیریت حجم داده میتواند تأثیر مستقیمی بر سرعت و پایداری سامانه داشته باشد.
تیم فنی توسعه فناوری اطلاعات لاندا در زمینه طراحی، توسعه و بهینهسازی پایگاه دادههای SQL Server، بهینهسازی Queryها، طراحی Schema و Migration پایگاه داده به سازمانها کمک میکند.
اگر در پروژه SQL Server خود با Queryهای کند، حجم بالای داده، طراحی نامناسب پایگاه داده یا چالشهای Performance مواجه هستید، برای بررسی تخصصی و بهینهسازی راهکار مناسب با لاندا در تماس ✆ باشید.


No comment