MySQL子查詢(IN)碰到的問題,深刻分析

在項目中碰到一個SQL的慢查詢,查閱以後發現是由於SQL中使用了IN子查詢,也許大部分有開發經驗的人都會語重心長的告訴你「千萬別用IN,使用JOIN或者EXISTS代替它」。好吧,我認可我不喜歡這句話,由於任何事物都有它存在的理由,因此今天來探討一下IN關於子查詢的問題html

問題

首先定義一下表結構,假如如今有3張表employee,user,user_dept它們分別是僱員表,用戶表,用戶部門關係表java

CREATE TABLE `employee` (
  `id` int(20) NOT NULL AUTO_INCREMENT
  `user_id` varchar(50) DEFAULT NULL,
  `dept_id` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ID` (`id`) USING HASH
) ENGINE=InnoDB

CREATE TABLE `user_dept` (
  `id` int(22) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(50) DEFAULT NULL,
  `dept_id` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ID` (`id`) USING HASH
) ENGINE=InnoDB

CREATE TABLE `user` (
  `id` varchar(50) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ID` (`id`)
) ENGINE=InnoDB

如今須要查詢:某一個特定用戶所在部門下的全部人(聽起來有點拗口)mysql

首先咱們嘗試使用含有IN的子查詢SQL語句算法

select a.* from employee a, user b where a.user_id = b.id and a.dept_id IN (select dept_id from user_dept where user_id = 'specific user_id')

運行SQL後發現時間是6.783s(真實的employee表有5000多條記錄,user表有3000多條記錄, user_dept表有1000多條記錄)sql

數據量並很少的狀況下居然如此耗時,看來IN中嵌套子查詢確實不是什麼好主意,觀察一下該SQL的日誌發現Handler_read_rnd_next = 6677791,這意味着MYSQL在處理過程當中掃描(遍歷)表多達百萬級別,怪不得運行的時候會如此的長express

爲了找出緣由我嘗試將子查詢分開分別執行2次SQLapp

select a.* from employee a, user b where a.user_id = b.id
select dept_id from user_dept where user_id = 'specific user_id'

運行結果都是在毫秒級別,而且第一個sql的Handler_read_rnd_next = 5179,第二個sql的Handler_read_rnd_next = 1280 ide

細心的人會發現1280 乘以 5179 約等於 6677791, 這是否是意味着加入IN中含有子查詢,外圍的查詢每遍歷一次都須要在重複執行子查詢中的語句,也就是說IN中含有子查詢的算法複雜度爲
$$O(M * N) 其中M爲外圍查詢的時間,N爲子查詢的時間$$oop

接下來我嘗試使用JOIN語句來執行SQL優化

select a.* from employee a, user b, user_dept c where a.user_id = b.id and a.dept_id = c.dept_id and a.user_id = 'specific user_id'

執行時間在毫秒級別,而且Handler_read_rnd_next = 6459,而後我發現當表作JOIN查詢時候,遍歷表的總數 約等於 表中記錄總數的總和,算法複雜度爲$$O(M + N) 其中M,N爲關聯表的總記錄數$$

可是使人疑惑的地方出現了爲什麼在IN中使用獨立子查詢(和外圍查詢沒有任何關聯)的算法複雜度變成$$O(M * N)$$難道MYSQL不該該是先將獨立子查詢只運行一次,而後在由外圍查詢當條件使用這樣的效率最高嗎?僞代碼以下:

// 根據上面使用IN子查詢的SQL來編寫僞代碼,使用最簡單的Nested-Loop Join來實現

// 1.執行一次子查詢,獲取到子查詢的結果集合
Set subSet = subquery();
// 2.遍歷外圍查詢 
for(employee e : employeeList) {
    for(user u : userList) {
        // 根據條件過濾數據,這步相對於使用IN來判斷
        if(subSet.contains(e.dept_id) && e.user_id == u.user_id) {
            // 輸出數據
            sys.out();
        }
    }
}

理想中的算法複雜度應該是$$O(S1 + N M C)$$

其中S1爲子查詢的算法複雜度, N爲employee表的數量,M爲user表的數量,C爲子查詢結果集的大小,通常爲常數

複雜度爲$$O(N^2)$$
若是user表中的user_id有作索引的話,其算法複雜度爲:$$O(N)$$
這已是至關快的速度了,爲了驗證個人想法,我使用EXPLAIN命令查看MYSQL的執行計劃,結果很意外
mysql-EXPLAIN
子查詢的select_type居然是DEPENDENT SUBQUERY(相關子查詢),可是很顯然咱們的SQL子查詢中並無和外圍查詢有關聯的條件,難道MYSQL作了什麼特殊的優化?爲了考清楚這個問題,我嘗試在MYSQL官方手冊尋找答案,結果查找到一篇文章Optimizing Subqueries with EXISTS Strategy

其中有這麼一段:

outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)

MySQL evaluates queries 「from outside to inside.」 That is, it first obtains the value of the outer expression outer_expr, and then runs the subquery and captures the rows that it produces.

A very useful optimization is to 「inform」 the subquery that the only rows of interest are those where the inner expression inner_expr is equal to outer_expr. This is done by pushing down an appropriate equality into the subquery's WHERE clause. That is, the comparison is converted to this:

EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)

大概意思就是說當MYSQL碰到IN子查詢時MYSQL的優化器會將IN子查詢轉化爲EXISTS相關子查詢

因此我猜測MYSQL應該是在執行的時候把咱們上面的SQL改爲了

select a.* from employee a, user b where a.user_id = b.id and EXISTS(select 1 from user_dept c where c.user_id = 'specific user_id' and a.dept_id = c.dept_id)

使用EXPLAIN命令查看執行計劃發現和使用IN的使用徹底同樣
Oop~爲何MYSQL要作如此「多此一舉」的事呢?

for(employee e : employeeList) {
    for(user u : userList) {
        for(user_dept ud : user_deptList) {
            if(ud.dept_id == a.dept_id && e.user_id == u.user_id) {
                // 輸出數據
                sys.out();
            }
        }
    }
}

算法複雜度爲$$O(N^3)$$

結束語

關於MYSQL使用IN子查詢的問題就暫時告已段落了,因爲本人水平有限,不能再更深刻的研究下去,關於爲何MYSQL會將IN中的子查詢轉化爲EXISTS中的相關子查詢,若是有哪位高手知曉緣由請告知

對於那句「千萬別用IN,使用JOIN或者EXISTS代替它」,應該理解爲「盡力避免嵌套子查詢,使用索引來優化它們」

相關文章
相關標籤/搜索