Centralna procedura składowana do wykonania w kontekście wywołania bazy danych

17

Pracuję nad niestandardowym rozwiązaniem konserwacyjnym, korzystając z sys.dm_db_index_physical_statswidoku. Obecnie odwołuje się do niego z procedury przechowywanej. Teraz, gdy ta procedura przechowywana działa na jednej z moich baz danych, robi to, co chcę, i pobiera listę wszystkich rekordów dotyczących dowolnej bazy danych. Kiedy umieszczam go w innej bazie danych, wyświetla listę wszystkich rekordów odnoszących się tylko do tej bazy danych.

Na przykład (kod na dole):

  • Zapytanie uruchomione dla bazy danych 6 pokazuje [wymagane] informacje dla baz danych 1-10.
  • Zapytanie uruchomione dla bazy danych 3 pokazuje [wymagane] informacje tylko dla bazy danych 3.

Powodem, dla którego chcę tej procedury specjalnie dla bazy danych 3, jest to, że wolałbym przechowywać wszystkie obiekty konserwacji w tej samej bazie danych. Chciałbym, aby to zadanie znajdowało się w bazie danych konserwacji i działało tak, jakby było w tej bazie danych aplikacji.

Kod:

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 Wacławski
źródło
4
@JachachimIsaksson wydaje się, że pytanie brzmi: jak mieć jedną kopię procedury w ich bazie danych obsługi, która odwołuje się do DMV w innych bazach danych, zamiast konieczności umieszczania kopii procedury w każdej bazie danych.
Aaron Bertrand
Przepraszam, nie byłem bardziej wyraźny, gapiłem się na to przez kilka dni. Aaron jest na miejscu. Chcę, aby ten dodatek SP znajdował się w mojej bazie danych konserwacji z możliwością pobierania danych z całego serwera. W tej chwili, gdy znajduje się on w mojej bazie danych konserwacji, pobiera tylko fragmentację danych dotyczących samej bazy danych konserwacji. Nie jestem pewien, dlaczego, kiedy umieszczam ten sam SP w innej bazie danych i wykonuję go identycznie, czy pobiera on fragmentację danych z całego serwera? Czy istnieje ustawienie lub uprawnienie, które należy zmienić, aby ten dodatek SP działał jako taki z poziomu bazy danych konserwacji?
(Zauważ, że twoje obecne podejście ignoruje fakt, że mogą istnieć dwie tabele o tej samej nazwie pod dwoma różnymi schematami - oprócz sugestii w mojej odpowiedzi możesz rozważyć nazwę schematu jako część danych wejściowych i / lub wyjściowych.)
Aaron Bertrand

Odpowiedzi:

15

Jednym ze sposobów może być wykonanie procedury systemowej, mastera następnie utworzenie opakowania w bazie danych konserwacji. Pamiętaj, że będzie to działać tylko dla jednej bazy danych na raz.

Po pierwsze, w mistrzu:

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

Teraz w bazie danych konserwacji utwórz opakowanie, które używa dynamicznego SQL do prawidłowego ustawienia kontekstu:

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

(Powodem, dla którego nazwa bazy danych tak naprawdę nie może być, NULLjest to, że nie można dołączyć do rzeczy takich jak sys.objectsi sys.indexesponieważ istnieją one niezależnie w każdej bazie danych. Być może więc zastosuj inną procedurę, jeśli chcesz uzyskać informacje dotyczące całej instancji).

Teraz możesz to nazwać dla dowolnej innej bazy danych, np

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

I zawsze możesz utworzyć synonymw każdej bazie danych, więc nie musisz nawet odwoływać się do nazwy bazy danych konserwacji:

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

Innym sposobem byłoby użycie dynamicznego SQL, jednak to również zadziała tylko dla jednej bazy danych na raz:

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

Jeszcze innym sposobem byłoby utworzenie widoku (lub funkcji o wartościach przechowywanych w tabeli), aby połączyć nazwy tabel i indeksów wszystkich baz danych, jednak trzeba by na stałe zakodować nazwy baz danych w widoku i zachować je podczas dodawania / usuń bazy danych, które chcesz uwzględnić w tym zapytaniu. Umożliwiłoby to, w przeciwieństwie do innych, pobieranie statystyk dla wielu baz danych jednocześnie.

Po pierwsze widok:

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

Następnie procedura:

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
źródło
15

Cóż, są złe wieści, dobre wieści z haczykiem i kilka naprawdę dobrych wiadomości.

Złe wiadomości

