ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

三小时SQL课程笔记:从建表到窗口函数的完整实战链路

三小时SQL课程笔记:从建表到窗口函数的完整实战链路 简介SQL是数据分析和后端开发中必不可少的基础技能但真正掌握它需要理解数据库查询背后的原理比如SELECT语句的书写顺序与执行顺序的区别、JOIN时主表的选择、GROUP BY与HAVING的分工等。这些核心概念不仅影响着日常多表查询的准确度也决定了聚合统计与报表分析的效率。从建表、单表过滤、多表关联到子查询、窗口函数、视图等进阶特性系统化的学习路径能帮助开发者避免语法误用与性能陷阱快速适用真实业务库。本博客笔记以一门三小时的SQL实战课程为主线提炼了从数据库搭建到复杂查询的完整知识链路为想要快速上手SQL或补齐进阶能力的开发者提供一份高效的参考。1. 为什么一份三小时的SQL课程笔记能顶得上半本教材做后端和数据分析的人几乎没有不跟SQL打交道的。但真要把SQL系统学一遍多数人走的弯路是一样的先买本六百页的大部头看了两周还在讲关系代数或者刷了一堆“SQL面试题”却连一条多表查询都写不顺。Mosh在B站那套三小时的SQL课程原版是 Programming with Mosh 的 SQL 课程厉害的地方在于它把数据库从建表到窗口函数串成了一条完整链路三小时看完你至少能独立写查询、建表、做聚合分析再看任何业务库的SQL都不发怵。我之所以愿意为这份课程专门写笔记是因为它和看文档、刷题不一样Mosh用的是真实业务场景一一“客户、订单、产品、支付”那套经典电商库每个知识点都是先看现象、再讲语法、最后丢一个练习。这篇笔记会按课程主线拆开讲把他在课上敲的语句、参数的含义、以及新手最容易翻车的地方全部标注出来。适合两种人一是刚入行的后端和数分实习生需要快速上手写业务查询二是用了很久SQL但只会“SELECT *”的野路子开发想把窗口函数、子查询这类进阶能力补上。2. Mosh三小时在讲什么一套从建库到报表的完整SQL知识图谱2.1 课程主线不是教语法而是教你怎么“查一家公司”Mosh的课程开篇没有像传统教材那样先讲数据库原理而是直接丢一个电商库出来。这个库包含客户表、订单表、产品表、支付表每张表之间有外键关联。他的教学逻辑是你在公司拿到的数据库就是长这个样子的先学会“读库”——看表结构、看字段类型、看主外键再去写语句。课程第一阶段聚焦单表查询。这里有个关键点Mosh强调SELECT语句的书写顺序和执行顺序是两回事。书写顺序是 SELECT - FROM - WHERE - GROUP BY - HAVING - ORDER BY - LIMIT但数据库引擎真正执行的顺序是 FROM - WHERE - GROUP BY - HAVING - SELECT - ORDER BY - LIMIT。这个差异新手几乎必踩坑比如你在SELECT里给字段起了别名想在WHERE里直接用这个别名过滤结果报错。原因就是WHERE执行在SELECT之前别名还没来得及生成。第二阶段是多表查询。Mosh用了大量篇幅讲JOIN不是只讲INNER JOIN而是把四种JOIN放在同一个业务场景里对比同样的客户表和订单表LEFT JOIN、RIGHT JOIN、OUTER JOIN分别出来的结果集长什么样哪些行会因为匹配不到而出现NULL。他特别强调了一个判断方法先搞清楚“哪张表是主表”你要保留哪张表的全部记录就从哪张表出发做JOIN。这个思路比死记硬背“LEFT JOIN是左表全保留”要可靠得多。2.2 课里暗藏的五个进阶知识点很多老手都没吃透三小时课程里真正值钱的不是SELECT和JOIN而是穿插在中间和后面的几个进阶点我用表格把它们列出来方便你自查哪些已经掌握、哪些还需要回去补课。知识点课程出现的场景常见误区掌握标准UNION与UNION ALL合并多个查询结果混淆去重逻辑能说清两者性能差异并正确选用窗口函数ROW_NUMBER/RANK分组排名与Top N和GROUP BY混用导致行数丢失能写出按部门排工资名次的语句子查询关联/非关联WHERE和SELECT子句中的嵌套查询没搞清关联子查询的执行时机能解释每一行计算一次的执行过程视图VIEW封装复杂查询为虚拟表以为视图会复制数据明白视图只是存储的SQL语句存储过程和函数参数化查询复用分不清过程和函数的返回值差异能写出带输入参数的查询过程我见过很多自称“SQL熟练”的人写个LEFT JOIN没问题但一碰到窗口函数就绕回子查询硬写写出来的SQL又长又慢。Mosh这套课把这些点都覆盖了而且每个点都是在他那个电商库上现敲现演示你跟着敲一遍就能感受到这些语法在真实业务里是怎么落地的。2.3 这套课程和刷题、看文档的本质区别在哪刷SQL题你是被题目推着走每道题是一个孤立的点今天练了去重明天遇到排名又卡住。看官方文档语法是全面的但MySQL官方文档关于SELECT的说明就有几十页你很难判断哪些语法是日常工作高频的、哪些是八百年用不上的。Mosh的课是用“业务目标”把知识点串起来的比如他想讲聚合就抛出一个问题每个州的客户数有多少于是GROUP BY、COUNT、SUM、HAVING自然就引出来了。你想解决业务问题的时候语法自然记住了。另一个区别是他在课里专门做了配套练习和作业每一小节结束有一个“Exercise”比如“找出2018年下单超过两次的客户”。这类练习和面试题不一样它是贴着一个完整库来做的你必须动脑子组合JOIN、GROUP BY和HAVING才能写出来。做完那几个练习你对SQL的感觉会比刷三十道LeetCode题目更扎实因为你是在“经营一家公司”不是在解抽象题。3. 跟课实操把Mosh在课上敲的核心SQL全部复现一遍3.1 准备环境装一个最小化的MySQL并导入课程库Mosh的课用的数据库是MySQL但SQL语法在PostgreSQL、SQL Server上绝大多数通用。前期准备两步装MySQL然后导入他提供的库文件课程简介里通常有下载链接。装库的时候建议用Docker跑一个干净实例避免在自己机器上装出问题还得折腾环境变量。# 用Docker启动一个MySQL 8容器root密码设为root docker run --name mosh-sql \ -e MYSQL_ROOT_PASSWORDroot \ -p 3307:3306 \ -d mysql:8.0 # 进入容器内部执行mysql客户端 docker exec -it mosh-sql mysql -uroot -proot这段命令里有两个容易被忽略的参数-p 3307:3306把容器内部的3306端口映射到本机的3307因为很多人本机已经装了MySQL占了3306映射到3307能避免端口冲突MYSQL_ROOT_PASSWORD是容器初始化时设置root密码的环境变量第一次启动后改密码比较麻烦所以启动时就要想好。如果你本机没有装MySQL也可以直接下载MySQL Community Server安装安装时记住root密码即可。课程库导入的方式很简单前提是你已经拿到了sql_store.sql这类文件Mosh课程包里通常包含建库脚本和示例数据。注意导入的顺序要先建库再导数据如果库名已经存在会报错。# 在容器内执行建库脚本假设脚本放在容器的/tmp目录 docker exec -i mosh-sql mysql -uroot -proot sql_store.sql # 登录后查看库是否导入成功 docker exec -it mosh-sql mysql -uroot -proot -e SHOW DATABASES;执行成功后会看到sql_store这个数据库出现在列表里。日常工作中从同事手里拿到dump.sql文件导入就是这么做的符号把文件内容重定向给mysql客户端执行。这里有个坑如果sql_store.sql文件编码是UTF-8且包含中文数据Windows下导入容易乱码建议导入前先确认文件编码或者在命令行加上--default-character-setutf8mb4参数。3.2 单表查询实操过滤、排序与去重的正确写法环境准备好后跟着课程从最简单的查询开始。Mosh每讲一个语法都会先演示一个“出问题”的写法再改成正确写法。第一个高频场景是按条件过滤客户数据很多新手在WHERE里用别名过滤会翻车。-- 错误写法WHERE里使用SELECT中定义的别名会报错 SELECT customer_id, first_name AS name FROM customers WHERE name Mary; -- 正确写法WHERE使用原始列名 SELECT customer_id, first_name AS name FROM customers WHERE first_name Mary;为什么WHERE里面不能用别名这和SQL执行顺序有关。数据库执行查询时FROM先定位表WHERE逐行过滤数据最后SELECT才计算表达式和别名。当你写WHERE name Mary时name这个别名还没生成数据库根本不认识这个列。这不是MySQL特有的限制Oracle、PostgreSQL、SQL Server行为一致。理解了执行顺序这类报错就能自己推断出来不用死记。排序和去重是面试和日常工作都绕不开的点。去重我已经习惯用DISTINCT处理但要注意一个坑DISTINCT是对整行所有字段的组合去重不是只对第一个字段去重。-- 查询客户所在州去掉重复值 SELECT DISTINCT state FROM customers ORDER BY state; -- DISTINCT多列两列都相同才算重复 SELECT DISTINCT state, city FROM customers ORDER BY state, city;第一句是单列去重返回所有不重复的州结果就几个值。第二句是对state city的组合去重比如加利福尼亚有两个城市都叫San Jose即使state相同city不同两行都会保留。课程练习里有一道题要求“找出所有不同的州”很多同学写成SELECT DISTINCT state, city结果和预期完全对不上问题就出在这个理解上。排序方面ORDER BY默认升序要倒序加DESC多列排序时每列各自指定升降序这些Mosh在课上都会演示跟着敲一遍就能记住。3.3 多表JOIN实操INNER JOIN和LEFT JOIN怎么选多表查询是SQL进阶的第一道大坎。Mosh课程里用的场景是客户表和订单表一个客户可能有多笔订单也可能零订单。在这个场景下四种JOIN的结果一目了然。-- INNER JOIN客户和订单的交集只有下过单的客户才会出现 SELECT c.customer_id, c.first_name, o.order_id FROM customers c INNER JOIN orders o ON c.customer_id o.customer_id ORDER BY c.customer_id; -- LEFT JOIN保留全部客户没下过单的客户订单字段显示NULL SELECT c.customer_id, c.first_name, o.order_id FROM customers c LEFT JOIN orders o ON c.customer_id o.customer_id ORDER BY c.customer_id;执行这两条语句你能直观看到区别INNER JOIN只返回两表匹配成功的行LEFT JOIN返回customers全表数据没有订单的客户那一行order_id是NULL。判断用哪种JOIN我一般问自己一个问题查询结果的“主对象”是谁要统计所有客户的下单情况主对象是客户就得用LEFT JOIN防止客户被丢掉要分析“已经下单的客户”的特征主对象是订单INNER JOIN就够了。这里还有一个Mosh在课里特别强调但很容易被忽略的细节表别名。在上面语句里我给每张表起了单字母别名c和o这不是为了省事而是当你两张表都有customer_id这个列名时不写别名直接写customer_id数据库会报“歧义列名”错误。用别名修饰后SQL引擎才能确定你取的是哪张表的字段。实际业务里一张SELECT语句JOIN五张表很常见给每张表统一命名规则比如表名首字母会大幅提高可读性。3.4 聚合与分组实操GROUP BY和HAVING的边界聚合函数配上分组是SQL从“查数据”到“做分析”的分水岭。Mosh的演示场景是统计每个州的客户数量第一步用GROUP BY第二步用HAVING做条件过滤。这两者的关系新手经常搞混。-- 统计每个州的客户数 SELECT state, COUNT(*) AS customer_count FROM customers GROUP BY state ORDER BY customer_count DESC; -- 只保留客户数大于1的州 SELECT state, COUNT(*) AS customer_count FROM customers GROUP BY state HAVING customer_count 1 ORDER BY customer_count DESC;第一句很好理解按州分组每组数出多少行。第二句就容易出问题了——为什么过滤分组后的条件不能写WHERE customer_count 1回到执行顺序WHERE在GROUP BY之前执行而customer_count这个聚合结果是在GROUP BY之后才产生的WHERE根本看不到它。能对聚合结果过滤的只有HAVING。用WHERE在分组前过滤原始行用HAVING在分组后过滤组这是两个完全不同的阶段。实际写业务时我见过不少开发为了省事把本可以放在WHERE里的条件塞进HAVING比如HAVING state CA。语法上不报错但性能差很多WHERE在分组前就过滤掉大量行HAVING要先分组、再聚合、最后才过滤白白浪费计算资源。判断原则很简单条件是针对原始行字段的放WHERE条件是针对聚合结果COUNT、SUM、AVG、MAX等的放HAVING。这条原则在面试里也经常被问到记住它少走一半弯路。3.5 进阶语法实操子查询、窗口函数与视图三小时课程的后半段Mosh开始讲“真正让你和别人拉开差距”的内容。子查询这部分他对比了关联子查询和非关联子查询的执行差异新手最容易掉进的坑是以为子查询只执行一次。其实关联子查询是“外层每一行内层都算一遍”性能差但逻辑灵活。-- 非关联子查询子查询独立执行结果作为常量 SELECT first_name, last_name FROM customers WHERE customer_id ( SELECT AVG(customer_id) FROM customers ); -- 关联子查询每个客户订单金额大于自己平均订单金额的写法 SELECT o.customer_id, o.order_id, o.total_amount FROM orders o WHERE o.total_amount ( SELECT AVG(total_amount) FROM orders WHERE customer_id o.customer_id );第一条语句的子查询SELECT AVG(customer_id)独立执行一次拿到平均值后供外层比较这种叫非关联子查询。第二条语句就不一样了内层子查询引用外层表的o.customer_id数据库的行为是外层orders表每一行都要带着当前行的customer_id去执行一次内层查询拿到这个人自己的平均订单金额再和外层当前行的total_amount做比较。这叫关联子查询。理解这个执行模型很重要否则你写出来的关联子查询可能结果正确但性能极差——外层一万行内层就要执行一万次索引建不好能把数据库拖垮。窗口函数是整门课含金量最高的地方Mosh用一个“按客户分组按金额排名”的例子讲清楚了它和GROUP BY的本质区别。-- 按客户分组统计总金额GROUP BY会折叠行 SELECT customer_id, SUM(total_amount) AS total_amount FROM orders GROUP BY customer_id; -- 窗口函数不折叠行每行都保留同时看到汇总值 SELECT customer_id, order_id, total_amount, SUM(total_amount) OVER (PARTITION BY customer_id) AS customer_total FROM orders ORDER BY customer_id, order_id;这两条语句放一起执行差异立刻可见GROUP BY把每个客户的多笔订单折叠成一行你只能看到汇总值看不到订单明细窗口函数不丢行返回每一笔订单同时用OVER (PARTITION BY customer_id)给这一行附上他名下所有订单的总金额。这个“在明细行旁边开一列汇总值”的能力做报表时几乎天天要用。窗口函数还有ROW_NUMBER()、RANK()、DENSE_RANK()这类排名函数区分这两个函数的关键是并列名次是否占位两个人并列第二RANK()的下一个是第四名DENSE_RANK()的下一个是第三名。Mosh的课程里对这个区别有专门的演示和练习值得反复看两遍。视图的理解和实操也值得单独拿出来说。Mosh在课里演示了怎么把一段经常要用的查询封装成视图之后查询可以直接从视图里取数。这里有个常见的认知误区我强调一下视图不存数据它只存SQL语句。-- 创建视图把客户和订单的关联查询封装成客户订单明细 CREATE VIEW v_customer_orders AS SELECT c.customer_id, c.first_name, c.last_name, o.order_id, o.total_amount FROM customers c LEFT JOIN orders o ON c.customer_id o.customer_id; -- 之后查询可以直接查视图就像查一张表 SELECT * FROM v_customer_orders WHERE total_amount 100;创建视图后每次执行SELECT * FROM v_customer_orders数据库其实是把你存的SQL语句重新执行一遍视图只是“命名了的查询”不是“复制了一份数据”。所以视图的优势是简化查询、统一逻辑而不是提升性能——想提升性能得去研究索引和物化视图那是另一个话题。Mosh在课里提了一句“视图不占存储”很多人没在意其实这是理解视图的关键改了底层表的数据查视图看到的就是最新的数据不需要去更新视图。4. 跟着Mosh做笔记的避坑清单五条血泪教训4.1 版本差异带来的翻车课程用MySQL 8你的环境可能还在5.7Mosh录制课程时用的MySQL版本比较新他在课里演示的窗口函数ROW_NUMBER等MySQL从8.0版本才开始支持。如果你本机装的是MySQL 5.7或者更老的版本按课里的语法敲窗口函数会直接报语法错误界面提示You have an error in your SQL syntax。现象在MySQL 5.7里执行带窗口函数的SQL语句报语法错误执行带有WITH子句的CTE查询同样报错。原因窗口函数和CTE是MySQL 8.0引入的功能5.7及以下版本不支持这些语法这不是你写错了是版本压根没有这个能力。解决有两种靠谱方案。一是卸载5.7改用MySQL 8.0这是最直接的Docker部署的话换镜像tag为mysql:8.0即可。二是如果公司生产环境必须用5.7课里的窗口函数部分可以改用子查询和用户变量重写虽然代码会变长但结果一致。我建议学习阶段直接上8.0因为SQL Server和PostgreSQL早就有窗口函数了这是行业方向早晚要会。4.2 大小写与分号为什么别人能跑你不能Mosh在课里写的SQL关键字都是大写很多同学跟敲的时候改成小写也能运行于是觉得大小写无所谓。但实际上MySQL在Linux上对数据库名和表名是区分大小写的对SQL关键字和列名不区分。Windows和macOS上表名默认不区分大小写Linux上一套同样的代码可能就报Table sql_store.Customers doesnt exist。现象同一段SQL在Windows本机跑得好好的部署到Linux服务器上就报表不存在。原因MySQL在Linux下对表名的存储和查找是区分大小写的你建表时写的customers查的时候写Customers数据库会去找Customers这张表找不到就报错。解决统一规范最重要表和库名全部用小写字母加下划线SQL关键字全部用大写查询语句里表名严格与建表时保持一致。这个习惯从第一天学就养好后面上生产环境能少踩一半的坑。分号的问题更隐蔽Mosh在课里每条语句结尾都写了分号在MySQL命令行客户端里分号是语句的结束标志不写分号按回车客户端会认为语句没写完继续等着你输入看起来就像卡住了。4.3 NULL值参与计算的玄学结果为什么比你预期的小SQL里NULL代表未知不是0也不是空字符串。这个坑在做金额统计时最致命。Mosh的课里专门演示了订单表中某些订单没有备注字段时的查询结果很多同学在这里栽了跟头。现象对订单金额做SUM或AVG聚合算出来的结果比手工加总的值小或者某些行的计算结果莫名变成NULL。原因SUM、AVG在遇到NULL值时默认跳过不是把NULL当0。而NULL与任何值做算术运算结果都是NULL。比如total_amount字段有一行的值是NULL直接拿total_amount * 0.1算提成这一行结果就是NULL不是0。解决做算术运算前用IFNULL或COALESCE把NULL转成0。COALESCE支持多个参数返回第一个非NULL值。但这里又有第二个坑如果你把NULL全转成0再做AVG平均值会被拉低因为0被算进了分母而AVG本身忽略NULL时分母只算非NULL行。所以业务上要明确“没填的金额”到底该按0算还是按“不参与统计”算这决定了你用IFNULL还是直接保留NULL。Mosh在课里没有展开这个细节但实际写报表时几乎必遇到。4.4 课能看懂但练习做不出来卡住时先做这步很多学员反馈Mosh的课看一遍觉得都会了一做练习就卡在JOIN上半小时写不出一条语句。这个不是理解问题是熟练度问题。Mosh的练习通常不给参考答案所以卡住时你只能自己排查。现象课程配套练习“找出2018年下单超过2次的客户”看着题目知道要用JOIN、GROUP BY、HAVING但不知道从哪张表出发写。原因没有养成“先画表关系再写SQL”的习惯。你直接上手写SELECT写到JOIN时发现不知道该连哪张表、用什么条件。解决我一般会建议先停下来花两分钟做三件事。第一读题圈出题目涉及的名词客户、订单确定涉及哪几张表。第二找出表之间的关联字段比如订单表和客户表通过customer_id关联。第三判断主对象题目问的是“客户”所以以客户表为主表LEFT JOIN订单表然后按客户分组HAVING过滤下单次数大于2。这个流程理顺了SQL自然就出来了。课程练习的意义就是逼你走这个流程不要急着看答案先自己画一遍图。4.5 跟着课程敲代码还报错先检查是不是输入法问题这不是段子我陪跑过不少新手一半以上的“语法错误”是中文输入法引起的。Mosh在课里敲的分号、括号、逗号都是英文半角字符你跟敲的时候如果在中文输入法状态下输入标点会变成中文全角字符MySQL解析器不认。现象明明照着课程一行一字敲的报You have an error in your SQL syntax near...而且提示位置正好在分号或括号附近。原因SQL解析器只认英文半角字符中文逗号、中文分号、中文括号都会被识别为非法符号。解决写SQL的时候把输入法切成英文模式这是最直接的。如果已经写了一半在编辑器里打开显示空白字符的功能把全角标点替换成半角。MySQL Workbench和VS Code都支持这个功能。这个坑看起来小但会消耗你大量耐心建议提前留意。5. 学完三小时后怎么继续进阶验证学习效果的三招课程看完了练习也做了怎么验证自己真的掌握了我给自己定了三招验证方式你也可以照着做。第一招脱离课程库拿真实业务库练手。Mosh的库是为了教学设计的数据很规整字段命名也规范真实业务库脏得很字段叫cst_nm、addr1、crt_tm还有大量NULL和历史遗留冗余。你把课程学到的JOIN、聚合、窗口函数拿到这种库上写一遍才是真本事。具体做法是找一张你们公司订单表的脱敏副本试着回答三个问题本月销售TOP10的客户是谁、各区域销售额环比变化多少、每个销售名下客户的复购率是多少。这三个问题分别考到了排名窗口函数、日期函数和子查询。第二招把课上每一个练习改成不同的问法重做一遍。比如Mosh的练习统计“2018年下单超过2次的客户”你可以改成“2019年下单但从未退货的客户”或者“平均订单金额大于500元的客户”。改写问法等于强制你重新拆解表关系而不是背答案。我自己的经验是一个场景能用至少五种说法变着问每个变体都写通了这个场景涉及的SQL技能才算真会了。第三招也是最狠的一招给自己出题写一份周报SQL。假设老板让你出一份“按周汇总各产品线的订单量和销售额并标出每周排名第一的产品”。这道题要用到日期函数提取周、JOIN产品表和订单表、GROUP BY汇总、再套窗口函数做排名。你能在不查笔记的情况下独立写出来三小时课程的内容就真正消化得差不多了。写完之后和同事或网上社区里的参考答案对比看看哪里有优化空间这比刷十道题都有用。最后说点个人教训。我第一次看Mosh的课是快进看的感觉啥都会结果到公司写一条“统计各个城市销量前3的产品”就卡了半小时当时没意识到RANK()和ROW_NUMBER()在并列值上的行为差异有多大。后来老实回到课里把他的练习全部手敲一遍又把窗口函数那节反复看了三遍才彻底通了。所以我的习惯是新技能学完至少要亲手敲二十条完整SQL每条都跑通、看到预期结果这个技能才算真正长在身上。这条路没有捷径但Mosh的课已经把最短路径画出来了剩下的就是动手。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进