05-sel_optimize

  1. Mysql-Limit 优化open in new window
  2. Calcite官网open in new window
  3. Apache Calcite 简介open in new window
  4. 90_practice.md
image-20220603161538846

1. 查询慢的原因

  1. 网络
  2. CPU
  3. IO
  4. 上下文切换
  5. 系统调用
  6. 生成统计信息
  7. 锁等待时间

2. 优化数据访问

  1. 减少访问数据量
    1. 应用程序是否在检索大量超过需要的数据
    2. mysql_server是否在分析大量超过需要的数据行
  2. 向DB请求了不需要的数据
    1. 查询不需要的记录
    2. 多表关联时返回全部列
    3. 总是取出全部列
    4. 重复查询相同数据
image-20220603162240806

1. 减少检索数据量

image-20220603162247689

2. 减少响应数据量(IO量)

  1. 查询不需要的记录
    • 查询后面添加limit
  2. 多表关联时返回需要列,表加别名
# 返回需要的列
select actor.*
from actor
         inner join film_actor using (actor_id)
         inner join film using (film_id)
where film.title = 'Academy Dinosaur';
  1. 总是取出全部列
    • 禁止使用select *,影响查询的性能
  2. 重复查询相同的数据
    • 这部分数据缓存起来,提高查询效率。用Redis,内存淘汰策略:LRU

3. 执行过程优化

  1. 连接器
  2. 分析器
  3. 优化器
    1. CBO基于成本优化
    2. RBO基于规则优化
  4. 执行器
image-20220603194613675

1. 查询缓存

  1. 解析查询语句之前,如果查询缓存打开,优先检查是否命中查询缓存
  2. 如果查询恰好命中了查询缓存,检查用户权限,权限没有问题。跳过所有的阶段,直接从缓存中返回结果

2. 分析器

  1. AST(Abstract_Syntax_Tree)。语法解析器&预处理器
  2. 《语法解析器》通过关键字将sql解析成一棵AST,进行语法规则验证
    • eg:验证是否使用了错误的关键字、顺序是否正确……
  3. 《预处理器》会进一步检查AST是否合法
    • eg:表名和列名是否存在、验证权限……
  4. Calcite官网open in new window
  5. Apache Calcite 简介open in new window

Apache Calcite是一款开源SQL解析工具,可以将各种SQL语句解析成抽象语法树AST(Abstract_Syntax_Tree),之后通过操作AST就可以把SQL中所要表达的算法与关系体现在具体代码之中

image-20220603194803138
image-20220603194814512

3. 优化器

  1. AST没有问题,优化器将其转成执行计划,一条sql可以有非常多的执行方式,都可以得到结果
  2. 不同的执行方式效率不同,优化器去选择最有效的执行计划
  3. 基于成本的优化器,尝试预测sql某种执行计划的成本,选择其中成本最小的
    1. CBO:基于成本的优化
    2. RBO:基于规则的优化
image-20220603195616067

1. 优化成本来源

  1. 每个表或者索引的页面个数
  2. 索引的基数
  3. 索引和数据行的长度
  4. 索引的分布情况
select count(1) from film_actor;
+----------+
| count(1) |
+----------+
|     5462 |
+----------+

# 执行前有预判。经过一系列数据得到的预估值
# 最近一次查询的成本,执行后得到的(1104数据页)
show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 1104.399000 |
+-----------------+-------------+

2. 选择错误的执行计划

  1. 统计信息不准确
    • InnoDB因为其mvcc架构,并不能维护一个数据表的行数的精确统计信息
  2. 执行计划的成本估算不等同于实际执行的成本
    • 某个执行计划可能需要读取更多页面,但其成本却更小。因为这些页面顺序读或者已经在内存中,它的访问成本将很小。Mysql层面并不知道哪些在内存中,哪些在磁盘,所以实际执行到底需要多少次IO是无法得知的
  3. 不考虑其他并发执行的查询
  4. 不考虑不受其控制的操作成本
    • 执行存储过程或者用户自定义函数的成本

3. 优化策略

