什么是索引?
数据库索引是一种独立于表数据维护的数据结构。它按照一个或多个列的值组织键,并保存键到数据位置的映射,让数据库在大量记录中快速定位目标行,而不必每次都从第一行扫描到最后一行。
简单说,索引就是数据库里的目录。它解决的是“如何从大量数据中快速找到少量目标记录”这个问题,常见于用户手机号、邮箱、订单号、用户 ID、时间字段等经常出现在查询条件中的列。
索引的价值可以概括为三点:
- 减少需要检查的数据量:把全表扫描转化为索引定位和少量数据读取。
- 支持有序访问:B+Tree 等索引可以帮助完成范围查询和部分排序。
- 用空间换时间:索引需要额外磁盘空间,也会增加写入和维护成本。
因此,索引不是表数据的另一份完整复制,也不是“加上就一定更快”的开关。数据库仍会根据数据量、选择度、统计信息和查询条件决定是否使用它。

没有索引时,数据库为什么要全表扫描?
没有可用索引时,数据库通常只能执行全表扫描(Full Table Scan):从表的第一页开始读取数据,逐行判断是否满足 WHERE 条件,直到扫描完整张表或找到足够结果。
假设 users 表有一千万行数据,现在要查手机号为 13800000000 的用户:
SELECT *
FROM users
WHERE phone = '13800000000';
如果 phone 上没有索引,数据库无法直接知道目标行位于哪一页,只能检查第一行、第二行以及后续数据。目标记录刚好在末尾时,最坏情况就是检查一千万行;即使目标在前面,数据库通常也要继续扫描,以确认是否存在其他匹配行。
全表扫描的成本不仅是比较一千万次字符串。数据库还要读取数据页、把页面载入缓冲池,并完成行可见性和条件判断。表越大、查询越频繁,扫描带来的 CPU、内存和磁盘读压力越明显。

索引为什么像书的目录?
书的目录和数据库索引解决的是同一种定位问题:先通过有序的主题找到页码,再跳到目标内容,而不是从第一页逐页翻阅。
| 书本目录 | 数据库索引 |
|---|---|
| 章节或主题 | 索引键值,例如 phone 或 user_id |
| 章节对应的页码 | 记录位置、数据页位置或行指针 |
| 按章节顺序组织 | 按索引键顺序组织 |
| 先查目录再翻页 | 先查索引再读取表数据 |
例如,一个按手机号建立的索引会把手机号按顺序组织起来,并为每个键保存对应的数据位置。数据库查找 13800000000 时,先在索引中缩小范围,再根据叶子节点中的指针读取用户行。
这里的“位置”不一定是永远不变的物理地址。不同数据库和存储引擎可能使用行 ID、堆表中的 TID、数据页加槽位,或者直接把整行数据放在索引叶子节点中。索引真正提供的是一条由键到记录的可维护访问路径。

B+Tree 索引是如何查找数据的?
B+Tree 是关系型数据库中最常见的索引结构之一。它是一棵多路平衡搜索树:内部节点负责导航,叶子节点保存索引键和数据引用,所有叶子节点处于同一层,并且通常通过链表连接,方便连续读取。
一次等值查询可以拆成四步:
- 从根节点开始:数据库把目标值与根节点中的分隔键比较。
- 选择子节点:根据目标值所在的区间,沿指针进入对应的内部节点。
- 到达叶子节点:重复比较,直到找到目标键所在的叶子页。
- 读取数据行:如果叶子保存的是行指针,再根据指针读取表数据;如果索引本身包含所需列,则可能直接返回结果。
以查找 38 为例,如果根节点包含 30、60、90,目标值会进入 30 到 60 的子树;继续比较后,数据库在叶子节点找到 38,再读取对应行。
严格来说,B+Tree 的内部节点通常不保存完整数据行,而是保存用于导航的键和子指针;叶子节点保存完整索引键以及指向数据的引用。这个设计让内部节点可以放下更多键,从而降低树高。

B+Tree 为什么能减少磁盘读写?
B+Tree 的核心优势是“扇出大、树高低、叶子有序”。一个节点通常对应一个数据页,单个页可以容纳很多键,因此数据库不需要为每条记录单独增加一层树节点。
在数据量很大的情况下,一棵 B+Tree 的高度经常只有几层,但具体高度取决于键大小、页大小、数据量、填充率和存储引擎实现。查询时,数据库大致需要读取根页、若干内部页和叶子页;其中已经在缓冲池中的页面可能不需要再次从磁盘读取。
B+Tree 对范围查询也很友好:找到范围起点后,可以沿叶子节点链表顺序读取后续键,而不是为每个值重新从根节点查找。
SELECT order_id, amount
FROM orders
WHERE user_id = 1001
AND create_time >= '2026-09-01'
AND create_time < '2026-10-01'
ORDER BY create_time;
如果索引顺序与查询条件和排序需求匹配,数据库可以在索引中先定位用户和时间范围,再顺序读取相关键。需要注意的是,B+Tree 让访问路径更短,并不代表结果行读取、回表、排序和其他过滤条件都免费。

