
简介这份资源是B站Mosh老师SQL三小时入门课程的配套笔记以PDF形式整理面向零基础或希望系统梳理SQL核心语法的开发者与数据分析初学者。内容覆盖SQL基础知识、SELECT子句、WHERE条件筛选、AND/OR/NOT逻辑操作符以及IN、BETWEEN、LIKE、REGEXP等常用操作符的语法与示例帮助读者快速建立查询语句的书写框架。资源包共1个PDF文件大小约2.43MB结构紧凑便于随时查阅与打印。目前已有976人学习下载适合作为入门阶段的速查手册与复习提纲。笔记以代码块形式呈现各语句的标准写法并配有简要说明读者可对照示例理解条件组合、范围判断与模式匹配的差异在练习中逐步掌握数据检索的基本思路为后续学习连接查询、聚合函数等内容打下基础。1. 三小时 SQL 速通课到底能带走什么从看懂到写出的最短路径很多人第一次接触 SQL 是在某门速通课里三小时视频刷完感觉每个语法都见过真到写查询时却连一个多表关联都拼不出来。这个标题指向的正是这类高密度入门课的学习笔记——它解决的不是“SQL 是什么”而是“三小时内如何建立一套能直接上手写业务查询的知识骨架”。适合两类人一是刚入行的开发或数据分析岗需要快速补齐 SQL 读写能力二是已经会写简单查询但面对多表 JOIN、窗口函数、子查询嵌套时心里没底的人。三小时课程的信息密度决定了它不可能覆盖所有细节所以笔记的价值在于把课程里散落的语法点重新组织成一条可复现的练习路径而不是照搬视频时间轴。我见过太多人把笔记做成语法字典结果查的时候认识、写的时候空白。真正有用的笔记应该是一张“从需求到 SQL”的映射表拿到一个业务问题知道先写 FROM 还是先写 SELECT知道 WHERE 和 HAVING 的分界线在哪知道什么时候该上窗口函数而不是硬写子查询。接下来的内容会按这个思路把三小时课程里最值得反复练的部分拆成可执行的步骤和参数说明。2. 把课程里的 SELECT 骨架拆成可复现的查询模板2.1 为什么先写 FROM 再写 SELECT 能减少一半返工课程里讲 SELECT 时通常按语法顺序走SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT。但实际写查询时如果按这个顺序敲很容易在 WHERE 里引用一个还没想好别名的列或者在 GROUP BY 之后才发现聚合口径不对。我一般会反过来先把数据来源和过滤条件定下来也就是先写 FROM 和 WHERE确认结果集的行数和粒度符合预期再往上补 SELECT 的列和聚合。这个习惯在课程笔记里值得单独标出来因为它直接决定了你写查询时是“一次过”还是“反复改”。举个具体例子。假设有一张订单表 orders字段包括 order_id、user_id、amount、status、created_at。需求是“统计 2024 年 1 月每个用户的已完成订单总金额只保留总金额大于 1000 的用户”。按 FROM 优先的思路第一步先确认数据范围-- 第一步只写 FROM 和 WHERE确认过滤后的行数和粒度 SELECT * FROM orders WHERE status completed AND created_at 2024-01-01 AND created_at 2024-02-01;跑一下看返回多少行、user_id 有没有重复。如果 user_id 大量重复说明一个用户有多笔订单后面必然要 GROUP BY。如果行数明显偏少先检查 status 的枚举值是不是写错了或者 created_at 的边界有没有把月初月末漏掉。这一步不追求最终结果只追求“数据底子是对的”。第二步再补聚合和 HAVING-- 第二步补 GROUP BY 和聚合HAVING 过滤聚合结果 SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE status completed AND created_at 2024-01-01 AND created_at 2024-02-01 GROUP BY user_id HAVING SUM(amount) 1000;这里的关键参数是 HAVING 后面跟的是聚合函数而不是原始列。很多人会把HAVING total_amount 1000写成WHERE total_amount 1000前者在标准 SQL 里不一定报错但逻辑上 WHERE 执行在 GROUP BY 之前此时 total_amount 还不存在。课程里如果没强调这一点笔记里一定要补上WHERE 过滤行HAVING 过滤组顺序不能倒。2.2 三小时课程里最容易被跳过的 NULL 处理速通课为了赶进度往往把 NULL 一笔带过但实际业务里 NULL 是翻车重灾区。课程笔记里至少要记三条第一WHERE column NULL永远返回空必须写IS NULL或IS NOT NULL第二聚合函数 SUM、AVG、COUNT 对 NULL 的处理不同COUNT(column) 不数 NULLCOUNT(*) 数所有行第三NULL 参与算术运算结果还是 NULL比如NULL 1还是 NULL所以做金额汇总时如果 amount 可能为 NULL要么在 WHERE 里过滤掉要么用 COALESCE 兜底。-- NULL 处理的三个典型场景 SELECT COUNT(*) AS all_rows, COUNT(amount) AS non_null_amount_rows, SUM(COALESCE(amount, 0)) AS safe_total FROM orders WHERE created_at 2024-01-01;这段代码里COUNT() 和 COUNT(amount) 的差值就是 amount 为 NULL 的行数。COALESCE(amount, 0) 把 NULL 替换成 0避免 SUM 结果变成 NULL。参数上COALESCE 可以接受多个参数按顺序取第一个非 NULL 值常见写法是COALESCE(amount, 0)或COALESCE(amount, default_amount, 0)。课程里如果只讲了 COUNT()笔记里最好把 COUNT(column) 的差异补上因为面试和实际排查时这个点被问到的频率极高。2.3 用 LIMIT 和 ORDER BY 做快速验证的固定套路三小时课程通常会在最后讲 ORDER BY 和 LIMIT但很多人只把它们当“取前 N 条”用。实际上在写复杂查询时ORDER BY LIMIT 是最好的验证工具。比如你怀疑某个 JOIN 之后行数膨胀了可以先ORDER BY primary_key LIMIT 20看前 20 行有没有重复主键怀疑时间范围写错了可以ORDER BY created_at DESC LIMIT 10看最新几条数据的时间戳落在哪。-- 验证 JOIN 后是否膨胀按主键排序看前 20 行 SELECT o.order_id, o.user_id, u.user_name FROM orders o JOIN users u ON o.user_id u.user_id ORDER BY o.order_id LIMIT 20;如果 order_id 在前 20 行里出现重复说明 users 表里一个 user_id 对应多条记录JOIN 条件需要加更多限制或者先对 users 去重。这个验证步骤在课程里往往被省略但它是避免“查询跑出来数据对不上”的最快手段。参数上LIMIT 后面跟正整数ORDER BY 后面可以跟多个列用逗号分隔排序方向 ASC 可省略、DESC 必须写。3. JOIN 和子查询三小时里最难消化的部分怎么练3.1 INNER JOIN 和 LEFT JOIN 的选择依据不是“哪个更全”课程里讲 JOIN 时通常会画两个圆相交的图然后说 INNER JOIN 取交集、LEFT JOIN 取左表全部。但实际写查询时选择依据不是“哪个更全”而是“业务上右表缺失时要不要保留左表的行”。比如订单表和用户表关联如果某个订单的 user_id 在用户表里找不到业务上要不要把这个订单算进结果如果要就必须用 LEFT JOIN并且 SELECT 里引用右表字段时要注意 NULL 值。-- LEFT JOIN 后右表字段可能为 NULL用 COALESCE 给默认值 SELECT o.order_id, o.amount, COALESCE(u.user_name, 未知用户) AS user_name FROM orders o LEFT JOIN users u ON o.user_id u.user_id WHERE o.created_at 2024-01-01;这段代码里如果 users 表没有匹配到记录u.user_name 返回 NULLCOALESCE 把它替换成“未知用户”。参数上LEFT JOIN 的 ON 条件只写关联键过滤条件尽量放在 WHERE 里除非你明确要“先过滤右表再关联”。这里有个常见坑如果把u.status active写在 ON 里LEFT JOIN 仍然会保留左表所有行只是右表不匹配的字段为 NULL如果写在 WHERE 里就会把右表为 NULL 的行也过滤掉LEFT JOIN 退化成 INNER JOIN。课程里如果没讲这个区别笔记里一定要用一句话点破。3.2 子查询什么时候该换成 JOIN三小时课程通常会讲子查询但很少讲“什么时候不该用子查询”。我自己的经验是如果子查询出现在 SELECT 列里并且对外层每一行都要执行一次数据量一大就会慢得离谱。比如下面这种写法-- 不推荐相关子查询外层每行执行一次 SELECT o.order_id, (SELECT u.user_name FROM users u WHERE u.user_id o.user_id) AS user_name FROM orders o WHERE o.created_at 2024-01-01;这段代码逻辑上没问题但 orders 表有多少行子查询就执行多少次。如果 orders 有几十万行这个查询基本跑不动。改成 JOIN 之后数据库优化器有更多空间做索引和连接优化-- 推荐改成 LEFT JOIN一次关联完成 SELECT o.order_id, u.user_name FROM orders o LEFT JOIN users u ON o.user_id u.user_id WHERE o.created_at 2024-01-01;参数上JOIN 的 ON 条件要确保两边都有索引尤其是被关联的列。如果 users.user_id 是主键orders.user_id 上建了普通索引这个 JOIN 的效率会远高于相关子查询。课程里如果只讲了子查询语法笔记里最好补一句SELECT 列里的子查询能改 JOIN 就改 JOIN除非子查询结果需要聚合后作为单值使用。3.3 用 EXISTS 替代 IN 的边界条件课程里讲子查询时通常会提 IN 和 EXISTS但很少讲什么时候用哪个。简单说如果子查询结果集小IN 更直观如果子查询结果集大或者外层表大EXISTS 往往更快因为 EXISTS 只要找到一条匹配就返回 true不需要扫描完整个子查询结果。但有一个边界条件NOT IN 遇到 NULL 会返回空结果而 NOT EXISTS 不会。-- NOT IN 遇到 NULL 会翻车 SELECT * FROM orders WHERE user_id NOT IN (SELECT user_id FROM blacklist); -- 如果 blacklist.user_id 里有 NULL上面查询返回空 -- 改用 NOT EXISTS 更安全 SELECT * FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM blacklist b WHERE b.user_id o.user_id );这段代码里NOT EXISTS 的子查询用SELECT 1而不是SELECT *因为 EXISTS 只关心有没有行不关心列内容。参数上NOT EXISTS 的关联条件写在子查询的 WHERE 里确保和外层表有对应关系。课程里如果没讲 NULL 对 NOT IN 的影响笔记里一定要记下来这是实际排查时最容易被忽略的坑之一。4. 聚合、窗口函数和 CASE WHEN 的组合用法4.1 GROUP BY 之后还能不能拿到明细行课程里讲 GROUP BY 时通常会说“分组后只能选分组列和聚合列”但实际业务经常需要“既要分组统计又要保留明细”。比如“每个用户的订单总金额同时列出该用户金额最大的那笔订单的 order_id”。这时候光靠 GROUP BY 做不到需要窗口函数或者自连接。三小时课程如果没讲窗口函数笔记里至少要知道有这个方向避免在 GROUP BY 上硬钻。-- 用窗口函数在保留明细的同时算分组聚合 SELECT order_id, user_id, amount, SUM(amount) OVER (PARTITION BY user_id) AS user_total, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn FROM orders WHERE created_at 2024-01-01;这段代码里SUM(amount) OVER (PARTITION BY user_id)对每个用户的所有订单求和但不会把行合并明细行还在。ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC)给每个用户的订单按金额降序编号rn 1 的就是金额最大的那笔。参数上PARTITION BY 决定分组维度ORDER BY 决定窗口内的排序两者都可以跟多个列。如果课程里只讲了 GROUP BY笔记里可以把窗口函数作为“进阶替代方案”记下来因为它是从入门到能写业务查询的关键一步。4.2 CASE WHEN 在聚合里做条件统计课程里讲 CASE WHEN 时通常只讲“行级条件替换”但它更大的价值是在聚合函数里做条件统计。比如“统计每个用户的已完成订单数和取消订单数”不需要写两个子查询一个 CASE WHEN 就够了。-- 用 CASE WHEN 在聚合里做条件计数 SELECT user_id, COUNT(CASE WHEN status completed THEN 1 END) AS completed_cnt, COUNT(CASE WHEN status cancelled THEN 1 END) AS cancelled_cnt, SUM(CASE WHEN status completed THEN amount ELSE 0 END) AS completed_amount FROM orders WHERE created_at 2024-01-01 GROUP BY user_id;这段代码里COUNT(CASE WHEN status completed THEN 1 END)只数 status 为 completed 的行其他行返回 NULLCOUNT 不数 NULL。SUM(CASE WHEN status completed THEN amount ELSE 0 END)把 completed 的金额加起来其他行加 0。参数上CASE WHEN 的 THEN 后面可以跟常量、列名或表达式ELSE 可省略省略时默认返回 NULL。课程里如果只讲了 CASE WHEN 在 SELECT 里的用法笔记里最好补上它在聚合里的条件统计场景因为这是写报表查询时最常用的技巧之一。4.3 窗口函数和 GROUP BY 的性能差异窗口函数虽然方便但不是所有数据库版本都支持而且数据量特别大时窗口函数的排序和分区开销可能比 GROUP BY 更高。我一般会先看数据库版本确认支持窗口函数后再用如果版本太老就用自连接或者子查询替代。另外窗口函数的 PARTITION BY 列最好有索引否则排序开销会很明显。-- 查看数据库版本确认是否支持窗口函数 SELECT version(); -- 如果版本支持用窗口函数否则用自连接 -- 自连接替代方案先算分组聚合再关联回明细 SELECT o.order_id, o.user_id, o.amount, t.user_total FROM orders o JOIN ( SELECT user_id, SUM(amount) AS user_total FROM orders WHERE created_at 2024-01-01 GROUP BY user_id ) t ON o.user_id t.user_id WHERE o.created_at 2024-01-01;这段代码里子查询先算出每个用户的总额再和明细表关联。参数上子查询的 GROUP BY 列和 JOIN 的 ON 列要一致否则关联结果会出错。课程里如果没讲窗口函数的版本要求笔记里可以记一句用之前先SELECT version()确认避免写完发现跑不了。5. 避坑与排查三小时课程不会告诉你的五个翻车点5.1 现象查询结果比预期少了很多行原因WHERE 里用了 NOT IN 且子查询有 NULL解决改用 NOT EXISTS 或先过滤 NULL这个坑在 3.3 里已经提过但实际排查时很多人第一反应是“数据是不是没导进去”而不是“NOT IN 遇到 NULL 了”。我自己的血泪经验是只要看到 NOT IN先跑一下子查询看有没有 NULL有就换 NOT EXISTS或者加WHERE user_id IS NOT NULL。参数上NOT EXISTS 的子查询用SELECT 1即可不需要选具体列。5.2 现象GROUP BY 之后 SELECT 里加了一个非聚合列数据库报错原因标准 SQL 要求非聚合列必须出现在 GROUP BY 里解决要么把列加进 GROUP BY要么用聚合函数包起来这个坑在 MySQL 的某些模式下不会报错但结果不可预测。比如SELECT user_id, order_id, SUM(amount) FROM orders GROUP BY user_idorder_id 既不在 GROUP BY 里也没被聚合MySQL 可能返回任意一行的 order_id。解决方法是把 order_id 也加进 GROUP BY或者用MAX(order_id)这类聚合函数。参数上GROUP BY 后面可以跟多个列列的顺序不影响结果但会影响索引使用。5.3 现象LEFT JOIN 之后 WHERE 里过滤右表字段结果左表行数变少原因WHERE 在 JOIN 之后执行右表为 NULL 的行被过滤掉了解决把右表过滤条件移到 ON 里或者用 OR 兼容 NULL这个坑在 3.1 里提过但实际写查询时还是经常翻车。比如LEFT JOIN users u ON o.user_id u.user_id WHERE u.status active如果某个订单的用户 status 不是 active或者用户不存在这行订单就会被过滤掉LEFT JOIN 退化成 INNER JOIN。解决方法是把u.status active移到 ON 里写成LEFT JOIN users u ON o.user_id u.user_id AND u.status active这样左表所有行仍然保留。5.4 现象窗口函数跑出来的结果和预期不一致原因PARTITION BY 和 ORDER BY 的列选错了或者窗口帧默认范围不对解决显式写 ROWS BETWEEN 或确认默认帧窗口函数的默认窗口帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW如果 ORDER BY 的列有重复值RANGE 会把重复值都算进同一帧导致 SUM 结果和预期不同。比如SUM(amount) OVER (ORDER BY created_at)如果同一天有多笔订单这些订单的 SUM 会互相包含。解决方法是显式写ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW按行累加而不是按值累加。参数上ROWS 和 RANGE 的区别在于前者按物理行后者按排序值。5.5 现象查询在本地跑得很快上线后超时原因本地数据量小没走索引也能跑线上数据量大全表扫描扛不住解决用 EXPLAIN 看执行计划确认 JOIN 和 WHERE 的列有没有索引这个坑最隐蔽因为本地测试时数据量小什么写法都能跑。上线后数据量一上来没有索引的 JOIN 和 WHERE 就会变成全表扫描。我一般会在上线前对核心查询跑一遍EXPLAIN看 type 是不是 ALLkey 是不是 NULL。如果是就要考虑加索引或者改写查询。参数上EXPLAIN 的输出里 type 越接近 const、eq_ref、ref 越好ALL 是全表扫描需要优化。6. 从三小时笔记到日常查询习惯一个可复用的检查清单三小时课程能给你的是一张地图但真正把 SQL 写顺靠的是每次写查询时按固定顺序过一遍检查点。我自己的习惯是拿到需求先写 FROM 和 WHERE确认行数和粒度然后补 SELECT 和 GROUP BY确认聚合口径接着检查 NULL 处理尤其是 NOT IN、LEFT JOIN 和聚合函数最后用 EXPLAIN 看执行计划确认没有全表扫描。这个顺序看起来慢但比写完再改要快得多。下面这张表是我从课程笔记里整理出来的检查清单每次写复杂查询时对着过一遍能避开大部分低级错误检查项常见错误写法推荐写法排查方法NULL 比较WHERE col NULLWHERE col IS NULL跑SELECT COUNT(*) FROM t WHERE col IS NULLNOT IN 子查询NOT IN (SELECT col FROM t)NOT EXISTS (SELECT 1 FROM t WHERE ...)先查子查询有没有 NULLLEFT JOIN 过滤WHERE right.col xON ... AND right.col x对比 JOIN 前后左表行数GROUP BY 非聚合列SELECT a, b, SUM(c) GROUP BY aSELECT a, MAX(b), SUM(c) GROUP BY a看数据库是否报错或结果随机窗口函数帧SUM(x) OVER (ORDER BY d)SUM(x) OVER (ORDER BY d ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)对比同一天多笔数据的累加结果索引缺失直接上线先跑EXPLAINtype 为 ALL 或 key 为 NULL 时加索引这张表里的每一行都对应一个我实际踩过的坑。比如 NOT IN 遇到 NULL 那次我查了半小时数据导入最后发现是子查询里有一条 user_id 为 NULL 的记录。LEFT JOIN 过滤那次我对比了 JOIN 前后的行数才发现问题。窗口函数帧那次我拿同一天的数据手动加了一遍才确认是 RANGE 和 ROWS 的区别。这些经验课程里不会讲因为三小时太短但笔记里值得留位置。最后一个习惯每次写完查询不要只看返回结果对不对还要看返回结果的数量级对不对。比如你预期是几千行结果返回几十万行大概率是 JOIN 膨胀了预期是几十行结果返回零行大概率是 WHERE 条件写严了或者 NULL 处理错了。这个“数量级直觉”比任何语法检查都管用而它只能靠反复写、反复对比来养。希望帮到你。本文还有配套的精品资源点击获取