子查詢,通俗解釋就是查詢語句中嵌套著另一個查詢語句。相信日常工作中接觸到MySQL 的同學都了解或使用過子查詢,但是具體它是怎樣實現的呢? 查詢效率如何? 這些恐怕好多人就不太清楚了,下面咱們就圍繞這兩個問題共同探索一下。
這個任務需要使用三個表格,這三個表格都擁有一個主鍵索引 id 和一個索引 a,但字 b 上沒有索引。預存程序 idata() 往表 t1 插入的是 100 行數據,表 t2、t3 裡插入了 1000 行數據。建表語句如下:
CREATE TABLE `t1` ( `id` INT ( 11 ) NOT NULL, `t1_a` INT ( 11 ) DEFAULT NULL, `t1_b` INT ( 11 ) DEFAULT NULL, PRIMARY KEY ( `id` ), KEY `idx_a` ( `t1_a` )) ENGINE = INNODB; CREATE TABLE `t2` ( `id` INT ( 11 ) NOT NULL, `t2_a` INT ( 11 ) DEFAULT NULL, `t2_b` INT ( 11 ) DEFAULT NULL, PRIMARY KEY ( `id` ), KEY `idx_a` ( `t2_a` )) ENGINE = INNODB; CREATE TABLE `t3` ( `id` INT ( 11 ) NOT NULL, `t3_a` INT ( 11 ) DEFAULT NULL, `t3_b` INT ( 11 ) DEFAULT NULL, PRIMARY KEY ( `id` ), KEY `idx_a` ( `t3_a` )) ENGINE = INNODB; -- 向t1添加100条数据 -- drop procedure idata; delimiter ;; create procedure idata() begin declare i int; set i=1; while(i<=100)do insert into t1 values(i, i, i); set i=i+1; end while; end;; delimiter ; call idata(); -- 向t2添加1000条数据 drop procedure idata; delimiter ;; create procedure idata() begin declare i int; set i=101; while(i<=1100)do insert into t2 values(i, i, i); set i=i+1; end while; end;; delimiter ; call idata(); -- 向t2添加1000条数据,且t3_a列的值为倒叙 drop procedure idata; delimiter ;; create procedure idata() begin declare i int; set i=101; while(i<=1100)do insert into t3 values(i, 1101-i, i); set i=i+1; end while; end;; delimiter ; call idata();
子查詢的語法規定,子查詢可以在一個外層查詢的各種位置出現,這裡我們只介紹常用的幾個:
如SELECT m, n FROM (SELECT m2 1 AS m, n2 AS n FROM t2 WHERE m2 > 2) AS t;
這個例子中的子查詢是:(SELECT m2 1 AS m, n2 AS n FROM t2 WHERE m2 > 2
),這個放在FROM子句中的子查詢相當於一個表,但又和我們平常使用的表有點兒不一樣,這種由子查詢結果集組成的表稱之為派生表。
如:SELECT * FROM t1 WHERE m1 = (SELECT MIN(m2) FROM t2);
# SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2);
其他的還有SELECT 子句中,ORDER BY 子句中,GROUP BY 子句中,雖然文法支持,但沒啥意義,就不嘮叨這些情況了。
標量子查詢,只傳回單一值的子查詢稱為標量子查詢,例如:
SELECT * FROM t1 WHERE m1 = (SELECT m1 FROM t1 LIMIT 1);
行子查詢,就是只回傳一筆記錄的子查詢,不過這條記錄需要包含多個欄位(只包含一個欄位就成了標量子查詢了)。例如:SELECT * FROM t1 WHERE (m1, n1) = (SELECT m2, n2 FROM t2 LIMIT 1);
列子查詢,就是只傳回一個欄位的數據,不過這個列的資料需要包含多筆記錄(只包含一筆記錄就成了標量子查詢了)。例如:SELECT * FROM t1 WHERE m1 IN (SELECT m2 FROM t2);
表子查詢,就是子查詢的結果既包含很多筆記錄,又包含很多列,例如:
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);
#如下邊這個查詢語句:
mysql root@localhost:test> explain select * from t1 where t1_a = (select t2_a from t2 limit 1); +----+-------------+-------+-------+---------------+-------+---------+--------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+-------+---------+--------+------+-------------+ | 1 | PRIMARY | t1 | ref | idx_a | idx_a | 5 | const | 1 | Using where | | 2 | SUBQUERY | t2 | index | <null> | idx_a | 5 | <null> | 1000 | Using index | +----+-------------+-------+-------+---------------+-------+---------+--------+------+-------------+
它的執行方式:
先單獨執行( select t2_a from t2 limit 1) 這個子查詢。
然後在將上一步子查詢得到的結果當作外層查詢的參數再執行外層查詢 select * from t1 where t1_a = ...。
也就是說,對於包含不相關的標量子查詢或行子查詢的查詢語句來說,MySQL 會分別獨立的執行外層查詢和子查詢,就當作兩個單表查詢就好了。
例如下邊這個查詢:
mysql root@localhost:test> explain select * from t1 where t1_a = (select t2_a from t2 where t1.t1_b=t2.t2_b limit 1); +----+--------------------+-------+------+---------------+--------+---------+--------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+-------+------+---------------+--------+---------+--------+------+-------------+ | 1 | PRIMARY | t1 | ALL | <null> | <null> | <null> | <null> | 100 | Using where | | 2 | DEPENDENT SUBQUERY | t2 | ALL | <null> | <null> | <null> | <null> | 1000 | Using where | +----+--------------------+-------+------+---------------+--------+---------+--------+------+-------------+
它的執行方式就是這樣的:
先從外層查詢中取得一筆記錄,本例中也就是先從t1 表中取得一筆記錄。
接著從上一步驟取得的那筆記錄中找出子查詢中所涉及的值,就是 t1 表中找出 t1.t1_b 列的值,然後再執行子查詢。
最後根據子查詢的查詢結果來偵測外層查詢 WHERE 子句的條件是否成立,如果成立,就把外層查詢的那筆記錄加入到結果集,否則就丟棄。
然後重複上述步驟,直到 t1 中的記錄全部符合完畢。
如果子查詢的結果集中的記錄條數很少,那麼把子查詢和外層查詢分別看成兩個單獨的單表查詢效率還是蠻高的,但是如果單獨執行子查詢後的結果集太多的話,就會導致這些問題:
結果集太多,可能內存中都放不下~
對於外層查詢來說,如果子查詢的結果集太多,那就表示IN 子句中的參數特別多,這就導致:
1)无法有效的使用索引,只能对外层查询进行全表扫描。
2)在对外层查询执行全表扫描时,由于 IN 子句中的参数太多,这会导致检测一条记录是否符合和 IN 子句中的参数匹配花费的时间太长。
因此,可以将非相关子查询的结果集写入临时表中,而不直接将其作为外层查询的参数。写入临时表的过程是这样的:
该临时表的列就是子查询结果集中的列。
写入临时表的记录会被去重,让临时表变得更小,更省地方。
一般情况下子查询结果集不大时,就会为它建立基于内存的使用 Memory 存储引擎的临时表,而且会为该表建立哈希索引。
如果子查询的结果集非常大,超过了系统变量 tmp_table_size或者 max_heap_table_size,临时表会转而使用基于磁盘的存储引擎来保存结果集中的记录,索引类型也对应转变为 B+ 树索引。
将子查询结果集中的记录保存到临时表中的过程被称为物化(Materialize)。我们可以称存储子查询结果集的临时表为物化表,以便更加便利。正因为物化表中的记录都建立了索引(基于内存的物化表有哈希索引,基于磁盘的有 B+ 树索引),通过索引执行IN语句判断某个操作数在不在子查询结果集中变得非常快,从而提升了子查询语句的性能。
mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2); +----+--------------+-------------+--------+---------------+------------+---------+--------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------+-------------+--------+---------------+------------+---------+--------------+------+-------------+ | 1 | SIMPLE | t3 | ALL | idx_a | <null> | <null> | <null> | 1000 | Using where | | 1 | SIMPLE | <subquery2> | eq_ref | <auto_key> | <auto_key> | 5 | test.t3.t3_a | 1 | <null> | | 2 | MATERIALIZED | t2 | index | idx_a | idx_a | 5 | <null> | 1000 | Using index | +----+--------------+-------------+--------+---------------+------------+---------+--------------+------+-------------+
其实上边的查询就相当于表 t3 和子查询物化表进行内连接:
mysql root@localhost:test> explain select * from t3 left join t2 on t3.t3_a=t2.t2_a; +----+-------------+-------+------+---------------+--------+---------+--------------+------+--------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+--------+---------+--------------+------+--------+ | 1 | SIMPLE | t3 | ALL | <null> | <null> | <null> | <null> | 1000 | <null> | | 1 | SIMPLE | t2 | ref | idx_a | idx_a | 5 | test.t3.t3_a | 1 | <null> | +----+-------------+-------+------+---------------+--------+---------+--------------+------+--------+
此时 MySQL 查询优化器会通过运算来选择成本更低的方案来执行查询。
虽然,上面通过物化表的方式,将IN子查询转换成了联接查询,但还是会有建立临时表的成本,能不能不进行物化操作直接把子查询转换为连接呢?直接转换肯定不行。
-- 这里我们先构造了3条记录,其实也是构造不唯一的普通索引
+------+------+------+ | id | t2_a | t2_b | +------+------+------+ | 1100 | 1000 | 1000 | | 1101 | 1000 | 1000 | | 1102 | 1000 | 1000 | +------+------+------+ -- 加限制条件where t2.id>=1100是为了减少要显示的数据 mysql root@localhost:test> select * from t3 where t3_a in (select t2_a from t2 where t2.id>=1100); +-----+------+------+ | id | t3_a | t3_b | +-----+------+------+ | 101 | 1000 | 101 | +-----+------+------+ 1 row in set Time: 0.016s mysql root@localhost:test> select * from t3 left join t2 on t3.t3_a=t2.t2_a where t2.id>=1100; +-----+------+------+------+------+------+ | id | t3_a | t3_b | id | t2_a | t2_b | +-----+------+------+------+------+------+ | 101 | 1000 | 101 | 1100 | 1000 | 1000 | | 101 | 1000 | 101 | 1101 | 1000 | 1000 | | 101 | 1000 | 101 | 1102 | 1000 | 1000 | +-----+------+------+------+------+------+ 3 rows in set Time: 0.018s
所以说 IN 子查询和表联接之间并不完全等价。而我们需要的是另一种叫做半联接 (semi-join) 的联接方式 :对于 t3 表的某条记录来说,我们只关心在 t2 表中是否存在与之匹配的记录,而不关心具体有多少条记录与之匹配,最终的结果集中也只保留 t3 表的记录。
注意:semi-join 只是在 MySQL 内部采用的一种执行子查询的方式,MySQL 并没有提供面向用户的 semi-join 语法。
Table pullout (子查询中的表上拉)
当子查询的查询列表处只有主键或者唯一索引列时,可以直接把子查询中的表上拉到外层查询的 FROM 子句中,并把子查询中的搜索条件合并到外层查询的搜索条件中,比如这个:
mysql root@localhost:test> select * from t3 where t3_a in (select t2_a from t2 where t2.id=999) +-----+------+------+ | id | t3_a | t3_b | +-----+------+------+ | 102 | 999 | 102 | +-----+------+------+ 1 row in set Time: 0.024s mysql root@localhost:test> select * from t3 join t2 on t3.t3_a=t2.t2_a where t2.id=999; +-----+------+------+-----+------+------+ | id | t3_a | t3_b | id | t2_a | t2_b | +-----+------+------+-----+------+------+ | 102 | 999 | 102 | 999 | 999 | 999 | +-----+------+------+-----+------+------+ 1 row in set Time: 0.028s mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.id=999) +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+ | 1 | SIMPLE | t2 | const | PRIMARY,idx_a | PRIMARY | 4 | const | 1 | <null> | | 1 | SIMPLE | t3 | ref | idx_a | idx_a | 5 | const | 1 | <null> | +----+-------------+-------+-------+---------------+---------+---------+-------+------+--------+
FirstMatch execution strategy (首次匹配)
FirstMatch 是一种最原始的半连接执行方式,跟相关子查询的执行方式是一样的,就是说先取一条外层查询的中的记录,然后到子查询的表中寻找符合匹配条件的记录,如果能找到一条,则将该外层查询的记录放入最终的结果集并且停止查找更多匹配的记录,如果找不到则把该外层查询的记录丢弃掉。然后再开始取下一条外层查询中的记录,重复上边这个过程。
mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.t2_a=1000) +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+ | 1 | SIMPLE | t3 | ref | idx_a | idx_a | 5 | const | 1 | <null> | | 1 | SIMPLE | t2 | ref | idx_a | idx_a | 5 | const | 4 | Using index; FirstMatch(t3) | +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------+
DuplicateWeedout execution strategy (重复值消除)
转换为半连接查询后,t3 表中的某条记录可能在 t2 表中有多条匹配的记录,所以该条记录可能多次被添加到最后的结果集中,为了消除重复,我们可以建立一个临时表,并设置主键id,每当某条 t3 表中的记录要加入结果集时,就首先把这条记录的id值加入到这个临时表里,如果添加成功,说明之前这条 t2 表中的记录并没有加入最终的结果集,是一条需要的结果;如果添加失败,说明之前这条 s1 表中的记录已经加入过最终的结果集,直接把它丢弃。
LooseScan execution strategy (松散扫描)
这种虽然是扫描索引,但只取值相同的记录的第一条去做匹配操作的方式称之为松散扫描。
当然,并不是所有包含IN子查询的查询语句都可以转换为 semi-join,只有形如这样的查询才可以被转换为 semi-join:
SELECT ... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables ...) AND ... -- 或者这样的形式也可以: SELECT ... FROM outer_tables WHERE (oe1, oe2, ...) IN (SELECT ie1, ie2, ... FROM inner_tables ...) AND ...
用文字总结一下,只有符合下边这些条件的子查询才可以被转换为 semi-join:
该子查询必须是和IN语句组成的布尔表达式,并且在外层查询的 WHERE 或者 ON 子句中出现
外层查询也可以有其他的搜索条件,只不过和 IN 子查询的搜索条件必须使用AND 连接起来
该子查询必须是一个单一的查询,不能是由若干查询由 UNION 连接起来的形式
该子查询不能包含 GROUP BY 或者 HAVING 语句或者聚集函数
可以将 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)
当然这个过程中有一些特殊情况,比如在 outer_expr 或者 inner_expr 值为 NULL 的情况下就比较特殊。如果表达式中有任何一个操作数的值为 NULL,结果通常是 NULL。例如:
mysql root@localhost:test> SELECT NULL IN (1, 2, 3); +-------------------+ | NULL IN (1, 2, 3) | +-------------------+ | <null> | +-------------------+ 1 row in set
EXISTS 子查询的结果肯定是布尔型,值为 TRUE 或 FALSE。但是现实中我们大部分使用 IN 子查询的场景是把它放在 WHERE 或者 ON 子句中,而 WHERE 或者 ON 子句是不区分 NULL 和 FALSE 的,比方说:
mysql root@localhost:test> SELECT 1 FROM s1 WHERE NULL; +---+ | 1 | +---+ 0 rows in set Time: 0.016s mysql root@localhost:test> SELECT 1 FROM s1 WHERE FALSE; +---+ | 1 | +---+ 0 rows in set Time: 0.033s
所以只要我们的IN子查询是放在 WHERE 或者 ON 子句中的,那么 IN -> EXISTS 的转换就是没问题的。为什么需要进行转换呢?因为如果不进行转换,可能无法使用索引,例如下面的查询语句:
mysql root@localhost:test> explain select * from t3 where t3_a in (select t2_a from t2 where t2.t2_a>=999) or t3_b > 1000; +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+ | 1 | PRIMARY | t3 | ALL | <null> | <null> | <null> | <null> | 1000 | Using where | | 2 | SUBQUERY | t2 | range | idx_a | idx_a | 5 | <null> | 107 | Using where; Using index | +----+-------------+-------+-------+---------------+--------+---------+--------+------+--------------------------+
但是将它转为 EXISTS 子查询后却可以使用到索引:
mysql root@localhost:test> explain select * from t3 where exists (select 1 from t2 where t2.t2_a>=999 and t2.t2_a=t3.t3_a) or t3_b > 1000; +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+ | 1 | PRIMARY | t3 | ALL | <null> | <null> | <null> | <null> | 1000 | Using where | | 2 | DEPENDENT SUBQUERY | t2 | ref | idx_a | idx_a | 5 | test.t3.t3_a | 1 | Using where; Using index | +----+--------------------+-------+------+---------------+--------+---------+--------------+------+--------------------------+
需要注意的是,如果 IN 子查询不满足转换为 semi-join 的条件,又不能转换为物化表如果将其转换为物化表成本过高,那么就需要使用 EXISTS 查询。如果将其转换为物化表成本过高,那么就需要使用 EXISTS 查询。
以上是MySQL子查詢原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!