📈 第12章 关系数据库
📄 一句话概括

写 SQL 就是把路线翻译成一句话:SELECT 要什么,JOIN 接哪张,ON 用什么接。 加上 GROUP BY 就得到答案:AI 平均 60400 排第一,职场 46350 垫底。 JOIN 少写 ON 会得到 288 行(24 × 12)——不报错,数看着正常,其实毫无意义。

📝 本章和前面几章的关系

上一篇算清了维护账。这一篇把它写成查询,并演示最常见的那个错。 这里跑的是 SQLite。同样的查询换到 MySQL 或 PostgreSQL 上基本能直接跑。

一、SQL 就是把路线写下来

SELECT 选哪几列,FROM 从哪张表起步,JOIN 接哪张表,ON 用什么条件接。 这四样正好对应上一篇那三步路线:筛选、连接、汇总。 先只写前半截,把封面文章的期号和销量接出来,看是不是 12 行。

12 行对了,再加 GROUP BY。一步一步加,每加一步数一次行数。 一口气写完再看结果,出了问题根本不知道错在哪一步。

二、加上 GROUP BY 就是答案

封面主题 期数 平均销量
AI 4 60400
效率工具 3 53700
理财 3 49067
职场 2 46350

四行就是最终答案。AI 最高 60400,职场最低 46350,差 14050。 整个查询六行,一次复制粘贴都没有。 这就是关系数据库真正赢的地方:换问题只换一句话,数据一行都不用动。

三、JOIN 写错会怎样

最常见的一个错:忘了写 ON,或者 ON 的条件写错。 少写 ON,结果是 288 行 = 24 篇文章 × 12 期。这叫笛卡尔积。 它不报错。它只是把每篇文章和每一期都配了一遍。

拿这个结果去 GROUP BY 求平均,得到的数看起来完全正常,其实毫无意义。 JOIN 出错几乎从不报错,只会悄悄多行或少行。这是它和语法错误最大的区别。

四、三条对账办法

第一条:JOIN 之前数两边的行数,JOIN 之后数结果行数,看对不对得上。 第二条:用左连接找孤儿行——右边全为空的那种行。 第三条:GROUP BY 之后,各组计数加起来应该等于 JOIN 结果的行数。

三条都是「对账」,不是「算数」。对账比写查询更需要经验。 新人常犯的错是只学语法不对账,于是错得毫无察觉。

五、从表到库

这些表都存在一个文件里(这里是 magazine.db)。能对它跑 SQL 的软件叫 RDBMS。 SQLite、MySQL、PostgreSQL、SQL Server 都是。这一章的查询换过去基本能跑。 不同产品在类型、索引、权限上差别很大,但 SELECT 这一层几乎一样。

所以「会 SQL」这件事是能跨产品迁移的,这是它比任何图形界面都值钱的地方。 关系模型赢了电子表格,赢的不是容量,是「一次建模,无穷次比较」。

六、动手 Demo:SQL:把那条路线写下来

真建库、真跑三条查询:先接起来看行数,再 GROUP BY 出答案,最后故意漏写 ON。 三条查询的结果都用行数对账。

import sqlite3

ISSUES = [
    (1, 52400, "EFF"), (2, 49100, "MONEY"), (3, 58300, "AI"),
    (4, 47800, "MONEY"), (5, 61200, "AI"), (6, 53600, "EFF"),
    (7, 45900, "CAREER"), (8, 46800, "CAREER"), (9, 59700, "AI"),
    (10, 55100, "EFF"), (11, 50300, "MONEY"), (12, 62400, "AI"),
]

ARTS = []
aid = 0
for issue, copies, topic in ISSUES:
    for k in range(2):
        aid += 1
        ARTS.append((aid, issue, topic if k == 0 else "CAREER", k == 0))

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE issues "
            "(issue INTEGER PRIMARY KEY, copies INTEGER, cover TEXT)")
con.execute("CREATE TABLE articles "
            "(article_id INTEGER PRIMARY KEY, issue INTEGER, "
            "topic TEXT, is_cover INTEGER)")
con.executemany("INSERT INTO issues VALUES (?,?,?)", ISSUES)
con.executemany("INSERT INTO articles VALUES (?,?,?,?)", ARTS)
con.commit()

sql = ("SELECT a.topic, COUNT(*) n, AVG(i.copies) avg_copies "
       "FROM articles a JOIN issues i ON a.issue = i.issue "
       "WHERE a.is_cover = 1 GROUP BY a.topic ORDER BY avg_copies DESC")
