两个表关联没有连接条件的时候会产生笛卡儿积,这种表连接方式就叫笛卡儿连接。
我们在测试账号 scott 中运行如下 SQL。
set autot trace
select * from emp, dept;
56 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 2034389985
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 56 | 3248 | 8 (0)| 00:00:01 |
| 1 | MERGE JOIN CARTESIAN| | 56 | 3248 | 8 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL | DEPT | 4 | 80 | 3 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 14 | 532 | 5 (0)| 00:00:01 |
| 4 | TABLE ACCESS FULL | EMP | 14 | 532 | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------
执行计划中 MERGE JOIN CARTESIAN 就表示笛卡儿连接。笛卡儿连接会返回两个表的乘积。DEPT 有 4 行数据,EMP 有 14 行数据,两个表进行笛卡儿连接之后会返回 56 行数据。笛卡儿连接会对两表中其中一个表进行排序,执行计划中的 BUFFER SORT 就表示排序。
在多表关联的时候,两个表没有直接关联条件,但是优化器错误地把某个表返回的 Rows 算为 1 行(注意必须是 1 行),这个时候也可能发生笛卡儿连接。例子如下。
select * from table(dbms_xplan.display());
PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------
Plan hash value: 710264295
-----------------------------------------------------------------------------
| Id | Operation | Name | Rows |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | WINDOW SORT | | 1 |
|* 2 | TABLE ACCESS BY INDEX ROWID | F_AGT_GUARANTY_INFO_H | 1 |
| 3 | NESTED LOOPS | | 1 |
| 4 | NESTED LOOPS | | 1 |
| 5 | MERGE JOIN CARTESIAN | | 1 |
| 6 | TABLE ACCESS FULL | B_M_BUSINESS_CONTRACT | 1 |
| 7 | BUFFER SORT | | 61507 |
|* 8 | TABLE ACCESS FULL | F_AGT_GUARANTY_RELATIVE_H | 61507 |
| 9 | TABLE ACCESS BY INDEX ROWID| F_CONTRACT_RELATIVE | 1 |
|* 10 | INDEX UNIQUE SCAN | SYS_C0019578 | 1 |
|* 11 | INDEX RANGE SCAN | SYS_C005707 | 1 |
-----------------------------------------------------------------------------
执行计划中 Id=6 的表和 Id=8 的表就是进行笛卡儿连接的。
在这个执行计划中,为什么优化器会选择笛卡儿积连接呢?
因为 Id=6 这个表返回的 Rows 被优化器错误地估算为 1 行,优化器认为 1 行的表与任意大小的表进行笛卡儿关联,数据也不会翻番,这是安全的。所以这里优化器选择了笛卡儿连接。
Id=6 这步是全表扫描,而且没过滤条件(因为没有*),优化器认为它只返回 1 行。大家请思考,全表扫描返回 1 行并且无过滤条件,这个可能吗?难道表里面真的就只有 1 行数据?这不符合常识。那么显然是 Id=6 的表没有收集统计信息,导致优化器默认地把该表算为 1 行(当时数据库没开启动态采样)。下面是上述执行计划的 SQL 语句。
SELECT b.agmt_id,
b.corp_org,
b.cur_cd,
b.businesstype,
c.object_no,
c.guaranty_crsum,
row_number() over(PARTITION BY b.agmt_id, b.corp_org, c.object_no
ORDER BY b.agmt_id, b.corp_org, c.object_no) row_no
FROM b_m_business_contract b, --合同表
dwf.f_contract_relative c, --合同关联表
dwf.f_agt_guaranty_relative_h r, --业务合同、担保合同与担保物关联表
dwf.f_agt_guaranty_info_h g --担保物信息表
WHERE b.corp_org = c.corp_org
AND b.agmt_id = c.contract_seqno --业务合同号
AND c.object_type = 『GuarantyContract』
AND r.start_dt <= DATE 『2012-09-17』 /*当天日期*/
AND r.end_dt > DATE 『2012-09-17』 /*当天日期*/
AND c.contract_seqno = r.object_no --业务合同号
AND c.object_no = r.guaranty_no --担保合同编号
AND c.corp_org = r.corp_org --企业法人编码
AND r.object_type = 『BusinessContract』
AND r.agmt_id = g.agmt_id --担保物编号
AND r.corp_org = g.corp_org --企业法人编码
AND g.start_dt <= DATE 『2012-09-17』 /*当天日期*/
AND g.end_dt > DATE 『2012-09-17』 /*当天日期*/
AND g.guarantytype = 『020010』 --质押存款
执行计划中进行笛卡儿关联的表就是 b 和 r,在 SQL 语句中 b 和 r 没有直接关联条件。
如果两个表有直接关联条件,无法控制两个表进行笛卡儿连接。
如果两个表没有直接关联条件,我们在编写 SQL 的时候将两个表依次放在 from 后面并且添加 HINT:ordered,就可以使两个表进行笛卡儿积关联。
select /*+ ordered */
a.ename, a.sal, a.deptno, b.dname, c.grade
from dept b, salgrade c, emp a
where a.deptno = b.deptno
and a.sal between c.losal and c.hisal;
14 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 2197699399
----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 65 | 12 (9)| 00:00:01 |
|* 1 | HASH JOIN | | 1 | 65 | 12 (9)| 00:00:01 |
| 2 | MERGE JOIN CARTESIAN| | 20 | 1040 | 8 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | DEPT | 4 | 52 | 3 (0)| 00:00:01 |
| 4 | BUFFER SORT | | 5 | 195 | 5 (0)| 00:00:01 |
| 5 | TABLE ACCESS FULL | SALGRADE | 5 | 195 | 1 (0)| 00:00:01 |
| 6 | TABLE ACCESS FULL | EMP | 14 | 182 | 3 (0)| 00:00:01 |
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("A"."DEPTNO"="B"."DEPTNO")
filter("A"."SAL">="C"."LOSAL" AND "A"."SAL"<="C"."HISAL")
在 SQL 语句中,DEPT 与 SALGRADE 没有直接关联条件,HINT:ordered 表示根据 SQL 语句中 from 后面表的顺序依次关联。因为 DEPT 与 SALGRADE 没有直接关联条件,而且 SQL 语句中添加了 HINT:ordered,再有 SQL 语句中两个表是依次放在 from 后面的,所以 DEPT 与 SALGRADE 只能进行笛卡儿连接。
思考:当执行计划中有笛卡儿连接应该怎么优化呢?
首先应该检查表是否有关联条件,如果表没有关联条件,那么应该询问开发与业务人员为何表没有关联条件,是否为满足业务需求而故意不写关联条件。
其次应该检查离笛卡儿连接最近的表是否真的返回 1 行数据,如果返回行数真的只有 1 行,那么走笛卡儿连接是没有问题的,如果返回行数超过 1 行,那就需要检查为什么 Rows 会估算错误,同时要纠正错误的 Rows。纠正错误的 Rows 之后,优化器就不会走笛卡儿连接了。
我们可以使用 HINT /*+ opt_param('_optimizer_mjc_enabled', 'false')* /
禁止笛卡儿连接。