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

常见追问:

  • 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) 不统计 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. 索引与优化

常见索引原则:

  • 索引可以加快查询,但会降低写入速度
  • 索引适合建在 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. 面试高频题清单

建议重点练习:

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

16. 面试回答模板

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

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

示例回答:

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

17. 复习优先级

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

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