我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

金老师SQL Lesson 6-12练习复盘:从JOIN到窗口函数的进阶指南

金老师SQL Lesson 6-12练习复盘:从JOIN到窗口函数的进阶指南 金老师这套SQL Lesson 6-12练习我前前后后带人刷了三遍第一遍是自己推敲每题背后的考点第二遍是给A同学讲题第三遍是在模拟面试里反复用了其中几个场景。先说结论第6到12课是整套课程的分水岭前面五课练的是单表查询的基本功到第六课开始猛然进入多表关联、聚合分组、子查询、窗口函数和条件逻辑这些真正贴近业务的核心语法。很多同学在这里开始跟不住但也恰恰是这部分练透了SQL才算真正脱离“查字典”阶段。这篇就把金老师练习里的题目体系、关键考点、常见问题和我的实操心得完整梳理一遍适合刚上完基础课、准备进阶的同学也适合想用练习验收自己SQL水平的在职数据分析师。1. 第6到12课到底练什么课程版图拆解1.1 第6-7课多表连接的语义比语法更重要这两课的主线是JOIN。练习一开始会给你两张表比如一张用户表、一张订单表要求统计“每个用户的订单数量”或“从未下单的用户”。听起来简单但同一个需求可以用LEFT JOIN、RIGHT JOIN、INNER JOIN写出完全不同的结果。金老师练习很会在这上面“埋雷”比如订单表里有一个用户id在用户表中不存在用INNER JOIN会直接少一行用LEFT JOIN则能保留用户却带出NULL。格式上还会混用WHERE和ON条件。很多初学者分不清ON a.user_id b.user_id AND b.status 有效和WHERE b.status 有效的差别。前者是在连接时过滤后者是连接完成后再过滤。放到LEFT JOIN场景里前者可能仍保留左表的所有行后者会把不满足条件的右表行连同左表行一起剔除。这个知识点不是背诵能解决的得动手跑一遍才能体会。第7课还会引入自连接和不等值连接。同一个员工表里找“比自己的经理工资还高的员工”本质就是员工表和员工表自己连。金老师练习里专门用这种题目逼你画表关系先想清楚谁是主表、谁是被查表再写别名。自连接一开始容易绕晕但只要记住“每一张副本都当成一张新表”的思路写起来就不会乱。1.2 第8课子查询是另一种拆解问题的思路第8课的核心是子查询包括标量子查询、IN/EXISTS子查询、FROM子句里的派生表。金老师练习很少直接让你背语法而是反复用同一个需求要求你用不同写法完成比如“找出工资低于部门平均水平的员工”既可以用JOIN加GROUP BY也可以用关联子查询。我特别推荐先练IN和EXISTS的等价替换。IN适合子查询结果集较小的情况EXISTS更适合判断“是否存在”这种语义两者在业务上常常可以互换但性能表现不一样。金老师练习里有一道经典题查“没有选任何课程的学生”。用NOT EXISTS改写比用NOT IN更安全因为当子查询结果中出现NULL时NOT IN会整体返回空集这是新手最容易踩的坑。后来课程引入了公共表表达式CTE也就是WITH ... AS。练习中的复杂嵌套子查询到CTE这一步可读性一下子提升很多。金老师练习里会要求把超过两层嵌套的子查询改写成CTE这不只是格式问题更是训练你“把大问题拆成有名字的中间结果”的思维方式。实际工作中CTE比嵌套子查询更容易调试你可以单独跑中间每一段确认结果无误后再拼主查询。1.3 第9-10课聚合统计与窗口函数两个层级的计算第9课回到聚合函数重点不是COUNT、SUM本身而是GROUP BY的语义。金老师练习里的经典错误是SELECT student_name, COUNT(*) FROM student GROUP BY class_id在严格模式下直接报错在非严格模式下会随机返回一个学生名。练习真正想让你理解的是SELECT里每个非聚合列都必须出现在GROUP BY里或者被某个聚合函数包裹。这不是SQL的毛病而是结果集的逻辑要求分组之后组内有多行系统不知道你该拿哪个值。第10课引入窗口函数这是很多人第一次接触“不减少行数的聚合”。SUM(amount) OVER(PARTITION BY user_id)可以在每一行显示该用户累计金额同时保留订单明细。金老师练习用一张“每日销售额表”让你同时做两个需求普通聚合求每日总额窗口函数求截至每天的累计总额。一个直观对比就能明白窗口函数的作用。窗口函数里的排序函数也是重点ROW_NUMBER、RANK、DENSE_RANK在相同分数时的表现完全不同。练习题反复让你用这三种函数给成绩排名并观察并列情况下的结果差异。很多人写完只看前几行没注意并列名次结果排序错乱。把排名错的同学拉到一起对比这个坑一次就记住了。1.4 第11-12课CASE WHEN和查询优化入门第11课的CASE WHEN看起来只是条件分支但金老师练习的应用场景很接地气把连续分数划分成“优秀、良好、不及格”再做分组统计或者把一张“用户-行为”流水表改造成宽表也就是分别统计每个用户的下单金额、收藏次数、加购次数。这个练习实质是训练“行转列”思路报表里非常常用。第12课开始接触查询优化。练习不会直接让你设计索引而是用一张大量数据的环境让你感受不同写法的成本差异。比如SELECT DISTINCT和GROUP BY在特定版本和场景下性能不同又比如 WHERE后面给函数包住索引列会导致索引失效。金老师练习里最典型的题目是“明明在某个字段上建了索引为什么查询还是很慢”原因往往是写法让优化器无法直接使用索引。2. 金老师练习的题目设计逻辑和易错点2.1 所有题目都在训练“结果集形状”意识我做过的很多学习类练习只在题目里写“请实现某功能”但金老师练习几乎每一题都会附带预期输出表头。比如“统计每个班级的人数输出两列class_id, student_cnt”这个细节看起来多余其实非常关键。先想清楚最终输出每一行代表什么粒度再写SQL能避免一半错误。练习中常见的结果集错位是多表关联后产生重复行。假设学生表和成绩表关联一个学生有3门课成绩那么关联后学生信息会重复3行。如果你再跟另一张“社团表”关联而这个学生加入了2个社团结果集会变成3乘2等于6行。金老师练习会专门出这种“乘数效应”题目让你学会在关联前先用聚合把多行压缩成一行减少不必要的笛卡尔积。另一个“形状”意识体现在GROUP BY的粒度上。题目说“按课程统计平均分”你按“学生课程”分组虽然不报错但结果完全不对。金老师练习的讲评习惯是先让人说出“这个结果的每一行代表什么”作为验收标准。我自己后来写复杂报表也会先在草稿纸上写“一行 一个客户最近一次订单信息”这样的描述再反推SQL结构。2.2 业务背景不是摆设是用来逼你理解条件的金老师的练习题很多都有业务场景订单状态、会员等级、课程表、考试成绩。有人觉得这些背景是多余的直接看表结构就行。但实际很多考点藏在业务约束里。比如“统计有效订单”你不会知道“有效”意味着status字段要等于某个值“查询连续3天登录用户”你需要先理解“连续”在数据表里如何用日期差表达。这种训练能锻炼一种关键能力把业务语言翻译成SQL条件。题目里说“近30天”你要反应过来是时间点取当前日期减30题目说“非会员且未复购用户”你可能要用到LEFT JOIN加IS NULL判断。金老师练习里很多题目是同一个条件反复变形今天叫“首单用户”明天叫“没有任何后续订单的用户”本质上都是找“关联表里不存在匹配行”的记录。业务场景还会逼你注意字段含义和重复值。比如“同一用户一天内可能有多条订单”统计“每天下单人数”时要先用COUNT(DISTINCT user_id)而不是COUNT(user_id)。缺失业务背景时很容易想当然认为一个用户一行。所以做这套练习时不要跳过场景描述读两遍题目再写SQL往往能少走很多弯路。2.3 高频易错点分布我在批改中看到的真实情况我整理了带练过程中出错率最高的几类问题排在前面的是错误类型具体表现易错环节NULL处理不当用COUNT(column)统计数量时漏掉NULL聚合、JOIN多表关联重复行关联前没压缩粒度结果集膨胀JOINWHERE与HAVING混用用WHERE过滤分组聚合结果GROUP BY非聚合列未分组SELECT中出现非聚合列不在GROUP BY中GROUP BY窗口函数排序并列三种排名函数选错窗口函数NOT IN遇到NULL子查询结果含NULL导致整体为空子查询这些错误不只是在练习题里出现日常生产SQL同样会遇到。金老师练习的高明之处在于它把这些问题分散在每一课的题目里靠反复踩坑形成“肌肉记忆”。下面我会挑几道代表性题目做一次完整复盘。3. 核心题目实操复盘3.1 多表关联练习统计每个学生的选课数量表结构CREATE TABLE student ( student_id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE course_selection ( student_id INT, course_id INT );题目查询每个学生的选课数量没有选课的学生也要显示选课数为0。参考写法SELECT s.student_id, s.name, COUNT(cs.course_id) AS course_cnt FROM student s LEFT JOIN course_selection cs ON s.student_id cs.student_id GROUP BY s.student_id, s.name;这里最关键的是COUNT(cs.course_id)不是COUNT()。因为在LEFT JOIN时没有选课的学生右侧字段全是NULL。COUNT()会把这个“NULL行”也计成1导致结果变成1而不是0。用COUNT(cs.course_id)只统计非空值才能得到0。讲评时金老师会提醒连接条件写成s.student_id cs.student_id不是s.student_id cs.student_id AND cs.course_id IS NOT NULL后者会破坏LEFT JOIN的保留语义把没选课的学生过滤掉。如果你不理解动手跑一下就知道差异。另一种做法是子查询SELECT student_id, name, (SELECT COUNT(*) FROM course_selection cs WHERE cs.student_id s.student_id) AS course_cnt FROM student s;标量子查询在逻辑上更直白但数据量大时性能可能不如JOINGROUP BY。金老师练习要求两种方法都写就是为了让你明白同一结果可以有多条SQL路径。3.2 子查询与CTE找出高于部门平均工资的员工表结构CREATE TABLE employee ( employee_id INT PRIMARY KEY, name VARCHAR(50), department_id INT, salary DECIMAL(10,2) );题目查询每个员工姓名、工资、所在部门平均工资并且工资高于部门平均工资。第一反应可能会写出下面的错误SQLSELECT employee_id, name, salary, AVG(salary) FROM employee WHERE salary AVG(salary) GROUP BY department_id;这个写法在WHERE里直接用聚合函数几乎在所有数据库中都会报错。原因很简单WHERE是逐行过滤的过滤时还没有分组也没有聚合结果。正确的思路是先把部门平均工资算出来再和员工表关联。用CTE写法WITH dept_avg AS ( SELECT department_id, AVG(salary) AS avg_sal FROM employee GROUP BY department_id ) SELECT e.employee_id, e.name, e.salary, d.avg_sal FROM employee e JOIN dept_avg d ON e.department_id d.department_id WHERE e.salary d.avg_sal;我曾经问过A同学为什么不用WHERE他回答“老师不让用”。这个答案说明他没理解。不是不可以用而是语义上聚合函数不能在WHERE过滤阶段出现。你完全可以用HAVING配合GROUP BY实现相同结果但那时输出粒度会被压缩成部门级不再保留员工明细。CTE的优势就在这里先算出中间结果再和其他表做关联问题维度切换很自然。如果做练习时觉得WITH ... AS不好想可以先写子查询版本再改写成CTE。金老师练习里通常会要求你保留两种写法我自己也推荐新手先写嵌套子查询跑通结果后再升级成CTE既能锻炼拆解能力也能体会CTE的可读性。3.3 窗口函数按课程分组取成绩前三名表结构CREATE TABLE exam_score ( student_id INT, course_id INT, score DECIMAL(5,1) );题目每个课程取得分前三名的学生输出课程、学生、分数、排名。窗口函数写法SELECT course_id, student_id, score, rank_num FROM ( SELECT course_id, student_id, score, ROW_NUMBER() OVER ( PARTITION BY course_id ORDER BY score DESC ) AS rank_num FROM exam_score ) t WHERE t.rank_num 3;这里我故意用ROW_NUMBER而不是RANK因为在“取前三名”的场景里通常需要的是稳定且唯一的序号对并列名次不敏感。如果题目说“显示并列排名取前三名”应该用RANK或DENSE_RANK。金老师练习里有一道变体题测试三种函数对相同成绩的处理ROW_NUMBER即使分数相同也会依次编号1、2、3不会并列。RANK相同分数会占用同一个名次比如两个第一后会跳过第二直接到第三。DENSE_RANK相同分数占用同一个名次但下一个名次连续。实际业务中“取每个分组最新一条记录”用ROW_NUMBER最稳妥“比赛排名”用RANK更符合直觉“按成绩区间划分等级”用DENSE_RANK更方便。练习时建议把同一个查询分别用三种函数跑一遍再看结果差异。写这段代码容易踩坑的是在SELECT里直接使用窗口函数后再用WHERE过滤比如WHERE ROW_NUMBER() OVER (...) 3。很多数据库不允许在WHERE里引用窗口函数所以需要包一层子查询。这个练习会反复用到“子查询包一层”的技巧属于高频必会写法。3.4 CASE WHEN成绩分段统计和行转列表结构沿用exam_score。题目把成绩分成“优秀(90)、良好(75)、及格(60)、不及格(60)”统计每个等级的人数。常见写法SELECT CASE WHEN score 90 THEN 优秀 WHEN score 75 THEN 良好 WHEN score 60 THEN 及格 ELSE 不及格 END AS grade, COUNT(*) AS cnt FROM exam_score GROUP BY grade;这里的两个细节容易出错。第一CASE分支顺序很重要。如果把score 60放在score 75前面那么75分也会被归到“及格”后面的分支永远不触发。写的时候必须从高到低或从低到高不能乱排。第二GROUP BY后面直接写别名grade在MySQL、PostgreSQL里可以但在某些数据库或者严格SQL模式下可能不识别。更稳妥的写法是重复CASE表达式或者再包一层子查询。练习的目的是让你知道别名可用但不能依赖它。CASE WHEN还可以做行转列。比如把“学生-课程-分数”变成“每个学生的一行列上分别是语文、数学、英语成绩”SELECT student_id, MAX(CASE WHEN course_id 101 THEN score END) AS chinese_score, MAX(CASE WHEN course_id 102 THEN score END) AS math_score, MAX(CASE WHEN course_id 103 THEN score END) AS english_score FROM exam_score GROUP BY student_id;这里必须配合MAX或MIN聚合因为同一个学生在同一课程下可能有多条记录CASE只会选出符合条件的值聚合函数再把这组中的非NULL值取出来。如果不加聚合SQL会报错或出现不可预期的行数。金老师练习把这种写法放在第11课其实就是为后面的数据透视做铺垫。3.5 综合练习查询连续3天登录的用户表结构CREATE TABLE user_login ( user_id INT, login_date DATE );注意同一个用户一天可能有多次登录记录所以先要去重。题目找出连续登录3天及以上的用户。思路是“日期减去行号”分组。先把每个用户的所有登录日期按天去重再按日期排序并生成序号如果日期是连续的那么login_date - 序号会被固定到同一个日期上。按这个分组值计数就能得到连续天数。WITH distinct_date AS ( SELECT DISTINCT user_id, login_date FROM user_login ), numbered AS ( SELECT user_id, login_date, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) AS rn FROM distinct_date ) SELECT user_id FROM numbered GROUP BY user_id, DATE_SUB(login_date, INTERVAL rn DAY) HAVING COUNT(*) 3;这段SQL是窗口函数和聚合函数的综合应用第6到12课的知识点几乎全用上了。金老师练习里专门安排这种综合题要求不只写出答案还要解释为什么用DATE_SUB相减为什么GROUP BY要包含DATE_SUB(login_date, INTERVAL rn DAY)。我见过不少同学卡在这里原因都是没先做DISTINCT。同一个用户一天登录两次在排序前不去重的话同一日期会占多个行号连续日期计算就会错乱。这个坑提醒我们拿到表第一步不是写代码而是先分析数据可能存在什么样的重复。4. 常见问题与排查技巧实录4.1 练习时出现报错先分清四类原因有人写SQL报错后很慌直接复制错误信息全网搜。金老师练习的讲评里建议先自己分类再决定怎么查。我把常见问题归成四类类别典型案例排查方向语法错误字段名写错、逗号多了、ORDER BY位置不对看错误提示指向的行号检查关键字顺序逻辑错误结果不报错但行数不对、数据重复对比预期输出检查JOIN条件和GROUP BY粒度空值错误统计结果少数据或COUNT值比预期少用SELECT *查看NULL分布检查JOIN类型性能问题语句能跑但非常慢看执行计划检查是否扫描全表权限/环境问题连不上表、表不存在核对数据库名、表名、权限配置练习里大部分问题属于第二类和第三类。报错反而是好事因为系统能明确告诉你最怕的是SQL“成功”执行但结果是错的这种就需要自己验证。4.2 排查SQL问题的“三看”方法我根据金老师练习的复盘经验总结了一个简单排查流程。先看表结构。用DESC或SHOW COLUMNS确认字段名、类型、主键、是否允许NULL。别小看这一步很多NULL问题、类型转换问题到这里就能发现。再看中间结果。如果一条SQL很复杂不要一条写到底把子查询或CTE的部分单独执行。比如先用SELECT COUNT(*) FROM ...看表数据量再SELECT * FROM ... LIMIT 10看样例确认每一个中间步骤都对再合起来。最后看结果集数量。对比需求判断最终结果应该按什么粒度返回。如果“每用户一行”结果却返回了多行大概率是关联表把数据放大了。这时候可以在SELECT里加上关联表的唯一键值或数据条数辅助观察快速定位是哪个JOIN引入的重复。这三看方法实践下来能解决绝大多数练习题问题。金老师练习每次课后都会附一个“结果自检清单”核心就是这三步。4.3 做题时最容易被忽略的NULL三陷阱NULL是SQL练习里的隐形扣分点金老师练习里反复出现我单拎出来讲。第一个陷阱COUNT(column)不会统计NULL但COUNT(*)会统计所有行。统计“填了手机号的用户数”用COUNT(mobile)可能得到比较小用COUNT(*)则把没填手机号的也加进来了。所以要根据业务语义选函数。第二个陷阱NULL参与算术运算后结果仍为NULL。比如salary bonus任何一个为NULL相加就是NULL。业务上需要把NULL按0处理时要写COALESCE(salary, 0) COALESCE(bonus, 0)。第三个陷阱NOT IN遇上NULL。子查询里有NULL值时WHERE user_id NOT IN (SELECT user_id FROM blacklist)可能查不出任何结果。安全的做法是改写成NOT EXISTSSELECT u.user_id FROM user u WHERE NOT EXISTS ( SELECT 1 FROM blacklist b WHERE b.user_id u.user_id );这三个陷阱在真实业务中都会遇到。练习题里出现的“正确答案不一定直观”往往就是因为NULL。4.4 金老师练习里反复强调的做题流程现在很多学习者习惯拿到题目就敲代码追求一次跑通。金老师练习更强调先设计再编码。我带人刷题时也会强制按照下面这套流程来读题至少两遍圈出需求关键词时间范围、筛选条件、分组粒度、输出列。在纸上或注释里写出预期结果集的“一行代表什么”。判断需要用到的表以及表之间的关联关系。先写基础过滤条件跑通一个简单版本。逐步添加分组、聚合、窗口函数每一步都验证中间结果。最后用边界值检查空值、0条数据、重复数据、时间边界。这套流程看起来繁琐但养成习惯后写SQL的准确率会明显提高。我自己在真实工作中写复杂统计报表也是先写一个能跑的最小版本再一层层往里面加逻辑而不是试图一次写出完美SQL。5. 练习之外的实战串联把题目变成工作技能5.1 把Top N题变成“取每组最新一条记录”的万能写法练习题里的分组Top N看起来只用于排名实际最大的价值是可以改造成“按用户取最近一笔订单”“按商品取最新价格”“按部门取最近入职员工”。这类需求在工作里经常出现统一思路都是先排序编号再过滤编号等于1。我经常把练习里的窗口函数改成一个模板WITH ordered AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY group_id ORDER BY event_time DESC ) AS rn FROM source_table ) SELECT * FROM ordered WHERE rn 1;只要把PARTITION BY换成业务分组键把ORDER BY换成“最新”的判断字段就能应对绝大多数“取每组最新一条”的需求。金老师练习的题目虽然用的是成绩和订单但迁移能力才是关键。5.2 用CASE WHEN做报表中的行转列如果只在练习题里做分段统计CASE WHEN的价值就被低估了。实际报表开发中经常要把明细型数据转成宽表。比如运营要看每个渠道每天的曝光、点击、转化数原始表是一行一个动作报表需要的是一行一个渠道列上是不同指标。用CASE WHEN配合SUM可以这样写SELECT channel, COUNT(DISTINCT user_id) AS uv, SUM(CASE WHEN action_type click THEN 1 ELSE 0 END) AS click_cnt, SUM(CASE WHEN action_type order THEN 1 ELSE 0 END) AS order_cnt FROM user_behavior GROUP BY channel;这种写法比多张子查询外连接更简洁也不需要额外创建表。金老师练习第11课正好用模拟数据让人写这种语句当时觉得只是练习后来在报表工作中直接照搬。5.3 最后分享一点个人体会练完金老师这套Lesson 6-12我最强烈的感受是SQL不是语法背得越多越厉害而是要通过做题把“问题拆解能力”提上来。同样的需求会写的人会先想结果集的粒度再反推需要哪些表和条件不会写的人一上来就敲SELECT结果边写边改。我建议每个练完这套练习的人再做一次“一题多写”自测挑一道多表关联题、一道聚合题、一道窗口函数题分别用三种不同写法实现。这个动作比单纯刷十道新题更有价值。因为SQL很多写法可以互相替代你掌握的组合越多遇到真实问题时就越不容易卡住。踩过几次坑之后你会发现真正难的不是某个函数不会用而是不知道一个业务问题该从哪个切入口下去。金老师练习的价值就是用一套题目把你推到那个切入口面前。
返回列表