关于innodb中查询的定位方法

原创如有错误请指出

源码版本 5.7.14

涉及源码文件

page0cur.cc
page0page.h
page0page.cc
rem0cmp.cc

为什么谈及定位方法,因为在innodb中,比如一个插入语句我们需要定位在哪里插入(PAGE_CUR_LE),比如一个查询语句我们需要定位到其第一个需要读取数据的位置,因此定位方法是查询的根本。而找到这个记录位置后实际上是用一个叫做page_cur_t结构体进行存储,暂且叫他cursor游标

struct page_cur_t{
    const dict_index_t* index;
    rec_t*      rec;    /*!< pointer to a record on page */
    ulint*      offsets;
    buf_block_t*    block;  /*!< pointer to the block containing rec */
};

其中包含了本index的数据字典类容、实际的数据、记录所在块的信息等,下面我具体谈一下定位方法,同时结合源码来看它具体的实现。

我们先来明确一下概念:

  1. 记录(rec):通常为存储在内存中物理记录的完全拷贝,通常用一个unsigned char* 指针指向整个记录
  2. 元组(dtuple):物理记录的逻辑体现,他就复杂得多,但是一个记录(rec)对应一个元组(dtuple),由dtuple_t结构体表示,其中每一个field由一个dfield_t结构体表示,数据存储在dfied_t的一个叫做void* data的指针中

可自行参考运维内参等其他书籍,这里就在简单描述到这里,本文会出现相关术语。

一、查询模式(search mode)

在innodb中的常用的search mode有如下几个

/* Page cursor search modes; the values must be in this order! */
enum page_cur_mode_t {
    PAGE_CUR_UNSUPP = 0,
    PAGE_CUR_G  = 1,
    PAGE_CUR_GE = 2,
    PAGE_CUR_L  = 3,
    PAGE_CUR_LE = 4,
};
  1. PAGE_CUR_G(>)
  2. PAGE_CUR_GE(>=)
  3. PAGE_CUR_L(<)
  4. PAGE_CUR_LE(<=)

我们来讨论一个问题考虑如下有序的数组

1,2,3,3,4,5,6,7
规律1:

如果我们查询>=3(PAGE_CUR_GE)和<3(PAGE_CUR_L),那么自然我们需要将位置定位到2到3之间我们且用2-3表示

  • 如果是>=3那么我们需要将记录定位到3及[3(第一个),正无穷)
  • 如果是<3那么我们需要将记录定位到2及(负无穷,2]
    也就是说>=3和<3定位的区间是相同的2-3

如果我们查询<=3(PAGE_CUR_LE)和>3(PAGE_CUR_G),那么自然我们需要将位置定位到3到4之间我们且用3-4表示

  • 如果是<=3那么我们需要将记录定位到3及(负无穷,3(最后一个)]
  • 如果是>3那么我们需要将记录定位到4及[4,正无穷)
    也就是说<=3和>3定位的区间是相同的3-4

那么我们将这里的区间两个值记为low-high

规律2:

仔细分析后我们发现另外一个规律

  • (>) PAGE_CUR_G和(>=) PAGE_CUR_GE 都是取high
  • (< )PAGE_CUR_L和(<=) PAGE_CUR_LE 都是取low

为什么讲这个东西,因为这两个规律在innodb记录定位中起到了关键作用,也直接影响到了innodb记录查找的二分算法的实现方式。

二、matched_fields和matched_bytes

大家在源码中能看到matched_fields和matched_bytes两个值,那么他们代表什么意思呢?
以int类型为例,因为在函数cmp_dtuple_rec_with_match_bytes是逐个字段逐个字节进行比较的,关键代码如下

while (cur_field < n_cmp) {
rec_byte = *rec_b_ptr++;
dtuple_byte = *dtuple_b_ptr++;}

