2012 年,一朋友发来信息说以下 SQL 要跑 5 个小时,请求优化。

    1. SELECT
    2. B.AREA_ID,
    3. A.PARTY_ID,
    4. B.AREA_NAME,
    5. C.NAME CHANNEL_NAME,
    6. B.NAME PARTY_NAME,
    7. B.ACCESS_NUMBER,
    8. B.PROD_SPEC,
    9. B.START_DT,
    10. A.BO_ACTION_NAME,
    11. A.SO_STAFF_ID,
    12. A.ATOM_ACTION_ID,
    13. A.PROD_ID
    14. FROM DW_CHANNEL C,
    15. DW_CRM_DAY_USER B,
    16. DW_BO_ORDER A
    17. WHERE A.PROD_ID = B.PROD_ID AND
    18. A.CHANNEL_ID = C.CHANNEL_ID AND
    19. A.SO_STAFF_ID LIKE '36%' AND
    20. A.BO_ACTION_NAME IN ('新装''移机','资费变更') AND
    21. B.PROD_SPEC IN ('普通电话', 'ADSL','LAN', '手机',
    22. 'E8 - 2S''E6 移动版' 'E9 版 1M(老版)'
    23. '普通 E9''普通新版 E8'
    24. '全省_紧密融合型 E9 套餐产品规格'
    25. '(新) 全省_紧密融合型 E9 套餐产品规格'
    26. '新春欢乐送之 E8 套餐',
    27. '新春欢乐送之 E6 套餐') AND
    28. NOT EXISTS (SELECT *
    29. FROM DW_BO_ORDER D
    30. WHERE D.STAFF_ID LIKE '36%' AND
    31. A.PARTY_ID = D.PARTY_ID AND
    32. A.BO_ID != D.BO_ID AND
    33. A.PROD_ID != D.PROD_ID AND
    34. A.BO_ACTION_NAME IN
    35. ('新装' '移机''资费变更') AND
    36. A.COMPLETE_DT - INTERVAL '7' DAY < D.COMPLETE_DT);

    执行计划如下。

    1. Plan hash value: 2142862569
    2. -------------------------------------------------------------------------------------
    3. | Id |Operation |Name | Rows | Bytes | Cost (%CPU)| Time |
    4. -------------------------------------------------------------------------------------
    5. | 0 |SELECT STATEMENT | | 905 | 121K| 4152K (2)| 13:50:32 |
    6. |* 1 | FILTER | | | | | |
    7. |* 2 | HASH JOIN | | 905 | 121K| 12616 (2)| 00:02:32 |
    8. |* 3 | HASH JOIN | | 905 | 99550 | 12448 (2)| 00:02:30 |
    9. | 4 | PARTITION RANGE ALL| | 1979 | 108K| 9168 (2)| 00:01:51 |
    10. |* 5 | TABLE ACCESS FULL|DW_BO_ORDER | 1979 | 108K| 9168 (2)| 00:01:51 |
    11. |* 6 | TABLE ACCESS FULL |DW_CRM_DAY_USER| 309K| 15M| 3277 (2)| 00:00:40 |
    12. | 7 | TABLE ACCESS FULL |DW_CHANNEL | 48425 | 1276K| 168 (1)| 00:00:03 |
    13. |* 8 | FILTER | | | | | |
    14. | 9 | PARTITION RANGE ALL| | 1 | 29 | 9147 (2)| 00:01:50 |
    15. |*10 | TABLE ACCESS FULL |DW_BO_ORDER | 1 | 29 | 9147 (2)| 00:01:50 |
    16. -------------------------------------------------------------------------------------
    17. Predicate Information identified by operation id):
    18. ---------------------------------------------------
    19. 1 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM DW_BO_ORDER D WHERE (:B1='新装' OR :B2='
    20. 移机'OR :B3='资费变更' AND D」.「PARTY_ID」=:B4 AND TO_CHAR(「D」.「STAFF_ID」) LIKE
    21. '36%' AND D」.「COMPLETE_DT」>:B5-INTERVAL'+07 00:00:00' DAY(2) TO SECOND(0) AND
    22. D」.「PROD_ID」<>:B6 AND D」.「BO_ID」<>:B7))
    23. 2 - access(「A」.「CHANNEL_ID」=「C」.「CHANNEL_ID」)
    24. 3 - access(「A」.「PROD_ID」=「B」.「PROD_ID」)
    25. 5 - filter(「A」.「PROD_ID IS NOT NULL AND (「A」.「BO_ACTION_NAME」='新装' OR
    26. A」.「BO_ACTION_NAME」='移机' OR A」.「BO_ACTION_NAME」='资费变更' AND
    27. TO_CHAR(「A」.「SO_STAFF_ID」) LIKE 36%『)
    28. 6 - filter(「B」.「PROD_SPEC」='(新) 全省_紧密融合型 E9 套餐产品规格' OR B」.「PROD_SPEC ='ADSL'
    29. OR B」.「PROD_SPEC」='E6 移动版' OR B」.「PROD_SPEC」='E8 - 2S' OR
    30. B」.「PROD_SPEC」='E9 版 1M(老版)' OR B」.「PROD_SPEC」='LAN' OR B」. PROD_SPEC」='
    31. 普通 E9' OR B」.「PROD_SPEC」='普通电话' OR B」.「PROD_SPEC」='普通新版 E8' OR
    32. B」.「PROD_SPEC」='全省_紧密融合型 E9 套餐产品规格' OR B」.「PROD_SPEC」='手机' OR
    33. B」.「PROD_SPEC」='新春欢乐送之 E6 套餐' OR B」.「PROD_SPEC」='新春欢乐送之 E8 套餐')
    34. 8 - filter(:B1='新装' OR :B2='移机' OR :B3='资费变更'
    35. 10 - filter(「D」.「PARTY_ID」=:B1 AND TO_CHAR(「D」.「STAFF_ID」) LIKE '36%' AND
    36. D」.「COMPLETE_DT」>:B2-INTERVAL'+07 00:00:00' DAY(2) TO SECOND(0) AND
    37. D」.「PROD_ID」<>:B3 AND D」.「BO_ID」<>:B4)

    优化 SQL,必须看表大小,表大小信息如下。

    1. select count(*) from dw_bo_order ----200 万行数据
    1. COUNT(*)
    2. ----------
    3. 2282548
    1. select count(*) from dw_crm_day_user ----40 万行数据
    1. COUNT(*)
    2. ----------
    3. 420918
    1. select count(*) from dw_channel ---4 万行数据
    1. COUNT(*)
    2. ----------
    3. 48031

    SQL 语句中最大表 DW_BO_ORDER 才 200 万行数据,但是 SQL 执行了 5 个多小时,显然执行计划有问题。执行计划中,Id=1 是 Filter,而且 Filter 对应的谓词信息有 EXISTS(子查询:B1),这说明该 Filter 类似嵌套循环。Id=2 和 Id=8 是 Id=1 的儿子,因为这里的 Filter 类似嵌套循环,Id=2 就相当于 NL 驱动表,Id=8 相当于 NL 被驱动表,Id=8 是全表扫描过滤后的数据,所以 Id=8 可以看作全表扫描。本书反复强调过,NL 被驱动表必须走索引。但是 Id=10 并没有走索引。Id=2 估算返回 905 行数据,一般情况下 Rows 会算少,这里就暂且认为 Id=2 返回 905 行数据,那么 Id=8 会被扫描 905 次,也就是说 DW_BO_ORDER 这个 200 万行大表会被扫描 905 次,而且每次都是全表扫描,这就是为什么 SQL 会执行 5 个多小时。

    找到 SQL 的性能瓶颈之后,我们就可以想办法优化 SQL。本案例有两种优化思路,其一是让大表只被扫描一次,其二是不减少扫描次数,但是减少大表每次被扫描的体积。最优的解决方案是,想办法让 Id=2 和 Id=8 走 HASH 连接消除 Filter,这样就只需要扫描 1 次大表,因为当时数据库版本是 Oracle10g,where 子查询中有主表的过滤条件,在 not exists 子查询中添加 HINT:HASH_AJ 无法更改执行计划。我们可以将 not exists 改写为「外连接 + 子表连接列 is null」的形式,让其走 HASH 连接,但是当时没有采用这种改写方式。因为大表要被扫描 905 次,每次都是全表扫描,如果能减少扫描的体积,也能优化 SQL。我们可以在大表上建立一个组合索引,这样就能避免大表每次全表扫描,从而达到减少扫描体积的目的,但是当时朋友没权限建立索引。最终选择使用 with as 子句优化上述 SQL。

    1. set timi on
    2. WITH D AS
    3. (SELECT /*+ materialize */
    4. PARTY_ID,
    5. BO_ID,
    6. PROD_ID,
    7. COMPLETE_DT
    8. FROM DW_BO_ORDER
    9. WHERE STAFF_ID LIKE '36%' AND
    10. BO_ACTION_NAME IN ('新装'
    11. '移机'
    12. '资费变更'))
    13. SELECT
    14. B.AREA_ID,
    15. A.PARTY_ID,
    16. B.AREA_NAME,
    17. C.NAME CHANNEL_NAME,
    18. B.NAME PARTY_NAME,
    19. B.ACCESS_NUMBER,
    20. B.PROD_SPEC,
    21. B.START_DT,
    22. A.BO_ACTION_NAME,
    23. A.SO_STAFF_ID,
    24. A.ATOM_ACTION_ID,
    25. A.PROD_ID
    26. FROM DW_CHANNEL C,
    27. DW_CRM_DAY_USER B,
    28. DW_BO_ORDER A
    29. WHERE A.PROD_ID = B.PROD_ID AND
    30. A.CHANNEL_ID = C.CHANNEL_ID AND
    31. A.SO_STAFF_ID LIKE '36%' AND
    32. A.BO_ACTION_NAME IN ('新装''移机''资费变更') AND
    33. B.PROD_SPEC IN ('普通电话', 'ADSL''LAN', '手机',
    34. 'E8 - 2S','E6 移动版', 'E9 版 1M(老版)',
    35. '普通 E9''普通新版 E8',
    36. '全省_紧密融合型 E9 套餐产品规格'
    37. '(新) 全省_紧密融合型 E9 套餐产品规格'
    38. '新春欢乐送之 E8 套餐',
    39. '新春欢乐送之 E6 套餐') AND
    40. NOT EXISTS (SELECT *
    41. FROM D
    42. WHERE A.PARTY_ID = D.PARTY_ID AND
    43. A.BO_ID != D.BO_ID AND
    44. A.PROD_ID != D.PROD_ID AND
    45. A.COMPLETE_DT - INTERVAL '7' DAY < D.COMPLETE_DT);

    已选择 49 245 行。

    已用时间:00: 00: 12.37。

    执行计划如下。

    1. -------------------------------------------------------------------------------------
    2. Plan hash value: 2591883460
    3. -------------------------------------------------------------------------------------
    4. | Id |Operation |Name |Rows |Bytes| Cost(%CPU)|
    5. -------------------------------------------------------------------------------------
    6. | 0 |SELECT STATEMENT | | 905| 121K| 62428 (2)|
    7. | 1 | TEMP TABLE TRANSFORMATION| | | | |
    8. | 2 | LOAD AS SELECT |DW_BO_ORDER | | | |
    9. | 3 | PARTITION RANGE ALL | | 114K|3228K| 9127 (2)|
    10. |* 4 | TABLE ACCESS FULL |DW_BO_ORDER | 114K|3228K| 9127 (2)|
    11. |* 5 | FILTER | | | | |
    12. |* 6 | HASH JOIN | | 905| 121K| 12616 (2)|
    13. |* 7 | HASH JOIN | | 905|99550| 12448 (2)|
    14. | 8 | PARTITION RANGE ALL | | 1979| 108K| 9168 (2)|
    15. |* 9 | TABLE ACCESS FULL |DW_BO_ORDER | 1979| 108K| 9168 (2)|
    16. |*10 | TABLE ACCESS FULL |DW_CRM_DAY_USER | 309K| 15M| 3277 (2)|
    17. | 11 | TABLE ACCESS FULL |DW_CHANNEL |48425|1276K| 168 (1)|
    18. |*12 | FILTER | | | | |
    19. |*13 | VIEW | | 114K|6791K| 90 (3)|
    20. | 14 | TABLE ACCESS FULL |SYS_TEMP_0FD9D662E_D625B872| 114K|3228K| 90 (3)|
    21. -------------------------------------------------------------------------------------
    22. Predicate Information identified by operation id):
    23. ---------------------------------------------------
    24. 4 - filter(TO_CHAR(「STAFF_ID」) LIKE 36%』)
    25. 5 - filter( NOT EXISTS (SELECT /*+ */ 0 FROM (SELECT /*+ CACHE_TEMP_TABLE (「T1」) */ C0
    26. STAFF_ID」,「C1 PARTY_ID」,「C2 BO_ID」,「C3 PROD_ID」,「C4 COMPLETE_DT FROM
    27. SYS」.「SYS_TEMP_0FD9D662E_D625B872 T1」) D WHERE (:B1=『新装』 OR :B2=『移机』 OR :B3=『
    28. 资费变更』) AND TO_CHAR(「D」.「STAFF_ID」) LIKE 36%』 AND D」.「PARTY_ID」=:B4
    29. AND
    30. D」.「BO_ID」<>:B5 AND D」.「PROD_ID」<>:B6 AND D」.「COMPLETE_DT」>:B7-INTERVAL'+07
    31. 00:00:00' DAY(2) TO SECOND(0)))
    32. 6 - access(「A」.「CHANNEL_ID」=「C」.「CHANNEL_ID」)
    33. 7 - access(「A」.「PROD_ID」=「B」.「PROD_ID」)
    34. 9 - filter(「A」.「PROD_ID IS NOT NULL AND (「A」.「BO_ACTION_NAME」=』新装『 OR A」.「BO_ ACTION_NAME」=』
    35. 移机『 OR A」.「BO_ACTION_NAME」=』资费变更『) AND TO_CHAR(「A」.「SO_STAFF_ID」) LIKE 36%『)
    36. 10 - filter(「B」.「PROD_SPEC」=』(新) 全省_紧密融合型 E9 套餐产品规格『 OR B」.「PROD_SPEC」= ADSL OR
    37. B」.「PROD_SPEC」='E6 移动版' OR B」.「PROD_SPEC」='E8 - 2S' OR B」.「PROD_ SPEC」='E9 版
    38. 1M(老版)' OR B」.「PROD_SPEC」='LAN' OR B」.「PROD_SPEC」=』普通 E9 OR B」. PROD_SPEC」=』
    39. 普通电话『 OR B」.「PROD_SPEC」=』普通新版 E8 OR B」.「PROD_SPEC」=』全省_紧密融合型 E9 套餐
    40. 产品规格『 OR B」.「PROD_SPEC」=』手机『 OR B」.「PROD_SPEC」=』新春欢乐送之 E6 套餐『 OR
    41. B」.「PROD_SPEC」=』新春欢乐送之 E8 套餐『)
    42. 12 - filter(:B1=』新装『 OR :B2=』移机『 OR :B3=』资费变更『)
    43. 13 - filter(TO_CHAR(「D」.「STAFF_ID」) LIKE 36%『 AND D」.「PARTY_ID」=:B1 AND D」.「BO_ID」<>:B2 AND
    44. D」.「PROD_ID」<>:B3 AND D」.「COMPLETE_DT」>:B4-INTERVAL'+07 00:00:00' DAY(2) TO
    45. SECOND(0))
    46. 统计信息
    47. ----------------------------------------------------------
    48. 2 recursive calls
    49. 29 db block gets
    50. 110506 consistent gets
    51. 22 physical reads
    52. 656 redo size
    53. 2438096 bytes sent via SQL*Net to client
    54. 449 bytes received via SQL*Net from client
    55. 11 SQL*Net roundtrips to/from client
    56. 0 sorts memory
    57. 0 sorts disk
    58. 49245 rows processed

    使用 with as 子句将大表要被访问的字段查询出来,一共 4 个字段,然后过滤掉不需要的数据,添加 HINT:**MATERIALIZE** 将 with as 子句查询结果固化为临时表,这样就达到了减少扫描体积的目的。假设 200 万行的大表 DW_BO_ORDER 有占用 2GB 存储空间,表有 40 个字段,通过 with as 子句改写之后,只需要存储 4 个字段数据,这时只需 200MB 存储空间,而且 with as 子句中还有过滤条件,又可以过滤掉一部分数据,这时 with as 子句可能就只需要几十兆存储空间。虽然被扫描的次数没有改变,但是每次被扫描的体积大大减少,这样就解决了 SQL 查询性能。最终 SQL 可以在 12 秒左右跑完,一共返回 4.9 万行数据。