MySQL升级打怪手册-答案

MySQL升级打怪手册 - 学习指南

引导式学习答案 - 授人以鱼不如授人以渔

使用说明

本学习指南遵循引导式学习原则,通过提示和引导帮助你独立思考和解决问题,而不是直接给出答案。

📖 答案结构说明

每道题目包含以下部分:

  1. 💡 解题思路 - 分析问题,理解需求
  2. 🔍 关键提示 - 需要用到的知识点和关键词
  3. 📝 实现步骤 - 分步骤引导实现
  4. ✅ 验证方法 - 如何验证答案是否正确
  5. 🚀 扩展思考 - 其他解法和深入思考
  6. 💯 参考答案 - 完整答案(建议最后查看)

🎯 学习建议

  • 先思考再动手:看完解题思路后,先尝试自己写SQL
  • 逐步实现:按照实现步骤一步步完成
  • 多种方法:尝试用不同方法解决同一问题
  • 验证结果:养成验证结果的习惯
  • 扩展延伸:完成后思考扩展问题

第一关:基础入门 - 学习指南

题目1:创建学生表

💡 解题思路

这道题考查的是建表语法。你需要:

  1. 理解每个字段的含义和数据类型
  2. 知道如何设置主键和自动递增
  3. 掌握CREATE TABLE的基本语法

🔍 关键提示

  • 主键需要使用 PRIMARY KEY
  • 自动递增需要使用 AUTO_INCREMENT
  • 字符串类型使用 VARCHAR(长度)
  • 整数类型使用 INT
  • 单字符类型使用 CHAR(1)

📝 实现步骤

  1. 写出建表语句框架:CREATE TABLE students ( ... );
  2. 添加id字段:类型是什么?需要什么约束?
  3. 添加name字段:应该用什么类型存储最多50字符的姓名?
  4. 依次添加age、gender、email字段
  5. 为id字段设置主键和自动递增

✅ 验证方法

-- 创建后用以下命令验证
DESC students;
-- 查看是否有5个字段,每个字段的类型是否正确

🚀 扩展思考

  • 如果要让name字段不能为空,应该怎么做?
  • 如果要让email字段具有唯一性,应该怎么做?
  • 除了VARCHAR,还可以用什么类型存储姓名?各有什么优缺点?
💯 点击查看参考答案
CREATE TABLE students (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT,
    gender CHAR(1),
    email VARCHAR(100)
);

要点说明

  • INT PRIMARY KEY AUTO_INCREMENT:整型主键,自动递增
  • VARCHAR(50):可变长字符串,最多50个字符
  • CHAR(1):固定长度字符串,适合性别这种单字符

题目2:查看表结构

💡 解题思路

这题考查如何查看已创建表的结构。这是调试和验证的重要技能。

🔍 关键提示

  • MySQL提供了多个命令可以查看表结构
  • 最常用的命令是 DESC(DESCRIBE的缩写)
  • 还可以使用 SHOW 相关命令

📝 实现步骤

  1. 思考:要查看students表的结构,需要用什么命令?
  2. 尝试使用 DESC 表名 的格式
  3. 观察输出结果:字段名、类型、是否允许NULL、键类型等

🚀 扩展思考

  • DESC studentsSHOW COLUMNS FROM students 有什么区别?
  • 如何查看建表的完整SQL语句?(提示:SHOW CREATE TABLE
  • 如何查看数据库中所有的表?
💯 点击查看参考答案
-- 方法1:最常用
DESC students;

-- 方法2:更详细
SHOW COLUMNS FROM students;

-- 方法3:查看完整建表语句
SHOW CREATE TABLE students;

输出说明

  • Field:字段名
  • Type:数据类型
  • Null:是否允许NULL
  • Key:是否是键(PRI=主键)
  • Default:默认值
  • Extra:额外信息(如auto_increment)

题目3:插入学生记录

💡 解题思路

这题考查插入数据的语法。需要注意:

  1. 一次性插入多条数据比单条插入效率更高
  2. 字符串值需要用引号
  3. 字段列表和值列表要一一对应

🔍 关键提示

  • 使用 INSERT INTO 语句
  • 批量插入格式:VALUES (值1), (值2), (值3)
  • 字符串需要用单引号包围
  • id字段是自动递增的,可以不用填写

📝 实现步骤

  1. 写出INSERT INTO的基本框架
  2. 指定要插入的字段(除了id,其他字段都要)
  3. 使用VALUES添加第一条记录
  4. 用逗号分隔,继续添加第二、三条记录
  5. 注意性别应该是'男'还是'女'

✅ 验证方法

-- 插入后查询验证
SELECT * FROM students;
-- 应该看到3条记录
SELECT COUNT(*) FROM students;
-- 应该返回3

🚀 扩展思考

  • 如果分3次执行单条INSERT,和一次执行批量INSERT,哪个更快?为什么?
  • 如果你想让id从1001开始,应该怎么做?
  • 如果插入时某个邮箱重复了会怎样?(如果email有UNIQUE约束)
💯 点击查看参考答案
-- 推荐:批量插入(性能更好)
INSERT INTO students (name, age, gender, email) VALUES
('张三', 20, '男', '[email protected]'),
('李四', 22, '女', '[email protected]'),
('王五', 21, '男', '[email protected]');

-- 或者:单条插入(不推荐,仅作演示)
INSERT INTO students (name, age, gender, email)
VALUES ('张三', 20, '男', '[email protected]');

INSERT INTO students (name, age, gender, email)
VALUES ('李四', 22, '女', '[email protected]');

INSERT INTO students (name, age, gender, email)
VALUES ('王五', 21, '男', '[email protected]');

性能对比:批量插入可以减少与数据库的交互次数,性能提升10倍以上。


题目4:查询姓名和年龄

💡 解题思路

这是最基础的SELECT查询。关键是:

  1. 只查询需要的字段,不要SELECT *
  2. 理解字段列表的写法

🔍 关键提示

  • 基本语法:SELECT 字段1, 字段2 FROM 表名;
  • 多个字段用逗号分隔
  • 查询所有记录不需要WHERE条件

📝 实现步骤

  1. 确定要查询哪些字段?题目要求姓名和年龄
  2. 确定从哪个表查询?students表
  3. 组合成完整的SELECT语句

🚀 扩展思考

  • SELECT name, ageSELECT * 有什么区别?
  • 能否改变查询结果的字段顺序?如 SELECT age, name
  • 如何给查询结果的列名起别名?(提示:使用AS)
💯 点击查看参考答案
SELECT name, age FROM students;

-- 使用别名让结果更易读
SELECT name AS 姓名, age AS 年龄 FROM students;

最佳实践

  • 只查询需要的字段,避免SELECT *
  • 在实际项目中,可以用AS起中文别名让结果更直观

题目5:删除表

💡 解题思路

这题考查删除表的语法。注意:

  1. 删除表会永久删除所有数据和表结构
  2. 删除前最好确认一下

🔍 关键提示

  • 使用 DROP TABLE 语句
  • 删除操作是不可逆的,要谨慎
  • 可以先用 DESC 确认表是否存在

📝 实现步骤

  1. 回想删除表的关键字是什么?(DROP)
  2. 写出完整的DROP TABLE语句
  3. 删除后可以用 SHOW TABLES; 验证

✅ 验证方法

-- 删除前查看所有表
SHOW TABLES;
-- 删除
DROP TABLE students;
-- 删除后再次查看,students应该不在列表中
SHOW TABLES;

🚀 扩展思考

  • DROP TABLE studentsDELETE FROM students 有什么区别?
  • 如果想保留表结构只删除数据,应该用什么命令?
  • 如何避免误删表?(提示:可以先用IF EXISTS判断)
💯 点击查看参考答案
-- 基本语法
DROP TABLE students;

-- 更安全的写法(如果表存在才删除)
DROP TABLE IF EXISTS students;

对比说明

  • DROP TABLE:删除表结构和所有数据,不可恢复
  • DELETE FROM:只删除数据,保留表结构
  • TRUNCATE TABLE:快速清空所有数据,重置自增ID

安全提示:生产环境删除表前一定要备份!


第二关:单表查询 - 学习指南

题目6:查询工资大于5000的员工

💡 解题思路

这题引入了WHERE条件筛选。需要:

  1. 理解WHERE子句的作用
  2. 掌握比较运算符的使用
  3. 明确要查询哪些字段

🔍 关键提示

  • 基本结构:SELECT ... FROM ... WHERE 条件
  • 大于号用 >
  • 题目要求查询"姓名和工资",不是所有字段

📝 实现步骤

  1. 确定查询字段:name和salary
  2. 确定筛选条件:salary > 5000
  3. 组合成完整SQL语句
  4. 思考:条件应该放在SELECT后面还是FROM后面?

✅ 验证方法

-- 执行你的查询后,检查结果中是否所有salary都大于5000
-- 可以再查一次看有多少人工资<=5000
SELECT COUNT(*) FROM employees WHERE salary <= 5000;

🚀 扩展思考

  • 如果要查询工资大于等于5000,应该怎么改?
  • 如果要查询工资在5000到10000之间,有几种写法?
  • 查询工资不等于5000的员工,有几种写法?
💯 点击查看参考答案
-- 基本答案
SELECT name, salary FROM employees WHERE salary > 5000;

-- 扩展:使用别名
SELECT name AS 姓名, salary AS 工资
FROM employees
WHERE salary > 5000;

-- 扩展:排序显示(虽然题目没要求,但更实用)
SELECT name, salary
FROM employees
WHERE salary > 5000
ORDER BY salary DESC;

学习要点

  • WHERE子句用于过滤数据
  • 比较运算符:> < >= <= = !=<>
  • 只查询需要的字段可以提升性能

题目7:查询销售部或技术部员工

💡 解题思路

这题考查多条件查询。关键点:

  1. "或"的关系如何表达?
  2. 有多种写法,哪种更优雅?

🔍 关键提示

  • 逻辑运算符:OR 表示"或",AND 表示"且"
  • IN 运算符:字段 IN (值1, 值2, ...)
  • 两种方法都可以实现,但IN更简洁

📝 实现步骤

  1. 方法1:使用OR连接两个条件
    • department = '销售部' OR department = '技术部'
  2. 方法2:使用IN
    • department IN ('销售部', '技术部')
  3. 两种方法都试一试,对比一下哪个更清晰

✅ 验证方法

-- 执行查询后,检查department字段
-- 应该只有'销售部'和'技术部'两种值
SELECT DISTINCT department FROM employees
WHERE department IN ('销售部', '技术部');

🚀 扩展思考

  • 如果要查询除了销售部和技术部之外的员工,怎么写?
  • IN 和多个 OR 在性能上有区别吗?
  • 如果要查询3个、5个部门,哪种写法更好维护?
💯 点击查看参考答案
-- 方法1:使用IN(推荐,更简洁)
SELECT * FROM employees WHERE department IN ('销售部', '技术部');

-- 方法2:使用OR
SELECT * FROM employees
WHERE department = '销售部' OR department = '技术部';

-- 扩展:排除某些部门
SELECT * FROM employees WHERE department NOT IN ('销售部', '技术部');

-- 扩展:查询特定字段
SELECT name, department, salary
FROM employees
WHERE department IN ('销售部', '技术部');

对比分析

  • IN更简洁,当条件多时优势明显
  • OR更直观,但条件多时代码冗长
  • 性能上两者基本相同,优先选择可读性好的

题目8:姓名包含'张'的员工

💡 解题思路

这题考查模糊查询。需要掌握:

  1. LIKE运算符的使用
  2. 通配符 % 的含义
  3. 与等号的区别

🔍 关键提示

  • 使用 LIKE 运算符进行模糊匹配
  • % 匹配任意多个字符(包括0个)
  • _ 匹配单个字符
  • 题目要求"包含",所以前后都需要%

📝 实现步骤

  1. 思考:name = '张' 能满足要求吗?为什么不能?
  2. 改用LIKE:name LIKE '张%' 是什么意思?
  3. name LIKE '%张'name LIKE '%张%' 有什么区别?
  4. 哪个能满足"包含"的要求?

✅ 验证方法

-- 查询后检查结果
-- '张三'、'小张'、'张'、'王张李'都应该被查出来

🚀 扩展思考

  • '张%''%张''%张%' 三种写法各匹配什么样的数据?
  • 如何查询姓张的员工(张在最前面)?
  • 如何查询名字是三个字且第二个字是张的员工?(提示:用_
  • 模糊查询对索引有什么影响?
💯 点击查看参考答案
-- 基本答案:包含'张'
SELECT * FROM employees WHERE name LIKE '%张%';

-- 扩展:姓张(以张开头)
SELECT * FROM employees WHERE name LIKE '张%';

-- 扩展:名字最后一个字是张
SELECT * FROM employees WHERE name LIKE '%张';

-- 扩展:三个字且第二个字是张
SELECT * FROM employees WHERE name LIKE '_张_';

通配符说明

  • %:匹配0个或多个字符
    • '张%' → 张三、张小明
    • '%张' → 小张、老张
    • '%张%' → 张三、小张、王张李
  • _:匹配单个字符
    • '_张_' → 李张三
    • '张__' → 张小明(张+两个字符)

性能提示'%张%''%张' 无法使用索引,会全表扫描


题目9:工资8000-15000,降序排列

💡 解题思路

这题同时考查:

  1. 范围查询:如何表示"在...之间"
  2. 排序:如何指定排序字段和顺序

🔍 关键提示

  • 范围查询:BETWEEN ... AND ...(包含边界值)
  • 排序:ORDER BY 字段 DESC(DESC表示降序)
  • 两个子句要按顺序:WHERE → ORDER BY

📝 实现步骤

  1. 先写WHERE条件:salary在8000到15000之间
    • 可以用:BETWEEN 8000 AND 15000
    • 也可以用:salary >= 8000 AND salary <= 15000
  2. 再加排序:ORDER BY salary DESC
  3. 注意顺序:WHERE在前,ORDER BY在后

✅ 验证方法

-- 执行后检查:
-- 1. 第一条记录的工资应该最高(接近15000)
-- 2. 最后一条记录的工资应该最低(接近8000)
-- 3. 所有记录的工资都在8000-15000之间

🚀 扩展思考

  • BETWEEN和两个比较运算符哪个更好?
  • 如果要升序排列呢?ASC还是DESC?
  • 如果工资相同时,如何按姓名再排序?(提示:ORDER BY多个字段)
  • 能否先排序再筛选?顺序能颠倒吗?
💯 点击查看参考答案
-- 方法1:使用BETWEEN(推荐,更简洁)
SELECT * FROM employees
WHERE salary BETWEEN 8000 AND 15000
ORDER BY salary DESC;

-- 方法2:使用比较运算符
SELECT * FROM employees
WHERE salary >= 8000 AND salary <= 15000
ORDER BY salary DESC;

-- 扩展:工资相同时按姓名排序
SELECT * FROM employees
WHERE salary BETWEEN 8000 AND 15000
ORDER BY salary DESC, name ASC;

-- 扩展:只查询需要的字段
SELECT name, salary, department
FROM employees
WHERE salary BETWEEN 8000 AND 15000
ORDER BY salary DESC;

语法顺序(重要)

SELECT → FROM → WHERE → ORDER BY → LIMIT

排序说明

  • ASC:升序(从小到大),默认值
  • DESC:降序(从大到小)
  • 多字段排序:ORDER BY 字段1 DESC, 字段2 ASC

题目10:2020年后入职的前5名员工

💡 解题思路

这题综合考查:

  1. 日期比较
  2. LIMIT限制结果数量
  3. 完整的查询语句结构

🔍 关键提示

  • 日期比较:hire_date > '2020-12-31' 或使用YEAR函数
  • 限制数量:LIMIT 5
  • 可能需要排序:按入职日期排序更合理

📝 实现步骤

  1. 确定日期条件:2020年之后是指2021-01-01开始
    • 方法1:hire_date > '2020-12-31'
    • 方法2:hire_date >= '2021-01-01'
    • 方法3:YEAR(hire_date) > 2020
  2. 添加LIMIT限制:LIMIT 5
  3. 思考是否需要排序?按什么排序?

✅ 验证方法

-- 检查结果的hire_date是否都在2020年之后
-- 检查是否只返回5条记录
SELECT COUNT(*) FROM (...你的查询...) AS result;  -- 应该是5

🚀 扩展思考

  • 这三种日期比较方法性能有区别吗?
  • 如果要查询"2020年(含)之后",条件应该怎么改?
  • LIMIT 5LIMIT 0, 5 有区别吗?
  • 如果要查询第6-10名,LIMIT应该怎么写?
💯 点击查看参考答案
-- 方法1:日期字符串比较(推荐,性能最好)
SELECT * FROM employees
WHERE hire_date > '2020-12-31'
ORDER BY hire_date
LIMIT 5;

-- 或者
SELECT * FROM employees
WHERE hire_date >= '2021-01-01'
ORDER BY hire_date
LIMIT 5;

-- 方法2:使用YEAR函数(可读性好,但性能稍差)
SELECT * FROM employees
WHERE YEAR(hire_date) > 2020
ORDER BY hire_date
LIMIT 5;

-- 扩展:分页查询(第2页,每页5条)
SELECT * FROM employees
WHERE hire_date > '2020-12-31'
ORDER BY hire_date
LIMIT 5 OFFSET 5;  -- 等同于 LIMIT 5, 5

日期比较最佳实践

  • ✅ 使用日期字符串:hire_date > '2020-12-31'(可以使用索引)
  • ❌ 使用函数:YEAR(hire_date) > 2020(索引失效)

LIMIT语法

  • LIMIT 5:返回前5条
  • LIMIT 5, 10:从第6条开始,返回10条(偏移5)
  • LIMIT 10 OFFSET 5:同上,更清晰的写法

题目11-15:其他单表查询题目

💡 学习提示:后续题目请按照相同的思路:

  1. 先分析题目要求
  2. 确定需要用到的知识点
  3. 尝试自己编写SQL
  4. 验证结果是否正确
  5. 查看参考答案对比学习

(由于篇幅限制,这里提供关键提示,完整学习指南将继续补充...)

题目11:邮箱为空 - 关键词:IS NULL
题目12:去重查询 - 关键词:DISTINCT
题目13:工资最高的3名 - 关键词:ORDER BY ... DESC LIMIT 3
题目14:姓李且工资>6000 - 关键词:LIKE '李%' AND
题目15:部门不是财务部 - 关键词:!= 或 <>


第三关:多表联结

题目16:查询所有员工及其所在部门名称

💡 解题思路

这是最基础的INNER JOIN题目。需要理解:

  1. 两表如何通过共同字段关联
  2. INNER JOIN只返回两表都有匹配的记录
  3. 如何使用表别名简化SQL

🔍 关键提示

  • 需要关联两个表:employeesdepartments
  • 关联字段:employees.dept_id = departments.dept_id
  • 使用表别名:employees edepartments d
  • 语法:FROM employees e INNER JOIN departments d ON e.dept_id = d.dept_id

📝 实现步骤

  1. 先思考:需要哪些表?它们通过什么字段关联?
  2. 确定SELECT字段:员工姓名来自employees表,部门名称来自departments表
  3. 写JOIN语句:FROM employees INNER JOIN departments
  4. 添加ON条件:两表通过dept_id关联
  5. 使用表别名让SQL更简洁

✅ 验证方法

-- 检查结果
-- 1. 每个员工都应该有对应的部门名称
-- 2. 如果某员工没有部门(dept_id为NULL),INNER JOIN不会显示该员工
-- 3. 可以用COUNT验证返回的记录数
SELECT COUNT(*) FROM employees WHERE dept_id IS NOT NULL;

🚀 扩展思考

  • 如果要显示所有员工(包括没有部门的),应该用什么JOIN?
  • INNER JOIN 和 WHERE 子句的顺序能颠倒吗?
  • 如果两个表的关联字段名称相同,可以用 USING(dept_id) 代替 ON
  • 如果不写ON条件会发生什么?(笛卡尔积)
💯 点击查看参考答案
-- 方法1:使用表别名(推荐)
SELECT e.emp_name, d.dept_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;

-- 方法2:不使用别名(可读性差)
SELECT employees.emp_name, departments.dept_name
FROM employees
INNER JOIN departments ON employees.dept_id = departments.dept_id;

-- 方法3:使用USING(当关联字段名相同时)
SELECT e.emp_name, d.dept_name
FROM employees e
INNER JOIN departments d USING(dept_id);

-- 方法4:隐式JOIN(不推荐,WHERE子句混合了过滤和关联条件)
SELECT e.emp_name, d.dept_name
FROM employees e, departments d
WHERE e.dept_id = d.dept_id;

要点说明

  • INNER JOIN 只返回两表都匹配的记录
  • 使用表别名(ed)让SQL更简洁
  • ON 指定关联条件,类似WHERE但专门用于JOIN
  • 推荐使用显式JOIN语法(方法1),而不是隐式JOIN(方法4)

题目17:查询所有部门及其员工数量(包括没有员工的部门)

💡 解题思路

这题同时考查:

  1. LEFT JOIN:需要保留左表(departments)所有记录
  2. 聚合函数COUNT:统计每个部门的员工数
  3. GROUP BY:按部门分组统计

🔍 关键提示

  • 使用 LEFT JOIN 保留所有部门
  • 使用 COUNT(e.emp_id) 统计员工数(不要用COUNT(*))
  • 使用 GROUP BY d.dept_id 按部门分组
  • 没有员工的部门,COUNT应该返回0

📝 实现步骤

  1. 确定主表:departments(因为要保留所有部门)
  2. 选择JOIN类型:LEFT JOIN(保留左表所有记录)
  3. 添加COUNT函数:统计每个部门的员工数
  4. 添加GROUP BY:按部门分组
  5. 思考:为什么用COUNT(e.emp_id)而不是COUNT(*)?

✅ 验证方法

-- 验证步骤
-- 1. 检查是否所有部门都出现在结果中
SELECT COUNT(*) FROM departments;  -- 与结果行数比较

-- 2. 手动检查某个部门的员工数
SELECT COUNT(*) FROM employees WHERE dept_id = 1;

-- 3. 检查没有员工的部门是否显示为0

🚀 扩展思考

  • 为什么要用 COUNT(e.emp_id) 而不是 COUNT(*)
  • 如果用 INNER JOIN 会有什么问题?
  • 如果只想显示有员工的部门,应该怎么改?
  • HAVING COUNT(e.emp_id) > 0 和 WHERE 的区别是什么?
💯 点击查看参考答案
-- 标准答案
SELECT d.dept_name, COUNT(e.emp_id) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name;

-- 或者(如果只需要部门名称分组)
SELECT d.dept_name, COUNT(e.emp_id) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_name;

要点说明

  • LEFT JOIN:保留左表(departments)所有记录,右表没有匹配的显示NULL
  • COUNT(e.emp_id):统计具体字段,NULL不计数。没有员工的部门返回0
  • COUNT(*):统计所有行,即使e表字段全是NULL也会计数为1(错误)
  • GROUP BY:必须包含SELECT中的非聚合字段

错误示范

-- ❌ 使用COUNT(*)会导致没有员工的部门也显示为1
SELECT d.dept_name, COUNT(*) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_name;

-- ❌ 使用INNER JOIN会丢失没有员工的部门
SELECT d.dept_name, COUNT(e.emp_id) AS employee_count
FROM departments d
INNER JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_name;

题目18:查询每个员工及其直属上级的姓名

💡 解题思路

这是自连接(Self Join)的经典应用:

  1. 同一个表(employees)与自己连接
  2. 通过 manager_id 和 emp_id 建立关联
  3. 需要使用不同的表别名区分员工和经理

🔍 关键提示

  • 自连接:FROM employees e LEFT JOIN employees m
  • 两个别名:e(员工)和 m(经理/manager)
  • 关联条件:e.manager_id = m.emp_id
  • 使用LEFT JOIN:因为最高领导没有上级(manager_id为NULL)

📝 实现步骤

  1. 理解表结构:employees表中,manager_id 指向另一个员工的 emp_id
  2. 使用两个别名:e代表员工,m代表经理
  3. 写JOIN:FROM employees e LEFT JOIN employees m
  4. 添加ON条件:ON e.manager_id = m.emp_id
  5. 思考:为什么用LEFT JOIN而不是INNER JOIN?

✅ 验证方法

-- 验证步骤
-- 1. 检查最高领导(没有上级)是否也显示在结果中
SELECT * FROM employees WHERE manager_id IS NULL;

-- 2. 手动验证某个员工的上级是否正确
SELECT e.emp_name, e.manager_id, m.emp_name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id
WHERE e.emp_name = '某员工名';

🚀 扩展思考

  • 如果要查询每个经理及其所有下属,应该怎么写?
  • 如何查询某个员工的所有上级(多级)?需要递归查询
  • 如果用INNER JOIN会丢失谁?
  • 能否用RIGHT JOIN?效果有何不同?
💯 点击查看参考答案
-- 方法1:使用LEFT JOIN(推荐,显示所有员工)
SELECT
    e.emp_name AS employee_name,
    m.emp_name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;

-- 方法2:使用INNER JOIN(会丢失最高领导)
SELECT
    e.emp_name AS employee_name,
    m.emp_name AS manager_name
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id;

-- 方法3:使用COALESCE处理NULL(优化显示)
SELECT
    e.emp_name AS employee_name,
    COALESCE(m.emp_name, '无上级') AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;

要点说明

  • 自连接:同一张表使用不同别名与自己连接
  • LEFT JOIN:保留所有员工,包括没有上级的(最高领导)
  • 别名含义e 表示 employee(员工),m 表示 manager(经理)
  • 关联逻辑:员工的 manager_id 对应经理的 emp_id

场景对比

  • LEFT JOIN:显示所有员工,最高领导的上级显示为NULL
  • INNER JOIN:只显示有上级的员工,最高领导不会出现
  • RIGHT JOIN:实际上没有意义(等同于把e和m位置互换后用LEFT JOIN)

题目19:查询参与项目的员工姓名、部门名称和项目名称

💡 解题思路

这是三表连接的经典案例:

  1. 需要连接3个表:employees、departments、projects
  2. 还需要中间表 emp_projects 来建立员工和项目的多对多关系
  3. 总共4个表的连接

🔍 关键提示

  • 连接4个表的顺序:employees → departments → emp_projects → projects
  • 第一个JOIN:employees e INNER JOIN departments d ON e.dept_id = d.dept_id
  • 第二个JOIN:INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
  • 第三个JOIN:INNER JOIN projects p ON ep.project_id = p.project_id

📝 实现步骤

  1. 确定主表:employees(员工是查询的主体)
  2. 第一次连接:关联 departments 获取部门名称
  3. 第二次连接:关联 emp_projects 获取员工参与的项目
  4. 第三次连接:关联 projects 获取项目名称
  5. 思考:这里为什么都用INNER JOIN?

✅ 验证方法

-- 验证步骤
-- 1. 检查是否有重复的员工(一个员工参与多个项目会有多行)
SELECT emp_name, COUNT(*)
FROM (...你的查询...) AS result
GROUP BY emp_name;

-- 2. 手动验证某个员工参与的项目数
SELECT COUNT(*) FROM emp_projects WHERE emp_id = 1;

🚀 扩展思考

  • 如果要包括没有参与项目的员工,哪个JOIN要改成LEFT JOIN?
  • 如果要同时显示员工的角色(role),需要从哪个表获取?
  • 四个表的连接顺序可以调整吗?
  • 如何优化这个查询的性能?
💯 点击查看参考答案
-- 方法1:标准四表连接
SELECT
    e.emp_name,
    d.dept_name,
    p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id;

-- 方法2:同时显示员工在项目中的角色
SELECT
    e.emp_name,
    d.dept_name,
    p.project_name,
    ep.role
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id;

-- 方法3:包括没有参与项目的员工
SELECT
    e.emp_name,
    d.dept_name,
    p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
LEFT JOIN projects p ON ep.project_id = p.project_id;

要点说明

  • 多表连接顺序:从主表开始,逐步连接相关表
  • 中间表:emp_projects 是多对多关系的桥梁表
  • INNER JOIN:只显示参与了项目的员工
  • 一个员工多行:如果员工参与多个项目,会产生多行记录

连接路径

employees (员工)
    → departments (部门) 通过 dept_id
    → emp_projects (员工项目关联) 通过 emp_id
    → projects (项目) 通过 project_id

题目20:查询没有参与任何项目的员工

💡 解题思路

这题考查LEFT JOIN + IS NULL的经典用法:

  1. 使用LEFT JOIN连接员工和项目关联表
  2. 筛选右表为NULL的记录(即没有匹配的)
  3. 理解"找不存在的"这类问题的通用解法

🔍 关键提示

  • 使用LEFT JOIN:FROM employees e LEFT JOIN emp_projects ep
  • WHERE条件:ep.emp_id IS NULL(项目关联表中没有该员工的记录)
  • LEFT JOIN后,没有匹配的记录右表字段都是NULL

📝 实现步骤

  1. 思考:"没有参与项目"意味着在 emp_projects 表中找不到该员工
  2. 使用LEFT JOIN保留所有员工
  3. 添加WHERE条件筛选右表为NULL的记录
  4. 理解:为什么不能用INNER JOIN?

✅ 验证方法

-- 验证步骤
-- 1. 检查这些员工在emp_projects表中是否真的不存在
SELECT emp_id FROM emp_projects WHERE emp_id IN (
    -- 你的查询结果的emp_id列表
);  -- 应该返回空

-- 2. 反向验证:查询参与了项目的员工
SELECT DISTINCT e.emp_id, e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id;

🚀 扩展思考

  • 能否用子查询 NOT IN 实现?哪种性能更好?
  • 能否用 NOT EXISTS 实现?
  • 如果要查询"只参与了1个项目"的员工呢?
  • LEFT JOIN + IS NULL 这个模式可以用在哪些场景?
💯 点击查看参考答案
-- 方法1:LEFT JOIN + IS NULL(推荐,性能好)
SELECT e.emp_id, e.emp_name
FROM employees e
LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
WHERE ep.emp_id IS NULL;

-- 方法2:NOT IN 子查询(注意NULL值陷阱)
SELECT emp_id, emp_name
FROM employees
WHERE emp_id NOT IN (
    SELECT emp_id FROM emp_projects WHERE emp_id IS NOT NULL
);

-- 方法3:NOT EXISTS 子查询(性能较好)
SELECT e.emp_id, e.emp_name
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM emp_projects ep WHERE ep.emp_id = e.emp_id
);

-- 方法4:EXCEPT(MySQL 8.0+支持)
SELECT emp_id, emp_name FROM employees
EXCEPT
SELECT e.emp_id, e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id;

要点说明

  • LEFT JOIN + IS NULL:查找"不存在"的经典模式
  • 为什么用LEFT JOIN:保留左表所有记录,右表没匹配的为NULL
  • WHERE ep.emp_id IS NULL:筛选出没有参与项目的员工

NOT IN的陷阱

-- ❌ 危险:如果子查询结果包含NULL,整个查询返回空
WHERE emp_id NOT IN (SELECT emp_id FROM emp_projects)

-- ✅ 安全:过滤掉NULL
WHERE emp_id NOT IN (SELECT emp_id FROM emp_projects WHERE emp_id IS NOT NULL)

性能对比

  • LEFT JOIN + IS NULL ≈ NOT EXISTS > NOT IN(在有索引的情况下)
  • NOT IN 在子查询结果包含NULL时有逻辑陷阱

题目21:查询同时参与2个以上项目的员工姓名

💡 解题思路

这题结合了:

  1. 多表连接:员工表和项目关联表
  2. 分组统计:COUNT统计每个员工的项目数
  3. HAVING筛选:对分组后的结果进行过滤

🔍 关键提示

  • 连接表:employees e INNER JOIN emp_projects ep
  • 分组:GROUP BY e.emp_id, e.emp_name
  • 统计:COUNT(ep.project_id) AS project_count
  • 筛选:HAVING COUNT(ep.project_id) > 2

📝 实现步骤

  1. 连接employees和emp_projects表
  2. 按员工分组(GROUP BY)
  3. 使用COUNT统计每个员工参与的项目数
  4. 使用HAVING筛选项目数>2的员工
  5. 思考:为什么用HAVING而不是WHERE?

✅ 验证方法

-- 验证某个员工的项目数
SELECT emp_id, COUNT(*) AS project_count
FROM emp_projects
WHERE emp_id = 某个员工id
GROUP BY emp_id;

-- 查看所有员工的项目数分布
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
ORDER BY project_count DESC;

🚀 扩展思考

  • HAVING 和 WHERE 的区别是什么?
  • 如果要查询"恰好参与2个项目"的员工呢?
  • 如果要同时显示这些员工参与的所有项目名称呢?
  • COUNT(ep.project_id) 和 COUNT(*) 在这里有区别吗?
💯 点击查看参考答案
-- 方法1:标准答案
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(ep.project_id) > 2;

-- 方法2:只显示员工姓名(不显示项目数)
SELECT e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(ep.project_id) > 2;

-- 方法3:同时显示参与的项目名称(需要子查询或窗口函数)
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count,
    GROUP_CONCAT(p.project_name) AS projects
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(ep.project_id) > 2;

要点说明

  • HAVING vs WHERE
    • WHERE:过滤原始数据行(分组前)
    • HAVING:过滤分组后的结果(可以使用聚合函数)
  • GROUP BY:按员工分组,每个员工一行
  • COUNT:统计每个员工参与的项目数

错误示范

-- ❌ 不能在WHERE中使用聚合函数
SELECT e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
WHERE COUNT(ep.project_id) > 2  -- 错误!
GROUP BY e.emp_id, e.emp_name;

-- ✅ 必须用HAVING
HAVING COUNT(ep.project_id) > 2;

扩展:查询恰好2个项目的员工

HAVING COUNT(ep.project_id) = 2;

题目22:查询'技术部'员工参与的所有项目名称

💡 解题思路

这题需要:

  1. 多表连接(4个表)
  2. WHERE条件筛选特定部门
  3. 可能需要DISTINCT去重

🔍 关键提示

  • 连接路径:employees → departments → emp_projects → projects
  • WHERE条件:d.dept_name = '技术部'
  • 可能需要:DISTINCT 去重(多个技术部员工参与同一项目)
  • 选择字段:只需要项目名称 p.project_name

📝 实现步骤

  1. 从employees表开始,依次连接其他表
  2. 添加WHERE条件筛选部门名称
  3. SELECT只选择项目名称
  4. 思考:是否需要DISTINCT?为什么?

✅ 验证方法

-- 先查询技术部有哪些员工
SELECT e.emp_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_name = '技术部';

-- 再查询这些员工参与的项目
SELECT DISTINCT p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
WHERE d.dept_name = '技术部';

🚀 扩展思考

  • 如果不用DISTINCT会发生什么?
  • 如果要同时显示每个项目有多少技术部员工参与呢?
  • 能否用子查询实现?
  • 如果要查询"只有技术部员工参与"的项目呢?
💯 点击查看参考答案
-- 方法1:使用DISTINCT去重(推荐)
SELECT DISTINCT p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
WHERE d.dept_name = '技术部';

-- 方法2:使用GROUP BY去重
SELECT p.project_name
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
WHERE d.dept_name = '技术部'
GROUP BY p.project_id, p.project_name;

-- 方法3:同时显示每个项目的技术部参与人数
SELECT
    p.project_name,
    COUNT(DISTINCT e.emp_id) AS tech_emp_count,
    GROUP_CONCAT(DISTINCT e.emp_name) AS tech_employees
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
WHERE d.dept_name = '技术部'
GROUP BY p.project_id, p.project_name;

-- 方法4:使用子查询
SELECT DISTINCT p.project_name
FROM projects p
INNER JOIN emp_projects ep ON p.project_id = ep.project_id
WHERE ep.emp_id IN (
    SELECT e.emp_id
    FROM employees e
    INNER JOIN departments d ON e.dept_id = d.dept_id
    WHERE d.dept_name = '技术部'
);

要点说明

  • 为什么需要DISTINCT:如果多个技术部员工参与同一项目,项目名会重复
  • DISTINCT vs GROUP BY:都能去重,DISTINCT更简洁,GROUP BY可以配合聚合函数
  • 连接顺序:employees → departments(筛选部门)→ emp_projects → projects

场景对比

-- 不用DISTINCT:可能有重复项目
SELECT p.project_name FROM ...  -- 项目A, 项目A, 项目B

-- 用DISTINCT:去重后的结果
SELECT DISTINCT p.project_name FROM ...  -- 项目A, 项目B

题目23:查询与'张三'在同一部门的其他员工

💡 解题思路

这题需要:

  1. 子查询:先查出张三的部门ID
  2. 自连接或WHERE筛选:查询同部门的其他员工
  3. 排除自己:WHERE条件排除张三本人

🔍 关键提示

  • 方法1:子查询 WHERE dept_id = (SELECT dept_id FROM employees WHERE emp_name = '张三')
  • 方法2:自连接 FROM employees e1 JOIN employees e2 ON e1.dept_id = e2.dept_id
  • 排除张三:WHERE emp_name != '张三'WHERE e1.emp_id != e2.emp_id

📝 实现步骤

  1. 先写子查询:查询张三的dept_id
  2. 在主查询中使用这个dept_id作为条件
  3. 添加条件排除张三本人
  4. 思考:这道题有几种不同的实现方法?

✅ 验证方法

-- 先查张三的部门
SELECT dept_id FROM employees WHERE emp_name = '张三';

-- 然后手动查询该部门的所有员工
SELECT * FROM employees WHERE dept_id = (张三的部门ID);

-- 验证结果中不包含张三

🚀 扩展思考

  • 子查询和自连接两种方法哪种性能更好?
  • 如果有多个叫"张三"的员工怎么办?
  • 如果张三不存在,查询会返回什么?
  • 能否用EXISTS实现?
💯 点击查看参考答案
-- 方法1:使用子查询(推荐,简洁清晰)
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id = (
    SELECT dept_id FROM employees WHERE emp_name = '张三'
)
AND emp_name != '张三';

-- 方法2:使用自连接
SELECT e1.emp_id, e1.emp_name, e1.dept_id
FROM employees e1
INNER JOIN employees e2 ON e1.dept_id = e2.dept_id
WHERE e2.emp_name = '张三'
AND e1.emp_name != '张三';

-- 或者用emp_id排除(更严谨)
SELECT e1.emp_id, e1.emp_name, e1.dept_id
FROM employees e1
INNER JOIN employees e2 ON e1.dept_id = e2.dept_id
WHERE e2.emp_name = '张三'
AND e1.emp_id != e2.emp_id;

-- 方法3:使用EXISTS(性能较好)
SELECT e1.emp_id, e1.emp_name, e1.dept_id
FROM employees e1
WHERE EXISTS (
    SELECT 1 FROM employees e2
    WHERE e2.emp_name = '张三'
    AND e1.dept_id = e2.dept_id
    AND e1.emp_id != e2.emp_id
);

-- 方法4:先查部门ID再查询(分步骤)
-- 第一步
SET @zhang_dept = (SELECT dept_id FROM employees WHERE emp_name = '张三');
-- 第二步
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id = @zhang_dept
AND emp_name != '张三';

要点说明

  • 子查询:先查出张三的部门ID,再用这个ID查同部门员工
  • 排除自己:可以用姓名(!= '张三')或ID(!= e2.emp_id
  • 多个张三的问题:如果有同名者,用emp_id排除更准确

潜在问题

-- 如果张三不存在,子查询返回NULL
WHERE dept_id = NULL  -- 永远为FALSE,查不到任何结果

-- 解决方案:检查张三是否存在
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id = (
    SELECT dept_id FROM employees WHERE emp_name = '张三'
)
AND emp_name != '张三'
AND EXISTS (SELECT 1 FROM employees WHERE emp_name = '张三');

题目24:查询每个部门工资最高的员工信息

💡 解题思路

这是经典的分组取最大值问题:

  1. 需要先找出每个部门的最高工资
  2. 然后根据部门和工资找到对应的员工
  3. 可以用子查询或窗口函数实现

🔍 关键提示

  • 方法1:子查询 WHERE (dept_id, salary) IN (SELECT dept_id, MAX(salary) FROM employees GROUP BY dept_id)
  • 方法2:自连接 + GROUP BY
  • 方法3:窗口函数 ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)

📝 实现步骤

  1. 先思考:需要先找出每个部门的最高工资
  2. 子查询:SELECT dept_id, MAX(salary) FROM employees GROUP BY dept_id
  3. 用(dept_id, salary)组合作为条件匹配
  4. 思考:如果一个部门有多个人都是最高工资怎么办?

✅ 验证方法

-- 先查看每个部门的最高工资
SELECT dept_id, MAX(salary) AS max_salary
FROM employees
GROUP BY dept_id;

-- 验证查询结果中每个部门只有一个(或多个同工资的)员工
SELECT dept_id, COUNT(*)
FROM (...你的查询...) AS result
GROUP BY dept_id;

🚀 扩展思考

  • 如果一个部门有多个员工工资并列最高,都应该显示吗?
  • 子查询和窗口函数两种方法哪种更好?
  • 如果要查询每个部门工资前3名呢?
  • 能否用LEFT JOIN实现?
💯 点击查看参考答案
-- 方法1:使用子查询(推荐,兼容性好)
SELECT e.emp_id, e.emp_name, e.dept_id, e.salary
FROM employees e
WHERE (e.dept_id, e.salary) IN (
    SELECT dept_id, MAX(salary)
    FROM employees
    GROUP BY dept_id
);

-- 方法2:使用自连接
SELECT e.*
FROM employees e
INNER JOIN (
    SELECT dept_id, MAX(salary) AS max_salary
    FROM employees
    GROUP BY dept_id
) dept_max ON e.dept_id = dept_max.dept_id AND e.salary = dept_max.max_salary;

-- 方法3:使用窗口函数(MySQL 8.0+,推荐)
SELECT emp_id, emp_name, dept_id, salary
FROM (
    SELECT
        emp_id, emp_name, dept_id, salary,
        RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
    FROM employees
) ranked
WHERE rk = 1;

-- 或者使用ROW_NUMBER(只取一个最高工资员工)
SELECT emp_id, emp_name, dept_id, salary
FROM (
    SELECT
        emp_id, emp_name, dept_id, salary,
        ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
    FROM employees
) ranked
WHERE rn = 1;

-- 方法4:同时显示部门名称
SELECT e.emp_id, e.emp_name, d.dept_name, e.salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE (e.dept_id, e.salary) IN (
    SELECT dept_id, MAX(salary)
    FROM employees
    GROUP BY dept_id
);

要点说明

  • (dept_id, salary) IN:组合条件匹配,同时满足部门和工资
  • 并列最高工资
    • 子查询方法:会返回所有并列最高的员工
    • RANK():会返回所有并列最高的员工
    • ROW_NUMBER():只返回一个(按ORDER BY的第一个)

窗口函数解释

RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC)
-- PARTITION BY dept_id: 按部门分组
-- ORDER BY salary DESC: 每组内按工资降序
-- RANK(): 排名,并列的排名相同

