数据发散(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 跨数据库兼容
相关推荐
数据库小学妹22 分钟前
数据库连接池避坑指南:告别“连接超时”与“资源耗尽”,让系统跑得更快!
数据库·redis·sql·mysql·缓存·dba
dishugj36 分钟前
HANA 数据库备份与恢复
数据库·oracle
前进的李工44 分钟前
EXPLAIN输出格式全解析:JSON、TREE与可视化
开发语言·数据库·mysql·性能优化·explain
難釋懷1 小时前
Redis网络模型-IO多路复用模型-poll模式
网络·数据库·redis
dFObBIMmai1 小时前
如何在 CSS 中实现元素的绝对定位,使其不受窗口尺寸变化影响
jvm·数据库·python
treesforest2 小时前
IP精准定位服务:从城市轮廓到街道坐标,技术如何重塑空间感知
网络·数据库·网络协议·tcp/ip·ip
大明者省2 小时前
宝塔开了端口,Ubuntu 还得开相应端口才能打通
服务器·数据库·ubuntu
Teable任意门互动2 小时前
AI原生开源多维表格有哪些?主流开源多维表格对比解析
数据库·开源·excel·钉钉·飞书·开源软件·ai-native
TDengine (老段)3 小时前
MNode 内部机制深度解析 — SDB、事务引擎与 DDL 处理全链路
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
这个DBA有点耶3 小时前
数据库上云 vs 自建:从成本到人力的三维对比与决策框架
数据库·经验分享·sql·创业创新·dba