mysql子查詢(in)的實現

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  0x00000000005e41e8 in sub_select (join=0x6fe0f10, join_tab=0x6fe2b28, end_of_records=<value optimized out>) at sql_select.cc:11384   【s表】

#2  0x00000000005e3f5a in evaluate_join_record (join=0x6fe0f10, join_tab=0x6fe28d0, error=<value optimized out>) at sql_select.cc:11511
#3  0x00000000005e41e8 in sub_select (join=0x6fe0f10, join_tab=0x6fe28d0, end_of_records=<value optimized out>) at sql_select.cc:11384       【e表】

#4  0x00000000005e3f5a in evaluate_join_record (join=0x6fe0f10, join_tab=0x6fe2678, error=<value optimized out>) at sql_select.cc:11511
#5  0x00000000005e4215 in sub_select (join=0x6fe0f10, join_tab=0x6fe2678, end_of_records=<value optimized out>) at sql_select.cc:11391       【t表】

#6  0x0000000000601d30 in do_select (join=0x6fe0f10, fields=0x6f819a0, table=0x0, procedure=0x0) at sql_select.cc:11140
#7  0x000000000060a479 in JOIN::exec (this=0x6fe0f10) at sql_select.cc:2314
#8  0x000000000060ae0f in mysql_select (thd=0x6f7f980, rref_pointer_array=0x6f81a68, tables=0x6fd5198, wild_num=0, fields=@0x6f819a0, conds=0x6fdd218,    og_num=0, order=0x0, group=0x0, having=0x0, proc_param=0x0, select_options=2147764736, result=0x6fdd398, unit=0x6f81470, select_lex=0x6f81898)    at sql_select.cc:2509
#9  0x000000000060b481 in handle_select (thd=0x6f7f980, lex=0x6f813d0, result=0x6fdd398, setup_tables_done_option=0) at sql_select.cc:269
#10 0x000000000054c71a in execute_sqlcom_select (thd=0x6f7f980, all_tables=0x6fd5198) at sql_parse.cc:5075
#11 0x000000000055538c in mysql_execute_command (thd=0x6f7f980) at sql_parse.cc:2271
#12 0x000000000055ebd3 in mysql_parse (thd=0x6f7f980,

表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
#1  0x00000000005e41e8 in sub_select (join=0x6fe11d8, join_tab=0x6fe5148, end_of_records=<value optimized out>) at sql_select.cc:11384     【s表】

#2  0x00000000005e3f5a in evaluate_join_record (join=0x6fe11d8, join_tab=0x6fe4ef0, error=<value optimized out>) at sql_select.cc:11511
#3  0x00000000005e41e8 in sub_select (join=0x6fe11d8, join_tab=0x6fe4ef0, end_of_records=<value optimized out>) at sql_select.cc:11384      【e表】

#4  0x0000000000601d30 in do_select (join=0x6fe11d8, fields=0x6fd5300, table=0x0, procedure=0x0) at sql_select.cc:11140
#5  0x000000000060a479 in JOIN::exec (this=0x6fe11d8) at sql_select.cc:2314
#6  0x00000000004d5102 in subselect_single_select_engine::exec (this=0x6fdcda0) at item_subselect.cc:1987
#7  0x00000000004d26b1 in Item_subselect::exec (this=0x6fdccb0) at item_subselect.cc:280
#8  0x00000000004d1aaa in Item_in_subselect::val_bool (this=0x6fe11d8) at item_subselect.cc:880
#9  0x0000000000438821 in Item::val_bool_result (this=0x6fe11d8) at item.h:745
#10 0x0000000000476941 in Item_in_optimizer::val_int (this=0x6fe2a58) at item_cmpfunc.cc:1833
#11 0x00000000005e3d9a in evaluate_join_record (join=0x6fdce98, join_tab=0x6fe3538, error=<value optimized out>) at sql_select.cc:11434
#12 0x00000000005e4215 in sub_select (join=0x6fdce98, join_tab=0x6fe3538, end_of_records=<value optimized out>) at sql_select.cc:11391      【t表】

