排序合并连接

1.2.4.2.1  排序合并连接

排序合并连接(Sort Merge Join)是一种两个表在做表连接时用排序操作(Sort)和合并操作(Merge)来得到连接结果集的表连接方法。

如果两个表(这里将它们分别命名为表T1和表T2)在做表连接时使用的是排序合并连接,则Oracle会依次顺序执行如下步骤。

(1)首先以目标SQL中指定的谓词条件(如果有的话)去访问表T1,然后对访问结果按照表T1中的连接列来排序,排好序后的结果集我们记为结果集1。

(2)接着以目标SQL中指定的谓词条件(如果有的话)去访问表T2,然后对访问结果按照表T2中的连接列来排序,排好序后的结果集我们记为结果集2。

(3)最后对结果集1和结果集2执行合并操作,从中取出匹配记录来作为排序合并连接的最终执行结果。

我个人认为这个合并操作的具体执行步骤是这样的:首先遍历结果集1,即先取出结果集1中的第1条记录,然后去结果集2中按照连接条件判断是否存在匹配记录,然后再取出结果集1中的第2条记录,按照同样的连接条件再去结果集2中判断是否存在匹配的记录,直到最后遍历完结果集1中所有的记录。注意,这里去结果集2中判断是否存在匹配记录时会存在一个过滤的过程。因为结果集2已经按照表T2中的连接列排好序了,所以取出结果集1中的记录然后去找结果集2中的匹配记录时,只需要遍历结果集2中满足上述连接条件的那部分数据就可以了(这部分数据在结果集2中的存储位置肯定是在一起的),也就是说此时并不需要遍历结果集2中所有的记录,并且一旦在结果集2中找到匹配记录,就可以把该匹配记录从结果集2中删除,或者不删除但记录下其位置(这样下次再从结果集2中找匹配记录时就可以从这个位置开始了)。最后,结果集1和结果集2中所有的匹配结果就是上述排序合并连接的最终执行结果(注意,这个合并过程的具体执行步骤是我猜的,我不确定是否正确)。

对于排序合并连接的优缺点及适用场景,我们有如下总结。

通常情况下,排序合并连接的执行效率会远不如哈希连接,但前者的使用范围更广,因为哈希连接通常只能用于等值连接条件,而排序合并连接还能用于其他连接条件(例如、>=)。

通常情况下,排序合并连接并不适合OLTP类型的系统,其本质原因是因为对于OLTP类型的系统而言,排序是非常昂贵的操作,当然,如果能避免排序操作,那么即使是OLTP类型的系统,也还是可以使用排序合并连接的。比如两个表虽然是做排序合并连接,但实际上它们并不需要排序,因为这两个表在各自的连接列上都存在索引。

从严格意义上说,排序合并连接并不存在驱动表的概念,虽然我个人认为在执行合并的过程中,实际上还是存在驱动表和被驱动表的。

1.2.4.2.2  嵌套循环连接

嵌套循环连接(Nested Loops Join)是一种两个表在做表连接时依靠两层嵌套循环(分别为外层循环和内层循环)来得到连接结果集的表连接方法。

如果两个表(这里将它们分别命名为表T1和表T2)在做表连接时使用的是嵌套循环连接,则Oracle会依次顺序执行如下步骤。

(1)首先,优化器会按照一定的规则来决定表T1和T2中谁是驱动表、谁是被驱动表。驱动表用于外层循环,被驱动表用于内层循环。这里假设驱动表是T1,被驱动表是T2。

(2)接着以目标SQL中指定的谓词条件(如果有的话)去访问驱动表T1,访问驱动表T1后得到的结果集我们记为驱动结果集1。

(3)然后遍历驱动结果集1并同时遍历被驱动表T2,即先取出驱动结果集1中的第1条记录,接着遍历被驱动表T2并按照连接条件去判断T2中是否存在匹配的记录,然后再取出驱动结果集1中的第2条记录,按照同样的连接条件再去遍历被驱动表T2并判断T2中是否还存在匹配的记录,直到遍历完驱动结果集1中所有的记录为止。这里的外层循环是指遍历驱动结果集1所对应的循环,内层循环是指遍历被驱动表T2所对应的循环。显然,外层循环所对应的驱动结果集1有多少条记录,遍历被驱动表T2的内层循环就要做多少次,这就是所谓的"嵌套循环"的含义。

