SQL 面试知识点与案例
这份文档整理 SQL 面试中最常见的知识点、典型查询案例和容易被追问的细节,适合用于面试前快速复习。
1. SQL 执行顺序
SQL 的逻辑执行顺序通常是:
sql
FROM
JOIN
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
LIMIT
示例:
sql
SELECT department_id, COUNT(*) AS cnt
FROM employees
WHERE salary > 10000
GROUP BY department_id
HAVING COUNT(*) >= 3
ORDER BY cnt DESC;
含义:
- 先从
employees表取数据 - 再筛选
salary > 10000的员工 - 按
department_id分组 - 只保留人数不少于 3 的部门
- 最后按人数倒序排序
常见追问:
WHERE和HAVING的区别是什么?- 为什么
SELECT中的别名通常不能在WHERE中使用?
2. JOIN 连接
假设有两张表:
sql
employees(id, name, department_id, salary)
departments(id, dept_name)
查询员工及其部门名称:
sql
SELECT
e.name,
d.dept_name
FROM employees e
JOIN departments d
ON e.department_id = d.id;
常见连接类型:
| JOIN 类型 | 含义 |
|---|---|
INNER JOIN |
只保留两边都匹配的数据 |
LEFT JOIN |
保留左表全部数据,右表无匹配则为 NULL |
RIGHT JOIN |
保留右表全部数据,左表无匹配则为 NULL |
FULL JOIN |
保留两边所有数据,MySQL 不直接支持 |
案例:查询没有部门的员工。
sql
SELECT e.*
FROM employees e
LEFT JOIN departments d
ON e.department_id = d.id
WHERE d.id IS NULL;
案例:查询没有下过单的用户。
sql
SELECT u.*
FROM users u
LEFT JOIN orders o
ON u.id = o.user_id
WHERE o.id IS NULL;
常见追问:
LEFT JOIN后面的条件放在ON和WHERE中有什么区别?- 如何避免 JOIN 后数据量被放大?
3. GROUP BY 聚合
查询每个部门的平均工资:
sql
SELECT
department_id,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
查询平均工资大于 15000 的部门:
sql
SELECT
department_id,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 15000;
WHERE 和 HAVING 的区别:
| 关键字 | 执行阶段 | 用途 |
|---|---|---|
WHERE |
分组前 | 过滤原始行 |
HAVING |
分组后 | 过滤聚合结果 |
案例:查询订单数大于等于 3 的用户。
sql
SELECT
user_id,
COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 3;
4. DISTINCT 与去重统计
查询有订单的用户数:
sql
SELECT COUNT(DISTINCT user_id) AS user_count
FROM orders;
查询每个部门有多少员工:
sql
SELECT
department_id,
COUNT(*) AS employee_count
FROM employees
GROUP BY department_id;
注意:
COUNT(*)统计所有行COUNT(column)不统计NULLCOUNT(DISTINCT column)统计去重后的非空值
5. 子查询
查询工资高于公司平均工资的员工:
sql
SELECT *
FROM employees
WHERE salary > (
SELECT AVG(salary)
FROM employees
);
查询工资高于所在部门平均工资的员工:
sql
SELECT *
FROM employees e
WHERE salary > (
SELECT AVG(salary)
FROM employees
WHERE department_id = e.department_id
);
也可以用 JOIN 改写:
sql
SELECT e.*
FROM employees e
JOIN (
SELECT
department_id,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) t
ON e.department_id = t.department_id
WHERE e.salary > t.avg_salary;
常见追问:
- 相关子查询和非相关子查询有什么区别?
- 子查询和 JOIN 哪个性能更好?
6. 窗口函数
窗口函数是 SQL 面试高频重点,常用于排名、Top N、累计值、环比等场景。
查询每个部门员工工资排名:
sql
SELECT
name,
department_id,
salary,
RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS salary_rank
FROM employees;
常见排名函数区别:
| 函数 | 特点 | 示例排名 |
|---|---|---|
ROW_NUMBER() |
不允许并列 | 1, 2, 3 |
RANK() |
允许并列,跳号 | 1, 1, 3 |
DENSE_RANK() |
允许并列,不跳号 | 1, 1, 2 |
案例:查询每个部门工资最高的员工。
sql
SELECT *
FROM (
SELECT
e.*,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn
FROM employees e
) t
WHERE rn = 1;
如果要保留并列第一:
sql
SELECT *
FROM (
SELECT
e.*,
RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rk
FROM employees e
) t
WHERE rk = 1;
7. Top N 问题
查询工资前三名员工:
sql
SELECT *
FROM employees
ORDER BY salary DESC
LIMIT 3;
查询每个部门工资前三名员工:
sql
SELECT *
FROM (
SELECT
e.*,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS rn
FROM employees e
) t
WHERE rn <= 3;
查询销售额排名前 3 的商品:
sql
SELECT *
FROM (
SELECT
product_id,
SUM(order_amount) AS total_amount,
RANK() OVER (
ORDER BY SUM(order_amount) DESC
) AS sales_rank
FROM orders
GROUP BY product_id
) t
WHERE sales_rank <= 3;
8. 连续登录问题
表结构:
sql
login_log(user_id, login_date)
查询连续登录 3 天的用户:
sql
SELECT DISTINCT user_id
FROM (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL rn DAY) AS grp
FROM (
SELECT
user_id,
login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY login_date
) AS rn
FROM (
SELECT DISTINCT user_id, login_date
FROM login_log
) d
) t1
) t2
GROUP BY user_id, grp
HAVING COUNT(*) >= 3;
核心思想:
- 先对每个用户的登录日期排序
- 用登录日期减去连续序号
- 连续日期会得到相同的分组值
- 对分组计数,数量大于等于 3 即表示连续登录 3 天
注意:如果同一用户一天有多条登录记录,需要先去重。
9. 订单类经典题
表结构:
sql
orders(id, user_id, product_id, order_amount, order_date)
查询每个用户的首单时间:
sql
SELECT
user_id,
MIN(order_date) AS first_order_date
FROM orders
GROUP BY user_id;
查询每个用户首单金额:
sql
SELECT
user_id,
order_amount,
order_date
FROM (
SELECT
o.*,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY order_date
) AS rn
FROM orders o
) t
WHERE rn = 1;
查询每个月销售额:
sql
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(order_amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;
查询复购用户:
sql
SELECT
user_id
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2;
查询每月新增用户数:
sql
SELECT
DATE_FORMAT(first_order_date, '%Y-%m') AS month,
COUNT(*) AS new_user_count
FROM (
SELECT
user_id,
MIN(order_date) AS first_order_date
FROM orders
GROUP BY user_id
) t
GROUP BY DATE_FORMAT(first_order_date, '%Y-%m')
ORDER BY month;
10. 留存率案例
用户注册表:
sql
users(user_id, register_date)
登录表:
sql
login_log(user_id, login_date)
查询次日留存用户数:
sql
SELECT
u.register_date,
COUNT(DISTINCT u.user_id) AS register_count,
COUNT(DISTINCT l.user_id) AS next_day_retained_count
FROM users u
LEFT JOIN login_log l
ON u.user_id = l.user_id
AND l.login_date = DATE_ADD(u.register_date, INTERVAL 1 DAY)
GROUP BY u.register_date;
查询次日留存率:
sql
SELECT
u.register_date,
COUNT(DISTINCT l.user_id) / COUNT(DISTINCT u.user_id) AS next_day_retention_rate
FROM users u
LEFT JOIN login_log l
ON u.user_id = l.user_id
AND l.login_date = DATE_ADD(u.register_date, INTERVAL 1 DAY)
GROUP BY u.register_date;
注意:
- 留存类问题通常使用
LEFT JOIN - 登录表可能有重复记录,所以常用
COUNT(DISTINCT user_id) - 条件放在
ON中,避免把未留存用户过滤掉
11. 索引与优化
常见索引原则:
- 索引可以加快查询,但会降低写入速度
- 索引适合建在
WHERE、JOIN、ORDER BY、GROUP BY高频字段上 - 联合索引遵循最左前缀原则
- 不要在索引字段上使用函数,否则可能导致索引失效
LIKE '%abc'通常无法有效使用普通索引- 小表、低区分度字段不一定适合建索引
创建联合索引:
sql
CREATE INDEX idx_user_order_date
ON orders(user_id, order_date);
适合以下查询:
sql
SELECT *
FROM orders
WHERE user_id = 1001
ORDER BY order_date DESC;
可能导致索引失效的写法:
sql
SELECT *
FROM orders
WHERE DATE(order_date) = '2026-07-01';
更好的写法:
sql
SELECT *
FROM orders
WHERE order_date >= '2026-07-01'
AND order_date < '2026-07-02';
常见追问:
- 什么是最左前缀原则?
- 什么情况下索引会失效?
- 如何使用
EXPLAIN分析慢 SQL?
12. EXPLAIN 常看字段
示例:
sql
EXPLAIN
SELECT *
FROM orders
WHERE user_id = 1001
ORDER BY order_date DESC;
常看字段:
| 字段 | 含义 |
|---|---|
type |
访问类型,常见有 ALL、index、range、ref、const |
possible_keys |
可能使用的索引 |
key |
实际使用的索引 |
rows |
预估扫描行数 |
Extra |
额外信息,如 Using filesort、Using temporary |
一般来说,type 从差到好大致是:
text
ALL < index < range < ref < const
13. 事务
事务四大特性 ACID:
| 特性 | 含义 |
|---|---|
| Atomicity | 原子性,要么全部成功,要么全部失败 |
| Consistency | 一致性,事务前后数据满足约束 |
| Isolation | 隔离性,事务之间互不干扰 |
| Durability | 持久性,提交后数据持久保存 |
转账案例:
sql
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE user_id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE user_id = 2;
COMMIT;
如果中途失败:
sql
ROLLBACK;
常见隔离级别:
| 隔离级别 | 可能问题 |
|---|---|
READ UNCOMMITTED |
可能脏读 |
READ COMMITTED |
避免脏读 |
REPEATABLE READ |
避免不可重复读,MySQL 默认 |
SERIALIZABLE |
隔离最高,性能最低 |
常见并发问题:
- 脏读:读到了其他事务未提交的数据
- 不可重复读:同一事务中两次读取同一行,结果不同
- 幻读:同一事务中两次范围查询,结果行数不同
14. NULL 相关问题
NULL 表示未知,不等于任何值,也不等于另一个 NULL。
错误写法:
sql
SELECT *
FROM employees
WHERE department_id = NULL;
正确写法:
sql
SELECT *
FROM employees
WHERE department_id IS NULL;
判断非空:
sql
SELECT *
FROM employees
WHERE department_id IS NOT NULL;
注意:
COUNT(*)会统计NULL行COUNT(column)不统计该列为NULL的行NOT IN遇到子查询中有NULL时容易出现意外结果
更推荐使用 NOT EXISTS:
sql
SELECT *
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);
15. 面试高频题清单
建议重点练习:
- 查询每个部门工资最高的员工
- 查询每个用户最近一次登录记录
- 查询每个用户的第一笔订单
- 查询连续登录 N 天的用户
- 查询销售额排名前 3 的商品
- 查询每个月新增用户数
- 查询复购用户
- 查询没有下单的用户
- 查询订单金额超过平均值的订单
- 查询次日留存率
- 查询每个商品的累计销售额
- 查询环比增长率
- 解释
WHERE和HAVING - 解释
LEFT JOIN条件放在ON和WHERE的区别 - 分析一条慢 SQL 并给出优化方案
16. 面试回答模板
回答 SQL 题时,可以按这个顺序表达:
- 先说明要用哪些表,以及表之间如何关联
- 再说明先过滤什么条件
- 如果需要聚合,说明按什么字段分组
- 如果是 Top N,优先考虑窗口函数
- 如果涉及性能,说明是否需要索引以及索引字段顺序
示例回答:
text
这题可以先按用户统计订单数,再用 HAVING 筛选订单数大于等于 2 的用户。
如果订单表数据量很大,可以在 user_id 上建索引,方便分组和关联。
17. 复习优先级
如果时间有限,优先掌握:
JOINGROUP BY和HAVING- 子查询
- 窗口函数
- Top N 问题
- 连续登录问题
- 留存率问题
- 索引优化
- 事务与隔离级别
NULL处理