데이터베이스 컨텍스트 호출에서 실행할 중앙 저장 프로 시저


17

나는 다음을 사용하여 맞춤형 유지 보수 솔루션을 개발 중입니다. sys.dm_db_index_physical_stats뷰를 입니다. 현재 저장 프로 시저에서 참조하고 있습니다. 이제 저장 프로 시저가 내 데이터베이스 중 하나에서 실행될 때 원하는 작업을 수행하고 데이터베이스와 관련된 모든 레코드 목록을 가져옵니다. 다른 데이터베이스에 배치하면 해당 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를 참조하는 유지 관리 데이터베이스에 하나의 프로 시저 복사본을 갖는 방법이 문제인 것 같습니다.
Aaron Bertrand

더 명확하지 않아 죄송합니다. 며칠 동안 쳐다보고있었습니다. 아론이 현장에 있습니다. 서버 전체에서 데이터를 가져올 수있는 기능으로이 SP를 유지 관리 데이터베이스에 배치하고 싶습니다. 그대로 유지 관리 DB에있을 때 유지 관리 DB 자체에 대한 조각화 데이터 만 가져옵니다. 내가 혼란스럽게하는 이유는이 동일한 SP를 다른 데이터베이스에 배치하고 동일하게 실행할 때 서버 전체에서 조각화 데이터를 가져 오는 이유는 무엇입니까? 이 SP가 유지 관리 DB에서와 같이 작동하도록 변경해야하는 설정 또는 권한이 있습니까?

(현재 접근 방식은 서로 다른 두 스키마 아래에 동일한 이름을 가진 두 개의 테이블이있을 수 있다는 사실을 무시합니다. 내 답변에 대한 제안 외에도 스키마 이름을 입력 및 / 또는 출력의 일부로 고려할 수 있습니다.
Aaron Bertrand

답변:


15

한 가지 방법은 시스템 프로 시저 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

이제 유지 보수 데이터베이스에서 동적 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.objects그리고 sys.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을 사용하는 것이지만 한 번에 하나의 데이터베이스에서만 작동합니다.

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 개체는 데이터베이스가있는 데이터베이스에서 실행됩니다. 두 가지 예외가 있습니다 (별로 유용하지는 않음).

  1. 접두사가 붙은 이름 sp_이 있고 [master]데이터베이스에 존재하는 저장 프로 시저 (좋은 옵션은 아님 : 한 번에 하나의 DB,에 무언가 추가 [master], 각 DB에 동의어 추가, 각 새로운 DB에 대해 수행해야 함)
  2. 임시 저장 프로 시저 - 로컬 및 글로벌 (안 숙소가 각각의 시간을 만들 수 있고 당신이 함께있는 것과 같은 문제에 당신을 떠나지로 sp_에서 저장된 프로 시저 [master].

좋은 소식 (캐치 포함)

많은 (아마도?) 사람들은 실제로 일반적인 메타 데이터를 얻는 내장 함수를 알고 있습니다.

이 함수를 사용하면 sys.databases(이것은 실제로 문제는 아니지만) JOIN이 필요 하지 않고 sys.objects( sys.tables인덱싱 된 뷰를 제외 하는 것이 바람직 합니다) sys.schemas모든 것이 dbo스키마 에 있지는 않지만 그 중 하나가 누락되었습니다 ;-). 그러나 4 개의 JOIN 중 3 개를 제거하더라도 여전히 기능적으로 동일한 위치에 있습니다. 잘못 오!

OBJECT_NAME()OBJECT_SCHEMA_NAME()기능 의 멋진 기능 중 하나 는에 대한 선택적 두 번째 매개 변수가 있다는 것입니다 @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;

이제 "캐치"의 경우 : 서버 전체의 색인은 물론 색인 이름을 가져 오는 메타 데이터 기능이 없습니다. 그래요? 우리는 90 % 완전하고 여전히 데이터를 얻기 위해 특정 데이터베이스에 있어야 sys.indexes합니까? 동적 프로 시저를 사용하여 기본 프로 시저가 실행될 때마다 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에서도 작동해야합니다. (2016 년 4 월 Azure SQL Database에서 SQLCLR에 대한 지원이 다소 갑자기 제거되었습니다) .

따라서 기본 쿼리의 두 번째 패스 리팩토링은 다음을 제공합니다.

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 Scalar UDF와 유지 관리 T-SQL 저장 프로시 저는 모두 동일한 중앙 집중식 [maintenance]데이터베이스에있을 수 있습니다. 그리고 한 번에 하나의 데이터베이스를 처리 할 필요가 없습니다. 이제 서버 전체의 모든 종속 정보에 대한 메타 데이터 기능이 있습니다.

PS .IsNullT-SQL 래퍼 객체는 다음 WITH RETURNS NULL ON NULL INPUT옵션 으로 작성해야하므로 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 사용에 대한 자세한 내용은 SQL Server Central에서 작성중인 Stairway to SQLCLR 시리즈 참조하십시오 (무료 등록이 필요합니다. 해당 사이트의 정책을 제어하지 않습니다).

  • IndexName()위 SQLCLR 기능은 페이스트 빈에 설치하기 쉬운 스크립트에서, 미리 컴파일 할 수 있습니다. 이 스크립트는 "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.