自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

MySQL 自適應(yīng)哈希索引—構(gòu)造

數(shù)據(jù)庫 MySQL
AHI 構(gòu)造流程的前三步都是在判斷是否滿足某些條件,這些條件的范圍從大到小。先是索引級別,判斷索引被命中的次數(shù)。然后,是索引級別的構(gòu)造信息計(jì)數(shù)。

曾經(jīng)優(yōu)化慢查詢時(shí),經(jīng)常在日志中看到 truncate,當(dāng)時(shí)一直疑惑 truncate 為什么會(huì)慢。

轉(zhuǎn)到數(shù)據(jù)庫方向之后,又碰到過幾次 truncate 執(zhí)行時(shí)間過長,導(dǎo)致 MySQL 短暫卡住的問題。

經(jīng)過源碼分析和同事測試驗(yàn)證,發(fā)現(xiàn)這幾次的問題都跟自適應(yīng)哈希索引有關(guān),所以,深入研究下自適應(yīng)哈希索引就很有必要了。

自適應(yīng)哈希索引大概會(huì)有 3 篇文章。第 1 篇,我們來看看自適應(yīng)哈希索引的構(gòu)造過程。

本文基于 MySQL 8.0.32 源碼,存儲(chǔ)引擎為 InnoDB。

1、準(zhǔn)備工作

創(chuàng)建測試表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3

插入測試數(shù)據(jù):

INSERT INTO `t1`(`id`, `i1`)
VALUES (10, 101), (20, 201), (30, 301);

示例 SQL:

SELECT * FROM `t1`
WHERE `i1` >= 101 AND `i1` < 301

2、AHI 有什么好處?

自適應(yīng)哈希索引,英文全稱 Adaptive Hash Index,簡稱 AHI。

根據(jù)上下文需要,后續(xù)內(nèi)容會(huì)混用自適應(yīng)哈希索引和 AHI,不再單獨(dú)說明。

顧名思義,自適應(yīng)哈希索引也是一種索引,使用哈希表作為存儲(chǔ)結(jié)構(gòu),自適應(yīng)的意思是我們無法干涉它的創(chuàng)建、使用、更新、刪除,而是由 InnoDB 自行決定什么時(shí)候創(chuàng)建、怎么創(chuàng)建、什么時(shí)候更新和刪除。

我們知道 InnoDB 的主鍵索引、二級索引都是 B+ 樹結(jié)構(gòu)。執(zhí)行 SQL 語句時(shí),以下幾種場景都需要定位到索引葉子結(jié)點(diǎn)中的某條記錄:

  • insert 需要定位到插入目標(biāo)位置的前一條記錄。
  • 查詢優(yōu)化階段,select、update、delete 預(yù)估掃描區(qū)間記錄數(shù)量時(shí),需要定位到掃描區(qū)間的第一條最后一條記錄。
  • 查詢執(zhí)行階段,select、update、delete 需要定位到掃描區(qū)間的第一條記錄。

得益于多叉結(jié)構(gòu),B+ 樹的層級一般都比較少,通常情況下,從根結(jié)點(diǎn)開始,最多經(jīng)過 3 ~ 4 層就能定位到葉子結(jié)點(diǎn)中的記錄。

這個(gè)定位過程看起來挺快的,單次執(zhí)行也確實(shí)比較快,但是對于頻繁執(zhí)行的場景,還是有一點(diǎn)優(yōu)化空間的。

為了追求極致性能,InnoDB 實(shí)現(xiàn)了 AHI,目的就是能夠根據(jù)搜索條件直接定位到索引葉子結(jié)點(diǎn)中的記錄。

圖片圖片

上圖是一棵高為 4 的 B+ 樹示意圖,定位索引葉子結(jié)點(diǎn)記錄,需要從根結(jié)點(diǎn)開始,經(jīng)過內(nèi)結(jié)點(diǎn)、葉結(jié)點(diǎn),最后定位到記錄,路徑為:① -> ② -> ③ -> ④

圖片圖片

AHI 會(huì)使用多個(gè) hash 桶來存儲(chǔ)哈希值到記錄地址的映射,上圖是一個(gè) hash 桶的示意圖。

橙色塊表示記錄地址組成的鏈表,因?yàn)槎鄺l記錄可能被映射到 hash 桶中的同一個(gè)位置。

AHI 定位一條記錄時(shí),根據(jù) where 條件中的某些字段計(jì)算出哈希值,然后直接到 hash 桶中找到對應(yīng)的記錄。

如果多條記錄被映射到 hash 桶中的同一個(gè)位置,那么找到的是個(gè)鏈表,需要遍歷這個(gè)鏈表以找到目標(biāo)記錄。

3、原理介紹

AHI 能提升定位記錄的效率,但是,有得到必然有付出,天上掉餡餅的事在計(jì)算機(jī)世界里是不會(huì)發(fā)生的。

為了簡潔,這里我們把定位索引葉子結(jié)點(diǎn)中的記錄簡稱為定位記錄,后面內(nèi)容也依此定義,不再單獨(dú)說明。

高效定位記錄,付出的代價(jià)是存儲(chǔ)哈希值到記錄地址的映射占用的內(nèi)存空間、以及構(gòu)造映射花費(fèi)的時(shí)間。

