mysql的架構採用引擎分離的模式,innodb引擎負責最終的數據查詢。
mysql解析sql後,調用innodb進行搜索數據,這個過程並不是mysql一次性調用,然後等待innodb返回所有的結果。
innodb提供了一個查詢方法,每次只查詢一行記錄,然後返回記錄,直到查詢不出結果。
mysql innodb的核心查詢邏輯
方法位置:
storage/innobase/row/row0sel.cc
row_search_mvcc
單條查詢,每次只查詢一條,即使是全表掃描,也是用這個方法一條一條的讀出來
參數:
buf 最終結果將存儲到這個buf
mode 查詢模式,B+樹搜索/葉子節點正向遍歷/葉子節點逆向遍歷 PAGE_CUR_GE PAGE_CUR_L PAGE_CUR_G
prebuilt 表信息和查詢條件,其中的search_tuple就是要條件索引條件字段和值
prebuilt->search_tuple 就是要條件索引條件字段和值
prebuilt->pcur 指向上一條查詢出的記錄,
match_mode 精確匹配, prefix匹配(mysql的最左匹配原則)
direction 升序或逆序查詢
如果是精確匹配,並且如果是聯合索引,索引字段不為空。 那麼結果只可能是0或1個
if (match_mode == ROW_SEL_EXACT
&& dict_index_is_unique(index)
&& dtuple_get_n_fields(search_tuple)
== dict_index_get_n_unique(index)
&& (dict_index_is_clust(index)
|| !dtuple_contains_null(search_tuple))) {
搜索結果是唯一的
unique_search = TRUE;
開啓事務
trx_start_if_not_started(trx, false);
// 如果隔離級別是讀未提交或讀已提交,不用加gap鎖
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
&& prebuilt->select_lock_type != LOCK_NONE
&& trx->mysql_thd != NULL
&& thd_is_select(trx->mysql_thd)) {
set_also_gap_locks = FALSE;
}
升序降序
direction==0 默認或1(ROW_SEL_NEXT就是ASC)
moves_up = TRUE 設為升級遍歷
if (direction == 0) {
if (mode == PAGE_CUR_GE
|| mode == PAGE_CUR_G
|| mode >= PAGE_CUR_CONTAIN) {
moves_up = TRUE;
}
} else if (direction == ROW_SEL_NEXT) {
moves_up = TRUE;
}
取第一個索引,第一個索引必定是聚集索引
clust_index = dict_table_get_first_index(index->table);
事務read_view
若當前查詢不需要加鎖LOCK_NONE,
那就只需要保證一致性讀,也就是最普通的select查詢,
需要生成一個read_view,實現一致性讀
if (prebuilt->select_lock_type == LOCK_NONE) {
if (!srv_read_only_mode) {
trx_assign_read_view(trx);
}
prebuilt->sql_stat_start = FALSE;
}
S鎖
需要加鎖的情況
這裏先加了表鎖, 沒有直接加行鎖
若是S鎖,則加IS鎖,否則加IX鎖
else {
// 加鎖讀,先加意向表鎖
// 加表鎖,要麼是LOCK_IS,要麼是LOCK_IX
err = lock_table(0, index->table,
prebuilt->select_lock_type == LOCK_S
? LOCK_IS : LOCK_IX, thr);
if (err != DB_SUCCESS) {
table_lock_waited = TRUE;
goto lock_table_wait;
}
prebuilt->sql_stat_start = FALSE;
}
判斷查詢條件,是否在索引範圍內
這決定是否走索引查詢
索引查詢
if (dtuple_get_n_fields(search_tuple) > 0) { … }
利用索引遍歷B+樹
search_tuple就是查詢目標值,根據參數查詢到第0層(葉子節點)
btr_pcur_open_with_no_init(index, search_tuple, mode,
BTR_SEARCH_LEAF,
pcur, 0, &mtr);
主要邏輯就是遍歷樹
btr_cur_search_to_nth_level 找到第n層
先找到索引樹的根頁,加載到內存
page_cur_search_with_match(index,tuple,mode) 在當前頁進行查找(從根頁開始,深度優先遍歷)
node_ptr = page_cur_get_rec(page_cursor) 找到下一個node
loop 循環遍歷, 直到找到目標層或第0層,
page_cur_search_with_match_bytes() 找到目標行
根據搜索到的指針pcur,讀取找到的行記錄
rec = btr_pcur_get_rec(pcur)
在索引遍歷時,如果是降序遍歷,並且還需要加鎖
就會額外處理,找到當前行的下一行,增加GAP鎖
若當前id=100,下一個110,則會在100和110的間隙加間隙鎖。
if (!moves_up
&& !page_rec_is_supremum(rec)
&& set_also_gap_locks
&& !(srv_locks_unsafe_for_binlog
|| trx->isolation_level <= TRX_ISO_READ_COMMITTED)
&& prebuilt->select_lock_type != LOCK_NONE
&& !dict_index_is_spatial(index)) {
const rec_t* next_rec = page_rec_get_next_const(rec);
offsets = rec_get_offsets(next_rec, index, offsets,
ULINT_UNDEFINED, &heap);
err = sel_set_rec_lock(pcur,
next_rec, index, offsets,
prebuilt->select_lock_type,
LOCK_GAP, thr, &mtr);
switch (err) {
case DB_SUCCESS_LOCKED_REC:
err = DB_SUCCESS;
case DB_SUCCESS:
break;
default:
goto lock_wait_or_error;
}
}
非索引查詢
如果不走索引,那就進行全表掃描, 利用葉子節點的雙向鏈表遍歷。
PAGE_CUR_G 從B+樹葉子節點最左側開始掃描(最小節點升序遍歷)
PAGE_CUR_L 從B+樹葉子節點最右側開始掃描(最大節點降序遍歷)
else if (mode == PAGE_CUR_G || mode == PAGE_CUR_L) {
btr_pcur_open_at_index_side(
mode == PAGE_CUR_G, index, BTR_SEARCH_LEAF,
pcur, false, 0, &mtr);
}
row_search_mvcc方法內容非常多,主要的查詢邏輯就是這些。
還有鎖的處理,事務的處理,mvcc,將在後面分析。
各種查詢條件對應的查詢邏輯
範圍查詢
小於查詢
select * from t where id<10;
參數mode為 PAGE_CUR_G
search_tuple為空,取出根頁的最小記錄,查出最小頁,最小記錄。
然後根據葉子節點的鏈表,依次取出所有符合條件的記錄。
if(mode==PAGE_CUR_G || mode==PAGE_CUR_L)
btr_pcur_open_at_index_side()
btr_pcur_open_at_index_side方法將從葉子節點的最左或最右取值
mode決定了它從哪邊取值
PAGE_CUR_G從最左側(最小)取值,PAGE_CUR_L從最右側(最大)取值。
終結條件
compare_key(end_range)
找到大於end_range時,符合判斷跳出循環。
找到符合條件的記錄時,會判斷當前版本是不是readView可見
如果不可見,將從版本鏈的上一級取出。
lock_clust_rec_cons_read_sees(rec,index,offsets,trx_get_read_view(trx))
判斷找到的記錄是不是被刪了
rec_get_deleted_flag(rec, comp)
轉格式,存儲引擎是innodb,取出來的數據是innodb格式,需要轉成mysql
row_sel_store_mysql_rec()
大於查詢
select * from t where id>10;
和小於不同,search_tuple有值,也就是需要先查詢出“id=10"的記錄。
然後從"id=10"的記錄往後遍歷,剩餘部分同小於查詢
全表掃描
select * from t;
同小於查詢 mode類型為 PAGE_CUR_G ,只是沒有終結條件
升序查詢
聚集索引默認就是按升序存儲的,全表掃描就是升序查詢
MOVES_UP = true 升序
降序查詢
mode類型為 PAGE_CUR_L, 從葉子節點鏈表的最右側取值
然後從左往右遍歷
MOVES_UP = false 降序
葉子節點頁存在雙向鏈表指針,從而可以快速取出數據。
但記錄與記錄之間只有單向鏈表關係。
如果是升序取數據,可以利用記錄的鏈表關係。
如果是降序,只能通過頁之間的鏈表關係,頁內的記錄就無法通過鏈表依次取出,會比升序要複雜一些。
btr_pcur_move_to_pre()
btr_pcur_move_to_prev_on_page()
btr_pcur_move_to_pre(btr_pcur_get_page_cur(cursor))
頁內降序查詢的辦法比較笨,從頁內最小記錄開始遍歷,匹配到目標值,回退到上一個,就是反向遍歷。
如存在1-10的一組數據, 當前值10,從1開始遍歷,找到10,退回到9,返回
再從1開始遍歷,找到9,退回到8,返回
範圍+降序查詢
原本的小於查詢從最小頁開始遍歷,有降序條件後,將從範圍值開始降序遍歷
原本的大於查詢從範圍值開始遍歷,有降序條件後,將從最大頁開始降序遍歷,直到終結條件。
in查詢
原理頁就是逐個按主鍵取。
但它返回的數據是升序的,這説明它在查詢前就把in裏面的條件排序過了再逐個查詢。
輔助索引
通用也是走row_search_mvcc 不同的是傳入的index索引是輔助索引,search_tuple為輔助索引值 匹配到輔助索引的記錄後,再進行回表查詢。 回表查詢,因為輔助索引的葉子節點沒有記錄完整的數據,只存儲了主鍵id和輔助索引值,如果select的值超出了輔助索引存的值,就需要從聚集索引裏面取值,也就是再進行一次或多次主鍵索引查詢,查出完整的數據。 如果輔助索引是唯一索引,只會發生一次回表。 如果輔助索引不是唯一索引,就有可能多次回表。 ·
row_sel_get_clust_rec_for_mysql(prebuilt, index, rec,
thr, &clust_rec,
&offsets, &heap,
need_vrow ? &vrow : NULL,
&mtr);
這個方法最終調用前面用到的遍歷B+樹的方法從聚集索引查詢出數據
拿到聚集索引
clust_index = dict_table_get_first_index(sec_index->table);
遍歷聚集索引B+樹
btr_pcur_open_with_no_init
mysql通過bitmap來記錄當前表需要獲取的字段