📑 本页目录(点开跳转)
01 · 一条查询是怎么跑的
⏱ 54 分钟 | ⭐ SELECT 写在最前面,却几乎是最后才执行——搞懂这个顺序,一半的报错自己就解释了
🎯 一句话
SQL 的书写顺序和执行顺序完全是两回事:FROM 最先跑,SELECT 排在倒数第三。
这一条不是冷知识,它直接决定了「这个别名能不能在这里用」「这个函数能不能写在这里」「这一行为什么被扔了」。
🚦 一、八步,按真正的先后排
你写下的是这个顺序:
SELECT feature, SUM(cents) AS total
FROM calls
WHERE model = 'big'
GROUP BY feature
HAVING SUM(cents) > 1
ORDER BY total DESC
LIMIT 1
数据库执行的是另一个顺序:
| 步 | 子句 | 它做什么 | 做完之后手上是什么 |
|---|---|---|---|
| ① | FROM / JOIN |
把要用的表摆出来、连起来 | 一张宽表,行数可能比任何一张原表都多(⚠️ 02 章的正题) |
| ② | WHERE |
逐行判断,扔掉不要的行 | 还是行,只是少了 |
| ③ | GROUP BY |
把剩下的行按键塌成组 | 组,不再是行 |
| ④ | HAVING |
逐组判断,扔掉不要的组 | 组,只是少了 |
| ⑤ | SELECT |
算出每一组(或每一行)要输出的列,别名在这一步才诞生 | 结果行 |
| ⑥ | DISTINCT |
对结果行去重 | 结果行 |
| ⑦ | ORDER BY |
排序(⭐ 它能看见 ⑤ 造出来的别名) | 有序的结果行 |
| ⑧ | LIMIT / OFFSET |
截断 | 最终结果 |
把上面那条查询每一步剩多少行量出来:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("""CREATE TABLE calls(
user_id TEXT, feature TEXT, model TEXT, cents REAL)""")
db.executemany("INSERT INTO calls VALUES(?,?,?,?)", [
("u1", "chat", "small", 0.056),
("u1", "summary", "big", 2.64),
("u2", "summary", "big", 10.2),
("u2", "chat", "small", 0.03),
("u3", "translate","big", 0.40),
])
rows = db.execute("""
SELECT feature, SUM(cents) AS total
FROM calls
WHERE model = 'big'
GROUP BY feature
HAVING SUM(cents) > 1
ORDER BY total DESC
LIMIT 1
""").fetchall()
print("结果:", rows)
# 中途量一下每一步剩多少行
print("FROM 之后:", db.execute("SELECT COUNT(*) FROM calls").fetchone()[0])
print("WHERE 之后:", db.execute("SELECT COUNT(*) FROM calls WHERE model='big'").fetchone()[0])
print("GROUP BY 之后:", db.execute(
"SELECT COUNT(*) FROM (SELECT feature FROM calls WHERE model='big' GROUP BY feature)").fetchone()[0])
print("HAVING 之后:", db.execute(
"SELECT COUNT(*) FROM (SELECT feature FROM calls WHERE model='big' "
"GROUP BY feature HAVING SUM(cents) > 1)").fetchone()[0])
实跑输出:
结果: [('summary', 12.84)]
FROM 之后: 5
WHERE 之后: 3
GROUP BY 之后: 2
HAVING 之后: 1
⭐ 五行 → 三行 → 两组 → 一组。 注意第三步那个箭头跨过了一道坎:
前面是行,后面是组。 一旦跨过去,「这一行的 user_id 是谁」这个问题就没有答案了 ——
组里有好几个 user_id,你只能问「这一组里有几个不同的 user_id」。
⭐ 整章最值得记住的一句:
GROUP BY是一道单向门。 门前你操作的是行,门后你操作的是组。 报错信息里那句misuse of aggregate,十次有九次是因为你站在门的错误一侧。
🧩 二、这个顺序解释了四类日常报错
① WHERE 里用 SELECT 的别名
按逻辑顺序,WHERE(②)跑的时候 SELECT(⑤)还没执行,别名根本不存在。
⚠️ 但 SQLite 会放你过去 —— 这是它的一个扩展,不是标准行为:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE calls(feature TEXT, cents REAL)")
db.executemany("INSERT INTO calls VALUES(?,?)",
[("chat", 0.056), ("summary", 2.64), ("summary", 10.2)])
# ① ORDER BY 用 SELECT 里的别名
print("ORDER BY 用别名:",
db.execute("SELECT feature, cents*10 AS jiao FROM calls ORDER BY jiao DESC").fetchall())
# ② WHERE 用 SELECT 里的别名
try:
print("WHERE 用别名:",
db.execute("SELECT feature, cents*10 AS jiao FROM calls WHERE jiao > 1").fetchall())
except Exception as e:
print("WHERE 用别名 ->", type(e).__name__, ":", e)
# ③ WHERE 里放聚合函数
try:
print("WHERE 放聚合:",
db.execute("SELECT feature FROM calls WHERE SUM(cents) > 1 GROUP BY feature").fetchall())
except Exception as e:
print("WHERE 放聚合 ->", type(e).__name__, ":", e)
# ④ HAVING 放聚合
print("HAVING 放聚合:",
db.execute("SELECT feature, SUM(cents) FROM calls GROUP BY feature HAVING SUM(cents) > 1").fetchall())
# ⑤ HAVING 里能不能用 SELECT 的别名
try:
print("HAVING 用别名:",
db.execute("SELECT feature, SUM(cents) AS s FROM calls GROUP BY feature HAVING s > 1").fetchall())
except Exception as e:
print("HAVING 用别名 ->", type(e).__name__, ":", e)
实跑输出:
ORDER BY 用别名: [('summary', 102.0), ('summary', 26.400000000000002), ('chat', 0.56)]
WHERE 用别名: [('summary', 26.400000000000002), ('summary', 102.0)]
WHERE 放聚合 -> OperationalError : misuse of aggregate: SUM()
HAVING 放聚合: [('summary', 12.84)]
HAVING 用别名: [('summary', 12.84)]
💀 这里有个陷阱值得单独说:WHERE jiao > 1 在 SQLite 里跑通了。
你在本地测得好好的,同一条 SQL 拿到 Postgres 上会得到 column "jiao" does not exist,
MySQL 也会报 Unknown column 'jiao' in 'where clause'。
⭐ 判据:别名只在 SELECT 之后的子句里可靠(ORDER BY 一定可以;GROUP BY / HAVING 各库不同 —— ⚠️ Postgres 恰好是 GROUP BY 可以、HAVING 不行)。
想在 WHERE 里用一个算出来的值,把表达式重写一遍,或者用子查询/CTE 包一层(05、06 章)。
② WHERE 里放聚合函数
SUM() 要等 GROUP BY(③)把组分好才有意义,而 WHERE 在②。所以这次三个库口径一致:
SQLite 报 misuse of aggregate: SUM(),Postgres 报 aggregate functions are not allowed in WHERE。
⭐ 要按聚合结果过滤,那是 HAVING(④)的活。
③ HAVING 能用别名(SQLite / MySQL 可以,⚠️ Postgres 不行)
看起来和上面矛盾 —— HAVING(④)也在 SELECT(⑤)前面啊?
是的,标准 SQL 里 HAVING 用别名也是不合法的,但三个主流库都特意放宽了这一条,因为 HAVING SUM(cents) > 1 写两遍太蠢。
⚠️ 别把「三个库都放宽了」当成「顺序表错了」 —— 顺序表描述的是语义,各库在不改变语义的前提下加了糖。
④ ORDER BY 能用别名,而且能用没出现在 SELECT 里的列
ORDER BY(⑦)在 SELECT(⑤)之后,所以别名一定可见。
⚠️ 但加了 DISTINCT(⑥)之后就不行了 —— 去重之后那些没被选出来的列已经不存在了,没法拿来排序。
📋 三、一张表:每个子句「站在哪一侧」
⭐ 遇到「这里能不能写 X」,查这张表比背规则快:
| 子句 | 它看得见 | 它看不见 | 典型误用 |
|---|---|---|---|
ON |
参与连接的两张表的原始列 | 别名、聚合结果 | 把过滤条件写进 ON(⚠️ 对 LEFT JOIN 会改语义,02 章) |
WHERE |
单行的原始列 | 别名(标准上)、聚合结果、窗口函数结果 | 用它过滤聚合值 |
GROUP BY |
原始列、表达式、列序号 | 聚合结果 | 忘了把 SELECT 里的非聚合列也分组(03 章) |
HAVING |
整组:分组键 + 聚合结果 | 组内单行的其他列 | 用它做行级过滤(能跑,但慢且难读) |
SELECT |
分组键、聚合结果;未分组时是单行的列 | 自己刚起的别名(同一层内) | SELECT a AS x, x*2 |
ORDER BY |
别名、结果列、以及原表的列 | 加了 DISTINCT 之后:没被选出来的列 |
以为 ORDER BY 在 LIMIT 之后 |
LIMIT |
已排好序的结果 | —— | ⚠️ 不写 ORDER BY 就 LIMIT,返回哪几行是不确定的 |
💀 最后一格值得展开:SELECT * FROM t LIMIT 10 每次给你哪十行,SQL 标准不保证。
现在跑十次一样,不代表建了索引之后还一样、换了库之后还一样。
⭐ 判据:只要写了 LIMIT,就必须写 ORDER BY,而且排序键要能唯一定序(撞值时补一个主键兜底 —— 这正是 05 章那个复合游标的来历)。
🛑 读到这里可以停 —— 已经读了约 25 分钟。 最后一段还有(约 26 分钟):逻辑顺序 ≠ 物理执行顺序 ·
SELECT *的三个坑 · 把一个问题翻译成这八步 · 检查点与走神救援 回来的时候不用重读,直接从下一节接着看就行。
🚧 四、逻辑顺序 ≠ 物理执行顺序
上面那张八步表描述的是语义:结果必须看起来像是按这个顺序算出来的。 真正跑的时候,优化器可以随便重排,只要结果一样。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE calls(user_id TEXT, feature TEXT, cents REAL)")
db.executemany("INSERT INTO calls VALUES(?,?,?)",
[(f"u{i%50}", "chat" if i % 3 else "summary", i * 0.01) for i in range(5000)])
db.execute("CREATE INDEX idx_feature ON calls(feature)")
# 逻辑顺序说 FROM 先跑;物理上优化器可以先用索引把行挑出来
for r in db.execute("EXPLAIN QUERY PLAN "
"SELECT user_id, SUM(cents) FROM calls "
"WHERE feature='summary' GROUP BY user_id ORDER BY 2 DESC LIMIT 3"):
print(r)
print("---")
# 同一条查询,去掉 WHERE 之后计划变了
for r in db.execute("EXPLAIN QUERY PLAN "
"SELECT user_id, SUM(cents) FROM calls GROUP BY user_id"):
print(r)
实跑输出(5000 行,SQLite 3.50.4):
(9, 0, 62, 'SEARCH calls USING INDEX idx_feature (feature=?)')
(14, 0, 0, 'USE TEMP B-TREE FOR GROUP BY')
(58, 0, 0, 'USE TEMP B-TREE FOR ORDER BY')
---
(6, 0, 216, 'SCAN calls')
(8, 0, 0, 'USE TEMP B-TREE FOR GROUP BY')
⭐ 注意第一行:逻辑上「FROM 先把整张表摆出来,WHERE 再逐行扔」,
物理上数据库根本没有摆出整张表 —— 它直接拿 idx_feature 只把 feature='summary' 那些行捞出来。
WHERE 的条件被下推到了取数那一步。
⚠️ 这两件事各管各的,别混:
| 用来回答 | 在哪一章 | |
|---|---|---|
| 逻辑顺序(八步表) | 「这么写能不能跑」「这个结果为什么是这样」 | 本章 |
物理计划(EXPLAIN) |
「它快不快」「索引有没有被用上」 | 09 章 |
⭐ 一条实用建议:调试语义问题时看八步表,调试性能问题时看 EXPLAIN QUERY PLAN。
拿计划去解释「为什么这个数不对」,几乎总是走弯路。
🧾 五、SELECT * 的三个坑
SELECT * 在探索数据时非常好用,⭐ 但不该出现在任何一段会被存下来的 SQL 里(视图、报表、代码、定时任务)。三个理由:
① 列的顺序和数量不受你控制。
有人给表加了一列,你那段 cursor.fetchall() 之后按下标取 row[3] 的 Python 代码就悄悄取错了列 ——
⚠️ 又是一个不报错的错误。
② 多读了用不上的列。
09 章会讲「覆盖索引」:只读索引里有的列时数据库可以不回表。SELECT * 让覆盖索引永远不可能生效。
③ JOIN 之后会有重名列。
两张表都有 id,SELECT * 之后你拿到两个都叫 id 的列,按名字取值时拿到哪个取决于驱动实现。
⭐ 一个折中:探索时用
SELECT *,定稿前把列名一个个写出来。 写列名这件事的额外收益是:它逼你回答「这张表我到底要哪几列」, 而这个问题的答案往往会顺手暴露出「我连了一张根本用不上的表」。
🔁 六、把一个问题翻译成这八步
⭐ 这是整套教程的方法核心。 拿到一个自然语言问题,不要直接开始写 SELECT,按这个顺序问自己六句话:
| 问自己 | 对应哪一步 | 举例(「上周哪个功能最烧钱,只看大模型」) |
|---|---|---|
| ① 数据在哪几张表?怎么连? | FROM / JOIN |
一张 calls 就够,不用连 |
| ② ⭐ 连完之后,一行代表什么? | —— | 一行 = 一次模型调用 |
| ③ 哪些行不该参与? | WHERE |
model='big' 且 ts 在上周 |
| ④ 答案的粒度是什么? | GROUP BY |
一行 = 一个 feature |
| ⑤ 每一组要算什么? | SELECT 里的聚合 |
SUM(cents) |
| ⑥ 要不要扔掉一些组?怎么排?取几个? | HAVING / ORDER BY / LIMIT |
按总额降序取前 1 |
⭐ 第 ② 步是全教程最重要的一个问题:「现在一行代表什么?」
它在后面每一章都会以不同形式回来: 02 章的扇出是「连完之后一行不再代表一张订单」, 03 章的分组是「聚合之后一行代表一组」, 07 章的窗口函数则是「一行还是代表一行,只是多知道了一些邻居的事」。
⚠️ 绝大多数「不报错的错」,追到最后都是这一句答错了。
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 02-JOIN的几种语义.html | 第①步 FROM/JOIN 展开细讲。⭐ 本章说「连完之后行数可能比任何原表都多」,那一章告诉你多出来的行怎么把 SUM 从 150 变成 250 |
| 03-聚合与分组.html | 第③④⑤步的正题:GROUP BY 那道单向门的两侧各能做什么,WHERE 和 HAVING 的分界怎么用 |
| 04-NULL与三值逻辑.html | ⭐ 本章说「WHERE 逐行判断,扔掉不要的行」—— 那一章会告诉你 WHERE 其实只放行判为真的,判为「未知」的也一起扔了 |
| 05-子查询的三种形状.html | 想在 WHERE 里用一个算出来的值,正规解法是包一层子查询;本章那个 LIMIT 必须配唯一定序,也是那一章复合游标的来历 |
| 09-计划里的连接与索引失效.html | 本章第四节只演示了「逻辑顺序不是物理顺序」,EXPLAIN 怎么读、写法怎么把索引废掉在那一章 |
| AI 全栈 · 06 关系数据库 | 本章第四节用了一个 CREATE INDEX,但索引该不该建、怎么建是那一章的题目,本教程不重讲 |
✅ 检查点
- 按真正的执行顺序,八个步骤依次是哪些?
SELECT排第几? - 为什么标准 SQL 不允许在
WHERE里用SELECT起的别名?SQLite 上会怎样,Postgres 上会怎样? WHERE SUM(cents) > 1会报什么错?该换成什么?HAVING在SELECT之前执行,为什么 SQLite/MySQL 却允许HAVING s > 1(s是别名)?Postgres 呢?- 实跑那条查询在
FROM / WHERE / GROUP BY / HAVING四步之后分别剩几行(组)? - 为什么说
GROUP BY是一道「单向门」?跨过去之后哪类问题就问不出来了? - 只写
LIMIT不写ORDER BY有什么问题?判据是什么? - 逻辑顺序和物理计划各用来回答什么问题?实跑里
WHERE feature='summary'在物理上发生了什么? SELECT *的三个坑分别是什么?- 把一个问题翻译成 SQL 的六句话里,哪一句最重要?它在后面几章分别变成了什么形式?
👀 答案
FROM/JOIN→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT。SELECT排第五,倒数第三。- 因为
WHERE(第②步)跑的时候SELECT(第⑤步)还没执行,别名还不存在。 ⚠️ SQLite 是个例外,实跑WHERE jiao > 1跑通了(返回两行); Postgres 报column "jiao" does not exist,MySQL 报Unknown column 'jiao' in 'where clause'。 💀 所以本地测通不等于上线能跑。 - SQLite 报
OperationalError : misuse of aggregate: SUM()(Postgres 是aggregate functions are not allowed in WHERE)。 换成HAVING SUM(cents) > 1。 - 因为标准里确实不合法,SQLite/MySQL 特意加了这颗语法糖 —— 否则
HAVING SUM(cents) > 1要把表达式写两遍。 ⚠️ Postgres 没加:它的文档明写输出列名可用于ORDER BY/GROUP BY、但不能用于WHERE和HAVING(🗓️ 未实跑,据 PG 官方文档)。 ⚠️ 这不代表顺序表错了:顺序表描述语义,各库在不改语义的前提下加糖。 - 5 → 3 → 2 → 1(5 行 → 3 行 → 2 组 → 1 组),最终结果
[('summary', 12.84)]。 - 因为门前操作的是行,门后操作的是组。跨过去之后「这一行的
user_id是谁」没有答案了 —— 一组里有好几个user_id,只能问「这一组里有几个不同的user_id」。 报错misuse of aggregate十次有九次就是站错了侧。 - 返回哪几行标准不保证,现在跑十次一样不代表建了索引之后还一样。
判据:写了
LIMIT就必须写ORDER BY,且排序键要能唯一定序(撞值时补主键兜底)。 - 逻辑顺序答「能不能这么写」「结果为什么是这样」;物理计划答「快不快」「索引用上没有」。
实跑里
WHERE被下推到取数那一步:计划第一行是SEARCH calls USING INDEX idx_feature (feature=?), 数据库根本没有先摆出整张表;去掉WHERE之后同一条查询变成SCAN calls。 - ① 列的顺序和数量不受控(加一列,按下标取值的代码悄悄取错,不报错);
② 多读用不上的列,覆盖索引永远不可能生效;③
JOIN之后重名列,按名字取值拿到哪个看驱动。 - 「现在一行代表什么?」 02 章是「连完之后一行不再代表一张订单」, 03 章是「聚合之后一行代表一组」,07 章是「一行还是代表一行,只是多知道了邻居的事」。
🛑 可以停在这里
⚡ 走神救援
⭐ SQL 的书写顺序和执行顺序是两回事。 真正的顺序是八步,⭐ 而
SELECT排第五、倒数第三。⭐⭐ 第三步(分组)是一道单向门:门前操作的是行,门后操作的是组——跨过去之后「这一行的某列是谁」就没有答案了。那个「聚合函数用错地方」的报错,十次有九次是站错了侧。
这个顺序直接解释四类日常报错:过滤条件里用
SELECT的别名标准上不合法(过滤在第二步,别名第五步才诞生),💀 而某些库跑得通、换个库就报错——本地测通不等于上线能跑;要按聚合过滤就得用HAVING;⚠️ 有些库给HAVING加了别名这颗糖、有些没加(⭐ 顺序描述的是语义,加糖不改语义);排序在SELECT之后所以别名一定可见,⚠️ 但加了去重之后就不能再拿没选出来的列排序。💀 ⭐ 只写
LIMIT不写ORDER BY,返回哪几行标准不保证。 判据:写LIMIT就必须写ORDER BY,而且排序键要唯一定序——撞值时补主键兜底,这正是后面复合游标分页的来历。⚠️ 逻辑顺序不等于物理执行:看执行计划会发现数据库根本没先摆出整张表,过滤被下推到了取数那一步。⭐ 两者各管各的:逻辑顺序答「能不能写、为什么是这个结果」,物理计划答「快不快、索引用上没」。
SELECT *三个坑:列序和列数不受控(加一列,按下标取值的代码悄悄取错)、覆盖索引永远失效、连接后重名列。⭐ 最后是全教程的方法核心——拿到问题先问六句,⭐⭐ 其中「现在一行代表什么」是最重要的一句:绝大多数「不报错的错」追到最后都是它答错了。
下一节 👉 02-JOIN的几种语义.md