技術分享 | MySQL:查詢字段數量多少對查詢效率的影響

做者:高鵬
文章末尾有他著做的《深刻理解 MySQL 主從原理 32 講》,深刻透徹理解 MySQL 主從,GTID 相關技術知識。

這個問題是最近一個朋友問個人。恰好就好好看了一下,留下這樣的記錄。本文給出一些函數接口,末尾給出一些調用堆棧,爲感興趣的朋友作一個參考,也爲本身作一個筆記。html

1、問題由來

咱們知道執行計劃的不一樣確定會帶來效率的不一樣,可是在本例中執行計劃徹底一致,都是全表掃描,不一樣的只有字段個數而已。其次,測試中都使用了where 條件進行過濾(Using where),過濾後沒有數據返回,咱們常說的 where 過濾其實是在 MySQL 層,固然某些狀況下使用 ICP 會提早在 Innodb 層過濾數據,這裏咱們先不考慮 ICP,我會在後面的文章中詳細描述 ICP 的流程,本文也會給出 where 過濾的接口,供你們參考。mysql

下面的截圖來自兩個朋友,感謝他們的測試和問題提出。另外對於大數據量訪問來說可能涉及到物理 IO,首次訪問和隨後的訪問由於 Innodb buffer 的關係,效率不一樣是正常,須要多測試幾回。sql

測試1:緩存



測試2:mvc

咱們經過這兩個測試,能夠發現隨着字段的不斷減小,效率愈來愈高,而且主要的區別都在 sending data 下面,這個狀態我曾經大概描述過參考文章:ide

https://www.jianshu.com/p/46a...
https://www.jianshu.com/p/4cd...

簡單的說 Innodb 數據的獲取和 Innodb 數據到 MySQL 層數據的傳遞都包含在其中。函數

2、簡單的流程介紹

下面我主要結合字段多少和全表掃描2個方面作一個簡單的流程介紹。實際上其中有一個核心接口就是 row_search_mvcc,它大概包含了以下功能:測試

  • 經過預取緩存獲取數據
  • 打開事務
  • 定位索引位置(包含使用AHI快速定位)
  • 是否開啓 readview
  • 經過持久化遊標不斷訪問下一條數據
  • 加 Innodb 表鎖、加 Innodb 行鎖
  • 可見性判斷
  • 根據主鍵回表(可能回表須要加行鎖)
  • ICP 優化
  • SEMI update 優化

而且做爲訪問數據的必須經歷的接口,這個函數也是很值得你們細細研讀的。fetch

1.經過 select 字段構建 read_set(MySQL 層)大數據

首先須要構建一個叫作 read_set 的位圖,來表示訪問的字段位置及數量。它和 write set 一塊兒,在記錄 binlog 的 Event 的時候也會起着重要做用,能夠參考個人《深刻理解 MySQL 主從原理》中關於 binlog_row_image 參數一節。這裏構建的主要接口爲 TABLE::mark_column_used 函數,每一個須要訪問的字段都會調用它來設置本身的位圖。下面是其中的一段以下:

case MARK_COLUMNS_READ:
    bitmap_set_bit(read_set, field->field_index);

從棧幀來看這個構建 read_set 的過程位於狀態 ‘init’ 下面。棧幀見結尾棧幀1。

2.初次訪問定位的時候還會構建一個模板(mysql_row_templ_t)(Innodb 層)

本模板主要用於當 Innodb 層數據到 MySQL 層作轉換的時候使用,其中記錄了使用的字段數量、字段的字符集、字段的類型等等。接口 build_template_field 用於構建這個模板。棧幀見結尾棧幀2。
可是須要注意的是,這裏構建模板就會經過咱們上面說的 read_set 去判斷到底有多少字段須要構建到模板中,而後纔會調用 build_template_field 函數。以下是最重要的代碼,它位於 build_template_needs_field 接口中。

