🏠 总目录📚 本教程 01 · 一条查询是怎么跑的 ← →
📑 本页目录(点开跳转)

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,但索引该不该建、怎么建是那一章的题目,本教程不重讲

✅ 检查点

  1. 按真正的执行顺序,八个步骤依次是哪些?SELECT 排第几?
  2. 为什么标准 SQL 不允许在 WHERE 里用 SELECT 起的别名?SQLite 上会怎样,Postgres 上会怎样?
  3. WHERE SUM(cents) > 1 会报什么错?该换成什么?
  4. HAVING 在 SELECT 之前执行,为什么 SQLite/MySQL 却允许 HAVING s > 1(s 是别名)?Postgres 呢?
  5. 实跑那条查询在 FROM / WHERE / GROUP BY / HAVING 四步之后分别剩几行(组)?
  6. 为什么说 GROUP BY 是一道「单向门」?跨过去之后哪类问题就问不出来了?
  7. 只写 LIMIT 不写 ORDER BY 有什么问题?判据是什么?
  8. 逻辑顺序和物理计划各用来回答什么问题?实跑里 WHERE feature='summary' 在物理上发生了什么?
  9. SELECT * 的三个坑分别是什么?
  10. 把一个问题翻译成 SQL 的六句话里,哪一句最重要?它在后面几章分别变成了什么形式?
👀 答案
  1. FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。 SELECT 排第五,倒数第三。
  2. 因为 WHERE(第②步)跑的时候 SELECT(第⑤步)还没执行,别名还不存在。 ⚠️ SQLite 是个例外,实跑 WHERE jiao > 1 跑通了(返回两行); Postgres 报 column "jiao" does not exist,MySQL 报 Unknown column 'jiao' in 'where clause'。 💀 所以本地测通不等于上线能跑。
  3. SQLite 报 OperationalError : misuse of aggregate: SUM()(Postgres 是 aggregate functions are not allowed in WHERE)。 换成 HAVING SUM(cents) > 1。
  4. 因为标准里确实不合法,SQLite/MySQL 特意加了这颗语法糖 —— 否则 HAVING SUM(cents) > 1 要把表达式写两遍。 ⚠️ Postgres 没加:它的文档明写输出列名可用于 ORDER BY/GROUP BY、但不能用于 WHERE 和 HAVING(🗓️ 未实跑,据 PG 官方文档)。 ⚠️ 这不代表顺序表错了:顺序表描述语义,各库在不改语义的前提下加糖。
  5. 5 → 3 → 2 → 1(5 行 → 3 行 → 2 组 → 1 组),最终结果 [('summary', 12.84)]。
  6. 因为门前操作的是行,门后操作的是组。跨过去之后「这一行的 user_id 是谁」没有答案了 —— 一组里有好几个 user_id,只能问「这一组里有几个不同的 user_id」。 报错 misuse of aggregate 十次有九次就是站错了侧。
  7. 返回哪几行标准不保证,现在跑十次一样不代表建了索引之后还一样。 判据:写了 LIMIT 就必须写 ORDER BY,且排序键要能唯一定序(撞值时补主键兜底)。
  8. 逻辑顺序答「能不能这么写」「结果为什么是这样」;物理计划答「快不快」「索引用上没有」。 实跑里 WHERE 被下推到取数那一步:计划第一行是 SEARCH calls USING INDEX idx_feature (feature=?), 数据库根本没有先摆出整张表;去掉 WHERE 之后同一条查询变成 SCAN calls。
  9. ① 列的顺序和数量不受控(加一列,按下标取值的代码悄悄取错,不报错); ② 多读用不上的列,覆盖索引永远不可能生效;③ JOIN 之后重名列,按名字取值拿到哪个看驱动。
  10. 「现在一行代表什么?」 02 章是「连完之后一行不再代表一张订单」, 03 章是「聚合之后一行代表一组」,07 章是「一行还是代表一行,只是多知道了邻居的事」。

🛑 可以停在这里

⚡ 走神救援

⭐ SQL 的书写顺序和执行顺序是两回事。 真正的顺序是八步,⭐ 而 SELECT 排第五、倒数第三。

⭐⭐ 第三步(分组)是一道单向门:门前操作的是行,门后操作的是组——跨过去之后「这一行的某列是谁」就没有答案了。那个「聚合函数用错地方」的报错,十次有九次是站错了侧。

这个顺序直接解释四类日常报错:过滤条件里用 SELECT 的别名标准上不合法(过滤在第二步,别名第五步才诞生),💀 而某些库跑得通、换个库就报错——本地测通不等于上线能跑;要按聚合过滤就得用 HAVING;⚠️ 有些库给 HAVING 加了别名这颗糖、有些没加(⭐ 顺序描述的是语义,加糖不改语义);排序在 SELECT 之后所以别名一定可见,⚠️ 但加了去重之后就不能再拿没选出来的列排序。

💀 ⭐ 只写 LIMIT 不写 ORDER BY,返回哪几行标准不保证。 判据:写 LIMIT 就必须写 ORDER BY,而且排序键要唯一定序——撞值时补主键兜底,这正是后面复合游标分页的来历。

⚠️ 逻辑顺序不等于物理执行:看执行计划会发现数据库根本没先摆出整张表,过滤被下推到了取数那一步。⭐ 两者各管各的:逻辑顺序答「能不能写、为什么是这个结果」,物理计划答「快不快、索引用上没」。

SELECT * 三个坑:列序和列数不受控(加一列,按下标取值的代码悄悄取错)、覆盖索引永远失效、连接后重名列。

⭐ 最后是全教程的方法核心——拿到问题先问六句,⭐⭐ 其中「现在一行代表什么」是最重要的一句:绝大多数「不报错的错」追到最后都是它答错了。

下一节 👉 02-JOIN的几种语义.md

打卡记录保存在你的浏览器里,首页能看到总进度