RANK vs ROW_NUMBER

  • RANK():1, 1, 3, 4(有并列,跳号)
  • ROW_NUMBER():1, 2, 3, 4(没有并列,连续)

扩展:查询每个部门工资前3名

WHERE rk <= 3

题目25:查询没有下属的员工(不是任何人的经理)

💡 解题思路

这又是一个查找不存在的问题:

  1. 如果员工是经理,其emp_id会出现在其他员工的manager_id中
  2. 使用LEFT JOIN + IS NULL查找不在manager_id中的员工
  3. 或者使用NOT IN / NOT EXISTS

🔍 关键提示

  • 方法1:LEFT JOIN FROM employees e LEFT JOIN employees m ON e.emp_id = m.manager_id WHERE m.emp_id IS NULL
  • 方法2:NOT IN WHERE emp_id NOT IN (SELECT DISTINCT manager_id FROM employees WHERE manager_id IS NOT NULL)
  • 方法3:NOT EXISTS WHERE NOT EXISTS (SELECT 1 FROM employees e2 WHERE e2.manager_id = e1.emp_id)

📝 实现步骤

  1. 理解题意:没有下属 = 自己的emp_id不在任何人的manager_id中
  2. 使用LEFT JOIN将员工表自连接(e.emp_id = m.manager_id)
  3. 筛选右表为NULL的记录
  4. 思考:这道题和题目20的思路是否相似?

✅ 验证方法

-- 查看所有经理(有下属的员工)
SELECT DISTINCT manager_id
FROM employees
WHERE manager_id IS NOT NULL;

-- 验证查询结果中的员工ID是否都不在上述列表中
SELECT emp_id FROM (...你的查询...) AS result
WHERE emp_id IN (
    SELECT DISTINCT manager_id FROM employees WHERE manager_id IS NOT NULL
);  -- 应该返回空

🚀 扩展思考

  • 为什么NOT IN子查询中要过滤NULL?
  • LEFT JOIN和NOT EXISTS哪个性能更好?
  • 如果要查询"下属人数最多的经理"呢?
  • 能否同时显示员工的部门信息?
💯 点击查看参考答案
-- 方法1:LEFT JOIN + IS NULL(推荐)
SELECT e.emp_id, e.emp_name
FROM employees e
LEFT JOIN employees subordinate ON e.emp_id = subordinate.manager_id
WHERE subordinate.emp_id IS NULL;

-- 方法2:NOT IN(注意NULL陷阱)
SELECT emp_id, emp_name
FROM employees
WHERE emp_id NOT IN (
    SELECT DISTINCT manager_id
    FROM employees
    WHERE manager_id IS NOT NULL
);

-- 方法3:NOT EXISTS(性能较好)
SELECT e1.emp_id, e1.emp_name
FROM employees e1
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e2
    WHERE e2.manager_id = e1.emp_id
);

-- 方法4:使用EXCEPT(MySQL 8.0+)
SELECT emp_id, emp_name FROM employees
EXCEPT
SELECT DISTINCT e.emp_id, e.emp_name
FROM employees e
INNER JOIN employees subordinate ON e.emp_id = subordinate.manager_id;

-- 扩展:同时显示部门信息
SELECT e.emp_id, e.emp_name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
LEFT JOIN employees subordinate ON e.emp_id = subordinate.manager_id
WHERE subordinate.emp_id IS NULL;

要点说明

  • LEFT JOIN逻辑:将员工表与自己连接,关联条件是 e.emp_id = subordinate.manager_id
    • 如果某员工是经理,右表会有匹配记录(他的下属)
    • 如果某员工不是经理,右表为NULL
  • NOT IN的NULL陷阱:必须过滤NULL,否则查询返回空

NOT IN的NULL问题详解

-- ❌ 危险写法
WHERE emp_id NOT IN (SELECT manager_id FROM employees)
-- 如果manager_id有NULL值:
-- emp_id NOT IN (1, 2, NULL) 等价于
-- emp_id != 1 AND emp_id != 2 AND emp_id != NULL
-- 而 emp_id != NULL 永远是 NULL(不是TRUE也不是FALSE)
-- 导致整个条件为 NULL,返回空结果

-- ✅ 安全写法
WHERE emp_id NOT IN (
    SELECT manager_id FROM employees WHERE manager_id IS NOT NULL
)

扩展:查询每个经理的下属人数

SELECT
    e.emp_id,
    e.emp_name,
    COUNT(subordinate.emp_id) AS subordinate_count
FROM employees e
LEFT JOIN employees subordinate ON e.emp_id = subordinate.manager_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(subordinate.emp_id) > 0;

第四关:聚合函数与分组 - 学习指南

题目26:统计员工总数

💡 解题思路

这是最基础的聚合函数应用。需要理解:

  1. COUNT函数的作用是统计数量
  2. COUNT(*)和COUNT(字段)的区别
  3. 聚合函数返回单个值

🔍 关键提示

  • 使用 COUNT(*) 统计所有行数
  • 或使用 COUNT(字段) 统计非NULL值的数量
  • 不需要GROUP BY,因为是统计全表

📝 实现步骤

  1. 确定使用COUNT函数
  2. 决定是用COUNT(*)还是COUNT(字段)
  3. 写出完整的SELECT语句
  4. 思考:需要WHERE条件吗?

✅ 验证方法

-- 手动验证:查看所有员工
SELECT * FROM employees;
-- 数一数有多少行,应该与COUNT结果一致

🚀 扩展思考

  • COUNT(*)和COUNT(id)有什么区别?
  • 如果表中有NULL值,COUNT(*)和COUNT(某字段)结果会不同吗?
  • 如何统计去重后的数量?(提示:COUNT(DISTINCT 字段))
💯 点击查看参考答案
-- 方法1:使用COUNT(*)(推荐)
SELECT COUNT(*) AS total_employees FROM employees;

-- 方法2:使用COUNT(字段)
SELECT COUNT(id) AS total_employees FROM employees;

-- 方法3:使用COUNT(1)
SELECT COUNT(1) AS total_employees FROM employees;

要点说明

  • COUNT(*):统计所有行,包括所有字段都是NULL的行
  • COUNT(字段):只统计该字段非NULL的行数
  • COUNT(1):与COUNT(*)效果相同,性能也基本一致
  • 给结果起别名让输出更易读

区别示例

-- 假设某些员工的email为NULL
SELECT
    COUNT(*) AS all_count,        -- 返回所有员工数
    COUNT(email) AS email_count   -- 返回有邮箱的员工数
FROM employees;

题目27:计算所有员工的平均工资

💡 解题思路

这题考查AVG聚合函数的使用:

  1. AVG函数自动计算平均值
  2. 只能用于数值类型字段
  3. 自动忽略NULL值

🔍 关键提示

  • 使用 AVG(salary) 计算平均工资
  • AVG会自动忽略NULL值
  • 可以使用ROUND函数保留小数位数

📝 实现步骤

  1. 确定要计算平均值的字段:salary
  2. 使用AVG函数:AVG(salary)
  3. 考虑是否需要四舍五入
  4. 思考:如果有员工工资为NULL会怎样?

✅ 验证方法

-- 手动验证:查看所有工资并计算
SELECT salary FROM employees;
-- 用计算器算一下平均值,对比结果

🚀 扩展思考

  • AVG和SUM/COUNT的关系是什么?
  • 如何计算工资的中位数?
  • 如何保留2位小数?(提示:ROUND函数)
  • 如果某些员工工资为NULL,AVG如何处理?
💯 点击查看参考答案
-- 方法1:基本用法
SELECT AVG(salary) AS average_salary FROM employees;

-- 方法2:保留2位小数
SELECT ROUND(AVG(salary), 2) AS average_salary FROM employees;

-- 方法3:同时显示其他统计信息
SELECT
    AVG(salary) AS avg_salary,
    MAX(salary) AS max_salary,
    MIN(salary) AS min_salary,
    SUM(salary) AS total_salary,
    COUNT(*) AS employee_count
FROM employees;

-- 扩展:验证AVG的计算
SELECT
    SUM(salary) / COUNT(salary) AS manual_avg,
    AVG(salary) AS auto_avg
FROM employees;
-- 两个结果应该相同

要点说明

  • AVG(salary):自动计算平均值
  • ROUND(AVG(salary), 2):保留2位小数
  • AVG会自动忽略NULL值,只计算有值的记录
  • 可以同时使用多个聚合函数

NULL值处理

-- AVG自动忽略NULL
-- 假设5个员工工资为:1000, 2000, 3000, NULL, NULL
-- AVG结果 = (1000+2000+3000)/3 = 2000
-- 而不是 (1000+2000+3000)/5 = 1200

题目28:查询每个部门的员工数量

💡 解题思路

这是GROUP BY分组的入门题目:

  1. 需要按部门分组
  2. 对每个部门统计员工数
  3. 理解GROUP BY的作用

🔍 关键提示

  • 使用 GROUP BY department 按部门分组
  • 使用 COUNT(*) 统计每组的数量
  • SELECT中只能出现分组字段或聚合函数

📝 实现步骤

  1. 确定分组字段:department
  2. 确定聚合函数:COUNT(*)
  3. 写出SELECT语句:SELECT department, COUNT(*)
  4. 添加GROUP BY子句
  5. 思考:如果不加GROUP BY会怎样?

✅ 验证方法

-- 验证某个具体部门的员工数
SELECT COUNT(*) FROM employees WHERE department = '技术部';
-- 对比分组结果中技术部的数量是否一致

🚀 扩展思考

  • 如果SELECT中包含非分组字段会怎样?
  • 如何按多个字段分组?
  • 如何只显示员工数大于5的部门?(提示:HAVING)
  • GROUP BY和DISTINCT有什么区别?
💯 点击查看参考答案
-- 方法1:基本答案
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department;

-- 方法2:添加排序(按数量降序)
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
ORDER BY employee_count DESC;

-- 方法3:包含NULL值的处理
SELECT
    COALESCE(department, '未分配部门') AS department,
    COUNT(*) AS employee_count
FROM employees
GROUP BY department;

要点说明

  • GROUP BY department:按部门分组,每个部门返回一行
  • COUNT(*):统计每个分组的行数
  • SELECT中的字段必须是:
    • GROUP BY中的字段,或
    • 聚合函数(COUNT、SUM、AVG等)

错误示范

-- ❌ 错误:SELECT中包含非分组字段
SELECT department, name, COUNT(*)
FROM employees
GROUP BY department;
-- name不在GROUP BY中,也不是聚合函数,会报错

-- ✅ 正确:只包含分组字段和聚合函数
SELECT department, COUNT(*)
FROM employees
GROUP BY department;

执行过程理解

原始数据:
技术部  张三
技术部  李四
销售部  王五
销售部  赵六
技术部  孙七

GROUP BY department后:
技术部 → [张三, 李四, 孙七] → COUNT(*) = 3
销售部 → [王五, 赵六]       → COUNT(*) = 2

题目29:查询每个部门的平均工资,按平均工资降序排列

💡 解题思路

这题结合了分组、聚合、排序三个知识点:

  1. 按部门分组
  2. 计算每组的平均工资
  3. 对结果进行排序

🔍 关键提示

  • GROUP BY department 按部门分组
  • AVG(salary) 计算平均工资
  • ORDER BY AVG(salary) DESC 按平均工资降序
  • 注意SQL子句的顺序

📝 实现步骤

  1. 先写分组:GROUP BY department
  2. 添加聚合函数:AVG(salary)
  3. 在SELECT中选择需要的字段
  4. 添加ORDER BY排序
  5. 注意:ORDER BY必须在GROUP BY之后

✅ 验证方法

-- 验证某个部门的平均工资
SELECT AVG(salary) FROM employees WHERE department = '技术部';

-- 检查排序是否正确:第一行应该是平均工资最高的部门

🚀 扩展思考

  • 能否在ORDER BY中使用别名?
  • 如果同时按部门和职位分组呢?
  • 如何只显示平均工资前3名的部门?
  • ORDER BY中能直接写AVG(salary)还是必须用别名?
💯 点击查看参考答案
-- 方法1:基本答案
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;

-- 方法2:直接在ORDER BY中使用聚合函数
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY AVG(salary) DESC;

-- 方法3:保留2位小数,更美观
SELECT
    department,
    ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;

-- 方法4:同时显示员工数和平均工资
SELECT
    department,
    COUNT(*) AS employee_count,
    ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;

要点说明

  • 子句顺序(重要)SELECT → FROM → GROUP BY → ORDER BY
  • ORDER BY可以使用:
    • 别名:ORDER BY avg_salary DESC
    • 原表达式:ORDER BY AVG(salary) DESC
    • 位置编号:ORDER BY 2 DESC(2表示第2列)
  • DESC表示降序,ASC表示升序(默认)

完整执行顺序

1. FROM employees        -- 确定数据源
2. GROUP BY department   -- 按部门分组
3. AVG(salary)          -- 计算每组平均工资
4. SELECT               -- 选择要显示的列
5. ORDER BY             -- 对结果排序

多字段排序

-- 先按平均工资降序,平均工资相同时按部门名升序
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC, department ASC;

题目30:查询员工数量大于5的部门

💡 解题思路

这题引入了HAVING子句

  1. 先按部门分组统计员工数
  2. 再筛选出员工数>5的部门
  3. 理解HAVING和WHERE的区别

🔍 关键提示

  • 使用 GROUP BY department 分组
  • 使用 COUNT(*) 统计数量
  • 使用 HAVING COUNT(*) > 5 筛选分组结果
  • 不能用WHERE,因为WHERE在分组前执行

📝 实现步骤

  1. 先写基本的分组统计:SELECT department, COUNT(*) ... GROUP BY department
  2. 思考:应该用WHERE还是HAVING?
  3. 添加HAVING子句过滤分组结果
  4. 理解:为什么不能用WHERE COUNT(*) > 5?

✅ 验证方法

-- 先查看所有部门的员工数
SELECT department, COUNT(*) AS cnt
FROM employees
GROUP BY department;

-- 手动检查哪些部门的员工数>5

🚀 扩展思考

  • WHERE和HAVING的本质区别是什么?
  • 能否同时使用WHERE和HAVING?
  • HAVING COUNT(*) > 5WHERE COUNT(*) > 5 为什么后者会报错?
  • 如果要查询"技术部中员工数量>5的记录",如何写?
💯 点击查看参考答案
-- 方法1:基本答案
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;

-- 方法2:使用别名(注意:有些数据库不支持在HAVING中使用SELECT的别名)
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING employee_count > 5;  -- MySQL支持,标准SQL不支持

-- 方法3:同时使用WHERE和HAVING
SELECT department, COUNT(*) AS employee_count
FROM employees
WHERE salary > 3000  -- 先过滤:只统计工资>3000的员工
GROUP BY department
HAVING COUNT(*) > 5;  -- 再过滤:只显示员工数>5的部门

-- 方法4:添加排序
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY employee_count DESC;

WHERE vs HAVING 关键区别

对比项 WHERE HAVING
作用时机 分组过滤 分组过滤
作用对象 原始数据行 分组结果
能否使用聚合函数 ❌ 不能 ✅ 可以
执行顺序 2(FROM之后) 4(GROUP BY之后)

执行顺序示例

SELECT department, COUNT(*) AS cnt
FROM employees           -- 1. 获取数据
WHERE salary > 5000      -- 2. 过滤原始行(分组前)
GROUP BY department      -- 3. 按部门分组
HAVING COUNT(*) > 5      -- 4. 过滤分组结果(分组后)
ORDER BY cnt DESC;       -- 5. 排序

错误示范

-- ❌ 错误:WHERE不能使用聚合函数
SELECT department, COUNT(*)
FROM employees
WHERE COUNT(*) > 5  -- 报错!
GROUP BY department;

-- ✅ 正确:用HAVING
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING COUNT(*) > 5;

记忆技巧

  • WHERE = Where are the rows?(筛选行)
  • HAVING = Having these groups(筛选组)

题目31:查询每个部门工资最高和最低的差值

💡 解题思路

这题需要在分组中使用多个聚合函数

  1. 按部门分组
  2. 找出每组的最高工资和最低工资
  3. 计算两者的差值

🔍 关键提示

  • MAX(salary) 获取最高工资
  • MIN(salary) 获取最低工资
  • 可以在SELECT中进行算术运算:MAX(salary) - MIN(salary)
  • 给结果起个有意义的别名

📝 实现步骤

  1. 确定分组字段:department
  2. 使用MAX获取最高工资
  3. 使用MIN获取最低工资
  4. 计算差值:MAX - MIN
  5. 思考:能否直接在一个表达式中完成?

✅ 验证方法

-- 验证某个部门的最高和最低工资
SELECT MAX(salary), MIN(salary)
FROM employees
WHERE department = '技术部';
-- 手动计算差值,对比结果

🚀 扩展思考

  • 如何同时显示最高工资、最低工资和差值?
  • 如何找出工资差距最大的部门?
  • 能否在WHERE中使用MAX或MIN?
  • 差值为0说明什么?
💯 点击查看参考答案
-- 方法1:只显示差值
SELECT
    department,
    MAX(salary) - MIN(salary) AS salary_gap
FROM employees
GROUP BY department;

-- 方法2:同时显示最高、最低和差值(推荐)
SELECT
    department,
    MAX(salary) AS max_salary,
    MIN(salary) AS min_salary,
    MAX(salary) - MIN(salary) AS salary_gap
FROM employees
GROUP BY department;

-- 方法3:按差值降序排列,找出差距最大的部门
SELECT
    department,
    MAX(salary) AS max_salary,
    MIN(salary) AS min_salary,
    MAX(salary) - MIN(salary) AS salary_gap
FROM employees
GROUP BY department
ORDER BY salary_gap DESC;

-- 方法4:只显示差值大于5000的部门
SELECT
    department,
    MAX(salary) - MIN(salary) AS salary_gap
FROM employees
GROUP BY department
HAVING MAX(salary) - MIN(salary) > 5000;

要点说明

  • 可以在SELECT中进行算术运算
  • 多个聚合函数可以同时使用
  • 聚合函数可以在HAVING中作为过滤条件
  • 给计算结果起别名提高可读性

常用聚合函数组合

SELECT
    department,
    COUNT(*) AS emp_count,           -- 员工数
    SUM(salary) AS total_salary,      -- 工资总和
    AVG(salary) AS avg_salary,        -- 平均工资
    MAX(salary) AS max_salary,        -- 最高工资
    MIN(salary) AS min_salary,        -- 最低工资
    MAX(salary) - MIN(salary) AS gap  -- 差值
FROM employees
GROUP BY department;

实际意义

  • 工资差值大:说明部门内薪资差距大,可能有资深员工和新员工
  • 工资差值小:说明部门内薪资比较平均
  • 工资差值为0:说明该部门所有员工工资相同(或只有一个员工)

题目32:统计每个月入职的员工数量

💡 解题思路

这题结合了日期函数和分组

  1. 从入职日期中提取月份信息
  2. 按月份分组
  3. 统计每个月的员工数

🔍 关键提示

  • 使用 YEAR(hire_date)MONTH(hire_date) 提取年月
  • 或使用 DATE_FORMAT(hire_date, '%Y-%m') 格式化日期
  • 按提取的年月分组
  • 使用COUNT统计数量

📝 实现步骤

  1. 思考:如何从日期中提取月份?
  2. 决定分组方式:按年月还是只按月份?
  3. 写出GROUP BY子句
  4. 添加COUNT统计
  5. 考虑:2020年1月和2021年1月应该分开还是合并?

✅ 验证方法

-- 查看某个月入职的所有员工
SELECT * FROM employees
WHERE YEAR(hire_date) = 2020 AND MONTH(hire_date) = 3;
-- 数一数有多少人

🚀 扩展思考

  • 如何按年份统计?按季度统计?
  • DATE_FORMAT和YEAR/MONTH哪个更好用?
  • 如何让结果按时间顺序排列?
  • 如何只统计2020年的数据?
💯 点击查看参考答案
-- 方法1:使用YEAR和MONTH函数(推荐)
SELECT
    YEAR(hire_date) AS hire_year,
    MONTH(hire_date) AS hire_month,
    COUNT(*) AS employee_count
FROM employees
GROUP BY YEAR(hire_date), MONTH(hire_date)
ORDER BY hire_year, hire_month;

-- 方法2:使用DATE_FORMAT(更简洁)
SELECT
    DATE_FORMAT(hire_date, '%Y-%m') AS hire_month,
    COUNT(*) AS employee_count
FROM employees
GROUP BY DATE_FORMAT(hire_date, '%Y-%m')
ORDER BY hire_month;

-- 方法3:显示为中文格式
SELECT
    DATE_FORMAT(hire_date, '%Y年%m月') AS hire_month,
    COUNT(*) AS employee_count
FROM employees
GROUP BY YEAR(hire_date), MONTH(hire_date)
ORDER BY hire_month;

-- 方法4:只统计某一年的数据
SELECT
    MONTH(hire_date) AS month,
    COUNT(*) AS employee_count
FROM employees
WHERE YEAR(hire_date) = 2020
GROUP BY MONTH(hire_date)
ORDER BY month;

-- 扩展:按季度统计
SELECT
    YEAR(hire_date) AS year,
    QUARTER(hire_date) AS quarter,
    COUNT(*) AS employee_count
FROM employees
GROUP BY YEAR(hire_date), QUARTER(hire_date)
ORDER BY year, quarter;

要点说明

  • YEAR(hire_date):提取年份(如2020)
  • MONTH(hire_date):提取月份(1-12)
  • DATE_FORMAT(hire_date, format):格式化日期
    • '%Y-%m' → 2020-03
    • '%Y年%m月' → 2020年03月
    • '%Y-%m-%d' → 2020-03-15
  • QUARTER(hire_date):提取季度(1-4)

DATE_FORMAT常用格式

%Y  -- 四位年份(2020)
%y  -- 两位年份(20)
%m  -- 两位月份(01-12)
%c  -- 月份(1-12)
%d  -- 两位日期(01-31)
%H  -- 24小时制小时(00-23)
%i  -- 分钟(00-59)
%s  -- 秒(00-59)

分组注意事项

-- ❌ 错误:只按月份分组,会把不同年份的同一月份合并
SELECT MONTH(hire_date), COUNT(*)
FROM employees
GROUP BY MONTH(hire_date);
-- 结果:2020年3月和2021年3月会合并成一条记录

-- ✅ 正确:按年和月分组
SELECT YEAR(hire_date), MONTH(hire_date), COUNT(*)
FROM employees
GROUP BY YEAR(hire_date), MONTH(hire_date);
-- 结果:2020年3月和2021年3月是两条独立的记录

题目33:查询平均工资超过8000的部门名称和平均工资

💡 解题思路

这题需要多表JOIN + 分组 + HAVING

  1. 连接员工表和部门表获取部门名称
  2. 按部门分组计算平均工资
  3. 用HAVING筛选平均工资>8000的部门

🔍 关键提示

  • 需要JOIN employees和departments表
  • GROUP BY dept_iddept_name
  • HAVING AVG(salary) > 8000 筛选分组结果
  • 注意要显示部门名称,不是部门ID

📝 实现步骤

  1. 确定需要哪些表:employees(工资)、departments(部门名)
  2. 写JOIN语句连接两表
  3. 按部门分组
  4. 计算平均工资
  5. 使用HAVING筛选
  6. 思考:能否不JOIN,只用子查询?

✅ 验证方法

-- 先查看所有部门的平均工资
SELECT d.dept_name, AVG(e.salary)
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name;
-- 检查哪些部门平均工资>8000

🚀 扩展思考

  • 如果用LEFT JOIN会有什么问题?
  • 能否只用employees表实现(不显示部门名称)?
  • 如何同时显示员工数量?
  • WHERE和HAVING在这个查询中的区别是什么?
💯 点击查看参考答案
-- 方法1:标准答案(使用INNER JOIN)
SELECT
    d.dept_name,
    AVG(e.salary) AS avg_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING AVG(e.salary) > 8000;

-- 方法2:保留2位小数,添加排序
SELECT
    d.dept_name,
    ROUND(AVG(e.salary), 2) AS avg_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING AVG(e.salary) > 8000
ORDER BY avg_salary DESC;

-- 方法3:同时显示员工数量
SELECT
    d.dept_name,
    COUNT(e.emp_id) AS employee_count,
    ROUND(AVG(e.salary), 2) AS avg_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING AVG(e.salary) > 8000
ORDER BY avg_salary DESC;

-- 方法4:不使用JOIN(只显示部门ID,不显示名称)
SELECT
    dept_id,
    AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
HAVING AVG(salary) > 8000;

要点说明

  • 多表关联 + 分组是常见的组合查询模式
  • GROUP BY要包含SELECT中的非聚合字段
  • HAVING用于过滤分组后的聚合结果
  • 使用INNER JOIN只返回有匹配的记录

执行顺序

1. FROM employees e
2. INNER JOIN departments d  -- 关联表
3. GROUP BY d.dept_id       -- 分组
4. AVG(e.salary)           -- 计算聚合值
5. HAVING AVG > 8000       -- 过滤分组结果
6. SELECT                  -- 选择列
7. ORDER BY               -- 排序(如果有)

WHERE vs HAVING 在此题中的应用

-- 组合使用WHERE和HAVING
SELECT
    d.dept_name,
    AVG(e.salary) AS avg_salary
FROM employees e
INNER JOIN departments d ON e.dept_id = d.dept_id
WHERE e.hire_date > '2020-01-01'  -- WHERE: 只统计2020年后入职的员工
GROUP BY d.dept_id, d.dept_name
HAVING AVG(e.salary) > 8000;      -- HAVING: 筛选平均工资>8000的部门

GROUP BY的字段选择

-- 推荐:同时包含ID和名称
GROUP BY d.dept_id, d.dept_name

-- 或者只用ID(但MySQL在某些模式下可能报错)
GROUP BY d.dept_id

-- MySQL 5.7+可以设置sql_mode来控制GROUP BY的严格程度

题目34:查询每个经理管理的员工数量(不包括没有下属的)

💡 解题思路

这题需要自连接 + 分组

  1. 将员工表与自己连接(员工的manager_id = 经理的emp_id)
  2. 按经理分组统计下属数量
  3. 不显示没有下属的员工

🔍 关键提示

  • 自连接:FROM employees e JOIN employees m ON e.manager_id = m.emp_id
  • 按经理分组:GROUP BY m.emp_id, m.emp_name
  • 使用INNER JOIN自动排除没有下属的员工
  • 或使用HAVING COUNT(*) > 0

📝 实现步骤

  1. 理解表结构:manager_id指向另一个员工的emp_id
  2. 设计自连接:e表示员工,m表示经理
  3. 关联条件:e.manager_id = m.emp_id
  4. 按经理分组并统计
  5. 思考:用INNER JOIN还是LEFT JOIN?

✅ 验证方法

-- 先查看组织结构
SELECT e.emp_name AS 员工, m.emp_name AS 经理
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;

-- 手动数某个经理的下属数量
SELECT COUNT(*) FROM employees WHERE manager_id = 某经理ID;

🚀 扩展思考

  • 如果要包括没有下属的员工(显示0),应该怎么改?
  • 如何找出管理员工最多的经理?
  • 如何显示经理自己的职位信息?
  • 能否用子查询实现?
💯 点击查看参考答案
-- 方法1:使用INNER JOIN(推荐,自动排除没有下属的)
SELECT
    m.emp_id AS manager_id,
    m.emp_name AS manager_name,
    COUNT(e.emp_id) AS subordinate_count
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
GROUP BY m.emp_id, m.emp_name;

