索引唯一性开销


14

我一直在与我的办公室中的各种开发人员就索引的成本以及唯一性是有利还是昂贵(可能两者都有)进行辩论。问题的症结在于我们的竞争资源。

背景

之前,我曾读过一篇讨论,其中指出Unique索引并不需要额外维护,因为Insert操作会隐式地检查索引是否适合B树,并且如果在非唯一索引中找到重复项,则会在其后附加一个唯一化符。键的结尾,否则直接插入。在此事件序列中,Unique索引没有附加成本。

我的同事通过说这Unique是在寻求B树中的新职位之后强制执行的第二项操作来抗衡此声明,因此,与非唯一索引相比,维护成本更高。

最糟糕的是,我看到了带有标识列(本质上是唯一的)的表,该列是表的集群键,但明确地表示为非唯一。最糟糕的是我对唯一性的痴迷,并且所有索引都被创建为唯一,并且当不可能定义与索引的显式唯一关系时,我将表的PK附加到索引的末尾以确保唯一性得到保证。

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

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);

为什么将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

Answers:


16

我经常参与开发团队的代码审查,并且我需要能够提供一些一般性的指导方针,以使他们可以遵循。

我目前参与的环境有250个服务器和2500个数据库。我已经在具有30,000个数据库的系统上工作。索引编制准则应围绕命名约定等展开,而不是将哪些列包括在索引中的“规则”- 每个单独的索引都应被设计为适合该特定业务规则或与表格联系的代码的正确索引。

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

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

即使您认为执行唯一性不会增加任何额外开销的假设是正确的(在某些情况下也不是),那么通过不必要地使索引复杂化又可以解决什么呢?

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

正如David Browne在评论中提到的那样:

由于每个非聚集索引都存储为唯一索引,因此插入唯一索引不会产生任何额外费用。实际上,唯一的额外成本是无法将候选关键字声明为唯一索引,这将导致将聚集的索引关键字附加到索引关键字。

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

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
);

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

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);

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

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()));

如上所示,三行包含相同的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 *****************************************';

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

在此处输入图片说明

您可能会认为上述含义意味着在每个索引中都包含主键,并且将A Good Thing™定义为唯一,因为无论如何都是如此。我不会做这个假设,如果实际上索引中的自然数据已经是唯一的,则建议仅将索引定义为唯一。

Interwebz中有许多关于该主题的优秀资源,包括:

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


5

只是Max出色答案的一个附加组件

当涉及到创建非唯一的聚集索引时,SQL Server Uniquifier无论如何都会在后台创建一个称为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.

错误666(以上错误)发生在uniquifier一组非唯一键的占用多于2,147,483,647行时,。

因此,对于单个键值,您将需要〜20亿行,或者需要对单个键值进行约20亿次修改才能看到此错误。因此,您不太可能会遇到此限制。


我不知道隐藏的uniquifier可能会耗尽键空间,但是我想在某些情况下所有东西都是有限的。就像如何CaseIf结构限制为10级,这是有道理的,也有解决非唯一实体的限制。根据您的陈述,这听起来像只适用于聚类键不唯一的情况。这是一个问题吗?Nonclustered Index或者如果聚簇键是索引,Unique那么这没有问题Nonclustered吗?
Solonotix

(据我所知)唯一索引受列类型的大小限制(因此,如果它是BIGINT类型,则可以使用8个字节)。另外,根据Microsoft的官方文档,聚集索引的最大容量为900bytes,非聚集索引的最大容量为1700bytes(因为每个表可以有多个非聚集索引,并且只有一个聚集索引)。docs.microsoft.com/en-us/sql/sql-server/…–
Chessbrain

1
@Solonotix-来自非聚集索引的聚集索引的唯一化符。如果在没有主键的情况下运行示例中的代码(而是创建聚簇索引),则可以看到非唯一索引和唯一索引的输出相同。
Max Vernon

-2

