有时候多个列之间存在等值匹配的关系,比如这样:
a = b and b = c and c = 5
这个表达式可以被简化为:
a = 5 and b = 5 and c = 5
移除没用的条件( trivial_condition_removal)
对于一些明显永远为
TRUE
或者
FALSE
的表达式,优化器会移除掉它们,比如这个表达式:
(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
优化器是不会尝试对这些表达式进行化简的
。我们前边说过只有搜索条件中索引列和常数使用某些运算符连接起来才可能使用到索引,所以如果可以的话,
最好让索引列以单独的形式出现在表达式中
。
HAVING子句和 子 WHERE子句的合并
如果查询语句中没有出现诸如
SUM
、
MAX
等等的聚集函数以及
GROUP BY
子句,优化器就把
HAVING
子句和
WHERE
子句合并起来。
常量表检测
设计
MySQL
的大叔觉得下边这两种查询运行的特别快:
小贴士: 大家有没有觉得这一条有点儿不对劲,我还没开始查表呢咋就知道这表里边有几条记录呢?哈哈,这个其实依靠的是统计数据。不过我们说过InnoDB的统计数据数据不准确, 所以这一条不能用于使用InnoDB作为存储引擎的表,只能适用于使用Memory或者MyISAM存储引擎的表。
- 使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表。
设计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;
外连接消除
我们前边说过,
内连接的驱动表和被驱动表的位置可以相互转换,而左(外)连接和右(外)连接的驱动表和被驱动表是固定的
。这就导致
内连接
可能通过优化表的连接顺序来降低整体的查询成本,而
外连接
却无法优化表的连接顺序。为了故事的顺利发展,我们还是把之前介绍连接原理时用过的t1和t2表请出来,为了防止大家早就忘掉了,我们再看一下这两个表的结构:
CREATE TABLE t1 (
m1 int,
n1 char(1)
) Engine=InnoDB, CHARSET=utf8;
CREATE TABLE t2 (
m2 int,
n2 char(1)
) Engine=InnoDB, CHARSET=utf8;
为了唤醒大家的记忆,我们再把这两个表中的数据给展示一下:
我们之前说过,
外连接和内连接的本质区别就是:对于外连接的驱动表的记录来说,如果无法在被驱动表中找到匹配
ON子句中的过滤条件的记录,那么该记录仍然会被加入到结果集中,对应的被驱动表记录的各个字段使用
NULL
值填充;而内连接的驱动表的记录如果无法在被驱动表中找到匹配
ON
子句中的过滤条件的记录,那么该记录会被舍弃
。查询效果就是这样:
对于上边例子中的(左)外连接来说,由于驱动表t1中m1=1, n1='a'的记录无法在被驱动表t2中找到符合ON子句条件t1.m1 = t2.m2的记录,所以就直接把这条记录加入到结果集,对应的t2表的m2和n2列 的值都设置为NULL。
小贴士: 右(外)连接和左(外)连接其实只在驱动表的选取方式上是不同的,其余方面都是一样的,所以优化器会首先把右(外)连接查询转换成左(外)连接查询。我们后边就不再唠叨
右(外)连接了。
我们知道
WHERE
子句的杀伤力比较大,
凡是不符合
WHERE
子句中条件的记录都不会参与连接
。只要我们在搜索条件中指定关于被驱动表相关列的值不为
NULL
,那么外连接中在被驱动表中找不到符合
ON子句条件的驱动表记录也就被排除出最后的结果集了,也就是说:
在这种情况下:外连接和内连接也就没有什么区别了
!比方说这个查询:
由于指定了被驱动表
t2
的
n2
列不允许为
NULL
,所以上边的
t1
和
t2
表的左(外)连接查询和内连接查询是一样一样的。当然,我们也可以不用显式的指定被驱动表的某个列
IS NOT NULL,只要隐含的有这个意思就行了,比方说这样:
在这个例子中,我们在WHERE子句中指定了被驱动表t2的m2列等于2,也就相当于间接的指定了m2列不为NULL值,所以上边的这个左(外)连接查询其实和下边这个内连接查询是等价的:
我们把这种在外连接查询中,指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称之为空值拒绝(英文名:reject-NULL)。在被驱动表的WHERE子句符合空值拒绝的条件后,外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本,选出成本最低的那种连接顺序来执行查询。
子查询优化
我们的主题本来是唠叨
MySQL查询优化器是如何处理子查询的,但是我还是同学连子查询的语法都没掌握全,所以先唠叨什么是个子查询,然后再唠叨关于子查询优化的事儿。
子查询语法
想必大家都是妈妈生下来的吧,连孙猴子都有妈妈
——
石头人
。怀孕妈妈肚子里的那个东东就是她的孩子,类似的,在一个查询语句里的某个位置也可以有另一个查询语句,这个出现在某个查询语句的 某个位置中的查询就被称为
子查询
(我们也可以称它为宝宝查询哈哈),那个充当
“
妈妈
”
角色的查询也被称之为
外层查询。不像人们怀孕时宝宝们都只在肚子里,子查询可以在一个外层查询的各种位置出现,比如:
也就是我们平时说的查询列表中,比如这样
其中的(SELECT m1 FROM t1 LIMIT 1)就是我们唠叨的所谓的子查询。
比如
这个例子中的子查询是:
(SELECT m2 + 1 AS m, n2 AS n FROM t2 WHERE m2 > 2)
,很特别的地方是它出现在了
FROM
子句中。
FROM子句里边儿不是存放我们要查询的表的名称么,这里放进来一个子 查询是个什么鬼?其实这里我们可以把子查询的查询结果当作是一个表,子查询后边的
AS t
表明这个子查询的结果就相当于一个名称为
t
的表,这个名叫
t的表的列就是子查询结果中的列,比如例子中表
t
就有两个列:
m
列和
n
列。这个放在
FROM
子句中的子查询本质上相当于一个
表
,但又和我们平常使用的表有点儿不一样,设计
MySQL
的大叔把这种由子查询结果集组成的表称之为
派生表
。
把子查询放在外层查询的
WHERE
子句或者
ON
子句中可能是我们最常用的一种使用子查询的方式了,比如这样:
这个查询表明我们想要将
(SELECT m2 FROM t2)
这个子查询的结果作为外层查询的
IN
语句参数,整个查询语句的意思就是我们想找
t1
表中的某些记录,这些记录的
m1
列的值能在
t2
表的
m2列找到匹配的值。
虽然语法支持,但没啥子意义,不唠叨这种情况了。
同上~
按返回的结果集区分子查询
因为子查询本身也算是一个查询,所以可以按照它们返回的不同结果集类型而把这些子查询分为不同的类型:
那些只返回一个单一值的子查询称之为
标量子查询
,比如这样:
SELECT (SELECT m1 FROM t1 LIMIT 1);
或者这样:
SELECT * FROM t1 WHERE m1 = (SELECT MIN(m2) FROM t2);
这两个查询语句中的子查询都返回一个单一的值,也就是一个
标量
。这些标量子查询可以作为一个单一值或者表达式的一部分出现在查询语句的各个地方。
顾名思义,就是返回一条记录的子查询,不过这条记录需要包含多个列(只包含一个列就成了标量子查询了)。比如这样:
SELECT * FROM t1 WHERE (m1, n1) = (SELECT m2, n2 FROM t2 LIMIT 1);
其中的(SELECT m2, n2 FROM t2 LIMIT 1)就是一个行子查询,整条语句的含义就是要从t1表中找一些记录,这些记录的m1和n2列分别等于子查询结果中的m2和n2列。
列子查询自然就是查询出一个列的数据喽,不过这个列的数据需要包含多条记录(只包含一条记录就成了标量子查询了)。比如这样:
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2);
其中的(SELECT m2 FROM t2)就是一个列子查询,表明查询出t2表的m2列的值作为外层查询IN语句的参数。
顾名思义,就是子查询的结果既包含很多条记录,又包含很多个列,比如这样:
SELECT * FROM t1 WHERE (m1, n1) IN (SELECT m2, n2 FROM t2);
其中的
(SELECT m2, n2 FROM t2)
就是一个表子查询,这里需要和行子查询对比一下,行子查询中我们用了
LIMIT 1
来保证子查询的结果只有一条记录,表子查询中不需要这个限制。
按与外层查询关系来区分子查询
如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为
不相关子查询
。我们前边介绍的那些子查询全部都可以看作不相关子查询,所以也就不举例子了哈。
如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为
相关子查询
。比如:
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 WHERE n1 = n2);
例子中的子查询是(SELECT m2 FROM t2 WHERE n1 = n2),可是这个查询中有一个搜索条件是n1 = n2,别忘了n1是表t1的列,也就是外层查询的列,也就是说子查询的执行需要依赖于外层查询的 值,所以这个子查询就是一个相关子查询。
子查询在布尔表达式中的使用
你说写下边这样的子查询有啥意义:
SELECT (SELECT m1 FROM t1 LIMIT 1);
貌似没啥意义~ 我们平时用子查询最多的地方就是把它作为布尔表达式的一部分来作为搜索条件用在WHERE子句或者ON子句里。所以我们这里来总结一下子查询在布尔表达式中的使用场景。
- 使用=、>、<、>=、<=、<>、!=、<=>作为布尔表达式的操作符
这些操作符具体是啥意思就不用我多介绍了吧,如果你不知道的话,那我真的很佩服你是靠着啥勇气一口气看到这里的~ 为了方便,我们就把这些操作符称为
comparison_operator
吧,所以子查询
组成的布尔表达式就长这样:
操作数
comparison_operator (
子查询
)
这里的
操作数
可以是某个列名,或者是一个常量,或者是一个更复杂的表达式,甚至可以是另一个子查询。但是需要注意的是,
这里的子查询只能是标量子查询或者行子查询,也就是子查询的结果
只能返回一个单一的值或者只能是一条记录
。比如这样(标量子查询):
SELECT * FROM t1 WHERE m1 < (SELECT MIN(m2) FROM t2);
或者这样(行子查询):
SELECT * FROM t1 WHERE (m1, n1) = (SELECT m2, n2 FROM t2 LIMIT 1);
对于列子查询和表子查询来说,它们的结果集中包含很多条记录,这些记录相当于是一个集合,所以就不能单纯的和另外一个操作数使用
comparison_operator
来组成布尔表达式了,
MySQL通过下面 的语法来支持某个操作数和一个集合组成一个布尔表达式:
具体的语法形式如下:
操作数
[NOT] IN (
子查询
)
这个布尔表达式的意思是用来判断某个操作数在不在由子查询结果集组成的集合中,比如下边的查询的意思是找出
t1
表中的某些记录,这些记录存在于子查询的结果集中:
SELECT * FROM t1 WHERE (m1, n2) IN (SELECT m2, n2 FROM t2);
具体的语法形式如下:
操作数
comparison_operator ANY/SOME(
子查询
)
这个布尔表达式的意思是只要子查询结果集中存在某个值和给定的操作数做
comparison_operator
比较结果为
TRUE
,那么整个表达式的结果就为
TRUE
,否则整个表达式的结果就为
FALSE
。比方
说下边这个查询:
SELECT * FROM t1 WHERE m1 > ANY(SELECT m2 FROM t2);
这个查询的意思就是对于t1表的某条记录的m1列的值来说,如果子查询(SELECT m2 FROM t2)的结果集中存在一个小于m1列的值,那么整个布尔表达式的值就是TRUE,否则为FALSE,也就是说 只要m1列的值大于子查询结果集中最小的值,整个表达式的结果就是TRUE,所以上边的查询本质上等价于这个查询:
SELECT * FROM t1 WHERE m1 > (SELECT MIN(m2) FROM t2);
另外,=ANY相当于判断子查询结果集中是否存在某个值和给定的操作数相等,它的含义和IN是相同的。
具体的语法形式如下:
操作数
comparison_operator ALL(
子查询
)
这个布尔表达式的意思是子查询结果集中所有的值和给定的操作数做
comparison_operator
比较结果为
TRUE
,那么整个表达式的结果就为
TRUE
,否则整个表达式的结果就为
FALSE
。比方说下边
这个查询:
SELECT * FROM t1 WHERE m1 > ALL(SELECT m2 FROM t2);
这个查询的意思就是对于
t1
表的某条记录的
m1
列的值来说,如果子查询
(SELECT m2 FROM t2)
的结果集中的所有值都小于
m1
列的值,那么整个布尔表达式的值就是
TRUE
,否则为
FALSE,也就是 说只要
m1
列的值大于子查询结果集中最大的值,整个表达式的结果就是
TRUE
,所以上边的查询本质上等价于这个查询:
SELECT * FROM t1 WHERE m1 > (SELECT MAX(m2) FROM t2);
小贴士: 觉得ANY和ALL有点晕的同学多看两遍哈~
有的时候我们仅仅需要判断子查询的结果集中是否有记录,而不在乎它的记录具体是个啥,可以使用把
EXISTS
或者
NOT EXISTS
放在子查询语句前边,就像这样:
[NOT] EXISTS (
子查询
)
我们举一个例子啊:
SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2);
对于子查询(SELECT 1 FROM t2)来说,我们并不关心这个子查询最后到底查询出的结果是什么,所以查询列表里填*、某个列名,或者其他啥东西都无所谓,我们真正关心的是子查询的结果集中是 否存在记录。也就是说只要(SELECT 1 FROM t2)这个查询中有记录,那么整个EXISTS表达式的结果就为TRUE。
子查询语法注意事项
不扩起来的子查询是非法的,比如这样:
mysql> SELECT SELECT m1 FROM t1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELECT m1 FROM t1' at line 1
如果子查询结果集中有多个列或者多个行,都不允许放在
SELECT
子句中,也就是查询列表中,比如这样就是非法的:
mysql> SELECT (SELECT m1, n1 FROM t1);
ERROR 1241 (21000): Operand should contain 1 column(s)
- 在想要得到标量子查询或者行子查询,但又不能保证子查询的结果集只有一条记录时,应该使用LIMIT 1语句来限制记录数量。
- 对于[NOT] IN/ANY/SOME/ALL子查询来说,子查询中不允许有LIMIT语句。
比如这样是非法的:
mysql> SELECT * FROM t1 WHERE m1 IN (SELECT * FROM t2 LIMIT 2);
ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
为啥不合法?人家就这么规定的,不解释~ 可能以后的版本会支持吧。正因为
[NOT] IN/ANY/SOME/ALL
子查询不支持
LIMIT
语句,所以子查询中的这些语句也就是多余的了:
ORDER BY
子句
子查询的结果其实就相当于一个集合,集合里的值排不排序一点儿都不重要,比如下边这个语句中的
ORDER BY
子句简直就是画蛇添足:
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 ORDER BY m2);
DISTINCT
语句
集合里的值去不去重也没啥意义,比如这样:
SELECT * FROM t1 WHERE m1 IN (SELECT DISTINCT m2 FROM t2);
没有聚集函数以及
HAVING
子句的
GROUP BY
子句。
在没有聚集函数以及
HAVING
子句时,
GROUP BY
子句就是个摆设,比如这样:
SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2 GROUP BY m2);
对于这些冗余的语句,
查询优化器在一开始就把它们给干掉了
。
不允许在一条语句中增删改某个表的记录时同时还对该表进行子查询。
比方说这样:
mysql> DELETE FROM t1 WHERE m1 < (SELECT MAX(m1) FROM t1);
ERROR 1093 (HY000): You can't specify target table 't1' for update in FROM clause
子查询在 MySQL中是怎么执行的