对于嵌套循环连接的优缺点及适用场景,我们有如下总结。

从上述嵌套循环连接的具体执行过程可以看出:如果驱动表所对应的驱动结果集的记录数较少,同时在被驱动表的连接列上又存在唯一性索引(或者在被驱动表的连接列上存在选择性很好的非唯一性索引),那么此时使用嵌套循环连接的执行效率就会非常高;但如果驱动表所对应的驱动结果集的记录数很多,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也不会高。

只要驱动结果集的记录数较少,那就具备了做嵌套循环连接的前提条件,而驱动结果集是在对驱动表应用了目标SQL中指定的谓词条件(如果有的话)后所得到的结果集,所以大表也可以作为嵌套循环连接的驱动表,关键看目标SQL中指定的谓词条件(如果有的话)能否将驱动结果集的数据量降下来。

嵌套循环连接有其他连接方法所没有的一个优点:嵌套循环连接可以实现快速响应,即它可以第一时间先返回已经连接过且满足连接条件的记录,而不必等待所有的连接操作全部做完后才返回连接结果。虽然排序合并连接和哈希连接也可以先返回已经连接过且满足连接条件的记录,而不必等待所有的连接操作都做完,但是它们并不是第一时间返回,因为排序合并连接要等到排完序后做合并操作时才能开始返回数据,而哈希连接则要等到驱动结果集所对应的Hash Table全部建完后才能开始返回数据。

如果Oracle使用的是嵌套循环连接,且在被驱动表的连接列上存在索引,那么Oracle在访问该索引时通常会使用单块读,这意味着嵌套循环连接的驱动结果集有多少条记录,Oracle就需要访问该索引多少次。另外,如果目标SQL中的查询列并不能全部从被驱动表的相关索引中获得,那么Oracle在做完嵌套循环连接后就还需要对被驱动表执行回表操作(即用连接结果集中每一条记录所含的ROWID去回表获取被驱动表中的相关查询列)。这个回表操作通常也会使用单块读,这意味着做完嵌套循环连接后的连接结果集有多少条记录,Oracle就需要回表多少次。

对于这种单块读而言,如果待访问的索引块或数据块不在Buffer Cache中,Oracle就需要耗费物理I/O去相应的数据文件中获取。显然,在单块读的数量不降低的情况下,如果能减少这种单块读所需要耗费的物理I/O数量,那么嵌套循环连接的执行效率也会随之提高。

为了提高嵌套循环连接的执行效率,在Oracle 11g中,Oracle引入了向量I/O(Vector I/O)。在引入向量I/O后,Oracle就可以将原先一批单块读所需要耗费的物理I/O组合起来,然后用一个向量I/O去批量处理它们,这样就实现了在单块读的数量不降低的情况下减少这些单块读所需要耗费的物理I/O数量,也就提高了嵌套循环连接的执行效率。

向量I/O的引入也反映在嵌套循环连接所对应的执行计划上。在Oracle 11g中,你会发现明明一次嵌套循环连接就可以处理完毕的SQL,但其执行计划的显示内容中嵌套循环连接的数量却由之前的一个变为了现在的两个。

这里还是以"1.2.4.1.1 内连接"中的测试表T1和T2为例来说明。我们在表T2的列COL2上创建一个名为IDX_T2的索引:

SQL>create index idx_t2 on t2(col2);

Index created

表T1、T2所在Oracle数据库的版本为Oracle 11.2.0.1:

SQL>select * from v$version;

BANNER

--------------------------------------------------------------------------------

Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

PL/SQL Release 11.2.0.1.0 - Production

CORE    11.2.0.1.0  Production

TNS for 32-bit Windows: Version 11.2.0.1.0 - Production

NLSRTL Version 11.2.0.1.0 - Production

之前在"1.2.4.1.1 内连接"中介绍的范例SQL 10为如下所示:

select t1.col1, t1.col2, t2.col3

from t1, t2

wheret1.col2=t2.col2 ;

我们现在在范例SQL 10中加入让其走嵌套循环连接的Hint(即/*+ ordered use_nl(t2) */),并实际执行一次:

SQL>set autotrace traceonly

SQL>select /*+ ordered use_nl(t2) */t1.col1, t1.col2, t2.col3

2    from t1, t2

3   wheret1.col2=t2.col2;

