数据发散(Data Spreading)详解(附:示例 数据发散最大值是笛卡尔乘积)

数据发散是数据库查询中常见的陷阱,指当关联键不唯一时导致结果集行数异常膨胀的现象。


数据发散和连接条件有关,和连接类型无关。


典型场景出现在一对多关联时,如员工表关联奖金表(同一员工可能有多个奖金记录),原本14行的员工表可能输出16行结果。


数据发散会导致统计错误(如SUM值被放大)、性能下降和存储浪费。


解决方案包括:

1)对重复键表先进行去重;

2)先聚合再关联;

3)只取最新记录。


关键检查点包括确认关联键唯一性、评估预期结果行数,以及考虑先聚合后关联的策略。


在多表级联关联时,发散问题会呈指数级恶化,需特别注意。


数据发散(Data Spreading)详解


数据发散是指由于关联键不唯一,导致结果集行数爆炸式增长的现象。通常发生在多表关联时,其中一张表的关联键有重复值。


场景1:一对多关联导致发散

示例表结构

EMP 表(员工) - 关联键 MGR 有重复

EMPNO ENAME MGR DEPTNO
7369 SMITH 7902 20
7499 ALLEN 7698 30
7521 WARD 7698 30
7566 JONES 7839 20
7654 MARTIN 7698 30
7698 BLAKE 7839 30
7782 CLARK 7839 10
7788 SCOTT 7566 20
7839 KING NULL 10
7844 TURNER 7698 30
7876 ADAMS 7788 20
7900 JAMES 7698 30
7902 FORD 7566 20
7934 MILLER 7782 10

BONUS 表(奖金) - 关联键 ENAME 有重复

ENAME JOB SAL COMM
SMITH CLERK 800 100
SMITH CLERK 800 200
ALLEN SALESMAN 1600 300
ALLEN SALESMAN 1600 400
WARD SALESMAN 1250 500

发散演示:正常关联 vs 发散关联

情况1:正常关联(关联键唯一)

sql

复制代码
-- EMP 关联 DEPT(DEPTNO 在 DEPT 表中唯一)
SELECT E.ENAME, D.DNAME
FROM EMP E
LEFT JOIN DEPT D ON E.DEPTNO = D.DEPTNO;

结果:14 行(与 EMP 表行数一致,无发散)


情况2:数据发散(关联键不唯一)

sql

复制代码
-- EMP 关联 BONUS(ENAME 在 BONUS 表中有重复)
SELECT E.ENAME, E.JOB, B.COMM
FROM EMP E
LEFT JOIN BONUS B ON E.ENAME = B.ENAME
ORDER BY E.ENAME;

执行过程

text

复制代码
EMP 表(14行)         BONUS表(5行)
    ↓                      ↓
SMITH (1行)    →    与 BONUS 中的 2 行 SMITH 匹配 → 产生 2 行
ALLEN (1行)    →    与 BONUS 中的 2 行 ALLEN 匹配 → 产生 2 行  
WARD (1行)     →    与 BONUS 中的 1 行 WARD 匹配  → 产生 1 行
其他员工(11行)  →    与 BONUS 中无匹配            → 各产生 1 行

实际结果(共 16 行,比原 EMP 表多 2 行)

| ENAME | JOB | COMM |
|--------|-----------|------|----------------------|
| ADAMS | CLERK | NULL |
| ALLEN | SALESMAN | 300 |
| ALLEN | SALESMAN | 400 | ← 发散:1个ALLEN对应2个奖金记录 |
| BLAKE | MANAGER | NULL |
| CLARK | MANAGER | NULL |
| FORD | ANALYST | NULL |
| JAMES | CLERK | NULL |
| JONES | MANAGER | NULL |
| KING | PRESIDENT | NULL |
| MARTIN | SALESMAN | NULL |
| MILLER | CLERK | NULL |
| SCOTT | ANALYST | NULL |
| SMITH | CLERK | 100 |
| SMITH | CLERK | 200 | ← 发散:1个SMITH对应2个奖金记录 |
| TURNER | SALESMAN | NULL |
| WARD | SALESMAN | 500 |