for row in con.execute(sql):
    print("%-6s %2d 期  平均 %6.0f" % (row[0], row[1], row[2]))

print()
good = con.execute("SELECT COUNT(*) FROM articles a JOIN issues i ON a.issue = i.issue").fetchone()[0]
bad = con.execute("SELECT COUNT(*) FROM articles a JOIN issues i").fetchone()[0]
print("写对 ON:%d 行" % good)
print("漏写 ON:%d 行 = %d x %d" % (bad, len(ARTS), len(ISSUES)))

实跑输出:

--------------------------------------------------------------------
SQL:把那条路线写下来
--------------------------------------------------------------------

  ① JOIN 就是「沿路线接起来」
  --------------------------------------------------------------------
  SELECT 选哪几列,FROM 从哪张表起步,JOIN 接哪张表,ON 用什么条件接。
  这四样正好对应上一篇那三步路线。写 SQL 就是把路线翻译成一句话。
  这条查询返回 12 行——每期一行,正好是封面文章。

  ② 加上 GROUP BY 就是答案
  --------------------------------------------------------------------

| 封面主题 | 期数 | 平均销量 |
|---|---:|---:|
| AI | 4 | 60400 |
| 效率工具 | 3 | 53700 |
| 理财 | 3 | 49067 |
| 职场 | 2 | 46350 |

  四行就是最终答案。AI 排第一,平均 60400;职场垫底,46350。
  整个查询六行,没有一次复制粘贴。

  ③ JOIN 写错会怎样
  --------------------------------------------------------------------
  最常见的错:忘了写 ON,或者 ON 的条件写错。
  少写 ON 条件,结果是 288 行 = 24 篇文章 × 12 期。
  这叫笛卡尔积。它不报错,只是把每篇文章和每一期都配了一遍。
  拿这个结果去求平均,得到的数看起来正常,其实毫无意义。
  ! JOIN 出错很少报错,只会悄悄多行或少行。所以每次 JOIN 完都要数一遍行数。

  ④ 怎么自己查出来
  --------------------------------------------------------------------
  第一条:JOIN 之前单独数两边的行数,JOIN 之后数结果行数,看对不对得上。
  第二条:用 LEFT JOIN 找孤儿行(右边为 NULL 的那些)。
  第三条:GROUP BY 之后,各组计数之和应该等于 JOIN 结果的行数。
  三条都是「对账」,不是「算数」。对账这一步比写查询更需要经验。

  ⑤ 从表到库,最后一步
  --------------------------------------------------------------------
  刚才这些表都存在一个文件里:magazine.db。能对它跑 SQL 的软件,叫 RDBMS。
  SQLite、MySQL、PostgreSQL、SQL Server 都是。语法大同小异,这一章的查询换过去基本能跑。
  关系数据库的价值不在于「能存很多」,在于「能沿关系做无穷多种比较」。
  换一个问题,只换一句 SQL,数据一行都不用动。这就是关系模型赢了电子表格的地方。

两条查询只差一个 ON,行数从 24 变 288。这就是为什么每次 JOIN 都要数行数。 注意 GROUP BY 之后各组计数加起来是 4+3+3+2 = 12,正好等于封面文章数——这就是第三条对账。

30 秒实验

  1. 如果把 WHERE is_cover = 1 去掉,GROUP BY 的结果会变成什么?还能回答原问题吗?
  2. 想同时看「封面主题」和「次条主题」的销量,查询要怎么改?
  3. 想一想:为什么 SQL 里写错条件不报错,这算优点还是缺点?

常见误区

  1. 漏写 ON 就交结果:笛卡尔积不报错,得到的平均看起来还挺正常。
  2. GROUP BY 的列没选全:选了主题却按别的列分组,结果会莫名其妙地合并或拆开。
  3. 图省事写 SELECT 全列:列一多就看不出哪列是键、哪列该汇总。
  4. 以为换数据库就要重学:SELECT 这一层几乎通用,差别在类型、索引和权限。
自测三问(点开对答案) 1. JOIN 的 ON 条件漏写会得到什么? —— 笛卡尔积:左表行数乘右表行数。这里 24 × 12 = 288 行,而且不报错。 2. 怎么用行数对账? —— JOIN 前数两边行数,JOIN 后数结果行数;再用各组计数之和核对 GROUP BY 的结果。 3. 关系数据库比电子表格强在哪? —— 同一次建模能支撑无穷多种比较。换问题只换查询,数据不用重排。

第 12 章到此收口。第 13 章讲另一件苦活:数据拿到手是脏的,怎么整理成能用的样子。

相关笔记