image-20220604074235551
  1. 静态优化
    • 直接对解析树进行分析,并完成优化
  2. 动态优化
    • 动态优化与查询的上下文有关,也可能跟取值、索引对应的行数有关
  3. 静态优化只需要一次,动态优化在每次执行时都需要重新评估

4. 优化类型

image-20220603195709253
  1. 重新定义关联表的顺序
  2. 将外连接转化成内连接,内连接的效率要高于外连接。减少IO
  3. 等价变换规则,使用一些等价变化来简化并规划表达式
  4. 优化count(), min(), max()
    • 索引和列是否可以为空,通常可以帮助Mysql优化这类表达式
    • eg:要找到某一列的最小值,只需要查询索引的最左端的记录即可,不需要全文扫描比较
  5. 预估并转化为常数表达式
    • 检测到一个表达式可以转化为常数的时候,就一直把该表达式作为常数进行处理
  6. 覆盖索引
    • select列为index的子集
  7. 子查询优化
    • 子查询转换一种效率更高的形式,减少多个查询多次对数据进行访问
    • eg:将经常查询的数据放入到缓存中
  8. 等值传播
    • 两个列的值通过等式关联,其中一个列的where条件传递到另一个上
select * from emp e join dept d on d.deptno = e.deptno where e.deptno = '20';
# 上面、下面,等值的
select * from emp e join dept d on d.deptno = e.deptno where d.deptno = '20';

5. join查询

image-20220604084815749
1. join原理
  • Nested-Loop嵌套循环
1. Simple Nested-Loop Join
  • 简单嵌套循环
  • r驱动表,s匹配表。r中分别去匹配s的列,合并数据。对s进行r行数次访问,开销大
image-20220603120130679
2. Index Nested-Loop Join
  • index嵌套循环
  • s上有index。index为primary性能非常高,为nomal_index需要回表
image-20220603120138761
3. Block Nested-Loop Join
  • 块嵌套循环
  • 没有index,join buffer将r一批批与s匹配,减少对s的扫描
image-20220603134531007
  1. Join_Buffer会缓存所有参与查询的列而不是只有Join的列
  2. 可以调整join_buffer_size缓存大小
    • 默认值256K,最大值在MySQL 5.1.22版本前是4G,之后的版本才能在64位操作系统下申请大于4G
  3. Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch。默认开启
# block_nested_loop=on
show variables like 'optimizer_switch';

show variables like '%join_buffer%';
+----------------+------+
|Variable_name   |Value |
+----------------+------+
|join_buffer_size|262144|
+----------------+------+
Variable_nameValue
optimizer_switchindex_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
2. straight_join
# mysql优化器比手动指定的性能好
# 1. mysql默认的join顺序
explain
select a.film_id,
       a.title,
       a.release_year,
       c.actor_id,
       c.first_name,
       c.last_name
from film a
         inner join film_actor b using (film_id)
         inner join actor c using (actor_id);
+--+-----------+-----+------+
|id|select_type|table|type  |
+--+-----------+-----+------+
|1 |SIMPLE     |c    |ALL   |
|1 |SIMPLE     |b    |ref   |
|1 |SIMPLE     |a    |eq_ref|
+--+-----------+-----+------+

# 2. 指定join顺序
explain
select straight_join a.film_id,
                     a.title,
                     a.release_year,
                     c.actor_id,
                     c.first_name,
                     c.last_name
from film a
         inner join film_actor b using (film_id)
         inner join actor c using (actor_id);
+--+-----------+-----+------+
|id|select_type|table|type  |
+--+-----------+-----+------+
|1 |SIMPLE     |a    |ALL   |
|1 |SIMPLE     |b    |ref   |
|1 |SIMPLE     |c    |eq_ref|
+--+-----------+-----+------+

# 3. 查看执行的成本
show status like 'last_query_cost';

6. order by优化

image-20220603200100975

4. 优化特定类型的查询

1. count()

  1. count(字段) < count(primary) < count(1) ≈ count(*)
  2. 使用近似值
    • 在某些应用场景中,不需要完全精确的值,可以参考使用近似值来代替
    • eg:可以使用explain来获取近似的值,其实在很多OLAP的应用中,需要计算某一个列值的基数,有一个近似值的算法叫hyperloglog
