在编写SQL Server后端逻辑时,经常会遇到前端传来越来越长的逗号分隔字符串,需要在存储过程中将其拆成多行再关联业务表。过去我们习惯用WHILE循环配合CHARINDEX截取,但这种方式不仅代码冗长,而且在数据量上升时性能急剧下降。借助内置的STRING_SPLIT函数,可以用纯集合操作完成拆分,让存储过程更简洁也更高效。

一、STRING_SPLIT函数的基本用法
STRING_SPLIT是SQL Server 2016(兼容级别130及以上)引入的表值函数,它接收两个参数:待拆分的字符串和分隔符,返回一列名为value的结果集。与自定义拆分函数相比,它由数据库引擎原生实现,执行计划更优。
下面示例演示如何将一串产品编号拆成多行,并直接插入临时表供后续使用。注意分隔符当前仅支持单个字符,这是使用该函数的首要限制。
DECLARE @ids NVARCHAR(MAX) = '1001,1002,1003,1004'; SELECT value AS product_id INTO #tmp_products FROM STRING_SPLIT(@ids, ','); SELECT * FROM #tmp_products;
上述代码在存储过程中可改写为接收@input_str参数,从而让调用方动态传入字符串。由于返回的是表,我们可以直接用INNER JOIN替代早期写法中的游标逐行处理。
需要提醒的是,STRING_SPLIT不保证返回行的顺序。如果业务要求按原字符串中出现顺序处理,需要额外借助排序键,后文会给出兼容写法。
二、在存储过程中封装拆分逻辑
实际项目中,我们往往要把拆分结果和主表关联,过滤出存在的记录。下面的存储过程接收用户选中的部门编号串,返回这些部门下的活跃员工。
使用STRING_SPLIT后,原本几十行的循环截取代码缩减为一条JOIN语句。同时,由于输入是参数化变量,不存在拼接SQL带来的注入风险,比在应用端拼IN语句更安全。
CREATE PROCEDURE dbo.GetActiveEmployeesByDepts
@dept_ids NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
SELECT e.emp_id, e.emp_name, e.dept_id
FROM dbo.employee e
INNER JOIN STRING_SPLIT(@dept_ids, ',') s
ON e.dept_id = TRY_CAST(s.value AS INT)
WHERE e.status = 1;
END;
这里用TRY_CAST避免字符串里混入非数字导致整条语句报错。如果前端可能传入空白项,STRING_SPLIT会产生空字符串行,可在JOIN前用WHERE过滤,或者改用下面的CTE写法统一清洗。
在复杂存储过程里,建议先把拆分结果存入临时表并建索引,再参与多表关联。当拆分出的行数超过几千时,这种写法比内联JOIN更稳定,能显著减少重复拆分计算。
三、处理多分隔符与去重需求
STRING_SPLIT只支持单字符分隔符。如果历史数据混用了逗号、分号和竖线,需要先用REPLACE归一化。此外,用户重复勾选会造成重复编号,应当去重。
下面的示例先将多种符号统一为逗号,再嵌套调用STRING_SPLIT并配合DISTINCT得到干净编号列表,最后关联权限表。
CREATE PROCEDURE dbo.LoadRightsByCodes
@mixed_str NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @normalized NVARCHAR(MAX) = REPLACE(REPLACE(@mixed_str, ';', ','), '|', ',');
SELECT DISTINCT TRIM(s.value) AS right_code
INTO #rights
FROM STRING_SPLIT(@normalized, ',') s
WHERE LEN(TRIM(s.value)) > 0;
SELECT r.right_code, r.right_name
FROM dbo.right_def r
INNER JOIN #rights t ON r.right_code = t.right_code;
END;
TRIM函数用于清除首尾空格,LEN过滤空项。如果数据库兼容级别低于130,则需改用传统的XML或数字表拆分,但本文聚焦原生函数方案。
对于必须保序的场景,可先对原始串用序号标记,例如用OPENJSON配合自定义数组,或是在应用端传结构化JSON而非纯字符串,这样比强行给STRING_SPLIT加序更高效。
四、与传统WHILE截取写法对比
在没有STRING_SPLIT的年代,常用WHILE加CHARINDEX循环截取。下面给出典型旧写法,以便理解性能差异根源。
该写法每循环一次就要对剩余字符串做SUBSTRING和CHARINDEX,时间复杂度接近O(n^2),且产生大量批处理日志。在十万级字符串拆分测试中,旧写法常超过两秒,而STRING_SPLIT多在两百毫秒内完成。
DECLARE @s NVARCHAR(MAX) = '1,2,3,4,5,6,7,8,9,10';
DECLARE @pos INT, @item NVARCHAR(50);
CREATE TABLE #old_split(val NVARCHAR(50));
WHILE LEN(@s) > 0
BEGIN
SET @pos = CHARINDEX(',', @s);
IF @pos = 0
BEGIN
INSERT INTO #old_split VALUES(@s);
BREAK;
END
SET @item = LEFT(@s, @pos - 1);
INSERT INTO #old_split VALUES(@item);
SET @s = STUFF(@s, 1, @pos, '');
END
SELECT * FROM #old_split;
从维护角度看,旧写法需要十行以上且容易在边界条件(如末尾无逗号)出错;新写法一行搞定,逻辑错误概率大幅降低。
综合来看,在支持STRING_SPLIT的环境里,存储过程的字符串拆分应优先采用该函数,仅在需多字符分隔或严格保序时考虑辅助处理或升级到SQL Server 2022的STRING_SPLIT有序特性(若有启用)。
五、实战注意事项与小结
在存储过程使用STRING_SPLIT时,应注意传入参数长度用NVARCHAR(MAX)以防截断;关联前尽量清洗空项;对拆分结果建临时表索引可进一步优化多表JOIN。
此外,若前端使用Entity Framework等ORM,可直接调用该存储过程并传字符串,避免先在前端拆成列表再发多次查询,减少网络往返。将拆分逻辑留在数据库层,既清晰又利于统一权限校验。
-- 综合示例:按逗号拆分订单号并标记是否存在
CREATE PROCEDURE dbo.CheckOrderExists
@order_csv NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
SELECT
s.value AS order_no,
CASE WHEN o.order_id IS NOT NULL THEN 1 ELSE 0 END AS is_exist
FROM STRING_SPLIT(@order_csv, ',') s
LEFT JOIN dbo.orders o ON o.order_no = TRIM(s.value);
END;
通过上述示例可以看到,STRING_SPLIT让存储过程内的字符串拆分从繁琐脚本变为声明式查询。掌握它的限制与配套清洗技巧,就能在多数后台批量处理场景中写出高性能、易读且安全的SQL代码。
SQL存储过程STRING_SPLIT字符串拆分修改时间:2026-08-01 23:57:34