介绍#
索引(在 MySQL 中也称为 “键”)是存储引擎用来快速查找行的数据结构。它们还有其他一些有益的属性,我们将在本章中探讨。
因此,它们并不是标准化的:在每个引擎中,索引的工作方式略有不同,并且并非所有引擎都支持所有类型的索引。即使多个引擎支持相同的索引类型,它们在内部的实现方式也可能不同。
在 MySQL 中,存储引擎使用索引,就像你在书的索引中用来查找包含特定术语的页面一样。它在索引的数据结构中搜索一个值。当找到匹配项时,它可以找到包含该匹配项的行。假设你运行以下查询:
SELECT first_name FROM sakila.actor WHERE actor_id = 5;
[!info] 索引包含来自表中一个或多个列的值。如果你对多个列建立索引,列的顺序非常重要,因为 MySQL 只能在索引的最左前缀上高效搜索。
在两个列上创建索引与创建两个单独的单列索引并不相同,正如你将看到的那样。
索引类型#
有许多类型的索引,每种索引都旨在在不同的目的下表现良好。我们将涵盖以下内容:
-
B - 树索引
-
哈希索引
-
位图索引
[!info] 索引是在存储引擎层实现的,而不是在服务器层。
MySQL 默认在 CREATE TABLE
和其他语句中使用 B - 树索引。然而,存储引擎可能在内部使用不同的存储结构。
存储引擎中的性能可能会受到 B - 树索引使用方式的影响。例如,MyISAM 采用前缀压缩方法来减少索引大小,而 InnoDB 保持索引值未压缩。此外,MyISAM 索引根据物理存储位置识别被索引的行,而 InnoDB 则使用主键值(聚簇键)来引用它们,这将在后面详细说明。每种方法都有其优缺点。
B - 树的一般思想是所有值按顺序存储,每个叶子页面与根的距离相同。
该图显示了 InnoDB 存储引擎使用的 B - 树索引的抽象。

叶子页面的不同之处在于它们包含指向被索引数据的指针,而不是指向其他页面的指针。不同存储引擎之间 “指针” 的类型各不相同。
因为 B - 树按顺序存储索引列,所以它们在搜索数据范围时非常有用。
让我们举个例子:
CREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
);
这里的索引将包含表中每一行的 last_name、first_name 和 dob 列的值。以下是索引如何排列它存储的数据:

注意,索引根据在 CREATE TABLE 语句中给定的列的顺序对值进行排序。
了解何时使用 B - 树以及何时不使用 B - 树是非常重要的!
有一些类型的查询可以使用 B - 树索引,这些查询可以通过以下方式进行 查找:
-
完整键值
-
键范围
-
键前缀
[!danger] 只有在查找使用索引的最左前缀时,它们才有用。
考虑前面的例子,让我们看看索引何时会被使用,何时不会!
匹配完整值键 为索引中的所有列指定值。
SELECT first_name FROM People WHERE last_name = 'Allen' AND first_name = 'Cuba' AND dob = '1960-01-01'
匹配最左前缀 查找所有姓 Allen 的人,这将只使用索引中的第一列。
SELECT first_name FROM People WHERE last_name = 'Allen'
匹配列前缀 匹配列值的第一部分,例如,查找所有姓氏以 J 开头的人。
SELECT first_name FROM People WHERE last_name LIKE 'J%'
匹配值范围 你可以找到姓氏在 Allen 和 Meska 之间的人。
SELECT first_name FROM People WHERE last_name BETWEEN 'Allen' AND 'Meska';
精确匹配一部分并在另一部分上匹配范围 该索引可以帮助你找到所有姓 Allen 且名以字母 K 开头的人(如 Kim、Karl 等)。这是对 last_name 的精确匹配和对 first_name 的范围查询。
SELECT first_name FROM People WHERE last_name = 'Allen' AND first_name LIKE 'K%'
匹配仅索引查询 B - 树索引通常可以支持仅索引查询,即仅访问索引而不访问行存储的查询。我们将在后面的 “覆盖索引” 中讨论这种优化。
[!info] 使用 B - 树的另一个重要好处是以排序顺序查找值以及查找,因为 B - 树的节点是排序的。
因此,B - 树索引对 ORDER BY
和 GROUP BY
子句是有帮助的。
使用 B - 树索引的缺点和限制#
如果查找不从被索引列的最左侧 开始,它们就没有用。
SELECT first_name FROM People WHERE first_name = 'Yousef';
上述查询将不会使用索引。此外,你无法找到姓氏 以特定字母结尾 的人,例如,下面的查询将不会使用索引。
SELECT first_name FROM People WHERE last_name LIKE '%J';
存储引擎无法优化对第一个范围条件右侧的任何列的访问。例如,如果你的查询是:
WHERE last_name="Smith" AND first_name LIKE 'J%' AND dob='1976-12-23'
索引访问将仅使用索引中的前两列,因为 LIKE 是一个范围条件(尽管服务器可以将其余列用于其他目的)。对于具有有限值数量的列,你通常可以通过指定相等条件而不是范围条件来解决此问题。我们将在本章后面的索引案例研究中详细展示这一点。
这些限制中的一些并不是 B - 树索引固有的,而是 MySQL 查询优化器和存储引擎使用索引的结果。其中一些可能在未来被消除。
正如你现在理解的,列顺序的重要性不容小觑,因为这些限制都与之交织在一起。为了实现最佳性能,可能需要生成具有相同列但顺序不同的索引,以满足你的查询。
“内联查询” 是指所需信息存在于索引本身的情况。例如,在查询 “EXPLAIN ANALYZE SELECT id from employees where id = 1;” 中,其中 “id” 是主键(btree),则 “Heap Fetches” 值将为 0。相比之下,在查询 “SELECT name from employees where id = 2”(其中 “name” 没有索引)时,引擎将首先使用 “id”(在索引上)访问页面,该页面包含 “name” 的信息(存储在磁盘上,需要另一次读取)。如果上述查询重复执行,由于缓存,它将花费更少的时间。让我们检查另一个查询。
SELECT id from employee where name = 'Zsh';
这将导致顺序扫描,耗费大量时间。 [全表扫描]。尽管 MySQL 通过应用工作线程以某种方式智能地执行顺序扫描,因此它可以并行执行顺序扫描。如果我们现在在 name 上创建索引:
CREATE INDEX employee_idx on employees(name)
$ 位图索引创建。现在让我们搜索:
SELECT id,name FROM employees Where name = 'Yousef'; (index will be used)
SELECT id,name FROM employees WHERE name like '%You%' (index will not be used) #
因为实际上这个表达式不是一个单一的值,我们有很多可能性。
索引的好处#
高性能的索引策略#
由于各种优化技术和专门行为的存在,有许多方法可以有效地选择和实现索引。掌握确定使用哪种方法以及何时使用的方法,并评估你决策的性能后果,是一个需要时间来发展的技能。
在本节中,我们将涵盖以下策略:
-
隔离列
-
前缀索引和索引选择性
-
多列索引
-
选择良好的列顺序
-
聚簇索引和非聚簇索引
-
覆盖索引
隔离列#
我们常常看到一些查询会破坏索引或阻止 MySQL 使用可用的索引。MySQL 通常无法在列上使用索引,除非这些列在查询中是隔离的。“隔离” 列意味着它不应是表达式的一部分或在查询中的函数内。例如,以下查询无法使用 actor_id 上的索引:
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5
人类可以轻松看出 WHERE 子句等同于 actor_id = 4,但 MySQL 无法为 actor_id 解方程。你需要自己做到这一点。你应该养成简化 WHERE 条件的习惯,以便被索引的列单独位于比较运算符的一侧。
前缀索引和索引选择性#
有时,你可能需要对较长的字符列进行索引,这可能导致索引变得庞大且缓慢。一种方法是创建一个模拟哈希索引,如本章前面所示,尽管这可能并不总是足够。
在这种情况下可以做些什么呢?一种解决方案是仅对列的前几个字符进行索引,而不是整个值。这减少了索引的大小,但也降低了其选择性。索引选择性是不同索引值的数量(基数)与表中总行数(#T)的比率,范围从 1/#T 到 1。高度选择性的索引是理想的,因为它使 MySQL 在搜索匹配项时能够过滤掉更多行。唯一索引的选择性为 1,这是最佳值。
列的前缀通常足够选择性,以实现良好的性能。如果你正在对 BLOB 或 TEXT 列或非常长的 VARCHAR 列进行索引,则必须定义前缀索引,因为 MySQL 禁止对其进行全长索引。挑战在于选择一个足够长的前缀,以提供良好的选择性,但又足够短以节省空间。
前缀应该足够长,以使索引几乎与整个列索引一样有用。换句话说,前缀的基数应该接近完整列的基数。
要确定良好的前缀长度,找出最常见的值并将该列表与最常见的前缀列表进行比较。
假设我们有城市的示例数据集,让我们找出最常见的城市。
SELECT COUNT(*) AS cnt, city
FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10
注意,每个值大约有 45 到 65 次出现。现在我们找出最常见的城市名称前缀,从三个字母的前缀开始:
cnt | city | |
---|---|---|
65 | London | |
49 | Hiroshima | |
48 | Teboksary | |
48 | Pak Kret | |
48 | Yaound | |
47 | Tel Aviv-Jaffa | |
47 | Shimoga | |
45 | Cabuyao | |
45 | Callao | |
45 | Bisli |
SELECT COUNT(*) AS cnt, LEFT(city, 3) AS pref FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
cnt | pref |
---|---|
483 | San |
195 | Cha |
177 | Tan |
167 | Sou |
163 | al- |
163 | Sal |
146 | Shi |
136 | Hal |
130 | Val |
129 | Bat |
每个前缀的出现次数要多得多,因此唯一前缀的数量远少于唯一全长城市名称的数量。这个想法是增加前缀长度,直到前缀几乎与列的全长一样选择性。一些实验表明,7 是一个不错的值。
cnt | pref |
---|---|
70 | Santiag |
68 | San Fel |
65 | London |
61 | Valle d |
49 | Hiroshi |
48 | Teboksa |
48 | Pak Kre |
48 | Yaound |
47 | Tel Avi |
47 | Shimoga |
计算良好前缀长度的另一种方法是计算完整列的选择性,并尝试使前缀的选择性接近该值。以下是如何找到完整列的选择性:
通过计算总的不同值与表中值的数量的比率:
SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo;
如果我们目标选择性接近 0.031,前缀的选择性通常会差不多好。可以在一个查询中评估许多不同的长度,这在非常大的表上非常有用。以下是如何在一个查询中找到几个前缀长度的选择性:
SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6,
COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7
FROM sakila.city_demo;
该查询显示,增加前缀长度会导致逐渐减小的改进,因为它接近七个字符。仅查看平均选择性并不是一个好主意。需要注意的是,最坏情况下的选择性也很重要。平均选择性可能会让你认为四个或五个字符的前缀就足够了,但如果你的数据非常不均匀,那可能会是一个陷阱。如果你查看最常见城市名称前缀的出现次数,使用四个字符的值,你会清楚地看到不均匀性:
SELECT COUNT(*) AS cnt, LEFT(city, 4) AS pref
FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 5;
cnt | pref |
---|---|
205 | San |
200 | Sant |
135 | Sout |
104 | Chan |
91 | Toul |
使用四个字符时,最常见的前缀出现的频率明显高于最常见的全长值。也就是说,这些值的选择性低于平均选择性。如果你有比这个随机生成的样本更现实的数据集,你可能会看到这个效果更加明显。例如,在真实世界的城市名称上构建四个字符的前缀索引会在以 “San” 和 “New” 开头的城市上产生糟糕的选择性,因为这样的城市有很多。
现在我们已经为我们的示例数据找到了一个良好的值,以下是如何在列上创建前缀索引:
ALTER TABLE sakila.city_demo ADD KEY (city(7))
前缀索引提供了一种减少索引大小和提高性能的有用方法。然而,它们也有一些缺点:MySQL 不能在 ORDER BY 或 GROUP BY 查询中使用前缀索引,也不能将其用作覆盖索引。前缀索引可以在使用长十六进制标识符时非常有利。在上一章中,我们讨论了更有效的方法来存储此类标识符,但如果你使用的是不可修改的预打包解决方案呢?这在使用 MySQL 存储以长十六进制字符串为键的网站会话的 vBulletin 和其他应用程序中经常发生。在前八个字符(或大约)上添加索引通常会显著提高性能,这对应用程序是完全透明的。
多列索引#
多列索引通常被误解。常见的错误是将许多或所有列单独索引,或以错误的顺序索引列。我们将在下一节中讨论列顺序。第一个错误,单独索引许多列,在 SHOW CREATE TABLE
中有一个独特的特征:
CREATE TABLE t ( c1 INT, c2 INT, c3 INT, KEY(c1), KEY(c2), KEY(c3) )
[!info] 这种索引策略通常是由于人们给出模糊但听起来权威的建议,例如 “在 WHERE 子句中出现的列上创建索引。” 这个建议是非常错误的。它将导致最多只有一星的索引。这些索引的性能可能比真正的最佳索引慢几个数量级。有时,当你无法设计出三星级索引时,忽略 WHERE 子句并关注最佳行顺序或创建覆盖索引会更好。
在许多列上单独创建索引不会帮助 MySQL 提高大多数查询的性能。MySQL 5.0 及更高版本可以通过使用一种称为 索引合并 的策略稍微应对这种索引不良的表,这允许查询有限地使用来自单个表的多个索引来定位所需的行。早期版本的 MySQL 只能使用单个索引,因此当没有单个索引足够好时,MySQL 通常选择表扫描。
例如,film_actor 表在 film_id 和 actor_id 上都有索引,但在以下查询中,这两个索引都不是良好的选择:
SELECT film_id, actor_id FROM sakila.film_actor
WHERE actor_id = 1 OR film_id = 1;
在早期的 MySQL 版本中,除非你将其写成两个查询的 UNION,否则该查询将产生表扫描:
SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1 UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor WHERE film_id = 1 AND actor_id <> 1;
然而,在 MySQL 5.0 及更高版本中,该查询可以同时使用两个索引,扫描它们并合并结果。
该算法有三种变体:用于 OR 条件的并集,用于 AND 条件的交集,以及两者组合的并集。以下查询使用两个索引扫描的并集,你可以通过检查 Extra 列看到这一点:
EXPLAIN SELECT film_id, actor_id FROM sakila.film_actor WHERE actor_id = 1 OR film_id = 1
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: film_actor type: index_merge possible_keys: PRIMARY,idx_fk_film_id key: PRIMARY,idx_fk_film_id key_len: 2,2 ref: NULL rows: 29 Extra: **Using union(PRIMARY,idx_fk_film_id); Using where**
需要记住的是,索引合并在某种程度上是一项昂贵的操作。因此,如果你在 EXPLAIN
中看到 “索引合并”,这可能表明表的索引不良。
-
当服务器交叉索引(通常用于 AND 条件)时,通常意味着你需要一个包含所有相关列的单个索引,而不是多个必须组合的索引。
-
当服务器联合索引(通常用于 OR 条件)时,有时算法的缓冲、排序和合并操作会使用大量 CPU 和内存资源。如果并非所有索引都非常选择性,则扫描返回大量行以进行合并操作。当你在 EXPLAIN 中看到索引合并时,你应该检查查询和表结构,以查看这是否真的是你能得到的最佳结果。你可以使用 optimizer_switch 选项或变量禁用索引合并。你也可以使用 IGNORE INDEX。
选择良好的列顺序#
选择列顺序的一个老规则是:将最具选择性的列放在索引的前面。这个建议有多有用?在某些情况下,它可能会有所帮助,但通常比避免随机 I/O 和排序重要性要小得多,考虑到所有因素。(具体情况各异,因此没有一刀切的规则。这本身就应该告诉你,这条经验法则可能没有你想的那么重要。)
将最具选择性的列放在前面可能是个好主意,当没有排序或分组需要考虑时,因此索引的目的仅仅是优化 WHERE 查找。在这种情况下,确实可能设计索引,使其尽快过滤掉行,因此对于仅在 WHERE 子句中指定索引前缀的查询更具选择性。然而,这不仅取决于列的选择性(总体基数),还取决于你用来查找行的实际值 —— 值的分布。这与我们在选择良好前缀长度时探讨的考虑是相同的。你可能实际上需要选择列顺序,以便对你将运行的大多数查询尽可能选择性。
让我们举个例子查询并更好地诊断它:
SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;
我们应该在 (staff_id, customer_id) 上创建索引,还是应该反转列的顺序?
我们可以运行一些快速查询来帮助检查表中 值的分布,以确定哪个列具有更高的选择性。
让我们计算 WHERE 子句中每个谓词的基数:
SELECT SUM(staff_id = 2), SUM(customer_id = 584) FROM payment;
*************************** 1. row *************************** SUM(staff_id = 2): 7992 SUM(customer_id = 584): 30
根据经验法则,我们应该将 customer_id 放在索引的第一位,因为该谓词在表中匹配的行较少。然后我们可以再次运行查询,以查看在此特定客户 ID 选择的行范围内,staff_id 的选择性如何。
SELECT SUM(staff_id = 2) FROM payment WHERE customer_id = 584;
SUM(staff_id = 2): 17
但如果你没有具体的样本可以运行,可能最好使用老的经验法则,即查看整体的基数,而不仅仅是针对一个查询:
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment\G
staff_id_selectivity: 0.0001 customer_id_selectivity: 0.0373 COUNT(*): 16049
customer_id 的选择性更高,因此再次的答案是将该列放在索引的第一位:
ALTER TABLE payment ADD KEY(customer_id, staff_id);
最后,尽管关于选择性和基数的经验法则很有趣,但其他因素 —— 例如排序、分组以及查询 WHERE 子句中范围条件的存在 —— 可能对查询性能产生更大的影响。
聚簇索引与非聚簇索引#
聚簇索引#
聚簇索引并不是一种单独的索引类型。相反,它们是一种数据存储方法。确切的细节在不同的实现之间有所不同,但 InnoDB 的聚簇索引实际上将 B - 树索引和行一起存储在同一结构中。
当一个表具有聚簇索引时,其行实际上存储在索引的叶子页面中。
每个表只能有一个聚簇索引,因为你不能同时在两个地方存储行。
由于存储引擎负责实现索引,并非所有存储引擎都支持聚簇索引,但我们在这里关注的是 InnoDB
。

注意记录在聚簇索引中的布局。注意叶子页面包含完整的行,而节点页面仅包含索引列,如上图所示。聚簇索引(每个表只有一个,通常是主键)。
一些数据库服务器允许你选择要聚簇的索引,但截至本文撰写时,MySQL 的内置存储引擎没有这样做。InnoDB 按主键聚簇数据。
如果你没有定义主键,InnoDB 将尝试使用唯一的非空索引。如果没有这样的索引,InnoDB 将为你定义一个隐藏的主键,然后在此基础上进行聚簇。
聚簇可以帮助性能,但它也可能带来负面和严重的问题。
聚簇数据的优点是什么?
-
你可以将相关数据紧密放在一起。例如,在实现邮箱时,你可以按 user_id 聚簇,这样你可以通过从磁盘中获取少量页面来检索单个用户的所有消息。如果不使用聚簇,每条消息可能需要自己的磁盘 I/O。
-
数据访问速度快。聚簇索引将索引和数据一起保存在一个 B - 树 中,因此从聚簇索引中检索行通常比在非聚簇索引中进行相应查找要快。
-
使用覆盖索引的查询可以使用叶子节点中包含的主键值。
缺点是什么?
-
聚簇对 I/O 绑定的工作负载提供最大的改进。如果数据适合内存,则访问顺序实际上并不重要,因此聚簇并不会带来太多好处。
-
插入速度在很大程度上取决于插入顺序。按主键顺序插入行是将数据加载到 InnoDB 表中的最快方法。如果你没有按主键顺序加载大量数据,可能在加载后使用 OPTIMIZE TABLE 重新组织表是个好主意。
-
更新聚簇索引列的成本很高,因为这会迫使 InnoDB 将每个更新的行移动到新位置。
-
基于聚簇索引的表在插入新行时,或者当行的主键更新到必须移动的情况下,容易发生页面分裂。当行的键值决定该行必须放入已满的数据页面时,就会发生页面分裂。存储引擎必须将页面拆分为两个以容纳该行。页面分裂可能导致表在磁盘上使用更多空间。
-
聚簇表在全表扫描时可能会更慢,尤其是当行的密度较低或由于页面分裂而存储不连续时。
-
访问二级索引需要进行两次索引查找,而不是一次。最后一点可能有点令人困惑。为什么二级索引需要两次索引查找?答案在于二级索引存储的 “行指针” 的性质。请记住,叶子节点不存储指向引用行的物理位置的指针;相反,它存储行的主键值。
这意味着,要从二级索引中找到一行,存储引擎首先在二级索引中找到叶子节点,然后使用存储在其中的主键值导航主键并找到该行。这是双重工作:两次 B - 树 导航,而不是一次。
InnoDB 和 MyISAM 数据布局的比较#
聚簇和非聚簇数据布局之间的差异,以及主索引和二级索引之间的相应差异,可能会令人困惑和惊讶。让我们看看 InnoDB 和 MyISAM 如何布局以下表:
CREATE TABLE layout_test ( col1 int NOT NULL, col2 int NOT NULL, PRIMARY KEY(col1), KEY(col2));
假设我们已将表填充了从 1 到 10,000 的主键,随机插入,然后使用 OPTIMIZE TABLE
进行了优化。换句话说,数据在磁盘上以最佳方式排列,但行可能是随机顺序。col2 的值在 1 到 100 之间随机分配,因此存在许多重复值。
顺便说一下,非聚簇索引设计并不总是能够提供单次操作的行查找。当行发生变化时,它可能不再适合其原始位置,因此你可能会得到碎片化的行或表中的 “转发地址”,这两者都会导致查找行时的工作量增加。


MyISAM 的数据布局#
MyISAM 将行按插入顺序存储在磁盘上。

由于行是固定大小的,MyISAM 可以通过从表的开头寻求所需的字节数来找到任何行。(MyISAM 并不总是使用 “行号”,正如我们所示;它根据行是固定大小还是可变大小使用不同的策略。)
索引中的每个叶子节点可以简单地包含行号。
InnoDB 的数据布局#
按主键顺序插入行与 InnoDB#

/** 聚簇索引 **/
ALTER TABLE dbo.MyTable
ADD CONSTRAINT PK_MyTable PRIMARY KEY(Id);
/**非聚簇索引**/
ALTER TABLE dbo.MyTable
ADD CONSTRAINT UC_MyTable_Id UNIQUE(Id);


非聚簇索引假设我们在名称列上创建了一个非聚簇索引。


行定位器包含:
-
聚簇键(employee_id)
-
实际行 [name]
聚簇索引和非聚簇索引共同工作以查找数据!
查找实际行的步骤是什么?

-
SQL 服务器在名称列上使用非聚簇索引快速找到索引中的员工条目。
-
聚簇索引(employee_id)用于找到实际行!
在 SQL 服务器中,我们从上到下、从右到左读取执行计划!

当查询通过聚簇索引树导航到基础表数据时,这称为 聚簇索引查找。聚簇索引包含基础表数据本身,这就是为什么你可以创建一个聚簇索引。

非聚簇索引 与基础数据分开,基础数据可以作为聚簇索引存在,直接可用的数据可能有限,因为通常非聚簇索引仅包含表中部分列。
如果请求的列中不包含在索引中的值,查询可能会使用前面提到的引用导航回基础数据。如果查询优化器认为这样做成本太高,它可能会选择扫描基础数据而不是使用索引。
非聚簇索引与基础数据分开带来了几个重要特性。
过滤索引 仅包含满足用户定义谓词的行,要创建这些索引,你在索引定义中使用 WHERE
子句,因此不能使用聚簇索引,因为它必须包含表中的所有数据。
CREATE NONCLUSTERED INDEX IX_PhoneBook_NCI
ON dbo.PhoneBook(LastName, FirstName)
WHERE (LastName >= 'Yousef');
总结:聚簇索引是一种将数据整体表示的方法,而非聚簇索引是一个与基础数据物理分开的结构,引用基础数据并可以有不同的排序顺序。