Obiekty T-SQL są wykonywane w bazie danych, w której się znajdują. Istnieją dwa (niezbyt przydatne) wyjątki:

  1. procedury składowane z nazwami z prefiksem sp_i istniejącymi w [master]bazie danych (niezbyt dobra opcja: jedna baza danych na raz, dodawanie czegoś do [master], ewentualnie dodawanie synonimów do każdej bazy danych, co należy zrobić dla każdej nowej bazy danych)
  2. tymczasowe procedury składowane - lokalne i globalne (nie jest to praktyczna opcja, ponieważ muszą być tworzone za każdym razem i pozostawiają ci te same problemy, co problemy z sp_przechowywanym proc [master].

Dobra wiadomość (z haczykiem)

Wielu (być może większość?) Ludzi jest świadomych wbudowanych funkcji, aby uzyskać naprawdę popularne metadane:

Korzystanie z tych funkcji może wyeliminować potrzebę JOIN sys.databases(choć ten nie jest tak naprawdę problemem), sys.objects(preferowane w porównaniu z sys.tableswykluczeniem widoków indeksowanych) i sys.schemas(brakowało tego i nie wszystko jest w dboschemacie ;-). Ale nawet po usunięciu trzech z czterech JOINÓW, wciąż funkcjonalnie jesteśmy w tym samym miejscu, prawda? Źle-o!

Jedną z ciekawych funkcji OBJECT_NAME()i OBJECT_SCHEMA_NAME()jest to, że mają opcjonalny drugi parametr dla @database_id. Oznacza to, że chociaż DOŁĄCZENIE do tych tabel (z wyjątkiem sys.databases) jest specyficzne dla bazy danych, użycie tych funkcji pozwala uzyskać informacje o całym serwerze. Nawet OBJECT_ID () pozwala na uzyskanie informacji o całym serwerze, nadając mu w pełni kwalifikowaną nazwę obiektu.

Włączając te funkcje metadanych do głównego zapytania, możemy uprościć, jednocześnie rozszerzając zakres poza bieżącą bazę danych. Pierwszy przebieg refaktoryzacji zapytania daje nam:

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;

A teraz „złapanie”: nie ma funkcji metadanych, aby uzyskać nazwy indeksu, nie mówiąc już o nazwie obejmującej cały serwer. Więc to o to chodzi? Czy jesteśmy w 90% kompletni i nadal utknęliśmy, że musimy znajdować się w określonych bazach danych, aby uzyskać sys.indexesdane? Czy naprawdę musimy utworzyć procedurę składowaną, aby użyć dynamicznego SQL do zapełniania, za każdym razem, gdy uruchamia się nasz główny proc, tabelę temp wszystkich sys.indexeswpisów we wszystkich bazach danych, abyśmy mogli do niej dołączyć? NIE!

Naprawdę dobre wieści

Tak więc pojawia się mała cecha, której niektórzy uwielbiają nienawidzić, ale przy odpowiednim stosowaniu może robić niesamowite rzeczy. Tak: SQLCLR. Dlaczego? Ponieważ funkcje SQLCLR mogą oczywiście przesyłać instrukcje SQL, ale z samej natury przesyłania z kodu aplikacji jest to dynamiczny SQL. W przeciwieństwie do funkcji T-SQL, funkcje SQLCLR mogą wstrzyknąć nazwę bazy danych do zapytania przed jego wykonaniem. Czyli, możemy stworzyć własną funkcję lusterka zdolności OBJECT_NAME()i OBJECT_SCHEMA_NAME()wziąć database_idi uzyskać informacje o tej bazie danych.

Poniższy kod jest tą funkcją. Ale wymaga nazwy bazy danych zamiast identyfikatora, aby nie musiał wykonywać dodatkowego kroku w celu jej wyszukania (co czyni ją nieco mniej skomplikowaną i trochę szybszą).

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

Jeśli zauważysz, używamy połączenia kontekstowego, które jest nie tylko szybkie, ale działa również w SAFEzłożeniach. Tak, działa to w Zgromadzeniu oznaczonym jakoSAFE, więc to (lub jego odmiany) powinno nawet działać w bazie danych Azure SQL Database 12 (obsługa SQLCLR została usunięta dość gwałtownie z bazy danych SQL Azure w kwietniu 2016 r . ) .

Zatem nasze refaktoryzowanie drugiego zapytania do głównego zapytania daje nam następujące informacje:

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;

Otóż ​​to! Zarówno SQLCLR Scalar UDF, jak i twoja procedura przechowywana T-SQL mogą żyć w tej samej scentralizowanej [maintenance]bazie danych. I nie musisz przetwarzać jednej bazy danych na raz; teraz masz funkcje metadanych dla wszystkich zależnych informacji obejmujących cały serwer.

PS Nie ma .IsNullsprawdzania parametrów wejściowych w kodzie C #, ponieważ obiekt opakowania T-SQL powinien zostać utworzony z WITH RETURNS NULL ON NULL INPUTopcją:

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

Dodatkowe uwagi:

  • Opisaną tutaj metodę można również wykorzystać do rozwiązania innych, bardzo podobnych problemów związanych z brakującymi funkcjami metadanych między bazami danych. Następująca sugestia Microsoft Connect jest przykładem takiego przypadku. I widząc, że Microsoft zamknął go jako „Nie naprawi”, jasne jest, że nie są zainteresowani udostępnianiem wbudowanych funkcji, takich jak OBJECT_NAME()zaspokojenie tej potrzeby (stąd obejście, które jest zamieszczone w tej Sugestii :-).

    Dodaj funkcję metadanych, aby uzyskać nazwę obiektu z hobt_id

  • Aby dowiedzieć się więcej o korzystaniu z SQLCLR, zapoznaj się z serią „ Schody do SQLCLR ”, którą piszę w SQL Server Central (wymagana jest darmowa rejestracja; przepraszam, nie kontroluję zasad tej witryny).

  • Przedstawiona IndexName()powyżej funkcja SQLCLR jest dostępna, wstępnie skompilowana, w łatwym do zainstalowania skrypcie na Pastebin. Skrypt włącza funkcję „CLR Integration”, jeśli nie jest jeszcze włączona, a zespół jest oznaczony jako SAFE. Jest kompilowany z wersją .NET Framework w wersji 2.0, dzięki czemu będzie działał w SQL Server 2005 i nowszych (tj. We wszystkich wersjach obsługujących SQLCLR).

    SQLCLR Funkcja metadanych dla bazy danych IndexName ()

  • Jeśli ktoś jest zainteresowany IndexName()funkcją SQLCLR i ponad 320 innymi funkcjami i procedurami przechowywanymi, jest on dostępny w bibliotece SQL # (której jestem autorem). Należy pamiętać, że chociaż istnieje wersja darmowa, funkcja Sys_IndexName jest dostępna tylko w pełnej wersji (wraz z podobną funkcją Sys_AssemblyName ).

Solomon Rutzky
źródło