引言:数据库不只是存放数据的容器

文件、JSON、CSV 和电子表格都能保存数据,为什么软件系统还需要数据库?

因为“把数据写下来”只是问题的起点。只要数据需要被多个请求持续读取和修改,我们就必须继续回答:数据应该遵守什么结构?两个文件中的记录如何保持关联?两个人同时购买最后一件商品时该听谁的?程序写到一半崩溃后,哪些修改算成功?数据越来越多以后,怎样快速找到需要的记录?

数据库管理系统正是为这些问题而设计的。它不仅负责存储数据,还负责组织结构、执行查询、检查约束、协调并发、记录修改并在故障后恢复。

本文是数据库系列的开篇。我们不绑定 MySQL、PostgreSQL 等具体产品,也不深入某一种索引或事务实现,而是先建立一张完整的认知地图:

为什么需要数据库
    ↓
关系模型如何描述数据
    ↓
关系型数据库提供哪些核心能力
    ↓
数据库系统内部如何协作
    ↓
数据库为什么面临并发、性能、可靠性和扩展问题
    ↓
主流产品之间应该如何理解和选择

1. 为什么应用最终会需要数据库

在原型或个人工具中,把少量配置写进 JSON、把统计结果导出为 CSV,通常完全合理。问题不在于“文件不能保存数据”,而在于业务增长后,应用开始承担文件格式本身没有提供的管理责任。

以一个小型电商系统为例。最初,我们可以把用户、商品和订单分别保存在几个文件中。随着功能增加,很快会遇到下面的问题:

  • 同一个用户的姓名和地址出现在多个订单文件中,修改资料时容易遗漏。
  • 订单引用了一个已经删除的用户,但文件系统不会阻止这类非法引用。
  • 查询某位用户最近一个月购买的商品,需要读取并组合多个文件。
  • 两个请求同时读取库存为 1,又各自写回库存为 0,系统实际上卖出了两件商品。
  • 创建订单时,订单文件已经写入,库存文件却写入失败,业务停在“订单存在但库存未扣减”的中间状态。
  • 程序在覆盖文件的过程中崩溃,应用需要自行判断文件是否完整以及如何恢复。
  • 权限、审计、备份、迁移和数据演进逐渐变成一套新的基础设施。

当然,开发者可以继续在应用层解决这些问题:设计文件锁、维护索引、编写恢复脚本、实现权限检查。但是,当这些能力成为许多系统的共同需求时,将它们集中到一个经过长期验证的数据管理系统中更合理。

为什么需要数据库:从文件存储到关系型数据库

因此,数据库的核心价值不是“比文件更高级”,而是管理长期存在、持续变化、被并发共享的数据状态。关系型数据库则是在这种需求上,以关系模型作为组织数据的基础。

2. 什么是关系型数据库

讨论关系型数据库时,最好先区分三个层次:

  • 关系模型(Relational Model) 是一套描述数据结构、完整性约束和数据操作的理论模型。
  • 关系型数据库(Relational Database) 是按照关系模型组织的数据集合。
  • 关系型数据库管理系统(RDBMS) 是创建、查询、修改、保护和恢复这些数据的软件系统,例如 PostgreSQL、MySQL 和 SQLite。

日常交流中,人们常把“关系型数据库”和“RDBMS”混用,这通常不影响沟通。但在理解原理时,区分模型、数据与管理软件很重要:表和键来自数据模型,解析器、优化器、缓存与恢复机制则属于数据库系统的工程实现。

可以把关系型数据库概括为:

关系型数据库以关系模型为基础,将数据表示为若干关系,并通过键、约束和关系运算组织、关联与查询数据;RDBMS 则负责让这些操作在并发和故障环境中可靠执行。

这里最容易误解的是“关系”一词。关系型数据库中的 Relation 首先是数学意义上的关系,不是“两个业务对象之间存在联系”的简称。用户与订单之间的业务联系叫 Relationship,它可以借助键和中间关系表达,但两者不是同一个概念。

3. 从现实世界到关系模型

现实世界中有用户、商品、订单等对象。数据建模的任务,是选择哪些事实需要记录,以及用什么结构表达这些事实。