# 下面三种一模一样
explain select count(*) from rental;
explain select count(1) from rental;
explain select count(rental_id) from rental;

# 查询效率
show status like 'last_query_cost';
image-20220604144733604
image-20220703182450871

1. count(*)

  1. 《阿里巴巴Java开发手册》中强制要求count(*)SQL92定义的标准统计行数的语法,与数据库无关,与null无关
  2. 数据库进很多优化
    1. MyISAM:表级锁,无where条件,表总行数单独记录,直接返回
    2. InnoDB:MySQL 8.0.13以后,选择一个成本较低的index(非聚簇索引)

2. count(1)

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

  • COUNT(1), COUNT(*),MySQL的优化是完全一样的,根本不存在谁比谁快

3. count(expr)

  • 全表扫描,检索的行中expr的值不为NULL的数量。结果是一个BIGINT值

2. 关联查询

image-20220604145523812
  1. 确保on或者using子句中的列上有索引,创建索引时考虑到关联顺序
    • 当表A和表B使用列C关联的时候,如果优化器的关联顺序是B、A,那么就不需要在B表的对应列上建index,没有用到的index只会带来额外的负担
    • 一般情况下来说,只需要在关联顺序中的第二个表的相应列上创建index
  2. group byorder by中的表达式只涉及到一个表中的列,才可能使用index

3. 子查询

  • 用关联查询join代替。因为其要使用临时表,增加IO

4. group by, distinct(无意义)

  1. index最有效。Mysql使用相同的方法来优化group bydistinct查询
    • 无法使用index,可以使用临时表或者filesort来分组
  2. 如果对关联查询做分组,并且是按照查找表中的某个列进行分组,那么可以采用查找表的标识列分组的效率比其他列更高(没有实际意义)
select a.first_name, a.last_name, count(*)
from film_actor fa
         inner join actor a using (actor_id)
group by a.first_name, a.last_name;

# group by 特例
# 查询字段不包含group by字段,不报错
# a表唯一,group by无意义
# a表不唯一,上下sql结果不一致
select a.first_name, a.last_name, count(*)
from film_actor fa
         inner join actor a using (actor_id)
group by a.actor_id;

5. limit

image-20220628191212841
  1. 数据进行分页,一般会使用limit,同时加上order by,这种方式有索引的帮助,效率通常不错。可是通常
    • order by:进行大量的文件排序操作
    • limit 10000, 10:偏移量非常大的时候,前面的大部分数据都会被抛弃,代价很高
  2. 要么是在页面中限制分页数量,要么优化大偏移量的性能
# 26s
explain
select * from oox_ooxxxx_state
order by crt_time desc limit 10000000, 5;
+--+-----------+----------------+----+-------------+----+-------+----+--------+--------+--------------+
|id|select_type|table           |type|possible_keys|key |key_len|ref |rows    |filtered|Extra         |
+--+-----------+----------------+----+-------------+----+-------+----+--------+--------+--------------+
|1 |SIMPLE     |oox_ooxxxx_state|ALL |NULL         |NULL|NULL   |NULL|21429734|100     |Using filesort|
+--+-----------+----------------+----+-------------+----+-------+----+--------+--------+--------------+


# 2.65s
explain
select * from oox_ooxxxx_state t1
    inner join (select id from oox_ooxxxx_state order by crt_time desc limit 10000000, 5) t2 using (id);
+--+-----------+----------------+------+-------------+------------+-------+-----+--------+--------+-----------+
|id|select_type|table           |type  |possible_keys|key         |key_len|ref  |rows    |filtered|Extra      |
+--+-----------+----------------+------+-------------+------------+-------+-----+--------+--------+-----------+
|1 |PRIMARY    |<derived2>      |ALL   |NULL         |NULL        |NULL   |NULL |10000005|100     |NULL       |
|1 |PRIMARY    |t1              |eq_ref|PRIMARY      |PRIMARY     |146    |t2.id|1       |100     |NULL       |
|2 |DERIVED    |oox_ooxxxx_state|index |NULL         |idx_crt_time|6      |NULL |10000005|100     |Using index|
+--+-----------+----------------+------+-------------+------------+-------+-----+--------+--------+-----------+


