SQL 查询语法速查表 - SELECT/JOIN/窗口函数大全

面向要写对业务又能跑快的 SQL 的开发者。SQL 的坑往往不在它本身,而在 NULL 的传播、GROUP BY 之后的筛选必须用 HAVING、窗口函数与普通聚合的边界这些最容易记混的地方。读完能写带分组、筛选子句的正确查询,用窗口函数做行号与同组内排名而非先 join 再自聚合,规避 NULL 比较与聚合陷阱,并在需要时把多条语句放进事务保证原子性。

数据库·共 35 条命令·最后更新 2026-08-02
sqldatabase查询db

典型使用场景

面向要写对业务又要跑快的后端开发者。SQL 的坑很少在语法本身,而在 NULL 的传播、GROUP BY 之后的筛选必须用 HAVING、窗口函数与普通聚合的分工,以及 JOIN 一对多导致的行数放大。本表覆盖基础查询、过滤、聚合、连接、窗口函数与事务写入六组,每组都标注了「什么时候会慢」或「什么时候会错」,可当排错清单用。

基础查询 Basic 6

SELECT * FROM users
查询全部列
SELECT name, age FROM users
查询指定列
SELECT * FROM users WHERE age > 18
带条件过滤
SELECT DISTINCT city FROM users
去重
SELECT * FROM users LIMIT 10 OFFSET 20
分页(跳过 20 取 10)
SELECT * FROM users ORDER BY age DESC
排序(降序)

过滤与逻辑 Filtering 6

WHERE name LIKE 'A%'
前缀匹配,命中前缀通常可走索引;% 开头则无法用普通索引,容易退化为全表扫
WHERE name LIKE '%son'
后缀匹配,以 % 开头会失去索引能力,数据量大时应考虑全文索引或反向存储
WHERE age BETWEEN 18 AND 30
范围
WHERE id IN (1, 2, 3)
集合匹配
WHERE a IS NULL
空值判断;SQL 里 NULL ≠ 空串,a = NULL 永远为假,必须用 IS NULL/IS NOT NULL
WHERE a AND b OR c
逻辑组合(AND 优先于 OR)

聚合与分组 Aggregation 5

SELECT COUNT(*) FROM users
计数
SELECT AVG(age) FROM users
平均值
SELECT city, COUNT(*) FROM users GROUP BY city
分组
HAVING COUNT(*) > 5
分组后过滤(对聚合结果)
SELECT MAX(age), MIN(age) FROM users
最大/最小值

连接 JOIN 5

FROM a JOIN b ON a.id = b.a_id
内连接,只返回两边都能匹配的行;连接一对多表会放大父表行数,需注意去重
FROM a LEFT JOIN b ON a.id = b.a_id
左连接(保留 a 全部)
FROM a RIGHT JOIN b ON a.id = b.a_id
右连接
FROM a CROSS JOIN b
笛卡尔积,返回 a×b 全部组合,行数会暴涨;仅在结果集可预期很小(如常量表)时使用
SELECT * 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 = 1
更新
DELETE FROM t WHERE id = 1
删除
BEGIN; ... COMMIT;
事务提交,把一组操作作为一个原子单元一次落盘,中途任一失败可回滚到 BEGIN 之前
ROLLBACK;
回滚事务,撤销本次事务内未提交的全部改动;多步操作用事务包裹更安全

表结构 DDL 4

CREATE TABLE t (id INT PRIMARY KEY, name TEXT)
建表
ALTER TABLE t ADD COLUMN age INT
加列
DROP TABLE t
删表
CREATE 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)前者计行数,后者跳过 NULLCOUNT(*) 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. 1先确认查询是否真的走了索引

    EXPLAIN SELECT * FROM orders WHERE customer_id = 42

    关注 type 列:ALL 表示全表扫描;Extra 出现 Using index 才算命中覆盖索引。

  2. 2排查空值条件的写法是否正确

    SELECT COUNT(*) FROM t WHERE deleted_at IS NULL

    用 IS NULL 而非 = NULL;两边数一数就能确认写法没写反。

  3. 3定位慢查询的执行计划

    EXPLAIN ANALYZE SELECT city, COUNT(*) FROM users GROUP BY city

    EXPLAIN ANALYZE 会真实执行并给出各阶段耗时,能看出是排序还是聚合慢。

  4. 4确认 JOIN 后行数被放大了多少

    SELECT COUNT(*) FROM a JOIN b ON a.id = b.a_id

    与 SELECT COUNT(*) FROM a 对比,差值即一对多放大的倍数。

  5. 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 DESC

WHERE 在分组前过滤行且不能用聚合函数,通常能走索引;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, rnk

GROUP 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日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们