Mysql中慢 SQL 优化实战:减少不必要的 JOIN 关联

Mysql中慢 SQL 优化实战:减少不必要的 JOIN 关联

一、问题背景

线上接口查询收货单的出库物流信息时出现慢 SQL。该 SQL 通过三表 JOIN(收货单主表 → 物流记录表 → 物流节点明细表)查询物流轨迹数据,其中收货单主表数据量超过 1200 万行。

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、优化流程

1. 定位慢 SQL

通过慢查询日志或 APM 监控系统,获取到慢 SQL 的完整文本、执行时间和调用链路(trace-id)。

2. 分析 SQL 结构

原始 SQL 的执行结构:

复制代码
receiving_goods_record (主表, 1200w+)
    INNER JOIN store_receiving_logistics_record (物流记录表)
        LEFT JOIN store_receiving_logistics_record_item (物流节点明细表)

3. 识别冗余 JOIN

sql 中直接可以从表中获取数据,并且主表中的 record_code 字段是有索引的。分析发现 SELECT 中从主表取的字段(record_codeorder_code)在物流记录表中同样存在且数据一致,关联主表纯属冗余。

4. 数据一致性验证

通过 SQL 查询验证两表字段的数据一致性(不一致记录数 = 0),确认去掉 JOIN 不会导致数据差异。

5. 修改并测试

修改 Mapper XML,去掉冗余 JOIN,本地启动服务验证接口正常返回。

三、核心技术点

3.1 EXPLAIN 执行计划

sql 复制代码
EXPLAIN SELECT ... FROM table_a a
INNER JOIN table_b b ON a.id = b.a_id
WHERE a.code = 'xxx';

关键指标:

字段 含义 关注点
type 访问类型 ALL(全表扫描) → ref(索引) → const(常量)
key 实际使用的索引 NULL 表示未走索引
rows 预估扫描行数 数值越小越好
Extra 额外信息 Using filesort / Using temporary 需关注

3.2 索引命中原则

sql 复制代码
-- 单列索引:WHERE 条件精确匹配时命中
CREATE INDEX idx_record_code ON logistics_record(record_code);
SELECT * FROM logistics_record WHERE record_code = 'xxx'; -- 命中

-- 联合索引:遵循最左前缀原则
CREATE INDEX idx_member_code ON logistics_record(member_id, order_code);
SELECT * FROM logistics_record WHERE member_id = 1;                    -- 命中
SELECT * FROM logistics_record WHERE member_id = 1 AND order_code = 'x'; -- 命中
SELECT * FROM logistics_record WHERE order_code = 'x';                 -- 不命中

3.3 JOIN 的代价

每增加一个 JOIN,MySQL 需要:

  1. 根据驱动表的结果集逐行去被驱动表查找匹配行
  2. 如果被驱动表没有合适索引,会产生全表扫描
  3. 结果集膨胀 → 内存消耗增大 → 排序代价增大

四、优化原理

4.1 减少 JOIN 表数量

这是本次优化的核心原理。当你发现:

SELECT 中从 A 表取的字段,在 B 表中也有相同的数据(冗余存储/数据同步字段)

就可以去掉对 A 表的关联,直接从 B 表取字段。

优化前(三表)

sql 复制代码
SELECT a.name, b.detail, c.item
FROM big_table a                    -- 1000w 行
INNER JOIN medium_table b ON a.id = b.a_id
LEFT JOIN small_table c ON b.id = c.b_id
WHERE a.code = 'xxx';

优化后(两表)

sql 复制代码
SELECT b.name, b.detail, c.item    -- name 字段从 b 表直接取
FROM medium_table b
LEFT JOIN small_table c ON b.id = c.b_id
WHERE b.code = 'xxx';              -- b 表的 code 有索引

4.2 驱动表选择

MySQL 的 JOIN 执行顺序由优化器决定,但通常:

  • 小表驱动大表性能更优
  • WHERE 条件能快速过滤的表适合作为驱动表

五、常见的慢 SQL 优化方案

方案一:消除冗余 JOIN(本次使用)

适用场景:JOIN 的表只是为了取几个字段,而这些字段在其他已关联的表中也有。

sql 复制代码
-- 优化前:三表 JOIN 只为取 order 表的 order_no
SELECT o.order_no, d.product_name, d.qty
FROM orders o
INNER JOIN order_details d ON o.id = d.order_id
WHERE o.id = 12345;

