标签 InnoDB 下的文章

作者:Prabakaran Thirumalai,MySQL 服务器运行时咨询成员技术人员。

原文:https://blogs.oracle.com/mysql/no-more-hidden-changes-how-mys...,Jan 30, 2026

爱可生开源社区翻译,本文约 2700 字,预计阅读需要 9 分钟。

640 (87).webp

MySQL 通过重新思考外键约束和级联的管理方式,迈出了重要一步。MySQL 9.6 开始,外键检查和级联操作将由 SQL 引擎 直接处理,而非 InnoDB 存储引擎。这一改进解决了长期存在的变更跟踪、二进制日志复制和数据一致性方面的挑战,使 MySQL 在异构环境、变更数据捕获(CDC)管道和分析工作负载方面更加稳健。

1. InnoDB 中外键的先前工作方式

历史上,MySQL 在存储引擎层(特别是 InnoDB 数据库)强制执行外键约束和级联。其工作原理如下:

  • 外键级联:当对父表执行 DELETE 或 UPDATE 等语句时,InnoDB 会检查外键约束。如果定义了级联操作(例如 ON DELETE CASCADE ),InnoDB 会处理子表中相应行的更新或删除操作。
  • InnoDB 内部执行:所有级联操作均由 InnoDB 内部执行。SQL 引擎仅发起父级操作;所有对子表的依赖操作均由 InnoDB 管理。

    重要的是,这些子行更改对 SQL 层是不可见的。因此,在基于行的复制 (RBR) 模式下,InnoDB 内部执行的级联操作不会出现在 MySQL 二进制日志中。

  • 运行影响:由于这些变更对 SQL 引擎和二进制日志隐藏,下游系统(例如 CDC 管道和分析平台)可能无法检测到这些变更。这可能导致数据不一致、分析结果不可靠以及复制问题。

基于 InnoDB 的外键的局限性

随着 MySQL 部署规模和复杂性的增长,这种传统方法暴露出以下局限性:

  • 隐藏的数据更改:在 InnoDB 内部执行的级联父子更改对 SQL 层是不可见的,并且没有在更高级别上被捕获。
  • 系统日志不完整:二进制日志中经常缺少子行更改,导致复制和审计不完整。
  • 数据捕获差距:依赖二进制日志或完整变更历史记录的数据工具和下游系统无法始终跟踪与外键相关的每个更新或删除。
  • 复制风险: 在复杂的复制设置中,这些静默的更改可能会导致主服务器和副本之间的数据出现差异,从而导致操作上的挑战。

2. 新模型:SQL 引擎管理的外键强制执行

为了解决这些问题,MySQL 现在强制执行外键,并在 SQL 引擎内部管理级联操作。通过这项更改,父表和子表上的所有外键操作对 SQL 层都是完全可见的。

640 (88).webp

主要优势:

  • 完整日志记录:所有更改(包括级联更改)现在都可见、可审计,并完整记录在二进制日志中。
  • 可靠的复制:不再有隐藏的数据更改;复制现在更加值得信赖和准确。
  • 更佳的分析:数据采集和分析工具现在可以获得所有数据变化的完整、实时视图。
  • 创新基础:这种架构使得跨存储引擎扩展外键支持以及未来的复制和可观测性功能变得更加容易。

注意:对于除 InnoDB 之外的其他支持外键的存储引擎,强制执行和级联操作仍由相应的存储引擎管理。

性能比较

我们理解,对于考虑将外键强制执行机制从 InnoDB 迁移到 SQL 引擎的 MySQL 用户而言,性能是首要考虑因素。针对常见事务工作负载的大量基准测试证实,基于 SQL 引擎的外键强制执行和级联机制的性能与 InnoDB 方法 几乎完全相同。外键检查和级联的成本基本保持不变,因此 吞吐量和延迟方面没有出现任何可观察到的下降。 这使得即使在高吞吐量和关键任务部署中,采用新的实现方案也是安全的。

向后兼容性

