You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中表关联后处理结果与多查询方案的性能对比

多关联表场景下的用户列表查询性能对比分析

我在使用SQL和Spring JDBC开发时,遇到一个性能选型问题:数据库中有user、items_a、items_b、user_items_a、user_items_b五张表,表结构如下:

表结构

user表

IDNAME
1Jack
2Paul
3Dan

user_items_a表

USER IDITEM_A_ID
132
122

user_items_b表

USER IDITEM_B_ID
2232
12
315

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 08:30:36