注意,上述执行计划的显示内容中关键字"NESTED LOOPS"出现了两次,这说明在Oracle 11gR2中执行上述SQL时确实用了两次嵌套循环连接。这里第二次嵌套循环连接的被驱动表部分所对应的执行步骤是"TABLE ACCESS BY INDEX ROWID | T2",这可能是因为Oracle想表达此时在通过ROWID回表访问表T2时是把一批ROWID组合起来后通过一个向量I/O批量回表,而不是每拿到一个ROWID就用一次单块读回表。

我们现在在范例SQL 10中加入OPTIMIZER_FEATURES_ENABLE Hint(即optimizer_features_enable('9.2.0'))后再次执行:

SQL>select /*+ optimizer_features_enable('9.2.0') ordered use_nl(t2) */t1.col1,t1.col2, t2.col3

2    from t1, t2

3   wheret1.col2=t2.col2;

上述OPTIMIZER_FEATURES_ENABLE Hint的作用是让解析SQL 10的优化器的版本回退到Oracle 9iR2,这样我们就可以观察到同一个SQL在Oracle 11g之前执行嵌套循环连接时的情况。

注意,上述执行计划的显示内容中关键字"NESTED LOOPS"只出现了一次,这说明在Oracle 9iR2中执行上述SQL时确实只用了一次嵌套循环连接。

1.2.4.2.3  哈希连接(1)

哈希连接(Hash Join)是一种两个表在做表连接时主要依靠哈希运算来得到连接结果集的表连接方法。

在Oracle 7.3之前,Oracle数据库中的常用表连接方法就只有排序合并连接和嵌套循环连接这两种,但这两种方法都各有其明显缺陷。对于排序合并连接,如果两个表在施加了目标SQL中指定的谓词条件(如果有的话)后得到的结果集很大且需要排序,则排序合并连接的执行效率一定不高;而对于嵌套循环连接,如果驱动表所对应的驱动结果集的记录数很大,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也会同样不高。

为了解决排序合并连接和嵌套循环连接在上述情形下执行效率不高的问题,同时也为了给优化器提供一种新的选择,Oracle在Oracle 7.3中引入了哈希连接。从理论上来说,哈希连接的执行效率会比排序合并连接和嵌套循环连接要高,当然,实际情况并不总是这样。

在Oracle 10g及其以后的Oracle数据库版本中,优化器(实际上是CBO,因为哈希连接仅适用于CBO)在解析目标SQL时是否考虑哈希连接是受限于隐含参数_HASH_JOIN_ENABLED,而在Oracle 10g以前,CBO在解析目标SQL时是否考虑哈希连接则是受限于参数HASH_JOIN_ENABLED。

_HASH_JOIN_ENABLED的默认值是TRUE,表示允许CBO在解析目标SQL时考虑哈希连接。当然,即使将该参数的值改成了FALSE,使用USE_HASH Hint依然可以让CBO在解析目标SQL时考虑哈希连接,这说明USE_HASH Hint的优先级比参数_HASH_JOIN_ENABLED的优先级要高。

如果两个表(这里将它们分别命名为表T1和表T2)在做表连接时使用的是哈希连接,则Oracle会依次顺序执行如下步骤。

(1)首先Oracle会根据参数HASH_AREA_SIZE、DB_BLOCK_SIZE和_HASH_MULTIBLOCK_IO_COUNT的值来决定Hash Partition的数量(Hash Partition是一个逻辑上的概念,它实际上是一组Hash Bucket的集合。所有Hash Partition的集合就被称为Hash Table,即一个Hash Table由多个Hash Partition所组成,而一个Hash Partition又是由多个Hash Bucket所组成的)。

(2)表T1和T2在施加了目标SQL中指定的谓词条件(如果有的话)后,得到的结果集中数据量较少的那个结果集会被Oracle选为哈希连接的驱动结果集,这里我们假设T1所对应的结果集的数据量相对较少,记为S;T2所对应的结果集的数据量相对较多,记为B。显然这里S是驱动结果集,B是被驱动结果集。

(3)接着Oracle会遍历S,读取S中的每一条记录,并对每一条记录按照该记录在表T1中的连接列做哈希运算。这个哈希运算会使用两个内置哈希函数,这两个哈希函数会同时对该连接列计算哈希值,我们把这两个内置哈希函数分别记为hash_func_1和hash_func_2,它们所计算出来的哈希值分别记为hash_value_1和hash_value_2。