场景3:多表级联发散(最危险)

sql

复制代码
-- 三表关联,两个关联键都不唯一
SELECT E.ENAME, E.JOB, D.DNAME, B.COMM
FROM EMP E
LEFT JOIN DEPT D ON E.DEPTNO = D.DEPTNO
LEFT JOIN BONUS B ON E.ENAME = B.ENAME;

发散计算

  • 第1层关联:EMP(14行) × DEPT(1对1) = 14行

  • 第2层关联:14行 × BONUS(重复ENAME) = 最大可能 14 × 2 = 28行


如何识别和避免数据发散

1. 检查关联键唯一性

sql

复制代码
-- 检查 BONUS 表的 ENAME 是否有重复
SELECT ENAME, COUNT(*) 
FROM BONUS 
GROUP BY ENAME 
HAVING COUNT(*) > 1;

结果

ENAME COUNT(*)
SMITH 2
ALLEN 2

2. 解决发散的方法

方法1:去重后关联

sql

复制代码
-- 先对 BONUS 去重
WITH BONUS_DISTINCT AS (
    SELECT DISTINCT ENAME, COMM 
    FROM BONUS
)
SELECT E.ENAME, E.JOB, B.COMM
FROM EMP E
LEFT JOIN BONUS_DISTINCT B ON E.ENAME = B.ENAME;
方法2:聚合后关联

sql

复制代码
-- 按员工汇总奖金
WITH BONUS_SUM AS (
    SELECT ENAME, SUM(COMM) AS TOTAL_COMM
    FROM BONUS
    GROUP BY ENAME
)
SELECT E.ENAME, E.JOB, B.TOTAL_COMM
FROM EMP E
LEFT JOIN BONUS_SUM B ON E.ENAME = B.ENAME;

结果

| ENAME | JOB | TOTAL_COMM |
|-------|----------|------------|---------|
| SMITH | CLERK | 300 | ← 合并为一行 |
| ALLEN | SALESMAN | 700 | ← 合并为一行 |

方法3:只关联需要的行(如取最新记录)

sql

复制代码
-- 取每个员工最大的 COMM
WITH BONUS_RANKED AS (
    SELECT ENAME, COMM,
           ROW_NUMBER() OVER (PARTITION BY ENAME ORDER BY COMM DESC) AS RN
    FROM BONUS
)
SELECT E.ENAME, E.JOB, B.COMM
FROM EMP E
LEFT JOIN BONUS_RANKED B ON E.ENAME = B.ENAME AND B.RN = 1;

发散的业务影响

影响类型 具体表现
数据膨胀 结果集行数超过预期,可能从几十行变成几千行
统计错误 SUM、AVG、COUNT 等聚合函数结果被放大
性能下降 JOIN 操作产生大量中间结果,消耗内存和 CPU
报表失真 明明是10个员工,报表却显示15行
存储浪费 大量重复数据占据存储空间

实际案例:错误的销售统计

假设你要统计每个部门的总销售额

sql

复制代码
-- ❌ 错误写法(会发散)
SELECT D.DNAME, SUM(S.AMOUNT) AS TOTAL_SALES
FROM DEPT D
LEFT JOIN EMP E ON D.DEPTNO = E.DEPTNO
LEFT JOIN SALES S ON E.EMPNO = S.EMPNO   -- SALES 中每个员工有多条销售记录
GROUP BY D.DNAME;

问题:如果一个员工有5条销售记录,EMP 表行数会被复制5倍,导致 SUM 过程中员工被重复计算。

正确写法

sql

复制代码
-- ✅ 正确写法(先聚合再关联)
WITH EMP_SALES AS (
    SELECT E.EMPNO, E.ENAME, SUM(S.AMOUNT) AS EMP_TOTAL
    FROM EMP E
    LEFT JOIN SALES S ON E.EMPNO = S.EMPNO
    GROUP BY E.EMPNO, E.ENAME
)
SELECT D.DNAME, SUM(ES.EMP_TOTAL) AS TOTAL_SALES
FROM DEPT D
LEFT JOIN EMP E ON D.DEPTNO = E.DEPTNO
LEFT JOIN EMP_SALES ES ON E.EMPNO = ES.EMPNO
GROUP BY D.DNAME;

