Prosedur tersimpan pusat untuk mengeksekusi dalam konteks panggilan database

17

Saya sedang mengerjakan solusi perawatan khusus menggunakan sys.dm_db_index_physical_statstampilan. Saat ini saya telah dirujuk dari prosedur tersimpan. Sekarang ketika prosedur tersimpan itu berjalan di salah satu basis data saya, ia melakukan apa yang saya inginkan dan menarik daftar semua catatan mengenai basis data apa pun. Ketika saya letakkan di database yang berbeda itu menarik daftar semua catatan yang berkaitan hanya dengan DB itu.

Misalnya (kode di bawah):

  • Query run terhadap Database 6 menunjukkan informasi [yang diminta] untuk database 1-10.
  • Query run terhadap Database 3 memperlihatkan informasi [yang diminta] hanya untuk database 3.

Alasan saya menginginkan prosedur ini secara khusus pada basis data tiga adalah karena saya lebih suka menyimpan semua objek pemeliharaan di dalam basis data yang sama. Saya ingin pekerjaan ini ada di basis data pemeliharaan dan berfungsi seolah-olah berada di basis data aplikasi itu.

Kode:

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 Waclawski
sumber
4
@ JoachimIsaksson tampaknya pertanyaannya adalah bagaimana memiliki satu salinan tunggal prosedur dalam database pemeliharaan mereka, yang mereferensikan DMV di database lain, daripada harus meletakkan salinan prosedur di setiap database.
Aaron Bertrand
Maaf saya tidak lebih jelas, telah menatap ini selama beberapa hari. Aaron tepat. Saya ingin SP ini tersimpan di basis data pemeliharaan saya dengan kemampuan untuk mengambil data dari seluruh server. Seperti berdiri, ketika itu duduk di DB pemeliharaan saya, hanya menarik data fragmentasi tentang DB pemeliharaan itu sendiri. Apa yang saya bingung adalah mengapa, ketika saya menempatkan SP yang sama persis ini di database yang berbeda dan menjalankannya secara identik, apakah itu menariknya data fragmentasi dari seluruh server? Apakah ada pengaturan atau hak istimewa yang perlu diubah agar SP ini dapat beroperasi dari DB pemeliharaan?
(Perhatikan bahwa pendekatan Anda saat ini mengabaikan fakta bahwa mungkin ada dua tabel dengan nama yang sama di bawah dua skema berbeda - selain saran dalam jawaban saya, Anda mungkin ingin mempertimbangkan nama skema sebagai bagian dari input dan / atau output.)
Aaron Bertrand

Jawaban:

15

Salah satu caranya adalah dengan membuat prosedur sistem masterdan kemudian membuat pembungkus dalam database pemeliharaan Anda. Perhatikan bahwa ini hanya akan berfungsi untuk satu basis data pada satu waktu.

Pertama, di 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

Sekarang, di database pemeliharaan Anda, buat pembungkus yang menggunakan SQL dinamis untuk mengatur konteks dengan benar:

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

(Alasan mengapa nama basis data tidak dapat benar-benar NULLadalah karena Anda tidak dapat bergabung dengan hal-hal seperti sys.objectsdan sys.indexeskarena mereka ada secara independen di setiap basis data. Jadi, mungkin ada prosedur yang berbeda jika Anda menginginkan informasi luas.)

Sekarang Anda dapat memanggil ini untuk basis data lain, mis

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

Dan Anda selalu dapat membuat synonymdi setiap basis data sehingga Anda bahkan tidak perlu merujuk nama basis data pemeliharaan:

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

Cara lain adalah dengan menggunakan SQL dinamis, namun ini juga hanya akan bekerja untuk satu database pada suatu waktu:

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

Namun cara lain adalah dengan membuat tampilan (atau fungsi bernilai tabel) untuk menyatukan tabel dan nama indeks semua database Anda, namun Anda harus meng-hard-code nama-nama database ke dalam tampilan, dan mempertahankannya saat Anda menambahkan / hapus database yang ingin Anda izinkan dimasukkan dalam kueri ini. Ini tidak seperti yang lain, ini memungkinkan Anda untuk mengambil statistik untuk banyak basis data sekaligus.

Pertama, tampilan:

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

Lalu prosedurnya:

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
sumber
15

Nah, ada kabar buruk, kabar baik dengan tangkapan, dan beberapa kabar baik.

Berita buruknya

