一句 Executive Summary
数据库和服务层的性能问题本质上是“无谓工作”的累加:无谓扫描、无谓回表、无谓网络往返与无谓资源创建。通过索引与查询改写减少扫描行数、通过批量化与缓存减少往返、通过架构化治理减少阻塞,完成从“能跑”到“能扩展”的跃迁。
引子
公司某报表系统每天上午 9:00 准时卡顿:单条查询等待约 30 秒,用户抱怨不断。排查发现一条 SQL 未走索引,对百万级数据做全表扫描。优化后,耗时降为 0.1 秒。
比喻:数据库是图书馆,索引等于书籍编号。直接说“给我一本小说”相当于全表扫描;说“给我编号 A123 架第4层”则是索引查询——快速到位。
1. 为什么要做优化(本质)
- 问题核心:不合理的 SQL 与服务层写法会使数据库承担指数级的额外工作(扫描更多行、产生更多随机 IO、触发频繁回表、增加锁竞争)。
- 目标:把“无谓工作”降到最低,优先减少扫描行数与网络往返次数,并保证系统在数据增长与并发放大时可扩展。
2. 服务层:8 大性能陷阱与修复(工程化)
(示例代码与修复步骤)
陷阱 1 — Controller 写业务逻辑
问题:可维护性差、测试难度高、逻辑散落。
修复:Controller 只做入参校验与鉴权;Service 处理流程;Domain/Manager 处理规则;Repository 访问 DB。
陷阱 2 — Service 中 for 循环导致 N+1 查询(顶级性能杀手)
反例(典型 N+1)
for (Order order : orderList) {
User user = userMapper.findById(order.getUserId()); // 每次循环发一条 SQL
}
严重性:orderList = 1000 → 1001 次 SQL;数据库压力爆表。
优化方案:
- 批量 IN + Map 聚合(强推):
List<Long> ids = orderList.stream().map(Order::getUserId).collect(toList());
List<User> users = userMapper.findByIds(ids);
Map<Long, User> userMap = users.stream().collect(toMap(User::getId, u -> u));
SQL:
SELECT * FROM users WHERE id IN (1,2,3,...);
- 或 JOIN 一次取全:
SELECT o.*, u.name, u.level
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid';
- 或 Redis 缓存 + 批量回源。
量化:从 1001 次 SQL 与数百 ms 延迟,降到 1 次 SQL 与 <20 ms。
陷阱 3 — 循环内频繁创建昂贵资源(连接、HttpClient、文件句柄)
反例
for(...) {
HttpClient client = new HttpClient();
// ...请求
}
修复:客户端/连接池单例复用、线程池/对象池。
陷阱 4 — DAO/Service 层滥用 Map 当万能数据结构
问题:字段不明确、序列化开销、易出错。
修复:DTO/VO/DO 明确分层,严格类型契约。
陷阱 5 — 深度分页(大 OFFSET)
反例
SELECT * FROM orders ORDER BY id LIMIT 100000,20;
修复:Seek-pagination(WHERE id > last_id LIMIT n)或按时间/主键游标分页。
陷阱 6 — 无 traceId/无链路日志
问题:线上故障排查靠猜。
修复:入口统一生成 traceId,全链路埋点(Prometheus/Grafana/Jaeger)。
陷阱 7 — IO 阻塞链(外部接口超时、慢 SQL)
修复:超时/熔断/降级、异步 Worker、隔离线程池。
陷阱 8 — 滥用双重循环(O(n²))
反例
for (User u : users) {
for (Order o : orders) {
if (u.id == o.userId) {...}
}
}
修复:构建 Map 索引(id -> object)、预聚合、分片批处理。
3. SQL:21 条硬核优化策略(逐条,含反例/优化/原理)
以下每条都给出:反例(错误)→ 优化后示例 → 原理解释。确保示例覆盖你之前提供的所有典型写法。
基础篇(1–4)
1) 禁用 SELECT *
- 反例:
SELECT * FROM users WHERE status = 1;
- 优化:
SELECT id, name, email, status FROM users WHERE status = 1;
- 原理:减少 IO、网络与内存序列化;避免拉取大文本字段 (TEXT/BLOB)。
2) EXISTS vs IN:按子查询规模选择
- 反例(在大数据量下使用 IN):
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level > 3);
- 优化(大子集使用 EXISTS):
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.vip_level > 3);
- 原理:EXISTS 对每行短路判断更节省中间内存;IN 在某些引擎可能构建临时集合。
3) 避免 WHERE 中使用函数(保持索引命中)
- 反例:
SELECT * FROM orders WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-01';
SELECT * FROM products WHERE LOWER(name) = 'iphone';
- 优化:
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
SELECT * FROM products WHERE name = 'iPhone';
- 原理:对索引列应用函数使索引失效,导致全表扫描。
4) UNION ALL 优先(若不需要去重)
- 反例:
SELECT city FROM customers
UNION
SELECT city FROM suppliers;
- 优化:
SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;
- 原理:UNION 强制排序去重;UNION ALL 直接合并,性能好几倍。
索引篇(5–8)
5) 为高频 WHERE/ORDER 列建索引
- 反例:
SELECT * FROM orders WHERE status = 'pending';
- 优化:
CREATE INDEX idx_orders_status ON orders(status);
- 原理:索引将筛查量从百万级降为选择集,显著减少 IO。
6) 复合索引遵循最左前缀原则
- 示例索引:
CREATE INDEX idx_orders_status_time_user ON orders(status, create_time, user_id);
- 有效查询:
SELECT * FROM orders WHERE status='pending' AND create_time > '2024-01-01';
- 无效查询(无法充分利用索引):
SELECT * FROM orders WHERE create_time > '2024-01-01';
- 原理:复合索引可被用于以最左列开头的查询。
7) 避免索引列参与计算
- 反例:
SELECT * FROM products WHERE price + 100 > 500;
SELECT * FROM users WHERE YEAR(create_time) = 2024;
- 优化:
SELECT * FROM products WHERE price > 400;
SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
- 原理:计算改变列值形式,使 B+Tree 索引失效。
8) 索引不是越多越好
- 反例:对每个查询字段盲目加索引。
- 原理:写操作(INSERT/UPDATE/DELETE)要维护所有索引,过多索引导致写延迟、空间浪费,且优化器选择成本上升。建议:单表 3–5 个有效索引。
高级技巧篇(9–12)
9) 深度分页优化:告别大 OFFSET
- 反例:
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
- 优化(游标分页 / seek):
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;
- 原理:LIMIT offset 要先扫描 offset 条数据再丢弃,scan cost 随 offset 线性增加。
10) 批量操作:替代循环单次写入
- 反例(Java 单条插入循环):
for (User user : userList) {
userMapper.insert(user);
}
- 优化(批量插入):
INSERT INTO users(name, age) VALUES ('张三',25),('李四',30),('王五',28);
- 原理:批量减少事务提交与网络往返,日志合并写入(WAL 更高效)。
11) 子查询改 JOIN(或使用 STRAIGHT_JOIN 指定顺序)
- 反例:
SELECT * FROM products WHERE category_id IN (SELECT id FROM categories WHERE type = 'electronic');
- 优化:
SELECT p.* FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE c.type = 'electronic';
- 原理:JOIN 给优化器更多的执行计划选择空间,且可利用连接索引。
12) 覆盖索引以避免回表
- 反例(回表):
SELECT name FROM users WHERE age > 18;
- 优化(覆盖索引):
CREATE INDEX idx_users_age_name ON users(age, name);
SELECT name FROM users WHERE age > 18;
- 原理:索引页已经包含查询所需列,避免额外磁盘访问(回表)。
设计优化篇 (13–15)
13) 选择合适数据类型
- 反例:
id VARCHAR(50), age VARCHAR(10), create_time VARCHAR(20)
- 优化:
id BIGINT AUTO_INCREMENT, age TINYINT UNSIGNED, create_time DATETIME
- 原理:合适的类型减小存储、比较更快、索引更高效。
14) 谨慎使用 NULL 值
- 反例:
phone VARCHAR(20) DEFAULT NULL
- 优化:
phone VARCHAR(20) NOT NULL DEFAULT ''
- 原理:NULL 带来三值逻辑与统计复杂性,聚合会忽略 NULL。
15) 反规范化:用空间换时间(读多写少)
- 反例(频繁 JOIN):
SELECT u.name, o.order_no, p.product_name FROM users u JOIN orders o ... JOIN products p ...
- 优化:在 orders 表冗余 user_name、product_name,读时单表即可返回。
- 原理:减少多表 JOIN 成本,适合报表/OLAP 场景。
实战篇 (16–17)
16) 电商订单查询优化(2.3s → 0.02s)
- 反例:
SELECT * FROM orders WHERE user_id = 123 AND status IN ('paid','shipped') AND create_time BETWEEN '2024-01-01' AND '2024-06-30' ORDER BY create_time DESC;
-
优化步骤:
- EXPLAIN 查看执行计划
- 建复合索引:
CREATE INDEX idx_orders_user_status_time ON orders(user_id, status, create_time);
- 改写时间范围避免 BETWEEN:
SELECT order_id, user_id, amount, status, create_time FROM orders
WHERE user_id = 123 AND status IN ('paid','shipped')
AND create_time >= '2024-01-01' AND create_time < '2024-07-01'
ORDER BY create_time DESC;
- 原理:复合索引覆盖过滤和排序列,避免 filesort/temporary。
17) 报表统计优化(30s → 0.1s)
- 反例(每日统计直接扫描):
SELECT COUNT(*) AS total_orders, SUM(amount) AS total_amount, AVG(amount) AS avg_amount FROM orders WHERE DATE(create_time) = CURDATE();
- 优化方案 A(范围查询 + 索引):
SELECT COUNT(*) AS total_orders, SUM(amount) AS total_amount, AVG(amount) AS avg_amount
FROM orders
WHERE create_time >= DATE(CURDATE()) AND create_time < DATE(CURDATE()) + INTERVAL 1 DAY;
- 优化方案 B(预聚合汇总表):
CREATE TABLE order_daily_stats (
stat_date DATE PRIMARY KEY,
total_orders INT,
total_amount DECIMAL(15,2),
avg_amount DECIMAL(10,2)
);
-- 定时任务每日填充
SELECT * FROM order_daily_stats WHERE stat_date = CURDATE();
- 原理:预聚合把昂贵的聚合计算离线化,查询常数时间完成。
运维工具篇(18–20)
18) 深入理解 EXPLAIN(关注 type/key/rows/Extra)
- 反例:EXPLAIN 显示
type=ALL、rows巨大、Extra有Using filesort。 - 目标:
type为ref/range/const,key为合适索引,rows小,Extra无Using filesort或Using temporary。
19) 配置慢查询日志并定期分析
- 推荐配置:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
- 分析工具:
mysqldumpslow、pt-query-digest。
20) 定期维护:ANALYZE / OPTIMIZE / 检查未用索引
- 命令:
ANALYZE TABLE users, orders, products;
OPTIMIZE TABLE large_table;
SELECT * FROM sys.schema_unused_indexes;
- 原理:维护统计信息、重建碎片、清理无用索引,保证优化器决策质量。
工程实践篇(21 — 服务层 N+1,复述 + 扩展)
21) Service 层 N+1 查询治理(已覆盖并强化)
- 反例、优化方案、JOIN、缓存、ORM 批量 fetch 均已在服务层章节详述。
- 要点回顾:减少 DB 往返次数是首要目标,优先用批量 IN / JOIN / 缓存 / 批量 fetch。
4. 实战案例与量化结果(汇总)
- 报表
DATE()改范围 + 索引:30s → 0.1s。 - 订单查询建复合索引 + 字段精简:2.3s → 0.02s。
- N+1 问题:1000 条 for 循环(1001 次 SQL)→ 批量 IN/ JOIN:SQL 次数降到 1,延迟从数百 ms → <20 ms。
5. 工程化执行流程(可复制到团队流程)
阶段 A(0–1 周,快速收益)
- 开启慢查询日志(7 天收集)
- 排序 TOP N 慢 SQL(pt-query-digest)
- 针对 TOP 10 做 EXPLAIN,定位全表扫描 / filesort / temporary
- 快速修复:SELECT * → 精确字段;函数式 WHERE → 区间查询;添加高收益索引
阶段 B(1–4 周,稳固改进)
- 批量化改造(N+1 清理、批量写入)
- 深度分页替换为游标分页
- 增加预聚合表(报表场景)
- 引入缓存策略(Redis)并设置失效策略
阶段 C(1–3 月,体系化)
- 分区/分表评估并实施(大表)
- 自动化慢查询回归与计划回归测试
- 监控与告警完善(95%/99% 分位、扫描行数、buffer pool 命中率)
长期(持续)
- 架构分离(OLTP vs OLAP)
- 物化视图 / 专用分析引擎(ClickHouse、Druid)引入
6. 检查表(Deployment Checklist)
- 慢查询日志开启并分析过去 7 天数据
- TOP 20 慢 SQL 已执行 EXPLAIN 并记录 type/key/rows/Extra
- 消除 SELECT *、函数式 WHERE、非必要 UNION 去重
- 是否存在 N+1 查询(服务层循环 DB 调用)?已修复?
- 是否存在深度 LIMIT offset?已改游标分页?
- 建立或调整复合索引(遵循最左规则)
- 是否建立覆盖索引以减少回表?
- 是否对关键报表建立预聚合表?
- 是否配置全链路 trace 与监控?
- 周期性任务:ANALYZE / OPTIMIZE / 索引审计已排期
7. 运维与可观测(关键指标)
- 慢查询数量与 TOP SQL 列表(每日)
- 平均/95%/99% 响应时间(关键接口)
- 单查询扫描行数(rows)与使用索引情况(key)
- Buffer Pool 命中率 / IOps / 锁等待 / 活动连接数
8. 结论与交付选项
-
SQL 优化与服务层改造是系统工程:需要代码、索引、缓存、监控四方面协同。
-
建议优先解决“扫描行数”和“网络往返”问题:索引 + 批量/缓存。
-
可提供交付(任选其一或组合):
- 慢 SQL 体检报告(含 EXPLAIN、索引建议、变更脚本)
- 执行化优化任务清单 + 优先级甘特图
- 内部培训 PPT 或技术博客稿(含图表与例子)
如需我立即生成其中任一交付项(例如:把当前文档导出为 Markdown,或基于你提供的慢 SQL 的 EXPLAIN 输出生成逐条索引/改写脚本),请直接粘入慢 SQL 的原文及 EXPLAIN 输出,我将立即给出具体可执行的变更脚本与验证步骤。