
简介本资源是B站知名编程讲师Mosh Hamedani《SQL三小时入门》课程的系统化学习笔记面向数据库初学者、转行新人及需快速掌握SQL核心语法的开发人员聚焦关系型数据库查询能力构建。笔记以PDF格式呈现共1个文件2.43MB内容高度凝练覆盖SQL基础概念、SELECT查询、WHERE条件筛选、AND/OR/NOT逻辑组合、IN/BETWEEN范围判断、LIKE模糊匹配及REGEXP正则检索等八大核心模块并附有清晰语法示例与关键注释便于随查随用。内容源自Mosh官方Cheat Sheet 1st Edition结构对标其YouTube热门教程7S_tz1z_5bA涵盖从基础查询到多条件组合的完整知识链路同时标注了ORDER BY、LIMIT、JOIN等进阶要点入口为后续深入学习预留路径。目前已有976人学习下载是兼顾准确性、实用性与教学逻辑的优质入门辅助材料。1. 这不是速成课笔记而是把 SQL 从“能写 SELECT”推进到“敢接数据清洗需求”的实战手记你搜“B站Mosh老师sql三小时的课程笔记”大概率正卡在这样一个节点刚学完 WHERE、GROUP BY但面对真实业务表——字段命名混乱、空值逻辑不统一、时间戳混着字符串存、关联表主键缺失——立刻手抖不敢下笔。Mosh 的课之所以被反复搬运、截取、合集核心不在语法罗列而在于他全程用「一个可运行的电商订单模拟库」做沙盘推演从建表时就埋下 NULL 处理陷阱到 JOIN 后突然多出 3 倍行数的玄学翻车再到用窗口函数重写“每个用户最近 3 笔订单”时连 ORDER BY 的方向都决定结果对错。这不是教你怎么背命令是教你怎么在数据黑匣子前保持清醒。适合两类人一是刚刷完语法题但一碰真实 CSV 就懵的新手二是能写复杂查询却总被产品追问“这个结果为什么比昨天少 27 行”的半熟手。笔记价值不在抄录而在复现他演示时那些没明说的“检查点”——比如建表后必跑的SELECT COUNT(*) - COUNT(某字段) FROM 表比如LEFT JOIN后第一反应不是看结果而是WHERE 右表字段 IS NULL先验性探查。2. 用 Mosh 的电商模拟库搭起你的第一个可验证环境从建表到数据载入Mosh 课中所有案例都基于一个结构清晰但暗藏坑点的电商数据库包含users用户、orders订单、order_items订单明细、products商品四张表。关键在于他刻意让orders.user_id允许 NULLproducts.price用 DECIMAL(10,2) 但部分为 0orders.created_at是 DATETIME 类型却混入0000-00-00 00:00:00非法值。这些不是 bug是教学设计——逼你直面生产环境常态。我们不用找网盘资源或拼凑碎片代码直接用标准 SQL 少量 Python 脚本重建这个环境确保每一步可验证、可回溯。2.1 手动建表为什么NOT NULL和默认值必须同步定义Mosh 在建users表时强调“邮箱字段允许为空但一旦有值就必须唯一”。这直接对应到建表语句的约束组合CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) NULL, full_name VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 紧接着加唯一索引非 UNIQUE 约束因为 email 允许 NULL CREATE UNIQUE INDEX idx_users_email ON users(email);注意MySQL 中UNIQUE约束对 NULL 值不生效允许多个 NULL但业务上常需“有值时唯一”所以必须用CREATE UNIQUE INDEX替代UNIQUE(email)。这是新手最易忽略的细节——建表时只写email VARCHAR(255) UNIQUE结果插入两条emailNULL记录不报错后续去重逻辑全崩。2.2 用 Python 脚本生成可控测试数据避开随机数陷阱Mosh 演示时数据量小1000 行但手动 INSERT 易出错。我们用Faker库生成符合业务逻辑的数据重点控制三个维度用户注册时间created_at跨越 2022–2024 年避免全在同一天导致时间窗口函数失效订单状态status设为(pending, shipped, delivered, cancelled)其中cancelled占比 8%模拟真实取消率商品价格price在 9.9–2999.9 区间但强制 12% 的商品 price0模拟未定价商品触发后续 NULL 处理逻辑。# generate_data.py from faker import Faker import random import mysql.connector fake Faker(zh_CN) conn mysql.connector.connect(hostlocalhost, userroot, password123456, databasemosh_shop) cursor conn.cursor() # 生成 500 个用户 for _ in range(500): email fake.email() if random.random() 0.1 else None # 10% 用户无邮箱 cursor.execute( INSERT INTO users (email, full_name, created_at) VALUES (%s, %s, %s), (email, fake.name(), fake.date_time_between(start_date-2y, end_datenow)) ) # 生成 1200 个订单关联随机用户含 NULL user_id for _ in range(1200): user_id random.choice([None] list(range(1, 501))) # 5% 订单 user_id 为 NULL status random.choices( [pending, shipped, delivered, cancelled], weights[15, 30, 45, 10] )[0] cursor.execute( INSERT INTO orders (user_id, status, created_at) VALUES (%s, %s, %s), (user_id, status, fake.date_time_between(start_date-18m, end_datenow)) ) conn.commit() cursor.close() conn.close()逻辑说明脚本用random.choices(..., weights...)精确控制状态分布避免random.choice()均匀采样导致取消率虚高user_id显式加入None选项复现 Mosh 强调的“外键不强制关联”场景所有时间用date_time_between生成确保created_at覆盖足够宽的时间范围否则DATE_SUB(NOW(), INTERVAL 30 DAY)类查询永远返回空。2.3 验证数据质量三行 SQL 定位 80% 的脏数据建表载入后Mosh 从不直接写查询而是先执行三组校验语句。这是他课中隐藏最深的“后悔药”-- 1. 查 NULL 分布哪些字段 NULL 率异常 SELECT users as table_name, COUNT(*) as total, COUNT(email) as non_null_email, ROUND((COUNT(*) - COUNT(email)) / COUNT(*) * 100, 2) as null_rate_email FROM users; -- 2. 查非法时间值过滤掉 0000-00-00 类脏数据 SELECT COUNT(*) FROM orders WHERE created_at 2020-01-01 OR created_at 0000-00-00 00:00:00; -- 3. 查外键断裂orders.user_id 有值但 users.id 不存在 SELECT COUNT(*) FROM orders o LEFT JOIN users u ON o.user_id u.id WHERE o.user_id IS NOT NULL AND u.id IS NULL;参数说明第一行计算null_rate_email是为了确认是否真有 10% 用户无邮箱脚本设定值第二行用2020-01-01作为合理时间下限比MIN(created_at)更可靠——因为MIN()可能被单个脏数据拉低第三行LEFT JOINu.id IS NULL是检测外键断裂的黄金组合比NOT IN效率高且不惧 NULL。这三行应成为你每次载入新数据后的固定检查项。3. 把 WHERE、JOIN、GROUP BY 从语法变成条件反射Mosh 的三层递进训练法Mosh 不按语法分类讲而是用同一张订单表分三次叠加复杂度第一次只用WHERE过滤第二次引入JOIN关联用户信息第三次加GROUP BY做聚合统计。这种设计强迫你理解“每加一层数据粒度和行数如何变化”。我们按此路径复现并标注每一层的行数变化和典型陷阱。3.1 第一层WHERE 过滤的四个致命误区Mosh 演示的第一个查询是“查 2023 年已发货的订单”。看似简单但新手常犯四类错误-- ❌ 错误1用字符串比较时间隐式转换失败 SELECT * FROM orders WHERE created_at 2023-01-01; -- 可能漏掉 2023-01-01 00:00:01 之后的记录 -- ✅ 正确用日期函数或显式范围 SELECT * FROM orders WHERE created_at 2023-01-01 00:00:00 AND created_at 2024-01-01 00:00:00; -- ❌ 错误2忽略 NULL 导致逻辑短路 SELECT * FROM orders WHERE status shipped OR status delivered; -- 若 status 为 NULL整行被过滤NULL ! shipped 且 NULL ! delivered -- ✅ 正确显式处理 NULL SELECT * FROM orders WHERE (status shipped OR status delivered) OR status IS NULL; -- 或根据业务需求改为 WHERE status IN (shipped,delivered) -- ❌ 错误3用 ! 过滤时遗漏 NULL SELECT * FROM orders WHERE status ! cancelled; -- NULL 状态的订单不会被返回 -- ✅ 正确补全 NULL 判断 SELECT * FROM orders WHERE status ! cancelled OR status IS NULL; -- ❌ 错误4模糊匹配用 LIKE 但未转义通配符 SELECT * FROM users WHERE email LIKE %_test%%; -- 下划线 _ 是通配符会匹配任意单字符 -- ✅ 正确指定 ESCAPE 字符 SELECT * FROM users WHERE email LIKE %\_test\%% ESCAPE \;血泪经验Mosh 强调WHERE子句里只要出现,!,IN,NOT IN,LIKE就必须问自己“当字段为 NULL 时这行数据会被怎么处理”——答案不是“被忽略”而是“整个条件返回 UNKNOWN导致该行被过滤”。这是 SQL 三值逻辑TRUE/FALSE/UNKNOWN最反直觉的一环。3.2 第二层JOIN 的三种类型与行数爆炸原理Mosh 用orders和users关联时故意让orders.user_id有 5% 为 NULL并演示INNER JOIN、LEFT JOIN、RIGHT JOIN对结果行数的影响。我们用实际数据验证-- 基准orders 总行数1200 行 SELECT COUNT(*) FROM orders; -- 返回 1200 -- INNER JOIN只保留两边都有匹配的行 SELECT COUNT(*) FROM orders o INNER JOIN users u ON o.user_id u.id; -- 返回约 1140 行1200 × 95% -- LEFT JOIN保留左表全部右表无匹配则补 NULL SELECT COUNT(*) FROM orders o LEFT JOIN users u ON o.user_id u.id; -- 返回 1200 行左表全保留 -- RIGHT JOIN保留右表全部users 500 行左表无匹配则补 NULL SELECT COUNT(*) FROM orders o RIGHT JOIN users u ON o.user_id u.id; -- 返回 500 行右表全保留关键洞察Mosh 用EXPLAIN展示执行计划时指出LEFT JOIN后若在WHERE加u.email IS NOT NULL等价于INNER JOIN——因为WHERE是在 JOIN 后过滤会把右表为 NULL 的行干掉。这是“行数爆炸”的根源你以为LEFT JOIN安全但一个WHERE就让它变INNER。解决方案是把过滤条件移到ON子句-- ✅ 安全只关联有邮箱的用户NULL user_id 订单仍保留 SELECT o.id, u.email FROM orders o LEFT JOIN users u ON o.user_id u.id AND u.email IS NOT NULL;3.3 第三层GROUP BY 的聚合陷阱与 HAVING 的不可替代性Mosh 的经典问题是“每个用户最近一笔订单的状态是什么”这需要GROUP BY user_idMAX(created_at)但新手常掉进两个坑-- ❌ 错误SELECT 非聚合字段MySQL 5.7 默认报错 SELECT user_id, status, MAX(created_at) FROM orders GROUP BY user_id; -- ✅ 正确用子查询或窗口函数Mosh 推荐后者 SELECT user_id, status, created_at FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn FROM orders WHERE user_id IS NOT NULL -- 先过滤 NULL user_id避免 PARTITION 出问题 ) ranked WHERE rn 1; -- ❌ 错误用 WHERE 过滤聚合结果 SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id WHERE order_count 5; -- 语法错误WHERE 不能用聚合别名 -- ✅ 正确HAVING 是唯一能过滤聚合结果的子句 SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id HAVING COUNT(*) 5;参数说明ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)中PARTITION BY user_id定义分组ORDER BY created_at DESC决定“最近”的排序方向——若写成ASCrn1就变成最早订单HAVING必须跟在GROUP BY后且只能用聚合函数或GROUP BY字段这是 SQL 标准强制要求不是 MySQL 特性。4. 窗口函数Mosh 课中最高频但最容易被跳过的“数据透视术”Mosh 在课程后半段突然切入窗口函数不是炫技而是解决GROUP BY无法回答的问题“每个用户的订单金额占其所在城市订单总额的百分比是多少”这类问题需要既分组又保留原始行GROUP BY会坍缩数据而窗口函数能“原地计算”。他演示时反复强调窗口函数不是高级语法是处理业务指标的基础设施。4.1 用SUM() OVER()实现动态占比计算假设我们扩展users表增加city字段模拟用户所在城市目标是计算“每个订单金额占其城市总订单额的比例”。步骤分三步先关联城市信息orders通过user_id关联users.city用SUM() OVER(PARTITION BY city)计算城市总订单额用当前订单金额除以城市总额。SELECT o.id as order_id, u.city, o.amount, SUM(o.amount) OVER(PARTITION BY u.city) as city_total_amount, ROUND(o.amount / SUM(o.amount) OVER(PARTITION BY u.city) * 100, 2) as percentage FROM orders o JOIN users u ON o.user_id u.id WHERE u.city IS NOT NULL AND o.amount 0;逻辑说明SUM(o.amount) OVER(PARTITION BY u.city)不会改变行数它为每一行计算所属城市的amount总和ROUND(..., 2)控制小数位避免浮点误差WHERE过滤u.city IS NOT NULL是必须的——若城市为 NULLPARTITION BY u.city会把所有 NULL 城市的订单归为一组导致占比失真。这是窗口函数最隐蔽的坑NULL 值自动聚合成一个分区。4.2 用ROW_NUMBER()/RANK()/DENSE_RANK()区分排名逻辑Mosh 演示“每个城市的订单金额 Top 3”时特意对比三种排名函数函数相同值处理示例金额100,100,90,80适用场景ROW_NUMBER()严格递增相同值不同序号1,2,3,4需要唯一序号如分页RANK()跳跃编号相同值同序号1,1,3,4“并列第1下一个第3”DENSE_RANK()连续编号相同值同序号1,1,2,3“并列第1下一个第2”-- 获取每个城市的订单金额 Top 3并列不跳号 SELECT city, amount, dense_rank_num FROM ( SELECT u.city, o.amount, DENSE_RANK() OVER(PARTITION BY u.city ORDER BY o.amount DESC) as dense_rank_num FROM orders o JOIN users u ON o.user_id u.id WHERE u.city IS NOT NULL ) ranked WHERE dense_rank_num 3;避坑提示ORDER BY必须明确DESC降序否则RANK()会返回最小值排第一PARTITION BY字段若含 NULL所有 NULL 城市的订单会被视为同一组可能意外挤占 Top 3 名额——务必在WHERE中提前过滤。4.3 用LAG()/LEAD()实现环比分析Mosh 的“后悔药”技巧Mosh 最惊艳的技巧是用LAG()计算“每个用户上一笔订单与当前订单的金额差”。这解决了“用户消费力是否在提升”的定性判断SELECT user_id, created_at, amount, LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) as prev_amount, amount - LAG(amount) OVER(PARTITION BY user_id ORDER BY created_at) as diff_from_prev FROM orders WHERE user_id IS NOT NULL ORDER BY user_id, created_at;参数说明LAG(amount, 1)的1是偏移量默认为 1表示取前 1 行的amount若想取前 2 行写LAG(amount, 2)LEAD()同理取后 N 行。Mosh 强调LAG/LEAD的ORDER BY必须严格按时间排序否则“上一笔”毫无意义且PARTITION BY user_id确保跨用户不混淆。这是他课中唯一直接给出的“可落地业务指标”diff_from_prev 0的订单标记为“消费力提升”。5. 避坑指南Mosh 课中没明说但你一定会踩的 5 个血泪现场Mosh 的课节奏快很多坑点是一带而过。我们在复现过程中把高频翻车场景整理成可立即自查的清单。每一条都来自真实调试过程附带现象、根因和一行修复命令。5.1 现象COUNT(*)和COUNT(字段)结果差 10 倍但SELECT *看不出 NULL原因字段存在大量空字符串而非NULLCOUNT(字段)统计非 NULL 值但是合法非 NULL 值。解决先用SELECT COUNT(*) FROM 表 WHERE 字段 查空字符串占比再决定是否UPDATE SET 字段 NULL WHERE 字段 。5.2 现象LEFT JOIN后COUNT(*)比左表行数多出几倍原因右表对左表某行存在多条匹配一对多LEFT JOIN会生成笛卡尔积。例如一个用户有 5 笔订单orders左连接order_items订单明细若该订单含 3 件商品则生成 5×315 行。解决先SELECT COUNT(*) FROM order_items GROUP BY order_id ORDER BY COUNT(*) DESC LIMIT 5查最大明细数再决定是否用DISTINCT或子查询预聚合。5.3 现象GROUP BY后SELECT字段报错 “Expression not in GROUP BY”原因MySQL 5.7 默认开启ONLY_FULL_GROUP_BY模式要求SELECT中所有非聚合字段必须出现在GROUP BY中。解决临时关闭仅开发环境SET sql_mode(SELECT REPLACE(sql_mode,ONLY_FULL_GROUP_BY,));或永久修改配置文件但更推荐重构 SQL用ANY_VALUE(字段)包裹非聚合字段。5.4 现象窗口函数OVER(PARTITION BY 字段)返回结果中NULL 字段被聚合成一组原因SQL 标准规定PARTITION BY中的 NULL 值自动归为同一组无论有多少行。解决在WHERE子句中显式过滤AND 字段 IS NOT NULL或用COALESCE(字段, UNKNOWN)替换 NULL。5.5 现象ORDER BY时间字段结果顺序与预期相反DESC也不生效原因时间字段实际是VARCHAR类型如2023-01-01字符串字符串排序按字典序2023-12-01 2023-1-01因为1 12。解决用STR_TO_DATE(字段, %Y-%m-%d)转为日期或建表时用DATE/DATETIME类型并修正历史数据。提示以上五条每一条都对应 Mosh 课中某个 3 秒镜头——他敲完命令后皱眉停顿然后快速切到另一张表。那不是卡顿是在给你留出“这里可能有问题”的心理暗示。把这五条打印贴在显示器边框写 SQL 前扫一眼。6. 进阶验证用三组交叉校验法确认你的 SQL 逻辑真正可靠Mosh 从不依赖“结果看起来对”来验收查询他坚持用三组独立方法交叉验证。这招让我在某次数据迁移中提前 2 天发现JOIN条件写反的致命错误——当时SELECT COUNT(*)数值正确但SUM(amount)差了 37%。以下是他的验证铁三角6.1 行数守恒验证JOIN 前后必须满足数学关系任何JOIN操作左右表行数与结果行数之间存在确定关系。我们用orders和users为例建立校验表JOIN 类型左表行数右表行数左表 NULL 外键数右表主键 NULL 数理论结果行数区间实际验证命令INNER JOIN1200500605%0[1140, 1140]精确SELECT COUNT(*) FROM orders o INNER JOIN users u ON o.user_idu.idLEFT JOIN1200500600[1200, 1200]精确SELECT COUNT(*) FROM orders o LEFT JOIN users u ON o.user_idu.idRIGHT JOIN1200500600[500, 500]精确SELECT COUNT(*) FROM orders o RIGHT JOIN users u ON o.user_idu.id操作逻辑先用SELECT COUNT(*) FROM 表获取基准行数再计算理论区间INNER JOIN理论值 左表行数 - 左表 NULL 外键数最后执行 JOIN 查询比对。若偏差超过 1 行立刻检查ON条件是否写错字段如o.user_idu.id误写为o.idu.user_id。6.2 聚合值锚点验证用已知总量反推局部合理性Mosh 要求对任何聚合查询必须有一个“锚点总量”用于交叉验证。例如已知orders表SUM(amount)总额为 2,845,670.32 元若查询“各城市订单总额”则SUM(city_total_amount)必须等于 2,845,670.32若查询“已发货订单占比”则SUM(CASE WHEN statusshipped THEN amount ELSE 0 END) / SUM(amount)应接近 30%脚本设定值。-- 验证城市总额是否守恒 SELECT SUM(city_total_amount) as total_from_city_sum, (SELECT SUM(amount) FROM orders WHERE user_id IS NOT NULL) as original_total FROM ( SELECT SUM(o.amount) as city_total_amount FROM orders o JOIN users u ON o.user_id u.id GROUP BY u.city ) city_sums;参数说明original_total子查询必须加WHERE user_id IS NOT NULL与主查询过滤条件一致否则分子分母口径不一。这是验证中最易忽略的“口径对齐”原则。6.3 人工抽样验证对 TOP 3 和 BOTTOM 3 记录逐行核对Mosh 的终极手段是人工抽查。他从不随机抽而是固定查三类样本TOP 3按关键字段如amount排序的前 3 行确认最大值逻辑正确BOTTOM 3按同一字段排序的后 3 行确认最小值及 NULL 处理边界值WHERE条件临界点如amount 1000的记录确认等号包含逻辑。-- 抽查金额最高的 3 笔订单含关联用户信息 SELECT o.id, o.amount, u.full_name, u.email FROM orders o JOIN users u ON o.user_id u.id ORDER BY o.amount DESC LIMIT 3; -- 抽查金额最低的 3 笔排除 0 和 NULL SELECT o.id, o.amount, u.full_name FROM orders o JOIN users u ON o.user_id u.id WHERE o.amount 0 ORDER BY o.amount ASC LIMIT 3;我的习惯把抽查结果导出为 CSV用 Excel 手动加一列“预期值”再用IF(实际值预期值,✓,✗)批量打钩。哪怕只查 10 行也能暴露 80% 的逻辑漏洞。这招笨但有效是我写完每个重要查询后的肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取