bitmap_is_set(table->read_set, static_cast<uint>(i)

能夠看到這裏正在測試本字段是否出如今了 read_set 中,若是不在則跳過這個字段。下面是函數 build_template_needs_field 的註釋:

Determines if a field is needed in a m_prebuilt struct 'template'.
@return field to use, or NULL if the field is not needed */

到這裏咱們須要訪問的字段已經確立下來了。

3.初次定位數據,定位遊標到主鍵索引的第一行記錄,爲全表掃描作好準備(Innodb 層)

對於這種全表掃描的執行方式,定位數據就變得簡單了,咱們只須要找到主鍵索引的第一條數據就行了,它和平時咱們使用(ref/range)定位方式不一樣,不須要二分法的支持。所以對於全表掃描的初次定位調用函數爲 btr_cur_open_at_index_side_func,而不是一般咱們說的 btr_pcur_open_with_no_init_func。
若是大概看一下函數 btr_cur_open_at_index_side_func 的功能,咱們很容易看到,它就是經過 B+ 樹結構,定位到葉子結點的開頭第一個塊,而後調用函數page_cur_set_before_first,將遊標放到了全部記錄的開頭,目的只有一個爲全表掃描作好準備。棧幀見結尾棧幀3。
注意這裏正是經過咱們 row_search_mvcc 調用下去的。

4.獲取 Innodb 層的第一條數據(Innodb 層)

拿到了遊標事後就能夠獲取數據了,這裏也很簡單代碼就是一句以下:

rec = btr_pcur_get_rec(pcur);//獲取記錄 從持久化遊標   整行數據

可是須要注意的是這裏獲取的數據只是一個指針,言外之意能夠理解爲整行數據,其格式也是原始的 Innodb 數據,其中還包含了一些僞列好比(rollback ptr和trx id)。這裏實際上和訪問的字段個數無關。

5. 將第一行記錄轉換爲 MySQL 格式(Innodb 層)

這一步完成後咱們能夠認爲記錄已經返回給了 MySQL 層,這裏就是實際的數據拷貝了,並非指針,整個過程放到了函數 row_sel_store_mysql_rec 中。
咱們前面的模板(mysql_row_templ_t)也會在這裏發揮它的做用,這是一個字段過濾的過程,咱們先來看一個循環

for (i = 0; i < prebuilt->n_template; i++)
其中 prebuilt->n_template 就是字段模板的個數,咱們前面已經說過了,經過 read_set 的過濾,對於咱們不須要的字段是不會創建模板的。所以這裏的模板數量是和咱們訪問的字段個數同樣的。

而後在這個循環下面會調用 row_sel_store_mysql_field_func 而後調用 row_sel_field_store_in_mysql_format_func 將字段一個一個轉換爲 MySQL 的格式。咱們來看一下其中一種類型的轉換以下:

case DATA_INT:
    /* Convert integer data from Innobase to a little-endian
    format, sign bit restored to normal */

    ptr = dest + len;

    for (;;) {
        ptr--;
        *ptr = *data;//值拷貝 內存拷貝
        if (ptr == dest) {
            break;
        }
        data++;
    }

咱們能夠發現這是一種實際的轉換,也就是須要花費內存空間的。棧幀見結尾棧幀4。
到這裏咱們大概知道了,查詢的字段越多那麼着這裏轉換的過程越長,而且這裏都是實際的內存拷貝,而非指針指向。

最終這行數據會存儲到 row_search_mvcc 的形參 buffer 中返回給 MySQL 層,這個形參的註釋以下:

@param[out] buf     buffer for the fetched row in MySQL format

6.對第一條數據進行 where 過濾(MySQL 層)

拿到數據後固然還不能做爲最終的結果返回給用戶,咱們須要在 MySQL 層作一個過濾操做,這個條件比較位於函數 evaluate_join_record 的開頭,其中比較就是下面一句話

found= MY_TEST(condition->val_int()); //進行比較 調用到 條件和 返回會記錄的比較

若是和條件不匹配將會返回 False。這裏比較會最終調用 Item_func 的各類方法,若是等於則是 Item_func_eq,棧幀見結尾棧幀5。

7.訪問下一條數據

上面我已經展現了訪問第一條數據的大致流程,接下面須要作的就是繼續訪問下去,以下:

  • 移動遊標到下一行
  • 訪問數據
  • 根據模板轉換數據返回給 MySQL 層
  • 根據 where 條件過濾

整個過程會持續到所有主鍵索引數據訪問完成。可是須要注意的是上層接口有些變化,由 ha_innobase::index_first 會變爲 ha_innobase::rnd_next,統計數據由 Handler_read_first 變爲 Handler_read_rnd_next,這點能夠參考個人文章:

https://www.jianshu.com/p/25f...
而且 row_search_mvcc 的流程確定也會有變化。這裏再也不熬述。可是實際的獲取數據轉換過程和過濾過程並無改變。

注意了這些步驟除了步驟 1,基本都處於 sending data 下面。

3、回到問題自己

好了到這裏咱們大概知道全表掃描的訪問數據的流程了,咱們就來看看一下在全表掃描流程中字段的多少到底有哪些異同點:

不一樣點:

  • 構建的 read_set 不一樣,字段越多 read_set 中爲‘1’的位數越多
  • 創建的模板不一樣,字段越多模板數量越多
  • 每行數據轉換爲 MySQL 格式的時候不一樣,字段越多模板越多,那麼循環轉換每一個字段的循環次數也就越多,而且這是每行都要處理的。

相同點:

  • 訪問的行數一致
  • 訪問的流程一致
  • where 過濾的方式一致

在整個不一樣點中,我認爲最耗時的部分應該是每行數據轉換爲 MySQL 格式的消耗最大,由於每行每一個字段都須要作這樣的轉換,這也恰好是除以 sending data 狀態下面。咱們線上大於 10 個字段的表比比皆是,若是咱們只須要訪問其中的少許字段,咱們最好仍是寫實際的字段而不是‘*’,來規避這個問題。

4、寫在最後

雖然本文中以全表掃描爲列進行了解釋,可是實際上任何狀況下咱們都應該縮減訪問字段的數量,應該只訪問須要的字段。

5、備用棧幀

棧幀1 read_set 構建

#0  TABLE::mark_column_used (this=0x7ffe7c996c50, thd=0x7ffe7c000b70, field=0x7ffe7c997c88, mark=MARK_COLUMNS_READ)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/table.cc:6344
#1  0x00000000015449b4 in find_field_in_table_ref (thd=0x7ffe7c000b70, table_list=0x7ffe7c0071f0, name=0x7ffe7c006a38 "id", length=2, item_name=0x7ffe7c006a38 "id", 
    db_name=0x0, table_name=0x0, ref=0x7ffe7c006bc0, want_privilege=1, allow_rowid=true, cached_field_index_ptr=0x7ffe7c0071a0, register_tree_change=true, 
    actual_table=0x7fffec0f46d8) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_base.cc:7730
