慢查询优化

文章目录


一、什么是慢查询

1.1 定义

慢查询(Slow Query) 是指执行时间超过指定阈值的SQL语句。

判断标准

sql 复制代码
-- 查看慢查询阈值(默认10秒)
SHOW VARIABLES LIKE 'long_query_time';

-- 临时设置为1秒
SET GLOBAL long_query_time = 1;

-- 永久配置(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  # 记录未使用索引的查询

1.2 慢查询的影响

影响维度 具体表现
用户体验 页面响应慢,操作卡顿,超时报错
系统性能 CPU飙升,内存占用高,磁盘IO繁忙
数据库压力 连接池耗尽,锁等待,主从延迟
业务影响 订单失败,支付超时,用户流失
成本增加 需要扩容硬件,增加服务器

1.3 性能基准

合理的查询时间

复制代码
⭐⭐⭐⭐⭐ < 10ms    极佳(简单主键查询)
⭐⭐⭐⭐   < 50ms    优秀(索引查询)
⭐⭐⭐     < 100ms   良好(复杂查询)
⭐⭐       < 500ms   可接受(报表查询)
⭐         < 1s      需优化
❌         > 1s      严重问题

二、如何发现慢查询

2.1 方法1:慢查询日志(推荐)

开启慢查询日志
sql 复制代码
-- 1. 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 2. 临时开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;

-- 3. 永久配置(my.cnf或my.ini)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 100  # 至少扫描100行才记录
分析慢查询日志
bash 复制代码
# 使用mysqldumpslow工具分析
# 按查询时间排序,显示前10条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按查询次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log

# 按平均查询时间排序
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log

# 按锁定时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

# 过滤特定数据库
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log | grep 'database_name'

慢查询日志示例

复制代码
# Time: 2024-01-15T10:30:45.123456Z
# User@Host: root[root] @ localhost []  Id: 12345
# Query_time: 5.234567  Lock_time: 0.000123 Rows_sent: 100  Rows_examined: 1000000
SET timestamp=1705318245;
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-15';

字段解读

  • Query_time: 5.234567 - 查询耗时5.23秒
  • Lock_time: 0.000123 - 锁等待0.12毫秒
  • Rows_sent: 100 - 返回100行
  • Rows_examined: 1000000 - 扫描100万行(关键指标!

2.2 方法2:performance_schema(MySQL 5.6+)

sql 复制代码
-- 1. 开启performance_schema(需重启)
[mysqld]
performance_schema = ON

-- 2. 查询最慢的SQL(按总执行时间)
SELECT 
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,
    SUM_TIMER_WAIT / 1000000000000 AS total_time_sec,
    MIN_TIMER_WAIT / 1000000000000 AS min_time_sec,
    MAX_TIMER_WAIT / 1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

-- 3. 查询平均时间最长的SQL
SELECT 
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;

-- 4. 查询当前正在执行的SQL
SELECT 
    t.PROCESSLIST_ID,
    t.PROCESSLIST_USER,
    t.PROCESSLIST_HOST,
    t.PROCESSLIST_DB,
    t.PROCESSLIST_COMMAND,
    t.PROCESSLIST_TIME,
    t.PROCESSLIST_STATE,
    t.PROCESSLIST_INFO
FROM performance_schema.threads t
WHERE t.PROCESSLIST_COMMAND != 'Sleep'
ORDER BY t.PROCESSLIST_TIME DESC;

2.3 方法3:sys系统库(MySQL 5.7+)

sql 复制代码
-- 1. 查询执行时间最长的SQL
SELECT * FROM sys.statement_analysis
ORDER BY avg_latency DESC
LIMIT 10;

-- 2. 查询全表扫描的SQL
SELECT * FROM sys.statements_with_full_table_scans
ORDER BY total_latency DESC
LIMIT 10;

-- 3. 查询临时表使用情况
SELECT * FROM sys.statements_with_temp_tables
ORDER BY total_latency DESC
LIMIT 10;

-- 4. 查询排序操作
SELECT * FROM sys.statements_with_sorting
ORDER BY total_latency DESC
LIMIT 10;

-- 5. 查询未使用索引的SQL
SELECT 
    query,
    exec_count,
    sys.format_time(total_latency) AS total_latency,
    rows_sent,
    rows_examined,
    rows_sent_avg,
    rows_examined_avg
FROM sys.statements_with_full_table_scans
WHERE db = 'your_database'
ORDER BY total_latency DESC
LIMIT 10;

2.4 方法4:SHOW PROCESSLIST

sql 复制代码
-- 实时查看正在执行的查询
SHOW FULL PROCESSLIST;

-- 查找执行时间超过5秒的查询
SELECT 
    id,
    user,
    host,
    db,
    command,
    time,
    state,
    info
FROM information_schema.processlist
WHERE time > 5
AND command != 'Sleep'
ORDER BY time DESC;

-- 杀掉慢查询
KILL QUERY 12345;  -- 12345是进程ID

2.5 方法5:应用层监控

方式1:日志记录
java 复制代码
// Java示例
long startTime = System.currentTimeMillis();
try {
    // 执行SQL
    List<User> users = userMapper.selectByAge(20);
} finally {
    long endTime = System.currentTimeMillis();
    long duration = endTime - startTime;
    if (duration > 1000) {  // 超过1秒记录
        log.warn("慢查询警告: 耗时{}ms, SQL: {}", duration, sql);
    }
}
方式2:APM工具
  • SkyWalking:链路追踪
  • Pinpoint:性能监控
  • Arthas:在线诊断
  • CAT(美团):实时监控
方式3:数据库中间件
  • ShardingSphere:SQL审计
  • MyCAT:慢查询统计
  • ProxySQL:查询路由和监控

三、慢查询的常见原因

3.1 索引问题(80%的慢查询原因)

1. 没有索引
sql 复制代码
-- ❌ 全表扫描
SELECT * FROM users WHERE email = 'test@example.com';

-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
2. 索引失效
sql 复制代码
-- ❌ 函数导致索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;

-- ✅ 改写SQL
SELECT * FROM orders 
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

-- 详见:数据库索引失效场景详解.md
3. 索引选择不当
sql 复制代码
-- 优化器选错索引
-- 使用 FORCE INDEX 强制指定
SELECT * FROM orders FORCE INDEX(idx_create_time)
WHERE create_time > '2024-01-01';

3.2 SQL写法问题

1. SELECT *
sql 复制代码
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;

-- ✅ 只查询需要的列
SELECT id, order_no, amount FROM orders WHERE user_id = 123;
2. 深度分页
sql 复制代码
-- ❌ 偏移量过大
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- ✅ 使用子查询优化
SELECT * FROM orders 
WHERE id >= (SELECT id FROM orders ORDER BY id LIMIT 1000000, 1)
LIMIT 10;

-- ✅ 使用游标分页
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
3. 大批量操作
sql 复制代码
-- ❌ 一次插入10万条
INSERT INTO logs VALUES (1,...), (2,...), ... (100000,...);

-- ✅ 分批插入(每次1000条)
INSERT INTO logs VALUES (1,...), ... (1000,...);
-- 循环100次

3.3 数据量问题

1. 单表数据量过大
复制代码
建议:
- 单表 < 500万行:正常使用
- 500万 ~ 2000万:需优化索引和查询
- > 2000万:考虑分表

解决方案

sql 复制代码
-- 水平分表(按时间)
CREATE TABLE orders_2024_01 LIKE orders;
CREATE TABLE orders_2024_02 LIKE orders;

-- 水平分表(按用户ID)
CREATE TABLE orders_0 LIKE orders;  -- user_id % 10 = 0
CREATE TABLE orders_1 LIKE orders;  -- user_id % 10 = 1
2. 数据分布不均
sql 复制代码
-- 查询倾斜数据
SELECT status, COUNT(*) AS cnt
FROM orders
GROUP BY status;

-- 结果:
-- status=0: 1000万(95%)
-- status=1: 50万(5%)

-- 查询status=0会很慢(即使有索引)

3.4 锁和并发问题

1. 锁等待
sql 复制代码
-- 查看锁等待
SELECT * FROM sys.innodb_lock_waits;

-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;

-- 查看锁等待超时时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';  -- 默认50秒
2. 长事务
sql 复制代码
-- 查找长事务(执行超过60秒)
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

-- 查看事务详情
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_requested_lock_id,
    trx_wait_started,
    trx_weight,
    trx_mysql_thread_id,
    trx_query
FROM information_schema.innodb_trx;

3.5 JOIN问题

1. 大表JOIN
sql 复制代码
-- ❌ 两个大表直接JOIN
SELECT *
FROM orders o  -- 1000万
INNER JOIN order_details od ON o.id = od.order_id  -- 5000万
WHERE o.user_id = 123;

-- ✅ 先过滤再JOIN
SELECT *
FROM (SELECT * FROM orders WHERE user_id = 123) o
INNER JOIN order_details od ON o.id = od.order_id;
2. JOIN列没有索引
sql 复制代码
-- ❌ 关联列无索引
SELECT * FROM orders o
INNER JOIN users u ON o.user_email = u.email;

-- ✅ 添加索引
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_user_email ON orders(user_email);

3.6 服务器资源问题

1. CPU瓶颈
bash 复制代码
# 查看CPU使用率
top
# 或
vmstat 1

# 查看MySQL进程CPU占用
ps aux | grep mysql
2. 内存不足
sql 复制代码
-- 查看内存配置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';  -- InnoDB缓冲池
SHOW VARIABLES LIKE 'key_buffer_size';          -- MyISAM缓冲

-- 查看缓存命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
3. 磁盘IO瓶颈
bash 复制代码
# 查看磁盘IO
iostat -x 1

# 关键指标:
# %util: 磁盘利用率(>80%表示繁忙)
# await: 平均等待时间(>10ms需关注)

四、系统的排查流程

4.1 五步排查法

复制代码
Step 1: 发现慢查询
   ↓
Step 2: 分析执行计划(EXPLAIN)
   ↓
Step 3: 定位具体原因
   ↓
Step 4: 制定优化方案
   ↓
Step 5: 验证优化效果

4.2 详细排查步骤

Step 1:获取慢SQL
sql 复制代码
-- 从慢查询日志获取
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

-- 或从performance_schema获取
SELECT 
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,
    SUM_TIMER_WAIT / 1000000000000 AS total_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

Step 2:EXPLAIN分析
sql 复制代码
-- 执行EXPLAIN
EXPLAIN SELECT * FROM orders WHERE DATE(create_time) = '2024-01-15';

-- 关键指标:
-- 1. type: ALL → 全表扫描(需优化)
-- 2. key: NULL → 未使用索引
-- 3. rows: 1000000 → 扫描100万行
-- 4. Extra: Using filesort → 需要排序优化

详细分析见EXPLAIN执行计划详解.md


Step 3:定位问题

检查清单

检查项 检查方法 问题表现
索引 SHOW INDEX FROM table key=NULL, type=ALL
表结构 SHOW CREATE TABLE table 字段类型、字符集
数据量 SELECT COUNT(*) FROM table 单表过大
数据分布 SELECT col, COUNT(*) GROUP BY col 数据倾斜
锁等待 SHOW ENGINE INNODB STATUS Lock wait timeout
连接数 SHOW PROCESSLIST Too many connections
服务器资源 top, iostat, free CPU/内存/IO瓶颈

Step 4:制定方案

决策树

复制代码
问题类型?
├─ 索引问题
│  ├─ 无索引 → 创建索引
│  ├─ 索引失效 → 改写SQL
│  └─ 索引选择错误 → 优化索引或强制索引
│
├─ SQL写法
│  ├─ SELECT * → 只查需要的列
│  ├─ 深度分页 → 改用游标分页
│  └─ 大批量操作 → 分批处理
│
├─ 数据量
│  ├─ 单表过大 → 分表
│  └─ 返回结果过多 → 添加LIMIT
│
├─ JOIN问题
│  ├─ 大表JOIN → 先过滤再JOIN
│  └─ JOIN列无索引 → 添加索引
│
└─ 服务器资源
   ├─ CPU瓶颈 → 优化SQL或扩容
   ├─ 内存不足 → 调整缓冲池或扩容
   └─ IO瓶颈 → 使用SSD或优化查询

Step 5:验证效果
sql 复制代码
-- 1. 执行优化后的SQL,记录耗时
SET profiling = 1;
SELECT ...;  -- 优化后的SQL
SHOW PROFILES;

-- 2. 对比EXPLAIN结果
EXPLAIN 优化前的SQL;
EXPLAIN 优化后的SQL;

-- 3. 压力测试
-- 使用sysbench或mysqlslap测试

-- 4. 监控线上效果
-- 观察慢查询日志、QPS、响应时间

五、优化方法详解

5.1 索引优化

优化1:创建合适的索引
sql 复制代码
-- 场景:WHERE条件列
CREATE INDEX idx_user_id ON orders(user_id);

-- 场景:ORDER BY排序列
CREATE INDEX idx_create_time ON orders(create_time);

-- 场景:JOIN关联列
CREATE INDEX idx_order_id ON order_details(order_id);

-- 场景:GROUP BY分组列
CREATE INDEX idx_status ON orders(status);

-- 场景:覆盖索引(包含查询的所有列)
CREATE INDEX idx_user_order_amount ON orders(user_id, order_no, amount);
优化2:联合索引顺序
sql 复制代码
-- 高频查询
SELECT * FROM orders 
WHERE status = 1 AND user_id = 123 AND create_time > '2024-01-01';

-- ❌ 错误顺序(范围查询在前)
CREATE INDEX idx_wrong ON orders(create_time, status, user_id);

-- ✅ 正确顺序(等值查询在前,范围查询在后)
CREATE INDEX idx_right ON orders(status, user_id, create_time);

排序原则

  1. 等值查询(=) > 范围查询(>、<、BETWEEN)
  2. 高频查询列 > 低频查询列
  3. 高区分度列 > 低区分度列
优化3:删除冗余索引
sql 复制代码
-- 查看表的所有索引
SHOW INDEX FROM orders;

-- 冗余情况
CREATE INDEX idx_a ON orders(user_id);
CREATE INDEX idx_ab ON orders(user_id, status);
-- idx_a 是冗余的,可以删除

-- 删除冗余索引
ALTER TABLE orders DROP INDEX idx_a;

-- 使用sys库查找冗余索引(MySQL 5.7+)
SELECT * FROM sys.schema_redundant_indexes;

5.2 SQL改写优化

优化1:避免SELECT *
sql 复制代码
-- ❌ 查询所有列
SELECT * FROM orders WHERE user_id = 123;

-- ✅ 只查询需要的列
SELECT id, order_no, amount, status FROM orders WHERE user_id = 123;

-- 优势:
-- 1. 减少网络传输
-- 2. 可能使用覆盖索引
-- 3. 减少内存占用
优化2:深度分页优化
sql 复制代码
-- ❌ 偏移量过大(扫描+跳过100万行)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- ✅ 方案1:使用子查询(延迟关联)
SELECT o.*
FROM orders o
INNER JOIN (
    SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t ON o.id = t.id;

-- ✅ 方案2:使用游标(记录上次位置)
SELECT * FROM orders 
WHERE id > 1000000  -- 上次查询的最后一条记录ID
ORDER BY id 
LIMIT 10;

-- ✅ 方案3:使用ES等搜索引擎
-- 数据同步到Elasticsearch,使用scroll API分页
优化3:IN优化
sql 复制代码
-- ❌ IN中元素过多(>1000)
SELECT * FROM orders WHERE user_id IN (1, 2, 3, ..., 10000);

-- ✅ 方案1:分批查询
SELECT * FROM orders WHERE user_id IN (1, 2, ..., 1000);
SELECT * FROM orders WHERE user_id IN (1001, 1002, ..., 2000);

-- ✅ 方案2:使用临时表
CREATE TEMPORARY TABLE tmp_users (user_id INT);
INSERT INTO tmp_users VALUES (1), (2), ..., (10000);
SELECT o.* FROM orders o
INNER JOIN tmp_users t ON o.user_id = t.user_id;
优化4:OR改写为UNION
sql 复制代码
-- ❌ OR可能导致索引失效
SELECT * FROM orders WHERE status = 1 OR amount > 1000;

-- ✅ 改写为UNION ALL
SELECT * FROM orders WHERE status = 1
UNION ALL
SELECT * FROM orders WHERE amount > 1000 AND status != 1;
优化5:子查询改写为JOIN
sql 复制代码
-- ❌ 关联子查询(N+1问题)
SELECT 
    u.id,
    u.name,
    (SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count
FROM users u;

-- ✅ 改写为JOIN
SELECT 
    u.id,
    u.name,
    COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

5.3 表结构优化

优化1:选择合适的数据类型
sql 复制代码
-- ❌ 使用过大的类型
CREATE TABLE users (
    id BIGINT,              -- INT就够用
    age TINYINT UNSIGNED,   -- ✅ 正确
    status VARCHAR(255),    -- 只需要CHAR(1)
    amount DECIMAL(20,2)    -- DECIMAL(10,2)就够
);

-- ✅ 优化后
CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    age TINYINT UNSIGNED,
    status CHAR(1),
    amount DECIMAL(10,2)
);

-- 优势:
-- 1. 减少存储空间
-- 2. 提升索引效率
-- 3. 减少内存占用
优化2:字段拆分
sql 复制代码
-- ❌ 大字段和常用字段混在一起
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    author VARCHAR(50),
    content TEXT,         -- 大字段,但不常查询
    view_count INT
);

-- ✅ 垂直拆分
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    author VARCHAR(50),
    view_count INT
);

CREATE TABLE article_contents (
    article_id INT PRIMARY KEY,
    content TEXT,
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

-- 优势:减少IO,提升常用查询性能
优化3:表分区
sql 复制代码
-- 按时间分区(适合历史数据查询)
CREATE TABLE orders (
    id INT,
    user_id INT,
    amount DECIMAL(10,2),
    create_time DATETIME
)
PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 查询时只扫描特定分区
SELECT * FROM orders WHERE create_time >= '2024-01-01';
-- 只扫描p2024分区

5.4 查询优化

优化1:避免全表扫描
sql 复制代码
-- ❌ 没有WHERE条件
SELECT * FROM orders;

-- ✅ 添加WHERE和LIMIT
SELECT * FROM orders WHERE status = 1 LIMIT 1000;
优化2:使用LIMIT限制结果
sql 复制代码
-- ❌ 返回全部结果
SELECT * FROM orders WHERE status = 1;

-- ✅ 限制返回数量
SELECT * FROM orders WHERE status = 1 LIMIT 100;

-- ✅ EXISTS只需判断存在性
SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = 123) AS has_order;
优化3:批量操作
sql 复制代码
-- ❌ 逐条插入
INSERT INTO logs (user_id, action) VALUES (1, 'login');
INSERT INTO logs (user_id, action) VALUES (2, 'logout');
-- 执行1000次...

-- ✅ 批量插入
INSERT INTO logs (user_id, action) VALUES
(1, 'login'),
(2, 'logout'),
...
(1000, 'view');

-- ✅ 批量更新
UPDATE orders SET status = 1 WHERE id IN (1, 2, 3, ..., 1000);

5.5 缓存优化

优化1:应用层缓存
java 复制代码
// Redis缓存示例
public User getUserById(Long id) {
    // 1. 先查缓存
    String cacheKey = "user:" + id;
    User user = redisTemplate.opsForValue().get(cacheKey);
    
    if (user != null) {
        return user;  // 缓存命中
    }
    
    // 2. 缓存未命中,查数据库
    user = userMapper.selectById(id);
    
    // 3. 写入缓存
    if (user != null) {
        redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
    }
    
    return user;
}
优化2:查询缓存(Query Cache)
sql 复制代码
-- 注意:MySQL 8.0已移除查询缓存

-- MySQL 5.7及以下
-- 开启查询缓存
SET GLOBAL query_cache_type = ON;
SET GLOBAL query_cache_size = 67108864;  -- 64MB

-- 查看缓存状态
SHOW VARIABLES LIKE 'query_cache%';
SHOW STATUS LIKE 'Qcache%';

5.6 读写分离和分库分表

优化1:主从复制+读写分离
java 复制代码
// 使用动态数据源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "master";
}

