检索存储过程结果集的列定义

43
我正在使用SQL Server 2008中的存储过程,并了解到我必须预定义一个临时表并使用INSERT INTO才能处理数据。这没问题,但如果不是我编写存储过程的人,除了列出其定义并阅读代码之外,我该如何确定如何定义我的临时表呢?
例如,针对`EXEC sp_stored_procedure`的临时表应该是什么样子的?这是一个简单的存储过程,我可能可以猜测数据类型,但似乎有一种方式可以直接从执行过程返回的列中读取类型和长度。

2
@Mitch,抱歉我误以为将答案标记为正确答案是承认某人付出努力的正确方式。我一直在给其他人的问题和回答投票。我会更加注意SO周围的惯例。 - cjbarth
4
在一个问题中,您只能选择一个答案,但您可以点赞任何有帮助、学到东西或您欣赏他们付出的努力的答案(无论是在您自己的问题上还是在别人的问题上),即使它不是最正确的答案也可以。这里已经发展出了几种礼仪形式,但基本上需要记住的一件事情就是,您的投票和采纳可能是激励别人回答您问题的唯一动力。您想鼓励更多的回应,因为最好的答案来自于集体,而不是单一的... 在我看来。 - Aaron Bertrand
6个回答

57

假设您在tempdb中有一个存储过程:

USE tempdb;
GO

CREATE PROCEDURE dbo.my_procedure
AS
BEGIN
    SET NOCOUNT ON;

    SELECT foo = 1, bar = 'tooth';
END
GO

有一种相当复杂的方法可以确定存储过程将输出的元数据。其中包括存储过程只能输出单个结果集,并且如果无法精确确定数据类型,则会进行最佳猜测。它需要使用OPENQUERY和一个回环链接服务器,该服务器的'DATA ACCESS'属性设置为true。您可以检查sys.servers以查看是否已经有有效的服务器,但让我们手动创建一个名为loopback的服务器:

EXEC master..sp_addlinkedserver 
    @server = 'loopback',  
    @srvproduct = '',
    @provider = 'SQLNCLI',
    @datasrc = @@SERVERNAME;

EXEC master..sp_serveroption 
    @server = 'loopback', 
    @optname = 'DATA ACCESS',
    @optvalue = 'TRUE';

现在您可以将其作为链接服务器查询,您可以使用任何查询的结果(包括存储过程调用)作为常规 SELECT。因此,您可以执行以下操作(请注意,数据库前缀很重要,否则将出现错误11529和2812):

SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_procedure;');

如果我们可以执行SELECT *,那么我们也可以执行SELECT * INTO:
SELECT * INTO #tmp FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_procedure;');

一旦 #tmp 表存在,我们可以通过以下方式确定元数据(假设是 SQL Server 2005 或更高版本):

SELECT c.name, [type] = t.name, c.max_length, c.[precision], c.scale
  FROM sys.columns AS c
  INNER JOIN sys.types AS t
  ON c.system_type_id = t.system_type_id
  AND c.user_type_id = t.user_type_id
  WHERE c.[object_id] = OBJECT_ID('tempdb..#tmp');

(如果您使用的是 SQL Server 2000,则可以使用 syscolumns 进行类似操作,但我手头没有 2000 实例来验证等效查询。)
结果:
name      type    max_length precision scale
--------- ------- ---------- --------- -----
foo       int              4        10     0
bar       varchar          5         0     0

在德纳利中,这将变得更加容易。虽然仍有第一结果集的限制,但您无需设置关联服务器并跳过所有这些麻烦。您只需要说:
DECLARE @sql NVARCHAR(MAX) = N'EXEC tempdb.dbo.my_procedure;';

SELECT name, system_type_name
    FROM sys.dm_exec_describe_first_result_set(@sql, NULL, 1);

结果:

name      system_type_name
--------- ----------------
foo       int             
bar       varchar(5)      

在Denali之前,我建议你自己动手找出数据类型会更容易。这不仅因为按照上述步骤是很繁琐的,而且因为你很可能比引擎更能做出正确(或至少更准确)的猜测,因为引擎猜测的数据类型将基于运行时输出,而没有任何关于可能值域的外部知识。在Denali中,这个因素仍然存在,所以不要认为新的元数据发现功能是万能的,它们只是使上述过程稍微不那么繁琐。

另外,有关OPENQUERY的其他潜在陷阱,请参见Erland Sommarskog的文章:

http://www.sommarskog.se/share_data.html#OPENQUERY


2
哇...这很复杂。我读过在Denali上更容易,但我不知道在SQL Server 2008上有多么复杂。感谢您非常详尽的回答。我在Google搜索中没有看到如此信息丰富的内容。 - cjbarth
1
你的回答隐含地帮助我理解了我从 OPENQUERY 得到的错误信息:OLE DB 提供程序 "SQLNCLI10" 为链接服务器 "someserver" 提供的某列元数据不一致。名称在执行时已更改。 - cdonner
太棒了!我用这个工具来比较两个存储过程的输出结果。我做了一些更改,想要确保原始和新的存储过程的结果是相同的。SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_proc_old;') EXCEPT SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_proc_new;') - David Coster

13

7