在电商示例中,我们可以得到四个关系:

  • users:用户是谁。
  • products:系统销售哪些商品。
  • orders:某位用户创建了哪张订单。
  • order_items:一张订单包含哪些商品、数量和成交单价。

现实世界如何映射为关系模型

3.1 关系、元组、属性与域

关系(Relation) 通常表现为一张表。它描述同一类事实,例如 products 关系描述商品。

元组(Tuple) 通常表现为表中的一行。一行商品数据表示一个具体商品。

属性(Attribute) 通常表现为一列,例如商品的 namepricestock

域(Domain) 表示属性允许取值的集合。商品价格可以被定义为非负的小数,库存可以被定义为非负整数。数据库中的数据类型和 CHECK 等约束,都是表达域规则的常用方式。

这里还有两个经常被表格界面掩盖的事实:

  1. 关系中的元组没有天然顺序。SQL 查询如果没有显式使用 ORDER BY,数据库不承诺结果按主键、插入时间或磁盘位置排列。
  2. 纯关系模型中的关系是元组集合,不包含重复元组;SQL 实现通常采用更接近多重集合的语义,查询结果可能重复,因此需要键、DISTINCT 或其他约束表达唯一性。

3.2 键与实体之间的联系

候选键(Candidate Key) 是能够唯一标识一行的最小属性集合。一张表可能有多个候选键,例如用户的内部编号和经过验证的唯一邮箱都可能具备唯一性。

主键(Primary Key) 是被选作主要标识的候选键。主键的意义是稳定地标识事实,不等于“必须使用自增整数”。

外键(Foreign Key) 声明一列或多列引用另一个关系中的候选键,用来维护引用完整性。例如 orders.user_id 引用 users.id,数据库便可以拒绝属于不存在用户的订单。

用户与订单是一对多:一个用户可以有多张订单,一张订单属于一个用户。订单与商品则是多对多:一张订单可以包含多个商品,同一个商品也可以出现在多张订单中。order_items 把这个多对多联系展开成两个一对多联系,并同时记录数量和成交单价。

下面是一组简化的表定义。示例采用接近 SQL 标准的写法,不同数据库对自增列、时间类型和标识符的语法可能略有差异:

CREATE TABLE users (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  email VARCHAR(320) NOT NULL UNIQUE,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE products (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name VARCHAR(200) NOT NULL,
  price NUMERIC(12, 2) NOT NULL CHECK (price >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);

CREATE TABLE orders (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  user_id BIGINT NOT NULL REFERENCES users(id),
  status VARCHAR(20) NOT NULL
    CHECK (status IN ('pending', 'paid', 'cancelled')),
  created_at TIMESTAMP NOT NULL
);

CREATE TABLE order_items (
  order_id BIGINT NOT NULL REFERENCES orders(id),
  product_id BIGINT NOT NULL REFERENCES products(id),
  quantity INTEGER NOT NULL CHECK (quantity > 0),
  unit_price NUMERIC(12, 2) NOT NULL CHECK (unit_price >= 0),
  PRIMARY KEY (order_id, product_id)
);

这段定义不仅创建了四个“容器”,还声明了数据之间必须长期成立的规则。数据库可以利用这些规则阻止一部分非法状态,而不必相信每一个调用方都正确实现了相同检查。

4. 表、键、约束与规范化

Schema 不只是列名清单,它是数据库对数据形状和有效状态的正式约定。

  • NOT NULL 表示某个事实不可缺失。
  • UNIQUE 防止本应唯一的值重复。
  • CHECK 限定单行数据必须满足的条件。
  • PRIMARY KEY 提供稳定且唯一的行标识。
  • FOREIGN KEY 保证引用目标存在,并规定目标被更新或删除时的行为。
  • 数据类型限定值的基本表示范围。

例如,下列写入会失败,因为订单引用的用户不存在:

INSERT INTO orders (id, user_id, status, created_at)
VALUES (10001, 99999, 'pending', CURRENT_TIMESTAMP);

但数据库约束不是全部业务规则。数据库很容易检查 quantity > 0,却未必知道“新用户首单最多使用一张新人券”或“特定地区周末暂停配送”。复杂规则可能由数据库约束、事务、应用服务和跨系统协议共同维护。好的设计不是把所有规则都塞进数据库,而是让关键不变量在最可靠、最接近数据的位置得到保护。

4.1 为什么需要规范化

假设每张订单都完整复制用户姓名、邮箱和当前地址。当用户修改邮箱时,历史订单中的多份副本可能只更新了一部分,系统便无法回答哪一份才是权威数据。这类问题称为更新异常;类似地,还可能出现插入异常和删除异常。

规范化(Normalization)通过识别不同事实的依赖关系,将职责不同的数据放进合适的关系,以减少没有必要的重复和修改异常。在当前模型中:

  • 用户资料的权威值保存在 users
  • 商品当前价格和库存保存在 products
  • 订单属于哪个用户保存在 orders
  • 购买数量和成交时单价保存在 order_items

order_items.unit_priceproducts.price 看起来重复,却表达不同事实:前者是成交时的历史价格,后者是商品当前价格。保留历史快照不是随意冗余,而是业务语义的一部分。

第一范式、第二范式和第三范式提供了逐步分析数据依赖的方法,但规范化不是“表拆得越多越好”。报表、搜索和高频读取有时会有意识地保存派生结果,这叫反规范化。关键是明确权威来源、同步方式和允许的不一致窗口,而不是机械地追求零重复。

5. 关系型数据库的核心能力

关系模型回答数据如何表达,RDBMS 还必须提供一组让数据真正可用的能力。

关系型数据库核心能力全景

5.1 声明式查询

SQL 的重要特征是声明式:使用者描述想得到什么,数据库决定怎样得到。下面的查询表达“找出某位用户最近创建的订单及其商品”,却没有指定先读哪张表、使用哪种 Join 算法或是否使用索引:

SELECT
  o.id AS order_id,
  o.created_at,
  p.name AS product_name,
  oi.quantity,
  oi.unit_price
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.id
JOIN products AS p ON p.id = oi.product_id
WHERE o.user_id = 42
ORDER BY o.created_at DESC;

数据库会结合 SQL 结构、可用索引和统计信息生成执行计划。数据分布或索引变化后,同一条 SQL 可能使用不同计划,而应用不必重写成另一套逐行读取流程。

5.2 完整性约束

约束是数据库能够理解并强制执行的数据规则。它们的价值不只在于提前报错,还在于形成统一边界:无论写入来自 Web 服务、批处理脚本还是管理工具,都必须满足相同规则。

约束也能为开发者提供明确反馈。重复邮箱会触发唯一约束,负数库存会触发检查约束,无效用户会触发外键约束。相比让错误数据悄悄进入系统,在写入边界失败通常更容易定位和修复。

5.3 事务与 ACID

创建订单往往包含多项修改:写入订单、写入明细、扣减库存。这些语句在业务上属于一个工作单元,任何一步失败都不应该留下半完成状态。

BEGIN;

INSERT INTO orders (id, user_id, status, created_at)
VALUES (10001, 42, 'pending', CURRENT_TIMESTAMP);

INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES (10001, 7, 2, 199.00);

UPDATE products
SET stock = stock - 2
WHERE id = 7 AND stock >= 2;

-- 以下两条由应用根据 UPDATE 的影响行数二选一:
-- COMMIT;   -- 影响行数等于 1
-- ROLLBACK; -- 影响行数等于 0

实际应用还必须检查 UPDATE 是否真的更新了一行;如果库存不足,应回滚而不是继续提交。事务不会替应用自动推导业务成功条件。

ACID 描述事务通常需要具备的四类性质:

  • Atomicity(原子性) :事务中的修改作为整体提交或回滚,避免只留下部分结果。
  • Consistency(一致性) :事务在数据库约束与应用不变量共同作用下,从一个有效状态进入另一个有效状态。
  • Isolation(隔离性) :并发事务之间可以看到什么,由隔离级别和具体实现控制;它不是“并发事务完全互不影响”的无条件承诺。
  • Durability(持久性) :提交结果能够在数据库及其部署方案承诺的故障范围内恢复。

因此,“使用了事务”不等于业务必然正确。应用仍要划分合理的事务边界、检查影响行数、选择隔离级别,并处理锁等待、死锁、序列化失败和重试。

5.4 索引与高效访问

没有合适索引时,数据库可能需要检查大量行才能找到少量结果。索引维护从键值到数据位置的辅助结构,使数据库有机会缩小访问范围。

例如,系统经常按用户和时间查询订单,可以考虑在 orders(user_id, created_at) 上建立联合索引。但索引不是免费的:它占用空间,每次插入、更新和删除都可能需要同步维护;低选择性条件或返回大量行的查询,也不一定适合索引扫描。

“为查询建立索引”只是起点。真正的判断需要结合执行计划、数据分布和真实负载,这会在索引与查询优化专题中展开。

5.5 安全、权限与审计

关系型数据库通常提供身份认证、用户与角色、对象权限和审计能力。权限可以限制某个角色能否连接数据库、访问指定 Schema,或者对某张表执行 SELECTINSERTUPDATEDELETE。部分产品还提供列级或行级访问控制。

数据库权限不是应用权限系统的替代品。应用仍然需要判断“当前用户能否修改这张订单”等业务规则;数据库则负责限制连接它的账号可以触及哪些数据和操作。两层边界共同存在,可以降低单个应用缺陷直接扩大为全库越权的风险。

6. 关系型数据库系统的通用架构

一条 SQL 背后并不是一个单独的“读表函数”,而是一组协作模块。

关系型数据库通用架构

这张图是一种通用认知模型,不代表所有产品都有完全相同的进程、线程或模块边界。PostgreSQL、MySQL 等客户端—服务器数据库通常由独立服务进程管理连接和存储;SQLite 则嵌入应用进程,直接读写数据库文件。它们仍然需要处理 SQL、事务和存储,只是部署形态与内部边界不同。

6.1 连接与会话

客户端首先通过驱动或协议建立连接。数据库在会话中维护身份、权限、事务状态、参数和临时对象等上下文。连接池可以复用连接,但不会消除数据库对并发会话的资源管理。

6.2 解析、规划与执行

解析器检查 SQL 语法并形成内部结构;语义分析确认表、列、类型和权限是否有效。优化器再枚举或比较候选方案,例如先访问哪张表、使用顺序扫描还是索引扫描、采用哪种 Join 方法,并依据统计信息估算成本。

执行器按照计划调用扫描、过滤、连接、聚合和排序等算子。优化器选择的是预计成本较低的计划,不是对所有运行时情况都绝对最快的计划;统计信息过期、参数分布倾斜或估算模型有限,都可能导致误判。

6.3 事务、并发与恢复

事务管理并不是 SQL 处理完成后的单独步骤,它贯穿读取和写入。数据库需要判断当前事务能看到哪些版本、写入是否与其他事务冲突、何时等待或回滚,以及提交前哪些日志必须持久化。

锁和 MVCC 是常见机制,但两者不是互相替代的单选项。许多数据库同时使用多版本数据支持并发读取,又使用不同粒度的锁保护结构或协调写冲突。隔离级别决定允许哪些并发现象,恢复机制则负责在进程或机器故障后重建一致状态。

6.4 缓冲区、数据页、索引与日志

数据库通常以页为单位组织磁盘数据,并在内存缓冲区中缓存热点页。索引提供额外的访问路径,日志以适合恢复的形式记录修改。使用预写日志时,描述修改的日志需要先于相关数据页达到持久化要求。故障恢复时,数据库可以根据日志重放尚未写入数据页的修改,并结合事务状态或 Undo 等机制识别和处理未完成事务;具体流程因产品而异。

缓存提高了性能,也使“事务提交”和“所有数据页立刻写回磁盘”不再是同一件事。检查点、日志刷新策略、存储设备保证和副本配置共同决定性能与故障边界。

6.5 从一条 SQL 看模块如何协作

以下查询用于查看用户最近十张订单:

SELECT id, status, created_at
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
FETCH FIRST 10 ROWS ONLY;

这里使用接近 SQL 标准的限制行数写法;MySQL、SQLite 和 SQL Server 等产品可能使用 LIMITTOPOFFSET ... FETCH 等不同语法。

它在数据库内部大致经历以下过程:

  1. 连接层接收 SQL,并确认当前会话是否有查询权限。
  2. 解析与语义分析识别 orders、列名、条件和排序要求。
  3. 优化器结合统计信息和索引,选择扫描、过滤与排序方案。
  4. 执行器启动计划中的算子,并向事务系统请求当前语句可见的数据版本。
  5. 存储层从缓冲区取得数据页;未命中时再读取持久化存储。
  6. 执行器完成过滤、排序或索引有序读取,将前十行返回客户端。

这条路径足以把架构模块串起来,但还没有回答优化器如何估算成本、B+Tree 如何定位页、MVCC 如何判断可见性等问题。它们值得分别展开,而不是挤进一篇开篇文章。

7. 数据库系统面对的核心技术挑战

数据库的许多复杂机制,都可以追溯到四类工程问题:并发、性能、可靠性和扩展性。

关系型数据库核心技术挑战

7.1 并发:正确性与吞吐量之间的平衡

两个事务同时修改同一商品库存,可能产生丢失更新;两个转账事务以不同顺序锁定账户,可能互相等待形成死锁;一个事务连续读取两次,也可能在另一个事务提交后看到不同结果。

数据库使用锁、MVCC、隔离级别、冲突检测和事务重试等机制控制这些现象。更严格的隔离通常减少应用能够观察到的并发异常,但可能增加锁等待、版本维护、冲突回滚或重试成本。正确做法不是一律选择最强或最弱隔离,而是先确定业务不变量,再选择能够保护它的机制。

7.2 性能:不存在对所有查询都最优的结构

顺序扫描适合读取表中很大比例的数据,索引扫描适合定位较少记录;嵌套循环、哈希连接和归并连接各有适用条件。数据库需要根据统计信息估算行数、I/O 和 CPU 成本,再选择计划。

索引可以加速读取,却增加写放大和存储占用;缓存减少磁盘访问,却受到内存容量和淘汰策略限制;反规范化减少 Join,却提高同步复杂度。性能优化本质上是针对真实访问模式做权衡,应使用 EXPLAIN、慢查询记录和生产负载证据,而不是套用“有索引就快”的规则。

7.3 可靠性:日志、备份与复制解决不同问题

进程崩溃、操作系统故障、磁盘损坏、误删除和机房故障不是同一种风险。预写日志和崩溃恢复主要帮助数据库恢复事务状态;检查点控制恢复所需处理的日志范围;副本可以提高可用性或提供冗余;备份与时间点恢复则用于应对数据损坏、误操作和更长时间范围的恢复需求。

复制不能替代备份:误删除可能被忠实地复制到所有副本。备份也不等于已经能够恢复:只有定期执行恢复演练,才能验证备份文件、密钥、日志链和操作流程确实可用。

7.4 扩展性:把数据分开会引入新的协调成本

单机可以通过更快的 CPU、更多内存和更好的存储纵向扩展,也可以通过读副本分担部分读取。表分区把一张逻辑表拆成多个分区,便于裁剪扫描范围和管理数据;分片则把数据分布到多个独立节点,扩大容量或写吞吐。

这些概念不能混为一谈。分区不一定跨机器,读副本通常不能扩展单主写入,分片则会让跨分片事务、全局唯一性、Join、再均衡和故障处理变复杂。分布式关系型数据库尝试把一部分协调封装进系统,但网络延迟、一致性和可用性的取舍不会因此消失。

8. 主流关系型数据库生态

关系模型是一套共同基础,不等于某个具体产品。不同 RDBMS 在许可证、部署形态、扩展机制、工具链和兼容语法上各有选择。

代表性关系型数据库及其常见定位

产品 可核验的基本定位 选型时需要继续确认
PostgreSQL 开源 RDBMS,强调可扩展性并持续支持 SQL 标准能力 扩展、托管方案、团队运维经验
MySQL 提供开源 Community Edition,广泛用于 Web 与通用事务应用 版本、存储引擎、发行版和商业支持
SQLite 嵌入应用进程、无独立数据库服务、主数据库通常是单文件 并发写入、网络共享和服务端访问需求
Oracle Database 商业数据库产品,提供集成的数据管理与运维能力 许可证、版本功能、现有 Oracle 技术栈
SQL Server 商业 RDBMS,使用 T-SQL,可部署在 Windows、Linux、容器和 Azure 等环境 版本授权、Microsoft 数据平台集成
MariaDB 开源 RDBMS,源自 MySQL,保留较高生态兼容性但已独立演进 具体版本的 SQL、复制、驱动和工具兼容性

这张表不是产品排名,也不表示某个产品只能用于所列场景。“企业级”“高可靠”“生态成熟”如果没有版本、部署和衡量指标,只是无法比较的口号。选型时应把问题具体化:系统部署在哪里?需要哪些 SQL 和事务能力?团队熟悉什么?许可证和支持成本是多少?如何备份、升级和监控?现有驱动、ORM 与数据平台能否兼容?

产品也在持续演进。同一个名称之下可能有社区版、商业版、云托管版和不同发布周期,最终决定必须依据目标版本的官方文档和实际验证。

9. 关系型数据库与 NoSQL 的关系

NoSQL 不是一个具有统一数据模型和一致性语义的产品类别。键值、文档、宽列和图数据库解决的问题并不相同,因此“关系型数据库还是 NoSQL”通常只是讨论的开头。

对比维度 关系型数据库 常见 NoSQL 数据库
数据模型 关系、键、约束与关系运算 键值、文档、宽列、图等不同模型
Schema 通常显式定义并通过迁移演进 常允许更灵活的记录结构,也可能提供 Schema 校验
关联查询 SQL、Join 和聚合是核心能力 常围绕聚合边界或特定访问路径设计,能力因产品而异
事务与一致性 普遍提供事务和多种隔离级别 支持范围、一致性选项和事务语义因产品而异
扩展方式 单机、复制、分区、分片或分布式产品 许多产品从特定模型出发设计水平扩展
主要代价 Schema 演进、Join 和分布式协调需要设计 跨记录约束、临时查询或多模型组合可能更依赖应用

“Schema 灵活”不等于没有 Schema:数据结构仍然存在,只是约束可能从数据库转移到应用、序列化格式或校验层。“支持水平扩展”也不是 NoSQL 独占能力,许多关系型产品同样支持复制、分片或分布式部署,只是具体语义和成本不同。

真实系统经常采用多种存储:关系型数据库保存订单和账户等核心状态,Redis 承担缓存或特定实时结构,搜索引擎建立全文检索索引,对象存储保存大文件。合理的多存储架构不是“每种数据库都用一点”,而是为每份数据明确权威来源、同步方式和故障处理策略。

10. 为什么关键业务仍大量依赖关系型数据库

订单、账户、库存、权限和工作流有一个共同特点:数据之间存在明确关联,错误状态需要被尽早阻止,多项修改经常需要作为一个工作单元提交,而且数据会经历多年的查询、审计和结构演进。

关系模型、约束、事务和声明式查询恰好为这些需求提供了通用基础。它们让系统能够明确表达“什么数据存在”“数据之间如何关联”“哪些状态不允许出现”,也让后来的查询不必完全受最初写入接口限制。

这不意味着所有数据都应该放进同一个关系型数据库。缓存、消息系统、搜索引擎和对象存储仍有各自价值。关键是让关系型数据库承担它擅长的共享业务状态,并明确其他组件与权威数据之间的边界。

11. 总结:关系型数据库究竟是什么

关系型数据库不是“用表格保存数据的软件”。更准确地说,它以关系模型表达事实,通过键和约束定义有效状态,通过 SQL 声明查询与修改,再由数据库管理系统使用事务、并发控制、索引、缓存、日志和恢复机制,让这些操作能够在真实工程环境中执行。

可以用五句话回顾全文:

  1. 关系模型定义数据如何表示,Relation 不等于实体之间的 Relationship。
  2. 键和约束让一部分业务不变量成为所有写入者共同遵守的数据边界。
  3. SQL 描述目标,优化器和执行器决定具体访问路径。
  4. 事务、锁、MVCC、日志与恢复共同处理并发和故障,但每种保证都有边界与成本。
  5. PostgreSQL、MySQL、SQLite 等产品共享关系模型基础,却在部署、功能、许可证和工程实现上各有选择。

后续文章将继续深入 SQL 执行、索引、事务、并发控制、恢复与分布式数据库等主题。

参考资料