呼び出し側データベースコンテキストで実行する中央ストアドプロシージャ


17

sys.dm_db_index_physical_statsビューを使用して、カスタマイズされたメンテナンスソリューションに取り組んでいます。現在、ストアドプロシージャから参照されています。これで、そのストアドプロシージャがデータベースの1つで実行されると、必要な処理が実行され、データベースに関するすべてのレコードのリストがプルダウンされます。別のデータベースに配置すると、そのDBのみに関連するすべてのレコードのリストがプルダウンされます。

例(下部のコード):

  • データベース6に対して実行されたクエリは、データベース1〜10の[要求された]情報を示します。
  • データベース3に対して実行されたクエリは、データベース3のみの[要求された]情報を表示します。

特にデータベース3でこの手順が必要な理由は、同じデータベース内にすべてのメンテナンスオブジェクトを保持したいからです。このジョブをメンテナンスデータベースに配置し、あたかもそのアプリケーションデータベースにあるかのように動作させたいと思います。

コード:

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は、各データベースにプロシージャのコピーを配置するのではなく、他のデータベースのDMVを参照するメンテナンスデータベースにプロシージャのコピーを1つだけ保持する方法が問題のようです。
アーロンバートランド

申し訳ありませんが、私はこれ以上明確ではなく、数日間これを見つめていました。アーロンがスポットです。このSPをメンテナンスデータベースに配置して、サーバー全体からデータを取得できるようにします。現状では、メンテナンスDBにあるときは、メンテナンスDB自体に関する断片化データのみを取り込みます。私が混乱しているのは、まったく同じSPを別のデータベースに配置して同じように実行すると、サーバー全体から断片化データがプルされるのはなぜですか?このSPがメンテナンスDBからそのように動作するために変更する必要がある設定または特権はありますか?

(。 -私の答えで提案に加えて、あなたは、入力および/または出力の一部としてスキーマ名を検討する必要がありますあなたの現在のアプローチは、2つの異なるスキーマの下で同じ名前を持つ2つのテーブルがあるかもしれないという事実を無視することに注意してください)
アーロンバートランド

回答:


15

1つの方法は、システムプロシージャをmaster作成し、メンテナンスデータベースにラッパーを作成することです。これは一度に1つのデータベースに対してのみ機能することに注意してください。

まず、マスターで:

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

次に、メンテナンスデータベースで、動的SQLを使用してコンテキストを正しく設定するラッパーを作成します。

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

(データベース名が実際にすることはできません理由はNULL、あなたがのようなものに参加することができないためであるsys.objectssys.indexes彼らはそれぞれのデータベースに独立して存在して以来。あなたは、インスタンス全体の情報をしたいのであれば、おそらく別の手順があります。)

これで、他のデータベースに対してこれを呼び出すことができます。例えば

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

またsynonym、各データベースにいつでもを作成できるため、メンテナンスデータベースの名前を参照する必要さえありません。

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

別の方法は動的SQLを使用することですが、これも一度に1つのデータベースに対してのみ機能します。

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

さらに別の方法は、すべてのデータベースのテーブル名とインデックス名を結合するビュー(またはテーブル値関数)を作成することですが、データベース名をビューにハードコーディングし、追加するときにそれらを維持する必要がありますこのクエリに含めることを許可するデータベースを削除します。これにより、他とは異なり、複数のデータベースの統計を一度に取得できます。

まず、ビュー:

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

次に手順:

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

さて、悪いニュース、キャッチ付きの良いニュース、そしていくつかの本当に良いニュースがあります。

悪いニュース

