SQL Server中将任意SELECT查询结果存入临时表的通用方法
问题前提
我们需要处理的SELECT查询满足以下约定:
- 顶层不包含
order by子句 - 不属于动态SQL
- 为标准
select语句而非存储过程调用 - 是单查询语句,不会返回多个结果集
- 所有返回列均有显式命名
要求方案可通过客户端侧SQL处理或T-SQL内置技巧实现,支持程序化自动执行,不需要针对特定SQL手动改写。
常见的错误通用方案
以下两种写法看起来简单,但都不支持带CTE的查询场景,不具备通用性:
- 子查询套层写法:
select * into #tmp from (undl) x(undl为原始查询)。如果原始查询以CTE(with子句)开头,拼接后的SQL会把CTE放在子查询内部,违反T-SQL中CTE必须定义在语句顶层的语法规则,直接执行报错。例如原始查询为with mycte as (select 5 as mycol) select mycol from mycte时,拼接后的语句在MSSQL 2016中属于非法语法。 - 外层套CTE写法:
with x as (undl) select * into #tmp from x。T-SQL不支持with子句嵌套,原始查询本身带CTE时会直接语法报错。
高成本可行方案
目前已知语法上完全兼容的原生写法是定位到查询的顶层select关键字,在其对应的from子句前插入into #tmp。但这种方案的实现成本极高:无法通过简单字符串匹配定位顶层select的位置,例如查询with mycte as (select 5 as mycol) select mycol from mycte except select 6中,into #tmp需要插入到CTE定义后的第一个顶层select之后,而非except后的select位置,要做到100%准确定位必须完整解析SQL生成语法树,开发量很大。
另有思路提出先创建封装目标查询的用户定义函数,再执行select * into #tmp from dbo.my_function()存表后删除函数。这种方案虽然可行,但需要创建持久化数据库对象,要求账号有创建函数的权限,并发场景下容易出现函数名冲突,操作痕迹重,不是最优解。
零SQL解析的最优通用方案
不需要修改原始SQL内部结构,也不需要创建持久化对象,通过T-SQL内置元数据能力即可实现,兼容所有符合约定的查询场景,程序化实现成本极低:
- 获取查询结果元数据:调用系统存储过程
sp_describe_first_result_set,传入原始查询语句,即可在不实际执行查询的前提下,拿到返回结果所有列的名称、数据类型、长度、精度、可空性等完整定义。 - 创建结构匹配的临时表:根据上一步返回的元数据,动态拼接
CREATE TABLE #tmp(列定义)语句,创建和查询结果结构完全一致的空临时表。 - 直接插入数据:不需要修改原始查询的任何内容,直接在原始查询语句前拼接
INSERT INTO #tmp后执行即可。
方案合理性说明
T-SQL语法中,INSERT INTO 表名后可以直接接任何合法的顶层查询语句,无论查询是以WITH(CTE)开头还是直接以SELECT开头,也无论查询是否包含UNION/EXCEPT/INTERSECT等集合运算,语法均完全合法,不会出现CTE嵌套的问题。
实现示例
针对原始查询:
with mycte as (select 5 as mycol) select mycol from mycte except select 6
第一步获取元数据后,可知查询仅返回INT类型的mycol列,先创建临时表:
CREATE TABLE #tmp(mycol INT NOT NULL);
第二步直接拼接执行插入语句,不需要修改原始查询内容:
INSERT INTO #tmp with mycte as (select 5 as mycol) select mycol from mycte except select 6;
执行完成后查询#tmp即可拿到原始查询的结果。
注:
sp_describe_first_result_set在SQL Server 2012及以上版本原生支持,更老版本可以通过SET FMTONLY ON执行原始查询的方式获取列元数据,整体逻辑不变。
内容的提问来源于stack exchange,提问作者Ed Avis

