MySQL 8.0 参考手册——8.2优化 SQL 语句(二)

8.2.1.13 条件过滤 

在连接处理中,前缀行是从连接中的一个表传递到下一个表的行。一般来说,优化程序会尝试将前缀数较少的表放在连接顺序的前面,以防止行组合数迅速增加。如果优化器能使用从一个表中选择并传递到下一个表的行的条件信息,就能更准确地计算行估计值并选择最佳执行计划。

如果不使用条件过滤,那么表的前缀行数将基于 WHERE 子句根据优化器选择的访问方法估计行数。条件过滤使优化器能够在访问方法未考虑到的WHERE子句中使用其他相关条件,从而改进其前缀行行数估计值。例如,即使有一种基于索引的访问方法可用于在连接中从当前表中选择行,但在 WHERE 子句中也可能存在表的其他条件,可以过滤(进一步限制)传递到下一个表的合格行的估计值。

只有在以下情况下,条件才会对过滤估计有所帮助:

  • 它指的是当前表格。
  • 它取决于一个常量值或来自连接序列中早期表的值。
  • 访问方法尚未将其考虑在内。

在 EXPLAIN 输出中,行列 表示所选访问方法的行估计值,过滤列反映条件过滤的效果。最大值为 100,表示没有过滤行。从 100 开始递减的值表示过滤量在增加。

前缀行数(在连接中从当前表传递到下一个表的估计行数)是行数与过滤值的乘积。也就是说,前缀行数是估计的行数,再减去估计的过滤效果。例如,如果行数为 1000,过滤值为 20%,则条件过滤会将估计行数 1000 减少为前缀行数 1000 × 20% = 1000 × 0.2 = 200。

请看下面的查询:

SELECT *
  FROM employee JOIN department ON employee.dept_no = department.dept_no
  WHERE employee.first_name = 'John'
  AND employee.hire_date BETWEEN '2018-01-01' AND '2018-06-01';

假设该数据集具有以下特征:

  • 雇员表有 1024 行。
  • 部门表有 12 行。
  • 两个表都有一个关于 dept_no 的索引。
  • 雇员表在 first_name 上有一个索引。
  • 有 8 条记录满足 employee.first_name 上的条件:
employee.first_name = 'John'
  • 有 150 条记录满足 employee.hire_date 的这一条件:

 

employee.hire_date BETWEEN '2018-01-01' AND '2018-06-01'
  • 有 1 行同时满足这两个条件:
  • employee.first_name = 'John'
    AND employee.hire_date BETWEEN '2018-01-01' AND '2018-06-01'

     

如果不进行条件过滤,EXPLAIN 会产生这样的输出结果:

+----+------------+--------+------------------+---------+---------+------+----------+
| id | table      | type   | possible_keys    | key     | ref     | rows | filtered |
+----+------------+--------+------------------+---------+---------+------+----------+
| 1  | employee   | ref    | name,h_date,dept | name    | const   | 8    | 100.00   |
| 1  | department | eq_ref | PRIMARY          | PRIMARY | dept_no | 1    | 100.00   |
+----+------------+--------+------------------+---------+---------+------+----------+

就雇员而言,名称索引的访问方法会获取与名称 "John "匹配的 8 条记录。没有进行过滤(过滤率为 100%),因此所有记录都是下一个表的前缀行:前缀行计数为行 × 过滤 = 8 × 100% = 8。

通过条件过滤,优化器会额外考虑访问方法未考虑的 WHERE 子句中的条件。在本例中,优化器使用启发式方法估算了 employee.hire_date 的 BETWEEN 条件的过滤效果为 16.31%。因此,EXPLAIN 的输出结果如下:

+----+------------+--------+------------------+---------+---------+------+----------+
| id | table      | type   | possible_keys    | key     | ref     | rows | filtered |
+----+------------+--------+------------------+---------+---------+------+----------+
| 1  | employee   | ref    | name,h_date,dept | name    | const   | 8    | 16.31    |
| 1  | department | eq_ref | PRIMARY          | PRIMARY | dept_no | 1    | 100.00   |
+----+------------+--------+------------------+---------+---------+------+----------+

现在,前缀行数为行 × 筛选 = 8 × 16.31% = 1.3,更接近实际数据集。

通常情况下,优化器不会计算最后一个连接表的条件过滤效果(前缀行数减少),因为没有下一个表可以传递行。但 EXPLAIN 的情况例外:为了提供更多信息,会计算所有连接表(包括最后一个表)的过滤效果。

要控制优化器是否考虑额外的过滤条件,可以使用 optimizer_switch 系统变量的 condition_fanout_filter 标志(参见第 8.9.2 节 "可切换优化")。默认情况下启用该标志,但也可以禁用该标志来抑制条件过滤(例如,如果发现某个查询在不使用条件过滤的情况下性能更好)。

如果优化器高估了条件过滤的效果,性能可能会比不使用条件过滤时更差。在这种情况下,这些技术可能会有所帮助:

  • 如果未对某列进行索引,则对其进行索引,这样优化器就能获得一些关于列值分布的信息,从而改进对行的估计。
  • 同样,如果没有列直方图信息,可以生成直方图(参见第 8.9.6 节 "优化器统计")。
  • 更改连接顺序。实现这一点的方法包括连接顺序优化提示(参见第 8.9.3 节 "优化提示")、紧跟 SELECT 的 STRAIGHT_JOIN 以及 STRAIGHT_JOIN 连接操作符。
  • 禁用会话的条件筛选:
  • SET optimizer_switch = 'condition_fanout_filter=off';

    或者,对于给定查询,使用优化器提示:

  • SELECT /*+ SET_VAR(optimizer_switch = 'condition_fanout_filter=off') */ ...

     

 

