慢SQL优化方法与思路总结
慢SQL优化方法与思路总结
定位慢的SQL(数据库层排查)
慢查询知识
一般日志(General Log):记录所有查询,用于调试
-- 查看一般日志相关配置 SHOW VARIABLES LIKE 'general_log%'; SHOW VARIABLES LIKE 'log_output'; -- 开启一般日志 SET GLOBAL general_log = 'ON'; -- OFF 关闭 -- 设置日志文件路径(如果需要) SET GLOBAL general_log_file = 'D:\\file\\SlowLogMySQL\\mysql-general.log';慢查询日志(Slow Query Log):只记录执行时间超过阈值的查询
开启并分析数据库慢查询日志:
-
确认慢查询日志是否开启:
-- MySQL SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'slow_query_log_file'; SHOW VARIABLES LIKE 'log_output'; -
如果没开,立即开启(需重启或动态设置):
-- MySQL SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 设置慢查询阈值,例如2秒 -- SET SESSION long_query_time = 2; -- 当前会话也设置 不设置这个的话需要重新连接才生效 SET GLOBAL slow_query_log_file = '/path/to/your/slow-query.log'; -- 绝对路劲 SET GLOBAL slow_query_log_file = 'D:\\file\\SlowLogMySQL\\slow-query.log'; SET GLOBAL log_output = 'FILE'; -- 有需要的话,可以关闭一般日志(避免干扰),默认好像就是关的 SET GLOBAL general_log = 'OFF'; -- 查看关闭状态 SHOW VARIABLES LIKE 'general_log'; -
分析慢日志: 直接打开日志文件查看,或者使用
mysqldumpslow或pt-query-digest(Percona Toolkit) 工具分析慢日志文件。# 列出平均耗时最长的前10条SQL mysqldumpslow -s at -t 10 /path/to/slow.log # 使用pt-query-digest进行更专业的分析 pt-query-digest /path/to/slow.log -
从分析结果中,你会得到:
- 具体的慢SQL语句。
- 执行次数、平均/最大/总耗时。直接打开日志文件查看不到,得用工具分析得到。
- 锁等待时间、扫描行数等信息。直接打开日志文件查看不到,得用工具分析得到。
实时监控数据库进程:
-
在功能变慢时,实时连接到数据库,查看当前正在执行的SQL。
SHOW FULL PROCESSLIST; -
这个命令会列出所有数据库连接及其正在执行的命令或SQL。寻找
Time列数值大、State列为Sending data,Copying to tmp table,Sorting result等状态的连接,其Info列就是正在执行的SQL。
使用数据库监控工具:
- 很多数据库管理工具(如 phpMyAdmin, MySQL Workbench, Navicat)都带有“服务器状态监控”或“进程列表”功能,可以图形化地查看慢查询和运行状态。
- 云服务商(如 AWS RDS,阿里云RDS)的控制台也提供了非常完善的性能洞察和慢SQL统计功能。
慢查询搭建
创建超大表(500万条数据)
-- 创建测试数据库
CREATE DATABASE IF NOT EXISTS extreme_slow_demo;
USE extreme_slow_demo;
-- 创建没有索引的大表
CREATE TABLE massive_orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
description TEXT
) ENGINE=InnoDB;
分批插入
-- 创建存储过程分批插入
DELIMITER $$
CREATE PROCEDURE InsertMassiveData()
BEGIN
DECLARE batch_count INT DEFAULT 0;
DECLARE total_batches INT DEFAULT 50; -- 50批,每批10万 = 500万
WHILE batch_count < total_batches DO
INSERT INTO massive_orders (user_id, product_id, amount, status, created_at, description)
SELECT
FLOOR(1 + RAND() * 100000),
FLOOR(1 + RAND() * 5000),
ROUND(RAND() * 1000, 2),
FLOOR(RAND() * 5),
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY),
REPEAT('Text data for slow query testing ', 30)
FROM
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t1,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t2,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t3,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t4,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t5,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t6,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t7,
(SELECT 1 a UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t8;
SET batch_count = batch_count + 1;
SELECT CONCAT('Inserted batch: ', batch_count) as progress;
END WHILE;
END$$
DELIMITER ;
-- 执行存储过程
CALL InsertMassiveData();
-- 删除存储过程
DROP PROCEDURE InsertTestData;
验证数据
-- 查看总数据量
SELECT COUNT(*) as total_records FROM massive_orders;
-- 查看数据样本
SELECT * FROM massive_orders LIMIT 10;
执行会导致慢查询的SQL
-- 执行一个肯定很慢的查询来测试
SELECT SLEEP(3); -- 这个查询应该被记录
-- 或者用您的表测试
SELECT COUNT(*) FROM massive_orders WHERE description LIKE '%test%';
-- 查询1:全表扫描 + 复杂条件 + 排序(保证慢)
SELECT * FROM massive_orders
WHERE user_id BETWEEN 1000 AND 2000
AND status = 1
AND amount > 500
AND created_at BETWEEN '2020-01-01' AND '2023-12-31'
AND description LIKE '%slow%'
ORDER BY amount DESC, created_at ASC
LIMIT 1000;
-- 查询2:多字段分组 + 复杂计算
SELECT
user_id,
product_id,
status,
COUNT(*) as total_orders,
AVG(amount) as avg_amount,
SUM(amount) as total_amount,
MIN(created_at) as first_order,
MAX(created_at) as last_order
FROM massive_orders
WHERE amount BETWEEN 100 AND 800
AND created_at > '2021-01-01'
GROUP BY user_id, product_id, status
HAVING total_orders > 5 AND total_amount > 3000
ORDER BY total_amount DESC
LIMIT 500;
-- 查询3:多表JOIN(如果需要更慢)
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(100),
vip_level TINYINT
);
慢sql日志分析
一个慢sql日志案例
# Time: 251126 20:51:40
# User@Host: root[root] @ localhost [127.0.0.1] Id: 207
# Query_time: 61.885823 Lock_time: 0.000000 Rows_sent: 3125000 Rows_examined: 3125000
SET timestamp=1764161500;
SELECT * FROM massive_orders;
日志关键信息分析
| 字段 | 值 | 含义与分析 |
|---|---|---|
| Id | 207 | 数据库连接的进程ID |
| Query_time | 61.885823 |
查询耗时约62秒 - 这是一个非常长的时间! |
| Rows_sent | 3,125,000 |
返回了312.5万行数据 |
| Rows_examined | 3,125,000 |
扫描了312.5万行数据 |
| SQL语句 | SELECT * FROM massive_orders; |
对massive_orders表的全量查询 |
SET timestamp=1764161500; |
2025-11-26 20:51:40 | 这是查询开始执行时的时间戳。 |
# Time: 251126 20:51:40 |
2025-11-26 20:51:40 | 查询开始时间或者日志记录时间 |
慢SQL分析
上面找到慢SQL后,现在进行分析
使用 EXPLAIN 分析SQL执行计划:
- 这是优化SQL的必备技能。在慢SQL前面加上
EXPLAIN关键字,查看数据库是如何执行这条SQL的。
EXPLAIN SELECT * FROM your_slow_sql;
- 重点关注:
- type列: 访问类型,至少应该是
range级别,最好能达到ref,eq_ref或const。避免出现ALL(全表扫描)。 - key列: 实际使用的索引。如果为NULL,说明没用到索引。
- rows列: 预估需要扫描的行数。这个值应该尽可能小。
- Extra列: 额外信息。注意是否出现
Using filesort(需要额外排序)或Using temporary(需要创建临时表),这些都是性能杀手。
- type列: 访问类型,至少应该是
1. type 列(访问类型)
核心概念:这列描述了 MySQL 如何查找表中的行。它揭示了查询效率的级别,从最优到最差排序。
从最优到最差,常见的类型有:
system&const:- 含义:对查询的某部分进行优化,并将其转换为一个常量。
system是const的特例(表只有一行,如系统表)。 - 场景:通过主键(PRIMARY KEY)或唯一索引(UNIQUE INDEX)进行等值查询时。
- 例子:
SELECT * FROM users WHERE id = 1;(id是主键) - 评价:最佳性能,最多只返回一行。
- 含义:对查询的某部分进行优化,并将其转换为一个常量。
eq_ref:- 含义:在表连接时,使用主键或唯一索引的所有部分被连接使用。
- 场景:多表连接查询,驱动表的每一行,都只能匹配被驱动表的一行。通常出现在
=操作符的 JOIN 条件中。 - 例子:
SELECT * FROM users JOIN orders ON users.id = orders.user_id;(orders.user_id是外键且是唯一索引) - 评价:非常高效的 JOIN 类型。
ref:- 含义:使用非唯一性索引或者唯一性索引的前缀部分进行等值查询。
- 场景:使用普通索引(INDEX)或联合索引的左前缀进行
=或<=>操作。 - 例子:
SELECT * FROM users WHERE email = 'foo@bar.com';(email字段有普通索引) - 评价:很好,可能会返回多行,但索引查找效率很高。
range:- 含义:使用索引检索给定范围的行。
- 场景:在索引列上使用
BETWEEN、>、<、>=、<=、IN()、LIKE 'prefix%'(前缀匹配)等操作符。 - 例子:
SELECT * FROM users WHERE created_at > '2023-01-01';(created_at有索引) - 评价:可以接受,它扫描的是一个索引范围,而不是全表。这是你提到的“至少应该达到”的水平。
index:- 含义:全索引扫描。遍历整个索引树来查找数据,与全表扫描类似,但因为它只扫描索引(通常比数据本身小),所以比
ALL稍快。 - 场景:查询的列都包含在某个索引中(覆盖索引),或者需要按索引的顺序进行排序。
- 例子:
SELECT id FROM users;(id是主键,这个查询可以直接从索引中获取所有 id,无需读数据行) - 评价:通常不理想,但如果数据量巨大,它比
ALL好。
- 含义:全索引扫描。遍历整个索引树来查找数据,与全表扫描类似,但因为它只扫描索引(通常比数据本身小),所以比
ALL:- 含义:全表扫描。MySQL 会读取整张表的每一行来找到匹配的行。
- 场景:没有索引可用于查询条件。
- 例子:
SELECT * FROM users WHERE name = 'foo';(name字段没有索引) - 评价:性能杀手,必须避免,尤其是在大表上。
2. key 列(实际使用的索引)
核心概念:显示 MySQL 实际决定使用的索引。如果优化器选择了复合索引,这里会显示索引的名字。
- 优化重点:
- 如果
key为NULL,这是一个强烈的警告信号,说明没有使用索引。你需要考虑为WHERE、JOIN、ORDER BY或GROUP BY子句中的列创建索引。 - 有时,
key列显示的不是你期望的索引。这可能是因为:- 存在多个可选索引,MySQL 优化器选择了一个它认为成本最低的(可能不正确)。
- 你的查询写法导致索引失效(例如对索引列使用了函数
WHERE YEAR(create_time) = 2023)。
- 你可以使用
FORCE INDEX或调整查询语句来引导优化器使用正确的索引。
- 如果
3. rows 列(预估扫描行数)
核心概念:MySQL 优化器预估为了找到所需的行,需要读取多少行数据。注意,这是一个预估值,并非精确值。
- 优化重点:
- 这个值应该尽可能小。一个良好的查询可能只扫描几十或几百行。
- 如果这个值非常大(例如,数万、数百万),即使
type不是ALL,也意味着查询效率低下。它表明虽然用了索引,但索引的选择性不高,或者查询需要访问大量索引项。 - 将此值与表的总行数对比。如果
rows接近总行数,通常意味着全表扫描或近似全表扫描。
4. Extra 列(额外信息)
核心概念:包含 MySQL 解决查询的详细信息,很多是性能优化的关键线索。
需要警惕的“坏”信息:
Using filesort:- 含义:MySQL 必须执行一个额外的排序操作,而无法直接利用索引顺序。这个“文件”排序可能在内存或磁盘上进行,效率较低。
- 原因:
ORDER BY的列没有合适的索引,或者索引顺序与ORDER BY顺序不一致。 - 优化:为
ORDER BY和GROUP BY的列创建索引。
Using temporary:- 含义:MySQL 需要创建一张临时表来存储中间结果,通常发生在处理
GROUP BY、DISTINCT或复杂的ORDER BY时。 - 原因:查询过于复杂,无法直接在一遍扫描中完成。
- 优化:尤其是当临时表很大,需要被写入磁盘时,性能会急剧下降。需要通过创建合适的索引或重写查询来避免。
- 含义:MySQL 需要创建一张临时表来存储中间结果,通常发生在处理
Using where:- 含义:MySQL 服务器在从存储引擎拿到行之后,还需要使用
WHERE条件进行过滤。 - 评价:这不一定总是坏的。如果
type是index或range,Using where是正常的。但如果type是ALL并且有Using where,说明它在进行全表扫描后逐行过滤,性能极差。
- 含义:MySQL 服务器在从存储引擎拿到行之后,还需要使用
值得高兴的“好”信息:
Using index:- 含义:出现了覆盖索引。这意味着查询所需的所有列都包含在索引中,因此 MySQL 只需读取索引,而无需回表读取数据行。这非常高效。
- 例子:有一个索引
INDEX (name, age),查询SELECT name, age FROM users WHERE name = 'foo';
总结与排查流程
- 先看
type:判断访问方法是否高效。目标是range及以上,杜绝ALL。 - 再看
key:确认是否用上了索引。如果是NULL,优先解决。 - 然后看
rows:评估扫描数据量。数值过大则需要优化。 - 最后看
Extra:发现隐藏问题。重点解决Using filesort和Using temporary,并争取实现Using index。
通过系统性地分析这四列,你可以精准地定位 SQL 的性能瓶颈,并采取相应的优化措施(主要是创建或调整索引)。
慢SQL优化方法与思路总结
案例:面试官:5000万数据,如何毫秒级查手机尾号?我差点懵了...
优化策略:加索引、改写SQL、调整设计
常见的优化手段:
- 为查询条件列添加索引: 这是最有效的手段。分析
WHERE,ORDER BY,GROUP BY子句中的列,为其创建合适的索引。 - 避免使用
SELECT \*: 只获取需要的列,减少数据传输量和数据库负载。 - 优化查询逻辑: 检查是否可以简化JOIN、避免子查询、或使用分页来减少单次数据量。
- 检查表结构设计: 是否存在不合理的字段类型、是否需要分区等。
一、 SQL语句与写法优化(这是成本最低的优化)
很多时候,性能问题是由于糟糕的SQL写法导致的。
-
避免使用
SELECT \*- 坏处:返回所有列,包括不需要的。这会增加网络传输、内存消耗和磁盘I/O。
- 优化:只取需要的列。
SELECT id, name, email FROM users;
-
避免在WHERE子句中对字段进行函数操作或计算
- 坏处:这会导致索引失效,进行全表扫描。
- 例子:
WHERE YEAR(create_time) = 2023(索引失效)WHERE amount / 2 > 100(索引失效)
- 优化:将操作转移到常量一侧。
WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'(可能使用索引)WHERE amount > 200(使用索引)
-
谨慎使用
OR,多用IN或UNION-
坏处:不当使用
OR容易导致索引失效。 -
例子:
WHERE status = 1 OR status = 2 -
优化:
-
使用
IN:WHERE status IN (1, 2)(更高效) -
对于不同字段的OR,可以考虑使用
UNION:-- 优化前 (可能全表扫描) SELECT * FROM table WHERE a = 1 OR b = 2; -- 优化后 (可能利用两个索引) SELECT * FROM table WHERE a = 1 UNION SELECT * FROM table WHERE b = 2;
-
-
-
优化
LIKE查询LIKE '%关键字%':前导通配符,索引完全失效,全表扫描。LIKE '关键字%':后导通配符,可以使用索引。- 优化:尽量避免使用前导通配符。如果必须使用,考虑使用全文索引(如Elasticsearch)。
-
使用
EXISTS代替IN-
当子查询结果集很大时,
IN的效率会很低。 -
EXISTS是关联性查询,一旦找到一条匹配记录就返回,通常性能更好。 -
例子:
-- 使用 IN SELECT * FROM A WHERE id IN (SELECT id FROM B); -- 使用 EXISTS (通常更快) SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE A.id = B.id);
-
-
合理使用分页,避免大偏移量
-
LIMIT 100000, 20会先读取100020条记录,然后扔掉前10万条,非常慢。 -
优化:使用“游标分页”或“基于ID的分页”。
-- 传统分页 (慢) SELECT * FROM posts ORDER BY id LIMIT 100000, 20; -- 优化分页 (快) SELECT * FROM posts WHERE id > 100000 ORDER BY id LIMIT 20;
-
二、 数据库设计与结构优化
- 范式与反范式的平衡
- 范式化(减少冗余)的好处是写操作快,数据一致性好。
- 反范式化(适当增加冗余)的好处是读操作快,避免了多表关联。
- 优化:在查询频繁且性能要求高的场景,可以适当牺牲范式,做数据冗余(如将用户名冗余到订单表,避免查订单时关联用户表)。
- 选择合适的数据类型
- 使用更小的数据类型(如
INTvsBIGINT,CHAR(10)vsVARCHAR(10))。 VARCHAR代替TEXT或BLOB,除非必要。- 使用
INT存储IP地址,而非VARCHAR。 - 这些都能减少磁盘和内存的使用,加快处理速度。
- 使用更小的数据类型(如
- 分区表
- 将一个大表按某种规则(如时间范围、哈希)分割成多个物理小文件。
- 好处:查询时优化器可以只扫描相关的分区,提升查询和维护效率。适用于日志表、历史订单表等。
三、 服务器与配置优化
- 调整关键配置参数
- 缓冲池大小
innodb_buffer_pool_size(MySQL):这是最重要的参数。它定义了InnoDB缓存表和索引数据的内存区域。通常建议设置为可用物理内存的50%-80%。 - 日志文件大小:确保重做日志文件足够大,以避免频繁的检查点。
- 连接数
max_connections:设置合理的值,避免过多连接导致资源争抢。
- 缓冲池大小
- 升级硬件
- 内存:越大越好,可以让更多数据留在内存中,减少磁盘I/O。
- SSD硬盘:相比机械硬盘,随机I/O性能有数量级的提升,对数据库是巨大福音。
- CPU:对于计算密集型的复杂查询,更强的CPU有帮助。
四、 架构层面优化(终极手段)
当单机数据库达到瓶颈时,就需要从架构上动手术。
- 读写分离
- 主数据库负责写操作,多个从数据库负责读操作。
- 通过复制机制将主库数据同步到从库。
- 极大地分担了主库的读压力,是互联网应用的标配。
- 分库分表
- 垂直分库/分表:按业务模块将不同表拆分到不同数据库,或者将一个大表的字段拆分成多个小表。
- 水平分表/分片:将一个大表的数据按某种规则(如用户ID哈希)分布到多个数据库的多个表中。
- 这是解决海量数据存储和高并发访问的终极方案,但会带来跨库事务、分布式查询等复杂性。
- 引入缓存
- 在应用和数据库之间加入缓存层(如Redis, Memcached)。
- 将频繁读取、很少变更的数据(如用户信息、配置信息、热门文章)放入缓存。
- 直接减少对数据库的查询次数,效果立竿见影。
- 使用专用查询引擎/分析型数据库
- 对于复杂的报表和分析查询(OLAP),传统事务型数据库(OLTP)可能不是最佳选择。
- 可以考虑将数据同步到列式存储数据库(如ClickHouse)或大数据平台(如Hive, Spark SQL)中进行处理。
优化步骤总结
当遇到慢SQL时,建议遵循以下步骤进行排查和优化:
- 定位:使用慢查询日志或性能监控工具找到具体的慢SQL。
- 分析:使用
EXPLAIN或EXPLAIN ANALYZE命令分析其执行计划,看是否使用了索引,是否有全表扫描、临时表、文件排序等昂贵操作。 - 优化:
- 第一层(首选):优化SQL语句本身和索引。
- 第二层(中期):考虑数据库设计、表结构、参数配置。
- 第三层(长远):进行架构层面的改造,如读写分离、缓存、分库分表。
记住,优化是一个持续的过程,需要根据具体的业务场景、数据量和访问模式来选择最合适的组合拳。
浙公网安备 33010602011771号