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 (PageOrderVO page PageHelper.startPage(1, 10)) { ListOrderVO 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获取完整的详情对象列表MapLong, ListOrderDetail 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提取菜品名称用于列表展示MapLong, ListString 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还可以轻松扩展其他聚合操作// 统计每个订单的菜品数量 MapLong, Long dishCountMap details.stream() .collect(Collectors.groupingBy( OrderDetail::getOrderId, Collectors.counting() )); // 计算每个订单的总金额从详情角度验证 MapLong, BigDecimal detailAmountMap details.stream() .collect(Collectors.groupingBy( OrderDetail::getOrderId, Collectors.reducing(BigDecimal.ZERO, OrderDetail::getAmount, BigDecimal::add) )); // 提取所有菜品ID用于缓存预热 SetLong dishIds details.stream() .map(OrderDetail::getDishId) .collect(Collectors.toSet());这种灵活性在 JOIN 方案中很难实现因此正如开头所说将主表查询与关联表查询分离后利用Stream API 即可高效组装数据。
返回列表