吃透 MySQL Join 底层:3 种关联算法的执行原理、适用场景与极致优化方案

简介: 本文深入解析MySQL三大JOIN算法(NLJ、BNLJ、Hash Join)的底层原理与优化实践,厘清驱动表选择、索引依赖、执行计划等关键误区,结合实战案例与代码实现,助开发者从根本上解决80%的慢JOIN问题。

在日常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 底层执行流程

完整执行步骤:

  1. 优化器根据过滤条件,选定结果集更小的表作为驱动表,另一张为被驱动表
  2. 遍历驱动表,取出符合WHERE条件的每一行数据
  3. 从该行数据中提取关联字段的值,去被驱动表的关联索引中查找匹配记录
  4. 若找到匹配记录,合并两行数据加入结果集;若未找到,跳过当前行
  5. 重复步骤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过滤后的结果集行数较小
  • 等值连接与非等值连接均支持,兼容性最好

核心优化方案

  1. 被驱动表的关联字段优先使用主键/唯一索引,减少回表开销
  2. 针对查询列构建覆盖索引,避免回表操作。示例中可创建联合索引:

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 执行流程

完整执行步骤:

  1. 优化器选定驱动表与被驱动表,申请Join Buffer内存空间,缓冲区大小由join_buffer_size参数控制,默认256KB
  2. 读取驱动表中符合WHERE条件的数据,分块加载到Join Buffer中,直到缓冲区填满
  3. 全表扫描被驱动表,取出每一行数据,与Join Buffer中的所有数据一次性匹配关联条件
  4. 匹配成功的行,合并后加入结果集
  5. 当前Join Buffer中的数据全部匹配完成后,清空缓冲区,继续加载驱动表的下一批数据
  6. 重复步骤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中

核心优化方案

  1. 优先给被驱动表的关联字段建立合适的索引,转为Index Nested-Loop Join,这是最根本的优化手段
  2. 禁止使用SELECT *,只查询业务需要的列,提升Join Buffer可存储的行数,减少被驱动表扫描次数
  3. 合理调整join_buffer_size参数,避免过小导致分块过多,同时注意:每个连接的每个JOIN都会分配一个独立的Join Buffer,多表JOIN会分配多个缓冲区,不可设置过大(建议单缓冲区不超过1MB)
  4. 严格控制驱动表的结果集大小,减少分块数量,降低被驱动表扫描次数

四、Hash Join(哈希连接)

Hash Join是MySQL 8.0.18版本正式引入的JOIN算法,专门用于优化无索引的等值连接场景,目前已成为MySQL 8.0+无索引等值连接的默认算法,性能远超传统的BNLJ算法。

4.1 底层执行原理

Hash Join的核心逻辑是:基于哈希表实现,分为构建阶段(Build Phase)探测阶段(Probe Phase)两个核心阶段,用哈希查找替代嵌套循环匹配,大幅提升大数据量下的关联性能。

4.1.1 执行流程

完整执行步骤:

  1. 优化器选定两张表中结果集更小的表作为build表(构建表),另一张作为probe表(探测表),对应NLJ中的驱动表与被驱动表
  2. 构建阶段:遍历build表,读取符合WHERE条件的每一行数据,对关联字段计算哈希值,以哈希值为key,行数据为value,在Join Buffer中构建内存哈希表,哈希冲突采用链地址法处理
  3. 探测阶段:遍历probe表,读取每一行数据,对关联字段使用相同的哈希函数计算哈希值,到内存哈希表中查找对应的bucket
  4. 若找到匹配的bucket,遍历bucket中的链表,校验关联条件是否完全匹配,匹配成功则合并行加入结果集;未找到则跳过当前行
  5. 重复步骤3-4,直到probe表遍历完成,返回最终结果集
  6. 若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及以上版本,是该场景下的默认首选算法
  • 两张表过滤后的结果集差异较大,小表可完全放入内存构建哈希表