哈希索引适合什么查询?
哈希索引通过哈希函数把索引键映射到桶或槽位,适合精确匹配一个值的等值查询。它不需要按大小逐层比较,理想情况下可以直接定位到目标桶。
SELECT *
FROM users
WHERE phone = '13800000000';
哈希索引的主要边界是:它通常只擅长 =,不擅长范围、排序和前缀匹配。下面的查询需要知道一段有序区间,哈希值本身并不保留原始大小关系,因此无法像 B+Tree 那样顺序找到 20 到 30 的所有值:
SELECT *
FROM users
WHERE age BETWEEN 20 AND 30;
实际能力还取决于数据库产品和版本。例如,PostgreSQL 的 Hash 索引面向等值比较;MySQL/InnoDB 常规业务中更常见的是 B+Tree 索引。选择索引类型时,应以目标数据库的官方文档和真实执行计划为准,而不是只看数据结构名称。

索引是不是越多越好?
索引越多,读查询可选择的路径越多,但写入、存储和维护成本也越高。每次插入、更新或删除数据时,数据库都可能需要同步修改受影响的索引页、维护树结构并产生额外日志。
| 维度 | 没有索引 | 合理建立索引 | 索引过多 |
|---|---|---|---|
| 读取 | 可能触发大范围扫描 | 可能快速定位少量数据 | 优化器需要从更多路径中选择 |
| 插入、更新、删除 | 维护成本较低 | 增加有限维护成本 | 写放大和锁竞争可能更明显 |
| 存储空间 | 只保存表数据 | 增加索引页和元数据 | 占用更多磁盘与缓冲池 |
| 适用状态 | 写多读少或小表 | 查询模式稳定且收益明确 | 重复、低收益或很少使用的索引 |
索引设计本质上是在查询速度、写入延迟、存储空间和维护复杂度之间做取舍。不要因为“以后可能会查”就给每个字段建立索引;应先观察真实 SQL、访问频率和执行计划,再决定是否增加或合并索引。

哪些列适合建立索引?
经常出现在 WHERE、JOIN ON、ORDER BY 或 GROUP BY 中,且能够有效缩小结果范围的列,通常更值得评估索引。
常见候选列包括:
- 高频等值查询列:用户表的手机号、邮箱,订单表的订单号。
- 关联列:订单表的
user_id、明细表的order_id,尤其是经常参与 JOIN 的列。 - 范围和排序列:订单的
create_time、商品的价格,但要结合查询是否会返回大量结果判断。 - 高选择度列:不同值很多的列更容易把候选数据缩小到较小范围。
选择度可以粗略理解为“列中不同值的数量占总行数的比例”。手机号和订单号通常选择度高;性别、是否删除、状态等低基数字段的选择度较低。低选择度列不是绝对不能建索引,但如果一次查询会命中表中很大比例的行,优化器可能认为直接扫描表更便宜。
建索引前至少要回答三个问题:这条查询是否足够频繁?索引能否显著减少候选行?写入维护成本是否可以接受?最终还要用 EXPLAIN 或数据库对应的执行计划工具验证,而不是只凭列名猜测。

什么是复合索引?最左前缀原则如何工作?
复合索引,也叫联合索引,是把多个列按固定顺序放进同一个索引。例如:
CREATE INDEX idx_orders_user_time
ON orders (user_id, create_time);
这个索引先按 user_id 排序;在相同 user_id 内,再按 create_time 排序。它适合“先筛用户,再筛时间”的查询:
SELECT *
FROM orders
WHERE user_id = 1001
AND create_time >= '2026-09-01';
最左前缀原则指的是:复合索引通常需要从最左列开始使用,后面的列才有机会继续参与索引定位。对于 (user_id, create_time):
| 查询条件 | 通常能否利用索引前缀 | 原因 |
|---|---|---|
user_id = 1001 | 可以 | 使用第一列 |
user_id = 1001 AND create_time >= ... | 可以 | 从第一列开始,并继续使用第二列 |
create_time >= ... | 通常不能充分利用 | 跳过了最左列 |
user_id = 1001 AND status = 'paid' | 可以使用 user_id 部分 | status 不在该索引中,仍需额外过滤 |
“索引失效”不是一个只由 SQL 文字决定的绝对结论。范围条件、函数包裹列、隐式类型转换、LIKE 以通配符开头、统计信息过期以及结果集过大,都可能影响优化器的选择。复合索引的列顺序应依据真实查询的过滤、排序和结果集特征设计,并通过执行计划确认。