#2  0x0000000001544efc in find_field_in_tables (thd=0x7ffe7c000b70, item=0x7ffe7c0070c8, first_table=0x7ffe7c0071f0, last_table=0x0, ref=0x7ffe7c006bc0, 
    report_error=IGNORE_EXCEPT_NON_UNIQUE, want_privilege=1, register_tree_change=true) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_base.cc:7914
#3  0x0000000000faadd8 in Item_field::fix_fields (this=0x7ffe7c0070c8, thd=0x7ffe7c000b70, reference=0x7ffe7c006bc0)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/item.cc:5857
#4  0x00000000015478ee in setup_fields (thd=0x7ffe7c000b70, ref_pointer_array=..., fields=..., want_privilege=1, sum_func_list=0x7ffe7c005d90, allow_sum_func=true, 
    column_update=false) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_base.cc:9047
#5  0x000000000161419d in st_select_lex::prepare (this=0x7ffe7c005c30, thd=0x7ffe7c000b70) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_resolver.cc:190

棧幀2 構建模板

#0  build_template_field (prebuilt=0x7ffe7c99b880, clust_index=0x7ffe7c999c20, index=0x7ffe7c999c20, table=0x7ffe7c996c50, field=0x7ffe7c997c88, i=0, v_no=0)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:7571
#1  0x00000000019d1dc1 in ha_innobase::build_template (this=0x7ffe7c997610, whole_row=false)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:8034
#2  0x00000000019d60f5 in ha_innobase::change_active_index (this=0x7ffe7c997610, keynr=0)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9805
#3  0x00000000019d682b in ha_innobase::rnd_init (this=0x7ffe7c997610, scan=true)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:10031
#4  0x0000000000f833b9 in handler::ha_rnd_init (this=0x7ffe7c997610, scan=true) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/handler.cc:3096
#5  0x00000000014e24d1 in init_read_record (info=0x7ffe7cf47d60, thd=0x7ffe7c000b70, table=0x7ffe7c996c50, qep_tab=0x7ffe7cf47d10, use_record_cache=1, 
    print_error=true, disable_rr_cache=false) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/records.cc:315

棧幀3 全表掃描初次定位棧幀

#0  page_cur_set_before_first (block=0x7fff4d02f4a0, cur=0x7ffe7c99bab0) at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/include/page0cur.ic:99
#1  0x0000000001c5187f in btr_cur_open_at_index_side_func (from_left=true, index=0x7ffe7c999c20, latch_mode=1, cursor=0x7ffe7c99baa8, level=0, 
    file=0x239d388 "/root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/include/btr0pcur.ic", line=562, mtr=0x7fffec0f3570)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/btr/btr0cur.cc:2422
