文章目录
- [1. 连接层](#1. 连接层)
- [2. 服务层](#2. 服务层)
-
- [2.1 SQL接口](#2.1 SQL接口)
- [2.2 解析器](#2.2 解析器)
- [2.3 优化器](#2.3 优化器)
- [2.4 执行器](#2.4 执行器)
- [3. 存储引擎层](#3. 存储引擎层)
- [4. 系统文件层](#4. 系统文件层)
1. 连接层
功能:处理客户端连接、身份认证和线程管理
连接池:管理并复用客户端连接,避免频繁创建和销毁线程的开销
核心参数:max_connections用于控制最大并发连接数
数据流转:
- 结构体 :本质是个连接上下文管理器,贯穿于连接的生命周期,后续使用
st_mysql代指- 封装信息:当前连接信息
- 结构体 :
st_mysql
- SQL语句:接收SQL字符串语句,并传递到后续流程
- 结构体伪代码:
C
// 代表一个数据库连接句柄的结构体定义
typedef struct st_mysql {
// 连接信息(主机、用户、密码、数据库等)
char *host, *user, *passwd, *unix_socket, *server_version, *host_info, *info, *db;
unsigned int port, client_flag, server_capabilities;
unsigned int protocol_version;
unsigned int field_count; // 字段数量
st_thd thd; // 服务器为当前连接分配的唯一线程
my_ulonglong affected_rows; // 受影响的行数
my_ulonglong insert_id; // 最后插入的AUTO_INCREMENT ID
enum mysql_status status; // 当前连接状态
MYSQL_FIELD *fields; // 字段信息数组
MEM_ROOT field_alloc; // 用于内存分配
my_bool free_me; // 关闭时是否需要释放
my_bool reconnect; // 是否自动重连
} MYSQL;
2. 服务层
功能:MySQL的大脑,负责SQL的解析、优化和执行
核心组件:
- SQL接口:服务层的调度者,接收到SQL后调用具体组件完成执行并组装结果返回
- 解析器:对SQL进行词法分析和语法分析,生成解析树
- 优化器:根据解析树使用采用基于成本的优化模型(CBO),选择最优的执行计划
- 执行器:根据执行计划,调用存储引擎提供的API接口来执行具体的数据操作
- 查询缓存:不推荐使用,MySQL 8.0已移除
- Binlog:记录修改数据的原始SQL或行变更,主要用于主从复制、按时间点恢复和数据审计
2.1 SQL接口
功能:SQL执行过程的调度中心
- 输入 :
st_mysql和SQL语句 - 输出 :
st_mysql_res结构体
THD:
- 定义:线程描述符
- 初始化 :连接层为当前连接分配的唯一线程
thd - 作用:解析器、优化器和执行器的执行线程,包含了一个语句所需的全部上下文
执行伪代码:
C
// 关键数据结构声明(简化)
// 连接句柄,包含连接参数、网络数据等
typedef struct st_mysql MYSQL;
// 线程描述符,核心上下文,贯穿整个SQL执行周期
typedef struct st_thd THD;
// 解析树,由解析器生成
typedef class st_lex LEX;
// 执行计划,由优化器生成
typedef class Query_plan Query_plan;
// SQL接口(SQL Interface):服务层入口
// 输入:客户端连接句柄和原始SQL字符串
// 输出:向客户端发送结果集或状态信息
void sql_interface_main(MYSQL *mysql_conn, const char *sql_string) {
// 从连接句柄中获取对应的线程描述符
THD *thd = mysql_conn->thd;
// 关键操作:将原始SQL字符串存入THD,后续组件都从THD中获取
thd->set_query(sql_string);
// 阶段1:调用解析器(Parser)生成解析树(LEX)
if (parse_sql(thd) != 0) {
// 从thd中获取错误信息
send_error(mysql_conn, thd->get_error());
return;
}
// 输出:解析树已存入thd->lex
LEX *lex_tree = thd->lex;
// 阶段2:调用优化器(Optimizer)生成执行计划(Query_plan)
if (optimize_query(thd, lex_tree) != 0) {
send_error(mysql_conn, thd->get_error());
return;
}
// 输出:执行计划已存入thd->query_plan
Query_plan *execution_plan = thd->query_plan;
// 阶段3:调用执行器(Executor)执行计划
if (execute_query(thd, execution_plan) != 0) {
send_error(mysql_conn, thd->get_error());
return;
}
// 执行结果(结果集或影响行数)也存储在THD中
send_result_to_client(mysql_conn, thd->get_result());
}
// 1. 解析器(Parser):语法分析,产出解析树(LEX)
// 输入:thd (内含SQL字符串)
// 输出:LEX解析树
int parse_sql(THD *thd) {
// 从THD中取出SQL字符串
const char *sql_text = thd->query().str;
// 核心操作:生成LEX解析树,并挂载到THD上
thd->lex = new LEX(sql_text);
if (thd->lex == NULL) return 1;
return 0;
}
// 2. 优化器(Optimizer):基于解析树生成最优执行计划
// 输入:thd, lex_tree;
// 输出:Query_plan
int optimize_query(THD *thd, LEX *lex_tree) {
// 核心操作:生成执行计划,并挂载到THD上
thd->query_plan = new Query_plan(lex_tree);
if (thd->query_plan == NULL) return 1;
return 0;
}
// 3. 执行器(Executor):根据计划调用存储引擎接口
// 输入:thd, execution_plan
// 输出:结果集存入THD
int execute_query(THD *thd, Query_plan *plan) {
// 核心操作:与存储引擎交互,将结果置回THD
MYSQL_RES *result_set = storage_engine_fetch_data(plan);
// 使用where条件过滤并处理limit和offset参数
evaluate_where_conditions_and_limit(result_set, plan);
thd->set_result(result_set);
return 0;
}
2.2 解析器
功能:将SQL文本转换为内部可理解执行的解析树(语法树)
- 输入:SQL字符串
- 输出:解析树结构体
执行流程:
- 词法分析 :
- 分词:将完整的SQL语句拆分为独立的单词
- 标注:识别单词的类型和值,过滤掉多余的空格和注释
- 语法分析 :
- 校验:检查单词组合是否符合SQL语法规范
- 构建树:校验通过,使用单词生成一颗解析树
示例分析:
- SQL语句:
SQL
SELECT u.name,a.value FROM users u
left join address a on u.user_id = a.address_id
WHERE u.id = 1;
- 解析树伪结构:
C
// 表示整个SELECT查询的框架(SELECT_LEX),树根节点
typedef struct SELECT_LEX {
SQL_I_List<TABLE_LIST> table_list; // FROM子句中的表链表:users和address
List<Item> item_list; // SELECT项列表:u.name, a.value
Item *where_condition; // WHERE条件表达式树:u.id = 1
} SELECT_LEX;
// 表示一个表引用(TABLE_LIST)的核心字段
typedef struct TABLE_LIST {
char *table_name; // 真实表名:users和address
char *alias; // 表别名:u和a
int join_type; // 连接类型:LEFT JOIN对应值
Item *join_condition; // ON条件表达式树:u.user_id = a.address_id
} TABLE_LIST;
// 表示一个列引用
typedef struct Item_ident : public Item {
Item_type type; // 表达式类型,如FIELD_ITEM
char *db_name; // 数据库名
char *table_name; // 表名/别名
char *field_name; // 字段名
} Item_ident;
// 表示一个二元操作(如 =, >)的条件表达式
typedef struct Item_cond : public Item {
Item_type type; // 表达式类型,如COND_ITEM
Item_func::Functype functype; // 函数类型,如EQ_FUNC
Item *left; // 左操作数,通常是一个Item_ident
Item *right; // 右操作数,可能是一个Item_ident或常量
} Item_cond;
2.3 优化器
实现MySQL高性能查询的关键,绝大多数SQL优化都是为了适应优化器的优化策略
功能 :为解析树中的每个表生成一个高效且可执行的执行计划
- 输入:解析树
- 输出:执行计划
优化策略:
- 逻辑优化 :基于关系代数原理,对解析树进行等价变换,会重写SQL
- 子查询优化 :逻辑优化中最复杂的部分,会将效率低下的子查询转换为高效的连接操作
- 转为连接 :
- 原语句:a.col1 in (select col1 from b)
- 重写后:a inner join b on a.col1=b.col1
- 半连接:对于exists和in子查询,使用半连接策略:只关系外表记录在内表中是否存在匹配,而不关心匹配次数
- 物化:若子查询结果很大,会将其结果存入临时表,再进行连接查询
- 转为连接 :
- 外连接消除 :若从表条件过滤后数据已经是非空,则转为内连接
- 条件化简 :
- 常量传递 :条件中有常量等值关系则进行传递
- 原语句:a = 5 and b > a
- 重写后:a = 5 and b > 5
- 移除无用条件:恒真和恒假条件会被移除或简化,如1 = 1或5 != 5
- 等价谓词重写 :为了更好利用索引,优化器会重写谓词
- 原语句:a like 'ABC%'
- 重写后:a >= 'ABC'and a < 'ABD'
- 常量传递 :条件中有常量等值关系则进行传递
- 子查询优化 :逻辑优化中最复杂的部分,会将效率低下的子查询转换为高效的连接操作
- 物理优化 :基于代价的优化,使用数据库的统计信息选择性能开销最小的操作,不会重写SQL
- 单表扫描选择 :评估不同访问路径的代价
- 覆盖索引扫描:如果查询的所有列都在某个索引中,代价最低
- 索引扫描:where条件有索引且数据量不多,使用索引扫描,代价中等
- 全表扫描:当需要访问大部分数据或没有索引时使用,代价最高
- 多表连接选择 :物理优化中最复杂的部分,根据代价估算调整表查询顺序
- 连接算法 :代价由小到大分别为:
- INLJ:索引嵌套循环连接,会使用索引,type一定不是ALL,extra=Using index,性能极佳
- BNLJ:块嵌套循环连接,缺少索引使用,type大概率是ALL,extra=Using join buffer(Block Nested Loop)
- Hash Join:哈希连接,大表无索引使用,8.0.18引入,type可能是ALL,extra=Using hash join
- 代价估算:总代价 = IO代价 + CPU代价,考虑因素有页面数和记录数
- 优化策略:使用贪婪算法等启发式方法,寻找较优解
- 连接算法 :代价由小到大分别为:
- 排序选择 :
- 索引排序:排序列顺序和索引顺序保持一致,type=index/range/ref
- 物理排序:使用内存或磁盘排序,单路排序开销小于双路排序,extra=Using filesort
- 分组选择 :分组选择前会进行排序选择
- 松散索引扫描:统计列满足最左前缀索引,通常使用min/max函数,性能最佳,extra=Using index for group-by
- 紧凑索引扫描:扫描索引一个范围进行分组聚合,extra=Using index
- 临时表分组:创建临时表来完成分组聚合,extra=Using temporary
- 单表扫描选择 :评估不同访问路径的代价
执行计划关键属性:
- explain属性 :执行计划的关键指标和结论
- type :基于代价模型选择的数据检索方式,代价由小到大为:
- const/rq_ref:通过主键/唯一索引定位单行
- ref:非唯一索引等值查找,可能返回多行
- range:索引范围扫描(如between和in)
- index:全索引扫描
- ALL:全表扫描
- key :使用代价最低的索引
- 索引名:使用索引定位数据
- null:使用全表扫描,索引选择性差或数据量小
- rows:基于统计信息预估的扫描行数,越小越好
- extra :优化器采用的特定优化措施或瓶颈,代价由小到大为:
- Using index:使用覆盖索引
- Using index condition:索引条件下推,子句包含复合索引列
- Using where:条件判断常见,需配合type判定性能开销
- Using join buffer:表连接时缺少索引,需优化
- Using temporary:使用临时表,常见于group/order by,需优化
- Using filesort:文件排序,常见于group by、distinct或union查询,需优化
- ref :多表连接时使用的匹配条件
- const:使用常量等值匹配
- 列名:使用嵌套循环连接匹配
- possible_keys:考虑到会使用,但不一定会使用的索引
- type :基于代价模型选择的数据检索方式,代价由小到大为:
- 内部操作指令 :
- 访问指令:数据的访问方式
- 索引指令:指定使用哪些索引辅助查询
- 表指令:查询哪些表,顺序是什么
- 连接指令:表和表之间的连接查询方式
- 过滤条件:过滤条件如何应用
- 分组排序指令:列数据如何排序及聚合
SQL语句优化思路:
- 第一指标 :type字段,数据访问方式
- ALL :
- 说明:全表扫描,效率最低下,需要警惕或优化
- 优化方向:结合后续指标创建合适索引
- index :
- 说明:扫描了索引,但效率不高,未能有效筛选数据
- 优化方向 :
- 索引范围扫描:优化查询,使其能使用索引进行范围扫描(range);
- 覆盖索引扫描:查询列都在索引中(覆盖索引),此时extra=Using index
- range/ref:关注后续指标,若后续指标不理想,需要优化索引选择性
- ALL :
- 第二指标 :rows字段,预估扫描行数
- 说明:代表访问实际成本,若type符合第一指标且rows值很大,则需优化
- 优化方向:根据后续指标进行对应优化
- 第三指标 :extra字段,提供了查询执行的细节信息
- Using filesort :
- 说明:通常因为order/group by没有合适索引
- 优化方向 :
- 排序分组索引:为order/group by的列创建索引
- 复合索引:若where子句也有条件,使用复合索引,并符合最左前缀原则
- Using temporary :
- 说明:常见于复杂的group by、distinct或union操作,性能开销较大
- 优化方向 :
- 分组索引:为group by列创建索引
- 重写查询:使用union all代替or条件
- Using where :
- 说明:存储引擎返回结果后使用了where子句条件过滤,type=ALL/index需优化
- 优化方向:索引使用不充分,考虑创建符合索引并符合最左前缀原则
- Using filesort :
- 第四指标 :key和possible_keys字段,说明了索引使用情况
- 说明:使用索引的成本可能高于全表扫描
- 优化方向:创建覆盖索引提供索引使用率
使用索引重要机制:
- 最左前缀原则 :使用复合索引 时必须满足下面条件
- 查询条件必须从复合索引的最左边列开始
- 不能跳过中间的列才能最高效使用索引查询
- 除了末尾列可使用范围查询,其它列不可使用范围查询
- 覆盖索引:效率非常高,查询字段和where子句列都在复合索引中可实现,但需考虑索引维护成本
- 复合索引顺序优化 :
- 5.6及之前:根据复合索引顺序优化等值条件判断顺序
- 5.7:引入索引条件下推(ICP),有范围查询也支持高效过滤
- 5.8:跳跃扫描,即使跳过了最左侧列,也能根据后序列有限使用复合索引
- 索引条件下推(ICP):属于复合索引但无法直接用索引查找的条件,直接在索引上进行初步过滤,再去存储引擎回表查询
- 二级索引回表 :二级索引只有主键+索引列,若通过该索引查询非索引列,存储引擎按索引列->主键->非索引列的路径查询,主键到非索引列的过程为回表
2.4 执行器
功能:使用执行计划中的指令集完成从存储引擎获取数据并完成复杂处理和流程控制
- 输入:执行计划
- 输出:结果集
读取执行流程:
- 获取驱动表:使用驱动表作为首层遍历对象
- 查询表数据 :根据驱动表执行计划查询数据行
- 覆盖索引:存储引擎根据索引查询,效率最高
- 二级索引:从存储引擎根据二级索引获取主键,再使用主键回表查询
- 索引下推:若where子句有索引,在存储引擎使用索引初步过滤查询,减少回表
- 全表扫描:依次获取全表数据行再使用where子句过滤
- 多表连接查询 :每得到驱动表一行数据立即用它去连接下一个表,再生成连接后的中间结果,并使用中间结果作为驱动表进入下次连接查询
- 索引嵌套循环连接:存储引擎根据条件列的索引连接
- 块嵌套循环连接:无索引,数据分块装入内存,批量查询数据连接
- Hash Join:无索引,小表数据生成哈希表,被驱动表数据和哈希表做查询连接
- 排序 :当所有表做完查询后再做排序
- 索引有序一致:存储引擎使用索引顺序返回数据
- 顺序不一致:在执行器内使用内存/磁盘文件排序
- 分组 :排序后再做分组,有时排序分组可同时完成
- 覆盖索引:存储引擎根据索引查询
- 临时表:申请一块内存作为临时表来完成分组
- 分页 :所有处理完成后在执行器内的最终控制
- offset :跳过前offset条已获取的数据,跳过的前offset条依然会做遍历查询排序
- limit:在跳过offset条数据的前提下,再获取limit条数据行
结果集伪结构:
C
// 结果集结构 - 存储查询返回的完整结果
typedef struct st_mysql_res {
my_ulonglong row_count; // 结果集行数
unsigned int field_count; // 字段数量
MYSQL_FIELD *fields; // 字段元数据数组
MYSQL_DATA *data; // 实际结果数据
MYSQL_ROWS *data_cursor; // 数据游标
MEM_ROOT field_alloc; // 内存池
MYSQL_ROW current_row; // 当前行
} MYSQL_RES;
// 单行数据 - 表示结果集中的一行
typedef char **MYSQL_ROW; // 字符串数组,每个元素代表一列的值
// 字段元数据 - 描述结果集中列的属性
typedef struct st_mysql_field {
char *name; // 字段名
char *table; // 所属表名
enum enum_field_types type; // 字段数据类型
unsigned int length; // 字段定义长度
unsigned int flags; // 字段标志(如主键、非空等)
} MYSQL_FIELD;
修改执行流程:
- 打开表:根据执行计划获取表的元信息,并调用存储引擎打开表
- 定位数据:根据where子句查询定位到需要修改的数据行
- 执行修改:执行器完成数据行逻辑修改,并调用存储引擎接口进行数据实际修改
- 一阶段完成通知:存储引擎完成一阶段后通知执行器
- 记录Binlog Buffer:将SQL语句或行变更写入Binlog Buffer
- 持久化Binlog:根据配置从Binlog Buffer读取并持久化到Binlog文件,最后通知存储引擎进行二阶段提交
- 返回结果:根据存储引擎执行结果进行处理并返回给客户端
3. 存储引擎层
服务层通过预定义的处理器接口和各种存储引擎交互,存储引擎负责数据的实际存储和提取
功能:定义和管理数据的逻辑结构、访问方法以及保证数据安全性
主要职责:
- 数据与索引的组织 :将数据和索引存储在页中,默认为16KB每页,页通过B+树索引组织
- 缓存管理 :弥补磁盘和CPU处理速度,涉及了缓冲池
- 读取:执行器要读取的数据页,会先被加载到缓冲池
- 修改:在缓冲池中进行,再由后台线程刷盘
- 事务与恢复保障 :
- 事务:用于回滚或为事务隔离级别提供一致性读
- 崩溃恢复:崩溃后可恢复之前的事务
核心组件:
- 缓冲池(Buffer Pool) :
- 核心目标:加速访问
- 补充:内存中用来缓存磁盘数据的区域,减少磁盘IO
- 读操作:首先检查需要的数据是否在内存中,若缓存命中直接返回;未命中则从磁盘加载数据到内存中再返回
- 写操作:直接修改缓冲池的数据页,该页标记为脏页,再延迟刷盘
- 日志缓冲区(Log Buffer) :
- 核心目标:保证持久性与崩溃恢复
- 补充:作为重做日志(Redo Log)的内存缓冲区,侧重写性能
- 写操作 :所有数据修改会暂存于此,事务提交时,根据配置
innodb_flush_log_at_trx_commit决定刷盘策略
- 重做日志(Redo Log) :
- 核心目标:崩溃恢复
- 补充:记录数据页的物理变化,侧重数据安全,保证已提交事务不会丢失
- 写操作:事务提交时,重做日志被持久化到磁盘成功后才算事务已提交
- 回滚日志(Undo Log) :
- 核心目标:保证事务原子性和多版本控制
- 补充:记录数据修改前的旧版本,侧重并发控制和事务回滚
- 读操作:实现一致性读,为查询提供数据的旧版本,确保特定隔离级别可读到一致性视图,避免读写阻塞
- 写操作:数据修改前,记录旧数据到回滚日志
- 更改缓冲区(Change Buffer) :
- 核心目标 :优化非唯一二级索引的写入
- 补充:侧重写性能,减少随机IO
- 写操作:修改非唯一二级索引且目标页不在缓冲池中,修改操作会缓存于此,等未来索引页被读取时再合并修改,提升性能
- 双写缓冲区(Doublewrite Buffer) :
- 核心目标:防止数据页只有部分写入
- 补充:侧重数据页的完整性
- 写操作:在脏页刷盘到实际表空间文件前,先完整写入到双写缓冲区,再写入到真正的位置。如果写入中断,可以从双写缓冲区恢复完整页,防止数据损坏
修改操作处理流程:
- 执行器调用 :
- 处理方:服务层的执行器
- 处理内容:执行器根据执行计划调用更新接口
- 记录Undo Log :
- 处理方:存储引擎
- 处理内容 :先把此行数据修改前的状态写入Undo Log,用于可能的回滚和MVCC,Undo Log的修改会写入Redo Log
- 修改内存数据 :
- 处理方:存储引擎
- 处理内容:在Buffer Pool中定位到对应的数据页并修改,标记页为脏页,若没有此页则从磁盘读取此页到Buffer Pool
- 写入Redo Log :
- 处理方:存储引擎
- 处理内容:将本次数据修改了什么写入Log Buffer,状态标记为PREPARE,并通知服务层一阶段提交成功
- 写入Binlog Buffer :
- 处理方:服务层的执行器
- 处理内容:执行器收到一阶段提交成功通知后,将本次逻辑层面的修改记录到Binlog Buffer
- Binlog刷盘 :
- 处理方:服务层的执行器
- 处理内容 :执行器根据
sync_binlog配置将Binlog Buffer中的内容写入磁盘,成功后通知存储引擎进行事务二阶段提交
- 事务提交与刷盘 :
- 处理方:存储引擎
- 处理内容:存储引擎事务提交时,确保Log Buffer中该事务被刷到了Redo Log磁盘文件中,状态标记为COMMIT
- 返回成功 :
- 处理方:存储引擎
- 处理内容:Redo Log刷盘成功代表事务成功,向服务层返回成功,服务层向客户端返回成功
- 后台异步刷脏页 :
- 处理方:存储引擎
- 处理内容 :Buffer Pool中的脏页由后台线程异步读取并刷入到磁盘的表空间文件(.ibd文件)
- 双写保护 :
- 处理方:存储引擎
- 处理内容:脏页写入表空间文件前,先将其写入双写缓冲区,最终写入表空间文件
读取操作处理流程:
- 执行器调用:服务层的执行器根据执行计划调用查询接口
- 检查Buffer Pool:存储引擎首先在Buffer Pool查找所需的数据页
- 缓存命中:若页在内存中,直接读取
- 缓存未命中:若页不在内存中,从磁盘的**表空间文件(.idb文件)**中加载到Buffer Pool再读取
- MVCC可见性判断:根据当前事务的隔离级别,若需要会从Undo Log日志中构建数据的历史版本,实现非锁定读
索引差异:
- 主键索引:也称为聚簇索引,B+树叶子节点的数据页含有所有列
- 二级索引:B+树叶子节点的
4. 系统文件层
功能:将所有逻辑数据、索引和日志,以文件的形式安全高效的持久化到物理磁盘上
文件信息:
- 日志文件(Log Files) :
- 重做日志(Redo Log):保证事务持久性,用于崩溃恢复。记录对数据页的物理修改
- 回滚日志(Undo Log):实现事务回滚和多版本并发控制 (MVCC),存储数据修改前的旧版本
- 二进制日志(Binlog):用于主从复制和数据恢复。记录引起数据变更的逻辑操作
- 错误日志、慢查询日志等:记录数据库运行状态、错误信息和性能诊断信息
- 数据文件(Data Files) :
- 表结构文件(.frm):每张表对应一个.frm文件,保存表的结构定义
- 独享表空间文件(.ibd) :推荐,使用独享表空间时,每张表的数据和索引存储在自己的.ibd文件中
- 共享表空间文件(.ibdata):使用共享表空间时,多个表的数据和索引共存于一个或多个.ibdata文件中
- 其它文件 :
- 配置文件(my.cnf/my.ini):存储MySQL服务器的所有配置参数
- 进程文件(pid file):记录MySQL服务器的进程ID