应优先用 STRING_AGG 替代游标拼接字符串,SQL Server 2017+ 支持原生聚合,性能远超游标;2016 及以下需用 FOR XML PATH('') 配合 STUFF 和 .value() 避免换行符冗余及 XML 转义问题。

用 STRING_AGG 替代游标拼接字符串
游标逐行拼接(如把多个 CONTAINER_NO 合并成换行分隔的字符串)是典型低效场景,8万行数据耗时70秒就是信号。SQL Server 2017+ 原生支持 STRING_AGG,它在引擎层做聚合,避免了游标上下文切换和多次 FETCH 开销。
直接改写原逻辑:
UPDATE r
SET CTN_NO = s.ctns
FROM #RESULT_COMP_LIST r
INNER JOIN (
SELECT
JOB_ORDER_ID,
STRING_AGG(CONTAINER_NO, NCHAR(13) + NCHAR(10)) AS ctns
FROM OZ_BK_CONTAINER
GROUP BY JOB_ORDER_ID
) s ON r.BUSINESS_ID = s.JOB_ORDER_ID;
注意点:
-
STRING_AGG默认不保留 NULL,若字段可能为空,需提前ISNULL(CONTAINER_NO, '') - 排序需求用
WITHIN GROUP (ORDER BY ...),否则结果顺序不确定 - SQL Server 2016 及更早版本不支持,需降级为
FOR XML PATH('')方案
SQL Server 2016 及以下用 FOR XML PATH('') 模拟聚合
老版本没有 STRING_AGG,但 FOR XML PATH('') 是成熟替代方案,性能仍远优于游标。关键在于避免在游标循环里反复查表,而是一次性聚合后 JOIN 更新。
示例写法:
UPDATE r
SET CTN_NO = s.ctns
FROM #RESULT_COMP_LIST r
INNER JOIN (
SELECT
JOB_ORDER_ID,
STUFF((
SELECT NCHAR(13) + NCHAR(10) + ISNULL(CONTAINER_NO, '')
FROM OZ_BK_CONTAINER c2
WHERE c2.JOB_ORDER_ID = c1.JOB_ORDER_ID
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 2, '') AS ctns
FROM OZ_BK_CONTAINER c1
GROUP BY JOB_ORDER_ID
) s ON r.BUSINESS_ID = s.JOB_ORDER_ID;
常见坑:
宝塔面板11.3.0是一款针对Linux服务器设计的可视化管理工具,通过重构核心模块实现资源占用显著降低,尤其适合低配置服务器环境。它将复杂的命令行操作转化为直观的图形界面,帮助开发者快速完成网站部署、环境配置及日常运维工作,无需专业技术背景即可高效管理服务器。
- 漏掉
STUFF(..., 1, 2, '')会导致开头多出一个换行符 - 没加
TYPE和.value(),XML 特殊字符(如&、)会被转义 - 子查询未限定
c2.JOB_ORDER_ID = c1.JOB_ORDER_ID,会变成笛卡尔积
为什么 WHILE 循环也比游标快?
有人试过用 WHILE + 临时表代替游标,发现提速明显——本质不是循环本身快,而是绕开了游标那套状态维护机制。游标每次 FETCH 都要校验锁、维护游标句柄、检查 @@FETCH_STATUS;而 WHILE 只是简单取最小 ID 再删掉,开销低得多。
可参考结构:
SELECT BUSINESS_ID INTO #work FROM #RESULT_COMP_LIST;
WHILE EXISTS (SELECT 1 FROM #work)
BEGIN
DECLARE @id NVARCHAR(50) = (SELECT MIN(BUSINESS_ID) FROM #work);
UPDATE #RESULT_COMP_LIST
SET CTN_NO = (
SELECT STRING_AGG(CONTAINER_NO, NCHAR(13)+NCHAR(10))
FROM OZ_BK_CONTAINER
WHERE JOB_ORDER_ID = @id
)
WHERE BUSINESS_ID = @id;
DELETE FROM #work WHERE BUSINESS_ID = @id;
END
但注意:这仍是“伪集合操作”,仅当必须保留逐行语义(比如中间要调用外部 API 或复杂条件分支)时才考虑;否则优先走纯集合方案。
游标声明时加 LOCAL FAST_FORWARD READ_ONLY 仍不够
即使你写了 DECLARE cur CURSOR LOCAL FAST_FORWARD READ_ONLY FOR ...,也无法改变逐行处理的本质。这类选项只减少部分资源开销(比如不建临时工作表、禁用滚动),但无法规避 IO 次数翻倍、无法并行、锁持有时间长等问题。
真正该做的判断是:
- 业务逻辑是否真需要“每行独立执行”?比如依赖上一行计算结果、或要捕获单行异常继续跑
- 能否把“行间依赖”下沉到单条 SQL 中(例如用窗口函数
LAG/LEAD) - 如果只是分组聚合、更新、插入,99% 的情况都能用
JOIN+GROUP BY+UPDATE ... FROM一步到位
游标不是语法错误,但它是设计层面的红灯——一旦出现,先别调优,先问一句:这个需求,真的不能用一条 SQL 解决吗?


















