线上业务卡了好几分钟,查了一条订单联表SQL,几百万行的订单表全量扫描,那感觉就像在书架里一本一本翻书找一句话。后来给它加了个联合索引,查询时间从秒级直接掉到毫秒级。就这一个改动,让我彻底明白了一个道理:SQL索引平时不显山不露水,真到了慢SQL排查那天,它就是整个数据库性能的命根子。
这篇文章不打算写成教科书式的索引科普,而是从实操视角把索引这套东西掰开揉碎讲清楚。内容包括索引底层原理、类型选型、设计准则、不同数据库的实现差异、慢SQL优化实战、索引失效场景和常见误区。不管你是刚入行的开发,还是被慢SQL折磨过的老手,这篇文章都能帮你把索引这块拼图补齐。
1. 索引到底是怎么加速SQL的
1.1 没有索引时数据库在干什么
先想一个最基础的问题:为什么一条SQL没走索引就慢?因为数据库只能做全表扫描。
全表扫描的意思就是,存储引擎把表里每一个数据页都读出来,一行一行判断是否满足WHERE条件。一个500万行的表,如果每行数据占200字节,那一趟全表扫描可能要读上千个数据页,每个数据页都是一次磁盘IO。磁盘IO是毫秒级操作,一千次IO加起来就是好几秒,这还不算CPU逐行比对的开销。
想象一下你在一个没有目录的图书馆里找一本书。你只能从第一个书架开始,一本一本地看封面,看到最后一本。图书馆管理员再熟练,也架不住这么干。索引就是那个目录,它告诉你目标书大概在第几排第几格,你直接走过去拿就行了。
数据库里这个“目录”通常是一棵B+树,它的核心价值就是把原来O(N)的线性查找复杂度降到了O(logN)级别的树查找。对于几百万行数据,B+树的树高通常只有3到4层,意味着最多做3到4次磁盘IO就能定位到目标数据。
1.2 B+树索引的工作机制
很多人背过“B+树叶子节点存数据”这句话,但理解不深。我换个说法:B+树是一棵矮胖的多路平衡树。
跟二叉搜索树不同,B+树的每个节点可以存储多个键值,所以树的宽度大、高度矮。InnoDB默认一个数据页16KB,假设主键是bigint(8字节)加指针(6字节),一个节点大约能存1170个键值。三层的B+树可以承载1170×1170×16≈两千万行的数据。这就是为什么千万级表走主键查询依然飞快——它只需要三次磁盘IO就能从根节点走到叶子节点。
B+树有两个关键设计:
第一,非叶子节点只存索引键值和指向子节点的指针,不存整行数据。这样每个数据页能容纳更多的键值,进一步加宽树、压低树高。
第二,叶子节点通过双向链表串联。这让范围查询(BETWEEN、>、<、ORDER BY)变得极其高效。找到起始位置后,顺着链表往后扫就行,不需要回到根节点重新查。
提到B+树就不得不提聚簇索引。InnoDB里,表本身就是按主键组织的B+树,主键索引的叶子节点直接存整行数据,这叫聚簇索引。非主键索引(二级索引)的叶子节点存的是主键值。所以用二级索引查数据时,先走二级索引B+树找到主键,再回聚簇索引里取整行,这个动作叫回表。
聚簇索引带来的一个隐性约束是:建表一定要有主键。如果没有,InnoDB会找第一个非空唯一索引当主键,再找不到就生成一个隐藏的rowid做主键。与其让它帮你随便搞,不如自己明确设计主键。
1.3 回表与覆盖索引:二级索引的成本问题
回表是二级索引的一大开销来源。每回表一次,就相当于多做一次主键查找。如果一次查询命中了1000行,那就多1000次主键查找。数据在Buffer Pool里还好,如果大部分页面在磁盘上,这1000次IO就是灾难。
解决回表问题的思路是覆盖索引。所谓覆盖索引,就是查询语句需要的所有列都已经包含在二级索引的叶子节点里,不需要再回表。比如表里有联合索引(user_id, create_time),执行 SELECT user_id, create_time FROM orders WHERE user_id = 1024,这个查询的所有字段都能从索引里拿,直接索引覆盖,Extra列会显示Using index。
注意:覆盖索引并非只能用于查索引列。如果你select的列恰好都是某个联合索引的组成部分,即使它们不是查询条件,也可能被覆盖到。这是一种典型的“以空间换IO”的优化策略。
2. 索引类型全盘点:到底该建哪种索引
2.1 五大基础索引类型对比
面试和工作中经常遇到的索引类型,就五种。我把核心要点整理成一张表:
| 索引类型 | 核心作用 | 关键限制 | 典型场景 |
|---|---|---|---|
| 主键索引 | 唯一标识一行,聚簇组织数据 | 一个表只能有一个,默认非空唯一 | 所有表都应该有 |
| 唯一索引 | 保证列值唯一,同时加速查询 | 允许NULL,但只允许一个NULL | 业务唯一标识,如手机号、订单号 |
| 普通索引 | 加速查询 | 无 | 高频WHERE条件列 |
| 联合索引 | 多条件组合查询加速 | 遵循最左前缀原则 | 多条件过滤、排序分组 |
| 全文索引 | 文本内容关键词搜索 | 索引维护成本高 | 文章标题、正文搜索 |
这里有一个容易踩的坑:唯一索引和普通索引在等值查询上的性能几乎没差别,但唯一索引在写入时有额外的一次唯一性校验。所以如果业务上不需要唯一约束,只是为了加速查询,建普通索引就够了,没必要为了“看着严谨”给每个查询列都上唯一索引。
2.2 联合索引与最左前缀原则
联合索引是工作中最常用的索引类型,也是最容易被用错的一种。
联合索引的原理可以理解为“先按第一个列排序,再按第二个列排序”。就像查字典,先按拼音首字母排,拼音相同再按字母顺序排。这就导致了一个核心规则:最左前缀原则。查询条件里必须包含联合索引的第一个列(最左列),索引才能生效。查询如果跳过最左列直接命中第二列,B+树就不知道从哪一段开始找。
我举个典型例子。表里有联合索引(A, B, C),下面这些查询的索引命中情况分别是:
- WHERE A = 1:能命中索引
- WHERE A = 1 AND B = 2:能命中索引
- WHERE A = 1 AND B = 2 AND C = 3:能命中索引
- WHERE B = 2:不能命中索引
- WHERE C = 3:不能命中索引
- WHERE A = 1 AND C = 3:能命中索引,但C列用不上,只能靠A列定位
联合索引列顺序的设计原则是:等值查询的列放前面,范围查询的列放后面。因为B+树在定位时,遇到第一个范围条件(>、<、BETWEEN、LIKE前缀匹配)后,后面的列就无法继续精确定位了,只能退化为过滤条件。
MySQL 5.6之后引入了索引下推(Index Condition Pushdown),允许在索引遍历过程中,先对索引包含的字段做WHERE过滤,减少回表次数。但索引下推不能解决最左前缀失效的问题,它只是优化了回表前的过滤时机。
2.3 全文索引与函数索引的特殊场景
全文索引和普通索引走的是完全不同的路子。普通B+树索引讲究“精确匹配”和“范围匹配”,而全文索引基于倒排索引,把文本内容拆成词条,再记录每个词条出现在哪些文档里。
全文索引最适合的场景是搜索文章标题、正文这类大文本字段。MySQL的全文索引默认支持英文分词,中文需要ngram解析器。如果项目里有复杂的中文搜索需求,更专业的做法是引入专门的搜索引擎组件,数据库全文索引更多是轻量级替代方案。
函数索引(表达式索引)是另一个高频需求。工作里最常见的SQL写法是 WHERE DATE(create_time) = '2024-01-01',这样写对create_time列套了函数,普通B+树索引会失效。解法有两种:要么改写SQL范围查询,要么建函数索引。
MySQL 8.0才开始支持函数索引,Oracle和PostgreSQL早就有了。函数索引的创建方式是 CREATE INDEX idx_create_date ON orders((DATE(create_time)))。建了它,上面那种DATE()包裹的查询就能走索引。但函数索引也有代价:每次INSERT和UPDATE都要重新计算函数值,写入性能会有一定损耗。另外还有一个值得注意的方向:Hibernate Search这类全文检索框架在做整库索引重建时,会把全量数据重新读取一遍并构建索引文件,重建线程数建议按CPU核数来配置。线程数开少了,重建周期长;开多了,CPU频繁上下文切换,反而拖慢整体进度。这个参数在配置文件的批处理部分调整即可。
3. 索引设计:什么时候该加,什么时候千万别加
3.1 该加索引的信号:高频查询条件列
索引设计的核心原则就一句话:为查询设计,不为表设计。加索引之前先问自己,这个列是不是经常出现在WHERE、JOIN、ORDER BY、GROUP BY里。
如果一个列经常出现在WHERE条件中,且查询频率高,这个列就应该有索引。JOIN的关联列同样重要,不加索引的JOIN会触发嵌套循环扫描,两个大表JOIN起来,全表扫描乘全表扫描,查询时间指数级上涨。
ORDER BY和GROUP BY列的索引也非常实用。B+树本身是有序结构,如果ORDER BY的列正好是索引列,排序操作可以直接利用索引的有序性,避免filesort临时排序。EXPLAIN结果里Extra列显示Using filesort,就是一个明确信号:这条SQL需要额外排序,可以考虑通过索引优化。
3.2 千万别加索引的三种情况
经验不足的同学容易走向另一个极端:给所有列都加上索引,美其名曰“全面覆盖”。这是大忌。
第一种不该加索引的列:低区分度列。区分度指的是列中不同值的比例。比如性别列只有男和女两个值,区分度极低。就算建了索引,WHERE gender = '男' 也会命中一半数据,优化器一算,走全表扫描比走索引效率更高,索引直接失效。索引的定位逻辑是“缩小范围”,低区分度列根本缩小不了范围。
第二种不该加索引的列:频繁更新的列。索引不是建好就完事了,每次UPDATE该列,B+树都要同步维护。列更新越频繁,索引维护成本越高。频繁更新加索引,等于每改一次数据都要重排一次“目录”,写性能会被拖垮。热点账号的余额字段就是典型——如果业务上需要通过账号查余额,建议单独建索引,同时评估写入频率是否可接受。
第三种不该加索引的场景:小表。几千行甚至几百行的表,全表扫描可能在0.01秒内就完成了,索引反而多了额外的维护开销和存储占用。这个阶段的优化重点应该是SQL写法本身。
3.3 区分度到底怎么算
区分度是一个可用于量化决策的概念:COUNT(DISTINCT col) / COUNT(*)。如果结果是1,说明列里每个值都不同,区分度最高;如果是0.01甚至更低,说明大部分值重复严重。
一般经验是区分度在0.2以上可以考虑建索引,低于0.1就要谨慎评估。但这里要区分两个概念:区分度和选择性。区分度高不一定代表能有效过滤。比如订单号列区分度是1,但 WHERE order_no = 'xxx' 通常只能命中一行,效率极高。再比如区域编码,区分度可能只有0.05,但业务查询常常是“查这个区域的用户”,命中比例可能只有几十分之一,反而很有效。
这个例子说明:要结合真实查询模式判断。不要光看区分度数字,要看它对“具体查询条件”的过滤能力。
3.4 索引冗余与过量
联合索引建多了,还会出现冗余问题。比如你已经建了(A, B)联合索引,又单独给A建了一个普通索引。由于联合索引(A, B)的第一列就是A,单独为A建的索引完全属于冗余索引。DROP掉单独索引,对查询几乎无影响,但索引维护成本和存储占用却实实在在减少了。
MySQL和PostgreSQL都有索引使用统计。MySQL 8.0的sys.schema_unused_indexes视图可以查出从未被使用的索引;PostgreSQL的pg_stat_user_indexes也能看到索引扫描次数。定期检查这些统计,把长期闲置的索引删掉,是DBA的日常功课之一。
提示:索引设计不是一劳永逸的。业务查询模式变了,旧的索引可能变成废索引;新需求上线前,要重新评估查询条件。我见过不少因为索引冗余导致写入性能下降的线上事故,本质都是“设计时拍脑袋,上线后不复查”。
4. 不同数据库的索引实现差异
4.1 MySQL InnoDB:聚簇结构与自适应哈希
MySQL的InnoDB存储引擎是我日常用得最多的,有两个特性值得展开说。
第一个是聚簇索引结构决定了“主键选型”的重要性。由于表数据本身按主键排序,如果主键是随机生成的UUID,插入时B+树需要不断调整节点位置,产生大量随机IO和页分裂。用自增主键或者雪花算法生成的有序ID,数据追加写入,页分裂明显减少,写入性能稳定。这是很多新人容易忽略的性能隐患。
第二个是自适应哈希索引。InnoDB在运行时会根据查询模式自动为热点页面建立哈希索引,加速等值查询。这个功能不需要人工创建,也不占显式索引空间。但它只支持等值查询,对范围查询无能为力。这正好解释了为什么我们既需要B+树索引,又需要哈希结构的辅助。
MySQL 8.0还支持索引跳跃扫描(Index Skip Scan)。它解决的是联合索引最左前缀失效的问题:查询条件只命中了联合索引的第二列,优化器如果判断成本可控,会自动跳过第一列去匹配第二列。但这是优化器的“锦上添花”,不能依赖它来掩盖索引设计失误。
4.2 SQL Server:聚簇索引、包含列与填充因子
SQL Server的索引概念跟MySQL高度相似,但有几个特征值得单独讲。
SQL Server支持在非聚集索引上定义包含列(INCLUDE)。包含列不参与索引排序和查找,只是把额外列“塞”进索引叶子节点,目的就是避免回表。这个设计和MySQL覆盖索引是同一个思路,但语法更明确:CREATE INDEX idx_orders_user ON orders(user_id) INCLUDE (order_no, create_time)。查询里如果只是把order_no和create_time取出来展示,就不再需要回表了。
SQL Server还有一个独特概念:填充因子(Fill Factor)。它控制索引页的填充比例,默认100表示完全填满。对于频繁UPDATE和INSERT的索引,页满之后再来新数据就会分裂,产生碎片。把填充因子设为70或80,让索引页预留一部分空间,减少页分裂频率。但填充因子过低也有代价——索引占用空间更大,扫描的页面数更多。这是一个需要权衡的参数。
日常维护中,SQL Server Management Studio的索引属性面板可以直接查看碎片率和填充因子配置,也可以执行ALTER INDEX REORGANIZE或ALTER INDEX REBUILD来整理碎片。如果你在用SSMS,可以通过图形化界面查看某个索引的扫描次数和碎片情况,比命令行直观不少。
4.3 Oracle:位图索引、反向键索引与函数索引
Oracle的索引体系比其他数据库更丰富,有两个索引类型在特定场景下特别好用。
位图索引是Oracle区别于MySQL的一张牌。它适合低区分度列,通过位图压缩快速匹配。比如销售系统中的区域列,只有华东、华北、华南几个值,用位图索引做多条件AND/OR组合查询非常快。但位图索引的致命弱点是并发DML写入容易锁冲突,数据仓库等只读场景用它很爽,OLTP业务千万别碰。
反向键索引解决的是另一类问题:顺序递增且并发插入的列,比如序列生成的主键ID。所有插入操作都会集中在B+树最右侧的叶节点上,形成热点块。反向键索引把键值反转存储,让新插入的数据分散到不同叶子节点,缓解写入竞争。但代价是范围查询会失效,所以使用时要想清楚业务查询模式。
Oracle对函数索引的支持也是老牌能力:CREATE INDEX idx_upper_name ON users(UPPER(name))。它的核心价值在于允许你在索引列上套函数,同时保证索引可用。MySQL 8.0开始有了同等能力,SQL Server则要依赖计算列或筛选索引来曲线救国。
4.4 PostgreSQL:物理结构、HOT更新与索引数量限制
PostgreSQL的索引机制和InnoDB有本质区别。PG的堆表按物理行存储,索引是独立的B+树结构,指向堆表的行指针。这种架构带来两个直接结果。
第一,PG没有聚簇索引的概念,可以建多个相似索引而不会引起数据重排。第二,UPDATE不会原地修改数据,而是生成新版本行(MVCC机制),旧版本留给事务复用。如果频繁更新索引列,索引里会持续积累指向旧版本行的指针,索引膨胀速度比MySQL快得多。PG的HOT(Heap-Only Tuple)更新机制只针对非索引列有效,索引列更新仍然绕不开索引维护的代价。
还有一个被很多团队忽略的问题:PG单表索引数量不宜过多。热词里提到的“pg栏位索引超过许可范围”本质上就是管理层面的约束——每个索引都有独立存储和写入维护成本,索引数量一旦泛滥,即使某个查询变快了,全表写入速度也会被拖垮。严格来说索引配额需要根据表写入频率和查询热度综合评估,通常单表单列索引加联合索引总数控制在五个以内比较合理,核心业务大表还要更谨慎。定期用pg_stat_user_indexes查看索引扫描次数,扫不着的索引就该清理。
5. 慢SQL优化实战:从定位到验证全流程
5.1 第一件事:开启慢查询日志并捕获目标SQL
优化慢SQL的前提是知道哪些SQL慢。我平时的工作流是这样的。
MySQL里开启慢查询日志只改一个参数:SET GLOBAL slow_query_log = ON; 同时设置阈值 long_query_time = 1,超过1秒的SQL会记录到日志。上线一段时间后,通过mysqldumpslow工具把日志聚合排序,找出执行次数最多、单次耗时最长的TOP N SQL。
SQL Server侧用DMV语句查询:sql SELECT TOP 10 * FROM sys.dm_exec_query_stats ORDER BY total_worker_time DESC;
拿到目标SQL之后,先别急着加索引。加索引是药,诊断是病,得先判断这个慢到底是“没有索引”还是“SQL写法有根本性问题”。
5.2 用EXPLAIN分析执行计划
EXPLAIN是MySQL分析SQL执行计划的核心工具,用法是在SQL前加EXPLAIN关键字。看执行计划时,我优先看四列:type、key、rows、Extra。
type列告诉你能用什么方式访问表。从好到差依次是system > const > eq_ref > ref > range > index > ALL。看到ALL意味着全表扫描,这是最需要警惕的。key列表示实际命中的索引名,如果为NULL说明没走索引。rows列是优化器预估需要扫描的行数,这个数字越小越好。Extra列信息量极大:Using filesort代表额外排序,Using temporary代表临时表,Using index代表覆盖索引,Using where表示存储引擎返回后还要过滤。
我拿一个真实案例来演示。有个订单表orders,500万行数据,SQL长这样:
SELECT id, order_no, amount, status FROM orders WHERE user_id = 1024 ORDER BY create_time DESC LIMIT 10;优化前EXPLAIN结果如下:
type: ALL key: NULL rows: 4580000 Extra: Using filesorttype是ALL,扫了458万行,还有Using filesort。一眼就能断定:user_id和create_time都没有合适索引,先全表扫再排序。
我的优化动作是建联合索引:
CREATE INDEX idx_orders_user_time ON orders(user_id, create_time);为什么用联合索引而不是两个单列索引?因为WHERE条件是user_id等值查询,ORDER BY是create_time排序。联合索引(user_id, create_time)让user_id精准定位后,create_time天然有序,连排序的filesort都省了。优化后EXPLAIN结果:
type: ref key: idx_orders_user_time rows: 23 Extra: NULL从扫458万行到扫23行,从Using filesort到无额外操作,这个优化只花了一分钟建索引的时间。
5.3 索引失效的七个经典场景
建了索引不等于一定走索引。以下七种场景在排查慢SQL时必须对照检查。
场景一:对索引列使用函数。WHERE DATE(create_time) = '2024-01-01',索引在create_time上,但在compare之前先做了函数运算,B+树无法定位。改写成 create_time >= '2024-01-01' AND create_time < '2024-01-02'。
场景二:隐式类型转换。手机号列phone是varchar类型,SQL写成 WHERE phone = 13800138000,数字常量会被隐式转为字符串,但转换过程中索引失效。正确写法是 WHERE phone = '13800138000'。
场景三:前导模糊查询。WHERE name LIKE '%张%',%号在最前面,B+树无法按前缀定位。改成 WHERE name LIKE '张%' 则能使用索引。前导模糊的优化方案要么用覆盖索引,要么上全文索引,要么引入搜索组件。
场景四:OR条件全列非索引。WHERE a = 1 OR b = 2,只有a列有索引,b列没有。优化器可能放弃索引走全表扫描。解法是把OR拆成两个查询UNION ALL,或者给b列也加上索引。
场景五:NOT IN、!=、NOT LIKE等否定条件。B+树擅长等值和范围定位,排除性条件很难精确定位。如果业务确实需要这类查询,还是要回到覆盖索引和改写SQL的思路上去。
场景六:优化器认为全表更快。低区分度列、小表、数据分布严重倾斜时,优化器经过成本估算后可能主动放弃索引。这种“失效”其实不叫失效,叫理性选择。与其纠结为什么没走索引,不如从SQL业务逻辑层面重新设计。
场景七:ORDER BY与GROUP BY的顺序不一致。索引要求ORDER BY和GROUP BY的列顺序完全一致,方向也要一致。如果一个是ASC一个是DESC,索引无法完全复用,排序动作还是会触发。
注意:MySQL 8.0的优化器越来越聪明,某些场景下即使用了不等于或OR也可能灵光闪现选择索引。但排查问题的思路不能变:先确认类型,再确认索引列,三查SELECT字段是否需要回表,最后看Extra提示。
6. 索引维护与常见排坑记录
6.1 碎片清理与统计信息更新
索引用久了会积累碎片,碎片来源主要是两类:随机插入导致的页分裂,以及频繁UPDATE导致的索引节点重排。碎片会让索引页利用率下降,需要扫描更多页面才能拿到同样多数据。
MySQL的常见处理是执行 OPTIMIZE TABLE table_name,InnoDB会重建表和索引,消除碎片。但注意OPTIMIZE TABLE会锁表,线上大表要选业务低峰期。
PG的命令是 REINDEX,SQL Server是 ALTER INDEX REORGANIZE 或 REBUILD。不管哪个数据库,维护思路都是:定期重建,消除碎片,重新统计信息。
统计信息同样重要。优化器选索引靠的就是统计信息里的行数评估,如果统计信息过旧,优化器可能选错索引。MySQL执行ANALYZE TABLE table_name、PG执行ANALYZE、SQL Server的开销计算器会自动更新,但大表变更后建议手动更新一次。
6.2 关于索引的七个常见误区
我整理了工作里遇到最多的几个错误认知,对照表如下:
| 误区 | 真相 |
|---|---|
| 索引越多越好 | 每条索引都拖慢INSERT/UPDATE,占用存储,还可能干扰优化器选择 |
| 唯一索引比普通索引快 | 两者查询性能几乎一致,唯一索引写入时还有额外校验开销 |
| 加了索引肯定能加速 | 区分度低、写法不对、优化器放弃,都会导致索引白建 |
| 主键用UUID无所谓 | 随机主键导致页分裂严重,写入性能断崖式下降 |
| 小表也应该建索引 | 小表全表扫描可能比索引查找更快 |
| 联合索引的列顺序无所谓 | 列顺序决定最左前缀能不能命中,顺序错了一条SQL都救不了 |
| 索引能防SQL注入 | 这是两回事。注入靠参数化查询防范,索引只管查询加速 |
最后一条误区值得展开说几句。索引和SQL注入完全是两个层面的问题。某次安全审计中,有同事认为“给username建了索引,注入查询就跑不快了”,这个想法很危险。SQL注入攻击的本质是拼接字符串绕过认证逻辑,比如万能密码绕过,核心手法是 ' OR 1=1 -- 这类输入让WHERE条件恒为真。即使username上有索引,攻击者构造的恒真条件照样全表读取。防御注入靠的是参数化查询、预编译Statement和输入校验,跟索引没有半点关系。
6.3 我自己的索引工单处理习惯
最后分享一套我个人排查索引问题的固定流程,也是这些年踩坑踩出来的经验。
拿到一条慢SQL,第一步永远先跑EXPLAIN确认执行计划,看type和key。第二步打开索引使用统计,确认这个表上有没有冗余索引和闲置索引。第三步才是动手加索引。加完索引不是结束,要在同样数据量下跑一遍原SQL,对比耗时和执行计划。第四步是观察业务高峰期,确认没有因为新索引拖慢写入。
有两次踩坑经历印象特别深。一次是给订单表一次加了三个联合索引,结果写入耗时翻了快一倍,原因就是忽视了索引维护成本。另一次是优化完SQL后发现执行计划里多了Using filesort,仔细一查才发现是ORDER BY的列顺序和联合索引不一致。从那之后我养成了一个习惯:所有索引设计都先用EXPLAIN验证,用数据说话,不做拍脑袋决策。
索引这块内容很容易让人觉得“道理我都懂”,但一到线上就被现实打脸。问题往往不出在单一知识点上,而是原理、设计、验证三个环节之间缺了闭环。希望你读完这篇文章,能从全表扫描的慢SQL里看出索引缺失的影子,也能在建索引之前多问自己一句:这个索引真的需要吗,真能用到吗。