新来一期,摊平表要动四处;关系数据库只要插两张表各一次。 漏改一处不会报错:期号表少一行,那期的文章在 JOIN 里静悄悄消失。 这就是关系数据库的核心交易:用一次性的连接成本,换永远不会分叉的数据。
上一篇算了摊平的容量账,这一篇算维护账。维护账才是真正贵的那笔。 书里这段的情绪是「复制粘贴太痛苦」。痛苦是可以量化的:数一下要动几处。
一、新来一期,要动几处
第 13 期到了:2026-01,销量 57900,封面 效率工具,两篇文章。 在摊平的电子表格里:文章区加两行、销量抄两遍、汇总表加一行、图表的数据源拉一下。 在关系数据库里:期号表插一行,文章表插两行。完。
四处对三处,看着差别不大。差别在「漏了会怎样」。 摊平表的四处是手工的四处,每一处都可能漏;数据库的三次插入是三个 INSERT 语句。
二、漏改一处会怎样
假设只往文章表插了两行,忘了往期号表插。 这时文章表 25 行、期号表 12 行。跑一个内连接,第 13 期那两篇文章直接消失。 数据库不报错。它给出一个 24 行的结果,看起来和之前一模一样。
这是最坏的一种错:结果变少了,但你不知道。 唯一能发现的办法是主动查:用左连接找出右边为空的行。这里能查出 1 行。
三、改一个数,要改几处
第 5 期销量录错了,61200 应该是 62100。 摊平表里它出现两次(两篇文章各一行);期号表里只有一处。 如果改了一处漏了另一处,同一期在表里就有两个销量值。
这种错最难查:每一行单看都对,只有把它们对起来才矛盾。 重复存储的真正代价不是空间,是「同一件事可以有多个版本」。
四、关系数据库怎么解决
销量只存一个地方:期号表。文章表只存期号,不存销量。 要改就改一处,永远不会出现两个版本。这个性质叫「一处修改,处处生效」。 代价是:每次要用都得 JOIN 一下,不能直接看到摊平的样子。
所以这是一笔交易:用一次性的连接成本,换永远不会分叉的数据。 数据改得越频繁、用得越久,这笔交易越划算。
五、手工维护的底线
如果非要手工维护表格,至少要做到一件事:每次改动都能撤干净。 做不到这一点,就不该手工改——因为你迟早会撤不干净。 更实际的做法:把「一处事实只存一处」当成硬规矩,重复的那部分用公式引用,不用复制。
复制是数据分叉的起点。任何一次 Ctrl+C 都在制造未来的一个矛盾。
六、动手 Demo:复制粘贴的痛苦
模拟「新来一期只改了一半」:往文章表插一行,期号表不动。 再看 JOIN 结果怎么静悄悄变少,以及怎么用左连接把它查出来。
import sqlite3
ISSUES = [(i, 40000 + i * 1000) for i in range(1, 13)]
ARTS = []
aid = 0
for issue, copies in ISSUES:
for k in range(2):
aid += 1
ARTS.append((aid, issue))
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE issues (issue INTEGER PRIMARY KEY, copies INTEGER)")
con.execute("CREATE TABLE articles (article_id INTEGER PRIMARY KEY, issue INTEGER)")
con.executemany("INSERT INTO issues VALUES (?,?)", ISSUES)
con.executemany("INSERT INTO articles VALUES (?,?)", ARTS)
con.commit()
print("改动前:文章 %d 行" % con.execute("SELECT COUNT(*) FROM articles").fetchone()[0])
print(" JOIN 结果 %d 行" % con.execute(
"SELECT COUNT(*) FROM articles a JOIN issues i ON a.issue = i.issue").fetchone()[0])
# 只插文章表,忘了插期号表
con.execute("INSERT INTO articles VALUES (?,?)", (25, 13))
con.commit()
print()
print("改动后:文章 %d 行" % con.execute("SELECT COUNT(*) FROM articles").fetchone()[0])
print(" JOIN 结果 %d 行(少了,但不报错)" % con.execute(
"SELECT COUNT(*) FROM articles a JOIN issues i ON a.issue = i.issue").fetchone()[0])
print(" 孤儿行 %d 行" % con.execute(
"SELECT COUNT(*) FROM articles a LEFT JOIN issues i ON a.issue = i.issue "
"WHERE i.issue IS NULL").fetchone()[0])
实跑输出:
--------------------------------------------------------------------
复制粘贴的痛苦
--------------------------------------------------------------------
① 新来一期,要动几个地方
--------------------------------------------------------------------
13 期到了:2026-01,销量 57900,封面主题 效率工具,两篇文章。
在摊平的电子表格里,要手动做的事:在文章区加两行、把销量抄两遍、更新汇总表、更新图表数据源。
在关系数据库里,要做的只有两件事:给期号表插一行,给文章表插两行。
② 漏改一处会怎样
--------------------------------------------------------------------
假设新一期只加进了文章表,忘了加进期号表。
文章表 25 行,期号表 12 行。
这时跑 JOIN,第 13 期的两篇文章会直接消失——不报错,只是少了两行。
能查出来的孤儿行:1 行。不主动查,你永远不知道少了几行。
③ 改一个数,要改几处
--------------------------------------------------------------------
第 5 期销量录错了,61200 应该是 62100。
摊平表里,它出现两次(两篇文章各一行);期号表里只有一处。
改了摊平表的一处、漏了另一处,同一期在表里就有两个销量值。
这种错最难查,因为每一行单看都没问题,只有对起来才发现矛盾。
④ 关系数据库怎么解决这件事
--------------------------------------------------------------------
销量只存一个地方:期号表。文章表只存期号,不存销量。
要改就改一处,永远不会出现两个版本。这个性质叫「一处修改,处处生效」。
代价是:每次要用都得 JOIN 一下,不能直接看到摊平的样子。
! 这就是关系数据库的核心交易:用一次性的连接成本,换永远不会分叉的数据。
⑤ 回到那 24 行
--------------------------------------------------------------------
把刚才插的脏数据撤掉,文章表回到 24 行。
手工维护表格的每一次改动,都得能撤干净。做不到这一点,就不该手工改。
文章表从 24 涨到 25,JOIN 结果却还是 24。两个数不一致,就是唯一的报警信号。 所以每次 JOIN 之后都要数行数。这不是洁癖,是唯一能自动发现问题的动作。
30 秒实验
- 如果漏的是期号表多插了一行(没有文章指向它),内连接结果会变吗?外连接呢?
- 「一处修改,处处生效」在多个人同时改数据时会带来什么新问题?
- 想一想:为什么说复制的代价要在半年后才会显现?
常见误区
- 相信「不报错就是对的」:丢行、笛卡尔积、重复计数,全都不报错。
- 改完就交,不复检行数:行数是唯一便宜的自动检查。
- 用复制来同步两份数据:一次复制就是一次分叉的开始。
- 以为关系数据库只是「更大」:它解决的是重复与分叉,不是容量。
自测三问(点开对答案)
1. 为什么「不报错」在数据库里是危险的? —— 因为丢行、多行、重复都不算语法错误。数据库只会执行,不会判断结果对不对。 2. 怎么发现漏插了一行? —— 比较 JOIN 前后的行数,或者用左连接找右侧为空的行。两个办法都便宜。 3. 关系数据库拿什么换什么? —— 用每次查询都要连接的成本,换数据永远只有一份、永远不会出现两个版本。账算清了。下一篇真的把这条路线写成 SQL,看它比复制粘贴快多少。
- ☐ (本篇完,下一篇 12.5 RDBMS、SQL 与封面)