SQL 引擎的外键强制执行和级联机制旨在 完全向后兼容,保留 InnoDB 外键强制执行的语义和行为。虽然整体用户体验保持不变,但仍有一些值得注意的改进和细微的行为差异:

  • 错误信息:虽然错误代码与以前的版本一致,但由于检查执行顺序不同,具体的错误信息文本(包括外键名称)可能会有所不同。
  • 自增间隙:如果外键约束失败,任何尝试插入操作都会增加自增计数器,这可能会导致值出现间隙,符合 MySQL 的标准行为。
  • 针对级联行更新统计信息:行级统计信息(例如 delete_rows )已更新,以包含受级联外键操作影响的行。这确保系统统计信息能够准确反映外键强制执行所执行的所有数据更改。
  • 更严格的排序规则验证:如果外键级联跨越不兼容的排序规则,则会引发显式错误,防止出现 静默数据问题,并提高用户的数据完整性。

3. 安全采用并内置备用方案

为了实现可控的升级,MySQL 引入了一个只读的启动变量 innodb_native_foreign_keys。这提供了平滑的升级路径,并最大限度地减少了版本过渡期间的意外变更。默认情况下,此变量设置为 FALSE ,这意味着默认行为是基于 SQL 引擎的外键强制执行 。在测试环境或早期生产部署期间,您可以将此变量设置为 TRUE ,以暂时恢复到 InnoDB 的原生外键处理方式。这在验证新的 SQL 引擎行为时提供了一个清晰的操作回退方案。

注意: 此系统变量旨在帮助简化迁移,随着 MySQL 社区全面采用基于 SQL 引擎的外键,该变量将在未来的版本中移除。

4. 总结:为什么这项改变至关重要?

通过将外键强制执行移至 SQL 引擎,MySQL 弥补了长期存在的架构缺陷。这一改进确保数据变更始终可见、被记录和被复制,使 MySQL 成为更强大的平台,适用于现代化的分布式合规数据环境。

总的来说,对于 MySQL 用户而言,这意味着更好的数据一致性、更可靠的复制,以及在分析和合规工作流程中更少的意外情况,而不会牺牲性能。

作者:马金友, 一名给 MySQL 找 bug 的初级 DBA。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文约 1500 字,预计阅读需要 5 分钟。

如果你注意到在 MySQL 中 ORDER BY DESC 查询比 ORDER BY ASC 稍微慢一些,不用担心 —— 这是已知且符合预期的行为。

这是因为 InnoDB 的设计和优化是为了进行正向扫描,它使用单向链表结构来组织页面上的记录。

因此,向前移动(ASC)的时间复杂度是 O(1),而向后移动(DESC)的时间复杂度是 O(n)

这篇博客将从存储层面的角度演示这两种算法。

1. InnoDB 页面结构

1.1 单向链表

InnoDB 使用单向链表来组织record。 每个页面有两个虚拟record:infimumsupremum,它们分别作为链表的头部尾部
一旦数据页面包含用户记录,链表就会按逻辑顺序显示。

infimum -> rec1 -> rec2 -> rec3 -> rec4 -> ... -> supremum

1.2 REC_NEXT

每条记录在记录头中额外占用 2 个字节(byte)来存储指向下一条记录的偏移量。

constexpr uint32_t REC_NEXT = 2;
constexpr uint32_t REC_NEXT_MASK = 0xFFFFUL;

例如,infimum 记录的 REC_NEXT 值是 0x00, 0x0d

/** The page infimum and supremum of an empty page in ROW_FORMAT=COMPACT */
static const byte infimum_supremum_compact[] = {
    /* the infimum record */
    0x01 /*n_owned=1*/, 0x00, 0x02 /* heap_no=0, REC_STATUS_INFIMUM */, 0x00,
    0x0d /* pointer to supremum */, 'i', 'n', 'f', 'i', 'm', 'u', 'm', 0,
    /* the supremum record */
    0x01 /*n_owned=1*/, 0x00, 0x0b /* heap_no=1, REC_STATUS_SUPREMUM */, 0x00,
    0x00 /* end of record list */, 's', 'u', 'p', 'r', 'e', 'm', 'u', 'm'};

通过 infimum 记录偏移 0x000d,可以得到 supremum 记录。