-- 方法2:按manager_id分组(更简单,但只显示ID)
SELECT
    manager_id,
    COUNT(*) AS subordinate_count
FROM employees
WHERE manager_id IS NOT NULL
GROUP BY manager_id;

-- 方法3:使用LEFT JOIN + HAVING(显式排除没有下属的)
SELECT
    m.emp_id AS manager_id,
    m.emp_name AS manager_name,
    COUNT(e.emp_id) AS subordinate_count
FROM employees m
LEFT JOIN employees e ON m.emp_id = e.manager_id
GROUP BY m.emp_id, m.emp_name
HAVING COUNT(e.emp_id) > 0;

-- 方法4:按下属数量降序排列,找出管理最多员工的经理
SELECT
    m.emp_name AS manager_name,
    COUNT(e.emp_id) AS subordinate_count
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
GROUP BY m.emp_id, m.emp_name
ORDER BY subordinate_count DESC;

-- 方法5:同时显示经理的部门信息
SELECT
    m.emp_name AS manager_name,
    d.dept_name AS manager_dept,
    COUNT(e.emp_id) AS subordinate_count
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
LEFT JOIN departments d ON m.dept_id = d.dept_id
GROUP BY m.emp_id, m.emp_name, d.dept_name;

要点说明

  • 自连接:同一张表使用不同别名与自己连接
  • INNER JOIN:只返回有下属的经理(因为必须匹配)
  • LEFT JOIN:返回所有员工,用HAVING过滤出有下属的
  • e表示员工(employee),m表示经理(manager)

自连接的逻辑

employees表:
emp_id  emp_name  manager_id
1       张三      NULL
2       李四      1
3       王五      1
4       赵六      2

自连接 e.manager_id = m.emp_id:
e.emp_name  e.manager_id  →  m.emp_id  m.emp_name
李四        1             →  1         张三
王五        1             →  1         张三
赵六        2             →  2         李四

按m分组统计:
张三 → [李四, 王五] → COUNT = 2
李四 → [赵六]       → COUNT = 1

INNER JOIN vs LEFT JOIN

-- INNER JOIN:只显示有下属的经理
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
-- 结果:只有张三和李四(因为他们有下属)

-- LEFT JOIN + HAVING:先包括所有,再过滤
FROM employees m
LEFT JOIN employees e ON m.emp_id = e.manager_id
HAVING COUNT(e.emp_id) > 0
-- 结果:所有员工,但HAVING过滤掉没有下属的

扩展:包括没有下属的员工

SELECT
    m.emp_name AS employee_name,
    COUNT(e.emp_id) AS subordinate_count
FROM employees m
LEFT JOIN employees e ON m.emp_id = e.manager_id
GROUP BY m.emp_id, m.emp_name
ORDER BY subordinate_count DESC;
-- 这样会显示所有员工,没有下属的显示0

题目35:查询参与项目数量最多的前3名员工

💡 解题思路

这题综合了多表JOIN + 分组 + 排序 + LIMIT

  1. 连接员工表和项目关联表
  2. 按员工分组统计项目数
  3. 按项目数降序排列
  4. 取前3名

🔍 关键提示

  • JOIN employees和emp_projects表
  • GROUP BY emp_id 按员工分组
  • COUNT(project_id) 统计项目数
  • ORDER BY COUNT(project_id) DESC 降序排列
  • LIMIT 3 只取前3名

📝 实现步骤

  1. 确定需要的表:employees、emp_projects
  2. 写JOIN关联两表
  3. 按员工分组
  4. 统计每个员工的项目数
  5. 排序并限制结果数量
  6. 思考:如果第3名有并列怎么办?

✅ 验证方法

-- 先查看所有员工的项目数
SELECT e.emp_name, COUNT(ep.project_id) AS project_count
FROM employees e
LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
ORDER BY project_count DESC;
-- 手动确认前3名

🚀 扩展思考

  • 如果要处理并列第3名的情况怎么办?
  • 如果员工没有参与任何项目,应该显示吗?
  • 如何同时显示参与的项目名称?
  • LIMIT和RANK()窗口函数有什么区别?
💯 点击查看参考答案
-- 方法1:基本答案
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
ORDER BY project_count DESC
LIMIT 3;

-- 方法2:同时显示员工ID
SELECT
    e.emp_id,
    e.emp_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
ORDER BY project_count DESC
LIMIT 3;

-- 方法3:处理并列情况(使用窗口函数,MySQL 8.0+)
SELECT emp_id, emp_name, project_count
FROM (
    SELECT
        e.emp_id,
        e.emp_name,
        COUNT(ep.project_id) AS project_count,
        RANK() OVER (ORDER BY COUNT(ep.project_id) DESC) AS ranking
    FROM employees e
    INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
    GROUP BY e.emp_id, e.emp_name
) AS ranked
WHERE ranking <= 3;

-- 方法4:同时显示部门信息
SELECT
    e.emp_name,
    d.dept_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
LEFT JOIN departments d ON e.dept_id = d.dept_id
GROUP BY e.emp_id, e.emp_name, d.dept_name
ORDER BY project_count DESC
LIMIT 3;

-- 方法5:显示参与的项目名称列表
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count,
    GROUP_CONCAT(p.project_name) AS project_list
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
INNER JOIN projects p ON ep.project_id = p.project_id
GROUP BY e.emp_id, e.emp_name
ORDER BY project_count DESC
LIMIT 3;

要点说明

  • 多表JOIN:连接employees和emp_projects
  • 分组统计:按员工分组,统计项目数
  • ORDER BY + LIMIT:先排序再取前N名
  • INNER JOIN:只显示参与过项目的员工

LIMIT的问题

-- 假设项目数量排名:
-- 张三: 10个项目(第1名)
-- 李四: 8个项目(第2名)
-- 王五: 5个项目(第3名,并列)
-- 赵六: 5个项目(第3名,并列)

-- LIMIT 3 的结果:
-- 张三, 李四, 王五(赵六被排除了,虽然也是第3名)

-- 使用RANK()的结果:
-- 张三(rank=1), 李四(rank=2), 王五(rank=3), 赵六(rank=3)
-- WHERE ranking <= 3 会包括所有第3名

RANK vs LIMIT

  • LIMIT:严格限制返回行数,简单直接
  • RANK():处理并列情况,更符合"前3名"的语义

GROUP_CONCAT用法

-- GROUP_CONCAT: 将分组内的多个值连接成一个字符串
GROUP_CONCAT(p.project_name)  -- 默认逗号分隔
GROUP_CONCAT(p.project_name SEPARATOR '; ')  -- 自定义分隔符
GROUP_CONCAT(DISTINCT p.project_name ORDER BY p.project_name)  -- 去重并排序

完整查询解析

1. FROM employees e                    -- 主表:员工
2. INNER JOIN emp_projects ep          -- 关联:项目关联表
3. GROUP BY e.emp_id, e.emp_name       -- 分组:按员工
4. COUNT(ep.project_id)                -- 聚合:统计项目数
5. ORDER BY project_count DESC         -- 排序:按项目数降序
6. LIMIT 3                             -- 限制:只取前3条

第五关:子查询 - 学习指南

题目36:查询工资高于平均工资的员工信息

💡 解题思路

这是标量子查询的经典应用:

  1. 先计算平均工资(子查询)
  2. 再筛选工资大于平均值的员工(外层查询)
  3. 理解子查询返回单个值的特点

🔍 关键提示

  • 子查询:(SELECT AVG(salary) FROM employees)
  • 外层查询:WHERE salary > (子查询)
  • 子查询会先执行,返回一个数值
  • 这个数值被用于外层查询的比较

📝 实现步骤

  1. 先单独写子查询:计算平均工资
  2. 将子查询放入WHERE条件中
  3. 外层查询筛选符合条件的员工
  4. 思考:子查询会执行几次?

✅ 验证方法

-- 先查看平均工资是多少
SELECT AVG(salary) FROM employees;
-- 假设是6000

-- 然后验证查询结果
SELECT * FROM employees WHERE salary > 6000;
-- 应该与子查询的结果一致

🚀 扩展思考

  • 能否用JOIN实现这个需求?
  • 如果要查询低于平均工资的员工呢?
  • 如何查询工资在平均值上下10%范围内的员工?
  • 子查询和变量哪个性能更好?
💯 点击查看参考答案
-- 方法1:标准子查询
SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- 方法2:只查询需要的字段
SELECT emp_id, emp_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- 方法3:同时显示平均工资(使用CROSS JOIN)
SELECT
    e.*,
    avg_sal.avg_salary
FROM employees e
CROSS JOIN (SELECT AVG(salary) AS avg_salary FROM employees) AS avg_sal
WHERE e.salary > avg_sal.avg_salary;

-- 方法4:显示高出平均工资的百分比
SELECT
    emp_name,
    salary,
    (SELECT AVG(salary) FROM employees) AS avg_salary,
    ROUND((salary - (SELECT AVG(salary) FROM employees)) / (SELECT AVG(salary) FROM employees) * 100, 2) AS percent_above_avg
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);

-- 方法5:使用变量(避免重复计算子查询)
SELECT AVG(salary) INTO @avg_salary FROM employees;
SELECT * FROM employees WHERE salary > @avg_salary;

要点说明

  • 标量子查询:返回单个值(一行一列)
  • 执行顺序:子查询先执行,结果用于外层查询
  • 子查询用括号包围:(SELECT ...)
  • 可以在WHERE、SELECT、HAVING等位置使用

子查询类型

-- 标量子查询:返回单个值
WHERE salary > (SELECT AVG(salary) FROM employees)

-- 列子查询:返回一列多行
WHERE dept_id IN (SELECT dept_id FROM departments WHERE location = '北京')

-- 行子查询:返回一行多列
WHERE (salary, age) = (SELECT MAX(salary), MAX(age) FROM employees)

-- 表子查询:返回多行多列
FROM (SELECT * FROM employees WHERE salary > 5000) AS high_earners

性能优化

-- ❌ 不好:子查询在SELECT中,每行都执行一次
SELECT
    emp_name,
    salary,
    (SELECT AVG(salary) FROM employees) AS avg_sal,
    (SELECT AVG(salary) FROM employees) AS avg_sal2
FROM employees;

-- ✅ 好:用变量或JOIN避免重复计算
SELECT
    emp_name,
    salary,
    avg_sal.avg_salary
FROM employees
CROSS JOIN (SELECT AVG(salary) AS avg_salary FROM employees) AS avg_sal
WHERE salary > avg_sal.avg_salary;

常见变体

-- 查询低于平均工资的员工
WHERE salary < (SELECT AVG(salary) FROM employees)

-- 查询工资在平均值±10%范围内的员工
WHERE salary BETWEEN
    (SELECT AVG(salary) * 0.9 FROM employees)
    AND (SELECT AVG(salary) * 1.1 FROM employees)

-- 查询工资在中位数以上的员工(更复杂)
WHERE salary >= (
    SELECT salary
    FROM employees
    ORDER BY salary
    LIMIT 1 OFFSET (SELECT COUNT(*) FROM employees) / 2
)

题目37:查询工资最高的员工姓名和工资

💡 解题思路

这题有多种实现方式

  1. 使用子查询找出最高工资,再查询该员工
  2. 使用ORDER BY + LIMIT直接取第一名
  3. 注意:可能有多个员工并列最高工资

🔍 关键提示

  • 方法1:子查询 WHERE salary = (SELECT MAX(salary) FROM employees)
  • 方法2:排序 ORDER BY salary DESC LIMIT 1
  • 方法1能处理并列情况,方法2只返回一条
  • MAX函数返回最大值

📝 实现步骤

  1. 思考:最高工资是多少?用MAX查询
  2. 找出工资等于最高工资的员工
  3. 或者:直接按工资降序排列,取第一个
  4. 考虑:如果有多个员工工资都是最高,怎么办?

✅ 验证方法

-- 先查看最高工资
SELECT MAX(salary) FROM employees;

-- 查看有多少人是最高工资
SELECT COUNT(*) FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

🚀 扩展思考

  • 两种方法在有并列第一时结果有何不同?
  • 如何查询工资第二高的员工?
  • 如何查询每个部门工资最高的员工?
  • 哪种方法性能更好?
💯 点击查看参考答案
-- 方法1:子查询(推荐,能处理并列情况)
SELECT emp_name, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

-- 方法2:ORDER BY + LIMIT(只返回一条记录)
SELECT emp_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 1;

-- 方法3:使用ALL(等价于方法1)
SELECT emp_name, salary
FROM employees
WHERE salary >= ALL(SELECT salary FROM employees);

-- 方法4:使用窗口函数(MySQL 8.0+,处理并列)
SELECT emp_name, salary
FROM (
    SELECT
        emp_name,
        salary,
        RANK() OVER (ORDER BY salary DESC) AS ranking
    FROM employees
) AS ranked
WHERE ranking = 1;

-- 方法5:同时显示部门信息
SELECT e.emp_name, e.salary, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary = (SELECT MAX(salary) FROM employees);

要点说明

  • 子查询方式:能返回所有最高工资的员工(处理并列)
  • LIMIT方式:只返回一条,无法处理并列情况
  • MAX(salary):返回最大值
  • >= ALL:大于等于所有值,即最大值

方法对比

假设工资情况:
张三: 10000(最高)
李四: 10000(最高,并列)
王五: 9000

方法1(子查询)结果:
张三  10000
李四  10000
✅ 返回所有最高工资的员工

方法2(LIMIT 1)结果:
张三  10000
❌ 只返回一个,李四被遗漏了

查询第二高工资

-- 方法1:使用子查询排除最高工资
SELECT emp_name, salary
FROM employees
WHERE salary = (
    SELECT MAX(salary)
    FROM employees
    WHERE salary < (SELECT MAX(salary) FROM employees)
);

-- 方法2:使用LIMIT OFFSET
SELECT emp_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

-- 方法3:使用DISTINCT去重(更准确)
SELECT emp_name, salary
FROM employees
WHERE salary = (
    SELECT DISTINCT salary
    FROM employees
    ORDER BY salary DESC
    LIMIT 1 OFFSET 1
);

-- 方法4:使用窗口函数DENSE_RANK
SELECT emp_name, salary
FROM (
    SELECT
        emp_name,
        salary,
        DENSE_RANK() OVER (ORDER BY salary DESC) AS ranking
    FROM employees
) AS ranked
WHERE ranking = 2;

ALL关键字说明

-- >= ALL: 大于等于所有值
WHERE salary >= ALL(SELECT salary FROM employees)
-- 等价于
WHERE salary = (SELECT MAX(salary) FROM employees)

-- <= ALL: 小于等于所有值(最小值)
WHERE salary <= ALL(SELECT salary FROM employees)
-- 等价于
WHERE salary = (SELECT MIN(salary) FROM employees)

性能考虑

  • 子查询:需要扫描表两次(一次MAX,一次匹配)
  • ORDER BY + LIMIT:只需一次扫描,但无法处理并列
  • 窗口函数:一次扫描,能处理并列,推荐使用(MySQL 8.0+)

题目38:查询与'张三'同一部门的其他员工(使用子查询)

💡 解题思路

这题在第三关已经出现过,这里用子查询方式实现:

  1. 子查询:查询张三的部门ID
  2. 外层查询:查询该部门的其他员工
  3. 排除张三本人

🔍 关键提示

  • 子查询:(SELECT dept_id FROM employees WHERE emp_name = '张三')
  • 外层WHERE:dept_id = (子查询) AND emp_name != '张三'
  • 注意排除张三本人
  • 考虑如果有多个叫张三的员工

📝 实现步骤

  1. 先写子查询:查询张三的dept_id
  2. 在外层查询中使用这个dept_id
  3. 添加条件排除张三本人
  4. 思考:用emp_id排除还是用emp_name排除?

✅ 验证方法

-- 先查张三的部门
SELECT dept_id FROM employees WHERE emp_name = '张三';

-- 手动查询该部门的所有员工
SELECT * FROM employees WHERE dept_id = (张三的部门ID);

🚀 扩展思考

  • 这题和第三关的题目23有什么区别?
  • 子查询和自连接两种方法哪个更好?
  • 如果有多个叫"张三"的员工,结果会怎样?
  • 如何同时显示部门名称?
💯 点击查看参考答案
-- 方法1:标准子查询(使用姓名排除)
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id = (
    SELECT dept_id FROM employees WHERE emp_name = '张三'
)
AND emp_name != '张三';

-- 方法2:使用emp_id排除(更严谨)
SELECT e1.emp_id, e1.emp_name, e1.dept_id
FROM employees e1
WHERE e1.dept_id = (
    SELECT e2.dept_id
    FROM employees e2
    WHERE e2.emp_name = '张三'
    LIMIT 1  -- 如果有多个张三,只取第一个的部门
)
AND e1.emp_id != (
    SELECT e2.emp_id
    FROM employees e2
    WHERE e2.emp_name = '张三'
    LIMIT 1
);

-- 方法3:使用IN(如果有多个张三)
SELECT emp_id, emp_name, dept_id
FROM employees
WHERE dept_id IN (
    SELECT dept_id FROM employees WHERE emp_name = '张三'
)
AND emp_name != '张三';

-- 方法4:同时显示部门名称
SELECT e.emp_id, e.emp_name, d.dept_name
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
WHERE e.dept_id = (
    SELECT dept_id FROM employees WHERE emp_name = '张三' LIMIT 1
)
AND e.emp_name != '张三';

-- 对比:使用自连接(第三关的方法)
SELECT e1.emp_id, e1.emp_name, e1.dept_id
FROM employees e1
INNER JOIN employees e2 ON e1.dept_id = e2.dept_id
WHERE e2.emp_name = '张三'
AND e1.emp_name != '张三';

要点说明

  • 标量子查询:返回张三的dept_id
  • 排除自己:AND emp_name != '张三' 或 AND emp_id != (子查询)
  • 多个张三:用LIMIT 1限制或改用IN
  • 题目要求使用子查询,所以不用JOIN

子查询 vs 自连接对比

对比项 子查询 自连接
可读性 更清晰,逻辑分步 稍复杂
性能 可能执行多次 一次扫描
灵活性 易于修改条件 需要调整JOIN
适用场景 简单筛选 复杂关联

处理多个同名员工

-- 问题:如果有2个张三,在不同部门
emp_id  emp_name  dept_id
1       张三      1
2       李四      1
3       张三      2
4       王五      2

-- 方法1的结果(有歧义):
-- 子查询可能返回多个dept_id,导致错误

-- 正确处理:指定要查询哪个张三
WHERE dept_id = (
    SELECT dept_id
    FROM employees
    WHERE emp_name = '张三'
    AND emp_id = 1  -- 指定具体的张三
)

-- 或者:查询所有张三的同事
WHERE dept_id IN (
    SELECT dept_id FROM employees WHERE emp_name = '张三'
)
AND emp_name != '张三';

子查询位置

-- WHERE子句中
WHERE dept_id = (SELECT ...)

-- SELECT子句中
SELECT
    emp_name,
    (SELECT dept_name FROM departments d WHERE d.dept_id = e.dept_id) AS dept_name
FROM employees e;

-- FROM子句中
FROM (SELECT * FROM employees WHERE salary > 5000) AS高薪员工

题目39:查询没有员工的部门名称

💡 解题思路

这是查找不存在的经典问题:

  1. 方法1:LEFT JOIN + IS NULL
  2. 方法2:NOT IN 子查询
  3. 方法3:NOT EXISTS 子查询
  4. 理解这三种方法的异同

🔍 关键提示

  • 方法1:FROM departments d LEFT JOIN employees e ... WHERE e.dept_id IS NULL
  • 方法2:WHERE dept_id NOT IN (SELECT DISTINCT dept_id FROM employees)
  • 方法3:WHERE NOT EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id)
  • 注意NOT IN的NULL陷阱

📝 实现步骤

  1. 理解题意:部门表中存在,但员工表中没有人
  2. 思考:用LEFT JOIN还是子查询?
  3. 选择一种方法实现
  4. 验证:确保所有方法结果一致

✅ 验证方法

-- 先查看所有部门
SELECT * FROM departments;

-- 查看每个部门的员工数
SELECT d.dept_name, COUNT(e.emp_id) AS emp_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name;
-- 员工数为0的就是没有员工的部门

🚀 扩展思考

  • 三种方法的性能有区别吗?
  • NOT IN为什么要注意NULL值?
  • NOT EXISTS和NOT IN哪个更好?
  • 如何查询有员工的部门?
💯 点击查看参考答案
-- 方法1:LEFT JOIN + IS NULL(推荐,直观易懂)
SELECT d.dept_id, d.dept_name
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.dept_id IS NULL;

-- 方法2:NOT IN 子查询(注意NULL陷阱)
SELECT dept_id, dept_name
FROM departments
WHERE dept_id NOT IN (
    SELECT DISTINCT dept_id
    FROM employees
    WHERE dept_id IS NOT NULL  -- 必须过滤NULL
);

-- 方法3:NOT EXISTS(推荐,性能好)
SELECT d.dept_id, d.dept_name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.dept_id = d.dept_id
);

-- 方法4:使用子查询和聚合
SELECT d.dept_id, d.dept_name
FROM departments d
LEFT JOIN (
    SELECT DISTINCT dept_id FROM employees
) AS emp_depts ON d.dept_id = emp_depts.dept_id
WHERE emp_depts.dept_id IS NULL;

要点说明

  • LEFT JOIN + IS NULL:保留左表所有记录,右表没匹配的为NULL
  • NOT IN:不在子查询结果列表中
  • NOT EXISTS:子查询没有返回结果则为真
  • 三种方法逻辑等价,但性能和陷阱不同

NOT IN的NULL陷阱(重要!)

-- ❌ 危险写法
SELECT dept_id, dept_name
FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM employees);

-- 如果employees表中有dept_id为NULL的记录:
-- NOT IN (1, 2, NULL) 等价于
-- dept_id != 1 AND dept_id != 2 AND dept_id != NULL
-- 而 dept_id != NULL 永远是NULL(不是TRUE也不是FALSE)
-- 导致整个条件为NULL,查询返回空结果!

-- ✅ 安全写法:过滤NULL
WHERE dept_id NOT IN (
    SELECT dept_id FROM employees WHERE dept_id IS NOT NULL
)

性能对比

NOT EXISTS vs NOT IN:
- NOT EXISTS: 找到第一条匹配就停止(短路),性能好
- NOT IN: 需要完全计算子查询结果,性能稍差
- NOT IN有NULL陷阱,NOT EXISTS没有

LEFT JOIN vs NOT EXISTS:
- LEFT JOIN: 需要连接操作,但直观
- NOT EXISTS: 半连接优化,性能通常更好
- 小数据量差别不大,大数据量NOT EXISTS更优

推荐顺序:NOT EXISTS > LEFT JOIN + IS NULL > NOT IN

三种方法的执行逻辑

-- LEFT JOIN方式:
1. 将departments和employees进行LEFT JOIN
2. 保留所有部门,没有员工的部门右侧为NULL
3. WHERE e.dept_id IS NULL筛选出NULL的记录

-- NOT IN方式:
1. 子查询查出所有有员工的部门ID列表
2. 外层查询筛选不在这个列表中的部门

-- NOT EXISTS方式:
1. 对每个部门,检查employees表中是否存在该部门的员工
2. 如果不存在(NOT EXISTS为真),则返回该部门

相反问题:查询有员工的部门

-- 方法1:INNER JOIN
SELECT DISTINCT d.dept_id, d.dept_name
FROM departments d
INNER JOIN employees e ON d.dept_id = e.dept_id;

-- 方法2:IN
SELECT dept_id, dept_name
FROM departments
WHERE dept_id IN (
    SELECT DISTINCT dept_id FROM employees WHERE dept_id IS NOT NULL
);

-- 方法3:EXISTS
SELECT d.dept_id, d.dept_name
FROM departments d
WHERE EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id
);

题目40:查询工资高于本部门平均工资的员工

💡 解题思路

这是相关子查询的经典应用:

  1. 对每个员工,计算其所在部门的平均工资
  2. 比较员工工资与部门平均工资
  3. 子查询引用外层查询的字段(相关子查询)

🔍 关键提示

  • 相关子查询:子查询引用外层的字段
  • WHERE e1.salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.dept_id = e1.dept_id)
  • 子查询会对每行执行一次
  • 也可以用窗口函数实现

📝 实现步骤

  1. 理解题意:每个员工与本部门的平均工资比较,不是全公司
  2. 写子查询:计算某个部门的平均工资
  3. 让子查询引用外层的dept_id(相关子查询)
  4. 思考:这个子查询会执行多少次?

✅ 验证方法

-- 先查看每个部门的平均工资
SELECT dept_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY dept_id;

-- 手动检查某个员工是否高于本部门平均工资

🚀 扩展思考

  • 相关子查询和普通子查询有什么区别?
  • 如何用窗口函数实现?
  • 如何查询低于本部门平均工资的员工?
  • 性能会不会很差?如何优化?
