SQL 语法速查表
查询、连接、聚合、窗口函数到常见坑,含可直接套用的示例
共 104 条语法
SELECT col1, col2 FROM t查询骨架SELECT id, name, created_at FROM users
按列查询,生产环境别用 SELECT *,否则加字段会牵动所有调用方
SELECT * FROM t WHERE id = 1查询骨架SELECT * FROM users WHERE id = 1
查全部列,只适合临时排查
SELECT DISTINCT col FROM t查询骨架SELECT DISTINCT city FROM users
去重,注意它会对整行去重而不是单列
SELECT col AS alias FROM t查询骨架SELECT user_name AS name FROM users
给列或表达式起别名
SELECT ... ORDER BY col DESC查询骨架SELECT id FROM orders ORDER BY created_at DESC
排序,DESC 降序、ASC 升序(默认)
SELECT ... ORDER BY a ASC, b DESC查询骨架SELECT id, score FROM exam ORDER BY score DESC, id ASC
多列排序,先按 a 再按 b;分页时务必加唯一列兜底
SELECT ... LIMIT 10 OFFSET 20查询骨架SELECT id FROM orders ORDER BY id LIMIT 10 OFFSET 20
分页,取第 21 到 30 行
SELECT TOP 10 ... FROM t查询骨架SELECT TOP 10 id FROM orders ORDER BY id DESC
SQL Server 的取前 N 行写法
SELECT ... FETCH FIRST 10 ROWS ONLY查询骨架SELECT id FROM orders ORDER BY id FETCH FIRST 10 ROWS ONLY
标准 SQL 的取前 N 行,Oracle 与新版 PG 都支持
SELECT a FROM t1 UNION SELECT b FROM t2查询骨架SELECT email FROM users UNION SELECT email FROM leads
合并结果并去重,列数与类型必须一致
SELECT a FROM t1 UNION ALL SELECT b FROM t2查询骨架SELECT id FROM a UNION ALL SELECT id FROM b
合并但不去重,比 UNION 快很多
WITH cte AS (SELECT ...) SELECT * FROM cte查询骨架WITH paid AS (SELECT * FROM orders WHERE status = 1) SELECT COUNT(*) FROM paid
公用表表达式,把复杂查询拆成可读的步骤
WITH RECURSIVE cte AS (... UNION ALL ...) SELECT * FROM cte查询骨架WITH RECURSIVE tree AS (SELECT id, parent_id FROM nodes WHERE parent_id IS NULL UNION ALL SELECT n.id, n.parent_id FROM nodes n JOIN tree t ON n.parent_id = t.id) SELECT * FROM tree
递归 CTE,查树形或图结构
SELECT CASE WHEN cond THEN a ELSE b END FROM t查询骨架SELECT CASE WHEN score >= 60 THEN 1 ELSE 0 END AS passed FROM exam
条件表达式,在结果里做分支
SELECT CAST(col AS INT) FROM t查询骨架SELECT CAST(price AS DECIMAL(10,2)) FROM orders
显式类型转换,比隐式转换安全
SELECT COALESCE(a, b, 0) FROM t查询骨架SELECT COALESCE(nickname, user_name) FROM users
取第一个非 NULL 值,处理空值最常用
SELECT CONCAT(a, b) FROM t查询骨架SELECT CONCAT(first_name, surname) FROM users
字符串拼接,MySQL 与 PG 也支持 || 运算符
SELECT ROW_NUMBER() OVER (...) FROM t查询骨架SELECT ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users
加行号(详见窗口函数组)
WHERE col = value筛选条件WHERE status = 1
等值筛选
WHERE col <> value筛选条件WHERE status <> 0
不等筛选,也可写 !=
WHERE col IN (1, 2, 3)筛选条件WHERE status IN (1, 2, 5)
多值匹配,比多个 OR 好读
WHERE col NOT IN (SELECT ...)筛选条件WHERE id NOT IN (SELECT user_id FROM bans)
排除子查询结果,子查询返回 NULL 时整个条件会变空集
WHERE col BETWEEN 10 AND 20筛选条件WHERE created_at BETWEEN 2 AND 8
闭区间包含两端,日期用 BETWEEN 会漏掉当天末尾时刻
WHERE name LIKE 'ab%'筛选条件WHERE email LIKE 'admin%'
前缀匹配,能用到索引
WHERE name LIKE '%ab%'筛选条件WHERE remark LIKE '%退货%'
包含匹配,前导通配符用不上索引
WHERE col IS NULL筛选条件WHERE deleted_at IS NULL
判空必须用 IS NULL,= NULL 永远不成立
WHERE col IS NOT NULL筛选条件WHERE email IS NOT NULL
非空判断,也可用于过滤未赋值字段
WHERE a = 1 AND (b = 2 OR c = 3)筛选条件WHERE status = 1 AND (type = 2 OR type = 3)
括号决定优先级:AND 先于 OR,不写括号极易出逻辑错
WHERE EXISTS (SELECT 1 FROM b WHERE b.a_id = a.id)筛选条件WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
存在性判断,通常比 IN 更快且不受 NULL 影响
WHERE created_at >= '2026-01-01'筛选条件WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01'
时间范围推荐用左闭右开,不要对列套函数
SELECT ... FROM a INNER JOIN b ON a.id = b.a_id连接SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id
内连接,只保留两边都匹配的行
SELECT ... FROM a LEFT JOIN b ON a.id = b.a_id连接SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id
左连接,保留左表全部行,右表缺失补 NULL
SELECT ... FROM a FULL OUTER JOIN b ON ...连接SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id
全外连接,两边不匹配的行都保留
SELECT ... FROM a CROSS JOIN b连接SELECT s.size, c.color FROM sizes s CROSS JOIN colors c
笛卡尔积,生成组合矩阵时故意这么写
SELECT ... FROM a LEFT JOIN b ON ... WHERE b.id IS NULL连接SELECT u.* FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.id IS NULL
找左表有而右表没有的行(差集)
SELECT ... FROM a JOIN b USING (id)连接SELECT * FROM users JOIN profiles USING (user_id)
同名列连接,结果里该列只出现一次
SELECT ... FROM a x JOIN a y ON x.pid = y.id连接SELECT e.name, m.name AS manager FROM emp e JOIN emp m ON e.mgr_id = m.id
自连接,查层级关系
LEFT JOIN b ON ... AND b.status = 1连接SELECT u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 1
左连接的多余条件必须写在 ON 里;写进 WHERE 会退化成内连接
SELECT col, COUNT(*) FROM t GROUP BY col聚合与分组SELECT city, COUNT(*) FROM users GROUP BY city
按列分组统计
SELECT COUNT(*) FROM t聚合与分组SELECT COUNT(*) FROM orders
统计行数,包含 NULL 行
SELECT COUNT(col) FROM t聚合与分组SELECT COUNT(email) FROM users
统计该列非 NULL 的行数,和 COUNT(*) 结果可能不同
SELECT COUNT(DISTINCT col) FROM t聚合与分组SELECT COUNT(DISTINCT user_id) FROM orders
统计去重后的数量
SELECT SUM(col), AVG(col), MIN(col), MAX(col) FROM t聚合与分组SELECT SUM(amount), AVG(amount) FROM orders
求和、平均、最小、最大;AVG 忽略 NULL 会让分母变小
SELECT col FROM t GROUP BY col HAVING COUNT(*) > 1聚合与分组SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1
分组后筛选,找重复数据最常用
SELECT GROUP_CONCAT(col) FROM t GROUP BY g聚合与分组SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM users GROUP BY city
MySQL 把分组内多个值拼成一个字符串,PG 用 STRING_AGG 并指定分隔符
SELECT COUNT(*) FILTER (WHERE cond) FROM t聚合与分组SELECT COUNT(*) FILTER (WHERE status = 1) AS paid FROM orders
条件聚合,一次扫描算多个指标;MySQL 用 SUM(cond) 替代
SELECT a, b, COUNT(*) FROM t GROUP BY ROLLUP (a, b)聚合与分组SELECT city, status, COUNT(*) FROM orders GROUP BY ROLLUP (city, status)
自动补小计与总计行
SELECT a, b FROM t GROUP BY a, b聚合与分组SELECT city, status FROM orders GROUP BY city, status
多列分组;SELECT 里的非聚合列必须都出现在 GROUP BY 中
ROW_NUMBER() OVER (PARTITION BY g ORDER BY t DESC)窗口函数ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
组内排名,不并列,是「每组取最新一条」的核心
RANK() OVER (ORDER BY score DESC)窗口函数RANK() OVER (ORDER BY score DESC) AS r
并列排名,跳过后续名次(1,2,2,4)
DENSE_RANK() OVER (ORDER BY score DESC)窗口函数DENSE_RANK() OVER (ORDER BY score DESC) AS r
并列排名,不跳号(1,2,2,3)
LAG(col, 1) OVER (ORDER BY t)窗口函数LAG(amount, 1, 0) OVER (ORDER BY created_at) AS prev_amount
取上一行的值,算环比与差值
LEAD(col, 1) OVER (ORDER BY t)窗口函数LEAD(amount) OVER (ORDER BY created_at) AS next_amount
取下一行的值
SUM(x) OVER (ORDER BY t)窗口函数SUM(amount) OVER (ORDER BY created_at) AS running_total
累计求和,默认窗口是从开头到当前行
AVG(x) OVER (PARTITION BY g)窗口函数AVG(amount) OVER (PARTITION BY user_id) AS user_avg
组内平均,不折叠行数,可与明细并列显示
NTILE(4) OVER (ORDER BY x)窗口函数NTILE(4) OVER (ORDER BY amount DESC) AS quartile
把结果均分成 N 桶,分位数与分层抽样用
FIRST_VALUE(x) OVER (PARTITION BY g ORDER BY t)窗口函数FIRST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS first_amount
组内第一行的值
LAST_VALUE(x) OVER (... ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)窗口函数LAST_VALUE(amount) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_amount
取组内最后一行;不写完整窗口帧会得到当前行而非末行
SELECT * FROM (SELECT ..., ROW_NUMBER() OVER (...) rn FROM t) x WHERE rn = 1窗口函数SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM orders) x WHERE rn = 1
取每组最新一条的标准写法,必须套一层子查询
WHERE rn = 1 -- 窗口函数不能直接写在 WHERE 里窗口函数SELECT * FROM (SELECT id, ROW_NUMBER() OVER (PARTITION BY g ORDER BY t) rn FROM t) x WHERE rn <= 3
窗口函数在 WHERE 时还未求值,必须先物化成子查询或 CTE
INSERT INTO t (a, b) VALUES (1, 2), (3, 4)数据修改INSERT INTO users (name, email) VALUES ('Ada', 'ada@example.com')
插入一行或多行,多行写成多个括号更快
INSERT INTO t (a, b) SELECT a, b FROM src数据修改INSERT INTO archive SELECT * FROM orders WHERE created_at < 2025
从查询结果批量插入
INSERT ... ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col数据修改INSERT INTO s (id, v) VALUES (1, 10) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v
PostgreSQL 的 upsert:存在则更新,不存在则插入
INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col)数据修改INSERT INTO s (id, v) VALUES (1, 10) ON DUPLICATE KEY UPDATE v = VALUES(v)
MySQL 的 upsert 写法
UPDATE t SET col = value WHERE cond数据修改UPDATE users SET status = 1 WHERE id = 10
更新指定行,WHERE 漏写会更新全表
UPDATE t SET a = b.a FROM b WHERE t.id = b.id数据修改UPDATE orders o SET price = p.price FROM products p WHERE o.product_id = p.id
PostgreSQL 的连接更新;MySQL 写 JOIN,SQL Server 写 FROM
DELETE FROM t WHERE cond数据修改DELETE FROM sessions WHERE expires_at < NOW()
按条件删除
TRUNCATE TABLE t数据修改TRUNCATE TABLE staging_rows
清空整表,比 DELETE 快且重置自增值,但不可回滚(多数库)
MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE ...数据修改MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.v = s.v WHEN NOT MATCHED THEN INSERT VALUES (s.id, s.v)
标准 SQL 的合并写入,Oracle 与 SQL Server 支持
REPLACE INTO t (id, v) VALUES (1, 2)数据修改REPLACE INTO s (id, v) VALUES (1, 10)
MySQL 特有:先删后插,自增主键会被改变,触发器行为也和 upsert 不同
CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(50) NOT NULL)表结构CREATE TABLE users (id BIGINT PRIMARY KEY, name VARCHAR(50) NOT NULL, created_at TIMESTAMP DEFAULT NOW())
建表,主键与非空是最基本的约束
CREATE TABLE t (..., CONSTRAINT fk FOREIGN KEY (a_id) REFERENCES a(id))表结构CREATE TABLE orders (id BIGINT PRIMARY KEY, user_id BIGINT, CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id))
外键约束,保证引用完整性但会影响写入性能
CREATE UNIQUE INDEX uk ON t (col)表结构CREATE UNIQUE INDEX uk_email ON users (email)
唯一约束,也能阻止并发写入重复值
ALTER TABLE t ADD COLUMN c TYPE表结构ALTER TABLE users ADD COLUMN phone VARCHAR(20)
加字段,大表加非空默认值字段可能锁表
ALTER TABLE t DROP COLUMN c表结构ALTER TABLE users DROP COLUMN phone
删字段
ALTER TABLE t ALTER COLUMN c TYPE VARCHAR(100)表结构ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(30)
改字段类型(PostgreSQL 写法;MySQL 用 MODIFY COLUMN)
ALTER TABLE t RENAME TO t2表结构ALTER TABLE users RENAME TO app_users
改表名
DROP TABLE IF EXISTS t表结构DROP TABLE IF EXISTS tmp_import
删表,加 IF EXISTS 避免脚本重跑报错
CREATE VIEW v AS SELECT ...表结构CREATE VIEW v_paid AS SELECT * FROM orders WHERE status = 1
视图,只存查询定义不存数据
CREATE TABLE t AS SELECT ...表结构CREATE TABLE users_backup AS SELECT * FROM users
按查询结果建表,约束与索引不会带过来
CREATE INDEX idx ON t (col)索引与执行计划CREATE INDEX idx_created ON orders (created_at)
普通索引,加速查询但拖慢写入
CREATE INDEX idx ON t (a, b)索引与执行计划CREATE INDEX idx_user_time ON orders (user_id, created_at)
复合索引遵守最左前缀:只查 b 用不上它
DROP INDEX idx索引与执行计划DROP INDEX idx_created
删索引(SQL Server 需写 DROP INDEX t.idx)
EXPLAIN SELECT ...索引与执行计划EXPLAIN SELECT * FROM orders WHERE user_id = 1
查看执行计划,重点看 type 与 key(MySQL)或 Seq Scan(PG)
EXPLAIN ANALYZE SELECT ...索引与执行计划EXPLAIN ANALYZE SELECT COUNT(*) FROM orders
真正执行并给出实际耗时与行数,比 EXPLAIN 准
CREATE INDEX idx ON t (a) INCLUDE (b)索引与执行计划CREATE INDEX idx_user ON orders (user_id) INCLUDE (amount)
覆盖索引,把要查的列带上,免去回表
BEGIN事务与锁BEGIN
开启事务(也可写 START TRANSACTION)
COMMIT事务与锁COMMIT
提交事务
ROLLBACK事务与锁ROLLBACK
回滚事务
SAVEPOINT sp1事务与锁SAVEPOINT sp1
设置保存点,可部分回滚
ROLLBACK TO SAVEPOINT sp1事务与锁ROLLBACK TO SAVEPOINT sp1
回滚到保存点,之前的操作仍保留
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ事务与锁SET TRANSACTION ISOLATION LEVEL READ COMMITTED
设置隔离级别:读未提交/读已提交/可重复读/串行化
SELECT ... FOR UPDATE事务与锁SELECT * FROM accounts WHERE id = 1 FOR UPDATE
加写锁,防并发超卖;必须在事务内使用
SELECT ... FOR UPDATE SKIP LOCKED事务与锁SELECT * FROM jobs WHERE status = 0 ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED
跳过已加锁的行,做任务队列抢占的标准写法
col = NULL -- 永远不成立常见坑SELECT * FROM t WHERE deleted_at = NULL
NULL 表示未知,与任何值比较结果都是未知,必须用 IS NULL
NOT IN (子查询含 NULL) -- 结果为空常见坑SELECT * FROM a WHERE id NOT IN (SELECT a_id FROM b)
子查询返回哪怕一个 NULL,NOT IN 就会返回空集,改用 NOT EXISTS
WHERE id = '123' -- 数字列用字符串比较常见坑SELECT * FROM users WHERE phone = 13800000000
隐式类型转换会让索引失效,参数类型必须与列类型一致
WHERE DATE(created_at) = '2026-01-01'常见坑WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'
对列套函数会让索引失效,改写成范围条件
WHERE a = 1 OR b = 2常见坑SELECT * FROM t WHERE a = 1 UNION SELECT * FROM t WHERE b = 2
OR 跨列时单列索引都用不上,可拆成两次查询再 UNION
LIMIT 20 OFFSET 100000常见坑SELECT * FROM t WHERE id > 100000 ORDER BY id LIMIT 20
深分页要先扫描再丢弃前面所有行,改用游标式翻页
DELETE FROM t -- 没有 WHERE常见坑DELETE FROM t WHERE created_at < 2024
漏写 WHERE 会清空整表;执行前先用同条件的 SELECT 验证
SELECT ...
FROM t ORDER BY id -- 排序不稳定常见坑SELECT id, name FROM t ORDER BY score DESC, id ASC LIMIT 20
排序值有并列时顺序不确定,分页会漏行或重复,务必加唯一列兜底
一个事务里改几十万行常见坑-- 改成每批 1000 行循环提交 DELETE FROM logs WHERE created_at < 2024 LIMIT 1000;
大事务会长时间持锁、撑爆回滚段,改成小批多次提交
字符串比较受排序规则影响常见坑SELECT * FROM t WHERE name = 'Ada' -- 大小写敏感取决于 collation
MySQL 默认排序规则不区分大小写,换库后行为可能相反
这个工具不好用,或者遇到 bug?
去反馈