列举MySQL对于Where子句的优化

列举MySQL对于Where子句的优化

在MySQL8之前,我们自己额外注意一些查询语句的写法,牺牲了代码的可读性。在MySQL8中,会自动进行类似的优化。可以保持查询的易理解性和可维护性。
这边文章会列举这些优化点。

查询语句优化

  • 删除不必要的括号
   ((a AND b) AND c OR (((a AND b) AND (c AND d))))
-> (a AND b AND c) OR (a AND b AND c AND d)
  • 括号省略
   (a<b AND b=c) AND a=5
-> b>5 AND b=c AND a=5
  • 折叠常量表达式
   (b>=5 AND b=5) OR (b=6 AND 5=5) OR (b=7 AND 5=6)
-> b=5 OR b=6

在MySQL 8.0.14或者更高版本中,这些甚至不是在优化阶段发生的,而是准备阶段。

常量表达式被索引字段使用时,仅被计算一次

常量表达式(Constant Expression),指的是在查询执行期间其值不会改变的表达式。这些表达式可能包括数字、字符串常量或者任何不依赖于表中数据来计算其值的表达式。

假设有一个查询,它的 WHERE 子句中包含了一个如下的条件:

WHERE column_value = 3*4

如果 column_value 是一个被索引的列,那么 3*4 这个常量表达式就会在查询执行之初被计算一次,其结果 12 会被用在索引查找中,而不是对表中的每一行都计算一次 3*4

上面只是一个简单的例子,实际场景中我们没必要写3*4这样的表达式,不过,常量表达式在索引使用上的优势并不限于这种简单的例子。在一些情况下,使用常量表达式可以让查询更灵活,尤其是当表达式涉及到当前日期或动态计算值时。以下是一些在索引上使用常量表达式可能更有必要的例子:

    1. 使用当前日期和时间

假设你想查询所有在过去一周内创建的记录,可以使用如下查询:

WHERE create_date >= CURDATE() - INTERVAL 7 DAY

在这里,CURDATE() - INTERVAL 7 DAY 是一个常量表达式,因为它在查询执行时被计算一次,然后该值在整个查询中保持不变。如果create_date列有索引,这样的查询可以高效利用索引。

    1. 根据计算结果过滤

如果你需要根据某些动态计算的结果来过滤记录,比如查询所有价格在平均价格之上的商品,可以写成:

WHERE price > (SELECT AVG(price) FROM products)

虽然这个例子中子查询的结果不是传统意义上的“常量表达式”,但它在外层查询执行前计算一次,然后作为常量值用于过滤。这种情况下,优化器的行为可能更复杂,但原理相似。

    1. 动态计算的范围查询

考虑一个场景,你需要根据用户输入的某个数值动态计算范围,例如查询某个数值在给定范围内的记录:

WHERE column_value BETWEEN 10 * input_value AND 20 * input_value

这里,input_value是一个外部提供的变量,10 * input_value20 * input_value 构成了基于用户输入动态计算的范围。如果column_value列有索引,且input_value在查询执行前是已知的,则这两个边界值会被视为常量表达式。

    1. 结合函数使用

有时候,查询条件中可能会结合使用函数,如:

WHERE YEAR(create_date) = YEAR(CURDATE())

这个查询检索create_date在当前年份的记录。YEAR(CURDATE())是一个常量表达式,因为它在查询执行时仅被计算一次。虽然直接在索引列上使用函数可能会阻止索引的使用,但这个例子展示了常量表达式在查询中的应用。

常量值超出数据类型范围时,移除或折叠

从 MySQL 8.0.16 版本开始,对数值类型列与常量值进行比较时,会检查并折叠或移除无效或超出范围的值:

# CREATE TABLE t (c TINYINT UNSIGNED NOT NULL);
  SELECT * FROM t WHERE c ≪ 256;
-SELECT * FROM t WHERE 1;

MyISAMMEMORY引擎中使用COUNT(*)时,如果查询单个表不带WHERE语句,将会直接查询表信息而不是数据行。也适用于任何NOT NULL的列

