学习- SQL 实战练习题清单

以下是按 基础→进阶→优化 三阶段划分的 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 应为 rangerefkey 显示使用的索引。

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) 可能导致索引失效。
优化方案

  1. 预处理统计结果 (适用于非实时场景):

    • 创建统计中间表,定时(如每日凌晨)用定时任务(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';
  2. 分区表优化 (适用于实时场景):

    • 给订单表按 order_date 分区(如按年/月分区),查询时仅扫描 2023 年的分区,减少数据扫描量。

四、练习建议

  1. 环境搭建 :本地安装 MySQL 8.0+,用 Navicat/MySQL Workbench 执行 SQL,通过 EXPLAIN 分析执行计划;
  2. 循序渐进 :先完成基础题(确保语法无死角),再攻克进阶题(重点练习窗口函数、CTE),最后挑战优化题(结合 EXPLAIN 理解原理);
  3. 拓展场景:尝试将 SQL 与 Spark SQL 结合(如用 Spark 执行大数据量统计),适配大数据处理场景;
  4. 错题复盘:对优化题,记录"原始 SQL→问题→优化方案→验证结果",形成自己的优化手册。
相关推荐
QiZhang | UESTC17 小时前
学习日记day45
学习
菜鸟‍17 小时前
【论文学习】通过编辑习得分数函数实现扩散模型中的图像隐藏
人工智能·学习·机器学习
知识分享小能手17 小时前
CentOS Stream 9入门学习教程,从入门到精通,CentOS Stream 9 配置网络功能 —语法详解与实战案例(10)
网络·学习·centos
Dragon online17 小时前
数据分析师成长之路--从SQL恐惧到数据掌控者的蜕变
数据库·sql
瑶光守护者17 小时前
【学习笔记】5G RedCap:智能回落5G NR驻留的接入策略
笔记·学习·5g
你想知道什么?17 小时前
Python基础篇(上) 学习笔记
笔记·python·学习
SHOJYS17 小时前
学习离线处理 [CSP-J 2022 山东] 部署
数据结构·c++·学习·算法
weixin_4093831218 小时前
简单四方向a*学习记录4 能初步实现从角色到目的地寻路
学习·a星
モンキー・D・小菜鸡儿18 小时前
Android 系统TTS(文字转语音)解析
android·tts
2501_9159090618 小时前
iOS 反编译防护工具全景解析 从底层符号到资源层的多维安全体系
android·安全·ios·小程序·uni-app·iphone·webview