Procedimento armazenado central para executar no contexto do banco de dados de chamada

17

Estou trabalhando em uma solução de manutenção personalizada usando a sys.dm_db_index_physical_statsvisualização. Atualmente, ele está sendo referenciado a partir de um procedimento armazenado. Agora, quando esse procedimento armazenado é executado em um dos meus bancos de dados, ele faz o que eu quero e faz uma lista de todos os registros relacionados a qualquer banco de dados. Quando o coloco em um banco de dados diferente, ele exibe uma lista de todos os registros relacionados apenas a esse banco de dados.

Por exemplo (código na parte inferior):

  • A consulta executada no banco de dados 6 mostra informações [solicitadas] para os bancos de dados 1-10.
  • A consulta executada no banco de dados 3 mostra informações [solicitadas] apenas para o banco de dados 3.

O motivo pelo qual desejo esse procedimento especificamente no banco de dados três é porque prefiro manter todos os objetos de manutenção no mesmo banco de dados. Eu gostaria que esse trabalho estivesse no banco de dados de manutenção e funcionasse como se estivesse no banco de dados do aplicativo.

Código:

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
Josh Waclawski
fonte
4
@JoachimIsaksson parece que a pergunta é como ter uma única cópia do procedimento em seu banco de dados de manutenção, que faz referência ao DMV em outros bancos de dados, em vez de precisar colocar uma cópia do procedimento em cada banco de dados.
Aaron Bertrand
Desculpe por não ter sido mais claro, encarando isso por alguns dias. Aaron está no local. Quero que esse SP fique no meu banco de dados de manutenção com a capacidade de capturar dados de todo o servidor. Tal como está, quando está no meu banco de dados de manutenção, ele apenas extrai dados de fragmentação sobre o próprio banco de dados de manutenção. O que me deixa confuso é por que, quando coloco exatamente esse mesmo SP em um banco de dados diferente e o executo de forma idêntica, ele extrai os dados de fragmentação do servidor? Existe uma configuração ou privilégio que precise ser alterado para que esse SP funcione como tal a partir do banco de dados de manutenção?
(Observe que sua abordagem atual ignora o fato de que pode haver duas tabelas com o mesmo nome em dois esquemas diferentes - além das sugestões na minha resposta, você pode considerar o nome do esquema como parte da entrada e / ou saída.)
Aaron Bertrand

Respostas:

15

Uma maneira seria executar um procedimento do sistema mastere criar um wrapper no banco de dados de manutenção. Observe que isso funcionará apenas para um banco de dados por vez.

Primeiro, no mestre:

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

Agora, no seu banco de dados de manutenção, crie um wrapper que use SQL dinâmico para definir o contexto corretamente:

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

(A razão pela qual o nome do banco de dados não pode realmente ser NULLé porque você não pode ingressar em coisas como sys.objectse, sys.indexesuma vez que elas existem independentemente em cada banco de dados. Portanto, talvez tenha um procedimento diferente se desejar informações em toda a instância.)

Agora você pode chamar isso para qualquer outro banco de dados, por exemplo

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

E você sempre pode criar um synonymem cada banco de dados para não precisar nem fazer referência ao nome do banco de dados de manutenção:

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

Outra maneira seria usar o SQL dinâmico, mas isso também funcionará apenas para um banco de dados por vez:

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

Outra maneira seria criar uma visualização (ou função com valor de tabela) para unir os nomes de tabelas e índices de todos os seus bancos de dados; no entanto, você teria que codificar os nomes dos bancos de dados na visualização e mantê-los à medida que você adiciona / remova os bancos de dados que você deseja permitir que sejam incluídos nesta consulta. Ao contrário dos outros, isso permitiria recuperar estatísticas para vários bancos de dados de uma só vez.

Primeiro, a visão:

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

Então o procedimento:

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
Aaron Bertrand
fonte
15

Bem, há más notícias, boas notícias e algumas realmente boas.

As más notícias

