#执行计划
# SQL与计划
执行计划是整个SQL语句执行过程的描述。一个执行计划的优秀与否,很大程度的决定了SQL执行的效率。
也正是因为一条SQL语句可以由不同的计划来进行执行,SQL调优才有了意义。
下面将结合示例来讲解,计划与调优具体之间的相关联系,阐明计划如何影响性能。
# 计划与算子
示例
EXPLAIN SELECT * FROM area WHERE area_no = 1;
PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 3728302104
Optimizer: ADOPT_C
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type | Name | Owner | Rows | Cost(%CPU) | Partition info |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| 0 | SELECT STATEMENT | | | | | |
|* 1 | TABLE ACCESS FULL | AREA | SALES | 1| 13( 0)| |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
Operation Information (identified by operation id):
---------------------------------------------------
1 - Predicate : filter("AREA"."AREA_NO" = 1)
对于需要查询的table来说,能够通过多种不同的方式进行扫描(获取表中数据),这些不同的扫描方式就是不同的扫描算子。
因为存在不同的算子,才能使得一条SQL语句存在不同的执行计划,不同SQL语句的最适合、效率最高的算子也可能各不相同。
SQL调优的终极目标,就是在这些不同的执行计划中找到效率最高的那一个。
回到上述示例,table表在简单进行扫描时。选择何种算子最优,也不能一概而论。一种很流行的错误观点是无论任何扫描场景都应选择索引扫描,这是由于没有深层理解算子的实际执行过程所发生的误解。实际上,在需要回表时需采用table full scan,不需要回表时才采用index scan效率更高。
因此在进行调优之前,需要对算子进行深入了解。
# 算子效率评估
不同算子的效率既然不同,那就需要一套标准来评估各个算子性能的高低,这一套标准就是执行计划中展示出来的cost(执行代价)。
示例
EXPLAIN SELECT area_no FROM area WHERE area_no = 1;
PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 1386453710
Optimizer: ADOPT_C
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type | Name | Owner | Rows | Cost(%CPU) | Partition info |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| 0 | SELECT STATEMENT | | | | | |
|* 1 | INDEX FAST FULL SCAN | SYS_C_33 | SALES | 1| 6( 0)| |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
Operation Information (identified by operation id):
---------------------------------------------------
1 - Predicate : filter("AREA"."AREA_NO" = 1)
通过该示例不难发现,正如上小节所说,在不需要回表时,采用index scan的代价小于table full scan。
一套能够反应算子真实代价的model,是生成高效计划的关键。但cost model也有可能失效,在一些较为复杂的语句上。cost model可能会表现的不够准确,此时需要人工介入去判断是否存在某些算子的cost评估不准确并视情况进行相应调整,规避不优秀的计划,例如使用hint的方式强行指定算子。
# 算子组合
计划往往不只是上述示例中所示,仅有少少的一层。当SQL变得复杂时,不仅需要考虑不同的算子,还要考虑如何选择多个算子的组合。
示例
EXPLAIN SELECT * FROM area a, branches b WHERE a.area_no = b.area_no;
PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 1557742453
Optimizer: ADOPT_C
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type | Name | Owner | Rows | Cost(%CPU) | Partition info |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| 0 | SELECT STATEMENT | | | | | |
| 1 | NESTED LOOPS INNER | | | 100000| 211( 0)| |
| 2 | TABLE ACCESS FULL | BRANCHES | SALES | 100000| 132( 0)| |
| 3 | TABLE ACCESS BY INDEX ROWID | AREA | SALES | | | |
|* 4 | INDEX UNIQUE SCAN | SYS_C_33 | SALES | 1| 13( 0)| |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
Operation Information (identified by operation id):
---------------------------------------------------
4 - Predicate : access("A"."AREA_NO" = "B"."AREA_NO")
阐述算子组合方式的最好示例是Join,对于Join来说,单纯只考虑下层的scan算子是不够的,还需考虑不同表放置于Join算子中的位置(左侧或右侧),例如hash join中,建议将小表放在右边(build表)以降低建表的成本。
如何选择最优的算子组合与选择最优的算子一样至关重要,而选出最优的组合也是调优中最困难的部分,具体的调优规则可查阅SQL调优原理与规则。