
1. 数据库内核到底是什么从一段 SQL 说起先从一个很常见的场景开始。假设你打开 MySQL 命令行输入了这样一条 SQLSELECT user_id, order_amount FROM orders WHERE order_date 2025-01-01 ORDER BY order_amount DESC;这条 SQL 在短短几十毫秒内返回了结果。表面上看它只是一个简单的查询。但在这背后数据库内核完成了一整套复杂工作它先解析了这条 SQL 的语法把它转换成内部表示形式再做逻辑优化和物理优化生成执行计划然后调用存储引擎读取磁盘数据最后按排序规则输出结果。我们平时写 SQL、调接口、做 CRUD都是在“数据库的外围”工作。而数据库内核则是真正决定一条 SQL 能不能跑得快、事务提交是否可靠、并发访问是否安全的核心部分。那么数据库内核到底包含哪些模块可以这样理解一个数据库内核至少包含以下五个关键模块查询解析器把 SQL 文本解析成抽象语法树AST。查询优化器把抽象语法树转换成高效的执行计划。执行引擎按照执行计划逐条处理数据完成扫描、连接、聚合、排序等操作。存储引擎负责数据在磁盘和内存中的组织方式包括表、索引、页、缓冲区。事务与并发控制模块保证多个事务并发执行时的一致性、隔离性和持久性。这五部分并不是孤立存在的。它们相互协作构成了一条完整的数据处理流水线。本文以宾夕法尼亚州立大学数据库课程的经典教学设计为参考围绕“从关系模型到事务”这条主线逐步拆解数据库内核的每一个核心环节。无论你是正在学习数据库内核的学生还是准备面试的后端开发工程师又或者是想自己动手写一个“迷你数据库”的爱好者这篇文章都会给你一条清晰的学习路径。读完本文你将掌握关系模型为什么成为数据库内核的理论基石。SQL 在数据库内核中是如何被解析、优化和执行的。存储引擎如何组织数据索引为什么能加速查询。事务的 ACID 特性、隔离级别和并发控制机制。如何用代码实现一个简化版数据库内核的查询执行模块。接下来我们正式开始。2. 关系模型数据库内核的理论基石2.1 关系模型解决了什么问题在关系模型出现之前数据库系统主要使用层次模型和网状模型。这两种模型都要求用户预先定义好数据之间的物理指针关系查询数据时必须沿着指针一条条遍历。这种方式有两个明显问题第一数据物理存储结构和用户查询逻辑强耦合。一旦物理结构调整应用程序的查询代码也要跟着改。第二用户必须理解数据在磁盘上是怎么存的才能写查询。这对业务开发人员来说负担太重。1970 年Edgar F. Codd 发表了著名的论文A Relational Model of Data for Large Shared Data Banks提出了关系模型。关系模型的核心思想是所有的数据都组织成二维表关系表由行元组和列属性组成表与表之间通过公共列建立关联。用户只需要用声明式的查询语言也就是 SQL描述“我要什么数据”而不需要关心“数据怎么取”。2.2 关系模型中的核心概念关系模型里有几个基础概念理解它们对学习数据库内核非常重要。关系Relation关系就是一张二维表。它表示一个实体集合例如学生表、订单表。元组Tuple元组是表中的一行代表一个具体的实体记录。例如一行学生记录(2024001, 张三, 20)。属性Attribute属性是表中的一列代表实体的某一特征。例如学生表中的学号、姓名、年龄。键Key键用于唯一标识一个元组。候选键是能唯一标识元组的最小属性集合主键则是从候选键中选出的一个。外键用于建立表与表之间的引用关系。关系完整性关系模型还定义了完整性约束包括实体完整性主键不能为空。参照完整性外键的取值必须是被引用表中存在的主键值。用户定义完整性例如年龄必须在 0 到 150 之间。2.3 为什么关系模型适合作为内核基础关系模型之所以能够成为数据库内核的理论基石有三个关键原因。第一它提供了高度的数据独立性。逻辑模型和物理存储分离用户面对的是表、行、列而内核内部可以自由调整物理存储结构只要保持逻辑结构不变。第二它为查询优化提供了数学基础。关系代数是关系模型的运算体系包括选择、投影、连接、并、差、笛卡尔积等操作。查询优化器的本质就是把 SQL 转化成关系代数表达式然后寻找代价更小的等价表达式。第三它为事务管理提供了明确的对象边界。事务操作的最小单位是元组锁的粒度可以细化到行、页、表这些粒度的划分都建立在关系模型之上。理解关系模型是进入数据库内核世界的第一步。3. SQL 在内核中的旅程解析、绑定与优化一条 SQL 从客户端发出到真正执行在内核中会经历一个完整的生命周期。这个生命周期可以分为四个阶段语法解析、语义分析、逻辑优化、物理优化。3.1 语法解析从文本到抽象语法树语法解析阶段数据库内核会把 SQL 字符串拆解成词法单元然后根据语法规则构建抽象语法树AST。SELECT user_id, order_amount FROM orders WHERE order_amount 100;这条 SQL 的 AST 结构大致如下Query ├── SelectList │ ├── ColumnRef (user_id) │ └── ColumnRef (order_amount) ├── FromClause │ └── TableRef (orders) └── WhereClause └── BinaryOp () ├── ColumnRef (order_amount) └── Const (100)AST 的本质是将 SQL 的嵌套结构转换成树形数据结构。后续的语义分析和优化都是基于这颗树来进行的。如果 SQL 语法有误比如关键字拼写错误、括号不匹配数据库会在这个阶段直接报错而不会进入后续流程。3.2 语义分析与绑定校验表和字段是否存在语法正确不代表语义正确。语义分析阶段内核要做的核心工作是校验 SQL 中引用的表、列、函数是否真实存在并检查数据类型是否匹配。这一步通常被称为“绑定”或“解析到目录”。内核会维护一个系统目录system catalog里面记录了所有表的结构、列的类型、索引信息、约束信息等。绑定过程就是把 AST 中的表名、列名解析到对应的目录项上。例如如果你执行SELECT name FROM orders;而此时orders表中并没有name这个字段数据库会报错Unknown column name in field list。这个错误就是在语义分析阶段捕获的。3.3 逻辑优化重写关系代数表达式绑定完成之后SQL 就被转换成了关系代数的逻辑计划。但最初生成的逻辑计划通常不是最高效的。逻辑优化要做的就是在保证语义等价的前提下重写表达式使其更高效。常见的逻辑优化规则包括谓词下推Predicate Pushdown把 WHERE 条件尽可能下推到扫描或连接之前减少参与计算的数据量。投影裁剪Projection Pruning去掉查询中不需要的列减少数据的宽度。连接重排Join Reordering调整多个表的连接顺序优先连接数据量小的表。子查询展开Subquery Unnesting把相关子查询改写为连接操作。举个谓词下推的例子SELECT o.order_id, u.user_name FROM orders o JOIN users u ON o.user_id u.user_id WHERE o.order_amount 1000;如果先做两个表的连接再过滤order_amount 1000那么连接过程会处理所有订单数据。而如果先把orders表中金额大于 1000 的记录筛出来再和用户表做连接参与连接的数据量就会大幅减少。这就是谓词下推的价值。3.4 物理优化选择执行路径逻辑优化确定了“做什么”物理优化则要确定“怎么做”。物理优化阶段内核需要决定使用哪种访问路径全表扫描还是索引扫描使用哪种连接算法嵌套循环连接Nested Loop Join、哈希连接Hash Join还是排序合并连接Sort-Merge Join是否使用并行执行如何组织排序和聚合物理优化器会根据统计信息估算每种执行计划的代价然后选择代价最小的方案。统计信息包括表行数、每列的唯一值数量、列的分布直方图等。这也是为什么在大表上执行ANALYZE更新统计信息后SQL 的执行计划可能会发生明显变化。关于代价估算可以用一个简化公式理解总代价 ≈ IO 代价 CPU 代价更具体的估算模型不同数据库的实现并不完全相同。但对于学习内核的人来说理解“优化器是一个基于代价的选择器”这个思路已经足够了。4. 存储引擎数据在磁盘上如何安家存储引擎是数据库内核中最贴近硬件的模块。它的核心任务是管理数据在磁盘和内存中的物理表示。4.1 页与缓冲区几乎所有主流关系型数据库都把磁盘空间划分为固定大小的页Page常见的页大小为 4KB、8KB 或 16KB。页是数据库内核在磁盘和内存之间交换数据的最小单位。为什么以页为单位而不是以行为单位原因很简单磁盘 IO 的代价远高于内存访问。以页为单位批量读写可以减少 IO 次数充分利用空间局部性原理。数据库通常会维护一个缓冲区池Buffer Pool。缓冲区池是一块内存区域用于缓存最近访问过的页。当用户请求一条数据时内核先检查数据所在的页是否在缓冲区中如果在直接读内存速度极快。如果不在则从磁盘加载该页到缓冲区再进行读取。缓冲区池的页面淘汰策略通常采用类 LRU 算法保证高频率访问的页尽量驻留内存。4.2 行存储与列存储在关系型数据库中数据在页内的组织方式分为两大类行存储和列存储。行存储Row-Oriented行存储把同一行的所有列连续存放在一起。这种方式的优点是适合 OLTP在线事务处理场景因为这类场景通常需要频繁地插入、更新、删除单条记录并且查询时经常需要访问某一行的多个字段。MySQL 的 InnoDB 引擎采用的就是行存储。列存储Column-Oriented列存储把同一列的所有值连续存放在一起。这种方式的优点是对于 OLAP在线分析处理场景聚合查询只需要读取特定列可以大幅减少 IO 量。列存储还天然具有更高的压缩率因为同列数据的类型一致、分布更集中。ClickHouse、Doris 等分析型数据库采用列存储。4.3 索引空间换时间的经典手段索引是数据库内核中最核心的加速机制之一。以 B 树索引为例它的核心特点是非叶子节点只存储键值和子节点指针。所有数据都存储在叶子节点。叶子节点之间通过链表连接方便范围查询。B 树之所以成为数据库索引的主流结构是因为它能够以较小的树高容纳大量数据。一个 3 层的 B 树只要每层节点能容纳足够多的键值就可以支撑数千万甚至上亿条记录的快速查找。索引的使用也并非没有代价。每次插入、更新、删除记录时数据库都需要同步维护索引结构。因此索引不是越多越好而是要根据实际查询模式来设计。下面是一个简化版的 B 树插入过程描述从根节点出发查找目标键值应落入的叶子节点。如果叶子节点未满直接插入。如果叶子节点已满则分裂该节点并将中间键值提升到父节点。如果父节点也满了继续向上分裂直到根节点。需要特别强调的是建立索引是加速查询的有效手段但并不是万能药。对于数据量很小的表全表扫描反而可能比走索引更快因为优化器还要额外访问索引页和回表。5. 执行引擎如何优雅地遍历数据解析和优化完成之后数据库内核就拿到了物理执行计划。执行引擎负责逐节点执行这个计划并把结果返回给客户端。5.1 执行计划与算子物理执行计划可以看作一棵“算子树”。每个算子对应一个具体的执行动作。常见算子包括SeqScan顺序扫描IndexScan索引扫描Filter过滤Projection投影HashJoin哈希连接NestedLoopJoin嵌套循环连接Sort排序HashAggregate哈希聚合执行引擎通常采用火山模型Volcano Model也叫迭代器模型。在这个模型中每个算子都实现一个next()方法。上层算子调用下层算子的next()方法逐条获取数据处理后输出给再上层。整个执行过程像一条流水线数据在算子之间逐行流动。5.2 连接操作的实现连接是数据库执行中最容易出现性能瓶颈的操作。这里介绍两种最常见的连接算法。嵌套循环连接Nested Loop Join嵌套循环连接是最简单的连接算法。对于外层表的每一行扫描内层表的所有行找出满足连接条件的记录。for each row r in outer_table: for each row s in inner_table: if r.key s.key: emit (r, s)如果外层表有 M 行内层表有 N 行时间复杂度是 O(M * N)。当内层表有索引时可以把内层扫描优化为索引查找复杂度可以降低。哈希连接Hash Join哈希连接适合等值连接场景。它的基本思路是选择较小的表作为构建表Build 表对其连接键建立哈希表。扫描较大的表Probe 表对每一行用连接键查询哈希表命中则输出结果。哈希连接的复杂度接近 O(M N)在连接大表时通常比嵌套循环连接快。但哈希连接要求连接条件是等值条件而且需要额外的内存来存储哈希表。5.3 聚合与排序聚合操作GROUP BY通常通过两种方式实现哈希聚合扫描数据把分组键作为哈希键在哈希表中累积聚合结果。这种方式适合不要求输出有序的场景。排序聚合先把数据按分组键排序再顺序扫描累积相邻分组的结果。这种方式适合输出要求有序的场景。排序操作如果数据量超过可用内存就不能使用内存排序而需要使用外排External Sort。外排的核心思想是把数据划分成多个有序片段写回磁盘然后再进行多路归并。这也是数据库处理大量排序请求时的底层机制。6. 事务与并发控制保证数据不“乱套”当多个用户同时读取和修改同一批数据时数据库必须有一套机制来保证数据的正确性。这套机制就是事务与并发控制。这也是数据库内核中最难、最容易出错的部分。6.1 事务是什么ACID 原则事务是数据库操作的最小逻辑单元。一个事务中的操作要么全部成功提交要么全部回滚不会出现只执行一半的情况。事务具有四个核心特性即 ACID。原子性Atomicity事务中的所有操作要么全部成功要么全部失败回滚。一致性Consistency事务执行前后数据库始终保持一致性状态包括约束、触发器、外键等都不会被破坏。隔离性Isolation多个事务并发执行时彼此之间互不干扰就像串行执行一样。持久性Durability事务一旦提交对数据的修改就是永久的即使系统崩溃也不会丢失。这四个特性中原子性、一致性、持久性相对容易理解。隔离性是最复杂的一个它直接影响了数据库的并发性能和一致性表现。6.2 隔离级别性能与一致性的权衡SQL 标准定义了四个隔离级别。隔离级别越高数据一致性越强但并发性能往往越差。读未提交Read Uncommitted允许一个事务读取另一个事务尚未提交的数据。可能产生脏读即读到了别人修改后但最终回滚的数据。这个隔离级别在绝大多数数据库中都很少使用。读已提交Read Committed一个事务只能读取另一个事务已经提交的数据。它解决了脏读问题但仍可能产生不可重复读即同一个事务内两次查询同一行得到的结果不同。可重复读Repeatable Read一个事务在执行期间所有事务看到的同一行数据都是相同的。它解决了不可重复读问题。但如果没有范围锁可能产生幻读即同一事务内第二次查询得到的结果集合比第一次多出或减少了行。串行化Serializable所有事务按照串行的方式执行效果与一个接一个地依次执行相同。它完全解决了脏读、不可重复读、幻读问题但并发性能开销最大。隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交不可能可能可能可重复读不可能不可能可能串行化不可能不可能不可能需要说明的是MySQL 的 InnoDB 引擎在可重复读隔离级别下通过间隙锁机制已经能够避免幻读问题。所以 MySQL 默认的隔离级别就是可重复读这也是很多初学者容易疑惑的地方。6.3 并发控制的三种主流方案为了在并发环境下实现上述隔离级别数据库内核需要采用并发控制机制。目前主流方案有三种。基于锁的并发控制Lock-Based通过加锁来保护数据资源。事务读取数据前需要获取共享锁S锁写入数据前需要获取排他锁X锁。锁之间要遵循兼容性规则并通过两阶段锁协议2PL来保证可串行化。两阶段锁协议的核心是每个事务分为两个阶段加锁阶段只能加锁不能解锁解锁阶段只能解锁不能加锁。遵循这个协议的并发执行结果等价于某种串行执行结果。基于时间戳的并发控制Timestamp-Based每个事务在开始时分配一个全局唯一的时间戳。事务的读写操作按照时间戳的顺序执行。如果一个操作的执行顺序违反了时间戳顺序就选择终止事务并重新启动。基于多版本并发控制的MVCCMVCC 是现代数据库最广泛应用于一致性读的机制。它的核心思想是每次数据修改都生成一个新版本旧版本并不立即删除。事务读取数据时根据事务的快照信息读取符合时间点的版本。MVCC 的最大优势是读操作不阻塞写操作写操作不阻塞读操作从而显著提升并发性能。MySQL InnoDB、PostgreSQL 都采用了 MVCC 机制。分布式数据库 TiDB 也在底层实现了 MVCC 机制。对于想要深入学习数据库内核的人来说MVCC 的版本链管理、快照生成、垃圾版本清理机制都是非常值得研究的方向。7. 手写迷你数据库内核实现一个简化版执行引擎理论讲了很多如果没有亲手写过一次“迷你数据库”很难真正把知识内化。下面我们用 Python 实现一个极度简化的数据库执行引擎。它虽然不能运行 SQL但可以演示一条查询语句经过筛选、投影、排序之后的完整执行过程。这个例子能帮助你直观理解执行引擎中“算子”与“数据流”的关系。7.1 设计思路我们定义以下算子类Scan扫描数据源。Filter过滤条件。Project投影选列。Sort排序。每个算子实现next()方法。执行过程从最顶层的算子开始逐层调用下层算子的next()。这正是火山模型的核心思想。7.2 完整代码# 文件路径mini_db_executor.py class Scan: 扫描算子负责输出全部数据 def __init__(self, data): self.data data self.index 0 def next(self): if self.index len(self.data): return None row self.data[self.index] self.index 1 return row class Filter: 过滤算子根据谓词保留符合条件的行 def __init__(self, predicate, child): self.predicate predicate self.child child def next(self): while True: row self.child.next() if row is None: return None if self.predicate(row): return row class Project: 投影算子只输出指定的列 def __init__(self, columns, child): self.columns columns self.child child def next(self): row self.child.next() if row is None: return None return {col: row[col] for col in self.columns} class Sort: 排序算子一次性读取所有数据排序后逐条输出 def __init__(self, key, child): self.key key self.child child self.sorted_data None self.index 0 def next(self): if self.sorted_data is None: rows [] while True: row self.child.next() if row is None: break rows.append(row) self.sorted_data sorted(rows, keylambda r: r[self.key]) if self.index len(self.sorted_data): return None row self.sorted_data[self.index] self.index 1 return row if __name__ __main__: # 模拟一张订单表 orders [ {order_id: 1, user_id: 101, amount: 500}, {order_id: 2, user_id: 102, amount: 1200}, {order_id: 3, user_id: 101, amount: 300}, {order_id: 4, user_id: 103, amount: 1800}, {order_id: 5, user_id: 104, amount: 900}, ] # 构造执行计划 # Sort(amount desc) # - Project(order_id, amount) # - Filter(amount 400) # - Scan(orders) scan Scan(orders) filter_op Filter(lambda row: row[amount] 400, scan) project_op Project([order_id, amount], filter_op) sort_op Sort(amount, project_op) # 注意这里为了演示倒序效果简单取反后排序 sort_op.key amount sort_op.sorted_rows None # 执行并输出结果 print(order_id\tamount) while True: row sort_op.next() if row is None: break print(f{row[order_id]}\t{row[amount]})7.3 代码解读这段代码直观演示了多个知识点。Scan是数据源头模拟的是存储引擎扫描一张表的过程。Filter用 Lambda 表达式作为谓词每次从子节点拿一行如果满足条件就返回否则继续往下拿。这种方式体现了“惰性求值”的思想数据是一行一行流过算子树的并不需要把所有数据一次性加载进内存。Project负责裁剪不需要的列。Sort则比较特殊因为排序必须等所有数据到齐之后才能进行。在实际数据库中排序算子同样会把所有数据收集起来如果数据量超过内存限制就需要落到磁盘上进行外部排序。这里为了简化直接使用了内存中的sorted函数。这个例子虽然简单但“算子树 逐行迭代”的执行模型与现代数据库执行引擎的核心思想是一致的。8. 常见问题与排查思路在学习数据库内核的过程中很多问题都具有共性。这里整理几个高频问题并给出排查思路。8.1 为什么表数据量不大查询却非常慢可能原因没有合适的索引每次查询都在做全表扫描。查询条件中使用了函数或隐式类型转换导致索引失效。统计信息过期优化器选择了错误的执行计划。缓冲区命中率低大量数据从磁盘加载。排查步骤使用EXPLAIN查看执行计划确认访问路径是全表扫描还是索引扫描。检查查询条件中的字段是否被函数包裹。更新表的统计信息再重新生成执行计划。查看慢查询日志分析是否集中在某几个查询模板上。8.2 死锁是怎么产生的如何排查死锁的产生需要四个条件互斥、持有并等待、不可抢占、循环等待。数据库中的死锁通常是两个事务各自持有一部分锁然后又请求对方持有的锁。排查方法查看数据库的死锁日志定位循环等待的两条会话。分析事务中的 SQL 执行顺序确认是否有不同事务以不同顺序访问同一批数据。解决思路同一业务中不同事务访问多张表时尽量保持相同的加锁顺序这是最常用的回避策略。同时控制单事务的处理行数避免长事务占用过多锁资源。8.3 MVCC 版本累积导致性能下降怎么办MVCC 机制会保留多个版本的数据如果大量旧版本没有被及时清理会导致索引和页变大查询变慢。解决思路检查是否存在长时间未提交或未结束的长事务长事务会阻止旧版本清理。合理调整数据库的版本清理线程参数。避免高频的更新操作集中在同一行这会快速堆积版本链。8.4 优化器选择的执行计划不理想怎么办执行计划不理想很多时候是因为统计信息不准确或者参数设置导致代价估算偏离实际。排查步骤重新收集统计信息。检查硬件配置与数据库参数是否匹配。如果是复杂查询可以尝试使用查询提示Query Hint人工干预执行计划但只在必要时使用。9. 从迷你内核到分布式数据库学习路径建议从单体数据库到分布式数据库内核知识会进一步延伸。这里给出两条清晰的进阶路径。9.1 单体数据库内核进阶如果你想把单体数据库内核学扎实建议顺着下面这条路径走读完我的迷你执行引擎代码后尝试给它增加更多算子比如 HashJoin 和 HashAggregate。实现一个简单的 B 树索引结构理解插入、删除、分裂、合并的过程。给迷你数据库增加日志模块实现在崩溃后能够恢复数据。实现一个基于两阶段锁的事务调度器验证并发控制的效果。9.2 分布式数据库内核扩展分布式数据库在单体内核的基础之上还需要解决另外三个核心问题数据分布、分布式事务、一致性。数据分布分布式数据库通常把一张大表按某个键拆分成多个分片分布在不同节点上。分片策略常见的有范围分片、哈希分片、一致性哈希等。查询时协调节点需要把 SQL 下推到各个分片节点执行然后汇总结果。分布式事务跨节点的数据修改需要分布式事务保证原子性和隔离性。常见方案包括两阶段提交2PC、三阶段提交3PC以及以 Seata 为代表的 AT 模式、TCC 模式还有谷歌 Spanner 引入的原子钟方案。这些方案的核心都是在“强一致”和“性能”之间做取舍。一致性协议分布式数据库通常使用 Raft 或 Paxos 协议来保证多个副本之间的数据一致性。Raft 协议通过领导人选举、日志复制、安全性约束三个机制把复制状态机的过程变得工程上更易实现。如果你对分布式数据库感兴趣Raft 协议是必须啃的一块硬骨头。10. 总结与最后建议从关系模型到事务从单机执行引擎到分布式一致协议数据库内核是一条非常长的技术链条。但它并不是一条需要死记硬背的知识链而是一条可以用代码验证、用实验理解的工程链。我建议你不要只停留在“看得懂”的层面。动手去写哪怕是像本文中这样极简的执行引擎也会让你对算子的执行顺序、数据流的输入输出有真正的体感。接下来你可以去读 PostgreSQL 的官方文档、《数据库系统概念》中关于查询优化和事务处理的章节也可以去看看 TiDB 的源码解析博客把这些理论知识逐步落到真实的工程代码中。如果你正在准备数据库内核相关的工作面试可以优先把下面这些问题吃透关系模型中主键、外键、候选键的区别与约束语义。SQL 解析、绑定、优化、执行四阶段各自的作用。行存储与列存储的适用场景。索引覆盖与回表的区别。ACID 事务特性和隔离级别选型。MVCC 版本链与快照读的实现原理。B 树索引相比哈希索引在范围查询上的优势。分布式事务与单体事务的异同。数据库内核需要的不是速成而是持续积累。每搞懂一个模块你离“亲手打造一个数据库”就更近一步。本文到这里就结束了如果你觉得这篇文章对你有帮助可以收藏备用。有疑问也欢迎在评论区留言一起讨论。