Procédure stockée centrale à exécuter dans le contexte de la base de données appelante


17

Je travaille sur une solution de maintenance personnalisée à l'aide de la sys.dm_db_index_physical_statsvue. Je l'ai actuellement référencé à partir d'une procédure stockée. Maintenant, lorsque cette procédure stockée s'exécute sur l'une de mes bases de données, elle fait ce que je veux qu'elle fasse et extrait une liste de tous les enregistrements concernant n'importe quelle base de données. Lorsque je le place sur une autre base de données, il extrait une liste de tous les enregistrements relatifs à cette base de données uniquement.

Par exemple (code en bas):

  • L'exécution de la requête sur la base de données 6 affiche les informations [demandées] pour les bases de données 1-10.
  • L'exécution de la requête sur la base de données 3 affiche les informations [demandées] pour la base de données 3 uniquement.

La raison pour laquelle je veux cette procédure spécifiquement sur la base de données trois est parce que je préfère garder tous les objets de maintenance dans la même base de données. J'aimerais que ce travail soit dans la base de données de maintenance et fonctionne comme s'il se trouvait dans cette base de données d'application.

Code:

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 semble la question est de savoir comment avoir une seule copie de la procédure dans leur base de données de maintenance, qui référence le DMV dans d'autres bases de données, plutôt que d'avoir à mettre une copie de la procédure dans chaque base de données.
Aaron Bertrand

Désolé, je n'ai pas été plus clair, je regarde ça depuis quelques jours. Aaron est sur place. Je veux que ce SP se trouve dans ma base de données de maintenance avec la possibilité de récupérer des données à travers le serveur. En l'état, lorsqu'elle se trouve dans ma base de données de maintenance, elle ne récupère que des données de fragmentation sur la base de données de maintenance elle-même. Ce qui me dérange, c'est pourquoi, lorsque je place exactement ce même SP dans une autre base de données et l'exécute à l'identique, tire-t-il les données de fragmentation à travers le serveur? Existe-t-il un paramètre ou un privilège qui doit être modifié pour que ce SP fonctionne comme tel à partir de la base de données de maintenance?

(Notez que votre approche actuelle ignore le fait qu'il pourrait y avoir deux tables avec le même nom sous deux schémas différents - en plus des suggestions dans ma réponse, vous voudrez peut-être considérer le nom du schéma comme faisant partie de l'entrée et / ou de la sortie.)
Aaron Bertrand

Réponses:


15

Une façon serait de créer une procédure système dans master, puis de créer un wrapper dans votre base de données de maintenance. Notez que cela ne fonctionnera que pour une base de données à la fois.

Tout d'abord, 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

Maintenant, dans votre base de données de maintenance, créez un wrapper qui utilise SQL dynamique pour définir correctement le contexte:

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 raison pour laquelle le nom de la base de données ne peut pas vraiment être NULLparce que vous ne pouvez pas vous associer à des choses comme sys.objectset sys.indexespuisqu'elles existent indépendamment dans chaque base de données. Donc peut-être avoir une procédure différente si vous voulez des informations à l'échelle de l'instance.)

Vous pouvez maintenant l'appeler pour n'importe quelle autre base de données, par exemple

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

Et vous pouvez toujours créer un synonymdans chaque base de données afin que vous n'ayez même pas à référencer le nom de la base de données de maintenance:

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

Une autre façon serait d'utiliser SQL dynamique, mais cela ne fonctionnera que pour une base de données à la fois:

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

Encore une autre façon serait de créer une vue (ou une fonction table) pour réunir les noms de table et d'index de toutes vos bases de données, mais vous devrez coder en dur les noms de base de données dans la vue et les conserver pendant que vous ajoutez / supprimer les bases de données que vous souhaitez autoriser à inclure dans cette requête. Contrairement aux autres, cela vous permettrait de récupérer les statistiques de plusieurs bases de données à la fois.

Tout d'abord, la vue:

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

Puis la procédure:

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

Eh bien, il y a de mauvaises nouvelles, de bonnes nouvelles avec un hic et quelques très bonnes nouvelles.

Les mauvaises nouvelles

Les objets T-SQL s'exécutent dans la base de données où ils résident. Il y a deux exceptions (pas très utiles):

  1. procédures stockées avec des noms préfixés sp_et qui existent dans la [master]base de données (pas une excellente option: une base de données à la fois, en ajoutant quelque chose [master], éventuellement en ajoutant des synonymes à chaque base de données, ce qui doit être fait pour chaque nouvelle base de données)
  2. procédures stockées temporaires - locales et globales (pas une option pratique car elles doivent être créées à chaque fois et vous laisser avec les mêmes problèmes que vous avez avec le sp_proc stocké [master].

La bonne nouvelle (avec un hic)

Beaucoup (peut-être la plupart?) Connaissent les fonctions intégrées pour obtenir des métadonnées vraiment communes:

L'utilisation de ces fonctions peut éliminer le besoin pour les JOIN sys.databases(bien que celui-ci ne soit pas vraiment un problème), sys.objects(préféré à sys.tablesce qui exclut les vues indexées) et sys.schemas(vous manquiez celui-ci, et tout n'est pas dans le dboschéma ;-). Mais même en supprimant trois des quatre JOIN, nous sommes toujours fonctionnellement au même endroit, non? Faux-o!

