江小黑
发布于 2025-06-19 / 13 阅读
0
0

MyBatis 开发中 SQL 关联查询优化:从数据冗余到批量高效查询

MyBatis 开发中数据库查询优化:解决 left join 与循环单查难题

  • 在基于 MyBatis 的 Java 应用开发领域,数据库查询作为业务逻辑实现的核心环节,其性能与准确性直接影响着系统的整体表现。当面对复杂业务场景时,表与表之间的关联关系处理成为开发人员必须攻克的难题。其中,left join 作为常用的表连接方式,若运用不当,往往会引发诸多问题;同时,一对多表关联关系下的循环单查,也因效率低下成为开发过程中的一大痛点。本文将深入剖析这些问题,结合 SQL in 批量查询与 Java Stream 流特性,为开发者提供高效的解决方案,并通过实际代码案例,帮助读者加深理解与应用。

一、left join 使用引发的数据冗余问题

  • left join(左连接)在数据库查询中起着重要作用,它会返回左表中的所有记录,以及右表中满足连接条件的记录。若右表中不存在匹配记录,则使用 NULL 值进行填充。然而,在实际业务场景中,频繁使用 left join 进行关联查询时,由于 MySQL 自身的特性,数据冗余问题极易出现。

  • 以常见的订单系统为例,存在 orders(订单表)和 order_items(订单项表)两张表,二者呈现一对多的关系,即一个订单可以包含多个订单项。当我们使用如下 left join 语句查询订单及其订单项信息时:

<select id="getOrderWithItems" resultMap="OrderWithItemsResultMap">
   SELECT *
   FROM orders o
   LEFT JOIN order_items oi ON o.order_id = oi.order_id
</select>

  • 假设某个订单拥有 5 个订单项,那么在查询结果中,该订单的信息将会重复 5 次,每次都与不同的订单项信息进行组合返回。这种数据冗余不仅会显著增加数据在网络传输过程中的流量,还会在 Java 代码处理数据时带来额外的复杂度,造成不必要的性能损耗,严重影响系统的运行效率。

二、一对多表关联下的循环单查问题

  • 为了规避 left join 带来的数据冗余问题,部分开发者可能会选择在 Java 代码中实现大部分业务逻辑。具体做法是,先查询出所有订单,然后针对每个订单,再单独查询其对应的订单项。以下是这种实现方式的代码示例:

List<Order> orderList = orderMapper.getOrders();
  for (Order order : orderList) {
     List<OrderItem> orderItemList = orderItemMapper.getOrderItemsByOrderId(order.getOrderId());
     order.setOrderItems(orderItemList);
  }

  • 尽管这种方式成功解决了数据冗余的问题,但却带来了新的挑战 —— 数据库查询次数大幅增加。若系统中存在 100 个订单,就需要执行 101 次查询操作(1 次查询订单,100 次查询订单项)。随着数据量的不断增大,这种频繁的数据库交互会严重降低系统性能,导致响应时间变长,用户体验下降,甚至可能引发系统瓶颈。

三、实际案例:优化网格管理查询

3.1 数据库查询语句优化

  • 在实际开发中,我们遇到了类似的问题。原始的查询语句由于大量使用 left join,导致数据冗余严重,影响系统性能。原始查询语句如下:

SELECT g.*,
               d2.dept_id   AS pid_dept_id,
               d2.dept_name AS pid_dept_name,
               u2.user_id   as uid_user_id,
               u2.nick_name as uid_nick_name,
               u2.avatar    as uid_avatar
        FROM people_grid_info g
                 LEFT JOIN people_info_dept pid ON pid.info_id = g.id
                 LEFT JOIN sys_dept d2 ON pid.dept_id = d2.dept_id
                 LEFT JOIN people_info_user uid ON uid.info_id = g.id
                 LEFT JOIN sys_user u2 ON uid.user_id = u2.user_id
                 LEFT JOIN sys_dept d ON d.dept_id = g.dept_id

  • 经过优化后,查询语句发生了显著变化:

<select id="selectPeopleGridInfoList" parameterType="PeopleGridInfo" resultMap="PeopleGridInfoResult">
        SELECT g.*
        FROM people_grid_info g
        LEFT JOIN sys_dept d ON d.dept_id = g.dept_id
        <where>
            <!-- 部门条件使用 EXISTS -->
            <if test="parentId != null and parentId != ''">
                and EXISTS (
                SELECT 1 FROM people_info_dept pid
                WHERE pid.info_id = g.id AND pid.dept_id = #{parentId}
                )
            </if>
            <!-- 用户昵称条件使用 EXISTS -->
            <if test="nickName != null and nickName != ''">
                and EXISTS (
                SELECT 1
                FROM people_info_user uid
                JOIN sys_user u ON uid.user_id = u.user_id
                WHERE uid.info_id = g.id
                AND u.nick_name LIKE CONCAT('%', #{nickName}, '%')
                )
            </if>
            <if test="gridName != null and gridName != ''">
                AND g.grid_name LIKE CONCAT('%', #{gridName}, '%')
            </if>
            ${params.dataScope}
        </where>
        ORDER BY g.create_time ASC
    </select>

该优化后的查询语句具有以下核心特点:

  • 减少数据冗余:主查询仅查询网格信息的基本字段,避免了复杂的多表 JOIN 操作,从根源上防止了数据冗余的产生。

  • 灵活的条件处理:使用 EXISTS 子查询来处理部门和用户的关联条件,替代直接 JOIN 操作,使查询更加灵活高效。

  • 动态 SQL 构建:通过动态 SQL 条件,能够根据前端传入的参数,灵活构建查询条件,满足不同业务场景的需求。

3.2 Stream 流处理数据

  1. 在获取到所有关联信息后,Java 8 引入的 Stream 流特性为我们提供了强大的数据处理能力。通过 Stream 流,我们可以高效地将关联信息与对应的网格管理进行关联,并排除重复数据。

  2. 在网格管理查询中,获取部门和用户的关联信息后,具体的数据处理和填充代码如下:

 List<PeopleGridInfo> peopleGridInfos = peopleGridInfoMapper.selectPeopleGridInfoList(peopleGridInfo);

        // 如果没有数据直接返回
        if (CollectionUtils.isEmpty(peopleGridInfos)) {
            return peopleGridInfos;
        }

        // 1. 收集所有网格ID
        List<Long> infoIds = peopleGridInfos.stream()
                .map(PeopleGridInfo::getId)
                .collect(Collectors.toList());

        // 2. 批量查询部门关联信息
        List<PeopleInfoDept> allPeopleInfoDepts = peopleInfoDeptMapper.selectByGridIdList(infoIds);
        // 3. 批量查询用户关联信息
        List<PeopleInfoUser> allPeopleInfoUsers = peopleInfoUserMapper.selectByGridIdList(infoIds);

        // 4. 提取所有部门ID和用户ID
        Set<Long> deptIds = allPeopleInfoDepts.stream()
                .map(PeopleInfoDept::getDeptId)
                .collect(Collectors.toSet());
        Set<Long> userIds = allPeopleInfoUsers.stream()
                .map(PeopleInfoUser::getUserId)
                .collect(Collectors.toSet());

        // 5. 批量查询部门和用户信息
        Map<Long, SysDept> deptMap = !deptIds.isEmpty() ?
                sysDeptMapper.selectDeptByIds(new ArrayList<>(deptIds)).stream()
                        .collect(Collectors.toMap(
                                SysDept::getDeptId,
                                dept -> dept,
                                (existing, replacement) -> existing
                        )) :
                Collections.emptyMap();

        Map<Long, SysUser> userMap = !userIds.isEmpty() ?
                sysUserMapper.selectUserByIds(new ArrayList<>(userIds)).stream()
                        .collect(Collectors.toMap(
                                SysUser::getUserId,
                                user -> user,
                                (existing, replacement) -> existing // 保留第一个值
                        )) :
                Collections.emptyMap();

        // 6. 构建网格ID与部门/用户的映射关系
        Map<Long, List<Long>> gridDeptMap = allPeopleInfoDepts.stream()
                .collect(Collectors.groupingBy(PeopleInfoDept::getInfoId,
                        Collectors.mapping(PeopleInfoDept::getDeptId, Collectors.toList())));

        Map<Long, List<Long>> gridUserMap = allPeopleInfoUsers.stream()
                .collect(Collectors.groupingBy(PeopleInfoUser::getInfoId,
                        Collectors.mapping(PeopleInfoUser::getUserId, Collectors.toList())));

        // 7. 填充结果集
        for (PeopleGridInfo gridInfo : peopleGridInfos) {
            Long gridId = gridInfo.getId();

            // 填充部门
            List<SysDept> depts = gridDeptMap.getOrDefault(gridId, Collections.emptyList()).stream()
                    .map(deptMap::get)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            gridInfo.setSysDepts(depts);

            // 填充用户
            List<SysUser> users = gridUserMap.getOrDefault(gridId, Collections.emptyList()).stream()
                    .map(userMap::get)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            gridInfo.setSysUsers(users);
        }

        return peopleGridInfos;
  • 其中批量查询的sql也只在业务层执行一次

<select id="selectByGridIdList" resultType="com.ruoyi.people.domain.PeopleInfoDept">
      <include refid="selectPeopleInfoDeptVo"/>
      where info_id in
      <foreach item="infoId" collection="infoIds" open="(" separator="," close=")">
          #{infoId}
      </foreach>
 </select>
  • 在上述代码中,首先利用 Collectors.groupingBy 方法,将关联信息按照网格 ID 进行分组,得到以网格 ID 为键,关联 ID 列表为值的 Map。随后,遍历网格管理列表,通过 getOrDefault 方法获取每个网格对应的关联 ID 列表,若不存在则返回空列表,进而获取具体的关联实体,最终完成网格管理与关联信息的精准关联,同时有效避免了重复处理数据的问题。

四、总结

  1. 在 MyBatis 开发过程中,数据库查询方式的合理选择是保障系统性能的关键所在。高频率使用 left join 容易引发数据冗余,而循环单查又会降低查询效率,二者都会对系统性能产生负面影响。通过巧妙结合 SQL in 批量查询与 Java Stream 流特性,我们能够在避免数据冗余的同时,显著减少数据库查询次数,从而有效提升系统的整体性能。从实际的网格管理查询代码案例中可以清晰地看到,这种优化方案在复杂业务场景下同样具有出色的可行性和有效性。在实际项目开发中,开发者应根据具体业务需求与场景特点,灵活运用这些优化技巧,打造出高效、稳定且性能卓越的应用系统


评论