移除不必要的括号
有时候表达式里有许多无用的括号,比如这样:
((a = 5 AND b = c) OR ((a > c) AND (c < 5)))
看着就很烦,优化器会把那些用不到的括号给干掉,就是这样:
(a = 5 and b = c) OR (a > c AND c < 5)
常量传递(constant_propagation)
有时候某个表达式是某个列和某个常量做等值匹配,比如这样:
a = 5
当这个表达式和其他涉及列 a 的表达式使用 AND 连接起来时,可以将其他
表达式中的 a 的值替换为 5,比如这样:
a = 5 AND b > a
有时候多个列之间存在等值匹配的关系,比如这样:
a = b and b = c and c = 5
这个表达式可以被简化为:
a = 5 and b = 5 and c = 5
移除没用的条件(trivial_condition_removal)
(a < 1 and b = b) OR (a = 6 OR 5 != 5)
很明显,b = b 这个表达式永远为 TRUE,5 != 5 这个表达式永远为 FALSE,所
以简化后的表达式就是这样的:
(a < 1 and TRUE) OR (a = 6 OR FALSE)
可以继续被简化为
a < 1 OR a = 6
表达式计算
在查询开始执行之前,如果表达式中只包含常量的话,它的值会被先计算出
来,比如这个:
a = 5 + 1
因为 5 + 1 这个表达式只包含常量,所以就会被化简成:
a = 6
但是这里需要注意的是,如果某个列并不是以单独的形式作为表达式的操作
数时,比如出现在函数中,出现在某个更复杂表达式中,就像这样:
ABS(a) > 5
或者:
-a < -8
优化器是不会尝试对这些表达式进行化简的。我们前边说过只有搜索条件中
索引列和常数使用某些运算符连接起来才可能使用到索引,所以如果可以的话,
最好让索引列以单独的形式出现在表达式中。
常量表检测
使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表。
MySQL 觉得这两种查询花费的时间特别少,少到可以忽略,所以也把通过这 两种方式查询的表称之为常量表(英文名:constant tables)。优化器在分析一 个查询语句时,先首先执行常量表查询,然后把查询中涉及到该表的条件全部替换成常数,最后再分析其余表的查询成本,比方说这个查询语句:
SELECT * FROM table1 INNER JOIN table2 ON table1.column1 = table2.column2 WHERE table1.primary_key = 1;
很明显,这个查询可以使用主键和常量值的等值匹配来查询 table1 表,也就 是在这个查询中 table1 表相当于常量表,在分析对 table2 表的查询成本之前, 就会执行对 table1 表的查询,并把查询中涉及 table1 表的条件都替换掉,也就是上边的语句会被转换成这样:
SELECT table1 表记录的各个字段的常量值, table2.* FROM table1 INNER JOIN table2 ON table1 表 column1 列的常量值 = table2.column2;
我们前边说过,内连接的驱动表和被驱动表的位置可以相互转换,而左(外) 连接和右(外)连接的驱动表和被驱动表是固定的。这就导致内连接可能通过优化表的连接顺序来降低整体的查询成本,而外连接却无法优化表的连接顺序。
外连接和内连接的本质区别就是:对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配 ON 子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用 NULL 值填充;而内连接的驱动表的记录如果无法在被驱动表中找到匹配 ON 子句中的过滤条件的记录,那么该记录会被舍弃。查询效果就是这样:
SELECT * FROM e1 INNER JOIN e2 ON e1.m1 = e2.m2;
m1 | n1 | m2 | n2 |
---|---|---|---|
2 | b | 2 | b |
3 | c | 3 | c |
SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2;
m1 | n1 | m2 | n2 |
---|---|---|---|
2 | b | 2 | b |
3 | c | 3 | c |
1 | a | null | null |
对于上边例子中的(左)外连接来说,由于驱动表 e1 中 m1=1, n1='a’的记录无法 在被驱动表 e2 中找到符合 ON 子句条件 e1.m1 = e2.m2 的记录,所以就直接把这条记录加入到结果集,对应的 e2 表的 m2 和 n2 列的值都设置为 NULL。
因为凡是不符合 WHERE 子句中条件的记录都不会参与连接。只要我们在搜 索条件中指定关于被驱动表相关列的值不为 NULL,那么外连接中在被驱动表中 找不到符合ON子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:在这种情况下:==外连接和内连接也就没有什么区别了!==比方说这个查询:
SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.n2 IS NOT NULL;
由于指定了被驱动表 e2 的 n2 列不允许为 NULL,所以上边的 e1 和 e2 表的 左(外)连接查询和内连接查询是一样的。当然,我们也可以不用显式的指定被 驱动表的某个列 IS NOT NULL,只要隐含的有这个意思就行了,比方说这样:
SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.m2 = 2;
在这个例子中,我们在 WHERE 子句中指定了被驱动表 e2 的 m2 列等于 2, 也就相当于间接的指定了 m2 列不为 NULL 值,所以上边的这个左(外)连接查询其实和下边这个内连接查询是等价的:
SELECT * FROM e1 INNER JOIN e2 ON e1.m1 = e2.m2 WHERE e2.m2 = 2;
我们把这种在外连接查询中,指定的 WHERE 子句中包含被驱动表中的列不为 NULL 值的条件称之为空值拒绝(英文名:reject-NULL)。在被驱动表的 WHERE句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。
把子查询放在外层查询的 WHERE 子句或者 ON 子句中可能是我们最常用的 一种使用子查询的方式了,比如这样:
这个查询表明我们想要将(SELECT m2 FROM e2)这个子查询的结果作为外层 查询的 IN 语句参数,整个查询语句的意思就是我们想找 e1 表中的某些记录,这 些记录的 m1 列的值能在 e2 表的 m2 列找到匹配的值。
那些只返回一个单一值的子查询称之为标量子查询,比如这样:
SELECT (SELECT m1 FROM e1 LIMIT 1);
SELECT * FROM e1 WHERE m1 = (SELECT MIN(m2) FROM e2)
SELECT * FROM e1 WHERE m1 < (SELECT MIN(m2) FROM e2);
这两个查询语句中的子查询都返回一个单一的值,也就是一个标量。这些标 量子查询可以作为一个单一值或者表达式的一部分出现在查询语句的各个地方。
如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之 为相关子查询。比如:
例子中的子查询是(SELECT m2 FROM e2 WHERE n1 = n2),可是这个查询中有 一个搜索条件是 n1 = n2,别忘了 n1 是表 e1 的列,也就是外层查询的列,也就 是说子查询的执行需要依赖于外层查询的值,所以这个子查询就是一个相关子查 询。
具体的语法形式如下:
这个布尔表达式的意思是用来判断某个操作数在不在由子查询结果集组成 的集合中,比如下边的查询的意思是找出 e1 表中的某些记录,这些记录存在于 子查询的结果集中:
具体的语法形式如下:
这个布尔表达式的意思是只要子查询结果集中存在某个值和给定的操作数 做比较操作,比较结果为 TRUE,那么整个表达式的结果就为 TRUE,否则整个表 达式的结果就为 FALSE。比方说下边这个查询:
具体的语法形式如下:
这个布尔表达式的意思是子查询结果集中所有的值和给定的操作数做比较 操作比较结果为 TRUE,那么整个表达式的结果就为 TRUE,否则整个表达式的结 果就为 FALSE。比方说下边这个查询:
有的时候我们仅仅需要判断子查询的结果集中是否有记录,而不在乎它的记 录具体是个啥,可以使用把 EXISTS 或者 NOT EXISTS 放在子查询语句前边,就像 这样:
对于子查询(SELECT 1 FROM e2)来说,我们并不关心这个子查询最后到底查 询出的结果是什么,所以查询列表里填*、某个列名,或者其他啥东西都无所谓, 我们真正关心的是子查询的结果集中是否存在记录。也就是说只要(SELECT 1 FROM e2)这个查询中有记录,那么整个 EXISTS 表达式的结果就为 TRUE。
前提,对于in查询,in中数量小于200个,是精确统计页面,大于200个时,是桶index divive去统计的
想象中子查询的执行方式是这样的:
如果该子查询是不相关子查询,比如下边这个查询:
如果该子查询是相关子查询,比如下边这个查询:
但真的是这样吗?其实 MySQL 用了一系列的办法来优化子查询的执行,大 部分情况下这些优化措施其实挺有效的,下边我们来看看各种不同类型的子查询 具体是怎么执行的。
对于不相关标量子查询或者行子查询来说,它们的执行方式很简单,比方说 下边这个查询语句:
对于相关的标量子查询或者行子查询来说,比如下边这个查询:
物化表就是临时表,进内存还是进磁盘,取决于表大小,还有系统变量和堆大小可以控制
对于不相关的 IN 子查询,比如这样:
order_no = ‘a’);
我们最开始的感觉就是这种不相关的 IN 子查询和不相关的标量子查询或者 行子查询是一样一样的,都是把外层查询和子查询当作两个独立的单表查询来对 待。但是 MySQL 为了优化 IN 子查询下了很大力气,所以整个执行过程并不像我 们想象的那么简单。
对于不相关的 IN 子查询来说,如果子查询的结果集中的记录条数很少,那 么把子查询和外层查询分别看成两个单独的单表查询效率很高,但是如果单独执 行子查询后的结果集太多的话,就会导致这些问题:
在对外层查询执行全表扫描时,由于 IN 子句中的参数太多,这会导致检测 一条记录是否符合和 IN 子句中的参数匹配花费的时间太长。
如果 IN 子句中的参数比较多时,比如这样:
MySQL 的改进是不直接将不相关子查询的结果集当作外层查询的参数,而是 将该结果集写入一个临时表里。写入临时表的过程是这样的:
一般情况下子查询结果集不会大的离谱,所以会为它建立基于内存的使用 Memory 存储引擎的临时表,而且会为该表建立哈希索引。
如果子查询的结果集非常大,超过了系统变量 tmp_table_size 或者 max_heap_table_size,临时表会转而使用基于磁盘的存储引擎来保存结果集中的 记录,索引类型也对应转变为 B+树索引。
MySQL 把这个将子查询结果集中的记录保存到临时表的过程称之为物化(英 文名:Materialize)。为了方便起见,我们就把那个存储子查询结果集的临时表 称之为物化表。正因为物化表中的记录都建立了索引(基于内存的物化表有哈希 索引,基于磁盘的有 B+树索引),通过索引执行 IN 语句判断某个操作数在不在 子查询结果集中变得非常快,从而提升了子查询语句的性能。
事情到这就完了?我们还得重新审视一下最开始的那个查询语句:
转化成内连接之后就有意思了,查询优化器可以评估不同连接顺序需要的成 本是多少,选取成本最低的那种查询方式执行查询。我们分析一下上述查询中使 用外层查询的表 s1 和物化表 materialized_table 进行内连接的成本都是由哪几部 分组成的:
虽然将子查询进行物化之后再执行查询都会有建立临时表的成本,但是不管 怎么说,我们见识到了将子查询转换为连接的强大作用,MySQL 继续开脑洞:能不能不进行物化操作直接把子查询转换为连接呢?让我们重新审视一下上边的 查询语句:
将 s1 表和 s2 表进行半连接的意思就是:对于 s1 表的某条记录来说,我们 只关心在 s2 表中是否存在与之匹配的记录,而不关心具体有多少条记录与之匹 配,最终的结果集中只保留 s1 表的记录。为了让大家有更直观的感受,我们假设 MySQL 内部是这么改写上边的子查询的:
Table pullout (子查询中的表上拉)
DuplicateWeedout execution strategy (重复值消除)
对于这个查询来说:
SELECT * FROM s1 WHERE order_note IN (SELECT order_note FROM s2 WHERE order_no= ‘a’);
转换为半连接查询后,s1 表中的某条记录可能在 s2 表中有多条匹配的记录, 所以该条记录可能多次被添加到最后的结果集中,为了消除重复,我们可以建立 一个临时表,比方说这个临时表长这样:
CREATE TABLE tmp (
id PRIMARY KEY
);
LooseScan execution strategy (松散扫描)
semi-join 的适用条件
MySQL 对不能转为 semi-join 查询的子查询优化
1、对于不相关子查询来说,可以尝试把它们物化之后再参与查询 比如我们上边提到的这个查询:
2、不管子查询是相关的还是不相关的,都可以把 IN 子查询尝试转为 EXISTS 子查询
其实对于任意一个 IN 子查询来说,都可以被转为 EXISTS 子查询,通用的例 子如下:
outer_expr IN (SELECT inner_expr FROM … WHERE subquery_where)
可以被转换为:
EXISTS (SELECT inner_expr FROM … WHERE subquery_where AND outer_expr=inner_expr)
为啥要转换呢?这是因为不转换的话可能用不到索引,比方说下边这个查询:
SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.order_note = s2.order_note) OR insert_time > ‘2021-03-22 18:28:28’;
这个查询中的子查询是一个相关子查询,而且子查询执行的时候不能使用到 索引,但是将它转为 EXISTS 子查询后却可以使用到索引:
SELECT * FROM s1 WHERE EXISTS (SELECT 1 FROM s2 where s1.order_note = s2.order_note AND s2.order_no= s1.order_no) OR insert_time > ‘2021-03-22 18:28:28’00;
转为 EXISTS 子查询时便可能使用到 s2 表的 idx_order_no 索引了。 需要注意的是,如果 IN 子查询不满足转换为 semi-join 的条件,又不能转换 为物化表或者转换为物化表的成本太大,那么它就会被转换为 EXISTS 查询。
在 MySQL5.5 以及之前的版本没有引进 semi-join 和物化的方式优化子查询时, 优化器都会把 IN 子查询转换为 EXISTS 子查询,所以当时好多声音都是建议大家 把子查询转为连接,不过随着 MySQL 的发展,最近的版本中引入了非常多的子 查询优化策略,内部的转换工作优化器会为大家自动实现。
如果 IN 子查询符合转换为 semi-join 的条件,查询优化器会优先把该子查询 转换为 semi-join,然后再考虑下边 5 种执行半连接的策略中哪个成本最低:
如果 IN 子查询不符合转换为 semi-join 的条件,那么查询优化器会从下边两 种策略中找出一种成本更低的方式执行子查询:
如果 ANY/ALL 子查询是不相关子查询的话,它们在很多场合都能转换成我们 熟悉的方式去执行,比方说:
原始表达式转换为
如果[NOT] EXISTS 子查询是不相关子查询,可以先执行子查询,得出该[NOT] EXISTS 子查询的结果是 TRUE 还是 FALSE,并重写原先的查询语句,比如对这个 查询来说:
对于相关的[NOT] EXISTS 子查询来说,比如这个查询:
一般来说,不需要去理解 MySQL 通信协议的内部实现细节,只需要大致理 解通信协议是如何工作的。MySQL 客户端和服务器之间的通信协议是“半双工” 的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由 客户端向服务器发送数据,这两个动作不能同时发生。所以,我们无法也无须将一个消息切成小块独立来发送。
这种协议让 MySQL 通信简单快速,但是也从很多地方限制了 MySQL。一个 明显的限制是,这意味着没法进行流量控制。一旦一端开始发生消息,另一端要 接收完整个消息才能响应它。这就像来回抛球的游戏﹔在任何时刻,只有一个人 能控制球,而且只有控制球的人才能将球抛回去(发送消息)。
客户端用一个单独的数据包将查询传给服务器。这也是为什么当查询的语句 很长的时候,参数 max_allowed_packet 就特别重要了。一旦客户端发送了请求, 它能做的事情就只是等待结果了。
相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服 务器开始响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单 地只取前面几条结果,然后让服务器停止发送数据。这种情况下,客户端若接收 完整的结果,然后取前面几条需要的结果,或者接收完几条结果后就“粗暴”地 断开连接,都不是好主意。这也是在必要的时候一定要在查询中加上 LIMIT 限制(分页) 的原因。
换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数 据的过程,但实际上是 MySQL 在向客户端推送数据的过程。客户端不断地接收 从服务器推送的数据,客户端也没法让服务器停下来。 多数连接 MySQL 的库函数都可以获得全部结果集并缓存到内存里,还可以 逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL 通常 需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接 收全部结果并缓存通常可以减少服务器的压力,让查询能够早点结束、早点释放 相应的资源。
当使用库函数从 MySQL 获取数据时,其结果看起来都像是从 MySQL 服务器 获取数据,而实际上都是从这个库函数的缓存获取数据。多数情况下这没什么问 题,但是如果需要返回一个很大的结果集的时候,这样做并不好,因为库函数会 花很多时间和内存来存储所有的结果集。
对于 Java 程序来说,很有可能发生 OOM,所以 MySQL 的 JDBC 里提供了 setFetchSize() 之类的功能,来解决这个问题:
1、当 statement 设置以下属性时,采用的是流数据接收方式,每次只从服 务器接收部份数据,直到所有数据处理完毕,不会发生 JVM OOM。 setResultSetType(ResultSet.TYPE_FORWARD_ONLY); setFetchSize(Integer.MIN_VALUE);
2、调用 statement 的 enableStreamingResults 方法,实际上 enableStreamingResults 方法内部封装的就是第 1 种方式。
3、设置连接属性 useCursorFetch=true (5.0 版驱动开始支持),statement 以 TYPE_FORWARD_ONLY 打开,再设置 fetch size 参数,表示采用服务器端游标,每 次从服务器取 fetch_size 条数据。
con = DriverManager.getConnection(url);
ps = (PreparedStatement) con.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
ps.setFetchSize(Integer.MIN_VALUE);
ps.setFetchDirection(ResultSet.FETCH_REVERSE);
rs = ps.executeQuery();
while (rs.next()) {……实际的业务处理}
The thread is creating a temporary table in memory or on disk. If the table is created in memory but later is converted to an on-disk table, the state during that operation is Copying to tmp table on disk.
该线程正在内存或磁盘上创建临时表。如果表在内存中创建但稍后转换为磁 盘表,则该操作期间的状态将为 Copying to tmp table on disk
select @@profiling;
set profiling=1;
block io、contextswitch、page faults 等明细类型来查看 MySQL 在使用什么资源上 耗费了过高的时间:
show profile all for query 1\G
能够发现 Sending data 状态下,时间主要消耗在 CPU 上了。 所以show profile能够在做SQL优化时帮助我们了解时间都耗费到哪里去了,同时如果 MySQL 源码感兴趣,还可以通过 show profile source for query 查看 SQL 解析执行过程中每个步骤对应的源码的文件、函数名以及具体的源文件行数。
我们知道 MySQL 支持一些变长的数据类型,比如 VARCHAR(M)、 VARBINARY(M)、各种 TEXT 类型,各种 BLOB 类型,我们也可以把拥有这些数据 类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们 在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。如果该可变 字段允许存储的最大字节数(M×W)超过 255 字节并且真实存储的字节数(L) 超过 127 字节,则使用 2 个字节,否则使用 1 个字节。
表中的某些列可能存储 NULL 值,如果把这些 NULL 值都放到记录的真实数 据中存储会很占地方,所以 Compact 行格式把这些值为 NULL 的列统一管理起来,存储到 NULL 值列表。每个允许存储 NULL 的列对应一个二进制位,二进制位的 值为 1 时,代表该列的值为 NULL。二进制位的值为 0 时,代表该列的值不为 NULL。
还有一个用于描述记录的记录头信息,它是由固定的 5 个字节组成。5 个字 节也就是 40 个二进制位,不同的位代表不同的意思。
记录的真实数据除了我们自己定义的列的数据以外,MySQL 会为每个记录默 认的添加一些列(也称为隐藏列),包括:
InnoDB 表对主键的生成策略是:优先使用用户自定义主键作为主键,如果 用户没有定义主键,则选取一个 Unique 键作为主键,如果表中连 Unique 键都没 有定义的话,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键。 DB_TRX_ID(也可以称为 trx_id) 和 DB_ROLL_PTR(也可以称为 roll_ptr) 这两 个列是必有的,但是 row_id 是可选的(在没有自定义主键以及 Unique 键的情 况下才会添加该列)。
其他的行格式和 Compact 行格式差别不大。
MySQL5.7 的默认行格式就是 Dynamic,Dynamic 和 Compressed 行格式和 Compact 行格式挺像,只不过在处理行溢出数据时有所不同。Compressed 行格式和 Dynamic 不同的一点是,Compressed 行格式会采用压缩算法对页面进行压 缩,以节省空间。
什么叫数据溢出?