Java中表关联后处理结果与多查询方案的性能对比
多关联表场景下的用户列表查询性能对比分析
我在使用SQL和Spring JDBC开发时,遇到一个性能选型问题:数据库中有user、items_a、items_b、user_items_a、user_items_b五张表,表结构如下:
表结构
user表
| ID | NAME |
|---|---|
| 1 | Jack |
| 2 | Paul |
| 3 | Dan |
user_items_a表
| USER ID | ITEM_A_ID |
|---|---|
| 1 | 32 |
| 1 | 22 |
user_items_b表
| USER ID | ITEM_B_ID |
|---|---|
| 2 | 232 |
| 1 | 2 |
| 3 | 15 |
items_a和items_b存储对应实体的详细数据,此处省略。对应的Java实体类User定义如下:
public class User { private Long id; private String name; private List<ItemA> itemsA; private List<ItemB> itemsB; }
现在需要实现获取用户列表的逻辑,有三种方案,想知道哪种性能更优:
选项A:多表关联查询后用HashMap去重组装
private List<User> getUsers() { String sql = "SELECT * FROM user LEFT JOIN user_items_a uia ON uia.user_id = user.id LEFT JOIN items_a ia ON ia.id = uia.item_a_id LEFT JOIN user_items_b uib ON uib.user_id = user.id LEFT JOIN items_b ib ON ib.id = uib.item_b_id"; class AuxRow { Long id; String name; Long item_a_id; Long item_b_id; String item_a_value; String item_b_value; public AuxRow() { } } RowMapper<AuxRow> rm = (rs, rowNum)->{ AuxRow aux = new AuxRow(); aux.id= rs.getLong("id"); // 填充所有字段值..... return aux; }; List<AuxRow> result = jdbcTemplate.query(sql, rm); // 用user_id作为key的User映射 Map<Long, User> usersMap = new HashMap<>(); for(AuxRow aux : result){ if(usersMap.containsKey(aux.id)) { // 向已有User中添加ItemA/ItemB,需避免重复(可改用Set存储) } else { // 创建User对象并加入Map } } return new ArrayList<>(usersMap.values()); }
选项B:先查用户列表,再逐个查询关联项
private List<User> getUsers() { String sql = "SELECT * FROM user"; List<User> users = jdbcTemplate.query(sql, userRowMapper); for(User user : users){ // 查询当前用户的ItemA列表 user.setItemsA(jdbcTemplate.query("SELECT ia.* FROM items_a ia JOIN user_items_a uia ON ia.id = uia.item_a_id WHERE uia.user_id = ?", itemARowMapper, user.getId())); // 查询当前用户的ItemB列表 user.setItemsB(jdbcTemplate.query("SELECT ib.* FROM items_b ib JOIN user_items_b uib ON ib.id = uib.item_b_id WHERE uib.user_id = ?", itemBRowMapper, user.getId())); } return users; }
选项C:先查用户列表,再批量查询所有关联项后分配
private List<User> getUsers() { String sql = "SELECT * FROM user"; List<User> users = jdbcTemplate.query(sql, userRowMapper); // 提取所有用户ID List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList()); // 批量查询所有用户的ItemA List<ItemA> itemsA = jdbcTemplate.query("SELECT ia.*, uia.user_id FROM items_a ia JOIN user_items_a uia ON ia.id = uia.item_a_id WHERE uia.user_id IN (?)", itemAWithUserIdRowMapper, String.join(",", userIds.stream().map(String::valueOf).collect(Collectors.toList()))); // 批量查询所有用户的ItemB List<ItemB> itemsB = jdbcTemplate.query("SELECT ib.*, uib.user_id FROM items_b ib JOIN user_items_b uib ON ib.id = uib.item_b_id WHERE uib.user_id IN (?)", itemBWithUserIdRowMapper, String.join(",", userIds.stream().map(String::valueOf).collect(Collectors.toList()))); // 构建用户ID到User的映射 Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity())); // 分配ItemA到对应User for(ItemA item: itemsA){ User user = userMap.get(item.getUserId()); if(user != null) { user.getItemsA().add(item); } } // 分配ItemB到对应User for(ItemB item: itemsB){ User user = userMap.get(item.getUserId()); if(user != null) { user.getItemsB().add(item); } } return users; }
性能分析与选型建议
选项A的优劣
- 优势:仅发起1次数据库查询,减少了网络IO次数,适合网络延迟较高的部署场景(如数据库与应用服务器跨机房)。
- 劣势:多表左连接会产生大量重复用户数据(例如一个用户有2个ItemA和3个ItemB,会返回6条重复的用户记录),数据库需处理超大结果集,内存占用剧增;应用层还要额外做去重和数据组装,CPU与内存消耗进一步上升。若用户关联项数量多,结果集膨胀会导致性能急剧下降。
选项B的优劣
- 优势:代码逻辑简单直观,易维护;当查询用户数量极少(如分页查询每页数据少、单用户详情查询)时,总查询次数(1 + 2*N,N为用户数)的开销可忽略,且每个查询结果集小,内存压力低。
- 劣势:当用户数量N较大时,会触发N+1查询问题(此处为1+2N次查询),大量数据库请求会耗尽连接池资源,频繁网络IO成为性能瓶颈,数据库处理大量小查询的效率会显著降低。
选项C的优劣
- 优势:仅发起3次数据库查询(1次用户查询 + 2次批量关联项查询),既避免了N+1问题,也不会出现选项A的结果集膨胀问题。数据库处理精准的批量查询,结果集大小可控;应用层组装逻辑简单,综合性能在多数场景下表现均衡。
- 劣势:需提前提取用户ID构造IN查询,若用户数量过千,IN子句长度可能触及数据库参数限制,此时需拆分为多个批量查询或改用临时表,但这种场景在业务中并不常见。
场景化结论
- 少量用户查询场景(如单用户详情、小分页查询):优先选选项B,代码简洁且性能足够。
- 大量用户查询场景(如全量用户导出、批量用户处理):选项C性能最优,平衡了查询次数与结果集大小。
- 选项A仅在网络延迟极高且用户关联项极少的特殊场景下可能有优势,绝大多数业务场景下不推荐,因为结果集膨胀带来的开销远大于减少网络查询次数的收益。
内容的提问来源于stack exchange,提问作者Miguel
相关产品推荐
相关产品推荐

