一、执行计划分析与索引原理
数据库优化的起点是理解查询的执行路径。EXPLAIN 是诊断 MySQL 性能问题最重要的一把钥匙。
1.1 EXPLAIN 深度解读
EXPLAIN FORMAT=JSON SELECT ...
-- 关键字段解析:
-- type: ref / range / index / ALL(从优到劣)
-- key: 实际使用的索引
-- rows: 估算扫描行数(越少越好)
-- Extra: Using filesort / Using temporary(危险信号)
-- Using index condition (ICP) — 索引下推,优)
-- 通过 optimizer_trace 查看代价模型
SET optimizer_trace="enabled=on";
SELECT ...;
SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
1.2 索引内部结构:B+Tree vs Hash
MySQL InnoDB 采用 B+Tree,聚簇索引叶子节点存储完整行数据,二级索引叶子节点存储主键值。理解这个模型才能写出高效的查询。
- 聚簇索引:数据文件本身就是索引文件,叶子节点包含完整数据行,按主键排序
- 二级索引:叶子节点存储主键值,查询需要回表(先查索引再查主键索引)
- 覆盖索引:查询列全部在索引中,无需回表,Using index
- 最左前缀原则:联合索引 (A,B,C) 可命中 A、(A,B)、(A,B,C),无法命中 (B,C)
1.3 索引失效的常见场景
-- ❌ 索引失效场景
SELECT * FROM orders WHERE YEAR(create_time) = 2026; -- 函数运算
SELECT * FROM users WHERE name LIKE '%张%'; -- 左通配符
SELECT * FROM products WHERE price > 100 AND seller_id = 5; -- 范围断链
SELECT * FROM t WHERE id + 1 = 100; -- 表达式
SELECT * FROM t WHERE status IN ('a','b') OR city = 'BJ'; -- OR破坏
-- ✅ 正确做法
-- 用范围列放在联合索引末尾
ALTER TABLE orders ADD INDEX idx_seller_time(seller_id, create_time);
SELECT * FROM orders WHERE seller_id = 5 AND create_time BETWEEN '2026-01-01' AND '2026-12-31';
二、SQL 语句优化
2.1 分页优化(深度分页)
当 OFFSET 很大时( OFFSET 1000000),MySQL 需要先扫描跳过前100万行,极其低效:
-- ❌ 深度分页:扫描100万行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 方案A:游标分页(最优,适用于有单调递增主键)
SELECT * FROM orders WHERE id > #{lastId} ORDER BY id LIMIT 20;
-- ✅ 方案B:子查询延迟关联
SELECT * FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 20
) t ON o.id = t.id;
-- ✅ 方案C:记录总数缓存(前端只展示页码,不查总数)
SELECT SQL_CALC_FOUND_ROWS * FROM orders LIMIT 1000000, 20;
SELECT FOUND_ROWS(); -- 单独查询(MySQL 8.0.17废弃,推荐方案B)
2.2 JOIN 优化与 MRR
-- MRR(Multi-Range Read):将随机磁盘读转化为顺序读
SET optimizer_switch='mrr=on,mrr_cost_based=off';
-- 小表驱动大表:EXPLAIN 查看 join_type
-- 当 t1(100行) JOIN t2(1000万行) 时:
SELECT * FROM t1 JOIN t2 ON t1.id = t2.t1_id;
-- t1 为驱动表,t2 为被驱动表(用索引)
-- 原则:驱动表尽可能小,被驱动表关联列必须有索引
-- ✅ Batched Key Access(BAK):一次批量获取多行
SET optimizer_switch='batched_tree_access=on';
-- Java 中使用 JPA 写高效 JOIN 查询
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.userId = :uid")
List findOrdersWithItems(@Param("uid") Long userId);
三、连接池调参与监控
3.1 HikariCP 核心参数调优
# application.yml
spring:
datasource:
hikari:
# 连接池大小 = (核心数 * 2) + 有效磁盘数
maximum-pool-size: 20
minimum-idle: 5
# 连接最大生命周期(短于MySQL wait_timeout)
max-lifetime: 1800000 # 30分钟
connection-timeout: 30000 # 获取连接超时
idle-timeout: 600000 # 空闲超时
# 验证连接有效性
connection-test-query: SELECT 1
# 监控
pool-name: OrderHikariPool
# 监控指标(JMX/Micrometer)
# - Active Connections:活跃连接数,接近 max 时说明瓶颈
# - Idle Connections:空闲连接,接近 0 说明连接不够用
# - Threads Awaiting Connection:等待获取连接的线程数
# - Connection Timeout Count:连接超时次数
3.2 Druid 连接池防泄漏
// Druid 配置:连接泄漏检测
DruidDataSource ds = new DruidDataSource();
ds.setRemoveAbandoned(true);
ds.setRemoveAbandonedTimeout(180); // 180秒未归还则强制回收
ds.setLogAbandoned(true); // 打印堆栈追踪泄漏源头
// 配合 DruidStatFilter 做 SQL 监控
@WebFilter(urlPatterns = "/*", initParams = {
@WebInitParam(name = "profileEnable", value = "true")
})
public class DruidStatFilter extends DruidStatFilter {}
// 获取慢 SQL 记录
List slowSqls = ds.getProperty().getSlowSQLRecords();
四、分库分表与 ShardingSphere
4.1 分片策略选型
| 策略 | 适用场景 | 数据均匀性 |
|---|---|---|
| Hash分片 | 用户ID/订单ID分片 | ✅ 均匀 |
| 按时间分片 | 日志、流水、订单 | ⚠️ 冷热不均 |
| Range分片 | ID范围有序访问 | ❌ 易倾斜 |
| 多字段自定义 | 复杂查询路由 | ✅ 可控 |
4.2 ShardingSphere-JDBC 配置
-- 分表 DDL
CREATE TABLE orders_2026 (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2),
INDEX idx_user_id(user_id),
INDEX idx_create_time(create_time)
) ENGINE=InnoDB;
-- Java ShardingSphere 配置
ShardingRuleConfiguration config = new ShardingRuleConfiguration();
config.getTables().add(orderTableRuleConfig());
TableRuleConfiguration orderRule = new TableRuleConfiguration("t_order", "ds_${0..1}.t_order_${0..15}");
orderRule.setDatabaseShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id",
new PreciseShardingValueAlgorithm() {
@Override
public String doSharding(String targetNames, PreciseShardingValue value) {
long id = Long.parseLong(value.getValue().toString());
return "ds_" + (id % 2);
}
}));
orderRule.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("create_time",
new RangeShardingValueAlgorithm() {
@Override
public Collection doSharding(String targetNames,
RangeShardingValue value) {
// 按年月路由到对应分表
}
}));
// 分布式唯一ID:Snowflake + 时钟回拨处理
public class SnowflakeIdGenerator {
private final long twepoch = 1609459200000L; // 2021-01-01
private final long workerIdBits = 5;
private final long datacenterIdBits = 5;
private final long sequenceBits = 12;
// 解决时钟回拨:等待补偿或拒绝服务
}
五、线上 SQL 质量门禁
-- 质量门禁规则(MyBatis-Plus 拦截器实现)
@Intercepts({
@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class})
})
public class SQLAuditInterceptor implements Interceptor {
@Override
public Object intercept(Invocation inv) throws Throwable {
StatementHandler sh = (StatementHandler) PluginUtil.getTarget(inv.getTarget());
String sql = sh.getBoundSql().getSql().toLowerCase();
// 规则1:禁止 SELECT *
if (sql.contains("select *")) {
throw new IllegalSQLException("禁止 SELECT *,必须指定字段");
}
// 规则2:扫描行数阈值
RoutingConnection rc = (RoutingConnection) getConnection();
ExplainResult er = rc.explain(sql);
if (er.getEstimatedRows() > 10000) {
throw new IllegalSQLException("估算扫描行数 " + er.getEstimatedRows() + " 超过阈值");
}
// 规则3:禁止无 LIMIT 的全表 DELETE/UPDATE
if ((sql.contains("delete") || sql.contains("update"))
&& !sql.contains("where") && !sql.contains("limit")) {
throw new IllegalSQLException("DML必须带WHERE或LIMIT");
}
return inv.proceed();
}
}