In [1]: infimum_supremum_compact = [
   ...:     0x01 , 0x00, 0x02 , 0x00,
   ...:     0x0d , 'i', 'n', 'f', 'i', 'm', 'u', 'm', 0,
   ...:     0x01 , 0x00, 0x0b, 0x00,
   ...:     0x00, 's', 'u', 'p', 'r', 'e', 'm', 'u', 'm'
   ...: ]
   ...:

In [2]: infimum_supremum_compact[5]
Out[2]: 'i'

In [3]: infimum_supremum_compact[5+0x000d]
Out[3]: 's'

1.3 页面目录 (Page Directory)

由于单向链表的数据结构,InnoDB 必须扫描整个链表才能找到一条 record,这效率很低。

InnoDB 在每个数据页的末尾维护一个动态数组(page directory),数组中的每个元素(槽/slot)存储一条record的位置。

/* We define a slot in the page directory as two bytes */
constexpr uint32_t PAGE_DIR_SLOT_SIZE = 2;

它不是存储每条记录的地址,而是每个槽指向该槽所管理记录中的最后一条记录。一个槽通常管理 4 到 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. */
constexpr uint32_t PAGE_DIR_SLOT_MAX_N_OWNED = 8;
constexpr uint32_t PAGE_DIR_SLOT_MIN_N_OWNED = 4;

第一个槽总是指向 infimum,最后一个槽总是指向 supremum

1.4 N_OWNED

每条记录在记录头中占用 4 个位(bit)来存储 N_OWNED

constexpr uint32_t REC_NEW_N_OWNED = 5; /* This is single byte bit-field */
constexpr uint32_t REC_N_OWNED_MASK = 0xFUL;

如果记录是槽中的最后一条记录,它的值就是该槽拥有的记录数。否则,值为 0

2. 示例

下图展示了数据页面的布局

微信图片_20260120101037_46_176.jpg

  • 橙色箭头连接了从 rec0rec23 的 24 条用户记录。
  • 灰色箭头指向槽所管理的最后一条记录。
    0 指向 infimum,它包含 1 条记录。
    n 指向 supremum,它包含 5 条记录。
    1 指向 rec3,它包含 4 条记录。

3. 算法

我们将使用以下逻辑 InnoDB 页面布局来理解这两种扫描算法。

微信图片_20260120101032_45_176.jpg

3.1 正向扫描 (Forward Scan)

rec10 找到页面上的下一条记录很容易。

  1. 读取 REC_NEXT 偏移量
field_value = mach_read_from_2(rec - REC_NEXT);
  1. 获取下一条记录的位置
return (ut_align_offset(rec + field_value, UNIV_PAGE_SIZE));

3.2 反向扫描 (Backward Scan)

rec10找到页面上的前一条记录会更困难。

3.2.1 查找哪个槽管理了当前记录 (page_dir_find_owner_slot)

① 扫描从当前记录开始的所有记录,直到 n_owned 不为 0

while (rec_get_n_owned_new(r) == 0) {
      r = rec_get_next_ptr_const(r, true);
      ...
    }

它会检查 rec10,然后是 rec11。

[rec10] --> [rec11]
  ^

  
[rec10] --> [rec11]
              ^

因为 rec11 的 n_owned 是 4,所以会跳转到步骤 1.2。

② 检查所有槽,直到找到指向步骤 1.1 中记录 r 的槽。

rec_offs_bytes = mach_encode_2(r - page);

  while (UNIV_LIKELY(*(uint16 *)slot != rec_offs_bytes)) {
  ....
    slot += PAGE_DIR_SLOT_SIZE;
  }
  
  return (((ulint)(first_slot - slot)) / PAGE_DIR_SLOT_SIZE);

它会从最后一个槽(slot n)开始扫描到 slot 0。

[n]...[4][3][2][1][0]
 ^

因为 slot n 指向 supremum(不是 rec11),所以会检查下一个槽(slot 4)。

[n]...[4][3][2][1][0]
       ^

因为 slot 4 指向 rec15(不是 rec11),所以会检查下一个槽(slot 3)。

[n]...[4][3][2][1][0]
          ^

因为 slot 3 指向 rec11,所以会返回 3。

3.2.2 扫描当前slot group 以查找前一条记录

① 跳转到前一个槽。 因为 slot 3 只持有slot group的最后一条记录,它无法扫描 slot 3 中的所有记录。

