数据世界的起点:什么是数据库?
第 1 章 数据世界的起点:什么是数据库?
想象这样一个场景:你正在手机上玩一款开放世界游戏,角色已经升到 87 级,背包里堆满了稀有装备。突然,手机因为电量耗尽自动关机。你慌忙找到充电器,重新启动游戏——却发现所有数据都消失了,角色回到了 1 级。这种"数据消失"(data loss)的噩梦,在数字世界里每天都在发生。无论是手机相册里突然不见的生日照片,还是写作业时 Word 文档突然崩溃,都指向同一个根本问题:我们该如何让数据"活"在时间中,让它在断电、崩溃、误操作之后依然存在? This is the foundational question that drives the entire field of database systems.
数据库(Database)正是人类为回答这一问题而发明出来的工具。但它的意义远不止"把数据存起来"这么简单。在本章中,我们将一起探索:为什么不能直接用文件来保存数据?数据库管理系统(DBMS, Database Management System)究竟在幕后做了什么?以及为什么 SQL 这种"声明式语言"(declarative language)能够改变整个软件工程的样貌?Let's begin our journey into the data kingdom.
1.1 数据管理三难困境:持久性、并发性与可扩展性
在回答"什么是数据库"之前,我们必须先理解数据管理面临的三大根本性挑战。我把它们称为"三难困境"(the data trilemma),因为它们之间存在微妙的张力,几乎所有数据库系统的设计都是在三者之间做权衡(trade-off)。
1.1.1 持久性(Durability):让数据穿越断电与崩溃
持久性意味着,一旦数据被确认写入,它就必须在那里——无论之后发生什么:突然断电(power failure)、操作系统崩溃(OS crash)、磁盘损坏、机房失火,都不能让它凭空消失。Durability is the "P" in the famous ACID properties, which we will dissect in detail in Chapter 4.
对于青少年读者来说,最直观的类比是"游戏存档"。你玩《塞尔达传说》时,每到一个篝火就保存一次进度。这个"保存"动作,本质上是在做一次持久化操作。但和游戏不同,真实业务系统要求每一次操作都必须持久化。试想一下:如果你正在用支付宝转账 1000 元给朋友,系统告诉你"转账成功",但三天后银行账户里钱没到账——这就是持久性失败。在数据库领域,这种"看似成功但实际未持久化"的灾难被称为丢失更新(lost update),它是数据库工程师的噩梦。
要实现真正的持久性,系统必须做两件事:第一,把数据写入非易失性存储(non-volatile storage),最常见的是硬盘(HDD)或固态硬盘(SSD);第二,在写入过程中采用预写日志(WAL, Write-Ahead Logging)——先记下"我要做什么",再真正执行。This way, even if the system crashes mid-operation, it can recover by replaying the log. 这种"双重保险"机制,是任何一个严肃数据库的标配。
1.1.2 并发性(Concurrency):当 1000 个用户同时下单
第二个挑战是并发性。假设你在运营一个电商网站,2024 年双十一零点整,预计有 100 万用户同时点击"购买"按钮。如果系统一次只能处理一个用户的请求,队列就会排到明年。但如果让 100 万个请求"同时"执行,又会出现新的问题:两个用户同时购买同一件限量商品,库存明明只有 1 件,结果两个订单都成功了,商家就要亏本发货。
这就是著名的超卖问题(overselling problem),也是并发控制的经典案例。数据库必须提供一种机制,让多个事务(transaction)能够"看似同时"执行,却不会破坏数据的一致性(consistency)。这种机制叫做并发控制(concurrency control),最常见的实现方式有两类:悲观锁(pessimistic locking)和多版本并发控制(MVCC, Multi-Version Concurrency Control)。
MVCC 的核心思想非常优雅:每当一行数据被修改时,数据库不直接覆盖旧值,而是生成一个新的版本(version),同时保留旧版本。这样,读操作和写操作就不会互相阻塞——读者看到的是某个时间点的"快照"(snapshot),写者在自己的版本上工作,互不干扰。PostgreSQL 和 MySQL 的 InnoDB 引擎都采用了这种设计。We will explore the magic of MVCC in Chapter 7.
1.1.3 可扩展性(Scalability):从 1 GB 到 1 PB
第三个挑战是可扩展性。今天你的应用只有 100 个用户,数据量 1 GB,存放在一台服务器上完全够用。但半年后用户涨到 1000 万,数据量飙升到 1 PB($10^{15}$ 字节,相当于 100 万 GB),原来的单台服务器已经远远不够用。此时你就面临水平扩展(horizontal scaling,加机器)和垂直扩展(vertical scaling,加配置)的选择。
对于关系型数据库(如 MySQL、PostgreSQL)来说,水平扩展是一个巨大的挑战,因为数据之间存在复杂的关联(foreign key、join 等),把它们分散到多台机器上会破坏 ACID 特性。这就是为什么近年来出现了分布式数据库(distributed database)和NewSQL 运动——它们试图在保持 SQL 和 ACID 的同时,实现水平扩展。CockroachDB、TiDB、Google Spanner 都是这条路上的先锋。We will dive into the CAP theorem and distributed transactions in Chapter 12.
理解了这三难困境,你就抓住了数据库系统设计的"灵魂三角"。接下来我们要回答的是:为什么简单的文件系统解决不了这些问题?
1.2 DBMS vs. 文件系统:抽象层次的跃迁
一个自然的问题是:操作系统已经提供了文件系统(file system)——Windows 上的 NTFS、macOS 上的 APFS、Linux 上的 ext4——我们为什么还需要数据库?直接用文件来存数据不行吗?
答案是:能,但代价巨大。让我们用一个具体例子来说明。
1.2.1 用文件存数据会怎样?
假设你决定用文本文件(CSV 格式)来保存学生信息:
```csv id,name,age,major,gpa 1,Alice,20,Computer Science,3.8 2,Bob,21,Mathematics,3.5 3,Charlie,19,Physics,3.9 ```
这种格式简单直观,Excel 就能打开。但当你开始写代码查询"所有 GPA 大于 3.7 的计算机科学学生"时,问题就来了:
1. 解析开销:每次查询都要把整个文件读入内存,逐行解析。如果文件有 10 GB,这一读就要等几十秒。 2. 并发灾难:当用户 A 在修改文件时,用户 B 同时在读取,可能读到半新半旧的数据——这就是脏读(dirty read)。 3. 崩溃无恢复:如果程序在写入过程中突然崩溃,文件可能处于损坏状态,部分行写入而部分行没有。 4. 无索引:每次查询都是"全表扫描"(full table scan),没有高效的方式找到"GPA=3.9"的学生。 5. 无安全控制:任何能访问文件的人都能修改它,无法细粒度地控制"谁能读、谁能写哪个字段"。
数据库管理系统正是为了解决这些痛点而诞生的。它在文件系统之上,构建了一个更高层次的抽象(abstraction),把"如何存"和"如何找"这两件事做得又快又稳。
1.2.2 DBMS 的核心职责
一个完整的 DBMS 通常承担以下职责:
- **存储管理**(Storage Management):把数据组织成页面(page),通过 B 树、B+ 树等数据结构建立索引(index),让查询从 $O(N)$ 降到 $O(\log N)$。
- **事务管理**(Transaction Management):通过日志(log)和锁(lock)保证 ACID 特性。
- **查询处理**(Query Processing):把用户写的 SQL 解析、优化、执行。优化器(optimizer)会自动选择最高效的执行计划。
- **并发控制**(Concurrency Control):让多个用户同时操作而不冲突。
- **恢复机制**(Recovery):系统崩溃后能自动恢复到一致状态。
- **安全管理**(Security):通过用户、角色、权限(privilege)控制访问。
This is a massive amount of work — and it's all hidden behind a simple `SELECT` statement. The genius of database systems lies precisely in this abstraction leap: users describe what they want, not how to get it.
1.2.3 请求流转图:数据是如何被读出来的
为了让你直观理解 DBMS 在系统中的位置,请看下面这张图,它展示了一个用户查询"我的电影收藏"时,请求是如何流转的:
```mermaid flowchart LR User[用户 / User] App[应用层 / Application Layer] Parser[查询解析器 / Parser] Optimizer[查询优化器 / Optimizer] Executor[执行引擎 / Executor] Buffer[缓冲区 / Buffer Pool] Disk[磁盘 / Disk Storage] Log[事务日志 / Transaction Log]
User -->|发送SQL| App App -->|提交查询| Parser Parser -->|语法树| Optimizer Optimizer -->|执行计划| Executor Executor -->|读页面| Buffer Buffer -->|缓存未命中| Disk Buffer -->|写日志| Log Executor -->|返回结果| App App -->|展示数据| User ```
注意看这张图的几个关键点:第一,应用层(Application Layer)从不直接读写磁盘,它只和 DBMS 对话;第二,缓冲区(Buffer Pool)是内存中的缓存,绝大多数查询会命中缓存而不必访问慢速的磁盘;第三,事务日志(Transaction Log)是独立的写入流,即使主数据丢失,也能从日志恢复。This three-layer architecture is what makes databases both fast and reliable.
1.3 数据的三层架构:外模式、概念模式与内模式
1975 年,ANSI/SPARC 数据库架构委员会提出了著名的"三层模式"(Three-Schema Architecture),把数据视图分成三个层次。这种分层思想深刻影响了后续几十年的数据库设计。
1.3.1 内模式(Internal Schema):数据究竟怎么存?
内模式描述的是数据物理上怎么存。它关心的是:哪些数据放在哪个磁盘文件里?索引是用 B+ 树还是哈希?压缩算法用什么?页大小是 4 KB 还是 16 KB?这些问题对应用程序员(application developer)通常是隐藏的,但却是数据库管理员(DBA, Database Administrator)的日常。
以 PostgreSQL 为例,它的内模式包括:表文件(heap file)由多个 8 KB 的页面(page)组成;每个页面包含若干行(tuple)的指针;元组本身被切分成堆元组(heap tuple)和TOAST(The Oversized-Attribute Storage Technique,用于存储超过 2 KB 的大字段,如长文本、图片)。这种细粒度的存储管理,是数据库能够高效处理从几字节到几 GB 数据的关键。
1.3.2 概念模式(Conceptual Schema):全局数据模型
概念模式是整个数据库的逻辑结构(logical structure)。它定义了有哪些表,每个表有哪些列,列与列之间有什么关系。它是组织全局数据视图的"蓝图",通常用 ER 图(Entity-Relationship Diagram)来表示。
例如,在我们的"电影收藏"数据库中,概念模式可能包含:
- **Movies 表**:title、year、director、rating
- **Actors 表**:name、birth_year
- **MovieActors 表**:连接 Movies 和 Actors 的多对多关系
概念模式独立于任何具体的应用程序,也独立于物理存储。它是数据库设计的"中间层",起到承上启下的作用。
1.3.3 外模式(External Schema / View):每个用户看到的世界
外模式(也叫视图,view)是某个特定用户或应用看到的数据子集。比如,人力资源部门只能看到员工表的"姓名、工号、部门"三个字段,看不到"工资、家庭住址"等敏感信息。这种"按需可见"的机制,是数据库安全的重要组成部分。
在 SQL 中,视图是通过查询定义的虚拟表:
```sql CREATE VIEW PublicMovies AS SELECT title, year, rating FROM Movies WHERE rating >= 4.0; ```
This view is recomputed every time it's queried (or materialized for performance). 视图机制让我们能够在不动底层表结构的情况下,为不同用户定制不同的数据视图。This is the essence of data abstraction — and it's why your bank's mobile app can show you only your account, while the same database serves millions of customers simultaneously.
三层架构的价值在于数据独立性(data independence):当物理存储改变时(如从 HDD 迁移到 SSD),不需要修改概念模式;当概念模式改变时(如增加一列),只要视图层做了相应调整,老的应用可以继续运行。这种"解耦"是大型系统能够演进而不断裂的关键。
1.4 关系模型与 SQL:声明式编程的力量
1.4.1 关系的诞生:从层次模型到关系模型
在 1970 年之前,主流的数据库是层次模型(hierarchical model)和网状模型(network model)。这些模型的共同特点是:数据以"树"或"图"的形式组织,查询必须通过指针(pointer)遍历。这意味着程序员必须精确知道数据"怎么存",才能写查询——非常痛苦。
1970 年,IBM 的年轻研究员埃德加·科德(Edgar F. Codd)发表了一篇划时代的论文 *"A Relational Model of Data for Large Shared Data Banks"*。他提出:数据应该组织成关系(relation),即一张二维表(table),每行是一个元组(tuple),每列是一个属性(attribute)。查询不需要知道物理结构,只描述"我要什么",系统会自己想办法找出来。
Codd's relational model was revolutionary because it separated logical data structure from physical storage. 这个思想直接催生了 SQL(Structured Query Language),也奠定了整个现代数据库产业的基石。
1.4.2 关系模型的核心概念
要理解关系模型,必须掌握以下几个术语(其中英文术语在学术界和工业界都广泛使用):
| 中文术语 | English Term | 解释 | |---------|--------------|------| | 关系 / 表 | Relation / Table | 二维的数据结构 | | 元组 / 行 | Tuple / Row | 表中的一条记录 | | 属性 / 列 | Attribute / Column | 数据的一个字段 | | 域 | Domain | 列的取值范围 | | 主键 | Primary Key | 唯一标识一行的列 | | 外键 | Foreign Key | 引用其他表主键的列 | | 候选键 | Candidate Key | 能唯一标识一行的最小列集 |
主键(Primary Key)是关系模型的核心概念。它是一列(或几列的组合),其值在表中必须唯一且非空。例如,学生表的 `id` 列就是典型的主键。In a well-designed database, every table should have a primary key — otherwise you risk duplicate rows and update anomalies.
1.4.3 SQL:声明式编程的革命
SQL 是关系模型的具体实现语言。它的革命性在于:你只描述"我要什么",不描述"怎么取"。这就是所谓的声明式编程(declarative programming),与之相对的是命令式编程(imperative programming)。
举个例子:如