简介:面向 MySQL 初学者与开发者的 SQL 单表及多表查询题集,含四套单表、四套多表练习题,覆盖从基础语法到多表关联的常见场景,可帮助读者快速定位查询薄弱环节并强化训练。压缩包为 RAR 格式,共 19 个文件,其中文本文件保存各套题目与练习说明,SQL 脚本用于建库、插入数据并执行查询,另附带答案压缩包,整体仅 22KB,下载和本地练习都很便捷。这套题集已有 911 人学习/下载,常被用于查询入门、期末复习与面试前集中刷题,题型设计贴近实际工作需求。内容按单表、多表模块化整理,单表题重点训练条件过滤、排序、分组与聚合函数,多表题则覆盖内连接、左连接、右连接与子查询等核心操作。结合每套答案与对应脚本一起对照练习,既能理清查询执行的先后顺序,也能较快发现常见易错点,适合想要系统提升数据库查询能力的学习者反复演练。 我见过太多人,SQL是“看会”的——教程刷了几十篇,收藏夹里塞满sql语句大全,可真到面试现场或者接手一个取数需求,连单表的分组统计都写不顺。这不是基础差,是练得少。SQL这门技能很特殊,语法规则几天就能背完,但它真正难的地方,在于把脑子里的模糊诉求翻译成准确、高效的单表或多表查询语句。所以这次我整理了单表四套、多表四套,共八套SQL练习题,从最基础的条件查询一路做到分组聚合、自连接、关联子查询和排名场景,专门用来把手练熟。不论你是刚学完语法、缺少实战的初学者,还是准备跳槽面试前想快速找回手感的老手,这套题都值得从头到尾完整写一遍。
1. 这套题为什么按“单表四套+多表四套”来设计
1.1 单表先于多表:先把一张表玩明白
很多人一开始就冲JOIN和子查询,结果连一张表里的WHERE和GROUP BY都没理顺。SQL的难度是递进的,单表查询是地基,多表连接是在地基上盖楼。你连“每个部门平均工资”这种单表聚合都写不利索,换成“每个部门里工资最高的员工”这种多表场景,只会更懵。这八套题,前四套只碰一张表,把所有过滤、排序、分组、函数处理练扎实;后四套才引入第二张表,专注于各种连接类型和关联写法。顺序上不要跳,单表不过关,多表一定卡壳。
1.2 四套题不是重复,是四个能力层次
同样都是单表,第一套和第二套练的是“把数据找出来、排好序”,第三套开始练“把数据算出来”,第四套则是“把数据加工出来”。这四个层次对应了日常取数工作中最常见的四类需求:查明细、看排名、做统计、做转换。多表四套也一样,第一套是标准的内连接取数,第二套是外连接处理“有或无”的问题,第三套是自连接和子查询这种稍微绕一点的逻辑,第四套是甩开单表思维、直接模拟真实经营分析场景。每套解决一类问题,写完之后你再看实际业务需求,心里会先自动做归类。
1.3 每道题控制在十分钟内写完
这套题的定位是“练习”而不是“考试难点收集”,所以我没有故意出偏题怪题。几乎所有题目都是实际工作或面试里反复出现的写法。我建议你拿到题目后不要先看答案,给自己十分钟,脑子里过一遍逻辑再动手敲。能独立写出来的题,说明这个知识点真的是你的了;写不出来的,对照答案之后一定要再默写一遍。这套题能不能发挥作用,不取决于题目本身,而取决于你“重复写”的次数。
2. 单表四套:把WHERE、排序、分组、函数这几个基本功逐层夯实
2.1 第一套(单表):条件查询,WHERE是SQL一切能力的起点
第一套题表面上简单,但它的坑也最多。这四道题覆盖了WHERE子句里最常见的比较、区间、模糊匹配和NULL判断。
题目:查询工资大于8000的员工姓名和工资。
SELECT emp_name, salary FROM employee WHERE salary > 8000;题目:查询2018年1月1日之后入职、并且姓名以“张”开头的员工。
SELECT emp_name, hire_date FROM employee WHERE hire_date > '2018-01-01' AND emp_name LIKE '张%';题目:查询部门编号为1或2的员工。
SELECT emp_name, dept_id FROM employee WHERE dept_id IN (1, 2);题目:查询没有分配部门(部门编号为空)的员工。
SELECT emp_name, dept_id FROM employee WHERE dept_id IS NULL;为什么强调这套题?因为工作中大量查询的起点就是“把满足某几个条件的记录先捞出来”。这里有三个非常容易翻车的点:第一,判断NULL不能用等号,必须用IS NULL或IS NOT NULL,用= NULL查出来永远是空结果;第二,多个过滤条件混用时,AND和OR的优先级容易搞混,拿不准就直接加括号;第三,字符串和时间比较要用引号包起来,而且日期格式尽量写成'YYYY-MM-DD',别写成人人都看不懂的'2018/1/1'。这套题只要能一边不看答案、一边顺利写出来,就说明你已经跨过了“能看懂SQL但写不出来”那道坎。
2.2 第二套(单表):排序与分页,ORDER BY没有你想的那么简单
排序只是加一个ORDER BY吗?是,但排序和分页组合在一起,才是面试里反复出现的考点。
题目:查询工资最高的5名员工。
SELECT emp_name, salary FROM employee ORDER BY salary DESC LIMIT 5;如果是在SQL Server里,写法要换成:
SELECT TOP 5 emp_name, salary FROM employee ORDER BY salary DESC;题目:按部门编号升序、工资降序排列所有员工。
SELECT emp_name, dept_id, salary FROM employee ORDER BY dept_id ASC, salary DESC;题目:分页查询第2页数据,每页10条。
SELECT emp_name, salary FROM employee ORDER BY emp_id LIMIT 10 OFFSET 10;写这道题时我倒想多说一句:LIMIT 10 OFFSET 10的意思是跳过前10条、取接下来的10条,也就是第二页;如果你用了LIMIT 10, 10这种逗号写法,含义一样,但别跟LIMIT 10 OFFSET 10记混。很多人在分页上翻车,不是不知道语法,而是搞不清“每页10条取第2页”到底该跳过几条。第1页是OFFSET 0,第2页是OFFSET 10,每页条数越大,OFFSET就是(页号-1)乘以每页条数。排序和分页是报表类需求里最高频的组合,多写几次就自然记住了。
2.3 第三套(单表):聚合与分组,GROUP BY是单表查询的分水岭
从这套题开始,你已经不只是“查数据”,而是“算数据”了。聚合函数配合GROUP BY,能不能把分组逻辑想清楚,基本决定了你SQL水平的上限。
题目:统计每个部门的员工人数。
SELECT dept_id, COUNT(*) AS emp_count FROM employee GROUP BY dept_id;题目:统计每个部门的平均工资,并且只显示平均工资大于8000的部门。
SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id HAVING AVG(salary) > 8000;题目:查询公司里最高工资和最低工资的差额。
SELECT MAX(salary) - MIN(salary) AS salary_diff FROM employee;很多人在这个阶段最常犯的错误,是把WHERE和HAVING搞混。WHERE是在分组之前过滤原始记录,HAVING是在分组之后过滤聚合结果。比如“统计每个部门里工资大于8000的员工人数”,这个“大于8000”是在分组之前先按条件筛掉一部分人,应该写在WHERE里;而“只显示平均工资大于8000的部门”,这个条件依赖聚合结果,必须写在HAVING里。另一个容易忽视的点是:COUNT(*)统计的是行数,而COUNT(salary)统计的是salary列非空的个数,二者在有NULL值的时候结果可能完全不一样。这套题是整套练习里最重要的关卡,建议每题都手动敲两遍以上。
2.4 第四套(单表):DISTINCT、CASE WHEN与日期函数
这一套开始涉及“加工数据”——不是单纯地把数据库里的值拿出来,而是把它们变成你想要的样子。
题目:查询公司一共有多少个不同的部门编号。
SELECT COUNT(DISTINCT dept_id) AS dept_count FROM employee;题目:给员工按工资划分等级,8000以上为“高”,5000到8000为“中”,5000以下为“低”。
SELECT emp_name, salary, CASE WHEN salary > 8000 THEN '高' WHEN salary >= 5000 THEN '中' ELSE '低' END AS salary_level FROM employee;题目:查询2018年以后入职的员工姓名和入职年份。
SELECT emp_name, YEAR(hire_date) AS hire_year FROM employee WHERE hire_date >= '2018-01-01';题目:统计男女员工各多少人。
SELECT gender, COUNT(*) FROM employee GROUP BY gender;这套题的核心就是让你意识到:SQL不只可以做“过滤”和“计算”,还可以做“变换”。CASE WHEN在真实报表里使用频率非常高,比如把存量的状态值转成可读的中文标签、把连续数值切成几个区间段来做分布统计。日期函数在不同数据库里写法差异比较大,MySQL的YEAR(hire_date)在SQL Server里同样可用,但如果是Oracle就要用EXTRACT(YEAR FROM hire_date)。我建议你练习时把自己常用的那款数据库的日期函数查一遍,记在笔记里,因为日期处理永远是SQL实践里绕不开的大头。
3. 多表四套:连接、自关联、子查询与排名场景的真实业务组合
3.1 第一套(多表):INNER JOIN,连接的基础逻辑
多表查询是从这里真正开始的。前面四套题不管你写得怎么样,只要到这个地方还在用“先把两张表拼成一张大表”的思维做题,那后面几套会越来越吃力。
题目:查询员工的姓名和所属部门名称。
SELECT e.emp_name, d.dept_name FROM employee e INNER JOIN department d ON e.dept_id = d.dept_id;题目:查询工资大于8000的员工姓名、部门名称和工资。
SELECT e.emp_name, d.dept_name, e.salary FROM employee e INNER JOIN department d ON e.dept_id = d.dept_id WHERE e.salary > 8000;题目:查询每个部门的部门名称和它的经理姓名。这里假设department表里的manager_id对应employee表的emp_id。
SELECT d.dept_name, e.emp_name AS manager_name FROM department d INNER JOIN employee e ON d.manager_id = e.emp_id;为什么强调要使用表别名?因为多表查询里两张表可能都有emp_id、dept_id这种同名字段,不写别名数据库根本分不清你在说哪一个,最后只能报“字段不明确”的错误。第一套题的关键在于理解INNER JOIN的本质:它只返回两张表里能匹配上的行,匹配不上的记录会被直接丢弃。这意味着如果员工没有分配部门,那么他在这个查询结果里就“消失”了。能不能接受这种“消失”,取决于业务需要什么,这也是下一套题要解决的问题。
3.2 第二套(多表):LEFT JOIN与NULL的坑
上一套里员工没部门会消失,这一套题就是为了处理“即使没匹配上,我也要看到它”的场景。
题目:列出所有部门,以及每个部门的员工人数,没有员工的部门也要显示,人数显示为0。
SELECT d.dept_name, COUNT(e.emp_id) AS emp_count FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_id, d.dept_name;题目:查找没有任何员工的部门。
SELECT d.dept_name FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id WHERE e.emp_id IS NULL;这两道题是整套练习里最容易写错的地方,而且错得非常隐蔽。拿第一题来说,很多人会写COUNT(*)而不是COUNT(e.emp_id)。如果某个部门没有员工,LEFT JOIN的结果里会出现一行“部门信息 + 全是NULL的员工字段”,COUNT(*)会把这一行也算进去,结果人数显示成1。这是“看起来对、实际错”的典型代表,面试官一眼就能识别出来。第二题用WHERE e.emp_id IS NULL来筛出“右表没匹配上”的部门,思路其实就是“反连接”。这套题练完,你对LEFT JOIN的理解会上升一大截。
3.3 第三套(多表):自连接与关联子查询
自连接和关联子查询,是SQL里公认的“分水岭”,很多人学到这儿会卡很久。但其实把它们拆开看,就是把同一张表当成两张表用,或者在内层查询里引用外层查询的字段。
题目:查询员工姓名和直接上级的姓名。假设employee表里的manager_id存的是上级的emp_id。
SELECT e.emp_name AS employee_name, m.emp_name AS manager_name FROM employee e LEFT JOIN employee m ON e.manager_id = m.emp_id;题目:查询工资高于本部门平均工资的员工姓名、部门编号和工资。
SELECT e.emp_name, e.dept_id, e.salary FROM employee e WHERE e.salary > ( SELECT AVG(salary) FROM employee WHERE dept_id = e.dept_id );自连接的写法核心是在FROM里同一张表出现两次,分别起不同的别名,连接条件就写“员工表的经理ID等于经理表的员工ID”。这里我用的是LEFT JOIN,因为最大的老板没有上级,如果用INNER JOIN他会被丢掉,不符合业务直觉。关联子查询的执行逻辑比较反直觉:很多人以为子查询只执行一次,其实对于外层表的每一行,关联子查询都会用当前行的dept_id去算一次平均工资,然后判断当前行是否大于这个平均值。数据量大时这种写法性能不一定好,但作为练习题,它能帮你把“一行一行地思考SQL”这个思维方式建起来。
3.4 第四套(多表):排名场景与多表聚合
走到这一套,基本就是在模拟真实的报表分析需求了。前面所有练习都会在这里汇合。
题目:查询每个部门工资前三名的员工。
先用基础写法,不依赖窗口函数:
SELECT e.emp_name, e.dept_id, e.salary FROM employee e WHERE ( SELECT COUNT(*) FROM employee WHERE dept_id = e.dept_id AND salary > e.salary ) < 3 ORDER BY e.dept_id, e.salary DESC;再给出窗口函数版本,以MySQL 8.0或SQL Server为例:
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 employee ) t WHERE rn <= 3;题目:查询人数超过2人的部门,显示部门名称和人数,按人数降序排列。
SELECT d.dept_name, COUNT(e.emp_id) AS emp_count FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_id, d.dept_name HAVING COUNT(e.emp_id) > 2 ORDER BY emp_count DESC;第一道题是面试里高频的“分组TopN”问题。不用窗口函数的写法,思路是数一数“同一部门里工资比我高的人有几个”,如果少于3个,那我就是前三名。这个写法能跑,但数据量大时性能一般,因为每一行都要执行一次子查询。窗口函数ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)是标准的分组排序工具,PARTITION BY按部门分组,ORDER BY salary DESC在组内排序,最后在子查询外面过滤rn小于等于3。如果你还不会窗口函数,强烈建议把这套玩法和ROWNUMBER、RANK、DENSE_RANK这三个常用排序函数的区别查清楚,这已经是现代SQL里躲不开的技能了。
4. 建表语句和造数建议:先把这几张表跑起来
4.1 建表语句
纸上谈兵没意思,这套题要真练,先把环境搭起来。我用的是MySQL风格建表,换成SQL Server只需调整等小细节:
CREATE TABLE department ( dept_id INT PRIMARY KEY, dept_name VARCHAR(50), manager_id INT ); CREATE TABLE employee ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50), dept_id INT, manager_id INT, salary DECIMAL(10, 2), hire_date DATE, gender CHAR(1) );4.2 造数建议
表建好之后,建议先插入部门数据,再插入员工数据。如果你手头没有现成的数据生成工具,可以人工造十来个部门、几十个员工,注意几个坑:设计一些没有员工的部门,让LEFT JOIN的题能用上;给个别员工的dept_id留成NULL,让IS NULL的判断有数据可测;manager_id要保证指向的员工确实存在,否则自连接那题会查不到结果。我实际测试时,大约造了8个部门、30个员工,就足够覆盖全部题目了。
4.3 不同数据库的语法差异提示
这套题里的SQL以MySQL为主,但它其实不绑定任何数据库。唯一需要留意的是几个差异:分页上MySQL用LIMIT ... OFFSET ...,SQL Server用OFFSET ... FETCH NEXT ... ROWS ONLY或TOP;字符串拼接MySQL用CONCAT(),SQL Server用+;日期函数方面MySQL有YEAR(),SQL Server也有YEAR(),但其他日期处理函数差异比较大。练的时候先选一款你实际环境里有的数据库,把语法对照着改一遍,本身就是很有价值的练习。
5. 刷完这八套题之后,我总结出的SQL练习心得
5.1 做题时最容易踩的六个坑
这套题我自己来回验过好几遍,也看着不少初学者写过,总结出六个最高频的坑:
第一,COUNT(*)和COUNT(列名)混用。LEFT JOIN统计右表匹配数量时,想按某列非空计数就别滥用星号,否则NULL值那行会多算进去。
第二,WHERE和HAVING用反。分组前的过滤条件写到HAVING里,轻则结果和预期不一样,重则直接报语法错误。
第三,判断NULL用等号。数据库里NULL代表“未知”,任何= NULL的运算结果都是“未知”,所以查不到任何行,必须用IS NULL。
第四,多表查询漏写连接条件。一旦JOIN后面忘记写ON,两张表会做笛卡尔积,返回行数瞬间爆炸,数据量一大还会把数据库拖垮。
第五,排序和分组字段不一致。GROUP BY选了dept_id,SELECT里却混入emp_id这种非聚合、非分组字段,MySQL默认可能不报错,但结果毫无意义。
第六,不看实际结果只背答案。SQL不是默写题,同一个需求有无数种写法,手写一遍、执行一遍、看到真实输出,才叫真的会了。
5.2 一套题的检验标准
最后说说怎么算“刷完了”。我的标准一直都很简单:把答案全部遮住,重新打开一个新的SQL窗口,从建表开始,到所有题都能不看笔记独立写出来,并且执行结果和预期完全一致,这才叫过关。第一次做不出来的题不丢人,丢人的是做完之后从不回头再写一遍。平时练的时候,我建议每写完一行SQL就顺手格式化一下,保持缩进清晰,因为你在练习里养成的书写习惯,会原封不动带进面试和实际工作里。这套八套题,至少值得你刷两轮——第一轮学思路,第二轮拼速度,两轮都过完之后,你再看那些“sql语句大全”类的收藏,会发现大部分内容你已经不需要背了。
本文还有配套的精品资源,点击获取