资讯动态

SQL四章笔记:从基础查询到事务与性能优化全解

发布时间:2026/10/21 6:27:01 来源:尧图企业网站定制
1. 为什么是这四章事情要从一次数据库崩溃说起。某天夜里我临时接到通知说业务系统的报表查询把库压死了慢日志里全是几百秒的大查询。我用 explain 一个个拆发现大部分问题不是索引没建而是同事写 SQL 的时候连最基本的执行逻辑都没想清楚。那次之后我花了两周时间把自己脑子里的 SQL 知识重新捋了一遍整理成了四章笔记。这个“SQL四章笔记”不是一个课程大纲也不是书目录的搬运而是我实际排查问题、写接口、做数据分析时反复用到的最小知识集。简单说四章是基础查询与过滤、多表连接、聚合分组、事务与性能优化。每一章都对应一类最常见的线上事故或者说每一个都是能直接救命的工具。适合谁看如果你是刚接触数据库的开发者这四章可以让你少走大半年的弯路如果你已经写了几年 SQL但全靠复制粘贴改改条件这四章可以帮你把经验系统化以后遇到问题不再是试出来的而是推断出来的。我尽量不端着讲理论所有内容都是实操里验证过的能直接用。2. 基础查询别只盯着语法理解执行顺序才是关键2.1 SELECT 不只有 SELECT很多人学 SQL 的第一课就是SELECT * FROM table然后以为 SQL 就是“查出来给我看”。但实际上你写下的每一个子句执行顺序和书写顺序是完全不同的。理解这个顺序是所有 SQL 优化的地基。SQL 的逻辑执行顺序大致是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT。我见过太多人刚学完就觉得“WHERE 是先过滤SELECT 是最后选列”却忽略了这背后的一个重要事实你写的列别名在 WHERE 里不能用。因为 WHERE 在 SELECT 之前执行别名根本还没生成。这个问题我在带新人时几乎每次都要强调一次。再一个基础但很容易被忽略的点SELECT DISTINCT和GROUP BY去重在结果等价的情况下执行路径可能完全不同。Distinct 是对结果集做一次去重操作而 GROUP BY 通常还会伴随聚合。如果一个查询只是想去重但把 GROUP BY 加上了不必要的聚合函数性能上会有明显差异。实测在百万行表上无意义的MAX(id)会让查询慢上好多。2.2 WHERE 过滤的性价比差异WHERE 子句看起来简单但不同写法的代价天差地别。我把常见写法分成了几类在索引列上进行函数运算比如WHERE DATE(create_time) 2024-01-01索引直接失效全表扫描。使用LIKE %keyword%前缀模糊索引同样用不上。对可空字段写WHERE column ! xNULL 行会被排除这往往不是你想要的结果。多条件组合时条件的书写顺序并不决定执行顺序优化器会自己重排但你要保证每条过滤本身是高效的。我自己最常用的替代方案是把对列的函数操作改写成范围条件。比如上面的日期过滤写成WHERE create_time 2024-01-01 AND create_time 2024-01-02执行计划立刻改善。像这种改动在某些数据库里会从全表扫描变成索引范围扫描线上效果差距几十倍是很正常的。2.3 别小看 NULL 和三值逻辑讲个真事有次统计订单金额同事用WHERE amount ! 0想把无金额的单子过滤掉结果发现部分单子靠标记位判断不出来。原因就是amount字段允许 NULL而 NULL 参与比较时结果不是 TRUE 也不是 FALSE而是 UNKNOWN整行被过滤掉了。很多看似“不可能出错”的业务统计就是栽在这种小细节上的。处理 NULL 的正确姿势要么在建表时加NOT NULL DEFAULT要么在 WHERE 条件里显式把 NULL 纳入判断。我前几年写 SQL 经常图快不写字段约束后来统计口径反复对不上才彻底改了习惯凡是业务上不允许为空的字段一律加约束。这种事写进笔记第一页很值得。3. 多表连接别看到业务就 JOIN先想清楚连接方向3.1 INNER JOIN、LEFT JOIN 到底在干嘛多表连接是 SQL 里最容易被“用错但结果碰巧对”的部分。INNER JOIN 是取交集LEFT JOIN 是以左表为准补右表RIGHT JOIN 用得少是因为你换个顺序就能等价。真正让我警觉的是很多人在写 LEFT JOIN 时对右表加了过滤条件比如SELECT a.id, b.name FROM orders a LEFT JOIN users b ON a.user_id b.id WHERE b.status 1;这个写法的问题在于一旦 WHERE 里用到右表字段LEFT JOIN 的外连接语义就被破坏了结果等同于 INNER JOIN。如果你本意是“所有订单都要用户信息能取则取”那这个查询会悄悄把那些没有对应用户的订单滤掉。线上数据量越大这种 bug 越隐蔽。正确写法是把这个条件挪到 JOIN 子句里SELECT a.id, b.name FROM orders a LEFT JOIN users b ON a.user_id b.id AND b.status 1;这个例子我几乎每次培训都会讲因为它是连接语义的经典误用而且结果不会报错不会红牌警告只会静静给你一份缺失数据的报表。3.2 JOIN 方向选择与结果行数预估还有一种更容易埋雷的情况两张表本来是一对多关系你从多表那边做 LEFT JOIN结果会产生重复行。比如一个订单有多个支付记录你要查订单信息再关联支付流水一单多流水会把订单数据复制多份。如果你的统计维度是“订单数”直接 COUNT(订单表.id) 就会偏大因为行数膨胀了。我处理这类问题通常先问业务上“主表是哪张、粒度是什么”再决定连接方向。粒度不清晰JOIN 的结果就不可信。如果是订单主表做维度我宁可先把支付流水按订单维度聚合一次再关联回来也不冒行数膨胀的风险。这个“先聚合再关联”的思路在报表查询里极其常用比盲目 JOIN 要快得多。另一个实践中常见的场景是分页 JOIN。比如要分页查“订单列表附带用户昵称”很多人的写法是先 JOIN 再 LIMIT这在数据量大时会导致数据库先完成整张大表的连接再取出分页区间前几页还好翻到很后面就明显变慢。更聪明的做法是先从主表查出当前页的主键集合再用主键去关联其他表取字段。这个技巧我叫它“推迟 JOIN”实测在深分页场景能显著缩短查询耗时。3.3 连接条件里的类型隐患连接条件里字段类型不一致是索引失效和结果错误的双料元凶。比如a.user_id是 VARCHARb.id是 INT很多数据库会有隐式转换走不了索引甚至结果错乱。我遇到过一次同事把用户ID存成字符串关联时忘了转直接导致某些 ID 匹配不上业务侧查了半天还以为是数据问题。这种事情写笔记时必须放在显眼位置连接字段类型必须一致或者显式 CAST。最稳妥的办法是建表时就把关联字段设计成同类型。在字段命名、类型上多花十分钟后面能少流很多汗。4. 聚合分组核心不是函数怎么用而是数据维度怎么定4.1 GROUP BY 的真正语义很多人把 GROUP BY 理解为“分组”这没错但不够深刻。我更喜欢把它理解成“把多行压成一行”而这个“压”的过程要求 SELECT 里出现的非聚合列必须出现在 GROUP BY 里。这个限制不是数据库在给你添麻烦而是防止你写出语义有歧义的查询。但需要注意的是MySQL 曾经有个“宽松模式”允许 SELECT 里出现不在 GROUP BY 中的列并且返回的只是这一组里的随机值。这在低版本里是坑在高版本里默认被禁用。如果你还在维护老项目尤其要小心这种隐形的非确定性结果。笔记里我专门加了一条检查项所有 SELECT 中的非聚合列逐一核对是否在 GROUP BY 中。4.2 聚合前的过滤与聚合后的过滤WHERE 和 HAVING 的定位区别很多人背过“WHERE 在前HAVING 在后”但实际写的时候还是会混用。WHERE 是聚合前的行级过滤HAVING 是聚合后的组级过滤。如果你要“排除某些订单再算金额”条件是放在 WHERE 里的如果你要“只保留总金额大于某值的分组”就得用 HAVING。从性能角度看能用 WHERE 完成的过滤就不要留给 HAVING。因为提前过滤掉的数据不会进入聚合计算能显著降低中间结果集的大小。我见过一个慢查询就是对几百万行做 GROUP BY 之后再用 HAVING 过滤如果把条件提前到 WHERE耗时下降非常明显。这是个性价比极高的优化。4.3 COUNT、SUM 的隐式陷阱COUNT(字段) 和 COUNT() 的区别看起来是常识但依然常有人中招。COUNT() 是统计行数COUNT(字段) 是统计该字段非 NULL 的行数。如果你的字段允许 NULL那这两个结果对不上统计口径直接出问题。我习惯在统计行数时总是用 COUNT(*)不为别的就是不给 NULL 留任何操作空间。SUM 也有类似的问题字段全为 NULL 时SUM 返回 NULL 而不是 0。业务里展示“合计金额”时如果结果集为空页面显示就可能变成空白。处理方式很简单用COALESCE(SUM(amount), 0)兜底。这种细节很琐碎但正是这些琐碎决定了线上报表可不可信。4.4 分组后的排序与分页如果是报表场景分组之后几乎必然要 ORDER BY比如按销售额倒序取 TOP N。这里容易踩的一个坑是排序字段是否在 SELECT 里以及排序是在分组前还是分组后。ORDER BY 的生效范围一定是在分组之后如果你想“每个类目下取最新一条”直接用 ORDER BY 加 GROUP BY 是做不到的得用窗口函数或者子查询先排好序。聚合分组这一章我一般建议配合真实业务反复练单看函数文档很难建立起“维度”这个思维。你脑子里得先有“我要看什么维度、什么度量、粒度是什么”这三个问题然后才是写 GROUP BY。5. 事务与性能优化笔记的终点是线上稳定5.1 事务隔离级别的实际影响事务这章我放在笔记靠后的位置但它绝对是核心中的核心。很多人知道 ACID但不太清楚隔离级别对线上行为的影响。以 MySQL 默认的 REPEATABLE READ 为例它在同一个事务内多次读取同一批数据结果是一致的这给业务带来了便利但也意味着你可能读到的是快照数据而不是最新提交。在实际开发里我遇到最多的场景是一个事务内先查询某个值再根据这个值去更新。如果隔离级别控制不好或者没加锁两条并发请求可能都读到旧值然后各自更新造成覆盖写入。这类问题比慢查询更危险因为它是逻辑层面的并发错误报错都报不出来。解决思路要根据业务来选择能接受一致性弱一点就读已提交必须严格并发控制就要考虑加锁或使用乐观锁机制。5.2 索引失效的常见场景与自查清单性能优化这章我把索引相关的内容做成了自查清单模式因为索引问题太常见了几乎每次排查线上慢查询都要用到。失效的常见场景包括对索引列使用函数、隐式类型转换、LIKE 左侧模糊、OR 连接非索引条件、在索引列上做计算。这些场景如果避不开至少要知道它会导致索引失效从而评估是否值得改写。另外一个很经典的场景是复合索引的最左前缀原则。要发挥复合索引的作用查询条件必须从索引的最左列开始匹配。很多人建了(a, b, c)索引然后只查b和c索引完全帮不上忙。我见过最可惜的情况是字段顺序建反了实际高频查询恰好不走索引而改一个索引顺序就能解决问题。建索引前先列一遍业务最常用的几种查询模式比用默认方式随意加索引靠谱得多。5.3 EXPLAIN 怎么看别只盯着 type 和 rowsEXPLAIN 几乎是每个 DBA 都会让开发者先跑一遍的命令但很多人只会看 type 是不是 ALL。确实ALL 代表全表扫描看到了会紧张但这不是全部。我很看重的还有 key 是否用到了预设索引、rows 的预估行数是否符合预期、Extra 里有没有出现讨厌的Using filesort或Using temporary。Using filesort意味着排序没有走索引在这个字段上做排序或分组时如果有高频业务建议把排序字段一起放进索引。Using temporary更值得关注它往往意味着查询过程中建了临时表这在数据量大时可能导致磁盘临时表成本飙升。我自己的排查流程是先看 type 是否出现 const 或 ref再看 key 和 rows最后盯 Extra。三样都过关再谈其他优化。这样能避免只抓住一个点而漏掉整条查询链路的问题。5.4 优化不是堆索引改 SQL 优先于加索引最后我想说一个思维方式的问题很多人一遇到慢查询就条件反射“加索引”。但真实场景里很多慢是因为 SQL 写法本身绕了远路比如子查询套娃、连接顺序不合理、在循环里逐条查数据库。这些情况下加索引只能治标改写法才能治本。举个例子。我排查过一个统计查询原 SQL 把数据全部查出后在应用层循环几千次再去查明细库。当时没有任何单个查询是慢的但整体接口却慢得离谱。把逻辑改写成一个 JOIN 聚合后响应时间从十几秒降到两百毫秒以内。这种优化比加索引有价值得多。所以笔记里我特意留了一段能改 SQL 写法的不要先动索引能减少查询次数的不要增加查询复杂度。6. 这套笔记的说是方法也是避坑手册回看这四章本质上就是一套从“能跑”到“跑得好”的进阶路径。基础查询章解决的是语法层次的理解偏差多表连接章解决的是数据关系上的语义混淆聚合分组章解决的是统计口径和粒度问题事务与性能优化章解决的是线上稳定性的底线问题。如果你现在是一个刚接触 SQL 不久的人我建议你不要只是读一定要拿自己的业务数据把每一章的示例跑一遍尤其是故意踩一遍那些坑比如在 LEFT JOIN 后加右表过滤或者在 WHERE 里对索引列做函数计算。很多东西只有在自己手上翻过车下次才能彻底长记性。我个人在实际操作中的体会是用笔记记录知识只是起点真正有价值的是把笔记变成自己排查问题的思维习惯。我后来每次写 SQL 都会先问自己几个问题我要的粒度是什么连接关系会不会膨胀过滤条件是否让索引失效事务里有没并发覆盖的可能。这些问题就是这四章笔记浓缩下来的骨架每一条背后都是一次线上教训。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