快速检查清单

在写多表关联 SQL 前,先问自己:

  1. ✅ 每个关联键在右表中是否唯一?

  2. ✅ 如果不唯一,是否真的需要全部保留?

  3. ✅ 是否可以先聚合/去重再关联?

  4. ✅ 确认过关联后的预期行数吗?

黄金法则 :在 LEFT JOIN 右表关联键不唯一时,结果集会至少按重复倍数膨胀。


示例:数据发散最大值是笛卡尔乘积。(m*n)


sql 复制代码
-- 总结:
--   A表有ID 列 存在 1~100  B表有 ID 列 61 ~120,问:
--   A INNER JOIN B ON A.ID = B.ID 返回 40  行
--   A LEFT JOIN B ON A.ID = B.ID 返回 100  行
--   A RIGHT JOIN B ON A.ID = B.ID 返回  60 行
--   A FULL JOIN B ON A.ID = B.ID 返回 120  行
--
--   A表 10 条数据  B表有 5  条数据 问:
--   A INNER JOIN B 最多有 50 行    最少有 0  行
--   A LEFT JOIN B  最多有 50  行   最少有 10  行
--   A RIGHT JOIN B  最多有 50  行  最少有 5 行
--   A FULL JOIN B  最多有 50  行   最少有 10 行

--练习:
-- 1,创建 TABLEA(ID NUMBER)  TABLEB(ID NUMBER);
-- 2,往 TABLEA 插入 1~10 10行数据
--   往 TABLEB 插入 1~5  5 行数据
-- 3,依次 使用 INNER JOIN  / LEFT JOIN / RIGHT JOIN / FULL JOIN
--   验证 上述的 极值。
-- 比如:
--   SELECT *
--   FROM TABLEA M
--   FULL JOIN TABLEB N ON 1=2;

--练习 建表
create table testM(
    id NUMBER(10)
);
--插入10条数据
insert into testM values(1);
insert into testM values(2);
insert into testM values(3);
insert into testM values(4);
insert into testM values(5);
insert into testM values(6);
insert into testM values(7);
insert into testM values(8);
insert into testM values(9);
insert into testM values(10);
commit;
select * from testM;
create table testN(
    id NUMBER(10)
);
--插入5条数据
insert into testN values(1);
insert into testN values(2);
insert into testN values(3);
insert into testN values(4);
insert into testN values(5);
commit;

select * from testN;

--验证
--   A INNER JOIN B 最多有 50 行    最少有 0  行
select * from testM join testN on 1=1;
select * from testM join testN on 1=2;
--   A LEFT JOIN B  最多有 50  行   最少有 10  行
select * from testM LEFT join testN on 1=1;
select * from testM LEFT join testN on 1=2;
--   A RIGHT JOIN B  最多有 50  行  最少有 5 行
select * from testM RIGHT join testN on 1=1;
select * from testM RIGHT join testN on 1=2;
--   A FULL JOIN B  最多有 50  行   最少有 10 行
select * from testM FULL join testN on 1=1;
select * from testM FULL join testN on testM.id=testN.id;

SQL优化:演示如何使用 ROWNUMCONNECT BY 快速生成 1~100 的数据。


方法一:使用 ROWNUM(最常用、最简单)

sql

复制代码
-- 生成 1~100
SELECT ROWNUM AS id
FROM dual
CONNECT BY ROWNUM <= 100;

方法二:使用 CONNECT BY(更灵活)

sql

复制代码
-- 方式1:直接生成
SELECT LEVEL AS id
FROM dual
CONNECT BY LEVEL <= 100;

-- 方式2:带递归限制(Oracle 推荐写法)
SELECT LEVEL AS id
FROM dual
CONNECT BY LEVEL <= 100;

方法三:使用递归 CTE(WITH 子句,可读性好)

sql

复制代码
WITH numbers (id) AS (
    SELECT 1 FROM dual
    UNION ALL
    SELECT id + 1 FROM numbers WHERE id < 100
)
SELECT id FROM numbers;

