在項目中碰到一個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的執行計劃,結果很意外
子查詢的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代替它」,應該理解爲「盡力避免嵌套子查詢,使用索引來優化它們」