数据库基础体系 · 第 1/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
数据库学习不能从“记住几条 SQL”开始,也不能止于“会调用 ORM”。一条完整路线应当回答以下问题:
- 数据如何被建模,为什么表结构能够表达业务约束?
- SQL 如何从声明式查询变成执行计划?
- 多个并发事务同时读写时,数据库如何保持正确性?
- 索引为什么能加速查询,又为什么会拖慢写入?
- 单机容量、可用性或吞吐不足时,复制、分片和共识分别解决什么问题?
- 向量检索与传统等值、范围、排序查询有什么根本差异?
- 在真实系统中,如何在正确性、性能、可运维性之间做取舍?
本文以关系型数据库为主线,补充分布式数据库和向量检索。SQL 示例以 PostgreSQL 语义为主,并在需要时说明 MySQL 8.4 的差异。SQL 标准、具体数据库实现和工程经验不是同一层次,文中会明确区分。
一、先建立总模型:数据库到底在管理什么
数据库可以抽象为一个维护状态的系统:
其中:
- 是时刻 的数据库状态;
- 是一次操作,例如插入、更新、删除或查询;
- 是数据库执行操作并维护约束的过程;
- 是操作完成后的新状态。
对于并发系统,问题不再是单个操作是否正确,而是多个操作交错执行后,最终状态是否等价于某个正确的串行执行顺序。
因此,数据库学习可以分成四个层次:
- 数据模型:数据是什么、实体如何关联、哪些状态合法。
- 查询与存储:如何表达访问需求,如何用索引和执行计划降低代价。
- 事务与并发:多个操作如何组成可靠的状态变更。
- 分布式与新型检索:数据跨节点、跨分片或从精确匹配扩展到相似性匹配。
关系模型、事务、索引、分布式数据库和向量检索并不是互相独立的知识点。它们共同回答的是:如何在数据规模、并发和故障存在时,仍然得到可解释、可验证的结果。
二、关系模型:先定义数据,再定义操作
2.1 关系、元组、属性和域
关系模型中的“关系”可以近似理解为一张表,但它比表格概念更严格。
- 关系(relation):一个满足模式约束的元组集合。
- 元组(tuple):关系中的一行。
- 属性(attribute):元组中的一个命名字段。
- 域(domain):某个属性允许取值的集合,例如整数、日期或有限状态值。
- 关系模式(relation schema):关系名以及属性定义,例如
在数学关系模型中,关系是集合,因此理论上没有重复元组,也没有固定顺序。SQL 表则是工程实现:
- SQL 查询结果默认不保证顺序,除非使用
ORDER BY; - SQL 表在没有唯一约束时可能存在重复行;
- SQL 引入了
NULL,这会使逻辑从二值逻辑变成带未知值的三值逻辑。
例如:
CREATE TABLE student (
student_id bigint PRIMARY KEY,
name text NOT NULL,
department text
);
这里:
student_id是属性;bigint定义其域的一部分;PRIMARY KEY要求值唯一且非空;department没有NOT NULL,因此可以是NULL。
NULL 不是字符串 "NULL",也不是数字 0。它表示“未知”或“不适用”。例如:
SELECT *
FROM student
WHERE department = NULL;
通常得不到预期结果,因为 department = NULL 的结果是 UNKNOWN,而 WHERE 只保留结果为 TRUE 的行。正确写法是:
SELECT *
FROM student
WHERE department IS NULL;
2.2 键:如何唯一识别一行
超键、候选键和主键
设关系为 ,属性集合为 。
- 若 能唯一确定一行,则 是超键;
- 若 是超键,且删除其中任意属性后都不再唯一,则 是候选键;
- 从候选键中选出的一个作为主要标识,就是主键。
例如:
Enrollment(student_id, course_id, semester, score)
如果同一学生在同一学期同一门课程只能有一条成绩,则:
是候选键。单独的 student_id 不是键,因为一个学生可以选多门课。
SQL 表达为:
CREATE TABLE enrollment (
student_id bigint NOT NULL,
course_id bigint NOT NULL,
semester text NOT NULL,
score numeric(5, 2),
PRIMARY KEY (student_id, course_id, semester)
);
自然键和代理键
- 自然键:业务中本来就有意义的标识,例如身份证号、ISBN。
- 代理键:系统生成的标识,例如自增整数、UUID。
代理键不等于业务唯一性。下面的设计仍然需要业务唯一约束:
CREATE TABLE account (
account_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
account_id 负责稳定引用;email 负责防止两个账户使用相同邮箱。只设置代理主键而不设置业务唯一约束,会把数据合法性留给应用层,且容易在并发下失效。
2.3 外键和引用完整性
外键表达的是跨表约束:
CREATE TABLE orders (
order_id bigint PRIMARY KEY,
user_id bigint NOT NULL,
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES app_user(user_id)
);
它要求每个 orders.user_id 都能在 app_user.user_id 中找到对应值,除非该外键允许 NULL。
删除或更新被引用行时,需要定义动作:
FOREIGN KEY (user_id)
REFERENCES app_user(user_id)
ON DELETE RESTRICT
常见动作包括:
RESTRICT或NO ACTION:存在引用时拒绝删除;CASCADE:级联删除或更新;SET NULL:将引用列设为NULL,前提是列允许为空;SET DEFAULT:设置为默认值。
CASCADE 不是“更方便的默认选择”。如果父表是一条核心业务记录,级联删除可能在一次操作中删除大量数据。删除策略必须符合业务生命周期。
三、函数依赖与规范化:为什么表结构会产生异常
3.1 函数依赖
函数依赖表示:
含义是:对关系中的任意两行,只要它们在属性集合 上相等,那么它们在属性集合 上也必须相等。
例如:
Student(student_id, student_name, department_id, department_name)
通常有:
以及:
因此:
但这并不表示 department_name 应该直接存储在学生表中。
3.2 反例:重复数据和更新异常
假设课程选课表如下:
| student_id | student_name | course_id | course_name | teacher |
|---|---|---|---|---|
| 1 | 张三 | C1 | 数据库 | 李老师 |
| 2 | 李四 | C1 | 数据库 | 李老师 |
此时课程名称和教师信息被重复存储。
更新异常
如果课程 C1 更换教师,只更新了第一行,就得到:
| course_id | course_name | teacher |
|---|---|---|
| C1 | 数据库 | 王老师 |
| C1 | 数据库 | 李老师 |
数据库中出现两个互相矛盾的事实。
插入异常
如果课程尚未有学生选修,就无法只依靠这张表记录课程本身。
删除异常
如果删除课程 C1 的最后一名学生,课程和教师信息也会被一起删除。
这些异常并不是“数据量大后才出现”,而是由依赖关系和存储粒度不匹配造成的。
3.3 第一、第二、第三范式
第一范式:属性值应当原子化
第一范式通常要求每个字段存放一个不可再分的值,而不是列表或重复组。
不推荐:
user_id | phone_numbers
1 | 138...,139...
推荐拆成:
user(user_id, ...)
user_phone(user_id, phone_number)
不过“原子”依赖业务语义。一个完整地址是否应拆成省、市、街道,不是数据库理论单独决定的,而取决于系统是否需要分别查询、排序和约束这些部分。
第二范式:消除对复合键的部分依赖
第二范式要求:
- 表已经满足第一范式;
- 每个非主属性都完全依赖于候选键,而不是只依赖复合键的一部分。
继续看:
Enrollment(student_id, course_id, student_name, course_name, score)
候选键为:
依赖关系包括:
student_name 只依赖 student_id,course_name 只依赖 course_id,因此存在部分依赖。应拆为:
Student(student_id, student_name)
Course(course_id, course_name)
Enrollment(student_id, course_id, score)
第三范式:消除非键属性之间的传递依赖
第三范式要求表已经满足第二范式,并且非键属性不应依赖于另一个非键属性。
例如:
Employee(employee_id, department_id, department_name)
有:
所以:
department_name 通过 department_id 间接依赖员工主键,属于传递依赖。应拆为:
Employee(employee_id, department_id)
Department(department_id, department_name)
3.4 规范化不是越高越好
规范化的目标是让每个事实尽量只存储一次,并使约束清晰。它并不意味着任何重复列都必须删除。
例如订单明细中可以保存下单时的商品单价:
Product(product_id, current_price)
OrderItem(order_id, product_id, quantity, unit_price)
Product.current_price 表示当前价格;OrderItem.unit_price 表示历史成交价格。两者语义不同,即使数值相同,也不能简单合并。
这属于有意的反规范化,成立的前提是:
- 两个字段代表不同业务事实;
- 写入时明确快照时机;
- 可以解释它们何时应该不同;
- 有约束、测试或对账机制防止无意漂移。
反规范化常见于:
- 读取路径极其频繁;
- 连接多个大表成本过高;
- 聚合结果需要缓存;
- 历史快照必须保留;
- 分布式环境中跨分片连接代价过高。
它的代价是更新路径更复杂,数据一致性责任从数据库约束部分转移到了事务、消息或重建任务。
四、SQL:从声明式查询到执行计划
4.1 SQL 的逻辑处理顺序
下面的 SQL:
SELECT u.user_id, COUNT(o.order_id) AS order_count
FROM app_user AS u
LEFT JOIN orders AS o
ON o.user_id = u.user_id
WHERE u.status = 'active'
GROUP BY u.user_id
HAVING COUNT(o.order_id) >= 3
ORDER BY order_count DESC;
逻辑上大致按以下顺序理解:
FROM和JOIN形成行集;WHERE过滤普通行;GROUP BY形成分组;- 聚合函数计算每组结果;
HAVING过滤分组;SELECT投影列;ORDER BY排序;LIMIT截断结果。
实际执行计划不必按照这个顺序执行。优化器可能先过滤、使用索引、改变连接顺序或采用哈希聚合,但结果必须符合 SQL 语义。
LEFT JOIN 的关键边界是:右表条件放在 ON 和 WHERE 中,语义不同。
-- 保留没有已支付订单的用户
SELECT u.user_id, o.order_id
FROM app_user u
LEFT JOIN orders o
ON o.user_id = u.user_id
AND o.status = 'paid';
如果写成:
SELECT u.user_id, o.order_id
FROM app_user u
LEFT JOIN orders o
ON o.user_id = u.user_id
WHERE o.status = 'paid';
那么没有订单的用户会因 WHERE o.status = 'paid' 为 UNKNOWN 而被过滤,效果接近内连接。
4.2 SQL 不保证顺序
没有 ORDER BY 时,结果顺序没有语义保证。即使今天执行计划总是返回某种顺序,也不能把实现偶然性当作契约。
分页应使用稳定排序:
SELECT *
FROM orders
WHERE (created_at, order_id) < (:last_created_at, :last_order_id)
ORDER BY created_at DESC, order_id DESC
LIMIT 50;
这里使用 (created_at, order_id) 作为排序游标,避免只按时间排序时同一时间戳造成重复或漏项。
4.3 用执行计划验证假设
PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM orders
WHERE user_id = 42
AND status = 'paid';
EXPLAIN 展示计划估算;ANALYZE 会实际执行查询并展示真实行数、耗时和循环次数。由于 EXPLAIN ANALYZE 会执行语句,对 UPDATE、DELETE 等语句尤其要谨慎。
重点比较:
- 估算行数与实际行数是否严重偏差;
- 是否发生全表扫描;
- 连接算法是否适合数据规模;
- 是否发生大量磁盘排序;
BUFFERS显示的数据页访问是否异常。
MySQL 可使用:
EXPLAIN ANALYZE
SELECT ...
具体输出格式和可用选项以对应版本为准。执行计划不是永久承诺:数据分布、统计信息、参数和版本变化都可能改变计划。
五、索引:用额外结构换取更低的访问代价
5.1 B-tree 索引解决什么问题
最常见的 B-tree 索引适合:
- 等值匹配;
- 范围查询;
- 按索引顺序读取;
- 部分前缀匹配。
例如:
CREATE INDEX orders_user_status_created_idx
ON orders (user_id, status, created_at DESC);
它可以帮助:
SELECT *
FROM orders
WHERE user_id = 42
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
复合索引中,列顺序很重要。对于 (a, b, c),通常最容易支持:
a = ?a = ? AND b = ?a = ? AND b = ? AND c > ?
但只查询 b = ? 时,不能期待它像以 b 开头的索引一样高效。所谓“最左前缀”是常见 B-tree 实现的访问规律,不是所有索引类型的统一定律。
5.2 选择性不是唯一标准
选择性可以粗略理解为过滤后剩余行的比例。低选择性的列,例如只有 true/false 两种值,单独建立索引通常收益有限,但以下情况仍可能有价值:
true只占极少数;- 查询同时包含其他列;
- 可以使用部分索引;
- 索引可以直接提供排序;
- 索引覆盖了查询所需列。
PostgreSQL 示例:
CREATE INDEX orders_unpaid_idx
ON orders (created_at)
WHERE status = 'unpaid';
这类部分索引只索引满足谓词的行。查询条件必须能够被优化器证明与索引谓词兼容,否则不一定使用该索引。
5.3 索引的代价
索引不是免费的缓存。每次插入、删除或更新相关列时,数据库都要维护索引结构,代价包括:
- 写放大;
- 额外磁盘空间;
- 缓存占用;
- 页分裂或维护成本;
- 批量写入速度下降;
- 统计和维护复杂度增加。
因此索引设计必须从真实查询出发,而不是“每个字段都建索引”。
还要注意表达式是否破坏索引使用:
-- 可能无法直接使用普通 created_at 索引
WHERE DATE(created_at) = DATE '2025-01-01'
可以改写为范围:
WHERE created_at >= TIMESTAMP '2025-01-01 00:00:00'
AND created_at < TIMESTAMP '2025-01-02 00:00:00'
但时区语义必须先明确。若业务按用户时区计算日期,不能简单套用 UTC 边界。
5.4 不同索引类型的边界
以 PostgreSQL 为例:
- B-tree:等值、范围和排序的通用选择;
- Hash:等值比较,适用范围较窄;
- GIN:倒排结构,常用于数组、全文检索等;
- GiST:可扩展的通用搜索树,常用于范围、几何等;
- BRIN:利用物理存储顺序压缩索引,适合超大且与物理顺序相关的表。
MySQL InnoDB 的主要索引结构是 B-tree 族实现;索引、聚簇主键和二级索引的存储组织与 PostgreSQL 不同,不能直接把一种引擎的页结构经验套到另一种引擎。
六、事务:把多步操作变成一个正确状态变化
6.1 ACID 的准确含义
事务是数据库执行的一组操作单元。ACID 包括:
- 原子性(Atomicity):事务中的操作要么整体生效,要么整体不生效;
- 一致性(Consistency):事务提交前后都满足数据库约束和业务不变量;
- 隔离性(Isolation):并发事务之间的可见性和交错方式受到规定;
- 持久性(Durability):提交成功后,即使随后发生崩溃,数据也应按数据库承诺保留。
一致性不是数据库凭空知道的业务正确性。数据库能直接维护主键、外键、唯一性、检查约束等;“账户余额不能为负”“库存不能超卖”则需要由约束、锁、事务逻辑共同表达。
6.2 一个完整转账事务
BEGIN;
UPDATE account
SET balance = balance - 100
WHERE account_id = 1
AND balance >= 100;
-- 应用检查受影响行数必须为 1。
-- 若为 0,表示账户不存在或余额不足,应回滚。
UPDATE account
SET balance = balance + 100
WHERE account_id = 2;
-- 应用还应检查第二次更新是否成功。
COMMIT;
事务的正确性不只在于写了 BEGIN 和 COMMIT,还在于:
- 每个必要条件都被检查;
- 任一步失败都会回滚;
- 锁定或并发控制覆盖了真正的不变量;
- 客户端断线、超时和重试不会造成重复业务效果。
如果第二个账户不存在,而应用没有检查影响行数,第一笔扣款可能仍被提交,转账就不再保持守恒。
更稳妥的建模方式是对两个账户都先锁定,并固定锁定顺序,降低死锁概率:
BEGIN;
SELECT account_id, balance
FROM account
WHERE account_id IN (1, 2)
ORDER BY account_id
FOR UPDATE;
-- 应用在事务内检查两行均存在、余额充足
UPDATE account
SET balance = CASE account_id
WHEN 1 THEN balance - 100
WHEN 2 THEN balance + 100
END
WHERE account_id IN (1, 2);
COMMIT;
FOR UPDATE 的具体锁行为依赖数据库和隔离级别;它不是跨系统的万能“禁止一切并发”。
七、隔离级别与异常现象
7.1 三类经典异常
考虑两个并发事务:
脏读
事务 B 读取事务 A 尚未提交的数据。如果 A 回滚,B 读到的内容从未成为持久状态。
不可重复读
事务 B 两次读取同一行,中间事务 A 修改并提交,B 两次得到不同结果。
幻读
事务 B 按条件读取一组行,中间事务 A 插入或删除满足条件的行,B 再次按同一条件读取时,结果集合发生变化。
还存在更一般的写偏差(write skew):
- 两个事务分别读取同一业务规则;
- 每个事务修改不同的行;
- 单独看每个事务都合法;
- 一起提交后,联合状态违反规则。
例如规则是“至少一名值班医生保持在岗”。两个事务分别看到两名医生都在岗,然后各自把不同医生设为休息,最终没有人值班。即使没有两个事务更新同一行,也可能破坏约束。
7.2 隔离级别不是一个跨产品完全相同的实现
SQL 标准定义了隔离级别及其允许现象,但具体数据库的实现和额外保证可能不同。
Read Uncommitted
允许最低级别的隔离。在一些实现中可能允许脏读;在 PostgreSQL 中,READ UNCOMMITTED 实际按更高的 READ COMMITTED 处理,而不是实现真正的脏读。
Read Committed
每条语句看到的通常是语句开始时已提交的数据。PostgreSQL 中,同一事务内两条普通 SELECT 可能看到不同快照。MySQL InnoDB 的具体可见性也受其 MVCC 实现影响。
Repeatable Read
目标是让事务内重复读取更稳定,但“可重复读”不等于所有并发业务规则都自动安全。PostgreSQL 的实现基于事务快照,并对某些冲突报告序列化失败;MySQL InnoDB 的默认隔离级别通常是 Repeatable Read,并结合间隙锁等机制处理部分范围并发。两者不能只根据名称推断完全相同的行为。
Serializable
要求并发执行结果等价于某种串行顺序。数据库可能通过锁、谓词锁、SSI 或其他机制实现。代价是:
- 冲突时阻塞更多;
- 可能出现死锁;
- 可能出现序列化失败;
- 应用必须能够重试整个事务。
序列化失败不是数据库“坏了”,而是数据库拒绝了无法安全排列的并发结果。
7.3 用例子理解写偏差
假设表中有值班记录:
CREATE TABLE doctor_duty (
doctor_id bigint PRIMARY KEY,
on_call boolean NOT NULL
);
错误逻辑:
事务 A:查询 on_call=true,发现医生 1 和 2 都在岗
事务 B:查询 on_call=true,发现医生 1 和 2 都在岗
事务 A:将医生 1 设为不在岗
事务 B:将医生 2 设为不在岗
A、B 都提交
如果约束是“至少一人值班”,单纯锁住各自要更新的行不一定足够,因为两个事务的决定依赖的是集合条件,而不是某一行。
解决路径有三类:
- 使用
SERIALIZABLE,让数据库检测并发冲突; - 引入一个所有变更都必须锁定的汇总行;
- 将规则改写为数据库可直接约束的结构,例如使用唯一约束或状态表。
选择哪一种取决于写入频率、冲突概率和业务模型。不能只说“加锁”而不说明锁住的对象与不变量之间的关系。
7.4 死锁与重试
死锁是两个或多个事务互相等待:
事务 A 持有行 1,等待行 2
事务 B 持有行 2,等待行 1
数据库通常会检测死锁并回滚其中一个事务。应用应:
- 对死锁和序列化失败进行有限次数重试;
- 每次重试重新开启完整事务;
- 使用稳定的锁定顺序;
- 缩短事务时间;
- 不在事务中执行不可控的远程调用。
不能只重试最后一条 SQL。事务中间可能已经读取了不同数据,必须从事务起点重新执行。
八、MVCC、锁与持久化:事务背后的机制
8.1 MVCC 的基本思想
MVCC(多版本并发控制)不是让数据库只有一份正在变化的数据,而是让读者根据快照判断哪些版本可见。
抽象地说,一个事务读取数据时会有快照 。对某条记录的版本 :
不同数据库的版本元数据、清理方式和锁实现不同,但核心目标类似:读写在一定范围内可以并行,读者不必总等待写者。
MVCC 不会消除所有锁:
- 更新同一行仍可能相互等待;
- 唯一约束检查需要协调;
- 外键检查需要协调;
- 范围约束和串行化可能需要更强的冲突检测。
8.2 WAL 与崩溃恢复
WAL(Write-Ahead Logging,预写日志)的基本原则是:数据页落盘前,描述该修改的日志应先持久化。
崩溃恢复大致经历:
- 从检查点或日志起点开始扫描 WAL;
- 重做已提交或需要重做的修改;
- 撤销或忽略未完成事务的影响;
- 恢复到符合日志语义的状态。
因此“提交成功”不仅意味着 SQL 执行结束,还涉及日志是否满足持久化配置。同步提交、异步复制和客户端确认策略会影响“客户端看到成功”与“副本已经持久化”之间的边界。
九、分布式数据库:把单机问题扩展到网络故障
“分布式数据库”不是一个单一产品类别。它可能指:
- 主从或多副本数据库;
- 分片数据库;
- 分布式 SQL 数据库;
- 在应用层组合的多个数据库;
- 带分布式事务和共识协议的系统。
这些系统必须面对单机没有的故障:网络延迟、网络分区、节点宕机、消息重复、时钟不一致、部分成功和副本落后。
9.1 复制:同一数据的多个副本
典型主副本写入流程:
客户端
↓
主节点接收写请求
↓
写入本地日志
↓
发送日志给副本
↓
根据确认策略向客户端返回成功
如果采用异步复制:
主节点提交成功 → 客户端收到成功
副本复制稍后完成
主节点此时宕机,最近提交的数据可能尚未到达副本,形成复制丢失窗口。
如果采用同步或法定人数确认:
则提交延迟更高,但副本故障时通常能提供更强的数据保留保证。这里的 quorum 不是固定含义:需要结合副本数、故障域、日志持久化和选主规则理解。
读副本的真实边界
读副本可能存在:
- 复制延迟;
- 读到旧数据;
- 切换期间短暂不可用;
- 路由到错误拓扑;
- 读取确认已写入但副本尚未应用的数据。
因此“写后立刻读”若要求读到刚写入的数据,需要:
- 读主节点;
- 使用会话粘性;
- 使用带位置或时间约束的读;
- 或等待副本追平。
只写“主库负责写、从库负责读”并不能自动保证读写一致性。
9.2 CAP 的正确使用方式
CAP 讨论的是网络分区发生时,在一致性(Consistency)和可用性(Availability)之间的选择:
- 一致性:客户端得到的结果符合系统定义的单一、最新状态;
- 可用性:每个非故障节点收到请求后都能返回结果;
- 分区容错性:网络分区发生时系统仍尝试运行。
在真实分布式系统中,网络分区不能被可靠地排除,因此讨论通常聚焦于分区时更偏向一致性还是可用性。
CAP 不是“数据库只能选两个”的日常性能定理,也不能直接推出某产品“绝对 CP”或“绝对 AP”。还必须区分:
- 强一致读;
- 线性一致写;
- 最终一致复制;
- 会话一致性;
- 可串行化事务。
9.3 分片:把数据拆到多个节点
分片函数可以写成:
其中:
- 是分片键;
- 是哈希函数;
- 是分片数量。
哈希分片通常分布均匀,但范围查询不友好。范围分片例如按租户、时间或 ID 区间组织,范围查询较自然,却可能产生热点。
分片键选择决定很多能力:
- 是否能在单分片完成事务;
- 是否能避免跨分片连接;
- 数据是否均匀;
- 扩容是否需要大量迁移;
- 租户隔离是否清晰。
跨分片查询的数据流通常是:
协调节点解析查询
↓
计算目标分片
↓
并发发送子查询
↓
各分片局部过滤、排序、聚合
↓
协调节点合并结果
例如全局 ORDER BY created_at LIMIT 20,每个分片至少要返回足够候选,协调节点再进行归并。若每个分片只返回 20 行,在某些过滤和排序条件下可能无法得到正确的全局前 20 行。
9.4 分布式事务与 2PC
两阶段提交(2PC)包含:
- Prepare 阶段:协调者询问参与者能否提交,参与者持久化必要状态并回复;
- Commit 阶段:协调者根据结果发送提交或回滚决定。
问题在于协调者或网络故障可能使参与者长期处于 prepared 状态,锁和资源不能立即释放。2PC 提供跨参与者原子提交能力,但会带来阻塞、协调和恢复复杂度。
如果业务允许,常见替代方式是:
- 将强一致边界收敛到单分片;
- 使用本地事务加可靠消息;
- 使用 Outbox 表记录待发布事件;
- 使用可补偿的 Saga 流程。
这些方案不是自动等价于 2PC。它们通常牺牲即时原子性,换取更高可用性和更容易扩展的执行模型。
十、向量检索:从精确匹配到相似性搜索
10.1 Embedding 是什么
Embedding 是把文本、图片、用户行为或其他对象编码成固定维度的数值向量:
其中 是向量维度。Embedding 模型通过训练使“语义相近”的对象在向量空间中更接近。
数据库并不知道向量中的语言语义。数据库只负责:
- 存储向量;
- 计算距离或相似度;
- 组织索引;
- 执行过滤;
- 返回候选结果。
Embedding 模型本身决定了“什么叫相似”。换模型、换版本或换预处理方式,可能改变整个检索空间,因此模型版本应作为数据契约管理。
10.2 距离度量
欧氏距离
距离越小,向量越接近。平方根不影响排序时,可以比较平方欧氏距离:
点积
若向量已经归一化,点积与余弦相似度相等。
余弦相似度
其中:
余弦距离常定义为:
相似度越大越近,距离越小越近。工程中最常见的错误之一是把“距离升序”和“相似度降序”写反。
10.3 精确检索和近似检索
给定查询向量 ,从 个向量中返回距离最小的 个:
若逐个计算所有距离,这是精确检索,计算成本约为:
其中 是维度。
当 很大时,常使用 ANN(Approximate Nearest Neighbor,近似最近邻)索引,以少量精度损失换取更低延迟。典型结构包括:
- HNSW:多层图结构,通过邻居导航搜索;
- IVF:先把向量划分到若干簇,只搜索部分簇;
- PQ:对向量进行压缩,降低存储和距离计算成本。
近似检索的核心指标是召回率。若精确结果集合为 ,近似算法返回集合为 ,则可用:
评估时需要用同一批查询比较精确基线和近似索引结果,不能只看平均延迟。
10.4 过滤和检索顺序
实际搜索通常不是“只找最相似的文本”,而是:
在 tenant_id = 10 且 language = 'zh' 的文档中
返回与查询向量最相似的 10 条
这涉及过滤和向量搜索的组合顺序:
- 先过滤再向量搜索:候选集合小,结果约束清晰;
- 先向量搜索再过滤:向量索引利用率高,但前若干候选可能都被过滤掉;
- 混合策略:扩大候选数,再过滤并补召回;
- 分区或分片:让租户、语言等条件与物理数据布局一致。
如果先取 10 个近邻,再过滤掉 9 个,只返回 1 个结果,这不是“索引不准”,而是候选数和过滤条件不匹配。扩大候选数可能改善召回,但会增加延迟,且不能保证任意过滤条件都有效。
10.5 一个可运行的 PostgreSQL 向量示例
下面示例假设 PostgreSQL 已安装向量扩展 pgvector。vector 类型、距离操作符和 HNSW 索引属于扩展能力,不是 PostgreSQL 核心 SQL 的内置语义。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE document_chunk (
chunk_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id bigint NOT NULL,
content text NOT NULL,
embedding vector(3) NOT NULL,
CHECK (vector_dims(embedding) = 3)
);
INSERT INTO document_chunk (tenant_id, content, embedding)
VALUES
(10, 'PostgreSQL 支持事务和索引', '[0.90, 0.10, 0.00]'),
(10, '数据库可以执行向量相似性搜索', '[0.80, 0.20, 0.00]'),
(20, '分布式系统需要处理网络分区', '[0.00, 0.10, 0.90]');
使用余弦距离检索:
SELECT
chunk_id,
content,
embedding <=> '[0.85, 0.15, 0.00]'::vector AS cosine_distance
FROM document_chunk
WHERE tenant_id = 10
ORDER BY embedding <=> '[0.85, 0.15, 0.00]'::vector
LIMIT 2;
这里:
<=>表示余弦距离;ORDER BY必须升序,因为距离越小越相似;tenant_id = 10是结构化过滤;- 查询向量维度必须与
vector(3)一致。
可建立 HNSW 索引:
CREATE INDEX document_chunk_embedding_hnsw_idx
ON document_chunk
USING hnsw (embedding vector_cosine_ops);
HNSW 是近似索引。它通常需要在数据写入后构建或维护,索引参数和扩展版本会影响构建成本、内存占用、延迟和召回率。生产环境应通过精确扫描结果建立测试基线,再调整索引和查询参数,而不是仅凭少量人工样例判断质量。
10.6 向量一致性与数据生命周期
向量检索至少涉及三种一致性:
内容与向量一致
文档内容修改后,旧 Embedding 仍可能留在库中。应通过事务状态或版本字段区分:
document.version = 8
embedding.version = 7
检索时不能把版本 7 当作版本 8 的向量使用。
主数据与索引一致
文档删除后,向量索引是否立即不可见,取决于存储系统和索引维护机制。逻辑删除可以快速隐藏结果,但会留下空间和维护成本;物理删除则可能需要后台重建或清理。
多副本读取一致
写入向量后,查询若被路由到延迟副本,可能暂时搜不到刚写入的数据。这与传统数据库的复制延迟本质相同,只是向量索引通常还有独立构建或刷新过程。
可靠流程可以是:
写入文档和状态 pending
↓
生成 embedding
↓
在同一数据系统中写入向量和模型版本
↓
确认索引可见
↓
状态改为 ready
如果 Embedding 生成在数据库事务外完成,就必须处理任务重复、超时、模型版本变化和旧任务覆盖新结果等问题。
十一、从单机到分布式、从精确到相似的取舍
可以把数据库能力分成两条扩展轴。
11.1 精确性轴
- 主键、唯一约束、外键;
- 事务和锁;
- 可重复读、串行化;
- 同步复制;
- 跨分片事务。
越靠近强一致一端,通常越需要协调和等待,但这不是简单的“越强越好”。支付扣款需要强约束,日志搜索或推荐候选可能允许短暂不一致。
11.2 搜索轴
- 等值查询;
- 范围查询;
- 排序和聚合;
- 全文检索;
- 向量相似性;
- 结构化过滤与向量召回的混合查询。
向量检索不是关系查询的替代品。用户、租户、权限、时间范围和状态仍然适合用结构化字段表达;向量只负责表达模型空间中的相似关系。
十二、建议的完整学习路线
阶段一:关系模型和 SQL 基础
掌握:
- 关系、元组、属性、域;
- 主键、候选键、外键;
NULL和三值逻辑;SELECT、连接、聚合、子查询、窗口函数;- 约束与事务边界。
练习目标:设计用户、订单、商品、订单明细四张表,并说明每个字段的业务含义、候选键和删除策略。
阶段二:函数依赖和模式设计
掌握:
- 函数依赖;
- 部分依赖和传递依赖;
- 第一、第二、第三范式;
- 反规范化的语义边界;
- 数据异常的构造与修复。
练习目标:给出一个故意重复的订单表,分别演示更新异常、插入异常和删除异常,再拆分并恢复查询。
阶段三:执行计划和索引
掌握:
- B-tree 和复合索引;
- 最左前缀;
- 选择性与统计信息;
- 排序、连接、聚合的执行方式;
EXPLAIN与EXPLAIN ANALYZE;- 索引维护和写放大。
练习目标:对同一查询分别使用无索引、单列索引和复合索引,比较估算行数、实际行数、扫描方式和缓冲区访问。
阶段四:事务和并发控制
掌握:
- ACID 的实际边界;
- MVCC、锁和 WAL;
- 隔离级别;
- 脏读、不可重复读、幻读和写偏差;
- 死锁、超时和序列化失败;
- 事务重试与幂等。
练习目标:用两个数据库会话重现库存超卖、死锁和写偏差,再分别用行锁、约束和 SERIALIZABLE 修复。
阶段五:复制、分片和分布式事务
掌握:
- 主副本和读副本;
- 异步、同步和法定人数确认;
- 复制延迟与读写一致性;
- 分片键与热点;
- 跨分片查询;
- 2PC、Outbox 和 Saga 的边界;
- 网络分区与故障恢复。
练习目标:设计一个按租户分片的订单系统,明确单租户事务、跨租户统计、主节点故障、重复消息和副本落后的处理方式。
阶段六:向量检索
掌握:
- Embedding 的生成和版本化;
- 欧氏距离、点积、余弦相似度;
- 精确检索与 ANN;
- HNSW、IVF、PQ 的基本思想;
Recall@k和延迟评估;- 结构化过滤、权限过滤和候选扩展;
- 文档、向量、索引和副本之间的一致性。
练习目标:用同一批查询比较精确扫描与近似索引,记录召回率、延迟和过滤后的有效结果数。
十三、最终应形成的判断能力
完成这条路线后,面对一个数据库问题,至少应先问:
- 这是数据建模问题,还是查询性能问题?
- 约束应由数据库、事务逻辑还是异步流程保证?
- 查询需要精确结果,还是允许近似召回?
- 当前异常是隔离级别导致、索引导致,还是复制延迟导致?
- 多节点是为了容量、可用性、吞吐,还是故障域隔离?
- 发生失败后,系统能否识别“未执行、执行中、已提交但响应丢失”三种状态?
- 任何性能优化是否改变了数据语义、历史快照或权限边界?
数据库的核心不是某个产品的命令集合,而是对状态、约束、访问路径、并发交错和故障恢复建立可验证的模型。关系模型保证事实表达清晰,事务保证状态变化可控,索引降低访问代价,分布式机制处理节点和网络故障,向量检索则把“相似”纳入可计算的查询语义。掌握这些层次之间的因果关系,才算真正建立了数据库基础体系。
完整学习目录
一、数据库共同原理
- 关系模型与规范化:键、函数依赖、范式和反规范化边界
- SQL 完整基础:查询逻辑顺序、连接、子查询、窗口函数与集合运算
- 数据库 Schema 设计:数据类型、主外键、约束、NULL 与演进
- 数据库索引原理:B+Tree、联合索引、覆盖索引与写放大
- SQL 执行计划与查询优化:基数估计、Join 算法和慢查询诊断
- 数据库事务完整指南:ACID、隔离级别、异常现象与正确边界
- MVCC、锁与死锁:可见性、锁粒度、等待图和线上诊断
- 数据库连接与连接池:容量、超时、排队、泄漏和故障恢复
- 数据库迁移与在线变更:Expand-Contract、锁风险和可回滚发布
- 数据库备份与恢复:全量、增量、PITR、RPO/RTO 和恢复演练
- 数据库复制、分片与高可用:一致性、路由、故障转移和扩容
- 数据库安全治理:最小权限、加密、审计、脱敏与注入防护
二、MySQL
- MySQL 架构全景:连接层、优化器、执行器、存储引擎与日志
- MySQL InnoDB 存储与索引:页、聚簇索引、回表和 Buffer Pool
- MySQL 事务与锁:Read View、间隙锁、死锁和一致性读
- MySQL 查询优化:EXPLAIN、统计信息、Join、排序和慢日志
- MySQL 复制与高可用:Binlog、GTID、半同步、切换与一致性
- MySQL 生产运维:参数、容量、备份、监控与常见故障排查
三、PostgreSQL
- PostgreSQL 架构全景:进程模型、共享内存、WAL 与存储布局
- PostgreSQL 数据类型与 Schema:数组、范围、JSONB、约束和域
- PostgreSQL MVCC 与 Vacuum:Tuple 可见性、膨胀、冻结和诊断
- PostgreSQL 索引与优化器:B-tree、GIN、GiST、BRIN 和执行计划
- PostgreSQL 复制与高可用:流复制、复制槽、PITR 和故障切换
- PostgreSQL JSONB 与扩展生态:全文检索、分区、FDW 和扩展边界
四、Oracle
- Oracle 数据库架构:Instance、SGA、PGA、数据文件与后台进程
- Oracle SQL 与 PL/SQL:数据类型、包、过程、异常和批处理
- Oracle 事务与 Undo:读一致性、SCN、锁、Redo 和恢复机制
- Oracle 索引与优化器:统计信息、执行计划、Hint 和 SQL 调优
- Oracle RMAN、Data Guard 与 RAC:备份恢复和高可用边界
五、SQLite
- SQLite 架构与文件格式:嵌入式数据库、Pager、B-tree 和 VFS
- SQLite 事务与 WAL:锁状态、并发读写、Checkpoint 和忙等待
- SQLite 查询与索引:类型亲和性、查询计划、FTS5 和性能边界
- SQLite 工程实践:嵌入应用、备份、迁移、损坏恢复与安全
六、Redis
- Redis 数据结构完整指南:String、Hash、List、Set、ZSet 与 Stream
- Redis 持久化与内存:RDB、AOF、过期淘汰、Fork 和恢复
- Redis 复制、Sentinel 与 Cluster:槽位、故障转移和一致性
- Redis 缓存体系:一致性、穿透、击穿、雪崩和多级缓存
- Redis 分布式协调:锁、Lua、限流、Pub/Sub 与 Streams
七、Elasticsearch
- Elasticsearch Mapping 与分词:字段类型、Analyzer 和索引设计
- Elasticsearch 查询与聚合:Query DSL、相关性、分页和统计
- Elasticsearch 分片与集群:路由、副本、恢复、容量和故障诊断
- Elasticsearch 数据写入与运维:Bulk、Ingest、ILM、快照和升级
八、其他主流数据库
- MongoDB 文档建模:嵌入与引用、Schema、事务和一致性
- MongoDB 索引与副本集:查询计划、分片、选举和生产运维
- ClickHouse 列式建模:MergeTree、排序键、分区和数据跳过
- ClickHouse 查询与运维:批量写入、物化视图、集群和性能诊断
- SQL Server 数据库引擎:存储、事务日志、锁、索引和执行计划
- SQL Server 高可用与运维:Backup、Always On、监控和故障恢复
- Cassandra 分布式数据模型:Partition Key、Clustering 与查询驱动设计
- Cassandra 一致性与运维:复制、Quorum、Compaction、Repair 和故障
- Neo4j 图数据建模:节点、关系、属性、约束与 Cypher 基础
- Neo4j 查询优化与运维:遍历、索引、执行计划、集群和备份
- InfluxDB 时序数据:时间模型、Schema、写入、查询和保留策略
- DuckDB 嵌入式分析:列式执行、Parquet、SQL 和本地数据工程
- TiDB 分布式 SQL:计算存储分离、Raft、事务与扩缩容
- OceanBase 分布式数据库:分区、副本、事务、高可用和运维
- MariaDB Server:与 MySQL 的差异、存储引擎、复制和迁移边界
九、向量数据库
- 向量数据库基础:Embedding、距离度量、召回、过滤与一致性
- 向量索引原理:Flat、HNSW、IVF、PQ 的精度、内存和延迟
- pgvector 实战:PostgreSQL 向量类型、HNSW、IVFFlat 与混合查询
- Milvus 完整基础:Collection、Segment、索引、查询和集群部署
- Qdrant 实战:Collection、Payload 过滤、HNSW、分片和快照
- Weaviate 基础:Collection、Vectorizer、过滤、混合检索和多租户
- Pinecone 托管向量库:Index、Namespace、Metadata 与容量成本
- 混合检索与 RAG 数据层:全文、向量、融合、重排和引用
- 向量数据库生产运维:摄取、版本、评测、备份、权限和成本
十、SQL 深入专题
- SQL 查询逻辑处理顺序:FROM、WHERE、GROUP、HAVING、SELECT 和 ORDER
- SQL Join 完整指南:Inner、Outer、Semi、Anti、算法和陷阱
- SQL 子查询与 CTE:相关子查询、递归、物化和优化边界
- SQL 窗口函数:分区、排序、Frame、排名、累计和间隔分析
- SQL 聚合与集合:GROUPING SETS、ROLLUP、CUBE、UNION 和去重
- SQL 数据写入:INSERT、UPDATE、DELETE、MERGE、Upsert 与并发正确性
- SQL 批处理与分页:游标、Keyset、批量写、限速和断点恢复
- 数据库协议与驱动:连接握手、预处理、结果流、取消和兼容性
- 数据库测试体系:事务测试、迁移测试、Testcontainers 和故障注入
- 数据库可观测性:连接、事务、锁、执行计划、复制和 SLO
- 数据库容量规划:工作集、缓存命中、IOPS、连接、增长和压测
- 多租户数据库设计:共享表、独立 Schema、独立库和数据隔离
- 时态数据与审计历史:有效时间、系统时间、版本表和可追溯性
- 数据库 CDC:日志捕获、Debezium、Schema 演进、顺序和重复消费
- OLTP、OLAP 与 Lakehouse:工作负载、存储布局和数据链路选型
十一、MySQL 深入
- MySQL 数据类型、字符集与排序规则:精度、编码和索引影响
- MySQL 表设计:主键、行格式、NULL、生成列、分区与归档
- MySQL Redo、Undo 与 Binlog:提交链路、崩溃恢复和一致性
- MySQL Buffer Pool 与内存结构:页缓存、刷脏、Change Buffer 和 AHI
- MySQL 分区表:Range、List、Hash、裁剪、维护和适用边界
- MySQL SQL 实战:分页、批量、Upsert、JSON、窗口函数和锁定读
- MySQL 备份恢复:逻辑备份、物理备份、Binlog 与 PITR 演练
- MySQL 用户、角色与安全:认证插件、权限、TLS、审计和密钥
- MySQL 协议与客户端:握手、Prepared Statement、连接池和取消
- MySQL 分片与 Vitess:路由、VSchema、重分片和跨分片事务
- MySQL 升级与迁移:兼容检查、DDL、字符集、灰度和回滚
十二、PostgreSQL 深入
- PostgreSQL 高级 SQL:LATERAL、递归 CTE、窗口、数组和范围
- PostgreSQL 存储内部:Heap Page、Tuple、TOAST、FSM 和可见性图
- PostgreSQL WAL 与 Checkpoint:提交、崩溃恢复、归档和写入性能
- PostgreSQL 锁与 Serializable SSI:谓词冲突、死锁和咨询锁
- PostgreSQL 分区表:规划、裁剪、索引、维护和在线迁移
- PostgreSQL 全文与模糊检索:tsvector、GIN、Trigram 和相关性
- PostgreSQL 逻辑复制与 CDC:Publication、Slot、顺序和 Schema
- PostgreSQL 备份恢复:pg_dump、Base Backup、WAL 归档和 PITR
- PostgreSQL 监控与诊断:pg_stat、锁、膨胀、慢查询和容量
- PostgreSQL 参数与性能:内存、WAL、Planner、连接和 Autovacuum
- PostgreSQL PostGIS:空间类型、坐标系、空间索引和查询优化
十三、Oracle 与 SQLite 深入
- Oracle Schema 与数据类型:NUMBER、字符、日期、LOB 和对象
- Oracle 存储管理:Tablespace、Segment、Extent、Block 和 ASM
- Oracle Redo、归档与 Checkpoint:提交、恢复和写入路径
- Oracle 锁与隔离:行锁、ITL、读一致性、死锁和诊断
- Oracle 分区与大表治理:Range、List、Hash、交换和裁剪
- Oracle 物化视图与查询重写:刷新、日志、一致性和性能
- Oracle 安全治理:用户、角色、Profile、TDE、审计和最小权限
- Oracle 监控与调优:AWR、ASH、等待事件、统计和容量
- Oracle Data Pump 与迁移:导出导入、字符集、校验和停机窗口
- SQLite 类型系统与 STRICT 表:亲和性、存储类、约束和兼容
- SQLite JSON、FTS5 与扩展:虚拟表、Tokenizer 和安全加载
- SQLite Backup API 与在线复制:快照、一致性、锁和恢复验证
- SQLite 安全边界:文件权限、注入、扩展、加密选择和不可信数据库
十四、Redis 与 Elasticsearch 深入
- Redis 内核与事件循环:命令执行、IO 线程、阻塞点和延迟
- Redis 对象编码与内存:SDS、Dict、Listpack、碎片和大 Key
- Redis Pipeline、事务与 Lua:原子性、脚本缓存和集群边界
- Redis 过期与 Keyspace 通知:惰性删除、主动删除、事件和可靠性
- Redis Search 与向量检索:索引、查询、Hybrid 和容量边界
- Redis 监控与延迟诊断:Slowlog、Latency、内存、复制和热点
- Lucene 与倒排索引内部:Segment、Term、Posting、Merge 和 Cache
- Elasticsearch 中文检索:分词器、词典、同义词、拼音和版本治理
- Elasticsearch 相关性调优:BM25、Boost、Function Score 和评测集
- Elasticsearch 深分页与一致性:search_after、PIT、Scroll 和导出
- Elasticsearch 安全与多租户:TLS、角色、文档权限、审计和隔离
- Elasticsearch 快照、恢复与升级:兼容、重建索引、灰度和回滚
十五、分布式与数据治理
- 分布式数据库一致性模型:线性一致、顺序一致、因果和最终一致
- 数据库共识基础:Raft、Paxos、Leader、日志复制和成员变更
- 分布式数据库中的时间与 ID:时钟、序列、Snowflake、UUID 和顺序
- 数据库代理与网关:连接复用、读写路由、分片、审计和故障边界
- 云托管数据库选型:责任边界、高可用、扩缩容、成本和退出策略
- 数据质量与治理:血缘、口径、约束、校验、责任人和变更审计
- 数据库隐私与数据生命周期:分类、最小化、脱敏、留存和删除
- ETL 与 ELT 数据管道:批流处理、幂等、补数、校验和可观测
- 数据契约与 Schema Registry:兼容模式、演进、验证和消费者治理
- 数据库故障演练:延迟、断网、磁盘满、主库切换和恢复验证
系列导航与关联阅读
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论