转自:程序员越
查询优化的核心本质:在消耗MySQL最少系统资源(CPU、内存、磁盘I/O、网络带宽)的前提下,实现最快的查询响应速度。而实现这一目标的最佳实践,就是合理、高效地利用好索引——索引是MySQL查询的“加速键”,用好索引能避免全表扫描、减少资源消耗,反之则会导致查询效率低下,浪费系统资源。以下是经过实战验证的核心优化技巧,兼顾实用性和可操作性。
一、基础查询优化(减少无效资源消耗)
1. 只查询需要的字段,拒绝“SELECT *”
-
❌ 避免写法:SELECT * FROM table_name;(查询表中所有字段,无论是否需要) -
✅ 推荐写法:SELECT id, name, age FROM table_name;(精准指定所需字段) -
核心原因: -
减少网络传输量:无需传输无用字段的数据,尤其当表中存在TEXT、BLOB等大字段时,优化效果更明显; -
增加覆盖索引的使用机会:若查询的所有字段都包含在索引中,MySQL无需回表查询,直接从索引中获取数据,大幅提升速度; -
降低内存消耗:减少查询结果集的体积,降低MySQL缓冲区和应用层内存的占用。
-
2. 善用覆盖索引,避免回表查询
覆盖索引(Covering Index)是指查询的所有列(SELECT字段、WHERE条件字段)都包含在同一个索引中,MySQL无需通过索引再去硬盘查询数据表的完整数据行(即“回表”),直接从内存中的索引获取所有所需数据,查询速度极快。
-
示例:若users表的name字段建有索引,执行SELECT id, name FROM users WHERE name = ‘Alice’时,id(主键,默认包含在二级索引中)和name都在索引中,无需回表,查询效率大幅提升。 -
关键技巧:创建索引时,可将常用查询字段(SELECT、WHERE、ORDER BY、GROUP BY涉及的字段)纳入索引,构建“查询友好型”覆盖索引。
3. 禁止在索引列上做运算或函数操作,避免索引失效
MySQL的优化器无法识别索引列上的运算或函数操作,会直接放弃使用索引,转而执行全表扫描,导致资源消耗剧增、查询变慢。
-
❌ 避免写法:WHERE YEAR(create_time) = 2026;(对索引列create_time执行YEAR函数,索引失效) -
✅ 推荐写法:WHERE create_time >= ‘2026-01-01’ AND create_time < ‘2027-01-01’;(直接对字段值进行范围判断,正常使用索引) -
延伸提醒:类似的错误写法还有WHERE id + 1 = 100、WHERE SUBSTR(name, 1, 1) = ‘张’,均会导致索引失效,需提前规避。
4. 优化LIKE查询,避免前缀模糊匹配
LIKE查询的模糊匹配方式,直接影响索引的使用,核心原则是:避免“前缀模糊”(%开头),优先使用“后缀模糊”(%结尾)。
-
❌ 避免写法:WHERE name LIKE ‘%张%’;(前缀和后缀都模糊,无法使用索引,触发全表扫描) -
✅ 推荐写法:WHERE name LIKE ‘张%’;(仅后缀模糊,可正常使用name字段的索引) -
特殊场景处理:若必须实现“前缀模糊”查询(如模糊搜索用户名),可使用MySQL全文索引(FULLTEXT INDEX)替代LIKE,提升查询效率。
5. 优化OR和IN,避免索引失效
(1)OR查询优化
OR连接的多个条件中,只要有一个条件对应的列没有索引,整个查询就可能放弃索引,执行全表扫描。建议将OR查询拆分为UNION或UNION ALL(无重复数据时优先使用,效率更高)。
-- 索引可能失效(假设age字段无索引)
SELECT * FROMusersWHEREname = '张三'OR age = 25;
-- 优化为UNION(去重,效率略低)
SELECT * FROMusersWHEREname = '张三'
UNION
SELECT * FROMusersWHERE age = 25;
-- 优化为UNION ALL(无重复数据,效率更高,优先推荐)
SELECT * FROMusersWHEREname = '张三'
UNIONALL
SELECT * FROMusersWHERE age = 25;
(2)IN查询优化
IN列表中的值不要过多(建议不超过1000个),否则MySQL优化器可能放弃使用索引,转为全表扫描。若IN列表值过多,可拆分为多个小批量IN查询,或使用JOIN替代。
6. 优化分页查询(LIMIT),解决深分页问题
深分页(如LIMIT 1000000, 10)是MySQL分页查询的常见性能瓶颈,原因是MySQL会扫描前1000010条数据,再丢弃前1000000条,仅返回10条,导致磁盘I/O和CPU消耗剧增。推荐两种优化方案:
方案1:延迟关联(先查主键,再JOIN原表)
先通过索引查询出需要的主键ID,再通过主键JOIN原表获取完整数据,避免扫描大量无用数据。
SELECT t1.* FROMtable t1
INNERJOIN (
SELECTidFROMtableWHEREstatus = 2
ORDERBYidLIMIT1000000, 10
) t2 ON t1.id = t2.id;
方案2:记录上次位置(适用于连续翻页)
通过主键ID的范围查询替代LIMIT偏移量,直接定位到需要的数据,无需扫描前面的无效数据,效率极高。
-- 假设上一页最后一条数据的id为1000000
SELECT * FROM table WHERE id > 1000000 LIMIT 10;
7. 补充基础优化细节
-
优先使用UNION ALL替代UNION:UNION会进行去重操作(需排序),消耗额外资源;UNION ALL不去重,效率更高,无重复数据时必用。 -
LIMIT查询必须显式使用ORDER BY:若不指定ORDER BY,MySQL返回的结果顺序不确定,且可能无法利用索引优化,建议始终搭配ORDER BY(如按主键排序)。 -
ORDER BY列值有重复时,需加唯一索引列联合排序:避免排序结果混乱,同时提升排序效率(如ORDER BY age ASC, id asc,id为唯一主键)。
二、关联查询优化(JOIN优化,减少关联消耗)
JOIN查询是MySQL中最常用的复杂查询方式,优化核心是“减少循环次数、利用索引、避免临时表”,具体技巧如下:
1. 小表驱动大表,减少循环次数
JOIN查询的底层逻辑是“嵌套循环”,即通过小表的每条数据,去匹配大表的对应数据。小表驱动大表(小表作为驱动表,大表作为被驱动表)能大幅减少循环次数,降低CPU消耗。
示例:用用户表(小表,1000条数据)驱动订单表(大表,100万条数据),循环次数为1000次;若用订单表驱动用户表,循环次数为100万次,效率天差地别。
2. JOIN的连接字段必须加索引
JOIN的连接字段(如orders.user_id = users.id)是关联查询的核心,必须给这两个字段都建立索引,否则会导致被驱动表全表扫描,关联效率极低。
推荐做法:给orders.user_id建立二级索引,给users.id建立主键索引(默认已存在),确保关联时能快速匹配数据。
3. 明确ON与WHERE的区别,合理放置条件
ON和WHERE的条件放置位置,会影响查询结果和效率,尤其对LEFT JOIN影响极大,核心区别如下:
-
INNER JOIN:ON和WHERE条件效果基本一致,MySQL优化器会自动合并处理,无需刻意区分。 -
LEFT JOIN: -
ON中的条件:在生成临时表时过滤数据,不影响左表数据的返回(左表所有数据都会保留,右表匹配不到的字段为NULL); -
WHERE中的条件:在临时表生成后过滤数据,会过滤掉左表中右表为NULL的行,导致查询结果退化为INNER JOIN的效果; -
优化点:若想利用右表的索引过滤数据,且保留左表所有行,将条件写在ON中;若想强制过滤左表数据,将条件写在WHERE中。
-
4. 尽量避免子查询,用JOIN替代
子查询(尤其是非关联子查询)会创建临时表,临时表无法利用索引,且会消耗额外的内存和磁盘资源,效率较低。优先用JOIN替代子查询,提升查询效率。
# 差写法:子查询创建临时表,效率低
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE age > 20);
# 优写法:JOIN替代,利用索引,效率高
SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.age > 20;
5. 限制关联表数量
MySQL对多表关联的优化能力有限,关联表数量越多,嵌套循环次数越多,临时表体积越大,查询效率越低。建议关联表数不超过5个,若超过,可拆分为多个查询,在应用层聚合数据。
三、排序与分组优化(GROUP BY、ORDER BY优化)
GROUP BY和ORDER BY是查询中最消耗资源的操作之一(易触发文件排序、临时表),优化核心是“利用索引避免文件排序和临时表”。
1. 优化GROUP BY
-
关闭默认排序,避免无用消耗:GROUP BY默认会对分组结果进行排序,若无需排序,用ORDER BY NULL关闭,避免触发文件排序。 -
用索引优化GROUP BY:创建包含GROUP BY字段的索引,让MySQL直接从索引中分组,避免创建临时表和文件排序。
2. 优化ORDER BY
-
索引覆盖排序,避免文件排序:让ORDER BY的字段包含在索引中(结合WHERE过滤字段),构建联合索引,让MySQL直接从索引中获取排序后的数据,避免触发文件排序。 -
避免混合排序:不要同时使用ASC(升序)和DESC(降序)排序(除非MySQL 8.0+,支持降序索引),否则会触发文件排序。
提示:如果数据量很大,文件排序会使用磁盘排序,产生大量磁盘I/O,导致查询性能急剧下降,务必避免。
四、特殊场景优化(避坑指南)
1. 避免隐式类型转换,防止索引失效
索引字段的类型与查询值的类型不一致时,MySQL会触发隐式类型转换,导致索引失效,转而执行全表扫描。
-- 表中user_id是INT类型(建有索引)
-- 差:查询值为字符串'100',隐式转为INT,索引失效
SELECT * FROM orders WHERE user_id = '100';
-- 优:类型匹配,正常使用索引
SELECT * FROM orders WHERE user_id = 100;
2. 优化范围查询,合理设计联合索引顺序
联合索引中,范围查询(>、<、BETWEEN、IN)后的字段无法使用索引,因此设计联合索引时,需将范围查询字段放在最后,确保前面的字段能正常使用索引。
-- 索引:idx_orders_status_create_time (status, create_time)
-- 优:范围字段create_time放最后,status字段正常使用索引
SELECT * FROM orders WHERE status=1 AND create_time > '2026-01-01';
-- 差:范围字段create_time放前面,status字段无法使用索引,触发全表扫描
SELECT * FROM orders WHERE create_time > '2026-01-01' AND status=1;
3. 优化NULL值查询,减少索引统计复杂度
IS NULL/IS NOT NULL可以使用索引(前提是字段有索引),但NULL值会让索引统计和比较变得复杂,尽量避免频繁查询NULL值。
-- 有索引时可用,但不推荐频繁使用
SELECT * FROM orders WHERE remark IS NULL;
-- 优化方案:给字段设置默认值(如空字符串),替代NULL
ALTER TABLE orders MODIFY COLUMN remark VARCHAR(255) DEFAULT '';
SELECT * FROM orders WHERE remark = '';
4. 优化批量操作(插入/更新),减少事务和网络开销
(1)批量插入优化
单条插入会频繁建立和释放事务、发起网络请求,效率极低;批量插入可减少网络交互和事务开销,提升插入效率。
-- 差:单条插入,效率低
INSERT INTO orders (user_id, amount) VALUES (1, 100);
INSERT INTO orders (user_id, amount) VALUES (2, 200);
-- 优:批量插入,效率提升明显
INSERT INTO orders (user_id, amount) VALUES (1, 100), (2, 200);
(2)批量更新优化
多次单条更新会触发多次事务和索引维护,用CASE WHEN替代多次UPDATE,实现一次更新多条数据,减少资源消耗。
-- 差:多次更新,效率低
UPDATE orders SET amount=150WHERE order_id=1;
UPDATE orders SET amount=250WHERE order_id=2;
-- 优:一次更新多条数据,效率高
UPDATE orders
SET amount = CASE
WHEN order_id=1THEN150
WHEN order_id=2THEN250
END
WHERE order_id IN (1,2);
5. 避免使用SELECT DISTINCT,优先用GROUP BY
SELECT DISTINCT会通过排序实现去重,开销较大;若需去重,可改用GROUP BY + ORDER BY NULL,效果相同且优化器更友好。
-- 差:DISTINCT触发排序,消耗资源
SELECT DISTINCT user_id FROM orders WHERE status=1;
-- 优:GROUP BY + ORDER BY NULL 避免排序,效率更高
SELECT user_id FROM orders WHERE status=1 GROUP BY user_id ORDER BY NULL;
提示:若字段有索引,或使用MySQL 8.0+版本,SELECT DISTINCT的可读性更好,且与GROUP BY的效率差异极小,可根据可读性选择。
6. 避免IN子句中包含子查询
旧版本MySQL中,IN子句中的子查询会生成衍生表,无法使用索引,执行效率极低;推荐改写为JOIN,利用索引提升效率。
-- ❌ 低效:IN子句包含子查询,生成衍生表,无索引可用
SELECT * FROM A WHERE id IN (SELECT id FROM B WHERE status = 1);
-- ✅ 推荐:改写为JOIN,利用索引,效率大幅提升
SELECT A.* FROM A INNER JOIN B ON A.id = B.id WHERE B.status = 1;
7. 避免NOT IN子查询
若NOT IN子查询的结果中包含NULL,整个查询结果会为空,且执行效率极差;推荐用NOT EXISTS或LEFT JOIN … WHERE … IS NULL替代,兼容性和效率更优。
-- ❌ 低效且易出错:NOT IN子查询含NULL时结果为空
SELECT * FROM A WHEREidNOTIN (SELECTidFROM B WHEREstatus = 1);
-- ✅ 推荐写法1:NOT EXISTS(效率高,优先选择)
SELECT * FROM A WHERENOTEXISTS (SELECT1FROM B WHERE A.id = B.id AND B.status = 1);
-- ✅ 推荐写法2:LEFT JOIN + IS NULL(兼容性好)
SELECT A.* FROM A LEFTJOIN B ON A.id = B.id AND B.status = 1WHERE B.id ISNULL;
8. 规避相关子查询陷阱
若子查询引用了外层表的列(即相关子查询),子查询会对外层表的每一行都执行一次,循环次数多、效率极低;推荐改写为JOIN或使用MySQL 8.0+的窗口函数。
-- ❌ 低效:相关子查询,对外层users每一行执行一次子查询
SELECT u.name, u.salary,
(SELECTAVG(salary) FROM employees e WHERE e.dept_id = u.dept_id) AS dept_avg
FROMusers u WHERE u.status = 'active';
-- ✅ 推荐写法1:JOIN替代(兼容所有MySQL版本)
SELECT u.name, u.salary, d.dept_avg
FROMusers u
JOIN (SELECT dept_id, AVG(salary) AS dept_avg FROM employees GROUPBY dept_id) d
ON u.dept_id = d.dept_id
WHERE u.status = 'active';
-- ✅ 推荐写法2:MySQL 8.0+ 窗口函数(更简洁)
SELECT u.name, u.salary,
AVG(salary) OVER (PARTITIONBY dept_id) AS dept_avg
FROMusers u WHERE u.status = 'active';
9. 正确使用COUNT函数,选择最优方式
COUNT函数的不同用法,效率差异较大,InnoDB引擎下的最优选择如下:
-
COUNT(*):MySQL专门优化的写法,无需判断列是否为NULL,直接统计行数,速度最快,优先推荐; -
COUNT(col):需要判断列值是否为NULL,跳过NULL值统计,速度稍慢; -
COUNT(1):与COUNT()在InnoDB中的性能几乎一致,但习惯上优先使用COUNT(),可读性更好。
五、索引设计优化(核心中的核心)
索引是查询优化的基础,索引设计不合理,再优的查询语句也无法发挥作用。核心原则:索引不在多,在于精,既要提升查询效率,也要减少索引维护成本。
1. 用联合索引替代多个单列索引
若多个查询场景都涉及多个相同字段(如WHERE a=? AND b=?、ORDER BY a,b),创建联合索引(a,b)替代单独的索引a和索引b,既能减少索引数量,又能提升多字段查询的效率。
注意:联合索引遵循“最左前缀原则”,查询条件必须从第一个字段开始,不能跳过前面的字段直接查询后面的字段(如联合索引(a,b,c),无法直接用b或c作为查询条件使用索引)。
2. 区分度高的列优先建索引
索引的区分度( cardinality )是指字段中不同值的数量占比,区分度越高(重复值越少),索引的筛选效果越好,查询效率越高。
-
推荐建索引:id、username、phone等区分度高的字段; -
不推荐单独建索引:性别(男/女)、状态(0/1)等区分度低的字段,单独建索引的效果甚至不如全表扫描,可纳入联合索引的末尾。
3. 控制索引数量,平衡查询与写入效率
索引不是越多越好:
-
索引会占用磁盘空间,索引越多,磁盘占用越大; -
INSERT、UPDATE、DELETE操作时,MySQL需要同步维护所有相关索引,索引越多,写入效率越低。
建议:单表索引数量不超过5个,优先保留常用查询场景的索引,删除无用、冗余的索引。
4. 字符串字段适当使用前缀索引
对于很长的字符串字段(如URL、备注),直接建索引会导致索引体积过大,占用大量磁盘空间,且查询效率受影响。可只索引字符串的前N个字符(前缀索引),减小索引体积,提升效率。
-- 对url字段建立前缀索引(只索引前20个字符)
CREATE INDEX idx_url_prefix ON table_name(url(20));
注意:前缀长度需合理(如URL的前20个字符已能区分大部分数据),避免过短导致区分度降低,过⻓无法减小索引体积。
5. 选择合适的数据类型,减少资源消耗
数据类型的选择直接影响索引效率和磁盘占用,核心原则:够用就好,尽量精简。
-
整数类型:能用TINYINT(1字节)就不用INT(4字节),能用INT就不用BIGINT(8字节); -
字符串类型:能用VARCHAR就别用TEXT(除非需要存储大量文本),VARCHAR需指定合理长度(避免过长); -
尽量定义为NOT NULL:NULL值会增加索引统计和查询的复杂度,还会占用额外存储空间,建议给字段设置默认值替代NULL。
六、总结
MySQL查询优化的核心逻辑的是“减少无效消耗、利用索引加速”:
-
查询语句层面,避免全表扫描、文件排序、临时表; -
索引层面,合理设计索引、控制索引数量、利用覆盖索引; -
关联和批量操作层面,减少循环和事务开销。
优化的关键不是“堆砌技巧”,而是结合实际业务场景,分析查询执行计划(EXPLAIN),找到性能瓶颈,针对性优化——只有让每一次查询都尽可能少地消耗CPU、内存、磁盘I/O,才能实现最快的查询速度,同时保证MySQL系统的稳定高效运行。
想了解更多干货,可通过下方扫码关注

可扫码添加上智启元官方客服微信👇

17认证网








