# 接手没人设计过的客户数据库，先学哈佛这门免费 SQL 课

> CS50 SQL 用七周讲透查询、表设计和扩展，练习用真实数据，从 SQLite 讲到 PostgreSQL。

原文: https://fdetimes.net/zh/books-courses/harvard-cs50-sql-course-review/

哈佛有一门只教一种语言的免费课程。CS50's Introduction to Databases with SQL（通常简称 CS50 SQL）的课程主页写得很直白：这门课完全围绕 SQL。

不是哈佛学生也能学。所有讲座都放在 OpenCourseWare 上；如果需要证书，可以另外通过 edX 获取。

对想成为前线部署工程师（FDE）的人来说，这门课不是出于好奇随便看看的。客户现场的项目，往往始于一个不是你写的数据库，外加一份没人记得是谁建的电子表格。表设计错了，建在上面的一切，从仪表盘到 Agent，也都跟着错。

## 两位讲师，七周，一种语言

课程由 David J. Malan 和 Carter Zenke 讲授。Harvard Online 还列出 Zenke 是 Harvard Extension School 的 Preceptor。课程共七周，编号从第 0 周到第 6 周：查询（Querying）、关联（Relating）、设计（Designing）、写入（Writing）、视图（Viewing）、优化（Optimizing）和扩展（Scaling）。

这个顺序对学习者很合理。先要能读数据、理解表与表如何关联，才谈得上自己设计表、写入数据、自动化和优化。

课程先用 SQLite，因为它小巧、便于携带，到最后才引入 PostgreSQL 和 MySQL。练习用的是真实数据，而不是玩具表。

CS50 SQL 也不同于 CS50x。CS50x 广泛覆盖计算机科学，涉及 C、Python、SQL 和 JavaScript；CS50 SQL 只深挖 SQL。两门可以同时学，也可以只学 CS50 SQL。

## 一家公司算成两家：第 2 周的教训

最有价值的内容在第 2 周“设计”。课程教你把现实世界的实体和关系建模成表，选择合适的数据类型，并通过范式化消除重复、降低出错概率。讲义把这种拆分数据的做法称为 normalizing（范式化），并用 ER 图画出关系。

设想一家物流客户发来一份订单文件，每一行都记录了下单公司的名称。在 1,000 行数据里，同一个客户可能这里写成“ABC Company”，那里写成“ABC Co”。按客户统计营收的报表就会把一家公司拆成两家，任何查询都救不回来。

范式化正好解决这个问题。建一张 `customers` 表，以 `id` 为主键，`orders` 表只存 `customer_id`。第 2 周的讲座给出两条值得记住的约束：主键必须唯一；外键的值必须存在于关联表的主键列中。

有一步不能省：外键本身不会把“ABC Company”和“ABC Co”合并成一个。导入数据之前，仍然要清理名称的各种写法，在 `customers` 中合并成一行，再把每条已有订单映射到这个 `id`。

清理完成后，SQLite 中的最小版本大致如下：

```sql
PRAGMA foreign_keys = ON;

CREATE TABLE customers (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
  id INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL,
  created_at TEXT,
  FOREIGN KEY (customer_id) REFERENCES customers(id)
);

INSERT INTO customers (id, name) VALUES (1, 'ABC Company');
INSERT INTO orders (customer_id, created_at) VALUES (1, '2026-10-01');  -- 有效
INSERT INTO orders (customer_id, created_at) VALUES (99, '2026-10-01'); -- 被拒绝
```

第一条订单插入有效；最后一行指向不存在的 99 号客户，数据库会直接拒绝，而不是让错误流进报表。公司名称现在只存在一个地方，改一次，所有订单都随之更正。

**要点:** schema 中的约束能在脏数据进入客户报表之前把它拦下。

开始一个新项目时，第一件事是画出客户数据库的 ER 图。如果发现某张表同时存着客户名称和订单明细，那就是写任何代码之前需要先问清楚的地方。