// 写操作走主库
@DataSource("master")
public void createOrder(Order order) {
    orderMapper.insert(order);
}

// 读操作走从库
@DataSource("slave")
public Order getOrderById(Long id) {
    return orderMapper.selectById(id);
}
优化2:分库分表
java 复制代码
// 使用ShardingSphere实现分库分表
// 按user_id取模分表
spring.shardingsphere.rules.sharding.tables.orders.actual-data-nodes=ds0.orders_$->{0..9}
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-algorithm-name=orders-inline

spring.shardingsphere.rules.sharding.sharding-algorithms.orders-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.orders-inline.props.algorithm-expression=orders_$->{user_id % 10}

六、实战优化案例

6.1 案例1:电商订单查询优化

问题描述
复制代码
业务场景:用户查看订单列表
问题:订单表1000万数据,查询超时
SQL执行时间:8秒
原始SQL
sql 复制代码
SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;
排查过程

Step 1:EXPLAIN分析

sql 复制代码
EXPLAIN SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;

结果

复制代码
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows     | Extra                       |
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 10000000 | Using where; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+-----------------------------+

问题分析

  1. type = ALL:全表扫描
  2. key = NULL:未使用索引
  3. rows = 10000000:扫描全表
  4. Extra = Using filesort:需要排序

