SQL单表与多表查询练习题:四套题单从入门到实战 简介这份SQL语句练习题资源面向MySQL初学者与希望巩固查询能力的开发者通过单表与多表各四套题目帮助系统提升SQL编写水平。单表部分覆盖SELECT、WHERE过滤、ORDER BY排序及GROUP BY分组聚合等基础操作多表部分则围绕INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN与子查询展开训练多数据源整合能力。资源包共19个文件以13个txt题目与答案说明、5个sql脚本及1个zip压缩包为主sql文件可直接导入MySQL环境练习整体约22KB轻量便于携带。目前已有918人学习下载适合边练边查、对照答案复盘逐步掌握高效查询思路与常见写法。1. 单表查询练手为什么我建议从这四套题开始很多人学 SQL 的路径是看一遍 SELECT 语法然后直接冲多表 JOIN结果写出来的语句自己都不敢跑第二遍。问题不在语法在于单表查询的“手感”没建立起来。这份「sql语句练习题单表多表各四套」的资源核心价值就是把这个手感拆成可量化的训练单元——单表四套打底多表四套进阶每套都围绕 WHERE 过滤、GROUP BY 聚合、HAVING 二次筛选、ORDER BY 排序、LIMIT 截断这几个高频动作反复练。它适合刚学完基础语法但一写就卡壳的新手也适合工作两三年、发现自己写复杂查询时逻辑总是绕不清的从业者。MySQL 环境下直接建表就能跑不需要额外装工具。2. 单表四套题拆解从 WHERE 到窗口函数的递进逻辑2.1 建表与数据准备先把练习环境搭对拿到题单第一步不是急着写 SELECT而是把表结构和数据灌进去。常见做法是建一个练习库把四套题对应的表统一放进去。下面这段脚本我一般用来初始化字段类型和索引都按真实业务场景设避免练的时候因为类型隐式转换导致结果对不上。-- 创建练习库 CREATE DATABASE IF NOT EXISTS sql_practice DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE sql_practice; -- 员工表单表题的核心载体 CREATE TABLE emp ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(32) NOT NULL, dept_id INT NOT NULL, salary DECIMAL(10,2) NOT NULL DEFAULT 0.00, hire_date DATE NOT NULL, commission DECIMAL(10,2) DEFAULT NULL, INDEX idx_dept (dept_id), INDEX idx_salary (salary) ) ENGINEInnoDB; -- 插入测试数据覆盖 NULL、边界值、重复值 INSERT INTO emp (emp_name, dept_id, salary, hire_date, commission) VALUES (张一, 10, 8500.00, 2019-03-01, NULL), (李二, 10, 12000.00, 2018-07-15, 500.00), (王三, 20, 6500.00, 2020-01-10, NULL), (赵四, 20, 6500.00, 2021-05-20, 300.00), (孙五, 30, 15000.00, 2017-11-01, 1200.00), (周六, 30, 9800.00, 2022-02-14, NULL), (吴七, 10, 7200.00, 2023-06-30, NULL);这段脚本的关键点有三个一是commission字段故意留 NULL后面练IS NULL和IFNULL时才有真实数据可测二是salary上建了索引练WHERE salary ?时可以用EXPLAIN看命中情况三是dept_id有重复值为 GROUP BY 分组做准备。字符集用utf8mb4是为了避免中文姓名插入时出现乱码这个坑我见过太多次。2.2 四套题的难度梯度与对应语法点单表四套不是随便凑的它有一条清晰的难度线。第一套只考WHERE单条件过滤和ORDER BY排序比如“查出薪资大于 8000 的员工按入职时间倒序”。第二套引入AND/OR/NOT组合条件和BETWEEN、IN、LIKE这时候最容易翻车的是OR和AND的优先级——WHERE dept_id 10 OR dept_id 20 AND salary 7000和加括号的结果完全不同。第三套上聚合函数COUNT、SUM、AVG、MAX、MIN配合GROUP BY。这里有个玄学现象很多人写SELECT dept_id, emp_name, AVG(salary) FROM emp GROUP BY dept_id在 MySQL 的ONLY_FULL_GROUP_BY模式下直接报错因为emp_name不在 GROUP BY 里也不是聚合列。第四套是HAVING加子查询比如“查出平均薪资高于全公司平均薪资的部门”需要先用子查询算出全局平均值再在 HAVING 里比较。提示如果本地 MySQL 版本是 5.7 以上ONLY_FULL_GROUP_BY默认开启写 GROUP BY 时非聚合列必须出现在 GROUP BY 子句中否则报错。这不是 bug是 SQL 标准的要求。2.3 用 EXPLAIN 验证你的单表查询有没有走索引写完一条查询别只看结果对不对还要看它怎么执行的。EXPLAIN是单表练习阶段就该养成的习惯。下面这条语句用来检查按部门过滤时索引是否生效-- 查看执行计划重点关注 type 和 key 两列 EXPLAIN SELECT emp_name, salary FROM emp WHERE dept_id 10 AND salary 7000 ORDER BY hire_date DESC;输出里type如果是ref或range说明用上了索引如果是ALL就是全表扫描。key列显示实际使用的索引名。rows列是预估扫描行数越小越好。Extra里出现Using filesort说明排序没走索引数据量小的时候无所谓数据量大了就是性能杀手。我一般会对比加索引前后的rows变化直观感受索引的价值。参数调整方面如果想让练习更贴近生产可以把innodb_buffer_pool_size调大但本地练习默认值就够。真正要关注的是查询本身的写法WHERE条件里对索引列做函数运算比如YEAR(hire_date) 2019会导致索引失效这是单表练习里最值得反复提醒自己的点。3. 多表四套题实战JOIN 写不对多半是驱动表选错了3.1 多表环境搭建部门表与员工表的关联设计多表题的前提是有第二张表。我一般会加一张部门表和一张薪资等级表形成一对多和多对一的关联关系。建表时外键约束可加可不加练习阶段建议不加避免插入顺序限制影响练手节奏。-- 部门表 CREATE TABLE dept ( dept_id INT PRIMARY KEY, dept_name VARCHAR(32) NOT NULL, loc VARCHAR(32) DEFAULT NULL ) ENGINEInnoDB; INSERT INTO dept VALUES (10, 研发部, 北京), (20, 市场部, 上海), (30, 财务部, 广州), (40, 人事部, 深圳); -- 注意40号部门暂时没有员工 -- 薪资等级表用于非等值连接练习 CREATE TABLE sal_grade ( grade_id INT PRIMARY KEY, low_sal DECIMAL(10,2), high_sal DECIMAL(10,2) ) ENGINEInnoDB; INSERT INTO sal_grade VALUES (1, 0, 5000), (2, 5001, 8000), (3, 8001, 12000), (4, 12001, 20000);dept表里 40 号部门故意没有对应员工这是为了练 LEFT JOIN 时能观察到 NULL 行。sal_grade表用来练非等值连接——emp.salary BETWEEN sal_grade.low_sal AND sal_grade.high_sal这种连接条件不是新手容易写成等值连接然后发现结果为空。3.2 INNER JOIN、LEFT JOIN、RIGHT JOIN 的结果差异多表四套题的核心训练目标是给定一个业务问题能判断该用哪种 JOIN。INNER JOIN 只返回两表匹配上的行LEFT JOIN 保留左表全部行右表没匹配的补 NULLRIGHT JOIN 反过来。下面三条语句查的是同一件事——“列出所有部门和对应员工”但结果集完全不同-- 只返回有员工的部门40号部门消失 SELECT d.dept_name, e.emp_name FROM dept d INNER JOIN emp e ON d.dept_id e.dept_id; -- 保留所有部门40号部门员工列显示 NULL SELECT d.dept_name, e.emp_name FROM dept d LEFT JOIN emp e ON d.dept_id e.dept_id; -- 保留所有员工但这里 dept 是右表效果等同于以 emp 为主 SELECT d.dept_name, e.emp_name FROM dept d RIGHT JOIN emp e ON d.dept_id e.dept_id;驱动表的选择直接影响性能。MySQL 的优化器一般会选小表做驱动表但在 LEFT JOIN 里左表是固定的驱动表。如果左表很大而右表很小LEFT JOIN 的效率可能不如先过滤再 JOIN。我一般会先用EXPLAIN看rows列确认驱动表是不是符合预期。3.3 子查询与 JOIN 的等价改写什么时候该换写法多表题里有一类典型场景WHERE 里套子查询。比如“查出薪资高于研发部平均薪资的员工”。子查询写法直观但性能上不一定最优。-- 写法一子查询 SELECT emp_name, salary FROM emp WHERE salary ( SELECT AVG(salary) FROM emp WHERE dept_id 10 ); -- 写法二JOIN 改写 SELECT e.emp_name, e.salary FROM emp e JOIN ( SELECT AVG(salary) AS avg_sal FROM emp WHERE dept_id 10 ) t ON e.salary t.avg_sal;两种写法结果一样但执行计划可能不同。子查询如果是相关子查询每行都要执行一次内层查询性能会随外层行数线性下降。非相关子查询只执行一次MySQL 会缓存结果。JOIN 改写的好处是优化器有更多重排空间坏处是可读性下降。我的习惯是数据量小的时候优先可读性数据量大的时候用EXPLAIN对比两种写法的rows和type再决定用哪个。注意MySQL 8.0 对子查询的优化比 5.7 强很多部分场景下子查询会被自动改写为半连接。如果练习环境是 8.0子查询的性能可能比想象中好。4. 练习中容易翻车的五个坑从 NULL 到 GROUP BY 报错4.1 NULL 参与运算和比较的陷阱现象写WHERE commission 500结果里没有 commission 为 NULL 的行。原因NULL 不等于任何值包括它自己NULL 500的结果是 UNKNOWN 而不是 TRUE。解决要查“佣金不等于 500 或没有佣金”的记录必须写成WHERE commission 500 OR commission IS NULL。同理COUNT(commission)不统计 NULL 行COUNT(*)才统计所有行。这个坑在单表第三套聚合题里出现频率最高。4.2 GROUP BY 与 ONLY_FULL_GROUP_BY 的冲突现象SELECT dept_id, emp_name, AVG(salary) FROM emp GROUP BY dept_id报错Expression #2 of SELECT list is not in GROUP BY clause。原因MySQL 5.7 起默认开启ONLY_FULL_GROUP_BY要求 SELECT 列表里的非聚合列必须出现在 GROUP BY 中。解决要么把emp_name从 SELECT 里去掉要么用ANY_VALUE(emp_name)包一层要么改 SQL 模式。我一般建议改写法而不是关模式因为生产环境不一定允许你改配置。4.3 JOIN 条件写在 WHERE 和 ON 里的区别现象LEFT JOIN 时把右表的过滤条件写在 WHERE 里结果 LEFT JOIN 退化成了 INNER JOIN。原因WHERE 是在 JOIN 之后执行的对右表列的 NULL 值过滤会直接排除掉未匹配的行。解决LEFT JOIN 中针对右表的过滤条件应该写在 ON 子句里这样不影响左表行的保留。这个坑在多表第二套题里几乎人人都会踩一次。4.4 隐式类型转换导致索引失效现象emp_id是 INT 类型写WHERE emp_id 100能查出结果但EXPLAIN显示全表扫描。原因MySQL 在比较时会把字符串转成数字但对索引列做隐式转换会导致索引失效。解决参数类型和列类型保持一致INT 列就传 INT 值。这个坑在练习时不容易发现因为数据量小全表扫描也很快但到了生产环境就是慢查询的元凶。4.5 LIMIT 与 ORDER BY 的执行顺序误解现象写SELECT * FROM emp ORDER BY salary DESC LIMIT 3以为先取前三行再排序。原因SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITORDER BY 在 LIMIT 之前执行。解决理解这个顺序后分页查询LIMIT offset, size的 offset 越大越慢因为要先扫描并跳过前面的行。优化分页的常见做法是用游标或记录上次的最大 ID而不是单纯加大 offset。5. 从练习到生产把题单变成可复用的查询模板练完这八套题如果只是对答案就浪费了。我自己的做法是把每套题里反复出现的查询模式抽成模板下次遇到类似需求直接套。比如“分组取 Top N”这个模式单表和多表里都出现过用窗口函数写比子查询干净得多-- 每个部门薪资最高的两名员工MySQL 8.0 SELECT dept_id, emp_name, salary FROM ( SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM emp ) t WHERE rn 2;这段代码的关键在ROW_NUMBER()配合PARTITION BY它给每个部门内部按薪资降序编号外层再过滤rn 2。如果 MySQL 版本低于 8.0就得用相关子查询或变量来实现写法会啰嗦很多。我一般会先确认版本再决定用哪种写法。另一个值得固化的习惯是每写完一条多表查询强制跑一遍EXPLAIN看type列有没有出现ALL看rows列有没有超过万级。如果出现了就回头检查 JOIN 条件有没有走索引、WHERE 里有没有对索引列做函数运算。这个习惯我坚持了两年多帮我拦下了至少十几次上线前的慢查询。验证练习是否真正掌握可以拿同一道题用两种不同写法实现然后对比执行计划和结果集。如果两种写法结果一致但执行计划差异很大说明你开始理解优化器的行为了。如果结果不一致那多半是 JOIN 类型或 NULL 处理上出了问题回到第 4 章逐条排查。从那以后我每次拿到新的 SQL 题单都会先建表灌数据再逐条写查询并跑 EXPLAIN最后把典型模式抽成模板存进自己的片段库。这套流程看起来慢但比反复查语法、反复调试快得多。希望帮到你。本文还有配套的精品资源点击获取