#13 0x0000000000601d30 in do_select (join=0x6fdce98, fields=0x6f819a0, table=0x0, procedure=0x0) at sql_select.cc:11140
#14 0x000000000060a479 in JOIN::exec (this=0x6fdce98) at sql_select.cc:2314
#15 0x000000000060ae0f in mysql_select (thd=0x6f7f980, rref_pointer_array=0x6f81a68, tables=0x6fd4d90, wild_num=1, fields=@0x6f819a0, conds=0x6fdccb0,    og_num=0, order=0x0, group=0x0, having=0x0, proc_param=0x0, select_options=2147764736, result=0x6fdce78, unit=0x6f81470, select_lex=0x6f81898)    at sql_select.cc:2509
#16 0x000000000060b481 in handle_select (thd=0x6f7f980, lex=0x6f813d0, result=0x6fdce78, setup_tables_done_option=0) at sql_select.cc:269
#17 0x000000000054c71a in execute_sqlcom_select (thd=0x6f7f980, all_tables=0x6fd4d90) at sql_parse.cc:5075
#18 0x000000000055538c in mysql_execute_command (thd=0x6f7f980) at sql_parse.cc:2271
#19 0x000000000055ebd3 in mysql_parse (thd=0x6f7f980,

表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
#1  0x000000000083953b in ha_innobase::change_active_index (this=0x6fdad30, keynr=0) at handler/ha_innodb.cc:4834
#2  0x0000000000839ef7 in ha_innobase::index_init (this=0x6fdad30, keynr=0, sorted=<value optimized out>) at handler/ha_innodb.cc:4508
#3  0x00000000006005f8 in join_read_key (tab=0x6fe4ef0) at handler.h:1180
#4  0x00000000005e41d3 in sub_select (join=0x6fe11d8, join_tab=0x6f7f980, end_of_records=128) at sql_select.cc:11383
#5  0x0000000000601d30 in do_select (join=0x6fe11d8, fields=0x6fd5300, table=0x0, procedure=0x0) at sql_select.cc:11140
#6  0x000000000060a479 in JOIN::exec (this=0x6fe11d8) at sql_select.cc:2314
#7  0x00000000004d5102 in subselect_single_select_engine::exec (this=0x6fdcda0) at item_subselect.cc:1987
#8  0x00000000004d26b1 in Item_subselect::exec (this=0x6fdccb0) at item_subselect.cc:280
#9  0x00000000004d1aaa in Item_in_subselect::val_bool (this=0x2aaaabbc40b8) at item_subselect.cc:880
#10 0x0000000000438821 in Item::val_bool_result (this=0x2aaaabbc40b8) at item.h:745
#11 0x0000000000476941 in Item_in_optimizer::val_int (this=0x6fe2a58) at item_cmpfunc.cc:1833
#12 0x00000000005e3d9a in evaluate_join_record (join=0x6fdce98, join_tab=0x6fe3538, error=<value optimized out>) at sql_select.cc:11434
#13 0x00000000005e4215 in sub_select (join=0x6fdce98, join_tab=0x6fe3538, end_of_records=<value optimized out>) at sql_select.cc:11391
#14 0x0000000000601d30 in do_select (join=0x6fdce98, fields=0x6f819a0, table=0x0, procedure=0x0) at sql_select.cc:11140
#15 0x000000000060a479 in JOIN::exec (this=0x6fdce98) at sql_select.cc:2314
#16 0x000000000060ae0f in mysql_select (thd=0x6f7f980, rref_pointer_array=0x6f81a68, tables=0x6fd4d90, wild_num=1, fields=@0x6f819a0, conds=0x6fdccb0,    og_num=0, order=0x0, group=0x0, having=0x0, proc_param=0x0, select_options=2147764736, result=0x6fdce78, unit=0x6f81470, select_lex=0x6f81898)   at sql_select.cc:2509

表 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
#1  0x000000000060120b in JOIN::cleanup (this=0x6fe0f10, full=<value optimized out>) at sql_select.cc:6982
#2  0x0000000000601734 in JOIN::join_free (this=0x6fe0f10) at sql_select.cc:6905
#3  0x0000000000601db2 in do_select (join=0x6fe0f10, fields=0x6f819a0, table=0x0, procedure=0x0) at sql_select.cc:11161
#4  0x000000000060a479 in JOIN::exec (this=0x6fe0f10) at sql_select.cc:2314
#5  0x000000000060ae0f in mysql_select (thd=0x6f7f980, rref_pointer_array=0x6f81a68, tables=0x6fd5198, wild_num=0, fields=@0x6f819a0, conds=0x6fdd218,    og_num=0, order=0x0, group=0x0, having=0x0, proc_param=0x0, select_options=2147764736, result=0x6fdd398, unit=0x6f81470, select_lex=0x6f81898)  at sql_select.cc:2509

注:這裏並沒有顯示這個函數名,但是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表】中間的部分。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章