在调用数据库上下文中执行的中央存储过程


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放在另一个数据库中并相同地执行它时,它是否会从服务器中提取碎片数据?是否需要更改此SP才能从维护DB进行操作的设置或特权?

(请注意,您当前的方法忽略了以下事实:在两个不同的模式下可能存在两个具有相同名称的表-除了我的回答中的建议之外,您可能还希望将模式名称视为输入和/或输出的一部分。)
亚伦·伯特兰(Aaron Bertrand)

Answers:


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.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,但是,这一次也仅适用于一个数据库:

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_存储proc中相同的问题[master]

好消息(有收获)

许多人(也许是大多数人?)知道内置函数来获取一些真正通用的元数据:

使用这些函数可以消除对JOIN的需求sys.databases(尽管这不是一个真正的问题),sys.objects(最好sys.tables不包括索引视图)和sys.schemas(您错过了那个,并且不是dbo模式中的所有内容都;;)。但是即使删除了四个JOIN中的三个,我们在功能上仍然是相同的地方,对吗?错了!

OBJECT_NAME()OBJECT_SCHEMA_NAME()函数的一个不错的功能是它们具有的可选第二个参数@database_id。意思是,虽然JOINing到那些表(除外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并获得该数据库的信息。

以下代码是该函数。但是它使用数据库名称而不是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数据库V12上运行 (在2016年4月,从Azure SQL数据库中突然删除了对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标量UDF和您的维护T-SQL存储过程都可以存在于同一集中式[maintenance]数据库中。而且,您不必一次处理一个数据库。现在,您具有服务器范围内所有相关信息的元数据功能。

PS没有.IsNull检查C#代码中的输入参数,因为应该使用以下WITH RETURNS NULL ON NULL INPUT选项创建T-SQL包装对象:

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上编写的SQLCLR系列楼梯(需要免费注册;抱歉,我不控制该站点的策略)。

  • IndexName()上面显示的SQLCLR函数可以在Pastebin上的易于安装的脚本中进行预编译。如果该脚本尚未启用,则它将启用“ CLR集成”功能,并且程序集标记为SAFE。它是针对.NET Framework 2.0版进行编译的,因此它将在SQL Server 2005和更高版本(即支持SQLCLR的所有版本)中运行。

    跨数据库IndexName()的SQLCLR元数据功能

  • 如果有人对IndexName()SQLCLR函数以及超过320个其他函数和存储过程感兴趣,可以在SQL#库(我是作者)中找到它。请注意,虽然有免费版本,但Sys_IndexName函数仅在完整版本中可用(以及类似的Sys_AssemblyName函数)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.