Procedura memorizzata centrale da eseguire nel contesto del database chiamante


17

Sto lavorando a una soluzione di manutenzione personalizzata utilizzando la sys.dm_db_index_physical_statsvista. Attualmente ho fatto riferimento a una stored procedure. Ora, quando quella procedura memorizzata viene eseguita su uno dei miei database, fa quello che voglio e fa un elenco di tutti i record relativi a qualsiasi database. Quando lo inserisco in un database diverso, viene visualizzato un elenco di tutti i record relativi solo a quel DB.

Ad esempio (codice in basso):

  • La query eseguita sul Database 6 mostra le informazioni [richieste] per i database 1-10.
  • La query eseguita sul database 3 mostra le informazioni [richieste] solo per il database 3.

Il motivo per cui voglio questa procedura in particolare sul database tre è perché preferirei mantenere tutti gli oggetti di manutenzione all'interno dello stesso database. Vorrei che questo lavoro si trovasse nel database di manutenzione e funzionasse come se fosse nel database dell'applicazione.

Codice:

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 sembra che la domanda sia come avere una singola copia della procedura nel loro database di manutenzione, che fa riferimento al DMV in altri database, piuttosto che dover mettere una copia della procedura in ciascun database.
Aaron Bertrand

Mi dispiace non sono stato più chiaro, lo sto fissando da alcuni giorni. Aaron è perfetto. Voglio che questo SP si trovi nel mio database di manutenzione con la possibilità di acquisire dati da tutto il server. Allo stato attuale, quando si trova nel mio DB di manutenzione, estrae solo i dati di frammentazione sul DB di manutenzione stesso. Ciò su cui sono confuso è perché, quando inserisco esattamente lo stesso SP in un database diverso e lo eseguo in modo identico, estrae i dati di frammentazione dal server? Esiste un'impostazione o un privilegio che deve essere modificato affinché questo SP funzioni come tale dal DB di manutenzione?

(Nota che il tuo approccio attuale ignora il fatto che potrebbero esserci due tabelle con lo stesso nome in due schemi diversi - oltre ai suggerimenti nella mia risposta potresti voler considerare il nome dello schema come parte dell'input e / o dell'output.)
Aaron Bertrand

Risposte:


15

Un modo sarebbe quello di fare una procedura di sistema in master e quindi creare un wrapper nel database di manutenzione. Si noti che funzionerà solo per un database alla volta.

Innanzitutto, in 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

Ora, nel tuo database di manutenzione, crea un wrapper che utilizza SQL dinamico per impostare correttamente il contesto:

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 ragione per cui il nome del database non può davvero essere NULLè perché non puoi unirti a cose del genere sys.objectse sys.indexespoiché esistono in modo indipendente in ciascun database. Quindi forse hai una procedura diversa se vuoi informazioni a livello di istanza.)

Ora puoi chiamarlo per qualsiasi altro database, ad es

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

E puoi sempre creare un synonymin ogni database in modo da non dover nemmeno fare riferimento al nome del database di manutenzione:

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

Un altro modo sarebbe utilizzare SQL dinamico, tuttavia anche questo funzionerà solo per un database alla volta:

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

Un altro modo sarebbe quello di creare una vista (o una funzione con valori di tabella) per unire i nomi di tabella e indice di tutti i database, tuttavia dovresti codificare i nomi dei database nella vista e mantenerli mentre aggiungi / rimuovere i database che si desidera consentire per essere inclusi in questa query. Ciò, a differenza degli altri, consente di recuperare statistiche per più database contemporaneamente.

Innanzitutto, 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

Quindi la 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

15

Bene, ci sono cattive notizie, buone notizie con un trucco e alcune notizie davvero buone.

Le cattive notizie

