Oracle处理m:n连接表

时间:2016-11-28 08:59:18

标签: oracle join

另一个看似简单问题的案例让我感到困惑:Oracle在通过m:n连接表执行连接时创建了一个令人惊讶的执行计划。我不是说,这是错的,但我很乐意作出解释。

在调查生产应用程序减速到爬行的原因时,我使用连接表重写了一个查询。这有效但执行计划我'看到让我感到疑惑。所以我创建了一个人工测试用例来帮助我理解我所见过的东西。

设置很简单:两个表,都有一个生成的主键和一些额外的"有效载荷"列:

CREATE TABLE left_table
(
  id     NUMBER(9) PRIMARY KEY,
  value1 VARCHAR2(32 CHAR) NOT NULL,
  ...

);

CREATE TABLE right_table
(
  id     NUMBER(9) PRIMARY KEY,
  value1 VARCHAR2(32 CHAR) NOT NULL,
...
);

和联接表:

CREATE TABLE left_to_right
(
   left_id   NUMBER(9) NOT NULL,
   right_id  NUMBER(9) NOT NULL,
   CONSTRAINT fk_left FOREIGN KEY (left_id) REFERENCES left_table(id),
   CONSTRAINT fk_right FOREIGN KEY (right_id) REFERENCES right_table(id)
);

直接向前,对吧?

但是这里出现了查询(左表和右表填充了1,000,000个随机值,映射表包含49,000行):

(a)仅在连接表上进行简单查询:

SELECT
 m.left_id,
 m.right_id,
 l.id     AS l_id,
 r.id     AS r_id
 --l.value1 AS lval
FROM LEFT_table l,
 RIGHT_table r,
 left_to_right m
WHERE m.left_id BETWEEN 100000 AND 200000
   AND m.left_id  = l.id
   AND m.right_id = r.id;

从连接表中选择任意范围的左表ID,从连接表中选择相同的id。执行计划完全符合预期:Oracle使用连接表上的密钥。

-------------------------------------------------------------------------------------
| Id  | Operation    | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |          | 49265 |  1250K|   270   (0)| 00:00:01 |
|*  1 |  INDEX RANGE SCAN| PK_LEFT_TO_RIGHT | 49265 |  1250K|   270   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

(b)从左表中添加元素 现在我们从左侧表格中添加一列到投影:

SELECT
  m.left_id,
  m.right_id,
  l.id     AS l_id,
  r.id     AS r_id,
  l.value1 AS lval -- <- added a row from the left table
FROM LEFT_table l,
  RIGHT_table r,
  left_to_right m
WHERE m.left_id BETWEEN 100000 AND 200000
AND m.left_id  = l.id
AND m.right_id = r.id

执行计划

------------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name              | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                                    | 22222 |  2278K|       |   680   (1)| 00:00:01 |
|   1 |  MERGE JOIN                     |                   | 22222 |  2278K|       |   680   (1)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID   | LEFT_TABLE        |   100K|  7739K|       |    42   (0)| 00:00:01 |
|*  3 |    INDEX RANGE SCAN             | SYS_C0010389      |   100K|       |       |     3   (0)| 00:00:01 |
|*  4 |   SORT JOIN                     |                   | 49265 |  1250K|  3496K|   638   (1)| 00:00:01 |
|*  5 |    INDEX RANGE SCAN             | PK_LEFT_TO_RIGHT  | 49265 |  1250K|       |   270   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------

我原本希望数据库从连接表中选择适当的行,然后使用索引访问左侧表。相反,使用排序连接,推动总体成本。

(c)从右侧表格中添加一列:

SELECT
  m.left_id,
  m.right_id,
  l.id     AS l_id,
  r.id     AS r_id,
  l.value1 AS lval, -- <- column from left table
  r.value1 as rval --  <- column from right table
FROM LEFT_table l,
  RIGHT_table r,
  left_to_right m
WHERE m.left_id BETWEEN 100000 AND 200000
AND m.left_id  = l.id
AND m.right_id = r.id
----------------------------------------------------------------------------------------------------------
| Id  | Operation             | Name         | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT          |          | 22222 |  3993K|   | 11821   (1)| 00:00:01 |
|*  1 |  HASH JOIN            |          | 22222 |  3993K|  2544K| 11821   (1)| 00:00:01 |
|   2 |   MERGE JOIN              |          | 22222 |  2278K|   |   680   (1)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID| LEFT_TABLE   |   100K|  7739K|   |    42   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN          | SYS_C0010389     |   100K|   |   |     3   (0)| 00:00:01 |
|*  5 |    SORT JOIN              |          | 49265 |  1250K|  3496K|   638   (1)| 00:00:01 |
|*  6 |     INDEX RANGE SCAN          | PK_LEFT_TO_RIGHT | 49265 |  1250K|   |   270   (0)| 00:00:01 |
|   7 |   TABLE ACCESS FULL       | RIGHT_TABLE  |   958K|    72M|   |  6889   (1)| 00:00:01 |
----------------------------------------------------------------------------------------------------------