我不会考虑索引是否应该唯一以及该方法是否还有更多开销的问题。但是有些事情困扰着您的总体设计

  1. dt datetime不为null默认值(current_timestamp)。Datetime是一种较旧的格式,您可以使用datetime2()和sysdatetime()至少节省一些空间。
  2. 在#test_index(is_deleted)include(val)上创建索引[nonunique_nonclustered_example]。这困扰着我。看一下如何访问数据(我敢打赌,还有更多WHERE is_deleted = 0),并看一下使用过滤索引的方法。我什至会考虑使用2个过滤索引,一个用于where is_deleted = 0另一个where is_deleted = 1

从根本上讲,这看起来更像是用来测试假设而不是实际问题/解决方案的编码练习,但是,这两种模式绝对是我在代码审查中所需要的。


使用datetime2而不是datetime最多可以节省1个字节,也就是说,如果您的精度小于3,则意味着精度会降低几分之一秒,这并不总是可行的解决方案。至于所提供的示例索引,该设计保持简单易行,以专注于我的问题。甲Nonclustered索引将具有附加到用于在内部键查找数据行的末端聚集键。因此,这两个索引在物理上是相同的,这就是我的问题。
Solonotix

从规模上讲,我们会迅速节省一两个字节。而且我以为,由于您使用的日期时间不精确,因此我们可以降低精度。对于索引,我再次声明,将位列作为索引的前导列是一种我认为不佳的选择。与所有事物一样,您的行驶里程可能会有所不同。las,近似模型的缺点。
Toby

-4

看起来您只是使用PK来创建备用较小的索引。因此,它的性能更快。

您可以在拥有大量数据表(例如主数据表)的公司中看到这一点。有人决定在其上建立一个大型的聚集索引,以期望它可以满足各个报告组的需求。

但是,一组可能只需要该索引的几个部分,而另一组则需要其他部分..因此,仅在太阳下的每一列中拍一下索引以“优化性能”并没有真正的帮助。

同时,将其分解以创建多个较小的目标索引通常可以解决该问题。

而且,这似乎就是您在做什么。您拥有性能如此糟糕的海量聚集索引,然后使用PK创建具有更少列(性能更好)的另一个索引。

因此,只需进行分析并弄清楚是否可以采用单个聚簇索引并将其分解为特定作业所需的较小的目标索引。

然后必须从“单索引与多索引”的角度分析性能,因为创建和更新索引会产生开销。但是,您必须从整体角度进行分析。

EG:对于一个大型聚簇索引来说,它的资源消耗可能较小,而对于多个较小的目标索引,它的资源消耗却更多。但是,如果您随后能够在后端更快地运行目标查询,并在那里节省时间(和金钱),那可能是值得的。

因此,您必须进行端到端分析。不仅要看它如何影响您自己的世界,还要看它如何影响最终用户。

我只是觉得您在滥用PK标识符。但是,您可能正在使用仅允许1个索引(?)的数据库系统,但是如果您使用PK,则可以潜入另一个数据库系统(如今,每个关系数据库系统似乎都自动为PK编制索引)。但是,大多数现代RDBMS都应允许创建多个索引。可以建立的索引数量应该没有限制(而不是1 PK的限制)。

因此,通过使PK whiwhih就像alt索引一样起作用..您正在耗尽PK,如果以后扩展该表的作用,则可能需要用到它。

这并不是说您的表不需要PK。SOPDB的101表示“每个表都应具有PK”。但是,在数据仓库等情况下,在表上具有PK可能只是多余的开销,而您并不需要这些开销。或者,确保不重复添加欺骗条目可能是天赐良机。这实际上与您在做什么以及为什么这么做有关。

但是,海量表无疑从拥有索引中受益。但是,假设单个大型聚集索引将是最好的……可能是最好的..但是我建议在一个测试环境中进行测试,将索引分为多个针对特定用例场景的较小索引。

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.