下载工作台
Java 数据库实战

EXPLAIN 与查询优化

试读上半部分 · 解锁后可读全文

第 14 章 · EXPLAIN 与查询优化

本章目标:掌握 MySQL EXPLAIN / EXPLAIN ANALYZE 执行计划解读;配置 慢查询日志 与 JDBC/JPA 慢 SQL 观测;识别 索引失效 典型场景;设计 覆盖索引 优化商品列表与报表;在 shop-db / db-demo 上完成优化清单;对照 spring-boot-web ch17 的 N+1、连接池与 Hibernate 统计思路。

学时建议:4~5 小时(含 2 小时 EXPLAIN 实验)

前置:完成 java-database ch05 索引设计、ch08~ch13 JDBC/JPA/Repository;建议浏览 spring-boot-web ch17 性能章节。


14.1 场景说明:商品列表 P99 飙升

shop-dbdb-demo 共用贤紫优选商品域模型,核心字段约定:

字段类型说明
slugVARCHAR(128) UNIQUEURL 标识,如 java-handbook
is_publishedTINYINT(1)是否上架
priceBIGINT价格(分),禁止 float

运营反馈:商品列表接口 P99 从 40ms 升到 800ms。JPA 已用 JOIN FETCH,但 MySQL 仍出现 type=ALL 全表扫描。

HTTP GET /api/v1/products?page=1
        │
        ▼
JPA / JDBC ──► EXPLAIN 审计
        │
        ├── 慢查询 log(>200ms)
        ├── 索引是否命中
        └── N+1:循环内二次 SELECT?(spring-boot-web ch17)

优化顺序:度量(慢日志 + EXPLAIN)→ 索引 / 覆盖索引 → 改写 SQL → ORM 批量加载 → 架构扩展(ch15)。


14.2 EXPLAIN 基础

db-demo 连接 MySQL 后执行:

EXPLAIN SELECT id, slug, name, price, is_published
FROM products
WHERE is_published = 1
ORDER BY id DESC
LIMIT 20;

MySQL 8.0 推荐同时看 EXPLAIN ANALYZE(真实耗时):

EXPLAIN ANALYZE
SELECT id, slug, name, price, is_published
FROM products
WHERE is_published = 1
ORDER BY id DESC
LIMIT 20;

14.2.1 核心列解读

含义健康值
idSELECT 内子查询序号数字
select_type查询类型SIMPLE 为主
table访问表名
type访问方式const/ref/range;避免 ALL
possible_keys可能用到的索引非空为佳
key实际索引与预期一致
key_len索引使用字节数联合索引越完整越大
ref索引比较列const/列名
rows预估扫描行数越小越好
filtered按条件过滤比例高为佳
Extra附加信息见下表

14.2.2 Extra 常见值

Extra含义行动
Using index覆盖索引,不回表✅ 理想
Using where存储引擎过滤正常
Using filesort额外排序考虑索引含 ORDER BY 列
Using temporary临时表GROUP BY / DISTINCT 需优化
Using index conditionICP 索引下推8.0 常见,一般 OK

14.2.3 type 优先级(从好到差)

system > const > eq_ref > ref > range > index > ALL

教学验收:列表查询至少 range,点查 slug 应 const/ref


14.3 慢查询日志

14.3.1 MySQL 服务端配置

my.cnf 或 Docker Compose 挂载:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.2
log_queries_not_using_indexes = 1

Docker 开发环境示例:

# docker-compose.dev.yml 片段
services:
  mysql:
    image: mysql:8.0
    command:
      - --slow-query-log=1
      - --long-query-time=0.2
      - --log-queries-not-using-indexes=1
    ports:
      - "3306:3306"

14.3.2 分析工具

# pt-query-digest(选修)
pt-query-digest /var/log/mysql/slow.log | head -40

# MySQL 8 内置
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

14.3.3 JDBC 层慢 SQL 日志

HikariCP + 自定义 DataSource 包装或使用 p6spy(spring-boot-web ch17 同款思路):

<!-- pom.xml 选修 -->
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>
# spy.properties
driverlist=com.mysql.cj.jdbc.Driver
logMessageFormat=com.p6spy.engine.spy.appender.Slf4JLogger
executionThreshold=200
# spring-boot-web application-dev.yml 对照
spring:
  datasource:
    url: jdbc:p6spy:mysql://127.0.0.1:3306/shop_db

纯 Java shop-db 脚本可在 ReportDailyMain 中记录耗时:

long t0 = System.nanoTime();
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    // ...
} finally {
    long ms = (System.nanoTime() - t0) / 1_000_000;
    if (ms > 200) log.warn("slow sql {} ms: {}", ms, sql);
}

14.4 shop-db 索引设计复习

14.4.1 推荐索引(ch05/ch07)

-- slug 点查(详情页)
UNIQUE KEY uk_products_slug (slug);

-- 已发布列表 + 排序
KEY idx_products_published_id (is_published, id DESC);

-- 分类下已发布商品
KEY idx_products_category_published (category_id, is_published, id DESC);

-- 订单报表
KEY idx_orders_created (created_at);
KEY idx_orders_user_created (user_slug, created_at DESC);

14.4.2 覆盖索引示例

列表只需 id, slug, name, price, is_published

CREATE INDEX idx_products_list_cover
ON products (is_published, id DESC, slug, name, price);

EXPLAIN 期望 Extra=Using index

14.4.3 失效场景清单

SQL 写法问题改写
WHERE YEAR(created_at)=2026函数破坏索引created_at >= '2026-01-01' AND < '2027-01-01'
WHERE slug LIKE '%handbook'左模糊全文索引或 ES(扩展)
OR is_published=1 OR price>10000可能全表UNION 两条索引查询
隐式类型转换 slug=123列变字符串比较参数类型匹配
SELECT * + 宽行无法覆盖只 SELECT 必要列

14.5 典型 SQL 优化实战

14.5.1 已发布商品分页

优化前(filesort + 全表):

SELECT * FROM products WHERE is_published = 1 ORDER BY id DESC LIMIT 20;

优化后(命中 idx_products_published_id):

EXPLAIN SELECT id, slug, name, price, is_published
FROM products
WHERE is_published = 1
ORDER BY id DESC
LIMIT 20;

验收:type=rangerefkey=idx_products_published_id

14.5.2 slug 点查

EXPLAIN SELECT slug, price, is_published, name
FROM products
WHERE slug = 'java-handbook';

期望:type=constkey=uk_products_slugrows=1

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。