SQL 速查表
SQL 速查:SELECT、JOIN、GROUP BY、INSERT、UPDATE、DELETE、聚合函数与约束等常用语句的语法与示例,写查询时快速查阅。
原理解析
SQL 是操作关系型数据库的标准声明式语言:你描述「要什么」,优化器决定「怎么取」。DML(SELECT/INSERT/UPDATE/DELETE)处理数据,DDL(CREATE/ALTER/DROP)定义结构,DCL(GRANT)管权限。
数据以表(行/列)组织,表间靠外键关联,JOIN 把多表拼成结果集。索引加速查询但拖慢写入;事务以 ACID(原子/一致/隔离/持久)保证并发安全。写查询时优先用参数化语句防 SQL 注入。
什么时候该查这张表
这张表在你要查 SQL 语法但不想开文档站时有用。它的价值是覆盖面:DDL、DML、聚合、窗口函数、事务控制按类别排好,缺什么能立刻定位。但 SQL 方言差异极大,同一句「取前 N 行」在 MySQL 是 LIMIT、在 SQL Server 是 TOP n、在 Oracle 是 ROWNUM,跨库时必须转换。
不该查的情况:你在调查询性能。索引类型、执行计划、代价模型属于数据库调优范畴,一条语法正确但全表扫描的查询比语法错误慢得多。本表不提供任何性能判据。
判断标准:语法拼接、报表查询、一次性导出用本表就够了;涉及事务隔离、锁行为、并发写入时,要看具体数据库的文档而不是通用语法——同样一句 UPDATE 在不同隔离级别下的行为可能完全不同。
特别注意 SQL 注入:字符串拼接而非参数绑定是后端最常见的漏洞来源。本表列出的语法本身没问题,但任何含变量占位的写法都应当用参数化查询(Prepared Statement),不要用引号包裹变量。
边界与常见陷阱
NULL 参与任何比较都返回 NULL
这是最反直觉的一点:NULL <> 1 的结果不是 true 而是 NULL,因此 WHERE 会把它过滤掉。需要显式写成 IS NULL / IS NOT NULL。聚合函数同样忽略 NULL(COUNT(列) 不计空值但 COUNT(*) 计行数),而 AVG 与 SUM 对全 NULL 返回 NULL 而非 0。
DELETE 与 TRUNCATE 的语义差别很大
DELETE 是逐行删除并写事务日志,可回滚、有 WHERE 时只删部分;TRUNCATE 是整体清空,通常不可回滚(依数据库与引擎而定)、重置自增主键、不触发 DELETE 触发器。在有外键约束时 TRUNCATE 往往直接失败,而 DELETE 可以先删子表。误用会在生产上丢数据或失败。
UPDATE 忘了写 WHERE 会清空整表
这条每年都在发生。工程上可用的防护:执行 UPDATE/DELETE 前先跑同样条件的 SELECT 看影响行数;数据库层面对无 WHERE 的 UPDATE 拒绝执行或要求显式确认;权限上把 UPDATE 与 DELETE 收窄到必须带条件的视图或存储过程。单靠「小心」不可靠。
JOIN 的 NULL 语义容易被忽略
LEFT JOIN 后右表的条件写在 WHERE 里会把结果退化成 INNER JOIN(因为 NULL <> x 为 NULL,不成立)。条件应写在 ON 子句中。此外 COUNT(右表.列) 在 LEFT JOIN 下不统计未匹配的行,统计「总行数」要用 COUNT(*) 而不是 COUNT(列),这是报表数字对不上的常见根因。
与相邻内容的分工
要查某个具体数据库(MySQL / PostgreSQL / SQLite / SQL Server)的方言差异与版本特性,应查该数据库官方文档,本表只给通用写法。
要格式化或校验 SQL 语句,用站内 SQL 工具;本表不含 SQL 解析与语法高亮能力。
要理解索引为何不生效、EXPLAIN 怎么看,需数据库调优知识,超出本表范围。
若目的是把查询结果安全导出(处理引号、编码分隔符),那是导出流程问题,不是 SQL 语法问题。
实战片段
要点:① UPDATE/DELETE 前先跑同条件 SELECT 看影响行数,事务包裹并在核对后提交;② NULL 只能用 IS NULL 判断,= NULL 恒为 NULL;③ LEFT JOIN 的右表条件必须放 ON,放 WHERE 会退化成 INNER JOIN,且统计行数用 COUNT(*) 而非 COUNT(列);④ 变量一律参数绑定,字符串拼接是 SQL 注入的主要入口。
-- ① 改写前先看影响面:把 UPDATE 的 WHERE 套进 SELECT
SELECT id, name, status
FROM orders
WHERE status = 'pending' AND created_at < '2024-01-01';
-- 确认行数合理,再执行 UPDATE;行数异常就是条件写错了
BEGIN;
UPDATE orders SET status = 'expired'
WHERE status = 'pending' AND created_at < '2024-01-01';
-- 核对受影响行数与上一步一致
COMMIT; -- 不满意就 ROLLBACK;
-- ② NULL 必须显式判断
SELECT name FROM users WHERE manager_id IS NULL; -- 不是 = NULL
SELECT name FROM users WHERE manager_id IS NOT NULL;
-- ③ LEFT JOIN 的条件写在 ON,不要写 WHERE
SELECT c.name AS customer, o.total
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.status = 'paid'; -- 放在 ON 里才保留未付费客户
-- 统计总行数用 COUNT(*),不是 COUNT(o.total)
SELECT COUNT(*) AS customers, COUNT(o.total) AS paid_rows
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid';
-- ④ 用参数绑定,不要拼字符串(SQL 注入防线)
PREPARE stmt FROM 'SELECT * FROM users WHERE email = ?';
SET @email = 'a@example.com';
EXECUTE stmt USING @email;
DEALLOCATE PREPARE stmt;实践注解
- NULL 参与的比较一律返回 UNKNOWNWHERE col = NULL 永不成立,必须写 IS NULL。同理 NOT IN 子查询若结果集含 NULL,整个条件会变成 UNKNOWN 导致返回空集——这是「明明有数据却查不到」的经典原因。
- COUNT(*) 与 COUNT(col) 语义不同COUNT(*) 数行数,COUNT(col) 只数该列非 NULL 的行。做「非空率」统计时若用错,会把有 NULL 的列算成偏小;COUNT(DISTINCT col) 又会忽略 NULL。
- 隐式类型转换会让索引失效WHERE varchar_col = 123 会让多数数据库把列侧转成数字,索引无法使用,全表扫描。字段类型与传入参数类型必须一致,尤其注意字符集与排序规则也要匹配。
数据来源
- ISO/IEC 9075 (SQL)
- Database vendor dialect documentation
上述编号、名称与定义均来自公开规范,本站不主张对规范数据本身的独创性;本页的编排方式、中文释义与实践注解由本站撰写。
| 语句 | 说明 | 示例 |
|---|---|---|
| SELECT | 查询列(* 表示全部) | SELECT id, name FROM users |
| FROM | 指定数据来源表 | FROM orders |
| WHERE | 行过滤条件 | WHERE status = 'paid' |
| GROUP BY | 按列分组聚合 | GROUP BY category |
| HAVING | 对分组结果再过滤 | HAVING COUNT(*) > 1 |
| ORDER BY | 排序(ASC/DESC) | ORDER BY created_at DESC |
| LIMIT | 限制返回行数 | LIMIT 10 |
| OFFSET | 跳过前 N 行(分页) | OFFSET 20 |
| DISTINCT | 去重返回 | SELECT DISTINCT city |
| JOIN | 内连接两表 | FROM a JOIN b ON a.id = b.a_id |
| LEFT JOIN | 左连接,保留左表全部行 | FROM a LEFT JOIN b ON a.id = b.a_id |
| RIGHT JOIN | 右连接,保留右表全部行 | FROM a RIGHT JOIN b ON a.id = b.a_id |
| INNER JOIN | 内连接(同 JOIN) | FROM a INNER JOIN b ON a.id = b.a_id |
| ON | 连接条件 | ON a.id = b.a_id |
| AS | 表或列别名 | SELECT name AS 姓名 |
| INSERT INTO | 插入行 | INSERT INTO users (name) VALUES ('Tom') |
| UPDATE | 更新行 | UPDATE users SET name = 'Tom' WHERE id = 1 |
| DELETE | 删除行 | DELETE FROM users WHERE id = 1 |
| CREATE TABLE | 建表 | CREATE TABLE t (id INT PRIMARY KEY) |
| ALTER TABLE | 修改表结构 | ALTER TABLE t ADD COLUMN age INT |
| DROP TABLE | 删除表 | DROP TABLE t |
| PRIMARY KEY | 主键约束 | id INT PRIMARY KEY |
| FOREIGN KEY | 外键约束 | FOREIGN KEY (a_id) REFERENCES a(id) |
| INDEX | 创建索引加速查询 | CREATE INDEX idx_name ON users(name) |
| UNION | 合并两个查询结果(去重) | SELECT a FROM t1 UNION SELECT a FROM t2 |
| LIKE | 模糊匹配(% 任意,_ 单字) | WHERE name LIKE 'T%' |
| IN | 值属于集合 | WHERE status IN ('paid','done') |
| BETWEEN | 范围闭区间 | WHERE age BETWEEN 18 AND 30 |
| NULL / IS NULL | 空值判断(不能用 =) | WHERE deleted_at IS NULL |
| COUNT / SUM / AVG | 聚合函数 | SELECT COUNT(*) FROM users |
| CASE WHEN | 条件表达式 | CASE WHEN age>18 THEN 'adult' ELSE 'minor' END |
| EXPLAIN | 查看执行计划 | EXPLAIN SELECT * FROM users |
常见问题
INNER JOIN 和 LEFT JOIN 有什么区别?
INNER JOIN 只返回两表都匹配的行;LEFT JOIN 返回左表全部行,右表无匹配时对应列填 NULL。需要"保留主表所有记录"时用 LEFT JOIN,只需交集时用 INNER JOIN。
WHERE 和 HAVING 有什么区别?
WHERE 在分组前过滤行;HAVING 在 GROUP BY 之后过滤分组结果,且可引用聚合函数(如 COUNT(*))。不能在 WHERE 中使用聚合函数。
LIKE 的 % 和 _ 有什么区别?
% 匹配任意长度(含零)字符;_ 只匹配单个字符。如 'T%' 匹配以 T 开头任意串,'T_m' 只匹配 T 后一个任意字符再接 m(如 Tom)。
如何判断 NULL?
NULL 表示未知,不能用 = NULL 判断,必须用 IS NULL / IS NOT NULL。在多数数据库中 NULL 与任何值比较都返回未知(非真),容易踩坑。