# 报错:his version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
select * from oox_ooxxxx_state
where id in (select id from oox_ooxxxx_state order by crt_time desc limit 10000000, 5);

6. union

  1. union:创建并填充临时表。尽量使用union all
  2. union all
  3. intersect:交集
  4. minus:差集
image-20220605000428146
  1. Mysql通过创建并填充临时表来执行union查询,很多优化策略在union都没法很好使用。经常需要手工的将where, limit, order by等子句下推到各个子查询中,以便优化器可以充分利用这些条件进行优化
  2. 除非确实需要服务器消除重复的行,否则一定要使用union allunion会给临时表加上distinct的关键字,代价很高

1. 行转列

7. 自定义变量

  • 类似于oracle_rowNum
  • 开窗函数了解一下(8以后支持)
image-20220628204411629
# @:自定义变量,@@:系统变量
select @@autocommit;

set @one := 1;
select @one;

set @i := 1;
select @i := @i + 1;

set @max_actor := (select max(actor_id)
                   from actor);
select @max_actor;

# 上一周
set @last_week := current_date - interval 1 week;
select @last_week;

1. 限制

  1. 无法使用查询缓存
  2. 不能在使用常量或者标识符的地方使用自定义变量。eg:表名、列名、limit子句
  3. 用户自定义变量的生命周期是在一个连接中有效,所以不能用它来做连接间的通信。和事务无关
  4. 不能显式地声明自定义变量的类型
  5. Mysql优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想地方式运行
  6. 赋值符号:=的优先级非常低,在使用赋值表达式的时候应该明确的使用括号
  7. 使用未定义变量不会产生任何语法错误

2. 排名语句

# 1. 给一个变量赋值的同时使用这个变量
set @rowNum := 0;
select actor_id, @rowNum := @rowNum + 1 as rownum
from actor
limit 10;

# 2. 出演电影次数最多的前10名演员,倒序排名
set @actor_number := 0;
select actor_id, cnt, @actor_number := @actor_number + 1
from (select actor_id, count(*) as cnt
      from film_actor
      group by actor_id
      order by cnt desc
      limit 10) t;

3. 查询刚更新数据

update ooxx set upd_time = now() where id = 'id1';
select upd_time from ooxx where id = 'id1';

# 高效更新时间戳,同时返回时间戳
update ooxx set upd_time = now() where id = 'id1' and @now := now();
select @now;

4. 取值顺序问题

# where和select在查询的不同阶段执行,所以看到查询到两条记录,这不符合预期
# 1. 一行一行处理data。where => select => where => select
set @rowNum := 0;
select actor_id, @rowNum := @rowNum + 1 as cnt
from actor
where @rowNum <= 1;
+--------+------+
|actor_id|rowNum|
+--------+------+
|58      |1     |
|92      |2     |
+--------+------+

# 2. 引入了order by之后,打印出了全部结果。order by进行了filesort
# 显示200行
# 整体结果集处理,where => order by => select
set @rowNum := 0;
select actor_id, @rowNum := @rowNum + 1 as cnt
from actor
where @rowNum <= 1
order by first_name;
+----------+--------+---+
|first_name|actor_id|cnt|
+----------+--------+---+
|ADAM      |71      |1  |
|ADAM      |132     |2  |
|AL        |165     |3  |
|ALAN      |173     |4  |
|ALBERT    |125     |5  |
|ALBERT    |146     |6  |
|ALEC      |29      |7  |
|...       |...     |...|
+----------+--------+---+

# 3. 解决这个问题的关键在于让变量的赋值和取值发生在执行查询的同一阶段
# 一行一行处理data,where => select
set @rowNum := 0;
select actor_id, @rowNum as cnt
from actor
where (@rowNum := @rowNum + 1) <= 1;
+--------+------+
|actor_id|rowNum|
+--------+------+
|58      |1     |
+--------+------+