完整示例:创建 A 表 1~100,B 表 61~120

sql

复制代码
-- 创建 A 表并插入 1~100
CREATE TABLE tableA AS
SELECT ROWNUM AS id
FROM dual
CONNECT BY ROWNUM <= 100;

-- 创建 B 表并插入 61~120
CREATE TABLE tableB AS
SELECT 60 + ROWNUM AS id
FROM dual
CONNECT BY ROWNUM <= 60;

-- 验证数据
SELECT COUNT(*) FROM tableA;  -- 100 行
SELECT COUNT(*) FROM tableB;  -- 60 行
SELECT MIN(id), MAX(id) FROM tableA;  -- 1, 100
SELECT MIN(id), MAX(id) FROM tableB;  -- 61, 120

复制代码
总结:
--   A表有ID 列 存在 1~100  B表有 ID 列 61 ~120,问:
--   A INNER JOIN B ON A.ID = B.ID 返回 40  行
--   A LEFT JOIN B ON A.ID = B.ID 返回 100  行
--   A RIGHT JOIN B ON A.ID = B.ID 返回  60 行
--   A FULL JOIN B ON A.ID = B.ID 返回 120  行

验证结论(40 / 100 / 60 / 120)

sql

复制代码
-- INNER JOIN:返回 40 行(61~100)
SELECT COUNT(*) FROM tableA INNER JOIN tableB ON tableA.id = tableB.id;

-- LEFT JOIN:返回 100 行(所有 A 表记录)
SELECT COUNT(*) FROM tableA LEFT JOIN tableB ON tableA.id = tableB.id;

-- RIGHT JOIN:返回 60 行(所有 B 表记录)
SELECT COUNT(*) FROM tableA RIGHT JOIN tableB ON tableA.id = tableB.id;

-- FULL JOIN:返回 120 行(100 + 60 - 40)
SELECT COUNT(*) FROM tableA FULL JOIN tableB ON tableA.id = tableB.id;

快速查看实际数据

sql

复制代码
-- 查看匹配的 ID(61~100)
SELECT * FROM tableA INNER JOIN tableB ON tableA.id = tableB.id ORDER BY tableA.id;

-- 查看不匹配的 A 表 ID(1~60)
SELECT * FROM tableA LEFT JOIN tableB ON tableA.id = tableB.id
WHERE tableB.id IS NULL ORDER BY tableA.id;

-- 查看不匹配的 B 表 ID(101~120)
SELECT * FROM tableA RIGHT JOIN tableB ON tableA.id = tableB.id
WHERE tableA.id IS NULL ORDER BY tableB.id;

小贴士

方法 优点 适用场景
ROWNUM 简单直接 单表生成序列
CONNECT BY LEVEL 语法清晰 Oracle 常用方式
递归 CTE 标准 SQL 跨数据库兼容
相关推荐
a9511416421 小时前
c++如何解析二进制协议中的可选字段读取逻辑及其反序列化【详解】
jvm·数据库·python
weixin_580614002 小时前
golang如何实现时间格式化_golang时间格式化方法详解
jvm·数据库·python
forEverPlume2 小时前
c++怎么利用std--span实现在不拷贝数据的前提下解析大规模文件【进阶】
jvm·数据库·python
FinTech老王2 小时前
逻辑删除不等于物理销毁:KingbaseES敏感数据标记与销毁实操指南
数据库·安全·oracle
HHHHH1010HHHHH2 小时前
Tailwind CSS如何快速定义固定宽高比_使用aspect-square实现CSS正方形
jvm·数据库·python
梦想的旅途22 小时前
解构自动化办公新思路:实现外部群聊能力的深度集成与交互
java·数据库·rpa
m0_515098422 小时前
c++怎么获取文件的Inode节点信息_stat结构体深度解析【详解】
jvm·数据库·python
m0_674294642 小时前
HTML怎么限制输入字符数_HTML input maxlength属性用法【详解】
jvm·数据库·python
maqr_1102 小时前
layui table单元格编辑 layui表格如何实现可编辑
jvm·数据库·python