Step 2:优化方案

sql 复制代码
-- 1. 创建联合索引(user_id + create_time)
CREATE INDEX idx_user_create ON orders(user_id, create_time);

-- 2. 优化SQL(只查询需要的列)
SELECT id, order_no, amount, status, create_time
FROM orders 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;

Step 3:验证优化效果

sql 复制代码
EXPLAIN SELECT id, order_no, amount, status, create_time
FROM orders 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 10;

优化后结果

复制代码
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+
| id | select_type | table  | type | possible_keys    | key              | key_len | ref   | rows | Extra       |
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+
|  1 | SIMPLE      | orders | ref  | idx_user_create  | idx_user_create  | 4       | const |  100 | Using where |
+----+-------------+--------+------+------------------+------------------+---------+-------+------+-------------+

优化效果

  • type = ref:使用索引
  • key = idx_user_create:使用了新索引
  • rows = 100:只扫描100行(从1000万降到100)
  • ✅ 无 Using filesort:利用索引排序
  • ⏱️ 执行时间:从8秒降到0.01秒(提升800倍)

6.2 案例2:报表统计优化

问题描述
复制代码
业务场景:后台订单统计
问题:统计查询超时
SQL执行时间:25秒
原始SQL
sql 复制代码
SELECT 
    DATE(create_time) AS date,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    AVG(amount) AS avg_amount
FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01'
GROUP BY DATE(create_time)
ORDER BY date;
排查过程

EXPLAIN分析

复制代码
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows     | Extra                                        |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 10000000 | Using where; Using temporary; Using filesort |
+----+-------------+--------+------+---------------+------+---------+------+----------+----------------------------------------------+

问题

  1. type = ALL:全表扫描
  2. Extra = Using temporary:使用临时表
  3. Extra = Using filesort:需要排序
  4. ❌ GROUP BY使用了函数DATE()
优化方案

方案1:添加冗余字段

sql 复制代码
-- 1. 添加日期字段
ALTER TABLE orders ADD COLUMN order_date DATE;

-- 2. 创建索引
CREATE INDEX idx_order_date ON orders(order_date);

-- 3. 更新历史数据
UPDATE orders SET order_date = DATE(create_time);

-- 4. 应用层写入时自动填充
INSERT INTO orders (user_id, amount, create_time, order_date) 
VALUES (123, 100.00, NOW(), CURDATE());

-- 5. 优化后的SQL
SELECT 
    order_date AS date,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    AVG(amount) AS avg_amount
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2024-02-01'
GROUP BY order_date
ORDER BY order_date;

方案2:汇总表(更优)

sql 复制代码
-- 1. 创建日统计表
CREATE TABLE order_daily_stats (
    stat_date DATE PRIMARY KEY,
    order_count INT,
    total_amount DECIMAL(15,2),
    avg_amount DECIMAL(10,2),
    update_time DATETIME
);

