2016 年,一互联网彩票行业的朋友说以下 SQL 要跑几十分钟(数据库环境 Oracle11gR2)。

    1. select count(distinct a.user_name), count(distinct a.invest_id)
    2. from base_data_login_info@agent a
    3. where a.str_day <= '20160304'
    4. and a.str_day >= '20160301'
    5. and a.channel_id in (select channel_rlat
    6. from tb_user_channel a, tb_channel_info b
    7. where a.channel_id = b.channel_id
    8. and a.user_id = 5002)
    9. and a.platform = a.platform;
    1. Plan hash value: 2367445948
    2. ----------------------------------------------------------------------------------
    3. | Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
    4. ----------------------------------------------------------------------------------
    5. | 0 | SELECT STATEMENT | | 1 | 130 | 754 (2)|
    6. | 1 | SORT GROUP BY | | 1 | 130 | |
    7. |* 2 | HASH JOIN | | 4067K| 504M| 754 (2)|
    8. |* 3 | HASH JOIN | | 11535 | 360K| 258 (1)|
    9. |* 4 | TABLE ACCESS FULL| TB_USER_CHANNEL | 11535 | 157K| 19 (0)|
    10. | 5 | TABLE ACCESS FULL| TB_CHANNEL_INFO | 11767 | 206K| 238 (0)|
    11. | 6 | REMOTE | BASE_DATA_LOGIN_INFO | 190K| 17M| 486 (1)|
    12. ----------------------------------------------------------------------------------
    13. Predicate Information (identified by operation id):
    14. ---------------------------------------------------
    15. 2 - access("A"."CHANNEL_ID"="CHANNEL_RLAT")
    16. 3 - access("A"."CHANNEL_ID"="B"."CHANNEL_ID")
    17. 4 - filter("A"."USER_ID"=5002)
    18. Remote SQL Information (identified by operation id):
    19. ----------------------------------------------------
    20. 6 - SELECT "USER_NAME","INVEST_ID","STR_DAY","CHANNEL_ID","PLATFORM" FROM
    21. "BASE_DATA_LOGIN_INFO" "A" WHERE "STR_DAY"<='20160304' AND "STR_DAY">='20160301'
    22. AND "PLATFORM" IS NOT NULL (accessing 'AGENT' )

    想要优化 SQL,必须要知道表大小。TBUSER_CHANNEL 有 1 万行数据,TB_CHANNEL INFO 有 1 万行左右,BASE_DATA_LOGIN_INFO 有 19 万行,过滤之后剩下 4 万行左右。执行计划走的是 HASH 连接,每个表都只扫描一次,虽然是全表扫描,但是最大表才 19 万行,按道理说不应该执行几十分钟,正常情况下应该可以 1 秒左右出结果。起初我们怀疑是 SQL 中 DBLINK 传输数据导致性能问题,于是在本地创建一个一模一样的表,但是该 SQL 还是执行缓慢。

    我们只能一步一步排查 SQL 哪里出了问题,让朋友执行如下 SQL。

    1. select count(*) ---改动了这里
    2. from base_data_login_info@agent a
    3. where a.str_day <= '20160304'
    4. and a.str_day >= '20160301'
    5. and a.channel_id in select channel_rlat
    6. from tb_user_channel a, tb_channel_info b
    7. where a.channel_id = b.channel_id
    8. and a.user_id = 5002
    9. and a.platform = a.platform

    上面 SQL 可以秒出。于是朋友继续执行如下 SQL。

    1. select counta.user_name ---改动了这里
    2. from base_data_login_info@agent a
    3. where a.str_day <= '20160304'
    4. and a.str_day >= '20160301'
    5. and a.channel_id in select channel_rlat
    6. from tb_user_channel a, tb_channel_info b
    7. where a.channel_id = b.channel_id
    8. and a.user_id = 5002
    9. and a.platform = a.platform

    上面 SQL 也可以秒出。我们继续排查。

    1. select counta.user_name), counta.invest_id ---改动了这里
    2. from base_data_login_info@agent a
    3. where a.str_day <= '20160304'
    4. and a.str_day >= '20160301'
    5. and a.channel_id in select channel_rlat
    6. from tb_user_channel a, tb_channel_info b
    7. where a.channel_id = b.channel_id
    8. and a.user_id = 5002
    9. and a.platform = a.platform

    以上 SQL 还是可以秒出,我们继续排查。

    1. select countdistinct a.user_name), counta.invest_id ---改动了这里
    2. from base_data_login_info@agent a
    3. where a.str_day <= 20160304
    4. and a.str_day >= 20160301
    5. and a.channel_id in select channel_rlat
    6. from tb_user_channel a, tb_channel_info b
    7. where a.channel_id = b.channel_id
    8. and a.user_id = 5002
    9. and a.platform = a.platform

    上面 SQL 依然可以秒出。现在我们找到引起 SQL 慢的原因了,select 中同时 count(distinct a.user_name),count(distinct a.invest_id)导致 SQL 查询缓慢。

    在实际工作中,要优先解决问题,再去查找问题的根本原因。我们将 SQL 进行如下改写。

    1. with t1 as
    2. (select /*+ materialize */
    3. a.user_name, a.invest_id
    4. from base_data_login_info@agent a
    5. where a.str_day <= '20160304'
    6. and a.str_day >= '20160301'
    7. and a.channel_id in (select channel_rlat
    8. from tb_user_channel a, tb_channel_info b
    9. where a.channel_id = b.channel_id
    10. and a.user_id = 5002)
    11. and a.platform = a.platform)
    12. select count(distinct user_name) ,count(distinct invest_id) from t1;

    为什么改写成以上 SQL 能解决性能问题呢?因为在排查问题的时候 count 不加 distinct 是可以秒出的,所以我们先将能秒出的 SQL 放到 with as 子句,通过添加 HINT::/+ materialize /生成临时表,再对临时表进行 **count(distinct...),count(distinct)**,这样就能解决问题。改写后的 SQL 执行计划如下。

    1. Plan hash value: 901326807
    2. -------------------------------------------------------------------------------------
    3. | Id |Operation | Name | Rows |Bytes|Cost(%CPU)|
    4. -------------------------------------------------------------------------------------
    5. | 0 |SELECT STATEMENT | | 1 | 54| 1621 (1)|
    6. | 1 | TEMP TABLE TRANSFORMATION| | | | |
    7. | 2 | LOAD AS SELECT | SYS_TEMP_0FD9D6720_EB8EA | | | |
    8. |* 3 | HASH JOIN RIGHT SEMI | | 190K| 22M| 744 (1)|
    9. | 4 | VIEW | VW_NSO_1 | 11535 | 304K| 258 (1)|
    10. |* 5 | HASH JOIN | | 11535 | 360K| 258 (1)|
    11. |* 6 | TABLE ACCESS FULL | TB_USER_CHANNEL | 11535 | 157K| 19 (0)|
    12. | 7 | TABLE ACCESS FULL | TB_CHANNEL_INFO | 11767 | 206K| 238 (0)|
    13. | 8 | REMOTE | BASE_DATA_LOGIN_INFO | 190K| 17M| 486 (1)|
    14. | 9 | SORT GROUP BY | | 1 | 54| |
    15. | 10 | VIEW | | 190K| 9M| 878 (1)|
    16. | 11 | TABLE ACCESS FULL | SYS_TEMP_0FD9D6720_EB8EA | 190K| 9M| 878 (1)|
    17. -------------------------------------------------------------------------------------
    18. Predicate Information (identified by operation id):
    19. ---------------------------------------------------
    20. 3 - access("A"."CHANNEL_ID"="CHANNEL_RLAT")
    21. 5 - access("A"."CHANNEL_ID"="B"."CHANNEL_ID")
    22. 6 - filter("A"."USER_ID"=5002)
    23. Remote SQL Information (identified by operation id):
    24. ----------------------------------------------------
    25. 8 - SELECT "USER_NAME","INVEST_ID","STR_DAY","CHANNEL_ID","PLATFORM" FROM "BASE_DATA_LOGIN_INFO"
    26. "A" WHERE "STR_DAY"<='20160304' AND "STR_DAY">='20160301'
    27. AND "PLATFORM" IS NOT NULL (accessing 'AGENT' )

    解决问题之后,现在我们来查找 SQL 缓慢的根本原因。现在对比缓慢 SQL 的执行计划与秒出 SQL 的执行计划,缓慢 SQL 的执行计划如下。

    1. ----------------------------------------------------------------------------------
    2. | Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
    3. ----------------------------------------------------------------------------------
    4. | 0 | SELECT STATEMENT | | 1 | 130 | 754 (2)|
    5. | 1 | SORT GROUP BY | | 1 | 130 | |
    6. |* 2 | HASH JOIN | | 4067K| 504M| 754 (2)|
    7. |* 3 | HASH JOIN | | 11535 | 360K| 258 (1)|
    8. |* 4 | TABLE ACCESS FULL| TB_USER_CHANNEL | 11535 | 157K| 19 (0)|
    9. | 5 | TABLE ACCESS FULL| TB_CHANNEL_INFO | 11767 | 206K| 238 (0)|
    10. | 6 | REMOTE | BASE_DATA_LOGIN_INFO | 190K| 17M| 486 (1)|
    11. ----------------------------------------------------------------------------------

    秒出 SQL 的执行计划如下。

    1. -------------------------------------------------------------------------------------
    2. | Id |Operation |Name | Rows | Bytes |Cost(%CPU)|
    3. -------------------------------------------------------------------------------------
    4. | 0 |SELECT STATEMENT | | 1 | 54 | 1621 (1)|
    5. | 1 | TEMP TABLE TRANSFORMATION| | | | |
    6. | 2 | LOAD AS SELECT |SYS_TEMP_0FD9D6720_EB8EA| | | |
    7. |* 3 | HASH JOIN RIGHT SEMI | | 190K| 22M| 744 (1)|
    8. | 4 | VIEW |VW_NSO_1 | 11535 | 304K| 258 (1)|
    9. |* 5 | HASH JOIN | | 11535 | 360K| 258 (1)|
    10. |* 6 | TABLE ACCESS FULL |TB_USER_CHANNEL | 11535 | 157K| 19 (0)|
    11. | 7 | TABLE ACCESS FULL |TB_CHANNEL_INFO | 11767 | 206K| 238 (0)|
    12. | 8 | REMOTE |BASE_DATA_LOGIN_INFO | 190K| 17M| 486 (1)|
    13. | 9 | SORT GROUP BY | | 1 | 54 | |
    14. | 10 | VIEW | | 190K| 9M| 878 (1)|
    15. | 11 | TABLE ACCESS FULL |SYS_TEMP_0FD9D6720_EB8EA| 190K| 9M| 878 (1)|
    16. -------------------------------------------------------------------------------------

    我们注意仔细对比执行计划,缓慢 SQL 执行计划中 Id=2 是 HASH JOIN,而秒出 SQL 的执行计划中 Id=3 是 HASH JOIN RIGHT SEMI。SEMI 是半连接特有关键字,缓慢 SQL 的执行计划中没有 SEMI 关键字,这说明 CBO 将半连接等价改写成了内连接;秒出 SQL 的执行计划有 SEMI 关键字,这说明 CBO 没有将半连接等价改写成内连接。现在我们得到结论,该 SQL 查询缓慢是因为 CBO 内部将半连接改写为内连接导致。

    大家还记得半连接与内连接接区别吗?半连只返回一个表的数据,关联之后数据量不会翻番,内连接表关联之后数据量可能会翻番。该 SQL 查询缓慢是被改成内连接导致,现在我们有充分理由怀疑内连接关联之后返回的数据量太大,因为如果关联返回的数据量很少是不可能出性能问题的。于是检查两个表连接列的数据分布。

    1. select channel_id, count(*)
    2. from base_data_login_info
    3. group by channel_id
    4. order by 2;
    1. CHANNEL_ID COUNT(*)
    2. -------------------------------------------------- ----------
    3. 011a1 2
    4. 003a1 3
    5. 021a1 3
    6. 006a1 12
    7. 024h2 16
    8. 013a1 19
    9. 007a1 24
    10. 012a1 25
    11. 005a1 27
    12. EPT01 36
    13. 028h2 109
    14. 008a1 139
    15. 029a1 841
    16. 009a1 921
    17. 014a1 1583
    18. 000a1 1975
    19. a0001 2724
    20. 004a1 5482
    21. 001a1 16329
    22. 026h2 160162

    in 子查询关联列数据分布如下。

    1. select channel_rlat, count(*)
    2. from tb_user_channel a, tb_channel_info b
    3. where a.channel_id = b.channel_id
    4. and a.user_id = 5002
    5. group by channel_rlat
    6. order by 2 desc;
    1. channel_rlat count(*)
    2. 026h2 10984
    3. 024h2 7
    4. 002h2 6
    5. 023a2 2
    6. 007s001022001 1
    7. 007s001022002 1
    8. 007s001024007 1
    9. 007s001024009 1
    10. 007s001022009 1
    11. 001s001006 1
    12. 001s001008 1
    13. 001s001001001 1
    14. 001s001001003 1
    15. 001s001001007 1
    16. 001s001001014 1
    17. 007s001018003 1
    18. 007s001018007 1
    19. 007s001019005 1
    20. 007s001019008 1
    21. 001s001002011 1
    22. 007s001011003 1
    23. 007s001034 1
    24. 007s001023005 1

    两表的数据分布果然有问题,其中 026h2 这条数据倾斜特别明显。如果让两表进行内连接,026h2 这条数据关联之后返回结果应该是 16016210984,现在我们终于发现该 SQL 执行缓慢的根本原因,是因为*两个表的连接列中有部分数据倾斜非常严重。

    最初采用的是 with as 子句加/*+ materialize */临时解决 SQL 的性能问题,我们也可以使用 rownum 优化 SQL,rownum 可以让一个查询被当成一个整体

    1. with t1 as
    2. (select
    3. a.user_name, a.invest_id
    4. from base_data_login_info@agent a
    5. where a.str_day <= '20160304'
    6. and a.str_day >= '20160301'
    7. and a.channel_id in (select channel_rlat
    8. from tb_user_channel a, tb_channel_info b
    9. where a.channel_id = b.channel_id
    10. and a.user_id = 5002)
    11. and a.platform = a.platform and rownum>0)
    12. select count(distinct user_name) ,count(distinct invest_id) from t1;

    如果大家想模拟本案例,可以跟着下面实验步骤执行(请在 11g 中模拟)。

    我们先创建如下两个测试表。

    1. create table a as select * from dba_objects;
    2. create table b as select * from dba_objects;

    要执行的缓慢的 SQL 如下。

    1. select count(distinct owner), count(distinct object_name)
    2. from a
    3. where owner in (select owner from b);

    优化改写之后的 SQL 如下。

    1. with t as(select owner, object_name
    2. from a
    3. where owner in (select owner from b)
    4. and rownum > 0)
    5. select count(distinct owner), count(distinct object_name)
    6. from t;

    我们也可以对子查询先去重,将子查询变成 1 的关系,这样也能优化 SQL。

    1. select count(distinct owner), count(distinct object_name)
    2. from a
    3. where owner in (select owner from b group by owner);

    请思考为什么 Oracle11g CBO 会将 SQL 改写为内连接?大家是否还记得半连接的内容?

    **select ... from 1** 的表 **where owner in (select owner from n 的表)**改写为内连接,需要加 distinct。

    **select ... from n**的表 **where owner in (select owner from 1 的表)**改写为内连接,不需要加 distinct。

    我们的 SQL 是select count(distinct ),count(distinct),所以 CBO 直接将 SQL 改写为 select count(distinct a.owner),count(distinct object_name)from a,b where a.owner=b.owner这个问题在 12c 中已得到纠正。最后我们想说的就是,不管以后优化器进步有多大,我们始终不能依赖优化器,唯一可以依靠的就是自己所掌握的知识。