SQL这玩意儿,说难不难,说简单也不简单。我见过不少人,书翻了好几遍,语法都背得滚瓜烂熟,可一到写实际查询就卡壳。也见过一些半路出家的同行,靠着一本《SQL必知必会》入门,然后疯狂刷题,硬是把SQL练成了吃饭的家伙。我自己带新人有个习惯,不管基础怎么样,先扔五十道题过去,做完、讲透、再改错,这一轮下来,基本的增删改查和常见的统计需求基本就难不倒人了。
这篇东西,就是围绕那套“sql语句练习50题”展开的。我会把这五十道题的设计思路、背后考察的知识点、每类题的解题套路,以及我当时踩过的坑和总结出来的排查技巧,一次性说清楚。不管你是刚摸到数据库边缘的初学者,还是准备面试想查漏补缺的候选人,只要你能静下心把这五十题啃透,SQL的基础盘就算彻底打牢了。
1. 内容整体设计与思路拆解
1.1 为什么是50道题,而不是20道或100道
市面上SQL练习题不少,但很多要么太简单,翻来覆去就是SELECT和WHERE,要么上来就是大厂面试题的难度,直接把人劝退。我用的这套50题,核心逻辑是“阶梯式覆盖”,从最简单的单表查询,一路做到多表关联、子查询、窗口函数,最后落到慢SQL优化和防注入的实战意识。50道题,刚好是一个能把常用语法过一遍,又能留出重复练习余地的量。
- 20题太薄:基本只能覆盖SELECT、WHERE、JOIN,对分组聚合和子查询的训练不够。
- 100题太厚:容易陷入机械刷题,而且很多题目是重复的,边际效益很低。
- 50题刚好:每个知识点能分到3到5道题,既有基础巩固,又有变形题和陷阱题,练完不会觉得枯燥,也不会觉得没练透。
这套题我用下来,新人一般一到两周能做完第一遍,第二遍复习只要三五天,效率很高。从学习的“必要长度”来说,50道是性价比最高的。
1.2 题目分层的核心逻辑
整套题我分成了五个模块,每个模块解决一种能力:
第一层,单表基础查询。对应的是SELECT、WHERE、ORDER BY、DISTINCT、BETWEEN AND、LIKE这些最底层的语法。这一层过不去,后面全是空中楼阁。
第二层,分组与聚合。涉及GROUP BY、HAVING、COUNT、SUM、AVG、MAX、MIN。这一层考察的是“把行变成组”的思维转换,是所有统计报表的核心。
第三层,多表关联。也就是JOIN系列。INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN,以及自连接。这部分是SQL面试的分水岭。
第四层,子查询与窗口函数。包括IN/EXISTS子查询、标量子查询、ROW_NUMBER、RANK、LAG/LEAD等。这一层解决的是相对复杂的业务问题,比如分组TopN、同比环比。
第五层,综合应用与优化意识。虽然叫“sql语句练习50题”,但如果只练到会写,远远不够。所以最后几题会故意放一些“能跑但效率极低”的SQL,引导你去分析执行计划、加索引、改写法。毕竟在真实业务里,跑得快的SQL才是好SQL。
1.3 这类题目实际对应的工作场景
有的同学练习时会觉得枯燥:“我天天写SELECT * FROM table,练这些有什么用?”实际上,每一道题背后都能映射到一个真实的业务需求。你想想看:
- 按部门统计平均工资:这不就是人力资源系统里的薪酬报表吗?
- 查询连续登录三天的用户:这是用户运营的基本功,留存分析里天天用。
- 找出每个类别下销量最高的商品:电商后台的“爆款榜”就是这么做出来的。
- 用一条SQL把一张表的数据更新到另一张表:数据同步、ETL清洗里的高频操作。
所以,刷题不是目的,学会把业务问题翻译成SQL逻辑,才是这套50题真正想让你掌握的东西。带着场景去练,比干巴巴地记语法有效得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 单表查询:先搞清楚SELECT的执行顺序
很多新手写SQL,上来就写SELECT,然后一路往下堆,等报错了才回头改。这里我建议你养成一个习惯:先理清SQL的逻辑执行顺序。虽然你写的时候是先SELECT后FROM,但数据库真正执行的时候是这样的:
- FROM:先确定数据来自哪张表(或哪些表)
- WHERE:对行进行过滤
- GROUP BY:对过滤后的行分组
- HAVING:对分组后的结果过滤
- SELECT:确定要输出的列,如果是聚合函数,在这一步计算
- ORDER BY:对结果排序
- LIMIT/OFFSET:最后做分页
这个顺序太重要了,尤其是对WHERE和HAVING的区分。有人问:“查平均工资大于5000的部门,能不能用WHERE AVG(salary) > 5000?”答案是绝对不能。因为WHERE在GROUP BY之前执行,那时候还没有分组,哪来的AVG?你只能用HAVING AVG(salary) > 5000。
我在练习50题的第一阶段,会刻意让学生先把执行顺序默写三遍,再开始做题。别看这个动作简单,它能帮你避免后面百分之六十的语法错误。
2.2 分组聚合:这50题最容易丢分的区域
分组聚合的题,表面上考的是GROUP BY,实际上考的是“分组思想”。很多人的误区在于:SELECT后面出现了非聚合列,却没有放到GROUP BY里。比如:
sql复制-- 错误写法
SELECT dept_id, MAX(salary)
FROM employee
WHERE gender = 'M'
GROUP BY dept_id;
这其实是很多数据库的默认行为允许的(MySQL的ONLY_FULL_GROUP_BY关闭时),但逻辑上是危险的。因为你SELECT了dept_id和MAX(salary),系统只能确定dept_id是分组键,但如果你再加一列employee_name,它到底返回哪一个人的名字?不确定。这就是典型的“逻辑漏洞”。
所以我在题目里会专门放几道这种“能跑但不对”的题,逼你思考:如果我想看每个部门里工资最高的人是谁,不能只靠GROUP BY,得用子查询或者窗口函数。这就是练习到后面模块的铺垫。
实际做这些题的时候,建议把聚合函数单独列个表自己梳理一遍:
| 聚合函数 | 作用 | 注意事项 |
|---|---|---|
| COUNT(*) | 统计行数 | 会统计NULL行 |
| COUNT(列名) | 统计非空值数量 | 自动忽略NULL |
| SUM(列名) | 求和 | NULL会被忽略 |
| AVG(列名) | 求平均 | NULL忽略,小心分母 |
| MAX/MIN | 求最大/最小 | 对文本类型也有效 |
2.3 多表关联:分清JOIN的底层逻辑
JOIN是50题里的重头戏,也是面试时最高频的考点。很多人死记硬背“LEFT JOIN就是左边表的全部数据加上右边匹配到的数据”,但一旦出现一对多关系,数据翻倍了,就彻底懵了。
我建议你用集合思维来理解JOIN:两张表就是两个集合,JOIN就是按条件把集合里的元素配对。
- INNER JOIN:两个集合的交集。
- LEFT JOIN:左集合的全部,配上右集合的匹配项,没匹配到就补NULL。
- RIGHT JOIN:反过来。
- FULL OUTER JOIN:两个集合的并集,两边没匹配到的都补NULL。
但这里有个最容易翻车的点:LEFT JOIN之后,数据量可能会变多。比如订单表left join订单明细表,一个订单对应三条明细,那结果就是三行,订单信息会重复三次。如果你这时候再做SUM(金额),得出来的金额会翻三倍。这个坑我在实际业务里见过太多次了,很多人没意识到“一对多关联导致数据发散”,统计结果一塌糊涂。
所以在50题里,我会专门设计一道“订单表与订单明细表关联后统计订单总金额”的陷阱题。正确的做法是:要么先明细表聚合好再关联,要么用COUNT(DISTINCT 订单ID)去重。这种“先聚合再关联”和“先关联再聚合”的选择题,几乎是所有SQL笔试的必考题。
2.4 子查询与窗口函数:拉开差距的分水岭
基础查询和JOIN掌握后,大部分人写常规报表已经没问题了。但如果你只会这两样,遇到“查询每个部门工资最高的员工”“查询各部门薪资排名前三的人”这类需求,就会很被动。
这种题,最漂亮的解法是窗口函数。ROWNUMBER() OVER(PARTITION BY dept_id ORDER BY salary DESC)这个语法,就是个“分组排名”的利器。我见过很多人第一次用它时,会惊讶于一行代码就搞定了原来要用复杂子查询才能解决的问题。
50题里,窗口函数的练习主要分三块:
- 排名函数:ROW_NUMBER、RANK、DENSE_RANK。三者区别要记牢。ROW_NUMBER是物理序号,永远连续;RANK会出现并列且跳号,比如两个第二名后直接是第四名;DENSE_RANK也会并列,但不跳号。
- 聚合窗口函数:SUM(salary) OVER(PARTITION BY dept_id),可以算累加值、移动平均。
- 位移函数:LAG和LEAD,可以取出前一行的值或后一行的值,做环同比的时候特别好用。
子查询则分两种:IN/EXISTS相关的子查询和标量子查询。EXISTS比IN在某些场景下性能更好,尤其是子查询表很大的时候。因为EXISTS一旦找到匹配行就会短路返回,而IN通常要等子查询完全执行完。当然,这不是绝对的,不同数据库的优化器行为不一样,但作为面试回答,这个点是加分项。
2.5 去重、空值、BETWEEN AND这些“小坑”
热词里提到的“sql语句去重查询”“sql去除空值”“sql between and的用法总结”,其实都是基础阶段的高频坑点,在50题里也占了不少重量。
去重有两种:DISTINCT和GROUP BY。两者都能去重,但DISTINCT更直接,GROUP BY更灵活,因为可以配合聚合函数。但有个细节,DISTINCT多列时,是“多列组合去重”,不是“单列去重”。所以如果你只想对某列去重,又不想让其他列干扰,就别用SELECT DISTINCT *,老老实实写清楚列名。
空值处理要看数据库方言。MySQL里是IFNULL(column, 0),SQL Server里是ISNULL(column, 0),通用写法是COALESCE(column, 0)。别小看这个函数,实际业务里“NULL导致金额计算错误”的案例比比皆是。而且注意,NULL不能和任何值用=比较,要用IS NULL或者IS NOT NULL。这是新手最容易犯的错误之一。
BETWEEN AND则是闭区间,比如BETWEEN 100 AND 200,是包含100和200的。对于日期类型要格外小心边界值。比如你想查2024年1月的数据,如果写成BETWEEN '2024-01-01' AND '2024-01-31',那可能漏掉1月31号当天的时间部分。稳妥的做法是写成大于等于2024-01-01且小于2024-02-01。
3. 实操过程与核心环节实现
这50道题,建议你亲自动手敲一遍。只看不练是学不会SQL的,这点我太有感触了。下面我挑几个核心模块的典型题目,连题目带解题思路和答案一起展示,你可以直接抄过去练习。
3.1 基础查询实战:几道必会的送分题
第一题:查询员工表中所有性别为“女”的员工姓名和工资。
sql复制SELECT emp_name, salary
FROM employee
WHERE gender = '女';
这题没什么难度,关键是检查你有没有掌握WHERE的字符串等值判断。注意不同数据库对字符串引号要求不一样,有的是单引号,有的是双引号,别搞混了。
第二题:查询工资在5000到10000之间的员工。
sql复制SELECT emp_name, salary
FROM employee
WHERE salary BETWEEN 5000 AND 10000;
BETWEEN AND是闭区间,所以会包含5000和10000,这个前面说过了。如果你想要“5000以上,10000以下”,那就得用salary > 5000 AND salary < 10000。这两者在面试现场很容易被追问。
第三题:查询员工表中有哪些不同的部门。
sql复制SELECT DISTINCT dept_id
FROM employee;
这题考的是去重。如果你还想知道每个部门有多少人,就得用GROUP BY:
sql复制SELECT dept_id, COUNT(*) AS cnt
FROM employee
GROUP BY dept_id;
3.2 分组聚合实战:统计类题目怎么做才不出错
第四题:统计每个部门的平均工资,并且只显示平均工资大于6000的部门。
sql复制SELECT dept_id, AVG(salary) AS avg_salary
FROM employee
GROUP BY dept_id
HAVING AVG(salary) > 6000;
这里有两个点需要注意:一是AVG会忽略NULL,如果工资列有NULL值,实际平均值会被拉高,必要时先处理空值;二是HAVING后面可以用聚合函数,但WHERE后面不行。这是分组题的核心考点。
第五题:统计每个部门男女员工的人数。
sql复制SELECT dept_id, gender, COUNT(*) AS cnt
FROM employee
GROUP BY dept_id, gender;
这是一道经典的多列分组题。GROUP BY多列后,结果是按组合维度统计。很多人在纸上想不明白,跑一下就懂了:先按部门分,再在部门内部按性别分。
3.3 多表关联实战:不同JOIN的典型场景
第六题:查询每个员工的姓名及其部门名称。
sql复制SELECT e.emp_name, d.dept_name
FROM employee e
LEFT JOIN department d ON e.dept_id = d.dept_id;
这里用LEFT JOIN而非INNER JOIN,是因为要确保所有员工都出现在结果里,就算他还没分配部门,部门名称显示为NULL。如果你用INNER JOIN,这些未分配部门的员工会直接被过滤掉。这就是业务语义的区别。
第七题:查询所有没有任何订单的客户。
sql复制SELECT c.customer_id, c.customer_name
FROM customer c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;
这是一个“找不在某张表里”的经典写法。通过LEFT JOIN后右表主键为NULL来判断不存在。它的等价写法是用NOT EXISTS子查询,两种都可以。但前者在某些数据库上性能更优,后者语义更直观。
3.4 窗口函数实战:分组TopN问题
第八题:查询每个部门工资排名前两名的员工。
sql复制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 employee
) t
WHERE t.rn <= 2;
这题是窗口函数模块的核心题。子查询里先算排名,外层再过滤。注意这里用的是ROW_NUMBER,如果工资相同,它是随机排名的;如果希望并列,用RANK或DENSE_RANK。这是面试里极容易追问的细节。
3.5 更新与去重实战:清洗数据时的看家本领
第九题:将一张临时表中的工资数据更新到员工表。
sql复制UPDATE employee e
JOIN temp_salary t ON e.emp_id = t.emp_id
SET e.salary = t.salary;
这是MySQL的写法。SQL Server或Oracle里JOIN更新写法不同,SQL Server是:
sql复制UPDATE e
SET e.salary = t.salary
FROM employee e
JOIN temp_salary t ON e.emp_id = t.emp_id;
热词里提到的“sql更新一个表中列为另外一个表中的列”,指的就是这个场景。实际工作中做数据订正、月度工资更新,都靠它。但这里有个比语法更重要的建议:UPDATE之前一定要先SELECT验证一下关联关系,看看会不会把数据更新串了,或者影响行数是不是符合预期。我见过有人用错关联条件,一条UPDATE下去,全表工资都变成了同一个人的,那种事故一旦发生,恢复数据极其痛苦。
第十题:删除表里完全重复的行,只保留一条。
在MySQL里,可以借助一个自增主键来删除:
sql复制DELETE t1
FROM employee t1
JOIN employee t2
ON t1.emp_name = t2.emp_name
AND t1.salary = t2.salary
AND t1.dept_id = t2.dept_id
WHERE t1.id > t2.id;
核心思路是:把一个重复组内id最小的留下来,其他的删掉。如果表里连主键都没有,那就得先加一个自增ID列,这又是一个完整的坑。这类“清洗去重”的题目,在五十题里我会放在稍后面一点,因为你得先理解多表自连接,才能写出来。
4. 常见问题与排查技巧实录
做SQL练习,最打击人的不是不会写,而是“我觉得写对了但报错”或者“结果看起来对但总觉得哪里不对”。这一章我重点整理一下我这几年在带新人、陪练五十题时经常遇到的典型问题,也可以当做一份速查表收藏。
4.1 语法层面的高频报错
| 报错信息 | 原因 | 解决思路 |
|---|---|---|
| Unknown column 'xx' in field list | 列名拼错或别名用错 | 先DESC表结构,确认列名 |
| You can't specify target table for update in FROM clause | MySQL里不能直接UPDATE又SELECT同一张表 | 包一层派生表,或者把条件里的子查询再包一层 |
| ORA-00979: not a GROUP BY expression | SELECT列没出现在GROUP BY里 | 把SELECT中的非聚合列都加到GROUP BY |
| Invalid use of group function | WHERE里用了聚合函数 | 改用HAVING |
| Column 'xx' in field list is ambiguous | 多表关联时列名冲突 | 给表起别名,列名前加上别名前缀 |
这些报错看起来简单,但如果没人告诉你原因,新手可能卡半小时都找不到头绪。我建议你把报错信息和SQL同时贴在搜索引擎里,基本上都能定位到原因。比起死磕,掌握“如何高效定位语法错误”的能力,才是工作里真正需要的。
4.2 逻辑层面的隐蔽陷阱
有的SQL不报错,但结果就是不对。这类问题比语法错误更可怕。
第一个陷阱:COUNT()和COUNT(列名)的混淆。COUNT()会统计所有行,COUNT(列名)会忽略NULL。如果你统计的是“有多少人填了手机号”,用COUNT(phone)才对;如果你想统计“表里有多少行记录”,用COUNT(*)。这个区别在五十题里我会反复出,因为它直接关系到报表数字的准确性。
第二个陷阱:LEFT JOIN后数据膨胀。前面提过,一对多关联会导致行数变多。如果你之后再聚合,不做去重或者不预先聚合,统计出来的一定是多算的。碰到这种问题,排查思路是:先用COUNT(*)看原表行数,再用COUNT(DISTINCT 主键)看JOIN后的行数,对比一下就能发现问题。
第三个陷阱:GROUP BY和DISTINCT的混用。有同学在同一个查询里同时写SELECT DISTINCT和GROUP BY,其实二者表达的是两种完全不同的逻辑,硬凑在一起,轻则结果不对,重则报错。想清楚你到底是要“去重展示”还是“分组统计”,二者只能选一个。
4.3 执行计划怎么看
五十题做到最后几题,我会引导大家看EXPLAIN(MySQL里的关键字)或执行计划。这可能是新手觉得最难的部分,但只要会看三列就够入门:
- type列:访问类型。从好到差依次是const、eq_ref、ref、range、index、ALL。ALL就是全表扫描,通常意味着没走索引,数据量大了就是性能炸弹。
- key列:实际使用的索引。如果是NULL,说明没命中任何索引。
- rows列:预估扫描的行数。这个数字越小越好,它侧面反映了SQL的效率。
举个例子,一个慢SQL,EXPLAIN出来是type=ALL,rows=几百万,那就是典型的全表扫描。这时候你应该思考:WHERE条件里的列有没有建索引?关联字段的类型是不是不一致?是不是写了函数在索引列上导致索引失效?这些点,五十题里会通过几道“不用改SQL逻辑但需要加索引才能跑快”的练习来体现。慢SQL优化不是玄学,就是一条链路:定位慢SQL -> EXPLAIN -> 看type和rows -> 对症下药。
4.4 常见的注入隐患
虽然五十题本身不教安全,但热词里有“sql注入万能密码绕过”,我必须强调一句:写SQL时养成参数化的习惯,不要用字符串拼接的方式拼SQL。很多系统被脱库,不是因为数据库有多难攻破,而是因为开发者在代码里直接写了类似这样的逻辑:
java复制String sql = "SELECT * FROM user WHERE name = '" + userName + "' AND pwd = '" + password + "'";
如果用户在用户名输入框里输入了 ' OR '1'='1,那拼出来的SQL就变成了:
sql复制SELECT * FROM user WHERE name = '' OR '1'='1' AND pwd = '随便'
这条SQL因为OR '1'='1'的存在,条件恒为真,等于把整个用户表都查出来了。这就是网上说的“万能密码绕过”。解决方式很简单:用PreparedStatement或者ORM框架的参数化查询。这个意识,希望你从练习SQL的第一天就刻在脑子里,不要等到上线了被别人拖了库再来后悔。
5. 把50题变薄:一套可复用的总结方法论
五十题做完一遍之后,不建议立刻丢到一边。我会把每一道题背后的方法论浓缩成一张“SQL速查表”,平时遇到不会的查询,先看这个表,想清楚属于哪一类问题,再动手写。这里我把自己压缩过的版本分享出来:
| 业务需求 | 核心思路 | 关键字/函数 |
|---|---|---|
| 去重查看 | 找出不同的值 | SELECT DISTINCT |
| 分组统计 | 按维度汇总 | GROUP BY + 聚合函数 |
| 过滤分组结果 | 分组后再筛选 | HAVING |
| 查两表匹配数据 | 求交集 | INNER JOIN |
| 保留左表全部 | 左表为主 | LEFT JOIN |
| 查“不存在”于另一表 | 左连接后IS NULL | LEFT JOIN ... WHERE ... IS NULL |
| 分组取前几名 | 排名后过滤 | ROW_NUMBER/RANK OVER |
| 环比同比 | 取上一行值 | LAG() OVER() |
| 行转列 | 条件聚合 | SUM(CASE WHEN ... THEN 1 ELSE 0 END) |
| 多列合并成一列 | 拼接字符串 | CONCAT或 |
这张表不是万能的,但它能覆盖日常80%以上的查询需求。你要做的是在刷题过程中不断往里面填充自己容易忘的写法,形成自己的“SQL知识库”。等到面试或者实际工作的时候,这就是你的武器库。
我个人在实际操作中的体会是,SQL这门技能,不需要你有多高的智商,但它极度依赖“手感”。手感怎么来?就是在五十题里一遍一遍地写,写错、改错、再写对的过程中磨出来的。很多年前我自己也是靠类似的笨办法,在数据库里造了一大堆测试数据,然后天天给自己出题,硬生生把菜鸟期的短板补上了。
最后再分享一个小技巧:每做完一道题,试着用两种以上不同的写法去实现同一个结果。比如能用子查询的,试试JOIN;能用窗口函数的,试试派生表。这样举一反三练下来,你对SQL的理解深度会远超那些只会背标准答案的人。等哪一天你拿到一个业务需求,脑子里第一反应不是“这个SQL怎么写”,而是“这个逻辑可以拆成哪几步”,那你就出师了。