💯 点击查看参考答案
-- 方法1:相关子查询(标准方法)
SELECT e1.emp_id, e1.emp_name, e1.dept_id, e1.salary
FROM employees e1
WHERE e1.salary > (
    SELECT AVG(e2.salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id  -- 相关:引用外层的dept_id
);

-- 方法2:使用JOIN和分组子查询
SELECT e.emp_id, e.emp_name, e.dept_id, e.salary
FROM employees e
INNER JOIN (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY dept_id
) AS dept_avg ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.avg_salary;

-- 方法3:使用窗口函数(MySQL 8.0+,性能最好)
SELECT emp_id, emp_name, dept_id, salary
FROM (
    SELECT
        emp_id,
        emp_name,
        dept_id,
        salary,
        AVG(salary) OVER (PARTITION BY dept_id) AS dept_avg_salary
    FROM employees
) AS with_avg
WHERE salary > dept_avg_salary;

-- 方法4:同时显示部门平均工资和差值
SELECT
    e.emp_name,
    e.salary,
    dept_avg.avg_salary AS dept_avg,
    e.salary - dept_avg.avg_salary AS above_avg
FROM employees e
INNER JOIN (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY dept_id
) AS dept_avg ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.avg_salary
ORDER BY above_avg DESC;

要点说明

  • 相关子查询:子查询引用外层查询的字段(e1.dept_id)
  • 执行次数:外层每一行都会执行一次子查询
  • 性能:相关子查询性能较差,窗口函数性能最好
  • 三种方法结果相同,但执行效率不同

相关子查询 vs 普通子查询

-- 普通子查询:执行一次,返回结果供外层使用
WHERE salary > (SELECT AVG(salary) FROM employees)
-- 执行1次子查询

-- 相关子查询:外层每行执行一次
WHERE salary > (
    SELECT AVG(salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id
)
-- 如果外层有100行,子查询执行100次

执行过程示例

假设employees表:
emp_id  emp_name  dept_id  salary
1       张三      1        8000
2       李四      1        6000
3       王五      2        9000
4       赵六      2        7000

1. 处理张三(dept_id=1):
   子查询:AVG(salary) WHERE dept_id=1 → (8000+6000)/2 = 7000
   比较:8000 > 7000 → 真,张三被选中

2. 处理李四(dept_id=1):
   子查询:AVG(salary) WHERE dept_id=1 → 7000
   比较:6000 > 7000 → 假,李四不被选中

3. 处理王五(dept_id=2):
   子查询:AVG(salary) WHERE dept_id=2 → (9000+7000)/2 = 8000
   比较:9000 > 8000 → 真,王五被选中

4. 处理赵六(dept_id=2):
   子查询:AVG(salary) WHERE dept_id=2 → 8000
   比较:7000 > 8000 → 假,赵六不被选中

最终结果:张三、王五

性能优化对比

-- 慢:相关子查询(N次查询)
-- 外层100行 → 子查询执行100次

-- 快:JOIN方式(2次查询)
-- 1. 子查询统计各部门平均工资(扫描一次employees)
-- 2. JOIN和筛选(扫描一次employees)

-- 最快:窗口函数(1次查询)
-- 一次扫描完成所有计算

窗口函数详解

AVG(salary) OVER (PARTITION BY dept_id)
-- OVER: 窗口函数标志
-- PARTITION BY dept_id: 按部门分组
-- AVG(salary): 计算每组的平均工资
-- 结果:为每行添加一个字段,显示该行所在部门的平均工资

扩展:查询工资是本部门最高的员工

-- 相关子查询
SELECT emp_name, dept_id, salary
FROM employees e1
WHERE salary = (
    SELECT MAX(salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id
);

-- 窗口函数
SELECT emp_name, dept_id, salary
FROM (
    SELECT
        emp_name, dept_id, salary,
        RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
    FROM employees
) ranked
WHERE rk = 1;

题目41:查询至少参与过一个项目的员工姓名

💡 解题思路

这题可以用多种方法实现:

  1. EXISTS子查询(推荐)
  2. IN子查询
  3. INNER JOIN
  4. 理解"至少一个"的含义

🔍 关键提示

  • EXISTS:WHERE EXISTS (SELECT 1 FROM emp_projects ep WHERE ep.emp_id = e.emp_id)
  • IN:WHERE emp_id IN (SELECT DISTINCT emp_id FROM emp_projects)
  • JOIN:SELECT DISTINCT e.emp_name FROM employees e INNER JOIN emp_projects ep
  • DISTINCT去重很重要

📝 实现步骤

  1. 理解题意:只要参与过项目(至少1个)
  2. 思考:用EXISTS、IN还是JOIN?
  3. 选择一种方法实现
  4. 注意:可能一个员工参与多个项目,需要去重

✅ 验证方法

-- 先查看有多少员工参与了项目
SELECT COUNT(DISTINCT emp_id) FROM emp_projects;

-- 验证结果数量是否一致

🚀 扩展思考

  • EXISTS和IN哪个性能更好?
  • 为什么JOIN需要DISTINCT?
  • 如何查询没有参与项目的员工?
  • 如何查询参与超过3个项目的员工?
💯 点击查看参考答案
-- 方法1:使用EXISTS(推荐,性能好)
SELECT emp_name
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM emp_projects ep
    WHERE ep.emp_id = e.emp_id
);

-- 方法2:使用IN
SELECT emp_name
FROM employees
WHERE emp_id IN (
    SELECT DISTINCT emp_id FROM emp_projects
);

-- 方法3:使用INNER JOIN(需要DISTINCT去重)
SELECT DISTINCT e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id;

-- 方法4:同时显示参与的项目数量
SELECT
    e.emp_name,
    COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name;

-- 方法5:使用窗口函数统计项目数
SELECT DISTINCT emp_name, project_count
FROM (
    SELECT
        e.emp_name,
        COUNT(ep.project_id) OVER (PARTITION BY e.emp_id) AS project_count
    FROM employees e
    INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
) AS with_count;

要点说明

  • EXISTS:半连接,找到第一条就停止,性能最好
  • IN:先计算子查询,再匹配,需要DISTINCT去重
  • JOIN:产生笛卡尔积,需要DISTINCT去重
  • "至少一个"意味着>=1,EXISTS和IN都可以

为什么JOIN需要DISTINCT?

假设数据:
employees: 张三(id=1), 李四(id=2)
emp_projects:
  emp_id=1, project_id=101
  emp_id=1, project_id=102
  emp_id=2, project_id=103

不用DISTINCT的结果:
张三  (来自project_id=101)
张三  (来自project_id=102)
李四  (来自project_id=103)

用DISTINCT的结果:
张三
李四

EXISTS vs IN 性能对比

-- EXISTS: 短路优化
-- 对每个员工,只要找到一条匹配的项目记录就停止
-- 如果员工参与10个项目,只需检查第1个

-- IN: 完全计算
-- 先计算所有参与项目的员工ID列表
-- 再检查每个员工是否在列表中

推荐:EXISTS(尤其是关联表数据量大时)

相反问题:查询没有参与项目的员工

-- NOT EXISTS
SELECT emp_name
FROM employees e
WHERE NOT EXISTS (
    SELECT 1 FROM emp_projects ep WHERE ep.emp_id = e.emp_id
);

-- NOT IN
SELECT emp_name
FROM employees
WHERE emp_id NOT IN (
    SELECT emp_id FROM emp_projects WHERE emp_id IS NOT NULL
);

-- LEFT JOIN + IS NULL
SELECT e.emp_name
FROM employees e
LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
WHERE ep.emp_id IS NULL;

扩展:查询参与超过3个项目的员工

SELECT e.emp_name
FROM employees e
WHERE (
    SELECT COUNT(*) FROM emp_projects ep WHERE ep.emp_id = e.emp_id
) > 3;

-- 或使用JOIN + GROUP BY + HAVING
SELECT e.emp_name
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(ep.project_id) > 3;

题目42:查询工资排名前10%的员工

💡 解题思路

这题考查百分位数计算

  1. 先计算总员工数
  2. 计算前10%的人数
  3. 按工资降序取前N名
  4. 或使用窗口函数PERCENT_RANK

🔍 关键提示

  • 计算10%人数:CEIL(COUNT(*) * 0.1)
  • 使用LIMIT:先排序再LIMIT
  • 窗口函数:PERCENT_RANK() OVER (ORDER BY salary DESC)
  • 向上取整:CEIL函数

📝 实现步骤

  1. 方法1:先统计总数,计算10%,再LIMIT
  2. 方法2:用子查询一次完成
  3. 方法3:使用窗口函数(MySQL 8.0+)
  4. 思考:10%如何向上取整?

✅ 验证方法

-- 先查看总员工数
SELECT COUNT(*) FROM employees;
-- 假设100人,10%就是10人

-- 查看第10名和第11名的工资
SELECT salary FROM employees ORDER BY salary DESC LIMIT 9, 2;

🚀 扩展思考

  • 如果总数不能被10整除怎么办?
  • PERCENT_RANK和NTILE有什么区别?
  • 如何查询工资中位数?
  • 如何查询每个部门前10%的员工?
💯 点击查看参考答案
-- 方法1:使用变量和LIMIT
SET @total = (SELECT COUNT(*) FROM employees);
SET @top_count = CEIL(@total * 0.1);

SELECT emp_name, salary
FROM employees
ORDER BY salary DESC
LIMIT @top_count;

-- 方法2:使用子查询一次完成
SELECT emp_name, salary
FROM employees
ORDER BY salary DESC
LIMIT (SELECT CEIL(COUNT(*) * 0.1) FROM employees);

-- 方法3:使用窗口函数PERCENT_RANK(MySQL 8.0+)
SELECT emp_name, salary, percentile
FROM (
    SELECT
        emp_name,
        salary,
        PERCENT_RANK() OVER (ORDER BY salary DESC) AS percentile
    FROM employees
) AS ranked
WHERE percentile <= 0.1;

-- 方法4:使用NTILE分桶(MySQL 8.0+)
SELECT emp_name, salary, bucket
FROM (
    SELECT
        emp_name,
        salary,
        NTILE(10) OVER (ORDER BY salary DESC) AS bucket
    FROM employees
) AS bucketed
WHERE bucket = 1;

-- 方法5:同时显示排名和百分位
SELECT
    emp_name,
    salary,
    rank_num,
    ROUND(rank_num * 100.0 / total, 2) AS percentile
FROM (
    SELECT
        emp_name,
        salary,
        ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank_num,
        COUNT(*) OVER () AS total
    FROM employees
) AS with_rank
WHERE rank_num <= CEIL(total * 0.1);

要点说明

  • CEIL函数:向上取整(100 * 0.1 = 10, CEIL(101 * 0.1) = 11)
  • PERCENT_RANK:计算百分位排名(0到1之间)
  • NTILE(n):将数据分成n个桶
  • 前10%可以用多种方法实现

PERCENT_RANK 详解

-- PERCENT_RANK() 返回 (rank - 1) / (total_rows - 1)
假设5个员工,工资降序:
工资    PERCENT_RANK
10000   (1-1)/(5-1) = 0      -- 0%
9000    (2-1)/(5-1) = 0.25   -- 25%
8000    (3-1)/(5-1) = 0.5    -- 50%
7000    (4-1)/(5-1) = 0.75   -- 75%
6000    (5-1)/(5-1) = 1.0    -- 100%

NTILE 详解

-- NTILE(10) 将数据平均分成10组
-- 第1组就是前10%

NTILE(10) OVER (ORDER BY salary DESC)
-- 100个员工分成10组,每组10人
-- bucket=1 的就是前10名(前10%)

处理小数问题

-- 假设103个员工
-- 前10% = 103 * 0.1 = 10.3

CEIL(10.3) = 11  -- 向上取整,取11人
FLOOR(10.3) = 10 -- 向下取整,取10人
ROUND(10.3) = 10 -- 四舍五入,取10人

推荐用CEIL,确保至少包含10%

扩展:每个部门前10%

SELECT dept_id, emp_name, salary, percentile
FROM (
    SELECT
        dept_id,
        emp_name,
        salary,
        PERCENT_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS percentile
    FROM employees
) AS ranked
WHERE percentile <= 0.1;

中位数计算

-- MySQL 8.0+
SELECT
    AVG(salary) AS median_salary
FROM (
    SELECT
        salary,
        ROW_NUMBER() OVER (ORDER BY salary) AS rn,
        COUNT(*) OVER () AS total
    FROM employees
) AS numbered
WHERE rn IN (FLOOR((total + 1) / 2), CEIL((total + 1) / 2));

题目43:查询每个部门工资第二高的员工

💡 解题思路

这是分组排名的经典问题:

  1. 按部门分组
  2. 在每组内按工资排名
  3. 取每组的第2名
  4. 需要处理并列情况

🔍 关键提示

  • 窗口函数:RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC)
  • 或使用:DENSE_RANK()ROW_NUMBER()
  • 子查询方式较复杂
  • 注意:第二高可能有并列

📝 实现步骤

  1. 理解题意:每个部门内部排名第2
  2. 使用窗口函数(推荐)或相关子查询
  3. 选择合适的排名函数(RANK vs DENSE_RANK vs ROW_NUMBER)
  4. 思考:如果第2名有并列怎么办?

✅ 验证方法

-- 先查看每个部门的工资排名
SELECT
    dept_id,
    emp_name,
    salary,
    RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
FROM employees
ORDER BY dept_id, rk;

🚀 扩展思考

  • RANK、DENSE_RANK、ROW_NUMBER的区别?
  • 如果部门只有1个人怎么办?
  • 如何查询第二低的工资?
  • 相关子查询和窗口函数哪个更好?
💯 点击查看参考答案
-- 方法1:使用DENSE_RANK(推荐,处理并列工资)
SELECT emp_name, dept_id, salary
FROM (
    SELECT
        emp_name,
        dept_id,
        salary,
        DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
    FROM employees
) AS ranked
WHERE rk = 2;

-- 方法2:使用RANK(并列时可能跳号)
SELECT emp_name, dept_id, salary
FROM (
    SELECT
        emp_name,
        dept_id,
        salary,
        RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
    FROM employees
) AS ranked
WHERE rk = 2;

-- 方法3:使用ROW_NUMBER(不处理并列,只取一个)
SELECT emp_name, dept_id, salary
FROM (
    SELECT
        emp_name,
        dept_id,
        salary,
        ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
    FROM employees
) AS ranked
WHERE rn = 2;

-- 方法4:使用相关子查询(复杂但通用)
SELECT e1.emp_name, e1.dept_id, e1.salary
FROM employees e1
WHERE (
    SELECT COUNT(DISTINCT e2.salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id
    AND e2.salary > e1.salary
) = 1;

-- 方法5:使用子查询 + LIMIT
SELECT e1.emp_name, e1.dept_id, e1.salary
FROM employees e1
WHERE e1.salary = (
    SELECT DISTINCT salary
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id
    ORDER BY salary DESC
    LIMIT 1 OFFSET 1
);

要点说明

  • DENSE_RANK:密集排名,1,2,2,3(推荐用于找第N高)
  • RANK:跳号排名,1,2,2,4
  • ROW_NUMBER:连续排名,1,2,3,4(不处理并列)
  • 窗口函数比相关子查询性能好得多

排名函数对比

假设某部门工资:10000, 9000, 9000, 8000

DENSE_RANK():  1, 2, 2, 3
RANK():        1, 2, 2, 4
ROW_NUMBER():  1, 2, 3, 4

查询第二高(9000):
- DENSE_RANK WHERE rk=2: 返回两个9000 ✅
- RANK WHERE rk=2: 返回两个9000 ✅
- ROW_NUMBER WHERE rn=2: 只返回一个9000 ⚠️

DENSE_RANK vs RANK 选择

-- 部门工资:12000, 10000, 10000, 8000

-- 用RANK找第2高:
-- rk=1: 12000
-- rk=2: 10000, 10000
-- rk=4: 8000
WHERE rk = 2 ✅ 返回10000(正确)

-- 用RANK找第3高:
-- WHERE rk = 3 ❌ 没有结果!(因为跳到rk=4了)

-- 用DENSE_RANK找第3高:
-- rk=1: 12000
-- rk=2: 10000, 10000
-- rk=3: 8000
WHERE rk = 3 ✅ 返回8000(正确)

结论:查找第N高工资用DENSE_RANK更准确

相关子查询详解

-- 逻辑:找出有多少个不同的工资比我高
WHERE (
    SELECT COUNT(DISTINCT e2.salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id  -- 同部门
    AND e2.salary > e1.salary      -- 比我高
) = 1  -- 恰好1个工资比我高,说明我是第2高

-- 示例:
-- 部门1工资:10000, 9000, 8000
-- 对于9000这条记录:
--   比9000高的工资:10000(1个)
--   COUNT = 1,满足条件,9000是第2高 ✅

处理特殊情况

-- 如果部门没有第2高(只有1个人或所有人工资相同)
-- 窗口函数会返回空结果(WHERE rk = 2没有匹配)

-- 包含部门信息,显示哪些部门没有第2高
SELECT
    d.dept_name,
    e.emp_name,
    e.salary
FROM departments d
LEFT JOIN (
    SELECT emp_name, dept_id, salary
    FROM (
        SELECT
            emp_name, dept_id, salary,
            DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
        FROM employees
    ) ranked
    WHERE rk = 2
) e ON d.dept_id = e.dept_id;

扩展:第二低工资

-- 只需将DESC改为ASC
SELECT emp_name, dept_id, salary
FROM (
    SELECT
        emp_name, dept_id, salary,
        DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary ASC) AS rk
    FROM employees
) ranked
WHERE rk = 2;

题目44:查询比所有'销售部'员工工资都高的员工

💡 解题思路

这题考查ALL关键字的使用:

  1. 先找出销售部的最高工资
  2. 查询工资高于这个最高值的员工
  3. 或使用 > ALL 子查询
  4. 理解"所有"的含义

🔍 关键提示

  • ALL关键字:WHERE salary > ALL (SELECT salary FROM employees WHERE dept = '销售部')
  • 等价于:WHERE salary > (SELECT MAX(salary) FROM employees WHERE dept = '销售部')
  • 注意NULL值问题
  • 可以用JOIN实现

📝 实现步骤

  1. 理解题意:比销售部所有人都高,即比最高的还高
  2. 方法1:找出销售部最高工资,再比较
  3. 方法2:使用ALL关键字
  4. 思考:如果销售部没有员工怎么办?

✅ 验证方法

-- 先查看销售部的最高工资
SELECT MAX(salary) FROM employees WHERE department = '销售部';

-- 验证结果中的员工工资是否都大于这个值

🚀 扩展思考

  • ALL和ANY/SOME有什么区别?
  • 如果要查询比任意一个销售部员工工资高的呢?
  • 如果销售部为空,ALL会返回什么?
  • 能否用NOT EXISTS实现?
💯 点击查看参考答案
-- 方法1:使用ALL(推荐,语义清晰)
SELECT emp_name, salary
FROM employees
WHERE salary > ALL (
    SELECT salary
    FROM employees
    WHERE department = '销售部'
);

-- 方法2:使用MAX(等价,更常用)
SELECT emp_name, salary
FROM employees
WHERE salary > (
    SELECT MAX(salary)
    FROM employees
    WHERE department = '销售部'
);

-- 方法3:使用NOT EXISTS(逻辑等价)
SELECT e1.emp_name, e1.salary
FROM employees e1
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e2
    WHERE e2.department = '销售部'
    AND e2.salary >= e1.salary
);

-- 方法4:使用JOIN
SELECT e1.emp_name, e1.salary
FROM employees e1
CROSS JOIN (
    SELECT MAX(salary) AS max_sales_salary
    FROM employees
    WHERE department = '销售部'
) sales_max
WHERE e1.salary > sales_max.max_sales_salary;

-- 方法5:排除销售部,且工资高于销售部最高工资
SELECT emp_name, salary, department
FROM employees
WHERE department != '销售部'
AND salary > (
    SELECT MAX(salary)
    FROM employees
    WHERE department = '销售部'
);

要点说明

  • > ALL:大于所有值,等价于大于最大值
  • < ALL:小于所有值,等价于小于最小值
  • > ANY/SOME:大于任意一个值,等价于大于最小值
  • ALL/ANY主要用于提高SQL可读性

ALL vs ANY 对比

-- 假设销售部工资:5000, 7000, 9000

-- > ALL: 大于所有值
WHERE salary > ALL (...)
等价于 WHERE salary > 9000  (大于最大值)

-- > ANY: 大于任意一个值
WHERE salary > ANY (...)
等价于 WHERE salary > 5000  (大于最小值)

-- < ALL: 小于所有值
WHERE salary < ALL (...)
等价于 WHERE salary < 5000  (小于最小值)

-- < ANY: 小于任意一个值
WHERE salary < ANY (...)
等价于 WHERE salary < 9000  (小于最大值)

等价关系总结

> ALL  ≈  > MAX
< ALL  ≈  < MIN
> ANY  ≈  > MIN
< ANY  ≈  < MAX
= ANY  ≈  IN
<> ALL ≈  NOT IN

NULL值处理

-- 如果子查询包含NULL
WHERE salary > ALL (SELECT salary FROM ...)
-- salary > NULL 结果是NULL(不是TRUE也不是FALSE)
-- 导致整个条件为NULL,该行被排除

-- 安全写法:过滤NULL
WHERE salary > ALL (
    SELECT salary FROM employees
    WHERE department = '销售部'
    AND salary IS NOT NULL
)

特殊情况:子查询为空

-- 如果销售部没有员工
SELECT salary FROM employees WHERE department = '销售部'
-- 返回空集

WHERE salary > ALL (空集)
-- 永远为TRUE!所有员工都满足

WHERE salary > (SELECT MAX(salary) FROM ... WHERE department = '销售部')
-- 子查询返回NULL
-- salary > NULL 为NULL,所有行被排除

推荐用MAX方式,逻辑更清晰

实际应用示例

-- 查询工资比所有技术部员工都低的员工
WHERE salary < ALL (
    SELECT salary FROM employees WHERE department = '技术部'
)

-- 查询工资比任意一个管理层员工高的员工
WHERE salary > ANY (
    SELECT salary FROM employees WHERE position = '经理'
)

-- 查询部门在北京或上海的员工
WHERE dept_id = ANY (
    SELECT dept_id FROM departments WHERE location IN ('北京', '上海')
)
-- 等价于
WHERE dept_id IN (
    SELECT dept_id FROM departments WHERE location IN ('北京', '上海')
)

题目45:查询参与项目数量超过平均值的员工

💡 解题思路

这题综合了子查询、分组、聚合

  1. 先计算每个员工的项目数
  2. 再计算平均项目数
  3. 筛选超过平均值的员工
  4. 可以用多种方法实现

🔍 关键提示

  • 步骤1:计算平均项目数 SELECT AVG(cnt) FROM (每个员工的项目数)
  • 步骤2:计算每个员工的项目数并比较
  • 需要两层子查询或HAVING
  • 注意:没参与项目的员工不计入

📝 实现步骤

  1. 内层:统计每个员工的项目数
  2. 中层:计算平均项目数
  3. 外层:筛选超过平均的员工
  4. 思考:如何在一个查询中完成?

✅ 验证方法

-- 先查看每个员工的项目数
SELECT emp_id, COUNT(*) AS cnt
FROM emp_projects
GROUP BY emp_id;

-- 计算平均项目数
SELECT AVG(cnt) FROM (
    SELECT COUNT(*) AS cnt FROM emp_projects GROUP BY emp_id
) AS counts;

🚀 扩展思考

  • 能否用窗口函数简化?
  • 如何同时显示员工的项目数和平均值?
  • 如何查询项目数在平均值以下的员工?
  • 没参与项目的员工是否应该算入平均值?
💯 点击查看参考答案
-- 方法1:使用子查询(标准方法)
SELECT e.emp_name, COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
HAVING COUNT(ep.project_id) > (
    SELECT AVG(cnt)
    FROM (
        SELECT COUNT(*) AS cnt
        FROM emp_projects
        GROUP BY emp_id
    ) AS avg_projects
);

-- 方法2:使用HAVING和子查询
SELECT e.emp_name, COUNT(ep.project_id) AS project_count
FROM employees e
INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
GROUP BY e.emp_id, e.emp_name
HAVING project_count > (
    SELECT AVG(project_count)
    FROM (
        SELECT emp_id, COUNT(*) AS project_count
        FROM emp_projects
        GROUP BY emp_id
    ) AS emp_project_counts
);

-- 方法3:使用窗口函数(MySQL 8.0+,更优雅)
SELECT emp_name, project_count
FROM (
    SELECT
        e.emp_name,
        COUNT(ep.project_id) AS project_count,
        AVG(COUNT(ep.project_id)) OVER () AS avg_project_count
    FROM employees e
    INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
    GROUP BY e.emp_id, e.emp_name
) AS with_avg
WHERE project_count > avg_project_count;

-- 方法4:先计算平均值,再筛选
WITH project_stats AS (
    SELECT
        emp_id,
        COUNT(*) AS project_count
    FROM emp_projects
    GROUP BY emp_id
),
avg_count AS (
    SELECT AVG(project_count) AS avg_projects
    FROM project_stats
)
SELECT e.emp_name, ps.project_count
FROM employees e
INNER JOIN project_stats ps ON e.emp_id = ps.emp_id
CROSS JOIN avg_count ac
WHERE ps.project_count > ac.avg_projects;

-- 方法5:同时显示项目数、平均值和差值
SELECT
    emp_name,
    project_count,
    avg_count,
    project_count - avg_count AS above_avg
FROM (
    SELECT
        e.emp_name,
        COUNT(ep.project_id) AS project_count,
        (
            SELECT AVG(cnt)
            FROM (SELECT COUNT(*) AS cnt FROM emp_projects GROUP BY emp_id) AS t
        ) AS avg_count
    FROM employees e
    INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
    GROUP BY e.emp_id, e.emp_name
) AS with_avg
WHERE project_count > avg_count;

要点说明

  • 两层子查询:内层统计每人项目数,外层计算平均
  • HAVING:用于过滤分组后的聚合结果
  • 窗口函数:AVG() OVER() 在每行都显示平均值
  • 题目隐含:只统计参与了项目的员工

执行逻辑分解

步骤1:统计每个员工的项目数
emp_id  project_count
1       5
2       3
3       8
4       2

步骤2:计算平均项目数
AVG(5, 3, 8, 2) = 4.5

步骤3:筛选 > 4.5 的员工
emp_id  project_count
1       5
3       8

窗口函数详解

AVG(COUNT(ep.project_id)) OVER ()
-- 难点:聚合函数嵌套窗口函数
-- 执行顺序:
-- 1. GROUP BY 先执行,每组计算 COUNT(ep.project_id)
-- 2. AVG() OVER() 对所有分组的COUNT结果求平均
-- 3. 每行都显示这个平均值

示例结果:
emp_name  project_count  avg_project_count
张三      5              4.5
李四      3              4.5
王五      8              4.5
赵六      2              4.5

然后WHERE过滤 project_count > avg_project_count

WITH CTE 方式(推荐,可读性最好)

-- Common Table Expression,公用表表达式
WITH project_stats AS (
    -- 第一步:统计每人项目数
    SELECT emp_id, COUNT(*) AS project_count
    FROM emp_projects
    GROUP BY emp_id
),
avg_count AS (
    -- 第二步:计算平均值
    SELECT AVG(project_count) AS avg_projects
    FROM project_stats
)
-- 第三步:筛选和展示
SELECT
    e.emp_name,
    ps.project_count,
    ac.avg_projects
FROM employees e
INNER JOIN project_stats ps ON e.emp_id = ps.emp_id
CROSS JOIN avg_count ac
WHERE ps.project_count > ac.avg_projects;

是否包括0项目的员工?

-- 题目:"参与项目数量超过平均值"
-- 理解1:只统计参与了项目的员工的平均值
SELECT AVG(cnt) FROM (
    SELECT COUNT(*) AS cnt FROM emp_projects GROUP BY emp_id
)
-- 如果10个员工,5个参与项目(数量2,3,4,5,6),平均=4

-- 理解2:统计所有员工,没参与的算0
SELECT AVG(cnt) FROM (
    SELECT e.emp_id, COUNT(ep.project_id) AS cnt
    FROM employees e
    LEFT JOIN emp_projects ep ON e.emp_id = ep.emp_id
    GROUP BY e.emp_id
)
-- 10个员工,5个参与(2,3,4,5,6),5个不参与(0,0,0,0,0),平均=2

通常题目指理解1(只统计参与的)

扩展:查询项目数前20%的员工

SELECT emp_name, project_count, percentile
FROM (
    SELECT
        e.emp_name,
        COUNT(ep.project_id) AS project_count,
        PERCENT_RANK() OVER (ORDER BY COUNT(ep.project_id) DESC) AS percentile
    FROM employees e
    INNER JOIN emp_projects ep ON e.emp_id = ep.emp_id
    GROUP BY e.emp_id, e.emp_name
) AS ranked
WHERE percentile <= 0.2;

学习进度建议

本学习指南已完成第一关到第五关的详细引导,共45道题目的完整解析。

已完成关卡

✅ 第一关:基础入门(题目1-5)

  • 重点掌握:建表、插入、查询、删除表的基本操作
  • 多练习:至少独立完成3次以上
  • 核心知识:CREATE TABLE、INSERT、SELECT、DROP TABLE

✅ 第二关:单表查询(题目6-15)

  • 重点掌握:WHERE、ORDER BY、LIMIT的组合使用
  • 对比练习:每道题尝试2-3种不同写法
  • 核心知识:WHERE条件、模糊查询LIKE、排序、分页

✅ 第三关:多表联结(题目16-25)

  • 重点掌握:INNER JOIN、LEFT JOIN、自连接
  • 难点突破:理解JOIN的执行逻辑和不同JOIN类型的区别
  • 核心知识:INNER JOIN、LEFT JOIN、自连接、多表关联

✅ 第四关:聚合函数与分组(题目26-35)

  • 重点掌握:GROUP BY、HAVING、聚合函数(COUNT、SUM、AVG、MAX、MIN)
  • 难点突破:WHERE与HAVING的区别、复杂分组统计
  • 核心知识:分组统计、日期函数、多表JOIN+分组

✅ 第五关:子查询(题目36-45)

  • 重点掌握:标量子查询、相关子查询、EXISTS、IN、NOT IN
  • 难点突破:子查询性能优化、NULL陷阱、ALL/ANY关键字
  • 核心知识:子查询类型、窗口函数、百分位计算、ALL/ANY

学习建议

  1. 循序渐进学习

    • 按关卡顺序学习,不要跳跃
    • 每关的练习题至少完成80%再进入下一关
    • 遇到困难回头复习前面的基础内容
  2. 实践为主

    • 看完解题思路后先自己尝试编写SQL
    • 遇到困难再查看关键提示
    • 最后对比参考答案,学习多种解法
  3. 总结归纳

    • 每完成一关做一次总结
    • 整理易错点和常用技巧
    • 建立自己的SQL知识体系
  4. 扩展学习

    • 完成扩展思考题目
    • 尝试用不同方法实现同一需求
    • 对比不同方法的性能和可读性

第六关:高级查询技巧 - 学习指南

题目46:查询本月入职的员工和上月入职的员工,合并显示

💡 解题思路

这道题考查UNION合并查询日期函数的使用。需要:

  1. 分别查询本月和上月入职的员工
  2. 使用UNION或UNION ALL合并两个查询结果
  3. 理解UNION和UNION ALL的区别

🔍 关键提示

  • 使用DATE_FORMAT()格式化日期进行比较
  • 使用YEAR()MONTH()函数提取年月
  • UNION会去重,UNION ALL不去重
  • 可以添加标识字段区分是本月还是上月入职

📝 实现步骤

  1. 获取当前年月:使用YEAR(CURDATE())MONTH(CURDATE())
  2. 编写本月入职员工查询
  3. 编写上月入职员工查询(注意跨年情况)
  4. 使用UNION ALL合并(因为本月和上月不会重复)
  5. 可选:添加一个字段标识入职月份

✅ 验证方法

-- 插入测试数据
INSERT INTO employees (name, hire_date) VALUES
('本月员工1', CURDATE()),
('上月员工1', DATE_SUB(CURDATE(), INTERVAL 1 MONTH));

-- 执行查询,检查是否同时显示本月和上月员工

🚀 扩展思考

  • UNION和UNION ALL的性能差异是什么?
  • 如何查询最近3个月入职的员工?
  • 如果要按月份分组统计,应该怎么写?
💯 点击查看参考答案

方法1:使用UNION ALL(推荐)

SELECT name, hire_date, '本月入职' AS period
FROM employees
WHERE YEAR(hire_date) = YEAR(CURDATE())
  AND MONTH(hire_date) = MONTH(CURDATE())
UNION ALL
SELECT name, hire_date, '上月入职' AS period
FROM employees
WHERE YEAR(hire_date) = YEAR(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))
  AND MONTH(hire_date) = MONTH(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))
ORDER BY hire_date DESC;

方法2:使用日期范围(性能更好)

SELECT name, hire_date,
       CASE
           WHEN hire_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01') THEN '本月入职'
           ELSE '上月入职'
       END AS period
FROM employees
WHERE hire_date >= DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
  AND hire_date < DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
ORDER BY hire_date DESC;

方法3:使用DATE_FORMAT

SELECT name, hire_date, '本月入职' AS period
FROM employees
WHERE DATE_FORMAT(hire_date, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
UNION ALL
SELECT name, hire_date, '上月入职' AS period
FROM employees
WHERE DATE_FORMAT(hire_date, '%Y-%m') = DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m');

要点说明

  • UNION ALL vs UNION:这里使用UNION ALL因为本月和上月的数据不会重复,性能更好
  • 日期函数YEAR()MONTH()DATE_FORMAT()都可以提取日期部分
  • 性能考虑:方法2使用范围查询,可以利用索引,性能最好
  • 可读性:添加period字段清楚标识数据来源

性能对比

  • 方法1:在索引字段上使用函数,可能导致索引失效
  • 方法2:使用范围查询,能够充分利用索引(推荐)
  • 方法3:使用DATE_FORMAT,代码简洁但可能索引失效

题目47:根据工资将员工分为'高薪'、'中薪'、'低薪'三个等级

💡 解题思路

这道题考查CASE WHEN条件表达式的使用。需要:

  1. 定义工资等级的划分标准
  2. 使用CASE WHEN进行条件判断
  3. 为结果列起一个有意义的别名

🔍 关键提示

  • 使用CASE WHEN ... THEN ... ELSE ... END语法
  • 条件按从具体到一般的顺序编写
  • 记得给CASE表达式起别名
  • 可以使用简单CASE或搜索CASE

📝 实现步骤

  1. 确定等级划分:高薪(>10000)、中薪(5000-10000)、低薪(<5000)
  2. 编写SELECT语句选择基本字段
  3. 添加CASE WHEN表达式判断工资等级
  4. 测试边界值(5000和10000)

✅ 验证方法

-- 验证不同工资范围的员工是否正确分类
SELECT salary, salary_level
FROM (你的查询)
WHERE salary IN (4999, 5000, 10000, 10001);

🚀 扩展思考

  • 如果等级划分标准改变,如何快速调整?
  • 如何统计每个等级的人数和平均工资?
  • CASE WHEN可以嵌套吗?什么场景下需要嵌套?
💯 点击查看参考答案

方法1:搜索CASE(推荐,更灵活)

SELECT
    name,
    salary,
    CASE
        WHEN salary > 10000 THEN '高薪'
        WHEN salary >= 5000 THEN '中薪'
        ELSE '低薪'
    END AS salary_level
FROM employees
ORDER BY salary DESC;

方法2:带统计的版本

SELECT
    name,
    salary,
    CASE
        WHEN salary > 10000 THEN '高薪'
        WHEN salary >= 5000 AND salary <= 10000 THEN '中薪'
        WHEN salary < 5000 THEN '低薪'
        ELSE '未知'
    END AS salary_level
FROM employees
ORDER BY salary DESC;

方法3:按等级分组统计

SELECT
    CASE
        WHEN salary > 10000 THEN '高薪'
        WHEN salary >= 5000 THEN '中薪'
        ELSE '低薪'
    END AS salary_level,
    COUNT(*) AS emp_count,
    AVG(salary) AS avg_salary,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary
FROM employees
GROUP BY salary_level
ORDER BY avg_salary DESC;

方法4:更细致的等级划分

SELECT
    name,
    salary,
    CASE
        WHEN salary >= 20000 THEN '特高薪'
        WHEN salary >= 15000 THEN '高薪'
        WHEN salary >= 10000 THEN '中高薪'
        WHEN salary >= 5000 THEN '中薪'
        WHEN salary >= 3000 THEN '低薪'
        ELSE '最低薪'
    END AS salary_level,
    -- 计算与中位数的比例
    ROUND(salary / (SELECT AVG(salary) FROM employees) * 100, 2) AS salary_percentage
FROM employees
ORDER BY salary DESC;

要点说明

  • CASE WHEN顺序:从高到低判断,匹配第一个为真的条件就返回
  • 边界处理:5000和10000要明确归属哪个等级(这里5000属于中薪)
  • 简化条件:第二个条件可以省略AND salary <= 10000,因为前面已经判断了> 10000
  • ELSE子句:建议加上ELSE处理意外情况(如NULL值)
  • 性能:CASE WHEN在SELECT中,不影响WHERE索引的使用

实际应用扩展

-- 结合其他条件:查询技术部的高薪员工
SELECT name, dept, salary
FROM (
    SELECT
        name,
        dept,
        salary,
        CASE
            WHEN salary > 10000 THEN '高薪'
            WHEN salary >= 5000 THEN '中薪'
            ELSE '低薪'
        END AS salary_level
    FROM employees
) AS emp_levels
WHERE dept = '技术部' AND salary_level = '高薪';

题目48:查询每个部门工资排名前3的员工

💡 解题思路

这道题考查窗口函数(MySQL 8.0+)的使用。需要:

  1. 理解RANK()、DENSE_RANK()、ROW_NUMBER()的区别
  2. 使用PARTITION BY按部门分组
  3. 使用ORDER BY定义排名规则
  4. 筛选排名前3的记录

🔍 关键提示

  • 窗口函数:RANK() OVER (PARTITION BY ... ORDER BY ...)
  • PARTITION BY相当于分组,但不会减少行数
  • 需要用子查询或CTE筛选排名结果
  • 注意处理工资相同的情况

📝 实现步骤

  1. 使用ROW_NUMBER()或RANK()为每个部门的员工排名
  2. 按部门分组(PARTITION BY dept_id)
  3. 按工资降序排序(ORDER BY salary DESC)
  4. 用子查询筛选排名<=3的记录

✅ 验证方法

-- 检查每个部门是否都有最多3条记录
SELECT dept_id, COUNT(*)
FROM (你的查询结果)
GROUP BY dept_id;

🚀 扩展思考

  • RANK()、DENSE_RANK()、ROW_NUMBER()在工资相同时结果有何不同?
  • 如果不用窗口函数,如何实现(MySQL 5.7)?
  • 如何查询每个部门工资排名第2的员工?
💯 点击查看参考答案

方法1:使用ROW_NUMBER()(每个排名唯一)

SELECT dept_id, name, salary, rank_num
FROM (
    SELECT
        dept_id,
        name,
        salary,
        ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank_num
    FROM employees
) AS ranked_emp
WHERE rank_num <= 3
ORDER BY dept_id, rank_num;

方法2:使用RANK()(工资相同排名相同,会跳号)

SELECT dept_id, name, salary, salary_rank
FROM (
    SELECT
        dept_id,
        name,
        salary,
        RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS salary_rank
    FROM employees
) AS ranked_emp
WHERE salary_rank <= 3
ORDER BY dept_id, salary_rank;

方法3:使用DENSE_RANK()(工资相同排名相同,不跳号)

SELECT dept_id, name, salary, dense_rank
FROM (
    SELECT
        dept_id,
        name,
        salary,
        DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS dense_rank
    FROM employees
) AS ranked_emp
WHERE dense_rank <= 3
ORDER BY dept_id, dense_rank;

方法4:不使用窗口函数(MySQL 5.7兼容)

SELECT e1.dept_id, e1.name, e1.salary
FROM employees e1
WHERE (
    SELECT COUNT(DISTINCT e2.salary)
    FROM employees e2
    WHERE e2.dept_id = e1.dept_id
      AND e2.salary >= e1.salary
) <= 3
ORDER BY e1.dept_id, e1.salary DESC;

方法5:使用变量(MySQL 5.7兼容,但不推荐)

SELECT dept_id, name, salary, rank_num
FROM (
    SELECT
        dept_id,
        name,
        salary,
        @rank := IF(@dept = dept_id, @rank + 1, 1) AS rank_num,
        @dept := dept_id
    FROM employees, (SELECT @rank := 0, @dept := NULL) AS vars
    ORDER BY dept_id, salary DESC
) AS ranked
WHERE rank_num <= 3;

要点说明

三种排名函数的区别(假设工资:10000, 9000, 9000, 8000):

  • ROW_NUMBER(): 1, 2, 3, 4(连续,工资相同也不同排名)
  • RANK(): 1, 2, 2, 4(跳号,两个第2名后面是第4名)
  • DENSE_RANK(): 1, 2, 2, 3(不跳号,两个第2名后面是第3名)

选择建议

  • 需要唯一排名:用ROW_NUMBER()
  • 工资相同应同排名:用RANK()DENSE_RANK()
  • 需要连续排名(不跳号):用DENSE_RANK()

性能对比

  • 窗口函数(方法1-3):性能最好,代码最简洁
  • 子查询方式(方法4):性能较差,但兼容性好
  • 变量方式(方法5):不推荐,MySQL 8.0后可能不稳定

扩展示例:同时显示部门名称

SELECT
    d.dept_name,
    e.name AS emp_name,
    e.salary,
    e.salary_rank
FROM (
    SELECT
        dept_id,
        name,
        salary,
        DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS salary_rank
    FROM employees
) AS e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary_rank <= 3
ORDER BY d.dept_name, e.salary_rank;

题目49:计算每个员工的工作年限(到当前日期)

💡 解题思路

这道题考查日期计算函数的使用。需要:

  1. 计算入职日期到当前日期的时间差
  2. 选择合适的日期函数
  3. 处理不同的时间单位(年、月、天)

🔍 关键提示

  • TIMESTAMPDIFF(unit, start_date, end_date):计算时间差
  • DATEDIFF(date1, date2):计算天数差
  • YEAR(CURDATE()) - YEAR(hire_date):简单计算年份差
  • 注意闰年和不同月份天数的影响

📝 实现步骤

  1. 使用CURDATE()获取当前日期
  2. 使用TIMESTAMPDIFF计算年份差
  3. 可选:同时显示月数、天数
  4. 可选:格式化显示为"X年Y月"

✅ 验证方法

-- 插入已知日期的测试数据
INSERT INTO employees (name, hire_date) VALUES
('测试员工', '2020-01-01');
-- 检查计算结果是否正确

🚀 扩展思考

  • 如何计算精确到月的工作年限(如2年3个月)?
  • 如何找出工龄最长的前10名员工?
  • 如何统计各工龄段的员工数量?
💯 点击查看参考答案

方法1:使用TIMESTAMPDIFF(推荐)

SELECT
    name,
    hire_date,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS work_years,
    TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) AS work_months,
    TIMESTAMPDIFF(DAY, hire_date, CURDATE()) AS work_days
FROM employees
ORDER BY work_years DESC, work_months DESC;

方法2:精确计算年和月

SELECT
    name,
    hire_date,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years,
    TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) % 12 AS months,
    CONCAT(
        TIMESTAMPDIFF(YEAR, hire_date, CURDATE()), '年',
        TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) % 12, '个月'
    ) AS work_duration
FROM employees
ORDER BY hire_date;

方法3:使用DATEDIFF计算天数再转换

SELECT
    name,
    hire_date,
    DATEDIFF(CURDATE(), hire_date) AS total_days,
    FLOOR(DATEDIFF(CURDATE(), hire_date) / 365) AS work_years,
    ROUND(DATEDIFF(CURDATE(), hire_date) / 365, 2) AS work_years_decimal
FROM employees
ORDER BY total_days DESC;

方法4:简单年份差(不够精确)

SELECT
    name,
    hire_date,
    YEAR(CURDATE()) - YEAR(hire_date) AS year_diff,
    -- 更精确的计算:考虑月和日
    YEAR(CURDATE()) - YEAR(hire_date) -
    (DATE_FORMAT(CURDATE(), '%m%d') < DATE_FORMAT(hire_date, '%m%d')) AS work_years
FROM employees;

方法5:工龄分段统计

SELECT
    CASE
        WHEN work_years >= 10 THEN '10年以上'
        WHEN work_years >= 5 THEN '5-10年'
        WHEN work_years >= 3 THEN '3-5年'
        WHEN work_years >= 1 THEN '1-3年'
        ELSE '1年以内'
    END AS seniority_level,
    COUNT(*) AS emp_count,
    GROUP_CONCAT(name ORDER BY hire_date) AS employees
FROM (
    SELECT
        name,
        hire_date,
        TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS work_years
    FROM employees
) AS emp_seniority
GROUP BY seniority_level
ORDER BY MIN(work_years) DESC;

