Oracle优化器的基础知识(二)
一、 表连接
顾名思义,表连接就是指多个表之间用连接条件连接在一起,使用表连接的目标SQL的目的就是从多个表获取存储在这些表中的不同维度的数据。体现在SQL语句上,含表连接的目标SQL的from部分会出现多个表,而这些SQL的where条件部分则会定义具体的表连接条件。
当优化器解析含表连接的目标SQL时,它除了会根据目标SQL的SQL文本的写法来决定表连接的类型之外,还必须决定如下三件事情才能得到最终的执行计划。
1.表连接顺序
不管目标SQL中有多少个表做表连接,Oracle在实际执行该SQL时都只能先两两做表连接,再依次执行这样的两两表连接过程,直到目标SQL中所有的表都已连接完毕。从严格意义上来说,这里的表连接顺序包含两层含义:一层含义是当两个表做表连接时,优化器需要决定这两个表中谁是驱动表(outer table),谁是被驱动表(inner table);另外一层含义是当多表(超过两个以上的表)做表连接时,优化器需要决定这些表中谁和谁先做表连接,然后决定这个表连接结果所在的结果集再和剩余表中的哪一个再做表连接,这个两两表连接的过程会一直持续下去,直到目标SQL中所有的表都已经连接完为止。
2.表连接方法
在Oracle数据库中,两个表之间的表连接方法有排序合并(sort merge join)、嵌套循环连接(nested loops join)、哈希连接(hash join)和笛卡儿连接(cross join)这4种,所以优化器在解析含表连接的目标SQL时,都需要从上述四种方法中选择一种,作为每一对表两两做表连接时所需要采用的方法。
3.访问单表的方法
对于优化器而言,仅决定表连接顺序和表连接方法是不够的,这还不中以得到目标SQL的最终执行计划,因为优化器在对目标SQL中的各个表两两做表连接时,还必须决定如何去获取存储在这些表里的不同维度的数据,即优化器还要决定访问单表的方法。比如在访问某个单表时,是采用全表扫描还是走索引,如果是走索引,应该采用什么样的索引访问方法等。
1.1 表连接的类型
通常情况下,我们可以认为Oracle数据库中的表连接分为内连接和外连接这两种类型,表连接的类型会直接决定表连接的结果,而目标SQL的SQL文本的写法又直接决定了表连接的类型。
1.1.1 内连接
内连接(Inner Join)是指表连接的结果只包含那些完全满足连接条件的记录。对于包含表连接的目标SQL而言,只要其where条件中没有写那些标准SQL中定义或者Oracle中自定义的表示外连接的关键字(比如标准SQL中的left outer join、right outer join、full outer join,或者Oracle中自定义的用来表示外连接的关键字"(+)"),则该SQL的连接类型就是内连接。
Oracle自定义的内连接写法:
目标表1,目标表2 where 连接条件
标准SQL中内连接是用JOIN ON或者JOIN USING。
JOIN ON的语法:
目标表1 join 目标表2 on (连接条件)
JOIN USING的语法:
目标表1 join 目标表2 using(连接列集合)。
对于使用JOIN USING的目标SQL而言,如果有多个连接列,其语法中"(连接列集合)"里的各个连接列之间应使用逗号来分隔。需要注意的时,使用JOIN USING的连接语法,如果连接列同时又出现在查询列中,则该连接列前不能带上表名或者表名的别名(alias),否则Oracle会报错(ORA-25154)。
标准SQL中还有一种特殊的JOIN USING,我们称之为NATURAL JOIN,其含义是使用NATURAL JOIN的表连接的连接列是表连接的两个表所有的同名列。语法:
目标表1 natural join 目标表2
这实际相当于目标表1 join 目标表2 using(目标表1和目标表2的所有同名列集合)。使用NATURAL JOIN好外是无须写连接列集合,但其坏处是增加了表连接的执行结果出错的风险,因为两个表之间的同名列不一定在含义上就完全相同,也许只是恰好同名,而即使含义相同,也不一定就需要将它们作为连接列。
1.1.2 外连接
外连接(Outer Join)是对内连接的一种扩展,它是指表连接的连接结果除了包含那些完全满足连接条件的记录之外还会包含驱动表中所有不满足该条件的记录。
标准SQL中的外连接分为左外连接(Left Outer Join)、右连接(Right Outer Join)和全连接(Full Outer Join)这三种,它们在标准SQL中对应的关键字分别为left outer join、right outer join、full outer join,都可以和JOIN ON 或JOIN USING连用。
左连接的语法:
目标表1 left outer join 目标表2 on(连接条件) 或目标表1 left outer join 目标表2 using (连接列集合)
其含义是目标表1和目标表2按括号中的连接条件来做表连接,位于关键字左边的表1作为驱动表(outer table),此时的连接结果包含了表1和表2中所有满足该连接条件的记录外,还会包含驱动表(表1)中所有不满足该连接条件的记录,同时,驱动表中所有不满足该连接条件的记录所对应的被驱动表(表2)中的查询列均会以NULL值来填充。
右连接的语法:
目标表1 right outer join 目标表2 on(连接条件) 或目标表1 right outer join 目标表2 using (连接列集合)
含义与左连接相似,不过,这次位于关键字右表的表2为驱动表。
全连接语法:
目标表1 full outer join 目标表2 on(连接条件) 或目标表1 full outer join 目标表2 using (连接列集合)
其含义是目标表1和目标表2按括号中的连接条件来做表连接。此时的连接结果除了包含表1和表2中所有满足该连接条件的记录外,还会包含目标表1和目标表2中所有不满足该连接条件的记录,同时,表1和表2中所有不满足该连接条件的记录所对就的另外一个表中的查询列均会以NULL值来填充。
上面介绍的范例SQL中除了带连接条件外,并没有带其他额外的限制条件。如果目标SQL中除了表连接条件之外还带了额外的限制条件,则目标SQL中表连接的类型和该额外限制条件在目标SQL的SQL文本中出现的位置都可能会对最终执行结果产生影响。
内连接添加其他限制条件实例:
对内连接而言,除了表连接条件之外的额外限制条件在目标SQL的SQL文本中所处的位置不会影响该SQL的实际执行结果。
外连接添加其他限制条件实例:
对于外连接而言,如果额外限制条件在外连接关键字对应的括号内,这表示该限制条件会在表t1和表t2做右连接之前就被应用在表t1上,而如果额外限制条件在外连接关键字对应的括号外,表示该限制条件在表t1和表t2做完右连接后,才会被应用在表t1和表t2的连接结果集上。
所以,对于外连接而言,除了表连接条件之外的额外限制条件在目标SQL的SQL文本中所处的位置确实可能会影响该SQL的实际执行结果。
和标准SQL里表示外连接的语法不同,Oracle用自定义的关键字"(+)"来表示外连接。关键字"(+)"的位置在目标SQL连接条件中某一个表的连接列后面,其含义是关键字"(+)"出现在哪个表的连接列后面,就表明哪个表会以NULL值来填充那不满足连接条件找位置该表中的查询列,此时应该以关键字"(+)"对应的表作为外连接的驱动表,这是的关键是哪个表是驱动表!
之前提到过:对于外连接而言,表连接条件之外的额外限制条件在目标SQL的SQL文本中所处位置的不同可能会影响该SQL的实际执行结果。那如果使用Oracle自定义的关键字"(+)"来表示外连接的话,那么如何体现呢?很简单,Oracle是通过在额外限制条件的目标列的后面带上同样的关键字"(+)"来体现出上述影响的:
select t1.col1,t1.col2,t2.col3
from t1,t2
where t1.col2(+)=t2.col2
and t1.col1(+)=1;
前面提到的NATURAL JOIN不仅适用于内连接,也同样适用于外连接:
select t1.col1,col2,t2.col3
from t1 natural left outer join t2 ;
1.2 表连接的方法
之前介绍过,优化器在解析含表连接的目标SQL时,当它根据目标SQL的SQL文本的写法决定表连接的类型之后,接下来要做的事情之一就是决定表连接的方法。
在Oracle数据库中,两个表之间的表连接方法有排序合并连接、嵌套循环连接、哈希连接和笛卡儿连接这四种。这四种表连接各有优缺点,也各有其适用场景,接下来分别介绍它们
1.2.1 排序合并连接
排序合并连接(Sort Merge Join)是一种两个表在做表连接时用排序操作(Sort)和合并操作(Merge)来得到连接结果集的表连接方法。
如果两个表(假如为T1和T2)做表连接时使用的是排序合并连接,则Oracle会依次顺序执行如下步骤:
首先以目标SQL中指定的谓词条件(如果有的话)去访问表T1,然后对访问结果按照表T1中的连接来排序,排好序后的结果集我们记为结果集1。
接着以目标SQL中指定的谓词条件(如果有的话)去访问表T2,然后对访问结果按照表T2中的连接来排序,排好序后的结果集我们记为结果集2。
最后对结果集1和结果集2执行合并操作,从中取出匹配记录来作为排序合并连接的最终执行结果。
对于排序合并连接的优缺点及适用场景,总结如下:
通常情况下,排序合并连接的执行效率会远不如哈希连接,但前者的使用范围更广,因为哈希连接通常只能用于等值连接,而排序合并连接并不能用于其他条件(例如<、<=、>、>=)。
通常情况下,排序合并连接并不短途OLTP类型的系统,其本质原因是因为对OLTP类型的系统而言,排序是非常昂贵的操作,当然,如果能避免排序操作,那么即使是OLTP类型的系统,也还是可以使用排序合并连接的。比如两个表虽然是排序合并连接,但实际上它们并不需要排序,因为这两个表各自的连接列上都存在索引。
从严格意义上说,排序合并连接不存在驱动表的概念。
1.2.2 嵌套循环连接
嵌套循环连接(Nested Loops Join)是一种两个表在做表连接时依靠两层嵌套循环(分别为外层循环和内层循环)来得到连接结果集的表连接方法。
如果两个表(假如为T1和T2)在做表连接时使用的是嵌套循环连接,则Oracle会依次顺序执行如下步骤:
首先,优化器会按照一定的规则来决定表T1和T2中谁是驱动表、谁是被驱动表。驱动表用于外层循环,被驱动表用于内存循环。这是假设驱动表是T1,被驱动表是T2。
接着以目标SQL中指定的谓词条件(如果有的话)去访问驱动表T1,访问驱动表T1后得到的结果集我们记为驱动结果集1。
然后遍历驱动结果集1并同时遍历被驱动表T2,即先取出驱动结果集1中的第1条记录,接着遍历被驱动表T2并按照连接条件去判断T2中是否存在匹配的记录,然后再取出驱动结果集1中的第2条记录,按照同样的连接条件再去遍历被驱动表T2并判断T2中是否还存在匹配的记录,直到遍历完驱动结果集1中所有的记录为止。这里的外层循环是指遍历驱动结果集1所对应的循环,内层循环是指遍历被驱动表T2所对应的循环。显然,外层循环所对应的驱动结果集1有多少条记录,遍历被驱动表T2的内层循环就要做多少次,这就是所谓的"嵌套循环"的含义。
嵌套循环连接的优缺点及适用场景总结如下:
从上述嵌套循环连接的具体执行过程可以看出:如果驱动表所对应的驱动结果集的记录数较少,同时被驱动表的连接列上又存在唯一性索引(或者在被驱动表的连接列上存在选择性很的的非唯一性索引),那么此时使用嵌套循环连接的执行效率就会非常高;但如果驱动表所对应的驱动结果集的记录数很多,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也不会高。
只要驱动结果集的记录数较少,那就具备了做嵌套循环连接的前提条件,而驱动结果集是在对驱动表应用了目标SQL中指定的谓词条件(如果有的话)后所得到的结果集,所以大表也可以作为嵌套循环连接的驱动表,关键看目标SQL中指定的谓词条件(如果有的话)能否将驱动结果集的数据量降下来。
嵌套循环连接有其他连接方法所没有的一个优点:嵌套循环连接可以实现快速响应,即它可以第一时间返回已经连接过具满足连接条件的记录,而不必等待所有的连接操作全部做完才返回连接结果。虽然排序合并连接也可以,但它们并不是第一时间返回,因为排序合并连接要等到排序完后做合并操作时才能开始返回数据,而哈希连接则要等到驱动结果集所对应的Hash Table全部建完后才能开始返回数据。
如果Oracle使用的是嵌套循环连接,且在被驱动表的连接列上存在索引,那么Oracle在访问索引时通常会使用单块读,这意味着嵌套循环连接的驱动结果集有多少条记录,Oracle就会需要访问该索引多少次。另外,如果目标SQL中的查询列并不能全部从驱动表的相关索引中获得,那么Oracle在做完嵌套循环连接后还需要对被驱动表执行回表操作。这个回青操作通常也会使用单块读,这意味着做完嵌套循环连接后的连接结果集有多少条记录,Oracle就需要回表多少次。
为了提高嵌套循环连接的执行效率,在Oracle 11g中,Oracle引入了向量I/O(Vector I/O)。在引入向量I/O后,Oracle就可以将原先一批单块读所需要耗费的物理I/O结合起来,然后用一个向量I/O去批处理它们,这样就实现了在单块读的数量不降低的情况下减少这些单块读所需要耗费的物理I/O数量,也就提高了嵌套循环连接的执行效率。
1.2.3 哈希连接
哈希连接(Hash Join)是一种两个表在做表连接时主要依靠哈希运算来得到连接结果集的表连接方法。
在Oracle7.3之前,Oracle数据库中常用的表连接方法就只有排序合并连接和嵌套循环连接这两种,但这两种方法都各有其明显缺陷。对于排序合并连接,如果两个表在施加了目标SQL中指定的谓词条件(如果有的话)后得到的结果集很大且需要排序,则排序合并连接的执行效率一定不高;而对于嵌套循环连接,如果驱动表所对应的驱动结果集的记录数很大,即便在被驱动表的连接列上存在索引,此时使用嵌套循环连接的执行效率也会同样不高。为了上述情形下效率不高的问题,同时也为了给优化器提供一种新的选择,Oracle在7.3中引入了哈希连接。从理论上来说,哈希连接的执行效率会比排序合并连接和嵌套循环连接要高,当然,实际情况并不总是这样。
在Oracle 10g及其以后的Oracle数据库版本中,优化器(实际上是CBO,因为哈希连接仅适用于CBO)在解析目标SQL时是否考虑哈希连接是受限于隐含参数_HASH_JOIN_ENABLED,而在Oracle10g以前,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会依次顺序执行如下步骤:
首先Oracle会根据参数HASH_AREAS_SIZE、DB_BLOCK_SIZE和_HASH_MULTIBLOCK_IO_COUNT的值来决定Hash Partition的数据(Hash Partition是一个逻辑上的概念,它实际上是一组Hash Bucket的集合。所有Hash Partition的集合就被称为Hash Table,即一个Hash Table由多个Hash Partition所组成,而一个HashPartition又由多个Hash Bucket所组成的)。
表T1和T2在施加了目标SQL中指定的谓词条件(如果有的话)后,得到的结果集中数量较少的那个结果集会被Oracle选为哈希连接的驱动结果集,这里我们假设T1所对应的结果集的数据量相对较少,记为S;T2所对应的结果集的数据相对较多,记为B。显然这里S是驱动结果集,B是被驱动结果集。
接着Oracle会遍历S,读取S中的每一条记录,并对每一条记录按照该记录在表T1中的连接列做哈希运算。这个哈希运算会使用两个内置哈希函数,这两个哈希函数会同时对该连接列计算哈希值,我们把这两个内置哈希函数分别记为hash_func_1和hash_func_2,它们所计算出来的哈希值分别记为hash_vale_1和hash_value_2。
然后Oracle会按照hash_value_1的值把相应的S中的对应记录存储在不同的Hash Partition的不同Hash Bucket里,同时和该记录存储在一起的还有该记录用hash_func_2计算出来的hash_value_2。注意,存储在Hash Bucket里的记录并不是目标表的完整行记录,只需要存储位置目标SQL中与目标表相关的查询列和连接列就足够了。我们把S所对应的每一个Hash Partition记为Si。
在构建Si的同时,Oracle会构建一个位图(BITMAT),这个位置用来标记Si所包含的每一个Hash Bucket是否记录(即记录数是否大于0)。
如果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_vale_2直接加到这个已经位于磁盘上的Hash Partition的相应Hash Bucket中。注意,极端情况下可能会出现只有某个Hash Partition的部分记录不觉 在内存中,该Hash Partition的剩余部分和余下的所有Hash Partition都已经被写回到磁盘上。
上述构建S所对应的Hash Table的过程会一直持续下去,直到遍历完S中的所有记录为止。
接着,Oracle会对所有的Si按照它们所包含的记录数来排序,然后把这些已经诽好序的Hash Partition按顺序依次且尽可能全部放到内存中(PGA的工作区),当然,如果实在放不下,放不下的那部分Hash Partition还是会位于磁盘上。
至此Oracle已经处理完S,现在可以开始处理B了。
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中的每一条记录的连接列,看是否是真的匹配(即这里要校验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中的对应记录也可以Hahs Partition的方式写回到磁盘上,同时和该记录存储在一起的还有该记录用hash_func_2计算出来的hash_value_2的值。如果位图显示该Hash Bucket在Si中对应 的记录数等于0,则Oralce就无须把上述hash_value_1所对应B中的记录写回磁盘了,因为这条记录必须不满足目标SQL的连接条件。这个根据位置来决定是否将hash_value_1所对就B的记录写回到磁盘的动作就是所谓的"位图过滤"(Oralce不一定会启用位图过滤,因为如果所有的Si本来就都在内存中,也没发生过将Si写回到磁盘的操作,那么这里Oracle就不需要启用位图过滤了)。我们把B所对应的每一个Hash Partition记为Bj。
上述去Si中查找匹配Hash Bucket和构建Bj的过程会一直持续下去,直到遍历完B中的所有记录为止。
至此Oracle已经处理完所有位于内存中的Si和对应的Bj,现在只剩下位于磁盘上的si和Bj还未处理。
因为在构建Si和Bj时用的是同样的哈希函数hash_func_1和hash_func_2,所以Oracle在处理位于磁盘上的Si和Bj的时候可以放心地配对处理,即只有对应Hash Partition Number值相同的Si和Bj才可能会产生满足连接条件的记录。这里我们用Sn和Bn来表示位于磁盘上且对应Hash Partition Number值相同的Si和Bj。
对于每一对Sn和Bn,它们之中记录数较少的会被当作驱动结果集,然后Oracle会用这个驱动结果集Hash Bucket里的记录的hash_vale_2来构建新的Hash Table,另外一个记录数较多的会被当作被驱动结果集,然后Oracle会用这个被驱动结果集Hash Bucket里记录的hash_value_2去上述构建的新的Hash Table中找匹配记录。注意,对每一对Sn和Bn而言,Oracle始终会选择它们中记录数较少的来作为驱动结果集,所以每一对Sn和Bn的驱动结果集都可能会发生变化,这就是所谓的"动态角色互换"。
步骤14中如果存在匹配记录,则该匹配记录会作为满足目标SQL连接条件的记录返回。
上述处理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 笛卡儿连接
笛卡儿连接(Cross Join)又称为笛卡儿乘积(Caresian Product),这是一种两个表在做表连接时没有任何连接条件的表连接方法。
如果两个表(假如为T1和T2)在做表连接时使用的是笛卡儿连接,则Oracle会依次顺序执行如下步骤:
首先以目标SQL中指定的谓词条件(如果有的话)访问表T1,此时得到的结果集我们记为结果集1,这里假设结果集1的记录数为m。
接着以目标SQL中指定的谓词条件(如果有的话)访问表T2,此时得到的结果集我们记为结果集2,这里假设结果集2的记录数为n。
最后对结果集1和结果集2执行合并操作,从中取出匹配记录来作为笛卡儿连接的最终执行结果。这里的特殊之处在于对于笛卡儿连接而言,因为淌有表连接条件,所以在对结果集1和结果集2执行合并操作时,对于结果集1中的任意一条记录,结果集2中的所有记录都满足条件,即它们都会是匹配记录,所以上述笛卡儿连接的连接结果的记录数就是m和n的乘积(即m×n)。
语句示例:select t1.col1,t2.col3 from t1,t2;
标准SQL用关键字"CROSS JOIN"来表示笛卡儿连接,如select t1.col1,t2.col3 from t1 cross join t2;
对于笛卡儿连接的优缺点及适用场景总结如下:
笛卡儿连接的出现通常是由于目标SQL中漏写了表连接条件,所以笛卡儿连接一般是不好的,除非刻意这样做(比如有些情况下可以利用笛卡儿连接来减少对目标SQL中大表的全表扫描次数)。
有时候出现笛卡儿连接是因为目标SQL中使用了ORDERED Hint,同时在该SQL的SQL文本中位置相邻的两个表之间又没有直接的关联条件。
有时候出现笛卡儿连接是因为目标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的SQL优化》