FULL JOIN易触发笛卡尔积式重复,因未用序号列对齐一对多关系;必须在JOIN前按PersonID、ClassID、dt_ClassData分区,并以dt_GetTime/dt_ReturnTime升序生成getnum/returnnum,再在ON条件中同时匹配业务主键与序号列。

为什么FULL JOIN容易触发笛卡尔积式重复
当两个表都存在一对多关系(比如同一人同一天有多条领灯记录、也有多条还灯记录),直接FULL JOIN且只靠业务主键(如PersonID, dt_ClassData)关联,数据库会把左边每条匹配行和右边每条匹配行全量组合——不是“第一条对第一条”,而是“1×1, 1×2, 2×1, 2×2…”。结果行数 = 左表匹配数 × 右表匹配数,完全失控。
典型表现:查出 6 行,但业务上只该有 2 对(领-还配对),其余全是错位组合;COUNT(*)远超预期,GROUP BY后聚合值翻倍。
- 根本原因不是JOIN写错了,而是缺少“序号对齐”逻辑
-
ON条件只能过滤行,不能控制配对顺序 - 没有辅助序号列时,数据库无法知道“哪条领灯该配哪条还灯”
用ROW_NUMBER生成对齐序号是关键一步
必须在JOIN前,分别给左右表按相同维度打序号,让“第1条领灯”和“第1条还灯”能对上。这个序号不是随机的,要反映业务先后顺序(如时间先后、插入顺序)。
错误写法:ROW_NUMBER() OVER (PARTITION BY PersonID ORDER BY id) —— 如果id是自增主键,它可能和业务发生顺序不一致;更糟的是没带上dt_ClassData,会导致跨天数据混排。
- 分区字段必须包含所有业务对齐维度:
PARTITION BY PersonID, ClassID, dt_ClassData - 排序字段优先用可信时间:
ORDER BY dt_GetTime ASC(领灯)或dt_ReturnTime ASC(还灯) - 时间可能为空?加
COALESCE(dt_GetTime, '1970-01-01')兜底,避免NULL干扰序号 - MySQL不支持
NULLS LAST,别依赖它
FULL JOIN时用序号列精确对齐
JOIN条件里必须同时包含业务主键和序号列,缺一不可。只连PersonID + dt_ClassData会回到笛卡尔积;只连getnum = returnnum又会把不同人的记录错配。
正确ON写法:
ON laGet.PersonID = laReturn.PersonID AND laGet.ClassID = laReturn.ClassID AND laGet.dt_ClassData = laReturn.dt_ClassData AND laGet.getnum = laReturn.returnnum
注意:getnum和returnnum必须是同一套逻辑生成(比如都用ASC,或都用DESC),否则1对不上1。
- 如果领灯按时间升序编号,还灯也必须升序编号,才能保证“最早领”配“最早还”
- 别在JOIN后才加序号——那是在笛卡尔积结果上编号,毫无意义
- LEFT JOIN / RIGHT JOIN同样适用此法,只要子表有重复,就先序号化再关联
容易被忽略的NULL和边界情况
实际数据里,dt_GetTime或dt_ReturnTime为空很常见。一旦参与ORDER BY,不同数据库处理方式不同:PostgreSQL默认NULLS FIRST,MySQL行为不稳定。这会导致序号分配错乱,比如空时间排第1,把有效记录挤到后面。
更隐蔽的问题是“时间精度不一致”:日志里有的存到秒,有的只到天,CAST(... AS DATE)后可能全变成同一天,排序失效。
- 强制非空:用
COALESCE(dt_GetTime, DATEADD(day, -1, GETDATE()))(SQL Server)或IFNULL(dt_GetTime, '1970-01-01')(MySQL) - 统一精度:若业务只关心日期,先
DATE(dt_GetTime)再排序,避免时分秒干扰 - 验证序号是否连续:查
SELECT getnum, COUNT(*) FROM v_LampHistoryDataGet GROUP BY getnum,确保没跳号或重复
序号列本身不是银弹——它解决的是“如何配对”,但配对逻辑是否符合业务真实流程,得靠你定义的PARTITION BY和ORDER BY来保证。漏掉一个字段,或颠倒一次排序方向,结果就不可信。

















