数据库入门到初级 · 第 02/22 篇。实验口径:MySQL 8.4、InnoDB、utf8mb4。案例:小型商城。
如果把商城所有信息放在一张表里,一笔订单可能反复填写姓名、邮箱和商品资料。时间一长,小林改了邮箱,有的行更新了,有的行仍然保留旧邮箱。理解表、行、列和编号,是避免这类问题的起点。
1. 表、行和列分别回答什么问题
一张表通常围绕一类事物或一类业务事实组织。users 表描述用户,orders 表描述订单。表名不是随便起的标签,它表达这张表中的“一行到底是什么”。
| id | name | |
|---|---|---|
| 1 | 小林 | lin@example.test |
| 2 | 小周 | zhou@example.test |
这里,一行是一名用户,称为记录;name 是列,也叫字段;“小林”是具体的字段值。表结构规定有哪些列、每列接受什么类型以及需要遵守哪些规则。
数据库的表没有默认的展示顺序。即使今天查出来按编号排列,也不能据此假设以后永远如此。需要顺序时,必须在查询里写 ORDER BY。
2. 主键:给每行一个稳定身份
姓名可能重名,手机号和邮箱可能变化。为了准确地指向某一行,我们需要主键。上表的 id 就承担这个职责。
主键需要唯一,并且不能为 NULL。一张表只有一个主键,但这个主键可以由多列共同组成。初学时常使用自增整数编号,后面再讨论复合主键和业务编号。
“自增”只负责分配编号,不保证编号连续。失败的插入、回滚等情况可能留下空缺。不要用“最大编号减最小编号”统计人数,也不要为了好看而重新排列历史主键。
3. 外键:说明这一行属于谁
订单表可以这样记录:
| id | user_id | status | total_amount |
|---|---|---|---|
| 1001 | 1 | paid | 288.00 |
| 1002 | 1 | paid | 129.00 |
| 1003 | 2 | pending | 178.00 |
user_id=1 表示订单属于用户 1,而不是“第一行用户”。即使查询结果改变排序,编号的含义仍然稳定。

我们可以给 orders.user_id 定义外键约束,要求它指向 users.id 中已经存在的值。这样,给不存在的用户 999 创建订单时,数据库会拒绝操作。
外键约束负责检查关联合法性。JOIN 负责查询时把表组合起来,两者是不同能力;没有定义外键的两张表也能连接查询,但数据完整性需要其他措施保障。
4. 一对多不是在一个格子里塞多个编号
一个用户有多张订单,因此每张订单保存一个 user_id。不要在用户表里增加 order_ids='1001,1002',也不要不断增加 order1、order2、order3 列。那会让筛选、约束和新增订单变得困难。
同理,一张订单有多个商品时,需要订单明细表。每条明细表达“某订单买了某商品多少件”,而不是把全部商品名称拼成一段文字。
5. 用一条查询验证关联
准备好第 6 篇的数据后,执行:
SELECT o.id, u.name, o.total_amount
FROM orders AS o
JOIN users AS u ON u.id = o.user_id
WHERE o.id = 1001;
结果应为订单 1001、用户“小林”、金额 288.00。这里 o 和 u 是表的临时别名,只在这条语句里帮助我们区分字段,不会把数据库里的表改名。
练习与答案
练习一:用户“小林”能不能直接作为主键? 技术上可给姓名加主键,但姓名难以保证唯一而且可能修改,通常不适合这个业务。
练习二:订单的 user_id 能重复吗? 能。同一用户可以多次下单;订单自己的 id 才不能重复。
练习三:删除一个有订单的用户,数据库会怎么办? 取决于外键删除规则。本系列未设置级联删除,存在引用时会拒绝直接删除,避免意外带走历史订单。
上一篇:第 01 篇 · 系列目录 · 下一篇:第 03 篇