☰
图解SQL经典50题:先画图再写SQL,通关数据分析面试
2026/10/15 6:57:41 网站建设 项目流程

每年到了招聘季,后台和数据分析师的岗位面试里,SQL题几乎是必考项。我面试过不少候选人,也带过一批准备转行的人,发现一个共性现象:很多人把网上流传的那套“经典50题”反复刷了好几遍,题目都快背下来了,可面试官把学生表换成订单表、课程表换成商品表,立刻就卡壳。问题不在语法,在他们眼里每道题都是孤立的背诵题,没有建立起从数据关系推导SQL的直觉。

这篇内容要做的就是把这50题当素材,演示一套“先画图、再写SQL”的方法。你不用画多精美的图,一支笔一张白纸,把表、外键、筛选条件和分组折叠画清楚,绝大多数面试题都能在草稿上解出来。它适合三类人:准备面试的应届生、想转数据分析的工程师、还有需要带新人的技术负责人——如果你能把这套图形思维讲出口,SQL面试题对你来说基本就不再是障碍了。

1. 经典50题的题库地图与图解思维

1.1 五十道题到底覆盖了哪些考点

网上流传的“SQL经典面试50题”,大多以一个选课系统为背景,包含学生、课程、教师、成绩四张表。别小看这几张表,它们组合出来的题目,几乎把关系型数据库的基础考点全覆盖了。我自己习惯把题库里的题目按能力层级分成五层。

能力层代表题型典型难度
条件查询单表 WHERE、LIKE、IN、BETWEEN简单
聚合计算COUNT、SUM、AVG、MAX/MIN 配合 GROUP BY中等
多表连接INNER JOIN、LEFT JOIN、自连接中等偏上
子查询EXISTS、NOT IN、嵌套 SELECT较难
窗口函数ROW_NUMBER、RANK、DENSE_RANK中上

带新人时我发现,绝大多数人不缺语法知识,缺的是“拿到题先想清楚数据以什么形状流动”。比如题目一换说法,“没选过某教师课程的学生”和“所有课程成绩都大于80分的学生”,表面看是两道题,底层图形完全一致,都是从某个主体出发,去子查询里检查存在性。这类判断没办法靠背,必须靠理解。

1.2 为什么“画图”比“背SQL”更高效

人脑的工作记忆大概只能同时装下四五个信息块,一长串带嵌套和JOIN的SQL语句,在脑子里几乎留不住。可如果把SQL的执行逻辑映射成图,用圆圈代表表,用箭头代表连接路径,用方框代表筛选条件,那整段逻辑就变成了一张可以反复检视的地图。

我辅导过一位准备转行的朋友,他一开始是典型的背书型选手,照抄答案很顺,遇到变体就崩。后来我强制他每题先画图再写代码,两周以后他自己都能给同事讲题了。原因很简单:画图逼着你回答三个问题——数据从哪里来,行数和行数怎么对应,条件到底在分组前还是分组后生效。这三个问题想清楚了,SQL只是把它们翻译出来而已。

1.3 画图前必须搞懂的三类关系

关系型数据库的本质就是表和表的关联。读懂50题的前提,是搞清楚三种关系。

  • 一对一关系:一张表的某一行,在另一张表里至多对应一行,比如学生和学籍卡。
  • 一对多关系:一张表的某一行,在另一张表里对应很多行,比如学生和成绩,一个学生可以考很多门课。
  • 多对多关系:需要中间表搭桥,比如学生和课程,它们之间必须通过成绩表衔接。

选课系统里的score成绩表,就是最典型的中间表。学生表到成绩表是一对多,课程表到成绩表也是一对多,学生和课程通过score构成了多对多。在草稿纸上画图时,你只要盯住外键在哪张表,连接路径就不可能画反。

2. 准备一套可以反复使用的数据模型

2.1 建表脚本与示例数据

无论你用什么数据库,我都建议从一套标准的数据模型开始。网上很多版本的50题表结构和数据都不一样,经常出现“我答案明明对,但结果和你不一样”的尴尬。以最常用的MySQL为例,建表脚本可以长这样。

CREATE TABLE student ( stu_id VARCHAR(20) PRIMARY KEY, stu_name VARCHAR(20), birth DATE, gender CHAR(1) ); CREATE TABLE teacher ( tea_id VARCHAR(20) PRIMARY KEY, tea_name VARCHAR(20) ); CREATE TABLE course ( cou_id VARCHAR(20) PRIMARY KEY, cou_name VARCHAR(20), tea_id VARCHAR(20), FOREIGN KEY (tea_id) REFERENCES teacher(tea_id) ); CREATE TABLE score ( stu_id VARCHAR(20), cou_id VARCHAR(20), score DECIMAL(4,1), PRIMARY KEY (stu_id, cou_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id), FOREIGN KEY (cou_id) REFERENCES course(cou_id) );

这里score表的主键是(stu_id, cou_id)联合主键,这是中间表的标准写法。我在面试时问过不少候选人,很多人会忽略联合主键这回事,结果后续插入数据时出现重复记录,排查半天才发现是主键设计的问题。学习阶段,宁可多花一点时间把数据模型搞干净,后面刷题会省很多事。

2.2 用关系图把四张表串起来

拿起笔来画这四张表时,我通常把student放在左上角,course放在右上角,teacher挂在course的外侧,score放在正中间。score同时向student和course引出箭头,整个图形像一个小雪花。

画的时候重点盯线,看谁和谁之间存在外键。所有“查询某学生选了哪些课”的题目,路径一定是student → score → course,必须经过中间的那张成绩表。你不能直接从student跳到course,因为两张表之间没有直达线。这就是图形方法最直观的价值:连接路径一目了然,谁和谁能直接JOIN,谁必须绕路,图上清清楚楚。

2.3 自制一套“图例”符号

每个人的图不需要好看,但要有统一的符号。我习惯用三种符号:

  • 圆圈代表数据表,圆圈内写表名。
  • 箭头代表连接路径,箭头上标主键与对应的外键。
  • 方框代表筛选条件,框内写类似“score > 80”或“gender = '女'”这样的条件。

我还习惯在筛选框右上角标一个小字母,说明这个条件应该出现在WHERE、HAVING还是JOIN的ON里面。别小看这个小标记,它能帮你控制同一个条件在不同位置产生的语义变化,比如同样一个“score>80”,写在WHERE和写在ON里,结果集完全不同。

3. 画图三步法:把任意SQL翻译成图形

3.1 第一步:确定你要的是“行”还是“组”

面试题从结果形态上分两类:一类要你查明细行,比如“分数大于80的学生姓名”,结果每一行都是一个学生;另一类要你查组汇总,比如“每门课的平均分”,结果按课程折叠成一行。

这个判断很关键,它直接决定要不要写GROUP BY。我在图上通常会先画一个输出框,把需要的列写在框顶部。如果题目里出现“每、各、平均、超过某个汇总值”这类词,那输出框马上变成分组框,要开始考虑折叠。举个例子,“查询平均成绩大于60的学生”这句话里,有“平均”又有“大于”,它必定是先按学生分组,再过滤组。

3.2 第二步:顺着外键画连接路径

确定要输出哪张表的数据后,第二步是把涉及的表按外键串起来。比如要查“选了某课程的学生的姓名”,那就要从student表出发,沿着score表,再走到course表,整个路径有三张表,中间那张就是桥。

我常见到新人直接写FROM student JOIN course,然后想当然认为可以通过学生ID和课程ID直接关联。可实际上两句之间根本没外键,这么写必然出错。画图之后这种情况能少一大半,因为你画不出没有线的连接。这条规则在自连接里也适用——自连接本质上是在同一张表里,通过不同的字段建立“逻辑上的两张表”。

3.3 第三步:把筛选条件放到正确位置

