拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis 分离查询优化策略

MyBatis 分离查询优化策略

传统的JOIN查询容易导致致的数据膨胀和分页问题,对于该问题的解决方案就是:将主表查询与关联表查询分离,通过 Stream API 高效组装数据。

方案对比

方案 A:传统 JOIN 查询

┌─────────────────────────────────────────────────────────────┐ │ SQL: SELECT * FROM orders o │ │ LEFT JOIN order_detail od ON od.order_id = o.id │ │ LIMIT 0, 10 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 数据库返回: │ │ ┌──────┬──────┬─────────┬──────────┬──────────┬──────────┐ │ │ │ 订单1 │ 详情1 │ (重复) │ (重复) │ (重复) │ (重复) │ │ │ │ 订单1 │ 详情2 │ (重复) │ (重复) │ (重复) │ (重复) │ │ │ │ 订单1 │ 详情3 │ (重复) │ (重复) │ (重复) │ (重复) │ │ │ │ 订单2 │ 详情1 │ (重复) │ (重复) │ (重复) │ (重复) │ │ │ │ ... │ ... │ │ │ │ │ │ │ └──────┴──────┴─────────┴──────────┴──────────┴──────────┘ │ │ │ │ 问题: │ │ ❌ 返回 30 行(3个订单×每个10个详情) │ │ ❌ 但实际只有 3 个独立订单! │ │ ❌ PageHelper 错误地认为有 30 条记录 │ │ ❌ 分页结果不准确! │ │ │ └─────────────────────────────────────────────────────────────┘

方案 B:当前分离查询方案

┌─────────────────────────────────────────────────────────────┐ │ Step 1: 查询主表(分页) │ │ │ │ SQL: SELECT * FROM orders LIMIT 0, 10 │ │ │ │ 返回: [订单1, 订单2, 订单3] ← 精确的 3 条!✅ │ │ │ ├─────────────────────────────────────────────────────────────┤ │ Step 2: 批量提取ID │ │ │ │ orderIds = [1, 2, 3] │ │ │ ├─────────────────────────────────────────────────────────────┤ │ Step 3: 批量查询详情 │ │ │ │ SQL: SELECT * FROM order_detail │ │ WHERE order_id IN (1, 2, 3) │ │ │ │ 返回: [详情1-1, 详情1-2, ..., 详情3-10] ← 所有详情 │ │ │ ├─────────────────────────────────────────────────────────────┤ │ Step 4 & 5: 内存中组装 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 订单1 │ │ │ │ ├─ orderDetailList: [详情1-1, 详情1-2, ...] │ │ │ │ └─ orderDishes: "宫保鸡丁、麻婆豆腐、..." │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ 订单2 │ │ │ │ ├─ orderDetailList: [详情2-1, 详情2-2, ...] │ │ │ │ └─ orderDishes: "红烧肉、糖醋里脊、..." │ │ │ ├─────────────────────────────────────────────────────┤ │ │ │ 订单3 │ │ │ │ ├─ orderDetailList: [详情3-1, ...] │ │ │ │ └─ orderDishes: "鱼香肉丝、..." │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ✅ 分页准确! │ │ ✅ 数据完整! │ │ ✅ 性能优秀! │ │ │ └─────────────────────────────────────────────────────────────┘

以上是两种方案的思路对比,对于分离查询优化策略,有四大优势体现出来。

四大优势:

优势1:解决分页数据膨胀问题(最关键!)

问题场景:

-- 传统 JOIN 查询 SELECT o.*, od.* FROM orders o LEFT JOIN order_detail od ON od.order_id = o.id WHERE ... LIMIT 0, 10; -- 希望获取前 10 个订单

实际情况:

订单数平均详情数实际返回行数预期行数
10550 行10 行
1010100 行10 行
1020200 行10 行

后果:

// PageHelper 会错误地认为: page.getTotal() = 200 // ❌ 错误!(应该是 10) page.getResult().size() = 200 // ❌ 应该是 10 个订单对象 // 前端分页组件会显示错误的总页数! // 用户点击第 2 页时可能看到重复数据或遗漏数据

解决方案效果:

// 当前方案:精确的分页 try (Page<OrderVO> page = PageHelper.startPage(1, 10)) { List<OrderVO> orders = orderMapper.queryOrders(dto); // 只查主表 // page.getTotal() = 真实的订单总数(如 156) // orders.size() = 精确的 10 个订单对象 ✅ }

