写 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 秒实验
- 如果把 WHERE is_cover = 1 去掉,GROUP BY 的结果会变成什么?还能回答原问题吗?
- 想同时看「封面主题」和「次条主题」的销量,查询要怎么改?
- 想一想:为什么 SQL 里写错条件不报错,这算优点还是缺点?
常见误区
- 漏写 ON 就交结果:笛卡尔积不报错,得到的平均看起来还挺正常。
- GROUP BY 的列没选全:选了主题却按别的列分组,结果会莫名其妙地合并或拆开。
- 图省事写 SELECT 全列:列一多就看不出哪列是键、哪列该汇总。
- 以为换数据库就要重学:SELECT 这一层几乎通用,差别在类型、索引和权限。
自测三问(点开对答案)
1. JOIN 的 ON 条件漏写会得到什么? —— 笛卡尔积:左表行数乘右表行数。这里 24 × 12 = 288 行,而且不报错。 2. 怎么用行数对账? —— JOIN 前数两边行数,JOIN 后数结果行数;再用各组计数之和核对 GROUP BY 的结果。 3. 关系数据库比电子表格强在哪? —— 同一次建模能支撑无穷多种比较。换问题只换查询,数据不用重排。第 12 章到此收口。第 13 章讲另一件苦活:数据拿到手是脏的,怎么整理成能用的样子。
- ☐ (本篇完,下一篇 13.1 一份捡来的客户名单)