这是整个图解流程里最容易翻车的一个步骤。WHERE、HAVING、ON三个位置,过滤时机完全不同。

  • WHERE在分组之前处理单行数据。
  • HAVING在分组之后处理小组数据。
  • ON在JOIN执行时限制来自哪张表的行。

画图时我会把筛选框放在对应位置:如果这个框要画在分组折叠之前,它就对应WHERE;如果画在折叠之后,就对应HAVING。有一次我问一个候选人“平均成绩大于60”怎么写,他毫不犹豫把条件塞到WHERE里,结果报错。这就是没搞清折叠先后顺序。画图能直接规避这类问题,因为折叠前和折叠后,在图上就是两个完全不同的区域。

3.4 图解窗口函数:给每一行开一扇透视窗

窗口函数和其它SQL逻辑的最大区别,在于它不改变行数。普通聚合像把多行压成一行,是一台压面机;窗口函数更像在每行旁边挂一个计算器,这一行还在原位置,只是多了一列计算结果。

绘制时,我在原表右侧画一个独立的小框,框里写着“PARTITION BY cou_id ORDER BY score DESC”,然后从这个小框拉一条线到结果列,表示每行获得的窗口值。这个图画出来以后,为什么“窗口函数可以和明细列共存,而GROUP BY不行”就一目了然了:窗口函数没有折叠过程,它只是横向透视。

4. 经典50题中五道代表性题目的完整图解

4.1 平均成绩大于60分的学生:HAVING的典型陷阱

“查询平均成绩大于60分的学生学号和平均成绩”几乎是50题里必考的一道。很多人条件反射写出WHERE AVG(score)>60,数据库直接报错,原因就在折叠顺序上:score表必须先按stu_id折叠成组,AVG才能计算出来,所以条件只能放在HAVING。

SELECT stu_id, AVG(score) AS avg_score FROM score GROUP BY stu_id HAVING AVG(score) > 60;

注意:多数数据库不允许在HAVING里直接写别名avg_score,必须完整写AVG(score)。我面试时见过太多候选人在这栽跟头。

画图时,score表的右边画一个折叠矩形,按stu_id折叠,输出只有两列:stu_id和均值。折叠完成后,再画一个大于60的筛选框套在矩形外面。图形一旦画出来,整个过程的顺序想忘都难。

4.2 所有课程成绩都大于80分的学生:NOT EXISTS的画法

这道题的常见错误是写NOT IN,把子查询里选出“有任意一门成绩小于等于80”的学生,再在外面取反。逻辑上好像通,但只要成绩表里出现NULL,结果就可能变成空集。更稳健的写法是用NOT EXISTS,它的图形逻辑非常直接。

SELECT stu_id, stu_name FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.stu_id = s.stu_id AND sc.score <= 80 );

画图时,student表在左边,对每一行学生,执行右边那个子查询框。子查询框通过stu_id连线关联回外层,检查这个学生是否存在成绩不达标的记录。如果子查询结果集是空的,这一行学生就放行;如果非空,就过滤掉。图上能清楚看到,这是一个逐行检查的过程,和NOT IN那种“把整列值取反”的逻辑完全不同。

4.3 选修了全部课程的学生:关系除法模型

这道题被很多人叫作“50题里最难的一批”。其实拆开看,它是一个集合包含关系:全部课程是一个集合,某学生选修的课程是另一个集合,只要后者包含前者,就满足条件。最常见的写法是利用数量相等来完成。

SELECT stu_id FROM score GROUP BY stu_id HAVING COUNT(DISTINCT cou_id) = (SELECT COUNT(*) FROM course);

图形上就是两条线:一条从course表拉出总课程数,另一条从score表按学生分组统计选课数,两条线最终汇合到一个等号比较器。HAVING负责比较每个分组是否等于总课程数。候选人学过关系除法概念当然更好,但实际面试中,能用COUNT版本解决已经足够稳妥。我还见过有人在没有成绩的学生身上纠结,放心,没选课的学生根本不会出现在score表里,自然不会被统计。