因?yàn)橛袝r(shí)間、空間成本,所以,InnoDB 希望構(gòu)造 AHI 之后,能夠盡可能多的用上,做到收益大于成本。直白點(diǎn)說,就是需要滿足一定的條件,才能構(gòu)造 AHI。

AHI 采用的是組團(tuán)構(gòu)造邏輯,也就是以數(shù)據(jù)頁為單位,滿足一定條件之后,就會(huì)為數(shù)據(jù)頁中的所有記錄構(gòu)造 AHI,主要流程如下:

圖片圖片

第 1 步,為數(shù)據(jù)頁所屬的索引計(jì)數(shù)(index->search_info->hash_analysis)。

SQL 執(zhí)行過程中,每次定位某個(gè)索引葉子結(jié)點(diǎn)中的記錄,該索引的計(jì)數(shù)都會(huì)加 1。

如果索引計(jì)數(shù)達(dá)到 17,進(jìn)入第 2 步,否則,執(zhí)行流程到此結(jié)束。

因?yàn)榈?2、3 步為構(gòu)造信息計(jì)數(shù)、為數(shù)據(jù)頁計(jì)數(shù)也是需要時(shí)間成本的,所以,這里設(shè)置了第 1 道檻,只有索引被使用一定次數(shù)之后,才會(huì)執(zhí)行第 2、3 步。

第 2 步,為構(gòu)造信息計(jì)數(shù)(index->search_info->n_hash_potential)。

對于 select、insert、update、delete,定位記錄時(shí),搜索條件和葉子結(jié)點(diǎn)中的記錄比較,會(huì)產(chǎn)生兩個(gè)邊界,左邊為下界,右邊為上界,基于下界和上界可以得到用于構(gòu)造 AHI 的信息,我們稱之為構(gòu)造信息。

圖片圖片

以上是定位記錄時(shí)產(chǎn)生的下界、上界示意圖。定位記錄過程中,下界和上界會(huì)不斷向目標(biāo)記錄靠近,最終,下界或上界的其中一個(gè)會(huì)指向目標(biāo)記錄。

如果某次定位記錄時(shí),基于下界或上界得到的構(gòu)造信息,和索引對象中保存的構(gòu)造信息一致,該構(gòu)造信息計(jì)數(shù)加 1。否則,該索引計(jì)數(shù)清零,構(gòu)造信息計(jì)數(shù)清零或重置為 1(具體見下一小節(jié)的介紹)。

第 3 步,為數(shù)據(jù)頁計(jì)數(shù)(block->n_hash_helps)。

定位到索引葉子結(jié)點(diǎn)記錄之后,就知道了該記錄所屬的數(shù)據(jù)頁,如果本次得到的構(gòu)造信息和數(shù)據(jù)頁對象中保存的構(gòu)造信息相同,數(shù)據(jù)頁計(jì)數(shù)加 1,否則數(shù)據(jù)頁計(jì)數(shù)重置為 1。

第 4 步,為數(shù)據(jù)頁構(gòu)造 AHI。

如果滿足以下兩個(gè)條件,第 3 步的數(shù)據(jù)頁就具備了構(gòu)造 AHI 的資格:

  • 構(gòu)造信息計(jì)數(shù)大于等于 100。
  • 數(shù)據(jù)頁計(jì)數(shù)大于數(shù)據(jù)頁中記錄數(shù)量的十六分之一。

具備構(gòu)造 AHI 的資格之后,對于以下三種情況之一,會(huì)為數(shù)據(jù)頁構(gòu)造 AHI:

  • 該數(shù)據(jù)頁之前沒有構(gòu)造過 AHI。
  • 該數(shù)據(jù)頁之前構(gòu)造過 AHI,并且構(gòu)造 AHI 之后,數(shù)據(jù)頁會(huì)從零開始重新計(jì)數(shù),重新計(jì)數(shù)大于數(shù)據(jù)頁中記錄數(shù)量的兩倍。
  • 該數(shù)據(jù)頁之前構(gòu)造過 AHI,但是本次確定的構(gòu)造信息和之前不一樣了。

注意:從各個(gè)條件判斷,到最終構(gòu)造 AHI 的整個(gè)流程,并不是在執(zhí)行一條 SQL 的過程中完成的,而是在執(zhí)行多條 SQL 的過程中完成的。

到這里,構(gòu)造 AHI 的主要流程就介紹完了,構(gòu)造過程的具體細(xì)節(jié),請繼續(xù)往下看。

4、構(gòu)造過程

定位記錄會(huì)調(diào)用 btr_cur_search_to_nth_level(),這也是 AHI 的構(gòu)造入口:

// storage/innobase/btr/btr0cur.cc
void btr_cur_search_to_nth_level(...) {
  ...
  if (btr_search_enabled && 
      !index->disable_ahi) {
    // AHI 的構(gòu)造入口
    btr_search_info_update(cursor);
  }
  ...
}

btr_search_enabled = true(默認(rèn)值),表示 InnoDB 啟用了 AHI。

!index->disable_ahi = true,即 index->disable_ahi = false,表示 index 對應(yīng)的索引沒有禁用 AHI。

只有內(nèi)部臨時(shí)表、沒有主鍵的表禁用了 AHI。

也就是說,對于正常的用戶表、系統(tǒng)表,!index->disable_ahi = true,會(huì)調(diào)用 btr_search_info_update(),進(jìn)入 AHI 的構(gòu)造流程。

