【问题标题】:A reliable way to verify T-SQL stored procedures一种验证 T-SQL 存储过程的可靠方法
【发布时间】:2011-02-16 14:40:24
【问题描述】:

我们正在从 SQL Server 2005 升级到 2008。几乎 2005 实例中的每个数据库都设置为 2000 兼容模式,但我们正在跳转到 2008。我们的测试已经完成,但我们了解到,我们需要加快速度。

我发现了一些存储过程,它们要么从丢失的表中选择数据,要么尝试对不存在的列进行排序。

包装 SQL 以在 SET PARSEONLY ON 中创建过程并在 try/catch 中捕获错误只会捕获 ORDER BY 中的无效列。它没有发现从缺失表中选择数据的过程的错误。然而,SSMS 2008 的智能感知确实找到了问题,但我仍然可以继续并成功运行该过程的 ALTER 脚本而不会抱怨。

那么,为什么我什至可以创建一个运行时失败的过程呢?有没有比我尝试过的工具做得更好的工具?

我发现的第一个工具不是很有用:DbValidator from CodeProject,但它发现的问题比我在 SqlServerCentral 上找到的这个脚本少,后者发现了无效的列引用。

-------------------------------------------------------------------------
-- Check Syntax of Database Objects
-- Copyrighted work.  Free to use as a tool to check your own code or in 
--  any software not sold. All other uses require written permission.
-------------------------------------------------------------------------
-- Turn on ParseOnly so that we don't actually execute anything.
SET PARSEONLY ON 
GO

-- Create a table to iterate through
declare @ObjectList table (ID_NUM int NOT NULL IDENTITY (1, 1), OBJ_NAME varchar(255), OBJ_TYPE char(2))

-- Get a list of most of the scriptable objects in the DB.
insert into @ObjectList (OBJ_NAME, OBJ_TYPE)
SELECT   name, type
FROM     sysobjects WHERE type in ('P', 'FN', 'IF', 'TF', 'TR', 'V')
order by type, name

-- Var to hold the SQL that we will be syntax checking
declare @SQLToCheckSyntaxFor varchar(max)
-- Var to hold the name of the object we are currently checking
declare @ObjectName varchar(255)
-- Var to hold the type of the object we are currently checking
declare @ObjectType char(2)
-- Var to indicate our current location in iterating through the list of objects
declare @IDNum int
-- Var to indicate the max number of objects we need to iterate through
declare @MaxIDNum int
-- Set the inital value and max value
select  @IDNum = Min(ID_NUM), @MaxIDNum = Max(ID_NUM)
from    @ObjectList