slot = page_dir_get_nth_slot(page, slot_no - 1);

  rec2 = page_dir_slot_get_rec(slt);

幸运的是,它可以利用一个槽组的最后一条记录来扫描当前槽组中的所有record。

通过检查 slot 2,它会找到 rec7。

② 扫描槽组中的所有记录所有 record 匹配当前 record。

while (rec != rec2) {
      prev_rec = rec2;
      rec2 = page_rec_get_next_low(rec2, true);
    }
    
    return (prev_rec);

它会检查 rec7、rec8、rec9,然后是 rec10,直到找到 rec10 的前一条 record,即 rec9。

[rec7] --> [rec8] --> [rec9] --> [rec10] --> [rec11]
  ^
[rec7] --> [rec8] --> [rec9] --> [rec10] --> [rec11]
             ^
[rec7] --> [rec8] --> [rec9] --> [rec10] --> [rec11]
                        ^
[rec7] --> [rec8] --> [rec9] --> [rec10] --> [rec11]
                                   ^

4. 时间复杂度

正向扫描是 O(1),但反向扫描是 O(n),其中 n 是页面目录中的槽数。

5. 基准测试

5.1 正向扫描 (Forward scan)

mysql > select k from sbtest1 order by k asc limit 9999999, 1;
+---------+
| k       |
+---------+
| 8670945 |
+---------+
1 row in set (1.41 sec)

mysql > desc select k from sbtest1 order by k asc limit 9999999, 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: sbtest1
   partitions: NULL
         type: index
possible_keys: NULL
          key: k_1
      key_len: 4
          ref: NULL
         rows: 9864216
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

5.2 反向扫描 (Backward scan)

mysql > select k from sbtest1 order by k desc limit 9999999, 1;
+---------+
| k       |
+---------+
| 1184614 |
+---------+
1 row in set (2.01 sec)

mysql > desc select k from sbtest1 order by k desc limit 9999999, 1\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: sbtest1
   partitions: NULL
         type: index
possible_keys: NULL
          key: k_1
      key_len: 4
          ref: NULL
         rows: 9864216
     filtered: 100.00
        Extra: Backward index scan; Using index
1 row in set, 1 warning (0.00 sec)

References

作者:ba0tiao
编者按:
在AI浪潮席卷全球的今天,有人认为传统关系型数据库已走向黄昏,MySQL 的生命力正在被边缘化。但事实真的如此吗?AliSQL,作为 MySQL 的重要分支,自2010年诞生以来,始终默默支撑着阿里巴巴集团核心业务的高并发、高可用需求。它从未消失,只是沉寂太久。
2026年,AliSQL社区的一帮开发者们,开始为AliSQL注入创新的血液!这是他们的第一篇,系统阐述了MySQL深度融合DuckDB的重大技术实践。这不仅是对“MySQL 只擅长 TP”这一行业共识的突破性回应,更是一次兼具工程魄力与架构远见的创新——在保持 MySQL 协议、语法、运维体系完全兼容的前提下,以轻量、高效、零侵入的方式,为MySQL 注入了 OLAP 能力。
国内首场《2026 AliSQL Innovate 用户大会暨 AliSQL DuckDB 开源发布会》将于2月3日在杭州开启!
席位有限,快来报名吧https://page.aliyun.com/form/act1162737496/index.htm

MySQL的插件式存储引擎架构

