在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
这样的表达式,不过,常量表达式在索引使用上的优势并不限于这种简单的例子。在一些情况下,使用常量表达式可以让查询更灵活,尤其是当表达式涉及到当前日期或动态计算值时。以下是一些在索引上使用常量表达式可能更有必要的例子:
假设你想查询所有在过去一周内创建的记录,可以使用如下查询:
WHERE create_date >= CURDATE() - INTERVAL 7 DAY
在这里,CURDATE() - INTERVAL 7 DAY
是一个常量表达式,因为它在查询执行时被计算一次,然后该值在整个查询中保持不变。如果create_date
列有索引,这样的查询可以高效利用索引。
如果你需要根据某些动态计算的结果来过滤记录,比如查询所有价格在平均价格之上的商品,可以写成:
WHERE price > (SELECT AVG(price) FROM products)
虽然这个例子中子查询的结果不是传统意义上的“常量表达式”,但它在外层查询执行前计算一次,然后作为常量值用于过滤。这种情况下,优化器的行为可能更复杂,但原理相似。
考虑一个场景,你需要根据用户输入的某个数值动态计算范围,例如查询某个数值在给定范围内的记录:
WHERE column_value BETWEEN 10 * input_value AND 20 * input_value
这里,input_value
是一个外部提供的变量,10 * input_value
和 20 * input_value
构成了基于用户输入动态计算的范围。如果column_value
列有索引,且input_value
在查询执行前是已知的,则这两个边界值会被视为常量表达式。
有时候,查询条件中可能会结合使用函数,如:
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;
MyISAM
和MEMORY
引擎中使用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在处理查询时,会提前检测到一些基于常量表达式的查询条件是不可能满足的,因此它会立即返回空结果集,而不是执行无用的查询操作。
比如:
不可能满足的比较
SELECT * FROM employees WHERE 1=0;
这里的条件1=0
是永远不可能为真的,因此MySQL不会实际执行这个查询,而是直接返回一个空结果集。
逻辑表达式中包含不可能的条件
SELECT * FROM employees WHERE salary > 10000 AND salary < 5000;
在这个例子中,salary
字段同时满足大于10000和小于5000是不可能的,因此MySQL会识别这个查询为不可能的情况,不执行查询,直接返回空结果集。
使用常量表达式的结果为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内部不需要区分WHERE
和HAVING
的逻辑,直接在数据读取时过滤条件不匹配的行。
WHERE
子句,尽早跳过行。MySQL在执行包含连接(JOIN
)操作的查询时,优化WHERE
子句的处理以提高查询效率。这种优化策略包括两个关键点:
为每个参与连接的表构造更简单的WHERE
子句:在执行连接操作时,MySQL会尝试为每个单独的表构建一个简化版本的WHERE
子句。这意味着,即使原始查询中的WHERE
子句可能包含多个表的复杂条件,MySQL也会试图为每个表单独提取与之直接相关的条件。这样做的目的是使得在访问表数据时能够更快地评估这些条件。
尽早跳过行:通过为每个表单独构造简化的WHERE
子句,MySQL可以在处理连接操作时更早地确定哪些行不满足条件,从而跳过这些行。这意味着,如果某个表的行不满足其简化WHERE
子句的条件,MySQL可以立即排除这些行,而不必等到所有的连接操作完成。这种策略大大减少了不必要的数据处理,提高了查询的整体效率。
假设有两个表employees
和departments
,并且我们想要查询所有在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 KEY
或UNIQUE
索引的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 BY
和GROUP BY
子句中所有的列都来自同一个表时,MySQL查询优化器会优先考虑这个表进行连接。当执行包含多个表的JOIN
查询时,确定哪种连接顺序最有效是一个复杂的问题。MySQL查询优化器会尝试所有可能的表连接组合来找到最佳的执行计划。这个过程考虑了多种因素,包括但不限于:
JOIN
条件优化器的目标是最小化查询的总执行时间,这可能包括减少磁盘I/O、减少网络传输开销(在分布式数据库系统中)以及减少CPU处理时间。
当ORDER BY
和GROUP BY
子句中所有的列都来自同一个表时,MySQL查询优化器会优先考虑这个表进行连接。这是因为:
ORDER BY
或GROUP BY
可以完全通过一个表的索引来满足,那么这个查询可能会更快地执行,因为数据可以在读取时就已经是排序或分组好的,减少了后续处理的需要。ORDER BY
和GROUP BY
子句查询不同表的字段时,会创建一个临时表来处理查询。不同的ORDER BY
和GROUP BY
逻辑:当ORDER BY
和GROUP BY
涉及不同的列时,MySQL需要先根据GROUP BY
条件对结果集进行分组,然后再根据ORDER BY
条件对这些分组的结果进行排序。由于这两个操作涉及不同的数据处理逻辑,MySQL可能需要使用临时表来存储GROUP BY
的中间结果,然后再应用ORDER BY
条件进行排序。
涉及多表的列:在连接操作中,如果ORDER BY
或GROUP BY
子句使用了除了连接队列中第一个表以外的表的列,那么MySQL需要在所有相关表的数据都可用后才能执行排序或分组操作。在这种情况下,使用临时表可以存储连接和过滤后的中间结果,以便于之后执行ORDER BY
或GROUP BY
操作。
性能考虑:使用临时表可能会影响查询的性能,尤其是在处理大量数据时。创建临时表需要额外的磁盘I/O(如果临时表很大而不能完全放在内存中的话)和CPU资源来管理额外的数据处理步骤。
优化策略:了解这一点对于数据库设计和查询优化非常重要。例如,尽可能地让ORDER BY
和GROUP BY
子句使用相同的列,或者确保它们引用的是连接队列中的第一个表的列,可以避免创建临时表,从而提高查询效率。
除非优化器认为使用表扫描更高效,否则会使用最佳索引。曾经,是否使用扫描是基于最佳索引覆盖了超过表中30%的数据来决定的,但现在固定的百分比不再决定使用索引还是扫描。优化器现在更加复杂,它的估计基于额外的因素,如表的大小、行数和I/O块大小。
https://dev.mysql.com/doc/refman/8.0/en/where-optimization.html