主题在文章表里,销量在期号表里。两张表只靠一个共同字段接起来:期号。 那个共同字段叫键。文章表里的期号是外键,它指向期号表的主键。 JOIN 之前必须先查外键干净不干净——断了的外键不会报错,只会悄悄丢行。
第 11 章把一张表上的回归讲完了。第 12 章换赛道:数据从一个表变成多张表。 这一章不用「平台原样」:本沙箱有 sqlite3,每一条 SQL 都是真跑的。
一、杂志社要回答的问题
《数据邦新闻》想知道:封面做哪个主题,销量最高。 这句话听着简单,落到数据上要跨两张表:主题在文章表里,销量在期号表里。 两张表各存一半,中间靠一个共同的字段接起来。
这就是关系数据库的全部起点:数据分开放,用共同字段再接起来。 分开放的理由很实在——一期有两篇文章,如果硬塞进一张表,销量就得写两遍。
二、两张表长什么样
| 期号表 | 内容 |
|---|---|
| 期号 | 1 到 12,一期一个 |
| 月份 | 2025-01 到 2025-12 |
| 销量 | 四万六到六万二 |
| 封面主题 | AI/理财/效率工具/职场 |
| 文章表 | 内容 |
|---|---|
| 文章号 | 1 到 24,一篇一个 |
| 期号 | 1 到 12,同一期重复出现 |
| 主题 | 这篇属于哪个主题 |
| 封面 | 1 表示封面文章,0 表示次条 |
| 标题 | 文章名 |
期号表 12 行,一期一行;文章表 24 行,一篇一行。 两张表都有「期号」这一列,这不是重复,是它们唯一的连接点。
三、那个共同字段叫键
期号表里的期号,每一行都不同,能唯一确定一行——这叫主键。 文章表里的期号,同一期会出现两次(两篇文章),它指向期号表的主键——这叫外键。 主键保证唯一,外键负责指向。一对多的关系就是这么搭出来的。
一期两篇文章,就是「一对多」:一个期号对应多篇文章。
四、先确认键是干净的
在写任何 JOIN 之前,先跑一句检查:文章表里有没有期号在期号表里找不到? 答案是 0 篇。这说明外键是完整的,可以放心接。 如果答案是 2,那这 2 篇文章在任何 JOIN 里都会凭空消失,而且数据库不会报错。
它会安安静静地给你一个少了两行的结果,你如果不知道,就会拿它去算平均、去汇报。 这条检查是关系数据库分析的第一道工序,比任何统计方法都靠前。
五、接下来要做的
表认清了,键也验过了。下一步是从问题出发,找一条穿过表的路线。 「封面主题 → 销量」这条路线具体经过哪几步,下一篇拆开看。
六、动手 Demo:两张表,一个键
真的建一个 SQLite 库:期号表 12 行、文章表 24 行。 把两张表各打几行出来看,再跑一次外键完整性检查。
import sqlite3
ISSUES = [
(1, "2025-01", 52400, "AI"), (2, "2025-02", 49100, "MONEY"),
(3, "2025-03", 58300, "AI"), (4, "2025-04", 47800, "MONEY"),
(5, "2025-05", 61200, "AI"), (6, "2025-06", 53600, "EFF"),
(7, "2025-07", 45900, "CAREER"), (8, "2025-08", 46800, "CAREER"),
(9, "2025-09", 59700, "AI"), (10, "2025-10", 55100, "EFF"),
(11, "2025-11", 50300, "MONEY"), (12, "2025-12", 62400, "AI"),
]
ARTS = []
aid = 0
for issue, month, copies, topic in ISSUES:
aid += 1
ARTS.append((aid, issue, topic, 1, "art%d" % aid))
aid += 1
ARTS.append((aid, issue, "CAREER", 0, "art%d" % aid))
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE issues "
"(issue INTEGER PRIMARY KEY, month TEXT, "
"copies INTEGER, cover TEXT)")
con.execute("CREATE TABLE articles "
"(article_id INTEGER PRIMARY KEY, issue INTEGER, "
"topic TEXT, is_cover INTEGER, title TEXT)")
con.executemany("INSERT INTO issues VALUES (?,?,?,?)", ISSUES)
con.executemany("INSERT INTO articles VALUES (?,?,?,?,?)", ARTS)
con.commit()
print("期号表 %d 行,文章表 %d 行"
% (con.execute("SELECT COUNT(*) FROM issues").fetchone()[0],
con.execute("SELECT COUNT(*) FROM articles").fetchone()[0]))
print("孤儿文章 %d 篇" % con.execute(
"SELECT COUNT(*) FROM articles WHERE issue NOT IN (SELECT issue FROM issues)").fetchone()[0])
print("封面文章 %d 篇(应该等于期数)" % con.execute(
"SELECT COUNT(*) FROM articles WHERE is_cover = 1").fetchone()[0])
实跑输出:
--------------------------------------------------------------------
两张表,一个键
--------------------------------------------------------------------
① 杂志社要回答的问题
--------------------------------------------------------------------
《数据邦新闻》想知道:封面做哪个主题,销量最高。
这个问题听起来一句话,落到数据上要跨两张表:主题在文章表里,销量在期号表里。
中间只靠一个共同的东西接起来:期号。
② 期号表
--------------------------------------------------------------------
| 期号 | 月份 | 销量 | 封面主题 |
|:---:|---:|---:|---|
| 1 | 2025-01 | 52,400 | 效率工具 |
| 2 | 2025-02 | 49,100 | 理财 |
| 3 | 2025-03 | 58,300 | AI |
| 4 | 2025-04 | 47,800 | 理财 |
| 5 | 2025-05 | 61,200 | AI |
共 12 行,一期一行。销量只有这里有,别处没有。
③ 文章表
--------------------------------------------------------------------
| 文章号 | 期号 | 主题 | 封面 | 标题 |
|:---:|:---:|:---:|---|---|
| 1 | 1 | 效率工具 | 是 | 自动化脚本 |
| 2 | 1 | 效率工具 | 否 | 日历管理 |
| 3 | 2 | 理财 | 是 | 存款搬家 |
| 4 | 2 | 职场 | 否 | 跳槽窗口期 |
| 5 | 3 | AI | 是 | AI 与就业 |
| 6 | 3 | AI | 否 | 模型是怎么学的 |
共 24 行,一篇文章一行。同一期有两篇,所以期号会重复出现。
主题、标题、是不是封面,这些只有这里有。
④ 两张表是靠什么接起来的
--------------------------------------------------------------------
文章表里有「期号」,期号表里也有「期号」。这个共同的字段就是关系。
它有个正式名字:键。文章表里的期号叫外键,它指向期号表的主键。
! 没有这个共同的键,两张表就是两堆互不相干的数。
⑤ 先确认键是干净的
--------------------------------------------------------------------
文章 24 篇,期号 12 个,其中找不到对应期号的文章 0 篇。
这个检查必须先跑。外键断了,后面所有 JOIN 都会悄悄丢数据,而且不报错。
代码块里主题用了英文缩写、标题用了 art1 这种占位名,只为了把行缩短。表结构和数都是真的。 最后一行检查最有用:封面文章数 = 期数,说明每期恰好一篇封面。 这个等式一破,后面按封面求平均就全错。
30 秒实验
- 如果一期有三篇文章,文章表会变成多少行?期号表会变吗?
- 如果把销量也抄到文章表里,会出现什么问题?用刚才的行数说一下。
- 想一想:如果没有「期号」这个共同字段,还有别的办法把两张表接起来吗?
常见误区
- 不验外键就 JOIN:断了的外键不报错,只丢行。最危险的一类错。
- 把外键当主键用:文章表的期号重复出现,不能唯一确定一行。
- 以为表越多越专业:拆表的唯一理由是避免重复,不是为了好看。
- 用标题当键:标题会改、会重名,键必须是稳定且唯一的。
自测三问(点开对答案)
1. 主键和外键的区别是什么? —— 主键唯一确定本表的一行;外键指向另一张表的主键,本表里可以重复出现。 2. 为什么两张表不能合成一张? —— 一期有两篇文章,合起来销量就要重复写两遍。重复就是将来算错和改漏的来源。 3. JOIN 之前为什么要数一遍行数? —— 因为外键断了或 ON 条件写错时数据库不报错,只会悄悄多行或少行。数一遍才能发现。表认全了。下一篇从一个具体问题出发,找一条穿过这两张表的路线。
- ☐ (本篇完,下一篇 12.2 关系与那条贯穿的路线)