外观
一条 SQL 的旅程:查询、事务与恢复
数据库向应用提供数据模型、查询与事务承诺。它内部仍要处理内存、磁盘、并发和失败;SQL 隐藏的是执行选择,而不是这些成本。本篇沿一条查询和一次更新建立完整流程,参考 CMU 15-445。
从文本到执行计划
sql
SELECT name FROM students
WHERE department = 'CS' AND year >= 3
ORDER BY name LIMIT 20;解析器建立语法树;绑定阶段查找表、列、类型与权限;逻辑计划表达过滤、投影、排序等操作;优化器根据统计信息和代价估计选择物理算子;执行器实际获取记录并返回结果。数据库可以选索引扫描,也可以顺序扫描,SQL 文本本身不指定它必须采用哪一种。
flowchart LR A[SQL 文本] --> B[解析与绑定] B --> C[逻辑计划] C --> D[代价优化] D --> E[物理执行计划] E --> F[缓冲池与存储页] F --> G[结果行]
查看流程图文本
flowchart LR A[SQL 文本] --> B[解析与绑定] B --> C[逻辑计划] C --> D[代价优化] D --> E[物理执行计划] E --> F[缓冲池与存储页] F --> G[结果行]
若过滤条件只选中很少行,索引可减少读取;若多数行都符合条件,索引定位后反复回表可能比顺序扫描更贵。优化器依赖行数、分布、相关性等估计;统计过时会让“看起来便宜”的计划实际很慢。因此调优应看执行计划和实际行数,而不仅看是否“用了索引”。
索引把什么成本降下来
B+ 树用较大扇出降低树高,内部节点导航、叶子保存有序键及记录引用或数据。若把根、下一层内部节点、叶子算作三层,内部节点扇出约 100,则可连接约
复合索引 (department, year) 按元组字典序排列,因此适合先约束 department 再约束 year;它并不会自动满足 ORDER BY name。覆盖索引若已包含查询所需列,可以减少回表。每增加一个索引也增加写入、空间和维护成本。
LSM 树把写入先积累到内存结构,再刷成有序磁盘文件,通过 compaction 合并。它以额外的合并写入和查询多个层次等成本换取写入组织优势。Bloom filter 可以快速排除“肯定不存在”的文件,但可能误报存在;它不能返回完整记录,也不能取代范围索引。
缓冲池与页
磁盘以块传输,数据库把热页缓存在缓冲池。页被访问时需要固定或引用保护,避免操作尚未完成就被淘汰;脏页需要写回。页闩锁保护短时内存结构,事务锁约束业务读写可见性,作用范围和生命周期不同。把所有锁都叫 mutex 会掩盖这些区别。
并发:MVCC 保留多个版本
MVCC 让读取依据快照选择可见版本,减少读写直接互相阻塞。更新产生新版本,旧版本在不再被活动快照需要后才可回收。长事务可能阻碍旧版本清理,导致膨胀;“读不阻塞写”不意味着没有任何锁或写写冲突。
隔离级别决定允许观察哪些异常。读已提交允许同一事务的不同语句看到不同已提交状态;可重复读或快照隔离通常提供更稳定视图,但不同数据库具体语义不同。快照隔离可能出现 write skew:两笔事务分别更新不同记录,却共同破坏跨行约束。需要可串行化保证、显式锁或重新设计约束。
更新何时可以称为持久
WAL 的核心顺序是:与某个数据页更新相关的日志,必须先于该脏页落盘。否则断电后看到了部分新数据,却没有足够的日志解释或恢复它。事务提交一般还需要把提交所需日志刷到稳定存储,具体 durability 设置会影响承诺。
数据页可以稍后批量刷盘,崩溃后根据日志 redo 已应当保留的更改,并按具体恢复算法处理未完成事务。checkpoint 缩小恢复范围,但不能简单理解成“数据库所有数据在一个瞬间全部一致写完”。文件系统日志与数据库 WAL 保护的对象不同,文件系统保证结构完整不自动等于应用事务完整。
三个工程问题
- 慢查询:是扫描太多行、排序溢出、随机 I/O、锁等待,还是客户端取数慢?
- 连接过多:更多连接不一定更多吞吐,可能只是更大竞争;需要连接池与背压。
- 重复提交:事务重试可能必要,应用需处理序列化失败;外部副作用应考虑幂等和 outbox。
自测:给每一列都建索引,查询是否一定更快?
不是。索引占空间且增加写维护成本;选择性低、访问大部分表或统计错误时,扫描更合理。组合索引的顺序也决定可利用的前缀,需要针对实际查询计划分析。
来源:CMU 数据库课程、PostgreSQL 事务隔离、PostgreSQL WAL。与跨分片事务对照阅读。