比如int 2,int 3在innodb中内部表示为0X80000002和0X80000003,如果他们进行比较那么最终此field的比较为不相等(-1),那么matched_fields=0但是

  • 0X 800000 02
  • 0X 800000 03
    我们能够发现其中有3个字节是相同的及0X80 0X00 0X00 所以matched_bytes=3
    简单的说matched_fields为相同field数量,如果field不相同则会返回相同的字节数。
    当然cmp_dtuple_rec_with_match_bytes对不同数据类型的比较方式也不相同如下:
        switch (type->mtype) {
        case DATA_FIXBINARY:
        case DATA_BINARY:
        case DATA_INT:
        case DATA_SYS_CHILD:
        case DATA_SYS:
            break;
        case DATA_BLOB:
            if (type->prtype & DATA_BINARY_TYPE) {
                break;
            }
        default:
            ret = cmp_data(type->mtype, type->prtype,
                       dtuple_b_ptr, dtuple_f_len,
                       rec_b_ptr, rec_f_len);

            if (!ret) {
                goto next_field;
            }

            cur_bytes = 0;
            goto order_resolved;
        }

具体可以参考一下源码,这里不再过多解释

三、块内二分查询方法再析

为什么叫做再析,因为如运维内参已经对本函数进行了分析,这里主要分析查询模式对二分法实现的影响,并且用图进行说明你会有新的感悟!当然如果你对什么slot还不清楚请自行参考运维内参

简单的说page_cur_search_with_match_bytes会调用cmp_dtuple_rec_with_match_bytes函数进行元组和记录之间的比较,而块内部比较方法就是先对所有的slot进行二分查找确定到某个slot以快速缩小范围,然后在对slot内部使用类似二分查找的方法等到记录,我们主要来分析一下slot内部的类二分法,因为它完全是我们查询模式中两个规律的完美体现,如下简化的代码片段以及我写的注释:

/* Perform linear search until the upper and lower records come to
    distance 1 of each other. */

    while (page_rec_get_next_const(low_rec) != up_rec) {  //如果low_rec和up_rec相差1则结束循环,否则继续

        mid_rec = page_rec_get_next_const(low_rec);//这里并没有除以2作为mid_rec而是简单的取下一行,因为rec是单链表这样显然很容易完成

        ut_pair_min(&cur_matched_fields, &cur_matched_bytes,
                low_matched_fields, low_matched_bytes,
                up_matched_fields, up_matched_bytes);

        offsets = rec_get_offsets(
            mid_rec, index, offsets_,
            dtuple_get_n_fields_cmp(tuple), &heap);//获得记录的各个字段的偏移数组

        cmp = cmp_dtuple_rec_with_match_bytes(
            tuple, mid_rec, index, offsets,
            &cur_matched_fields, &cur_matched_bytes);//进行比较 0为相等  1 元组大于记录 -1记录大于元组,并且传出field和bytes

        if (cmp > 0) { //如果元组大于mid_rec记录
low_rec_match://当然简单的将mid_rec指针赋予给low_rec即可
            low_rec = mid_rec;
            low_matched_fields = cur_matched_fields;
            low_matched_bytes = cur_matched_bytes;

        } else if (cmp) { //如果元组小于mid_rec记录
up_rec_match://当然简单的将mid_rec指针赋予给up_rec即可,这一步可以跳过很多记录
            up_rec = mid_rec;
            up_matched_fields = cur_matched_fields;
            up_matched_bytes = cur_matched_bytes;
        } 

               //下面是相等情况的判断非常关键符合我们规律1算法
               //如果元组等于mid_rec
               else if (mode == PAGE_CUR_G || mode == PAGE_CUR_LE //如果是>(PAGE_CUR_G)和<=(PAGE_CUR_LE)
               ) {
            goto low_rec_match; //执行low_rec_match
        } else //如果是>=(PAGE_CUR_GE)和<(PAGE_CUR_L)
               {
            goto up_rec_match;//执行up_rec_match
        }
    }
        //下面体现我们的规律2算法
        //如果是> PAGE_CUR_G和>= PAGE_CUR_GE 都是取high
        //如果是< PAGE_CUR_L和<= PAGE_CUR_LE 都是取low
        //因为是enum类型直接比较
    if (mode <= PAGE_CUR_GE) {
        page_cur_position(up_rec, block, cursor);
    } else {
        page_cur_position(low_rec, block, cursor);
    }
    *iup_matched_fields  = up_matched_fields;
    *iup_matched_bytes   = up_matched_bytes;
    *ilow_matched_fields = low_matched_fields;
    *ilow_matched_bytes  = low_matched_bytes;

