1、MySQL索引是什么?
在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标志这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。
索引提供指向存储在表的指定列中的数据值的指针,然后根据您指定的排序顺序对这些指针排序。数据库使用索引以找到特定值,然后顺指针找到包含该值的行。这样可以使对应于表的SQL语句执行得更快,可快速访问数据库表中的特定信息。
索引是为了加速对表中数据行的检索而创建的一种分散的存储结构。索引是针对表而建立的,它是由数据页面以外的索引页面组成的,每个索引页面中的行都会含有逻辑指针,以便加速检索物理数据。
2、索引的优势
- 提高查询速度:索引可以加速数据的检索过程,通过索引,数据库引擎可以快速定位到存储在特定值或范围内的数据,从而大幅减少了查询所需的时间。
- 减少磁盘IO操作:索引可以减少磁盘IO操作的次数,因为索引可以帮助数据库引擎快速定位到存储在特定位置的数据,而无需逐个扫描整个数据表。
- 加速排序和分组操作:索引可以加速排序和分组操作,因为索引已经按特定的顺序存储了数据,数据库引擎可以直接利用索引来完成排序和分组操作,而无需再对原始数据进行排序和分组。
- 提高数据的完整性和唯一性:通过在索引上创建唯一索引或主键索引,可以保证数据的完整性和唯一性,避免数据重复和冗余。
3、索引的劣势
1、索引需要占物理空间:创建索引就像建立图书馆的索引卡片系统,需要额外的空间和资源。在数据库中,这意味着需要更多的存储空间和时间来维护索引。
2、降低数据写入(DML)性能:当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。
3、增加维护:数据库需要额外的开销来维护索引的有序性和一致性。
4、优化器负担:过多的索引会增加查询优化器选择执行路径的难度和成本。
4、索引为什么会快?
索引的核心原理是:通过额外的数据结构(空间换时间)和精巧的设计,将随机的全表扫描变为有序的、局部的快速查找。
1. 高效的数据结构:基于B+树的优化设计
索引通常采用如B+树(B+ Tree)这类高度优化的数据结构。B+树是一种多路平衡搜索树,其设计充分考虑了磁盘的读写特性。它保持树的平衡,确保从根节点到任何叶子节点的查找路径长度都完全相同且最短,这使得查找、插入和删除操作的时间复杂度稳定在O(log n),极其高效。
B+树的所有数据都存储在叶子节点,并形成有序双向链表,而内部节点(非叶子节点)仅存储键值和指向子节点的指针。这种设计使得单个节点可以存储大量键值,从而极大地降低了树的高度。更矮的树意味着在查找任何一条记录时,需要加载的磁盘块(I/O次数)更少,这是速度快的根本原因之一。
2. 减少数据访问量:从全表扫描到范围定位
没有索引时,数据库必须执行全表扫描(Full Table Scan),即逐行读取表中的每一块数据来判断是否满足条件。对于数百万甚至上亿行记录的大表,这无疑是灾难性的,因为它需要加载整个表的数据,其中绝大部分都是无用的。
索引通过存储排序的键值及其对应数据行的指针,像一本书的目录一样工作。数据库引擎首先在索引结构中进行快速查找(在B+树中只需几次I/O),迅速定位到目标数据行的起始位置。它不再需要检查每一行,而是直接“跳转”到相关的数据区块,极大地减少了需要访问和检查的数据量,从而实现了性能的飞跃。
3. 优化磁盘I/O:减少次数与数据量
数据库性能的主要瓶颈往往是磁盘I/O(输入/输出),而索引正是优化I/O的利器。首先,索引文件本身比完整的数据文件小得多,因为它只包含被索引的列和指向数据的指针。更小的文件意味着从磁盘读取它所需的I/O次数更少、速度更快。
其次,由于索引结构的有序性(特别是B+树的叶子节点链表),它可以将原本需要随机访问磁盘的操作(因为数据行可能散落在磁盘各处)转变为顺序I/O。顺序读取磁盘的速度远快于随机读取。此外,小的索引文件也更容易被完整地缓存到内存中,后续的查询甚至可能完全无需访问磁盘,直接在内存中完成,速度极快。
4. 提升缓存效率:增强数据访问局部性
现代数据库系统会使用大量内存作为缓存池(Buffer Pool)来存储最近访问过的数据和索引页。索引显著提升了缓存的空间局部性。当执行一次索引查找时,不仅目标索引页会被加载到缓存,其相邻的索引页也可能被预读进来。这意味着后续的范围查询或邻近值查询很可能直接在内存中命中,避免了物理磁盘读取。
同样,当通过索引定位到数据行时,由于同一数据页上的多行记录很可能被一次I/O加载到缓存中。如果这些相关记录被后续查询访问,它们已经在内存里,从而极大地提升了整体系统的吞吐量和响应速度。
5. 预先排序以加速操作:避免昂贵排序
如果查询包含ORDER BY
(排序)、GROUP BY
(分组)或DISTINCT
(去重)等子句,而没有索引,数据库就必须先将所有符合条件的数据集加载到内存或临时磁盘空间中进行排序,这是一个非常消耗CPU和内存的昂贵操作。
如果索引(特别是B+树索引)的键顺序正好与ORDER BY
或GROUP BY
的字段顺序匹配,数据库引擎就可以简单地按索引的顺序读取数据。因为索引本身就是一个已经排好序的数据结构,数据库可以直接返回有序的结果集,完全避免了执行费时费力的临时排序操作,从而再次大幅提升查询性能。
5、索引为什么使用B+树?
B+树作为MySQL InnoDB存储引擎的默认索引结构,其设计充分考虑了磁盘I/O效率和数据访问模式,在处理大规模数据时展现出显著的性能优势。
首先,B+树采用了矮胖的多层平衡树结构,非叶子节点仅存储键值而不包含实际数据,使得单个节点能够容纳大量索引项。在InnoDB默认16KB的页大小支持下,单个节点可存储更多键值,显著降低树的高度。这种设计直接减少了查询过程中需要的磁盘I/O次数,从而大幅提升点查询和等值查询的性能表现。
其次,B+树将所有数据记录集中存储在叶子节点,并通过指针将叶子节点连接成有序双向链表。这种结构使得范围查询异常高效,只需定位到起始节点即可沿链表顺序扫描,避免了不必要的中间节点访问。对于需要排序、分组或去重的查询操作,这种有序链表结构也能提供最优的访问路径。
第三,由于所有数据访问最终都会到达叶子节点,B+树的磁盘I/O次数始终保持稳定,基本等于树的高度。这种可预测的I/O特性使得查询性能不会因数据分布或查询模式的变化而产生剧烈波动,为数据库性能优化提供了可靠保障。
此外,叶子节点形成的完整数据链表使全局扫描操作异常高效。进行全表扫描时,系统只需顺序遍历叶子节点链表即可获取全部数据,避免了B树那样需要遍历整个树结构的开销,极大提升了批量数据处理的效率。
最后,B+树在数据插入方面也经过特殊优化。当采用自增整型主键时,新数据总是追加到链表末尾,最小化叶子节点分裂的概率。这种顺序写入特性显著减少了索引维护开销,使得高并发写入场景下仍能保持稳定的性能表现。
6、什么是B+树?
B+树是一种专门为磁盘或其他直接存取的辅助存储设备而设计的多路平衡搜索树。它在数据库和文件系统中至关重要,是MySQL InnoDB存储引擎的默认索引结构。其核心目标是最小化磁盘I/O次数,从而高效地管理大量数据。
B+树的核心特性
一棵经典的B+树具备以下关键特征:
多路平衡树:B+树是“矮胖”的,而不是“高瘦”的。每个节点可以有多个子节点(通常是几百个),这使得树的高度非常低。一棵拥有数百万条记录的B+树可能只有3-4层高,这意味着查找任何一条记录最多只需要3-4次磁盘I/O,效率极高。
数据只存储在叶子节点:这是B+树与B树的一个关键区别。非叶子节点(也称为内部节点)只存储键(索引键值),不存储实际的数据记录。内部节点的作用纯粹是充当指引路径的“路标”。
叶子节点通过指针串联:所有叶子节点都包含了所有的键值及其对应的完整数据记录(或指向数据记录的指针)。此外,叶子节点之间使用双向指针链表连接起来,形成了一个有序的链表。
7、索引失效场景?
1. 使用OR条件
当查询条件中包含OR
时,即使OR
两侧的列都单独建有索引,MySQL也通常只能使用其中一个索引,而不是合并索引。它会分别根据两个索引查找出结果集,然后进行合并与去重,这个过程可能比直接进行全表扫描更耗时。因此,优化器在某些情况下可能会选择放弃使用索引而进行全表扫描。
举例: SELECT * FROM users WHERE age = 25 OR name = 'John';
如果age
和name
上都有单列索引,MySQL可能不会使用这两个索引,而是选择全表扫描来避免复杂的索引合并操作。
2. 字符串字段未用引号括起来
如果查询条件中的字符串类型字段没有被引号括起来,MySQL会隐式地进行类型转换,试图将字符串值转换为字段对应的数据类型(通常是数字)。这个转换过程会使索引失效,因为MySQL无法直接使用索引树中的字符串值来匹配一个经过函数转换后的表达式。
举例: SELECT * FROM users WHERE phone = 1234567890;
(phone
字段是VARCHAR
类型)。这里MySQL会将索引中的字符串phone
值转换为数字,再与1234567890
比较,导致索引失效。应写为 phone = '1234567890'
。
3. 使用LIKE通配符(以%开头)
最左前缀匹配原则是索引有效的基础。当LIKE
语句的通配符%
出现在字符串的开头时,索引就失去了其有序性的优势。MySQL无法知道什么字符串会出现在%
之后,因此只能从索引树的第一个条目开始遍历所有值,检查其是否符合条件,这实质上等同于全索引扫描或全表扫描。
举例: SELECT * FROM articles WHERE content LIKE '%database%';
这个查询无法利用content
字段上的索引。如果必须使用前缀模糊查询,可考虑使用全文索引(FullText Index)。
4. 联合索引的条件列顺序问题
联合索引(复合索引)的键值存储顺序是按照索引定义的列顺序排列的。查询必须使用联合索引的“最左前缀”(即从第一列开始且连续不跳过中间列),索引才能被有效使用。如果查询条件没有从联合索引的第一个列开始,或者跳过了中间的列,那么后续列的索引部分将无法被使用。
举例: 假设有一个联合索引 INDEX (last_name, first_name)
。查询 WHERE first_name = 'John'
无法使用该索引,因为它缺少最左边的last_name
条件。而查询 WHERE last_name = 'Smith' AND first_name = 'John'
则可以高效地使用整个联合索引。
5. 在索引列上使用内置函数
对索引列使用函数(如DATE()
, UPPER()
, CONCAT()
等)会在查询时对每一行数据的该列值进行计算,生成一个新的结果。索引中存储的是原始数据,而不是函数计算后的结果,因此MySQL无法将函数条件与索引内容进行直接匹配,导致索引失效。
举例: SELECT * FROM orders WHERE DATE(order_date) = '2023-10-24';
虽然order_date
字段有索引,但DATE()
函数使其失效。应改为范围查询: WHERE order_date >= '2023-10-24 00:00:00' AND order_date < '2023-10-25 00:00:00'
。
6. 索引列上的运算
与使用函数类似,在索引列上进行算术运算(如 +
, -
, *
, /
)或逻辑运算,都会导致MySQL无法直接使用索引中存储的原始值,因为它需要先对每一行的列值进行计算,然后再与条件进行比较。这破坏了索引的直接可比性。
举例: SELECT * FROM products WHERE price * 1.1 > 100;
即使price
字段有索引,这个查询也会失效。应重构查询,将运算移到运算符的另一侧: WHERE price > 100 / 1.1
。
7. 使用不等于或范围查询
不等于操作符(!=
或 <>
)和某些范围查询(NOT IN
, NOT LIKE
)本质上需要排除掉一部分数据。由于索引是排序的,对于“等于”条件可以快速定位,但对于“不等于”条件,它需要检查索引中的几乎所有条目(因为要返回所有非特定值的记录),其效率有时并不比全表扫描高,因此优化器可能选择不使用索引。
举例: SELECT * FROM customers WHERE status != 'active';
如果status
字段只有少数几种值(如'active'
, 'inactive'
),且'inactive'
数据量很大,优化器可能会判断全表扫描更快,从而导致索引失效。
8. 索引字段上的NULL检查
虽然MySQL允许在包含NULL
值的列上建立索引,并且索引会记录NULL
值,但使用IS NULL
或IS NOT NULL
查询时,索引的使用效率取决于数据的分布。如果NULL
值或非NULL
值的数量非常大,优化器可能会认为扫描整个索引(或全表)比通过索引树定位特定部分更高效,从而选择不使用索引。
举例: SELECT * FROM contacts WHERE email IS NULL;
如果表中绝大多数记录的email
都不为NULL
,只有几条是NULL
,这个查询很可能会使用索引。反之,如果一半记录都是NULL
,优化器就可能选择全表扫描。
9. 连接查询中的字段编码不一致
当进行表连接(如LEFT JOIN
)时,如果关联字段的字符集(Charset)或排序规则(Collation)不一致,MySQL需要先对其中一个字段进行转换才能进行比较。这个隐式的转换操作相当于在字段上使用了函数,会导致关联字段上的索引失效。
举例: TableA.utf8mb4_string
与 TableB.latin1_string
进行JOIN
。MySQL必须转换其中一个编码以便比较,这使得TableB.latin1_string
上的索引无法使用。解决方案是确保连接字段使用相同的字符集和排序规则。
10. MySQL优化器的选择
MySQL优化器是一个基于成本的优化器(Cost-Based Optimizer, CBO)。它会根据统计信息(如表的行数、索引的选择性、数据分布等)来估算不同执行路径(使用索引或全表扫描)的成本。如果它估算出需要访问的数据行数超过表总行数的一个较高比例(通常认为是20%-30%),或者索引的选择性非常低(如性别字段),它就会认为使用索引(需要回表查询)的成本高于直接扫描全表的成本,从而选择全表扫描。
举例: 在一个有10万条记录的用户表中,执行 SELECT * FROM users WHERE gender = 'F';
(假设性别分布均匀)。即使gender
字段上有索引,优化器也会因为需要返回约5万条记录(50%的数据)而选择全表扫描,因为顺序I/O读取整个表比随机I/O通过索引回表5万次更快。
8、索引类型
1. 主键索引 (Primary Key Index)
解释:
主键索引是关系数据库中最核心的索引类型。它基于主键约束建立,其核心特性是唯一且非空。每个表只能有一个主键索引。它不仅是用于加速查询的索引,更定义了表中数据的逻辑顺序和唯一性约束。InnoDB存储引擎使用主键索引作为“聚簇索引”,这意味着表数据本身就直接存储在主键索引的叶子节点上。因此,通过主键进行查找是速度最快的操作。
举例:
在一个 users
(用户)表中,user_id
列是唯一标识每个用户的字段,不允许重复也不允许为NULL。将它设为主键是最合适的选择。任何通过 user_id
来查询特定用户信息的操作都会非常高效。
创建与删除代码:
-- 创建表时直接指定(推荐)
CREATE TABLE users (user_id INT AUTO_INCREMENT PRIMARY KEY, -- 在此指定username VARCHAR(50) NOT NULL
);-- 为已存在的表添加主键
ALTER TABLE users ADD PRIMARY KEY (user_id);-- 删除主键索引
ALTER TABLE users DROP PRIMARY KEY;
注意:删除主键索引会同时移除主键约束。如果字段是自增的,需要先修改字段属性再删除主键。
2. 唯一索引 (Unique Index)
解释:
唯一索引的主要作用是保证数据唯一性,而不是 primarily 为了提升性能(虽然它也能加速查询)。它与主键索引的关键区别在于:唯一索引允许有NULL值(除非同时有NOT NULL约束),并且一个表可以创建多个唯一索引。当插入或更新数据时,如果会导致唯一索引列出现重复值(多个NULL值在MySQL中不被视为重复),数据库会阻止该操作并抛出错误。它确保了业务层面上数据的唯一性,如防止邮箱、手机号重复注册。
举例:
在 users
表中,email
(邮箱)和 phone_number
(手机号)列都要求不能有重复值(NULL除外)。可以为这两列分别创建唯一索引,以确保数据库不会存入两个相同邮箱或手机号的用户账号。
创建与删除代码:
-- 创建表时指定
CREATE TABLE users (user_id INT PRIMARY KEY,email VARCHAR(100) UNIQUE, -- 在此指定phone_number VARCHAR(20) UNIQUE
);-- 为已存在的表添加唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
-- 或使用ALTER TABLE语法
ALTER TABLE users ADD UNIQUE KEY idx_phone (phone_number);-- 删除唯一索引
DROP INDEX idx_email ON users;
3. 普通索引 (Normal Index / Index)
解释:
普通索引(也称为非唯一索引)是最基本、最常用的索引类型,它没有任何唯一性限制的唯一目的就是提高查询速度。它允许被索引的列包含重复值和NULL值。当你的查询条件(WHERE
)、排序(ORDER BY
)或分组(GROUP BY
)涉及到某一列或某些列时,为其创建普通索引可以避免全表扫描,大幅减少需要检查的数据行数。
举例:
在 orders
(订单)表中,经常需要根据 customer_id
(客户ID)来查询某个客户的所有订单。由于一个客户可以有多个订单,customer_id
必然重复,此时为其创建一个普通索引是最佳选择,可以极大加速这类查询。
创建与删除代码:
-- 创建表时指定
CREATE TABLE orders (order_id INT PRIMARY KEY,customer_id INT,INDEX idx_customer_id (customer_id) -- 在此指定
);-- 为已存在的表添加普通索引
CREATE INDEX idx_customer_id ON orders(customer_id);
-- 或使用ALTER TABLE语法
ALTER TABLE orders ADD INDEX idx_customer (customer_id);-- 删除索引
DROP INDEX idx_customer_id ON orders;
4. 全文索引 (Full-Text Index)
解释:
全文索引是一种专门用于对大量文本内容进行高效词语匹配和搜索的特殊索引。它与前三种基于精确匹配的索引原理完全不同,其核心是“分词”。MySQL会将文本字符串按词进行拆分,并建立倒排索引,从而能够快速查找包含特定词语或短语组合的行。它适用于实现像文章搜索、商品关键词搜索这样的功能,使用 MATCH(...) AGAINST(...)
语法进行查询。
举例:
在一个 articles
(文章)表中,用户需要在 title
(标题)和 content
(正文)中搜索包含“人工智能”关键词的文章。为这两个列创建全文索引后,就可以进行高效的关键词检索,而不是低效的 LIKE '%人工智能%'
操作。
创建与删除代码:
-- 创建表时指定(仅限InnoDB和MyISAM引擎)
CREATE TABLE articles (id INT PRIMARY KEY,title TEXT,content TEXT,FULLTEXT KEY ft_index (title, content) -- 在此指定
);-- 为已存在的表添加全文索引
ALTER TABLE articles ADD FULLTEXT ft_index (title, content);
-- 或
CREATE FULLTEXT INDEX ft_index ON articles(title, content);-- 删除全文索引
DROP INDEX ft_index ON articles;
如何使用全文索引
全文索引的使用核心在于 MATCH(...) AGAINST(...)
语法。MATCH
子句指定要搜索的列(这些列必须包含在同一个全文索引中),AGAINST
子句指定要搜索的关键词(也称为搜索字符串或查询字符串),并可以指定搜索模式。
1. 自然语言搜索 (NATURAL LANGUAGE MODE) - 默认模式
这是默认模式,用于查找包含至少一个搜索词的行。结果集会自动按相关性(Relevance)进行降序排序,相关性是一个非负浮点数,表示匹配程度。
示例查询:
-- 查找包含“人工智能”的文章
SELECT id, title,-- 可以使用MATCH(...) AGAINST(...)来获取相关性得分MATCH(title, content) AGAINST('人工智能' IN NATURAL LANGUAGE MODE) AS score
FROM articles
WHERE MATCH(title, content) AGAINST('人工智能' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC;
2. 布尔搜索 (BOOLEAN MODE)
这种模式允许使用特殊的操作符来构造更复杂、更强大的查询。你可以要求某些词必须出现(+),某些词必须排除(-),或者使用通配符(*)。
常用操作符:
+
:必须包含。+apple
表示结果必须包含 "apple"。-
:必须排除。-banana
表示结果不能包含 "banana"。*
:通配符。data*
可以匹配 "data", "database", "dataset" 等。""
:短语搜索。"machine learning"
表示必须精确匹配整个短语 "machine learning"。>
<
:提高或降低词的贡献值。
示例查询:
-- 查找必须包含“区块链”但不能包含“金融”的文章
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('+区块链 -金融' IN BOOLEAN MODE);-- 查找包含以“data”开头的词(如database, data)的文章
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('data*' IN BOOLEAN MODE);-- 查找精确包含短语“深度学习框架”的文章
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('"深度学习框架"' IN BOOLEAN MODE);
3. 查询扩展搜索 (WITH QUERY EXPANSION)
这种模式会进行两次搜索:第一次用给定的关键词进行搜索;然后,它会从第一次找到的最相关行中提取出一些重要的词,将这些词加入到原搜索词中进行第二次搜索。这有助于扩大搜索范围,找到更多相关结果,但也可能引入一些不相关的噪声。适用于用户搜索词过于简短或模糊的情况。
示例查询:
-- 使用查询扩展来搜索“数据库”
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('数据库' WITH QUERY EXPANSION);
5. 组合索引 (Composite Index / Compound Index)
解释:
组合索引是指由多个列共同组成的一个索引。它并不是一个独立的索引类型,而是一种创建索引的策略(可以是普通组合索引,也可以是唯一组合索引等)。它的核心优势是最左前缀原则。查询时,如果条件中包含了索引定义中最左边的列,那么这个组合索引就可以被使用。一个设计良好的组合索引,其效能往往远高于多个单列索引,因为它可以优化多列查询、排序和避免回表操作。
举例:
在 users
表中,有一个非常常见的查询是:“查找来自‘北京’(city)且姓‘张’(last_name)的所有用户”。如果为 (city, last_name)
创建一个组合索引,数据库可以高效地利用这个索引直接定位到所需数据,效率远高于分别对 city
和 last_name
建两个独立索引。
创建与删除代码:
-- 创建表时指定
CREATE TABLE users (user_id INT PRIMARY KEY,country VARCHAR(50),city VARCHAR(50),last_name VARCHAR(50),INDEX idx_location_name (country, city, last_name) -- 在此指定
);-- 为已存在的表添加组合索引
CREATE INDEX idx_location_name ON users(country, city, last_name);
-- 或
ALTER TABLE users ADD INDEX idx_location_name (country, city, last_name);-- 删除组合索引(与删除普通索引语法相同)
DROP INDEX idx_location_name ON users;
6. 覆盖索引 (Covering Index)
解释:
覆盖索引不是一种物理上特殊的索引,而是一种高性能的查询优化场景。当一个索引包含了查询所需要的所有字段时(即SELECT的列、WHERE的条件、JOIN的字段等都在同一个索引的列中),数据库引擎就无需再“回表”去读取数据行本身,只需扫描索引本身即可得到全部结果。这极大地减少了I/O操作,是数据库优化的重要技术之一。通常需要通过创建恰当的组合索引来实现覆盖索引的效果。
举例:
有一个高频查询:SELECT user_id, age, score FROM user_profiles WHERE age > 25;
。如果在 user_profiles
表上只有一个在 age
列的单列索引,那么查询需要根据索引找到所有age>25
的行ID,再根据这些ID回表去取 user_id
和 score
的值。但如果创建一个 (age, user_id, score)
的组合索引,这个索引本身就包含了查询所需的所有数据,引擎只需扫描这个索引就能完成整个查询,速度极快。
创建与删除代码:
覆盖索引本身不是一种索引类型,而是通过创建特定的组合索引来实现的。其创建和删除语法与创建组合索引完全相同。
-- 为了实现上述举例中的覆盖索引,我们创建组合索引
CREATE INDEX idx_age_cover ON user_profiles(age, user_id, score);-- 删除索引
DROP INDEX idx_age_cover ON user_profiles;
9、聚簇索引和非聚簇索引
聚簇索引 (Clustered Index)
聚簇索引并不是一个单独的索引类型,而是一种数据存储方式。在InnoDB中,表数据本身(即数据行)就存储在聚簇索引的B+树的叶子节点上。因此,一个表有且只有一个聚簇索引,因为它决定了表中数据的物理存储顺序。
关键特性:
- 表即索引:数据行和主键索引是绑定在一起的。
- 物理有序:数据行按照聚簇索引键值的顺序在磁盘上存储。相邻的键值,其数据行在物理上也尽可能相邻。
- 快速主键查询:通过主键查找数据非常快,因为只需一次B+树查找就能直接定位到数据行。
如何创建?
- 首选主键:如果你为表定义了主键(PRIMARY KEY),InnoDB会自动使用它来构建聚簇索引。
- 唯一非空索引:如果没有主键,InnoDB会选择第一个所有列都定义为
NOT NULL
的UNIQUE
索引作为聚簇索引。 - 隐藏行ID:如果以上两者都没有,InnoDB会在内部生成一个名为
GEN_CLUST_INDEX
的隐藏聚簇索引,包含一个6字节的ROWID
值。这个行ID会在插入新行时自增。但强烈不建议依赖于此,最好显式定义主键。
举例说明:
假设我们有一个users
表,结构如下:
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, -- 主键,将成为聚簇索引username VARCHAR(50) NOT NULL,email VARCHAR(100),created_at DATETIME
);
数据是如何存储的?
聚簇索引(基于id
列)的B+树结构大致如下:
- 根节点/中间节点:存储主键
id
的范围和指向子节点的指针。 - 叶子节点:直接存储了整个
users
表的所有行数据(id
,username
,email
,created_at
)。
id (PK) | username | | created_at |
1 | alice | alice@example.com | 2023-01-01 10:00:00 |
2 | bob | bob@example.com | 2023-01-02 11:00:00 |
3 | charlie | charlie@example.com | 2023-01-03 12:00:00 |
... | ... | ... | ... |
因为这些数据按id
排序存储,所以查询SELECT * FROM users WHERE id = 2;
的速度极快,B+树能直接导航到包含该id
数据的叶子页。
非聚簇索引 (Non-Clustered Index / Secondary Index)
非聚簇索引,也叫二级索引或辅助索引,是独立于聚簇索引的传统意义上的索引。它的叶子节点不包含完整的行数据,而是包含两部分:
- 索引列的键值:你创建索引时指定的列的值。
- 主键值:指向对应数据行的主键(即聚簇索引的键)。
关键特性:
- 逻辑指针:它通过主键值这个“指针”来指向实际的数据行。
- 一个表可以有多个:你可以根据需要创建很多个二级索引(如
INDEX
,UNIQUE
)。 - 需要回表查询:使用二级索引查找数据通常需要两次B+树查找:
-
- 第一次:在二级索引的B+树中查找,得到对应的主键值。
- 第二次:拿着这个主键值,回到聚簇索引的B+树中查找,最终拿到完整的行数据。这个过程称为回表(Bookmark Lookup)。
举例说明:
我们在users
表的username
列上创建一个非聚簇索引:
CREATE INDEX idx_username ON users(username);
这个索引是如何存储的?
索引idx_username
的B+树结构:
- 根节点/中间节点:存储
username
的范围和指针。 - 叶子节点:存储的是
username
的值和对应的主键id
。
username (Indexed) | id (PK) |
alice | 1 |
bob | 2 |
charlie | 3 |
... | ... |
查询过程分析:
执行查询:SELECT * FROM users WHERE username = 'bob';
- 步骤一(二级索引查找):数据库在
idx_username
的B+树中快速找到username = 'bob'
的条目,并取出其对应的主键值id = 2
。 - 步骤二(回表查询):数据库拿着这个
id = 2
,回到聚簇索引的B+树中,像最初那个例子一样,查找id = 2
的条目,最终从该叶子节点中取出所有列的数据(id=2, username='bob', email=...
)并返回。
特性 | 聚簇索引 | 非聚簇索引(二级索引) |
数量 | 每表唯一一个 | 每表可创建多个 |
内容 | 在叶子节点存储完整的数据行 | 在叶子节点存储索引列的值 + 主键值 |
速度 | 主键查询极快,一次查找即可 | 查询需要两次查找(二级索引 -> 聚簇索引,即回表) |
物理顺序 | 数据行的物理顺序与索引顺序一致 | 索引顺序与数据行的物理顺序无关 |
依赖性 | 决定数据的物理存储 | 依赖于聚簇索引(需要主键来“定位”数据行) |