sql优化入门

175 阅读9分钟

table访问

现有表miku,在col1上建立索引index,在col3,col4上有m_index

TABLE ACCESS FULL

全表扫描:按照过滤谓词查找符合条件的记录返回。 HINT: /*+full(table_name)*/

INDEX UNIQUE SCAN

索引唯一扫描:从索引中查询唯一数据,其中索引查询次数取决于索引树的高度 需要注意的是,索引中并不记录全部数据,如果select列不索引上时,需要回表查询。 例如执行select col2 from miku where col1 = 1,由于索引中只记录了col1的信息,查询col2则需要回到表中根据rowid查询。根据rowid回到表中查询简称为回表。 HINT: /*+index(index_name)*/

INDEX RANGE SCAN

索引范围扫描:

range_scan.png

范围扫描和唯一扫描相似,只不过在找到一个符合条件的数据之后,通过叶结点间指针向后读取数据,直到访问到不满足条件的数据。又因为索引上数据有序性,所以范围扫描得到数据本身就是有序的,可以利用这一点从而避免排序。

如果符合条件的范围扫描数据过多,其性能可能比全表扫描的开销更大。 因为在索引上移动时,数据库的物理地址不连续,需要考虑磁盘寻道的时间。如果命中数据过多,相比全表扫描时顺序IO读取,索引IO读取花费的时间可能更多。

例如

select col1 from miku where col1 between 1 and 10

在确定col1=1的叶结点位置后,不再通过索引进行查询,而是在叶结点间指针上移动,完成范围查询。

范围扫描还可以应用在对字符串的前导匹配上

select col3 from miku where col3 = 'te%'

除单列索引外,范围扫描还可以应用于联合索引上。

select col4 from miku where col3 = 'test'

上述sql就可以应用范围扫描。

HINT:

INDEX FULL SCAN

索引全扫描:扫描索引的所有叶结点,通过叶结点间指针移动。

索引全扫描可以看作索引范围扫描的一种特例,扫描的范围是全集。正如上文所说,命中大量数据时,可能会降低执行速度。不过这并不代表索引全扫描一无是处,因为索引是有序的,所以索引全扫描得出的结果集是天然有序的,在某些情况下,可以一利用这一点,避免数据库对结果集进行排序,毕竟排序也是很耗时的。

当索引列允许为空时,INDEX FULL SCAN是不生效的,因为此时的索引不包含全部数据,即使HINT,数据库仍会采用TABLE ACCESS FULL访问策略

INDEX FAST FULL SCAN

索引快速扫描: 这种访问方式仅适用于CBO(cost-based optimizer),可以多块读和并行执行。但是查询的结果不一定有序,因为索引快速扫描是根据索引行在磁盘上的物理地址顺序读取,而不是通过叶结点间指针跳转。这种访问方式适用于,索引中的列包含了所有需要查询的列,不需要回表查询的场景

HINT: /+index_ffs()/

INDEX SKIP SCAN

索引跳跃扫描: 传说,这玩意不应该出现在执行计划中,如果出现说明是有问题的,通常是因为复合索引的前导列区分度不高的情况下,会出现这种访问方式。

在复合索引中,查询条件没有使用索引的前导列,而是使用了后续的列作为查询条件,这是有可能出现索引跳跃扫描。

示例说明如下:

select col3 from miku where col4 = 100

先执行

select distinct col3 from miku

如果col3上不同的值数量不多,假设col3上只有test和prod两种情况,上述语句会以

select col3 from miku where col3='test' and col4 = 100
union all 
select col3 from muku where col3='prod' and col4 = 100

的形式执行,从而利用索引

附: 索引下推

我们先来看一下一条sql在mysql数据库中执行的顺序。

在使用二级索引查询的时候,mysql通过执行器操作存储引擎读取唯一标识,然后回表查询,将记录全部信息返回匹配其他条件。索引下推就发生在读取二级索引中唯一标识时,利用二级索引的其他信息提前过滤,减少回表查询。

例如查询sql,有二级索引(name,city)

select * from user where name="LiSi" and city like "%Z%" and age > 25;

如果不使用索引下推,那么二级索引只用使用name字段,将所有name="LiSi"的记录返回

如果索引下推,那么存储引擎提前根据city条件过滤

img

当使用了索引下推之后,在执行器中就可以看到Extra字段中有特殊标注。

mysql> explain select * from user where name="LiSi" and city like "%Z%" and age > 25;
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key           | key_len | ref   | rows | filtered | Extra                              |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+------+----------+------------------------------------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_city | idx_name_city | 99      | const |    4 |     7.69 | Using index condition; Using where |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+------+----------+------------------------------------+
1 row in set, 1 warning (0.00 sec)
​

table连接

以如下sql语句进行说明

select f.* from foo f join dep d on f.id = d.id

HASH JOIN

假设foo中数据数量较小,sql优化器会将数据量小的表作为驱动表。将foo中id字段hash写入内存,再扫描数据量较大的表dep,将dep.id在写入内存的hash值中查找,完成表连接。

注意如果foo中数据量太大,不能一次都加载到内存,那么会分为不同分区写入磁盘中辅助存储

NESTED LOOP

