索引唯一性开销

Sol*_*tix 14 sql-server database-internals index-tuning unique-constraint

我一直在与我办公室的各种开发人员就索引的成本以及唯一性是否有益或成本高昂(可能两者兼而有之)进行持续辩论。问题的关键是我们的竞争资源。

背景

我之前读过一个讨论,该讨论指出Unique索引不需要额外的维护成本,因为Insert操作会隐式检查它适合 B 树的位置,并且如果在非唯一索引中发现重复项,则将唯一符附加到键的末尾,否则直接插入。在这一系列事件中,Unique索引没有额外成本。

我的同事反对这种说法,他说这Unique是在寻找 B 树中的新位置之后作为第二个操作强制执行的,因此比非唯一索引维护成本更高。

最坏的情况是,我见过带有标识列(固有唯一)的表,它是表的集群键,但明确声明为非唯一。最糟糕的另一方面是我对唯一性的痴迷,所有索引都被创建为唯一的,当无法定义与索引的显式唯一关系时,我将表的 PK 附加到索引的末尾以确保唯一性得到保证。

我经常参与开发团队的代码审查,我需要能够为他们提供通用的指导方针。是的,应该评估每个索引,但是当您有五台服务器,每个服务器有数千个表,而一个表上的索引多达 20 个时,您需要能够应用一些简单的规则来确保一定的质量水平。

Insert与维护非唯一索引的成本相比,唯一性在后端是否有额外成本?其次,将表的主键附加到索引的末尾以确保唯一性有什么问题?

示例表定义

create table #test_index
    (
    id int not null identity(1, 1),
    dt datetime not null default(current_timestamp),
    val varchar(100) not null,
    is_deleted bit not null default(0),
    primary key nonclustered(id desc),
    unique clustered(dt desc, id desc)
    );

create index
    [nonunique_nonclustered_example]
on #test_index
    (is_deleted)
include
    (val);

create unique index
    [unique_nonclustered_example]
on #test_index
    (is_deleted, dt desc, id desc)
include
    (val);
Run Code Online (Sandbox Code Playgroud)

例子

我将Unique键添加到索引末尾的一个例子是在我们的一个事实表中。有一个Primary Key那是一Identity列。但是,它Clustered Index是分区方案列,后跟三个没有唯一性的外键维度。这个表上的选择性能非常糟糕,我经常使用Primary Key键查找来获得更好的寻道时间,而不是利用Clustered Index. 其他遵循类似设计但将Primary Key附加到末尾的表具有明显更好的性能。

-- date_int is equivalent to convert(int, convert(varchar, current_timestamp, 112))
if not exists(select * from sys.partition_functions where [name] = N'pf_date_int')
    create partition function 
        pf_date_int (int) 
    as range right for values 
        (19000101, 20180101, 20180401, 20180701, 20181001, 20190101, 20190401, 20190701);
go

if not exists(select * from sys.partition_schemes where [name] = N'ps_date_int')
    create partition scheme 
        ps_date_int
    as partition 
        pf_date_int all 
    to 
        ([PRIMARY]);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.bad_fact_table'))
    create table dbo.bad_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        fk_id int not null,
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_bad_fact_table] clustered (date_int, group_id, group_entity_id, fk_id)
        )
    on ps_date_int(date_int);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.better_fact_table'))
    create table dbo.better_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_better_fact_table] clustered(date_int, group_id, group_entity_id, id)
        )
    on ps_date_int(date_int);
go
Run Code Online (Sandbox Code Playgroud)

Han*_*non 16

我经常参与开发团队的代码审查,我需要能够为他们提供通用的指导方针。

我目前参与的环境有 250 台服务器和 2500 个数据库。我曾在具有30,000 个数据库的系统上工作过。索引指南应该围绕命名约定等,而不是将哪些列包含在索引中的“规则”——每个单独的索引都应该被设计为适合该特定业务规则或接触表的代码的正确索引。