这是我停止理解任何关于联接的点:从右侧表中添加一列导致优化器在填充良好的表上选择全表扫描(记住:1,000,000行) 。这会将执行成本通过上限发送,尽管存在直接访问的索引(是的,我刷新了数据库统计信息)。

虽然为了我的目的执行仍然足够快(应用程序仍然能够读取每秒~22,000行),但我想了解为什么oracle对右表使用全表扫描。在我天真的理解中,通过主键加入会更快...... 谁能向我解释这个谜团?

此致

编辑:我使用postgresql数据库完成了相同的测试:执行计划比我预期的要多得多: 使用左表和右表的主键:


       Nested Loop  (cost=7072.76..53476.36 rows=48290 width=86)
          ->  Hash Join  (cost=7072.33..22497.73 rows=48290 width=49)
                Hash Cond: (m.left_id = l.id)
                ->  Seq Scan on left_to_right m  (cost=0.00..7549.00    rows=490000 width=12)
                ->  Hash  (cost=5070.45..5070.45 rows=98551 width=37)
                      ->  Index Scan using left_table_pkey on left_table l     (cost=0.42..5070.45 rows=98551 width=37)
                            Index Cond: ((id >= 100000) AND (id   Index Scan using right_table_pkey on right_table r     (cost=0.42..0.63 rows=1 width=37)
                Index Cond: (id = m.right_id)


另一个更新:感谢您的回答。 我思考了Evgeniy K.发表的评论,并尝试了以下内容: 我在右表上添加了列id和value1的索引。对于最终查询,执行计划更改如下:

------------------------------------------------------------------------------------------------------------------
| Id  | Operation                 | Name         | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |          |   147K|    27M|   | 22059   (1)| 00:00:01 |
|*  1 |  HASH JOIN                |          |   147K|    27M|    18M| 22059   (1)| 00:00:01 |
|*  2 |   HASH JOIN               |          |   147K|    16M|  9112K|  2694   (1)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| LEFT_TABLE   |   102K|  7906K|   |    16   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN              | SYS_C0010428     |   100K|   |   |     2   (0)| 00:00:01 |
|*  5 |    INDEX RANGE SCAN           | PK_LEFT_TO_RIGHT |   228K|  8716K|   |  1683   (0)| 00:00:01 |
|   6 |   INDEX FAST FULL SCAN            | FOO      |  2959K|   222M|   |  5717   (1)| 00:00:01 |
------------------------------------------------------------------------------------------------------------------

(请不要注意增加的行号:我实验性地将表格大小增加到了left_table和right_table的3,000,000行。基本上查询仍会访问right_table的所有行)

所以,附加索引会 (i)用索引范围扫描替换全表扫描 (ii)成本相同(访问所有行) 因此,这个指数在这种情况下并没有真正帮助。另一个有趣的事实:如果我从right_table中选择另一列,甚至在索引中也不会使用索引。我的解释是索引以或多或少的方式使用,因为索引的净效应为零。

2 个答案:

答案 0 :(得分:1)

在第一个查询中 CBO从执行计划中删除了两个表,因为它已经在索引PK_LEFT_TO_RIGHT中已经拥有了所有需要的信息,所以为什么在任何其他表中看不到任何表访问/索引扫描。

提示:要查看计划中的其他表,请尝试删除fk或使用左/右连接

在第二次查询中 SORT JOIN因操作之间而出现。 CBO假设将数据抛出索引PK_LEFT_TO_RIGHT更便宜,对它们进行排序然后加入而使用简单的nl。

在第三个查询中添加不在索引中的列,并且CBO需要表访问权限,这就是它使用的原因

 TABLE ACCESS FULL       | RIGHT_TABLE

为什么它使用哈希加入?因为LEFT_TABLE很小而且RIGHT_TABLE很大,所以CBO决定使用散列连接。由于hj的机制,它不能在RIGHT_TABLE(探测表)上的唯一扫描上使用索引范围。再看看hj的工作原理。

小提示:像r.id = 10一样添加smt,你会看到哈希联接的唯一扫描,如下所示:

HASH JOIN
  .....
  TABLE ACCESS BY INDEX ROWID
     INDEX UNIQUE SCAN      

答案 1 :(得分:0)

有时,优化器会计算并确定为每行执行搜索和光盘访问的成本比执行表扫描然后过滤行的成本更高。

请记住,全表扫描在单个I / O读取中占用的磁盘数量多于嵌套循环,后者返回单个页面。此完整扫描行为由数据库系统的db_file_multiblock_read_count参数确定。

因此,优化器决定读取整个表成本更低(不一定更快)然后过滤内存中的行而不是为每行执行表查找。

此外,您选择的数据的偏差可能会影响您选择此执行计划。

这是我在这个问题上的2美分,所以它可能会让你对此有所了解,但它绝对不是一个完全涵盖所有可能原因的答案,可能还有其他因素会影响这种行为。< / em>的