Zentrale gespeicherte Prozedur, die im aufrufenden Datenbankkontext ausgeführt werden soll

17

Ich arbeite an einer maßgeschneiderten Wartungslösung unter Verwendung der sys.dm_db_index_physical_statsAnsicht. Ich habe es derzeit von einer gespeicherten Prozedur verwiesen. Wenn diese gespeicherte Prozedur nun in einer meiner Datenbanken ausgeführt wird, wird ausgeführt, was ich möchte, und eine Liste aller Datensätze zu einer Datenbank wird abgerufen. Wenn ich es in eine andere Datenbank lege, ruft es eine Liste aller Datensätze auf, die sich nur auf diese Datenbank beziehen.

Zum Beispiel (Code unten):

  • Der Abfragelauf für Datenbank 6 zeigt [angeforderte] Informationen für die Datenbanken 1-10 an.
  • Der Abfragelauf für Datenbank 3 zeigt [angeforderte] Informationen nur für Datenbank 3 an.

Der Grund, warum ich diese Prozedur speziell für Datenbank drei möchte, ist, dass ich es vorziehen würde, alle Wartungsobjekte in derselben Datenbank zu belassen. Ich möchte diesen Job in der Wartungsdatenbank haben und so arbeiten, als ob er in dieser Anwendungsdatenbank wäre.

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
Josh Waclawski
quelle
4
@JoachimIsaksson scheint die Frage zu sein, wie eine einzige Kopie der Prozedur in ihrer Wartungsdatenbank gespeichert werden soll, die auf die DMV in anderen Datenbanken verweist, anstatt dass in jeder Datenbank eine Kopie der Prozedur abgelegt werden muss.
Aaron Bertrand
Tut mir leid, dass ich nicht klarer war und ein paar Tage lang darauf gestarrt habe. Aaron ist genau richtig. Ich möchte, dass dieser SP in meiner Wartungsdatenbank gespeichert wird und Daten von überall auf dem Server abgerufen werden können. So wie es aussieht, werden in meiner Wartungsdatenbank nur Fragmentierungsdaten über die Wartungsdatenbank selbst abgerufen. Was ich verwirrt bin, ist, warum, wenn ich genau den gleichen SP in eine andere Datenbank lege und ihn identisch ausführe, es Fragmentierungsdaten über den Server zieht? Gibt es eine Einstellung oder Berechtigung, die geändert werden muss, damit dieser SP als solcher in der Wartungsdatenbank ausgeführt werden kann?
(Beachten Sie, dass Ihr derzeitiger Ansatz ignoriert die Tatsache , dass es zwei Tabellen mit demselben Namen unter zwei verschiedenen Schemata sein könnte -. Zusätzlich zu den Vorschlägen in meiner Antwort Sie können Schemanamen als Teil des Ein- und / oder Ausgangs zu berücksichtigen)
Aaron Bertrand

Antworten:

15

Eine Möglichkeit wäre, eine Systemprozedur masterin Ihre Wartungsdatenbank einzufügen und anschließend einen Wrapper zu erstellen. Beachten Sie, dass dies jeweils nur für eine Datenbank funktioniert.

Erstens im Meister:

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

Erstellen Sie nun in Ihrer Wartungsdatenbank einen Wrapper, der dynamisches SQL verwendet, um den Kontext korrekt festzulegen:

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

(Der Grund, warum der Datenbankname nicht wirklich sein kann, NULLliegt darin, dass Sie sich nicht mit Dingen wie sys.objectsund verknüpfen können, sys.indexesda sie unabhängig in jeder Datenbank existieren. Wenn Sie instanzweite Informationen wünschen, müssen Sie möglicherweise eine andere Prozedur anwenden.)

Jetzt können Sie dies für jede andere Datenbank aufrufen, z

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

Und Sie können jederzeit eine synonymin jeder Datenbank erstellen, sodass Sie nicht einmal auf den Namen der Wartungsdatenbank verweisen müssen:

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

Eine andere Möglichkeit wäre, dynamisches SQL zu verwenden. Auch dies funktioniert jedoch jeweils nur für eine Datenbank:

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