-- 2. 定时任务汇总数据(凌晨执行)
INSERT INTO order_daily_stats (stat_date, order_count, total_amount, avg_amount, update_time)
SELECT 
    DATE(create_time),
    COUNT(*),
    SUM(amount),
    AVG(amount),
    NOW()
FROM orders
WHERE DATE(create_time) = CURDATE() - INTERVAL 1 DAY
ON DUPLICATE KEY UPDATE
    order_count = VALUES(order_count),
    total_amount = VALUES(total_amount),
    avg_amount = VALUES(avg_amount),
    update_time = VALUES(update_time);

-- 3. 查询汇总表(毫秒级)
SELECT * FROM order_daily_stats
WHERE stat_date >= '2024-01-01' AND stat_date < '2024-02-01'
ORDER BY stat_date;

优化效果

  • ⏱️ 执行时间:从25秒降到0.005秒(提升5000倍)
  • 💾 空间成本:增加一个汇总表(31行/月)
  • 🎯 适用场景:历史数据统计、报表查询

6.3 案例3:JOIN查询优化

问题描述
复制代码
业务场景:用户订单详情
问题:多表关联查询慢
SQL执行时间:12秒
原始SQL
sql 复制代码
SELECT 
    u.name,
    o.order_no,
    o.amount,
    p.product_name,
    od.quantity
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_details od ON o.id = od.order_id
INNER JOIN products p ON od.product_id = p.id
WHERE u.status = 1
AND o.create_time >= '2024-01-01';
EXPLAIN分析
复制代码
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | u     | ALL  | NULL          | NULL | NULL    | NULL | 1000000 | Using where                 |
|  1 | SIMPLE      | o     | ALL  | NULL          | NULL | NULL    | NULL | 5000000 | Using where; Using join...  |
|  1 | SIMPLE      | od    | ALL  | NULL          | NULL | NULL    | NULL | 10000000| Using where; Using join...  |
|  1 | SIMPLE      | p     | ALL  | NULL          | NULL | NULL    | NULL | 100000  | Using where; Using join...  |
+----+-------------+-------+------+---------------+------+---------+------+---------+-----------------------------+

