Надежный способ проверки хранимых процедур T-SQL

Мы обновляемся с SQL Server 2005 до 2008 года. Почти каждая база данных в экземпляре 2005 года настроена на режим совместимости 2000 года, но мы прыгаем до 2008 года. Наше тестирование завершено, но мы узнали, что мы нужно ускориться.

Я обнаружил некоторые хранимые процедуры, которые либо выбрали данные из отсутствующих таблиц, либо попытались использовать ORDER BY столбцами, которые не существуют.

Обтекание SQL для создания процедур в SET PARSEONLY ON, а ошибки захвата в try/catch только улавливают недопустимые столбцы в ORDER BY. Он не находит ошибку с процедурой, выбирающей данные из отсутствующей таблицы. SSMS 2008 intellisense, однако, НАДЕЕТСЯ найти проблему, но я все еще могу продолжить и успешно запустить ALTER script для процедуры, не жалуясь.

Итак, почему я даже могу избежать создания процедуры, которая терпит неудачу при ее запуске? Есть ли там инструменты, которые могут сделать лучше, чем я пробовал?

Первый инструмент, который я нашел, не очень полезен: DbValidator из CodeProject, но он находит меньше проблем, чем этот script, я нашел на 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

Ответ 1

Вы можете выбрать разные способы. Прежде всего, SQL SERVER 2008 поддерживает зависимости, которые существуют в зависимых от базы данных зависимостях STORED PROCEDURE (см. http://msdn.microsoft.com/en-us/library/bb677214%28v=SQL.100%29.aspx, http://msdn.microsoft.com/en-us/library/ms345449.aspx и http://msdn.microsoft.com/en-us/library/cc879246.aspx). Вы можете использовать sys.sql_expression_dependencies и sys.dm_sql_referenced_entities, чтобы увидеть и проверить там.

Но самый простой способ сделать проверку всей НЕПРЕРЫВНОЙ ПРОЦЕДУРЫ:

  • экспортировать все ЗАПОМНЕННЫЕ ПРОЦЕДУРЫ
  • удалить старые существующие ЗАПОМНЕННЫЕ ПРОЦЕДУРЫ
  • импортировать только что экспортированную ЗАПОМНЕННУЮ ПРОЦЕДУРУ.

Если вы обновите БД, существующая Хранимая процедура не будет проверена, но если вы создадите новую, процедура будет проверена. Поэтому после экспорта и экспорта всех сохраненных процедур вы получаете всю существующую сообщаемую ошибку.

Вы также можете просмотреть и экспортировать код хранимой процедуры с помощью кода, следующего за

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. В комментарии к моему ответу Мартин Смит предложил использовать sys.sp_refreshsqlmodule вместо воссоздания хранимой процедуры. Так что с кодом

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

один получает script, который может использоваться для проверки зависимостей хранимой процедуры. Результат будет выглядеть следующим образом (пример с 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'

Ответ 2

Вот что сработало для меня:

-- 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 blog и анонимный комментатор...

Ответ 3

Я обожаю использовать Display Estimated Execution Plan. Он подсвечивает много ошибок разумно, даже не имея возможности запустить proc.

Ответ 4

У меня была такая же проблема в предыдущем проекте и написала TSQL checker на SQL2005, а затем Программа Windows, реализующая ту же функциональность.

Ответ 5

Когда я столкнулся с этим вопросом, мне было интересно найти безопасную, неинвазивную и быструю технику для проверки ссылок на синтаксис и объект (таблицу, столбец).

Хотя я согласен с тем, что на самом деле выполнение каждой хранимой процедуры, скорее всего, вызовет больше проблем, чем просто их компиляция, следует проявлять осторожность в отношении прежнего подхода. То есть вам нужно знать, что на самом деле безопасно выполнять каждую хранимую процедуру (например, удаляет ли некоторые таблицы, например?). Эта проблема безопасности может быть решена путем обертывания выполнения транзакции и ее возврата, так что изменения не являются постоянными, как это предлагается в ответе devio. Тем не менее, этот подход может занять довольно много времени в зависимости от того, сколько данных вы манипулируете.

Код в вопросе и первая часть ответа Олега, оба предполагают повторное создание каждой хранимой процедуры, поскольку это действие перекомпилирует процедуру и делает именно такую ​​синтаксическую проверку. Но этот подход является инвазивным - он подходит для частной тестовой системы, но может нарушить работу других разработчиков на сильно используемой тестовой системе.

Я наткнулся на статью Проверить действительность хранимых процедур, представлений и функций SQL Server, которая представляет .NET-решение, но это последующий пост внизу "ddblue", который заинтриговал меня больше. Этот подход получает текст каждой хранимой процедуры, преобразует ключевое слово create в alter, чтобы его можно было скомпилировать, а затем скомпилировать proc. И это точно сообщает о любых плохих таблицах и столбцах. Код запускается, но я быстро столкнулся с некоторыми проблемами из-за шага преобразования create/alter.

Преобразование из "create" to "alter" ищет "CREATE" и "PROC", разделенные одним пробелом. В реальном мире могут быть пробелы или вкладки, и может быть один или несколько. Я добавил вложенную последовательность "replace" (спасибо, эта статья от Джеффа Модена!), Чтобы преобразовать все такие вхождения в одно пространство, что позволяет преобразование для продолжения, как первоначально было разработано. Затем, поскольку это нужно было использовать везде, где использовалось исходное выражение "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