如何识别聚集索引候选者

Mag*_*ier 5 sql-server clustered-index uniqueidentifier index-tuning

我发现自己处于继承了数据库的情况,人们抱怨数据库性能。我确定了大约十几个没有聚集索引或没有精心规划的聚集索引的表,其中一些数据量大且经常使用。我实际上正在努力找出为堆实现 CI 的最佳候选者。经过大量阅读后,我仍然不知道该怎么做。我知道这取决于。所以我想给出一张表作为样本,希望得到一些建议。

该表有 122 列。是的,我知道...它包含大约 800 000 行。结构(摘录):

CREATE TABLE [dbo].[tO](
[ID] [uniqueidentifier] NOT NULL,
[MaID] [uniqueidentifier] NULL,
[OrID] [uniqueidentifier] NOT NULL,
[UsID] [uniqueidentifier] NOT NULL,
[CrUsID] [uniqueidentifier] NOT NULL,
[SuID] [uniqueidentifier] NULL,
[SOID] [uniqueidentifier] NULL,
[DeA1ID] [uniqueidentifier] NULL,
[DeA2ID] [uniqueidentifier] NULL,
[PayID] [uniqueidentifier] NULL,
[MAdID] [uniqueidentifier] NULL,
[ShID] [uniqueidentifier] NULL,
[SessID] [varchar](32) NULL,
[OrUID] [uniqueidentifier] NULL,
[PurOfUID] [uniqueidentifier] NULL,
[AddiUID] [uniqueidentifier] NULL,
[SupUID] [uniqueidentifier] NULL,
[PayTID] [uniqueidentifier] NULL,
[OPSID] [uniqueidentifier] NULL,
[StoRSID] [uniqueidentifier] NULL,
[PayMeID] [uniqueidentifier] NULL,
...
[OrderDate] [datetime] NULL,
[CreateDate] [datetime] NOT NULL,
[UpdateDate] [datetime] NULL,
[RowVersion] [timestamp] NOT NULL,
[Deleted] bit NOT NULL,
...
[CTR] int itentity(1,1) NOT NULL
CONSTRAINT [PK_Order] PRIMARY KEY NONCLUSTERED <= uses column [ID]
Run Code Online (Sandbox Code Playgroud)

在这个表上存在 29 个非聚集索引。我猜他们中的一些人是在绝望中创造出来的......

我知道这个和其他表的设计不是很好。它的方式很宽,应该分开。这是在我的名单上。

查询几乎总是在列 [OrID] 上进行过滤。此列不是唯一的(具有一个 OrID 的最高行数 > 310 000)。所有查询都在 DELETED = 0 上进行过滤。它们也在不同的数字列上进行过滤。

联接是在不同的 uid 列上执行的。

该表包含 FK,也在 uid 列上,并且有一个父表,它们连接到使用 PK [id] 连接的 uid 和子表之一。

在此表上尝试了一些跟踪的用户查询后,我发现他们通常会过滤一行,而且这根本不是很糟糕。查看执行计划,我发现通常会查找非聚集索引之一,然后执行 RID 查找。这花费了大约 30% - 70% 的查询。

所以我想它可以表现得更好。另外,有了聚集索引,我假设可以删除几个非聚集索引。

编辑 10/16/15 附加信息:

sp_BlitzIndex说:“22651751 转发提取,针对堆删除 0 次:dbo.tO (0) 自厌恶索引:带有转发记录或删除的堆”

编辑结束

现在我对这个(和其他样本)的问题是我们到处都有那些丑陋的唯一标识符。我阅读了很多可比较的帖子,但仍然不知道如何处理。

  • 我是否应该停止思考并向主键 [ID] 添加聚集索引,然后遵循一般建议,即在 99.9% 的所有情况下,拥有聚集索引基本上比没有聚集索引更好,即使它与 CI 不完全匹配标准(不狭窄,不不断增加)?

  • 我应该更好地使用像 [CreateDate] 那样不那么独特但不断增加的列吗?

  • 我应该添加一个新的 IDENTITY 列并将 CI 放在这个列上吗?

其他桌子上的情况可以与那张桌子相比。

编辑 Okt-17 2016:根据评论中的要求,我添加了实际存在的(混淆的)索引。(我知道其中一些可以在没有任何工作的情况下轻松删除,例如 [p] 被 [s] 100% 覆盖......但这不是堆/CI 问题的核心)。

CREATE UNIQUE NONCLUSTERED INDEX [a]
    ON [dbo].[tO]([Deleted] ASC, [Status] ASC, [ID] ASC)
    INCLUDE([NumericA], [CreateDate], [char3], [Bool1], [Bool2], [varchar1], [OrderDate], [INT1], [SuID], [varcharT], [UsID], [MaID], [varcharS], [TinyINTd], [PurOfUID], [AddiUID]) WITH (FILLFACTOR = 90);            
GO
CREATE NONCLUSTERED INDEX [b]
    ON [dbo].[tO]([ID] ASC, [UsID] ASC, [SuID] ASC, [Deleted] ASC, [Status] ASC, [OrID] ASC, [TINYINTm] ASC, [varchar1] ASC, [varcharT] ASC, [OrderDate] ASC, [CreateDate] ASC, [PAN_decimal] ASC, [char3] ASC, [INT1] ASC, [NumericA] ASC)
    INCLUDE([Bool1], [Bool2]);
GO
CREATE NONCLUSTERED INDEX [c]
    ON [dbo].[tO]([ID] ASC, [Status] ASC, [Deleted] ASC, [UsID] ASC, [SuID] ASC, [OrID] ASC, [TINYINTm] ASC, [varchar1] ASC, [varcharT] ASC, [OrderDate] ASC, [CreateDate] ASC)
    INCLUDE([INT1], [char3], [PAN_decimal], [NumericA], [Bool1], [Bool2]);
GO
CREATE NONCLUSTERED INDEX [d]
    ON [dbo].[tO]([MaID] ASC);
GO
CREATE NONCLUSTERED INDEX [e]
    ON [dbo].[tO]([UsID] ASC);
GO
CREATE NONCLUSTERED INDEX [f]
    ON [dbo].[tO]([SuID] ASC);
GO
CREATE NONCLUSTERED INDEX [g]
    ON [dbo].[tO]([AddiUID] ASC, [OrID] ASC, [INT_CNR] ASC, [Deleted] ASC)
    INCLUDE([ID]);
GO
CREATE NONCLUSTERED INDEX [h]
    ON [dbo].[tO]([varchar1] ASC);
GO
CREATE NONCLUSTERED INDEX [i]
    ON [dbo].[tO]([OrID] ASC, [INT_CNR] ASC, [Deleted] ASC)
    INCLUDE([ID], [MaID], [md_Date], [ouID], [Status], [AddiUID], [PurOfUID], [varcharS], [UsID]);
GO
CREATE NONCLUSTERED INDEX [j]
    ON [dbo].[tO]([OrderDate] ASC);
GO
CREATE NONCLUSTERED INDEX [k]
    ON [dbo].[tO]([SCC_int] ASC);
GO
CREATE NONCLUSTERED INDEX [l]
    ON [dbo].[tO]([PurOfUID] ASC);
GO
CREATE NONCLUSTERED INDEX [m]
    ON [dbo].[tO]([OrID] ASC, [INT_CNR] ASC, [UsID] ASC, [Deleted] ASC)
    INCLUDE([ID]);
GO
CREATE NONCLUSTERED INDEX [n]
    ON [dbo].[tO]([Status] ASC)
    INCLUDE([ID], [UsID], [SuID], [varchar1], [INT1], [char3], [OrderDate], [CreateDate], [Bool1]);
GO
CREATE NONCLUSTERED INDEX [o]
    ON [dbo].[tO]([INT_CNR] ASC, [Status] ASC)
    INCLUDE([ID], [OrID], [UsID], [SuID], [SessionID], [varchar1], [char3], [TinyINTd], [OrderDate], [CreateDate], [RowVersion], [Deleted]);
GO
CREATE NONCLUSTERED INDEX [p]
    ON [dbo].[tO]([Deleted] ASC, [INT_CNR] ASC)
    INCLUDE([ID]);
GO
CREATE NONCLUSTERED INDEX [q]
    ON [dbo].[tO]([OrID] ASC, [Deleted] ASC)
    INCLUDE([ID], [SuID], [varchar1], [ouID]);
GO
CREATE NONCLUSTERED INDEX [r]
    ON [dbo].[tO]([OrID] ASC, [Status] ASC, [Deleted] ASC, [INT_CNR] ASC)
    INCLUDE([ID], [UsID], [varchar1], [varcharT], [varcharS], [PurOfUID], [AddiUID]);
GO
CREATE NONCLUSTERED INDEX [s]
    ON [dbo].[tO]([Deleted] ASC, [INT_CNR] ASC, [Status] ASC, [OrderDate] ASC)
    INCLUDE([ID], [cfct_decimal]);
GO
CREATE NONCLUSTERED INDEX [t]
    ON [dbo].[tO]([UsID] ASC, [INT_CNR] ASC, [Deleted] ASC, [Status] ASC, [OrderDate] ASC)
    INCLUDE([NumericA], [cfct_decimal]);
GO
CREATE NONCLUSTERED INDEX [u]
    ON [dbo].[tO]([SOID] ASC, [Status] ASC, [Deleted] ASC, [INT_CNR] ASC)
    INCLUDE([ID], [MaID], [varchar1]);
GO
CREATE NONCLUSTERED INDEX [v]
    ON [dbo].[tO]([Status] ASC, [Deleted] ASC, [ISD_Bool] ASC, [OrderDate] ASC)
    INCLUDE([ID], [MaID], [varchar1]);
GO
CREATE NONCLUSTERED INDEX [w]
    ON [dbo].[tO]([OrID] ASC, [INT_CNR] ASC, [Status] ASC, [Deleted] ASC, [ISD_Bool] ASC)
    INCLUDE([ID], [MaID], [varchar1], [CreateDate], [OrderDate], [SuID], [UGRID], [ouID], [UsID], [DeA1ID], [md_Date], [MAdID], [PurOfUID]);
Run Code Online (Sandbox Code Playgroud)

以下是使用此表的很少且没有优先级的示例查询(或其中的例外):

...Select tO.ID From tO WHERE tO.[OrID] = @OrID AND [tO].[INT_CNR ] = 0 AND tO.[UsID] = @UserID AND tO.[Deleted] = 0    ...

... ID, [Deleted], [RowVersion] FROM [tO] LEFT JOIN O_details ON [tO].[ID] = [O_details].[OID] ...
    WHERE tO.INT_CNR = 0 AND tO.RowVersion > @rv ...

... INNER JOIN [tO] ON [tO].ID = xyz.OID ...


...From O_Details d LEFT JOIN [tO] ON d.OID = [tO].ID
                                WHERE [tO].[Status] = 4 
                                AND d.AID is not null
                                AND [tO].[ID] IN(...)...


... UPDATE [tO] SET [SomeVarcharCol]='...' WHERE [ID]=@id...


...UPDATE [tO] SET [NumericA]=@a, [PAN_decimal]=@b, [anydecimal]=@c, [UpdateDate]=@UpdateDate WHERE [ID]=@ID AND [RowVersion]=@RowVersion ...
Run Code Online (Sandbox Code Playgroud)

编辑结束

Ava*_*rkx 3

原帖

老实说,在你的情况下,我会继续对 [ID] 进行聚类。令人担忧的是,它似乎没有默认值,但与此同时,如果幸运的话,它可能正在填充某种形式的调用NEWSEQUENTIALID。如果不是,这也不是世界末日,如果索引/统计信息得到维护,< 1MM 行不应该成为显示障碍。

这样做将使您能够开始专注于重新处理一些过滤/包含索引,当您完成这些工作时,也许您会遇到 4 个痛点,而不是 12 个。

除此之外的任何建议都可能导致对您的问题进行“冰山一角”分类 - 正如您似乎很清楚的那样,问题实际上是关于糟糕的设计,而不是发动机性能。

更新

我无法找到一种方法来清楚地回答您的评论,所以这里的信息太多了。

正如前面所说,如果您有一个薄的、可访问的增量列(例如列)IDENTITY,请在其上放置聚集索引 - 这是理所当然的。

然而:

如果不存在这样的列,并且您正在查看具有 10 多个索引的堆(并且仍然最终进行 RID 查找!!),那么ID现在只需对 进行集群,即使它是一个UNIQUEIDENTIFIER. 您最需要的是清理存储利用率,这将从集群开始(某些事情),如果只是将前向获取数据拉回与行的其余部分保持一致。即使您之后立即删除聚集索引,您至少现在已经组织好了堆,显着减少了前向指针(如果不是完全消除的话),并最终减少了(不必要的过度劳累)I/O 子系统上的争用。这样做实际上没有什么坏处,所以就这样做吧。然后你可以去弄清楚为什么有 29 个可用索引和查询仍然需要进行 RID 匹配。

忽略索引蔓延并专注于对堆进行聚类,使用FILLFACTOR大约 70 应该可以很好地处理UNIQUEIDENTIFIER值。这是因为 B 树(SQL Server 用于索引的实际数据结构)在INSERT仅随机值操作的情况下往往达到 69% 左右的节点利用率。这个数字是一个数学真理,雷曼和姚这对非常聪明的家伙在他们对该主题的研究中证明了这一点。将DELETE足够数量的 s 放入混合中可以降低节点利用率目标,但作为一般规则,aFILLFACTOR为 70 是开始调整非顺序/随机聚集索引的好地方,因为您实际上告诉 SQL Server 不要甚至不必费心尝试填充每页的 30%,因为无论如何你都不会使用该部分 - 因为从数学上讲,你不会使用该部分。

关于使用非最佳集群键的潜在页面拆分,虽然这是一个有效的问题,但如果您碰巧有权访问 Sql Server 2012+ 安装,您可以按照此演示进行操作,从设置扩展事件开始跟踪页面拆分的会话:

DROP EVENT SESSION [TrackPageSplits]
ON  SERVER;
GO

CREATE EVENT SESSION [TrackPageSplits]
ON    SERVER
ADD EVENT sqlserver.transaction_log(
    WHERE operation = 11  -- LOP_DELETE_SPLIT 
      AND database_id = 2 -- Watch TempDB;
)
ADD TARGET package0.histogram(
    SET filtering_event_name = 'sqlserver.transaction_log',
        source_type = 0, -- Event Column
        source = 'alloc_unit_id');
GO

-- Start the Event Session Again
ALTER EVENT SESSION [TrackPageSplits]
ON SERVER
STATE=START;
GO
Run Code Online (Sandbox Code Playgroud)

一旦启动并运行,您可以设置和播种一些表,以了解不同填充因子下不同类型的集群键对索引密度的影响以及插入它们可能导致的页面拆分数量。对于这个例子,我使用了一个很好的聚类示例,在INTEGERIDENTITYFILL_FACTOR 100,一个糟糕的聚类示例,在和下UNIQUEIDENTIFIER使用非序列,然后在 s 为 75、70 和 65下使用另外三个非序列聚类键,每个都以 2.5MM 为种子记录。NEWID()FILL_FACTOR 100UNIQUEIDENTIFIERFILL_FACTOR

USE tempdb;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'PageSplitIdentity'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.PageSplitIdentity;
    CREATE TABLE dbo.PageSplitIdentity
    (
        PageSplitIdentity_PK    INTEGER IDENTITY( 1, 1 ) NOT NULL,
        Foo                     VARBINARY( 512 ) NOT NULL
    );

    ALTER TABLE dbo.PageSplitIdentity
    ADD CONSTRAINT PK__PageSplitIdentity
        PRIMARY KEY CLUSTERED ( PageSplitIdentity_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 )
    ON  [PRIMARY];

    ALTER TABLE dbo.PageSplitIdentity
    ADD CONSTRAINT DF__PageSplitIdentity__Foo
        DEFAULT CONVERT( VARBINARY( 512 ), REPLICATE( 0x01, 512 ) ) FOR Foo;

    SET NOCOUNT ON;
    DECLARE @i                      INTEGER = 0;
    WHILE ( @i < 2500000 )
    BEGIN
        INSERT INTO dbo.PageSplitIdentity DEFAULT VALUES;
        SET @i = @i + 1;
    END;
    SET NOCOUNT OFF;
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'PageSplitNewID'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.PageSplitNewID;
    CREATE TABLE dbo.PageSplitNewID
    (
        PageSplitNewID_PK       UNIQUEIDENTIFIER NOT NULL,
        Foo                     VARBINARY( 512 ) NOT NULL
    );

    ALTER TABLE dbo.PageSplitNewID
    ADD CONSTRAINT PK__PageSplitNewID
        PRIMARY KEY CLUSTERED ( PageSplitNewID_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 )
    ON  [PRIMARY];

    ALTER TABLE dbo.PageSplitNewID
    ADD CONSTRAINT DF__PageSplitNewID__PageSplitNewID_PK
        DEFAULT NEWID() FOR PageSplitNewID_PK;

    ALTER TABLE dbo.PageSplitNewID
    ADD CONSTRAINT DF__PageSplitNewID__Foo
        DEFAULT CONVERT( VARBINARY( 512 ), REPLICATE( 0x01, 512 ) ) FOR Foo;

    SET NOCOUNT ON;
    DECLARE @i                      INTEGER = 0;
    WHILE ( @i < 2500000 )
    BEGIN
        INSERT INTO dbo.PageSplitNewID DEFAULT VALUES;
        SET @i = @i + 1;
    END;
    SET NOCOUNT OFF;
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'PageSplitNewIDFillFactor75'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.PageSplitNewIDFillFactor75;
    CREATE TABLE dbo.PageSplitNewIDFillFactor75
    (
        PageSplitNewIDFillFactor75_PK
                                UNIQUEIDENTIFIER NOT NULL,
        Foo                     VARBINARY( 512 ) NOT NULL
    );

    ALTER TABLE dbo.PageSplitNewIDFillFactor75
    ADD CONSTRAINT PK__PageSplitNewIDFillFactor75
        PRIMARY KEY CLUSTERED ( PageSplitNewIDFillFactor75_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 75 )
    ON  [PRIMARY];

    ALTER TABLE dbo.PageSplitNewIDFillFactor75
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor75__PageSplitNewIDFillFactor75_PK
        DEFAULT NEWID() FOR PageSplitNewIDFillFactor75_PK;

    ALTER TABLE dbo.PageSplitNewIDFillFactor75
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor75__Foo
        DEFAULT CONVERT( VARBINARY( 512 ), REPLICATE( 0x01, 512 ) ) FOR Foo;

    SET NOCOUNT ON;
    DECLARE @i                      INTEGER = 0;
    WHILE ( @i < 2500000 )
    BEGIN
        INSERT INTO dbo.PageSplitNewIDFillFactor75 DEFAULT VALUES;
        SET @i = @i + 1;
    END;
    SET NOCOUNT OFF;
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'PageSplitNewIDFillFactor70'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.PageSplitNewIDFillFactor70;
    CREATE TABLE dbo.PageSplitNewIDFillFactor70
    (
        PageSplitNewIDFillFactor70_PK
                                UNIQUEIDENTIFIER NOT NULL,
        Foo                     VARBINARY( 512 ) NOT NULL
    );

    ALTER TABLE dbo.PageSplitNewIDFillFactor70
    ADD CONSTRAINT PK__PageSplitNewIDFillFactor70
        PRIMARY KEY CLUSTERED ( PageSplitNewIDFillFactor70_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 70 )
    ON  [PRIMARY];

    ALTER TABLE dbo.PageSplitNewIDFillFactor70
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor70__PageSplitNewIDFillFactor70_PK
        DEFAULT NEWID() FOR PageSplitNewIDFillFactor70_PK;

    ALTER TABLE dbo.PageSplitNewIDFillFactor70
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor70__Foo
        DEFAULT CONVERT( VARBINARY( 512 ), REPLICATE( 0x01, 512 ) ) FOR Foo;

    SET NOCOUNT ON;
    DECLARE @i                      INTEGER = 0;
    WHILE ( @i < 2500000 )
    BEGIN
        INSERT INTO dbo.PageSplitNewIDFillFactor70 DEFAULT VALUES;
        SET @i = @i + 1;
    END;
    SET NOCOUNT OFF;
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'PageSplitNewIDFillFactor65'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.PageSplitNewIDFillFactor65;
    CREATE TABLE dbo.PageSplitNewIDFillFactor65
    (
        PageSplitNewIDFillFactor65_PK
                                UNIQUEIDENTIFIER NOT NULL,
        Foo                     VARBINARY( 512 ) NOT NULL
    );

    ALTER TABLE dbo.PageSplitNewIDFillFactor65
    ADD CONSTRAINT PK__PageSplitNewIDFillFactor65
        PRIMARY KEY CLUSTERED ( PageSplitNewIDFillFactor65_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 65 )
    ON  [PRIMARY];

    ALTER TABLE dbo.PageSplitNewIDFillFactor65
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor65__PageSplitNewIDFillFactor65_PK
        DEFAULT NEWID() FOR PageSplitNewIDFillFactor65_PK;

    ALTER TABLE dbo.PageSplitNewIDFillFactor65
    ADD CONSTRAINT DF__PageSplitNewIDFillFactor65__Foo
        DEFAULT CONVERT( VARBINARY( 512 ), REPLICATE( 0x01, 512 ) ) FOR Foo;

    SET NOCOUNT ON;
    DECLARE @i                      INTEGER = 0;
    WHILE ( @i < 2500000 )
    BEGIN
        INSERT INTO dbo.PageSplitNewIDFillFactor65 DEFAULT VALUES;
        SET @i = @i + 1;
    END;
    SET NOCOUNT OFF;
END;
GO
Run Code Online (Sandbox Code Playgroud)

对于初始播种后的索引密度,使用以下查询:

SELECT  si.name, sips.avg_fragment_size_in_pages, 
        sips.page_count, sips.fragment_count, 
        sips.avg_fragmentation_in_percent,
        sips.avg_page_space_used_in_percent
FROM    sys.indexes si
CROSS APPLY sys.dm_db_index_physical_stats( DB_ID(), si.object_id, si.index_id, DEFAULT, 'DETAILED' ) sips 
WHERE   si.name IN ( 'PK__PageSplitIdentity', 'PK__PageSplitNewID', 
            'PK__PageSplitNewIDFillFactor75', 'PK__PageSplitNewIDFillFactor70', 'PK__PageSplitNewIDFillFactor65' )
    AND sips.index_level = 0;
Run Code Online (Sandbox Code Playgroud)

以下是密度结果(请注意最后一列中 s 的 ~69% UNIQUEIDENTIFIER):

种子密度

正如您所预料的那样,非顺序索引在 2.5MM 插入并且无需维护之后变得非常非常碎片化。继续通过检查我们之前设置的扩展事件来检查发生的页面拆分,我们可以使用以下查询:

SELECT  si.name, sc.split_count, si.fill_factor
FROM (  SELECT  allocation_unit_id = slot.value( '( value )[ 1 ]', 'BIGINT' ),
                split_count = slot.value( '( @count )[ 1 ]', 'BIGINT' )
        FROM (  SELECT  target_data = CONVERT( XML, target_data ) 
                FROM    sys.dm_xe_sessions xs
                INNER JOIN sys.dm_xe_session_targets xst
                    ON xs.address = xst.event_session_address
                WHERE   xs.name = 'TrackPageSplits'
                    AND xst.target_name = 'histogram' ) s
        CROSS APPLY s.target_data.nodes( 'HistogramTarget/Slot' ) n ( slot ) ) sc
INNER JOIN sys.allocation_units sau
    ON  sc.allocation_unit_id = sau.allocation_unit_id
INNER JOIN sys.partitions sp
    ON  sau.container_id = sp.partition_id
INNER JOIN sys.indexes si
    ON  sp.object_id = si.object_id
    AND sp.index_id = si.index_id;
Run Code Online (Sandbox Code Playgroud)

结果?

种子分裂

好的集群有 13 个页面拆分,每个非顺序页面拆分超过 12500 个。这在所有非顺序情况下都是不好的,但正如最初提到的,维护索引是完成这项工作的重要部分,所以让我们清理它们。

ALTER INDEX PK__PageSplitIdentity
    ON  dbo.PageSplitIdentity REBUILD;

ALTER INDEX PK__PageSplitNewID
    ON  dbo.PageSplitNewID REBUILD;

ALTER INDEX PK__PageSplitNewIDFillFactor75
    ON  dbo.PageSplitNewIDFillFactor75 REBUILD;

ALTER INDEX PK__PageSplitNewIDFillFactor70
    ON  dbo.PageSplitNewIDFillFactor70 REBUILD;

ALTER INDEX PK__PageSplitNewIDFillFactor65
    ON  dbo.PageSplitNewIDFillFactor65 REBUILD;
Run Code Online (Sandbox Code Playgroud)

现在:

保养后

好的,有了新的索引,页面分割现在已经消失了,每个表的碎片基本上是相同的,并且它们符合它们的FILL_FACTOR标准。由于大小和密度差异的原因,非顺序表会给 I/O 带来比顺序表更大的压力,但其性能仍然可以接受。现在我们可以模拟对这些表的一些负载,看看与页面拆分相关的性能问题是否可能首先克服集群表的好处:

SET NOCOUNT ON;
DECLARE @i                      INTEGER = 0;
WHILE ( @i < 50000 )
BEGIN
    INSERT INTO dbo.PageSplitIdentity DEFAULT VALUES;
    SET @i = @i + 1;
END;
SET NOCOUNT OFF;
GO

SET NOCOUNT ON;
DECLARE @i                      INTEGER = 0;
WHILE ( @i < 50000 )
BEGIN
    INSERT INTO dbo.PageSplitNewID DEFAULT VALUES;
    SET @i = @i + 1;
END;
SET NOCOUNT OFF;
GO

SET NOCOUNT ON;
DECLARE @i                      INTEGER = 0;
WHILE ( @i < 50000 )
BEGIN
    INSERT INTO dbo.PageSplitNewIDFillFactor75 DEFAULT VALUES;
    SET @i = @i + 1;
END;
SET NOCOUNT OFF;
GO

SET NOCOUNT ON;
DECLARE @i                      INTEGER = 0;
WHILE ( @i < 50000 )
BEGIN
    INSERT INTO dbo.PageSplitNewIDFillFactor70 DEFAULT VALUES;
    SET @i = @i + 1;
END;
SET NOCOUNT OFF;
GO

SET NOCOUNT ON;
DECLARE @i                      INTEGER = 0;
WHILE ( @i < 50000 )
BEGIN
    INSERT INTO dbo.PageSplitNewIDFillFactor65 DEFAULT VALUES;
    SET @i = @i + 1;
END;
SET NOCOUNT OFF;
Run Code Online (Sandbox Code Playgroud)

结果:

插入后

插入另外 50000 行后,唯一受页面拆分影响的表UNIQUEIDENTIFIERFILL_FACTOR 100. 这绝对可能会导致性能问题。然而,如图所示,任何其他测试FILLFACTOR级别都不会导致额外的页面拆分。实际上,在测试期间我开始在表格上出现页面分割之前,我实际上需要在表格中插入另一个 0.5MM dbo.PageSplitNewIDFillFactor75,而且无论如何,这个长度都超过了数学阈值。

所以,重申一下,显然,如果可以选择一个精简的、顺序的聚类键,那么它是最好的选择。然而,正如我最初所说,如果负责任地完成,没有更好的选择,对非顺序键进行聚类是完全可行的行动方案。您所说的性能问题不是页面拆分问题,而是堆上的前向提取问题,可以通过添加聚集索引来解决。所以添加一个。如果您的堆有一个非聚集主键,并且UNIQUEIDENTIFIER正在整个软件套件、集群中使用FILL_FACTOR 70,那么从那里进行测量和调整 - 您花在救火和解释性能指标上的时间越多,时间就越少你必须将模型调整为更合适的形式。

不要忘记放弃您的延长活动会话!

DROP EVENT SESSION [TrackPageSplits]
ON  SERVER;
GO
Run Code Online (Sandbox Code Playgroud)