Procedimiento central almacenado para ejecutar en el contexto de la base de datos de llamada

17

Estoy trabajando en una solución de mantenimiento personalizada utilizando la sys.dm_db_index_physical_statsvista. Actualmente tengo referencia de un procedimiento almacenado. Ahora, cuando ese procedimiento almacenado se ejecuta en una de mis bases de datos, hace lo que quiero que haga y despliega una lista de todos los registros relacionados con cualquier base de datos. Cuando lo coloco en una base de datos diferente, despliega una lista de todos los registros relacionados solo con esa base de datos.

Por ejemplo (código en la parte inferior):

  • La consulta ejecutada en la Base de datos 6 muestra información [solicitada] para las bases de datos 1-10.
  • La consulta ejecutada en la Base de datos 3 muestra información [solicitada] solo para la base de datos 3.

La razón por la que quiero este procedimiento específicamente en la base de datos tres es porque prefiero mantener todos los objetos de mantenimiento dentro de la misma base de datos. Me gustaría tener este trabajo en la base de datos de mantenimiento y trabajar como si estuviera en la base de datos de esa aplicación.

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
fuente
44
@JoachimIsaksson parece que la pregunta es cómo tener una sola copia del procedimiento en su base de datos de mantenimiento, que haga referencia al DMV en otras bases de datos, en lugar de tener que poner una copia del procedimiento en cada base de datos.
Aaron Bertrand
Lo siento, no estaba más claro, he estado mirando esto por unos días. Aaron es perfecto. Quiero que este SP se asiente en mi base de datos de mantenimiento con la capacidad de capturar datos de todo el servidor. Tal como está, cuando se encuentra en mi base de datos de mantenimiento, solo extrae datos de fragmentación sobre la base de datos de mantenimiento en sí. Lo que me confunde es por qué, cuando coloco exactamente este mismo SP en una base de datos diferente y lo ejecuto de manera idéntica, ¿extrae los datos de fragmentación de todo el servidor? ¿Hay alguna configuración o privilegio que necesite cambiar para que este SP funcione como tal desde la base de datos de mantenimiento?
(Tenga en cuenta que su enfoque actual ignora el hecho de que podría haber dos tablas con el mismo nombre bajo dos esquemas diferentes; además de las sugerencias en mi respuesta, es posible que desee considerar el nombre del esquema como parte de la entrada y / o salida)
Aaron Bertrand

Respuestas:

15

Una forma sería hacer un procedimiento del sistema mastery luego crear un contenedor en su base de datos de mantenimiento. Tenga en cuenta que esto solo funcionará para una base de datos a la vez.

Primero, en 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

Ahora, en su base de datos de mantenimiento, cree un contenedor que use SQL dinámico para establecer el contexto correctamente:

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

(La razón por la que el nombre de la base de datos no puede ser realmente NULLes porque no puede unirse a cosas como sys.objectsy sys.indexesdado que existen independientemente en cada base de datos. Por lo tanto, tal vez tenga un procedimiento diferente si desea información de toda la instancia).

Ahora puede llamar a esto para cualquier otra base de datos, p. Ej.

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

Y siempre puede crear un synonymen cada base de datos para que ni siquiera tenga que hacer referencia al nombre de la base de datos de mantenimiento:

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

Otra forma sería usar SQL dinámico, sin embargo, esto también funcionará solo para una base de datos a la 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

Otra forma sería crear una vista (o función con valores de tabla) para unir los nombres de tabla e índice de todas sus bases de datos, sin embargo, tendría que codificar los nombres de las bases de datos en la vista y mantenerlos a medida que agrega / eliminar las bases de datos que desea permitir que se incluyan en esta consulta. Esto, a diferencia de los demás, le permitirá recuperar estadísticas de varias bases de datos a la vez.

Primero, la vista:

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

Entonces el procedimiento:

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
fuente
15

Bueno, hay malas noticias, buenas noticias con una trampa, y algunas muy buenas noticias.

Las malas noticias

Los objetos T-SQL se ejecutan en la base de datos donde residen. Hay dos excepciones (no muy útiles):

  1. procedimientos almacenados con nombres con prefijo sp_y que existen en la [master]base de datos (no es una gran opción: una base de datos a la vez, agregando algo [master], posiblemente agregando sinónimos a cada base de datos, lo que debe hacerse para cada nueva base de datos)
  2. procedimientos almacenados temporales: locales y globales (no es una opción práctica, ya que deben crearse cada vez y dejarlo con los mismos problemas que tiene con el proceso sp_almacenado) [master].

Las buenas noticias (con una trampa)

Muchas personas (¿quizás la mayoría?) Conocen las funciones integradas para obtener algunos metadatos realmente comunes:

El uso de estas funciones puede eliminar la necesidad de unir sys.databases(aunque este no es realmente un problema), sys.objects(preferido sobre el sys.tablesque excluye las vistas indexadas) y sys.schemas(se perdió esa, y no todo está en el dboesquema ;-). Pero incluso con la eliminación de tres de las cuatro uniones, seguimos siendo funcionalmente el mismo lugar, ¿verdad? Wrong-o!

