深入解析 MySQL 8 C++ 源码:二级索引回表操作

发布于:2025-02-22 ⋅ 阅读:(17) ⋅ 点赞:(0)

在数据库系统中,索引是优化查询性能的关键技术之一。MySQL 的 InnoDB 存储引擎支持多种索引类型,其中二级索引(非聚簇索引)和聚簇索引(主键索引)是最常见的两种。然而,由于二级索引的叶子节点只包含索引列和主键值,而完整的行数据存储在聚簇索引中,因此在查询中涉及二级索引时,可能需要执行回表操作(Row Lookup),即通过主键值从聚簇索引中获取完整的行数据。本文将从 MySQL 8 的 C++ 源码角度,深入解析二级索引的回表操作。

一、什么是二级索引回表?

在 InnoDB 中,表的数据是按照聚簇索引(主键索引)组织的,这意味着每行数据都存储在聚簇索引的叶子节点中。而二级索引则只包含索引列和对应的主键值。当查询涉及二级索引时,InnoDB 可以通过二级索引快速定位到主键值,但为了获取完整的行数据,还需要通过主键值回表到聚簇索引中。

例如,假设有一个表 t,主键为 id,并有一个二级索引 idx_name

sql复制

CREATE TABLE t (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    INDEX idx_name (name)
);

当执行以下查询时:

sql复制

SELECT * FROM t WHERE name = 'Alice';

InnoDB 首先会通过二级索引 idx_name 查找到满足条件的记录,获取其主键值 id,然后通过主键值回表到聚簇索引中获取完整的行数据。

二、MySQL 8 中的回表操作实现

在 MySQL 8 的 InnoDB 源码中,回表操作的核心逻辑由函数 Row_sel_get_clust_rec_for_mysql::operator() 实现。以下是从源码角度对回表操作的详细解析。

1. 函数签名

cpp复制

[[nodiscard]] dberr_t Row_sel_get_clust_rec_for_mysql::operator()(
    row_prebuilt_t *prebuilt, dict_index_t *sec_index, const rec_t *rec,
    que_thr_t *thr, const rec_t **out_rec, ulint **offsets,
    mem_heap_t **offset_heap, const dtuple_t **vrow, mtr_t *mtr,
    lob::undo_vers_t *lob_undo);
  • prebuilt:预构建的结构体,包含表和索引信息。

  • sec_index:二级索引。

  • rec:当前二级索引记录。

  • thr:查询线程。

  • out_rec:输出参数,存储找到的聚簇索引记录。

  • offsets:记录的偏移数组。

  • offset_heap:内存堆,用于分配临时内存。

  • vrow:虚拟列数据(如果需要)。

  • mtr:Mini-transaction,用于管理事务。

  • lob_undo:LOB(大对象)的 undo 版本信息。

2. 构建聚簇索引的搜索条件

回表操作的第一步是从二级索引记录中提取主键值,并构建用于搜索聚簇索引的条件:

cpp复制

row_build_row_ref_in_tuple(prebuilt->clust_ref, rec, sec_index, *offsets);
  • prebuilt->clust_ref:存储主键值的元组。

  • rec:当前二级索引记录。

  • sec_index:二级索引。

  • *offsets:二级索引记录的偏移数组。

3. 打开聚簇索引的游标

接下来,使用主键值在聚簇索引中搜索对应的记录:

cpp复制

prebuilt->clust_pcur->open_no_init(clust_index, prebuilt->clust_ref,
                                   PAGE_CUR_LE, BTR_SEARCH_LEAF, 0, mtr,
                                   UT_LOCATION_HERE);
  • clust_index:表的聚簇索引。

  • prebuilt->clust_ref:主键值。

  • PAGE_CUR_LE:搜索模式(小于等于)。

  • mtr:Mini-transaction。

4. 获取聚簇索引记录

通过游标获取聚簇索引中的记录:

cpp复制

clust_rec = prebuilt->clust_pcur->get_rec();

如果未找到匹配的记录,或者记录被删除,则会返回错误。

5. 锁定记录(如果需要)

在事务隔离级别较高时,需要对聚簇索引记录加锁:

cpp复制

if (prebuilt->select_lock_type != LOCK_NONE) {
    err = lock_clust_rec_read_check_and_lock(
        lock_duration_t::REGULAR, prebuilt->clust_pcur->get_block(), clust_rec,
        clust_index, *offsets, prebuilt->select_mode,
        static_cast<lock_mode>(prebuilt->select_lock_type), LOCK_REC_NOT_GAP,
        thr);
}

6. 获取记录的旧版本(如果需要)

如果当前事务隔离级别不允许读取未提交的数据,则需要获取记录的旧版本:

cpp复制

if (trx->isolation_level > TRX_ISO_READ_UNCOMMITTED &&
    !lock_clust_rec_cons_read_sees(clust_rec, clust_index, *offsets,
                                   trx_get_read_view(trx))) {
    err = row_sel_build_prev_vers_for_mysql(
        trx->read_view, clust_index, prebuilt, clust_rec, offsets,
        offset_heap, &old_vers, vrow, mtr, lob_undo);
}

