大多数开发者通过ORM使用数据库,对底层发生的事情有功能性但表面的理解。关系数据库的内部不仅仅是学术性的——它们直接解释了性能特征、故障模式和数据库系统之间的权衡。以下是实际发生的事情。
存储:数据如何写入磁盘
关系数据库将数据以页(通常8KB或16KB)的形式写入磁盘。页是基本的I/O单位——数据库每次读写一个完整的页,即使只有一行发生了变化。存储结构:堆(基本结构——行按插入顺序存储在页中;读取行需要知道其页和槽位);以及聚集索引(一种B树结构,其中数据按主键顺序存储在索引本身中——按主键读取行是单次B树遍历;PostgreSQL使用堆+二级索引,MySQL InnoDB使用聚集主键)。预写日志(WAL):在修改任何数据页之前,修改被写入WAL(顺序日志文件)。这是启用崩溃恢复的机制——如果数据库在写入中途崩溃,WAL在重启时被重放以使数据进入一致状态。WAL也支撑复制——副本重放主的WAL以保持同步。MVCC(多版本并发控制):大多数现代数据库(PostgreSQL、MySQL InnoDB、Oracle)使用MVCC来允许读者和写者互不阻塞。当行被更新时,数据库不会覆盖旧版本——它创建一个新版本。读者看到在其事务开始时当前的版本;写者创建新版本。旧版本由后台进程清理(PostgreSQL中的VACUUM)。MVCC是`SELECT COUNT(*) FROM large_table`在PostgreSQL中慢的原因——它必须只计算对当前事务可见的行,即使表已经建立了索引,这也需要全表扫描。
索引:它们是什么以及为什么有代价
索引是一种单独的数据结构,可以通过特定列或列集进行快速查找。B树(平衡树):默认索引类型。支持等值查找(`WHERE column = value`)和范围查询(`WHERE column BETWEEN x AND y`),并且已排序,因此无需排序即可支持`ORDER BY`。查找时间:O(log n)树遍历。哈希索引:对于等值查找比B树更快(O(1)哈希查找),但不支持范围查询或`ORDER BY`。在PostgreSQL和MySQL中作为明确选择支持;MongoDB使用哈希索引进行哈希分片。全文索引(PostgreSQL中的GIN,MySQL中的FULLTEXT):反转文本——对于每个词,存储包含它的行列表。以速度启用`WHERE content @@ to_tsquery(‘keyword’)`。为什么索引有代价:每次写入(INSERT、UPDATE、DELETE)还必须更新表上的所有索引。有10个索引的表每次数据写入需要10次索引写入。对于写入密集的表,太多索引是性能反模式。索引膨胀:已删除的行留下”死”索引条目,不会立即删除——VACUUM会清理它们,但繁重的DELETE流量会导致索引膨胀,增加索引大小并减慢读取速度。部分索引:行子集上的索引——`CREATE INDEX ON orders (customer_id) WHERE status = ‘pending’`。仅覆盖满足WHERE子句的行,对于选择性条件比完整索引小得多。PostgreSQL中使用最少的功能之一。
事务和ACID
原子性:事务是全有或全无的。如果事务中的任何语句失败,所有语句都会回滚。一致性:事务使数据库从一个有效状态转变为另一个有效状态(提交后满足所有约束)。隔离性:并发事务互相隔离——隔离程度由隔离级别控制。读已提交(PostgreSQL和大多数数据库中的默认值):每条语句只看到执行时已提交的数据。可重复读:每个事务从事务开始时看到数据的一致快照。可串行化:最强隔离——事务执行得好像它们是一次一个串行运行的(没有并发异常)。更高的隔离=更少的异常=更多的锁定开销=更低的吞吐量。持久性:事务提交后,即使数据库崩溃也不会丢失——WAL确保这一点。WAL中的提交记录是原子”提交完成”信号。性能现实:ACID保证不是免费的。每次提交的WAL写入会产生I/O开销;MVCC版本管理会产生存储开销和VACUUM需求;可串行化隔离需要检测冲突,产生CPU开销。了解每种保证的代价有助于你做出数据库设计决策。