核心优化方案

  1. 优先保证关联条件为等值连接,确保Hash Join可用
  2. 严格控制build表的结果集大小,尽量让哈希表完全放入内存,避免磁盘分区带来的IO开销
  3. 合理调整join_buffer_size参数,为哈希表提供足够的内存空间,同时避免全局设置过大导致内存溢出
  4. 避免在关联字段上使用函数、表达式,导致哈希值计算不一致,无法匹配
  5. 若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 高频易混淆点澄清

  1. 误区:小表驱动大表就是表总行数少的作为驱动表澄清:驱动表的选择核心是WHERE条件过滤后的结果集行数,而非表的总行数。即使表有100万行,过滤后只剩100行,也会被选为驱动表。
  2. 误区:Hash Join一定比BNLJ快澄清:当驱动表结果集非常小时(比如几十行),BNLJ的性能可能优于Hash Join,因为Hash Join有哈希表构建的固定开销。只有当数据量达到一定规模后,Hash Join的优势才会体现。
  3. 误区:LEFT JOIN的左表一定是驱动表,无法改变澄清:LEFT JOIN的左表默认是驱动表,仅当WHERE条件中包含右表的强制过滤条件,且过滤后右表结果集更小,优化器可能会转为INNER JOIN,重新选择驱动表。
  4. 误区:JOIN查询性能一定比子查询差澄清:MySQL 8.0+的子查询优化器已非常成熟,会将IN子查询转为SEMI JOIN,使用和JOIN相同的算法执行,性能无差异。EXISTS子查询在大数据量去重场景下,性能甚至优于JOIN。
  5. 误区:JOIN的表越多,性能越差澄清:性能差的核心原因是无合适的索引或关联条件不合理,而非表的数量。MySQL优化器可处理多表JOIN,选择最优的关联顺序和算法,只要索引设计合理,3-5表JOIN也能保持高性能。

六、JOIN查询通用优化黄金法则

6.1 索引设计优化法则

  1. 被驱动表的关联字段必须建立索引:这是JOIN优化的第一原则,优先保证关联字段有高效索引,尽量使用主键/唯一索引,其次是高选择性二级索引。
  2. 优先使用覆盖索引:将查询需要的列加入关联索引,构建联合覆盖索引,避免回表操作,这是NLJ算法性能提升的核心手段。
  3. 关联字段属性必须完全一致:关联字段的类型、字符集、排序规则必须完全相同,避免隐式类型转换导致索引失效。例如bigint和varchar关联、utf8mb4和utf8字符集关联,都会导致索引失效。
  4. 避免索引失效场景:禁止在关联字段上使用函数、表达式、算术运算,否则会导致索引失效,退化为BNLJ或Hash Join。

6.2 SQL写法优化法则

  1. 永远小结果集驱动大结果集:INNER JOIN场景下,通过WHERE条件优先过滤出小结果集,让优化器选择正确的驱动表;LEFT/RIGHT JOIN场景下,尽量让小表作为驱动表。
  2. 禁止使用SELECT:只查询业务需要的列,减少数据传输、Join Buffer的内存占用,提升覆盖索引的命中率。
  3. 控制JOIN表的数量:建议单条SQL的JOIN表数量不超过3个,超过5个表的JOIN会导致优化器选择成本过高,容易选错执行计划,建议拆分查询。
  4. 避免非等值连接作为主关联条件:非等值连接无法使用Hash Join,也很难使用高效的NLJ,性能极差,尽量转为等值连接处理。
  5. WHERE条件优先过滤驱动表:将过滤条件尽量加在驱动表上,减少外层循环的行数,从根源上降低循环次数。

6.3 数据库参数优化法则

  1. 合理设置join_buffer_size:默认256KB,建议单连接单缓冲区不超过1MB,全局设置不可超过10MB,避免多连接并发时内存溢出。
  2. 开启Hash Join优化:MySQL 8.0.18+版本默认开启,无需手动关闭,无索引等值连接场景下优先使用Hash Join。
  3. 优化器开关配置:保持optimizer_switch的默认配置,开启semijoinmaterialization等子查询优化开关,提升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;

原始执行计划核心问题:

  1. 三张表均为全表扫描,无可用索引
  2. LEFT JOIN导致大表作为驱动表,外层循环行数超过10万行
  3. 关联字段无索引,退化为BNLJ算法,被驱动表多次全表扫描
  4. WHERE条件中包含右表的过滤条件,LEFT JOIN实际转为INNER JOIN,优化器选错驱动表

7.2 优化步骤

  1. 修正SQL语义:WHERE条件包含右表的非空过滤条件,LEFT JOIN无意义,改为INNER JOIN,让优化器自主选择驱动表
  2. 添加高效索引

-- 驱动表过滤索引
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);

  1. 优化过滤条件:优先过滤驱动表,减少结果集行数,将用户表的时间过滤条件收紧,减少外层循环行数

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 &lt;= #{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,需尽量控制驱动表的结果集大小。

目录
相关文章
|
7天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
34477 17
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
19天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
45307 142
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
8天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4874 21
|
1天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
1990 6
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
7天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1819 5
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案