(4)然后Oracle会按照hash_value_1的值把相应的S中的对应记录存储在不同Hash Partition的不同Hash Bucket里,同时和该记录存储在一起的还有该记录用hash_func_2计算出来的hash_value_2。注意,存储在Hash Bucket里的记录并不是目标表的完整行记录,只需要存储位于目标SQL中与目标表相关的查询列和连接列就足够了。我们把S所对应的每一个Hash Partition记为Si。

(5)在构建Si的同时,Oracle会构建一个位图(BITMAP),这个位图用来标记Si所包含的每一个Hash Bucket是否有记录(即记录数是否大于0)。

(6)如果S的数据量很大,那么在构建S所对应的Hash Table时,就可能会出现PGA的工作区(WORK AREA)被填满的情况。这时候Oracle会把工作区中包含记录数最多的Hash Partition写到磁盘上(TEMP表空间)。接着Oracle会继续构建S所对应的Hash Table,在继续构建的过程中,如果工作区又满了,则Oracle会继续重复上述动作,即挑选包含记录数最多的Hash Partition并写回到磁盘上。如果要构建的记录所对应的Hash Partition已经事先被Oracle写回磁盘,则此时Oracle就会去磁盘上更新该Hash Partition,即把该条记录和hash_value_2直接加到这个已经位于磁盘上的Hash Partition的相应Hash Bucket中。注意,极端情况下可能会出现只有某个Hash Partition的部分记录还在内存中,该Hash Partition的剩余部分和余下的所有Hash Partition都已经被写回到磁盘上。

(7)上述构建S所对应的Hash Table的过程会一直持续下去,直到遍历完S中的所有记录为止。

(8)接着,Oracle会对所有的Si按照它们所包含的记录数来排序,然后把这些已经排好序的Hash Partition按顺序依次且尽可能全部放到内存中(PGA的工作区),当然,如果实在放不下,放不下的那部分Hash Partition还是会位于磁盘上。我个人认为这个按照Si的记录数来排序的动作不是必须要做的,因为这个排序动作的根本目的就是为了尽可能多地把那些记录数较小的Hash Partition保留在内存中,而将那些已经被写回磁盘、记录数较大且现有内存已经放不下的Hash Partition保留在磁盘上,显然,如果所有的Si本来就都在内存中,也没发生过将Si写回到磁盘的操作,这里根本就不需要排序了。

(9)至此Oracle已经处理完S,现在可以开始处理B了。

(10)Oracle会遍历B,读取B中的每一条记录,并按照该记录在表T2中的连接列做哈希运算,这个哈希运算和步骤3中的哈希运算是一模一样的,即还是会用步骤3中的hash_func_1和hash_func_2,并且也会计算出两个哈希值hash_value_1和hash_value_2。

接着Oracle会按照该记录所对应的哈希值hash_value_1去Si里找匹配的Hash Bucket;如果能找到匹配的Hash Bucket,则Oracle还会遍历该Hash Bucket中的每一条记录,并校验存储于该Hash Bucket中的每一条记录的连接列,看是否是真的匹配(即这里要校验S和B中的匹配记录所对应的连接列是否真的相等,因为对于哈希运算而言,不同的值经过哈希运算后的结果可能是相同的)。如果是真的匹配,则上述hash_value_1所对应B中记录的位于目标SQL中的查询列和该Hash Bucket中的匹配记录便会组合起来,一起作为满足目标SQL连接条件的记录返回。如果找不到匹配的Hash Bucket,则Oracle就会去访问步骤5中构建的位图。

如果位图显示该Hash Bucket在Si中对应的记录数大于0,则说明该Hash Bucket虽然不在内存中,但它已经被写回磁盘,则此时Oracle就会按照hash_value_1的值把相应B中的对应记录也以Hash Partition的方式写回到磁盘上,同时和该记录存储在一起的还有该记录用hash_func_2计算出来的hash_value_2的值。如果位图显示该Hash Bucket在Si中对应的记录数等于0,则Oracle就无须把上述hash_value_1所对应B中的记录写回磁盘了,因为这条记录必然不满足目标SQL的连接条件。这个根据位图来决定是否将hash_value_1所对应B中的记录写回到磁盘的动作就是所谓的"位图过滤"(Oracle不一定会启用位图过滤,因为如果所有的Si本来就都在内存中,也没发生过将Si写回到磁盘的操作,那么这里Oracle就不需要启用位图过滤了)。我们把B所对应的每一个Hash Partition记为Bj。