要点说明

  • TIMESTAMPDIFF:最精确的方法,可以指定单位(YEAR、MONTH、DAY、HOUR等)
  • DATEDIFF:只能计算天数差,需要手动转换为年
  • 简单年份差:不考虑具体月日,可能不准确
  • CURDATE() vs NOW():CURDATE()返回日期,NOW()返回日期时间

不同方法的精确度

-- 假设今天是2024-03-15,员工2023-06-01入职
-- TIMESTAMPDIFF(YEAR, ...)  → 0年(不满1年)
-- YEAR(...) - YEAR(...)     → 1年(不准确)
-- DATEDIFF(...) / 365       → 0.77年(约9个月)

实际应用扩展

-- 计算工龄工资(每年增加200元)
SELECT
    name,
    salary AS base_salary,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS work_years,
    salary + (TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) * 200) AS total_salary
FROM employees
ORDER BY total_salary DESC;

-- 查询即将满整年工龄的员工(还有30天内)
SELECT
    name,
    hire_date,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS current_years,
    DATE_ADD(hire_date, INTERVAL (TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) + 1) YEAR) AS next_anniversary,
    DATEDIFF(
        DATE_ADD(hire_date, INTERVAL (TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) + 1) YEAR),
        CURDATE()
    ) AS days_to_anniversary
FROM employees
WHERE DATEDIFF(
    DATE_ADD(hire_date, INTERVAL (TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) + 1) YEAR),
    CURDATE()
) <= 30
ORDER BY days_to_anniversary;

题目50:将员工姓名和部门名称连接成一个字段显示

💡 解题思路

这道题考查字符串函数多表联结的组合使用。需要:

  1. 使用字符串连接函数拼接多个字段
  2. JOIN关联员工表和部门表
  3. 格式化输出,增加可读性

🔍 关键提示

  • CONCAT(str1, str2, ...):连接字符串
  • CONCAT_WS(separator, str1, str2, ...):用分隔符连接
  • 需要JOIN部门表获取部门名称
  • 注意NULL值的处理

📝 实现步骤

  1. JOIN员工表和部门表
  2. 使用CONCAT或CONCAT_WS连接姓名和部门名
  3. 添加适当的分隔符或格式化文本
  4. 起一个有意义的别名

✅ 验证方法

-- 检查输出格式是否正确
-- 检查是否有NULL值影响连接结果

🚀 扩展思考

  • CONCAT和CONCAT_WS有什么区别?
  • 如果某个字段是NULL,CONCAT的结果是什么?
  • 如何连接多个字段并添加不同的分隔符?
💯 点击查看参考答案

方法1:使用CONCAT

SELECT
    e.emp_id,
    CONCAT(e.name, ' - ', d.dept_name) AS employee_info
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_id;

方法2:使用CONCAT_WS(更方便)

SELECT
    e.emp_id,
    CONCAT_WS(' - ', e.name, d.dept_name) AS employee_info
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_id;

方法3:更详细的格式

SELECT
    e.emp_id,
    CONCAT(e.name, '(', IFNULL(d.dept_name, '未分配部门'), ')') AS employee_info
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_id;

方法4:多字段连接

SELECT
    e.emp_id,
    CONCAT_WS(' | ',
        e.name,
        d.dept_name,
        CONCAT('工资:', e.salary),
        CONCAT('入职:', DATE_FORMAT(e.hire_date, '%Y年%m月'))
    ) AS full_info
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_id;

方法5:使用COALESCE处理NULL

SELECT
    e.emp_id,
    CONCAT(
        e.name,
        ' - ',
        COALESCE(d.dept_name, '未分配')
    ) AS employee_info,
    -- 或者使用IFNULL
    CONCAT(
        e.name,
        ' - ',
        IFNULL(d.dept_name, '未分配')
    ) AS employee_info2
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id
ORDER BY e.emp_id;

要点说明

CONCAT vs CONCAT_WS

-- CONCAT:任意参数为NULL,结果为NULL
SELECT CONCAT('张三', NULL, '技术部');  -- 结果:NULL

-- CONCAT_WS:会忽略NULL值,只用分隔符连接非NULL值
SELECT CONCAT_WS(' - ', '张三', NULL, '技术部');  -- 结果:'张三 - 技术部'

NULL处理的重要性

-- 错误示例:没有处理NULL
SELECT CONCAT(name, ' - ', dept_name)  -- 如果dept_name是NULL,结果是NULL

-- 正确示例:处理NULL
SELECT CONCAT(name, ' - ', IFNULL(dept_name, '未知部门'))

其他字符串函数

SELECT
    -- 连接
    CONCAT(name, ' ', dept_name) AS method1,
    CONCAT_WS(' ', name, dept_name) AS method2,

    -- 大小写转换
    UPPER(name) AS upper_name,
    LOWER(name) AS lower_name,

    -- 截取
    SUBSTRING(name, 1, 1) AS first_char,
    LEFT(name, 2) AS first_two_chars,
    RIGHT(name, 2) AS last_two_chars,

    -- 长度
    LENGTH(name) AS byte_length,
    CHAR_LENGTH(name) AS char_length,

    -- 替换
    REPLACE(name, '张', 'Zhang') AS replaced,

    -- 去空格
    TRIM(name) AS trimmed,

    -- 重复
    REPEAT('-', 10) AS separator
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;

实际应用场景

-- 生成员工卡片信息
SELECT
    CONCAT(
        '姓名:', e.name, '\n',
        '部门:', IFNULL(d.dept_name, '未分配'), '\n',
        '工资:¥', FORMAT(e.salary, 2), '\n',
        '入职:', DATE_FORMAT(e.hire_date, '%Y年%m月%d日')
    ) AS employee_card
FROM employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;

-- 生成邮件标题
SELECT
    CONCAT(
        '[', d.dept_name, '] ',
        e.name, ' - ',
        '工资通知'
    ) AS email_subject
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;

题目51:查询每个员工的工资排名(全公司范围)

💡 解题思路

这道题考查窗口函数的全局排名。与题目48不同,这里不需要分区:

  1. 使用RANK()、DENSE_RANK()或ROW_NUMBER()
  2. 不使用PARTITION BY(全公司范围)
  3. 按工资降序排序

🔍 关键提示

  • 窗口函数不带PARTITION BY就是全局排名
  • RANK() OVER (ORDER BY salary DESC)
  • 考虑工资相同的情况应该如何排名
  • 可以同时显示多种排名方式对比

📝 实现步骤

  1. 选择合适的排名函数
  2. 使用ORDER BY salary DESC定义排序规则
  3. 可选:同时显示三种排名函数的结果进行对比

✅ 验证方法

-- 检查排名是否从1开始
-- 检查工资相同的员工排名是否正确
SELECT salary, COUNT(*) FROM (你的查询) GROUP BY salary HAVING COUNT(*) > 1;

🚀 扩展思考

  • 如何查询工资排名前10%的员工?
  • 如何计算每个员工的工资百分位?
  • 如何查询工资中位数?
💯 点击查看参考答案

方法1:使用RANK()(相同工资同排名,跳号)

SELECT
    name,
    dept_id,
    salary,
    RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
ORDER BY salary_rank;

方法2:使用DENSE_RANK()(相同工资同排名,不跳号)

SELECT
    name,
    dept_id,
    salary,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
ORDER BY salary_rank;

方法3:使用ROW_NUMBER()(唯一排名)

SELECT
    name,
    dept_id,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
ORDER BY salary_rank;

方法4:三种排名对比

SELECT
    name,
    salary,
    RANK() OVER (ORDER BY salary DESC) AS rank_with_gaps,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS rank_no_gaps,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_number,
    -- 计算与最高工资的差距
    (SELECT MAX(salary) FROM employees) - salary AS diff_from_max,
    -- 计算工资百分位
    PERCENT_RANK() OVER (ORDER BY salary DESC) AS percentile
FROM employees
ORDER BY salary DESC;

方法5:查询TOP 10%员工

SELECT
    name,
    salary,
    salary_rank,
    total_emp,
    ROUND(salary_rank * 100.0 / total_emp, 2) AS rank_percentage
FROM (
    SELECT
        name,
        salary,
        RANK() OVER (ORDER BY salary DESC) AS salary_rank,
        COUNT(*) OVER () AS total_emp
    FROM employees
) AS ranked_emp
WHERE salary_rank <= CEIL((SELECT COUNT(*) FROM employees) * 0.1)
ORDER BY salary_rank;

方法6:不使用窗口函数(MySQL 5.7兼容)

SELECT
    e1.name,
    e1.salary,
    COUNT(DISTINCT e2.salary) AS salary_rank
FROM employees e1
LEFT JOIN employees e2 ON e2.salary >= e1.salary
GROUP BY e1.emp_id, e1.name, e1.salary
ORDER BY salary_rank;

要点说明

三种排名的区别示例(工资:15000, 12000, 12000, 10000):

RANK():         1, 2, 2, 4  -- 跳号
DENSE_RANK():   1, 2, 2, 3  -- 不跳号
ROW_NUMBER():   1, 2, 3, 4  -- 唯一排名(相同工资也不同排名)

其他有用的窗口函数

SELECT
    name,
    salary,
    -- 排名相关
    RANK() OVER (ORDER BY salary DESC) AS rank_num,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_num,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
    NTILE(4) OVER (ORDER BY salary DESC) AS quartile,  -- 分成4组
    PERCENT_RANK() OVER (ORDER BY salary DESC) AS percent_rank,  -- 百分位排名
    CUME_DIST() OVER (ORDER BY salary DESC) AS cumulative_dist,  -- 累积分布

    -- 聚合窗口函数
    AVG(salary) OVER () AS company_avg_salary,
    MAX(salary) OVER () AS max_salary,
    MIN(salary) OVER () AS min_salary,
    SUM(salary) OVER (ORDER BY salary DESC) AS cumulative_salary,

    -- LAG和LEAD(访问前后行)
    LAG(salary, 1) OVER (ORDER BY salary DESC) AS prev_salary,
    LEAD(salary, 1) OVER (ORDER BY salary DESC) AS next_salary,
    salary - LAG(salary, 1) OVER (ORDER BY salary DESC) AS diff_from_prev
FROM employees
ORDER BY salary DESC;

实际应用示例

-- 查询工资中位数
SELECT AVG(salary) AS median_salary
FROM (
    SELECT
        salary,
        ROW_NUMBER() OVER (ORDER BY salary) AS row_num,
        COUNT(*) OVER () AS total_count
    FROM employees
) AS numbered
WHERE row_num IN (FLOOR((total_count + 1) / 2), CEIL((total_count + 1) / 2));

-- 查询每个工资段的人数分布
SELECT
    quartile,
    COUNT(*) AS emp_count,
    MIN(salary) AS min_salary,
    MAX(salary) AS max_salary,
    AVG(salary) AS avg_salary
FROM (
    SELECT
        salary,
        NTILE(4) OVER (ORDER BY salary) AS quartile
    FROM employees
) AS quartiles
GROUP BY quartile
ORDER BY quartile;

题目52:提取每个员工邮箱的域名部分

💡 解题思路

这道题考查字符串处理函数的组合使用。需要:

  1. 找到@符号的位置
  2. 提取@之后的部分
  3. 处理可能的NULL值或格式错误

🔍 关键提示

  • SUBSTRING_INDEX(str, delimiter, count):按分隔符截取
  • SUBSTRING(str, pos):从指定位置截取
  • LOCATE('@', email):查找@的位置
  • 处理边界情况(NULL、没有@、多个@)

📝 实现步骤

  1. 使用SUBSTRING_INDEX提取@后面的部分
  2. 或者使用LOCATE找到@的位置,再用SUBSTRING截取
  3. 添加NULL值处理
  4. 可选:同时提取用户名部分

✅ 验证方法

-- 测试各种邮箱格式
INSERT INTO employees (name, email) VALUES
('测试1', '[email protected]'),
('测试2', '[email protected]'),
('测试3', NULL),
('测试4', 'invalid');

🚀 扩展思考

  • 如何同时提取用户名部分(@之前)?
  • 如何统计每个域名的员工数量?
  • 如何验证邮箱格式是否正确?
💯 点击查看参考答案

方法1:使用SUBSTRING_INDEX(最简单)

SELECT
    name,
    email,
    SUBSTRING_INDEX(email, '@', -1) AS domain
FROM employees
WHERE email IS NOT NULL
ORDER BY domain;

方法2:同时提取用户名和域名

SELECT
    name,
    email,
    SUBSTRING_INDEX(email, '@', 1) AS username,
    SUBSTRING_INDEX(email, '@', -1) AS domain
FROM employees
WHERE email IS NOT NULL AND email LIKE '%@%'
ORDER BY domain;

方法3:使用LOCATE和SUBSTRING

SELECT
    name,
    email,
    SUBSTRING(email, LOCATE('@', email) + 1) AS domain,
    SUBSTRING(email, 1, LOCATE('@', email) - 1) AS username
FROM employees
WHERE email IS NOT NULL
  AND LOCATE('@', email) > 0
ORDER BY domain;

方法4:添加格式验证

SELECT
    name,
    email,
    CASE
        WHEN email IS NULL THEN '无邮箱'
        WHEN email NOT LIKE '%@%.%' THEN '格式错误'
        ELSE SUBSTRING_INDEX(email, '@', -1)
    END AS domain,
    CASE
        WHEN email IS NOT NULL AND email LIKE '%@%.%' THEN '有效'
        ELSE '无效'
    END AS email_status
FROM employees
ORDER BY email_status DESC, domain;

方法5:统计每个域名的员工数

SELECT
    SUBSTRING_INDEX(email, '@', -1) AS domain,
    COUNT(*) AS emp_count,
    GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') AS employees
FROM employees
WHERE email IS NOT NULL AND email LIKE '%@%'
GROUP BY domain
ORDER BY emp_count DESC, domain;

方法6:提取顶级域名(com、cn、org等)

SELECT
    name,
    email,
    SUBSTRING_INDEX(email, '@', -1) AS full_domain,
    SUBSTRING_INDEX(SUBSTRING_INDEX(email, '@', -1), '.', -1) AS top_level_domain
FROM employees
WHERE email IS NOT NULL AND email LIKE '%@%'
ORDER BY top_level_domain, email;

要点说明

SUBSTRING_INDEX参数说明

SUBSTRING_INDEX(str, delim, count)
-- count > 0: 从左边数第count个分隔符之前的部分
-- count < 0: 从右边数第count个分隔符之后的部分

示例:'[email protected]'
SUBSTRING_INDEX(email, '@', 1)   → 'user'
SUBSTRING_INDEX(email, '@', -1)  → 'mail.example.com'
SUBSTRING_INDEX(email, '.', 1)   → 'user@mail'
SUBSTRING_INDEX(email, '.', -1)  → 'com'
SUBSTRING_INDEX(email, '.', -2)  → 'example.com'

字符串函数组合技巧

-- 提取二级域名(如example.com中的example)
SUBSTRING_INDEX(SUBSTRING_INDEX(email, '@', -1), '.', 1)

-- 提取完整域名并转小写
LOWER(SUBSTRING_INDEX(email, '@', -1))

-- 检查是否是公司邮箱
SUBSTRING_INDEX(email, '@', -1) LIKE '%.company.com'

实际应用场景

-- 分类统计不同类型的邮箱
SELECT
    CASE
        WHEN email LIKE '%@gmail.com' THEN 'Gmail'
        WHEN email LIKE '%@qq.com' THEN 'QQ邮箱'
        WHEN email LIKE '%@163.com' OR email LIKE '%@126.com' THEN '网易邮箱'
        WHEN email LIKE '%@company.com' THEN '公司邮箱'
        ELSE '其他'
    END AS email_type,
    COUNT(*) AS count
FROM employees
WHERE email IS NOT NULL
GROUP BY email_type
ORDER BY count DESC;

-- 验证邮箱格式并分类
SELECT
    name,
    email,
    CASE
        WHEN email IS NULL THEN '未填写'
        WHEN email NOT LIKE '%@%' THEN '缺少@'
        WHEN email NOT LIKE '%@%.%' THEN '缺少域名'
        WHEN email LIKE '%@%@%' THEN '多个@'
        WHEN LENGTH(SUBSTRING_INDEX(email, '@', 1)) = 0 THEN '用户名为空'
        WHEN LENGTH(SUBSTRING_INDEX(email, '@', -1)) < 3 THEN '域名太短'
        ELSE '格式正确'
    END AS validation_result
FROM employees
ORDER BY
    CASE validation_result
        WHEN '格式正确' THEN 1
        ELSE 2
    END,
    email;

其他字符串处理示例

SELECT
    email,
    -- 提取和转换
    SUBSTRING_INDEX(email, '@', -1) AS domain,
    UPPER(SUBSTRING_INDEX(email, '@', 1)) AS username_upper,
    LOWER(email) AS email_lower,

    -- 替换
    REPLACE(email, '@', ' [at] ') AS email_safe,

    -- 长度和位置
    LENGTH(email) AS total_length,
    CHAR_LENGTH(email) AS char_count,
    LOCATE('@', email) AS at_position,

    -- 截取
    LEFT(email, 5) AS first_5_chars,
    RIGHT(email, 10) AS last_10_chars,
    SUBSTRING(email, 1, LOCATE('@', email) - 1) AS username,

    -- 填充
    LPAD(SUBSTRING_INDEX(email, '@', 1), 10, '*') AS padded_username,

    -- 反转(不常用)
    REVERSE(email) AS reversed
FROM employees
WHERE email IS NOT NULL;

题目53:查询入职满1年的员工数量

💡 解题思路

这道题考查日期比较聚合函数的结合使用。需要:

  1. 计算入职日期与当前日期的时间差
  2. 判断是否满1年
  3. 统计符合条件的数量

🔍 关键提示

  • 使用DATE_ADD或DATE_SUB计算1年前的日期
  • 或使用TIMESTAMPDIFF计算时间差
  • 使用COUNT统计数量
  • 注意"满1年"的定义(≥365天还是≥1年)

📝 实现步骤

  1. 计算1年前的日期:DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
  2. 比较hire_date是否小于等于这个日期
  3. 使用COUNT(*)统计数量

✅ 验证方法

-- 同时查看明细和汇总
SELECT name, hire_date, TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years
FROM employees
WHERE TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1;

🚀 扩展思考

  • 如何查询入职不满1年的员工?
  • 如何查询入职满N年的员工(N可变)?
  • 如何统计各工龄段的员工数量?
💯 点击查看参考答案

方法1:使用DATE_SUB(推荐)

SELECT COUNT(*) AS employees_over_1_year
FROM employees
WHERE hire_date <= DATE_SUB(CURDATE(), INTERVAL 1 YEAR);

方法2:使用TIMESTAMPDIFF

SELECT COUNT(*) AS employees_over_1_year
FROM employees
WHERE TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1;

方法3:使用DATEDIFF(按天数计算)

SELECT COUNT(*) AS employees_over_1_year
FROM employees
WHERE DATEDIFF(CURDATE(), hire_date) >= 365;

方法4:详细查询(显示明细)

SELECT
    name,
    hire_date,
    DATEDIFF(CURDATE(), hire_date) AS days_worked,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years_worked,
    TIMESTAMPDIFF(MONTH, hire_date, CURDATE()) AS months_worked
FROM employees
WHERE TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1
ORDER BY hire_date;

方法5:分段统计

SELECT
    CASE
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) < 1 THEN '不满1年'
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) < 3 THEN '1-3年'
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) < 5 THEN '3-5年'
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) < 10 THEN '5-10年'
        ELSE '10年以上'
    END AS seniority_group,
    COUNT(*) AS emp_count,
    ROUND(AVG(salary), 2) AS avg_salary
FROM employees
GROUP BY seniority_group
ORDER BY MIN(TIMESTAMPDIFF(YEAR, hire_date, CURDATE()));

方法6:使用SUM和CASE

SELECT
    SUM(CASE WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1 THEN 1 ELSE 0 END) AS over_1_year,
    SUM(CASE WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) < 1 THEN 1 ELSE 0 END) AS under_1_year,
    COUNT(*) AS total_employees,
    ROUND(
        SUM(CASE WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*),
        2
    ) AS percentage_over_1_year
FROM employees;

要点说明

不同方法的精确度比较

-- 假设今天是2024-03-15,员工2023-03-16入职(差1天满1年)

-- 方法1: DATE_SUB
hire_date <= '2023-03-15'  → 不满足(3月16日 > 3月15日)

-- 方法2: TIMESTAMPDIFF(YEAR, ...)
TIMESTAMPDIFF(YEAR, '2023-03-16', '2024-03-15')  → 0(不满1年)

-- 方法3: DATEDIFF
DATEDIFF('2024-03-15', '2023-03-16')  → 364天(不满365天)

结论:三种方法在临界情况下结果一致

性能对比

-- ✅ 好:范围查询,可以使用索引
WHERE hire_date <= DATE_SUB(CURDATE(), INTERVAL 1 YEAR)

-- ⚠️ 一般:函数计算,可能导致索引失效
WHERE TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1
WHERE DATEDIFF(CURDATE(), hire_date) >= 365

实际应用扩展

-- 查询即将满1年的员工(还有30天内)
SELECT
    name,
    hire_date,
    DATE_ADD(hire_date, INTERVAL 1 YEAR) AS anniversary_date,
    DATEDIFF(DATE_ADD(hire_date, INTERVAL 1 YEAR), CURDATE()) AS days_until_anniversary
FROM employees
WHERE hire_date > DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
  AND hire_date <= DATE_SUB(CURDATE(), INTERVAL 1 YEAR - 30 DAY)
ORDER BY days_until_anniversary;

-- 按入职年份统计满1年员工数
SELECT
    YEAR(hire_date) AS hire_year,
    COUNT(*) AS emp_count,
    AVG(TIMESTAMPDIFF(YEAR, hire_date, CURDATE())) AS avg_years_worked
FROM employees
WHERE TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1
GROUP BY hire_year
ORDER BY hire_year DESC;

-- 计算员工留存率(入职满1年的比例)
SELECT
    CONCAT(
        ROUND(
            COUNT(CASE WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 1 THEN 1 END) * 100.0 /
            NULLIF(COUNT(CASE WHEN hire_date <= DATE_SUB(CURDATE(), INTERVAL 1 YEAR) THEN 1 END), 0),
            2
        ),
        '%'
    ) AS retention_rate
FROM employees;

题目54:按季度统计员工入职人数

💡 解题思路

这道题考查日期函数分组统计的组合。需要:

  1. 提取入职日期的年份和季度
  2. 按年份和季度分组
  3. 统计每组的人数

🔍 关键提示

  • 使用QUARTER(date)函数获取季度(1-4)
  • 使用YEAR(date)获取年份
  • 使用GROUP BY对年份和季度分组
  • 可以使用CONCAT格式化输出

📝 实现步骤

  1. 提取年份:YEAR(hire_date)
  2. 提取季度:QUARTER(hire_date)
  3. 按年份和季度分组
  4. 统计数量并排序

✅ 验证方法

-- 检查每个季度的记录
-- 验证季度值是否在1-4之间
-- 检查是否有遗漏的季度

🚀 扩展思考

  • 如何同时显示同比增长?
  • 如何可视化季度趋势(如用*表示数量)?
  • 如何计算季度平均入职人数?
💯 点击查看参考答案

方法1:基本统计

SELECT
    YEAR(hire_date) AS hire_year,
    QUARTER(hire_date) AS hire_quarter,
    COUNT(*) AS emp_count
FROM employees
GROUP BY hire_year, hire_quarter
ORDER BY hire_year, hire_quarter;

方法2:格式化显示

SELECT
    CONCAT(YEAR(hire_date), '-Q', QUARTER(hire_date)) AS year_quarter,
    COUNT(*) AS emp_count,
    GROUP_CONCAT(name ORDER BY hire_date) AS employees
FROM employees
GROUP BY YEAR(hire_date), QUARTER(hire_date)
ORDER BY YEAR(hire_date), QUARTER(hire_date);

方法3:添加季度日期范围

SELECT
    YEAR(hire_date) AS year,
    QUARTER(hire_date) AS quarter,
    CONCAT(YEAR(hire_date), '-Q', QUARTER(hire_date)) AS period,
    MIN(hire_date) AS quarter_start,
    MAX(hire_date) AS quarter_end,
    COUNT(*) AS emp_count
FROM employees
GROUP BY year, quarter
ORDER BY year DESC, quarter DESC;

方法4:横向展示(行转列)

SELECT
    YEAR(hire_date) AS year,
    SUM(CASE WHEN QUARTER(hire_date) = 1 THEN 1 ELSE 0 END) AS Q1,
    SUM(CASE WHEN QUARTER(hire_date) = 2 THEN 1 ELSE 0 END) AS Q2,
    SUM(CASE WHEN QUARTER(hire_date) = 3 THEN 1 ELSE 0 END) AS Q3,
    SUM(CASE WHEN QUARTER(hire_date) = 4 THEN 1 ELSE 0 END) AS Q4,
    COUNT(*) AS year_total
FROM employees
GROUP BY year
ORDER BY year DESC;

方法5:计算环比增长

SELECT
    year_quarter,
    emp_count,
    prev_quarter_count,
    emp_count - prev_quarter_count AS diff,
    CASE
        WHEN prev_quarter_count > 0 THEN
            CONCAT(ROUND((emp_count - prev_quarter_count) * 100.0 / prev_quarter_count, 2), '%')
        ELSE 'N/A'
    END AS growth_rate
FROM (
    SELECT
        CONCAT(YEAR(hire_date), '-Q', QUARTER(hire_date)) AS year_quarter,
        COUNT(*) AS emp_count,
        LAG(COUNT(*), 1) OVER (ORDER BY YEAR(hire_date), QUARTER(hire_date)) AS prev_quarter_count
    FROM employees
    GROUP BY YEAR(hire_date), QUARTER(hire_date)
) AS quarterly_stats
ORDER BY year_quarter DESC;

方法6:完整的季度分析

SELECT
    YEAR(hire_date) AS year,
    QUARTER(hire_date) AS quarter,
    CONCAT(YEAR(hire_date), 'Q', QUARTER(hire_date)) AS period,
    -- 季度起止日期
    DATE_FORMAT(
        MAKEDATE(YEAR(hire_date), 1) + INTERVAL (QUARTER(hire_date) - 1) QUARTER,
        '%Y-%m-%d'
    ) AS quarter_start_date,
    DATE_FORMAT(
        MAKEDATE(YEAR(hire_date), 1) + INTERVAL QUARTER(hire_date) QUARTER - INTERVAL 1 DAY,
        '%Y-%m-%d'
    ) AS quarter_end_date,
    -- 统计数据
    COUNT(*) AS emp_count,
    ROUND(AVG(salary), 2) AS avg_salary,
    MIN(hire_date) AS first_hire,
    MAX(hire_date) AS last_hire,
    -- 累计统计
    SUM(COUNT(*)) OVER (ORDER BY YEAR(hire_date), QUARTER(hire_date)) AS cumulative_count
FROM employees
GROUP BY year, quarter
ORDER BY year DESC, quarter DESC;

要点说明

QUARTER函数说明

QUARTER('2024-01-01')  → 1  (1-3月)
QUARTER('2024-04-01')  → 2  (4-6月)
QUARTER('2024-07-01')  → 3  (7-9月)
QUARTER('2024-10-01')  → 4  (10-12月)

计算季度起止日期

-- Q1: 1月1日 - 3月31日
-- Q2: 4月1日 - 6月30日
-- Q3: 7月1日 - 9月30日
-- Q4: 10月1日 - 12月31日

-- 季度第一天
DATE_FORMAT(
    MAKEDATE(YEAR(date), 1) + INTERVAL (QUARTER(date) - 1) QUARTER,
    '%Y-%m-%d'
)

-- 季度最后一天
LAST_DAY(
    MAKEDATE(YEAR(date), 1) + INTERVAL QUARTER(date) QUARTER - INTERVAL 1 DAY
)

实际应用场景

-- 按季度统计招聘效果
SELECT
    CONCAT(YEAR(hire_date), 'Q', QUARTER(hire_date)) AS quarter,
    COUNT(*) AS total_hired,
    SUM(CASE WHEN dept_id = 1 THEN 1 ELSE 0 END) AS tech_dept,
    SUM(CASE WHEN dept_id = 2 THEN 1 ELSE 0 END) AS sales_dept,
    ROUND(AVG(salary), 2) AS avg_starting_salary
FROM employees
WHERE hire_date >= DATE_SUB(CURDATE(), INTERVAL 2 YEAR)
GROUP BY YEAR(hire_date), QUARTER(hire_date)
ORDER BY quarter DESC;

-- 季度入职趋势可视化(简单文本图表)
SELECT
    CONCAT(YEAR(hire_date), 'Q', QUARTER(hire_date)) AS quarter,
    COUNT(*) AS emp_count,
    REPEAT('█', COUNT(*)) AS bar_chart
FROM employees
GROUP BY YEAR(hire_date), QUARTER(hire_date)
ORDER BY quarter;

-- 查询入职最多的季度
SELECT
    CONCAT(YEAR(hire_date), 'Q', QUARTER(hire_date)) AS quarter,
    COUNT(*) AS emp_count
FROM employees
GROUP BY YEAR(hire_date), QUARTER(hire_date)
ORDER BY emp_count DESC
LIMIT 5;

其他时间维度统计

-- 按月统计
SELECT
    DATE_FORMAT(hire_date, '%Y-%m') AS month,
    COUNT(*) AS emp_count
FROM employees
GROUP BY month
ORDER BY month DESC;

-- 按周统计
SELECT
    YEARWEEK(hire_date) AS year_week,
    COUNT(*) AS emp_count
FROM employees
GROUP BY year_week
ORDER BY year_week DESC;

-- 按星期几统计
SELECT
    DAYNAME(hire_date) AS day_of_week,
    DAYOFWEEK(hire_date) AS day_num,
    COUNT(*) AS emp_count
FROM employees
GROUP BY day_of_week, day_num
ORDER BY day_num;

题目55:查询每个部门内工资的累计和

💡 解题思路

这道题考查窗口函数的聚合应用。需要:

  1. 使用SUM窗口函数
  2. 按部门分区(PARTITION BY)
  3. 按某个字段排序计算累计和

🔍 关键提示

  • SUM(salary) OVER (PARTITION BY dept_id ORDER BY ...)
  • PARTITION BY按部门分组
  • ORDER BY定义累计顺序(通常按入职日期或员工ID)
  • 理解累计和的概念

📝 实现步骤

  1. 选择要显示的字段
  2. 使用SUM窗口函数计算累计和
  3. PARTITION BY dept_id按部门分区
  4. ORDER BY定义累计顺序

✅ 验证方法

-- 检查每个部门的最后一条累计和是否等于部门工资总和
SELECT dept_id, SUM(salary) FROM employees GROUP BY dept_id;

🚀 扩展思考

  • 如何计算移动平均值?
  • 如何计算每个员工的工资占部门总工资的百分比?
  • 如何同时显示多个累计指标?
💯 点击查看参考答案

方法1:按入职日期累计(推荐)

SELECT
    dept_id,
    name,
    hire_date,
    salary,
    SUM(salary) OVER (
        PARTITION BY dept_id
        ORDER BY hire_date
    ) AS cumulative_salary
FROM employees
ORDER BY dept_id, hire_date;

方法2:按员工ID累计

SELECT
    dept_id,
    emp_id,
    name,
    salary,
    SUM(salary) OVER (
        PARTITION BY dept_id
        ORDER BY emp_id
    ) AS cumulative_salary,
    -- 同时显示部门总工资
    SUM(salary) OVER (PARTITION BY dept_id) AS dept_total_salary,
    -- 计算百分比
    ROUND(
        salary * 100.0 / SUM(salary) OVER (PARTITION BY dept_id),
        2
    ) AS salary_percentage
FROM employees
ORDER BY dept_id, emp_id;

方法3:完整的累计分析

SELECT
    d.dept_name,
    e.name,
    e.hire_date,
    e.salary,
    -- 累计工资
    SUM(e.salary) OVER (
        PARTITION BY e.dept_id
        ORDER BY e.hire_date
    ) AS cumulative_salary,
    -- 累计人数
    ROW_NUMBER() OVER (
        PARTITION BY e.dept_id
        ORDER BY e.hire_date
    ) AS cumulative_emp_count,
    -- 累计平均工资
    AVG(e.salary) OVER (
        PARTITION BY e.dept_id
        ORDER BY e.hire_date
    ) AS cumulative_avg_salary,
    -- 部门总计
    SUM(e.salary) OVER (PARTITION BY e.dept_id) AS dept_total,
    COUNT(*) OVER (PARTITION BY e.dept_id) AS dept_emp_count
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
ORDER BY d.dept_name, e.hire_date;

方法4:移动平均(最近3人)

