作为前端开发者,我一直觉得"数据库是后端的事"。大学考完就忘,工作后更是碰都不碰。直到有一天,我发现看不懂后端给的慢查询日志,debug 全靠猜——那一刻才知道,SQL 这债迟早要还。
这篇文章把 SQL 的核心知识串一遍,从建表到多表查询,偏实战、不堆语法。
一切从建表开始。假设我们要做一个简易进销存系统:
CREATE DATABASE shop;
CREATE TABLE product (
id CHAR(4) NOT NULL,
name VARCHAR(100) NOT NULL,
type VARCHAR(32) NOT NULL,
sale_price INTEGER,
purchase_price INTEGER,
regist_date DATE
);
两个易错点:
CHAR vs VARCHAR:CHAR(4) 定长,不足 4 位自动补空格;VARCHAR(100) 变长,存多长占多长。字符串字段用 VARCHAR 更省空间。-- 查看表结构
DESC product;
-- 新增列
ALTER TABLE product ADD COLUMN name_pinyin VARCHAR(200);
-- 删除列
ALTER TABLE product DROP COLUMN name_pinyin;
-- 删除整张表(真删,没有回收站)
DROP TABLE product;
START TRANSACTION;
INSERT INTO product VALUES ('0001', 'T恤衫', '衣服', 1000, 500, '2009-09-20');
INSERT INTO product VALUES ('0002', '打孔器', '办公用品', 500, 320, '2009-09-11');
INSERT INTO product VALUES ('0003', '运动T恤', '衣服', 4000, 2800, NULL);
INSERT INTO product VALUES ('0004', '菜刀', '厨房用具', 3000, 2800, '2009-09-20');
INSERT INTO product VALUES ('0005', '高压锅', '厨房用具', 6800, 5000, '2009-01-15');
INSERT INTO product VALUES ('0006', '叉子', '厨房用具', 500, NULL, '2009-09-20');
INSERT INTO product VALUES ('0007', '擦菜板', '厨房用具', 880, 790, '2008-04-28');
INSERT INTO product VALUES ('0008', '圆珠笔', '办公用品', 100, NULL, '2009-11-11');
COMMIT;
START TRANSACTION + COMMIT 套住多条 INSERT,要么全成功要么全回滚。不加事务的话每条独立提交,出了错没法撤回。
-- 查特定列
SELECT id, name, sale_price FROM product;
-- 查所有列(线上慎用,扫全表开销大)
SELECT * FROM product;
-- 去重
SELECT DISTINCT type FROM product;
-- 别名
SELECT product_id AS id, product_name AS name FROM product;
执行顺序:FROM → WHERE → SELECT。数据库先找到表,再按条件筛行,最后挑出你要的列。
-- 等值
SELECT * FROM product WHERE type = '衣服';
-- 比较
SELECT * FROM product WHERE sale_price >= 3000;
-- 不等于
SELECT * FROM product WHERE type <> '办公用品';
-- 组合条件(AND 优先级高于 OR,用括号明确意图)
SELECT * FROM product
WHERE type = '办公用品'
AND (regist_date = '2009-09-11' OR regist_date = '2009-09-20');
-- 列间运算(毛利大于 500 的商品)
SELECT *, sale_price - purchase_price AS profit
FROM product
WHERE sale_price - purchase_price > 500;
NULL 不是 0,不是空字符串,是"不知道"。两条核心规则:
100 + NULL = NULL。= 或 <> 比较 NULL。WHERE price = NULL 永远返回空结果集。正确姿势:
-- 查找 purchase_price 为空的商品
SELECT * FROM product WHERE purchase_price IS NULL;
-- 查找 purchase_price 不为空的商品
SELECT * FROM product WHERE purchase_price IS NOT NULL;
-- 数行数(NULL 不计入 COUNT)
SELECT COUNT(*) FROM product; -- 8
SELECT COUNT(purchase_price) FROM product; -- 6(两行的 purchase_price 是 NULL)
-- 求和/平均/最大/最小
SELECT SUM(sale_price) FROM product;
SELECT AVG(sale_price) FROM product;
SELECT MAX(sale_price), MIN(sale_price) FROM product;
-- 按类型分组统计
SELECT type, COUNT(*), AVG(sale_price)
FROM product
GROUP BY type;
-- 分组后过滤(WHERE 筛行,HAVING 筛组)
SELECT type, COUNT(*)
FROM product
GROUP BY type
HAVING COUNT(*) >= 2;
WHERE vs HAVING:WHERE 在分组前筛行,HAVING 在分组后筛组。WHERE 里不能用聚合函数,HAVING 里可以。
子查询就是 SELECT 里面套 SELECT,从里到外执行。
-- 查找价格高于平均价的商品
SELECT * FROM product
WHERE sale_price > (SELECT AVG(sale_price) FROM product);
-- 查找和最贵商品同类型的商品
SELECT * FROM product
WHERE type = (SELECT type FROM product ORDER BY sale_price DESC LIMIT 1);
子查询的结果可以当一张临时表用,也可以当标量(单个值)嵌在 WHERE 里。
只返回两表都有匹配的行。
-- 假设有一张库存表 stock(product_id, quantity)
SELECT p.name, p.sale_price, s.quantity
FROM product AS p
INNER JOIN stock AS s ON p.id = s.product_id;
LEFT JOIN 保留左表所有行,右表没匹配的填 NULL。RIGHT JOIN 反过来。
-- 所有商品及其库存(没库存的显示 NULL)
SELECT p.name, s.quantity
FROM product AS p
LEFT JOIN stock AS s ON p.id = s.product_id;
一个经典的坑:什么时候用 LEFT、什么时候用 RIGHT?
记住一条原则——你想保留哪个表的全量数据,就把它放哪边。想要所有商品信息(不管有没有库存),product 放左边,用 LEFT JOIN。想要所有库存记录(不管有没有对应商品),stock 放左边。
-- 按价格降序,前 3 个
SELECT * FROM product ORDER BY sale_price DESC LIMIT 3;
-- 分页:跳过前 2 条,取 3 条(第 2 页)
SELECT * FROM product ORDER BY id LIMIT 2, 3;
SELECT * FROM employees ORDER BY hire_date DESC LIMIT 1;
注意:同一天可能有多人入职,不能简单 LIMIT 2,1。
SELECT * FROM employees
WHERE hire_date = (
SELECT DISTINCT hire_date
FROM employees
ORDER BY hire_date DESC
LIMIT 2, 1
);
先用子查询找出倒数第三个入职日期,再用主查询找该日期所有员工。
两张表:salaries(emp_no, salary, ...) 和 dept_manager(emp_no, dept_no, ...)。查出每个部门领导的薪水详情,输出结果带上部门编号。
SELECT s.emp_no, s.salary, s.from_date, s.to_date, d.dept_no
FROM salaries AS s
RIGHT JOIN dept_manager AS d ON s.emp_no = d.emp_no
ORDER BY s.emp_no;
为什么用 RIGHT JOIN 而不是 LEFT?因为 dept_manager 是主表——我们关心的是"每个部门领导",要以这张表为基准保留所有行。
不同数据库的 SQL 语法不完全一样:
LIMIT 1 取第一行LIMIT 1 一样,但 LIMIT 2, 1 写作 LIMIT 1 OFFSET 2SELECT TOP 1 或 OFFSET ... FETCHWHERE ROWNUM <= 1这也是 ORM(Prisma、TypeORM、Drizzle)流行的原因——它们帮你屏蔽方言差异。但理解原生 SQL 仍然是排查性能问题的基础。
| 概念 | 一句话 |
|---|---|
CHAR vs VARCHAR |
定长补空格 vs 变长省空间 |
WHERE vs HAVING |
分组前筛行 vs 分组后筛组 |
LEFT JOIN vs RIGHT JOIN |
保留左表全量 vs 保留右表全量 |
NULL 比较 |
必须用 IS NULL / IS NOT NULL |
| 子查询 | SELECT 套 SELECT,从内向外执行 |
| 事务 | 多条操作打包,要么全成功要么全回滚 |
SQL 与其说是学语法,不如说是学"怎么用集合的思维描述数据需求"。同一个需求可以写出五六种等价的 SQL,好的 SQL 和烂的 SQL 性能差几个数量级——但那是优化的话题了。
这篇文章够你从零写到能应付日常业务查询。剩下的事,多写多查执行计划,慢慢练。