Центральна збережена процедура для виконання в контексті виклику бази даних


17

Я працюю над індивідуальним рішенням технічного обслуговування за допомогою sys.dm_db_index_physical_statsперегляду. В даний час на нього посилається збережена процедура. Тепер, коли ця збережена процедура запускається на одній з моїх баз даних, вона робить те, що я хочу це робити, і знімає список всіх записів стосовно будь-якої бази даних. Коли я розміщую його в іншій базі даних, вона видаляє список усіх записів, що стосуються лише цієї БД.

Наприклад (код внизу):

  • Запит на базу даних 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 в інших базах даних, а не вкладати копію процедури в кожну базу даних.
Аарон Бертран

Вибачте, що я не був більш зрозумілий, дивився на це кілька днів. Аарон на місці. Я хочу, щоб цей ІП знаходився в моїй базі даних технічного обслуговування з можливістю захоплення даних з усіх серверів. На сьогоднішній день, коли він знаходиться в моїй БД технічного обслуговування, він лише отримує фрагментарні дані про саму БД обслуговування. Що мене бентежить, це те, чому, коли я розміщую цей самий той самий SP в іншій базі даних і виконую його однаково, він витягує його фрагментарні дані з сервера? Чи є налаштування чи привілей, які потребують зміни цього SP для роботи як такої з БД технічного обслуговування?

(Зверніть увагу, що ваш поточний підхід ігнорує той факт, що за двома різними схемами можуть бути дві таблиці з однаковою назвою - крім пропозицій у моїй відповіді, ви можете розглянути ім'я схеми як частина введення та / або виводу.)
Аарон Бертран

Відповіді:


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]базі даних (не чудовий варіант: одна БД за один раз, додаючи щось до[master] , можливо додавання синонімів до кожної БД, що потрібно зробити для кожної нової БД)
  2. тимчасово зберігаються процедури - локальні та глобальні (не практичний варіант, оскільки їх доводиться створювати щоразу і залишати перед вами ті самі проблеми, що і у вас із sp_збереженим процесором [master].

Хороша новина (з уловом)

Багато (можливо, більшість?) Люди знають про вбудовані функції, щоб отримати деякі дійсно поширені метадані:

Використання цих функцій може усунути необхідність приєднатись до JOIN sys.databases(хоча це насправді не є проблемою), sys.objects(перевагу над sys.tablesяким виключає індексований перегляд), і sys.schemas(у вас цього не було, і це не все в dboсхемі ;-). Але навіть вилучивши три з чотирьох ПРИЄДНАЙТЕСЬ, ми все ще функціонально одне і те ж місце, правда? Неправильно!

Однією з приємних особливостей 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дані? Чи дійсно нам потрібно створити збережену процедуру для використання Dynamic SQL для заповнення кожного разу, коли запускається наш основний процесор, тимчасової таблиці всіх sys.indexesзаписів у всіх базах даних, щоб ми могли приєднатися до неї? НІ!

Справді хороша новина

Тож разом з цим є невелика особливість, яку деякі люди люблять ненавидіти, але при правильному використанні вони можуть робити дивовижні речі. Так: SQLCLR. Чому? Оскільки функції SQLCLR , очевидно , можуть подати заяви SQL, але по самій природі уявлення з програми коду, то є динамічний SQL. Отже, на відміну від функцій T-SQL, функції SQLCLR можуть вводити ім'я бази даних у запит перед його виконанням. Це означає, що ми можемо створити власну функцію для відображення здатності OBJECT_NAME()та OBJECT_SCHEMA_NAME()прийому database_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 була видалена досить різко з бази даних Azure SQL у квітні 2016 року) .

Отже, наше рефакторинг основного запиту дає нам таке:

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;

Це воно! І цей скалярний UDF SQLCLR, і процедура зберігання T-SQL, що зберігається, можуть жити в одній централізованій[maintenance] базі даних. І, вам не потрібно обробляти по одній базі даних одночасно; тепер у вас є функції метаданих для всієї залежної інформації, що є загальною для сервера.

PS Немає .IsNullперевірки вхідних параметрів у коді C #, оскільки об’єкт обгортки T-SQL повинен бути створений з 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 закрила його як "Won't Fix", зрозуміло, що вони не зацікавлені в наданні вбудованих функцій, таких як OBJECT_NAME()задоволення цієї потреби (отже, вирішення, яке розміщено в цій пропозиції :-).

    Додайте функцію метаданих, щоб отримати ім'я об’єкта від hobt_id

  • Щоб дізнатися більше про використання SQLCLR, перегляньте серію " Сходи до SQLCLR ", про яку я пишу на SQL Server Central (потрібна безкоштовна реєстрація; вибачте, я не контролюю політику цього сайту).

  • Функція IndexName()SQLCLR, показана вище, доступна заздалегідь складеною в простому для встановлення скрипті на Pastebin. Сценарій вмикає функцію "Інтеграція CLR", якщо вона ще не включена, а Асамблея позначена як SAFE. Він компілюється проти .NET Framework версії 2.0, щоб він працював у SQL Server 2005 та новіших версіях (тобто всі версії, що підтримують SQLCLR).

    Функція метаданих SQLCLR для крос-бази даних IndexName ()

  • Якщо когось цікавить IndexName()функція SQLCLR та понад 320 інших функцій та збережених процедур, вона доступна у бібліотеці SQL # (автор якої я). Зверніть увагу, що при наявності безкоштовної версії функція Sys_IndexName доступна лише у повній версії (разом із аналогічною функцією Sys_AssemblyName ).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.