White Meta
登录Admin
语言
中文EN
Theme: ...

© 2026 White Meta

回到顶部

返回文章列表

SQL 入门:从只会 SELECT * 到能写业务查询

作为前端开发者,我一直觉得"数据库是后端的事"。大学考完就忘,工作后更是碰都不碰。直到有一天,我发现看不懂后端给的慢查询日志,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
);

两个易错点:

  • 最后一列的行尾不能有逗号——SQL 不像 JavaScript 那么宽容。
  • 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 三件套

基础查询

-- 查特定列
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。数据库先找到表,再按条件筛行,最后挑出你要的列。

WHERE 条件

-- 等值
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:SQL 里最坑的概念

NULL 不是 0,不是空字符串,是"不知道"。两条核心规则:

  1. 任何算术运算遇到 NULL,结果都是 NULL。100 + NULL = NULL。
  2. 不能用 = 或 <> 比较 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 里。


表关联(JOIN):SQL 的灵魂

INNER JOIN(内连接)

只返回两表都有匹配的行。

-- 假设有一张库存表 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 / RIGHT JOIN(外连接)

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 放左边。


ORDER BY 与 LIMIT

-- 按价格降序,前 3 个
SELECT * FROM product ORDER BY sale_price DESC LIMIT 3;

-- 分页:跳过前 2 条,取 3 条(第 2 页)
SELECT * FROM product ORDER BY id LIMIT 2, 3;

实战:三道经典面试题

1. 查找最晚入职员工

SELECT * FROM employees ORDER BY hire_date DESC LIMIT 1;

2. 查找入职时间倒数第三的员工

注意:同一天可能有多人入职,不能简单 LIMIT 2,1。

SELECT * FROM employees
WHERE hire_date = (
    SELECT DISTINCT hire_date
    FROM employees
    ORDER BY hire_date DESC
    LIMIT 2, 1
);

先用子查询找出倒数第三个入职日期,再用主查询找该日期所有员工。

3. 联表查询:部门领导薪水

两张表: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 方言问题

不同数据库的 SQL 语法不完全一样:

  • MySQL:LIMIT 1 取第一行
  • PostgreSQL:LIMIT 1 一样,但 LIMIT 2, 1 写作 LIMIT 1 OFFSET 2
  • SQL Server:用 SELECT TOP 1 或 OFFSET ... FETCH
  • Oracle:WHERE 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 性能差几个数量级——但那是优化的话题了。

这篇文章够你从零写到能应付日常业务查询。剩下的事,多写多查执行计划,慢慢练。