4.4 三种排名函数的区别图解

排名题近年来出现频率越来越高,因为业务里排行榜需求太多了。核心考点就是ROW_NUMBER、RANK、DENSE_RANK三兄弟的区别。

函数分数排名结果
ROW_NUMBER90, 90, 851, 2, 3
RANK90, 90, 851, 1, 3
DENSE_RANK90, 90, 851, 1, 2

画图时,只要在score表右侧画一个排序窗口,窗口内部先按cou_id分成多个分区,每个分区内部按分数降序排列,然后依次编号。三个函数在普通成绩上没有任何区别,区别只在遇到并列分数时怎么处理:ROW_NUMBER强制分先后,哪怕两个人分数一样也排成1和2;RANK并列后跳过后续名次;DENSE_RANK并列后不跳号。业务里如果做“前10名领奖”,通常不适合用RANK,因为跳号会导致上榜数量不足10名,这种细节在面试里讲出来很加分。

4.5 行列转换:SUM(CASE WHEN)的网格透视

行列转换题是很多人的噩梦,但画图特别简单。题面往往长这样:“统计每门课程各分数段的人数”,你只需在纸上画一个二维网格,行是课程,列是分数段,每个交叉格是人数。

SELECT cou_id, SUM(CASE WHEN score >= 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN score >= 80 AND score < 90 THEN 1 ELSE 0 END) AS good FROM score GROUP BY cou_id;

这个SQL的图就一个动作:把CASE WHEN看成横向展开的列,按cou_id纵向折叠。每行成绩进入对应分数段的格子加1,进不了就加0。SUM(CASE WHEN...)本质上是条件计数,比直接写COUNT(CASE WHEN...)更通用,因为SUM里可以叠加权重,扩展性更强。实际做报表时,这种网格透视的思路随处可见,学会了很能打。

5. 刷题时最容易翻车的五个点

5.1 NULL是默认的隐形杀手

成绩表里一旦出现NULL,COUNT(score)不会统计它,但COUNT(*)会把那一行算进去。同样的表,两个COUNT结果不一样,这在图上看就是统计哪一列的区别。

更隐蔽的是WHERE score = NULL这种写法。在SQL里,NULL不等于任何值,也不等于NULL自身,所以这个条件永远查不到数据。正确写法是IS NULL。我排查过不少线上统计报表的故障,最后定位都是计算口径没处理NULL。刷题阶段建议养成一个习惯:凡是要聚合的列,先看一眼数据里有没有NULL,有就先在图上标出来,再决定是排除还是补默认值。

5.2 SELECT和GROUP BY列不对齐

几乎所有关系数据库都有这个硬性规则:SELECT中出现的非聚合列,必须全部出现在GROUP BY中。画图后原因很清楚:GROUP BY把多行折叠成一行后,没有被分组的列已经没有“唯一值”可言了。

比如一个学生选了五门课,你按stu_id分组后,还想直接输出course_name,逻辑上你让数据库从五门课里挑哪一门?显然说不通。碰到这类错误,检查GROUP BY里缺了哪一列就行。我建议在图上折叠矩形里,把参与分组的列用粗线标出来,不在粗线里的列,要么聚合,要么不选。

5.3 一对多JOIN后行数膨胀

学生表连接成绩表以后,同一个学生在结果里会出现多次,因为一个学生有多条成绩记录。这就是一对多JOIN带来的行数膨胀。

看起来很基础,但在复杂查询里很容易被忽略。我曾在一次数据报表开发里,用COUNT(*)统计连接后的结果,结果学生数凭空翻了好几倍,排查了大半天,问题就出在没有去重。现在我在图上遇到一对多关系时,会顺手在箭头旁边写个倍数,比如“1:N”,提醒自己结果集的行数是放大过的。需要统计主表行数时,一定要用COUNT(DISTINCT 主键)。

5.4 IN与EXISTS的性能误区

