直接在存储过程中用select返回百万行以上结果集会导致客户端卡死、网络超时和tempdb爆涨,根本原因是传输方式错误;应改用for json/for xml聚合后通过输出参数返回、分页流式响应或临时表+bcp导出压缩文件等方式绕过瓶颈。

直接在存储过程中用 SELECT 返回百万行以上结果集,客户端会卡死、网络超时、tempdb 爆涨——这不是数据量“大”的问题,而是传输方式错了。SQL Server 2016 不支持原生结果集压缩(如 gzip),但可以绕过网络瓶颈,把压缩逻辑下沉到应用层或改用更可控的返回机制。
用输出参数 + FOR XML 或 FOR JSON 手动拼接压缩载荷
SQL Server 2016 支持 FOR JSON AUTO(需 SP2+)和 FOR XML,可将多行结果聚合成单个字符串字段,再通过输出参数返回。这避免了多结果集协议开销,也便于应用层调用 zlib / Gzip 压缩。
-
FOR JSON更轻量,但注意:JSON 字符串长度受varchar(max)限制(2GB),实际建议单次不超过 50MB,否则CONCAT或字符串拼接易触发隐式转换失败 - 必须加
SET CONCAT_NULL_YIELDS_NULL OFF,否则含 NULL 列时整个 JSON 构造失败 - 输出参数类型必须是
varchar(max)或nvarchar(max),不能用text(已弃用) - 示例片段:
DECLARE @json_output nvarchar(max);<br>SELECT @json_output = (SELECT id, name, amount FROM orders WHERE status = 'shipped' FOR JSON AUTO);<br>SELECT @json_output AS result;
分页返回 + 客户端流式解压(推荐用于 Web API 场景)
当调用方是 ASP.NET 或 Node.js 等支持流式响应的环境时,不要一次性取完所有数据。让存储过程只负责分页逻辑,压缩由 HTTP 层自动完成(如 IIS 启用 dynamic compression、Kestrel 配置 ResponseCompression)。
- 存储过程用
OFFSET @page OFFSET @size ROWS FETCH NEXT @size ROWS ONLY,但注意:2016 的OFFSET/FETCH在深度分页(>10 万行偏移)时性能陡降,务必在ORDER BY字段建覆盖索引 - 禁止在
WHERE中对索引字段用函数(如YEAR(create_time)),否则索引失效,OFFSET变成全表扫描 - 客户端首次请求传
page=0,服务端返回TotalCount(需额外SELECT COUNT(*)或用估算值),后续按需拉取 - ASP.NET 中用
SqlDataReader配合yield return流式读取,避免DataSet.Fill()一次性加载全部内存
用临时表 + BULK INSERT 导出到文件再压缩(适合后台批处理)
如果目标是生成压缩包供下游系统下载(如每日订单快照),别让存储过程“返回”数据,让它写文件,再由外部脚本压缩。
- 在存储过程中创建本地临时表(
#orders_export),INSERT 分批结果(每批 ≤ 5000 行,带显式事务) - 用
xp_cmdshell调用bcp导出:bcp "SELECT * FROM #orders_export" queryout "D:\export\orders_20260929.csv" -c -t"," -S localhost -T;注意:需启用xp_cmdshell且 SQL Server 服务账户有写权限 - 导出后立即调用
7z a -tzip D:\export\orders_20260929.zip D:\export\orders_20260929.csv(需提前部署 7-Zip) - 风险点:临时表名不能重复,建议加
RAND()后缀;bcp不支持 UTF-8,中文需用-C 65001参数(SQL Server 2016 SP2+)
真正容易被忽略的是:所谓“压缩数据集”,本质是减少网络传输字节或磁盘 IO,而不是让 SQL Server 自己做压缩计算。SQL Server 2016 没有 COMPRESS() 函数(那是 2016 SP1+ 才加的),硬要在 T-SQL 里做 base64 或 zlib 编码只会拖慢执行、撑爆 tempdb。优先把压缩交给更擅长的环节——HTTP 层、应用进程或 OS 工具。











