花莲县网站建设_网站建设公司_营销型网站_seo优化
2025/12/31 8:33:39 网站建设 项目流程

线上告警:接口响应超时。一查,又是慢SQL。

这种场景太常见了。数据库是后端最常见的性能瓶颈,而慢SQL是罪魁祸首。这篇把我排查慢SQL的套路整理出来。


第一步:找到慢SQL

1. 开启慢查询日志

-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';-- 动态开启(重启失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 超过1秒算慢
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

永久生效写my.cnf:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1  # 没用索引的也记录

2. 分析慢查询日志

# 用mysqldumpslow分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log# -s t: 按查询时间排序
# -t 10: 显示前10条# 输出示例
# Count: 156  Time=3.21s (500s)  Lock=0.00s (0s)  Rows=1000.0 (156000)
# SELECT * FROM orders WHERE user_id = N AND status = N

3. 实时查看正在执行的SQL

-- 查看当前进程
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;  -- 显示完整SQL-- 看哪个SQL执行时间长
SELECT * FROM information_schema.processlist 
WHERE command != 'Sleep' 
ORDER BY time DESC;-- 杀掉卡住的查询
KILL 进程ID;

4. performance_schema分析

-- 开启(MySQL 5.6+默认开启)
-- 查询最耗时的SQL
SELECT DIGEST_TEXT,COUNT_STAR AS exec_count,SUM_TIMER_WAIT/1000000000000 AS total_time_sec,AVG_TIMER_WAIT/1000000000 AS avg_time_ms,SUM_ROWS_EXAMINED AS rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

第二步:分析执行计划

找到慢SQL后,用EXPLAIN分析。

EXPLAIN基本用法

EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 1;

输出关键字段:

字段 含义 关注点
type 访问类型 ALL全表扫描要优化
key 实际用的索引 NULL说明没用索引
rows 预估扫描行数 越小越好
Extra 额外信息 Using filesort、Using temporary要注意

type类型(从好到差)

system > const > eq_ref > ref > range > index > ALL- const: 主键或唯一索引等值查询,最多一条
- eq_ref: 多表join时,被驱动表用主键/唯一索引
- ref: 普通索引等值查询
- range: 索引范围扫描
- index: 全索引扫描(比ALL好,但也不理想)
- ALL: 全表扫描,必须优化

真实案例分析

-- 问题SQL
EXPLAIN SELECT * FROM orders 
WHERE create_time > '2024-01-01' 
AND status = 1 
ORDER BY id DESC 
LIMIT 100;-- 输出
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | Extra                       |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+
|  1 | SIMPLE      | orders | ALL  | idx_create    | NULL | NULL    | NULL | 500000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+--------+-----------------------------+

问题:

  1. type=ALL:全表扫描
  2. key=NULL:没用索引
  3. Using filesort:额外排序

第三步:索引优化

1. 联合索引的最左前缀原则

-- 有索引 (a, b, c)
-- 能用上索引
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3-- 用不上或部分用上
WHERE b = 2              -- 不能用(跳过了a)
WHERE a = 1 AND c = 3    -- 只用到a(跳过了b)
WHERE b = 2 AND c = 3    -- 不能用

2. 覆盖索引

-- 原SQL,需要回表
SELECT * FROM users WHERE age > 20;-- 优化:只查索引包含的列
SELECT id, name, age FROM users WHERE age > 20;-- 如果有索引 (age, name),就是覆盖索引,不用回表
-- Extra显示 Using index

3. 索引失效的常见原因

-- 1. 对索引列做运算或函数
SELECT * FROM orders WHERE YEAR(create_time) = 2024;  -- ❌
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';  -- ✅-- 2. 隐式类型转换
-- phone是varchar类型
SELECT * FROM users WHERE phone = 13800138000;  -- ❌ 数字,会转换
SELECT * FROM users WHERE phone = '13800138000';  -- ✅ 字符串-- 3. LIKE以%开头
SELECT * FROM users WHERE name LIKE '%张';  -- ❌
SELECT * FROM users WHERE name LIKE '张%';  -- ✅-- 4. OR条件中有非索引列
SELECT * FROM users WHERE id = 1 OR name = '张三';  -- name没索引则全表-- 5. 不等于 != 或 <>
SELECT * FROM users WHERE status != 0;  -- 可能全表扫描-- 6. IS NULL 或 IS NOT NULL(视情况)
-- 如果NULL值很多,优化器可能选择全表扫描

4. 索引设计原则

-- 查询条件优先
-- 经常出现在WHERE、JOIN、ORDER BY的列建索引-- 区分度高的列优先
-- 区分度 = COUNT(DISTINCT col) / COUNT(*)
-- 性别区分度低(0.5),手机号区分度高(接近1)-- 联合索引:把区分度高的放前面
CREATE INDEX idx_user ON orders(user_id, status);  -- user_id区分度更高-- 适当冗余,避免回表
-- 如果经常查 SELECT id, name FROM users WHERE age > 20
-- 可以建 INDEX idx_age_name (age, name)

第四步:SQL改写优化

