外观
SQLite 架构与工作原理
了解 SQLite 的内部架构和工作原理对于 DBA 优化数据库性能、排查问题和制定最佳实践至关重要。本文将详细介绍 SQLite 的架构设计、核心组件和工作原理。
SQLite 架构设计
整体架构
SQLite 采用分层架构设计,从上到下分为以下几个主要层次:
| 层次 | 主要功能 | 核心组件 |
|---|---|---|
| 接口层 | 提供编程接口 | C API、SQL 编译器 |
| 编译层 | SQL 语句处理 | 词法分析器、语法分析器、代码生成器 |
| 核心层 | 数据库核心功能 | B 树管理器、页管理器、事务管理器 |
| 存储层 | 磁盘 I/O 操作 | OS 接口、VFS(虚拟文件系统) |
架构特点
- 模块化设计:各层之间通过清晰的接口交互,便于维护和扩展
- 可移植性:通过 VFS 抽象层,实现跨平台支持
- 高效性:核心操作直接访问磁盘,减少中间层开销
- 可靠性:采用事务机制和 WAL 模式,确保数据完整性
核心组件详解
接口层
C API
- 功能:提供 SQLite 的核心编程接口
- 主要函数:
sqlite3_open():打开数据库连接sqlite3_exec():执行 SQL 语句sqlite3_prepare():准备 SQL 语句sqlite3_step():执行准备好的 SQL 语句sqlite3_finalize():释放资源
- 特点:简单易用,支持多种编程语言绑定
SQL 编译器
- 功能:将 SQL 语句转换为可执行的字节码
- 组成部分:
- 词法分析器:将 SQL 语句分解为标记(tokens)
- 语法分析器:检查 SQL 语句的语法正确性,生成语法树
- 语义分析器:检查 SQL 语句的语义正确性
- 代码生成器:将语法树转换为字节码
编译层
词法分析器
- 功能:将 SQL 语句分解为标记(tokens)
- 示例:将
SELECT * FROM users WHERE id = 1;分解为SELECT、*、FROM、users、WHERE、id、=、1、;等标记 - 实现:使用有限状态机实现
语法分析器
- 功能:根据 SQL 语法规则,检查标记序列的语法正确性,生成语法树
- 实现:使用 LALR(1) 语法分析器生成器(如 yacc)生成
- 输出:抽象语法树(AST)
代码生成器
- 功能:将语法树转换为 SQLite 虚拟机(VM)可执行的字节码
- 优化:对字节码进行优化,提高执行效率
- 输出:SQLite 虚拟机字节码
核心层
SQLite 虚拟机(VM)
- 功能:执行字节码指令
- 特点:
- 基于栈的虚拟机设计
- 支持事务处理
- 高效的内存管理
- 主要操作:
- 数据读取和写入
- 索引操作
- 事务管理
B 树管理器
- 功能:管理数据库的索引和表数据
- 数据结构:
- 表 B 树:存储表数据
- 索引 B 树:存储索引数据
- 特点:
- 平衡树结构,确保查询效率
- 支持范围查询和排序
- 高效的插入、删除和更新操作
页管理器
- 功能:管理数据库文件的页分配和缓存
- 页大小:默认 4KB,可在创建数据库时指定(512B-64KB)
- 缓存管理:
- 页缓存:减少磁盘 I/O 操作
- 缓存替换策略:LRU(最近最少使用)
事务管理器
- 功能:管理数据库事务,确保 ACID 特性
- 核心组件:
- 锁管理器:管理数据库锁,确保并发访问安全
- 日志管理器:管理事务日志,支持崩溃恢复
- 事务状态:
- 开始(BEGIN)
- 进行中(ACTIVE)
- 提交(COMMIT)
- 回滚(ROLLBACK)
锁管理器
- 功能:管理数据库的锁,确保并发访问安全
- 锁类型:
- 共享锁(Shared Lock):允许读取操作,阻止写入操作
- 预留锁(Reserved Lock):表示准备写入,允许其他读取操作
- 排他锁(Exclusive Lock):独占数据库,阻止所有其他操作
- 未决锁(Pending Lock):表示即将获得排他锁,阻止新的读取操作
- 锁升级流程:共享锁 → 预留锁 → 未决锁 → 排他锁
存储层
虚拟文件系统(VFS)
- 功能:提供统一的文件系统接口,实现跨平台支持
- 主要操作:
- 文件打开、关闭、读取和写入
- 目录管理
- 锁定操作
- 随机访问
- 特点:
- 抽象操作系统差异
- 支持多种存储设备
- 可扩展,支持自定义 VFS 实现
OS 接口
- 功能:与操作系统交互,执行实际的 I/O 操作
- 支持平台:Windows、Linux、macOS、iOS、Android 等
- 主要操作:
- 文件 I/O
- 内存管理
- 线程同步
- 时钟获取
SQLite 工作原理
SQL 语句执行流程
当应用程序执行一条 SQL 语句时,SQLite 内部的执行流程如下:
- 连接数据库:应用程序通过 C API 打开数据库连接
- SQL 编译:
- 词法分析:将 SQL 语句分解为标记
- 语法分析:生成语法树
- 语义分析:检查语义正确性
- 代码生成:生成字节码
- 执行计划生成:优化器生成最优执行计划
- 字节码执行:SQLite 虚拟机执行字节码
- 数据访问:
- 从 B 树中读取或写入数据
- 管理事务和锁
- 返回结果:将执行结果返回给应用程序
- 关闭连接:释放资源
数据存储原理
表数据存储
- 数据结构:使用 B 树存储表数据
- 行存储:每行数据作为一个记录存储在 B 树节点中
- 主键索引:表数据本身就是一个以主键为键的 B 树
- 无主键表:自动生成隐藏的 ROWID 作为主键
索引存储
- 数据结构:使用 B 树存储索引数据
- 索引项:包含索引键和指向表数据的指针(ROWID)
- 多列索引:将多个列的值组合作为索引键
- 唯一性索引:确保索引键唯一
数据类型存储
- 动态类型:SQLite 使用动态类型系统,值的类型由值本身决定,而非列的声明类型
- 类型亲和性:列具有类型亲和性,会尝试将存储的值转换为适合的类型
- 存储格式:
- NULL:1 字节
- INTEGER:1-9 字节,变长整数
- REAL:8 字节,浮点数
- TEXT:变长字符串,UTF-8 或 UTF-16 编码
- BLOB:变长二进制数据,原样存储
事务处理原理
事务日志
- 回滚日志(Rollback Journal):默认模式,在修改数据前,将原始数据写入回滚日志文件
- 预写日志(WAL - Write-Ahead Logging):在修改数据前,将修改内容写入 WAL 文件
事务提交
回滚日志模式:
- 开始事务,获取锁
- 将修改的页复制到回滚日志
- 修改数据库文件
- 提交事务,释放锁
- 删除或截断回滚日志
WAL 模式:
- 开始事务,获取锁
- 将修改内容写入 WAL 文件
- 提交事务,释放锁
- 定期执行 checkpoint,将 WAL 中的修改合并到数据库文件
崩溃恢复
- 回滚日志模式:启动时检查回滚日志,如果存在未完成的事务,使用回滚日志恢复数据
- WAL 模式:启动时检查 WAL 文件,将未合并的修改应用到数据库文件
并发控制原理
锁机制
- 共享锁(S):多个读取操作可以同时持有共享锁
- 预留锁(R):一个写入操作可以持有预留锁,允许其他读取操作
- 未决锁(P):一个写入操作准备获取排他锁,阻止新的读取操作
- 排他锁(X):一个写入操作持有排他锁,阻止所有其他操作
多版本并发控制(MVCC)
- 实现方式:在 WAL 模式下,读取操作从数据库文件读取数据,写入操作写入 WAL 文件
- 读取一致性:读取操作不会被写入操作阻塞,始终读取一致的数据版本
- 写入原子性:写入操作要么完全成功,要么完全失败
索引工作原理
B 树索引结构
- 节点结构:每个节点包含多个索引项和指向下级节点的指针
- 平衡机制:插入或删除操作时,自动调整树结构,保持平衡
- 查询效率:
- 点查询:O(log n) 时间复杂度
- 范围查询:O(log n + k) 时间复杂度,k 为结果数量
索引查找过程
- 从根节点开始,比较查询键与索引项
- 根据比较结果,选择对应的子节点
- 重复上述过程,直到到达叶节点
- 在叶节点中查找匹配的索引项
- 根据索引项中的指针(ROWID)获取表数据
SQLite 版本差异
架构演进
| 版本 | 主要架构改进 |
|---|---|
| 3.7.0 (2010) | 引入 WAL 模式,支持更好的并发性能 |
| 3.8.0 (2013) | 改进查询优化器,支持索引覆盖查询 |
| 3.9.0 (2015) | 引入 JSON1 扩展,支持 JSON 数据类型 |
| 3.11.0 (2016) | 改进 VACUUM 命令,支持增量 VACUUM |
| 3.22.0 (2018) | 引入窗口函数支持 |
| 3.31.0 (2020) | 引入原生 JSON 数据类型 |
| 3.44.0 (2023) | 引入向量数据库支持 |
配置差异
不同版本的 SQLite 可能支持不同的配置选项和参数:
- 页大小:早期版本默认 1KB,现在默认 4KB
- WAL 模式:3.7.0 及以上版本支持
- JSON 支持:3.9.0 及以上版本支持 JSON1 扩展
SQLite 性能优化原理
查询优化
- 索引使用:优化器自动选择最优索引
- 执行计划:生成高效的执行计划
- 避免全表扫描:通过索引减少数据扫描量
- 覆盖索引:如果查询只需要索引中的列,直接从索引获取数据,避免访问表数据
写入优化
- WAL 模式:提高并发写入性能
- 事务批量处理:减少事务次数,降低磁盘 I/O
- 延迟写入:允许在事务提交前缓存写入操作
- 异步 I/O:支持异步写入操作
内存优化
- 页缓存:减少磁盘 I/O 操作
- 内存管理:高效的内存分配和释放
- 缓存大小调整:根据可用内存调整缓存大小
常见问题(FAQ)
Q: SQLite 为什么使用 B 树存储数据?
A: B 树是一种平衡树结构,具有以下优点:
- 平衡结构,确保查询效率稳定
- 支持范围查询和排序
- 高效的插入、删除和更新操作
- 适合磁盘存储,减少磁盘寻道次数
Q: WAL 模式与回滚日志模式有什么区别?
A: WAL 模式与回滚日志模式的主要区别:
- 写入位置:WAL 模式将修改写入 WAL 文件,回滚日志模式将原始数据写入回滚日志
- 并发性能:WAL 模式支持读写并发,回滚日志模式写入时阻塞读取
- 恢复速度:WAL 模式恢复速度更快
- 文件大小:WAL 模式需要管理 WAL 文件大小
Q: SQLite 如何确保数据完整性?
A: SQLite 通过以下机制确保数据完整性:
- 事务处理:确保 ACID 特性
- 锁机制:确保并发访问安全
- 完整性约束:支持主键、外键、唯一约束等
- 崩溃恢复:通过事务日志支持崩溃恢复
Q: 什么是 SQLite 的类型亲和性?
A: SQLite 使用动态类型系统,但列具有类型亲和性,会尝试将存储的值转换为适合的类型。主要的类型亲和性有:
- TEXT:文本类型
- NUMERIC:数值类型
- INTEGER:整数类型
- REAL:浮点类型
- BLOB:二进制类型
Q: SQLite 如何处理并发访问?
A: SQLite 通过锁机制和 WAL 模式处理并发访问:
- 共享锁:允许多个读取操作
- 排他锁:只允许一个写入操作
- WAL 模式:支持读写并发,读取操作从数据库文件读取,写入操作写入 WAL 文件
Q: 如何优化 SQLite 的查询性能?
A: 可以通过以下方法优化 SQLite 的查询性能:
- 创建适当的索引
- 优化 SQL 语句,避免全表扫描
- 使用覆盖索引
- 调整页大小和缓存大小
- 启用 WAL 模式
总结
SQLite 的架构设计简洁高效,采用分层架构,便于维护和扩展。核心组件包括接口层、编译层、核心层和存储层,各层之间通过清晰的接口交互。
了解 SQLite 的工作原理对于 DBA 至关重要,包括 SQL 语句执行流程、数据存储原理、事务处理机制、并发控制原理和索引工作原理。通过理解这些原理,可以更好地优化数据库性能、排查问题和制定最佳实践。
不同版本的 SQLite 在架构和功能上有所差异,DBA 需要关注版本演进,选择适合应用需求的版本,并根据版本特性进行配置和优化。
SQLite 的性能优化原理包括查询优化、写入优化和内存优化,DBA 可以通过创建适当的索引、优化 SQL 语句、启用 WAL 模式、调整缓存大小等方式提高 SQLite 数据库的性能。