注意一个slot的own记录为最多8条如下定义:

/* The maximum and minimum number of records owned by a directory slot. The
number may drop below the minimum in the first and the last slot in the
directory. */
#define PAGE_DIR_SLOT_MAX_N_OWNED   8
#define PAGE_DIR_SLOT_MIN_N_OWNED   4

如果大于了8则进行分裂

        if (n_owned == PAGE_DIR_SLOT_MAX_N_OWNED) {
            page_dir_split_slot(
                page, NULL,
                page_dir_find_owner_slot(owner_rec));
        }

下面我们画一个slot内部定位的图,我们以如下有序数据为例,假设每一个数字代表一个记录(rec)

1 2 2 2 3 3 4 4

我们可以看到有大量重复的记录,但是本算法也可以进行精确的定位,我们约定:

  • 红色箭头为最后定位到的值
  • 黄色箭头为mid rec
  • 黑色箭头分别表示low rec\high rec
如果是我们要定位到>2,那么我们明显要定位到2-3同时取high值3,我们用源码中的代码推导出整个过程如下:
  1. mid为2显然已经等于了元组的中的2,如图


    Paste_Image.png
  2. 但是查询模式为PAGE_CUR_G 做low_rec_match操作、并且将mid取向下一条记录后如图


    Paste_Image.png
  3. mid为2显然已经等于了元组的中的2,但是查询模式为PAGE_CUR_G做low_rec_match后、并且将mid取向下一条记录如图


    Paste_Image.png
  4. mid为2显然已经等于了元组的中的2,但是查询模式为PAGE_CUR_G做low_rec_match后、并且将mid取向下一条记录如图


    Paste_Image.png
  5. mid为3显然已经大于了元组中的2,做up_rec_match后我们发现记录定位成功,为low 2-high 3。page_rec_get_next_const(low_rec) == up_rec 循环退出如图


    Paste_Image.png
  6. 因为我们的查询模式是PAGE_CUR_G所以我们执行page_cur_position(up_rec, block, cursor);取high值如图


    Paste_Image.png
如果我们要定位到<3,那么我们明显要定位到2-3.并且取low值2。我们用源码中的代码推导出整个过程如下
  1. mid为2显然小于元组的中的3,如图


    Paste_Image.png
  2. 做low_rec_match操作、并且将mid取向下一条记录后如图


    Paste_Image.png
  3. mid为2显然小于元组的中的3,做low_rec_match操作、并且将mid取向下一条记录后如图


    Paste_Image.png
  4. mid为2显然小于元组的中的3,做low_rec_match操作、并且将mid取向下一条记录后如图


    Paste_Image.png
  5. mid为3显然等于元组的中的3,但是查询模式为PAGE_CUR_L做up_rec_match后、我们发现记录定位成功为low 2-high 3.page_rec_get_next_const(low_rec) == up_rec 循环退出如图


    Paste_Image.png
  6. 因为我们的查询模式是PAGE_CUR_L所以我们执行page_cur_position(low_rec, block, cursor);取low值如图


    Paste_Image.png

四、总结

我们slot内部的记录并不多最多为8条,二分算法slot内部并没有使用二分而是使用了取下一个记录的值的指针,非常容易实现因为记录中本来就包含了下一条记录的偏移量,并且通过访问模式两个规律将重复值过滤掉,最终找到边界。总之分析之后发现是一种精确高效的算法。

Paste_Image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • innblock | InnoDB page观察利器 特别鸣谢 笔者是知数堂早期学员,最初有写这么一个工具的想法也...
    重庆八怪阅读 1,491评论 0 3
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,719评论 0 33
  • 身体正在经受着春困的侵袭 每一分每一秒随时都可能陷入一场莫名的昏睡当中 …… 每天水果饮料酸奶的费用要超过我的一日...
    金澜爱写作阅读 238评论 0 0
  • 谨以丰子恺先生此诗,致已到来的新年。 《豁然开朗》 你若爱, 生活哪里都可爱; 你若恨, 生活哪里都可恨;...
    我是二丫阅读 658评论 1 51