(1)索引計(jì)數(shù)

構(gòu)造 AHI 的第 1 步,就是調(diào)用 btr_search_info_update() 進(jìn)行索引計(jì)數(shù)。

// storage/innobase/include/btr0sea.ic
static inline void btr_search_info_update(btr_cur_t *cursor) {
  const auto index = cursor->index;
  ...
  // 索引計(jì)數(shù)加 1
  const auto hash_analysis_value = ++index->search_info->hash_analysis;
  // BTR_SEARCH_HASH_ANALYSIS = 17(硬編碼)
  if (hash_analysis_value < BTR_SEARCH_HASH_ANALYSIS) {
    /* Do nothing */
    return;
  }
  ...
  btr_search_info_update_slow(cursor);
}

btr_search_info_update() 每次被調(diào)用都會(huì)增加索引計(jì)數(shù)(++index->search_info->hash_analysis)。

自增之后,如果索引計(jì)數(shù)小于 17,不需要進(jìn)入 AHI 構(gòu)造流程的下一步,直接返回。

如果索引計(jì)數(shù)大于等于 17,調(diào)用 btr_search_info_update_slow(),進(jìn)入 AHI 構(gòu)造流程的下一步。

看到這里,大家是否會(huì)有疑問:對于一條能使用索引的 select 語句,如果 where 條件只有一個(gè)掃描區(qū)間,執(zhí)行過程中,btr_search_info_update() 最多會(huì)被調(diào)用幾次?

我們通過 1. 準(zhǔn)備工作小節(jié)的示例 SQL 來揭曉答案,SQL 如下:

SELECT * FROM `t1`
WHERE `i1` >= 101 AND `i1` < 301

執(zhí)行計(jì)劃如下:

圖片圖片

通過執(zhí)行計(jì)劃可知,示例 SQL 執(zhí)行過程中,會(huì)使用索引 idx_i1。

查詢優(yōu)化階段,MySQL 需要定位到掃描區(qū)間的第一條和最后一條記錄,用于計(jì)算掃描區(qū)間覆蓋的記錄數(shù)量:

  • 定位掃描區(qū)間的第一條記錄,即滿足 id >= 101 的第一條記錄,第 1 次調(diào)用 btr_cur_search_to_nth_level()。
  • 定位掃描掃描區(qū)間的最后一條記錄,即滿足 id < 301 的最后一條記錄,第 2 次調(diào)用 btr_cur_search_to_nth_level()。

查詢執(zhí)行階段,從存儲(chǔ)引擎讀取第一條記錄之前,需要定位掃描區(qū)間的第一條記錄,即滿足 id >= 101 的第一條記錄,第 3 次調(diào)用 btr_cur_search_to_nth_level()。

定位掃描區(qū)間的第一條和最后一條記錄,都是定位索引葉子結(jié)點(diǎn)中的記錄。

到這里,我們就得到了前面那個(gè)問題的答案:3 次。

(2)構(gòu)造信息計(jì)數(shù)

如果某個(gè)索引的計(jì)數(shù)達(dá)到了 17,就會(huì)進(jìn)入 AHI 構(gòu)造流程的第 2 步,根據(jù)本次定位記錄過程中得到的下界和上界,確定使用索引的前幾個(gè)字段構(gòu)造 AHI,以及對于索引中前幾個(gè)字段值相同的一組記錄,構(gòu)造 AHI 時(shí)選擇這組記錄的第一條還是最后一條。

3.原理介紹小節(jié)的第 2 步,我們已經(jīng)把這些信息命名為構(gòu)造信息。

基于定位記錄時(shí)得到的下界和上界確定構(gòu)造信息、為構(gòu)造信息計(jì)數(shù)的邏輯由 btr_search_info_update_hash() 完成。

// storage/innobase/btr/btr0sea.cc
void btr_search_info_update_slow(btr_cur_t *cursor) {
  ...
  const auto block = btr_cur_get_block(cursor);
  ...
  btr_search_info_update_hash(cursor);
  ...
}

btr_search_info_update_hash() 的代碼有點(diǎn)長,我們分 2 段介紹。

介紹第 1 段代碼之前,我們先來看看表示構(gòu)造信息的結(jié)構(gòu)體定義(index->search_info->prefix_info):

// storage/innobase/include/buf0buf.h
// 為了方便閱讀,以下結(jié)構(gòu)體定義對源碼做了刪減
struct btr_search_prefix_info_t {
  /** recommended prefix: number of bytes in an incomplete field */
  uint32_t n_bytes;
  /** recommended prefix length for hash search: number of full fields */
  uint16_t n_fields;
  /** true or false, depending on whether the leftmost record of several records
  with the same prefix should be indexed in the hash index */
  bool left_side;
}

btr_search_prefix_info_t 包含 3 個(gè)屬性:

  • n_fields、n_bytes:索引的前 n_fields 個(gè)字段,第 n_fields + 1 個(gè)字段的前 n_bytes 字節(jié)用于構(gòu)造 AHI。
  • left_side:如果索引中多條記錄的前 n_fields 個(gè)字段內(nèi)容、第 n_fields + 1 個(gè)字段前 n_bytes 字節(jié)的內(nèi)容相同,我們把這樣的一組記錄稱為前綴相同的記錄。對于前綴相同的記錄:left_side = true 時(shí),選擇最左邊的記錄(第一條記錄)構(gòu)造 AHI。left_side = false 時(shí),選擇最右邊的記錄(最后一條記錄)構(gòu)造 AHI。