问题

  • 4个表都是全表扫描
  • 所有JOIN列都没有索引
优化方案
sql 复制代码
-- 1. 添加索引
CREATE INDEX idx_status ON users(status);
CREATE INDEX idx_user_create ON orders(user_id, create_time);
CREATE INDEX idx_order_id ON order_details(order_id);
CREATE INDEX idx_product_id ON order_details(product_id);

-- 2. 优化SQL(先过滤再JOIN)
SELECT 
    u.name,
    o.order_no,
    o.amount,
    p.product_name,
    od.quantity
FROM (
    SELECT id, name FROM users WHERE status = 1
) u
INNER JOIN (
    SELECT id, user_id, order_no, amount 
    FROM orders 
    WHERE create_time >= '2024-01-01'
) o ON u.id = o.user_id
INNER JOIN order_details od ON o.id = od.order_id
INNER JOIN products p ON od.product_id = p.id;

优化后EXPLAIN

复制代码
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+
| id | select_type | table | type   | possible_keys    | key              | key_len | ref       | rows  | Extra       |
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+
|  1 | SIMPLE      | u     | ref    | idx_status       | idx_status       | 1       | const     | 10000 | Using where |
|  1 | SIMPLE      | o     | ref    | idx_user_create  | idx_user_create  | 4       | u.id      |     5 | Using where |
|  1 | SIMPLE      | od    | ref    | idx_order_id     | idx_order_id     | 4       | o.id      |     3 | NULL        |
|  1 | SIMPLE      | p     | eq_ref | PRIMARY          | PRIMARY          | 4       | od.product_id|  1 | NULL        |
+----+-------------+-------+--------+------------------+------------------+---------+-----------+-------+-------------+