#2  0x0000000001b6e9c9 in btr_pcur_open_at_index_side (from_left=true, index=0x7ffe7c999c20, latch_mode=1, pcur=0x7ffe7c99baa8, init_pcur=false, level=0, 
    mtr=0x7fffec0f3570) at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/include/btr0pcur.ic:562
#3  0x0000000001b79a35 in row_search_mvcc (buf=0x7ffe7c997b50 "\377", mode=PAGE_CUR_G, prebuilt=0x7ffe7c99b880, match_mode=0, direction=0)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:5213
#4  0x00000000019d5493 in ha_innobase::index_read (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377", key_ptr=0x0, key_len=0, find_flag=HA_READ_AFTER_KEY)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9536
#5  0x00000000019d66ea in ha_innobase::index_first (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9977
#6  0x00000000019d6934 in ha_innobase::rnd_next (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:10075
#7  0x0000000000f83725 in handler::ha_rnd_next (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/handler.cc:3146
#8  0x00000000014e2b3d in rr_sequential (info=0x7ffe7cf47d60) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/records.cc:521

棧幀4 MySQL 格式的轉換

#0  row_sel_field_store_in_mysql_format_func (dest=0x7ffe7c997b51 "", templ=0x7ffe7c9a27f8, index=0x7ffe7c999c20, field_no=0, data=0x7fff4daec0a1 "\200", len=4, 
    prebuilt=0x7ffe7c99b880, sec_field=18446744073709551615) at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:2888
#1  0x0000000001b754b9 in row_sel_store_mysql_field_func (mysql_rec=0x7ffe7c997b50 "\377", prebuilt=0x7ffe7c99b880, rec=0x7fff4daec0a1 "\200", index=0x7ffe7c999c20, 
    offsets=0x7fffec0f3a80, field_no=0, templ=0x7ffe7c9a27f8, sec_field_no=18446744073709551615)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:3255
#2  0x0000000001b75c85 in row_sel_store_mysql_rec (mysql_rec=0x7ffe7c997b50 "\377", prebuilt=0x7ffe7c99b880, rec=0x7fff4daec0a1 "\200", vrow=0x0, rec_clust=0, 
    index=0x7ffe7c999c20, offsets=0x7fffec0f3a80, clust_templ_for_sec=false) at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:3434
#3  0x0000000001b7bd61 in row_search_mvcc (buf=0x7ffe7c997b50 "\377", mode=PAGE_CUR_G, prebuilt=0x7ffe7c99b880, match_mode=0, direction=0)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/row/row0sel.cc:6123
#4  0x00000000019d5493 in ha_innobase::index_read (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377", key_ptr=0x0, key_len=0, find_flag=HA_READ_AFTER_KEY)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9536
#5  0x00000000019d66ea in ha_innobase::index_first (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:9977
#6  0x00000000019d6934 in ha_innobase::rnd_next (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/storage/innobase/handler/ha_innodb.cc:10075
#7  0x0000000000f83725 in handler::ha_rnd_next (this=0x7ffe7c997610, buf=0x7ffe7c997b50 "\377")
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/handler.cc:3146
#8  0x00000000014e2b3d in rr_sequential (info=0x7ffe7cf47d60) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/records.cc:521
#9  0x0000000001584264 in join_init_read_record (tab=0x7ffe7cf47d10) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:2487
#10 0x0000000001581349 in sub_select (join=0x7ffe7cf47660, qep_tab=0x7ffe7cf47d10, end_of_records=false)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:1277
#11 0x0000000001580cce in do_select (join=0x7ffe7cf47660) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:950

棧幀5 String 的等值比較

#0  Arg_comparator::compare_string (this=0x7ffe7c0072f0) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/item_cmpfunc.cc:1669
#1  0x0000000000fde1e4 in Arg_comparator::compare (this=0x7ffe7c0072f0) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/item_cmpfunc.h:92
#2  0x0000000000fcb0a1 in Item_func_eq::val_int (this=0x7ffe7c007218) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/item_cmpfunc.cc:2507
#3  0x0000000001581af9 in evaluate_join_record (join=0x7ffe7c0077d8, qep_tab=0x7ffe7cb1dc70)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:1492
#4  0x000000000158145a in sub_select (join=0x7ffe7c0077d8, qep_tab=0x7ffe7cb1dc70, end_of_records=false)
    at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:1297
#5  0x0000000001580cce in do_select (join=0x7ffe7c0077d8) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:950
#6  0x000000000157eb8a in JOIN::exec (this=0x7ffe7c0077d8) at /root/mysqlall/percona-server-locks-detail-5.7.22/sql/sql_executor.cc:199
相關文章
相關標籤/搜索