🗄️ 数据库调优
数据库通常是整个系统链路的终极瓶颈——应用可以水平扩展,但数据库的扩展要复杂得多。 本指南涵盖执行计划分析、索引优化、连接池调优、读写分离架构及慢查询治理五大核心领域。
1. 执行计划分析(EXPLAIN)
执行计划是SQL调优的核心工具。通过 EXPLAIN 了解优化器的决策过程,
判断是否走索引、扫描行数、连接方式等关键信息。
📊 EXPLAIN 关键字段速查(MySQL)
| 字段 | 含义 | 理想值 | 危险信号 |
|---|---|---|---|
| id | SELECT 序号 | — | 多个不同id的复杂子查询 |
| select_type | 查询类型 | SIMPLE | DEPENDENT SUBQUERY、UNCACHEABLE SUBQUERY |
| type | 访问类型(最重要) | const > eq_ref > ref > range | ALL(全表扫描)、index(全索引扫描) |
| possible_keys | 可能使用的索引 | 不为NULL | NULL(无可用索引) |
| key | 实际使用的索引 | 不为NULL | NULL(未使用索引) |
| key_len | 使用索引的长度 | 越短越好 | 过长(复合索引使用不全) |
| rows | 预估扫描行数 | 越小越好 | 数万/数十万(扫描过多) |
| Extra | 额外信息 | Using index(覆盖索引) | Using filesort、Using temporary |
⚠️ type 字段等级排序(优→劣)
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
目标:至少达到 range 级别,尽量避免 ALL 和 index。
目标:至少达到 range 级别,尽量避免 ALL 和 index。
1.1 执行计划实战示例
🔍 慢SQL分析实例
-- 原始慢SQL
SELECT o.*, u.name FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2024-01-01' AND o.status = 'PAID'
ORDER BY o.create_time DESC LIMIT 20;
-- EXPLAIN 结果分析
+----+------+---------------+------+---------+------+-------+-----------------------------+
| id | type | possible_keys | key | key_len | rows | Extra |
+----+------+---------------+------+---------+-------+-----------------------------+
| 1 | ALL | NULL | NULL | NULL | 500万 | Using where; Using filesort |
| 1 | ref | idx_user_id | idx | 8 | 1 | Using where |
+----+------+---------------+------+---------+-------+-----------------------------+
-- 问题:orders 表全表扫描 500万行 + filesort
-- 解决:创建复合索引
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
-- 优化后 type=range, rows<100
2. 索引优化策略
2.1 索引设计原则
📐 索引设计十大原则
- 最左前缀原则:复合索引从最左列开始匹配,
WHERE a=? AND b=?可用INDEX(a,b),但WHERE b=?不行。 - 高选择性优先:区分度高的列放前面。区分度 =
COUNT(DISTINCT col)/COUNT(*),越接近1越好。 - 覆盖索引:查询列全部在索引中时,无需回表(Extra: Using index),性能提升显著。
- 避免冗余索引:
INDEX(a)+INDEX(a,b)中前者是冗余的,MySQL优化器可以只用后者。 - 避免在索引列上使用函数:
WHERE DATE(create_time) = '2024-01-01'不走索引,应写为WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。 - LIKE 前缀匹配:
LIKE 'abc%'走索引,LIKE '%abc'不走索引。 - 隐式类型转换:字符串列用数字查询(
WHERE phone=13800138000)不走索引,须加引号。 - OR 条件:每个OR分支都需要有索引,否则全表扫描。建议改用 UNION ALL。
- 不要过度索引:索引占用空间、拖慢写入。一个表的索引数建议控制在 5 个以内。
- 定期维护:
ANALYZE TABLE更新统计信息,OPTIMIZE TABLE整理碎片。
2.2 常见SQL性能问题速查
🩺 SQL 性能问题排查表
| 问题类型 | 典型症状 | 根因 | EXPLAIN信号 | 解决方案 |
|---|---|---|---|---|
| 全表扫描 | 大表查询慢、CPU高 | 无可用索引 | type=ALL, rows=大数 | 创建合适索引 |
| 回表过多 | 索引扫描快但查询慢 | 非覆盖索引+大量回表 | type=index/range, Extra无Using index | 覆盖索引或分页优化 |
| 索引失效 | 有索引但不走 | 函数/类型转换/OR | possible_keys有值但key=NULL | 改写SQL消除函数/转换 |
| filesort | 排序慢、temp表写磁盘 | ORDER BY列无索引 | Extra=Using filesort | 创建排序索引 |
| 临时表 | GROUP BY/DISTINCT慢 | 分组列无索引 | Extra=Using temporary | 创建分组索引 |
| 深分页 | OFFSET很大时变慢 | 扫描+丢弃大量行 | rows随OFFSET线性增长 | 游标分页/延迟关联 |
| 锁等待 | 写入阻塞、超时 | 长事务/间隙锁 | — | 缩短事务、优化索引减少锁范围 |
| Join顺序不当 | 多表Join极慢 | 驱动表太大 | rows乘积巨大 | STRAIGHT_JOIN指定驱动表 |
| 子查询低效 | IN子查询非常慢 | DEPENDENT SUBQUERY | select_type=DEPENDENT SUBQUERY | 改写为JOIN |
3. 连接池配置调优
数据库连接是昂贵的资源,连接池的合理配置直接决定了系统的并发能力。 注意:连接数不是越大越好——每个连接都会在MySQL服务端占用线程和内存。
🔌 连接池参数最佳实践
| 参数 | Druid | HikariCP | 推荐值 | 调优策略 |
|---|---|---|---|---|
| 最大连接数 | maxActive | maximumPoolSize | CPU核数*2+磁盘数 | 公式:connections = ((core_count * 2) + effective_spindle_count) |
| 最小空闲连接 | minIdle | minimumIdle | 同最大连接数 | HikariCP建议与最大值相同,避免突发创建连接 |
| 连接超时 | maxWait | connectionTimeout | 1~3s | 不可设置过大,避免请求堆积 |
| 空闲超时 | minEvictableIdleTimeMillis | idleTimeout | 10min | 小于MySQL的 wait_timeout(默认8h) |
| 连接存活时间 | maxAge / timeBetweenEvictionRunsMillis | maxLifetime | 30min | 小于MySQL的 wait_timeout 且小于基础设施连接超时 |
| 连接验证 | validationQuery | connectionTestQuery | SELECT 1 | HikariCP默认用JDBC4的isValid,无需配置 |
| 泄漏检测 | removeAbandoned | leakDetectionThreshold | 60s | 超过此时间未归还则告警/强制回收 |
💡 连接数瓶颈排查
- MySQL 端:
SHOW PROCESSLIST或SHOW STATUS LIKE 'Threads_connected' - 应用端:HikariCP 的
/actuator/prometheus暴露hikaricp_connections_active - 典型症状:应用日志出现
Connection is not available, request timed out after Xms
4. 读写分离架构
当单库成为瓶颈时,读写分离是最常用的水平扩展手段。核心思路是将读流量分流到从库, 主库专注于写入操作。
🏗️ 读写分离架构方案对比
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 客户端路由 | ShardingSphere-JDBC、MyBatis插件 | 无额外网络开销、配置简单 | 语言绑定、升级需重启应用 | 中小规模、Java技术栈 |
| 中间件代理 | ProxySQL、MySQL Router、ShardingSphere-Proxy | 对应用透明、多语言支持 | 增加一跳网络延迟、运维成本高 | 大规模、异构技术栈 |
| DB Mesh | Sidecar 模式 | 云原生、治理能力强大 | 架构复杂、学习成本高 | Service Mesh 环境 |
4.1 读写分离关键问题
⚠️ 主从延迟处理策略
读写分离最大的挑战是主从复制延迟(通常几百ms到几秒)。写后立刻读可能读到旧数据。
- 强制读主库:关键业务(如支付后查订单状态)写操作后指定时间段内读主库
- 延迟阈值路由:从库延迟超过N秒后,自动摘除该从库
- 等GTID:写操作获取GTID,读操作等待从库追上该GTID
- 最终一致性容忍:对于列表页、推荐等场景,接受短暂不一致
5. 慢查询治理体系
5.1 慢查询治理流程
📋 慢查询治理四阶段
| 阶段 | 目标 | 手段 | 工具 |
|---|---|---|---|
| 1. 发现 | 被动发现 + 主动监控 | 慢查询日志(slow_query_log)、APM告警 | slow_query_log、pt-query-digest、Prometheus |
| 2. 分析 | 定位根因、评估影响 | EXPLAIN分析、索引使用率、锁等待 | EXPLAIN、SHOW PROFILE、sys.schema_* |
| 3. 优化 | SQL改写、索引调整、架构变更 | 加索引、拆分大查询、去除非必要JOIN | 数据库管理工具、代码Review |
| 4. 防护 | 上线前拦截、容量规划 | SQL审核平台、索引自动推荐、压测基线 | Yearning、Archery、SOP |
5.2 慢查询日志配置
⚙️ MySQL 慢查询配置最佳实践
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 设置阈值(超过1秒计入慢查询)
SET GLOBAL long_query_time = 1;
-- 记录未使用索引的查询(开发环境开启,生产慎用)
SET GLOBAL log_queries_not_using_indexes = OFF;
-- 记录管理语句
SET GLOBAL log_slow_admin_statements = ON;
-- 慢查询分析工具
-- pt-query-digest /var/log/mysql/slow.log > slow_report.txt
-- 查询Top N慢查询
SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;
📌 数据库调优黄金法则
- 80%的性能问题来自索引不当——先优化SQL和索引,再考虑硬件升级
- 不要在生产环境直接执行 ALTER TABLE——使用 pt-online-schema-change 或 gh-ost
- 连接池大小不是越大越好——MySQL建议单实例总连接数控制在2000以内
- 读写分离不是银弹——先确认读流量是否真的占大多数,再看主从延迟是否可接受