7. 检查二级索引记录是否对应聚簇索引记录

在某些情况下,二级索引记录可能被删除,或者事务隔离级别较低,需要验证二级索引记录是否确实对应聚簇索引记录:

cpp复制

if (clust_rec &&
    (old_vers || trx->isolation_level <= TRX_ISO_READ_UNCOMMITTED ||
     dict_index_is_spatial(sec_index) ||
     rec_get_deleted_flag(rec, dict_table_is_comp(sec_index->table)))) {
    err = row_sel_sec_rec_is_for_clust_rec(rec, sec_index, clust_rec,
                                           clust_index, thr, rec_equal);
}

8. 返回结果

最终,通过 out_rec 返回找到的聚簇索引记录:

cpp复制

*out_rec = clust_rec;

三、回表操作的性能影响

回表操作虽然可以获取完整的行数据,但也带来了额外的性能开销。每次回表都需要访问聚簇索引,这可能导致额外的 I/O 操作,尤其是在二级索引和聚簇索引存储在不同位置时。因此,减少回表操作的次数是优化查询性能的关键之一。

以下是一些优化建议:

  1. 选择合适的索引:尽量使用覆盖索引(Covering Index),即查询的所有列都包含在索引中,从而避免回表操作。

  2. 减少索引列的冗余:避免在二级索引中包含过多列,以减少回表的频率。

  3. 调整事务隔离级别:在允许的情况下,使用较低的事务隔离级别(如 READ UNCOMMITTEDREAD COMMITTED),可以减少回表操作的复杂性。

四、总结

二级索引回表是 MySQL InnoDB 存储引擎中的一种重要机制,用于从二级索引记录中获取完整的行数据。通过 MySQL 8 的 C++ 源码,我们可以深入了解回表操作的实现细节,包括构建搜索条件、锁定记录、获取旧版本记录等关键步骤。虽然回表操作可以提高查询的灵活性,但也需要注意其对性能的影响,并通过合理的索引设计和事务隔离级别调整来优化查询性能。

##gdb调试堆栈

#0  Row_sel_get_clust_rec_for_mysql::operator() (this=0x7ec32c5fa7e0, prebuilt=0x7ec23c0820c8, sec_index=0x7ec32b476cf8, rec=0x7ec319344baa "", thr=0x7ec23c0837b0,
    out_rec=0x7ec32c5fa6e0, offsets=0x7ec32c5fa6f8, offset_heap=0x7ec32c5fa6f0, vrow=0x0, mtr=0x7ec32c5fb030, lob_undo=0x7ec23c0822e0)
    at /home/yym/mysql8/mysql-8.1.0/storage/innobase/row/row0sel.cc:3352
#1  0x00006225275e0280 in row_search_mvcc (buf=0x7ec23c06dab0 "\351?\200\377\255\003", mode=PAGE_CUR_GE, prebuilt=0x7ec23c0820c8, match_mode=1, direction=0)
    at /home/yym/mysql8/mysql-8.1.0/storage/innobase/row/row0sel.cc:5415
#2  0x00006225272faf93 in ha_innobase::index_read (this=0x7ec23c06c3e0, buf=0x7ec23c06dab0 "\351?\200\377\255\003", key_ptr=0x7ec23c0e26e0 "\003", key_len=202,
    find_flag=HA_READ_KEY_EXACT) at /home/yym/mysql8/mysql-8.1.0/storage/innobase/handler/ha_innodb.cc:10267
#3  0x0000622525e2ff5e in handler::index_read_map (this=0x7ec23c06c3e0, buf=0x7ec23c06dab0 "\351?\200\377\255\003", key=0x7ec23c0e26e0 "\003", keypart_map=18446744073709551615,
    find_flag=HA_READ_KEY_EXACT) at /home/yym/mysql8/mysql-8.1.0/sql/handler.h:5452
#4  0x0000622525e1b2eb in handler::ha_index_read_map (this=0x7ec23c06c3e0, buf=0x7ec23c06dab0 "\351?\200\377\255\003", key=0x7ec23c0e26e0 "\003",
    keypart_map=18446744073709551615, find_flag=HA_READ_KEY_EXACT) at /home/yym/mysql8/mysql-8.1.0/sql/handler.cc:3245
#5  0x0000622527132682 in dd::Raw_table::find_record (this=0x7ec23c025f00, key=..., r=std::unique_ptr<dd::Raw_record> = {...})
    at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/raw/raw_table.cc:79
#6  0x0000622527116ef3 in dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Abstract_table> (thd=0x7ec23c001050, key=..., isolation=ISO_READ_COMMITTED,
    bypass_core_registry=false, object=0x7ec32c5fb940) at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/cache/storage_adapter.cc:181