两层for循环,伪代码如下

for record_foo in foo :
	for record_dep in dep :
		if(record_foo.id == record_dep.id)
			break;

SORT MERGE

和NESTED LOOP相似,但适用于有序数据集的连接

半连接

对于IN和EXISTS关键字,数据库在分析时往往会采用半连接(semi-join)的方式进行优化。与join关键字不同,半连接不会根据连接条件,将所有符合数据做合并,仅查找是否有存在符合条件的数据

在mysql中,提供了4种半连接的执行策略

  1. firstmatch

    在子查询时,如果查找到符合条件的数据,立刻返回当前实例,而不是扫描出全部符合条件的实例再返回

  2. loosescan

    在子查询中可以使用索引

  3. duplicateweedout

    将重复数据删除后记为临时表,并像join一样执行半连接

  4. materialization

    将子查询去重具体化为一个带有索引的临时表(通常在内存中),并按join执行连接

对于NOT IN和NOT EXISTS,使用的则是反连接(anti-join),只有在没有任何符合条件数据的情况下,才会返回真。所以一旦找到任何符合条件的数据,就可以终止子查询了

附:等价sql

select * from T where exists (select 1 from sub_T where T.join_column = sub_T.join_column)

select T.* from join (select distinct sub_T.join_column from sub_T) sub_T on T.join_column = sub_T.join_column

VIEW

假设存在表emp,dept,loct

视图合并

所有的表都在一个查询块中,并且可以按需变换表访问顺序。延迟部分操作,利用连接条件过滤数据,从而提升性能。

以如下sql为例

select e.id,e.name,dep_loc_v.dep_name,dep_loc_v.loct_code
from emp e,
     (select d.dept_id,d.dep_name,d.dep_address,l.loct_code
      from dept d,loct l
      where d.dept_id = l.dept_id) dep_loc_v
where dep_loc_v.dept_id = e.dept_id
      e.name = 'angle'

数据库通常将视图看作一个整体,将查询结果半持久化的存储在内存中(如果数据太多也需要借助磁盘存储),避免重复查询,进而提升性能。 视图合并则取消了视图的独立性,将视图的查询条件合入查询块中

select e.id,e.name,d.dep_name,l.loct_code
from emp e,dept d,loct l
where d.dept_id = l.dept_id
and   d.dept_id = e.dept_id
and   e.name = 'angle'

经过试图合并之后,所有表都在一个查询块中,这样优化器可以自主选择驱动表,并且能更好的利用表中的索引。

对于有group by和distinct关键字的语句,CBO则会预估cost,决定是否要推迟其执行时间。

比如以下sql:

create view loct_view as
select sum(l.level) level, l.dept_id
from loct l
group by l.dept_id;
select d.dep_name
from dept d,loct_view
where d.dept_id = loct_view.dept_id
and   loct_view.level > 20

如果发生视图合并

select d.dep_name
from dept d,loct l
where d.dept_id = l.dept_id
group by d.dep_name
having sum(l.level) > 20

当发生视图合并时,相当于推迟了group by的发生时间,从view创建延迟到具体查询进行。如果表dept和表loct的连接能过滤掉loct中大量数据,那么group by的花费就会降低,从而降低整个查询的花费。

通常视图合并后,执行计划中不会出现VIEW,但是存在一部分情况,即使发生了试图合并,我们仍然可以在执行计划中找到VIEW

select e.id,e.name,d.dep_name
from emp e,dept d
     (select distinct l.dept_id,l.code from loct l) loct_view
where e.dept_id = d.dept_id
and   d.dept_id = loct_view.dept_id
and   loct_view.code = '365'

其等价sql如下

select nvvw.id,nvvw.name,nvvw.dep_name
from (
    select distinct e.id,e.name,d.dep_name,l.dept_id,l.code
    from emp e,dept d,loct l
    where e.dept_id = d.dept_id
    and   l.dept_id = d.dept_id
    and   l.code = '365'
) nvvw

为了保证视图合并前后,选择的列相同,这里引入了nvvw这个投影视图。我们将distinct关键字从视图提升到外部查询,把这个查询结果作为投影视图nvvw;同时为了保证视图合并不改变查询原有语义,选择相同的列,我们又从视图中选择部分列,保证语义不发生改变。

谓词推入

HINT:推入/+push_pred(view_name)/;不推入/+no_push_pred(view_name)/

视图被数据库认为是一个独立的数据集,所以对经常被查询的数据集建立VIEW,可以避免重复查询,从而提升效率。但是有时,CBO也会将外部的条件,推入视图的查询条件中,提前对谓词过滤,从而减少视图数据量,提升效率。 不过如果视图中存在limit关键字,CBO是不会进行推入的。因为如果推入谓词,就会造成查询结果改变,不能保证优化后与原意相同。所以VIEW中不推荐出现limit关键字。


参考文献:

[1]:索引跳跃式扫描(INDEX SKIP SCAN)

[2]:8.2.2.1 Optimizing IN and EXISTS Subquery Predicates with Semijoin Transformations

[3]:Optimizer Transformations: View Merging part 1

[4]:Optimizer Transformations: View Merging part 2

[5]:PostgreSQL常用SQL优化技巧