-- 优化后:order_details 表本身冗余存储了 order_no
SELECT d.order_no, d.product_name, d.qty
FROM order_details d
WHERE d.order_id = 12345;

方案二:添加合适索引

适用场景:WHERE 条件或 JOIN 条件的字段没有索引。

sql 复制代码
-- 慢查询:order_code 无索引,全表扫描
SELECT * FROM shipment_detail
WHERE member_id = 1179109 AND order_code = 'ER.240418.000673';

-- 添加联合索引
CREATE INDEX idx_member_order ON shipment_detail(member_id, order_code);

方案三:改写子查询为 JOIN

适用场景:IN 子查询在大数据量下性能差。

sql 复制代码
-- 优化前:子查询
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE level = 'VIP');

-- 优化后:改写为 JOIN
SELECT o.* FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
WHERE c.level = 'VIP';

方案四:分页优化(延迟关联)

适用场景:深分页 LIMIT offset, size 越往后越慢。

sql 复制代码
-- 优化前:LIMIT 100000, 10 需要扫描 100010 行
SELECT * FROM orders ORDER BY id DESC LIMIT 100000, 10;

-- 优化后:先定位 ID 区间,再取数据
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY id DESC LIMIT 100000, 1)
ORDER BY id DESC LIMIT 10;

方案五:大表数据归档

适用场景:因为某表数据越来越多,导致执行 sql 越来越慢,因为 sql 暂时无法优化,所以先按照备份数据的方式处理,把指定之间之前的数据全部备份到备份表中,一年一张表。

sql 复制代码
-- 按年归档:将历史数据迁移到归档表
INSERT INTO xxx_2025 SELECT * FROM xxx WHERE account_time < '2026-01';
DELETE FROM xxx WHERE account_time < '2026-01';

-- 或使用 pt-archiver 等工具无锁归档

方案六:为大表新增索引(Online DDL)

适用场景:因为只使用创建时间查询数据,所以需要增加无锁变更,把 create_time 增加索引,表数据共 5000w。

sql 复制代码
-- MySQL 5.6+ 支持 Online DDL,不阻塞 DML
ALTER TABLE store_inbound_master
ADD INDEX idx_create_time(create_time),
ALGORITHM=INPLACE, LOCK=NONE;

-- 或使用 gh-ost / pt-online-schema-change 进行无锁变更

六、优化效果评估

维度 优化前 优化后
JOIN 表数量 3 表 2 表
驱动表数据量 1200w+ (receiving_goods_record) 470w (store_receiving_logistics_record)
索引使用 orm.record_code 走唯一索引后再 JOIN sdlr.record_code 直接走索引
网络/内存开销 需传输主表行数据 无冗余数据传输

七、总结检查清单

在遇到慢 SQL 时,按以下顺序排查:

  1. 是否有不必要的 JOIN? --- 分析 SELECT 字段来源,确认是否可以从已有表获取
  2. WHERE/JOIN 条件是否走索引? --- 用 EXPLAIN 确认,必要时添加索引
  3. 是否有子查询可改写? --- IN 子查询改为 JOIN
  4. 结果集是否过大? --- 增加过滤条件、分页优化
  5. 表数据量是否可以缩减? --- 历史数据归档、分表
相关推荐
一个天蝎座 白勺 程序猿1 小时前
SQL Server数据迁移金仓性能优化实战:从慢SQL到系统瓶颈,一套组合拳打下来
sql·oracle·性能优化·kingbasees
.柒宇.2 小时前
运维常见面试题_02_数据库
运维·数据库·sql·面试
️学习的小王2 小时前
二级MySQL PHP综合应用题完整考点+历年真题风格练习题
android·mysql·php
Dxy123931021611 小时前
Python 如何使用 MySQL 的事务
python·mysql
xiaoye-duck16 小时前
MySQL 表约束全解:从基础约束到外键关联规则
数据库·mysql
冷凝娇18 小时前
【MySQL】2026总结(二)
数据库·mysql
xbgRS19 小时前
spring整合mybaits
java·spring·mybatis
wWYy.19 小时前
Mysql:Read View
数据库·mysql
—Miss. Z—20 小时前
存储过程、用户定义函数、触发器和游标
数据库·mysql