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

Jos*_*ski 17 index sql-server-2008 sql-server stored-procedures maintenance

我正在使用该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
Run Code Online (Sandbox Code Playgroud)

Aar*_*and 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
Run Code Online (Sandbox Code Playgroud)

现在,在您的维护数据库中,创建一个使用动态 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
Run Code Online (Sandbox Code Playgroud)

(数据库名称不能真正成为NULL的原因是因为您无法加入诸如此类的东西sys.objectssys.indexes因为它们独立存在于每个数据库中。因此,如果您想要实例范围的信息,则可能有不同的过程。)

现在您可以为任何其他数据库调用它,例如

EXEC YourMaintenanceDatabase.dbo.GetFragStats 
  @DatabaseName = N'AdventureWorks2012',
  @TableName    = N'SalesOrderHeader';
Run Code Online (Sandbox Code Playgroud)

而且您始终可以synonym在每个数据库中创建一个,这样您甚至不必引用维护数据库的名称:

USE SomeOtherDatabase;`enter code here`
GO
CREATE SYNONYM dbo.GetFragStats FOR YourMaintenanceDatabase.dbo.GetFragStats;
Run Code Online (Sandbox Code Playgroud)

另一种方法是使用动态 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
Run Code Online (Sandbox Code Playgroud)

另一种方法是创建一个视图(或表值函数)来联合所有数据库的表和索引名称,但是您必须将数据库名称硬编码到视图中,并在添加时维护它们/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
Run Code Online (Sandbox Code Playgroud)

然后程序:

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
Run Code Online (Sandbox Code Playgroud)


Sol*_*zky 15

嗯,有坏消息,有问题的好消息,还有一些非常好的消息。

坏消息

T-SQL 对象在它们所在的数据库中执行。有两个(不是很有用)例外:

  1. 名称前缀为sp_并且存在于[master]数据库中的存储过程(不是一个很好的选择:一次一个数据库,向 中添加一些东西[master],可能向每个数据库添加同义词,这必须为每个新数据库完成)
  2. 临时存储过程-局部和全局的(不是因为他们每次都被创建,让你与你,与同一问题的实际选择sp_的存储过程[master]

好消息(有收获)

许多(也许是大多数?)人都知道内置函数可以获取一些真正常见的元数据:

使用这些函数可以消除对 JOIN 的需要sys.databases(尽管这不是一个真正的问题),sys.objects(优先于sys.tables排除索引视图)和sys.schemas(您错过了那个,并且并非所有内容都在dbo架构中 ;-)。但即使删除了四个 JOIN 中的三个,我们在功能上仍然是同一个地方,对吧?错哦!

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;
Run Code Online (Sandbox Code Playgroud)

现在是“捕获”:没有元数据函数来获取索引名称,更不用说服务器范围的了。所以是这样吗?我们是否已经完成了 90%,但仍然需要在特定的数据库中才能获取sys.indexes数据?我们真的需要创建一个存储过程来使用动态 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您会注意到,我们正在使用上下文连接,它不仅速度快,而且适用于程序集SAFE。是的,这在标记为的程序集中有效SAFE,因此它(或它的变体)甚至可以在 Azure SQL 数据库 V12 上运行 (2016 年 4 月从 Azure SQL 数据库中突然删除了对 SQLCLR 的支持)

因此,我们对主查询的第二遍重构为我们提供了以下内容:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

就是这样!此 SQLCLR 标量 UDF 和您的维护 T-SQL 存储过程都可以存在于同一个集中式[maintenance]数据库中。而且,您不必一次处理一个数据库;现在您拥有服务器范围内所有相关信息的元数据函数。

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

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;
Run Code Online (Sandbox Code Playgroud)

补充说明:

  • 此处描述的方法还可用于解决其他非常相似的跨数据库元数据函数缺失问题。以下 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 的所有版本)中运行。

    SQLCLR 元数据函数,用于跨数据库 IndexName()

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