在報表開發、數據匯出、標籤聚合等場景中,常需「按某欄位分組,將另一欄位值用分隔符拼成單一字串」。SQL Server 原生不支援 GROUP_CONCAT(MySQL)或 STRING_AGG(PostgreSQL/SQL Server 2017+),但利用 STUFF + FOR XML PATH('') 這對「黃金搭檔」,即可在舊版本高效實現。本文以用戶表 tuser 為例,完整拆解原理與實戰模板。
原始表 tuser:
| userpwd | username |
|---|---|
| 123 | Alice |
| 123 | Bob |
| 456 | Charlie |
| 456 | David |
| 456 | Eve |
目標結果:按 userpwd 分組,username 用逗號拼接。
| userpwd | usernames |
|---|---|
| 123 | Alice,Bob |
| 456 | Charlie,David,Eve |
SELECT
userpwd,
usernames = STUFF((
SELECT ',' + username
FROM tuser t1
WHERE t1.userpwd = t.userpwd
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM tuser t
GROUP BY userpwd;
SELECT ',' + username
FROM tuser
WHERE userpwd = '123'
FOR XML PATH('');
輸出:,Alice,Bob(每行前自動加逗號,無 XML 標籤)。
語法:STUFF(字串, 起始位置, 刪除長度, 替換字串)
STUFF(',Alice,Bob', 1, 1, '') -- 刪除第 1 字符 (逗號) → 'Alice,Bob'
FOR XML PATH 會將 &、<、> 轉義為 & 等。加上 , TYPE 返回 XML 型別,再用 .value('.', 'NVARCHAR(MAX)') 還原為純文本,避免雙重轉義。
外層按 userpwd 分組,子查詢以當前組值為相關條件,實現分組聚合。
-- 1. 建表與測試數據
CREATE TABLE #tuser (userpwd VARCHAR(20), username NVARCHAR(50));
INSERT INTO #tuser VALUES
('123', N'Alice'), ('123', N'Bob'),
('456', N'Charlie'), ('456', N'David'), ('456', N'Eve');
-- 2. 分組拼接查詢
SELECT
userpwd,
usernames = STUFF((
SELECT ',' + username
FROM #tuser t1
WHERE t1.userpwd = t.userpwd
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM #tuser t
GROUP BY userpwd;
-- 3. 清理
DROP TABLE #tuser;
userpwd 上建立非叢集索引,子查詢可直接 Index Seek。STRING_AGG(2017+)。GROUP BY 可並行,子查詢為相關子查詢通常串行,整體仍優於游標/WHILE 迴圈。| SQL Server 版本 | 推薦寫法 |
|---|---|
| 2016 及以下 | STUFF + FOR XML PATH(本文方案) |
| 2017 / 2019 / 2022 | STRING_AGG(username, ',') WITHIN GROUP (ORDER BY username) |
| Azure SQL Database | STRING_AGG 完全支援 |
-- SQL Server 2017+ 現代寫法
SELECT userpwd, STRING_AGG(username, ',') AS usernames
FROM tuser
GROUP BY userpwd;
FOR XML PATH('') → 輸出 &FOR XML PATH(''), TYPE + .value('.', 'NVARCHAR(MAX)')ISNULL(username, '') 或 COALESCE 兜底。ORDER BY username 保證確定性。NVARCHAR(MAX) 是否足夠,極端情況下分批處理。-- 多欄位:username + '(' + role + ')'
STUFF((
SELECT ',' + username + '(' + role + ')'
FROM tuser t1
WHERE t1.userpwd = t.userpwd
AND t1.status = 1 -- 條件過濾
GROUP BY username, role -- 去重
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
STUFF + FOR XML PATH 是 SQL Server 2016 以前實現分組字串聚合的標準最優解。掌握其「XML 序列化 → 移除首分隔符 → 類型還原」三步曲,配合索引與 TYPE 安全轉換,即可在存儲過程、視圖、報表查詢中穩定高效使用。若環境已升級至 2017+,請無腦遷移至 STRING_AGG,語法更簡潔、效能更強、可維護性更高。