简介:本资源是B站知名编程讲师Mosh Hamedani《SQL三小时入门》课程的系统化学习笔记,面向数据库初学者、转行新人及需快速掌握SQL核心语法的开发人员,聚焦关系型数据库查询能力构建。笔记以PDF格式呈现,共1个文件(2.43MB),内容高度凝练,覆盖SQL基础概念、SELECT查询、WHERE条件筛选、AND/OR/NOT逻辑运算,以及IN、BETWEEN、LIKE、REGEXP等关键操作符的语法、示例与使用场景,还包含IS NULL、ORDER BY、LIMIT、各类JOIN及UNION等实用模块,结构清晰、术语准确,适合作为随查随用的速记手册与课后复盘依据。目前已有976人学习下载,笔记源自Mosh官方YouTube教程(7S_tz1z_5bA)及配套Cheat Sheet,语言简明、排版规范,省去视频回看耗时,助力高效掌握SQL核心查询技能。
1. 这不是速成口诀,是能直接贴进你 SQL 脚本里的实战备忘录:Mosh 的三小时课程笔记为什么值得你花 20 分钟重读一遍
你有没有过这种经历:刚在 B 站刷完 Mosh 老师那节被标榜为「SQL 三小时速通」的视频,关掉页面后打开自己的数据库,敲SELECT * FROM users;都下意识加了个分号——结果报错?不是 MySQL,是 PostgreSQL;不是本地测试库,是公司生产环境里那个连LIMIT都要写成FETCH FIRST 3 ROWS ONLY的老系统。这节被千万人点开、收藏、又悄悄遗忘的课程,真正值钱的从来不是“三小时”这个时间标签,而是它把 SQL 从语法说明书,还原成了工程师日常调试时手边那张油渍斑斑的 Cheat Sheet:WHERE state IN ('VA', 'NY', 'CA')后面没写注释,但你一眼就懂这是在筛高价值客户池;REGEXP '^my|se'不是炫技,是某次清洗脏数据时,批量捞出所有以 my 开头或含 se 的用户名的真实命令。它不教你怎么设计范式,也不讲事务隔离级别,但它确保你在接到「查下上月注册且积分超 1000 的用户」需求时,5 秒内写出可跑通的第一版语句。适合谁?刚转行的数据分析新人、需要临时补数据库能力的前端/测试、还有那些常年用 ORM 却某天被 DBA 叫去改慢查询的后端——你们缺的不是理论,是一份能立刻抄、敢直接改、改完就能验证的“最小可用 SQL 手册”。
2. 从SELECT *到精准取数:为什么你的 WHERE 条件总在测试环境跑通,上线就翻车
2.1 SELECT 不只是“选所有”,它是数据流的第一道闸门
Mosh 在笔记开头就强调:SELECT * FROM customers;是教学起点,但生产环境里,它是性能杀手。真实项目中,我见过因SELECT *拉取了包含TEXT类型简历字段的整张用户表,导致接口响应从 80ms 暴涨到 2.3s 的案例。正确姿势是明确投影列:
-- ✅ 推荐:只取业务必需字段,显式声明,避免隐式类型转换风险 SELECT customer_id, first_name, last_name, email, points FROM customers WHERE state = 'CA'; -- ❌ 风险:若 customers 表后续新增了 BLOB 字段(如用户头像二进制),此语句会无感知拖慢全链路 SELECT * FROM customers WHERE state = 'CA';提示:
SELECT后的字段顺序直接影响客户端解析逻辑。比如 Python 的pandas.read_sql()默认按列序生成 DataFrame 列名,若 SQL 中SELECT id, name和SELECT name, id返回结构不同,下游代码可能因列索引错位而静默出错。
2.2 WHERE 子句的四个隐形陷阱:等号、空值、时区、大小写
WHERE 是 SQL 的心脏,但也是血泪最多的地方。Mosh 笔记里轻描淡写的state = 'CA',背后藏着至少四层现实水深:
等号 vs 安全等号:MySQL 中
=允许NULL比较(结果恒为UNKNOWN),但 PostgreSQL 严格遵循三值逻辑。当state列允许为空时:-- ✅ 安全写法:显式处理 NULL,兼容所有主流 RDBMS SELECT * FROM customers WHERE state = 'CA' OR state IS NULL; -- ❌ 危险写法:在 PostgreSQL 中,state = NULL 永远返回 FALSE,漏掉所有空状态记录 SELECT * FROM customers WHERE state = NULL;字符串比较的隐式类型转换:当
points是INT类型,但 WHERE 中写成points = '1000',MySQL 会自动转字符串再比对,而 SQL Server 可能直接报错。Mosh 笔记里points > 1000的写法,本质是强制类型对齐的防御性编程。时区陷阱:
birthdate > '1990-01-01'看似简单,但'1990-01-01'是无时区日期字面量。若数据库时区设为UTC+8,而应用层传入的是2024-01-01T00:00:00Z(UTC 时间),实际比对的是1990-01-01 00:00:00+08vs1990-01-01 00:00:00+00,差 8 小时。解决方案是统一使用带时区的时间戳函数:-- ✅ 显式指定时区,避免隐式转换歧义 SELECT * FROM customers WHERE birthdate > TIMESTAMP '1990-01-01 00:00:00+00';大小写敏感性:Mosh 笔记说 “SQL 不区分大小写”,指的是关键字(
SELECT,WHERE)。但字符串值是否敏感,取决于列的排序规则(Collation)。MySQL 默认utf8mb4_0900_as_cs是大小写敏感,而utf8mb4_0900_as_ci是不敏感。first_name LIKE 'b%'在敏感排序下只会匹配小写 b 开头,大写 B 会被过滤掉。生产环境必须查清排序规则:-- 查看 customers 表 first_name 列的排序规则 SELECT COLUMN_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'customers' AND COLUMN_NAME = 'first_name';
2.3 逻辑运算符的执行优先级:AND/OR/NOT 组合时,括号不是可选项
Mosh 笔记中WHERE birthdate > '1990-01-01' AND points > 1000看似直白,但一旦加入OR,优先级就成雷区。SQL 标准规定NOT > AND > OR,但人类直觉常误判。例如:
-- ❌ 危险:本意是“1990年后出生 OR 积分超1000”,但实际执行为 “(1990年后出生 AND 积分超1000) OR 积分超2000” SELECT * FROM customers WHERE birthdate > '1990-01-01' AND points > 1000 OR points > 2000; -- ✅ 必须加括号,让意图 100% 显性化 SELECT * FROM customers WHERE (birthdate > '1990-01-01' AND points > 1000) OR points > 2000;血泪经验:我在某电商促销系统中修复过一个经典 Bug——活动规则配置表里存着
is_vip = 1 OR points > 5000,但开发人员忘了加括号,导致is_vip = 0 AND points < 5000的用户也被错误发放了优惠券。上线前用EXPLAIN看执行计划,发现OR导致索引失效,最终强制拆成UNION优化。
3. 模糊匹配的三种武器:LIKE、REGEXP、BETWEEN,何时该用哪一把?
3.1 LIKE:简单模式匹配的黄金法则与性能红线
Mosh 笔记中first_name LIKE 'b%'是入门必学,但它的性能代价常被低估。%在开头('%b')或中间('%b%')会导致索引失效,因为 B-Tree 索引只能高效支持左前缀匹配。
-- ✅ 高效:利用索引,扫描范围可控 SELECT * FROM customers WHERE first_name LIKE 'b%'; -- ❌ 低效:全表扫描,大数据量时秒变卡顿 SELECT * FROM customers WHERE first_name LIKE '%b'; -- ⚠️ 折中方案:若必须查后缀,可建反向索引(MySQL 5.7+) CREATE INDEX idx_first_name_reverse ON customers ((REVERSE(first_name))); SELECT * FROM customers WHERE REVERSE(first_name) LIKE REVERSE('b%');参数说明:
_匹配单字符,%匹配零或多个字符。注意ESCAPE子句处理转义字符:-- 查询 first_name 中含下划线的用户(_ 是通配符,需转义) SELECT * FROM customers WHERE first_name LIKE '%\_%' ESCAPE '\';
3.2 REGEXP:正则表达式的威力与跨数据库兼容性黑洞
Mosh 笔记里REGEXP '^a'展示了正则的简洁,但REGEXP是 MySQL 特有语法,PostgreSQL 用~,SQL Server 用LIKE模拟或 CLR 函数,SQLite 需加载扩展。跨数据库项目必须规避REGEXP。
-- ✅ 兼容写法:用标准 SQL 的 LIKE 模拟基础正则 -- 目标:first_name 以 'my' 开头 或 包含 'se' -- MySQL REGEXP '^my|se' 的等价标准写法: SELECT * FROM customers WHERE first_name LIKE 'my%' OR first_name LIKE '%se%'; -- ✅ PostgreSQL 原生写法(非标准,但更强大) SELECT * FROM customers WHERE first_name ~ '^my|se'; -- ❌ 绝对禁止:在需要部署到多数据库的项目中使用 REGEXP -- 因为一旦换库,整条 SQL 报错,且无降级路径。避坑 / 常见问题 / 排查
现象 1:WHERE first_name REGEXP 'b[ru]'在 MySQL 8.0 中返回空,但在 5.7 中正常。
原因:MySQL 8.0 默认启用utf8mb4_0900_as_cs排序规则,正则引擎对 Unicode 处理更严格,[ru]可能因字符集边界匹配失败。
解决:显式指定字符集COLLATE utf8mb4_0900_as_cs或降级为LIKE组合。现象 2:
REGEXP 'ey$|on$'在某些数据中漏匹配EY(大写)。
原因:正则默认大小写敏感,而LIKE受列排序规则影响。若列是_ci(case-insensitive),LIKE不敏感,但REGEXP仍敏感。
解决:添加(?i)标志REGEXP '(?i)ey$|on$'。现象 3:
REGEXP查询响应时间从 50ms 暴涨到 3s。
原因:正则引擎无法利用索引,且复杂模式(如嵌套*)触发回溯爆炸(Catastrophic Backtracking)。
解决:用EXPLAIN FORMAT=JSON查看是否type: ALL(全表扫描),将正则逻辑前置到应用层过滤,或建生成列索引(MySQL 5.7+):ALTER TABLE customers ADD COLUMN first_name_lower VARCHAR(50) GENERATED ALWAYS AS (LOWER(first_name)) STORED; CREATE INDEX idx_first_name_lower ON customers (first_name_lower); SELECT * FROM customers WHERE first_name_lower REGEXP '^(my|se)';
3.3 BETWEEN:闭区间陷阱与日期边界精确控制
BETWEEN 100 AND 200看似安全,但BETWEEN是闭区间(包含两端),且对日期类型极易踩坑。Mosh 笔记中points BETWEEN 100 AND 200没问题,但换成时间:
-- ❌ 危险:本意查 2024-01-01 全天数据,但 BETWEEN 包含 2024-01-01 00:00:00 和 2024-01-01 23:59:59.999999, -- 若订单时间精度为微秒,2024-01-01 23:59:59.9999999(10位)会被漏掉 SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-01-01'; -- ✅ 推荐:用半开区间,精确到纳秒级无歧义 SELECT * FROM orders WHERE order_date >= '2024-01-01' AND order_date < '2024-01-02';参数说明:
BETWEEN仅适用于连续值域(数字、日期、字符串字典序)。对字符串WHERE name BETWEEN 'Alice' AND 'Bob',会包含'Alice'到'Bob'字典序之间的所有值,但'Bob'本身是否包含,取决于数据库对字符串末尾空格的处理(ANSI SQL 标准要求忽略尾部空格)。
4. JOIN 的五种形态:从 INNER 到 CROSS,一张表关联的底层逻辑拆解
4.1 INNER JOIN:数据交集的严格契约,不是“默认选择”
Mosh 笔记中JOIN orders o ON c.customer_id = o.customer_id是最简写法,但JOIN默认即INNER JOIN。新手常误以为“只要关联就用 JOIN”,却不知它会主动丢弃无订单的客户。某次我帮某高校实验室做学生选课分析,原始 SQL 写:
SELECT s.student_id, s.name, c.course_name FROM students s JOIN courses c ON s.major_id = c.department_id;结果只返回了有专业归属的学生和对应课程,而major_id为 NULL 的新生(尚未分专业)全部消失。修正为LEFT JOIN后,才看到 237 名新生的course_name为 NULL,这才是真实数据分布。
-- ✅ LEFT JOIN:保留左表全部,右表无匹配则补 NULL SELECT s.student_id, s.name, c.course_name FROM students s LEFT JOIN courses c ON s.major_id = c.department_id; -- ✅ RIGHT JOIN:同理,但极少用,可读性差,建议统一用 LEFT + 表序调整 -- ❌ 避免:无 `ON` 条件的 `JOIN`(即笛卡尔积),除非真需要全组合4.2 USING 子句:当列名完全相同时的语法糖与隐式约束
Mosh 笔记强调USING (customer_id)比ON c.customer_id = o.customer_id更简洁。但USING有硬性前提:两表中用于连接的列名必须完全相同,且数据类型兼容。若customers表是customer_id INT,orders表是customer_id BIGINT,MySQL 会隐式转换,但 PostgreSQL 可能报错。
-- ✅ 安全:列名、类型均一致 SELECT * FROM customers c JOIN orders o USING (customer_id); -- ❌ 危险:若 orders 表列名为 order_customer_id,则 USING 无效,必须用 ON -- ✅ 替代:显式 ON,类型转换可控 SELECT * FROM customers c JOIN orders o ON c.customer_id = CAST(o.order_customer_id AS SIGNED);注意:
USING会合并同名列,结果集中customer_id只出现一次。而ON写法中,c.customer_id和o.customer_id作为两个独立列存在。若后续要GROUP BY customer_id,USING写法更干净;若需分别引用两表 ID 做计算,ON更灵活。
4.3 CROSS JOIN:笛卡尔积的正当用途与性能核弹
Mosh 笔记中CROSS JOIN sizes用于“组合所有颜色与尺寸”,这是 CROSS JOIN 的典型场景——生成全排列。但它的危险在于:若colors有 10 行,sizes有 5 行,结果就是 50 行;若两表各 10 万行,结果 100 亿行,瞬间 OOM。
-- ✅ 正当用途:生成测试数据、枚举配置、计算组合权重 INSERT INTO product_variants (color_id, size_id, sku_code) SELECT c.color_id, s.size_id, CONCAT(c.code, '-', s.code) FROM colors c CROSS JOIN sizes s; -- ❌ 绝对禁止:无 WHERE 过滤的 CROSS JOIN 关联业务主表 -- 如:CROSS JOIN users u WITH orders o —— 千万级用户 × 百万级订单 = 百万亿行,DB 直接宕机避坑 / 常见问题 / 排查
现象 1:CROSS JOIN查询执行超时,EXPLAIN显示rows: 1000000000。
原因:未加任何过滤条件,且表数据量预估错误。
解决:立即加LIMIT 100测试结果集大小;确认是否真需全组合,否则改用INNER JOIN+ 关联条件。现象 2:
CROSS JOIN结果中出现大量重复 SKU。
原因:colors或sizes表自身存在重复主键(如未设PRIMARY KEY),导致组合爆炸。
解决:先SELECT COUNT(*) FROM colors; SELECT COUNT(DISTINCT color_id) FROM colors;检查去重率,修复源表主键约束。现象 3:
CROSS JOIN在 MySQL 5.7 中快,在 8.0 中慢 10 倍。
原因:MySQL 8.0 优化器对CROSS JOIN的物化策略变更,可能触发临时表磁盘写入。
解决:强制使用内存临时表SET SESSION tmp_table_size = 268435456;(256MB),或改用应用层双循环生成。
5. 数据写入的确定性原则:INSERT 的单行、多行、DEFAULT 与 NULL 的精确控制
5.1 INSERT VALUES:显式字段列表是生产环境的铁律
Mosh 笔记中INSERT INTO customers(first_name, phone, points) VALUES ('Mosh', NULL, DEFAULT)是规范写法。但新手常犯的错是省略字段列表,直接INSERT INTO customers VALUES (...)。这在表结构稳定时可行,但一旦customers表新增一列(如created_at DATETIME DEFAULT CURRENT_TIMESTAMP),旧脚本就会因值数量不匹配而报错。
-- ✅ 铁律:永远显式声明插入字段,与 VALUES 一一对应 INSERT INTO customers (first_name, last_name, email, points, phone) VALUES ('Mosh', 'Hamedani', 'mosh@codewithmosh.com', 1000, NULL); -- ❌ 危险:依赖表结构顺序,脆弱且不可读 INSERT INTO customers VALUES ('Mosh', 'Hamedani', 'mosh@codewithmosh.com', 1000, NULL);参数说明:
DEFAULT关键字表示使用该列定义的默认值(如points INT DEFAULT 0),而非NULL。NULL是明确赋空值,DEFAULT是调用列级约束逻辑。
5.2 多值 INSERT:批量写入的性能分水岭
Mosh 笔记中INSERT ... VALUES (...), (...), (...)是 MySQL 批量插入的标准语法。其性能远超单条INSERT循环,因为减少了网络往返和事务开销。但需注意最大包限制:
-- ✅ 推荐:每批 1000 行以内,平衡内存与网络 INSERT INTO customers (first_name, email, points) VALUES ('Alice', 'alice@example.com', 500), ('Bob', 'bob@example.com', 800), ('Charlie', 'charlie@example.com', 1200); -- ❌ 危险:单条 INSERT 包含 10 万行,触发 max_allowed_packet 限制(默认 4MB) -- 解决:拆分为多批,或调大参数(需 DBA 权限) -- SET GLOBAL max_allowed_packet = 64*1024*1024; -- 64MB5.3 NULL 与 DEFAULT 的语义鸿沟:何时该用哪个?
Mosh 笔记中phone列插入NULL,points列用DEFAULT,这揭示了核心设计哲学:NULL 表示“未知”或“不适用”,DEFAULT 表示“采用系统预设值”。例如:
phone为NULL:用户未填写手机号,信息缺失;points为DEFAULT:用户注册时未获初始积分,按规则给 0(假设points INT DEFAULT 0)。
-- ✅ 语义清晰:NULL 表示无电话,DEFAULT 表示按规则给 0 分 INSERT INTO customers (first_name, phone, points) VALUES ('Mosh', NULL, DEFAULT); -- ❌ 语义混淆:用 0 代替 NULL,无法区分“用户填了 0”和“用户没填” INSERT INTO customers (first_name, phone, points) VALUES ('Mosh', 0, 0); -- phone=0 是有效号码吗?points=0 是初始值还是扣减后?避坑 / 常见问题 / 排查
现象 1:INSERT ... VALUES ('Mosh', NULL, DEFAULT)报错 “Field 'points' doesn't have a default value”。
原因:points列未定义DEFAULT约束,或定义为NOT NULL且无默认值。
解决:检查表结构SHOW CREATE TABLE customers;,添加默认值ALTER TABLE customers MODIFY points INT DEFAULT 0;。现象 2:
INSERT后SELECT发现phone列是空字符串''而非NULL。
原因:应用层代码将空输入转为''插入,而''和NULL在 SQL 中完全不同(''是字符串,NULL是空值)。
解决:在应用层校验,空字符串统一转为NULL;或数据库层用TRIGGER自动转换。现象 3:
DEFAULT插入后,points值为NULL而非预期的0。
原因:DEFAULT仅在INSERT语句中显式使用时生效;若列定义为points INT DEFAULT 0 NOT NULL,但INSERT中写了points = NULL,则NULL会覆盖DEFAULT(因NOT NULL约束会报错,故实际是NULL被静默转为0)。
解决:始终用DEFAULT关键字,避免NULL字面量赋值给有默认值的列。
6. 从笔记到生产:如何用这份 Cheat Sheet 构建你的 SQL 防御性编程习惯
6.1 建立“三查一测”工作流:每次写 SQL 前的强制动作
Mosh 的笔记不是让你背下来,而是给你一套肌肉记忆。我给自己定的铁规是:任何一条发往数据库的 SQL,必须经过“三查一测”——这不是流程,是保命线。
查一:查字段来源
在SELECT后,逐个确认每个字段是否真的来自目标表,而非拼写错误的别名。曾有个线上事故:SELECT user_id, user_name FROM orders(orders 表无user_name),因 MySQL 的STRICT_TRANS_TABLES未开启,它静默返回NULL,导致下游报表用户姓名全为空。现在我的编辑器里装了 SQLLint 插件,SELECT后自动高亮不存在的列。查二:查 WHERE 边界
对所有WHERE条件,问三个问题:- 这个字段是否允许
NULL?如果允许,=是否会漏掉NULL记录?→ 改用IS NULL或COALESCE。 - 这个值是用户输入吗?如果是,是否做了
ESCAPE防注入?→LIKE ? ESCAPE '\\',参数化绑定。 - 这个条件会走索引吗?→ 用
EXPLAIN看key和rows,rows > 10000就得优化。
- 这个字段是否允许
查三:查 JOIN 类型
看到JOIN,立刻停顿 2 秒:我要的是交集(INNER)、左表全集(LEFT)、还是全外连接(FULL OUTER)?绝不容忍“先写JOIN,后面再想”。某次我写JOIN关联用户和地址,本意是查所有用户及其地址(LEFT),却写了JOIN,导致 37% 的用户地址为空被过滤,报表 GMV 少算 200 万。一测:测 LIMIT 1
任何SELECT语句,上线前必须加LIMIT 1执行一次,确认:- 返回列数、类型、NULL 性是否符合预期;
EXPLAIN显示的type是const/ref(好),不是ALL(坏);- 响应时间 < 100ms(慢查询阈值)。
这一步耗时 3 秒,但能拦截 80% 的低级错误。
6.2 构建你的个人 SQL 模板库:从 Cheat Sheet 到可复用代码块
Mosh 的笔记是种子,你要把它长成自己的树。我维护一个sql-templates.md,里面全是带注释的、可直接复制粘贴的片段:
### 【安全 SELECT】带字段检查与 LIMIT 的模板 ```sql -- [用途] 查询用户基本信息,确保字段存在且索引有效 -- [检查] 1. customers 表是否有 first_name, email, points 列? 2. state 列有索引? -- [执行] 先 LIMIT 1 测试,再删 LIMIT 上线 SELECT customer_id, first_name, email, points FROM customers WHERE state = 'CA' AND points > 1000 ORDER BY points DESC LIMIT 100;【防 NULL WHERE】处理可能为空的字段
-- [用途] 查询有手机号的用户,同时兼容 phone 为 NULL 或空字符串 -- [检查] phone 列类型是 VARCHAR?是否允许 NULL? SELECT * FROM customers WHERE COALESCE(TRIM(phone), '') != '';【跨库 INSERT】兼容 MySQL/PostgreSQL 的批量插入
-- [用途] 批量插入用户,避免 REGEXP 和方言差异 -- [注意] PostgreSQL 用 $1, $2 占位符;MySQL 用 ?;此处用通用 ? 占位 INSERT INTO customers (first_name, email, points) VALUES (?, ?, ?);> **血泪教训**:三年前,我因在 `WHERE` 中用了 `REGEXP`,导致项目从 MySQL 迁移到 PostgreSQL 时,27 个 SQL 文件全部报错,加班 36 小时手动替换。从那以后,我每次写 SQL 都强制问自己:“这段代码,能在 PostgreSQL、SQL Server、SQLite 上跑通吗?” 如果答案是否定的,立刻重构为标准 SQL 或封装到应用层。希望帮到你。 <p> <a href="https://download.csdn.net/download/weixin_55804389/77552081" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>