接下來,我們開始介紹 btr_search_info_update_hash() 的第 1 段代碼邏輯。

// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
  dict_index_t *index = cursor->index;
  int cmp;
  ...
  // 索引中,通過幾個(gè)字段能唯一確定一條記錄?
  // 對于主鍵索引,n_unique = 主鍵段字?jǐn)?shù)量
  // 對于二級索引,n_unique = 二級索引字段數(shù)量 + 主鍵字段數(shù)量
  const uint16_t n_unique =
      static_cast<uint16_t>(dict_index_get_n_unique_in_tree(index));
  const auto info = index->search_info;
  
  /****** if_1 ******/
  // 構(gòu)造信息計(jì)數(shù)不等于 0
  // 說明之前已經(jīng)確定過構(gòu)造信息了
  if (info->n_hash_potential != 0) {
    // info->prefix_info 中
    // 保存了之前確定的構(gòu)造信息
    const auto prefix_info = info->prefix_info.load();
    ...

    /****** if_2 ******/
    // prefix_info.n_fields
    //   表示之前確定的構(gòu)造信息的字段數(shù)量
    // std::max(up_match, low_match)
    //   表示下界或上界字段數(shù)量中較大的那個(gè)
    if (prefix_info.n_fields == n_unique &&
        std::max(cursor->up_match, cursor->low_match) == n_unique) {
      // 兩個(gè)條件都滿足,說明:
      //   如果本次通過下界、上界確定構(gòu)造信息
      //   會(huì)和之前確定的構(gòu)造信息相同
      //   那么,構(gòu)造信息計(jì)數(shù)加 1
      info->n_hash_potential++;

      return;
    }
    ...
    const bool low_matches_prefix =
        0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
                         cursor->low_match, cursor->low_bytes);
    const bool up_matches_prefix =
        0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
                         cursor->up_match, cursor->up_bytes);
    /****** if_3 ******/
    // 這里的構(gòu)造信息指的是:
    //   索引對象中保存的之前確定的構(gòu)造信息
    // prefix_info.left_side = true
    //   如果構(gòu)造信息【大于】下界,且【小于等于】上界
    //   構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1
    // prefix_info.left_side = false
    //   如果構(gòu)造信息【小于等于】下界,且【大于】上界
    //   構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1
    if (prefix_info.left_side ? (!low_matches_prefix && up_matches_prefix)
                              : (low_matches_prefix && !up_matches_prefix)) {
      info->n_hash_potential++;
      return;
    }
  }
  ...
}

如果構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)不等于 0,if_1 條件成立,說明索引對象中已經(jīng)保存了之前確定的構(gòu)造信息。

但是,確定索引的 AHI 構(gòu)造信息之后,還需要該索引的構(gòu)造信息計(jì)數(shù)、某個(gè)數(shù)據(jù)頁的計(jì)數(shù)滿足條件,InnoDB 才會(huì)為該索引的該數(shù)據(jù)頁構(gòu)造 AHI。

所以,索引對象中已經(jīng)保存了之前確定的構(gòu)造信息對應(yīng)兩種情況:

  • 已經(jīng)用索引中保存的構(gòu)造信息為某個(gè)(些)數(shù)據(jù)頁構(gòu)造了 AHI。
  • 只確定了構(gòu)造信息,還沒有用它構(gòu)造過 AHI。

構(gòu)造信息計(jì)數(shù)被命名為 n_hash_potential(潛在的),就是因?yàn)榇嬖诘?2 種情況。

if_2、if_3 用于判斷:通過本次定位記錄時(shí)產(chǎn)生的下界或上界得到構(gòu)造信息,是否和索引對象中保存的構(gòu)造信息一致,如果一致,則增加構(gòu)造信息計(jì)數(shù)。

if_2 包含兩個(gè)表達(dá)式,如果值都為 true,說明上面的判斷結(jié)果為 true,構(gòu)造信息計(jì)數(shù)加 1(info->n_hash_potential++)。

如果 if_2 不成立,再判斷 if_3 是否成立。

前面介紹過,對于前綴相同的一組記錄,構(gòu)造 AHI 時(shí),由 left_side 決定選擇最左邊還是最右邊的記錄。

對于本次定位記錄得到的下界、上界,left_side 決定它們怎么和索引對象中保存的構(gòu)造信息比較。

left_side = true 時(shí),如果以下兩個(gè)條件同時(shí)滿足,構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1:

  • prefix_info.n_fields、n_bytes 大于下界(low_match、low_bytes)。
  • prefix_info.n_fields、n_bytes 小于等于上界(up_match、up_bytes)。

left_side = false 時(shí),如果以下兩個(gè)條件同時(shí)滿足,構(gòu)造信息計(jì)數(shù)(n_hash_potential)加 1:

  • prefix_info.n_fields、n_bytes 小于等于下界(low_match、low_bytes)。
  • prefix_info.n_fields、n_bytes 大于上界(up_match、up_bytes)。

如果 if_3 成立,說明本次定位記錄得到的下界或上界的字段數(shù)量、字節(jié)數(shù),和索引對象中保存的構(gòu)造信息的字段數(shù)量(n_fields)、字節(jié)數(shù)(n_bytes)一致,構(gòu)造信息計(jì)數(shù)加 1(info->n_hash_potential++)。

