索引重建期间的统计信息更新

udh*_*gam 2 sql-server filegroups index-maintenance sql-server-2017

我手头有一项任务是移动重建一个大表,以将 LOB 页面移动到 SQL Server 2017 Enterprise Edition 上的不同文件组。

我正在概念验证环境中测试脚本,我可以看到总共大约CREATE INDEX .. DROP_EXISTING=ON需要 6 小时。

CREATE UNIQUE CLUSTERED INDEX [PK_TABLE1]
ON [dbo].[TABLE1] ([Id] ASC)
 WITH (DROP_EXISTING = ON , FILLFACTOR = 100, PAD_INDEX = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, IGNORE_DUP_KEY = OFF, DATA_COMPRESSION = NONE, STATISTICS_NORECOMPUTE = OFF, ONLINE = ON, MAXDOP=2) 
ON PS_MOVE_HELPER_D59E24BC73414AA8A5FB2E5D8F93C3D8([Id] );

CREATE UNIQUE CLUSTERED INDEX [PK_TABLE1]
ON [dbo].[TABLE1] ([Id] ASC)
 WITH (DROP_EXISTING = ON , FILLFACTOR = 100, PAD_INDEX = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, IGNORE_DUP_KEY = OFF, DATA_COMPRESSION = NONE, STATISTICS_NORECOMPUTE = OFF, ONLINE = ON, MAXDOP=2) 
ON [LOB_DATA];

CREATE UNIQUE CLUSTERED INDEX [PK_TABLE1]
ON [dbo].[TABLE1] ([Id] ASC)
 WITH (DROP_EXISTING = ON , FILLFACTOR = 100, PAD_INDEX = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, IGNORE_DUP_KEY = OFF, DATA_COMPRESSION = NONE, STATISTICS_NORECOMPUTE = OFF, ONLINE = ON, MAXDOP=2) 
ON [ROW_DATA];
Run Code Online (Sandbox Code Playgroud)
  1. 每次索引重建时相关统计数据都会更新吗?
  2. 统计更新通常在索引重建完成后异步触发?
  3. 6小时的总时长是否也包括更新统计数据的时间?或者只有在索引重建完成 6 小时后才会异步触发统计信息更新?
  4. 我可以使用 auto_stats 扩展事件捕获此统计更新事件并查看它花费了多长时间?

Pau*_*ite 9

  1. 是的,重建索引时会更新统计信息。
  2. 不,统计数据通常是作为填充索引*的副作用而收集的。数据流已根据需要进行排序,因此同时填充统计信息是有意义的。需要明确的是,索引填充和统计信息收集使用相同的数据流和执行计划同时发生。
  3. 是的,总时间包括构建统计信息,但与重建索引相比,这是一个很小的开销。
  4. 不会,作为重建索引的副作用而更新的统计信息不会触发该auto_stats事件。您可以跟踪扩展事件的进度progress_report_online_index_operation,但这不会显示单独的统计构建事件,因为根本不存在该事件。

在线索引构建期间的示例调用堆栈
在线索引构建期间的示例调用堆栈

您的进程缓慢是因为您正在执行大量资源密集型工作,而不是因为统计数据刷新。在微软提供一种方法来支持您直接需要的操作(将单个分配单元移动到不同的文件组)之前,这实际上是无法避免的。

辅助分区方案的第一次重建将是单线程的,并且具有不必要的排序。

在 LOB 文件组上重建索引的成本很高,即使您只是为了删除分区而这样做。SQL Server 不知道您在做什么,因此只会按照您的要求重建整个表。

第三次重建也很昂贵,但这个过程确实可以让您最终到达需要去的地方,同时保持表大部分在线。

加速

BULK_LOGGED使用设置为或恢复模型的数据库执行重建,SIMPLE以尽可能利用最少的日志记录。

SWITCH如果能够与源表一起使用,则可以避免一次重建和单线程排序:

  • 在 上创建临时分区方案和空函数PRIMARY
  • 创建与源匹配但在新方案上分区的切换表。
  • 开始交易
    • SWITCH将源表写入分区表。这应该是仅瞬时元数据操作。无论如何指定该WAIT_AT_LOW_PRIORITY选项。
    • 删除原始表。
    • 重命名交换表及其主键以与原始表匹配。
  • 如果到目前为止操作成功,则提交事务。
  • 在线重建聚集索引以将所有内容移动到LOB_DATA文件组。
    • 该计划将使用并行性并避免排序。
    • 结果是一个非分区表,其中包含LOB_DATA文件组上的所有分配单元。
  • 删除临时分区方案和功能。
  • 再次在线重建聚集索引,将非 LOB 分配单元移动到ROW_DATA文件组。