(11)上述去Si中查找匹配Hash Bucket和构建Bj的过程会一直持续下去,直到遍历完B中的所有记录为止。

(12)至此Oracle已经处理完所有位于内存中的Si和对应的Bj,现在只剩下位于磁盘上的Si和Bj还未处理。

(13)因为在构建Si和Bj时用的是同样的哈希函数hash_func_1和hash_func_2,所以Oracle在处理位于磁盘上的Si和Bj的时候可以放心地配对处理,即只有对应Hash Partition Number值相同的Si和Bj才可能会产生满足连接条件的记录。这里我们用Sn和Bn来表示位于磁盘上且对应Hash Partition Number值相同的Si和Bj。

(14)对于每一对Sn和Bn,它们之中记录数较少的会被当作驱动结果集,然后Oracle会用这个驱动结果集Hash Bucket里记录的hash_value_2来构建新的Hash Table,另外一个记录数较多的会被当作被驱动结果集,然后Oracle会用这个被驱动结果集Hash Bucket里记录的hash_value_2去上述构建的新Hash Table中找匹配记录。注意,对每一对Sn和Bn而言,Oracle始终会选择它们中记录数较少的来作为驱动结果集,所以每一对Sn和Bn的驱动结果集都可能会发生变化,这就是所谓的"动态角色互换"。

(15)步骤14中如果存在匹配记录,则该匹配记录也会作为满足目标SQL连接条件的记录返回。

(16)上述处理Sn和Bn的过程会一直持续下去,直到遍历完所有的Sn和Bn为止。

对于哈希连接的优缺点及适用场景,我们有如下总结。

哈希连接不一定会排序,或者说大多数情况下都不需要排序。

哈希连接的驱动表所对应的连接列的可选择性应尽可能好,因为这个可选择性会影响对应Hash Bucket中的记录数,而Hash Bucket中的记录数又会直接影响从该Hash Bucket中查找匹配记录的效率。如果一个Hash Bucket里所包含的记录数过多,则可能会严重降低所对应哈希连接的执行效率,此时典型的表现就是该哈希连接执行了很长时间都没有结束,数据库所在数据库服务器上的CPU占用率很高,但目标SQL所消耗的逻辑读却很低,因为此时大部分时间都耗费在了遍历上述Hash Bucket里的所有记录上,而遍历Hash Bucket里的记录这个动作发生在PGA的工作区里,所以不耗费逻辑读。

哈希连接只适用于CBO,它也只能用于等值连接条件(即使是哈希反连接,Oracle实际上也是将其转换成了等价的等值连接)。

哈希连接很适合于小表和大表之间做表连接且连接结果集的记录数较多的情形,特别是在小表的连接列的可选择性非常好的情况下,这时候哈希连接的执行时间就可以近似看作是和全表扫描那个大表所耗费的时间相当。

当两个表做哈希连接时,如果在施加了目标SQL中指定的谓词条件(如果有的话)后得到的数据量较小的那个结果集所对应的Hash Table能够完全被容纳在内存中(PGA的工作区),则此时的哈希连接的执行效率会非常高。

1.2.4.2.3  哈希连接(2)

我们可以借助于10104事件所产生的trace文件来观察目标SQL在做哈希连接时的大致过程和一些统计信息(比如用了多少个Hash Partition、多少个Hash Bucket以及各个Hash Bucket都分别有多少条记录等),10104事件在实际诊断哈希连接的性能问题时非常有用。

使用10104事件观察目标SQL做哈希连接的具体过程为:

oradebug setmypid

oradebug event 10104 trace name context forever, level 1

set autotrace traceonly

实际执行目标SQL(必须要实际执行该SQL,不能用explain plan for)

oradebug tracefile_name

一个典型的10104事件所产生的trace文件内容如下所示:

kxhfInit(): enter

kxhfInit(): exit

*** RowSrcId: 1 HASH JOIN STATISTICS (INITIALIZATION) ***

Join Type: INNER join

Original hash-area size: 3642760

Memory for slot table: 2826240

Calculated overhead for partitions and row/slot managers: 816520

Hash-join fanout: 8

