一、创建两张实验用表:wireless_site.merchant和wireless_site.bb
SQL> select count(*) from wireless_site.merchant;
COUNT(*)
----------
14005
SQL>
SQL> select count(*) from wireless_site.clickthroughrate;
COUNT(*)
----------
2384026
SQL> create table wireless_site.bb as select * from wireless_site.clickthroughrate;
Table created.
SQL>
SQL> select count(*) from wireless_site.bb;
COUNT(*)
----------
2384026
SQL>
二、执行SQL语句并查看sql语句的真实执行计划:
SQL> alter session set statistics_level=all;
Session altered.
SQL>
2.1、首先查看Hash Join的执行计划:
执行SQL语句:select * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
由以上执行计划可以看出:
1、HASH JOIN的驱动表为BB,扫描方式为全表扫描,执行了1次,实际返回了1658行
2、HASH JOIN的被驱动表为MERCHANT,扫描方式为全表扫描,执行了1次,实际返回了12行
3、需要注意的是:HASH JOIN的驱动表被被驱动表都会被扫描一次,而nested loop则是驱动表扫描一次,被驱动表被扫描N次(具体N的值是根据驱动表返回的行数来决定的。)
2.2、接着使用HINT的方式让优化器强制走nested loop(使用wireless_site.merchant作为驱动表)
执行SQL语句:select /*+ leading(merchant) use_nl(bb) */ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
此时,wireless_site.merchant作为驱动表,加上过滤条件filter后,实际返回行数为:
由以上执行计划可以看出:
1、嵌套循环的驱动表为merchant,采用的扫描方式为全表扫描,执行了1次,返回了12行
2、嵌套循环的被驱动表为bb,采用的扫描方式是全表扫描,执行了12次,实际返回了1658行
2.3、然后再使用HINT的方式让优化器强制走nested loop(使用wireless_site.bb作为驱动表)
执行SQL语句:select /*+ leading(bb) use_nl(merchant) */ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
此时,wireless_site.bb作为驱动表,加上过滤条件filter后,实际返回行数为:
由以上执行计划可以看出:
1、表BB作为驱动表,扫描方式是全表扫描,实际返回行数为1658
2、表merchant作为被驱动表,采用的扫描方式为索引唯一扫描,执行了1658次,返回了1658行
3、被驱动表merchant索引扫描完成后,通过rowid回表读数执行了1658次,实际返回了1658行
2.4、接下来在wireless_site.bb表中的recordtype列上创建索引
SQL> create index wireless_site.bb_idx on wireless_site.bb(recordtype);
Index created.
SQL>
接着使用HINT的方式让优化器强制走nested loop(使用wireless_site.merchant作为驱动表):
执行SQL语句:select /*+ leading(merchant) use_nl(bb) */ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
由执行计划可以看出:
1、merchant作为驱动表,采用的是全表扫描的方式,实际返回行数为12
2、被驱动表BB的索引采用的是Index Range Scan的方式,实际返回行数为1658,共执行了12次
3、被驱动表BB索引被访问了12次,每次扫描完索引后又根据rowid回表读数(TABLE ACCESS BY INDEX ROWID),这样BB表就被访问了1658次
对比之前没有在wireless_site.bb表中的recordtype列上创建索引时的执行计划可以看出,创建索引后SQL花费不到1s的时间就可以出结果;没有创建索引时,需要花费1分32秒左右。
2.5、创建索引后,使用HINT的方式让优化器强制走nested loop(使用wireless_site.bb作为驱动表):
执行SQL语句:select /*+ leading(bb) use_nl(merchant) */ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
由执行计划可以看出:
1、嵌套循环的驱动表为bb,扫描方式为全表扫描,执行了1次,实际返回了1658行
2、嵌套循环的被驱动表的索引为SYS_C0011475,扫描方式为INDEX UNIQUE SCAN,执行了1658次,实际返回了1658行
3、嵌套循环的被驱动表为merchant,merchant的所用被扫描了1658次,每次扫描完索引后又根据rowid回表(TABLE ACCESS BY INDEX ROWID),执行了1658次,实际返回了1658行
4、sql语句执行完,大概需要7.42秒
2.6、创建索引后,使用HINT强制走HASH JOIN(wireless_site.bb作为驱动表)
执行SQL语句:select /*+ leading(bb) use_hash(merchant)*/ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
等待SQL语句执行完毕,然后再使用这条SQL语句:select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));查看真实执行计划:
由执行计划可以看出:
1、HASH JOIN的驱动表为BB,扫描方式为全表扫描,执行了1次,实际返回了1658行
2、HASH JOIN的被驱动表为merchant,扫描方式为全表扫描,执行了1次,实际返回了12行
3、使用HASH Join 执行时,花费时间约为8.54秒
三、结论:
3.1、在nested loop 中,驱动表返回一行数据,被驱动表要被扫描一次
3.2、在nested loop中,往往是将小表作为驱动表,大表作为被驱动表。所谓小表,不是说这个表的segment_size的大小,而是指返回数据的多少;返回数据量少,我们就称之为小表,返回数据量大,我们称之为大表。
3.3、在nested loop中,如果发现SQL的执行计划中的nested loop使用了全表扫描,这时我们就要特别注意了:直接select count(*) from 驱动表where 过滤条件...看它返回多少行(检查驱动表返回的总行数),看他是否能不走全表扫描。
3.4、HASH JOIN的驱动表和被驱动表都被扫描1次。
3.4、HASH JOIN会耗费PGA的work_area。嵌套循环不耗费PGA的work_area。
四、附录
4.1、在SQL语句优化过程中,我们经常会用到hint,现总结一下在SQL优化过程中常见Oracle HINT的用法:
1. /*+ALL_ROWS*/表明对语句块选择基于开销的优化方法,并获得最佳吞吐量,使资源消耗最小化.
例如:SELECT /*+ALL+_ROWS*/ merchantid,merchantname,status FROM merchant WHERE merchantid='3712098761';
2. /*+FIRST_ROWS*/表明对语句块选择基于开销的优化方法,并获得最佳响应时间,使资源消耗最小化.
例如:SELECT /*+FIRST_ROWS*/ merchantid,merchantname,status FROM merchant WHERE merchantid='3712098761';
3. /*+CHOOSE*/表明如果数据字典中有访问表的统计信息,将基于开销的优化方法,并获得最佳的吞吐量;表明如果数据字典中没有访问表的统计信息,将基于规则开销的优化方法;
例如:SELECT /*+CHOOSE*/ merchantid,merchantname,status FROM merchant WHERE merchantid='3712098761';
4. /*+RULE*/表明对语句块选择基于规则的优化方法.
例如:SELECT /*+ RULE */ merchantid,merchantname,status FROM merchant WHERE merchantid='3712098761';
5. /*+FULL(TABLE)*/表明对表选择全局扫描的方法.
例如:SELECT /*+FULL(A)*/ merchantid,merchantname,status FROM merchant WHERE merchantid='3712098761';
6. /*+ROWID(TABLE)*/提示明确表明对指定表根据ROWID进行访问.
例如:SELECT /*+ROWID(BSEMPMS)*/ * FROM merchant WHERE ROWID>='AAAAAAAAAAAAAA'AND merchantid='3712098761';
7. /*+CLUSTER(TABLE)*/提示明确表明对指定表选择簇扫描的访问方法,它只对簇对象有效.
例如:SELECT /*+CLUSTER */ merchant.merchantid,package.packagename FROM merchant,package where merchantid='3712098761' AND merchant.merchantid=package.merchantid;
8. /*+INDEX(TABLE INDEX_NAME)*/表明对表选择索引的扫描方法.
例如:SELECT /*+INDEX(bb bb_idx) */ FROM wireless_site.bb WHERE recordtype='3712098761';
9. /*+INDEX_ASC(TABLE INDEX_NAME)*/表明对表选择索引升序的扫描方法.
例如:SELECT /*+INDEX_ASC(bb bb_idx) */ FROM wireless_site.bb WHERE recordtype='3712098761';
10. /*+USE_HASH(TABLE)*/将指定的表与其他行源通过哈希连接方式连接起来.
例如:SELECT /*+USE_HASH(bb,merchant)*/ * FROM wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
or:select /*+ leading(bb) use_hash(merchant)*/ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
11. /*+USE_NL(TABLE)*/将指定表与嵌套的连接的行源进行连接,并把指定表作为内部表.
例如:SELECT /*+bb USE_NL(merchant)*/ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
or:select /*+ leading(merchant) use_nl(bb) */ * from wireless_site.merchant,wireless_site.bb where bb.recordtype = merchant.merchantid and merchant.merchantid like '%3210%';
Leading(): 指示Oracle在执行join(hash join, nested loop join, merge join)时的连接顺序。当执行计划不按照最优的表连接顺序时,考虑使用Leading 改变执行计划。 使用leading 的目的是减少检索数据量, 提高查询性能
4.2、查看某张表的统计信息是否过期
执行如下SQL语句查询:
SELECT OWNER,TABLE_NAME,OBJECT_TYPE,STALE_STATS,LAST_ANALYZED FROM DBA_TAB_STATISTICS WHERE (STALE_STATS='YES' OR LAST_ANALYZED IS NULL) AND OWNER NOT IN ('SYS','SYSTEM','SYSMAN','DMSYS','OLAPSYS','XDB','EXFSYS','CTXSYS','WMSYS','DBSNMP','ORDSYS','OUTLN','TSMSYS','MDSYS') AND TABLE_NAME NOT LIKE 'BIN%';
结果如下:
或者,这行这条SQL:
select owner,table_name,object_type,stale_stats,last_analyzed from dba_tab_statistics where table_name in (table_name) and owner = 'WIRELESS_SITE' and (stale_stats = 'YES' or last_analyzed in null);
或者执行这条SQL:
SELECT OWNER,TABLE_NAME,OBJECT_TYPE,STALE_STATS,LAST_ANALYZED FROM DBA_TAB_STATISTICS WHERE OWNER='&OWNER' AND TABLE_NAME='&TABLE_NAME';
首先创建一张实验表:
create table wireless_site.xxx as select rownum as col1, rownum + 1 as col2, rownum + 2 as col3 from dual connect by level <= 10000;
查看表结构
对该表收集统计信息:
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname => 'wireless_site',
tabname => 'xxx',
estimate_percent => 30,
method_opt => 'for all columns size repeat',
no_invalidate => FALSE,
degree => 8,
cascade => TRUE);
END;
/
运行如下两个脚本:
exec dbms_stats.flush_database_monitoring_info;
和
select owner,table_name,object_type,stale_stats,last_analyzed from dba_tab_statistics where table_name in (table_name) and owner = 'WIRELESS_SITE' and (stale_stats = 'YES' or last_analyzed in null);
结果显示没有记录是因为我的表是刚刚收集完统计信息,所以没有过期。
接下来删除表中20%的数据:
delete from wireless_site.xxx where rownum<=(select count(*) from wireless_site.xxx)*0.2;
然后再来运行脚本:exec dbms_stats.flush_database_monitoring_info;
查看结果,已过期:
或者:
或者: