一、SQL语句逻辑执行顺序
在聊sql优化之前,先了解一下sql语句逻辑执行顺序。
| 序号 | 执行顺序 | 核心操作 | 关键说明 |
|---|---|---|---|
| 1 | FROM & JOIN | 确定数据源表和连接方式(INNER/LEFT JOIN等) | 计算笛卡尔积后应用ON条件筛选连接结果,生成中间虚拟表vt1。外连接会在此阶段补全未匹配数据。 |
| 2 | WHERE | 对虚拟表vt1进行行级过滤 | 仅能使用普通列条件(如id>10),过滤后生成vt2。 |
| 3 | GROUP BY | 按指定列分组 | 生成分组虚拟表vt3。后续步骤仅能操作分组列或聚合函数结果。 |
| 4 | 聚合函数 | 执行COUNT/SUM/AVG等聚合运算 | 在分组后计算(如HAVING COUNT(*)>5),生成vt4。 |
| 5 | HAVING | 过滤分组后的数据 | 作用于vt4,可使用聚合函数(如AVG(salary)>5000),生成vt5。与WHERE不同,处理的是分组后结果。 |
| 6 | SELECT | 选择输出列并处理表达式 | 生成vt6。此时可定义列别名(如total AS t),但不可在WHERE中使用别名。 |
| 7 | DISTINCT | 去重操作 | 对vt6去重生成vt7。若存在GROUP BY,此步骤可能被优化。 |
| 8 | ORDER BY | 按指定列排序 | 生成有序游标vt8(非标准表结构)。 |
| 9 | LIMIT/OFFSET | 限制返回行数 | 最终截取结果(如分页查询)。深分页(LIMIT 100000,10)需结合ID游标优化。 |
二、SQL语句优化
1、避免使用select *,仅选择需要的字段,减少数据传输,同时避免回表查询,提高查询效率。
select id, order_num, user_name from `order`;
2、用连接查询代替子查询
众所周知,子查询语句可以通过in关键字实现,一个查询语句的条件落在另一个select语句的查询结果中。程序先运行在嵌套在最内层的语句,生成一个临时表,外层语句在临时表获取所需数据。执行完毕,需要删除临时表,造成额外的消耗。连接查询,可以避免临时表的生成,同时注意合理使用join,避免连接表过多。
#通过子查询找到用户对应订单号
select order_num
from order
where user_id in (select id from user where name = '李华' and status = 1);
#连表查询实现方式
select order_num
from order
left join user on order.user_id = user.id and user.status = 1
3、 避免使用or条件
OR条件可能导致索引失效,尽量使用UNION ALL替代
SELECT * FROM user
WHERE role = 'admin'
UNION ALL
SELECT * FROM user
WHERE role = 'client';
4、union all代替union
union会自动进行去重操作,比较耗时。
SELECT * FROM user WHERE role = 'admin'
UNION ALL
SELECT * FROM user WHERE role = 'client';
5、合理使用in和exists
在子查询返回大量数据时,exists比in更高效
SELECT * FROM user
WHERE EXISTS (SELECT 1 FROM order WHERE order.user_id = user.id);
在子查询返回数据少时,使用in可能更高效(小表驱动大表)
select order_num
from order
where user_id in (select id from user where status = 1);
6、多用limit
如果我们想获取某个用户首次下单的时间,在不使用limit情况下,可以对这个用户的订单数据按时间进行升序排序,在业务代码层面取出第一条数据即可,但是这样需要先查询出所有的数据,浪费时间。相反,加上limit 1限制条件,取出排序后的第一条,效率会更高一些。
select id, create_time
from `order`
where user_id = 4343242
order by create_time asc
limit 1;
7、分页查询
有时候,列表页在查询数据时,为了避免一次性返回过多的数据影响接口性能,我们一般会对查询接口做分页处理。在mysql中分页一般用的limit关键字:
select *
from user
limit 1000 20;
当表的数据量变大时,使用limit关键字进行分页的话效率就会降低很多。例如 limit 1000000, 20 时,他会查出1000020条数据,然后丢弃前面1000000条,只保留后面20条,可想而知这又多浪费时间。优化方式如下:每次分页之前,获取上次分页最大主键id(id需要连续)
select *
from user
where id > 1000000
limit 20;
三、索引优化
1、为经常用于查询条件的列创建索引
在大数据量的情况下,为经常用于查询条件的列创建索引可以加快查询速度。
create index idx_name on user(name);
2、避免过度索引
索引过多会增大增删改操作的成本,定期审查索引使用情况,删除不必要索引。
drop index idx_name on user;
3、使用复合索引
当查询条件涉及多列时,创建复合素引可以提高查询效率。注意列的顺序应根据查询频率和选择性来确定。
create index idx_order_id_no_status on order(id, no, status);
最左匹配原则:
使用复合索引时,当某个索引列为范围查询,可能导致后面索引列失效。
(1)场景一:等值查询+范围查询
explain select * from order where id = 1234 and no > 43535 and status = 1;
分析:idx_order_id_no_status索引生效,但是索引列id和no生效,status失效
(2)全范围查询
explain select * from order where id > 234 and no = 43535 and status = 1;
分析:idx_order_id_no_status索引生效,但是只有索引列id生效
4、使用覆盖索引
确保索引包含查询所需的所有列,减少回表查询。
create index idx_order_id_no_status on order(id, no, status);
select id, no, status from `order` where no = id = 43244;
四、数据库设计优化
1、合理设置字段类型,避免使用text类型。select查询中出现text类型字段,会消耗大量的网络和IO带宽,成本高。
2、在设计表结构时,根据具体情况可以冗余一些字段,提高查询效率。
3、针对数据量大情况,还可以进行分库分表,根据具体情况选择合适的方式,可以提高查询效率。例如,关于订单数据表我们可以根据下单时间进行水平分表,这样就可以根据规定时间点,去查询对应的表,提高查询效率。
五、数据库配置优化
1、根据情况选择合适的存储引擎,如InnoDB、MyISAM等。
2、增加数据库缓冲区,减少磁盘I/O操作。
3、根据实际运行情况调整数据库的各项参数,如缓冲池大小、日志文件大小等。
SET GLOBAL innodb_buffer_pool_size = 2G;
SET GLOBAL max_connections = 500;
4、使用主从复制+读写分离 ,减轻主数据库压力,主库负责写操作、从库负责读操作。
六、缓存的使用
1、开启数据库的查询缓存机制,合理配置缓存大小
-- 启用查询缓存
SET GLOBAL query_cache_type = 1;
-- 设置查询缓存大小
SET GLOBAL query_cache_size = 64 * 1024 * 1024;
2、对于不经常访问和不经常变化的数据,在业务层代码中可以采用redis进行存储,减少磁盘I/O次数,提高查询效率。
七、监控与调优验证工具
1、监控数据库性能指标
-- 查看当前连接数与线程状态
SHOW STATUS LIKE 'Threads_%';
-- 查询InnoDB缓冲池命中率(需定期采样计算)
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- 命中率 ≈ (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100%
2、慢日志开启
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 定义慢查询阈值(秒)
3、使用EXPLAIN关键字分析查询计划,找出性能瓶颈
explain select * from `order` where staus = 2;
八、总结
本章从多方面介绍了SQL优化内容,希望可以给大家带来帮助。如果您觉得有帮助的话,挥挥您发财的小手点个小赞。