先搞懂:为什么索引能让SQL变快?
要调优先懂原理——索引的底层是B+树结构(大部分关系型数据库如MySQL、PostgreSQL默认使用)。B+树的特点是:叶子节点有序且相连,非叶子节点存索引值,叶子节点存数据地址(或直接存数据,如覆盖索引)。

举个通俗的例子:假设你要从100万行的user
表中查id=123
的用户,如果没有索引,数据库要逐行扫描(全表扫描);如果建了id
的主键索引(默认是B+树),只需要从根节点往下查3-4层就能找到目标——这就是索引的“加速魔法”。
但注意:不是所有字段都适合建索引!比如“性别”字段(区分度只有2种),建索引反而会增加存储开销,因为索引文件本身要占空间,而且写入时还要维护索引结构。
踩过的坑:那些无效的索引设计
很多人建索引的误区是“越多越好”,但无效索引反而会拖慢性能。以下是我在项目中踩过的典型坑:
1. 冗余索引:联合索引包含单值索引
比如建了(user_id, create_time)
的联合索引,又单独建user_id
的单值索引——这就是冗余!因为联合索引的最左前缀原则(后面会讲),user_id
作为第一个字段,已经能覆盖单值查询的场景。
2. 重复索引:同一字段建多个索引
比如user
表的phone
字段,既建了UNIQUE
索引(唯一约束),又建了普通索引——完全没必要,唯一索引已经能满足查询需求。
3. 低区分度字段建索引
比如“性别”“状态(仅0/1)”这类字段,查出来的结果集占总数据的50%以上,数据库会认为“走索引不如全表扫描快”,直接跳过索引。
4. 高频率更新字段建索引
比如日志表的log_time
字段,每秒插入100条数据——建索引会导致每次插入都要调整B+树结构,写入性能下降50%以上!这种场景建议用“分区表”替代索引(比如按月份分区)。
实战技巧:索引设计的“黄金法则”
要建有效的索引,记住这4条原则:
原则1:优先给“频繁作为查询条件的字段”建索引
比如电商系统的order
表,user_id
(用户查自己的订单)、order_no
(根据订单号查详情)都是高频查询字段,必须建索引。
原则2:联合索引遵循“最左前缀原则”
如果你的查询是WHERE user_id=123 AND create_time>'2025-08-01'
,建(user_id, create_time)
的联合索引才有效——顺序不能乱!如果反过来建(create_time, user_id)
,查询时user_id
的条件无法用到索引。
小技巧:如果联合索引是(a,b,c)
,那么以下查询会走索引:
– a=?
– a=? AND b=?
– a=? AND b=? AND c=?
但b=? AND c=?
不会走索引(缺最左的a)。
原则3:用“覆盖索引”减少回表
“回表”是指:查询时用索引找到数据地址后,还要再查一次主键索引获取完整数据。比如SELECT id,name FROM user WHERE phone='138xxxx1234'
,如果phone
建了索引,且id,name
是索引字段(覆盖索引),就不用回表——直接从索引中取数据。
如何建覆盖索引? 把查询需要的字段都包含在索引里。比如查询SELECT order_id, total_amount FROM order WHERE user_id=123
,可以建(user_id, order_id, total_amount)
的联合索引,完全覆盖查询字段。
原则4:避免“索引失效”的写法
即使建了索引,以下写法会让索引“失效”:
– WHERE
子句中用函数:LEFT(phone,3)='138'
(改成phone LIKE '138%'
)
– 字段运算:age+1=30
(改成age=29
)
– OR
条件(除非所有字段都建了索引):WHERE user_id=123 OR phone='138xxxx'
(用UNION
替代)
实战技巧:SQL语句优化的8个关键点
索引是“硬件基础”,语句优化是“软件技巧”,以下是我在项目中常用的优化方法:
1. 用SELECT 具体字段
替代SELECT *
SELECT *
会导致:
– 无法使用覆盖索引(必须回表取所有字段);
– 传输更多无用数据(比如大文本字段)。
比如原语句:SELECT * FROM order WHERE user_id=123
优化后:SELECT order_id, create_time, total_amount FROM order WHERE user_id=123
(用覆盖索引)。
2. 避免“全表扫描”的关键词
以下关键词会触发全表扫描,尽量不用:
– WHERE
子句中的!=
或<>
;
– LIKE
的模糊匹配(%xxx%
,比如name LIKE '%张三%'
);
– ORDER BY
非索引字段(比如ORDER BY name
但name
没建索引)。
替代方案:LIKE '张三%'
(前缀匹配,能走索引);ORDER BY
用联合索引的字段(比如ORDER BY user_id, create_time
,对应联合索引(user_id, create_time)
)。
3. JOIN优化:小表驱动大表
比如查“用户的订单列表”,user
表有10万行,order
表有1000万行——正确的写法是:SELECT u.name, o.order_id FROM user u INNER JOIN order o ON u.id=o.user_id WHERE u.id=123
因为user
是小表,先查小表的结果,再关联大表,比“大表驱动小表”快10倍以上!
注意:LEFT JOIN
要让“左表是小表”,RIGHT JOIN
让“右表是小表”。
4. 用EXISTS
替代IN
(当子查询结果大时)
比如查“有订单的用户”:
原语句:SELECT * FROM user WHERE id IN (SELECT user_id FROM order)
优化后:SELECT * FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id=u.id)
因为IN
会先查子查询的所有结果,再匹配;EXISTS
只要找到一条匹配就停止,更高效。
工具辅助:用EXPLAIN定位慢查询根源
调优的关键是“找到问题在哪”——EXPLAIN
是MySQL自带的“慢查询显微镜”,能帮你看SQL的执行计划。
如何用EXPLAIN?
在SQL语句前加EXPLAIN
,比如:EXPLAIN SELECT * FROM user WHERE user_id=123
重点看这3个字段:
-
type:执行类型,从好到差的顺序是:
const
(常量查询,比如主键查询)>eq_ref
(唯一索引扫描)>ref
(非唯一索引扫描)>range
(范围扫描,比如BETWEEN
)>ALL
(全表扫描)。
如果type
是ALL
,说明没走索引,要优化! -
key:实际使用的索引,如果是
NULL
,说明没走索引。 -
rows:预估扫描的行数,数值越小越好(比如从100万降到100)。
案例:之前遇到一条慢查询:SELECT * FROM order WHERE user_id=123 AND create_time>'2025-08-01'
用EXPLAIN
看:type=ALL
,key=NULL
——原因是没建(user_id, create_time)
的联合索引!建了之后,type
变成range
,rows
从100万降到500,查询时间从3秒降到0.1秒。
最后想说:性能调优是个“持续过程”
调优不是“一次到位”,而是“监控-分析-优化-再监控”的循环:
1. 开启慢查询日志:MySQL中设置slow_query_log=1
,long_query_time=1
(记录超过1秒的查询);
2. 定期分析日志:用mysqldumpslow
工具统计慢查询的TOP10(比如mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
);
3. 迭代优化:比如某条SQL的rows
从10万降到1万,但还是慢——再看EXPLAIN
的Extra
字段(比如Using filesort
,说明排序用了临时文件,要优化ORDER BY
的字段)。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/405