Insert与维护非唯一索引的成本相比,唯一性在后端是否有额外成本?其次,将表的主键附加到索引的末尾以确保唯一性有什么问题?

将主键列添加到非唯一索引的末尾,使其在我看来是一种反模式。如果业务规则规定数据应该是唯一的,则向列添加唯一约束;这将自动创建一个唯一索引。如果您要为性能索引一列,为什么要向索引添加一列?

即使您认为强制唯一性不会增加任何额外开销的假设是正确的(在某些情况下并非如此),您通过不必要地使索引复杂化来解决什么问题?

在将主键添加到索引键的末尾以便您可以使索引定义包含UNIQUE修饰符的特定实例中,它实际上与磁盘上的物理索引结构零差异。这是由于 B 树索引键结构的性质,因为它们总是需要是唯一的。

正如大卫布朗在评论中提到的:

由于每个非聚集索引都存储为唯一索引,因此插入唯一索引不会产生额外成本。事实上,唯一的额外成本是未能将候选键声明为唯一索引,这将导致聚集索引键被附加到索引键。

采取以下最低限度完整且可验证的示例

USE tempdb;

DROP TABLE IF EXISTS dbo.IndexTest;
CREATE TABLE dbo.IndexTest
(
    id int NOT NULL
        CONSTRAINT IndexTest_pk
        PRIMARY KEY
        CLUSTERED
        IDENTITY(1,1)
    , rowDate datetime NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

我将添加两个相同的索引,除了在第二个索引键定义的尾端添加主键:

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);
Run Code Online (Sandbox Code Playgroud)

接下来,我们将几行添加到表中:

INSERT INTO dbo.IndexTest (rowDate)
VALUES (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 1, GETDATE()))
     , (DATEADD(SECOND, 2, GETDATE()));
Run Code Online (Sandbox Code Playgroud)

如上所示,三行包含相同的rowDate列值,两行包含唯一值。

接下来,我们将使用未记录的DBCC PAGE命令查看每个索引的物理页面结构:

DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE @indexid int;

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix01'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix02'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
Run Code Online (Sandbox Code Playgroud)

我已经使用 Beyond Compare 查看了输出,除了围绕分配页 ID 等的明显差异外,两个索引结构是相同的。

在此处输入图片说明

您可能认为上面的意思是在每个索引中包含主键,并将 at 定义为唯一性是一件好事™,因为无论如何这都是幕后发生的事情。我不会做出这样的假设,并且建议仅在索引中的自然数据实际上已经是唯一的情况下才将索引定义为唯一的。

Interwebz 中有几个关于这个主题的优秀资源,包括:

仅供参考,仅存在一identity列并不能保证唯一性。您需要将该列定义为主键或具有唯一约束,以确保该列中存储的值实际上是唯一的。该SET IDENTITY_INSERT schema.table ON;语句将允许您将非唯一值插入到定义为 的列中identity


Che*_*ain 5

只是Max 出色答案的附加内容。

在创建非唯一聚集索引时,SQL ServerUniquifier无论如何都会在后台创建一个叫做 a 的东西。

Uniquifier如果您的平台有很多 CRUD 操作,这可能会在将来导致潜在的问题,因为这Uniquifier只有 4 个字节大(一个基本的 32 位整数)。所以,如果你的系统有很多 CRUD 操作,你可能会用完所有可用的唯一编号,突然间你会收到一个错误,它不允许你在表中插入更多数据(因为它会不再有任何唯一值分配给新插入的行)。

发生这种情况时,您将收到此错误:

The maximum system-generated unique value for a duplicate group 
was exceeded for index with partition ID (someID). 

Dropping and re-creating the index may resolve this;
otherwise, use another clustering key.
Run Code Online (Sandbox Code Playgroud)

uniquifier一组非唯一键消耗超过 2,147,483,647 行时,会出现错误 666(上述错误)。

因此,您需要为单个键值设置约 20 亿行,或者您需要修改单个键值约 20 亿次才能看到此错误。因此,您不太可能遇到此限制。