SELECT
    dept_id,
    name,
    hire_date,
    salary,
    -- 移动平均(当前行及前2行)
    AVG(salary) OVER (
        PARTITION BY dept_id
        ORDER BY hire_date
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS moving_avg_3,
    -- 累计和
    SUM(salary) OVER (
        PARTITION BY dept_id
        ORDER BY hire_date
    ) AS cumulative_sum
FROM employees
ORDER BY dept_id, hire_date;

方法5:不使用窗口函数(MySQL 5.7兼容)

SELECT
    e1.dept_id,
    e1.name,
    e1.hire_date,
    e1.salary,
    (
        SELECT SUM(e2.salary)
        FROM employees e2
        WHERE e2.dept_id = e1.dept_id
          AND e2.hire_date <= e1.hire_date
    ) AS cumulative_salary
FROM employees e1
ORDER BY e1.dept_id, e1.hire_date;

方法6:多维度累计统计

SELECT
    dept_id,
    name,
    hire_date,
    salary,
    -- 按时间累计
    SUM(salary) OVER (
        PARTITION BY dept_id
        ORDER BY hire_date
    ) AS time_cumulative,
    -- 按工资排序累计(从高到低)
    SUM(salary) OVER (
        PARTITION BY dept_id
        ORDER BY salary DESC
    ) AS salary_rank_cumulative,
    -- 全部累计(部门总和)
    SUM(salary) OVER (PARTITION BY dept_id) AS dept_total,
    -- 全局累计(不分区)
    SUM(salary) OVER (ORDER BY hire_date) AS company_cumulative
FROM employees
ORDER BY dept_id, hire_date;

要点说明

窗口函数的帧(Frame)概念

-- 默认:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
-- 从分区开始到当前行(累计和的默认行为)

-- 其他选项:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW  -- 所有之前的行到当前
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW          -- 前2行到当前(移动平均)
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING  -- 当前到最后
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING          -- 前1行到后1行

累计和的验证

-- 最后一行的累计和应该等于分组总和
SELECT
    dept_id,
    MAX(cumulative_salary) AS last_cumulative,
    SUM(salary) AS actual_total
FROM (
    SELECT
        dept_id,
        salary,
        SUM(salary) OVER (PARTITION BY dept_id ORDER BY emp_id) AS cumulative_salary
    FROM employees
) AS cum_data
GROUP BY dept_id;
-- last_cumulative应该等于actual_total

实际应用场景

-- 查询累计工资达到目标的日期
SELECT
    dept_id,
    name,
    hire_date,
    cumulative_salary
FROM (
    SELECT
        dept_id,
        name,
        hire_date,
        SUM(salary) OVER (
            PARTITION BY dept_id
            ORDER BY hire_date
        ) AS cumulative_salary
    FROM employees
) AS cum_data
WHERE cumulative_salary >= 100000  -- 累计达到10万
ORDER BY dept_id, hire_date;

-- 计算每个员工加入后的部门平均工资变化
SELECT
    dept_id,
    name,
    hire_date,
    salary,
    ROUND(
        AVG(salary) OVER (
            PARTITION BY dept_id
            ORDER BY hire_date
        ),
        2
    ) AS avg_salary_after_join,
    ROUND(
        AVG(salary) OVER (PARTITION BY dept_id),
        2
    ) AS current_avg_salary
FROM employees
ORDER BY dept_id, hire_date;

-- 识别工资增长异常(与移动平均偏差过大)
SELECT
    dept_id,
    name,
    salary,
    moving_avg,
    ABS(salary - moving_avg) AS deviation,
    CASE
        WHEN ABS(salary - moving_avg) > moving_avg * 0.5 THEN '异常'
        ELSE '正常'
    END AS status
FROM (
    SELECT
        dept_id,
        name,
        salary,
        AVG(salary) OVER (
            PARTITION BY dept_id
            ORDER BY hire_date
            ROWS BETWEEN 5 PRECEDING AND 1 PRECEDING
        ) AS moving_avg
    FROM employees
) AS avg_data
WHERE moving_avg IS NOT NULL
ORDER BY deviation DESC;


第七关:数据操作 - 学习指南

题目56:向员工表插入一条新记录

💡 解题思路

这道题考查INSERT语句的基本用法。需要:

  1. 了解INSERT语句的基本语法
  2. 明确要插入哪些字段
  3. 注意数据类型和引号的使用

🔍 关键提示

  • INSERT INTO 表名 (字段列表) VALUES (值列表)
  • 字符串和日期需要用单引号包裹
  • 可以省略字段列表(但不推荐)
  • 自增字段可以不指定值

📝 实现步骤

  1. 确定要插入的字段(排除自增ID)
  2. 准备对应的值
  3. 编写INSERT语句
  4. 执行并验证

✅ 验证方法

-- 插入后查询验证
SELECT * FROM employees ORDER BY emp_id DESC LIMIT 1;
-- 或查询总数是否增加
SELECT COUNT(*) FROM employees;

🚀 扩展思考

  • 如何插入时忽略某些字段(使用默认值)?
  • 如果插入重复的唯一字段会怎样?
  • 如何在插入后获取自增ID?
💯 点击查看参考答案

方法1:指定所有字段(推荐)

INSERT INTO employees (name, department, salary, hire_date, manager_id)
VALUES ('张伟', '技术部', 8000.00, '2024-01-15', 1);

方法2:使用DEFAULT关键字

INSERT INTO employees (name, department, salary, hire_date, manager_id)
VALUES ('李娜', '销售部', DEFAULT, CURDATE(), NULL);

方法3:省略字段列表(不推荐)

-- 必须按表结构顺序提供所有字段的值
INSERT INTO employees
VALUES (NULL, '王强', '财务部', 7500.00, '2024-01-20', 2);

方法4:获取插入后的ID

INSERT INTO employees (name, department, salary, hire_date)
VALUES ('赵敏', '人事部', 6500.00, '2024-01-25');

-- 获取刚插入的ID
SELECT LAST_INSERT_ID() AS new_emp_id;

要点说明

  • 指定字段列表:明确哪些字段要插入值,代码更清晰
  • 字符串引号:MySQL中字符串用单引号或双引号都可以,推荐单引号
  • 日期格式:'YYYY-MM-DD'或使用函数如CURDATE()、NOW()
  • NULL值:可以显式插入NULL(如果字段允许)
  • DEFAULT:使用字段定义的默认值

常见错误

-- ❌ 错误:字符串没有引号
INSERT INTO employees (name) VALUES (张三);

-- ✅ 正确:字符串用引号
INSERT INTO employees (name) VALUES ('张三');

-- ❌ 错误:字段数和值数不匹配
INSERT INTO employees (name, salary) VALUES ('李四');

-- ✅ 正确:字段和值一一对应
INSERT INTO employees (name, salary) VALUES ('李四', 5000);

实用技巧

-- 插入并忽略重复(主键或唯一索引冲突时跳过)
INSERT IGNORE INTO employees (emp_id, name) VALUES (1, '已存在');

-- 插入或更新(存在则更新,不存在则插入)
INSERT INTO employees (emp_id, name, salary)
VALUES (1, '张三', 8000)
ON DUPLICATE KEY UPDATE salary = 8000;

-- 插入并返回完整记录(需要配合查询)
INSERT INTO employees (name, department, salary, hire_date)
VALUES ('新员工', '技术部', 9000, CURDATE());
SELECT * FROM employees WHERE emp_id = LAST_INSERT_ID();

题目57:批量插入5条部门记录

💡 解题思路

这道题考查批量INSERT的用法。需要:

  1. 掌握一次插入多条记录的语法
  2. 理解批量插入的性能优势
  3. 正确组织多条记录的数据

🔍 关键提示

  • 多条VALUES用逗号分隔
  • INSERT INTO 表名 (字段) VALUES (值1), (值2), (值3)
  • 批量插入比多次单条插入快得多
  • 建议每次插入500-1000条为佳

📝 实现步骤

  1. 确定要插入的字段
  2. 准备5条记录的数据
  3. 用逗号连接多个VALUES
  4. 一次性执行

✅ 验证方法

-- 验证插入的记录数
SELECT COUNT(*) FROM departments;
-- 查看新插入的记录
SELECT * FROM departments ORDER BY dept_id DESC LIMIT 5;

🚀 扩展思考

  • 批量插入和单条插入的性能差异有多大?
  • 一次性插入太多数据会有什么问题?
  • 如何批量插入时处理部分失败的情况?
💯 点击查看参考答案

方法1:标准批量插入(推荐)

INSERT INTO departments (dept_name, location, budget)
VALUES
    ('技术部', '北京', 500000.00),
    ('销售部', '上海', 300000.00),
    ('财务部', '北京', 200000.00),
    ('人事部', '深圳', 150000.00),
    ('市场部', '广州', 250000.00);

方法2:使用SET语法(不常用)

INSERT INTO departments
SET dept_name = '技术部', location = '北京', budget = 500000;

INSERT INTO departments
SET dept_name = '销售部', location = '上海', budget = 300000;
-- 需要执行5次,不推荐

方法3:从查询结果批量插入

-- 假设有临时表或其他来源
INSERT INTO departments (dept_name, location, budget)
SELECT dept_name, location, budget
FROM temp_departments
LIMIT 5;

方法4:批量插入并处理冲突

INSERT INTO departments (dept_id, dept_name, location, budget)
VALUES
    (1, '技术部', '北京', 500000.00),
    (2, '销售部', '上海', 300000.00),
    (3, '财务部', '北京', 200000.00),
    (4, '人事部', '深圳', 150000.00),
    (5, '市场部', '广州', 250000.00)
ON DUPLICATE KEY UPDATE
    dept_name = VALUES(dept_name),
    location = VALUES(location),
    budget = VALUES(budget);

要点说明

  • 性能优势:批量插入比单条插入快10-100倍
  • 事务处理:多条记录在一个事务中,要么全部成功,要么全部失败
  • 格式规范:每个VALUES一行,便于阅读和维护
  • 数量控制:建议每次500-1000条,太多可能导致超时或锁表

性能对比示例

-- ❌ 低效:单条插入(假设需要1秒/条)
INSERT INTO departments (dept_name) VALUES ('部门1');  -- 1秒
INSERT INTO departments (dept_name) VALUES ('部门2');  -- 1秒
INSERT INTO departments (dept_name) VALUES ('部门3');  -- 1秒
-- 总计:3秒

-- ✅ 高效:批量插入(假设只需0.1秒)
INSERT INTO departments (dept_name)
VALUES ('部门1'), ('部门2'), ('部门3');  -- 0.1秒
-- 性能提升:30倍

大数据量批量插入最佳实践

-- 如果要插入10000条记录,分批处理
START TRANSACTION;

-- 第1批:1-1000
INSERT INTO departments (dept_name, location)
VALUES
    ('部门1', '城市1'),
    ('部门2', '城市2'),
    -- ... 共1000条
    ('部门1000', '城市1000');

-- 第2批:1001-2000
INSERT INTO departments (dept_name, location)
VALUES
    ('部门1001', '城市1001'),
    -- ... 共1000条
    ('部门2000', '城市2000');

-- ... 继续分批

COMMIT;

错误处理

-- 使用IGNORE忽略错误(如主键冲突)
INSERT IGNORE INTO departments (dept_id, dept_name)
VALUES
    (1, '已存在部门'),  -- 如果ID=1存在,这条会跳过
    (2, '新部门'),      -- 正常插入
    (3, '另一个新部门'); -- 正常插入

题目58:将所有'技术部'员工的工资增加10%

💡 解题思路

这道题考查UPDATE语句和计算的使用。需要:

  1. 使用WHERE筛选特定部门
  2. 使用表达式计算新工资
  3. 先验证再执行更新

🔍 关键提示

  • UPDATE 表名 SET 字段=新值 WHERE 条件
  • 可以在SET中使用计算表达式
  • 务必先用SELECT验证WHERE条件
  • 没有WHERE会更新所有记录

📝 实现步骤

  1. 先用SELECT查询技术部员工
  2. 验证WHERE条件是否正确
  3. 编写UPDATE语句:salary = salary * 1.1
  4. 执行后再次查询验证

✅ 验证方法

-- 更新前:查看技术部员工的工资
SELECT name, department, salary FROM employees WHERE department = '技术部';
-- 执行UPDATE
-- 更新后:再次查询对比
SELECT name, department, salary FROM employees WHERE department = '技术部';

🚀 扩展思考

  • 如何同时更新多个字段?
  • 如何根据不同条件更新不同的值?
  • 如果误操作更新错了,如何回滚?
💯 点击查看参考答案

方法1:基本更新(推荐)

-- 第1步:先验证(非常重要!)
SELECT emp_id, name, department, salary, salary * 1.1 AS new_salary
FROM employees
WHERE department = '技术部';

-- 第2步:确认无误后执行更新
UPDATE employees
SET salary = salary * 1.1
WHERE department = '技术部';

-- 第3步:验证结果
SELECT name, department, salary FROM employees WHERE department = '技术部';

方法2:使用ROUND保留小数位

UPDATE employees
SET salary = ROUND(salary * 1.1, 2)
WHERE department = '技术部';

方法3:同时更新多个字段

UPDATE employees
SET
    salary = salary * 1.1,
    updated_at = NOW()
WHERE department = '技术部';

方法4:使用安全模式(事务)

-- 开启事务
START TRANSACTION;

-- 执行更新
UPDATE employees
SET salary = salary * 1.1
WHERE department = '技术部';

-- 验证结果
SELECT name, salary FROM employees WHERE department = '技术部';

-- 如果正确则提交,否则回滚
COMMIT;  -- 或 ROLLBACK;

方法5:条件更新(不同条件不同涨幅)

UPDATE employees
SET salary = CASE
    WHEN department = '技术部' THEN salary * 1.1
    WHEN department = '销售部' THEN salary * 1.15
    WHEN department = '财务部' THEN salary * 1.05
    ELSE salary
END;

方法6:限制更新数量

-- 只更新前10名技术部员工
UPDATE employees
SET salary = salary * 1.1
WHERE department = '技术部'
ORDER BY hire_date
LIMIT 10;

要点说明

  • 安全第一:更新前必须用SELECT验证WHERE条件
  • 计算表达式salary * 1.1表示原值的110%
  • 小数处理:使用ROUND()避免过多小数位
  • 影响行数:UPDATE执行后会显示影响的行数

常见错误

-- ❌ 危险:忘记WHERE条件,会更新所有记录!
UPDATE employees SET salary = salary * 1.1;

-- ✅ 正确:加上WHERE条件
UPDATE employees SET salary = salary * 1.1 WHERE department = '技术部';

-- ❌ 错误:直接设置为1.1
UPDATE employees SET salary = 1.1 WHERE department = '技术部';

-- ✅ 正确:乘以1.1
UPDATE employees SET salary = salary * 1.1 WHERE department = '技术部';

最佳实践流程

-- 步骤1:开启事务
START TRANSACTION;

-- 步骤2:执行更新
UPDATE employees
SET salary = salary * 1.1
WHERE department = '技术部';

-- 步骤3:查看影响的行数和结果
SELECT ROW_COUNT() AS affected_rows;  -- 查看影响行数
SELECT * FROM employees WHERE department = '技术部';  -- 验证结果

-- 步骤4:确认无误后提交
COMMIT;  -- 如果发现错误,执行 ROLLBACK;

记录修改历史

-- 先备份到历史表
INSERT INTO salary_history (emp_id, old_salary, new_salary, change_date, reason)
SELECT
    emp_id,
    salary AS old_salary,
    salary * 1.1 AS new_salary,
    NOW(),
    '技术部年度调薪10%'
FROM employees
WHERE department = '技术部';

-- 然后执行更新
UPDATE employees
SET salary = salary * 1.1
WHERE department = '技术部';

题目59:删除工资低于3000的员工记录

💡 解题思路

这道题考查DELETE语句的使用。需要:

  1. 使用WHERE条件筛选要删除的记录
  2. 删除前务必验证
  3. 理解DELETE的不可逆性

🔍 关键提示

  • DELETE FROM 表名 WHERE 条件
  • 删除前必须验证WHERE条件
  • DELETE没有WHERE会删除所有记录
  • 删除操作通常不可逆,务必谨慎

📝 实现步骤

  1. 先用SELECT查询符合条件的记录
  2. 确认是要删除的记录
  3. 执行DELETE语句
  4. 验证删除结果

✅ 验证方法

-- 删除前:查看要删除的记录
SELECT * FROM employees WHERE salary < 3000;
-- 执行DELETE
-- 删除后:验证是否还存在
SELECT * FROM employees WHERE salary < 3000;  -- 应该返回0条

🚀 扩展思考

  • DELETE和TRUNCATE有什么区别?
  • 如何安全地删除大量数据?
  • 误删除后如何恢复?
💯 点击查看参考答案

方法1:基本删除(推荐使用事务)

-- 第1步:先查询要删除的记录(非常重要!)
SELECT emp_id, name, salary
FROM employees
WHERE salary < 3000;

-- 第2步:开启事务(安全删除)
START TRANSACTION;

-- 第3步:执行删除
DELETE FROM employees
WHERE salary < 3000;

-- 第4步:验证结果
SELECT * FROM employees WHERE salary < 3000;  -- 应该返回空
SELECT COUNT(*) FROM employees;  -- 查看剩余记录数

-- 第5步:确认无误后提交
COMMIT;  -- 如果发现错误,立即执行 ROLLBACK;

方法2:先备份再删除

-- 备份要删除的记录
CREATE TABLE employees_backup AS
SELECT * FROM employees WHERE salary < 3000;

-- 然后删除
DELETE FROM employees WHERE salary < 3000;

-- 如果需要恢复
-- INSERT INTO employees SELECT * FROM employees_backup;

方法3:软删除(推荐用于生产环境)

-- 不真正删除,只标记为已删除
UPDATE employees
SET is_deleted = 1, deleted_at = NOW()
WHERE salary < 3000;

-- 查询时排除已删除记录
SELECT * FROM employees WHERE is_deleted = 0;

方法4:限制删除数量

-- 分批删除(避免锁表)
DELETE FROM employees
WHERE salary < 3000
ORDER BY emp_id
LIMIT 100;
-- 多次执行直到没有记录

方法5:使用子查询

DELETE FROM employees
WHERE emp_id IN (
    SELECT emp_id
    FROM (
        SELECT emp_id FROM employees WHERE salary < 3000
    ) AS temp
);

要点说明

  • 验证先行:删除前必须用SELECT验证条件
  • 事务保护:在事务中删除,发现错误可回滚
  • 软删除:生产环境推荐软删除,保留数据可追溯
  • 分批删除:大量数据删除时分批进行,避免锁表

DELETE vs TRUNCATE

-- DELETE:逐行删除,可回滚,可加WHERE条件,不重置自增ID
DELETE FROM employees WHERE salary < 3000;

-- TRUNCATE:直接删除整个表数据并重建,速度快,重置自增ID,不能回滚
TRUNCATE TABLE employees;  -- 删除所有数据

-- TRUNCATE特点:
-- ✓ 速度非常快
-- ✓ 重置AUTO_INCREMENT
-- ✗ 不能加WHERE条件(只能清空整个表)
-- ✗ 不能回滚

常见错误

-- ❌ 致命错误:忘记WHERE条件,删除所有记录!
DELETE FROM employees;

-- ✅ 正确:始终加WHERE条件
DELETE FROM employees WHERE salary < 3000;

-- ❌ 错误:在生产环境直接删除
DELETE FROM employees WHERE salary < 3000;

-- ✅ 正确:在事务中删除,便于回滚
START TRANSACTION;
DELETE FROM employees WHERE salary < 3000;
-- 验证后再 COMMIT 或 ROLLBACK

安全删除最佳实践

-- 1. 查询要删除的记录数量
SELECT COUNT(*) AS will_delete FROM employees WHERE salary < 3000;

-- 2. 查看具体记录
SELECT * FROM employees WHERE salary < 3000 ORDER BY emp_id;

-- 3. 备份(可选但推荐)
CREATE TABLE employees_low_salary_backup AS
SELECT * FROM employees WHERE salary < 3000;

-- 4. 在事务中执行删除
START TRANSACTION;

DELETE FROM employees WHERE salary < 3000;

-- 5. 验证删除结果
SELECT COUNT(*) FROM employees;  -- 查看总数是否正确
SELECT * FROM employees WHERE salary < 3000;  -- 应该返回空

-- 6. 确认无误后提交
COMMIT;

-- 如果发现错误,立即回滚
-- ROLLBACK;

关联删除(级联删除):

-- 删除员工的同时删除其相关记录
START TRANSACTION;

-- 先删除关联表的记录
DELETE FROM employee_projects WHERE emp_id IN (
    SELECT emp_id FROM employees WHERE salary < 3000
);

-- 再删除主表记录
DELETE FROM employees WHERE salary < 3000;

COMMIT;

题目60:将2019年之前入职的员工工资统一增加500元

💡 解题思路

这道题考查日期比较和UPDATE的结合使用。需要:

  1. 使用日期比较筛选员工
  2. 使用加法更新工资
  3. 先验证后更新

🔍 关键提示

  • 日期比较:WHERE hire_date < '2019-01-01'
  • 工资增加:SET salary = salary + 500
  • 可以使用YEAR()函数或直接比较日期

📝 实现步骤

  1. 确定日期条件(2019年之前)
  2. 先SELECT验证符合条件的员工
  3. 执行UPDATE增加工资
  4. 验证更新结果

✅ 验证方法

-- 更新前
SELECT name, hire_date, salary FROM employees WHERE hire_date < '2019-01-01';
-- 执行UPDATE
-- 更新后对比
SELECT name, hire_date, salary FROM employees WHERE hire_date < '2019-01-01';

🚀 扩展思考

  • 如何计算工龄并根据工龄调整工资?
  • 如何同时记录调薪历史?
  • 不同年份入职的员工如何差异化调整?
💯 点击查看参考答案

方法1:直接日期比较(推荐)

-- 第1步:验证条件
SELECT emp_id, name, hire_date, salary, salary + 500 AS new_salary
FROM employees
WHERE hire_date < '2019-01-01'
ORDER BY hire_date;

-- 第2步:执行更新
UPDATE employees
SET salary = salary + 500
WHERE hire_date < '2019-01-01';

方法2:使用YEAR函数

UPDATE employees
SET salary = salary + 500
WHERE YEAR(hire_date) < 2019;

方法3:使用DATE函数确保日期格式

UPDATE employees
SET salary = salary + 500
WHERE DATE(hire_date) < DATE('2019-01-01');

方法4:同时更新多个字段并记录

UPDATE employees
SET
    salary = salary + 500,
    updated_at = NOW(),
    last_raise_date = CURDATE(),
    last_raise_amount = 500
WHERE hire_date < '2019-01-01';

方法5:根据不同入职年份差异化调整

UPDATE employees
SET salary = CASE
    WHEN hire_date < '2015-01-01' THEN salary + 1000  -- 2015年前入职+1000
    WHEN hire_date < '2017-01-01' THEN salary + 800   -- 2015-2016年入职+800
    WHEN hire_date < '2019-01-01' THEN salary + 500   -- 2017-2018年入职+500
    ELSE salary
END
WHERE hire_date < '2019-01-01';

方法6:使用事务并记录历史

START TRANSACTION;

-- 记录调薪历史
INSERT INTO salary_changes (emp_id, old_salary, new_salary, change_amount, change_date, reason)
SELECT
    emp_id,
    salary,
    salary + 500,
    500,
    NOW(),
    '2019年前入职员工统一调薪'
FROM employees
WHERE hire_date < '2019-01-01';

-- 执行更新
UPDATE employees
SET salary = salary + 500
WHERE hire_date < '2019-01-01';

-- 验证
SELECT COUNT(*) AS affected_count FROM employees WHERE hire_date < '2019-01-01';

COMMIT;

要点说明

  • 日期格式:MySQL日期格式为'YYYY-MM-DD'
  • 比较运算符<表示早于指定日期
  • 性能考虑:直接日期比较比使用函数性能更好
  • 索引利用hire_date < '2019-01-01'可以利用索引

不同日期条件写法对比

-- ✅ 推荐:直接比较(可以利用索引)
WHERE hire_date < '2019-01-01'

-- ⚠️ 一般:使用函数(可能导致索引失效)
WHERE YEAR(hire_date) < 2019

-- ✅ 也可以:使用BETWEEN
WHERE hire_date BETWEEN '1900-01-01' AND '2018-12-31'

-- ✅ 灵活:使用DATE_SUB
WHERE hire_date < DATE_SUB(CURDATE(), INTERVAL 6 YEAR)  -- 6年前

根据工龄差异化调薪

UPDATE employees
SET salary = salary + (
    CASE
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 10 THEN 1000
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 5 THEN 800
        WHEN TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) >= 3 THEN 500
        ELSE 300
    END
)
WHERE hire_date < '2019-01-01';

完整的安全更新流程

-- 1. 查看影响范围
SELECT
    COUNT(*) AS total_employees,
    SUM(salary) AS current_total_salary,
    SUM(salary + 500) AS new_total_salary,
    SUM(500) AS total_increase
FROM employees
WHERE hire_date < '2019-01-01';

-- 2. 查看具体员工列表
SELECT emp_id, name, hire_date, salary, salary + 500 AS new_salary
FROM employees
WHERE hire_date < '2019-01-01'
ORDER BY hire_date;

-- 3. 在事务中执行
START TRANSACTION;

UPDATE employees
SET salary = salary + 500
WHERE hire_date < '2019-01-01';

-- 4. 验证结果
SELECT COUNT(*) AS updated_count FROM employees WHERE hire_date < '2019-01-01';

-- 5. 确认无误后提交
COMMIT;  -- 或 ROLLBACK;

批量更新性能优化

-- 如果员工数量很大,考虑分批更新
-- 第1批
UPDATE employees
SET salary = salary + 500
WHERE hire_date < '2019-01-01'
  AND emp_id BETWEEN 1 AND 1000;

-- 第2批
UPDATE employees
SET salary = salary + 500
WHERE hire_date < '2019-01-01'
  AND emp_id BETWEEN 1001 AND 2000;

-- 继续...

题目61:复制一张员工表的结构和数据到新表

💡 解题思路

这道题考查表复制的多种方法。需要:

  1. 区分只复制结构和同时复制数据
  2. 了解CREATE TABLE ... LIKE和CREATE TABLE ... AS SELECT的区别
  3. 掌握INSERT INTO ... SELECT的用法

🔍 关键提示

  • CREATE TABLE 新表 LIKE 原表 - 只复制结构
  • CREATE TABLE 新表 AS SELECT * FROM 原表 - 复制结构和数据
  • INSERT INTO 新表 SELECT * FROM 原表 - 向已存在的表插入数据

📝 实现步骤

  1. 确定是只复制结构还是同时复制数据
  2. 选择合适的SQL语句
  3. 执行并验证
  4. 检查索引和约束是否也被复制

✅ 验证方法

-- 验证表结构
DESC employees_copy;
-- 验证数据
SELECT COUNT(*) FROM employees_copy;
SELECT * FROM employees_copy LIMIT 10;

🚀 扩展思考

  • 复制表时索引会一起复制吗?
  • 如何只复制部分数据?
  • 如何复制表到另一个数据库?
💯 点击查看参考答案

方法1:CREATE TABLE ... AS SELECT(最常用)

-- 复制结构和全部数据
CREATE TABLE employees_copy AS
SELECT * FROM employees;

-- 验证
SELECT COUNT(*) FROM employees;
SELECT COUNT(*) FROM employees_copy;
-- 两个表记录数应该相同

方法2:CREATE TABLE ... LIKE + INSERT

-- 第1步:只复制表结构(包括索引)
CREATE TABLE employees_copy LIKE employees;

-- 第2步:插入数据
INSERT INTO employees_copy
SELECT * FROM employees;

方法3:复制部分字段和数据

-- 只复制特定字段
CREATE TABLE employees_simple AS
SELECT emp_id, name, department, salary
FROM employees;

方法4:复制部分数据(带条件)

-- 只复制技术部员工
CREATE TABLE tech_employees AS
SELECT * FROM employees
WHERE department = '技术部';

方法5:复制时修改数据

-- 复制并转换数据
CREATE TABLE employees_summary AS
SELECT
    emp_id,
    name,
    department,
    salary,
    YEAR(hire_date) AS hire_year,
    TIMESTAMPDIFF(YEAR, hire_date, CURDATE()) AS years_worked
FROM employees;

方法6:复制到另一个数据库

-- 在目标数据库中执行
CREATE TABLE target_db.employees AS
SELECT * FROM source_db.employees;

要点说明

LIKE vs AS SELECT 区别

-- CREATE TABLE ... LIKE
-- ✓ 完整复制表结构(字段类型、默认值、索引、约束)
-- ✗ 不复制数据
CREATE TABLE employees_structure LIKE employees;

-- CREATE TABLE ... AS SELECT
-- ✓ 复制表结构和数据
-- ✗ 不复制索引和约束(只有字段定义)
CREATE TABLE employees_data AS SELECT * FROM employees;

完整对比表

特性 LIKE AS SELECT
复制字段定义
复制数据
复制索引
复制外键
复制AUTO_INCREMENT
复制默认值
复制注释

最完整的复制方法

-- 1. 复制表结构(包括索引和约束)
CREATE TABLE employees_backup LIKE employees;

-- 2. 复制所有数据
INSERT INTO employees_backup SELECT * FROM employees;

-- 3. 验证
SELECT
    (SELECT COUNT(*) FROM employees) AS original_count,
    (SELECT COUNT(*) FROM employees_backup) AS backup_count;

-- 4. 比较表结构
SHOW CREATE TABLE employees;
SHOW CREATE TABLE employees_backup;

复制特定范围的数据

-- 只复制最近一年的数据
CREATE TABLE employees_recent AS
SELECT * FROM employees
WHERE hire_date >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR);

-- 只复制前1000条记录
CREATE TABLE employees_sample AS
SELECT * FROM employees
ORDER BY emp_id
LIMIT 1000;

-- 复制特定部门
CREATE TABLE employees_tech AS
SELECT * FROM employees
WHERE department IN ('技术部', '研发部');

定期备份表的实践

-- 带时间戳的备份表名
SET @backup_table = CONCAT('employees_backup_', DATE_FORMAT(NOW(), '%Y%m%d_%H%i%s'));

-- 创建备份(需要使用预处理语句)
SET @sql = CONCAT('CREATE TABLE ', @backup_table, ' AS SELECT * FROM employees');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 或者简单方式(手动替换日期)
CREATE TABLE employees_backup_20240115 AS SELECT * FROM employees;

手动重建索引(如果使用AS SELECT):

-- 1. 创建表和数据
CREATE TABLE employees_copy AS SELECT * FROM employees;

-- 2. 手动添加主键
ALTER TABLE employees_copy ADD PRIMARY KEY (emp_id);

-- 3. 手动添加索引
CREATE INDEX idx_department ON employees_copy(department);
CREATE INDEX idx_salary ON employees_copy(salary);

-- 4. 查看原表的所有索引
SHOW INDEX FROM employees;

-- 5. 根据原表的索引定义,逐一创建

跨数据库复制

-- 方法1:在同一MySQL服务器上
CREATE TABLE new_database.employees AS
SELECT * FROM old_database.employees;

-- 方法2:使用mysqldump(命令行)
-- mysqldump -u用户名 -p old_database employees > employees.sql
-- mysql -u用户名 -p new_database < employees.sql

-- 方法3:使用INSERT INTO ... SELECT
INSERT INTO new_database.employees
SELECT * FROM old_database.employees;

题目62:删除重复的部门记录,只保留ID最小的

💡 解题思路

这道题考查去重删除的技巧。需要:

  1. 识别重复记录(通常基于某些字段)
  2. 确定保留哪条记录(ID最小的)
  3. 删除其他重复记录

🔍 关键提示

  • 使用GROUP BY和HAVING找出重复记录
  • 使用子查询确定要删除的ID
  • 可以使用NOT IN或LEFT JOIN实现

📝 实现步骤

  1. 找出重复的部门名称
  2. 对每组重复记录,找出ID最小的
  3. 删除ID不是最小的记录
  4. 验证结果

✅ 验证方法

-- 检查是否还有重复
SELECT dept_name, COUNT(*) AS cnt
FROM departments
GROUP BY dept_name
HAVING COUNT(*) > 1;

🚀 扩展思考

  • 如何基于多个字段判断重复?
  • 如何保留最新的记录而不是ID最小的?
  • 大数据量时如何高效去重?
💯 点击查看参考答案

方法1:使用NOT IN子查询(推荐)

-- 第1步:查看重复记录
SELECT dept_name, COUNT(*) AS duplicate_count
FROM departments
GROUP BY dept_name
HAVING COUNT(*) > 1;

-- 第2步:查看要删除的记录
SELECT d1.*
FROM departments d1
WHERE d1.dept_id NOT IN (
    SELECT MIN(d2.dept_id)
    FROM departments d2
    GROUP BY d2.dept_name
);

-- 第3步:执行删除
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT * FROM (
        SELECT MIN(dept_id)
        FROM departments
        GROUP BY dept_name
    ) AS temp
);

方法2:使用LEFT JOIN

DELETE d1
FROM departments d1
LEFT JOIN (
    SELECT dept_name, MIN(dept_id) AS min_id
    FROM departments
    GROUP BY dept_name
) d2 ON d1.dept_id = d2.min_id
WHERE d2.min_id IS NULL;

方法3:使用窗口函数(MySQL 8.0+)

-- 标记要删除的记录
DELETE FROM departments
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT
            dept_id,
            ROW_NUMBER() OVER (PARTITION BY dept_name ORDER BY dept_id) AS rn
        FROM departments
    ) AS ranked
    WHERE rn > 1
);

方法4:创建临时表去重

-- 第1步:创建去重后的临时表
CREATE TABLE departments_temp AS
SELECT MIN(dept_id) AS dept_id, dept_name, location, budget
FROM departments
GROUP BY dept_name, location, budget;

-- 第2步:删除原表
DROP TABLE departments;

-- 第3步:重命名临时表
RENAME TABLE departments_temp TO departments;

-- 第4步:重建主键和索引
ALTER TABLE departments ADD PRIMARY KEY (dept_id);

方法5:使用EXISTS

DELETE FROM departments d1
WHERE EXISTS (
    SELECT 1
    FROM departments d2
    WHERE d2.dept_name = d1.dept_name
      AND d2.dept_id < d1.dept_id
);

要点说明

  • 去重逻辑:保留每组中dept_id最小的记录
  • 子查询嵌套:MySQL限制不能在FROM子句中直接引用要修改的表,需要再嵌套一层
  • 性能考虑:大数据量时使用临时表方法更高效
  • 数据安全:删除前务必备份或在事务中操作

完整的安全去重流程

-- 1. 备份原表
CREATE TABLE departments_backup AS SELECT * FROM departments;

-- 2. 查看重复情况
SELECT
    dept_name,
    COUNT(*) AS count,
    GROUP_CONCAT(dept_id ORDER BY dept_id) AS ids
FROM departments
GROUP BY dept_name
HAVING COUNT(*) > 1;

-- 3. 在事务中执行删除
START TRANSACTION;

-- 删除重复记录(保留ID最小的)
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT * FROM (
        SELECT MIN(dept_id)
        FROM departments
        GROUP BY dept_name
    ) AS temp
);

-- 4. 验证结果
SELECT dept_name, COUNT(*) FROM departments GROUP BY dept_name HAVING COUNT(*) > 1;
-- 应该返回空,说明没有重复了

-- 5. 确认无误后提交
COMMIT;

基于多个字段判断重复

-- 假设dept_name和location组合唯一
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT * FROM (
        SELECT MIN(dept_id)
        FROM departments
        GROUP BY dept_name, location
    ) AS temp
);

保留最新记录(ID最大)

-- 保留ID最大的,删除其他
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT * FROM (
        SELECT MAX(dept_id)
        FROM departments
        GROUP BY dept_name
    ) AS temp
);

保留最近创建的记录

-- 假设有created_at字段
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT * FROM (
        SELECT dept_id
        FROM (
            SELECT
                dept_id,
                ROW_NUMBER() OVER (
                    PARTITION BY dept_name
                    ORDER BY created_at DESC
                ) AS rn
            FROM departments
        ) AS ranked
        WHERE rn = 1
    ) AS temp
);

大数据量优化方案

-- 方案1:分批删除
SET @batch_size = 1000;
SET @deleted = 1;

WHILE @deleted > 0 DO
    DELETE FROM departments
    WHERE dept_id IN (
        SELECT dept_id FROM (
            SELECT d1.dept_id
            FROM departments d1
            WHERE EXISTS (
                SELECT 1
                FROM departments d2
                WHERE d2.dept_name = d1.dept_name
                  AND d2.dept_id < d1.dept_id
            )
            LIMIT @batch_size
        ) AS temp
    );

    SET @deleted = ROW_COUNT();
END WHILE;

添加唯一约束防止以后重复

-- 去重后添加唯一约束
ALTER TABLE departments
ADD UNIQUE INDEX uk_dept_name (dept_name);

-- 或使用UNIQUE约束
ALTER TABLE departments
ADD CONSTRAINT uk_dept_name UNIQUE (dept_name);

题目63:更新员工表,将没有部门的员工分配到'未分配部门'

💡 解题思路

这道题考查处理NULL值和关联更新。需要:

  1. 识别没有部门的员工(dept_id IS NULL)
  2. 找到或创建'未分配部门'
  3. 更新员工的部门ID

