In子查詢的原理
1. in原理
此調研的背景是同樣的select結果爲什麼使用子查詢會比不使用子查詢慢。我們使用的數據庫還是mysql官方的employees。進行的實驗操作爲下面兩個語句:
方法一:explain select sql_no_cache t.emp_no,t.title,t.from_date,t.to_datefrom titles t straight_join employees e on (e.emp_no=t.emp_no) straight_joinsalaries s on (s.emp_no=e.emp_no) where e.gender='F' and s.salary=90930;
圖1直接使用join
方法二:explain select sql_no_cache * from titles t where t.emp_no in (select s.emp_no from salaries s, employees e where s.emp_no=e.emp_no ande.gender='F' and s.salary=90930);
圖2使用in的子查詢
在下面的討論中我們直接使用直接join和in的稱呼來代表兩種不同的情況。(注:在我們的實驗中第一種情況use 3.5s;第二種情況use 5.7s)
首先我們來解釋一下圖2的dependent subquery是什麼意思:手冊上的解釋是,子查詢中的第一個select,取決於外面的查詢。就這麼一句話,其實它表達的意思是(以我們圖2的表來說明)子查詢(e join t)的第一個表(e)的查詢方式依賴於外部(t表)的查詢。換句話說就是e表的檢索方式依賴於t表的數據,如這裏t表得到的記錄t.emp_no(where t.emp_no in)剛好可以被e表作爲eq_ref方式來獲得它的相應的記錄;換種寫法如果此時t表掃描第一條記錄得到的t.emp_no爲10001的話,那麼後面子查詢的語句就類似於這樣的語句:
select s.emp_no from salaries s, employeese where s.emp_no=e.emp_no and e.gender='F' and s.salary=90930 ands.emp_no=10001。此時這個語句就會被優化拿來優化,變成了上面的子查詢的執行計劃。
通過這個解釋我們可以知道:對於上面的兩種方式,它們使用的索引及讀取數據的過程及方法是一樣的,全表掃描t表,將t的每條記錄傳遞給e表,e表通過eq_ref索引方式來獲得記錄判斷自身的條件,然後再傳遞給s給,s表使用ref方式來獲得記錄,再判斷自身的條件是否也得到滿足,如果也滿足的話,則找到一個滿足此查詢的語句。那麼爲什麼這兩種情況會有性能上的差距了?
首先我們通過bt上看一下,兩的具體執行流程,看它們的區別在哪裏?
#0 evaluate_join_record (join=0x6fe0f10, join_tab=0x6fe2b28, error=0) at sql_select.cc:11414 |
表1 join方式的bt
通過該bt我們也可以清楚的看到三層nest-loop的過程;注:通過在每一層的sub_select處查看join_tab->table->alias變量我們可以此時具體操作的表。
#0 evaluate_join_record (join=0x6fe11d8, join_tab=0x6fe5148, error=0) at sql_select.cc:11414 |
表2 in方式的bt
通過這兩個表我們可以發現在表t與表[e,s]之前插入了一些其它的函數,並且這個插入的時機是在t表執行evaluate_join_record函數時調用select_cond_result= test(select_cond->val_int());判斷它所擁有的條件是否滿足時進入的。對於表1直接join的情況該過程是沒有被執行的因爲它沒有自身的where cond。對於表2 in的方式,這個條件可能是由於在前面執行optimize時,確定外部查詢的執行計劃時確定的,這裏我們不再去確認。正常的情況這個test如果有條件的話那麼應該執行相應的條件判斷如對於e表它最終調用的判斷函數爲int Arg_comparator::compare_int_signed();而在這裏對於有子查詢的它調用的方法是:Item_subselect::exec,而它最終又調用各自的engine->exec(),如這裏它調用的是這個subselect_single_select_engine::exec方法,如果是第一次調用該函數的話那麼就先執行一次join->optimize(),即對子查詢(內部查詢)進行優化,而在mysql_select時調用的join->optimize()只是對外部查詢進行優化,它並不包括內部查詢的優化(執行計劃等,另外對於直接join的話沒有所謂的內外部查詢那麼它的整個執行計劃就是mysql_select完成),然後執行join->reinit(),最後再執行JOIN::exec;也就是說對於in這種情況t進行全表掃描,那麼它總共有443310,那麼這幾個函數就要被調用443310多次(join->optimize()除外,它在第一次調用子查詢確認執行計劃之後就不再調用)。
我們可以通過下面的圖來反應這兩種過程:
圖3 join方式的執行過程
圖4 in方式的執行過程
上面就是in子查詢的實現過程。下面我們將討論爲什麼in方式與join方式性能差的原因?
2. In比join慢的原因
首先我們通過oprofile來測試一下,兩種情況各自的性能損耗在哪裏?
Join |
In |
samples % symbol name 444 11.2065 buf_calc_page_new_checksum 337 8.5058 rec_get_offsets_func 326 8.2282 pthread_mutex_unlock 245 6.1837 pthread_mutex_lock 240 6.0575 cmp_dtuple_rec_with_match 226 5.7042 row_search_for_mysql 218 5.5023 row_sel_store_mysql_rec 104 2.6249 code_state 103 2.5997 ha_insert_for_fold 97 2.4483 page_cur_search_with_match 83 2.0949 pthread_mutex_trylock 79 1.9939 pthread_getspecific 77 1.9435 memcpy 76 1.9182 _db_enter_ 67 1.6911 btr_search_guess_on_hash 63 1.5901 evaluate_join_record(JOIN*, st_join_table*, int) 62 1.5649 safe_mutex_lock 52 1.3125 DoTrace 51 1.2872 Arg_comparator::compare_int_signed() 51 1.2872 _db_return_ 49 1.2367 btr_search_build_page_hash_index 46 1.1610 btr_cur_search_to_nth_level 46 1.1610 ha_innobase::general_fetch(unsigned char*, unsigned int, unsigned int) 37 0.9339 safe_mutex_unlock 33 0.8329 ha_innobase::unlock_row() 32 0.8077 ha_remove_all_nodes_to_page 30 0.7572 btr_search_drop_page_hash_index 28 0.7067 _db_doprnt_ 27 0.6815 _db_pargs_ 26 0.6562 join_read_key(st_join_table*) 26 0.6562 lock_clust_rec_cons_read_sees 24 0.6058 cp_buffer_from_ref(THD*, st_table*, st_table_ref*) 24 0.6058 row_get_rec_sys_field 19 0.4796 Field_long::val_int() 19 0.4796 ha_delete_hash_node 19 0.4796 ha_innobase::index_read(unsigned char*, unsigned char const*, unsigned int, ha_rkey_function) 19 0.4796 mtr_memo_slot_release 19 0.4796 row_sel_convert_mysql_key_to_innobase 18 0.4543 Item_field::val_int() |
samples % symbol name 432 7.4922 pthread_mutex_unlock 423 7.3361 buf_calc_page_new_checksum 373 6.4690 rec_get_offsets_func 283 4.9081 row_search_for_mysql 256 4.4398 pthread_mutex_lock 242 4.1970 _db_enter_ 242 4.1970 cmp_dtuple_rec_with_match 206 3.5727 row_sel_store_mysql_rec 190 3.2952 code_state 181 3.1391 _db_return_ 151 2.6188 pthread_getspecific 146 2.5321 DoTrace 124 2.1505 build_template(row_prebuilt_struct*, THD*, st_table*, unsigned int) 108 1.8730 page_cur_search_with_match 99 1.7170 ha_insert_for_fold 87 1.5088 btr_search_guess_on_hash 83 1.4395 evaluate_join_record(JOIN*, st_join_table*, int) 81 1.4048 pthread_mutex_trylock 80 1.3874 safe_mutex_lock 76 1.3181 memcpy 57 0.9886 JOIN::exec() 56 0.9712 __errno_location 54 0.9365 _db_doprnt_ 50 0.8672 btr_search_build_page_hash_index 50 0.8672 ha_innobase::general_fetch(unsigned char*, unsigned int, unsigned int) 47 0.8151 dict_index_copy_types 46 0.7978 btr_cur_search_to_nth_level 41 0.7111 _my_thread_var 39 0.6764 Field_long::val_int() 38 0.6590 Arg_comparator::compare_int_signed() 36 0.6243 safe_mutex_unlock 34 0.5897 cp_buffer_from_ref(THD*, st_table*, st_table_ref*) 33 0.5723 ha_innobase::index_read(unsigned char*, unsigned char const*, unsigned int, ha_rkey_function) 32 0.5550 _db_pargs_ 31 0.5376 JOIN::cleanup(bool) 31 0.5376 ha_innobase::unlock_row() 29 0.5029 join_read_key(st_join_table*) 29 0.5029 sub_select(JOIN*, st_join_table*, bool) |
通過對比我們發現在join出現的在in中也有,但是在in中佔的採樣比例比較高的幾個在join中並沒有(這裏的沒有並不是指join裏沒有調用它們,只是可能它們調用的次數太少導致在oprofile採樣時沒有獲得它們的數據)。通過gdb我們發現兩種方式都調用build_template,所以爲了進一步查看它們的性能損耗,我們通過gcov比較兩者的覆蓋情況。通過它們的輸出我們知道兩者調用build_template的次數分別是join VS in:174 620786;而這個函數的內部有一個for循環用於拷貝,判斷哪些字段是需要被保留的。對於in的方式這個for內部執行了3370262次,而join只執行了1036次。我們再來看一下這個函數的bt:
#0 build_template (prebuilt=0x2aaaabbc40b8, thd=0x6f7f980, table=0x6fd9780, templ_type=1) at handler/ha_innodb.cc:3564 |
表 3build_template的bt
通過它我們可以看到該函數是由index_init調用的,而index_init又是在sub_select每次讀取第一條記錄的時候執行的,對於eq_ref則是調用join_read_key函數(注:e表,對於s表則調用join_read_always_key,它也有類似的過程)
static int join_read_key(JOIN_TAB *tab) { int error; TABLE *table= tab->table; if (!table->file->inited) { table->file->ha_index_init(tab->ref.key, tab->sorted); //在函數內部把table->file->inited賦值爲INDEX; } … |
可以看到table->file->inited這個變量決定着index_init的執行。那麼它在哪裏重新被賦值爲0?答案就是ha_index_end(),而這個函數的執行是在:(再次bt。。。哈哈)
#0 st_join_table::cleanup (this=0x6fe28d0) at handler.h:1186 |
注:這裏並沒有顯示這個函數名,但是handler.h:1186這條語句就是在ha_index_end()。我們看到這個函數是在do_select進行清除操作的時候調用的,而我們前面也已經說明了do_select是nest-loop的入口及出口地方,那麼就是說每執行一交nest-loop的話都要進行一次index_init操作。對於in的類型,圖4我們已經說明了它有count(t)的nest-loop過程,所以它就要執行443308次,這個剛纔就等於gcov輸出的join_read_key:table->file->ha_index_init(tab->ref.key, tab->sorted);而join_read_always_key:table->file->ha_index_init(tab->ref.key,tab->sorted);總共執行了177224次,這個數字剛好是select count(*) from titles t straight_join employees e on(e.emp_no=t.emp_no) where e.gender='F';即每條滿足t與e join條件的記錄。通過這兩組數據,也驗證了我們上面關於in的執行過程的描述。同樣的這兩個函數在join方式下都只執行了一次,因爲它們只執行了一次nest-loop。
3. 總結
我們這裏只是分析了佔採樣比例較高的build_template情況,對於其它的也是一樣的分析方法,這裏不再贅述。對於直接join方式與in子查詢方式,兩者select的時間複雜度是一樣的(注:這裏的select是指獲得數據的方式,個數)。唯一不同的是對於in子查詢它每次執行內部查詢的時候都必須重新構造一個JOIN結構,完成相應的初始化操作,並且在這次內部查詢結束之後,要完成相應的析構函數,如index_init,index_end,而當外部查詢是全表掃描的時候這些操作的次數就是它的記錄數,那麼它們(構造,析構)所佔用的性能也是顯而易見的。簡單一句話子查詢的性能除了查詢外,還消耗在JOIN的構造與析構過程,也就是上面表2的【t表】與【e表】中間的部分。