很好的问题,因为它是一个如此重要的概念。不过,这是一个大话题,我将向您展示的是一个简化,以便您可以理解基本概念。
首先,当您看到聚簇索引思维表时。在SQL Server中,如果表不包含聚集索引,则它是堆。在表上创建聚簇索引实际上会将表转换为b树型结构。您的聚集索引就是您的表,它与表不是分开的
有没有想过为什么您只能有一个聚集索引?好吧,如果我们有两个聚集索引,我们将需要该表的两个副本。它毕竟包含数据。
我将尝试通过一个简单的例子来解释这一点。
注意:我在此示例中创建了表,并在其中填充了超过300万个随机条目。然后运行实际查询并在此处粘贴执行计划。
您真正需要掌握的是O符号或操作效率。假设您有下表。
CREATE TABLE [dbo].[Customer](
[CustomerID] [int] IDENTITY(1,1) NOT NULL,
[CustomerName] [varchar](100) NOT NULL,
[CustomerSurname] [varchar](100) NOT NULL,
CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED
(
[CustomerID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
, IGNORE_DUP_KEY = OFF,ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
因此,这里有一个基本表,在CustomerID上有一个集群键(默认情况下,主键是集群的)。因此,基于主键CustomerID对表进行了排列/排序。中间级别将包含CustomerID值。数据页将包含整行,因此它是表行。
我们还将在CustomerName字段上创建非聚集索引。下面的代码可以做到这一点。
CREATE NONCLUSTERED INDEX [ix_Customer_CustomerName] ON [dbo].[Customer]
(
[CustomerName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF
, DROP_EXISTING = OFF, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
因此,在此索引中,您将在数据页/叶级别节点上找到指向聚簇索引中的中间级别的指针。该索引围绕CustomerName字段进行排列/排序。因此,中间级别包含CustomerName值,叶级别包含指针(这些指针值实际上是主键值或CustomerID列)。
正确,所以如果我们执行以下查询:
SELECT * FROM Customer WHERE CustomerID = 1
SQL可能会通过搜索操作读取聚集索引。查找操作是一种二进制搜索,它比顺序搜索的扫描要有效得多。因此,在上面的示例中,读取了索引,并且通过使用二进制搜索SQL可以消除与我们要查找的条件不匹配的数据。有关查询计划,请参见所附的屏幕截图。
因此,查找操作的操作数或O表示法如下:
- 通过将搜索到的值与中间级别的值进行比较,对聚簇索引执行二进制搜索。
- 返回匹配的值(请记住,因为聚集索引中包含所有数据,所以可以返回索引中的所有列,因为它是行数据)
因此,这是两个操作。但是,如果我们执行以下查询:
SELECT * FROM Customer WHERE CustomerName ='John'
现在,SQL将使用CustomerName上的非聚集索引进行搜索。但是,由于这是非聚集索引,因此它不包含该行中的所有数据。
因此,SQL将在中间级别上进行搜索以找到匹配的记录,然后使用返回的值进行查找,以对聚簇索引(又名表)进行另一次搜索以检索实际数据。我知道这听起来令人困惑,但请继续阅读,一切都会变得清楚。
由于我们的非聚集索引仅包含CustomerName字段(存储在中间节点中的索引字段值)和指向作为CustomerID的数据的指针,因此该索引没有CustomerSurname的记录。必须从聚簇索引或表中获取CustomerSurname。
运行此查询时,我得到以下执行计划:
在上面的屏幕截图中,有两点需要您注意
- SQL表示我缺少索引(绿色文本)。SQL建议我在CustomerName上创建一个索引,其中包括CustomerID和CustomerSurname。
- 您还将看到查询的99%的时间都花在了对主键索引/聚集索引的键查找上。
为什么SQL再次建议在CustomerName上建立索引?好吧,由于索引仅包含CustomerID,而CustomerName SQL仍必须从表/聚集索引中查找CustomerSurname。
如果创建索引并将索引中包含CustomerSurname列,则SQL只需读取非聚集索引即可满足整个查询。这就是为什么SQL建议我更改非聚集索引的原因。
在这里,您可以看到SQL从集群键中获取CustomerSurname列所需执行的额外操作
因此,操作数如下:
- 通过将搜索到的值与中间级别的值进行比较,对非聚集索引执行二进制搜索
- 对于匹配的节点,请读取叶级节点,该节点将包含聚集索引中数据的指针(叶级节点将包含主键值)。
- 对于返回的每个值,请在聚集索引(表)上进行读取以获取行值,在此我们将读取CustomerSurname。
- 返回匹配的行
这是获取值的4个操作。与读取聚簇索引相比,所需的操作量增加了两倍。向您显示,聚集索引是包含所有数据的最强大的索引。
因此,仅澄清最后一点。为什么我说非聚集索引中的指针是主键值?很好地证明了非聚集索引的叶级节点包含主键值,我将查询更改为:
SELECT CustomerID
FROM Customer
WHERE CustomerName='Jane'
在此查询中,SQL可以从非聚集索引读取CustomerID。它不需要对聚集索引进行查找。您可以通过如下所示的执行计划看到。
注意此查询与上一个查询之间的区别。没有查找。SQL可以在非聚集索引中找到所有数据
希望您能开始理解聚集索引是表,而非聚集索引不包含所有数据。由于可以执行二进制搜索,但只有聚集索引包含所有数据,因此索引将加快选择速度。因此,对非聚集索引的搜索几乎总是会导致从聚集索引中加载值。这些额外的操作使非聚集索引的效率低于聚集索引的效率。
希望这可以清除一切。如果没有任何意义,请发表评论,我将尝试澄清。在这里已经很晚了,我的大脑感觉有点扁平。该换红色公牛了。