-- Begin iteration
while @IDNum <= @MaxIDNum
begin
  -- Load per iteration values here
  select  @ObjectName = OBJ_NAME, @ObjectType = OBJ_TYPE
  from    @ObjectList
  where   ID_NUM = @IDNum 

  -- Get the text of the db Object (ie create script for the sproc)
  SELECT @SQLToCheckSyntaxFor = OBJECT_DEFINITION(OBJECT_ID(@ObjectName, @ObjectType))

  begin try
    -- Run the create script (remember that PARSEONLY has been turned on)
    EXECUTE(@SQLToCheckSyntaxFor)
  end try
  begin catch
    -- See if the object name is the same in the script and the catalog (kind of a special error)
    if (ERROR_PROCEDURE() <> @ObjectName)
    begin
      print 'Error in ' + @ObjectName
      print '  The Name in the script is ' + ERROR_PROCEDURE()+ '. (They don''t match)'
    end
    -- If the error is just that this already exists then  we don't want to report that.
    else if (ERROR_MESSAGE() <> 'There is already an object named ''' + ERROR_PROCEDURE() + ''' in the database.')
    begin
      -- Report the error that we got.
      print 'Error in ' + ERROR_PROCEDURE()
      print '  ERROR TEXT: ' + ERROR_MESSAGE() 
    end
  end catch

  -- Setup to iterate to the next item in the table
  select  @IDNum = case
            when Min(ID_NUM) is NULL then @IDNum + 1
            else Min(ID_NUM)
          end  
  from    @ObjectList
  where   ID_NUM > @IDNum

end
-- Turn the ParseOnly back off.
SET PARSEONLY OFF 
GO

【问题讨论】:

  • 也许通过运行自动化回归测试,您会发现问题所在并能够在存储过程级别进行追踪。
  • 您也可以尝试 SET NOEXEC ON 并执行每个存储的过程,但我发现这也不会出现很多错误。 EXEC sys.sp_refreshsqlmodule 部分成功地捕获了一些错误。我发现唯一可靠的方法是运行一个工具来实际执行每个存储过程,表值函数为每个参数传递 NULL,然后将其回滚并记录任何失败。 (并且也从每个视图中进行选择)显然,这取决于您的程序有多耗时以及它们的确切性质,这将如何运作。
  • @John -- 哈!自动回归测试......不适用于这些数据库。近两年来,我们一直在努力清理这个客户的烂摊子!不过我同意,这是可行的方法,但我们没有时间开发测试。
  • 嗨!您是否使用存储过程的导出/导入或 sys.sp_refreshsqlmodule 进行了测试(请参阅我的答案)?它适用于您的数据库吗?
  • 在 DBA 站点上查看 this answer。您可以使用sys.procedures 代替sys.views

标签: sql sql-server tsql sql-server-2008 syntax


【解决方案1】:

这对我有用:

-- Based on comment from http://blogs.msdn.com/b/askjay/archive/2012/07/22/finding-missing-dependencies.aspx
-- Check also http://technet.microsoft.com/en-us/library/bb677315(v=sql.110).aspx

select o.type, o.name, ed.referenced_entity_name, ed.is_caller_dependent
from sys.sql_expression_dependencies ed
join sys.objects o on ed.referencing_id = o.object_id
where ed.referenced_id is null

您应该为您的 SP 获取所有缺少的依赖项,从而解决后期绑定问题。

异常is_caller_dependent = 1 并不一定意味着依赖关系中断。这只是意味着依赖关系在运行时解决,因为没有指定引用对象的模式。您可以避免它指定引用对象的架构(例如另一个 SP)。

感谢Jay's blog 和匿名评论者...

【讨论】:

  • 我用过这个,虽然它确实产生了一些误报。例如,如果一个存储过程引用了另一个数据库中的一个对象,它就会认为有一个错误的引用。此外,它还将系统存储过程标识为“问题”。例如,在我们的一些存储过程中使用了 sp_send_dbmail,并被标记为错误引用。不过,感谢分享这个。
  • 这将我对hierarchyid 列(例如GetAncestor、GetLevel)的所有操作标记为误报。他们都有is_ambiguous = 1,所以这似乎是一个额外的过滤器。
  • 这是查找错误的良好开端。
【解决方案2】:

您可以选择不同的方式。首先,SQL SERVER 2008 支持 STORED PROCEDURE 的 DB 包含依赖项中存在的依赖项(请参阅 http://msdn.microsoft.com/en-us/library/bb677214%28v=SQL.100%29.aspxhttp://msdn.microsoft.com/en-us/library/ms345449.aspxhttp://msdn.microsoft.com/en-us/library/cc879246.aspx)。您可以使用 sys.sql_expression_dependencies 和 sys.dm_sql_referenced_entities 在那里查看和验证。

但验证所有存储过程的最简单方法如下:

  1. 导出所有存储过程
  2. 删除旧的现有存储过程
  3. 导入刚刚导出的 STORED PROCEDURE。

如果你升级数据库,现有的存储过程将不会被验证,但如果你创建一个新的,这个过程将被验证。因此,在导出和导出所有存储过程后,您会收到报告的所有现有错误。

您还可以使用如下代码查看和导出存储过程的代码

SELECT definition
FROM sys.sql_modules
WHERE object_id = (OBJECT_ID(N'spMyStoredProcedure'))

更新:要查看存储过程 spMyStoredProcedure 引用的对象(如表和视图),您可以使用以下命令:

SELECT OBJECT_NAME(referencing_id) AS referencing_entity_name 
    ,referenced_server_name AS server_name
    ,referenced_database_name AS database_name
    ,referenced_schema_name AS schema_name
    , referenced_entity_name
FROM sys.sql_expression_dependencies 
WHERE referencing_id = OBJECT_ID(N'spMyStoredProcedure');

更新 2:在对我的回答的评论中,Martin Smith 建议使用 sys.sp_refreshsqlmodule 而不是重新创建存储过程。所以用代码

SELECT 'EXEC sys.sp_refreshsqlmodule ''' + OBJECT_SCHEMA_NAME(object_id) +
              '.' + name + '''' FROM sys.objects WHERE type in (N'P', N'PC')

接收一个脚本,该脚本可用于验证存储过程的依赖关系。输出将如下所示(以 AdventureWorks2008 为例):

EXEC sys.sp_refreshsqlmodule 'dbo.uspGetManagerEmployees'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetWhereUsedProductID'
EXEC sys.sp_refreshsqlmodule 'dbo.uspPrintError'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeeHireInfo'
EXEC sys.sp_refreshsqlmodule 'dbo.uspLogError'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeeLogin'
EXEC sys.sp_refreshsqlmodule 'HumanResources.uspUpdateEmployeePersonalInfo'
EXEC sys.sp_refreshsqlmodule 'dbo.uspSearchCandidateResumes'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetBillOfMaterials'
EXEC sys.sp_refreshsqlmodule 'dbo.uspGetEmployeeManagers'

【讨论】:

  • 除了编写数据库过程、函数、视图的脚本并将 CREATE 更改为 ALTER 之外,是否还有其他好处?或运行 EXEC sys.sp_refreshsqlmodule?我原以为这样会更好,因为您不依赖于创建顺序。
  • 我没有尝试,但是我的方法很简单。在 SQL Server Management Studio 中,您可以生成一个存储过程的脚本,也可以编写完整的数据库脚本。因此,单击几下后,您可以生成包含所有存储过程的脚本。只需删除所有存储过程并在那里创建。
  • @Martin 在我看来你是对的。和 EXEC sys.sp_refreshsqlmodule 'spMyStoredProcedure' 将优化我的建议。与 sp_MSforeachtable 一起使用会非常有效。
  • 我们最终采取了“找到问题就解决问题”的方法(我想做的最后一件事),因为我们没有时间创建一个彻底的解决方案。然而,这个答案似乎是解决这个问题的一个很好的方法,因此我会接受它作为一个答案。我希望我能尽快尝试一下:)
【解决方案3】:

我喜欢使用 Display Estimated Execution Plan。它合理地突出了许多错误,而无需真正运行 proc。

【讨论】:

    【解决方案4】:

    我在之前的项目中遇到了同样的问题,在 SQL2005 上写了一个TSQL checker,后来又写了一个Windows program,实现了相同的功能。

    【讨论】:

      【解决方案5】:

      当我遇到这个问题时,我有兴趣找到一种安全、非侵入性且快速的技术来验证语法和对象(表、列)引用。

      虽然我同意实际执行每个存储过程可能会出现比编译它们更多的问题,但必须谨慎使用前一种方法。也就是说,您需要知道执行每个存储过程实际上是安全的(例如,它是否会擦除某些表?)。正如 devio 的回答中所建议的那样,可以通过将执行包装在事务中并将其回滚来解决此安全问题,因此不会发生永久性更改。不过,这种方法可能需要相当长的时间,具体取决于您处理的数据量。

      问题中的代码和 Oleg 回答的第一部分都建议重新实例化每个存储过程,因为该操作会重新编译该过程并进行此类语法验证。但是这种方法是侵入性的——这对于私有测试系统来说很好,但可能会破坏其他开发人员在频繁使用的测试系统上的工作。

      我看到了文章Check Validity of SQL Server Stored Procedures, Views and Functions,它提出了一个.NET 解决方案,但更让我感兴趣的是底部的“ddblue”follow-up post。该方法获取每个存储过程的文本,将create关键字转换为alter以便可以编译,然后编译proc。这准确地报告了任何错误的表和列引用。代码运行,但由于创建/更改转换步骤,我很快遇到了一些问题。

      从“create”到“alter”的转换查找由单个空格分隔的“CREATE”和“PROC”。在现实世界中,可能有空格或制表符,可能有一个或多个。我添加了一个嵌套的“替换”序列(感谢 Jeff Moden 的this article!)将所有此类事件转换为一个空格,从而允许转换按最初设计的方式进行。然后,由于在使用原始“sm.definition”表达式的任何地方都需要使用它,所以我添加了一个公共表表达式以避免大量、难看的代码重复。所以这是我的代码更新版本:

      DECLARE @Schema NVARCHAR(100),
          @Name NVARCHAR(100),
          @Type NVARCHAR(100),
          @Definition NVARCHAR(MAX),
          @CheckSQL NVARCHAR(MAX)
      
      DECLARE crRoutines CURSOR FOR
      WITH System_CTE ( schema_name, object_name, type_desc, type, definition, orig_definition)
      AS -- Define the CTE query.
      ( SELECT    OBJECT_SCHEMA_NAME(sm.object_id) ,
                  OBJECT_NAME(sm.object_id) ,
                  o.type_desc ,
                  o.type,
                  REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(REPLACE(sm.definition, char(9), ' '))), '  ', ' ' + CHAR(7)), CHAR(7) + ' ', ''), CHAR(7), '') [definition],
                  sm.definition [orig_definition]
        FROM      sys.sql_modules (NOLOCK) AS sm
                  JOIN sys.objects (NOLOCK) AS o ON sm.object_id = o.object_id
        -- add a WHERE clause here as indicated if you want to test on a subset before running the whole list.
        --WHERE     OBJECT_NAME(sm.object_id) LIKE 'xyz%'
      )
      -- Define the outer query referencing the CTE name.
      SELECT  schema_name ,
              object_name ,
              type_desc ,
              CASE WHEN type_desc = 'SQL_STORED_PROCEDURE'
                   THEN STUFF(definition, CHARINDEX('CREATE PROC', definition), 11, 'ALTER PROC')
                   WHEN type_desc LIKE '%FUNCTION%'
                   THEN STUFF(definition, CHARINDEX('CREATE FUNC', definition), 11, 'ALTER FUNC')
                   WHEN type = 'VIEW'
                   THEN STUFF(definition, CHARINDEX('CREATE VIEW', definition), 11, 'ALTER VIEW')
                   WHEN type = 'SQL_TRIGGER'
                   THEN STUFF(definition, CHARINDEX('CREATE TRIG', definition), 11, 'ALTER TRIG')
              END
      FROM    System_CTE
      ORDER BY 1 , 2;
      
      OPEN crRoutines
      
      FETCH NEXT FROM crRoutines INTO @Schema, @Name, @Type, @Definition
      
      WHILE @@FETCH_STATUS = 0 
          BEGIN
              IF LEN(@Definition) > 0
                  BEGIN
                      -- Uncomment to see every object checked.
                      -- RAISERROR ('Checking %s...', 0, 1, @Name) WITH NOWAIT
                      BEGIN TRY
                          SET PARSEONLY ON ;
                          EXEC ( @Definition ) ;
                          SET PARSEONLY OFF ;
                      END TRY
                      BEGIN CATCH
                          PRINT @Type + ': ' + @Schema + '.' + @Name
                          PRINT ERROR_MESSAGE() 
                      END CATCH
                  END
              ELSE
                  BEGIN
                      RAISERROR ('Skipping %s...', 0, 1, @Name) WITH NOWAIT
                  END
              FETCH NEXT FROM crRoutines INTO @Schema, @Name, @Type, @Definition
          END
      
      CLOSE crRoutines
      DEALLOCATE crRoutines
      

      【讨论】:

      • 看起来很聪明。我不上班的时候试试看。
      • 然而,这段代码有几个小错误。在外部查询的“CASE”语句中,最后两种情况应该以“WHEN type_desc =”开头,而不是“WHEN type =”。
      【解决方案6】:

      在我第一次提出这个问题九年后,我刚刚发现了一个由 Microsoft 自己构建的惊人工具,它不仅可以可靠地验证 SQL Server 版本之间的存储过程兼容性,还可以验证所有其他内部方面。它被重命名了几次,但他们目前称之为:

      Microsoft® 数据迁移助手 v5.4*

      * 截至 2021 年 6 月 17 日的版本

      https://www.microsoft.com/en-us/download/details.aspx?id=53595

      数据迁移助手 (DMA) 使您能够通过检测可能影响新版本 SQL Server 上的数据库功能的兼容性问题来升级到现代数据平台。它建议为您的目标环境改进性能和可靠性。它不仅允许您移动架构和数据,还允许将未包含的对象从源服务器移动到目标服务器。

      上面使用 EXEC sys.sp_refreshsqlmodule 的答案是一个很好的开始,但是我们在 2008 R2 上运行它时遇到了一个主要问题:任何被重命名的存储过程或函数(使用 sp_rename,而不是 DROP/CREATE 模式) 运行刷新过程后恢复到之前的定义,因为内部元数据不会以新名称刷新。这是 SQL Server 2012 中修复的已知错误,但之后我们度过了愉快的一天。 (未来的读者,一种解决方法是在刷新引发错误时发出 ROLLBACK。)

      无论如何,时代变了,有新工具可用——而且是很好的工具——因此这个答案的后期添加。

      【讨论】:

      • 我刚刚试了一下。 DMA 工具确实检测到了一些潜在问题,但不是我正在寻找的问题。我有一些 2008 代码无法使用更新的兼容性设置进行编译。 (它需要从日期到日期时间的显式转换。) sp_refreshsql 模块确实检测到问题,而 DMA 没有。 DMA 不得进行编译。但是,重新编译可能会出现问题,例如丢失签名。我想知道回滚是否会恢复签名....
      猜你喜欢
      • 2018-02-11
      • 1970-01-01
      • 2018-10-03
      • 2013-05-28
      • 2011-08-03
      • 1970-01-01
      • 2012-10-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多