如果 if_3 不成立,說明構(gòu)造信息變了,需要執(zhí)行第 2 段代碼,確定新的構(gòu)造信息,并且重置構(gòu)造信息計(jì)數(shù)。

// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
  ...
  info->hash_analysis = 0;

  cmp = ut_pair_cmp(cursor->up_match, cursor->up_bytes, cursor->low_match,
                    cursor->low_bytes);
  /****** if_4 ******/
  if (cmp == 0) {
    // where 條件沒有定位到匹配的記錄
    // 構(gòu)造信息計(jì)數(shù)清零
    info->n_hash_potential = 0;
    // 雖然給構(gòu)造信息賦值了,但是這個(gè)信息不會(huì)被使用
    /* For extra safety, we set some sensible values here */
    info->prefix_info = {0, 1, true};

  /****** elseif_5 ******/
  } else if (cmp > 0) {
    // 上界(up_match、up_bytes)大于下界(low_match、low_bytes)
    // left_side 都會(huì)設(shè)置為 true
    // 構(gòu)造信息計(jì)數(shù)重置為 1
    info->n_hash_potential = 1;

    ut_ad(cursor->up_match <= n_unique);
    // 如果上界字段數(shù)量等于 n_unique
    // 使用上界作為新的構(gòu)造信息
    if (cursor->up_match == n_unique) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ n_unique,
        /* left_side */ true
      };

    // 下界字段數(shù)量(low_match)小于上界字段數(shù)量(up_match)
    // 使用下界作為新的構(gòu)造信息
    } else if (cursor->low_match < cursor->up_match) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ static_cast<uint16_t>(cursor->low_match + 1),
        /* left_side */ true
      };

    // 下界字段數(shù)量(low_match)等于上界字段數(shù)量(up_match)
    // 下界字節(jié)數(shù)(low_bytes)小于上界字節(jié)數(shù)(up_bytes)
    // 使用下界作為新的構(gòu)造信息
    } else {
      info->prefix_info = {
        /* n_bytes   */ static_cast<uint32_t>(cursor->low_bytes + 1),
        /* n_fields  */ static_cast<uint16_t>(cursor->low_match),
        /* left_side */ true
      };
    }

  /****** else_1 ******/
  } else {
    // 上界(up_match、up_bytes)小于下界(low_match、low_bytes)
    // left_side 都會(huì)設(shè)置為 false
    // 構(gòu)造信息計(jì)數(shù)重置為 1
    info->n_hash_potential = 1;

    ut_ad(cursor->low_match <= n_unique);
    // 如果下界字段數(shù)量等于 n_unique
    // 使用下界作為新的構(gòu)造信息
    if (cursor->low_match == n_unique) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ n_unique,
        /* left_side */ false
      };

    // 下界字段數(shù)量(low_match)大于上界字段數(shù)量(up_match)
    // 使用上界作為新的構(gòu)造信息
    } else if (cursor->low_match > cursor->up_match) {
      info->prefix_info = {
        /* n_bytes   */ 0,
        /* n_fields  */ static_cast<uint16_t>(cursor->up_match + 1),
        /* left_side */ false
      };
    
    // 下界字段數(shù)量(low_match)等于上界字段數(shù)量(up_match)
    // 下界字節(jié)數(shù)(low_bytes)大于上界字節(jié)數(shù)(up_bytes)
    // 使用上界作為新的構(gòu)造信息
    } else {
      info->prefix_info = {
        /* n_bytes   */ static_cast<uint32_t>(cursor->up_bytes + 1),
        /* n_fields  */ static_cast<uint16_t>(cursor->up_match),
        /* left_side */ false
      };
    }
  }
}

第 2 段代碼用于首次或者重新確定構(gòu)造信息,主要邏輯如下:

  • 索引計(jì)數(shù)(info->hash_analysis)清零,下次調(diào)用 btr_search_info_update_hash() 時(shí)重新開始計(jì)數(shù)。
  • 如果 left_side = true,并且本次定位記錄得到的下界字段數(shù)量等于 n_unique,使用下界作為新的構(gòu)造信息。
  • 如果 left_side = false,并且本次定位記錄得到的上界字段數(shù)量等于 n_unique,使用上界作為新的構(gòu)造信息。
  • 否則,選擇本次定位記錄得到的上界、下界中較小的那個(gè)作為新的構(gòu)造信息。

ut_pair_cmp(up_match, up_bytes, low_match, low_bytes) 比較本次定位記錄得到的上界、下界,比較結(jié)果保存到 cmp 中。

如果 cmp 等于 0,命中 if_4,說明 btr_cur_search_to_nth_level() 定位記錄時(shí),上界、下界相同(up_match 等于 low_match、up_bytes 等于 low_bytes),也就是沒有找到匹配的記錄,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 0。

如果 cmp 大于 0,命中 elseif_5,說明本次定位記錄時(shí),下界小于上界,構(gòu)造信息(prefix_info)的 left_side 屬性都會(huì)被設(shè)置為 true。

if (cursor->up_match == n_unique) 條件成立,說明搜索條件能夠唯一確定索引中的一條記錄,使用 up_match 作為新構(gòu)造信息的字段數(shù)量,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 1,重新開始計(jì)數(shù)。