优势2:减少网络传输和内存占用

数据量对比:

假设查询 20 个订单,每个订单平均 8 个详情项:

传统 JOIN 方案:

传输数据量: = 20 个订单 × (8 个详情 + 20 个订单字段) = 160 行完整数据 每行包含: - 订单字段: ~500 字节 - 详情字段: ~200 字节 总计: 160 × 700 字节 = 112,000 字节 ≈ 109 KB

分离查询方案:

Step 1 - 主表查询: = 20 行订单数据 = 20 × 500 字节 = 10,000 字节 ≈ 9.8 KB Step 2 - 详情查询: = 160 行详情数据 = 160 × 200 字节 = 32,000 字节 ≈ 31 KB 总计传输: 9.8 + 31 = 40.8 KB ≈ 40 KB

节省比例:

节省空间 = (109 - 40) / 109 = 63.3% ↓ 网络传输减少 63%! 内存占用减少 63%!

优势3:避免数据冗余和重复反序列化

传统 JOIN 的数据冗余示例:

[ { "id": 1, "number": "20240701001", "userId": 5, "status": 3, "amount": 158.00, "orderTime": "2024-07-01 12:00:00", // ... 其他 15 个字段全部重复 ... "detailId": 101, "dishName": "宫保鸡丁", "detailAmount": 38.00 }, { "id": 1, // ⚠️ 重复! "number": "20240701001", // ⚠️ 重复! "userId": 5, // ⚠️ 重复! // ... 所有订单字段都重复 ... "detailId": 102, "dishName": "麻婆豆腐", "detailAmount": 28.00 }, // 同一个订单的 20 个字段 × 8 个详情 = 160 个字段的冗余! ]

分离查询的数据结构:

{ "id": 1, "number": "20240701001", "userId": 5, "status": 3, "amount": 158.00, // ... 订单字段只出现一次 ✅ "orderDetailList": [ {"id": 101, "dishName": "宫保鸡丁", "amount": 38.00}, {"id": 102, "dishName": "麻婆豆腐", "amount": 28.00}, // ... 详情数据独立存储 ], "orderDishes": "宫保鸡丁、麻婆豆腐、..." }

优势4:灵活的数据组装能力

Stream API 的强大之处:

需求 1:获取完整的详情对象列表

Map<Long, List<OrderDetail>> detailMap = details.stream() .collect(Collectors.groupingBy(OrderDetail::getOrderId)); // 用途:订单详情弹窗展示所有信息 order.setOrderDetailList(detailMap.get(orderId));

输出示例:

"orderDetailList": [ { "id": 101, "name": "宫保鸡丁", "dishId": 12, "number": 1, "amount": 38.00, "image": "/images/dish/12.jpg" }, { "id": 102, "name": "麻婆豆腐", "dishId": 15, "number": 2, "amount": 56.00, "image": "/images/dish/15.jpg" } ]

需求 2:提取菜品名称用于列表展示

Map<Long, List<String>> orderDishesMap = details.stream() .collect(Collectors.groupingBy( OrderDetail::getOrderId, Collectors.mapping(OrderDetail::getName, Collectors.toList()) )); // 用途:订单列表快速预览 String dishes = String.join("、", orderDishesMap.get(orderId)); order.setOrderDishes(dishes);

输出示例:

"orderDishes": "宫保鸡丁、麻婆豆腐×2、米饭×3"

需求 3:还可以轻松扩展其他聚合操作

// 统计每个订单的菜品数量 Map<Long, Long> dishCountMap = details.stream() .collect(Collectors.groupingBy( OrderDetail::getOrderId, Collectors.counting() )); // 计算每个订单的总金额(从详情角度验证) Map<Long, BigDecimal> detailAmountMap = details.stream() .collect(Collectors.groupingBy( OrderDetail::getOrderId, Collectors.reducing(BigDecimal.ZERO, OrderDetail::getAmount, BigDecimal::add) )); // 提取所有菜品ID(用于缓存预热) Set<Long> dishIds = details.stream() .map(OrderDetail::getDishId) .collect(Collectors.toSet());

这种灵活性在 JOIN 方案中很难实现,因此正如开头所说,将主表查询与关联表查询分离后利用Stream API 即可高效组装数据。

返回列表