本节目标:彻底搞懂关系型数据库的核心概念——表、行、列、主键、外键、关系类型、约束机制。
数据库里的表(Table),你可以先理解为一个"超级 Excel 工作表"。如果你用过 Excel,理解数据库表就有了一个很好的起点。
先看相似的地方:
| Excel 概念 | 数据库概念 | 说明 |
|---|---|---|
| 工作表 Sheet | 表 Table | 一类数据一张表,比如 users 表存所有用户 |
| 一行数据 | 行 Row(也叫记录) | 一个具体的用户、一笔具体的订单 |
| 表头列名 | 列 Column(也叫字段) | 数据的属性,比如 phone、address |
再看不同的地方——这些差异才是数据库真正的价值,也是 Excel 永远做不到的:
数据类型严格。 Excel 里一列可以混放文字和数字,你在"年龄"列写个"很大"也没人拦你。数据库不行——integer 类型的列只能放整数,你写个文字进去,数据库直接报错拒绝。这看起来"不方便",但恰恰是保证数据质量的关键。想象小红的外卖平台,如果"价格"列里混进了文字,结算时就会出错。数据库的严格类型从源头杜绝了这种问题。
约束机制。 数据库能设置规则:手机号不能重复(UNIQUE)、昵称不能为空(NOT NULL)、订单的用户 ID 必须指向一个真实存在的用户(FOREIGN KEY)。违反规则的数据写不进去,从源头杜绝脏数据。这些规则一旦设定,无论是谁、通过什么方式写入数据,都必须遵守——不像 Excel,任何人都能随意修改任何格子。
表间关联。 这是数据库最核心的能力,也是它叫"关系型"数据库的原因。多张表可以通过"外键"建立关系,然后用 SQL 的 JOIN 操作把关联数据一次性查出来。比如"查出小红的所有订单以及每个订单里的菜品",一条 SQL 搞定。在 Excel 里,你得在多个 Sheet 之间来回跳,手动对照 ID,效率极低。
并发安全。 100 个用户同时下单,数据库通过事务和锁机制保证每笔订单都正确写入,不会互相覆盖、不会丢数据。Excel 做不到这一点——两个人同时编辑同一个文件,轻则产生冲突,重则丢失修改。
下面这个交互组件,让你直观感受一张表的结构——点击列名可以查看每个字段的详细说明:
如果说主键是"我是谁",那外键就是"我和谁有关系"。
外键(Foreign Key) 是一张表里指向另一张表主键的列。它的作用是建立表与表之间的关联,同时保证关联的数据真实存在。
举个例子:小红的 orders 表里有一列 user_id,它的值必须是 users 表里某个真实存在的 id。这就是外键。通过这个 user_id,你可以从一笔订单追溯到下单的用户——"这笔订单是谁下的?user_id 是 42,去 users 表查 id=42 的那行,哦,是小红。"
CREATE TABLE orders (
id serial PRIMARY KEY,
user_id integer REFERENCES users(id), -- 外键,指向 users.id
amount real NOT NULL,
status text DEFAULT '待支付'
);
外键带来三个好处:
保证数据一致性。 你不能给一个不存在的用户创建订单。如果 users 表里没有 id=999 的用户,往 orders 表插入 user_id=999 的订单会直接报错。这就是"引用完整性"——你引用的东西必须真实存在,不能指向一个不存在的用户。
建立关联。 通过 user_id,你可以从一笔订单追溯到下单的用户,也可以从一个用户查到他的所有订单。数据之间的关系变得清晰可查。
级联操作(连锁反应)。 删除一个用户时,他的所有订单怎么办?数据库可以自动删除他的所有订单(级联删除),或者阻止删除(如果还有关联订单就不让删)。这个行为可以在建表时配置。
PostgreSQL 不会自动给外键列创建索引。如果 orders 表有 10 万行,查"某个用户的所有订单"时,没有索引的 user_id 会导致全表扫描——从第一行翻到最后一行,像在没有目录的书里逐页找内容——慢 100 倍以上。所以每个外键列都要手动加索引。这个知识点在 6.4 会详细展开。
你可以在上方组件的「关系图」tab 中,点击连线查看外键的具体关联方式。
没有约束的数据库就像没有规则的仓库——什么都能往里塞,迟早乱套。今天塞进去一个"年龄 -5"的用户,明天塞进去一个"价格为空"的商品,后天你的应用就会在各种奇怪的地方崩溃,而且你根本不知道是数据的问题。
约束(Constraint) 是数据库自动执行的规则。每次写入数据时,数据库会检查这些规则,不合规的数据直接拒绝,连存都不让存。这就像机场安检——不管你是谁、从哪来,行李都要过 X 光机,违禁品一律拦截。
| 约束 | 作用 | 生活类比 |
|---|---|---|
| PRIMARY KEY(主键) | 唯一标识每行 | 身份证号,全国唯一 |
| FOREIGN KEY(外键) | 确保引用的数据存在 | 快递单上的收件人必须是真人 |
| NOT NULL(必填) | 不允许为空 | 表单里的必填项,不填不让提交 |
| UNIQUE(不可重复) | 不允许重复 | 手机号不能重复注册 |
| CHECK(条件检查) | 自定义条件检查 | 年龄必须大于 0,评分必须在 1-5 之间 |
| DEFAULT(默认值) | 未填时自动填入默认值 | 订单状态默认"待支付" |
在上方组件的「约束演示」tab 中,你可以看到每种约束的正确 vs 违规插入对比。
你可能会想:我在代码里做校验不就行了,为什么还要在数据库层面加约束?
三个原因:
代码可能有 bug,但数据库约束是最后一道防线。 你的校验逻辑写错了、漏了一个边界条件、某个 if 判断少了个等号——这些都可能让脏数据溜进去。数据库约束不会犯这种错误,它是铁面无私的守门员。
数据的入口不止一个。 你的应用有 API 接口、有后台管理面板、有数据迁移脚本、有定时任务……每个入口都可能写入数据。你不可能在每个入口都完美地实现一遍校验逻辑。但数据库约束只需要定义一次,所有入口都受保护。
脏数据的清理成本极高。 想象几万条订单的 user_id 指向了不存在的用户——你怎么修?删掉这些订单?那用户的钱怎么办?把 user_id 改成某个默认用户?那数据就失真了。预防永远比治疗便宜。
| SQLite | PostgreSQL | |
|---|---|---|
| 定位 | 文件型数据库,零配置 | 完整的数据库服务器 |
| 存储 | 单个 .db 文件 |
独立的数据库服务进程 |
| 并发 | 单写多读,适合小规模 | 高并发读写,适合生产环境 |
| 数据类型 | 宽松(实际上只有 5 种) | 严格且丰富(50+ 种) |
| JSON 支持 | 基础 | 强大的 jsonb 类型 |
| 适用场景 | 本地开发、嵌入式、原型验证 | 生产环境、团队协作、高并发 |
| 本教程建议 | 学习阶段可用,快速上手 | 实际项目推荐,Supabase 免费提供 |
如果你用 Supabase 或 Neon 等云数据库服务,直接就是 PostgreSQL,不需要纠结。本地开发想快速验证,SQLite 也完全够用——Drizzle ORM 支持两者无缝切换。
serial 自增