Gli oggetti T-SQL vengono eseguiti nel database in cui risiedono. Esistono due (non molto utili) eccezioni:

  1. stored procedure con nomi con prefisso sp_e che esistono nel [master]database (non un'ottima opzione: un DB alla volta, aggiungendo qualcosa a[master] , eventualmente aggiungendo sinonimi a ciascun DB, che deve essere fatto per ogni nuovo DB)
  2. stored procedure temporanee - locali e globali (non un'opzione pratica in quanto devono essere create ogni volta e ti lasciano con gli stessi problemi che hai con il sp_proc memorizzato in [master].

Le buone notizie (con un trucco)

Molte persone (forse la maggior parte?) Sono a conoscenza delle funzioni integrate per ottenere alcuni metadati davvero comuni:

L'uso di queste funzioni può eliminare la necessità dei JOIN sys.databases(sebbene questo non sia realmente un problema), sys.objects(preferito rispetto al sys.tablesquale esclude le viste indicizzate) esys.schemas (ti mancava quello, e non tutto è neldbo schema ;-). Ma anche rimuovendo tre dei quattro JOIN, funzionalmente siamo ancora nello stesso posto, giusto? Sbagliato-o!

Una delle belle caratteristiche delle funzioni OBJECT_NAME()e OBJECT_SCHEMA_NAME()è che hanno un secondo parametro opzionale per @database_id. Significato, mentre l'ACCESSO a quelle tabelle (eccetto sys.databases) è specifico del database, l'utilizzo di queste funzioni fornisce informazioni a livello di server. Anche OBJECT_ID () consente informazioni a livello di server assegnandogli un nome oggetto completo.

Incorporando queste funzioni di metadati nella query principale, possiamo semplificare e allo stesso tempo espanderci oltre il database corrente. Il primo passaggio del refactoring della query ci dà:

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 ora per la "cattura": non esiste una funzione di meta-dati per ottenere i nomi degli indici, per non parlare di uno a livello di server. Quindi è così? Siamo completi al 90% e ancora bloccati e dobbiamo essere in un database particolare per ottenere sys.indexesdati? Abbiamo davvero bisogno di creare una procedura memorizzata per utilizzare Dynamic SQL per popolare, ogni volta che viene eseguito il nostro proc principale, una tabella temporanea di tutte le sys.indexesvoci in tutti i database in modo da poterci unire ad esso? NO!

Le notizie davvero buone

Quindi arriva una piccola caratteristica che alcune persone amano odiare, ma se usato correttamente, può fare alcune cose incredibili. Sì: SQLCLR. Perché? Poiché le funzioni SQLCLR possono ovviamente inviare istruzioni SQL, ma per la natura stessa dell'invio dal codice dell'app, si tratta di Dynamic SQL. Quindi, diversamente dalle funzioni T-SQL, le funzioni SQLCLR possono iniettare un nome di database nella query prima di eseguirlo. Ciò significa che possiamo creare la nostra funzione per rispecchiare la capacità di OBJECT_NAME()e OBJECT_SCHEMA_NAME()prendere un database_ide ottenere le informazioni per quel database.

Il codice seguente è quella funzione. Ma prende un nome di database invece di ID in modo che non debba fare il passo aggiuntivo di cercarlo (il che lo rende un po 'meno complicato e un po' più veloce).

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 noterai, stiamo utilizzando la connessione di contesto, che non è solo veloce, ma funziona anche SAFE assiemi. Sì, funziona in un'Assemblea contrassegnata comeSAFE, quindi (o varianti) dovrebbe funzionare anche con il database SQL di Azure V12 (il supporto per SQLCLR è stato rimosso, piuttosto bruscamente, dal database SQL di Azure ad aprile 2016) .

Quindi il nostro refactoring di secondo passaggio della query principale ci fornisce quanto segue:

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;

Questo è tutto! Sia questo UDF scalare SQLCLR che la Stored procedure T-SQL di manutenzione possono vivere nella stessa centralizzata[maintenance] database . E, non è necessario elaborare un database alla volta; ora hai funzioni di meta-dati per tutte le informazioni dipendenti che sono a livello di server.

PS Non esiste alcun .IsNullcontrollo dei parametri di input nel codice C # poiché l'oggetto wrapper T-SQL deve essere creato con l' WITH RETURNS NULL ON NULL INPUTopzione:

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

Note aggiuntive:

  • Il metodo qui descritto può anche essere usato per risolvere altri problemi molto simili delle funzioni mancanti di metadati tra database. Il seguente suggerimento di Microsoft Connect è un esempio di uno di questi casi. E, visto che Microsoft lo ha chiuso come "Won't Fix", è chiaro che non sono interessati a fornire funzioni integrate come OBJECT_NAME()per soddisfare questa esigenza (da qui la soluzione alternativa pubblicata su quel suggerimento :-).

    Aggiungi la funzione metadati per ottenere il nome dell'oggetto da hobt_id

  • Per ulteriori informazioni sull'uso di SQLCLR, dai un'occhiata alla serie Stairway to SQLCLR che sto scrivendo su SQL Server Central (è richiesta la registrazione gratuita; mi dispiace, non controllo le politiche di quel sito).

  • La IndexName()funzione SQLCLR mostrata sopra è disponibile, precompilata, in uno script di facile installazione su Pastebin. Lo script abilita la funzione "Integrazione CLR" se non è già abilitata e l'Assemblea è contrassegnata come SAFE. È compilato su .NET Framework versione 2.0 in modo che funzioni in SQL Server 2005 e versioni successive (ovvero tutte le versioni che supportano SQLCLR).

    SQLCLR Funzione metadati per IndexName tra database ()

  • Se qualcuno è interessato alla IndexName()funzione SQLCLR e ad oltre 320 altre funzioni e stored procedure, è disponibile nella libreria SQL # (di cui sono l'autore). Si noti che mentre esiste una versione gratuita, la funzione Sys_IndexName è disponibile solo nella versione completa (insieme a una funzione Sys_AssemblyName simile ).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.