否則,剩下兩種情況,取下界(因?yàn)楸壬辖缧。┳鳛樾聵?gòu)造信息。

cursor->low_match、low_bytes 都從 0 開始,變成數(shù)量時(shí)需要加 1。

如果 cmp 小于 0,命中 else_1,說明本次定位記錄時(shí),下界大于上界,構(gòu)造信息(prefix_info)的 left_side 屬性都會(huì)被設(shè)置為 false。

if (cursor->low_match == n_unique) 條件成立,說明搜索條件能夠唯一確定索引中的一條記錄,使用 low_match 作為新構(gòu)造信息的字段數(shù)量,構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)重置為 1,重新開始計(jì)數(shù)。

否則,剩下兩種情況,取上界(因?yàn)楸认陆缧。┳鳛樾聵?gòu)造信息。

cursor->up_match、up_bytes 都從 0 開始,變成數(shù)量時(shí)需要加 1。

(3)數(shù)據(jù)頁計(jì)數(shù)

btr_search_update_block_hash_info() 的主要邏輯分為 2 段:

  • 第 1 段:更新數(shù)據(jù)頁計(jì)數(shù)。
  • 第 2 段:根據(jù)構(gòu)造信息計(jì)數(shù)、數(shù)據(jù)頁計(jì)數(shù),決定是否需要為 block 對應(yīng)的數(shù)據(jù)頁構(gòu)造或重新構(gòu)造 AHI。

先來看第 1 段代碼:

// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static bool btr_search_update_block_hash_info(...) {
  ...
  const auto info = cursor->index->search_info;
  // last_hash_succ 用于判斷 where 條件是否命中了 AHI
  // 先設(shè)置為 false
  info->last_hash_succ = false;
  ...
  // 數(shù)據(jù)頁計(jì)數(shù)是否大于 0
  if (block->n_hash_helps > 0 && 
      // 構(gòu)造信息計(jì)數(shù)是否大于 0
      info->n_hash_potential > 0 &&
      // 數(shù)據(jù)頁對象中保存的構(gòu)造信息
      //  (可能還沒有用來構(gòu)造過 AHI)
      // 和本次確定的構(gòu)造信息是否相同
      block->ahi.recommended_prefix_info.load() == info->prefix_info.load()) {
    if (block->ahi.index &&
        block->ahi.prefix_info.load() == info->prefix_info.load()) {
      // 數(shù)據(jù)頁對象中保存的構(gòu)造信息(用來構(gòu)造過 AHI)
      // 和本次確定的構(gòu)造信息相同
      // 說明 where 條件命中了 AHI
      // 把 info->last_hash_succ 設(shè)置為 true
      // 下一篇文章講 AHI 命中時(shí)會(huì)用到這個(gè)屬性
      info->last_hash_succ = true;
    }
    // 構(gòu)造信息沒有變化,構(gòu)造信息計(jì)數(shù)加 1
    block->n_hash_helps++;
  } else {
    // 構(gòu)造信息變了,重置數(shù)據(jù)頁計(jì)數(shù)
    block->n_hash_helps = 1;
    // 新確定的構(gòu)造信息保存到數(shù)據(jù)頁對象中
    block->ahi.recommended_prefix_info = info->prefix_info.load();
  }
  ...
}

如果以下 3 個(gè)條件都滿足:

  • 數(shù)據(jù)頁計(jì)數(shù)(block->n_hash_helps)大于 0。
  • 構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)大于 0。
  • 數(shù)據(jù)頁對象中保存的構(gòu)造信息(block->ahi.recommended_prefix_info)和本次確定的構(gòu)造信息相同。

說明數(shù)據(jù)頁的構(gòu)造信息沒有變化,數(shù)據(jù)頁計(jì)數(shù)加 1(block->n_hash_helps++)。

否則,數(shù)據(jù)頁計(jì)數(shù)重置為 1,并且保存新的構(gòu)造信息到數(shù)據(jù)頁對象中(block->ahi.recommended_prefix_info)。

// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static bool btr_search_update_block_hash_info(...) {
  ...
  // BTR_SEARCH_BUILD_LIMIT = 100
  // 同一個(gè)索引的 AHI 構(gòu)造信息
  // 連續(xù) 100 次相同
  if (info->n_hash_potential >= BTR_SEARCH_BUILD_LIMIT &&
      // BTR_SEARCH_PAGE_BUILD_LIMIT = 16
      // 同樣的 AHI 構(gòu)造信息
      // 命中同一個(gè)數(shù)據(jù)頁的次數(shù)
      // 達(dá)到了該數(shù)據(jù)頁中記錄數(shù)量的十六分之一
      block->n_hash_helps >
          page_get_n_recs(block->frame) / BTR_SEARCH_PAGE_BUILD_LIMIT) {
    // 滿足上面 2 個(gè)條件之后
    // 如果之前沒有構(gòu)造過 AHI,則返回 true
    // 表示本次需要構(gòu)造 AHI
    if (!block->ahi.index ||
        // 如果之前已經(jīng)構(gòu)造過 AHI
        // 需要命中同一個(gè)數(shù)據(jù)頁的次數(shù)
        //   達(dá)到該數(shù)據(jù)頁中記錄數(shù)量的 2 倍
        // 或者 AHI 構(gòu)造信息發(fā)生了變化
        // 才會(huì)重新構(gòu)造 AHI
        block->n_hash_helps > 2 * page_get_n_recs(block->frame) ||
        block->ahi.recommended_prefix_info.load() !=
            block->ahi.prefix_info.load()) {
      return true;
    }
  }

  // 不滿足上面的一系列條件
  // 返回 false
  // 表示本次不需要構(gòu)造 AHI
  return false;
}