Una de las buenas características de las funciones OBJECT_NAME()y OBJECT_SCHEMA_NAME()es que tienen un segundo parámetro opcional para @database_id. Es decir, mientras UNIRSE a esas tablas (excepto para sys.databases) es específico de la base de datos, el uso de estas funciones le brinda información de todo el servidor. Incluso OBJECT_ID () permite la información de todo el servidor al darle un nombre de objeto totalmente calificado.

Al incorporar estas funciones de metadatos en la consulta principal, podemos simplificar y al mismo tiempo expandirnos más allá de la base de datos actual. El primer paso de refactorizar la consulta nos da:

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;

Y ahora para la "captura": no hay una función de metadatos para obtener nombres de índice, y mucho menos uno para todo el servidor. ¿Entonces es eso? ¿Estamos al 90% completos y todavía estamos atrapados necesitando estar en una base de datos particular para obtener sys.indexesdatos? ¿Realmente necesitamos crear un procedimiento almacenado para usar Dynamic SQL para completar, cada vez que se ejecuta nuestro proceso principal, una tabla temporal de todas las sys.indexesentradas en todas las bases de datos para que podamos UNIRSE a ella? ¡NO!

La muy buena noticia

Entonces, viene una pequeña característica que a algunas personas les encanta odiar, pero cuando se usa correctamente, puede hacer cosas increíbles. Sí: SQLCLR. ¿Por qué? Debido a que las funciones SQLCLR obviamente pueden enviar sentencias SQL, pero por la misma naturaleza de enviar desde el código de la aplicación, es SQL dinámico. Entonces, a diferencia de las funciones T-SQL, las funciones SQLCLR pueden inyectar un nombre de base de datos en la consulta antes de ejecutarla. Es decir, podemos crear nuestra propia función de reflejar la capacidad de OBJECT_NAME()y OBJECT_SCHEMA_NAME()para tomar una database_idy obtener la información de la base de datos.

El siguiente código es esa función. Pero toma un nombre de base de datos en lugar de una ID para que no tenga que hacer el paso adicional de buscarlo (lo que lo hace un poco menos complicado y un poco más 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;
    }
}

Si se da cuenta, estamos utilizando la conexión de contexto, que no solo es rápida, sino que también funciona en SAFEensamblajes. Sí, esto funciona en una Asamblea marcada comoSAFE, por lo que (o sus variaciones) incluso debería funcionar en Azure SQL Database V12 (el soporte para SQLCLR se eliminó, de manera bastante abrupta, de Azure SQL Database en abril de 2016) .

Entonces, nuestra refactorización de segundo paso de la consulta principal nos da lo siguiente:

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;

¡Eso es! Tanto este SQLCLR Scalar UDF como su procedimiento almacenado T-SQL de mantenimiento pueden vivir en la misma [maintenance]base de datos centralizada . Y, no tiene que procesar una base de datos a la vez; ahora tiene funciones de metadatos para toda la información dependiente que abarca todo el servidor.

PS No hay .IsNullcomprobación de los parámetros de entrada en el código C # ya que el objeto contenedor T-SQL debe crearse con la WITH RETURNS NULL ON NULL INPUTopción:

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 adicionales:

  • El método descrito aquí también se puede usar para resolver otros problemas muy similares de la falta de funciones de metadatos entre bases de datos. La siguiente sugerencia de Microsoft Connect es un ejemplo de uno de esos casos. Y, al ver que Microsoft lo ha cerrado como "No solucionará", está claro que no están interesados ​​en proporcionar funciones integradas como OBJECT_NAME()para satisfacer esta necesidad (de ahí la solución que se publica en esa sugerencia :-).

    Agregue la función de metadatos para obtener el nombre del objeto de hobt_id

  • Para obtener más información sobre el uso de SQLCLR, eche un vistazo a la serie Stairway to SQLCLR que estoy escribiendo en SQL Server Central (se requiere registro gratuito; lo siento, no controlo las políticas de ese sitio).

  • La IndexName()función SQLCLR que se muestra arriba está disponible, precompilada, en un script fácil de instalar en Pastebin. La secuencia de comandos habilita la función "Integración CLR" si aún no está habilitada y el conjunto está marcado como SAFE. Se compila contra .NET Framework versión 2.0 para que funcione en SQL Server 2005 y versiones posteriores (es decir, todas las versiones que admiten SQLCLR).

    Función de metadatos SQLCLR para la base de datos cruzada IndexName ()

  • Si alguien está interesado en la IndexName()función SQLCLR y en más de 320 funciones y procedimientos almacenados, está disponible en la biblioteca SQL # (de la que soy autor). Tenga en cuenta que, si bien hay una versión gratuita, la función Sys_IndexName solo está disponible en la versión completa (junto con una función similar de Sys_AssemblyName ).

Solomon Rutzky
fuente