您现在拥有包含文件组上的 LOB 数据的原始表LOB_DATA以及文件组上的其他所有内容ROW_DATA

脚本

重新组织 Stack Overflow 示例数据库中的 Users 表的演示(首先创建文件组):

-- Temporary partitioning function and scheme
CREATE PARTITION FUNCTION PF (integer) AS RANGE FOR VALUES ();
CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

-- Switch table
CREATE TABLE [dbo].[Users_Switch]
(
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [AboutMe] [nvarchar](max) NULL,
    [Age] [int] NULL,
    [CreationDate] [datetime] NOT NULL,
    [DisplayName] [nvarchar](40) NOT NULL,
    [DownVotes] [int] NOT NULL,
    [EmailHash] [nvarchar](40) NULL,
    [LastAccessDate] [datetime] NOT NULL,
    [Location] [nvarchar](100) NULL,
    [Reputation] [int] NOT NULL,
    [UpVotes] [int] NOT NULL,
    [Views] [int] NOT NULL,
    [WebsiteUrl] [nvarchar](200) NULL,
    [AccountId] [int] NULL,
    CONSTRAINT [PK_Users_Switch_Id] 
        PRIMARY KEY CLUSTERED ([Id] ASC)
        ON PS (Id)
) ON PS (Id);

-- Optional, to match source table
EXECUTE sys.sp_tableoption
    @TableNamePattern = N'dbo.Users_Switch',
    @OptionName = 'large value types out of row',
    @OptionValue = 'on';

BEGIN TRY;
    BEGIN TRANSACTION;

    -- Switch
    ALTER TABLE dbo.Users
        SWITCH TO dbo.Users_Switch 
        PARTITION 1
        WITH
        (
            WAIT_AT_LOW_PRIORITY 
            ( 
                MAX_DURATION = 1 MINUTES, 
                ABORT_AFTER_WAIT = SELF
            )
        );

    -- Drop original
    DROP TABLE dbo.Users;

    -- Rename table
    EXECUTE sys.sp_rename 
        @objname = N'dbo.Users_Switch',
        @newname = N'Users',
        @objtype = 'OBJECT';

    -- Rename primary key
    EXECUTE sys.sp_rename 
        @objname = N'PK_Users_Switch_Id',
        @newname = N'PK_Users_Id',
        @objtype = 'OBJECT';

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;

-- Move everything to LOB_DATA
-- Parallel, no sort
CREATE UNIQUE CLUSTERED INDEX [PK_Users_Id] 
ON dbo.Users (Id)
WITH (ONLINE = ON, DROP_EXISTING = ON)
ON LOB_DATA;

-- Drop temporary partitioning function and scheme
DROP PARTITION SCHEME PS;
DROP PARTITION FUNCTION PF;

-- Move non-LOB data to ROW_DATA
-- Also parallel, no sort
CREATE UNIQUE CLUSTERED INDEX [PK_Users_Id] 
ON dbo.Users (Id)
WITH (ONLINE = ON, DROP_EXISTING = ON)
ON ROW_DATA;

-- Done
Run Code Online (Sandbox Code Playgroud)

要将 Users 表重置回所有内容PRIMARY

IF EXISTS
(
    SELECT * 
    FROM sys.partitions AS P
    JOIN sys.allocation_units AS AU 
        ON P.hobt_id = AU.container_id
    JOIN sys.filegroups AS FG 
        ON FG.data_space_id = AU.data_space_id
    WHERE
        P.[object_id] = OBJECT_ID(N'dbo.Users', 'U')
        AND FG.[name] != N'PRIMARY'
)
BEGIN
    -- Temporary partitioning function and scheme
    CREATE PARTITION FUNCTION PF (integer) AS RANGE FOR VALUES ();
    CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY]);

    -- Move everything to PRIMARY
    -- Single-threaded, sort
    CREATE UNIQUE CLUSTERED INDEX [PK_Users_Id] 
    ON dbo.Users (Id)
    WITH (ONLINE = ON, DROP_EXISTING = ON)
    ON PS (Id);

    -- Make table non-partitioned
    CREATE UNIQUE CLUSTERED INDEX [PK_Users_Id] 
    ON dbo.Users (Id)
    WITH (ONLINE = ON, DROP_EXISTING = ON)
    ON [PRIMARY];

    -- Drop temporary partitioning function and scheme
    DROP PARTITION SCHEME PS;
    DROP PARTITION FUNCTION PF;
END;
Run Code Online (Sandbox Code Playgroud)

* 第一次从非分区表重建分区表是此规则的一个例外示例。此索引构建可能会导致单独的统计信息刷新,但前提是 SQL Server 认为现有统计信息已过时。到目前为止我还无法重现这一点,所以它仍然只是一种可能性。