ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL查询实战:过滤、排序、JOIN与EXPLAIN性能优化全解析

MySQL查询实战:过滤、排序、JOIN与EXPLAIN性能优化全解析 你要是只会写SELECT * FROM user那 MySQL 查询这块其实还没真正入门。真正跑业务的时候面对的是“统计每个城市的订单金额”“找出一个月没下单的用户”“给一张千万级的大表做分页列表”这时候 SQL 写得好不好差别一下就出来了。这篇基础实战篇(2)专门聊数据查询操作过滤条件怎么写才不踩坑排序有哪些反直觉的细节聚合分组怎么不出错多表关联和 JOIN 背后的坑以及一条慢查询怎么用 EXPLAIN 快速定位。内容适合刚学完建库建表、准备开始写业务的读者也适合写了好几年 SQL 但没仔细抠过细节的同学对照检查。1. 先打个底SQL逻辑执行顺序与一套能练手的测试表1.1 书写顺序和逻辑顺序是两码事新手最容易困惑的是明明 SQL 写得很顺眼为什么一直报“未知列”为什么 WHERE 里不能直接用 SELECT 里的别名关键就在于SQL 真正执行的顺序和书写顺序不是一回事。一段 SQL 的书写顺序是SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT但 MySQL 内部的逻辑执行顺序大致是FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT这两者一对比很多问题就有了答案。比如 WHERE 里不能引用 SELECT 别名因为 WHERE 执行时 SELECT 还没开始计算而 ORDER BY 可以引用 SELECT 别名因为排序发生在 SELECT 之后。再比如分析一条 SQL 时最好从 FROM 开始看先确定数据源再一步步看过滤、分组、投影。举一个实际例子SELECT user.city, COUNT(*) AS order_cnt FROM orders o JOIN user u ON o.user_id u.id GROUP BY user.city HAVING order_cnt 1 ORDER BY order_cnt DESC;如果你写成WHERE order_cnt 1那一定会报错因为在 WHERE 阶段根本没有 order_cnt 这个字段。这个坑我见过太多新手踩他们以为 SELECT 里定义的别名全语句通用结果被报错卡了半天实际只要记住“先 FROM后 SELECT别名只能用于后面的环节”就够了。1.2 一套可以直接抄的演示表为了下面所有例子都能直接跑我先建两张表模拟一个极简的电商订单场景。一张用户表一张订单表。这里故意留了一条没有订单的用户同时在订单表里放了一条 user_id 不存在的“孤儿订单”这样后面讲 LEFT JOIN 时能直观看到 NULL 的来历。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, phone VARCHAR(11), age TINYINT, city VARCHAR(20), create_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, amount DECIMAL(10,2) NOT NULL, order_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;插入演示数据INSERT INTO user (username, phone, age, city, create_time) VALUES (张三, 13800000001, 28, 北京, 2023-01-05 10:00:00), (李四, 13800000002, 35, 上海, 2023-02-12 14:30:00), (王五, 13800000003, 22, 广州, 2023-03-08 09:20:00), (赵六, 13800000004, 40, 深圳, 2023-05-01 11:45:00), (孙七, 13800000005, NULL, 杭州, 2023-06-19 16:00:00); INSERT INTO orders (user_id, product_name, amount, order_time, status) VALUES (1, 手机, 3999.00, 2023-03-01 12:00:00, 1), (1, 耳机, 199.00, 2023-03-15 20:30:00, 1), (2, 键盘, 299.00, 2023-04-02 18:10:00, 1), (3, 显示器, 1299.00, 2023-04-18 09:30:00, 0), (3, 鼠标, 89.00, 2023-05-22 22:00:00, 1), (4, 手机, 3999.00, 2023-06-01 15:45:00, 0), (4, 电脑, 6999.00, 2023-06-10 10:00:00, 1), (6, 平板, 2999.00, 2023-07-01 08:00:00, 0);说明一下orders 表里 user_id6 的在 user 表里不存在这就是前面提到的“孤儿订单”。线上库经常因为删用户、历史迁移等原因留下这种数据所以我在演示数据里故意保留它。你直接把上面语句复制到本地 MySQL 跑一遍后续每个例子都能跟着验证。2. WHERE与ORDER BY过滤条件怎么写得既对又快2.1 WHERE条件的四个高频坑第一个坑NULL 判断。很多新手写SELECT * FROM user WHERE age NULL;结果永远查不到数据。因为 NULL 不是“等于什么值”而是“未知”状态判断只能用IS NULL或IS NOT NULLSELECT * FROM user WHERE age IS NULL; SELECT * FROM user WHERE age IS NOT NULL;类似地统计的时候COUNT(age)会忽略 NULL而COUNT(*)不会。语义完全不同用错之后统计数字偏差可能很隐蔽不容易被发现。第二个坑在列上套函数索引直接失效。比如想查某天下过单的记录SELECT * FROM orders WHERE DATE(order_time) 2023-03-15;这条 SQL 逻辑上没错但 order_time 列被 DATE() 函数包住后MySQL 只能老老实实全表扫描就算 order_time 上有索引也用不上。更好的写法是改成范围条件SELECT * FROM orders WHERE order_time 2023-03-15 00:00:00 AND order_time 2023-03-16 00:00:00;数据量一大这个改写带来的性能差距是几十倍甚至上百倍。类似的情况还有YEAR(create_time) 2023碰到这种需求先想能不能转成范围查询。第三个坑隐式类型转换。如果 phone 列是 varchar你拿数字去比较SELECT * FROM user WHERE phone 13800000001;MySQL 可能会把列转成数字再做比较索引照样失效。应该写成SELECT * FROM user WHERE phone 13800000001;这类问题在手机号、身份证、银行卡号等看起来像数字、实际是字符串的列上特别常见。第四个坑把简单的 IN 写成 OR。比如SELECT * FROM user WHERE city 北京 OR city 上海; SELECT * FROM user WHERE city IN (北京, 上海);两条语义等价但 IN 的写法更清晰优化器也更容易走正确的索引路径。尤其当 OR 两侧涉及不同的非索引列时处理难度会陡增能用 IN 就别用 OR。2.2 ORDER BY里那些反直觉的事排序默认升序这个大家都懂。但 NULL 值排序是个冷门知识点。在 MySQL 里ORDER BY age ASC时NULL 排在所有非 NULL 值的最前面ORDER BY age DESC时NULL 排在最后。如果你希望 NULL 固定放在最后可以这样写SELECT username, age FROM user ORDER BY ISNULL(age), age ASC;ISNULL(age)对 NULL 返回 1非 NULL 返回 0排序列会先排 0 再排 1所以 NULL 自然落到最后。这种写法在做“可选字段排序”时非常有用比如个人资料里的年龄、会员等级这类可空字段。多字段排序时字段顺序很敏感SELECT username, age, city FROM user ORDER BY city ASC, age DESC;这里会先按城市排序城市相同的再按年龄降序。如果把两个字段调换顺序排序优先级就完全变了。这种问题在报表导出、列表展示时特别容易踩需求说“先按城市再按年龄”结果代码里写反了最终数据顺序不对排查起来还很困难。还可以用 FIELD() 实现自定义顺序排序SELECT username, city FROM user ORDER BY FIELD(city, 北京, 上海, 广州), username ASC;FIELD() 会按你给定的列表顺序返回序号适合“重点城市优先展示”这类业务。不过要提醒自定义排序时索引基本帮不上忙数据量小问题不大数据量大要提前评估性能。排序还有一个高频问题Using filesort。简单说如果 MySQL 需要对结果集额外排序就会使用文件排序。这个操作可以尽量避免方法是让排序列尽量命中索引。比如ALTER TABLE orders ADD INDEX idx_user_time (user_id, order_time); EXPLAIN SELECT * FROM orders WHERE user_id 1 ORDER BY order_time DESC;有了这个 (user_id, order_time) 复合索引user_id 等值过滤后order_time 在索引里本来就是有序的Extra 里就不会再出现 Using filesort。很多人乱建索引没效果往往就是因为字段顺序不对优化器根本用不上。3. GROUP BY与聚合函数统计报表里的高频坑3.1 COUNT、SUM、AVG的细微差别先看 COUNT 这组函数SELECT COUNT(*), COUNT(age), COUNT(DISTINCT age) FROM user;COUNT(*) 统计行数COUNT(age) 只统计 age 非 NULL 的行COUNT(DISTINCT age) 去重后统计非 NULL 值。在刚才的演示数据里user 表有 5 行孙七年龄为 NULL所以这三列的结果分别是 5、4、4。实际业务里统计“有效用户数量”时到底用哪个很多人分不清一旦 NULL 占比高数字差异就很明显。条件聚合是报表里的常客。比如想在一行记录里同时拿到总订单数和已支付金额SELECT COUNT(*) AS total_orders, SUM(CASE WHEN status 1 THEN amount ELSE 0 END) AS paid_amount FROM orders;还有更简洁的写法SUM(status 1)MySQL 支持布尔表达式返回 0 或 1所以这样能直接算出 status1 的行数。不过我个人习惯用 CASE WHEN因为可读性更强换到其他数据库也能看懂思路不容易跑偏。AVG 和 SUM 都有个共同点遇到 NULL 会跳过。如果想让 NULL 按 0 参与计算用 IFNULL 处理SELECT user_id, AVG(IFNULL(amount, 0)) FROM orders GROUP BY user_id;这个场景更多出现在计算客单价、平均分时。统计口径不一样算出来的结果可能天差地别一定要先跟业务确认清楚“空值算不算”再写 SQL。3.2 ONLY_FULL_GROUP_BY模式MySQL 5.7以后的大变化从 MySQL 5.7 开始默认开启ONLY_FULL_GROUP_BY模式。这意味着 SELECT 后面出现的非聚合列必须出现在 GROUP BY 里。比如SELECT u.username, COUNT(*) FROM user u JOIN orders o ON u.id o.user_id GROUP BY o.user_id;这条 SQL 在旧版本可能能跑在 5.7 会直接报错报错信息会指出 username 不在 GROUP BY 里。为什么 MySQL 要管这么严因为 username 和 user_id 可能是多对一关系如果不强制分组数据库无法保证你取到的 username 是哪一行。设计这个模式的目的就是为了避免“不确定的结果”被当成正确结果。碰到这种报错规范做法是补全 GROUP BYSELECT u.id, u.username, COUNT(*) FROM user u JOIN orders o ON u.id o.user_id GROUP BY u.id, u.username;MySQL 也提供了ANY_VALUE()函数来绕过这个限制但这样会让结果不可复现生产环境我基本不用。宁可多写几个字段也别把不确定性留给线上报表。3.3 WHERE和HAVING别放错位置语义上WHERE 在分组前过滤行HAVING 在分组后过滤组。用“每个用户下单次数大于等于 2”这个需求来对比SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id HAVING cnt 2;有人会写成SELECT user_id, COUNT(*) AS cnt FROM orders WHERE COUNT(*) 2 GROUP BY user_id;这是错的WHERE 不能包含聚合函数。还有一个容易被忽视的性能点能在 WHERE 里过滤掉无用的行就不要拖到 HAVING 里过滤。假设只要统计 2023 年内的订单SELECT user_id, COUNT(*) AS cnt FROM orders WHERE order_time 2023-01-01 AND order_time 2024-01-01 GROUP BY user_id HAVING cnt 2;先 WHERE 后 GROUP参与分组的数据量明显变少。要是放到 HAVING虽然结果也能对但可能把整张订单表所有历史数据都分组算一遍再逐组过滤资源和速度完全不是一个量级。4. 多表关联JOIN连接条件比你想的更讲究4.1 该用INNER还是LEFT先想清楚业务语义INNER JOIN 返回两表匹配上的数据。想查“有订单的用户及订单信息”SELECT u.username, o.product_name FROM user u INNER JOIN orders o ON u.id o.user_id;结果里不会出现没下过单的孙七因为他在 orders 里没有匹配。LEFT JOIN 以左表为准右表没匹配上的会显示 NULL。想查“所有用户以及各自的订单没订单的也得列出来”SELECT u.username, o.product_name FROM user u LEFT JOIN orders o ON u.id o.user_id;这里孙七会带出订单 NULL正好对应“没下过单的用户”。而 orders 表里那条 user_id6 的孤儿订单在这个方向的查询里不会出现因为查询是以 user 表为驱动方。想看孤儿订单直接在 orders 表里查就行。RIGHT JOIN 我建议尽量少用。它和 LEFT JOIN 本质是对称的通过交换两张表的顺序基本都能改写成 LEFT JOIN。RIGHT JOIN 可读性差容易让看 SQL 的人误解驱动方向所以碰到它先停下来想想是不是表顺序写反了。4.2 连接条件和过滤条件的位置LEFT JOIN的大坑这个坑连工作几年的开发也容易栽。需求是“查每个用户已支付的订单”-- 错误示范把右表过滤条件放在WHERE SELECT u.username, o.product_name FROM user u LEFT JOIN orders o ON u.id o.user_id WHERE o.status 1;逻辑看着没毛病实际执行时WHERE 会把 o.status 为 NULL 的行全部过滤掉结果和 INNER JOIN 差不多孙七这种没有订单的用户直接被丢弃了。因为对 NULL 判断来说o.status 1永远不成立。正确写法是放在 ON 后面SELECT u.username, o.product_name FROM user u LEFT JOIN orders o ON u.id o.user_id AND o.status 1;这样 LEFT JOIN 的语义被保留所有用户都还在只是订单只显示已支付的。总结一句话对 LEFT JOIN 来说右表的过滤条件默认放 ON左表的过滤条件放 WHERE。把这个规则记牢能少改很多数据对不上的 bug。提示LEFT JOIN 的右表条件放 WHERE 还是放 ON是面试里很常考、工作中也很常踩的点。判断标准很简单查出来的用户数有没有变少变少了就是过滤太早丢掉了一批 NULL 行。4.3 JOIN字段的字符集和类型不一致索引直接作废这是线上真实踩过的坑。两张表一张是老库字符集 utf8一张是新库字符集 utf8mb4。按关联字段做连接SELECT u.username, o.user_id FROM user u JOIN orders o ON u.id o.user_id;如果两表关联字段的字符集不一致MySQL 做 JOIN 时会把一边转成另一边的字符集一旦发生隐式转换索引十有八九用不上。排查方法很直接EXPLAIN 里看 key 列是否为 NULL、type 是否变成 ALL。解决方法就是统一字符集能统一到 utf8mb4 最好。这个细节应该在架构设计阶段就定下来不然后面改表结构代价很大。关联字段类型不一致也有同样问题。比如一张表主键是 INT另一张表关联字段是 BIGINT数值范围看起来差不多但优化器做比较时可能触发类型转换照样影响索引。所以设计表结构时关联字段必须严格对齐类型这比命名规范重要得多。再说一个新手必踩的笛卡尔积如果写了FROM user u, orders o却没带 ON 条件结果会返回两表行数相乘。5 行用户加 8 行订单就是 40 行如果两个表都是千万级一次全量拼接就把数据库拖垮。所以多表查询第一步永远先检查 JOIN 条件有没有写全。5. 子查询的取舍IN、EXISTS还是JOIN5.1 四种子查询形态够用就行子查询按返回结果可以分成四类标量子查询返回一行一列列子查询返回一列多行行子查询返回一行多列表子查询返回多行多列标量子查询最典型的例子是在订单列表里顺便带出用户名SELECT o.id, o.product_name, (SELECT u.username FROM user u WHERE u.id o.user_id) AS username FROM orders o;这个方式直观但要注意大表上尽量不要用因为每一行都要执行一次子查询很容易变成隐性的循环查询。列子查询最常见。比如查所有下过单的用户SELECT username FROM user WHERE id IN (SELECT user_id FROM orders WHERE status 1);行子查询用得比较少但偶尔有奇效。比如查“城市名和最大年龄对应的那条用户资料”SELECT * FROM user WHERE (city, age) (SELECT city, MAX(age) FROM user);表子查询就是派生表比如统计每个用户的订单总额再反查用户SELECT u.username, t.total_amount FROM user u JOIN ( SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id ) t ON u.id t.user_id;这种嵌套方案在写复杂统计时很常见但它本质上是先造一张临时结果集再和原表关联。临时表会不会被物化、关联时能不能用上索引都得靠 EXPLAIN 来确认不能光靠直觉。5.2 IN vs EXISTS别背“小表驱动大表”的死结论网上经常看到“子查询结果集小用 IN外层表小用 EXISTS”的说法。这个说法有一定的合理性但 MySQL 5.7 之后的优化器已经相当聪明很多时候它会把 IN 改写成 EXISTS或者反过来。真实执行计划未必和你脑子里的“标准答案”一样。比如下面两条 SQL 逻辑等价SELECT u.* FROM user u WHERE u.id IN (SELECT user_id FROM orders WHERE status 1); SELECT u.* FROM user u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id u.id AND o.status 1 );存量数据多的时候SELECT 1确实是普遍做法因为 EXISTS 只关心“有没有”不关心具体值1 只是个占位符。如果你习惯在子查询里写SELECT *语义上没有错但会让人多写没必要的输出EXPLAIN 时也可能看到多余的成本。所以哪怕用 IN子查询也别写SELECT *。真正决定性能的是关联字段有没有索引、驱动表和被驱动表的数据量、最终结果集大小。在 orders.user_id 有索引的前提下这两种写法的性能差异并没有网上说的那么夸张。我的建议是优先写可读性更强的 IN 或 JOIN然后用 EXPLAIN 验证不要凭感觉去优化。5.3 能用JOIN尽量用JOIN子查询要防“多扫一遍”有一个情况要特别注意MySQL 5.7 之前派生表往往会被物化成临时表子查询很慢5.7 之后优化器默认会把派生表合并进外层查询这个行为由derived_merge控制。但如果是带有 LIMIT、聚合、DISTINCT 等稍复杂的派生表物化还是可能发生。所以我的原则是子查询能解决的80% 都能用 JOIN 解决得更直观。比如前文的订单总额统计用 JOIN 改写SELECT u.username, IFNULL(SUM(o.amount), 0) AS total_amount FROM user u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.id, u.username;一条 SQL 搞定还能保留没有订单的用户逻辑也清晰。子查询并不是不能写而是要理解它背后的“临时结果集”代价。写之前先问一句有没有 JOIN 方案可以替代如果子查询能让 SQL 更简洁同时数据量和索引都验证过没问题那就放心用。技术选型永远服务业务场景不是为了一段 SQL 看起来厉害。6. 用EXPLAIN判断一条查询到底慢在哪6.1 EXPLAIN里最重要的几列慢查询优化第一步永远是EXPLAIN。它输出的列不少但先看这么几个就够type访问类型从优到劣大致是 system const eq_ref ref range index ALL。看到 ALL基本就是全表扫描这是首要怀疑对象。key实际用到的索引NULL 表示没用到索引。rows预估扫描行数越小越好。Extra出现 Using filesort、Using temporary、Using index分别代表文件排序、临时表、覆盖索引。Using index 是好事其他两个通常值得检查。type 等级可以用一个表格概括type含义典型场景const最多匹配一行主键或唯一索引等值查询eq_ref每次关联只匹配一行JOIN 时用主键关联ref非唯一索引等值查询user_id3range索引上的范围扫描BETWEEN、IN、、index扫描整棵索引树覆盖索引中的全量扫描ALL全表扫描没有可用索引6.2 一个完整的慢查询排查案例就拿 orders 表查 user_id3 的下单记录来演示EXPLAIN SELECT * FROM orders WHERE user_id 3;在没有索引前type 大概率是 ALLrows 是 8Extra 为空。8 行数据看不出问题但如果这张表有 800 万行全表扫描就意味着要把 800 万行全部读出来再逐行过滤代价非常大。建索引ALTER TABLE orders ADD INDEX idx_user_id (user_id);再 EXPLAIN 同一条 SQLtype 会变成 refrows 变成 2key 变成 idx_user_id。这就是从全表扫描到索引查询的直观差别。很多初学者看到 EXPLAIN 输出一头雾水其实只要学会看 type 和 rows 两个列80% 的问题都能定位。再复杂一点看排序EXPLAIN SELECT id, user_id, order_time FROM orders WHERE user_id 1 ORDER BY order_time DESC;如果只建了 idx_user_idExtra 里会出现 Using filesort。这时候建一个 (user_id, order_time) 的复合索引排序就能直接复用索引filesort 消失。索引字段顺序至关重要很多人觉得“建了索引还是慢”大概率就是这里没搞对。还有一个覆盖索引的例子EXPLAIN SELECT user_id, order_time FROM orders WHERE user_id 1;当查询的列都在索引树里时Extra 会出现 Using index意味着不需要回表取完整行。这也是为什么我反复建议大家不要轻易写SELECT *明明可以覆盖索引搞定一写星号就得回表读整行性能白白损失。6.3 索引失效的几类常见场景归纳一下遇到以下情况时即使有索引也可能失效对索引列使用函数或表达式比如DATE(order_time) 2023-03-15前置模糊查询比如LIKE %手机隐式类型转换比如phone 13800000001OR 连接一个非索引列比如WHERE user_id 1 OR amount 0关联字段字符集不一致JOIN 时发生隐式转换优化器判断走全表扫描比走索引还便宜比如数据量很小或区分度很低遇到慢查询按这个清单逐条排查命中哪条改哪条。改完再用 EXPLAIN 验证直到 type 不再是 ALLExtra 里不再出现 filesort 和 temporary。这套流程熟练之后基本能解决日常开发里九成以上的查询性能问题。7. 翻页深了会慢查询快了也可能“栽”在客户端——收尾几个实战提醒7.1 深分页LIMIT 100000, 20 有多慢列表查询最常见的问题是深翻页。比如后台订单列表用户翻到第 5000 页SELECT * FROM orders ORDER BY id LIMIT 100000, 20;这会让 MySQL 先扫描 10 万行再丢弃前 10 万行只返回最后 20 行。数据量越大页数越深慢得越离谱。这种慢不是你多加个索引就能解决的因为偏移量本身就是很大的扫描量。优化思路之一用上一页的最大 id 代替偏移量SELECT * FROM orders WHERE id 100000 ORDER BY id LIMIT 20;这适合 id 单调递增的场景比如按时间倒序翻页。缺点是不能随意跳页只适合“上一页/下一页”的浏览模式。优化思路之二延迟关联SELECT o.* FROM orders o JOIN ( SELECT id FROM orders ORDER BY id LIMIT 100000, 20 ) t ON o.id t.id;先用覆盖索引快速定位需要的 id再回表取完整数据。相比直接SELECT *这种方式避免了扫描大量行时把全部数据拉进内存。这个技巧对订单量很大的后台列表特别有效。7.2 查询结果集太大数据库不慢客户端先崩还有一种慢是“假慢”数据库其实很快但一次查出百万行程序端全量 load 进内存内存不够直接 OOM或者把网络带宽打满整个服务跟着抖动。处理办法是分批拉取。比如按 id 分片每批查 5000 条或者用游标方式流式读取。JDBC 里可以配置useCursorFetchtrue和合适的fetchSize让客户端边读边消费而不是一次性把所有行装进内存。这套方案在同步数据、导出报表时非常常用。顺带提一句SQL_CALC_FOUND_ROWS这种“先算总行数再分页”的写法在 MySQL 8.0.17 之后已经被官方标记为废弃不建议再用。统计总条数用单独的SELECT COUNT(*)就好别让它拖慢主查询。7.3 查询前与查询后的一份自查清单写 SQL 之前先想清楚结果长什么样再动手。我自己通常会过一遍业务口径是什么计算“有订单的用户”还是“所有用户及订单”JOIN 类型完全不同。要查哪些列能写明确列名就不要SELECT *。过滤条件有没有函数包裹列、隐式转换、NULL 判断错误WHERE 和 HAVING 放对位置没有能提前过滤就不要拖到分组后。JOIN 的关联字段类型和字符集是否一致左连接时右表过滤条件是否放到了 ON写完之后跑一次 EXPLAIN确认 type 不是 ALLExtra 里没有 filesort 和 temporary。用一小段数据验证结果符不符合预期再放到全量数据上跑。这份清单看起来琐碎实际是日积月累的教训。线上慢查询百分之八十都能靠这几条解决剩下的才需要上升到执行计划细节、锁竞争、硬件配置等更底层的东西。最后说点个人体会。刚写 SQL 那几年我把“能查出数据”当目标后来发现查询写得对不对、快不快直接决定一个功能上线后要不要返工。基础查询操作里的过滤、排序、分组、关联、执行计划每一类都能在真实项目里找到对应的翻车事故。与其等线上出问题再被逼着学不如先把这些细节练扎实。这篇基础实战篇先到这里后面我会继续整理 MySQL 的其他操作大家把查询这块吃透后面的内容会顺很多。
返回列表