一种不太复杂的方式(在某些情况下可能足够):编辑原始存储过程,在最终SELECT语句之后,FROM子句之前添加INSERT INTO tmpTable以将SP结果保存到tmpTable中。

运行修改后的SP,最好使用有意义的参数以获取实际数据。恢复存储过程的原始代码。

现在,您可以从SQL Server Management Studio获取tmpTable的脚本,或者查询sys.columns以获取字段描述。


6
这是我写的一些代码。其想法是(正如其他人所述)获取SP代码,修改它并执行它。然而,我的代码没有改变原始SP。
第一步,获取SP的定义,去掉'Create'部分,如果存在参数声明后的'AS',则将其去掉。
Declare @SPName varchar(250)
Set nocount on

Declare @SQL Varchar(max), @SQLReverse Varchar(MAX), @StartPos int, @LastParameterName varchar(250) = '', @TableName varchar(36) = 'A' + REPLACE(CONVERT(varchar(36), NewID()), '-', '')

Select * INTO #Temp from INFORMATION_SCHEMA.PARAMETERS where SPECIFIC_NAME = 'ADMIN_Sync_CompareDataForSync'

if @@ROWCOUNT > 0
    BEGIN
        Select @SQL = REPLACE(ROUTINE_DEFINITION, 'CREATE PROCEDURE [' + ROUTINE_SCHEMA + '].[' + ROUTINE_NAME + ']', 'Declare') 
        from INFORMATION_SCHEMA.ROUTINES 
        where ROUTINE_NAME = @SPName

        Select @LastParameterName = PARAMETER_NAME + ' ' + DATA_TYPE + 
            CASE WHEN CHARACTER_MAXIMUM_LENGTH is not null THEN '(' + 
                CASE WHEN CHARACTER_MAXIMUM_LENGTH = -1 THEN 'MAX' ELSE CONVERT(varchar,CHARACTER_MAXIMUM_LENGTH) END + ')' ELSE '' END 
        from #Temp 
        WHERE ORDINAL_POSITION = 
            (Select MAX(ORDINAL_POSITION) 
            From #Temp)

        Select @StartPos = CHARINDEX(@LastParameterName, REPLACE(@SQL, '  ', ' '), 1) + LEN(@LastParameterName)
    END
else
    Select @SQL = REPLACE(ROUTINE_DEFINITION, 'CREATE PROCEDURE [' + ROUTINE_SCHEMA + '].[' + ROUTINE_NAME + ']', '') from INFORMATION_SCHEMA.ROUTINES where ROUTINE_NAME = @SPName

DROP TABLE #Temp

Select @StartPos = CHARINDEX('AS', UPPER(@SQL), @StartPos)

Select @SQL = STUFF(@SQL, @StartPos, 2, '')

请注意,根据唯一标识符创建了一个新的表名。 现在找到代码中最后一个“From”单词,假设这是执行选择返回结果集的代码。

(Note:根据一个唯一标识符生成新的表名称;现在假设以下代码执行了select语句并返回结果集,请找出代码中最后一个'From'单词。)
Select @SQLReverse = REVERSE(@SQL)

Select @StartPos = CHARINDEX('MORF', UPPER(@SQLReverse), 1)

将代码更改为将结果集选择到表中(基于唯一标识符的表)。
Select @StartPos = LEN(@SQL) - @StartPos - 2

Select @SQL = STUFF(@SQL, @StartPos, 5, ' INTO ' + @TableName + ' FROM ')

EXEC (@SQL)

结果集现在是一个表格,即使表格为空也没有关系!

让我们获取表格的结构

Select * from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @TableName

现在您可以使用此功能进行操作

不要忘记删除该唯一表格

Select @SQL = 'drop table ' + @TableName

Exec (@SQL)

希望这能帮到你!

1
为了获得可查询的结果集,可以使用sys.dm_exec_describe_first_result_set(适用于SQL Server 2012):
SELECT column_ordinal, name, system_type_name
FROM sys.dm_exec_describe_first_result_set(N'EXEC stored_procedure_name', NULL, 0);

db<>fiddle演示

然而,这种解决方案有一些限制,例如SP不能使用临时表。


-2

如果您在一个权限受限的环境中工作,像回环链接服务器这样的东西似乎是黑魔法,绝对是“不可能的事情!”但是您对模式有一些权限,并且只有几个存储过程需要处理,那么有一个非常简单的解决方案。

您可以使用非常有用的SELECT INTO语法,它将创建一个新表格,其中包含查询结果集。

假设您的存储过程包含以下Select查询:

SELECT x, y, z
FROM MyTable t INNER JOIN Table2 t2 ON t.id = t2.id...

而应该将其替换为:

SELECT x, y, z
INTO MyOutputTable
FROM MyTable t INNER JOIN Table2 t2 ON t.id = t2.id...

当您执行它时,它将使用查询返回的结果创建一个名为MyOutputTable的新表。

您只需右键单击其名称即可获取表定义。

就是这样!

SELECT INTO仅需要创建新表的能力,并且可以使用临时表(SELECT... INTO #MyTempTable),但是检索定义可能会更困难。

然而,如果您需要检索数千个SP的输出定义,则不是最快的方法:)


这个问题是关于存储过程的。 - Mike

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接