T-SQLオブジェクトは、それらが存在するデータベースで実行されます。2つの例外があります(あまり有用ではありません):

  1. 接頭辞が付けsp_られ、[master]データベースに存在する名前を持つストアドプロシージャ(素晴らしいオプションではありません:一度に1つのDBに何かを追加し[master]、場合によっては各DBにシノニムを追加します。これは新しいDBごとに行う必要があります)
  2. 一時ストアドプロシージャ-ローカルおよびグローバル(毎回作成する必要があるため、実際のオプションではありません。ストアドプロシージャでの問題と同じ問題がsp_残り[master]ます。

良いニュース(キャッチ付き)

多くの(おそらくほとんどの)人々は、組み込み関数を知っており、いくつかの本当に一般的なメタデータを取得します。

結合する場合の必要性をなくすことができ、これらの機能を使用するsys.databases(この1つは問題が本当にありませんが)、 sys.objects(好まsys.tablessys.schemasます(インデックス付きビューを除外するます)dbo。しかし、4つのJOINのうち3つを削除しても、機能的には同じ場所です。間違った!

OBJECT_NAME()およびOBJECT_SCHEMA_NAME()関数の優れた機能の1つは、のオプションの2番目のパラメーターがあることです@database_id。つまり、これらのテーブル(を除くsys.databases)への結合はデータベース固有ですが、これらの関数を使用するとサーバー全体の情報が取得されます。でもOBJECT_ID()で、完全修飾オブジェクト名を与えることでサーバー全体の情報を許可します。

これらのメタデータ関数をメインクエリに組み込むことにより、現在のデータベースを超えて拡張すると同時に簡素化できます。クエリのリファクタリングの最初のパスは次のとおりです。

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;

そして今、「キャッチ」のために:インデックス名を取得するためのメタデータ関数はありません。サーバー全体の名前は言うまでもありません。それはそれですか?sys.indexesデータを取得するために特定のデータベースにいる必要があるのに、私たちは90%完了していますか?ダイナミックSQLを使用して、メインプロシージャを実行するたびに、sys.indexesすべてのデータベースのすべてのエントリの一時テーブルにデータを追加するために、ストアドプロシージャを作成する必要が本当にありますか?番号!

本当に良いニュース

そのため、一部の人は嫌いなものですが、適切に使用すると驚くべきことができる小さな機能が登場します。はい:SQLCLR。どうして?SQLCLR機能が明らかにSQL文を提出することができますが、アプリのコードから提出の性質上、それがあるためである動的SQL。そのため、T-SQL関数とは異なり、SQLCLR関数はクエリを実行する前にデータベース名をクエリに挿入できます。つまり、私たちは私たち自身の機能の能力を反映するために作成することができますOBJECT_NAME()し、OBJECT_SCHEMA_NAME()取るdatabase_idとそのデータベースの情報を取得します。

次のコードはその関数です。ただし、IDの代わりにデータベース名を使用するため、ルックアップの追加ステップを実行する必要がありません(これにより、少し複雑さが少なくなり、少し速くなります)。

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

お気づきの方は、コンテキスト接続を使用しています。これは高速であるだけでなく、 SAFEアセンブリでます。うん、これはとしてマークされたアセンブリで動作しますSAFE、したがって(またはそのバリエーション)Azure SQL Database V12でも動作するはずです (SQLCLRのサポートは、2016年4月にAzure SQL Databaseからやや突然削除されました)

したがって、メインクエリの2回目のパスリファクタリングにより、次のことがわかります。

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;

それでおしまい!このSQLCLRスカラーUDFとメンテナンスT-SQLストアドプロシージャの両方は、同じ集中型[maintenance]データベースに存在できます。また、一度に1つのデータベースを処理する必要はありません。これで、サーバー全体のすべての依存情報のメタデータ関数を使用できます。

PS次.IsNullWITH RETURNS NULL ON NULL INPUTオプションを使用してT-SQLラッパーオブジェクトを作成する必要があるため、C#コードの入力パラメーターのチェックはありません。

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

その他の注意事項:

  • ここで説明する方法は、クロスデータベースメタデータ関数が見つからないという他の非常に類似した問題を解決するためにも使用できます。次のMicrosoft Connectの提案は、そのようなケースの例です。そして、Microsoftが「Wo n't Fix」としてそれを閉じたことを見ると、OBJECT_NAME()このニーズを満たすような組み込み関数を提供することに興味がないことは明らかです(その提案に掲載されている回避策:-)。

    メタデータ関数を追加して、hobt_idからオブジェクト名を取得します

  • SQLCLRの使用の詳細については、SQLCLR への階段をご覧ください。は、SQL Server Centralで書いシリーズを(無料登録が必要です。申し訳ありませんが、そのサイトのポリシーは管理していません)。

  • 上記のIndexName()SQLCLR関数は、Pastebinにインストールしやすいスクリプトでプリコンパイルされて使用できます。スクリプトは、「CLR統合」機能がまだ有効になっていない場合に有効になり、アセンブリはとしてマークされSAFEます。.NET Frameworkバージョン2.0に対してコンパイルされるため、SQL Server 2005以降(つまり、SQLCLRをサポートするすべてのバージョン)で動作します。

    クロスデータベースIndexName()のSQLCLRメタデータ関数

  • 誰かがIndexName()SQLCLR関数、および 320 超える他の関数とストアドプロシージャに興味がある場合は、SQL#ライブラリ(著者)で利用できます。無料版がありますが、Sys_IndexName関数は完全版でのみ使用できます(同様のSys_AssemblyName関数と一緒に)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.