排查 MyBatis 分页查询出现完全重复记录的问题
背景描述
在测试用户列表的分页查询接口时,发现返回的 records 列表中存在两条完全一模一样的数据。
返回的 JSON 结构简化如下:
1 | { |
初步排查了 role_agg 子查询,由于内部使用了 GROUP BY urr.user_id,保证了每个用户仅输出一行角色聚合数据,因此排除了角色表引起重复的可能。
疑点集中在 tenant_user_rel(租户用户关联表)的 LEFT JOIN 上。
数据排查
在数据库客户端针对异常用户的数据进行排查:
检查主表:执行
SELECT * FROM "user" WHERE user_id = '异常ID',确认主表中仅有一条记录。检查关联表:执行
SELECT * FROM tenant_user_rel WHERE user_id = '异常ID',发现该用户在同一租户下存在两条关联记录:一条
status_cd = '1000'(有效数据)一条
status_cd = '2000'(历史无效/逻辑删除数据)
问题原因
综合代码与数据状态,定位到问题的根本原因:
缺失有效性过滤:在 MyBatis XML 中,针对
tenant_user_rel表的关联(不论是ON还是WHERE阶段),均漏写了status_cd = '1000'的状态校验条件。笛卡尔积:由于该用户刚好存在一条脏数据(
2000状态),LEFT JOIN在匹配时将主表的一条记录与关联表的两条记录匹配,最终查出了两条数据。外层主记录字段完全一致,导致接口返回了两条一样的 JSON 对象。潜在隐患(排序不稳定):原 SQL 的分页排序仅使用了
ORDER BY usr.create_date ASC。在多条数据创建时间一致的情况下,MySQL/PostgreSQL 等数据库的排序是不稳定的,可能导致分页数据出现随机重复或遗漏。
修复与优化方案
为了彻底解决问题并提升 SQL 的健壮性,进行了以下调整:
修复数据过滤:补充
status_cd = '1000'条件。在获取到查询结果集后,根据 userId 的唯一性再进行去重兜底
总结
注意逻辑删除字段:在涉及包含软删除设计(
status_cd/is_deleted)的业务表关联时,必须时刻警惕是否在JOIN条件或查询条件中加上了有效性限制。区分 JOIN 与 EXISTS:如果仅仅是为了做条件判断(验证关系是否存在),而不提取副表的字段用于结果展示,应优先考虑使用
EXISTS,能有效规避一对多关联导致的笛卡尔积风险,且查询计划往往更优。规范分页排序:只要涉及分页查询(
LIMIT+OFFSET),ORDER BY字段必须具备唯一性(通常用主键兜底)。

