Центральная хранимая процедура для выполнения в контексте вызывающей базы данных

17

Я работаю над индивидуальным решением обслуживания, используя sys.dm_db_index_physical_statsпредставление. В настоящее время у меня есть ссылка из хранимой процедуры. Теперь, когда эта хранимая процедура выполняется в одной из моих баз данных, она делает то, что я хочу, и выводит список всех записей, относящихся к любой базе данных. Когда я помещаю его в другую базу данных, он записывает список всех записей, относящихся только к этой базе данных.

Например (код внизу):

  • Выполнение запроса к базе данных 6 показывает [запрошенную] информацию для баз данных 1-10.
  • Выполнение запроса к базе данных 3 показывает [запрошенную] информацию только для базы данных 3.

Причина, по которой я хочу эту процедуру специально для третьей базы данных, заключается в том, что я предпочел бы хранить все объекты обслуживания в одной базе данных. Я бы хотел, чтобы эта работа находилась в базе данных обслуживания и работала так, как если бы она была в этой базе данных приложения.

Код:

ALTER PROCEDURE [dbo].[GetFragStats] 
    @databaseName   NVARCHAR(64) = NULL
    ,@tableName     NVARCHAR(64) = NULL
    ,@indexID       INT          = NULL
    ,@partNumber    INT          = NULL
    ,@Mode          NVARCHAR(64) = 'DETAILED'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @databaseID INT, @tableID INT

    IF @databaseName IS NOT NULL
        AND @databaseName NOT IN ('tempdb','ReportServerTempDB')
    BEGIN
        SET @databaseID = DB_ID(@databaseName)
    END

    IF @tableName IS NOT NULL
    BEGIN
        SET @tableID = OBJECT_ID(@tableName)
    END

    SELECT D.name AS DatabaseName,
      T.name AS TableName,
      I.name AS IndexName,
      S.index_id AS IndexID,
      S.avg_fragmentation_in_percent AS PercentFragment,
      S.fragment_count AS TotalFrags,
      S.avg_fragment_size_in_pages AS PagesPerFrag,
      S.page_count AS NumPages,
      S.index_type_desc AS IndexType
    FROM sys.dm_db_index_physical_stats(@databaseID, @tableID, 
           @indexID, @partNumber, @Mode) AS S
    JOIN 
       sys.databases AS D ON S.database_id = D.database_id
    JOIN 
       sys.tables AS T ON S.object_id = T.object_id
    JOIN 
       sys.indexes AS I ON S.object_id = I.object_id
                        AND S.index_id = I.index_id
    WHERE 
        S.avg_fragmentation_in_percent > 10
    ORDER BY 
        DatabaseName, TableName, IndexName, PercentFragment DESC    
END
GO
Джош Вацлавский
источник
4
@JoachimIsaksson, кажется, вопрос в том, как иметь одну единственную копию процедуры в своей базе данных обслуживания, которая ссылается на DMV в других базах данных, вместо того, чтобы помещать копию процедуры в каждую базу данных.
Аарон Бертран
Извините, я не был более ясен, смотрел на это в течение нескольких дней. Аарон на месте. Я хочу, чтобы этот SP находился в моей базе данных обслуживания с возможностью сбора данных со всего сервера. В настоящее время, когда он находится в моей БД обслуживания, он только извлекает данные фрагментации о самой БД обслуживания. Что меня смущает, так это то, почему, когда я помещаю этот точно такой же SP в другую базу данных и выполняю его одинаково, он извлекает данные фрагментации со всего сервера? Есть ли настройка или привилегия, которые необходимо изменить, чтобы этот SP работал как таковой из БД обслуживания?
(Обратите внимание, что ваш текущий подход игнорирует тот факт, что могут быть две таблицы с одинаковыми именами в двух разных схемах - в дополнение к предложениям в моем ответе вы можете рассмотреть имя схемы как часть ввода и / или вывода.)
Аарон Бертран

Ответы:

15

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

Сначала в мастере:

USE [master];
GO
CREATE PROCEDURE dbo.sp_GetFragStats -- sp_prefix required
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  SELECT
    DatabaseName    = DB_NAME(),
    TableName       = t.name,
    IndexName       = i.name,
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
    -- shouldn't s.partition_number be part of the output as well?
  FROM sys.tables AS t
  INNER JOIN sys.indexes AS i
    ON t.[object_id] = i.[object_id]
    AND i.index_id = COALESCE(@indexID, i.index_id)
    AND t.name = COALESCE(@tableName, t.name)
  CROSS APPLY
    sys.dm_db_index_physical_stats(DB_ID(), t.[object_id], 
      i.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
  -- probably also want to filter on minimum page count too
  -- do you really care about a table that has 100 pages?
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;
END
GO
-- needs to be marked as a system object:
EXEC sp_MS_MarkSystemObject N'dbo.sp_GetFragStats';
GO

Теперь в вашей базе данных обслуживания создайте оболочку, которая использует динамический SQL для правильной установки контекста:

USE YourMaintenanceDatabase;
GO
CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName SYSNAME,      -- can't really be NULL, right?
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  DECLARE @sql NVARCHAR(MAX);

  SET @sql = N'USE ' + QUOTENAME(@DatabaseName) + ';
    EXEC dbo.sp_GetFragStats @tableName, @indexID, @partNumber, @Mode;';

  EXEC sp_executesql 
    @sql,
    N'@tableName NVARCHAR(128),@indexID INT,@partNumber INT,@Mode NVARCHAR(20)',
    @tableName, @indexID, @partNumber, @Mode;
END
GO

(Причина, по которой имя базы данных на самом деле не может быть, NULLзаключается в том, что вы не можете присоединиться к таким вещам, как sys.objectsи sys.indexesтак как они существуют независимо в каждой базе данных. Поэтому, возможно, есть другая процедура, если вам нужна информация для всего экземпляра.)

Теперь вы можете вызвать это для любой другой базы данных, например,

EXEC YourMaintenanceDatabase.dbo.GetFragStats 
  @DatabaseName = N'AdventureWorks2012',
  @TableName    = N'SalesOrderHeader';

И вы всегда можете создать synonymв каждой базе данных, так что вам даже не нужно ссылаться на имя базы данных обслуживания:

USE SomeOtherDatabase;`enter code here`
GO
CREATE SYNONYM dbo.GetFragStats FOR YourMaintenanceDatabase.dbo.GetFragStats;

Другой способ - использовать динамический SQL, однако это также будет работать только для одной базы данных одновременно:

USE YourMaintenanceDatabase;
GO
CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName SYSNAME,
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @sql NVARCHAR(MAX) = N'SELECT
    DatabaseName    = @DatabaseName,
    TableName       = t.name,
    IndexName       = i.name,
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
  FROM ' + QUOTENAME(@DatabaseName) + '.sys.tables AS t
  INNER JOIN ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS i
    ON t.[object_id] = i.[object_id]
    AND i.index_id = COALESCE(@indexID, i.index_id)
    AND t.name = COALESCE(@tableName, t.name)
  CROSS APPLY
    ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_physical_stats(
        DB_ID(@DatabaseName), t.[object_id], i.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;';

  EXEC sp_executesql @sql, 
    N'@DatabaseName SYSNAME, @tableName NVARCHAR(128), @indexID INT,
      @partNumber INT, @Mode NVARCHAR(20)',
    @DatabaseName, @tableName, @indexID, @partNumber, @Mode;
END
GO

Еще один способ - создать представление (или табличную функцию) для объединения имен таблиц и индексов всех ваших баз данных, однако вам придется жестко закодировать имена баз данных в представление и поддерживать их по мере добавления / удалить базы данных, которые вы хотите разрешить включить в этот запрос. Это, в отличие от других, позволит вам получать статистику для нескольких баз данных одновременно.

Во-первых, мнение:

CREATE VIEW dbo.CertainTablesAndIndexes
AS
  SELECT 
    db = N'AdventureWorks2012',
    t.[object_id],
    [table] = t.name,
    i.index_id,
    [index] = i.name
  FROM AdventureWorks2012.sys.tables AS t
  INNER JOIN AdventureWorks2012.sys.indexes AS i
  ON t.[object_id] = i.[object_id]

  UNION ALL

  SELECT 
    db = N'database2',
    t.[object_id],
    [table] = t.name,
    i.index_id,
    [index] = i.name
  FROM database2.sys.tables AS t
  INNER JOIN database2.sys.indexes AS i
  ON t.[object_id] = i.[object_id]

  -- ... UNION ALL ...
  ;
GO

Тогда процедура:

CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName NVARCHAR(128) = NULL,
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  SELECT
    DatabaseName    = DB_NAME(s.database_id),
    TableName       = v.[table],
    IndexName       = v.[index],
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
  FROM dbo.CertainTablesAndIndexes AS v
  CROSS APPLY sys.dm_db_index_physical_stats
    (DB_ID(v.db), v.[object_id], v.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
    AND v.index_id = COALESCE(@indexID, v.index_id)
    AND v.[table] = COALESCE(@tableName, v.[table])
    AND v.db = COALESCE(@DatabaseName, v.db)
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;
END
GO
Аарон Бертран
источник
15

Ну, есть плохие новости, хорошие новости с подвохом и некоторые действительно хорошие новости.

Плохие новости

Объекты T-SQL выполняются в базе данных, в которой они находятся. Есть два (не очень полезных) исключения:

  1. хранимые процедуры с именами с префиксом sp_и которые существуют в[master] базе данных (не очень хороший вариант: одна БД за раз, добавление чего-либо [master], возможно, добавление синонимов к каждой БД, что необходимо сделать для каждой новой БД)
  2. временные хранимые процедуры - локальные и глобальные (не практичный вариант, так как они должны создаваться каждый раз и оставлять вас с теми же проблемами, что и с sp_хранимым процессом в[master] .

Хорошие новости (с подвохом)

Многие (возможно, большинство?) Люди знают о встроенных функциях для получения действительно распространенных метаданных:

Использование этих функций может устранить необходимость в соединениях sys.databases(хотя эта на самом деле не является проблемой) sys.objects(предпочтительнее, чем sys.tablesисключение индексированных представлений) и sys.schemas(вы пропустили это, и не все находится вdbo схеме ;-). Но даже с удалением трех из четырех JOINов мы все еще функционально не изменились, верно? Неправильный-о!

Одной из приятных особенностей функций OBJECT_NAME()и OBJECT_SCHEMA_NAME()является то, что у них есть необязательный второй параметр для @database_id. Это означает, что, хотя СОЕДИНЕНИЕ к этим таблицам (кроме sys.databases) зависит от базы данных, использование этих функций позволяет получить информацию для всего сервера. Даже OBJECT_ID () позволяет получить информацию для всего сервера, дав ему полное имя объекта.

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

SELECT  DB_NAME(stat.database_id) AS [DatabaseName],
        OBJECT_SCHEMA_NAME(stat.[object_id], stat.database_id) AS [SchemaName],
        OBJECT_NAME(stat.[object_id], stat.database_id) AS [TableName],
        ind.name AS [IndexName],
        stat.index_id AS [IndexID],
        stat.avg_fragmentation_in_percent AS [PercentFragment],
        stat.fragment_count AS [TotalFrags],
        stat.avg_fragment_size_in_pages AS [PagesPerFrag],
        stat.page_count AS [NumPages],
        stat.index_type_desc AS [IndexType]
FROM sys.dm_db_index_physical_stats(@DatabaseID, @TableID, 
        @IndexID, @PartitionNumber, @Mode) stat
INNER JOIN sys.indexes ind
        ON ind.[object_id] = stat.[object_id]
       AND ind.[index_id] = stat.[index_id]
WHERE stat.avg_fragmentation_in_percent > 10
ORDER BY DatabaseName, TableName, IndexName, PercentFragment DESC;

А теперь для «уловки»: нет функции метаданных для получения имен индексов, не говоря уже о серверной. Так это все? Мы на 90% полны и все еще застряли, нуждаясь в определенных базах данных для получения sys.indexesданных? Действительно ли нам нужно создать хранимую процедуру, чтобы использовать динамический SQL для заполнения, каждый раз, когда запускается наш основной процесс, временной таблицы всех sys.indexesзаписей во всех базах данных, чтобы мы могли присоединиться к ней? НЕТ!

Действительно хорошие новости

Таким образом, появляется небольшая особенность, которую некоторые люди любят ненавидеть, но при правильном использовании могут делать удивительные вещи. Да: SQLCLR. Почему? Поскольку функции SQLCLR , очевидно , может подать заявления SQL, но самой природой представления из приложения кода, то есть динамический SQL. Таким образом, в отличие от функций T-SQL, функции SQLCLR могут вводить имя базы данных в запрос перед его выполнением. Это означает, что мы можем создать нашу собственную функцию, чтобы отразить способность OBJECT_NAME()и OBJECT_SCHEMA_NAME()получать database_idи получать информацию для этой базы данных.

Следующий код является этой функцией. Но он берет имя базы данных вместо идентификатора, поэтому ему не нужно выполнять дополнительный шаг поиска (что делает его немного менее сложным и немного быстрее).

public class MetaDataFunctions
{
    [return: SqlFacet(MaxSize = 128)]
    [Microsoft.SqlServer.Server.SqlFunction(IsDeterministic = true, IsPrecise = true,
        SystemDataAccess = SystemDataAccessKind.Read)]
    public static SqlString IndexName([SqlFacet(MaxSize = 128)] SqlString DatabaseName,
        SqlInt32 ObjectID, SqlInt32 IndexID)
    {
        string _IndexName = @"<unknown>";

        using (SqlConnection _Connection =
                                    new SqlConnection("Context Connection = true;"))
        {
            using (SqlCommand _Command = _Connection.CreateCommand())
            {
                _Command.CommandText = @"
SELECT @IndexName = si.[name]
FROM   [" + DatabaseName.Value + @"].[sys].[indexes] si
WHERE  si.[object_id] = @ObjectID
AND    si.[index_id] = @IndexID;
";

                SqlParameter _ParamObjectID = new SqlParameter("@ObjectID",
                                               SqlDbType.Int);
                _ParamObjectID.Value = ObjectID.Value;
                _Command.Parameters.Add(_ParamObjectID);

               SqlParameter _ParamIndexID = new SqlParameter("@IndexID", SqlDbType.Int);
                _ParamIndexID.Value = IndexID.Value;
                _Command.Parameters.Add(_ParamIndexID);

                SqlParameter _ParamIndexName = new SqlParameter("@IndexName",
                                                  SqlDbType.NVarChar, 128);
                _ParamIndexName.Direction = ParameterDirection.Output;
                _Command.Parameters.Add(_ParamIndexName);

                _Connection.Open();
                _Command.ExecuteNonQuery();

                if (_ParamIndexName.Value != DBNull.Value)
                {
                    _IndexName = (string)_ParamIndexName.Value;
                }
            }
        }

        return _IndexName;
    }
}

Если вы заметите, мы используем Context Connection, который не только быстрый, но и работает в SAFE сборках. Да, это работает в сборке, отмеченной какSAFEпоэтому он (или его разновидности) должен даже работать на базе данных SQL Azure V12 (поддержка SQLCLR была довольно неожиданно удалена из базы данных SQL Azure в апреле 2016 года) .

Таким образом, наш повторный рефакторинг основного запроса дает нам следующее:

SELECT  DB_NAME(stat.database_id) AS [DatabaseName],
        OBJECT_SCHEMA_NAME(stat.[object_id], stat.database_id) AS [SchemaName],
        OBJECT_NAME(stat.[object_id], stat.database_id) AS [TableName],
        dbo.IndexName(DB_NAME(stat.database_id), stat.[object_id], stat.[index_id])
                     AS [IndexName],
        stat.index_id AS [IndexID],
        stat.avg_fragmentation_in_percent AS [PercentFragment],
        stat.fragment_count AS [TotalFrags],
        stat.avg_fragment_size_in_pages AS [PagesPerFrag],
        stat.page_count AS [NumPages],
        stat.index_type_desc AS [IndexType]
FROM sys.dm_db_index_physical_stats(@DatabaseID, @TableID, 
        @IndexID, @PartitionNumber, @Mode) stat
WHERE stat.avg_fragmentation_in_percent > 10
ORDER BY DatabaseName, TableName, IndexName, PercentFragment DESC;

Это оно! И эта Scalar UDF SQLCLR, и ваша хранимая процедура T-SQL обслуживания могут находиться в одной и той же централизованной [maintenance]базе данных. И вам не нужно обрабатывать одну базу данных одновременно; теперь у вас есть функции метаданных для всей зависимой информации, которая распространяется на весь сервер.

PS В .IsNullкоде C # нет проверки входных параметров, поскольку объект-обертка T-SQL должен быть создан с WITH RETURNS NULL ON NULL INPUTопцией:

CREATE FUNCTION [dbo].[IndexName]
                   (@DatabaseName [nvarchar](128), @ObjectID [int], @IndexID [int])
RETURNS [nvarchar](128) WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [{AssemblyName}].[MetaDataFunctions].[IndexName];

Дополнительные замечания:

  • Описанный здесь метод также можно использовать для решения других, очень похожих проблем, связанных с отсутствием функций метаданных кросс-базы данных. Следующее предложение Microsoft Connect является примером одного такого случая. И, видя, что Microsoft закрыла его как «Не будет исправлять», становится ясно, что они не заинтересованы в предоставлении встроенных функций, подобных OBJECT_NAME()для удовлетворения этой потребности (отсюда и обходной путь, опубликованный в этом предложении :-).

    Добавьте функцию метаданных, чтобы получить имя объекта из hobt_id

  • Чтобы узнать больше об использовании SQLCLR, ознакомьтесь с серией статей Stairway to SQLCLR, которую я пишу на SQL Server Central (требуется бесплатная регистрация; извините, я не контролирую политики этого сайта).

  • IndexName()Функция SQLCLR , показанная выше , доступны, предварительно скомпилированных, в удобном для установки скрипта на Pastebin. Сценарий включает функцию «Интеграция CLR», если она еще не включена, и сборка помечена как SAFE. Он скомпилирован с .NET Framework версии 2.0, поэтому он будет работать в SQL Server 2005 и более поздних версиях (т.е. во всех версиях, поддерживающих SQLCLR).

    SQLCLR Функция метаданных для кросс-базы данных IndexName ()

  • Если кого-то интересует IndexName()функция SQLCLR и более 320 других функций и хранимых процедур, она доступна в библиотеке SQL # (автором которой я являюсь). Обратите внимание, что хотя есть бесплатная версия, функция Sys_IndexName доступна только в полной версии (вместе с аналогичной функцией Sys_AssemblyName ).

Соломон Руцкий
источник