Objek T-SQL mengeksekusi dalam database di mana mereka berada. Ada dua pengecualian (tidak terlalu berguna):

  1. disimpan prosedur dengan nama diawali dengan sp_dan yang ada dalam [master]database (bukan pilihan bagus: satu DB pada suatu waktu, menambahkan sesuatu ke [master], mungkin menambahkan Sinonim untuk setiap DB, yang harus dilakukan untuk setiap DB baru)
  2. prosedur tersimpan sementara - lokal dan global (bukan pilihan praktis karena harus dibuat setiap kali dan meninggalkan Anda dengan masalah yang sama dengan yang Anda miliki dengan sp_proc yang tersimpan di [master].

Berita bagus (dengan tangkapan)

Banyak (mungkin sebagian besar?) Orang menyadari fungsi builtin untuk mendapatkan beberapa meta-data yang sangat umum:

Menggunakan fungsi-fungsi ini dapat menghilangkan kebutuhan untuk GABUNGAN untuk sys.databases(meskipun yang ini tidak benar-benar masalah), sys.objects(lebih disukai daripada sys.tablesyang tidak termasuk Indexed Views), dan sys.schemas(Anda kehilangan satu itu, dan tidak semuanya ada dalam dboskema ;-). Tetapi bahkan dengan menghapus tiga dari empat BERGABUNG, kita masih secara fungsional berada di tempat yang sama, kan? Salah-o!

Salah satu fitur OBJECT_NAME()dan OBJECT_SCHEMA_NAME()fungsi yang bagus adalah mereka memiliki parameter opsional kedua @database_id. Berarti, sementara BERGABUNG ke tabel tersebut (kecuali untuk sys.databases) adalah basis data khusus, menggunakan fungsi-fungsi ini membuat Anda mendapatkan informasi di seluruh server. Bahkan OBJECT_ID () memungkinkan untuk info seluruh server dengan memberinya nama objek yang sepenuhnya memenuhi syarat.

Dengan menggabungkan fungsi meta-data ini ke dalam kueri utama, kami dapat menyederhanakan sementara pada saat yang sama memperluas di luar database saat ini. Pass pertama refactoring kueri memberi kita:

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;

Dan sekarang untuk "tangkapan": tidak ada fungsi meta-data untuk mendapatkan nama Indeks, apalagi yang server-wide. Jadi begitu ya? Apakah kita pada 90% selesai dan masih terjebak perlu dalam database tertentu untuk mendapatkan sys.indexesdata? Apakah kita benar-benar perlu membuat prosedur tersimpan untuk menggunakan SQL Dinamis untuk mengisi, setiap kali proc utama kita berjalan, tabel temp semua sys.indexesentri di semua database sehingga kita bisa BERGABUNG dengannya? TIDAK!

Berita bagus

Maka muncullah sebuah fitur kecil yang beberapa orang suka benci, tetapi ketika digunakan dengan benar, dapat melakukan beberapa hal luar biasa. Yap: SQLCLR. Mengapa? Karena fungsi SQLCLR jelas dapat mengirimkan pernyataan SQL, tetapi pada dasarnya mengirimkan dari kode aplikasi, itu adalah Dynamic SQL. Jadi tidak seperti fungsi T-SQL, fungsi SQLCLR dapat menyuntikkan nama database ke dalam kueri sebelum menjalankannya. Artinya, kita dapat membuat fungsi kita sendiri untuk mencerminkan kemampuan OBJECT_NAME()dan OBJECT_SCHEMA_NAME()untuk mengambil database_iddan mendapatkan info untuk database itu.

Kode berikut adalah fungsi itu. Tetapi dibutuhkan nama database daripada ID sehingga tidak perlu melakukan langkah ekstra untuk mencarinya (yang membuatnya sedikit kurang rumit dan sedikit lebih cepat).

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

Jika Anda perhatikan, kami menggunakan Koneksi Konteks, yang tidak hanya cepat, tetapi juga berfungsi di SAFEAssemblies. Yap, ini berfungsi di Majelis yang ditandai sebagaiSAFE, sehingga (atau variasi dari itu) bahkan harus bekerja pada Azure SQL Database V12 (dukungan untuk SQLCLR telah dihapus, agak tiba-tiba, dari Azure SQL Database pada bulan April 2016) .

Jadi refactoring pass kedua kami dari pertanyaan utama memberi kami yang berikut:

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;

Itu dia! Baik SQLCLR Scalar UDF ini dan Prosedur Penyimpanan T-SQL pemeliharaan Anda dapat hidup dalam [maintenance]database terpusat yang sama . DAN, Anda tidak perlu memproses satu basis data sekaligus; sekarang Anda memiliki fungsi meta-data untuk semua info dependen yang mencakup seluruh server.

PS Tidak ada .IsNullpengecekan parameter input dalam kode C # karena objek pembungkus T-SQL harus dibuat dengan WITH RETURNS NULL ON NULL INPUTopsi:

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

Catatan tambahan:

  • Metode yang dijelaskan di sini juga dapat digunakan untuk menyelesaikan masalah lain yang sangat mirip yaitu hilangnya fungsi meta-basis data database. Saran Microsoft Connect berikut ini adalah contoh dari satu kasus seperti itu. Dan, melihat bahwa Microsoft telah menutupnya sebagai "Won't Fix", jelas bahwa mereka tidak tertarik menyediakan fungsi built-in seperti OBJECT_NAME()untuk memenuhi kebutuhan ini (maka Solusi yang diposting pada Saran itu :-).

    Tambahkan fungsi metadata untuk mendapatkan nama objek dari hobt_id

  • Untuk mempelajari lebih lanjut tentang penggunaan SQLCLR, silakan lihat seri Stairway to SQLCLR yang saya tulis di SQL Server Central (pendaftaran gratis diperlukan; maaf, saya tidak mengontrol kebijakan situs itu).

  • Fungsi IndexName()SQLCLR yang ditunjukkan di atas tersedia, sudah dikompilasi sebelumnya, dalam skrip yang mudah dipasang di Pastebin. Script memungkinkan fitur "Integrasi CLR" jika belum diaktifkan, dan Majelis ditandai sebagai SAFE. Itu dikompilasi melawan .NET Framework versi 2.0 sehingga akan bekerja di SQL Server 2005 dan yang lebih baru (yaitu semua versi yang mendukung SQLCLR).

    SQLCLR Fungsi Meta-data untuk cross-database IndexName ()

  • Jika ada yang tertarik pada IndexName()fungsi SQLCLR dan lebih dari 320 fungsi lainnya dan prosedur tersimpan, ini tersedia di pustaka SQL # (yang saya penulis). Harap dicatat bahwa meskipun ada versi gratis, fungsi Sys_IndexName hanya tersedia dalam versi lengkap (bersama dengan fungsi Sys_AssemblyName serupa ).

Solomon Rutzky
sumber