Os objetos T-SQL são executados no banco de dados em que residem. Existem duas exceções (não muito úteis):

  1. procedimentos armazenados com nomes prefixados sp_e que existem no [master]banco de dados (não é uma ótima opção: um banco de dados de cada vez, adicionando algo a [master], possivelmente adicionando sinônimos a cada banco de dados, o que deve ser feito para cada novo banco de dados)
  2. procedimentos armazenados temporários - local e global (não é uma opção prática, pois eles precisam ser criados a cada vez e deixam os mesmos problemas que você tem com o processo sp_armazenado [master].

As boas notícias (com uma pegadinha)

Muitas pessoas (talvez a maioria?) Estão cientes das funções internas para obter alguns metadados realmente comuns:

O uso dessas funções pode eliminar a necessidade de os JOINs sys.databases(embora este não seja realmente um problema), sys.objects(preferencial sobre o sys.tablesque exclui as exibições indexadas) e sys.schemas(você estava sentindo falta dessa, e nem tudo está no dboesquema ;-). Mas mesmo com a remoção de três dos quatro JOINs, ainda estamos funcionalmente no mesmo lugar, certo? Errado!

Um dos bons recursos das funções OBJECT_NAME()e OBJECT_SCHEMA_NAME()é que eles têm um segundo parâmetro opcional para @database_id. O que significa que, enquanto JOIN nessas tabelas (exceto sys.databases) é específico do banco de dados, o uso dessas funções fornece informações para todo o servidor. Até o OBJECT_ID () permite informações em todo o servidor, fornecendo a ele um nome de objeto totalmente qualificado.

Ao incorporar essas funções de metadados na consulta principal, podemos simplificar e, ao mesmo tempo, expandir além do banco de dados atual. A primeira passagem de refatoração da consulta nos fornece:

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;

E agora para a "captura": não há função de meta-dados para obter nomes de índices, muito menos uma para todo o servidor. Então é isso? Estamos 90% completos e ainda assim precisamos estar em um banco de dados específico para obter sys.indexesdados? Realmente precisamos criar um procedimento armazenado para usar o SQL dinâmico para preencher, sempre que nosso proc principal for executado, uma tabela temporária de todas as sys.indexesentradas em todos os bancos de dados para que possamos ingressar nele? NÃO!

As boas notícias

Então vem um pequeno recurso que algumas pessoas adoram odiar, mas quando usadas corretamente, podem fazer coisas incríveis. Sim: SQLCLR. Por quê? Como as funções SQLCLR podem obviamente enviar instruções SQL, mas pela própria natureza de enviar a partir do código do aplicativo, é Dynamic SQL. Portanto, diferentemente das funções T-SQL, as funções SQLCLR podem injetar um nome de banco de dados na consulta antes de executá-la. Ou seja, podemos criar nossa própria função para espelhar a capacidade OBJECT_NAME()e OBJECT_SCHEMA_NAME()obter um database_ide obter as informações desse banco de dados.

O código a seguir é essa função. Mas é preciso um nome de banco de dados em vez de ID, para que não seja necessário fazer a etapa extra de procurá-lo (o que o torna um pouco menos complicado e um pouco mais rápido).

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;
    }
}

Se você perceber, estamos usando a Conexão de Contexto, que não é apenas rápida, mas também funciona em SAFEAssemblies. Sim, isso funciona em uma montagem marcada comoSAFE, portanto (ou variações dele) deve funcionar no Banco de Dados SQL do Azure V12 (o suporte ao SQLCLR foi removido, de forma abrupta, do Banco de Dados SQL do Azure em abril de 2016) .

Portanto, nossa refatoração de segunda passagem da consulta principal fornece o seguinte:

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;

É isso aí! Esse UDF escalar do SQLCLR e o procedimento armazenado T-SQL de manutenção podem viver no mesmo [maintenance]banco de dados centralizado . E você não precisa processar um banco de dados por vez; agora você tem funções de metadados para todas as informações dependentes de todo o servidor.

PS Não há .IsNullverificação dos parâmetros de entrada no código C #, pois o objeto wrapper T-SQL deve ser criado com a WITH RETURNS NULL ON NULL INPUTopção:

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];

Notas Adicionais:

  • O método descrito aqui também pode ser usado para resolver outros problemas muito semelhantes de funções ausentes de meta-dados entre bancos de dados. A sugestão a seguir do Microsoft Connect é um exemplo de um desses casos. E, visto que a Microsoft a fechou como "Não Consertará", fica claro que eles não estão interessados ​​em fornecer funções internas OBJECT_NAME()que atendam a essa necessidade (daí a Solução Alternativa publicada nessa Sugestão :-).

    Adicione a função de metadados para obter o nome do objeto de hobt_id

  • Para saber mais sobre o uso do SQLCLR, consulte a série Stairway to SQLCLR que estou escrevendo no SQL Server Central (registro gratuito é necessário; desculpe, não controlo as políticas desse site).

  • A IndexName()função SQLCLR mostrada acima está disponível, pré-compilada, em um script fácil de instalar no Pastebin. O script habilita o recurso "Integração do CLR", se ainda não estiver ativado, e o Assembly está marcado como SAFE. Ele é compilado no .NET Framework versão 2.0 para funcionar no SQL Server 2005 e em versões mais recentes (ou seja, todas as versões que oferecem suporte ao SQLCLR).

    Função de metadados SQLCLR para IndexName () de banco de dados

  • Se alguém estiver interessado na IndexName()função SQLCLR e mais de 320 outras funções e procedimentos armazenados, ele estará disponível na biblioteca SQL # (da qual sou o autor). Observe que, embora exista uma versão gratuita, a função Sys_IndexName só está disponível na versão completa (junto com uma função semelhante Sys_AssemblyName ).

Solomon Rutzky
fonte