仓库 · 219 字 · 1 分钟阅读

关系模型与 SQL:整理世界的数据

Edgar Codd 1970 年的论文给了我们数据库的数学基础,SQL 给了我们一门查询它的语言。IBM 压着它,Oracle 抢先发布了它。

#TL;DR

1970 年,IBM 的一位数学家 Edgar Codd 发表了一篇重新定义人类存取数据方式的论文。在 Codd 之前,数据库是迷宫——你得先知道数据的物理布局才能找到任何东西。Codd 说:把是什么怎么做分开。把数据组织成数学关系(表),用逻辑去查询,让机器去搞定物理细节。IBM 明白这很漂亮,然后花了好几年不把它发布出去。Oracle 读的正是 IBM 自己的研究论文,然后抢在 IBM 之前推向市场。Codd 的同事们为了让他的数学可用而发明的那门语言——SQL——成了历史上使用最广的数据语言,五十年后仍然如此。

#迷宫问题

1970 年之前,数据库是围绕数据如何存储而不是数据意味着什么来构建的。

占主导地位的是层次模型——IBM 的 IMS(Information Management System),用来给阿波罗计划追踪零件,把记录组织成树。要查一个供应商的零件,你从根开始导航,经过部门、经过仓库,一直到想要的那条记录。这条路径被硬编码在你的程序里。改一改存储结构,所有动过它的应用就全坏。

网状数据库(CODASYL 标准)稍微灵活一点——记录可以有多个父——但程序员仍然得在记录之间顺着显式的指针走。你不是问你要什么,你是在描述如何导航过去。

这造成了一个根本问题:数据依赖。你的应用知道的物理布局的细节太多。一次存储重组——挪个文件、加个索引——就可能要求重写几千个程序。数据库建起来很贵,改起来几乎不可能。

Edgar Codd 在 IBM 圣何塞研究实验室楼里偏僻的那一头思考这个问题多年。他是数学家,不是工程师,他觉得工程师们彻底在解决错的问题。

#Codd 的洞见:数据独立性

Codd 1970 年的论文——“A Relational Model of Data for Large Shared Data Banks”,发表于 Communications of the ACM——给出了一个看似简单的论证:

把数据组织成关系(行和列的表)。用关系代数——那些从数学来的描述你要什么而不是怎么取的运算——去查询它。让查询优化器去搞定物理访问路径。

这给了应用程序数据独立性:程序不需要知道数据存在磁带上、有没有索引、有没有分区、或者有没有被重新组织过。它们只描述它们想要什么。数据库引擎的工作是搞清楚怎么把它交出来。

一个关系就是一张表:一组元组(行),每行符合相同的模式(列)。真正让它强大的是你可以对它应用的数学运算:

Orders 表:                Customers 表:
┌────┬─────────┬──────┐  ┌────┬──────────────┬──────────┐
│ id │ cust_id │  amt │  │ id │ name         │ city     │
├────┼─────────┼──────┤  ├────┼──────────────┼──────────┤
│  1 │     101 │  250 │  │101 │ Ada Lovelace │ London   │
│  2 │     102 │   80 │  │102 │ Alan Turing  │ London   │
│  3 │     101 │  430 │  └────┴──────────────┴──────────┘
└────┴─────────┴──────┘

按 cust_id = id 连接,并 WHERE city = 'London':
→ 伦敦客户下的所有订单

关键洞见是连接。通过共享值(外键)而不是物理指针来表达关系,数据就可以按当初设计者从未预见过的方式组合起来。你设计模式的时候不需要预测每一条查询——你只需要把数据建模对,剩下的交给关系代数。

#规范化:设计无冗余的数据

Codd 还形式化了规范化——一组构造表的规则,用来最小化冗余、防止不一致。

经典问题:在每一行订单里都存一份客户所在城市,等这位客户搬家时,你就有一千行要更新。漏掉一条,数据就自相矛盾了。把城市只在 customers 里存一次,用 ID 引用它,就只有一处要更新。

Codd 定义了范式——对表设计日益严格的约束:

-- 坏的:城市在每一行订单里重复
CREATE TABLE orders_bad (
    id       INT,
    cust_id  INT,
    cust_city TEXT,  -- ← 重复,会变陈旧
    amount   DECIMAL
);

-- 好的:城市在 customers 里只存一份,需要时再连接
CREATE TABLE customers (
    id   INT PRIMARY KEY,
    city TEXT
);
CREATE TABLE orders (
    id      INT PRIMARY KEY,
    cust_id INT REFERENCES customers(id),
    amount  DECIMAL
);

第一、第二、第三范式(1NF、2NF、3NF)逐级消除越来越微妙的冗余形式。大多数实用模式瞄准 3NF。Codd 后来与 Don Chamberlin 和 Ray Boyce 的一次交谈中发现 3NF 的一个漏洞,随后定义了 Boyce-Codd 范式(BCNF)。