MySQL的核心创新之一就是其插件式存储引擎架构(Pluggable Storage Engine Architecture),这种架构使得MySQL可以通过多种不同的存储引擎来扩展自己的能力,从而支持更多的业务场景。MySQL的插件式架构如下图所示:
图片
MySQL的插件式存储引擎架构可以划分为四个主要的部分:

  • 运行层(Runtime Layer):负责MySQL运行相关的任务,比如通讯、访问控制、系统配置、监控等信息。
  • Binlog层(Binlog Layer): 负责Binlog的生成、复制和应用。
  • SQL层(SQL Layer):复制SQL的解析、优化和SQL的执行。
  • 存储引擎层(Storage Engine Layer):负责数据的存储和访问。
    MySQL在SQL计算和数据存储之间设计了一套标准的数据访问控制接口(Plugable Engine Interface),SQL层通过这个标准的接口进行数据的更新、查询和管理,存储引擎得以作为独立组件实现“热插拔”式集成。
    目前MySQL中常用的存储引擎包括:
  • MyISAM:MySQL最早使用的引擎,因为不支持事务已经被InnoDB取代。但是一直到MySQL-5.7还是系统表的存储引擎。
  • InnoDB:MySQL的默认引擎。因期对事务的支持以及优秀的性能表现,逐步替代MyISAM成为MySQL最广泛使用的引擎。
  • CSV: CSV文件引擎,MySQL慢日志和General Log的存储引擎。
  • Memory:内存表存储引擎,也可作为SQL执行时内部临时表的存储引擎。
  • TempTable:MySQL-8.0引入的引擎,用于存储内部临时表。
    InnoDB作为引擎引入到MySQL,是MySQL插件式引擎架构的一个非常重要的里程碑。在互联网发展的初期,MyISAM因其简单高效的访问赢得了互联网业务的青睐,和Linux、Apach、PHP一起被称为LAMP架构。
    随着电商、社交互联网的兴起,MyIASAM的短板越来越明显。InnoDB因其对事务ACID的支持、在并发访问和性能上的优势,大大的拓展了MySQL的能力。在InnoDB的加持下,MySQL成为最流行的开源OLTP数据库。随着MySQL的广泛使用,我们看到有越来越多基于TP数据的分析型查询。InnoDB的架构是天然为OLTP设计,虽然在TP业务场景下能够有非常优秀的性能表现。但InnoDB在分析型业务场景下的查询效率非常的低。这大大的限制了MySQL的使用场景。时至今日,MySQL一直欠缺一个分析型查询引擎。DuckDB的出现让我们看到了一种可能性。

    DuckDB简介

    DuckDB 是一个开源的在线分析处理(OLAP)和数据分析工作负载而设计。因其轻量、高性能、零配置和易集成的特性,正在迅速成为数据科学、BI 工具和嵌入式分析场景中的热门选择。DuckDB主要有以下几个特点:

  • 卓越的查询性能:单机DuckDB的性能不但远高于InnoDB,甚至比ClickHouse和SelectDB的性能更好。
  • 优秀的压缩比:DuckDB采用列式存储,根据类型自动选择合适的压缩算法,具有非常高的压缩率。
  • 嵌入式设计:DuckDB是一个嵌入式的数据库系统,天然的适合被集成到MySQL中。
  • 插件化设计:DuckDB采用了插件式的设计,非常方便进行第三方的开发和功能扩展。
  • 友好的License:DuckDB的License允许任何形式的使用DuckDB的源代码,包括商业行为。
    基于以上的几个原因,我们认为DuckDB非常适合成为MySQL的AP存储引擎。因此我们将DuckDB集成到了AliSQL中。
    图片
    DuckDB引擎的定位是实现轻量级的单机分析能力,目前基于DuckDB引擎的RDS MySQL DuckDB只读实例已经上线,欢迎试用。未来我们还会上线主备高可用的RDS MySQL DuckDB主实例,用户可以通过DTS等工具将异构数据汇聚到RDS MySQL DuckDB实例,实现数据的分析查询。RDS MySQL DuckDB只读实例的架构
    图片
    DuckDB分析只读实例,采用读写分离的架构。分析型业务和主库业务分离,互不影响。和普通只读实例一样,通过Binlog复制机制从主库复制数据。DuckDB分析只读节点有以下优势:
  • 高性能分析查询:基于DuckDB的查询能力,分析型查询性能相比InnoDB提升高达200倍(详见性能部分)。
  • 存储成本低:基于DuckDB的高压缩率,DuckDB只读实例的存储空间通常只有主库存储空间的20%。
  • 100% 兼容MySQL语法,免去学习成本。DuckDB作为引擎集成到MySQL中,因此用户查询仍然使用MySQL语法,没有任何学习成本。
  • 无额外管理成本:DuckDB只读实例仍然是RDS MySQL实例,相比普通只读实例仅仅增加了一些MySQL参数。因此DuckDB和普通RDS MySQL实例一样管理、运维、监控。监控信息、慢日志、审计日志、RDS API等无任何差异。
  • 一键创建DuckDB只读实例,数据自动从InnoDB转成DuckDB,无额外操作。DuckDB 引擎的实现
    图片
    DuckDB只读实例使用上可以分为查询链路和Binlog复制链路。查询链路接受用户的查询请求,执行数据查询。Binlog复制链路连接到主实例进行Binlog复制。下面会分别从这两方面介绍其技术原理。

    查询链路

    图片
    查询执行流程如上图所示。InnoDB仅用来保存元数据和系统信息,如账号、配置等。所有的用户数据都存在DuckDB引擎中,InnoDB仅用来保存元数据和系统信息,如账号、配置等。
    用户通过MySQL客户端连接到实例。查询到达后,MySQL首先进行解析和必要的处理。然后将SQL发送到DuckDB引擎执行。DuckDB执行完成后,将结果返回到Server层,server层将结果集转换成MySQL的结果集返回给客户。
    查询链路最重要的工作就是兼容性的工作。DuckDB和MySQL的数据类型基本上是兼容的,但在语法和函数的支持上都和MySQL有比较大的差异,为此我们扩展了DuckDB的语法解析器,使其兼容MySQL特有的语法;重写了大量的DuckDB函数并新增了大量的MySQL函数,让常见的MySQL函数都可以准确运行。自动化兼容性测试平台大约17万SQL测试,显示兼容率达到99%。

    Binlog复制链路

    图片

    幂等回放

    由于DuckDB不支持两阶段提交,因此无法利用两阶段提交来保证Binlog GTID和数据之间的一致性,也无法保证DDL操作中InnoDB的元数据和DuckDB的一致性。因此我们对事务提交的过程和Binlog的回放过程进行了改造,从而保证实例异常宕机重启后的数据一致性。

    DML回放优化

    由于DuckDB本身的实现上,有利于大事务的执行。频繁小事务的执行效率非常低,会导致严重的复制延迟。因此我们对Binlog回放做了优化,采用攒批(Batch)的方式进行事务重放。优化后可以达到30万行/s的回放能力。在Sysbench压力测试中,能够做到没有复制延迟,比InnoDB的回放性能还高。
    图片

    并行Copy DDL

    MySQL中的一少部分DDL比如修改列顺序等,DuckDB不支持。为了保证复制的正常进行,我们实现了Copy DDL机制。DuckDB原生支持的DDL,采用Inplace/Instant的方式执行。当碰到DuckDB不支持的DDL时,会采用Copy DDL的方式创建一个新表替换原表。
    图片