当对一个单一表执行不带 WHERE 子句的 COUNT(*) 查询时,对于 MyISAM 和 MEMORY 类型的表,查询结果会直接从表的元数据中检索,而不是通过扫描表中的每一行来计算行数。这种优化可以显著提高查询的执行速度,因为直接访问元数据比逐行扫描要快得多。

当使用 COUNT(*) 并且查询中只涉及一个表时,对于任何标记为 NOT NULL 的表达式(即该表达式的结果永远不会为 NULL),MySQL 也会采取同样的优化措施。

这种优化方式仅适用于 MyISAM 和 MEMORY 表类型,并且是在查询不包含 WHERE 子句且只涉及一个表时。对于其他类型的表(如 InnoDB),由于其存储机制的不同,COUNT(*) 操作可能需要实际扫描表来确定行数,因此执行时间会更长。

提前发现无效常量表达式,不执行查询

Early detection of invalid constant expressions. MySQL quickly detects that some SELECT statements are impossible and returns no rows.

MySQL在处理查询时,会提前检测到一些基于常量表达式的查询条件是不可能满足的,因此它会立即返回空结果集,而不是执行无用的查询操作。

比如:

  1. 不可能满足的比较

    SELECT * FROM employees WHERE 1=0;
    

    这里的条件1=0是永远不可能为真的,因此MySQL不会实际执行这个查询,而是直接返回一个空结果集。

  2. 逻辑表达式中包含不可能的条件

    SELECT * FROM employees WHERE salary > 10000 AND salary < 5000;
    

    在这个例子中,salary字段同时满足大于10000和小于5000是不可能的,因此MySQL会识别这个查询为不可能的情况,不执行查询,直接返回空结果集。

  3. 使用常量表达式的结果为FALSE

    SELECT * FROM employees WHERE FALSE;
    

    因为条件永远不可能为真(FALSE就是假),所以MySQL不会执行这个查询,直接返回空结果集。

如果没有使用GROUP BY或者聚合函数的情况下使用HAVING关键字,会和WHERE子句合并处理。

通常,HAVING子句用于对GROUP BY聚合操作的结果进行条件过滤。如果查询中没有使用GROUP BY子句或者聚合函数(如COUNT(), MIN(), MAX(), SUM()等),MySQL会将HAVING子句的条件与WHERE子句的条件合并处理。

假设有一个简单的查询,既没有使用GROUP BY,也没有聚合函数,但是使用了HAVING

SELECT * FROM employees HAVING salary > 5000;

在这个例子中,HAVING子句的条件(salary > 5000)会被MySQL处理为WHERE子句的一部分,就好像这样写:

SELECT * FROM employees WHERE salary > 5000;

这样的处理可以提高查询效率,因为MySQL内部不需要区分WHEREHAVING的逻辑,直接在数据读取时过滤条件不匹配的行。

为每个参与连接的表构造更简单的WHERE子句,尽早跳过行。

MySQL在执行包含连接(JOIN)操作的查询时,优化WHERE子句的处理以提高查询效率。这种优化策略包括两个关键点:

  1. 为每个参与连接的表构造更简单的WHERE子句:在执行连接操作时,MySQL会尝试为每个单独的表构建一个简化版本的WHERE子句。这意味着,即使原始查询中的WHERE子句可能包含多个表的复杂条件,MySQL也会试图为每个表单独提取与之直接相关的条件。这样做的目的是使得在访问表数据时能够更快地评估这些条件。

  2. 尽早跳过行:通过为每个表单独构造简化的WHERE子句,MySQL可以在处理连接操作时更早地确定哪些行不满足条件,从而跳过这些行。这意味着,如果某个表的行不满足其简化WHERE子句的条件,MySQL可以立即排除这些行,而不必等到所有的连接操作完成。这种策略大大减少了不必要的数据处理,提高了查询的整体效率。

例子

假设有两个表employeesdepartments,并且我们想要查询所有在IT部门工作且工资超过5000的员工。原始查询可能如下:

SELECT e.name, e.salary, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.id
WHERE d.department_name = 'IT' AND e.salary > 5000;