第 2 段代碼,用于判斷是否需要為數(shù)據(jù)頁(block)構(gòu)造或重新構(gòu)造 AHI,滿足以下兩個(gè)條件,說明具備了為該數(shù)據(jù)頁構(gòu)造 AHI 的資格:

  • 構(gòu)造信息計(jì)數(shù)(info->n_hash_potential)大于等于 100。
  • 數(shù)據(jù)頁計(jì)數(shù)(block->n_hash_helps)大于數(shù)據(jù)頁中記錄數(shù)量的十六分之一。

數(shù)據(jù)頁(block)具備構(gòu)造 AHI 的資格之后,只有以下三種情況會(huì)構(gòu)造或重新構(gòu)造 AHI:

  • 該數(shù)據(jù)頁之前沒有構(gòu)造過 AHI(!block->ahi.index 為 true)。
  • 該數(shù)據(jù)頁之前構(gòu)造過 AHI,構(gòu)造信息沒有發(fā)生變化,但是數(shù)據(jù)頁計(jì)數(shù)大于數(shù)據(jù)頁中記錄數(shù)量的兩倍(2 * page_get_n_recs(block->frame))。
  • 該數(shù)據(jù)頁之前構(gòu)造過 AHI,但是構(gòu)造信息變了。

(4)構(gòu)造 AHI

滿足前面一系列條件之后,就可以為數(shù)據(jù)頁構(gòu)造 AHI 了。

// storage/innobase/btr/btr0sea.cc
static void btr_search_build_page_hash_index(...) {
  ...
  // block 是本次需要構(gòu)造 AHI 的數(shù)據(jù)頁控制塊
  // page 是數(shù)據(jù)頁對象
  const auto page = buf_block_get_frame(block);
  // 數(shù)據(jù)頁的 AHI 構(gòu)造信息
  const auto prefix_info = block->ahi.recommended_prefix_info.load();
  const auto n_fields_for_offsets = btr_search_get_n_fields(prefix_info);

  // 刪除之前為該數(shù)據(jù)頁構(gòu)造的 AHI 記錄
  if (block->ahi.index && block->ahi.prefix_info.load() != prefix_info) {
    btr_search_drop_page_hash_index(block);
  }
  ...
  // 數(shù)據(jù)頁中的記錄數(shù)量
  const auto n_recs = page_get_n_recs(page);
  ...
  // page_get_infimum_rec(page) 讀取 infimum 偽記錄
  // page_rec_get_next() 讀取數(shù)據(jù)頁中的第 1 條用戶記錄
  auto rec = page_rec_get_next(page_get_infimum_rec(page));

  Rec_offsets offsets;
  ...
  // 用于構(gòu)造第 1 條用戶記錄的 AHI 的 hash 種子
  const auto index_hash = btr_hash_seed_for_record(index);
  // 計(jì)算第 1 條用戶記錄的 AHI hash 值
  auto hash_value =
      rec_hash(rec, offsets.compute(rec, index, n_fields_for_offsets),
               prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);

  size_t n_cached = 0;
  // left_side = true
  // 對于前綴相同的一組記錄(可能有一條或多條記錄)
  // 標(biāo)記需要為這一組的第一條記錄構(gòu)造 AHI
  if (prefix_info.left_side) {
    hashes[n_cached] = hash_value;
    recs[n_cached] = rec;
    n_cached++;
  }

  // 循環(huán),標(biāo)記需要為數(shù)據(jù)頁中第 2 條及以后的用戶記錄構(gòu)造 AHI
  for (;;) {
    const auto next_rec = page_rec_get_next(rec);
    if (page_rec_is_supremum(next_rec)) {
      // left_side = false 時(shí)
      // 標(biāo)記需要為 supremum 偽記錄構(gòu)造 AHI
      // 然后結(jié)束循環(huán)
      if (!prefix_info.left_side) {
        hashes[n_cached] = hash_value;
        recs[n_cached] = rec;
        n_cached++;
      }

      break;
    }
    // 為當(dāng)前循環(huán)的記錄計(jì)算用于構(gòu)造 AHI 的 hash 值
    const auto next_hash_value = rec_hash(
        next_rec, offsets.compute(next_rec, index, n_fields_for_offsets),
        prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);
    // hash_value != next_hash_value
    // 說明換了一組記錄
    if (hash_value != next_hash_value) {
      /* Insert an entry into the hash index */
      // left_side = true
      // 為下一組的第一條記錄構(gòu)造 AHI
      if (prefix_info.left_side) {
        hashes[n_cached] = next_hash_value;
        recs[n_cached] = next_rec;
        n_cached++;
      } else {
        // left_side = false
        // 為本組的最后一條記錄構(gòu)造 AHI
        hashes[n_cached] = hash_value;
        recs[n_cached] = rec;
        n_cached++;
      }
    }

    rec = next_rec;
    hash_value = next_hash_value;
  }
  ...
  // 為數(shù)據(jù)頁構(gòu)造 AHI 之后
  // 重置數(shù)據(jù)頁計(jì)數(shù)
  block->n_hash_helps = 0;
  // 已經(jīng)用來構(gòu)造過 AHI 構(gòu)造信息保存到
  // 數(shù)據(jù)頁對象的 ahi.prefix_info 屬性中
  block->ahi.prefix_info = prefix_info;
  block->ahi.index = index;
  // 把每條記錄的 AHI hash 值和記錄地址
  // 插入到 AHI hash 表中
  const auto table = btr_get_search_table(index);
  for (size_t i = 0; i < n_cached; i++) {
    ha_insert_for_hash(table, hashes[i], block, recs[i]);
  }
  ...
}