#SEQUEL:让数学可读

Codd 的关系代数数学上很优雅,但对任何没有数学学位的人都几乎不可用。IBM 指派了两位研究员——Don ChamberlinRay Boyce——把它变成程序员能读的东西。

Chamberlin 听了一次 Codd 的报告,出来就坚信:

“我看到了 Codd 在做什么,我想:我可以写一门所有人都能用的语言。”

他们把它叫做 SEQUEL——Structured English Query Language。这个名字后来因为一家飞机公司的商标冲突被缩写成 SQL

设计目标是英语的可读性。不写关系代数记号,而是写一句看上去像一句话的东西:

-- 关系代数(Codd 的数学长什么样):
π name, city (σ city='London' (Customers))

-- SQL(Chamberlin 和 Boyce 设计出来的东西):
SELECT name, city
FROM customers
WHERE city = 'London';
-- 完整的威力:连接多表、聚合、过滤
SELECT
    c.name,
    COUNT(o.id)   AS order_count,
    SUM(o.amount) AS total_spent
FROM customers c
JOIN orders o ON o.cust_id = c.id
WHERE c.city = 'London'
GROUP BY c.name
HAVING SUM(o.amount) > 200
ORDER BY total_spent DESC;

SQL 描述取什么。数据库引擎决定怎么取——用哪个索引、哪种连接算法、是扫全表还是查某一行。这种关注点分离是激进的。

#IBM 压着它。Oracle 没有。

IBM 搭建了 System R(1974-1979),这是第一个关系数据库实现,作为研究项目。它证明了这个概念行得通。然后 IBM 花了好些年讨论要不要把它产品化,一部分原因是 IMS——他们现有的层次数据库——很赚钱、又铺得到处都是。

Larry Ellison 读了 IBM 公开发表的 System R 论文。1977 年他创立了 Software Development Laboratories——后来改名 Oracle——计划很简单:在 IBM 自己做出来之前,先把 IBM 描述的那个做出来。

Oracle 在 1979 年发布了第一个商业关系数据库,比 IBM 自己的产品还早一年。IBM 随后在 1981 年推出 SQL/DS,1983 年推出 DB2。到那时,Oracle 已经取得了再也没被追上的先机。

业界没有忽略这个教训:发表研究,是对你竞争对手的一种慷慨。

到 1983 年,关系数据库已经把层次数据库挤下了默认地位。1986 年,SQL 成为 ANSI 标准。今天,每一个重要的数据库——PostgreSQL、MySQL、SQLite、SQL Server、BigQuery——都在讲着两位 IBM 研究员为了让一位数学家的想法变得可用而设计出的那门语言的方言。

#ACID:数据库许下的承诺

当关系数据库进入生产,工程师们需要一个保证:如果一笔事务进行到一半突然断电,数据处在什么状态?

ACID——1983 年由 Theo Härder 和 Andreas Reuter 形式化——定义了一个数据库事务必须具备的四个属性:

  • 原子性(Atomicity) — 一笔事务要么完整完成,要么根本没发生。不存在写了一半的订单。
  • 一致性(Consistency) — 每一笔事务都让数据库处于合法状态。外键依然成立,约束依然被尊重。
  • 隔离性(Isolation) — 并发事务看不到彼此的中间状态。两个同时抢最后一个座位的用户,会得到正确的结果。
  • 持久性(Durability) — 一旦提交,事务就能挺过崩溃。写入已经在磁盘上,不只是在内存里。
BEGIN;
  UPDATE accounts SET balance = balance - 500 WHERE id = 1;
  UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
-- 两个更新都发生,或都不发生。永远不会只发生一个。

ACID 是数据库与文件之间的界限。正是因为 ACID,银行把钱交给关系数据库、医院把病历交给它、航空公司把座位库存交给它。这些保证实现起来很难——预写日志、多版本并发控制、两阶段锁——但程序员看不到这些。他们只是写 SQL,并相信那个承诺。

#关系模型做对了什么

Codd 1970 年的论文是计算机科学中被引用最多的论文之一,它的想法比那个时代几乎任何其他东西都更经得住时间:

  • 数据独立性 — 换存储引擎、加索引、给表分区——应用照常工作
  • 声明式查询 — 描述结果,不描述算法;让优化器找最好的路径
  • 连接 — 通过值而不是指针表达关系,让数据按没人预测过的方式可组合
  • 开放标准 — SQL 的 ANSI 标准化让知识在系统之间转移

关系模型不是完美的。它在处理层次数据(XML、JSON)、图关系和大规模时间序列时很吃力。NoSQL 在 2000 年代部分就是为了摆脱它的约束而出现的。但 NoSQL 数据库最后都一致地把 SQL 接口加了回来——因为事实证明,SQL 并不是约束,它就是解法。

互联网如今在通信上跑 TCP/IP,在数据上跑 SQL。两者都是 1970 年代初期设计的。两者都还在运行。