#7  0x0000622527111061 in dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Abstract_table> (
    this=0x62252af37a00 <dd::cache::Shared_dictionary_cache::instance()::s_cache>, thd=0x7ec23c001050, key=..., isolation=ISO_READ_COMMITTED, object=0x7ec32c5fb940)
    at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/cache/shared_dictionary_cache.cc:113
#8  0x0000622527110de8 in dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Abstract_table> (
    this=0x62252af37a00 <dd::cache::Shared_dictionary_cache::instance()::s_cache>, thd=0x7ec23c001050, key=..., element=0x7ec32c5fb9a8)
    at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/cache/shared_dictionary_cache.cc:98
#9  0x0000622527022d40 in dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Abstract_table> (this=0x7ec23c004fb0, key=..., object=0x7ec32c5fba38,
    local_committed=0x7ec32c5fba2d, local_uncommitted=0x7ec32c5fba2e) at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/cache/dictionary_client.cc:913
#10 0x0000622527001f63 in dd::cache::Dictionary_client::acquire<dd::Abstract_table> (this=0x7ec23c004fb0, schema_name="performance_schema", object_name="session_variables",
    object=0x7ec32c5fbb78) at /home/yym/mysql8/mysql-8.1.0/sql/dd/impl/cache/dictionary_client.cc:1382
#11 0x00006225258b54ab in get_table_share (thd=0x7ec23c001050, db=0x7ec23c19df60 "performance_schema", table_name=0x7ec23c19df78 "session_variables",
    key=0x7ec23c1a09df "performance_schema", key_length=37, open_view=true, open_secondary=false) at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:802
#12 0x00006225258b5ca2 in get_table_share_with_discover (thd=0x7ec23c001050, table_list=0x7ec23c1a05e0, key=0x7ec23c1a09df "performance_schema", key_length=37,
    open_secondary=false, error=0x7ec32c5fbd9c) at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:922
#13 0x00006225258bbb7d in open_table (thd=0x7ec23c001050, table_list=0x7ec23c1a05e0, ot_ctx=0x7ec32c5fc270) at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:3247
#14 0x00006225258c0208 in open_and_process_table (thd=0x7ec23c001050, lex=0x7ec23c004630, tables=0x7ec23c1a05e0, counter=0x7ec23c004688, prelocking_strategy=0x7ec32c5fc2f8,
    has_prelocking_list=false, ot_ctx=0x7ec32c5fc270) at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:5090
#15 0x00006225258c1d82 in open_tables (thd=0x7ec23c001050, start=0x7ec32c5fc2e0, counter=0x7ec23c004688, flags=0, prelocking_strategy=0x7ec32c5fc2f8)
    at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:5912
#16 0x00006225258c39df in open_tables_for_query (thd=0x7ec23c001050, tables=0x7ec23c2cb1c8, flags=0) at /home/yym/mysql8/mysql-8.1.0/sql/sql_base.cc:6795
#17 0x0000622525a7f0c3 in Sql_cmd_dml::prepare (this=0x7ec23c19d728, thd=0x7ec23c001050) at /home/yym/mysql8/mysql-8.1.0/sql/sql_select.cc:540
#18 0x0000622525a7fbb8 in Sql_cmd_dml::execute (this=0x7ec23c19d728, thd=0x7ec23c001050) at /home/yym/mysql8/mysql-8.1.0/sql/sql_select.cc:718
#19 0x0000622525a99285 in Sql_cmd_show::execute (this=0x7ec23c19d728, thd=0x7ec23c001050) at /home/yym/mysql8/mysql-8.1.0/sql/sql_show.cc:213
#20 0x00006225259f2841 in mysql_execute_command (thd=0x7ec23c001050, first_level=true) at /home/yym/mysql8/mysql-8.1.0/sql/sql_parse.cc:4797
#21 0x00006225259f4cb3 in dispatch_sql_command (thd=0x7ec23c001050, parser_state=0x7ec32c5fd9f0) at /home/yym/mysql8/mysql-8.1.0/sql/sql_parse.cc:5447
#22 0x00006225259ea0d7 in dispatch_command (thd=0x7ec23c001050, com_data=0x7ec32c5fe340, command=COM_QUERY) at /home/yym/mysql8/mysql-8.1.0/sql/sql_parse.cc:2112
#23 0x00006225259e7f77 in do_command (thd=0x7ec23c001050) at /home/yym/mysql8/mysql-8.1.0/sql/sql_parse.cc:1459
#24 0x0000622525c3f835 in handle_connection (arg=0x62255f6a4f40) at /home/yym/mysql8/mysql-8.1.0/sql/conn_handler/connection_handler_per_thread.cc:303
#25 0x0000622527b7ebdc in pfs_spawn_thread (arg=0x62255f694920) at /home/yym/mysql8/mysql-8.1.0/storage/perfschema/pfs.cc:3043
#26 0x00007ec33a694ac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#27 0x00007ec33a726850 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81