MySQL engine層到server層字段過濾優化

1.1 問題描述

  執行計劃的不同肯定會帶來效率的不同,但是在本例中執行計劃完全一致,都是全表掃描,不同的只有字段個數而已。其次,測試中都使用了where條件進行過濾(Using where),過濾後沒有數據返回,常說的where過濾實際上是在server層,當然某些情況下使用ICP會提前在Innodb層過濾數據,這裏不考慮ICP。
  對於大數據量訪問來講可能涉及到物理IO,首次訪問和隨後的訪問因爲Innodb buffer的關係,效率不同是正常,需要多測試幾次。
_
_
_
  通過上面的測試,可以發現隨着字段的不斷減少,效率越來越高,並且主要的區別都在sending data下面。簡單的說Innodb數據的獲取和Innodb數據到server層數據的傳遞都包含在其中。

2.2 理論依據

https://dev.mysql.com/doc/dev/mysql-server/latest/

全表訪問數據的流程

  這裏將簡單描述一下這種全表掃描的流程,實際上其中有一個核心接口就是row_search_mvcc,它大概包含了如下功能:

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

  下面對MySQL處理字段多少時的優化流程做出介紹:

1、通過select字段構建read_set(server 層)

  首先需要構建一個叫做read_set的位圖,來表示訪問的字段位置及數量。

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

  本模板主要用於當Innodb層數據到server層做轉換的時候使用,其中記錄了使用的字段數量、字段的字符集、字段的類型等等。

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

  對於這種全表掃描的執行方式,定位數據就變得簡單了,只需要找到主鍵索引的第一條數據就好了。對於全表掃描的初次定位調用函數爲btr_cur_open_at_index_side_fun。

  btr_cur_open_at_index_side_func的功能就是通過B+樹結構,定位葉子結點的開頭第一個塊,然後調用函數page_cur_set_before_first,將遊標放到了所有記錄的開頭,目的只有一個爲全表掃描做好準備。

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

  拿到了遊標過後就可以獲取數據了。但是這裏獲取的數據只是一個指針,言外之意可以理解爲整行數據,其格式也是原始的Innodb數據,其中還包含了一些僞列比如(rollback ptr和trx id)。這裏實際上和訪問的字段個數無關。

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

  這一步完成後可以認爲記錄已經返回給了server層,這裏就是實際的數據拷貝了,並不是指針,整個過程放到了函數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++;
        }

  可以發現這是一種實際的轉換,也就是需要花費內存空間的。查詢的字段越多那麼着這裏轉換的過程越長,並且這裏都是實際的內存拷貝,最終這行數據會存儲到row_search_mvcc的形參buffer中返回給server層。

6、對第一條數據進行where過濾(server層)

  拿到數據後當然還不能作爲最終的結果返回給用戶,需要在server層做一個過濾操作,這個條件比較位於函數evaluate_join_record的開頭。

  如果和條件不匹配將會返回False。這裏比較會最終調用Item_func的各種方法,如果等於則是Item_func_eq。

7、訪問下一條數據(server 層)

  上面已經展示了訪問第一條數據的大體流程,接下面需要做的就是繼續訪問下去,如下:

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

  整個過程會持續到全部主鍵索引數據訪問完成。

  並且row_search_mvcc的流程肯定也會有變化。但是實際的獲取數據轉換過程和過濾過程並沒有改變。注意這些步驟除了步驟1,基本都處於sending data下面。

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

不同點

  • 構建的read_set不同,字段越多read_set中爲‘1’的位數越多
  • 建立的模板不同,字段越多模板數量越多
  • 每行數據轉換爲MySQL格式的時候不同,字段越多模板越多,那麼循環轉換每個字段的循環次數也就越多,並且這是每行都要處理的。返回給server層的行內存消耗越大。

相同點

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

  在整個不同點中,認爲最耗時的部分應該是每行數據轉換爲MySQL格式的消耗最大,因爲每行每個字段都需要做這樣的轉換,這也剛好是除以sending data狀態下面。線上大於10個字段的表比比皆是,如果只需要訪問其中的少量字段,最好還是寫實際的字段而不是‘*’,來規避這個問題。

總結
  本文中以全表掃描爲列進行了解釋,但是實際上任何情況下都應該縮減訪問字段的數量,應該只訪問需要的字段。

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