🔍 关键提示

  • 使用IS NULL判断空值(不能用= NULL
  • 可能需要先确保'未分配部门'存在
  • 可以使用子查询获取部门ID

📝 实现步骤

  1. 检查是否有没有部门的员工
  2. 确保'未分配部门'存在,不存在则创建
  3. 更新员工记录,设置dept_id
  4. 验证更新结果

✅ 验证方法

-- 更新前
SELECT COUNT(*) FROM employees WHERE dept_id IS NULL;
-- 执行UPDATE
-- 更新后
SELECT COUNT(*) FROM employees WHERE dept_id IS NULL;  -- 应该为0

🚀 扩展思考

  • 如何同时处理其他默认值?
  • 如何避免硬编码部门ID?
  • 如果部门表没有'未分配部门'怎么办?
💯 点击查看参考答案

方法1:先创建部门再更新(推荐)

-- 第1步:检查'未分配部门'是否存在,不存在则创建
INSERT INTO departments (dept_name, location, budget)
SELECT '未分配部门', '总部', 0
WHERE NOT EXISTS (
    SELECT 1 FROM departments WHERE dept_name = '未分配部门'
);

-- 第2步:更新员工记录
UPDATE employees
SET dept_id = (
    SELECT dept_id
    FROM departments
    WHERE dept_name = '未分配部门'
    LIMIT 1
)
WHERE dept_id IS NULL;

方法2:使用INSERT IGNORE

-- 第1步:插入'未分配部门'(如果已存在则忽略)
INSERT IGNORE INTO departments (dept_name, location, budget)
VALUES ('未分配部门', '总部', 0);

-- 第2步:更新员工
UPDATE employees
SET dept_id = (SELECT dept_id FROM departments WHERE dept_name = '未分配部门')
WHERE dept_id IS NULL;

方法3:在事务中完成

START TRANSACTION;

-- 确保'未分配部门'存在
INSERT INTO departments (dept_name, location, budget)
SELECT '未分配部门', '总部', 0
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM departments WHERE dept_name = '未分配部门'
);

-- 更新员工
UPDATE employees e
SET e.dept_id = (
    SELECT d.dept_id
    FROM departments d
    WHERE d.dept_name = '未分配部门'
    LIMIT 1
)
WHERE e.dept_id IS NULL OR e.dept_id NOT IN (SELECT dept_id FROM departments);

-- 验证
SELECT COUNT(*) AS unassigned_count FROM employees WHERE dept_id IS NULL;

COMMIT;

方法4:使用变量存储部门ID

-- 第1步:获取或创建'未分配部门'的ID
INSERT INTO departments (dept_name, location, budget)
SELECT '未分配部门', '总部', 0
WHERE NOT EXISTS (SELECT 1 FROM departments WHERE dept_name = '未分配部门');

SET @unassigned_dept_id = (
    SELECT dept_id FROM departments WHERE dept_name = '未分配部门' LIMIT 1
);

-- 第2步:更新员工
UPDATE employees
SET dept_id = @unassigned_dept_id
WHERE dept_id IS NULL;

方法5:使用JOIN更新

-- 先确保部门存在
INSERT IGNORE INTO departments (dept_name) VALUES ('未分配部门');

-- 使用JOIN更新
UPDATE employees e
CROSS JOIN (
    SELECT dept_id FROM departments WHERE dept_name = '未分配部门' LIMIT 1
) d
SET e.dept_id = d.dept_id
WHERE e.dept_id IS NULL;

方法6:同时处理其他默认值

-- 确保未分配部门存在
INSERT IGNORE INTO departments (dept_name, location, budget)
VALUES ('未分配部门', '总部', 0);

-- 更新员工,同时设置其他默认值
UPDATE employees
SET
    dept_id = (SELECT dept_id FROM departments WHERE dept_name = '未分配部门'),
    updated_at = NOW(),
    status = 'pending_assignment'
WHERE dept_id IS NULL;

要点说明

  • NULL判断:必须使用IS NULLIS NOT NULL,不能用= NULL
  • 先确保存在:更新前先确保'未分配部门'存在
  • 事务保护:在事务中执行,保证数据一致性
  • 避免硬编码:使用子查询获取dept_id而不是硬编码数字

NULL值判断的正确方式

-- ❌ 错误:不能用等号判断NULL
WHERE dept_id = NULL    -- 永远返回FALSE
WHERE dept_id != NULL   -- 永远返回FALSE

-- ✅ 正确:使用IS NULL
WHERE dept_id IS NULL
WHERE dept_id IS NOT NULL

-- 也可以使用IFNULL或COALESCE
WHERE IFNULL(dept_id, 0) = 0
WHERE COALESCE(dept_id, 0) = 0

完整的安全更新流程

-- 1. 查看当前状况
SELECT
    COUNT(*) AS total_employees,
    SUM(CASE WHEN dept_id IS NULL THEN 1 ELSE 0 END) AS no_dept_count,
    SUM(CASE WHEN dept_id IS NOT NULL THEN 1 ELSE 0 END) AS has_dept_count
FROM employees;

-- 2. 查看没有部门的员工列表
SELECT emp_id, name, dept_id FROM employees WHERE dept_id IS NULL;

-- 3. 开启事务
START TRANSACTION;

-- 4. 确保'未分配部门'存在
INSERT INTO departments (dept_name, location, budget)
SELECT '未分配部门', '总部', 0
WHERE NOT EXISTS (
    SELECT 1 FROM departments WHERE dept_name = '未分配部门'
);

-- 5. 更新员工
UPDATE employees
SET dept_id = (
    SELECT dept_id FROM departments WHERE dept_name = '未分配部门' LIMIT 1
)
WHERE dept_id IS NULL;

-- 6. 验证结果
SELECT COUNT(*) AS still_null FROM employees WHERE dept_id IS NULL;
-- 应该返回0

SELECT * FROM employees
WHERE dept_id = (SELECT dept_id FROM departments WHERE dept_name = '未分配部门');

-- 7. 确认无误后提交
COMMIT;

处理部门ID不存在于部门表的情况

-- 更全面的更新:包括NULL和无效的dept_id
UPDATE employees e
SET e.dept_id = (
    SELECT dept_id FROM departments WHERE dept_name = '未分配部门' LIMIT 1
)
WHERE e.dept_id IS NULL
   OR e.dept_id NOT IN (SELECT dept_id FROM departments);

添加约束防止NULL(更新后):

-- 更新完成后,可以添加NOT NULL约束
ALTER TABLE employees
MODIFY dept_id INT NOT NULL;

-- 或添加外键约束
ALTER TABLE employees
ADD CONSTRAINT fk_dept
FOREIGN KEY (dept_id) REFERENCES departments(dept_id);

题目64:使用事务完成:插入新员工,同时更新部门人数

💡 解题思路

这道题考查事务的使用。需要:

  1. 理解事务的ACID特性
  2. 将多个操作放在一个事务中
  3. 正确使用BEGIN/START TRANSACTION、COMMIT、ROLLBACK

🔍 关键提示

  • START TRANSACTIONBEGIN 开启事务
  • COMMIT 提交事务
  • ROLLBACK 回滚事务
  • 事务中的操作要么全部成功,要么全部失败

📝 实现步骤

  1. 开启事务
  2. 插入新员工记录
  3. 更新对应部门的人数
  4. 验证结果
  5. 提交或回滚

✅ 验证方法

-- 检查员工是否插入成功
SELECT * FROM employees WHERE name = '新员工姓名';
-- 检查部门人数是否更新
SELECT dept_id, emp_count FROM departments WHERE dept_id = X;

🚀 扩展思考

  • 事务的隔离级别有哪些?
  • 什么情况下事务会自动回滚?
  • 如何处理事务中的错误?
💯 点击查看参考答案

方法1:基本事务(推荐)

-- 第1步:开启事务
START TRANSACTION;

-- 第2步:插入新员工
INSERT INTO employees (name, department, salary, hire_date, dept_id)
VALUES ('张新', '技术部', 8000.00, CURDATE(), 1);

-- 获取新插入的员工ID(可选)
SET @new_emp_id = LAST_INSERT_ID();

-- 第3步:更新部门人数
UPDATE departments
SET emp_count = emp_count + 1
WHERE dept_id = 1;

-- 第4步:验证结果
SELECT * FROM employees WHERE emp_id = @new_emp_id;
SELECT dept_id, dept_name, emp_count FROM departments WHERE dept_id = 1;

-- 第5步:如果一切正常,提交事务
COMMIT;

-- 如果发现问题,可以执行
-- ROLLBACK;

方法2:使用savepoint(保存点)

START TRANSACTION;

-- 插入员工
INSERT INTO employees (name, dept_id, salary, hire_date)
VALUES ('李新', 2, 7500.00, CURDATE());

SAVEPOINT after_insert;

-- 更新部门人数
UPDATE departments
SET emp_count = emp_count + 1
WHERE dept_id = 2;

-- 如果这步出错,可以回滚到savepoint
-- ROLLBACK TO after_insert;

-- 全部成功则提交
COMMIT;

方法3:使用存储过程封装

DELIMITER //

CREATE PROCEDURE add_employee_and_update_dept(
    IN p_name VARCHAR(50),
    IN p_dept_id INT,
    IN p_salary DECIMAL(10,2),
    IN p_hire_date DATE
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Error occurred, transaction rolled back' AS result;
    END;

    START TRANSACTION;

    -- 插入员工
    INSERT INTO employees (name, dept_id, salary, hire_date)
    VALUES (p_name, p_dept_id, p_salary, p_hire_date);

    -- 更新部门人数
    UPDATE departments
    SET emp_count = emp_count + 1
    WHERE dept_id = p_dept_id;

    COMMIT;

    SELECT 'Transaction committed successfully' AS result;
END //

DELIMITER ;

-- 调用存储过程
CALL add_employee_and_update_dept('王新', 3, 9000.00, '2024-01-15');

方法4:完整的错误处理

START TRANSACTION;

-- 检查部门是否存在
SELECT dept_id INTO @dept_exists
FROM departments
WHERE dept_id = 1;

IF @dept_exists IS NOT NULL THEN
    -- 插入员工
    INSERT INTO employees (name, dept_id, salary, hire_date)
    VALUES ('赵新', 1, 8500.00, CURDATE());

    -- 更新部门人数
    UPDATE departments
    SET emp_count = emp_count + 1
    WHERE dept_id = 1;

    -- 记录操作日志
    INSERT INTO operation_log (operation, table_name, record_id, created_at)
    VALUES ('INSERT', 'employees', LAST_INSERT_ID(), NOW());

    COMMIT;
ELSE
    -- 部门不存在,回滚
    ROLLBACK;
    SELECT 'Department not found, transaction rolled back' AS error;
END IF;

方法5:自动计算部门人数(触发器方式)

-- 创建触发器(更优雅的方案)
DELIMITER //

CREATE TRIGGER after_employee_insert
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
    UPDATE departments
    SET emp_count = emp_count + 1
    WHERE dept_id = NEW.dept_id;
END //

CREATE TRIGGER after_employee_delete
AFTER DELETE ON employees
FOR EACH ROW
BEGIN
    UPDATE departments
    SET emp_count = emp_count - 1
    WHERE dept_id = OLD.dept_id;
END //

DELIMITER ;

-- 之后只需要插入员工即可,部门人数自动更新
START TRANSACTION;
INSERT INTO employees (name, dept_id, salary, hire_date)
VALUES ('孙新', 1, 7000.00, CURDATE());
COMMIT;

要点说明

  • 原子性:事务中的所有操作要么全部成功,要么全部失败
  • 一致性:事务执行前后数据保持一致状态
  • 隔离性:并发事务互不干扰
  • 持久性:提交后的修改永久保存

事务的使用场景

-- 场景1:转账操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

-- 场景2:订单创建
START TRANSACTION;
INSERT INTO orders (user_id, total_amount) VALUES (1, 500);
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity) VALUES (@order_id, 10, 2);
UPDATE products SET stock = stock - 2 WHERE product_id = 10;
COMMIT;

-- 场景3:批量操作
START TRANSACTION;
DELETE FROM temp_data WHERE processed = 1;
INSERT INTO main_data SELECT * FROM temp_data;
UPDATE temp_data SET processed = 1;
COMMIT;

事务隔离级别

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;  -- 读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;    -- 读已提交
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;   -- 可重复读(MySQL默认)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;      -- 串行化

完整的事务最佳实践

START TRANSACTION;

-- 1. 验证前置条件
SELECT COUNT(*) INTO @dept_exists
FROM departments
WHERE dept_id = 1;

IF @dept_exists = 0 THEN
    ROLLBACK;
    SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Department does not exist';
END IF;

-- 2. 执行业务逻辑
INSERT INTO employees (name, dept_id, salary, hire_date)
VALUES ('新员工', 1, 8000.00, CURDATE());

SET @new_emp_id = LAST_INSERT_ID();

UPDATE departments
SET emp_count = emp_count + 1,
    updated_at = NOW()
WHERE dept_id = 1;

-- 3. 验证结果
SELECT emp_count INTO @new_count
FROM departments
WHERE dept_id = 1;

-- 4. 记录审计日志
INSERT INTO audit_log (operation, user_id, details, created_at)
VALUES ('ADD_EMPLOYEE', 1, CONCAT('Added employee ID: ', @new_emp_id), NOW());

-- 5. 提交
COMMIT;

-- 返回结果
SELECT
    @new_emp_id AS new_employee_id,
    @new_count AS department_employee_count;

错误处理示例

DELIMITER //

CREATE PROCEDURE safe_add_employee(
    IN p_name VARCHAR(50),
    IN p_dept_id INT,
    IN p_salary DECIMAL(10,2)
)
BEGIN
    DECLARE v_error INT DEFAULT 0;
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET v_error = 1;

    START TRANSACTION;

    INSERT INTO employees (name, dept_id, salary, hire_date)
    VALUES (p_name, p_dept_id, p_salary, CURDATE());

    UPDATE departments
    SET emp_count = emp_count + 1
    WHERE dept_id = p_dept_id;

    IF v_error = 1 THEN
        ROLLBACK;
        SELECT 'Error: Transaction rolled back' AS result;
    ELSE
        COMMIT;
        SELECT 'Success: Employee added and department updated' AS result;
    END IF;
END //

DELIMITER ;

题目65:删除没有任何员工的部门

💡 解题思路

这道题考查关联删除和子查询的使用。需要:

  1. 找出没有员工的部门
  2. 使用子查询或JOIN实现
  3. 安全地删除这些部门

🔍 关键提示

  • 使用NOT EXISTS或NOT IN子查询
  • 或使用LEFT JOIN找出没有匹配的部门
  • 删除前务必验证

📝 实现步骤

  1. 查询没有员工的部门列表
  2. 验证查询结果是否正确
  3. 执行删除操作
  4. 验证删除结果

✅ 验证方法

-- 删除前查看
SELECT d.* FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;
-- 删除后验证
-- 再次执行上面的查询,应该返回空

🚀 扩展思考

  • 如何同时删除部门的其他关联数据?
  • 如何实现软删除?
  • 大量数据时如何优化性能?
💯 点击查看参考答案

方法1:使用NOT EXISTS(推荐)

-- 第1步:查看要删除的部门
SELECT *
FROM departments d
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.dept_id = d.dept_id
);

-- 第2步:执行删除
DELETE FROM departments
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.dept_id = departments.dept_id
);

方法2:使用NOT IN子查询

-- 查看要删除的部门
SELECT * FROM departments
WHERE dept_id NOT IN (
    SELECT DISTINCT dept_id
    FROM employees
    WHERE dept_id IS NOT NULL
);

-- 执行删除
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT DISTINCT dept_id
    FROM employees
    WHERE dept_id IS NOT NULL
);

方法3:使用LEFT JOIN

-- 查看要删除的部门
SELECT d.*
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;

-- 执行删除(需要使用子查询)
DELETE FROM departments
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT d.dept_id
        FROM departments d
        LEFT JOIN employees e ON d.dept_id = e.dept_id
        WHERE e.emp_id IS NULL
    ) AS temp
);

方法4:使用聚合查询

-- 统计每个部门的员工数,删除员工数为0的
DELETE FROM departments
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT d.dept_id
        FROM departments d
        LEFT JOIN employees e ON d.dept_id = e.dept_id
        GROUP BY d.dept_id
        HAVING COUNT(e.emp_id) = 0
    ) AS temp
);

方法5:在事务中安全删除

START TRANSACTION;

-- 查看要删除的部门
SELECT
    d.dept_id,
    d.dept_name,
    COUNT(e.emp_id) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING COUNT(e.emp_id) = 0;

-- 执行删除
DELETE FROM departments
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT d.dept_id
        FROM departments d
        LEFT JOIN employees e ON d.dept_id = e.dept_id
        GROUP BY d.dept_id
        HAVING COUNT(e.emp_id) = 0
    ) AS temp
);

-- 验证
SELECT COUNT(*) AS deleted_count FROM departments
WHERE NOT EXISTS (
    SELECT 1 FROM employees WHERE dept_id = departments.dept_id
);
-- 应该返回0

COMMIT;

方法6:同时删除相关数据

START TRANSACTION;

-- 删除没有员工的部门的其他关联数据
DELETE FROM dept_budgets
WHERE dept_id IN (
    SELECT dept_id FROM (
        SELECT d.dept_id
        FROM departments d
        LEFT JOIN employees e ON d.dept_id = e.dept_id
        WHERE e.emp_id IS NULL
    ) AS temp
);

-- 删除部门本身
DELETE FROM departments
WHERE NOT EXISTS (
    SELECT 1 FROM employees WHERE dept_id = departments.dept_id
);

COMMIT;

要点说明

  • EXISTS vs IN:EXISTS性能通常更好,找到第一条匹配即停止
  • NULL处理:使用NOT IN时要注意NULL值的影响
  • 子查询嵌套:MySQL不允许直接在FROM中引用被修改的表,需要再嵌套一层
  • 验证重要:删除前务必验证查询结果

NOT EXISTS vs NOT IN 性能对比

-- ✅ 推荐:NOT EXISTS(性能更好)
DELETE FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id
);

-- ⚠️ 注意:NOT IN(要处理NULL)
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT dept_id FROM employees WHERE dept_id IS NOT NULL
);
-- 如果子查询中有NULL,NOT IN可能返回意外结果

NOT IN 的 NULL 陷阱

-- 假设employees表中有dept_id为NULL的记录
SELECT dept_id FROM employees;
-- 结果:1, 2, NULL

-- 这个查询可能不会删除任何记录!
DELETE FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM employees);

-- 原因:dept_id NOT IN (1, 2, NULL) 等价于
-- dept_id != 1 AND dept_id != 2 AND dept_id != NULL
-- 而 dept_id != NULL 永远返回 NULL(不是TRUE也不是FALSE)

-- ✅ 正确做法:过滤NULL
DELETE FROM departments
WHERE dept_id NOT IN (
    SELECT dept_id FROM employees WHERE dept_id IS NOT NULL
);

完整的安全删除流程

-- 1. 备份要删除的部门
CREATE TABLE departments_to_delete AS
SELECT d.*
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;

-- 2. 查看要删除的部门列表
SELECT * FROM departments_to_delete;

-- 3. 统计删除前后的数量
SELECT
    (SELECT COUNT(*) FROM departments) AS before_count,
    (SELECT COUNT(*) FROM departments_to_delete) AS will_delete,
    (SELECT COUNT(*) FROM departments) - (SELECT COUNT(*) FROM departments_to_delete) AS after_count;

-- 4. 在事务中执行删除
START TRANSACTION;

DELETE FROM departments
WHERE NOT EXISTS (
    SELECT 1 FROM employees WHERE dept_id = departments.dept_id
);

-- 5. 验证结果
SELECT ROW_COUNT() AS deleted_count;

-- 6. 再次检查
SELECT * FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
WHERE e.emp_id IS NULL;
-- 应该返回空

-- 7. 确认无误后提交
COMMIT;

软删除实现

-- 不真正删除,只标记
UPDATE departments d
SET
    is_active = 0,
    deleted_at = NOW()
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id
);

-- 查询时过滤掉已删除的部门
SELECT * FROM departments WHERE is_active = 1;

级联删除(如果有外键)

-- 如果设置了外键的ON DELETE CASCADE
-- 删除部门会自动删除相关的员工(危险!)

-- 更安全的做法:先检查是否有员工,有则不删除
DELETE FROM departments d
WHERE NOT EXISTS (
    SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id
)
AND NOT EXISTS (
    SELECT 1 FROM dept_projects p WHERE p.dept_id = d.dept_id
)
AND NOT EXISTS (
    SELECT 1 FROM dept_budgets b WHERE b.dept_id = d.dept_id
);

第七关学习总结

恭喜完成第七关的学习!通过这10道题目,你已经掌握了:

核心知识点回顾

  1. INSERT - 单条插入、批量插入、性能优化
  2. UPDATE - 单表更新、关联更新、条件更新
  3. DELETE - 条件删除、关联删除、去重删除
  4. 事务 - ACID特性、BEGIN/COMMIT/ROLLBACK、错误处理
  5. 数据安全 - 验证先行、事务保护、备份恢复

最佳实践要点

  • ✅ UPDATE/DELETE前必须先用SELECT验证WHERE条件
  • ✅ 重要操作使用事务保护,便于回滚
  • ✅ 批量INSERT性能远优于单条INSERT
  • ✅ 处理NULL值要用IS NULL,不能用= NULL
  • ✅ 大数据量操作要分批进行,避免锁表

后续关卡预告

接下来的关卡将学习:

  • 第八关:表设计与索引 - 约束、索引设计、ALTER TABLE
  • 第九关:性能优化 - EXPLAIN、慢查询优化、索引优化
  • 第十关:实战综合 - 综合项目实战

第八关:表设计与索引 - 学习指南

题目66:创建一个订单表,包含订单号、客户ID、订单日期、总金额

💡 解题思路

这题考查表结构设计 + CREATE TABLE 语法。要点:

  1. 订单号一般用自增主键或业务单号,这里使用自增整型主键;
  2. 客户ID通常是与用户表/客户表关联的外键(本题可先不强制外键,只保留字段);
  3. 订单日期用 DATEDATETIME,视业务需要;
  4. 总金额用 DECIMAL(10,2) 等定点数表达金额。

🔍 关键提示

  • 使用 CREATE TABLE orders (...)
  • order_id INT PRIMARY KEY AUTO_INCREMENT
  • customer_id 使用 INTBIGINT
  • order_date 可使用 DATETIME
  • total_amount DECIMAL(10,2) NOT NULL

📝 实现步骤

  1. 写出建表框架:CREATE TABLE orders (...);
  2. 添加 order_id 字段:整型、自增、主键;
  3. 添加 customer_id 字段:整型,可以允许为空或不为空视需求;
  4. 添加 order_date 字段:DATETIMETIMESTAMP
  5. 添加 total_amount 字段:DECIMAL(10,2) 并设置非空;
  6. 选择合适的存储引擎和字符集(如 InnoDB + utf8mb4)。

✅ 验证方法

DESC orders;
SHOW CREATE TABLE orders\G

检查:

  • 是否有4个字段;
  • 主键、自增、类型是否符合预期。

🚀 扩展思考

  • 如果系统中已经有 customers 表,应该如何增加外键约束?
  • 订单状态(待支付/已支付/已发货等)应该放在本表还是单独拆表?
  • 订单号是否需要使用有业务含义的字符串(如 20250101-0001),如何与主键关系设计?
💯 点击查看参考答案
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    order_date DATETIME NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

要点说明

  • 使用自增 order_id 作为主键,避免用有业务含义的字段做主键;
  • DECIMAL(10,2) 适合金额计算,避免浮点误差;
  • 实际项目中通常还会增加 statuscreated_atupdated_at 等字段。

题目67:为员工表的 name 字段添加索引

💡 解题思路

这题考查添加单列索引。要点:

  1. 确认表名为 employees,字段为 name
  2. 选择索引创建方式:CREATE INDEXALTER TABLE ... ADD INDEX
  3. 注意索引命名规范(建议加表名前缀,如 idx_employees_name)。

🔍 关键提示

  • 语法1:CREATE INDEX 索引名 ON 表名(字段);
  • 语法2:ALTER TABLE 表名 ADD INDEX 索引名 (字段);
  • 索引名建议:idx_表名_字段名

📝 实现步骤

  1. 写出索引名:idx_employees_name
  2. 选择 CREATE INDEX 方式;
  3. employees(name) 上创建索引;
  4. 使用 SHOW INDEX 验证。

✅ 验证方法

SHOW INDEX FROM employees;

确认:

  • 存在 idx_employees_name 索引;
  • Column_namename

🚀 扩展思考

  • name 建索引是否总是合适?如果表很小或模糊查询很多,会怎样?
  • 什么时候应改用前缀索引(如 name(10))?
  • 当同时在 nameemail 上建索引时,有没有必要建复合索引?
💯 点击查看参考答案
-- 推荐写法:CREATE INDEX
CREATE INDEX idx_employees_name ON employees(name);

-- 等价写法:ALTER TABLE
ALTER TABLE employees ADD INDEX idx_employees_name (name);

要点说明

  • 两种写法效果相同,推荐 CREATE INDEX 更直观;
  • 命名规范可以提升可读性和维护性。

题目68:为员工表添加一个'手机号'字段,要求唯一且非空

💡 解题思路

这题考查 ALTER TABLE 增加字段 + 约束

  1. 使用 ALTER TABLE ... ADD COLUMN 增加 phone 字段;
  2. NOT NULL 保证非空;
  3. UNIQUE 保证唯一性。

🔍 关键提示

  • ALTER TABLE employees ADD COLUMN phone VARCHAR(20) NOT NULL UNIQUE;
  • 可以使用 AFTER 某字段 指定插入位置(可选);
  • 唯一约束可以在字段级或表级声明。

📝 实现步骤

  1. 选择合适的数据类型:VARCHAR(20)
  2. 决定字段名:phone
  3. 使用 ADD COLUMN 增加字段,并加上 NOT NULL UNIQUE
  4. 可选:指定放在 email 字段后;
  5. 使用 DESCSHOW CREATE TABLE 验证。

✅ 验证方法

DESC employees;
SHOW CREATE TABLE employees\G

再尝试插入两条相同手机号的记录,第二条应报错。

🚀 扩展思考

  • 如果线上已有重复手机号的数据,直接加 UNIQUE 会发生什么?
  • 如何先清洗重复数据,再添加唯一约束?
  • 如果一个员工允许有多个手机号,应如何设计表结构?
💯 点击查看参考答案
ALTER TABLE employees
ADD COLUMN phone VARCHAR(20) NOT NULL UNIQUE
AFTER email;

要点说明

  • NOT NULL UNIQUE 即可实现“非空且唯一”;
  • AFTER email 只影响展示顺序,不影响逻辑。

题目69:将员工表的 salary 字段类型改为 DECIMAL(12,2)

💡 解题思路

这题考查 修改字段类型

  1. 使用 ALTER TABLE ... MODIFY
  2. 保持字段名不变,只修改类型;
  3. 注意保留原有约束(如 NOT NULLDEFAULT 等)。

🔍 关键提示

  • ALTER TABLE 表名 MODIFY 字段名 新数据类型 [约束];
  • 若原字段有 NOT NULL/DEFAULT,需要在 MODFIY 中一并写出。

📝 实现步骤

  1. 查看当前 salary 字段定义(方便保留约束);
  2. 写出 ALTER TABLE employees MODIFY salary DECIMAL(12,2);
  3. 如有默认值或非空约束,补上相应子句;
  4. 使用 DESC 验证。

✅ 验证方法

DESC employees;

确认 salary 类型为 decimal(12,2)

🚀 扩展思考

  • 金额字段为什么推荐用 DECIMAL 而不是 FLOAT/DOUBLE
  • 如果已有数据,类型变宽(位数变大)会有什么影响?
  • 如果从较大精度改为较小精度,可能造成哪些风险?
💯 点击查看参考答案
ALTER TABLE employees
MODIFY salary DECIMAL(12,2);

如果原字段是非空且默认0,可以写为:

ALTER TABLE employees
MODIFY salary DECIMAL(12,2) NOT NULL DEFAULT 0;

题目70:创建一个复合索引 (dept_id, salary)

💡 解题思路

考查复合索引 + 最左前缀原则

  1. 目标表为 employees,字段为 dept_id, salary
  2. 使用 CREATE INDEXALTER TABLE ... ADD INDEX
  3. 理解该索引适用于哪些查询模式。

🔍 关键提示

  • CREATE INDEX idx_dept_salary ON employees(dept_id, salary);
  • 该索引可以支持:
    • WHERE dept_id = ? AND salary > ?
    • WHERE dept_id = ?
  • 不适用于 WHERE salary > ? 单独条件(因为违反最左前缀)。

📝 实现步骤

  1. employees 创建索引 idx_dept_salary
  2. 使用 SHOW INDEX 验证;
  3. 使用 EXPLAIN 对比创建前后的执行计划。

✅ 验证方法

SHOW INDEX FROM employees;

EXPLAIN SELECT * FROM employees
WHERE dept_id = 1 AND salary > 8000;

🚀 扩展思考

  • 如果查询条件经常是 salary > ? 而不带 dept_id,这个索引是否合适?
  • 多列索引与两个单列索引(dept_id / salary)有什么区别?
  • 如果经常按 dept_id, salary 排序,这个索引有什么优势?
💯 点击查看参考答案
CREATE INDEX idx_dept_salary
ON employees(dept_id, salary);

要点说明

  • 复合索引的字段顺序很重要,一般将区分度更高、过滤更频繁的字段放前面;
  • WHERE dept_id = ? AND salary > ? 场景下效果最好。

题目71:查看 employees 表的所有索引

💡 解题思路

此题考查查看索引信息

  1. 使用 SHOW INDEX FROM 表名
  2. 理解关键字段含义:Key_name, Column_name, Non_unique 等。

🔍 关键提示

  • SHOW INDEX FROM employees;
  • 也可用 SHOW INDEXESSHOW KEYS

📝 实现步骤

  1. 直接执行 SHOW INDEX FROM employees;
  2. 观察输出中的索引名和对应字段;
  3. 判断哪些是主键、哪些是普通索引或唯一索引。

✅ 验证方法

查看结果中:

  • Key_name = PRIMARY 为主键索引;
  • Non_unique = 0 表示唯一索引;
  • 是否包含刚刚创建的 idx_employees_nameidx_dept_salary 等。

🚀 扩展思考

  • 如何只查看某个索引的定义?
  • 如何判断一个索引是否重复/冗余(例如覆盖关系)?
💯 点击查看参考答案
SHOW INDEX FROM employees;
-- 或
SHOW INDEXES FROM employees;
-- 或
SHOW KEYS FROM employees;

输出字段说明(部分)

  • Key_name:索引名;
  • Column_name:使用该索引的列;
  • Non_unique:0 表示唯一索引,1 表示非唯一索引。

题目72:删除 employees 表中的 name 索引

💡 解题思路

考查删除索引

  1. 先确认索引名称(如 idx_employees_name);
  2. 使用 DROP INDEXALTER TABLE ... DROP INDEX
  3. 注意不要误删主键索引。

🔍 关键提示

  • DROP INDEX 索引名 ON 表名;
  • ALTER TABLE 表名 DROP INDEX 索引名;
  • 若最初索引名就是 name,则按实际名称删除。

📝 实现步骤

  1. 通过 SHOW INDEX 找到 name 字段的索引名;
  2. 使用 DROP INDEX 语句删除;
  3. 再次 SHOW INDEX 验证是否已删除。

✅ 验证方法

SHOW INDEX FROM employees;

确认 Key_name = idx_employees_name(或真实索引名)已经不存在。

🚀 扩展思考

  • 删除索引会对正在执行的查询产生什么影响?
  • 在高并发生产环境中删除大型索引是否会引起阻塞?
💯 点击查看参考答案

假设索引名为 idx_employees_name

DROP INDEX idx_employees_name ON employees;
-- 或
ALTER TABLE employees DROP INDEX idx_employees_name;

要点说明

  • DROP INDEXALTER TABLE ... DROP INDEX 等价;
  • 删除索引不会删除数据,但会影响相关查询的性能。

题目73:创建一个商品表,包含自动递增的ID和默认值

💡 解题思路

考查建表 + 默认值

  1. 设计商品表字段:idnamepricestock 等;
  2. id 自增主键;
  3. stock 默认 0,created_at 默认当前时间。

🔍 关键提示

  • 使用 AUTO_INCREMENT + PRIMARY KEY
  • 使用 DEFAULT 指定默认值;
  • 一般会加上 created_at/updated_at 字段。

📝 实现步骤

  1. 确定表名:products
  2. 设计字段:
    • id INT PRIMARY KEY AUTO_INCREMENT
    • name VARCHAR(100) NOT NULL
    • price DECIMAL(10,2) NOT NULL DEFAULT 0
    • stock INT NOT NULL DEFAULT 0
    • created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  3. CREATE TABLE 语句并执行;
  4. DESC 验证。

✅ 验证方法

DESC products;
SHOW CREATE TABLE products\G

🚀 扩展思考

  • 如果商品有上下架状态,应该如何设计字段及默认值?
  • 如果希望在更新时自动刷新 updated_at,应如何定义?
💯 点击查看参考答案
CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) NOT NULL DEFAULT 0,
    stock INT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

要点说明

  • 默认值可以减少插入时需要指定的字段;
  • 对于库存类字段,DEFAULT 0 是常见做法。

题目74:为订单表和订单详情表建立外键关系

💡 解题思路

考查 外键约束 + ALTER TABLE

  1. 假设已有 orders(order_id)order_items(order_id, product_id, ...)
  2. order_items 上建立外键指向 orders.order_id
  3. 可选择 ON DELETE CASCADEON DELETE RESTRICT 等策略。

🔍 关键提示

  • 外键通常建在“子表”上(这里是 order_items);
  • 语法:ALTER TABLE 子表 ADD CONSTRAINT 名 FOREIGN KEY(字段) REFERENCES 父表(字段) [ON DELETE ...];
  • 要求父表字段有索引(主键或唯一)。

📝 实现步骤

  1. 确认 orders.order_id 为主键;
  2. order_items 上添加外键:
    • FOREIGN KEY (order_id) REFERENCES orders(order_id)
  3. 视需求决定是否加 ON DELETE CASCADE
  4. 使用 SHOW CREATE TABLE order_items 验证。

✅ 验证方法

SHOW CREATE TABLE order_items\G

并尝试:

  • 插入一个不存在的 order_id,应报错;
  • 删除 orders 中某条记录,观察 order_items 的行为(根据 ON DELETE 策略)。

🚀 扩展思考

  • 实战中是否总是建议使用外键?为什么很多大型系统选择“逻辑外键”而不用数据库外键?
  • 订单与用户、订单与支付记录等关系,外键如何设计?
💯 点击查看参考答案
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_order
FOREIGN KEY (order_id) REFERENCES orders(order_id)
ON DELETE CASCADE;

要点说明

  • ON DELETE CASCADE:删除订单时自动删除对应详情记录(要慎用);
  • 也可以使用 ON DELETE RESTRICT(默认)或 ON DELETE SET NULL

题目75:修改部门表,添加部门描述字段,默认值为'暂无描述'

💡 解题思路

考查 ALTER TABLE 添加字段 + 默认值

  1. departments 表中增加 description 字段;
  2. 类型可选 VARCHAR(255)
  3. 设置 DEFAULT '暂无描述'

🔍 关键提示

  • ALTER TABLE departments ADD COLUMN description VARCHAR(255) NOT NULL DEFAULT '暂无描述';
  • 可用 AFTER 控制字段位置。