Number of partitions: 8

Number of slots: 23

Multiblock IO: 15

Block size(KB): 8

Cluster (slot) size(KB): 120

Minimum number of bytes per block: 8160

Bit vector memory allocation(KB): 128

Per partition bit vector length(KB): 16

……省略显示部分内容

Slot table resized:old=23wanted=12got=12unload=0

*** RowSrcId: 1 HASH JOIN RESIZE BUILD (PHASE 1) ***

Total number of partitions: 8

Number of partitions which could fit in memory: 8

Number of partitions left in memory: 8

Total number of slots in in-memory partitions: 8

kxhfResize(enter): resize to 14 slots (numAlloc=8,max=12)

kxhfResize(exit): resized to 14 slots (numAlloc=8,max=14)

set work area size to: 2215K (14 slots)

*** RowSrcId: 1 HASH JOIN BUILD HASH TABLE (PHASE 1) ***

Total number of partitions: 8

Number of partitions left in memory: 8

Total number of rows in in-memory partitions: 1000

(used as preliminary number of buckets in hash table)

Estimated max # of build rows that can fit in avail memory: 79800

### Partition Distribution ###

Partition:0    rows:120        clusters:1      slots:1kept=1

Partition:1    rows:122        clusters:1      slots:1kept=1

……省略显示部分内容

Partition:6    rows:118        clusters:1      slots:1kept=1

Partition:7    rows:137        clusters:1      slots:1kept=1

*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***

Revised number of hash buckets (after flushing): 1000

Allocating new hash table.

*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***

Requested size of hash table: 256

Actual size of hash table: 256

Number of buckets: 2048

Match bit vector allocated: FALSE

*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***

Total number of rows (may have changed): 1000

Number of in-memory partitions (may have changed): 8

Final number of hash buckets: 2048

Size (in bytes) of hash table: 8192

qerhjBuildHashTable(): done hash-table onpartition=7,index=0last_slot#=3rows=137total_rows=137

qerhjBuildHashTable(): done hash-table onpartition=6,index=1last_slot#=4rows=118total_rows=255

……省略显示部分内容

qerhjBuildHashTable(): done hash-table onpartition=1,index=6last_slot#=2rows=122total_rows=880

qerhjBuildHashTable(): done hash-table onpartition=0,index=7last_slot#=5rows=120total_rows=1000

kxhfIterate(end_iterate):numAlloc=8,maxSlots=14

*** (continued) HASH JOIN BUILD HASH TABLE (PHASE 1) ***

### Hash table ###

# NOTE: The calculated number of rows in non-empty buckets may be smaller

#       than the true number.

Number of buckets with   0 rows:       1249

Number of buckets with   1 rows:        626

Number of buckets with   2 rows:        149

Number of buckets with   3 rows:         21

Number of buckets with   4 rows:          3

Number of buckets with   5 rows:          0

……省略显示部分内容

Number of buckets with between  90 and  99 rows:          0

Number of buckets with 100 or more rows:          0

### Hash table overall statistics ###

Total buckets: 2048 Empty buckets: 1249 Non-empty buckets: 799

Total number of rows: 1000

Maximum number of rows in a bucket: 4

Average number of rows in non-empty buckets: 1.251564

Disabled bitmap filtering: filteredrows=0minimumrequired=50outof=1000

qerhjFetch: max probe row length (mpl=0)

*** RowSrcId: 1, qerhjFreeSpace(): free hash-join memory

kxhfRemoveChunk: remove chunk 0 from slot table

注意上述显示内容中用粗体标出的部分,如"Number of in-memory partitions (may have changed): 8"、"Final number of hash buckets: 2048"、"Total buckets: 2048 Empty buckets: 1249 Non-empty buckets: 799"、"Total number of rows: 1000"、"Maximum number of rows in a bucket: 4"、"Disabled bitmap filtering: filtered rows=0 minimum required=50 out of=1000"等,这说明上述哈希连接驱动结果集的记录数为1,000,共有8个Hash Partition、2,048个Hash Bucket,这2,048个Hash Bucket中有1,249个是空的(即没有记录),799个有记录,包含记录数最多的那个Hash Bucket所含记录的数量为4,以及上述哈希连接并没有启用位图过滤。

1.2.4.2.4  笛卡儿连接

