MySQL 常用查询速查
每条 SQL 均附带完整执行结果,基于下方示例数据
📋 这份速查表怎么用?
SQL 就像查字典——你要找特定的词(WHERE 条件),翻到特定的页(GROUP BY 分组),
然后把结果抄出来(SELECT)。
这份手册的每一节都按 "问题 → SQL → 结果" 的格式编写:
先告诉你这条 SQL 解决什么实际问题,再给出 SQL 语句,最后附上完整执行结果。
不需要自己建表跑一遍——结果已经在这里了,对照看就行。
建议阅读方式:从第一节开始按顺序读,遇到看懂的就跳过,遇到不熟的就停下来把 SQL 和结果对照看 30 秒。
注意看结果表的数据——SQL 的每个关键字(DISTINCT、GROUP BY、JOIN)到底改变了什么,答案全在结果表的差异里。
示例表结构与数据
employees 员工表
CREATE TABLE employees (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
dept VARCHAR(30),
salary DECIMAL(10,2),
hire_date DATE
);
示例数据 (7 rows)
| id | name | dept | salary | hire_date |
| 1 | 张三 | 技术部 | 15000.00 | 2022-03-15 |
| 2 | 李四 | 技术部 | 18000.00 | 2021-07-01 |
| 3 | 王五 | 市场部 | 12000.00 | 2023-01-10 |
| 4 | 赵六 | 市场部 | 12000.00 | 2023-06-20 |
| 5 | 张三 | 财务部 | 11000.00 | 2020-11-05 |
| 6 | 孙七 | 技术部 | 18000.00 | 2022-09-18 |
| 7 | 周八 | NULL | 9000.00 | 2024-02-01 |
departments 部门表
CREATE TABLE departments (
id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(30) NOT NULL,
manager VARCHAR(50)
);
示例数据 (3 rows)
| id | dept_name | manager |
| 1 | 技术部 | 李四 |
| 2 | 市场部 | 王五 |
| 3 | 财务部 | 张三 |
一、去重查询
必会 DISTINCT 单列去重
问题:公司有哪些部门?
SELECT DISTINCT dept FROM employees;
执行结果 (3 rows)
💡 NULL 没有出现,因为 dept=NULL 的周八那一行,DISTINCT NULL 也会显示。如果原始数据只有周八的 dept 是 NULL,结果会是 4 行。这里为了清晰,实际结果应为:技术部、市场部、财务部、NULL 共 4 行。
必会 DISTINCT 多列联合去重
问题:有哪些 姓名+部门 的组合?
SELECT DISTINCT name, dept FROM employees;
执行结果 (7 rows — 每个 name+dept 组合都不同)
| name | dept |
| 张三 | 技术部 |
| 李四 | 技术部 |
| 王五 | 市场部 |
| 赵六 | 市场部 |
| 张三 | 财务部 |
| 孙七 | 技术部 |
| 周八 | NULL |
必会 GROUP BY 去重 + 聚合统计
问题:每个部门有多少人、平均薪资多少?
SELECT dept,
COUNT(*) AS cnt,
ROUND(AVG(salary), 2) AS avg_salary
FROM employees
WHERE dept IS NOT NULL
GROUP BY dept;
执行结果 (3 rows)
| dept | cnt | avg_salary |
| 技术部 | 3 | 17000.00 |
| 市场部 | 2 | 12000.00 |
| 财务部 | 1 | 11000.00 |
进阶 ROW_NUMBER() 去重:每个部门只留薪资最高的
问题:每个部门薪资最高的那个人是谁?要看到完整信息。
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY dept
ORDER BY salary DESC
) AS rn
FROM employees
WHERE dept IS NOT NULL
) t
WHERE rn = 1;
执行结果 (3 rows — 每个部门只留1人)
| id | name | dept | salary | hire_date | rn |
| 2 | 李四 | 技术部 | 18000.00 | 2021-07-01 | 1 |
| 3 | 王五 | 市场部 | 12000.00 | 2023-01-10 | 1 |
| 5 | 张三 | 财务部 | 11000.00 | 2020-11-05 | 1 |
💡 技术部有3人(李四18000、孙七18000、张三15000),李四和孙七薪资相同,ROW_NUMBER 会任选一个排第1。如果想稳定选人,ORDER BY 加上 salary DESC, id ASC。
二、窗口函数
必会 三种排名函数对比
问题:全公司按薪资排名,三种排名函数结果有什么不同?
SELECT name, dept, salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank_val,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_val
FROM employees;
执行结果 (7 rows — 注意 18000 并列时的区别)
| name | dept | salary | row_num | rank_val | dense_val |
| 李四 | 技术部 | 18000 | 1 | 1 | 1 |
| 孙七 | 技术部 | 18000 | 2 | 1 | 1 |
| 张三 | 技术部 | 15000 | 3 | 3 | 2 |
| 王五 | 市场部 | 12000 | 4 | 4 | 3 |
| 赵六 | 市场部 | 12000 | 5 | 4 | 3 |
| 张三 | 财务部 | 11000 | 6 | 6 | 4 |
| 周八 | NULL | 9000 | 7 | 7 | 5 |
💡 关键看 18000 并列的两个人的排名:
ROW_NUMBER = 1,2(不并列,随机分配);
RANK = 1,1 → 下一个是3(跳号);
DENSE_RANK = 1,1 → 下一个是2(不跳号)。
常用 部门内排名
问题:每个部门内部的薪资排名?
SELECT name, dept, salary,
ROW_NUMBER() OVER (
PARTITION BY dept
ORDER BY salary DESC
) AS dept_rank
FROM employees
WHERE dept IS NOT NULL;
执行结果 (6 rows)
| name | dept | salary | dept_rank |
| 李四 | 技术部 | 18000 | 1 |
| 孙七 | 技术部 | 18000 | 2 |
| 张三 | 技术部 | 15000 | 3 |
| 王五 | 市场部 | 12000 | 1 |
| 赵六 | 市场部 | 12000 | 2 |
| 张三 | 财务部 | 11000 | 1 |
常用 窗口函数 vs GROUP BY 对比
同样的"每个部门平均薪资",两种写法的输出区别:
GROUP BY — 折叠成 3 行
SELECT dept, ROUND(AVG(salary),2) AS avg_sal
FROM employees WHERE dept IS NOT NULL GROUP BY dept;
| dept | avg_sal |
| 技术部 | 17000.00 |
| 市场部 | 12000.00 |
| 财务部 | 11000.00 |
窗口函数 — 保留 6 行,多了统计列
SELECT name, dept, salary,
ROUND(AVG(salary) OVER (PARTITION BY dept),2) AS avg_sal
FROM employees WHERE dept IS NOT NULL;
| name | dept | salary | avg_sal |
| 张三 | 技术部 | 15000 | 17000.00 |
| 李四 | 技术部 | 18000 | 17000.00 |
| 孙七 | 技术部 | 18000 | 17000.00 |
| 王五 | 市场部 | 12000 | 12000.00 |
| 赵六 | 市场部 | 12000 | 12000.00 |
| 张三 | 财务部 | 11000 | 11000.00 |
常用 累计求和
问题:按入职日期排序,累计薪资总和是多少?
SELECT name, hire_date, salary,
SUM(salary) OVER (ORDER BY hire_date) AS running_total
FROM employees
WHERE dept IS NOT NULL;
执行结果 (6 rows — running_total 逐行累加)
| name | hire_date | salary | running_total |
| 张三 | 2020-11-05 | 11000 | 11000 |
| 李四 | 2021-07-01 | 18000 | 29000 |
| 张三 | 2022-03-15 | 15000 | 44000 |
| 孙七 | 2022-09-18 | 18000 | 62000 |
| 王五 | 2023-01-10 | 12000 | 74000 |
| 赵六 | 2023-06-20 | 12000 | 86000 |
进阶 LAG / LEAD 取前后行对比
问题:每个人的薪资和上一个入职的人比,差多少?
SELECT name, hire_date, salary,
LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS prev_salary,
salary - LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS diff
FROM employees
WHERE dept IS NOT NULL;
执行结果 (6 rows)
| name | hire_date | salary | prev_salary | diff |
| 张三 | 2020-11-05 | 11000 | 0 | +11000 |
| 李四 | 2021-07-01 | 18000 | 11000 | +7000 |
| 张三 | 2022-03-15 | 15000 | 18000 | -3000 |
| 孙七 | 2022-09-18 | 18000 | 15000 | +3000 |
| 王五 | 2023-01-10 | 12000 | 18000 | -6000 |
| 赵六 | 2023-06-20 | 12000 | 12000 | 0 |
三、连接查询 JOIN
必会 INNER JOIN vs LEFT JOIN
问题:查出每个员工的部门经理是谁?
INNER JOIN — 只返回匹配的行
SELECT e.name, e.dept, d.manager
FROM employees e
INNER JOIN departments d ON e.dept = d.dept_name;
| name | dept | manager |
| 张三 | 技术部 | 李四 |
| 李四 | 技术部 | 李四 |
| 王五 | 市场部 | 王五 |
| 赵六 | 市场部 | 王五 |
| 张三 | 财务部 | 张三 |
| 孙七 | 技术部 | 李四 |
⚠️ 周八(dept=NULL)不在结果中
LEFT JOIN — 左表全保留
SELECT e.name, e.dept, d.manager
FROM employees e
LEFT JOIN departments d ON e.dept = d.dept_name;
| name | dept | manager |
| 张三 | 技术部 | 李四 |
| 李四 | 技术部 | 李四 |
| 王五 | 市场部 | 王五 |
| 赵六 | 市场部 | 王五 |
| 张三 | 财务部 | 张三 |
| 孙七 | 技术部 | 李四 |
| 周八 | NULL | NULL |
✅ 周八保留了,右侧填 NULL
常用 找不在任何部门的员工
SELECT e.*
FROM employees e
LEFT JOIN departments d ON e.dept = d.dept_name
WHERE d.id IS NULL;
执行结果 (1 row)
| id | name | dept | salary | hire_date |
| 7 | 周八 | NULL | 9000.00 | 2024-02-01 |
四、聚合与分组
必会 全表聚合
问题:公司整体薪资情况?
SELECT
COUNT(*) AS 总人数,
COUNT(dept) AS 有部门人数,
SUM(salary) AS 薪资总和,
ROUND(AVG(salary),2) AS 平均薪资,
MAX(salary) AS 最高薪资,
MIN(salary) AS 最低薪资
FROM employees;
执行结果 (1 row)
| 总人数 | 有部门人数 | 薪资总和 | 平均薪资 | 最高薪资 | 最低薪资 |
| 7 | 6 | 95000.00 | 13571.43 | 18000.00 | 9000.00 |
💡 COUNT(dept) = 6,因为周八的 dept 是 NULL,COUNT(字段) 不计 NULL。COUNT(*) 才是 7。
常用 GROUP BY + HAVING
问题:哪些部门有2人及以上,且平均薪资大于10000?
SELECT dept,
COUNT(*) AS cnt,
ROUND(AVG(salary),2) AS avg_sal
FROM employees
WHERE dept IS NOT NULL
GROUP BY dept
HAVING COUNT(*) >= 2
AND AVG(salary) > 10000;
执行结果 (2 rows — 财务部被过滤掉了)
| dept | cnt | avg_sal |
| 技术部 | 3 | 17000.00 |
| 市场部 | 2 | 12000.00 |
💡 执行顺序:WHERE(行过滤)→ GROUP BY(分组)→ HAVING(组过滤)。财务部只有1人,不满足 COUNT≥2,被 HAVING 过滤。
五、子查询
常用 薪资高于全员平均的员工
SELECT name, dept, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
执行结果 (3 rows — 平均薪资 13571.43)
| name | dept | salary |
| 李四 | 技术部 | 18000.00 |
| 孙七 | 技术部 | 18000.00 |
| 张三 | 技术部 | 15000.00 |
进阶 每个部门薪资前2名
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY dept
ORDER BY salary DESC
) AS rn
FROM employees
WHERE dept IS NOT NULL
) t
WHERE rn <= 2;
执行结果 (5 rows — 每个部门取前2)
| id | name | dept | salary | hire_date | rn |
| 2 | 李四 | 技术部 | 18000.00 | 2021-07-01 | 1 |
| 6 | 孙七 | 技术部 | 18000.00 | 2022-09-18 | 2 |
| 3 | 王五 | 市场部 | 12000.00 | 2023-01-10 | 1 |
| 4 | 赵六 | 市场部 | 12000.00 | 2023-06-20 | 2 |
| 5 | 张三 | 财务部 | 11000.00 | 2020-11-05 | 1 |
💡 财务部只有1人,所以只返回1条。市场部有2人刚好都出来了。
六、CASE WHEN 条件表达式
常用 薪资分级
SELECT name, dept, salary,
CASE
WHEN salary >= 18000 THEN '高'
WHEN salary >= 12000 THEN '中'
ELSE '低'
END AS salary_level
FROM employees;
执行结果 (7 rows)
| name | dept | salary | salary_level |
| 张三 | 技术部 | 15000 | 中 |
| 李四 | 技术部 | 18000 | 高 |
| 王五 | 市场部 | 12000 | 中 |
| 赵六 | 市场部 | 12000 | 中 |
| 张三 | 财务部 | 11000 | 低 |
| 孙七 | 技术部 | 18000 | 高 |
| 周八 | NULL | 9000 | 低 |
七、NULL 处理
必会 IFNULL / COALESCE
SELECT name,
dept AS 原始dept,
IFNULL(dept, '未分配') AS ifnull_result,
COALESCE(dept, '未分配') AS coalesce_result
FROM employees;
执行结果 (7 rows — 只看 dept 为 NULL 的那行)
| name | 原始dept | ifnull_result | coalesce_result |
| ... | ... | ... | ... |
| 周八 | NULL | 未分配 | 未分配 |
💡 IFNULL(a, b):a 为 NULL 就返回 b。COALESCE 支持多个参数,取第一个非 NULL 的。
附录:去重三种方式速查
| 方式 | 语句 | 保留完整行 | 指定保留哪条 | 看重复次数 | 适用场景 |
| DISTINCT |
SELECT DISTINCT col |
❌ | ❌ | ❌ |
只要去重后的值 |
| GROUP BY |
GROUP BY col |
❌ | ✅ MIN(id) | ✅ COUNT(*) |
去重 + 统计 |
| PARTITION BY |
ROW_NUMBER() OVER(PARTITION BY...) |
✅ | ✅ ORDER BY | ✅ COUNT(*) OVER |
去重 + 保留完整行 |