Eine weitere Möglichkeit besteht darin, eine Ansicht (oder eine Tabellenwertfunktion) zu erstellen, um die Tabellen- und Indexnamen aller Ihrer Datenbanken zu vereinen. Sie müssen jedoch die Datenbanknamen in der Ansicht fest codieren und beim Hinzufügen beibehalten / remove Datenbanken, die in diese Abfrage aufgenommen werden sollen. Im Gegensatz zu den anderen können Sie so Statistiken für mehrere Datenbanken gleichzeitig abrufen.

Erstens ist die Ansicht:

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

Dann die Prozedur:

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

Nun, es gibt schlechte Nachrichten, gute Nachrichten mit einem Haken und einige wirklich gute Nachrichten.

Die schlechten Nachrichten

T-SQL-Objekte werden in der Datenbank ausgeführt, in der sie sich befinden. Es gibt zwei (nicht sehr nützliche) Ausnahmen:

  1. gespeicherte Prozeduren mit Namen, denen ein Präfix vorangestellt ist sp_und die in der [master]Datenbank vorhanden sind (keine gute Option: jeweils eine Datenbank, die etwas hinzufügt [master], möglicherweise Synonyme zu jeder Datenbank hinzufügt, was für jede neue Datenbank durchgeführt werden muss)
  2. Temporär gespeicherte Prozeduren - lokal und global (keine praktikable Option, da sie jedes Mal erstellt werden müssen und Sie dieselben Probleme haben wie beim sp_gespeicherten Prozess in [master].

Die gute Nachricht (mit einem Haken)

Viele (vielleicht die meisten?) Leute kennen die eingebauten Funktionen, um einige wirklich häufige Metadaten zu erhalten:

Die Verwendung dieser Funktionen kann die Notwendigkeit für die JOINs eliminieren sys.databases(obwohl dies kein wirkliches Problem ist), sys.objects(gegenüber sys.tablesdenen, die indizierte Ansichten ausschließen , bevorzugt ) und sys.schemas(Sie haben diese Funktion vermisst, und nicht alles ist im dboSchema enthalten ;-). Aber auch wenn wir drei der vier JOINs entfernen, sind wir funktionell immer noch am selben Ort, oder? Falsch!

Eine der netten Eigenschaften der OBJECT_NAME()und OBJECT_SCHEMA_NAME()Funktionen ist, dass sie einen optionalen zweiten Parameter für haben @database_id. Das heißt, während der Beitritt zu diesen Tabellen (mit Ausnahme von sys.databases) datenbankspezifisch ist, erhalten Sie mit diesen Funktionen serverweite Informationen. Sogar OBJECT_ID () ermöglicht serverweite Informationen, indem es einen vollständig qualifizierten Objektnamen erhält .

Indem wir diese Metadatenfunktionen in die Hauptabfrage integrieren, können wir sie vereinfachen und gleichzeitig über die aktuelle Datenbank hinaus erweitern. Der erste Durchgang des Refactorings der Abfrage ergibt Folgendes:

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;

Und jetzt zum "catch": Es gibt keine Metadatenfunktion, um Indexnamen zu erhalten, geschweige denn eine serverweite. Also ist es das? Sind wir zu 90% vollständig und müssen uns immer noch in einer bestimmten Datenbank befinden, um sys.indexesDaten abzurufen? Müssen wir wirklich eine gespeicherte Prozedur erstellen, um mithilfe von Dynamic SQL bei jeder Ausführung unseres Hauptprozesses eine temporäre Tabelle aller sys.indexesEinträge in allen Datenbanken aufzufüllen, damit wir der Prozedur beitreten können? NEIN!

Die wirklich guten Nachrichten

Ein kleines Feature, das manche Leute gerne hassen, aber bei richtiger Anwendung erstaunliche Dinge bewirken können. Ja: SQLCLR. Warum? Da SQLCLR-Funktionen offensichtlich SQL-Anweisungen übermitteln können, es sich jedoch aufgrund der Art der Übermittlung aus dem Anwendungscode um dynamisches SQL handelt. Im Gegensatz zu T-SQL-Funktionen können SQLCLR-Funktionen einen Datenbanknamen in die Abfrage einfügen, bevor sie ausgeführt werden. Das heißt, wir können unsere eigene Funktion erstellen, um die Fähigkeit von OBJECT_NAME()und OBJECT_SCHEMA_NAME()zu übernehmen database_idund die Informationen für diese Datenbank abzurufen.

Der folgende Code ist diese Funktion. Es wird jedoch ein Datenbankname anstelle einer ID verwendet, damit der zusätzliche Schritt zum Nachschlagen nicht erforderlich ist (wodurch die Suche etwas weniger kompliziert und etwas schneller wird).

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

Sie werden feststellen, dass wir die Kontextverbindung verwenden, die nicht nur schnell ist, sondern auch in SAFEBaugruppen funktioniert . Ja, dies funktioniert in einer Assembly, die als markiert istSAFEDaher sollte es (oder Abwandlungen davon) auch in Azure SQL Database V12 funktionieren (Die Unterstützung für SQLCLR wurde im April 2016 ziemlich plötzlich aus der Azure SQL-Datenbank entfernt .)

Unser zweites Refactoring der Hauptabfrage ergibt also Folgendes:

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;

Das ist es! Sowohl diese SQLCLR Scalar-UDF als auch Ihre gespeicherte T-SQL-Wartungsprozedur können in derselben zentralen [maintenance]Datenbank gespeichert werden. UND Sie müssen nicht immer nur eine Datenbank gleichzeitig verarbeiten. Jetzt haben Sie Metadaten-Funktionen für alle abhängigen Informationen, die serverweit sind.

PS: Es gibt keine .IsNullÜberprüfung der Eingabeparameter im C # -Code, da das T-SQL-Wrapperobjekt mit der folgenden WITH RETURNS NULL ON NULL INPUTOption erstellt werden sollte:

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

Zusätzliche Bemerkungen:

  • Die hier beschriebene Methode kann auch zur Lösung anderer, sehr ähnlicher Probleme bei fehlenden datenbankübergreifenden Metadatenfunktionen verwendet werden. Der folgende Microsoft Connect-Vorschlag ist ein Beispiel für einen solchen Fall. Und da Microsoft es als "Won't Fix" geschlossen hat, ist es offensichtlich, dass sie nicht daran interessiert sind, integrierte Funktionen OBJECT_NAME()bereitzustellen, die diese Anforderungen erfüllen (daher die Problemumgehung, die in diesem Vorschlag angegeben ist :-).

    Fügen Sie eine Metadatenfunktion hinzu, um den Objektnamen von hobt_id abzurufen

  • Um mehr über die Verwendung von SQLCLR zu erfahren, werfen Sie einen Blick auf die Treppen zu SQLCLR- Reihe, die ich in SQL Server Central schreibe (kostenlose Registrierung erforderlich; ich habe leider keine Kontrolle über die Richtlinien dieser Site).

  • Die IndexName()oben gezeigte SQLCLR-Funktion ist in einem einfach zu installierenden Skript in Pastebin vorkompiliert verfügbar. Das Skript aktiviert die Funktion "CLR-Integration", sofern sie nicht bereits aktiviert ist, und die Assembly wird als markiert SAFE. Es wurde mit .NET Framework Version 2.0 kompiliert, sodass es in SQL Server 2005 und neueren Versionen (dh allen Versionen, die SQLCLR unterstützen) funktioniert.

    SQLCLR Metadatenfunktion für datenbankübergreifenden IndexName ()

  • Wenn sich jemand für die IndexName()SQLCLR-Funktion und über 320 weitere Funktionen und gespeicherte Prozeduren interessiert , ist sie in der SQL # -Bibliothek (deren Autor ich bin) verfügbar . Beachten Sie, dass die Funktion Sys_IndexName , solange es eine kostenlose Version gibt, nur in der Vollversion verfügbar ist (zusammen mit einer ähnlichen Funktion Sys_AssemblyName ).

Solomon Rutzky
quelle