笛卡儿连接(Cross Join)又称为笛卡儿乘积(Cartesian Product),它是一种两个表在做表连接时没有任何连接条件的表连接方法。

如果两个表(这里将它们分别命名为表T1和表T2)在做表连接时使用的是笛卡儿连接,则Oracle会依次顺序执行如下步骤。

(1)首先以目标SQL中指定的谓词条件(如果有的话)访问表T1,此时得到的结果集我们记为结果集1,这里假设结果集1的记录数为m。

(2)接着以目标SQL中指定的谓词条件(如果有的话)访问表T2,此时得到的结果集我们记为结果集2,这里假设结果集2的记录数为n。

(3)最后对结果集1和结果集2执行合并操作,从中取出匹配记录来作为笛卡儿连接的最终执行结果。这里的特殊之处在于对于笛卡儿连接而言,因为没有表连接条件,所以在对结果集1和结果集2执行合并操作时,对于结果集1中的任意一条记录,结果集2中的所有记录都满足条件,即它们都会是匹配记录,所以上述笛卡儿连接的连接结果的记录数就是m和n的乘积(即m n)。

从上述笛卡儿连接的执行过程我们可以看出,笛卡儿连接实际上是一种特殊的"合并连接",这里的"合并连接"和排序合并连接类似,只不过笛卡儿连接不需要排序,并且在执行合并操作时没有连接条件而已。关于这一点,实际上可以从笛卡儿连接所对应的执行计划中看出些端倪。

这里还是以"1.2.4.1.1 内连接"中的测试表T1和T2为例来说明。表T1和T2还是和原来一样,各有3条记录,我们执行如下不带连接条件的SQL:

SQL>set autotrace on

SQL>select t1.col1,t2.col3 from t1,t2;

上述SQL的执行结果包含9条记录,这刚好就是表T1和表T2记录数的乘积(9 = 3 3)。

上述执行计划和排序合并连接所对应的执行计划非常相似,并且Id = 1的Operation列的值为"MERGE JOIN CARTESIAN",只是在排序合并连接的合并部分所对应的关键字"MERGE JOIN"后添加了一个单词"CARTESIAN",所以我们才说笛卡儿连接实际上就是一种特殊的"合并连接"。

标准SQL用关键字"CROSS JOIN"来表示笛卡儿连接,这里我们用标准SQL的方式来改写上述SQL并再执行一次:

SQL>select t1.col1,t2.col3 from t1 cross join t2;

从上述显示内容可以看出,用标准SQL改写后其执行结果和执行计划与原先的确实是一模一样的。

对于笛卡儿连接的优缺点及适用场景,我们有如下总结。

(1)笛卡儿连接的出现通常是由于目标SQL中漏写了表连接条件,所以笛卡儿连接一般是不好的,除非刻意这样做(比如有些情况下可以利用笛卡儿连接来减少对目标SQL中大表的全表扫描次数)。

(2)有时候出现笛卡儿连接是因为在目标SQL中使用了ORDERED Hint,同时在该SQL的SQL文本中位置相邻的两个表之间又没有直接的关联条件。

(3)有时候出现笛卡儿连接是因为目标SQL中相关表的统计信息不准。比如三个表T1、T2、T3做表连接,T1和T2的连接条件为T1.ID1=T2.ID1,T2和T3的连接条件为T2.ID2=T3.ID2,同时在表T2的连接列ID1和ID2上存在一个包含这两个连接列的组合索引。如果表T1和T3的统计信息不准,导致Oracle认为表T1和T3都只有很少量的记录(比如都只有1条记录),则此时Oracle很可能会选择先对表T1和T3做笛卡儿连接,然后再和表T2做表连接。因为Oracle认为表T1和T3做笛卡儿连接后连接结果集的Cardinality的值是1,并且连接结果中会同时包含列ID1和列ID2,这意味着此时Oracle就可以利用表T2中的上述组合索引了。这种笛卡儿连接通常是有问题的,还是拿这个例子来说,如果表T1和表T3的实际记录数并不都是1,而全部是1000,那么此时表T1和表T3做笛卡儿连接的连接结果集的Cardinality的值就将是100万,显然这种情况下如果还是按照笛卡儿连接的方式来执行的话,则该SQL的执行效率就会受到严重影响。