Copy DDL采用多线程并行执行,执行时间缩短7倍。
图片

DuckDB只读实例的性能

测试环境ECS 实例 32Cpu、128G内存、ESSD PL1云盘 500GB
测试类型TPC-H SF100
图片

结语

通过将DuckDB深度集成到AliSQL中,我们成功打造了兼具高性能与高兼容性的MySQL分析型实例。这一创新不仅弥补了MySQL长期以来在OLAP场景下的能力短板,也开创了一种全新的“HTAP轻量化”实现路径——无需复杂的分布式架构,即可实现强大的实时分析能力。
DuckDB引擎的引入,使得用户可以在不改变现有应用架构的前提下,轻松获得高达200倍的分析查询性能提升。更重要的是,用户可以使用MySQL协议、沿用熟悉的SQL语法、无需学习新工具、无需改造应用程序。一键创建、自动同步、无缝切换,真正做到了“分析能力即服务”。

未来已来,创新不止。我们将持续拓展 AliSQL DuckDB 引擎的能力边界,赋能更高效、更智能的数据处理新体验。
2026年2月3日(星期二)13:30–16:30,2026 AliSQL Innovate 用户大会 暨 AliSQL DuckDB 开发者线下活动 将在杭州盛大启幕!
以“Innovate”之名,我们重启 MySQL 生态的无限可能——重启 · 再创 · 向新而生
这是一场属于开发者的技术盛宴,一次思想碰撞与技术共创的深度交流。诚邀广大开发者、技术爱好者与行业伙伴齐聚杭州,共同见证 AliSQL 的进化之路,携手探索数据库的未来方向。
席位有限,立即扫码报名,锁定你的专属席位!我们在杭州,等你共赴创新之约!
图片