web-dev-qa-db-ja.com

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

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
17
Josh Waclawski

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;

もう1つの方法は動的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

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

まず、ビュー:

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
Aaron Bertrand

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

悪いニュース

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

  1. sp_で始まる名前が付けられ、[master]データベースに存在するストアドプロシージャ(優れたオプションではありません。一度に1つのDBを[master]に追加し、各DBにシノニムを追加します。新しいDBごとに実行する必要があります)
  2. 一時ストアドプロシージャ-ローカルおよびグローバル(毎回作成する必要があり、sp_[master]ストアドプロシージャと同じ問題が発生するため、実用的なオプションではありません。

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

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

これらの関数を使用すると、sys.databases(これは実際には問題ではありません)、sys.objects(インデックス付きビューを除外するsys.tablesよりも優先されます)、およびsys.schemas(不足しているため、dboにすべてではない)へのJOINの必要性を排除できます。スキーマ;-)。しかし、4つのJOINのうち3つを削除しても、機能的には同じ場所です。違います!

OBJECT_NAME()関数とOBJECT_SCHEMA_NAME()関数の優れた機能の1つは、@database_idのオプションの2番目のパラメーターがあることです。つまり、これらのテーブルへのJOIN処理(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;

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

本当に良いニュース

そのため、嫌いな人もいる小さな機能がありますが、適切に使用すると、驚くべきことができるようになります。はい、SQLCLR。どうして? SQLCLR関数は明らかにSQLステートメントを送信できますが、アプリコードから送信するという性質上、is動的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つのデータベースを処理する必要はありません。これで、サーバー全体のすべての依存情報のメタデータ関数ができました。

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

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がそれを「修正しない」として閉じていることを見て、このニーズを満たすためにOBJECT_NAME()などの組み込み関数を提供することに関心がないことは明らかです(そのため、回避策が投稿されています)提案:-)。

    hobt_idからオブジェクト名を取得するためのメタデータ関数を追加

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

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

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

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

15
Solomon Rutzky