优化效果

  • ⏱️ 执行时间:从12秒降到0.05秒(提升240倍)

七、监控与预防

7.1 慢查询监控

1. 数据库层监控
sql 复制代码
-- 定期检查慢查询
SELECT 
    DIGEST_TEXT AS query,
    COUNT_STAR AS exec_count,
    AVG_TIMER_WAIT / 1000000000000 AS avg_time_sec,
    MAX_TIMER_WAIT / 1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000000  -- 超过1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 20;
2. 应用层监控
java 复制代码
// AOP拦截慢SQL
@Aspect
@Component
public class SlowSqlAspect {
    
    @Around("execution(* com.example.mapper..*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            if (duration > 1000) {  // 超过1秒
                log.warn("慢SQL告警: {}ms, 方法: {}", 
                    duration, pjp.getSignature());
                // 发送告警
                alertService.sendSlowSqlAlert(pjp.getSignature(), duration);
            }
        }
    }
}
3. 监控平台
  • Prometheus + Grafana:可视化监控
  • Zabbix:告警通知
  • 阿里云RDS:自带慢查询分析
  • 腾讯云CDB:SQL审计

7.2 SQL审核

上线前审核
sql 复制代码
-- 使用EXPLAIN预审SQL
EXPLAIN SELECT ...;

-- 审核标准:
-- 1. type不能是ALL
-- 2. key不能是NULL
-- 3. rows不能超过10万
-- 4. Extra不能有Using temporary
SQL审核工具
  • Yearning:开源SQL审核平台
  • Archery:SQL上线审核系统
  • Soar:SQL优化和改写工具

7.3 数据库规范

索引规范
复制代码
1. 单表索引数量不超过5个
2. 联合索引字段数不超过5个
3. 索引字段总长度不超过767字节
4. 必须为JOIN字段创建索引
5. 禁止在低区分度字段建索引(如性别)
SQL规范
复制代码
1. 禁止SELECT *
2. 禁止在WHERE子句使用函数
3. 禁止隐式类型转换
4. 禁止LIKE '%keyword%'
5. 禁止大批量操作(单次超过1000条分批)
6. 禁止深度分页(OFFSET超过10000)
表设计规范
复制代码
1. 单表字段数不超过30个
2. 单表数据量不超过2000万
3. 必须有主键
4. 字符字段必须指定字符集
5. 禁止使用TEXT、BLOB(必要时拆分)