為數(shù)據(jù)頁構(gòu)造 AHI 主要分為三大步驟:

  • 循環(huán)讀取數(shù)據(jù)頁中的記錄,每讀取一條記錄,根據(jù) AHI 構(gòu)造信息計(jì)算記錄的哈希值,把哈希值保存到 hashes 數(shù)組中、記錄地址保存到 recs 數(shù)組中,一條記錄在 hashs、recs 數(shù)組中的下標(biāo)一樣,都是 n_cached。這一步有個(gè)特殊邏輯需要處理,就是對于前綴相同的一組記錄,根據(jù) left_side 決定為第一條還是最后一條記錄構(gòu)造 AHI。
  • 數(shù)據(jù)頁計(jì)數(shù)(block->n_hash_helps)重置為 0,重新開始計(jì)數(shù),用于為該數(shù)據(jù)頁重新構(gòu)造 AHI 作準(zhǔn)備。然后,把構(gòu)造信息(prefix_info)、數(shù)據(jù)頁所屬的索引(index)分別保存到數(shù)據(jù)頁對象的 ahi.prefix_info、ahi.index 屬性中,btr_search_update_block_hash_info() 會(huì)用到這兩個(gè)屬性。
  • 把 hashs、recs 數(shù)組中的哈希值、記錄地址一一對應(yīng)的插入到哈希表中,每條記錄的哈希值映射到該記錄的地址。

4、總結(jié)

AHI 構(gòu)造流程的前三步都是在判斷是否滿足某些條件,這些條件的范圍從大到小。

先是索引級別,判斷索引被命中的次數(shù)。

然后,是索引級別的構(gòu)造信息計(jì)數(shù)。

構(gòu)造信息來源于定位記錄過程中產(chǎn)生的下界、上界,其源頭是 where 條件,我們可以把它看成對 where 條件的抽象,或者更具體點(diǎn),把它看成 where 條件的分類。

某個(gè)構(gòu)造信息的計(jì)數(shù)達(dá)到指定次數(shù),意味著如果根據(jù)這個(gè)構(gòu)造信息(或者說這類 where 條件)構(gòu)造 AHI,命中率會(huì)比較高。

InnoDB 以數(shù)據(jù)頁為單位,一次性為某個(gè)數(shù)據(jù)頁中的所有記錄構(gòu)造 AHI。

構(gòu)造信息計(jì)數(shù)滿足條件之后,還需要進(jìn)一步?jīng)Q定為哪些數(shù)據(jù)頁構(gòu)造 AHI,于是就有了數(shù)據(jù)頁計(jì)數(shù)(實(shí)際上是數(shù)據(jù)頁級別的構(gòu)造信息計(jì)數(shù))。

當(dāng)索引計(jì)數(shù)、構(gòu)造信息計(jì)數(shù)、數(shù)據(jù)頁計(jì)數(shù)都滿足條件之后,某個(gè)數(shù)據(jù)頁就初步具備了構(gòu)造 AHI 的資格,最后,還會(huì)根據(jù)該數(shù)據(jù)頁是否構(gòu)造過 AHI、構(gòu)造信息是否發(fā)生變化等條件,做出終極決定:是否為該數(shù)據(jù)頁構(gòu)造 AHI。

本文轉(zhuǎn)載自微信公眾號「一樹一溪」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系一樹一溪公眾號。

責(zé)任編輯:姜華 來源: 一樹一溪
相關(guān)推薦

2025-04-08 08:20:00

2023-04-12 16:45:07

MySQL索引數(shù)據(jù)結(jié)構(gòu)

2017-06-06 10:30:12

前端Web寬度自適應(yīng)

2022-04-26 10:13:00

哈希索引MySQLInnoDB

2012-05-09 10:58:25

JavaMEJava

2010-08-30 09:52:03

DIV高度自適應(yīng)

2014-09-05 10:10:32

Android自適應(yīng)布局設(shè)計(jì)

2022-04-12 07:48:57

云技術(shù)SDN網(wǎng)絡(luò)

2024-05-22 09:31:07

2025-01-21 08:00:00

自適應(yīng)框架框架開發(fā)

2023-10-23 08:48:04

CSS寬度標(biāo)題

2010-08-30 10:26:20

DIV自適應(yīng)高度

2017-08-16 14:08:46

Android O圖標(biāo)視覺

2021-11-01 23:57:03

數(shù)據(jù)庫哈希索引

2009-04-23 11:24:09

2011-05-12 11:28:20

按比例縮放

2022-10-24 17:57:06

CSS容器查詢

2025-03-12 00:00:22

2015-06-08 10:49:04

2022-04-15 11:05:28

移動(dòng)端自適應(yīng)高清
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號