【问题标题】:Retrieve column definition for stored procedure result set检索存储过程结果集的列定义
【发布时间】:2011-11-14 04:40:14
【问题描述】:

我正在使用 SQL Server 2008 中的存储过程,我了解到我必须 INSERT INTO 一个已预定义的临时表才能使用数据。没关系,除非我不是编写存储过程的人,除了列出它的定义并通读代码之外,我如何弄清楚如何定义我的临时表?

例如,对于“EXEC sp_stored_procedure”,我的临时表是什么样的?这是一个简单的存储过程,我可能会猜到数据类型,但似乎必须有一种方法来读取执行过程返回的列的类型和长度。

【问题讨论】:

  • @Mitch,对不起,我的印象是,将答案标记为答案是承认某人付出的努力的正确方式。我一直在投票赞成其他人的问题和这些问题的答案。我会对围绕 SO 的公认做法更有良心。
  • 每个问题您只能接受一个答案,但您可以对任何有帮助、您从中学习或欣赏的答案(针对您自己的问题或其他任何人的问题)进行投票他们付出的努力(即使最终不是“最正确”的答案)。这里已经发展了几种礼仪形式,但基本上要记住的一件事是,您的投票和接受可能是激励人们回答您问题的唯一因素。你想鼓励更多的回应,因为最好的答案来自集体,而不是来自一个......恕我直言。

标签: sql-server sql-server-2008 stored-procedures


【解决方案1】:

假设你在 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

【讨论】:

  • 哇...这很复杂。我已经读过这在 Denali 上是如何更容易的,但我不知道它在 SQL Server 2008 上有多复杂。感谢您非常彻底的回答。在我的谷歌搜索中,我没有看到任何如此丰富的信息。
  • 您的回答隐含地帮助我理解了我从 OPENQUERY 收到的此错误消息:链接服务器“someserver”的 OLE DB 提供程序“SQLNCLI10”为列提供了不一致的元数据。名称在执行时已更改。
  • 太棒了!我用它来比较两个 SP 的输出。我做了一些更改,并希望确保原始和新 SP 的结果相同。 SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_proc_old;') 除了 SELECT * FROM OPENQUERY(loopback, 'EXEC tempdb.dbo.my_proc_new;')
【解决方案2】:

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

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

现在您可以从 SQL server management studio 中获取 tmpTable 的脚本或查询 sys.columns 以获取字段描述。

【讨论】:

    【解决方案3】:

    这是我写的一些代码。这个想法是(正如其他人所说)是获取 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”字,假设这是执行返回结果集的选择的代码。

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

    更改代码选择结果集成表(基于uniqueidentifier的表)

    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)
    

    希望这会有所帮助!

    【讨论】:

      【解决方案4】:

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

      您可以使用非常有用的 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 的输出定义,这不是最快的方法 :)

      【讨论】:

      • 问题是关于存储过程的
      【解决方案5】:

      看起来在 SQL 2012 中有一个新的 SP 可以帮助解决这个问题。

      exec sp_describe_first_result_set N'PROC_NAME'
      

      https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-describe-first-result-set-transact-sql

      【讨论】:

        【解决方案6】:

        为了获得可查询的结果集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 demo

        虽然 SP 不能使用临时表,但这种方法几乎没有限制。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-12-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多