八、优化效果评估

8.1 性能指标

指标 优化前 优化后 提升
响应时间 8秒 0.01秒 800倍
扫描行数 1000万 100 10万倍
CPU使用率 80% 20% 降低75%
QPS 10 1000 100倍
并发数 50 500 10倍

8.2 对比测试

sql 复制代码
-- 开启profiling
SET profiling = 1;

-- 执行优化前SQL
SELECT ...;  -- Query 1

-- 执行优化后SQL
SELECT ...;  -- Query 2

-- 查看对比
SHOW PROFILES;

-- 详细分析
SHOW PROFILE FOR QUERY 1;
SHOW PROFILE FOR QUERY 2;

九、常见问题FAQ

Q1:为什么添加了索引还是慢?

可能原因

  1. 索引失效(函数、类型转换、LIKE '%xx')
  2. 优化器没有选择索引(数据量太小或太大)
  3. 回表开销大(可以用覆盖索引)
  4. 锁等待

排查方法

sql 复制代码
EXPLAIN SELECT ...;
-- 查看key是否为NULL
-- 查看type是否为ALL

Q2:数据量不大为什么还慢?

可能原因

  1. 没有WHERE条件(全表扫描)
  2. 锁等待(长事务、表锁)
  3. 硬件问题(磁盘IO、CPU)
  4. 网络延迟

Q3:如何选择优化优先级?

优先级排序

复制代码
1. 紧急慢查询(影响线上业务)
2. 高频慢查询(执行次数多)
3. 低频慢查询(偶尔执行)
4. 报表查询(可离线处理)

Q4:索引是不是越多越好?

不是!索引的代价

  • 占用存储空间
  • 降低写入性能
  • 增加维护成本

建议

  • 单表索引不超过5个
  • 定期删除未使用的索引

Q5:什么时候需要分表?

建议阈值

  • 单表 < 500万:正常使用
  • 500万 ~ 2000万:优化索引和查询
  • 2000万:考虑分表

分表策略

  • 垂直分表:按字段拆分
  • 水平分表:按数据范围拆分

总结

慢查询优化核心思路

复制代码
1. 发现问题:慢查询日志 + 监控
2. 分析问题:EXPLAIN + 执行计划
3. 定位原因:索引 / SQL / 数据量 / 锁
4. 制定方案:索引优化 / SQL改写 / 分表
5. 验证效果:性能对比 + 压力测试
6. 持续监控:告警 + 定期review

优化方法速查

问题类型 优化方法 优先级
无索引 创建索引 ⭐⭐⭐⭐⭐
索引失效 改写SQL ⭐⭐⭐⭐⭐
全表扫描 添加WHERE+索引 ⭐⭐⭐⭐⭐
**SELECT *** 只查需要的列 ⭐⭐⭐⭐
深度分页 游标分页 ⭐⭐⭐⭐
大表JOIN 先过滤再JOIN ⭐⭐⭐⭐
单表过大 分表 ⭐⭐⭐
锁等待 优化事务 ⭐⭐⭐⭐

最佳实践

  1. 开发阶段:所有SQL必须EXPLAIN分析
  2. 测试阶段:压力测试,发现潜在问题
  3. 上线阶段:SQL审核,慢查询告警
  4. 运维阶段:定期review,持续优化

相关推荐
凸头6 小时前
MySQL 的四种 Binlog 日志处理工具:Canal、Maxwell、Databus和 阿里云 DTS
数据库·mysql·阿里云
观测云6 小时前
阿里云 RDS MySQL 可观测性最佳实践
mysql·阿里云·云计算
野犬寒鸦7 小时前
从零起步学习MySQL || 第八章:索引深入理解及高级运用(结合常见优化问题讲解)
java·服务器·数据库·后端·mysql
Sam_Deep_Thinking7 小时前
为超过10亿条记录的订单表新增字段
mysql
奥尔特星云大使7 小时前
Docker 拉取 MySQL 5.7 镜像、启动容器并进入 MySQL
数据库·mysql·docker·容器
czhc11400756638 小时前
Linux1023 mysql 修改密码等
android·mysql·adb
SamDeepThinking9 小时前
MySQL InnoDB Redo Log简单介绍
mysql
GOATLong9 小时前
MySQL内置函数
android·数据库·c++·vscode·mysql
张毫洁13 小时前
mysql、oracle的JDBC操作
数据库·mysql·oracle