8.2.1.14 常数折叠优化

常数折叠(Constant folding)以及常数传播(constant propagation)都是编译器最佳化技术,他们被使用在现代的编译器中。进阶的常数传播形式,或称之为稀疏有条件的常量传播(sparse conditional constant propagation),可以更精确地传播常数及无缝的移除无用的程式码。(提前把值算出来)

在常量和列值之间进行比较时,如果常量值超出范围或相对于列类型而言类型错误,现在会在查询优化过程中逐行处理,而不是在执行过程中处理。以这种方式处理的比较有 >、>=、<、<=、<>/!=、= 和 <=>。

请看下面语句创建的表格:

CREATE TABLE t (c TINYINT UNSIGNED NOT NULL);

在查询 SELECT * FROM t WHERE c < 256 中的 WHERE 条件包含整数常数 256,这超出了 TINYINT UNSIGNED 列的范围。以前的处理方法是将两个操作数都视为较大的类型,但现在,由于 c 的任何允许值都小于常数,WHERE 表达式可以折叠为 WHERE 1,这样查询就改写为 SELECT * FROM t WHERE 1。

这样,优化器就可以完全删除 WHERE 表达式。如果列 c 是可空的(即只定义为 TINYINT UNSIGNED),查询可以这样重写:

SELECT * FROM t WHERE ti IS NOT NULL

与支持的 MySQL 列类型相比,常量的折叠方法如下:

Integer column type 整数类型与以下类型的常量进行比较,如此处所述:

Integer value.如果常量超出列类型的范围,比较结果将折叠为 1 或 IS NOT NULL,如前所述。如果常量是范围边界,则比较结果将折叠为 =:

mysql> EXPLAIN SELECT * FROM t WHERE c >= 255;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 20.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS;
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `test`.`t`.`ti` AS `ti` from `test`.`t` where (`test`.`t`.`ti` = 255)
1 row in set (0.00 sec)

Floating- or fixed-point value.

如果常数是十进制类型(如 DECIMAL、REAL、DOUBLE 或 FLOAT)之一,且小数部分不为零,则不可能相等;因此要进行相应的折叠。对于其他比较,根据符号向上或向下舍入为整数值,然后执行范围检查,并按照前面所述的整数-整数比较进行处理。

如果一个 REAL 值太小,无法用十进制表示,则会根据符号四舍五入为 .01 或 -.01,然后按十进制处理。

String types. 

尝试将字符串值解释为整数类型,然后在整数值之间进行比较。如果失败,则尝试将值作为 REAL 处理。

 DECIMAL or REAL column. 十进制类型与以下类型的常量进行比较,如此处所述:

Integer value. 对列值的整数部分进行范围检查。如果没有折叠结果,则将常数转换为小数位数,小数位数与列值相同,然后将其作为小数位数进行检查(见下一步)。

DECIMAL or REAL value. 检查是否有溢出(即常数整数部分的位数是否超过列的十进制类型所允许的位数)。如果是,则折叠。

String value.  如果数值可以解释为整数类型,则按整数类型处理。否则,尽量按 REAL 类型处理。

FLOAT or DOUBLE column. 

FLOAT(m,n) 或 DOUBLE(m,n) 值与常量相比的处理方法如下:

如果数值超出列的范围,则折叠。

如果数值超过 n 个小数,则截断,在折叠过程中进行补偿。对于 = 和 <> 比较,如前所述,折叠为 TRUE、FALSE 或 IS [NOT] NULL;对于其他操作符,调整操作符。

如果数值的整数位数超过 m,则折叠。

限制  以下情况不能使用此优化:

使用 BETWEEN 或 IN 进行比较。

使用 BIT 列或使用日期或时间类型的列。

在准备语句的准备阶段,尽管可以在优化阶段实际执行准备语句时使用。这是因为在语句准备期间,常量的值尚未确定。

8.2.1.15 IS NULL 优化

MySQL 可以对 col_name IS NULL 执行与 col_name = constant_value 相同的优化。例如,MySQL 可以使用索引和范围来搜索 IS NULL 中的 NULL。

例如

SELECT * FROM tbl_name WHERE key_col IS NULL;

SELECT * FROM tbl_name WHERE key_col <=> NULL;

SELECT * FROM tbl_name
  WHERE key_col=const1 OR key_col=const2 OR key_col IS NULL;

如果 WHERE 子句包含一个 col_name IS NULL 条件,而该列已声明为 NOT NULL,那么该表达式将被优化掉。如果列无论如何都可能产生 NULL(例如,如果列来自 LEFT JOIN 右侧的表),则不会进行这种优化。

MySQL 还可以优化 col_name = expr OR col_name IS NULL 组合,这种形式在解析子查询中很常见。使用这种优化时,EXPLAIN 会显示 ref_or_null。

这种优化可以处理任何键部分的一个 IS NULL。

假设表 t2 的 a 列和 b 列上有索引,优化后查询的一些示例:

 

 

 

8.2.1.16 ORDER BY 优化

 

8.2.1.17 GROUP BY 优化

 

8.2.1.18 DISTINCT 优化

 

8.2.1.19 LIMIT 查询优化

 

8.2.1.20 函数调用优化

8.2.1.21 窗口函数优化

8.2.1.22 行构造表达式优化

8.2.1.23 避免全表扫描

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章