oracle 表名拼接_Oracle之3种表连接方式(排序合并连接、嵌套循环、哈希连接)...相关推荐

  1. 1.python 根据 oracle字段名和类型 生成 hive建表语句

    一.问题 遇到一个场景,要根据oracle表结构创建hive表. 本来想写一个根据oracle信息和表名自动在hive中创建表的脚本.如果在集群运行,python操作oracle的cx_Oracle在 ...

  2. MySQL 表(表名含有大写字母的所有表)

    表名含有大写字母的所有表: select table_name  from information_schema.tables  where table_schema='databasename' a ...

  3. oracle修改表字段名备注_oracle 增加修改删除表字段,添加修改表、以及表中字段的备注...

    添加字段的语法:alter table tablename add (column datatype [default value][null/not null],-.); 修改字段的语法:alter ...

  4. oracle 表名拼接_oracle_根据表名拼装语句

    1.-----批量删除用户下所有表数据------保留表结构 eg: 批量删除用户下的所有表数据 SELECT 'TRUNCATE TALBE '||TABLE_NAME||';' FROM USER ...

  5. oracle 表字段顺序_Oracle数据库如何修改表中字段顺序

    Oracle数据库如何修改表中字段顺序 发布时间:2020-07-09 15:53:15 来源:亿速云 阅读:166 本篇文章给大家分享的是有关Oracle数据库如何修改表中字段顺序,小编觉得挺实用的 ...

  6. mysql desc 显示备注_MySQL_Mysql中返回一个数据库的所有表名,列名数据类型备注,desc 表名; show columns from 表名; d - phpStudy...

    Mysql中返回一个数据库的所有表名,列名数据类型备注 desc 表名; show columns from 表名; describe 表名; show create table 表名; use in ...

  7. mysql表名不区分大小写_设置mysql表名不区分大小写

    查看mysql版本: [root@localhost logs]# mysql -V mysql Ver14.14 Distrib 5.6.37, for linux-glibc2.12 (x86_6 ...

  8. mysql 表名不加单引号_当表名“ match”没有用单引号引起来时,MySQL引发错误?...

    不要使用单引号.您需要在表名匹配周围使用反引号,因为它是MySQL中的保留名.以下是发生的错误:mysql> select *from match; ERROR 1064 (42000) : Y ...

  9. mysql 把表名改成大写_mysql 把表名自动改为大写

    最近要做sql标准化,要求所有的表名,字段名大写,我总不能把代码给改一边把,于是找到druid,使用语法分析,拿到sql,把表名改为大写. package com.topnet.dao.util; i ...

最新文章

  1. javascript小数相减会出现一长串的小数位数的原因
  2. CircleProgressView
  3. 一个关于小程序Iot的具体实现(MQTT版)
  4. 计算机图形学的核心领域,计算机图形学基础知识重点整理.pdf
  5. 刚刚,改造了下BaseDao……
  6. 实例26:python
  7. GitHub人脸属性编辑神器横空出世!让你爱上异性的自己!
  8. c++ cstring 转换 char_C语言进阶之路:字符串与整数之间的转换!
  9. 17APLab4:图标、菜单、加速键、消息框 python
  10. UICollectionViewCell 所遇到的问题
  11. 编程必会的100个代码大全,建议收藏
  12. 数独终盘生成器(调试成果)
  13. 计算机学院军训横幅,2020大学军训横幅标语句子精选100句
  14. dellt130服务器做系统,戴尔Dell R330;T130安装系统后键盘鼠标不能使用
  15. http报文格式、GET与POST的区别
  16. 换个格式输出整数 蓝桥杯 C语言
  17. A记录和CNAME记录的区别
  18. shell 脚本处理多行文本的记录 -- awk
  19. 工作随笔:如何开展测试工作
  20. 等额本息还款方式的年利率计算方法及java代码实现

热门文章

  1. Hadoop 信息集成平台,让大数据分析更简单!
  2. linux chromium安装falsh插件
  3. [PHP]如何在百度(BAE)和新浪(SAE)的云平台使用PHP连接MySQL并返回结果数据
  4. 半自动化运维之快速连接到指定环境(一)
  5. SecureCRT用证书方式登录
  6. Algorithm Gossip (21) 最大访客数
  7. R 语言在数据处理上的禀赋之——独特的数据类型
  8. JavaScript随机排序算法1
  9. JS中对于prototype的理解
  10. fragment中嵌套viewpager,vierpager中有多个fragment,不显示 .