非集群主键困境

Nonclustered primary key dilemma

假设我们必须为 Whosebug 问题定义最佳索引。但是,我们不要采用实际 Posts table 的架构,我们只包括那些实际相关的列:

create table Posts (
    Id int not null
        identity,
    PostTypeId tinyint not null,
    LastActivityDate datetime not null
        default getdate(),
    Title nvarchar(500) null, -- answers don't have titles
    Body nvarchar(max) not null,
    ...
)

我添加了 Id 作为标识,尽管 Data Stackexchange shows table 中的 none 对它们有主键约束,也没有标识列。有很多 unique/non-unique clustered/non-clustered 个索引。

使用场景

所以基本上帖子的两个主要场景:

  1. 它们按时间顺序按 LastActivityDate 列降序显示(或者可能 LastEditDate 我没有在上面包括因为它不是那么重要)
  2. 它们单独显示在问题详情中
  3. 答案按投票顺序显示在问题详细信息页面上(ScoreCount 列不是我上面代码的一部分)

索引优化

在上述情况下最好创建哪些索引,特别是如果我们说 #1 是最常见的情况,因此它必须非常快地工作。

我想说更好的可能性之一是创建这些索引:

-- index 1
alter table Posts
add primary key nonclustered (Id);

-- index 2
create clustered index IX_Posts_LastActivityDate
on Posts(LastActivityDate desc);

-- index 3
create index IX_Posts_ParentId
on Posts(ParentId, PostTypeId)
include (ScoreCount);

这样我们基本上得到了三个索引,其中第二个是聚类的。

因此,为了让 #1 真正快速地工作,我在 LastActivityDate 列上设置了聚簇索引,因为当我们对它们进行范围比较时,聚簇索引特别有用。我们将按时间顺序从最新到最旧对问题进行排序,因此我设置了排序方向并且还在聚集索引中包含了类型。

那么我们用这个解决了什么?

  1. 场景#1 被索引 2 非常有效地覆盖,因为它是聚集的并且完全覆盖;我们还可以轻松高效地进行结果分页;
  2. 场景 #2 在某种程度上被唯一索引 1(获取问题)和非唯一索引 3 覆盖,以获取按 ScoreCount 排序的所有相关答案(场景 #3);如果我们决定按时间顺序排列也包含在索引 2 中的答案;

问题 1

SQL 内部结构使得 SQL 隐式地将聚集键添加到非聚集索引 以便它可以在行存储中定位记录。

因为我还在 table 上添加了一个 非集群主键 (这在设计上必须是唯一的),我想知道是否 SQL 仍将在聚集非唯一索引上提供自己的唯一键,或者 它将使用非聚集主键来唯一标识每个记录 吗?

问题 2

因此,如果不使用主键来定位行存储(聚集索引)上的记录,那么实际创建 PK 是否有意义?在这种情况下,这样做会更好吗?

create unique index UX_Posts_Id
on Posts(Id);
-- include (Title, Body, ScoreCount);

最好也包括注释掉的列,但那样会使该索引效率低下,因为它在缓存方面会更糟...为什么我要问是否创建该索引而不是创建该索引会更好primary key 约束是因为我们可以在此索引中包含其他非键列,而当我们添加内部生成唯一索引的 PK 约束时我们不能这样做...

问题 3

我知道 LastActivityDate 更改是聚簇索引所不希望的,但我们必须考虑这样一个事实,即此列在变得或多或少静态之前更有可能更改一段时间,所以它不应该导致过多的索引碎片,因为每当 LastActivityDate 发生变化时,记录大部分都会附加到末尾。一些任意页面上的索引碎片永远不会发生,因为一些新记录将被插入到一些旧的(呃)页面中,因为 LastActivityDate 只会增加。因此,大多数修改将发生在最后一页。

所以问题是这些更改是否有害,因为 LastActivityDate 不是集群索引键的最佳候选者:

Since I've also added a nonclustered primary key on the table (which must by design be unique), I would like to know whether SQL will still supply its own unique key on clustered non-unique index or will it use nonclustered primary key to uniquely identify each records instead?

SQL 当给定的非唯一聚集索引键值不唯一时,服务器添加一个 4 字节 "uniqueifier"。所有非聚集索引叶节点(包括主键)都将包括 LastActivityDate 加上唯一标识符(如果存在)作为行定位符。此处仅对于具有相同 LastActivityDate 的帖子才需要内部唯一标识符,因此我预计实际上需要唯一标识符的行相对较少。

So if primary key isn't used to locate records on row store (clustered index) does it even make sense to actually create a PK? Would in this case be better to rather do this?

从数据建模的角度来看,每个关系 table 都应该有主键。可以根据优化性能的需要将隐式创建的索引声明为聚簇或非聚簇。如果 LastActivity 是性能更好的选择,那么主键索引必须是非聚集的。这个主键索引将提供检索单例帖子所需的索引。

不幸的是,SQL服务器没有提供一种方法来指定主键和唯一约束定义中包含的列。在这种情况下,可以改变规则并使用唯一索引而不是声明的主键约束,以避免冗余索引的成本和包含列的好处。唯一索引在功能上等同于主键,可以被外键约束引用。

So the question is whether these changes can be harmful as LastActivityDate isn't the best candidate for clustering index key

LastActivityDate 无论精度水平如何(禁止单线程插入或重试逻辑),都永远不能保证是唯一的。一种方法是 LastActivityDateId 上的复合主键。需要使用这两个值来检索个别帖子。这将消除对先前讨论的单独唯一索引 Id 的需要。

我对 LastActivityDate 作为最左边的聚簇索引键列的最大担忧是,它可能会因最近的帖子而经常更改。这将需要大量的行移动来维护逻辑键顺序,与当前静态 Id 键相比可能会显着影响并发性,并且需要在每次更改时更新非聚集索引行定位器值。因此,即使这个聚簇索引键可能是许多查询的最佳选择,但在高度事务性系统上的其他成本可能会超过收益。