很多老教程喜欢说IN性能差、EXISTS性能好,这个结论在现代数据库里已经不太成立,优化器会做等价改写。真正需要注意的是语义差异。

IN和EXISTS差在逻辑时点:IN是把子查询结果物化成一张值列表,然后判断外层值在不在列表里;EXISTS是逐行检查子查询是否返回数据。最危险的是NOT IN配合NULL。只要子查询结果列表里出现一个NULL,NOT IN的结果就变成空集,因为“不等于任何一个值”在NULL的三值逻辑里既不是真也不是假,整段SQL直接没数据。

所以我的经验是:只要子查询可能返回NULL,就优先写NOT EXISTS。画图时你会发现,NOT EXISTS是逐行检查,NULL不会参与比较,安全性高得多。

5.5 GROUP BY与窗口函数混用的分寸

有人写“查询每门课最高分及其学生姓名”,试图用一个GROUP BY解决全部,结果要么报错,要么拿到错误的学生名。

其实从图形逻辑上看,这是两类操作叠加:GROUP BY负责折叠统计,窗口函数负责在明细行上透视。正确做法是用窗口函数在明细层面算排名,再在外层过滤排名为1的行。

SELECT cou_id, stu_id, score FROM ( SELECT cou_id, stu_id, score, ROW_NUMBER() OVER (PARTITION BY cou_id ORDER BY score DESC) AS rn FROM score ) t WHERE t.rn = 1;

这里的思路是两个图层:内层开窗,外层过滤。把这两个动作硬塞进同一个分组逻辑,语义一定会打架。

6. 把图解思维带上真正的面试桌

6.1 面试现场,先画图再写SQL

我面试时喜欢观察候选人白板上的动作。如果对方上来就写SELECT,后面多半要反复涂改;如果对方先花三十秒画出表关系,再动手写SQL,出错的概率明显低很多。画图的动作本身就在帮你理清WHERE和HAVING的位置,JOIN路径错不错,分组前后的逻辑有没有反。

到了真实面试,你可以直接跟面试官说“我先把表关系梳理一下”,这完全不会扣分。相反,大多数面试官会觉得你思路成熟,因为真实工作中没人会对着空白编辑器盲写复杂SQL,都是先在脑子里把表结构过一遍。

6.2 从50题迁移到真实业务表

面试官把选课系统换成订单系统,本质没有变化。学生变成用户,课程变成商品,成绩变成订单明细,关系结构还是那个熟悉的学生-课程-成绩模型。

迁移的秘诀是永远先找主键和外键。找到外键,就找到了数据流动的方向。前阵子我帮业务团队写一份跨5张表的报表SQL,就是先在纸上把表名摆开,逐张画出外键线,全部梳理清楚之后,连写带调不到十分钟就完成了。这套能力靠的是平时把经典50题画图画出来的肌肉记忆。

6.3 刷题之后的第二轮复习建议

第一轮按顺序把50题刷完,求广不求深。第二轮按错误标签刷,把做错过的题目集中重做,找出错因规律。第三轮只看图不看代码,看着自己画过的关系图,尝试在脑子里重建SQL。三轮下来,基础SQL面试题基本全覆盖。

自己也测试过一套额外建议:把50题中的每一道,换一种数据库方言重写一遍,比如从MySQL换到PostgreSQL的口味。因为不同方言在分页、字符串函数、窗口函数上都有差异,强迫自己重写时,你对数据本身的理解反而会更深。这不是为了炫技,而是拓展边界,避免面试时因为“数据库不一样”而卡壳。

我个人带过的人里,进步最快的那位不是SQL语法最熟练的,而是画图最勤奋的。我第一次看到他把一张成绩表画得密密麻麻、连外键方向都标了箭头时,就知道他面试稳了。如果你也在准备SQL面试,不妨也找一支笔、一张白纸,从50题的第一题开始,先把图画出来,再写SELECT。坚持两周,你大概率会回来告诉我,这招比多背二十道题都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询