在日常MySQL开发中,JOIN关联查询是我们最常用的语法之一,但80%以上的慢SQL性能问题,都源于对JOIN底层执行逻辑的认知缺失。很多开发者遇到JOIN慢查询,第一反应就是“加索引”,但为什么加了索引还是慢?为什么同样的JOIN写法,数据量上来后性能天差地别?MySQL优化器到底是怎么选择关联顺序和执行算法的?
一、JOIN查询的核心基础:驱动表与被驱动表
在拆解算法之前,我们必须先搞懂两个核心概念:驱动表与被驱动表,这是理解所有JOIN算法的前提。
JOIN关联查询的本质,是从一张表中循环取出数据,拿着关联字段去另一张表中匹配符合条件的记录,最终合并结果集。其中:
- 驱动表:外层循环的表,也叫主表,是数据遍历的起点
- 被驱动表:内层循环的表,也叫匹配表,是每次循环匹配的对象
MySQL优化器选择驱动表的核心原则是:在满足业务逻辑的前提下,优先选择经过WHERE条件过滤后,结果集行数最少的表作为驱动表。这里的“小表”不是指表的总行数,而是过滤后的结果集大小,这是90%的开发者都会踩的误区。
举个例子:A表有100万行数据,WHERE过滤后剩余100行;B表有1万行数据,WHERE过滤后剩余500行。此时A表就是优化器眼中的“小表”,会被选为驱动表。
对于LEFT JOIN,左表默认是驱动表;RIGHT JOIN右表默认是驱动表;INNER JOIN则由优化器根据过滤结果集大小自动选择驱动表。
二、Nested-Loop Join(嵌套循环连接)
Nested-Loop Join(简称NLJ)是MySQL最基础的JOIN算法,也是索引场景下的首选算法,分为两种实现:Simple Nested-Loop Join与Index Nested-Loop Join。
2.1 Simple Nested-Loop Join(简单嵌套循环连接)
这是最原始的嵌套循环实现,逻辑等价于两层for循环,伪代码如下:
for(驱动表的每一行数据 R){
for(被驱动表的每一行数据 S){
if(R和S满足关联条件){
合并R和S,加入结果集;
}
}
}
该算法的时间复杂度为O(M*N),M为驱动表行数,N为被驱动表行数。每次内层循环都要全表扫描被驱动表,性能极差,MySQL优化器从未在生产场景中使用该算法,仅作为理论基础存在。
2.2 Index Nested-Loop Join(索引嵌套循环连接)
这是Simple Nested-Loop Join的优化版本,也是MySQL索引场景下的核心实现,核心优化点是:被驱动表的关联字段必须建立索引,内层循环通过索引快速匹配数据,替代全表扫描。
2.2.1 底层执行流程
完整执行步骤:
- 优化器根据过滤条件,选定结果集更小的表作为驱动表,另一张为被驱动表
- 遍历驱动表,取出符合WHERE条件的每一行数据
- 从该行数据中提取关联字段的值,去被驱动表的关联索引中查找匹配记录
- 若找到匹配记录,合并两行数据加入结果集;若未找到,跳过当前行
- 重复步骤2-4,直到驱动表遍历完成,返回最终结果集
2.2.2 性能核心优势
索引嵌套循环的性能提升,本质是把内层循环的全表扫描,替换成了索引查找:
- 若关联字段是主键/唯一索引,单次查找的时间复杂度为O(1),整体复杂度接近O(M)
- 若关联字段是普通二级索引,单次查找的时间复杂度为O(logN),整体复杂度为O(M*logN)
- 若查询的列全部包含在索引中(覆盖索引),可以避免回表操作,性能再提升一个量级
2.2.3 SQL示例
首先创建测试表并插入测试数据,以下SQL可直接在MySQL 8.0中执行:
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(64) NOT NULL COMMENT '用户名',
`age` int NOT NULL DEFAULT '0' COMMENT '年龄',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_name` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`order_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态 0-待付款 1-已付款 2-已取消',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';
-- 插入测试数据:1000条用户数据
INSERT INTO t_user (user_name, age)
WITH RECURSIVE num AS (
SELECT 1 AS n UNION ALL SELECT n+1 FROM num WHERE n < 1000
)
SELECT CONCAT('user_', n), FLOOR(RAND()*50)+18 FROM num;
-- 插入测试数据:10000条订单数据
INSERT INTO t_order (order_no, user_id, order_amount, order_status)
WITH RECURSIVE num AS (
SELECT 1 AS n UNION ALL SELECT n+1 FROM num WHERE n < 10000
)
SELECT CONCAT('ORD_', LPAD(n, 10, '0')), FLOOR(RAND()*1000)+1, ROUND(RAND()*1000,2), FLOOR(RAND()*3) FROM num;
执行Index Nested-Loop Join查询,并查看执行计划:
EXPLAIN
SELECT u.id, u.user_name, o.order_no, o.order_amount
FROM t_user u
INNER JOIN t_order o ON u.id = o.user_id
WHERE u.age < 30;
执行计划核心结果:
| id | select_type | table | type | key | Extra |
| 1 | SIMPLE | u | ALL | NULL | Using where |
| 1 | SIMPLE | o | ref | idx_user_id | NULL |
执行计划解读:
- id列值相同,代表执行顺序为从上到下,u表为驱动表,o表为被驱动表
- o表的type列为ref,代表通过普通二级索引进行等值匹配,符合NLJ算法特征
- Extra列无Using join buffer,说明未使用缓冲区,完全通过索引匹配,是标准的Index Nested-Loop Join执行逻辑
2.2.4 适用场景与优化方案
适用场景:
- 被驱动表的关联字段可以建立高效索引(主键、唯一索引或高选择性二级索引)
- 驱动表经过WHERE过滤后的结果集行数较小
- 等值连接与非等值连接均支持,兼容性最好
核心优化方案:
- 被驱动表的关联字段优先使用主键/唯一索引,减少回表开销
- 针对查询列构建覆盖索引,避免回表操作。示例中可创建联合索引:
ALTER TABLE t_order ADD INDEX idx_user_id_include (user_id, order_no, order_amount);
再次执行查询,执行计划Extra列会出现Using index,代表使用了覆盖索引,性能大幅提升。 3. 严格控制驱动表的结果集大小,WHERE条件优先过滤驱动表数据,减少外层循环次数 4. 避免在关联字段上使用函数、表达式,导致索引失效
三、Block Nested-Loop Join(块嵌套循环连接)
当被驱动表的关联字段没有可用索引时,MySQL不会使用性能极差的Simple Nested-Loop Join,而是使用Block Nested-Loop Join(简称BNLJ),核心优化点是引入Join Buffer连接缓冲区,减少被驱动表的全表扫描次数。
3.1 底层执行原理
BNLJ算法的核心逻辑是:不再逐行读取驱动表数据,而是将驱动表的多行数据分块加载到Join Buffer中,然后全表扫描被驱动表,将每一行数据与Join Buffer中的所有数据一次性匹配,大幅减少被驱动表的扫描次数。
3.1.1 执行流程
完整执行步骤:
- 优化器选定驱动表与被驱动表,申请Join Buffer内存空间,缓冲区大小由
join_buffer_size参数控制,默认256KB - 读取驱动表中符合WHERE条件的数据,分块加载到Join Buffer中,直到缓冲区填满
- 全表扫描被驱动表,取出每一行数据,与Join Buffer中的所有数据一次性匹配关联条件
- 匹配成功的行,合并后加入结果集
- 当前Join Buffer中的数据全部匹配完成后,清空缓冲区,继续加载驱动表的下一批数据
- 重复步骤2-5,直到驱动表所有数据块匹配完成,返回最终结果集
3.1.2 性能核心逻辑
BNLJ算法的性能提升,本质是通过批量匹配减少被驱动表的全表扫描次数:
- 若驱动表过滤后有M行数据,Join Buffer最多可存储K行数据,那么被驱动表的全表扫描次数从M次降低到M/K次
- Join Buffer中只存储查询需要的列,而非整行数据,查询的列越少,缓冲区可存储的行数越多,扫描次数越少
- 该算法仅需驱动表过滤一次,被驱动表扫描M/K次,相比Simple Nested-Loop Join性能提升显著
3.2 SQL示例
删除被驱动表的关联索引,触发BNLJ算法,执行以下SQL:
-- 删除关联索引
ALTER TABLE t_order DROP INDEX idx_user_id;
-- 查看执行计划
EXPLAIN
SELECT u.id, u.user_name, o.order_no, o.order_amount
FROM t_user u
INNER JOIN t_order o ON u.id = o.user_id
WHERE u.age < 30;
执行计划核心结果:
| id | select_type | table | type | key | Extra |
| 1 | SIMPLE | u | ALL | NULL | Using where |
| 1 | SIMPLE | o | ALL | NULL | Using where; Using join buffer (Block Nested Loop) |
执行计划解读:
- 驱动表u和被驱动表o均为全表扫描(type列为ALL),无可用索引
- Extra列出现
Using join buffer (Block Nested Loop),代表MySQL使用了BNLJ算法 - 注:MySQL 8.0.18+版本默认优先使用Hash Join,需执行
SET optimizer_switch = 'hash_join=off';关闭Hash Join后,才会触发BNLJ算法
3.3 适用场景与优化方案
适用场景:
- 被驱动表的关联字段无法建立可用索引
- MySQL 5.7及以下版本,无索引的关联查询
- 非等值连接场景,无法使用Hash Join
- 驱动表过滤后的结果集较小,可完全放入Join Buffer中
核心优化方案:
- 优先给被驱动表的关联字段建立合适的索引,转为Index Nested-Loop Join,这是最根本的优化手段
- 禁止使用
SELECT *,只查询业务需要的列,提升Join Buffer可存储的行数,减少被驱动表扫描次数 - 合理调整
join_buffer_size参数,避免过小导致分块过多,同时注意:每个连接的每个JOIN都会分配一个独立的Join Buffer,多表JOIN会分配多个缓冲区,不可设置过大(建议单缓冲区不超过1MB) - 严格控制驱动表的结果集大小,减少分块数量,降低被驱动表扫描次数
四、Hash Join(哈希连接)
Hash Join是MySQL 8.0.18版本正式引入的JOIN算法,专门用于优化无索引的等值连接场景,目前已成为MySQL 8.0+无索引等值连接的默认算法,性能远超传统的BNLJ算法。
4.1 底层执行原理
Hash Join的核心逻辑是:基于哈希表实现,分为构建阶段(Build Phase)和探测阶段(Probe Phase)两个核心阶段,用哈希查找替代嵌套循环匹配,大幅提升大数据量下的关联性能。
4.1.1 执行流程
完整执行步骤:
- 优化器选定两张表中结果集更小的表作为build表(构建表),另一张作为probe表(探测表),对应NLJ中的驱动表与被驱动表
- 构建阶段:遍历build表,读取符合WHERE条件的每一行数据,对关联字段计算哈希值,以哈希值为key,行数据为value,在Join Buffer中构建内存哈希表,哈希冲突采用链地址法处理
- 探测阶段:遍历probe表,读取每一行数据,对关联字段使用相同的哈希函数计算哈希值,到内存哈希表中查找对应的bucket
- 若找到匹配的bucket,遍历bucket中的链表,校验关联条件是否完全匹配,匹配成功则合并行加入结果集;未找到则跳过当前行
- 重复步骤3-4,直到probe表遍历完成,返回最终结果集
- 若build表数据量超过Join Buffer大小,MySQL会自动采用分区Hash Join,将两张表按关联字段哈希值分成多个磁盘分区,逐个分区执行Hash Join,避免内存溢出
4.1.2 性能核心优势
Hash Join的性能碾压BNLJ的核心原因,是匹配逻辑的本质差异:
- BNLJ的匹配是嵌套循环比对,时间复杂度为O(M*N/K)
- Hash Join的匹配是哈希查找,单次查找时间复杂度接近O(1),整体复杂度为O(M+N)
- 大数据量无索引场景下,Hash Join的性能比BNLJ高出10倍以上
- 哈希表仅构建一次,probe表仅需扫描一次,相比BNLJ的多次扫描优势显著
4.2 SQL示例
开启Hash Join(默认开启),删除关联索引,执行以下SQL:
-- 开启Hash Join
SET optimizer_switch = 'hash_join=on';
-- 删除关联索引(若已删除可跳过)
ALTER TABLE t_order DROP INDEX IF EXISTS idx_user_id;
-- 查看执行计划
EXPLAIN
SELECT u.id, u.user_name, o.order_no, o.order_amount
FROM t_user u
INNER JOIN t_order o ON u.id = o.user_id
WHERE u.age < 30;
执行计划核心结果:
| id | select_type | table | type | key | Extra |
| 1 | SIMPLE | u | ALL | NULL | Using where |
| 1 | SIMPLE | o | ALL | NULL | Using where; Using join buffer (Hash Join) |
执行计划解读:
- 两张表均为全表扫描,无可用索引
- Extra列出现
Using join buffer (Hash Join),代表MySQL使用了Hash Join算法 - 优化器选择结果集更小的u表作为build表,o表作为probe表,符合Hash Join的执行逻辑
4.3 适用场景与优化方案
适用场景:
- 等值连接场景,仅支持等值关联条件,不支持非等值连接、LIKE模糊匹配等
- 被驱动表无可用索引,大数据量关联查询
- MySQL 8.0.18及以上版本,是该场景下的默认首选算法
- 两张表过滤后的结果集差异较大,小表可完全放入内存构建哈希表
核心优化方案:
- 优先保证关联条件为等值连接,确保Hash Join可用
- 严格控制build表的结果集大小,尽量让哈希表完全放入内存,避免磁盘分区带来的IO开销
- 合理调整
join_buffer_size参数,为哈希表提供足够的内存空间,同时避免全局设置过大导致内存溢出 - 避免在关联字段上使用函数、表达式,导致哈希值计算不一致,无法匹配
- 若build表过大,可通过WHERE条件拆分查询,分批执行,避免磁盘分区
五、三大JOIN算法核心对比与易混淆点澄清
5.1 核心参数对比表
| 算法类型 | 核心依赖 | 适用场景 | 连接类型支持 | 时间复杂度 | 核心优势 | 核心劣势 |
| Index Nested-Loop Join | 被驱动表关联字段索引 | 被驱动表有高效索引,驱动表结果集小 | 等值/非等值连接均支持 | 接近O(M) | 性能稳定,内存开销极小,无全表扫描 | 强依赖索引,无索引时性能极差 |
| Block Nested-Loop Join | Join Buffer缓冲区 | 无可用索引,MySQL 5.7及以下,非等值连接 | 等值/非等值连接均支持 | O(M*N/K) | 减少被驱动表全表扫描次数,无索引时可用 | 仍需全表扫描,大数据量性能差,内存开销随缓冲区增大而增加 |
| Hash Join | 哈希表与Join Buffer | 无可用索引的等值连接,MySQL 8.0.18+,大数据量关联 | 仅支持等值连接 | 接近O(M+N) | 大数据量无索引场景性能远超BNLJ,内存可控 | 有哈希表构建开销,仅支持等值连接,小数据量场景优势不明显 |
5.2 高频易混淆点澄清
- 误区:小表驱动大表就是表总行数少的作为驱动表澄清:驱动表的选择核心是WHERE条件过滤后的结果集行数,而非表的总行数。即使表有100万行,过滤后只剩100行,也会被选为驱动表。
- 误区:Hash Join一定比BNLJ快澄清:当驱动表结果集非常小时(比如几十行),BNLJ的性能可能优于Hash Join,因为Hash Join有哈希表构建的固定开销。只有当数据量达到一定规模后,Hash Join的优势才会体现。
- 误区:LEFT JOIN的左表一定是驱动表,无法改变澄清:LEFT JOIN的左表默认是驱动表,仅当WHERE条件中包含右表的强制过滤条件,且过滤后右表结果集更小,优化器可能会转为INNER JOIN,重新选择驱动表。
- 误区:JOIN查询性能一定比子查询差澄清:MySQL 8.0+的子查询优化器已非常成熟,会将IN子查询转为SEMI JOIN,使用和JOIN相同的算法执行,性能无差异。EXISTS子查询在大数据量去重场景下,性能甚至优于JOIN。
- 误区:JOIN的表越多,性能越差澄清:性能差的核心原因是无合适的索引或关联条件不合理,而非表的数量。MySQL优化器可处理多表JOIN,选择最优的关联顺序和算法,只要索引设计合理,3-5表JOIN也能保持高性能。
六、JOIN查询通用优化黄金法则
6.1 索引设计优化法则
- 被驱动表的关联字段必须建立索引:这是JOIN优化的第一原则,优先保证关联字段有高效索引,尽量使用主键/唯一索引,其次是高选择性二级索引。
- 优先使用覆盖索引:将查询需要的列加入关联索引,构建联合覆盖索引,避免回表操作,这是NLJ算法性能提升的核心手段。
- 关联字段属性必须完全一致:关联字段的类型、字符集、排序规则必须完全相同,避免隐式类型转换导致索引失效。例如bigint和varchar关联、utf8mb4和utf8字符集关联,都会导致索引失效。
- 避免索引失效场景:禁止在关联字段上使用函数、表达式、算术运算,否则会导致索引失效,退化为BNLJ或Hash Join。
6.2 SQL写法优化法则
- 永远小结果集驱动大结果集:INNER JOIN场景下,通过WHERE条件优先过滤出小结果集,让优化器选择正确的驱动表;LEFT/RIGHT JOIN场景下,尽量让小表作为驱动表。
- 禁止使用SELECT:只查询业务需要的列,减少数据传输、Join Buffer的内存占用,提升覆盖索引的命中率。
- 控制JOIN表的数量:建议单条SQL的JOIN表数量不超过3个,超过5个表的JOIN会导致优化器选择成本过高,容易选错执行计划,建议拆分查询。
- 避免非等值连接作为主关联条件:非等值连接无法使用Hash Join,也很难使用高效的NLJ,性能极差,尽量转为等值连接处理。
- WHERE条件优先过滤驱动表:将过滤条件尽量加在驱动表上,减少外层循环的行数,从根源上降低循环次数。
6.3 数据库参数优化法则
- 合理设置join_buffer_size:默认256KB,建议单连接单缓冲区不超过1MB,全局设置不可超过10MB,避免多连接并发时内存溢出。
- 开启Hash Join优化:MySQL 8.0.18+版本默认开启,无需手动关闭,无索引等值连接场景下优先使用Hash Join。
- 优化器开关配置:保持
optimizer_switch的默认配置,开启semijoin、materialization等子查询优化开关,提升JOIN相关的优化能力。
七、生产级慢查询优化实战案例
7.1 原始慢SQL场景
某电商系统用户订单查询SQL,3表关联,数据量:用户表100万行,订单表1000万行,订单详情表5000万行,原始SQL执行时间12.6秒,严重影响业务性能。
原始SQL:
SELECT
u.user_name, o.order_no, d.product_name, d.product_num, d.sale_price
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
LEFT JOIN t_order_detail d ON o.id = d.order_id
WHERE u.create_time >= '2024-01-01'
AND o.order_status = 1
AND d.sale_price > 100;
原始执行计划核心问题:
- 三张表均为全表扫描,无可用索引
- LEFT JOIN导致大表作为驱动表,外层循环行数超过10万行
- 关联字段无索引,退化为BNLJ算法,被驱动表多次全表扫描
- WHERE条件中包含右表的过滤条件,LEFT JOIN实际转为INNER JOIN,优化器选错驱动表
7.2 优化步骤
- 修正SQL语义:WHERE条件包含右表的非空过滤条件,LEFT JOIN无意义,改为INNER JOIN,让优化器自主选择驱动表
- 添加高效索引:
-- 驱动表过滤索引
ALTER TABLE t_user ADD INDEX idx_create_time (create_time, id, user_name);
-- 订单表关联+过滤+覆盖索引
ALTER TABLE t_order ADD INDEX idx_user_id_status (user_id, order_status, id, order_no);
-- 订单详情表关联+过滤+覆盖索引
ALTER TABLE t_order_detail ADD INDEX idx_order_id_price (order_id, sale_price, product_name, product_num);
- 优化过滤条件:优先过滤驱动表,减少结果集行数,将用户表的时间过滤条件收紧,减少外层循环行数
7.3 优化后SQL
SELECT
u.user_name, o.order_no, d.product_name, d.product_num, d.sale_price
FROM t_user u
INNER JOIN t_order o ON u.id = o.user_id
INNER JOIN t_order_detail d ON o.id = d.order_id
WHERE u.create_time >= '2024-01-01'
AND u.create_time < '2024-02-01'
AND o.order_status = 1
AND d.sale_price > 100;
7.4 优化效果
优化后执行计划:
- 优化器选择用户表作为驱动表,经过时间过滤后结果集仅800行
- 订单表和订单详情表均通过关联索引匹配,type列为ref,使用覆盖索引,无回表操作
- 执行算法为Index Nested-Loop Join,无Join Buffer使用
- 最终执行时间从12.6秒优化至8毫秒,性能提升1500倍以上
八、生产级JOIN查询代码实现
以下为基于Spring Boot 3.2.x、MyBatis-Plus 3.5.7、JDK 17的生产级JOIN查询代码实现。
8.1 核心依赖配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
</dependencies>
8.2 实体类定义
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类
* @author ken
*/
@Data
@TableName("t_user")
@Schema(description = "用户信息实体")
public class User {
@TableId(type = IdType.AUTO)
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "zhangsan")
private String userName;
@Schema(description = "年龄", example = "25")
private Integer age;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
* @author ken
*/
@Data
@TableName("t_order")
@Schema(description = "订单信息实体")
public class Order {
@TableId(type = IdType.AUTO)
@Schema(description = "订单ID", example = "1")
private Long id;
@Schema(description = "订单编号", example = "ORD20240409001")
private String orderNo;
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "订单金额", example = "99.99")
private BigDecimal orderAmount;
@Schema(description = "订单状态 0-待付款 1-已付款 2-已取消", example = "1")
private Integer orderStatus;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}
8.3 VO类定义
package com.jam.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 用户订单关联查询VO
* @author ken
*/
@Data
@Schema(description = "用户订单关联查询返回VO")
public class UserOrderVO {
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "用户名", example = "zhangsan")
private String userName;
@Schema(description = "订单编号", example = "ORD20240409001")
private String orderNo;
@Schema(description = "订单金额", example = "99.99")
private BigDecimal orderAmount;
}
8.4 Mapper层定义
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import com.jam.demo.vo.UserOrderVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户Mapper接口
* @author ken
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 关联查询用户订单信息
* @param maxAge 最大年龄过滤条件
* @return 用户订单列表
*/
List<UserOrderVO> listUserOrderByAge(@Param("maxAge") Integer maxAge);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.UserMapper">
<select id="listUserOrderByAge" resultType="com.jam.demo.vo.UserOrderVO">
SELECT u.id AS user_id, u.user_name, o.order_no, o.order_amount
FROM t_user u
INNER JOIN t_order o ON u.id = o.user_id
WHERE u.age <= #{maxAge}
</select>
</mapper>
8.5 Service层定义
package com.jam.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;
import com.jam.demo.vo.UserOrderVO;
import java.util.List;
/**
* 用户服务接口
* @author ken
*/
public interface UserService extends IService<User> {
/**
* 根据最大年龄查询用户订单列表
* @param maxAge 最大年龄
* @return 用户订单列表
*/
List<UserOrderVO> getUserOrderList(Integer maxAge);
}
package com.jam.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import com.jam.demo.vo.UserOrderVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.Collections;
import java.util.List;
/**
* 用户服务实现类
* @author ken
*/
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public List<UserOrderVO> getUserOrderList(Integer maxAge) {
if (ObjectUtils.isEmpty(maxAge) || maxAge < 0) {
log.warn("查询用户订单列表,参数maxAge非法:{}", maxAge);
return Collections.emptyList();
}
List<UserOrderVO> resultList = baseMapper.listUserOrderByAge(maxAge);
if (CollectionUtils.isEmpty(resultList)) {
log.info("查询用户订单列表,无匹配数据,maxAge:{}", maxAge);
return Collections.emptyList();
}
log.info("查询用户订单列表,匹配数据条数:{}", resultList.size());
return resultList;
}
}
8.6 Controller层定义
package com.jam.demo.controller;
import com.jam.demo.service.UserService;
import com.jam.demo.vo.UserOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 用户控制器
* @author ken
*/
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户相关接口")
public class UserController {
private final UserService userService;
@GetMapping("/order/list")
@Operation(summary = "查询用户订单列表", description = "根据最大年龄过滤,关联查询用户订单信息")
public ResponseEntity<List<UserOrderVO>> listUserOrder(
@Parameter(description = "最大年龄", example = "30", required = true)
@RequestParam Integer maxAge
) {
List<UserOrderVO> result = userService.getUserOrderList(maxAge);
return ResponseEntity.ok(result);
}
}
8.7 项目启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
总结
MySQL JOIN查询的性能核心,本质上是减少循环次数与单次循环的开销。Index Nested-Loop Join是性能最优的首选方案,核心是给被驱动表的关联字段建立合适的索引;无索引的等值连接场景,MySQL 8.0+优先使用Hash Join,性能远超传统的BNLJ;非等值连接无索引场景,只能使用BNLJ,需尽量控制驱动表的结果集大小。