在这个查询中:

  • 对于employees表,MySQL会构造一个简化的WHERE子句,比如e.salary > 5000,以便尽早排除不满足条件的员工记录。
  • 对于departments表,简化的WHERE子句可能是d.department_name = 'IT',用于快速排除非IT部门的记录。

查询中,所有常量表会优先于其他表被读取。

在MySQL中,一个“常量表”指的是满足以下任一条件的表:

  • 空表或只有一行数据的表:这样的表在查询中被视为常量表,因为它们的返回结果是固定的(要么是空,要么是唯一的行)。

  • 使用PRIMARY KEYUNIQUE索引的WHERE子句进行查询的表,其中所有索引部分都与常量表达式进行比较,并且这些索引列都被定义为NOT NULL。这意味着,查询可以直接通过索引定位到特定的行,因此这样的表也被视为常量表。

比如:

SELECT * FROM t WHERE primary_key=1;
SELECT * FROM t1,t2
  WHERE t1.primary_key=1 AND t2.primary_key=t1.id;

ORDER BYGROUP BY子句中所有的列都来自同一个表时,MySQL查询优化器会优先考虑这个表进行连接。

当执行包含多个表的JOIN查询时,确定哪种连接顺序最有效是一个复杂的问题。MySQL查询优化器会尝试所有可能的表连接组合来找到最佳的执行计划。这个过程考虑了多种因素,包括但不限于:

  • 各个表的大小
  • 可用的索引
  • JOIN条件
  • 查询中的过滤条件等

优化器的目标是最小化查询的总执行时间,这可能包括减少磁盘I/O、减少网络传输开销(在分布式数据库系统中)以及减少CPU处理时间。

ORDER BYGROUP BY子句中所有的列都来自同一个表时,MySQL查询优化器会优先考虑这个表进行连接。这是因为:

  • 如果可以先从这个表开始查询,并且在查询的早期阶段就对数据进行排序或分组,那么可能会减少后续操作中需要处理的数据量。
  • 这种优化特别有用,因为它可以利用索引来快速完成排序和分组操作,从而避免了昂贵的临时表操作或文件排序。
  • 如果ORDER BYGROUP BY可以完全通过一个表的索引来满足,那么这个查询可能会更快地执行,因为数据可以在读取时就已经是排序或分组好的,减少了后续处理的需要。

含有ORDER BYGROUP BY子句查询不同表的字段时,会创建一个临时表来处理查询。

为什么需要临时表?

  • 不同的ORDER BYGROUP BY逻辑:当ORDER BYGROUP BY涉及不同的列时,MySQL需要先根据GROUP BY条件对结果集进行分组,然后再根据ORDER BY条件对这些分组的结果进行排序。由于这两个操作涉及不同的数据处理逻辑,MySQL可能需要使用临时表来存储GROUP BY的中间结果,然后再应用ORDER BY条件进行排序。

  • 涉及多表的列:在连接操作中,如果ORDER BYGROUP BY子句使用了除了连接队列中第一个表以外的表的列,那么MySQL需要在所有相关表的数据都可用后才能执行排序或分组操作。在这种情况下,使用临时表可以存储连接和过滤后的中间结果,以便于之后执行ORDER BYGROUP BY操作。

临时表的影响

  • 性能考虑:使用临时表可能会影响查询的性能,尤其是在处理大量数据时。创建临时表需要额外的磁盘I/O(如果临时表很大而不能完全放在内存中的话)和CPU资源来管理额外的数据处理步骤。

  • 优化策略:了解这一点对于数据库设计和查询优化非常重要。例如,尽可能地让ORDER BYGROUP BY子句使用相同的列,或者确保它们引用的是连接队列中的第一个表的列,可以避免创建临时表,从而提高查询效率。

使用索引,或者全表扫描,优化器的决定。

除非优化器认为使用表扫描更高效,否则会使用最佳索引。曾经,是否使用扫描是基于最佳索引覆盖了超过表中30%的数据来决定的,但现在固定的百分比不再决定使用索引还是扫描。优化器现在更加复杂,它的估计基于额外的因素,如表的大小、行数和I/O块大小。

参考文档

https://dev.mysql.com/doc/refman/8.0/en/where-optimization.html

  • 51
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑风风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值