L'une des caractéristiques intéressantes des fonctions OBJECT_NAME()et OBJECT_SCHEMA_NAME()est qu'elles ont un deuxième paramètre facultatif pour @database_id. Cela signifie que, même si JOINDRE à ces tables (à l'exception de sys.databases) est spécifique à la base de données, l'utilisation de ces fonctions vous permet d'obtenir des informations à l'échelle du serveur. Même OBJECT_ID () permet des informations à l'échelle du serveur en lui donnant un nom d'objet complet.

En incorporant ces fonctions de métadonnées dans la requête principale, nous pouvons simplifier tout en élargissant au-delà de la base de données actuelle. La première passe de refactorisation de la requête nous donne:

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;

Et maintenant pour le "catch": il n'y a pas de fonction de métadonnées pour obtenir les noms d'index, et encore moins un nom à l'échelle du serveur. Alors, c'est ça? Sommes-nous à 90% terminés et toujours bloqués devant être dans une base de données particulière pour obtenir des sys.indexesdonnées? Avons-nous vraiment besoin de créer une procédure stockée pour utiliser Dynamic SQL pour remplir, chaque fois que notre proc principal s'exécute, une table temporaire de toutes les sys.indexesentrées dans toutes les bases de données afin que nous puissions nous y joindre? NON!

La très bonne nouvelle

Ainsi vient une petite fonctionnalité que certaines personnes aiment détester, mais lorsqu'elle est utilisée correctement, elle peut faire des choses incroyables. Oui: SQLCLR. Pourquoi? Parce que les fonctions SQLCLR peuvent évidemment soumettre des instructions SQL, mais par la nature même de la soumission à partir du code d'application, il s'agit de Dynamic SQL. Contrairement aux fonctions T-SQL, les fonctions SQLCLR peuvent injecter un nom de base de données dans la requête avant de l'exécuter. Ce qui signifie, nous pouvons créer notre propre fonction pour refléter la capacité OBJECT_NAME()et OBJECT_SCHEMA_NAME()de prendre database_idet obtenir les informations de cette base de données.

Le code suivant est cette fonction. Mais il faut un nom de base de données au lieu d'un ID pour qu'il n'a pas besoin de faire l'étape supplémentaire de la recherche (ce qui le rend un peu moins compliqué et un peu plus rapide).

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 vous le constatez, nous utilisons la connexion contextuelle, qui est non seulement rapide, mais fonctionne également dans les SAFEassemblages. Oui, cela fonctionne dans une assemblée marquée commeSAFE, donc il (ou des variantes de celui-ci) devrait même fonctionner sur Azure SQL Database V12 (la prise en charge de SQLCLR a été supprimée, plutôt brutalement, d'Azure SQL Database en avril 2016) .

Notre refactorisation de deuxième passe de la requête principale nous donne donc les informations suivantes:

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;

C'est ça! Cette UDF scalaire SQLCLR et votre procédure stockée T-SQL de maintenance peuvent vivre dans la même [maintenance]base de données centralisée . ET, vous n'avez pas besoin de traiter une base de données à la fois; vous disposez maintenant de fonctions de métadonnées pour toutes les informations dépendantes qui concernent tout le serveur.

PS Il n'y a pas de .IsNullvérification des paramètres d'entrée dans le code C # car l'objet wrapper T-SQL doit être créé avec l' WITH RETURNS NULL ON NULL INPUToption:

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

Notes complémentaires:

  • La méthode décrite ici peut également être utilisée pour résoudre d'autres problèmes très similaires de fonctions de métadonnées inter-bases de données manquantes. La suggestion Microsoft Connect suivante est un exemple d'un tel cas. Et, voyant que Microsoft l'a fermé comme "Won't Fix", il est clair qu'ils ne sont pas intéressés à fournir des fonctions intégrées comme OBJECT_NAME()pour répondre à ce besoin (d'où la solution de contournement publiée sur cette suggestion :-).

    Ajouter une fonction de métadonnées pour obtenir le nom de l'objet à partir de hobt_id

  • Pour en savoir plus sur l'utilisation de SQLCLR, veuillez consulter la série Stairway to SQLCLR que j'écris sur SQL Server Central (une inscription gratuite est requise; désolé, je ne contrôle pas les politiques de ce site).

  • La IndexName()fonction SQLCLR illustrée ci-dessus est disponible, précompilée, dans un script facile à installer sur Pastebin. Le script active la fonction "Intégration CLR" si elle n'est pas déjà activée et l'assembly est marqué comme SAFE. Il est compilé avec .NET Framework version 2.0 afin qu'il fonctionne dans SQL Server 2005 et plus récent (c'est-à-dire toutes les versions qui prennent en charge SQLCLR).

    Fonction de métadonnées SQLCLR pour la base de données croisée IndexName ()

  • Si quelqu'un s'intéresse à la IndexName()fonction SQLCLR et à plus de 320 autres fonctions et procédures stockées, elle est disponible dans la bibliothèque SQL # (dont je suis l'auteur). Veuillez noter que bien qu'il existe une version gratuite, la fonction Sys_IndexName est uniquement disponible dans la version complète (avec une fonction Sys_AssemblyName similaire ).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.