以下是按 基础→进阶→优化 三阶段划分的 SQL 实战练习题清单,覆盖电商、员工管理、订单统计等真实业务场景,参考答案附详细思路解析,适配 MySQL 8.0+(标注数据库差异点):
一、基础阶段(巩固核心语法,覆盖 80% 日常简单查询)
业务场景说明
基于 3 张核心表(电商常用场景):
sql
-- 1. 员工表(employees):存储员工基本信息
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT, -- 员工ID
emp_name VARCHAR(50) NOT NULL, -- 员工姓名
dept_id INT NOT NULL, -- 部门ID(关联部门表)
salary DECIMAL(10,2) NOT NULL, -- 月薪
hire_date DATE NOT NULL, -- 入职日期
gender CHAR(1) CHECK (gender IN ('男', '女')) -- 性别
);
-- 2. 部门表(departments):存储部门信息
CREATE TABLE departments (
dept_id INT PRIMARY KEY AUTO_INCREMENT, -- 部门ID
dept_name VARCHAR(50) NOT NULL UNIQUE -- 部门名称(如:销售部、技术部)
);
-- 3. 订单表(orders):存储用户订单信息
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT, -- 订单ID
user_id INT NOT NULL, -- 用户ID
order_date DATE NOT NULL, -- 下单日期
total_amount DECIMAL(10,2) NOT NULL, -- 订单总金额
pay_status TINYINT NOT NULL CHECK (pay_status IN (0,1)), -- 支付状态:0未支付,1已支付
emp_id INT, -- 关联员工表(负责该订单的员工,可为NULL)
INDEX idx_order_date (order_date), -- 订单日期索引
INDEX idx_emp_id (emp_id) -- 员工ID索引
);
-- 插入测试数据(可直接执行)
INSERT INTO departments (dept_name) VALUES ('销售部'), ('技术部'), ('人事部'), ('财务部');
INSERT INTO employees (emp_name, dept_id, salary, hire_date, gender)
VALUES
('张三', 1, 8000.00, '2020-01-15', '男'),
('李四', 1, 9500.00, '2019-03-20', '男'),
('王五', 2, 12000.00, '2018-07-05', '女'),
('赵六', 3, 6000.00, '2021-09-10', '女'),
('孙七', 2, 15000.00, '2017-11-30', '男');
INSERT INTO orders (user_id, order_date, total_amount, pay_status, emp_id)
VALUES
(101, '2024-01-05', 599.00, 1, 1),
(102, '2024-01-10', 1299.00, 1, 2),
(103, '2024-02-15', 899.00, 0, 1),
(104, '2024-02-20', 2999.00, 1, 5),
(105, '2024-03-01', 199.00, 1, 2),
(106, '2024-03-10', 4999.00, 0, 5),
(107, '2023-12-25', 799.00, 1, 1),
(108, '2023-12-30', 1599.00, 1, 2);
基础练习题(10 题)
1. 简单查询:查询所有技术部员工的姓名、月薪和入职日期
要求 :按入职日期降序排序,只显示前 2 条记录。
参考答案:
sql
SELECT e.emp_name, e.salary, e.hire_date
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE d.dept_name = '技术部'
ORDER BY e.hire_date DESC
LIMIT 2;
思路:多表内连接(INNER JOIN 可省略 INNER),过滤部门名称,排序后限制结果集。
2. 条件查询:查询 2024 年 1 月 1 日后下单的已支付订单,显示订单ID、下单日期和总金额
要求 :总金额大于 1000 元。
参考答案:
sql
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_date >= '2024-01-01'
AND pay_status = 1
AND total_amount > 1000;
思路 :日期条件用 >= 精准匹配,多条件用 AND 连接,过滤支付状态和金额。
3. 聚合查询:统计每个部门的员工人数和平均月薪
要求 :显示部门名称、人数(别名:emp_count)、平均月薪(别名:avg_salary,保留 2 位小数)。
参考答案:
sql
SELECT d.dept_name,
COUNT(e.emp_id) AS emp_count,
ROUND(AVG(e.salary), 2) AS avg_salary
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name; -- 分组字段需包含部门表主键/唯一字段
思路 :左连接(确保无员工的部门也显示,人数为 0),COUNT(emp_id) 忽略 NULL,ROUND 函数保留小数。
4. 增删改:给财务部添加 1 名新员工,姓名"周八",月薪 7500,入职日期 2024-04-01,性别男
参考答案:
sql
-- 先查询财务部的dept_id(假设为4)
INSERT INTO employees (emp_name, dept_id, salary, hire_date, gender)
VALUES ('周八', 4, 7500.00, '2024-04-01', '男');
-- 验证插入结果
SELECT * FROM employees WHERE emp_name = '周八';
拓展 :批量插入用 INSERT INTO ... VALUES (...), (...)。
5. 分组过滤:查询平均月薪大于 10000 元的部门,显示部门名称和平均月薪
要求 :部门人数至少 2 人。
参考答案:
sql
SELECT d.dept_name, ROUND(AVG(e.salary), 2) AS avg_salary
FROM departments d
JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name
HAVING AVG(e.salary) > 10000 AND COUNT(e.emp_id) >= 2;
思路 :HAVING 过滤分组后的结果(WHERE 过滤行,HAVING 过滤组)。
6. 模糊查询:查询姓名包含"张"或"李"的员工,显示姓名、部门ID和月薪
参考答案:
sql
SELECT emp_name, dept_id, salary
FROM employees
WHERE emp_name LIKE '%张%' OR emp_name LIKE '%李%';
拓展 :MySQL 中 LIKE 区分大小写(取决于字符集),不区分大小写用 ILIKE(PostgreSQL)或 LIKE BINARY(MySQL 反向)。
7. 多表关联:查询每个员工负责的已支付订单总金额,显示员工姓名、部门名称和总订单金额
要求 :无订单的员工总金额显示为 0。
参考答案:
sql
SELECT e.emp_name, d.dept_name, COALESCE(SUM(o.total_amount), 0) AS total_order_amount
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
LEFT JOIN orders o ON e.emp_id = o.emp_id AND o.pay_status = 1 -- 条件写在JOIN后,避免过滤掉无订单的员工
GROUP BY e.emp_id, e.emp_name, d.dept_name;
关键 :COALESCE 函数将 NULL 转为 0;关联时过滤条件写在 ON 后(而非 WHERE),保留左表所有数据。
8. 日期函数:查询 2020 年及以后入职的员工,显示姓名、入职日期和入职年限(保留 1 位小数)
参考答案:
sql
SELECT emp_name, hire_date,
ROUND(DATEDIFF(CURDATE(), hire_date)/365, 1) AS work_years
FROM employees
WHERE YEAR(hire_date) >= 2020;
函数说明 :CURDATE() 获取当前日期,DATEDIFF 计算日期间隔,YEAR() 提取年份。
9. 约束应用:给订单表添加"订单备注"字段(remark),类型为 VARCHAR(200),允许为空
参考答案:
sql
ALTER TABLE orders
ADD COLUMN remark VARCHAR(200) DEFAULT NULL;
-- 验证字段添加结果
DESCRIBE orders;
拓展 :修改字段用 ALTER TABLE ... MODIFY,删除字段用 ALTER TABLE ... DROP COLUMN。
10. 去重查询:查询所有有订单记录的员工ID(去重),并按员工ID升序排序
参考答案:
sql
SELECT DISTINCT emp_id
FROM orders
WHERE emp_id IS NOT NULL -- 排除无负责员工的订单
ORDER BY emp_id;
思路 :DISTINCT 去重,IS NOT NULL 过滤 NULL 值(避免结果中出现 NULL)。
二、进阶阶段(复杂查询 + 数据库对象,覆盖企业级业务)
进阶练习题(8 题)
1. 子查询:查询月薪高于本部门平均月薪的员工,显示姓名、部门名称、月薪和本部门平均月薪
参考答案:
sql
-- 方法1:相关子查询(每行都执行一次子查询)
SELECT e.emp_name, d.dept_name, e.salary,
(SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id) AS dept_avg_salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id);
-- 方法2:派生表(子查询作为临时表,效率更高)
SELECT e.emp_name, d.dept_name, e.salary, dept_avg.salary_avg
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
JOIN (SELECT dept_id, AVG(salary) AS salary_avg FROM employees GROUP BY dept_id) dept_avg
ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.salary_avg;
对比:派生表比相关子查询效率高(子查询只执行一次),优先使用。
2. 窗口函数:查询每个部门月薪前 2 名的员工,显示姓名、部门名称、月薪和部门内排名
要求 :月薪相同并列排名(如:2 人月薪相同为第 1 名,下一名为第 3 名)。
参考答案:
sql
SELECT emp_name, dept_name, salary, rank_num
FROM (
SELECT e.emp_name, d.dept_name, e.salary,
RANK() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) AS rank_num
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
) t
WHERE rank_num <= 2;
窗口函数说明:
PARTITION BY:按部门分组(窗口范围);RANK():并列排名(跳过重复名次),DENSE_RANK()不跳过,ROW_NUMBER()不并列。
3. CTE(通用表表达式):查询 2024 年各月的已支付订单总金额,显示月份和总金额
要求 :按月份升序排序,显示所有月份(无订单的月份总金额为 0)。
参考答案:
sql
WITH months AS (
-- 生成2024年1-12月的临时表
SELECT 1 AS month UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL
SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL
SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL
SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
)
SELECT m.month, COALESCE(SUM(o.total_amount), 0) AS monthly_total
FROM months m
LEFT JOIN orders o
ON MONTH(o.order_date) = m.month
AND YEAR(o.order_date) = 2024
AND o.pay_status = 1
GROUP BY m.month
ORDER BY m.month;
思路:用 CTE 生成所有月份(避免遗漏无订单月份),左连接订单表聚合。
4. 行转列:统计每个部门不同性别的员工人数,显示部门名称、男性人数、女性人数
参考答案:
sql
SELECT d.dept_name,
SUM(CASE WHEN e.gender = '男' THEN 1 ELSE 0 END) AS male_count,
SUM(CASE WHEN e.gender = '女' THEN 1 ELSE 0 END) AS female_count
FROM departments d
LEFT JOIN employees e ON d.dept_id = e.dept_id
GROUP BY d.dept_id, d.dept_name;
核心 :CASE WHEN 实现行转列(将性别维度的行数据转为列)。
5. 存储过程:创建存储过程,输入部门ID,输出该部门员工的姓名、月薪和入职日期
参考答案:
sql
-- 创建存储过程
DELIMITER // -- 修改结束符(避免与SQL语句中的;冲突)
CREATE PROCEDURE GetDeptEmployees(IN dept_id INT)
BEGIN
SELECT emp_name, salary, hire_date
FROM employees
WHERE dept_id = dept_id_param; -- 注意参数名与字段名区分(避免歧义)
END //
DELIMITER ; -- 恢复默认结束符
-- 调用存储过程(查询部门ID=2的员工)
CALL GetDeptEmployees(2);
-- 删除存储过程(如需)
-- DROP PROCEDURE IF EXISTS GetDeptEmployees;
数据库差异 :Oracle 存储过程语法用 CREATE OR REPLACE PROCEDURE,需声明变量类型。
6. 索引应用:给员工表的"姓名"字段创建普通索引,给订单表的"user_id + order_date"创建复合索引
参考答案:
sql
-- 普通索引(优化姓名查询)
CREATE INDEX idx_emp_name ON employees(emp_name);
-- 复合索引(优化按用户ID+日期查询的场景)
CREATE INDEX idx_user_date ON orders(user_id, order_date);
-- 查看索引(MySQL)
SHOW INDEX FROM employees;
SHOW INDEX FROM orders;
索引原则 :复合索引遵循"最左前缀匹配",查询时优先使用前缀字段(如 WHERE user_id = 101 可命中,WHERE order_date = '2024-01-05' 不可命中)。
7. 集合操作:查询 2024 年 1 月已支付订单的用户ID,与 2024 年 2 月已支付订单的用户ID的交集(即两个月都有下单的用户)
参考答案:
sql
-- 方法1:INTERSECT(MySQL 8.0+ 支持,Oracle/PostgreSQL 原生支持)
SELECT user_id FROM orders WHERE YEAR(order_date)=2024 AND MONTH(order_date)=1 AND pay_status=1
INTERSECT
SELECT user_id FROM orders WHERE YEAR(order_date)=2024 AND MONTH(order_date)=2 AND pay_status=1;
-- 方法2:JOIN 替代(兼容低版本MySQL)
SELECT DISTINCT o1.user_id
FROM orders o1
JOIN orders o2
ON o1.user_id = o2.user_id
AND YEAR(o1.order_date)=2024 AND MONTH(o1.order_date)=1 AND o1.pay_status=1
AND YEAR(o2.order_date)=2024 AND MONTH(o2.order_date)=2 AND o2.pay_status=1;
集合运算说明 :UNION ALL 不去重(效率高),UNION 去重,EXCEPT 取差集。
8. 偏移窗口函数:查询每个员工的月薪,以及前一名员工的月薪(按入职日期升序)
要求 :按部门分组计算前一名。
参考答案:
sql
SELECT emp_name, dept_name, salary, hire_date,
LAG(salary, 1, 0) OVER (PARTITION BY e.dept_id ORDER BY e.hire_date) AS prev_emp_salary
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;
窗口函数说明 :LAG(字段, 偏移量, 默认值) 取窗口内前 N 行数据,无数据时返回默认值(0)。
三、优化阶段(性能调优 + 大数据场景,企业面试重点)
优化练习题(5 题)
1. 慢查询优化:以下 SQL 执行缓慢(订单表数据量 100 万+),请分析原因并优化
原始 SQL(查询 2024 年 3 月用户ID=101 的已支付订单):
sql
SELECT * FROM orders
WHERE DATE_FORMAT(order_date, '%Y-%m') = '2024-03'
AND user_id = 101
AND pay_status = 1;
问题分析 :DATE_FORMAT(order_date, '%Y-%m') 对字段进行函数操作,导致索引 idx_order_date 失效,触发全表扫描。
优化方案:避免字段函数操作,用范围查询匹配索引:
sql
-- 优化后 SQL
SELECT order_id, user_id, order_date, total_amount, pay_status, emp_id -- 避免SELECT *
FROM orders
WHERE order_date BETWEEN '2024-03-01' AND '2024-03-31 23:59:59'
AND user_id = 101
AND pay_status = 1;
-- 进一步优化:调整复合索引顺序(将过滤性强的字段放前面)
ALTER TABLE orders DROP INDEX idx_user_date;
CREATE INDEX idx_pay_user_date ON orders(pay_status, user_id, order_date); -- 匹配查询条件顺序
验证 :用 EXPLAIN 查看执行计划,type 应为 range 或 ref,key 显示使用的索引。
2. 分页优化:以下分页 SQL 在大数据量(100 万+ 订单)时执行缓慢,请优化
原始 SQL(查询第 1000 页,每页 10 条订单):
sql
SELECT * FROM orders
ORDER BY order_id DESC
LIMIT 10000, 10; -- OFFSET 10000 需扫描前 10010 条数据
问题分析 :LIMIT offset, size 中,offset 越大,扫描的数据越多,效率越低。
优化方案:基于主键的"跳页查询"(适用于主键自增场景):
sql
-- 假设上一页最后一条订单ID为 10000(需记录上一页的最大ID)
SELECT * FROM orders
WHERE order_id < 10000
ORDER BY order_id DESC
LIMIT 10;
优势:利用主键索引(聚簇索引)直接定位,扫描行数仅 10 条,效率提升 1000+ 倍。
3. 关联查询优化:以下多表关联 SQL 执行缓慢,请分析并优化
原始 SQL(查询员工及其负责的订单,数据量:员工 1 万,订单 100 万):
sql
SELECT e.emp_name, o.order_id, o.total_amount
FROM employees e
LEFT JOIN orders o ON e.emp_id = o.emp_id
WHERE e.dept_id = 1;
问题分析 :可能缺少 employees.dept_id 索引,或 orders.emp_id 索引失效,导致关联时全表扫描。
优化方案:
sql
-- 1. 给员工表添加 dept_id 索引(优化过滤条件)
CREATE INDEX idx_emp_dept ON employees(dept_id);
-- 2. 确保订单表 emp_id 索引存在(已创建 idx_emp_id)
-- 3. 避免 SELECT *,只查询需要的字段(减少数据传输)
SELECT e.emp_name, o.order_id, o.total_amount
FROM employees e
LEFT JOIN orders o ON e.emp_id = o.emp_id
WHERE e.dept_id = 1;
-- 进阶优化:覆盖索引(避免回表查询)
ALTER TABLE orders DROP INDEX idx_emp_id;
CREATE INDEX idx_emp_order_amount ON orders(emp_id, order_id, total_amount); -- 包含查询所需字段
原理 :覆盖索引可让查询直接从索引中获取所有需要的字段,无需回表查询主键索引(Using index 优化)。
4. 事务优化:以下事务在高并发场景下容易出现死锁,请分析并优化
原始 SQL(两个并发事务分别更新员工薪资和部门信息):
sql
-- 事务1
BEGIN;
UPDATE employees SET salary = salary + 1000 WHERE dept_id = 1;
UPDATE departments SET dept_name = '销售一部' WHERE dept_id = 1;
COMMIT;
-- 事务2
BEGIN;
UPDATE departments SET dept_name = '销售二部' WHERE dept_id = 1;
UPDATE employees SET salary = salary + 1000 WHERE dept_id = 1;
COMMIT;
问题分析 :两个事务更新顺序相反(事务1:员工→部门,事务2:部门→员工),可能导致循环等待,引发死锁。
优化方案:统一事务更新顺序,避免循环等待:
sql
-- 事务1(统一顺序:先更新部门,再更新员工)
BEGIN;
UPDATE departments SET dept_name = '销售一部' WHERE dept_id = 1;
UPDATE employees SET salary = salary + 1000 WHERE dept_id = 1;
COMMIT;
-- 事务2(与事务1更新顺序一致)
BEGIN;
UPDATE departments SET dept_name = '销售二部' WHERE dept_id = 1;
UPDATE employees SET salary = salary + 1000 WHERE dept_id = 1;
COMMIT;
额外建议:
- 缩短事务时长(避免事务中包含查询、等待等操作);
- 给更新条件字段加索引(减少锁等待时间)。
5. 大数据量统计优化:以下统计 SQL 在 1000 万+ 订单数据中执行缓慢,请优化
原始 SQL(统计 2023 年各部门负责的订单总金额):
sql
SELECT d.dept_name, SUM(o.total_amount) AS total_amount
FROM departments d
JOIN employees e ON d.dept_id = e.dept_id
JOIN orders o ON e.emp_id = o.emp_id
WHERE YEAR(o.order_date) = 2023
GROUP BY d.dept_id, d.dept_name;
问题分析 :大数据量下实时聚合计算耗时久,YEAR(o.order_date) 可能导致索引失效。
优化方案:
-
预处理统计结果 (适用于非实时场景):
- 创建统计中间表,定时(如每日凌晨)用定时任务(Cron/Airflow)执行聚合计算,查询时直接读中间表;
sql-- 创建中间表 CREATE TABLE order_dept_stat ( stat_date DATE PRIMARY KEY, -- 统计日期(如2023-12-31) dept_id INT NOT NULL, dept_name VARCHAR(50) NOT NULL, total_amount DECIMAL(12,2) NOT NULL, UNIQUE KEY idx_dept_date (dept_id, stat_date) ); -- 定时执行的聚合SQL(如统计2023年全年) INSERT INTO order_dept_stat (stat_date, dept_id, dept_name, total_amount) SELECT '2023-12-31' AS stat_date, d.dept_id, d.dept_name, SUM(o.total_amount) FROM departments d JOIN employees e ON d.dept_id = e.dept_id JOIN orders o ON e.emp_id = o.emp_id WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY d.dept_id, d.dept_name ON DUPLICATE KEY UPDATE total_amount = VALUES(total_amount); -- 查询时直接读中间表 SELECT dept_name, total_amount FROM order_dept_stat WHERE stat_date = '2023-12-31'; -
分区表优化 (适用于实时场景):
- 给订单表按
order_date分区(如按年/月分区),查询时仅扫描 2023 年的分区,减少数据扫描量。
- 给订单表按
四、练习建议
- 环境搭建 :本地安装 MySQL 8.0+,用 Navicat/MySQL Workbench 执行 SQL,通过
EXPLAIN分析执行计划; - 循序渐进 :先完成基础题(确保语法无死角),再攻克进阶题(重点练习窗口函数、CTE),最后挑战优化题(结合
EXPLAIN理解原理); - 拓展场景:尝试将 SQL 与 Spark SQL 结合(如用 Spark 执行大数据量统计),适配大数据处理场景;
- 错题复盘:对优化题,记录"原始 SQL→问题→优化方案→验证结果",形成自己的优化手册。