## 视图管业务逻辑，索引管速度

第 4、5 周“视图”和“优化”讲的两个工具，新手常常混淆。视图把反复运行的查询自动化；索引让查询跑得更快。

在客户现场，这个区别决定了你要做什么。运营团队每天都问“哪些订单延误超过三天？”，就写一个视图，让所有人复用同一个定义。同一个问题在大表上跑得慢，就需要索引。

视图让整个团队用同一个定义；索引只是让查询更快返回。课程还讲了如何把 SQL 接入 Python 和 Java，这正是应用代码在实际中与数据库协作的方式。

## 新手常犯的两个错误

第一个是用 SQLite 时忘了 `PRAGMA foreign_keys = ON`。不开启它，SQLite 不会强制执行外键，上面那条指向 99 号客户的 `INSERT` 就会悄无声息地通过。建议：每个连接都开启，然后故意插入一行坏数据，确认约束确实生效。

第二个是看到索引能加速查询，就给每一列都建索引。每次写入都要更新所有索引，还会占用额外存储。更稳妥的做法是只给慢查询的 `WHERE` 或 `JOIN` 子句中真正出现的列建索引，并在前后各测一次。

## 从 SQLite 到数据库服务器，以及 SQL 注入陷阱

第 6 周“扩展”点明了许多演示略过的一点：SQLite 是嵌入式数据库，而 MySQL 和 PostgreSQL 是数据库服务器。这一周的讲座还讲了复制，客户系统一旦用户多起来，你就会碰到它。

同样在第 6 周，课程讲解了如何在 MySQL 中用预处理语句防止 SQL 注入。对 FDE 来说，这是必备技能。如果你做的工具接收用户输入，并直接拼接进针对客户真实数据运行的 SQL，一条恶意输入就足以让整张表暴露。

不要拼接字符串，而是在语句中用 `?` 作占位符，再把值传进去：

```sql
PREPARE find_orders FROM
  'SELECT id, created_at FROM orders WHERE customer_id = ?';
SET @cid = 1;
EXECUTE find_orders USING @cid;
DEALLOCATE PREPARE find_orders;
```

`@cid` 的值只会被当作数据，永远不会成为语句的一部分。审查任何接触客户数据库的代码时，这是第一件要检查的事。

## 按什么顺序学，简历上写什么

如果你已经写了几年 SQL，可以快速浏览第 0、1 周，把大部分时间花在第 2 周和第 6 周。这两周最贴近客户现场的工作：设计表，以及把数据库带进真实环境。

如果你只用过 ORM，就按顺序学完七周，因为每一周都建立在前一周之上。

学完之后，把成果写进简历，而不只是写“会 SQL”。加一行说明你如何把一份真实数据集范式化、设了哪些约束、加了哪些索引，并附上代码仓库链接。

阅读 FDE 岗位描述时，如果看到“data modeling”或“customer data”这类字眼，就准备好一个范式化案例和一张自己画的 ER 图带去面试。这是证明你真正做过、而不只是学过的最具体的方式。

在客户现场，第一个问题通常不是“哪个模型最好”，而是“这张表能不能信”。CS50 SQL 能帮你在客户想到要问之前就给出答案。

**本周动手试试:**

- 在 cs50.harvard.edu/sql 观看第 2 周讲座并阅读讲义，然后为工作中正在处理的一份电子表格画一张 ER 图
- 找一个客户名称有重复的 CSV 文件，导入 SQLite，清理名称，拆成两张用外键关联的表，再试着插入一行违反约束的数据
- 在代码库里找一处用字符串拼接构造 SQL 的地方，改写成预处理语句

## 信源

- [CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/)

- [CS50's Introduction to Databases with SQL (Harvard Online)](https://pll.harvard.edu/node/23373)

- [Lecture 2 - CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/notes/2/)

- [Lecture 6 - CS50's Introduction to Databases with SQL](https://cs50.harvard.edu/sql/notes/6/)
