本文是“Stairway系列:SQL Server索引的阶梯”的一部分
索引是数据库设计的基础,并告诉开发人员使用数据库关于设计者的意图。不幸的是,当性能问题出现时,索引往往被添加为事后考虑。这里最后是一个简单的系列文章,应该使他们快速地使任何数据库专业人员“快速”
这个阶段的前面的层次提供了一般索引和非聚集索引的概述。它以下面关于SQL Server索引的关键概念结束。当请求到达您的数据库时,无论是SELECT语句还是INSERT,UPDATE或DELETE语句,SQL Server都只有三种可能的方式来访问语句中引用的表的数据:
?只访问非聚集索引并避免访问表。这只能在索引包含查询请求的这个表的所有数据时才有可能
?使用搜索键访问索引,然后使用选定的书签访问表中的单个行。
?忽略索引并在表中搜索请求的行。
这个级别的重点是上面列表中的第三个选项。搜索桌子。这反过来又会引导我们讨论聚集索引。在第二级提到但没有涉及的主题。
我们将在此级别使用的主要AdventureWorks数据库表是SalesOrderDetail表。在121,317行,它足以说明在表上有聚集索引的一些好处。而且,有两个外键,足以说明一些关于聚簇索引的设计决策。
示例数据库
尽管我们已经讨论过一级的样本数据库,但是这个时候还是要重复的。 在整个这个阶段,我们将用例子来说明概念。 这些示例基于Microsoft AdventureWorks示例数据库。 我们专注于销售订单。 五张表将给我们交易和非交易数据的良好组合; Customer,SalesPerson,Product,SalesOrderHeader和SalesOrderDetail。 为了保持重点,我们使用列的一个子集。 由于AdventureWorks规范化很好,销售人员信息被分解为三个表:SalesPerson,Employee和Contact。
在整个阶梯中,我们使用以下两个术语来交换订单上的单行:“订单项”和“订单明细”。 前者是更常见的业务术语; 后者出现在AdventureWorks表的名字内。
图1显示了一整套表格及其之间的关系。
图1:这个Stairway的例子中使用的表
注意:
在这个楼梯级别显示的所有TSQL代码可以与文章一起下载。
聚集索引
我们首先提出以下问题:如果不使用非聚集索引,需要多少工作才能在表中找到一行?在表中搜索请求的行意味着扫描无序表中的每一行吗?或者,SQL Server可以永久性地对表中的行进行排序,以便通过搜索关键字快速访问它们,就像通过搜索关键字快速访问非聚集索引的条目一样?答案取决于您是否指示SQL Server在表上创建聚簇索引。
与非聚簇索引是一个独立的对象并占用他们自己的空间不同,聚簇索引和表是一样的。通过创建聚集索引,可以指示SQL Server将表中的行排序为索引键序列,并在将来的数据修改期间维护该序列。即将到来的级别将查看生成的内部数据结构来完成此操作。但现在,把聚簇索引看作是一个有序表。给定一个行的索引键值,SQL Server可以快速访问该行;并可以从该行按顺序进行。
为了演示目的,我们创建了示例表SalesOrderDetail的两个副本;一个没有索引,一个有聚集索引。关于索引的关键字段,我们做出与AdventureWorks数据库的设计者做出相同的选择:SalesOrderID / SalesOrderDetailID。清单1中的代码创建了SalesOrderDetail表的副本。我们可以随时重新运行这个代码,我们希望从一个“干净的石板”开始。
IF EXISTS (SELECT * FROM sys.tables
WHERE OBJECT_ID = OBJECT_ID(‘dbo.SalesOrderDetail_index‘))
DROP TABLE dbo.SalesOrderDetail_index;
GO
IF EXISTS (SELECT * FROM sys.tables
WHERE OBJECT_ID = OBJECT_ID(‘dbo.SalesOrderDetail_noindex‘))
DROP TABLE dbo.SalesOrderDetail_noindex;
GO
SELECT * INTO dbo.SalesOrderDetail_index FROM Sales.SalesOrderDetail;
SELECT * INTO dbo.SalesOrderDetail_noindex FROM Sales.SalesOrderDetail;
GO
CREATE CLUSTERED INDEX IX_SalesOrderDetail
ON dbo.SalesOrderDetail_index (SalesOrderID, SalesOrderDetailID)
GO
清单1:创建SalesOrderDetail表的副本
因此,在创建聚集索引之前,假设SalesOrderDetail表如下所示:
SalesOrderID SalesOrderDetailID ProductID OrderQty UnitPrice
69389 102201 864 3 38.10
56658 59519 711 1 34.99
59044 70000 956 2 1430.442
48299 22652 853 4 44.994
50218 31427 854 8 44.994
53713 50716 711 1 34.99
50299 32777 739 1 744.2727
45321 6303 775 6 2024.994
72644 115325 873 1 2.29
48306 22705 824 4 141.615
69134 101554 876 1 120.00
48361 23556 760 3 469.794
53605 50098 888 1 602.346
48317 22901 722 1 183.9382
66430 93291 872 1 8.99
65281 90265 889 2 602.346
52248 43812 871 1 9.99
47978 20189 794 2 1308.9375
在创建如上所示的聚集索引之后,生成的表/聚集索引将如下所示:
SalesOrderID SalesOrderDetailID ProductID OrderQty UnitPrice
43668 106 722 3 178.58
43668 107 708 1 20.19
43668 108 733 3 356.90
43668 109 763 3 419.46
43669 110 747 1 714.70
43670 111 710 1 5.70
43670 112 709 2 5.70
43670 113 773 2 2,039.99
43670 114 776 1 2,024.99
43671 115 753 1 2,146.96
43671 116 714 2 28.84
43671 117 756 1 874.79
43671 118 768 2 419.46
43671 119 732 2 356.90
43671 120 763 2 419.46
43671 121 755 2 874.79
43671 122 764 2 419.46
43671 123 716 1 28.84
43671 124 711 1 20.19
43671 125 708 1 20.19
43672 126 709 6 5.70
43672 127 776 2 2,024.99
43672 128 774 1 2,039.99
43673 129 754 1 874.79
43673 130 715 3 28.84
43673 131 729 1 183.94
当您查看上面显示的示例数据时,您可能会注意到每个SalesOrderDetailID值都是唯一的。不要混淆; SalesOrderDetailID不是表的主键。 SalesOrderID / SalesOrderDetailID的组合是表的主键;以及聚簇索引的索引键。
了解群集索引的基础知识
聚簇索引键可以由您选择的任何列组成;它不必以主键为基础。在我们的例子中,最重要的是最左边的一列是一个外键,即SalesOrderID值。因此,销售订单的所有行项目都会在SalesOrderDetail表中连续出现。
请记住以下有关SQL Server聚簇索引的附加要点:
?由于聚簇索引的条目是表的行,聚簇索引条目中没有书签值。当SQL Server已经在一行时,它不需要一条信息告诉它在哪里找到那一行。
?聚簇索引始终覆盖查询。由于索引和表是一样的,表的每一列都在索引中。
?在表上创建聚簇索引不会影响在该表上创建非聚簇索引的选项。
选择聚集索引键列
每个表最多可以有一个聚簇索引。表格的行只能是一个序列。你需要决定什么样的顺序,如果有的话,对每个表最好;并在可能的情况下在表格填充数据之前创建聚集索引。在做出这个决定时,要记住排序不仅意味着排序,而且意味着分组;如按销售订单对订单项进行分组。
这就是为什么AdventureWorks数据库的设计者选择SalesOrderID内的SalesOrderDetailID作为SalesOrderDetail表的顺序的原因;这是订单项的自然顺序。
例如,如果用户请求订单的订单项,则通常会请求该订单的所有订单项。一个典型的销售订单表单告诉我们,订单的印刷版本总是包含所有的行项目。销售订单业务的性质是按销售订单对行项目进行分组。仓库偶尔会要求按产品而不是销售订单查看订单项,但大部分的要求;如销售人员或客户,打印发票的程序或计算每个订单总价值的查询;将需要所有销售订单的所有行项目。
然而,用户需求本身并不能决定什么是最好的聚集索引。本系列的未来级别将覆盖指标的内部;因为索引的某些内部方面也会影响你对聚簇索引列的选择。
堆
如果表中没有聚集索引,则该表称为堆。每个表都是一个堆或一个聚集索引。所以,虽然我们经常说每一个指标都属于聚类或非聚类两种类型之一,同样重要的是要注意,每张桌子都属于两种类型之一;它是一个聚集索引或它是一堆。开发人员经常说,一个表“有”或“没有”聚集索引,但更有意义的说,表“是”或“不是”聚集索引。
SQL Server在查找行(不包括使用非聚簇索引)时搜索堆只有一种方法,这是从表的第一行开始,然后继续执行表,直到读取所有行。没有序列,没有搜索键,也无法快速导航到特定的行。
比较聚簇索引和堆
为了评估聚簇索引与堆的性能,清单1创建了SalesOrderDetailtable的两个副本。一个副本是堆版本,另一个是创建原始表(SalesOrderID,SalesOrderDetailID)上的同一个聚集索引。这两个表都没有任何非聚集索引。
我们将对每个版本的表执行相同的三个查询;一个检索单个行,一个检索单个订单的所有行,一个检索单个产品的所有行。我们在下面的表格中给出了SQL和每个执行的结果。
我们的第一个查询检索单个行,执行细节显示在表1中。
SQL |
SELECT * FROM SalesOrderDetail WHERE SalesOrderID = 43671 AND SalesOrderDetailID = 120 |
Heap |
(1 row(s) affected) Table ‘SalesOrderDetail_noindex‘. Scan count 1, logical reads 1495. |
Clustered Index |
(1 row(s) affected) Table ‘SalesOrderDetail_noindex‘. Scan count 1, logical reads 3. |
Impact of having the Clustered Index |
IO reduced from 1495 reads to 3 reads. |
Comments |
No surprise. Table scanning 121,317 rows to find just one is not very efficient. |
表1:检索单行
我们的第二个查询检索单个销售订单的所有行,您可以在表2中看到执行的详细信息。
SQL |
SELECT * FROM SalesOrderDetail WHERE SalesOrderID = 43671 |
Heap |
(11 row(s) affected) Table ‘SalesOrderDetail_noindex‘. Scan count 1, logical reads 1495. |
Clustered Index |
(11 row(s) affected) Table ‘SalesOrderDetail_noindex‘. Scan count 1, logical reads 3. |
Impact of having the Clustered Index |
IO reduced from 1495 reads to 3 reads. |
Comments |
Same statistics as the previous query. The heap still required a table scan, while the clustered index grouped the 11 detail rows of the requested order sufficiently close together so that the IO required to retrieve 11 rows was the same as the IO required to retrieve one row. An upcoming Level will explain in detail why no additional reads were required to retrieve the additional 10 rows. |
表2:检索单个SalesOrder的所有行
我们的第三个查询检索单个产品的所有行,执行结果如表3所示。
SQL |
SELECT * FROM SalesOrderDetail WHERE ProductID = 755 |
Heap |
(228 row(s) affected) Table ‘SalesOrderDetail_noindex‘. Scan count 1, logical reads 1495. |
Clustered Index |
(228 row(s) affected) Table ‘SalesOrderDetail_index‘. Scan count 1, logical reads 1513. |
Impact of having the Clustered Index |
IO slightly greater for the clustered index version; 1513 reads versus 1495 reads. |
Comments |
Without a nonclustered index on the ProductID column to help find the rows for a single Product, both versions had to be scanned. Because of the overhead of having a clustered index, the clustered index version is the slightly larger table; therefore scanning it required a few more reads than scanning the heap. |
表3:检索单个产品的所有行
前两个查询大大受益于聚簇索引的存在;第三个是大致相等的。有时聚集索引是有害的吗?答案是肯定的,主要与插入,更新和删除行有关。像在这些早期阶段遇到的索引的很多其他方面一样,这也是一个更高级别更详细的主题。
一般来说,检索效益大于维护损害;使聚簇索引更适合堆。如果您要在Azure数据库中创建表,则别无选择。每个表都必须是聚簇索引。
结论
聚集索引是一个有序表,其顺序由您在创建索引时指定,并由SQL Server维护。根据其关键值,该表中的任何行都可以快速访问。在索引键序列中,任何一组行都可以通过键的范围快速访问。
每个表只能有一个聚簇索引。哪些列应该是聚簇索引键列的决定是您将为任何表格做出的最重要的索引决定。
在我们的四级中,我们将重点从逻辑转向物理,介绍页面和范围,并检查索引的物理结构。
可下载的代码