SQL 查询语法速查表 - SELECT/JOIN/窗口函数大全
面向要写对业务又能跑快的 SQL 的开发者。SQL 的坑往往不在它本身,而在 NULL 的传播、GROUP BY 之后的筛选必须用 HAVING、窗口函数与普通聚合的边界这些最容易记混的地方。读完能写带分组、筛选子句的正确查询,用窗口函数做行号与同组内排名而非先 join 再自聚合,规避 NULL 比较与聚合陷阱,并在需要时把多条语句放进事务保证原子性。
典型使用场景
面向要写对业务又要跑快的后端开发者。SQL 的坑很少在语法本身,而在 NULL 的传播、GROUP BY 之后的筛选必须用 HAVING、窗口函数与普通聚合的分工,以及 JOIN 一对多导致的行数放大。本表覆盖基础查询、过滤、聚合、连接、窗口函数与事务写入六组,每组都标注了「什么时候会慢」或「什么时候会错」,可当排错清单用。
基础查询 Basic 6
SELECT * FROM usersSELECT name, age FROM usersSELECT * FROM users WHERE age > 18SELECT DISTINCT city FROM usersSELECT * FROM users LIMIT 10 OFFSET 20SELECT * FROM users ORDER BY age DESC过滤与逻辑 Filtering 6
WHERE name LIKE 'A%'WHERE name LIKE '%son'WHERE age BETWEEN 18 AND 30WHERE id IN (1, 2, 3)WHERE a IS NULLWHERE a AND b OR c聚合与分组 Aggregation 5
SELECT COUNT(*) FROM usersSELECT AVG(age) FROM usersSELECT city, COUNT(*) FROM users GROUP BY cityHAVING COUNT(*) > 5SELECT MAX(age), MIN(age) FROM users连接 JOIN 5
FROM a JOIN b ON a.id = b.a_idFROM a LEFT JOIN b ON a.id = b.a_idFROM a RIGHT JOIN b ON a.id = b.a_idFROM a CROSS JOIN bSELECT * FROM a NATURAL JOIN b窗口函数 Window 4
ROW_NUMBER() OVER (ORDER BY age)RANK() OVER (PARTITION BY city ORDER BY age)SUM(amount) OVER (PARTITION BY user_id)LAG(price) OVER (ORDER BY date)写入与事务 DML/TX 5
INSERT INTO t (a, b) VALUES (1, 'x')UPDATE t SET a = 2 WHERE id = 1DELETE FROM t WHERE id = 1BEGIN; ... COMMIT;ROLLBACK;表结构 DDL 4
CREATE TABLE t (id INT PRIMARY KEY, name TEXT)ALTER TABLE t ADD COLUMN age INTDROP TABLE tCREATE INDEX idx_name ON t(name)参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
LIKE 'A%' | 前缀匹配,通常能走索引 | WHERE name LIKE 'A%' |
LIKE '%son' | 后缀匹配,% 开头失去索引能力 | WHERE name LIKE '%son' |
IS NULL | 空值判断;a = NULL 恒为假 | WHERE deleted_at IS NULL |
GROUP BY | 分组,压多行为一行 | GROUP BY city |
HAVING | 分组后过滤,可引用聚合值 | HAVING COUNT(*) > 5 |
LEFT JOIN | 保留左表全部行 | a LEFT JOIN b ON a.id = b.a_id |
ROW_NUMBER() | 窗口函数排名,行内计算不丢明细 | ROW_NUMBER() OVER (PARTITION BY city ORDER BY age) |
LAG(col) | 取上一行值,用于环比计算 | LAG(price) OVER (ORDER BY date) |
BEGIN / COMMIT | 事务:多步操作原子落盘,可回滚 | BEGIN; UPDATE ...; COMMIT; |
CREATE INDEX | 建索引加速查询,代价是写入变慢 | CREATE INDEX idx_name ON t(name) |
LIMIT n OFFSET m | 分页;大 OFFSET 会退化为全量扫描 | LIMIT 10 OFFSET 20 |
COUNT(*) vs COUNT(col) | 前者计行数,后者跳过 NULL | COUNT(*) FROM t |
易错点与避坑指南
现象WHERE a = NULL 查询返回空结果,但表里明明有 NULL 行。
原因NULL 表示「值未知」,与任何值的比较结果都是 UNKNOWN 而非 TRUE。
处置改用 WHERE a IS NULL / IS NOT NULL 判断空值。
现象JOIN 订单与明细后统计订单数,结果比实际订单数多。
原因一对多 JOIN 会把父表的一行复制成 N 行,直接 COUNT 会重复计算。
处置先对子表聚合再 JOIN,或用 COUNT(DISTINCT 父表.id)。
现象WHERE 子句加了函数后查询明显变慢,原本秒级变成分钟级。
原因对字段用函数或表达式使索引失效,退化为全表扫描。
处置改写为范围条件(如 created_at >= ? AND created_at < ?),让索引能用上。
现象事务里执行了 CREATE INDEX 或 ALTER TABLE,之后无法回滚。
原因多数数据库中 DDL 会隐式提交当前事务。
处置把 DDL 移出事务;长事务中避免混用 DDL。
现象分页到很后面(如 OFFSET 100000)时查询突然变得极慢。
原因数据库仍需扫描并丢弃前 10 万行。
处置改用基于游标的分页(WHERE id > 上一页末尾 ORDER BY id LIMIT n)。
现象在 GROUP BY 之后的 SELECT 里引用了未分组的列,报「列必须出现在 GROUP BY 中」。
原因标准 SQL 要求 SELECT 的非聚合列必须包含在分组键里。
处置把该列加进 GROUP BY,或用聚合函数包裹(如 MAX(name))。
现象LIKE '%关键词%' 搜索在数据量大时超时。
原因前置通配符使 B-tree 索引无法定位,只能全表扫。
处置改用全文索引(MySQL FULLTEXT / PostgreSQL tsvector),或加搜索引擎承接。
排障路径
1先确认查询是否真的走了索引
EXPLAIN SELECT * FROM orders WHERE customer_id = 42关注 type 列:ALL 表示全表扫描;Extra 出现 Using index 才算命中覆盖索引。
2排查空值条件的写法是否正确
SELECT COUNT(*) FROM t WHERE deleted_at IS NULL用 IS NULL 而非 = NULL;两边数一数就能确认写法没写反。
3定位慢查询的执行计划
EXPLAIN ANALYZE SELECT city, COUNT(*) FROM users GROUP BY cityEXPLAIN ANALYZE 会真实执行并给出各阶段耗时,能看出是排序还是聚合慢。
4确认 JOIN 后行数被放大了多少
SELECT COUNT(*) FROM a JOIN b ON a.id = b.a_id与 SELECT COUNT(*) FROM a 对比,差值即一对多放大的倍数。
5给高频筛选字段补索引
CREATE INDEX idx_orders_customer ON orders(customer_id, created_at)复合索引遵循最左前缀,字段顺序按实际查询模式排;建完重新 EXPLAIN 验证。
命令示例
正确判断空值(a = NULL 永远不成立)
SELECT name FROM users WHERE deleted_at IS NULL
-- 错误写法:WHERE deleted_at = NULL 恒返回空结果NULL 表示「值未知」,任何与它的比较都得到 UNKNOWN 而非 TRUE。必须用 IS NULL / IS NOT NULL,这是 SQL 三值逻辑最常见的陷阱。
输出
alice bob carol
分组后过滤用 HAVING
SELECT city, COUNT(*) AS cnt
FROM users
GROUP BY city
HAVING COUNT(*) > 5
ORDER BY cnt DESCWHERE 在分组前过滤行且不能用聚合函数,通常能走索引;HAVING 在 GROUP BY 之后,可以引用 COUNT/AVG。先用 WHERE 缩小范围再 HAVING 更快。
输出
city | cnt -----------+----- Shanghai | 412 Beijing | 287 Shenzhen | 163
窗口函数排名,无需子查询
SELECT name, city, age,
ROW_NUMBER() OVER (PARTITION BY city ORDER BY age DESC) AS rnk
FROM users
ORDER BY city, rnkGROUP BY 会把多行压成一行并丢失明细,窗口函数在保留每行的同时计算聚合值。每组取前 N 条、累计求和这类需求用窗口函数一步到位。
输出
name | city | age | rnk --------+----------+-----+----- ali | Beijing | 34 | 1 bo | Beijing | 28 | 2 cat | Shanghai | 31 | 1
JOIN 一对多导致行数放大
SELECT u.name, COUNT(i.id) AS items
FROM users u
LEFT JOIN order_items i ON i.user_id = u.id
GROUP BY u.name
ORDER BY items DESC订单 join 明细时父表字段会重复出现,直接 count(*) 会多算。要么只 select 需要的父字段,要么先对子表聚合再 join。
输出
name | items ------+------- ali | 12 bo | 3 cat | 1
常见坑与注意事项
- WHERE a = NULL 恒不成立,空值判断必须用 IS NULL / IS NOT NULL。
- JOIN 一对多表会放大父表行数,直接统计总数会重复计算;先聚合子表再关联。
- WHERE 中对字段用函数或表达式会使索引失效,改写为范围条件才能走索引。
- LIKE 通配符前置(%abc)无法使用普通索引,数据量大时应改用全文索引。
- 未加 LIMIT 的全表查询或 UPDATE/DELETE 在生产库上可能锁表并拖垮服务,执行前先确认影响行数。
- 事务中混用 DDL(如 CREATE INDEX)会隐式提交,无法回滚,长事务里要避免。
提示
- AND 优先级高于 OR,复杂条件务必加括号。
- GROUP BY 的列必须出现在 SELECT(聚合列除外)。
- LIKE 区分大小写取决于数据库排序规则。
- 改数据前先用 BEGIN; 开启事务,确认后 COMMIT。
常见问题
为什么 a = NULL 永远查不到数据?
因为 SQL 里 NULL 表示「值未知」,不是某个具体值,任何与 NULL 的比较结果都是 UNKNOWN 而不是 TRUE,因此不会进入结果集。必须用 IS NULL 和 IS NOT NULL 判断空值。这是 SQL 三值逻辑最常见的陷阱,写条件时把 NULL 当成 0 或空串来比较是最典型的错误来源。
WHERE 和 HAVING 有什么区别?
WHERE 在分组之前过滤原始行,作用于单行且不能使用聚合函数,因此通常能走索引;HAVING 在 GROUP BY 之后过滤分组结果,可以引用 count、avg 这类聚合值。过滤行数优先用 WHERE,只有需要按聚合结果筛选时才用 HAVING,例如 HAVING COUNT(*) > 5。
JOIN 一对多时为什么行数会变多?
因为父表的一行会与子表的 N 行各自匹配,最终结果是 1×N 而不是 1。例如订单表 join 到订单明细,一单 3 个商品就产生 3 行,父表字段随之重复三次。此时应只 select 需要的父表字段,或先对子表聚合再 join,否则统计总数时会重复计算,把 count(*) 直接放在 join 后就会数多。
什么时候该加索引?
看查询模式而不是看感觉:出现在 WHERE、JOIN、ORDER BY 里的字段才是索引候选,且要看它是否真的出现在慢查询中。建索引会拖慢写入并占用存储,所以不是越多越好。一个实用起点是给主键、外键和高频筛选字段建索引,然后用执行计划验证是否命中,而不是建完就认为生效。
窗口函数相比 GROUP BY 的优势是什么?
GROUP BY 会把多行压成一行,丢失明细;窗口函数在保留每一行的同时计算聚合值。ROW_NUMBER() OVER (ORDER BY age) 给每行排名,SUM(amount) OVER (PARTITION BY user_id) 算分区内累计,LAG(price) OVER (ORDER BY date) 取上一行值。这类「排名、去重保留最新、按组累计」的需求用 GROUP BY 需要绕路,窗口函数一步到位。
由 巧匠 维护
公开更新于 2026年8月2日,内容持续校对官方文档。