1. 避免SELECT *

-- 差
SELECT * FROM orders WHERE user_id = 123;-- 好:只查需要的列
SELECT id, order_no, amount, status FROM orders WHERE user_id = 123;

2. 小表驱动大表

-- user表1000条,order表100万条-- 差:大表驱动小表
SELECT * FROM orders o 
LEFT JOIN users u ON o.user_id = u.id;-- 好:小表驱动大表
SELECT * FROM users u 
LEFT JOIN orders o ON u.id = o.user_id;-- 或者用EXISTS
SELECT * FROM orders o 
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 1);

3. 分页优化

-- 深分页问题:OFFSET很大时很慢
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- 要扫描100020行,丢弃前100000行-- 优化1:用上一页的ID
SELECT * FROM orders WHERE id > 上一页最后一条的ID ORDER BY id LIMIT 20;-- 优化2:延迟关联
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t
ON o.id = t.id;
-- 子查询只查主键,速度快

4. 避免在循环中查询

// 差:N+1查询
users := getUsers()
for _, user := range users {orders := getOrdersByUserID(user.ID)  // 循环里查询
}// 好:批量查询
users := getUsers()
userIDs := extractUserIDs(users)
orders := getOrdersByUserIDs(userIDs)  // 一次查出来
orderMap := groupByUserID(orders)

5. UNION ALL 替代 UNION

-- UNION会去重,需要排序
SELECT id FROM orders WHERE status = 1
UNION
SELECT id FROM orders WHERE status = 2;-- UNION ALL不去重,更快
SELECT id FROM orders WHERE status = 1
UNION ALL
SELECT id FROM orders WHERE status = 2;

第五步:表结构优化

1. 选择合适的数据类型

-- 整数类型
TINYINT    -- 1字节,-128~127
SMALLINT   -- 2字节
INT        -- 4字节
BIGINT     -- 8字节-- 状态字段用TINYINT足够,别用INT
status TINYINT NOT NULL DEFAULT 0-- 时间戳用INT存储(4字节)比DATETIME(8字节)省空间
-- 但DATETIME可读性好,看业务需求-- 金额用DECIMAL,别用FLOAT/DOUBLE(精度问题)
amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00

2. 大表拆分

-- 垂直拆分:把大字段拆出去
-- orders表
id, user_id, order_no, amount, status, create_time-- order_details表(大字段)
id, order_id, goods_info, remark-- 水平拆分:按时间或ID分表
orders_2024, orders_2025
orders_0, orders_1, orders_2 ... orders_15  -- 按user_id取模

3. 归档历史数据

-- 定期把老数据移到归档表
INSERT INTO orders_archive 
SELECT * FROM orders WHERE create_time < '2023-01-01';DELETE FROM orders WHERE create_time < '2023-01-01' LIMIT 10000;
-- 小批量删除,避免锁表太久

真实案例:一次慢查询排查

现象:订单列表接口偶尔超时

排查过程

  1. 查慢查询日志,找到问题SQL:
SELECT * FROM orders 
WHERE user_id = 123 
AND status IN (1, 2, 3) 
AND create_time > '2024-01-01'
ORDER BY create_time DESC 
LIMIT 20;
  1. EXPLAIN分析:
type: ref
key: idx_user_id
rows: 50000
Extra: Using where; Using filesort

虽然用了索引,但rows很大,还有filesort。

  1. 分析:

    • 这个用户订单很多(大客户)
    • idx_user_id单列索引,筛选后还要回表过滤status和时间
    • ORDER BY create_time和索引顺序不一致,要filesort
  2. 优化方案:

-- 新建联合索引
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);-- 改SQL,把IN改成多个条件
SELECT * FROM orders 
WHERE user_id = 123 
AND status = 1 
AND create_time > '2024-01-01'
ORDER BY create_time DESC 
LIMIT 20UNION ALLSELECT * FROM orders 
WHERE user_id = 123 
AND status = 2 
AND create_time > '2024-01-01'
ORDER BY create_time DESC 
LIMIT 20...

或者接受现状,在应用层做缓存。

  1. 结果:查询时间从2s降到50ms。

工具推荐

  1. pt-query-digest:分析慢查询日志,比mysqldumpslow强大
  2. Percona Toolkit:一套MySQL工具集
  3. MySQL Workbench:可视化执行计划
  4. Explain Analyze(MySQL 8.0+):真实执行统计
-- MySQL 8.0+ 用EXPLAIN ANALYZE
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- 显示真实执行时间,而不是预估

总结

慢SQL排查的套路:

  1. 定位:慢查询日志、SHOW PROCESSLIST、performance_schema
  2. 分析:EXPLAIN看执行计划,重点关注type、key、rows、Extra
  3. 优化
    • 索引优化:联合索引、覆盖索引、避免索引失效
    • SQL改写:避免SELECT *、分页优化、小表驱动大表
    • 表结构优化:合适的数据类型、大表拆分

记住一点:看数据量,看场景。100条数据的表,怎么写都快;上亿数据的表,细节决定成败。


有问题评论区聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询