电商订单表应该怎样使用索引?
电商订单页是索引最直观的应用:用户打开页面时,系统通常需要按用户 ID 查找订单,并按创建时间倒序排列。
SELECT order_id, status, amount, create_time
FROM orders
WHERE user_id = 1001
ORDER BY create_time DESC
LIMIT 20;
如果 orders 有几百万行而没有相关索引,数据库可能每次请求都扫描大量订单,再排序并截取前 20 条。可以评估与访问模式匹配的索引:
CREATE INDEX idx_orders_user_time
ON orders (user_id, create_time DESC);
这个索引先定位 user_id = 1001 的订单,再按时间顺序读取,通常比扫描整张订单表更合适。若查询只需要少量固定列,还可以评估包含这些列的覆盖索引或数据库特有的 INCLUDE 能力,减少回表,但索引会因此变大,写入成本也会增加。
反过来,如果为同一张表建立五个高度重叠的用户、用户加状态、用户加时间、用户加金额、用户加商品索引,下单时每一条订单写入都可能需要维护多份结构。正确做法不是堆叠索引,而是收集主要查询,检查现有索引是否能复用,再用执行计划和线上指标验证收益。

建索引时有哪些常见坑?
索引常见的问题不是“不会创建”,而是创建了不适合访问模式的索引。下面这些情况尤其需要留意:
- 把所有列都建索引:索引数量增加不等于查询一定更快,重复索引还会增加写入和维护成本。
- 忽略列顺序:复合索引的顺序应由真实查询模式决定,不能简单按建表字段顺序排列。
- 只看是否命中索引:命中索引不代表成本最低;如果需要读取大量行,优化器可能选择全表扫描。
- 对索引列使用函数或表达式:例如
WHERE DATE(create_time) = ...可能无法直接使用普通列索引,通常应改写范围条件或评估函数索引。 - 忽略隐式类型转换:比较两种不同类型的值可能使索引利用率下降,甚至带来错误的比较语义。
- 用低选择度列期待高收益:状态、性别、布尔值等字段要结合数据分布和查询比例判断。
- 忽略回表成本:二级索引找到的是行指针时,数据库还要再读取表数据;返回列很多时,索引收益可能被回表抵消。
- 忘记维护统计信息:优化器依赖统计信息估算成本,数据分布变化后应按数据库产品配置分析或更新统计信息。
诊断索引问题时,可以从一条真实慢 SQL 开始,检查执行计划中的访问类型、扫描行数、过滤行数、排序和回表情况,再决定调整 SQL、索引顺序还是数据模型。不要只根据“有没有走索引”这一项下结论。
常见问题
索引一定会让查询变快吗?
不一定。索引只有在能够减少实际工作量且自身访问成本低于全表扫描时才更有价值。小表、低选择度条件、返回大部分行或统计信息不准确时,数据库可能主动选择全表扫描。
主键是不是自动有索引?
大多数关系型数据库会为主键创建唯一索引或等价的唯一访问结构,但具体实现和索引是否聚簇取决于数据库产品。主键索引解决的是唯一性与定位问题,不代表其他查询列也自动拥有索引。
索引和主键有什么区别?
索引是加速查找的数据结构;主键是表中唯一标识一行记录的约束。主键通常会依赖索引实现,但普通索引不要求值唯一,也不承担主键的实体标识职责。
B+Tree 和哈希索引应该怎么选?
如果查询包含范围、排序、前缀匹配或需要有序扫描,通常优先评估 B+Tree;如果访问模式几乎只有等值匹配,并且目标数据库对哈希索引有成熟支持,才评估哈希索引。最终选择应以真实执行计划和基准测试为准。
复合索引中列越多越好吗?
不是。列越多,索引越大,写入维护成本越高,更新任意一列时也可能触发更多索引工作。应围绕主要查询设计最小且有用的列集合,并检查是否存在覆盖、重复或长期未使用的索引。
为什么建了索引,查询还是很慢?
可能是查询返回行太多、条件选择度低、发生回表或排序,也可能是函数、隐式转换、最左前缀不匹配、统计信息过期或优化器判断全表扫描更便宜。使用 EXPLAIN 检查实际执行计划,才能定位具体原因。
总结:索引的本质是什么?
索引是数据库用额外空间和写入维护成本换取查询速度的数据结构。它通过预先组织键值,把“从整张表里逐行寻找”变成“沿索引路径定位,再读取少量数据”。B+Tree 适合等值、范围和有序访问;哈希索引主要适合等值查询;复合索引则要求根据访问模式设计列顺序。
真正重要的不是记住“索引越多越快”,而是建立一套判断流程:
- 找到真实且高频的查询。
- 评估条件的选择度、排序需求和结果集大小。
- 选择合适的单列或复合索引,并控制重复结构。
- 用
EXPLAIN、慢查询日志和线上指标验证。 - 持续关注写入延迟、索引空间、统计信息和长期使用情况。
一句话记住:索引不是免费的加速器,而是用写入开销和存储空间,换取少量数据的快速定位;没有最好的索引,只有最适合查询模式的索引。


