整理一份 SQL 面试常考知识点 + 典型案例,适合快速复习

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 的部门
  • 最后按人数倒序排序

常见追问:

  • WHEREHAVING 的区别是什么?
  • 为什么 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 后面的条件放在 ONWHERE 中有什么区别?
  • 如何避免 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;

WHEREHAVING 的区别:

关键字 执行阶段 用途
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) 不统计 NULL
  • COUNT(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. 索引与优化

常见索引原则:

  • 索引可以加快查询,但会降低写入速度
  • 索引适合建在 WHEREJOINORDER BYGROUP 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 访问类型,常见有 ALLindexrangerefconst
possible_keys 可能使用的索引
key 实际使用的索引
rows 预估扫描行数
Extra 额外信息,如 Using filesortUsing 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. 面试高频题清单

建议重点练习:

  1. 查询每个部门工资最高的员工
  2. 查询每个用户最近一次登录记录
  3. 查询每个用户的第一笔订单
  4. 查询连续登录 N 天的用户
  5. 查询销售额排名前 3 的商品
  6. 查询每个月新增用户数
  7. 查询复购用户
  8. 查询没有下单的用户
  9. 查询订单金额超过平均值的订单
  10. 查询次日留存率
  11. 查询每个商品的累计销售额
  12. 查询环比增长率
  13. 解释 WHEREHAVING
  14. 解释 LEFT JOIN 条件放在 ONWHERE 的区别
  15. 分析一条慢 SQL 并给出优化方案

16. 面试回答模板

回答 SQL 题时,可以按这个顺序表达:

  1. 先说明要用哪些表,以及表之间如何关联
  2. 再说明先过滤什么条件
  3. 如果需要聚合,说明按什么字段分组
  4. 如果是 Top N,优先考虑窗口函数
  5. 如果涉及性能,说明是否需要索引以及索引字段顺序

示例回答:

text 复制代码
这题可以先按用户统计订单数,再用 HAVING 筛选订单数大于等于 2 的用户。
如果订单表数据量很大,可以在 user_id 上建索引,方便分组和关联。

17. 复习优先级

如果时间有限,优先掌握:

  1. JOIN
  2. GROUP BYHAVING
  3. 子查询
  4. 窗口函数
  5. Top N 问题
  6. 连续登录问题
  7. 留存率问题
  8. 索引优化
  9. 事务与隔离级别
  10. NULL 处理