📝 实现步骤

  1. 确定字段名、类型和默认值;
  2. 写出 ALTER TABLE 语句;
  3. 使用 DESCSHOW CREATE TABLE 验证。

✅ 验证方法

DESC departments;

插入一条未指定 description 的记录,确认其值为 '暂无描述'

🚀 扩展思考

  • 如果以后需要支持多语言描述(中/英),字段设计如何扩展?
  • 部门描述是否应该拆到独立表(如多语言、多版本说明)?
💯 点击查看参考答案
ALTER TABLE departments
ADD COLUMN description VARCHAR(255) NOT NULL DEFAULT '暂无描述';

要点说明

  • NOT NULL DEFAULT '暂无描述' 保证字段始终有值;
  • 默认值文字要与产品/业务约定一致。

第八关学习总结

恭喜完成第八关的学习!通过这10道题目,你已经掌握了:

核心知识点回顾

  1. CREATE TABLE - 建表语法、字段类型选择、约束设置
  2. ALTER TABLE - 添加字段、修改字段、删除字段、添加约束
  3. 索引设计 - 普通索引、唯一索引、复合索引、索引命名规范
  4. 约束管理 - 主键、外键、唯一约束、非空约束、默认值
  5. 表结构优化 - 数据类型选择、索引设计原则、最左前缀原则

最佳实践要点

  • ✅ 每个表必须有主键,推荐使用自增整型
  • ✅ 金额字段使用DECIMAL,避免浮点数精度问题
  • ✅ 为WHERE、ORDER BY、JOIN中频繁使用的字段建索引
  • ✅ 复合索引遵循最左前缀原则,区分度高的字段放前面
  • ✅ 索引不是越多越好,要平衡查询性能和写入性能

后续关卡预告

接下来的关卡将学习:

  • 第九关:性能优化 - EXPLAIN、慢查询优化、索引优化
  • 第十关:实战综合 - 综合项目实战

第九关:性能优化 - 学习指南

题目76:使用 EXPLAIN 分析一个复杂查询的执行计划

💡 解题思路

本题不要求写出业务 SQL,只要会用 EXPLAIN 包裹原查询并解读关键字段

  1. 在原查询前加上 EXPLAIN
  2. 关注 typekeyrowsExtra 列;
  3. 根据输出判断是否存在全表扫描、临时表、文件排序等问题。

🔍 关键提示

  • 语法:EXPLAIN 原查询;
  • 常见关注点:
    • type 是否为 ALL(全表扫描);
    • key 是否使用了期望的索引;
    • rows 预估扫描行数是否过大;
    • Extra 中是否有 Using filesortUsing temporary

📝 实现步骤

  1. 找到你要分析的复杂查询,例如:
    SELECT e.name, d.dept_name, e.salary
    FROM employees e
    JOIN departments d ON e.dept_id = d.dept_id
    WHERE e.salary > 8000
    ORDER BY e.salary DESC;
    
  2. 在前面加上 EXPLAIN
    EXPLAIN SELECT ...;
    
  3. 观察结果,对照文档理解各列含义;
  4. 根据结果判断是否需要加索引或改写 SQL。

✅ 验证方法

执行:

EXPLAIN SELECT e.name, d.dept_name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 8000
ORDER BY e.salary DESC;

检查:

  • 是否使用了 dept_idsalary 相关索引;
  • type 是否优于 ALL

🚀 扩展思考

  • 尝试在 employees(dept_id, salary) 上加索引,再次 EXPLAIN,对比前后差异;
  • 思考:当 EXPLAIN 显示 Using filesort 时,如何通过索引或 SQL 改写消除?
💯 点击查看参考答案

参考写法:

EXPLAIN
SELECT e.name, d.dept_name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > 8000
ORDER BY e.salary DESC;

解读示例(伪输出):

  • type = ALL:说明是全表扫描,需要考虑加索引;
  • key = NULL:没有使用索引;
  • rows = 100000:预估扫描行数较大;
  • Extra = Using filesort:ORDER BY 触发文件排序,可尝试建合适的索引优化。

题目77:优化查询——查找姓名以 '张' 开头的员工(考虑索引)

💡 解题思路

要点:

  1. 查询语句使用 LIKE '张%',避免前置 %
  2. name 字段创建合适索引(可为整个字段或前缀索引);
  3. 使用 EXPLAIN 验证是否走索引。

🔍 关键提示

  • SQL 形式:
    SELECT * FROM employees WHERE name LIKE '张%';
    
  • name 建索引:CREATE INDEX idx_name ON employees(name);

📝 实现步骤

  1. 建立索引(如尚未建立):
    CREATE INDEX idx_employees_name ON employees(name);
    
  2. 执行查询:
    SELECT * FROM employees WHERE name LIKE '张%';
    
  3. 使用 EXPLAIN 查看是否使用 idx_employees_name 索引。

✅ 验证方法

EXPLAIN SELECT * FROM employees WHERE name LIKE '张%';

检查:

  • type 是否为 range
  • key 是否为 idx_employees_name
  • rows 是否明显减少。

🚀 扩展思考

  • 如果使用 LIKE '%张%',索引会失效,如何替代?(如全文索引、搜索引擎)
  • 对中文名字是否有必要使用前缀索引(如 name(2))?
💯 点击查看参考答案
-- 建索引
CREATE INDEX idx_employees_name ON employees(name);

-- 优化后的查询
SELECT *
FROM employees
WHERE name LIKE '张%';

要点说明

  • 以常量开头的 LIKE '张%' 可以使用普通 B+Tree 索引;
  • % 开头的模糊查询无法利用该索引。

题目78:改写查询以提高性能(YEAR(hire_date) = 2020)

💡 解题思路

原查询:

SELECT * FROM employees WHERE YEAR(hire_date) = 2020;

问题:

  1. 在 WHERE 中对索引字段使用函数,会导致索引失效;
  2. 应改写为日期范围查询。

🔍 关键提示

  • 使用:
    WHERE hire_date >= '2020-01-01'
      AND hire_date <  '2021-01-01'
    
  • 这样可以使用 hire_date 上的索引。

📝 实现步骤

  1. 保留原有业务语义(筛选 2020 年的记录);
  2. 将函数过滤改写为区间过滤;
  3. 若无索引,在 hire_date 上创建索引;
  4. 使用 EXPLAIN 对比优化前后。

✅ 验证方法

EXPLAIN SELECT * FROM employees WHERE YEAR(hire_date) = 2020;

EXPLAIN SELECT * FROM employees
WHERE hire_date >= '2020-01-01'
  AND hire_date <  '2021-01-01';

对比 typekeyrows 等字段。

🚀 扩展思考

  • 对其他日期函数(如 DATE(hire_time)MONTH(...))也要避免在 WHERE 中使用;
  • 如果经常按年份统计,是否需要冗余一列 hire_year 存储年份?
💯 点击查看参考答案
-- 不推荐的写法(函数导致索引失效)
SELECT * FROM employees WHERE YEAR(hire_date) = 2020;

-- 推荐的优化写法
SELECT *
FROM employees
WHERE hire_date >= '2020-01-01'
  AND hire_date <  '2021-01-01';

要点说明

  • WHERE 子句中,对索引列做函数/计算,往往会让优化器放弃索引;
  • 改写为区间查询既保持语义又提升性能。

题目79:为查询 dept_id = 5 AND salary > 8000 创建合适的索引

💡 解题思路

目标查询:

SELECT * FROM employees
WHERE dept_id = 5 AND salary > 8000;

要点:

  1. dept_idsalary 建复合索引;
  2. 遵循最左前缀原则,将过滤更严格的字段放前(通常 dept_id)。

🔍 关键提示

  • CREATE INDEX idx_dept_salary ON employees(dept_id, salary);
  • 该索引能很好支持上述条件和按工资排序。

📝 实现步骤

  1. 创建复合索引:
    CREATE INDEX idx_dept_salary
    ON employees(dept_id, salary);
    
  2. 使用 EXPLAIN 查看查询是否使用该索引;
  3. 对比没有索引、只有单列索引与复合索引三种情况的执行计划。

✅ 验证方法

EXPLAIN SELECT * FROM employees
WHERE dept_id = 5 AND salary > 8000;

检查 key 是否为 idx_dept_salarytype 是否为 range

🚀 扩展思考

  • 如果经常只按 salary > 8000 查询,这个索引是否合适?
  • 若顺序反过来 (salary, dept_id),对上述查询有何影响?
💯 点击查看参考答案
CREATE INDEX idx_dept_salary
ON employees(dept_id, salary);

然后执行:

EXPLAIN SELECT * FROM employees
WHERE dept_id = 5 AND salary > 8000;

要点说明

  • 针对组合条件,复合索引通常优于多个单列索引;
  • 字段顺序根据实际查询模式决定。

题目80:分析并优化 salary * 1.1 > 10000 的慢查询

💡 解题思路

原查询:

SELECT * FROM employees
WHERE salary * 1.1 > 10000;

问题:

  1. 对索引字段 salary 做运算,导致索引失效;
  2. 可以把计算移到常量一侧。

🔍 关键提示

  • 将条件改写为:
    WHERE salary > 10000 / 1.1
    
  • 确保 salary 上有索引。

📝 实现步骤

  1. 观察当前执行计划(通常是 ALL 全表扫描);
  2. salary 建索引(如尚未建);
  3. 将条件从 salary * 1.1 > 10000 改写为 salary > 10000 / 1.1
  4. 使用 EXPLAIN 对比优化前后。

✅ 验证方法

EXPLAIN SELECT * FROM employees
WHERE salary * 1.1 > 10000;

EXPLAIN SELECT * FROM employees
WHERE salary > 10000 / 1.1;

对比 typekeyrows

🚀 扩展思考

  • 其他类似写法,如 age + 1 > 30price * 0.8 < 100,也都应改写;
  • 在复杂业务逻辑中,是否可以把这些计算前移到应用层完成?
💯 点击查看参考答案
-- 原始写法(不推荐)
SELECT * FROM employees
WHERE salary * 1.1 > 10000;

-- 推荐写法
SELECT *
FROM employees
WHERE salary > 10000 / 1.1;

配合索引:

CREATE INDEX idx_salary ON employees(salary);

第九关学习总结

恭喜完成第九关的学习!通过这5道题目,你已经掌握了:

核心知识点回顾

  1. EXPLAIN执行计划 - type、key、rows、Extra字段解读,识别性能瓶颈
  2. 索引优化 - 最左前缀原则、覆盖索引、避免索引失效
  3. 查询优化 - 避免SELECT *、合理使用LIMIT、避免函数运算
  4. SQL改写技巧 - JOIN代替子查询、OR改为IN、函数移到右侧
  5. 性能分析 - 慢查询定位、执行计划分析、优化方案制定

最佳实践要点

  • ✅ 养成使用EXPLAIN分析查询的习惯
  • ✅ 避免在WHERE中对索引字段使用函数或运算
  • ✅ 只查询需要的字段,避免SELECT *
  • ✅ 合理使用LIMIT限制结果集大小
  • ✅ 定期审查慢查询日志,持续优化

后续关卡预告

接下来的关卡将学习:

  • 第十关:实战综合 - 综合项目实战,应用前面所有知识点

第十关:实战综合 - 学习指南

题目81:设计一个电商系统的数据库(用户、商品、订单、订单详情)

💡 解题思路

本题考查完整的表设计与关联关系

  1. 识别核心实体:用户(users)、商品(products)、订单(orders)、订单详情(order_items);
  2. 为每个实体设计字段及主键;
  3. 使用外键建立合理的表间关系;
  4. 考虑索引和常见查询场景。

🔍 关键提示

  • 每个表使用自增主键(BIGINT/INT);
  • 订单与用户:orders.user_id → users.user_id
  • 订单详情与订单/商品:order_items.order_id → orders.order_idorder_items.product_id → products.product_id
  • 为常用查询字段建索引(如 orders.user_idorders.order_timeproducts.category_id)。

📝 实现步骤

  1. 先在纸上画出实体及关系(ER 图);
  2. 为每个表设计字段(参考主手册中的示例);
  3. 编写 CREATE TABLE 语句,注意主键、外键、索引;
  4. 依赖顺序:先创建无外键依赖的表,再创建依赖它们的表。

✅ 验证方法

SHOW TABLES;
SHOW CREATE TABLE users\G
SHOW CREATE TABLE products\G
SHOW CREATE TABLE orders\G
SHOW CREATE TABLE order_items\G

检查:

  • 主键/外键是否正确;
  • 字段类型与业务含义是否合理。

🚀 扩展思考

  • 增加更多业务表,如:购物车、评价、优惠券、地址等;
  • 考虑分库分表、读写分离时,主键和路由键如何设计。
💯 点击查看参考答案

可参考主手册中的示例(简化版):

CREATE TABLE users (
    user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE products (
    product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    status TINYINT DEFAULT 0,
    order_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    INDEX idx_user (user_id),
    INDEX idx_status (status),
    INDEX idx_time (order_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE order_items (
    item_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id),
    INDEX idx_order (order_id),
    INDEX idx_product (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

题目82:查询每个类别下销量最高的3个商品

💡 解题思路

考查分组 + 聚合 + 窗口函数/子查询

  1. 计算每个商品在所属类别下的总销量(基于 order_itemsproducts / categories);
  2. 在类别维度内按销量排序并取前 3 名;
  3. 可使用窗口函数 RANK()/ROW_NUMBER() 或相关子查询。

🔍 关键提示

  • 使用窗口函数(MySQL 8.0+):
    RANK() OVER (PARTITION BY category_id ORDER BY SUM(quantity) DESC)
    
  • 再在外层筛选 rank <= 3

📝 实现步骤

  1. 连接表:products porder_items oicategories c
  2. GROUP BY 按商品统计销量:SUM(oi.quantity)
  3. 在子查询中增加窗口函数计算名次;
  4. 外层过滤 rank <= 3

✅ 验证方法

-- 先查看某个类别下所有商品销量和排序
SELECT category_name, product_name, total_sales, rank
FROM (...子查询...) t
WHERE category_name = '某类别';

确认每个类别最多 3 条记录。

🚀 扩展思考

  • 如果数据库版本不支持窗口函数,可以如何改写(如使用相关子查询)?
  • 如何改写为“销量前 10% 的商品”而不是固定前 3 个?
💯 点击查看参考答案
SELECT *
FROM (
    SELECT
        p.product_id,
        p.product_name,
        c.category_name,
        SUM(oi.quantity) AS total_sales,
        RANK() OVER (
            PARTITION BY p.category_id
            ORDER BY SUM(oi.quantity) DESC
        ) AS rnk
    FROM products p
    JOIN order_items oi ON p.product_id = oi.product_id
    JOIN categories c ON p.category_id = c.category_id
    GROUP BY p.product_id, p.product_name, c.category_name, p.category_id
) AS ranked
WHERE rnk <= 3;

题目83:计算每个用户的累计消费金额,并按消费金额排名

💡 解题思路

考查聚合 + 排名

  1. 按用户聚合订单金额:SUM(total_amount)
  2. 可统计订单数等额外指标;
  3. 使用窗口函数 RANK()DENSE_RANK() 按总消费额排序。

🔍 关键提示

  • 只统计已完成订单(如 status = 3);
  • 使用:
    RANK() OVER (ORDER BY SUM(total_amount) DESC)
    

📝 实现步骤

  1. 连接 users uorders o
  2. WHERE o.status = 3 过滤已完成订单;
  3. GROUP BY u.user_id, u.username 计算每个用户总消费;
  4. 使用窗口函数加上排名列;
  5. 按总消费降序输出。

✅ 验证方法

SELECT username, total_spent, spending_rank
FROM (...你的查询...)
ORDER BY total_spent DESC;

确认排名与金额排序一致。

🚀 扩展思考

  • 如何限制只显示前 100 名高消费用户?
  • 如果要按“平均客单价”排名,SQL 应如何调整?
💯 点击查看参考答案
SELECT
    u.user_id,
    u.username,
    SUM(o.total_amount) AS total_spent,
    COUNT(o.order_id) AS order_count,
    RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS spending_rank
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.status = 3
GROUP BY u.user_id, u.username
ORDER BY total_spent DESC;

题目84:查询连续 3 天都有订单的日期

💡 解题思路

考查日期处理 + 连续性判断

  1. 先统计每天是否有订单(可从 orders.order_time 提取日期);
  2. 通过自连接或窗口函数判断某天与前两天是否都有订单;
  3. 输出连续区间中的日期。

🔍 关键提示

  • 先得到每天一条记录:
    SELECT DATE(order_time) AS dt
    FROM orders
    GROUP BY DATE(order_time);
    
  • 使用自连接:
    t1.dt = t2.dt + INTERVAL 1 DAY
    t2.dt = t3.dt + INTERVAL 1 DAY
    

📝 实现步骤

  1. 使用子查询 daily_orders 得到所有有订单的日期;
  2. 将该子查询别名为 t1, t2, t3 三次自连接;
  3. 使用日期加减 1/2 天的条件判断连续性;
  4. 选择合适的输出列(如输出这 3 天中最早的一天或中间一天)。

✅ 验证方法

-- 查看中间结果
SELECT DATE(order_time) AS dt, COUNT(*) cnt
FROM orders
GROUP BY DATE(order_time)
ORDER BY dt;

手动检查有无连续 3 天,然后对比 SQL 结果。

🚀 扩展思考

  • 如何扩展为连续 N 天都有订单(如 7 天)?
  • 使用窗口函数(如 LAG)能否写出更简洁的解法?
💯 点击查看参考答案

自连接解法示例:

WITH daily_orders AS (
    SELECT DISTINCT DATE(order_time) AS dt
    FROM orders
)
SELECT t1.dt AS day3, t2.dt AS day2, t3.dt AS day1
FROM daily_orders t1
JOIN daily_orders t2 ON t2.dt = t1.dt - INTERVAL 1 DAY
JOIN daily_orders t3 ON t3.dt = t1.dt - INTERVAL 2 DAY
ORDER BY t3.dt;

也可以只输出最早的一天 t3.dt,视需求而定。


题目85:推荐系统——购买过商品 A 的用户还购买过哪些商品

💡 解题思路

考查自关联 + 子查询

  1. 找出“购买过商品 A 的所有订单”;
  2. 在这些订单中,统计除 A 以外被购买的其他商品及购买次数;
  3. 按购买次数降序输出推荐列表。

🔍 关键提示

  • 使用子查询获取 order_id 集合;
  • 主查询在 order_items 上统计;
  • 排除商品 A 本身。

📝 实现步骤

  1. 用子查询:
    SELECT order_id FROM order_items WHERE product_id = ?;
    
  2. 外层在 order_items 中筛选 order_id 落在上述集合内;
  3. COUNT(DISTINCT order_id)SUM(quantity) 统计关联度;
  4. 排除 product_id = ? 自身。

✅ 验证方法

手动选一个 product_id(如 100),执行 SQL:

SELECT p.product_name, purchase_count
FROM (...你的查询...) t
JOIN products p ON t.product_id = p.product_id;

查看结果是否合理。

🚀 扩展思考

  • 如何限定推荐“同一类别下的商品”?
  • 如何将购买次数转换为推荐分数(如 TF-IDF 之类思想)?
💯 点击查看参考答案
SELECT
    p.product_name,
    COUNT(DISTINCT oi.order_id) AS purchase_count
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
WHERE oi.order_id IN (
    SELECT order_id
    FROM order_items
    WHERE product_id = 100    -- 商品A的ID
)
AND oi.product_id <> 100
GROUP BY p.product_id, p.product_name
ORDER BY purchase_count DESC
LIMIT 10;

题目86:统计每月的销售额环比增长率

💡 解题思路

考查按月聚合 + 窗口函数 LAG

  1. 先统计每个月的销售总额;
  2. 使用 LAG 获取上个月的销售额;
  3. 计算 (本月 - 上月) / 上月 作为环比增长率。

🔍 关键提示

  • 按月聚合:
    DATE_FORMAT(order_time, '%Y-%m') AS ym
    
  • 使用:
    LAG(month_total) OVER (ORDER BY ym)
    

📝 实现步骤

  1. 子查询 monthly:按月 GROUP BY 统计 SUM(total_amount)
  2. 在外层对 monthly 使用窗口函数计算上月值;
  3. 计算环比:(this_month - last_month) / last_month
  4. 处理上个月为 0 或 NULL 的情况。

✅ 验证方法

SELECT * FROM (...你的查询...) ORDER BY ym;

手动核对某几个月的计算是否正确。

🚀 扩展思考

  • 如何改写为“同比”(与去年同月相比)增长率?
  • 数据量特别大时,是否需要预聚合表来加速统计?
💯 点击查看参考答案
WITH monthly AS (
    SELECT
        DATE_FORMAT(order_time, '%Y-%m') AS ym,
        SUM(total_amount) AS month_total
    FROM orders
    WHERE status = 3  -- 已完成订单
    GROUP BY DATE_FORMAT(order_time, '%Y-%m')
)
SELECT
    ym,
    month_total,
    LAG(month_total) OVER (ORDER BY ym) AS last_month_total,
    CASE
        WHEN LAG(month_total) OVER (ORDER BY ym) IS NULL
             OR LAG(month_total) OVER (ORDER BY ym) = 0
        THEN NULL
        ELSE (month_total - LAG(month_total) OVER (ORDER BY ym))
             / LAG(month_total) OVER (ORDER BY ym)
    END AS mom_growth_rate
FROM monthly
ORDER BY ym;

题目87:查询购买过所有类别商品的用户

💡 解题思路

考查集合覆盖 + 分组计数

  1. 先统计系统中类别总数 total_categories
  2. 对每个用户,统计其购买过的不同类别数
  3. 过滤“其类别数 == 总类别数”的用户。

🔍 关键提示

  • 类别总数:
    SELECT COUNT(*) FROM categories;
    
  • 用户-类别统计:
    SELECT user_id, COUNT(DISTINCT category_id) AS cat_cnt
    FROM ...
    GROUP BY user_id;
    

📝 实现步骤

  1. 使用子查询或 CTE 得到 total_categories
  2. 连接 ordersorder_itemsproductscategories
  3. user_id 分组统计 COUNT(DISTINCT category_id)
  4. 在 HAVING 子句中与总数比较。

✅ 验证方法

对结果用户随机抽查,手动验证其订单中是否覆盖所有类别。

🚀 扩展思考

  • 如果只要求“购买过至少 N 个不同类别的用户”,HAVING 条件应如何调整?
  • 类别树有父子关系时,可以按“一级类别”或“二级类别”进行统计。
💯 点击查看参考答案
WITH total_cat AS (
    SELECT COUNT(*) AS total_categories FROM categories
),
user_cat AS (
    SELECT
        o.user_id,
        COUNT(DISTINCT p.category_id) AS cat_cnt
    FROM orders o
    JOIN order_items oi ON o.order_id = oi.order_id
    JOIN products p ON oi.product_id = p.product_id
    GROUP BY o.user_id
)
SELECT u.user_id
FROM user_cat u
CROSS JOIN total_cat t
WHERE u.cat_cnt = t.total_categories;

题目88:实现分页查询(第 3 页,每页 20 条)

💡 解题思路

考查LIMIT 分页

  1. 第 1 页从 0 开始:LIMIT 0, 20
  2. 第 3 页的偏移量为 (3-1)*20 = 40
  3. 标准写法:LIMIT offset, size

🔍 关键提示

  • 偏移量 offset = (page - 1) * pageSize
  • 深度分页性能问题:offset 很大时会变慢。

📝 实现步骤

  1. 选择分页表(如 orders),按主键排序;
  2. 写出分页 SQL:
    SELECT * FROM orders
    ORDER BY order_id
    LIMIT 40, 20;
    
  3. 可只查询必要字段而非 *

✅ 验证方法

SELECT COUNT(*) FROM orders;
SELECT * FROM orders ORDER BY order_id LIMIT 40, 20;

检查:

  • 第 3 页数据与前 2 页不重叠;
  • 每页 20 条(除非数据总数不足)。

🚀 扩展思考

  • 大数据量下如何优化分页(如基于“游标/上次最大 ID”的方式)?
  • 如何写出通用分页 SQL(传入 page/pageSize 参数)?
💯 点击查看参考答案
-- 第3页,每页20条
SELECT *
FROM orders
ORDER BY order_id
LIMIT 40, 20;

延伸:基于ID的高性能分页

-- 假设上一页最后一条记录的ID为 last_id
SELECT *
FROM orders
WHERE order_id > :last_id
ORDER BY order_id
LIMIT 20;

题目89:查询用户的复购率(购买 2 次以上的用户比例)

💡 解题思路

考查分组统计 + 条件聚合

  1. user_id 分组,统计每个用户的订单数;
  2. 统计订单数 ≥2 的用户数量;
  3. 用“复购用户数 / 总用户数”得到比例。

🔍 关键提示

  • 子查询统计每个用户订单数;
  • 外层用 COUNT(DISTINCT ...)SUM(CASE WHEN ...) 计算比例。

📝 实现步骤

  1. 子查询 user_orders
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    WHERE status = 3
    GROUP BY user_id;
    
  2. 外层统计:
    • 复购用户数:COUNT(CASE WHEN order_count >= 2 THEN 1 END)
    • 总用户数:COUNT(*)COUNT(user_id)
  3. 计算百分比。

✅ 验证方法

SELECT * FROM user_orders ORDER BY order_count DESC;

手动核对几位用户的订单数和复购判断是否正确。

🚀 扩展思考

  • 如何按月份统计“当月复购率”?
  • 如何进一步细分不同用户群体(如新客/老客)的复购率?
💯 点击查看参考答案
WITH user_orders AS (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    WHERE status = 3
    GROUP BY user_id
)
SELECT
    COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 /
    COUNT(*) AS repurchase_rate
FROM user_orders;

题目90:设计并实现一个简单的权限管理系统(用户-角色-权限)

💡 解题思路

考查多对多关系建模 + 关联查询

  1. 用户(users)、角色(roles)、权限(permissions)是三个核心实体;
  2. 用户与角色是多对多,需要中间表 user_roles
  3. 角色与权限是多对多,需要中间表 role_permissions
  4. 通过多表关联查询某个用户拥有哪些权限。

🔍 关键提示

  • 表结构示意:
    • users(user_id, username, ...)
    • roles(role_id, role_name, ...)
    • permissions(perm_id, perm_name, ...)
    • user_roles(user_id, role_id)
    • role_permissions(role_id, perm_id)

📝 实现步骤

  1. 设计 5 张表结构;
  2. 为中间表设置联合主键或唯一键,避免重复关系;
  3. 插入一些测试数据;
  4. 写查询:给定用户,查询其所有权限。

✅ 验证方法

-- 查询某个用户拥有的所有权限
SELECT DISTINCT p.perm_name
FROM users u
JOIN user_roles ur ON u.user_id = ur.user_id
JOIN roles r ON ur.role_id = r.role_id
JOIN role_permissions rp ON r.role_id = rp.role_id
JOIN permissions p ON rp.perm_id = p.perm_id
WHERE u.username = 'alice';

🚀 扩展思考

  • 如何增加“菜单权限 / 接口权限 / 数据权限”等不同层级?
  • 如何在应用层缓存权限信息以减少数据库压力?
💯 点击查看参考答案

建表示例:

CREATE TABLE roles (
    role_id INT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(50) NOT NULL UNIQUE
);

CREATE TABLE permissions (
    perm_id INT PRIMARY KEY AUTO_INCREMENT,
    perm_name VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (role_id) REFERENCES roles(role_id)
);

CREATE TABLE role_permissions (
    role_id INT NOT NULL,
    perm_id INT NOT NULL,
    PRIMARY KEY (role_id, perm_id),
    FOREIGN KEY (role_id) REFERENCES roles(role_id),
    FOREIGN KEY (perm_id) REFERENCES permissions(perm_id)
);

查询某个用户所有权限:

SELECT DISTINCT p.perm_name
FROM users u
JOIN user_roles ur ON u.user_id = ur.user_id
JOIN roles r ON ur.role_id = r.role_id
JOIN role_permissions rp ON r.role_id = rp.role_id
JOIN permissions p ON rp.perm_id = p.perm_id
WHERE u.username = 'alice';

第十关学习总结

恭喜完成第十关的学习!通过这10道综合实战题目,你已经掌握了:

核心知识点回顾

  1. 数据库设计 - 从需求分析到ER图,再到表结构设计
  2. 复杂查询 - 窗口函数、子查询、多表关联的综合运用
  3. 业务分析 - 销量排名、用户消费分析、推荐系统、复购率统计
  4. 性能优化 - 分页优化、统计查询优化、索引设计
  5. 系统设计 - 权限管理、电商系统等完整业务场景

最佳实践要点

  • ✅ 设计阶段先画ER图,明确表之间的关系
  • ✅ 复杂查询分步实现,先实现功能再优化性能
  • ✅ 合理使用窗口函数简化排名和累计计算
  • ✅ 大数据量分页使用游标方式而非OFFSET
  • ✅ 统计查询尽量一次完成,避免多次查询

学习成果

恭喜你完成了MySQL升级打怪手册的全部10关学习!

通过这90道题目的练习,你已经:

  • ✅ 掌握了MySQL的基础语法和高级特性
  • ✅ 理解了数据库设计和优化的核心原则
  • ✅ 具备了解决实际业务问题的能力
  • ✅ 建立了完整的SQL知识体系

后续学习建议

完成全部10关后,建议进一步学习:

  1. MySQL高级特性

    • 存储过程和函数
    • 触发器
    • 视图和物化视图
    • 分区表
  2. 性能调优深入

    • 慢查询日志分析
    • Query Profile分析
    • InnoDB存储引擎优化
    • 读写分离和分库分表
  3. 运维与架构

    • 主从复制
    • 高可用方案(MHA、MGR等)
    • 备份与恢复策略
    • 监控和告警
  4. 实战项目

    • 参与开源项目
    • 设计完整的业务系统数据库
    • 性能优化实战
    • 故障排查和解决

完整学习进度

已完成详细指南

  • ✅ 第一关:基础入门(题目1-5)
  • ✅ 第二关:单表查询(题目6-15)
  • ✅ 第三关:多表联结(题目16-25)
  • ✅ 第四关:聚合函数与分组(题目26-35)
  • ✅ 第五关:子查询(题目36-45)
  • ✅ 第六关:高级查询技巧(题目46-55)
  • ✅ 第七关:数据操作(题目56-65)
  • ✅ 第八关:表设计与索引(题目66-75)
  • ✅ 第九关:性能优化(题目76-80)
  • ✅ 第十关:实战综合(题目81-90)

学习建议总结

引导式学习的优势

  1. 培养思考能力:不是背答案,而是学会分析问题
  2. 加深理解:通过步骤引导理解SQL执行逻辑
  3. 扩展视野:每题都提供多种解法和扩展思考
  4. 建立信心:逐步引导让学习更有成就感

学习方法建议

  • 先思考,后查答案:看到题目先自己尝试30分钟
  • 对比学习:你的答案和参考答案有什么不同?
  • 举一反三:完成一题后,尝试变换条件再练习
  • 记录笔记:把易错点和技巧记录下来
  • 动手实践:每道题都要自己敲代码,不要只看

进阶学习路径

完成全部10关后,建议进一步学习:

  1. MySQL高级特性

    • 存储过程和函数
    • 触发器
    • 视图和物化视图
    • 分区表
  2. 性能调优深入

    • 慢查询日志分析
    • Query Profile分析
    • InnoDB存储引擎优化
    • 读写分离和分库分表
  3. 运维与架构

    • 主从复制
    • 高可用方案(MHA、MGR等)
    • 备份与恢复策略
    • 监控和告警
  4. 实战项目

    • 参与开源项目
    • 设计完整的业务系统数据库
    • 性能优化实战
    • 故障排查和解决

🎉 恭喜你完成MySQL升级打怪手册的学习!

记住:学习数据库是一个持续的过程,要在实践中不断积累经验。
保持好奇心,遇到问题多思考、多实验、多总结。
祝你在MySQL的道路上越走越远!


附录:常用SQL速查

基础查询

-- 基本查询
SELECT 字段 FROM 表 WHERE 条件 ORDER BY 字段 LIMIT 数量;

-- 去重
SELECT DISTINCT 字段 FROM 表;

-- 模糊查询
SELECT * FROM 表 WHERE 字段 LIKE '%关键词%';

聚合与分组

-- 聚合函数
SELECT COUNT(*), SUM(字段), AVG(字段), MAX(字段), MIN(字段) FROM 表;

-- 分组统计
SELECT 字段, COUNT(*) FROM 表 GROUP BY 字段 HAVING COUNT(*) > 10;

多表联结

-- 内连接
SELECT * FROM A INNER JOIN B ON A.id = B.a_id;

-- 左连接
SELECT * FROM A LEFT JOIN B ON A.id = B.a_id;

-- 自连接
SELECT e.name, m.name AS manager FROM emp e LEFT JOIN emp m ON e.mgr_id = m.id;

子查询

-- 标量子查询
SELECT * FROM emp WHERE salary > (SELECT AVG(salary) FROM emp);

-- IN子查询
SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE location='北京');

-- EXISTS子查询
SELECT * FROM emp e WHERE EXISTS (SELECT 1 FROM orders o WHERE o.emp_id = e.id);

窗口函数

-- 排名
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rank FROM emp;

-- 分区排名
SELECT name, dept_id, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rank FROM emp;

-- 累计和
SELECT name, SUM(salary) OVER (ORDER BY hire_date) AS cumulative FROM emp;

数据操作

-- 插入
INSERT INTO 表 (字段1, 字段2) VALUES (值1, 值2);
INSERT INTO 表 (字段1, 字段2) VALUES (值1, 值2), (值3, 值4);

-- 更新
UPDATE 表 SET 字段=值 WHERE 条件;

-- 删除
DELETE FROM 表 WHERE 条件;

-- 事务
START TRANSACTION;
-- SQL操作
COMMIT;  -- 或 ROLLBACK;

表结构

-- 建表
CREATE TABLE 表名 (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 修改表
ALTER TABLE 表 ADD COLUMN 字段 类型;
ALTER TABLE 表 MODIFY COLUMN 字段 新类型;
ALTER TABLE 表 DROP COLUMN 字段;

-- 索引
CREATE INDEX idx_name ON 表(字段);
CREATE INDEX idx_multi ON 表(字段1, 字段2);
DROP INDEX idx_name ON 表;

常用函数

-- 字符串函数
CONCAT(str1, str2), SUBSTRING(str, pos, len), LENGTH(str), UPPER(str), LOWER(str)

-- 日期函数
NOW(), CURDATE(), DATE_ADD(date, INTERVAL 1 DAY), DATEDIFF(date1, date2)
YEAR(date), MONTH(date), DAY(date), DATE_FORMAT(date, '%Y-%m-%d')

-- 数值函数
ROUND(num, 2), CEIL(num), FLOOR(num), ABS(num)

-- 条件函数
IF(condition, true_value, false_value)
IFNULL(value, default_value)
CASE WHEN condition THEN value ELSE default END

Happy Learning! 🚀

posted @ 2026-01-09 14:39  理想赵雷  阅读(11)  评论(0)    收藏  举报