Skip to content

01 · 关系模型与 SQL 基础 ​

前端最常用的 array.filter().map() 只是"单表思维"。数据库是集合思维:一次声明"我要什么样的行",由引擎决定怎么取。本篇帮你建立关系模型心智,并把 JOIN、子查询、GROUP BY、窗口函数一次讲透。

速览卡 ​

项内容
一句话定位从"前端数组操作"迁移到"集合思维"的 SQL 心智课
核心内容关系模型、SELECT 执行顺序、JOIN 类型、子查询、GROUP BY 与窗口函数
读完能回答JOIN 和子查询什么时候等价?为什么我写的 SQL 慢但数组操作不慢?
相关板块DB02 索引原理 | C10 数据建模与访问 | SDB01 SQL 速查

一、集合思维:SQL 不是"一行行处理" ​

前端处理数据是命令式的:遍历、条件、拼装。 SQL 是声明式的:你说"我要哪些行",引擎决定怎么找。

前端操作SQL 对应差异
arr.filter(...)WHERESQL 在磁盘/内存层筛,你只描述条件
arr.map(...)SELECT 列 / 表达式投影
arr.sort()ORDER BY通常要排序或走索引
arr.reduce()SUM/COUNT/AVG聚合,且要配合 GROUP BY
嵌套循环查关联JOIN由引擎选连接算法

关键认知:SQL 不保证行处理顺序。不要在 SELECT 里依赖"上一步的结果",一切以 WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT 的逻辑顺序理解。


二、SELECT 逻辑执行顺序(背下来) ​

text
FROM / JOIN    先定"从哪些表取"
WHERE          行级过滤
GROUP BY       分组
HAVING         组级过滤(只能用聚合结果)
SELECT         投影与计算
ORDER BY       排序
LIMIT          取前 N

为什么重要:WHERE 里不能用 SELECT 里定义的别名(别名还没算出来);HAVING 里能用聚合(因为分组已完成)。这两条是新手报错的高频点。


三、JOIN:四种连接到底取什么 ​

sql
-- 以 用户 users 左连订单 orders 为例
SELECT u.name, o.id FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
JOIN语义前端类比
INNER JOIN两边都匹配的行两个数组做交集
LEFT JOIN左表全保留,右表无匹配补 NULL左表做 map 找不到就补空
RIGHT JOIN右表全保留对称的 LEFT
FULL OUTER JOIN两边都保留并集,无匹配补 NULL

最常见 bug:LEFT JOIN 后 WHERE 里过滤右表列会让它"变回 INNER"。 因为 o.id IS NOT NULL 把无匹配行筛掉了。


四、子查询 vs JOIN:什么时候等价 ​

写法适合
JOIN要取被关联表的列、或做聚合拼接
IN (子查询)只要"存在即算",且子查询结果不大
EXISTS只要存在性判断,子查询很大时更优(可提前短路)
派生表 FROM (...)先算一个中间结果再继续查(注意必须起别名)
sql
-- 三种表达"买过东西的用户",结果可等价但代价不同
SELECT DISTINCT u.name FROM users u JOIN orders o ON o.user_id = u.id;
SELECT u.name FROM users u WHERE u.id IN (SELECT user_id FROM orders);
SELECT u.name FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

别纠结"哪个更快"的玄学:把执行计划(EXPLAIN)跑出来看,而不是猜。


五、GROUP BY 与窗口函数:聚合 vs 逐行保留 ​

5.1 GROUP BY ​

sql
-- 每个用户的下单数
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;   -- 组级过滤

SELECT 里出现的非聚合列必须出现在 GROUP BY 里(否则大多数数据库报错,只有 MySQL 宽松)。这是"分组后一行代表一组"的后果。

5.2 窗口函数:不压缩行数 ​

sql
-- 每个用户最近一单的排名,行数不变
SELECT user_id, amount,
       ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders;
函数用途
ROW_NUMBER()编号,排名不并列
RANK()/DENSE_RANK()排名,可并列(RANK 会跳号)
SUM() OVER (...)累计/分组小计,保留每一行
LAG()/LEAD()取上/下一行,环比常用

判断框架:要"每行都保留、同时算一个组内统计" → 窗口函数;要"每组只剩一行" → GROUP BY。


六、事务与索引:SQL 之外的"能不能跑快、准" ​


七、自检清单 ​

  • [ ] 能用集合思维描述一条 SQL,而不只是"遍历"
  • [ ] 背得出 SELECT 逻辑执行顺序,知道 WHERE 里不能用别名
  • [ ] 知道 LEFT JOIN + WHERE 过滤右表列会变回 INNER
  • [ ] 知道子查询 vs JOIN vs EXISTS 各自的适用场景
  • [ ] 会用 GROUP BY + HAVING,并知道非聚合列要进 GROUP BY
  • [ ] 会用窗口函数保留每一行做排名/累计
  • [ ] 慢查询会用 EXPLAIN 看执行计划,而不是猜

八、常见误区 ​

误区事实
"SQL 是逐行执行的"声明式,引擎决定执行计划
"GROUP BY 后还能随便选列"非聚合列必须进 GROUP BY
"LEFT JOIN 后加 WHERE 右表列 = 某值 没问题"会把 LEFT 变回 INNER
"JOIN 一定比子查询快"取决于执行计划,看 EXPLAIN
"窗口函数和 GROUP BY 一样"窗口函数不压缩行数
"LIMIT 就少取数据"排序/分组仍可能全量计算,索引才真正减少代价

九、动手练习 ​

练习 A:集合思维转换 ​

把前端代码翻译成 SQL:orders.filter(o => o.user_id === 3 && o.amount > 100).sort((a,b) => b.created_at - a.created_at).slice(0, 10)。

练习 B:追一次 LEFT JOIN 丢失 ​

SELECT u.name, o.id FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.id = 123 会丢什么?怎么改才对?

练习 C:写一个"每人最近一单" ​

只用窗口函数,不 GROUP BY。要求保留每单的完整信息。


相关文档 ​

本区 ​

其他域 ​