导读:这份"常用SQL汇总",不是给你背的
从 MySQL 实战系列第一篇写到现在,我一直在强调一个观念:工作中真正值钱的,不是你会写多么复杂的 SQL,而是你在面对具体业务问题时,能快速想起"这个场景该用哪条 SQL、怎么写才对"。第七篇这篇"常用 SQL 汇总",说白了我就是想把日常开发、排查问题、面试突击时用到的高频 SQL 做一次系统梳理。
这篇内容的定位很明确:不教语法,只讲实战中怎么用、怎么写、怎么避坑。你能解决什么问题?比如数据量大了之后分页变慢怎么改写、多表关联更新怎么处理、如何快速定位某个表里哪些字段有空值、如何判断一条查询到底走没走索引。适合谁看?适合已经会增删改查、但还没形成系统 SQL 知识框架的开发者,也适合准备面试、想快速把常用 SQL 过一遍的人。
网上类似"SQL 速查"的文章非常多,但大部分只是把官方文档的语法抄了一遍。这套汇总,我会把每条 SQL 背后的适用场景、常见误区、性能影响一起讲清楚。你读完拿过去就能用,用的时候心里有底。
1. 这份汇总的整体思路:别背语法,按业务场景组织知识
先交代一下我整理这份 SQL 汇总的方法论。MySQL 的 SQL 语句说多不多、说少不少,如果按"增删改查"四个字去分类,写出来就是流水账,看完记不住、用的时候也想不起来。我的建议是:直接从真实业务场景出发,反推这条 SQL 为什么存在。
举个例子。INSERT ... ON DUPLICATE KEY UPDATE 这条语法,很多人在网上看到过,但不知道什么时候用。如果你做过订单同步、用户积分累加这类业务,你就会发现"有则更新、无则插入"是一个非常高频的需求。没有这条语法的时候,你得先 SELECT 查一遍,再根据结果决定 UPDATE 还是 INSERT,不仅多一次交互,还存在并发覆盖的风险。有了它,一切都简化了。
这就是我把这份汇总分成六大板块的原因:
- 数据变更类 SQL:不只是 INSERT/UPDATE/DELETE 的裸语法,而是"插入时冲突怎么办""更新时怎么连带其他表""删除时如何避免误删全表"这些实战场景;
- 高频查询场景 SQL:去重、分页、条件过滤、排序,每个场景都有"看起来对但其实有问题"的写法;
- 聚合统计与分组:从 GROUP BY 到 HAVING,再到统计函数的使用边界;
- 表结构与索引管理:这是很多开发者的弱项,平时写 SQL 多、看表结构少,但出了问题还得回来看这里;
- 元数据查询:information_schema 相关的几条 SQL,关键时刻能救命;
- 性能优化辅助 SQL:EXPLAIN、慢查询日志、索引分析,是排查慢 SQL 的起点。
按这个结构去整理,你脑子里会形成一张"场景 → SQL → 注意点"的映射表。等哪天产品过来说"这个列表要加个按分类筛选",你立马能想到 WHERE 后面怎么拼接、要不要加索引,而不是拿着语法手册现查。
2. 数据变更类 SQL 实战要点:INSERT、UPDATE、DELETE 的进阶写法与实际坑点
2.1 INSERT 的三种进阶姿势:批量插入、忽略冲突、冲突更新
INSERT 是每个开发者最早学会的 SQL,但用得熟练与否差距很大。我这里重点说三个场景。
批量插入。 一次插入多条记录时,标准写法是 INSERT INTO table (col1, col2) VALUES (v1, v2), (v3, v4), ...。这里最常见的坑是单条 SQL 不要太大,建议控制在 500~1000 条一批。我在实际项目中遇到过有人一条 INSERT 塞了两万条数据,直接把 max_allowed_packet 打爆,报错 Packet for query is too large。处理办法很简单:代码里做分批,或者调大 max_allowed_packet 参数。
INSERT IGNORE。 当插入的数据可能会撞唯一索引时,INSERT IGNORE 会静默跳过冲突行,不报错。这在我做数据清洗、幂等插入时经常用到。要注意的是,它不只忽略主键冲突,任何唯一索引冲突都会忽略,所以用之前一定要确认好唯一键的设定是否符合预期。
INSERT ... ON DUPLICATE KEY UPDATE 是 INSERT IGNORE 的进阶版。它遇到冲突时不跳过,而是执行后面的 UPDATE 逻辑。比如用户表的积分字段:
sql复制INSERT INTO user_points (user_id, points)
VALUES (1001, 10)
ON DUPLICATE KEY UPDATE points = points + 10;
这条 SQL 的语义是:如果 user_id=1001 的记录不存在,就插入一条 points=10 的记录;如果存在,就在原值上加 10。我做过一次用户签到功能,每天签到加积分,用的就是这张表这种写法。但这里必须提醒一句:UPDATE 后面的赋值表达式里,建议用 VALUES() 函数引用本次插入的值,避免手写出错。MySQL 8.0.20 之后 VALUES() 被标记为 deprecated,新版本推荐用别名语法:
sql复制INSERT INTO user_points (user_id, points)
VALUES (1001, 10) AS new
ON DUPLICATE KEY UPDATE points = user_points.points + new.points;
这个写法看起来啰嗦一点,但它把"本次要插入的值"绑定到了别名 new 上,语义更清晰,也兼容未来版本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2.2 UPDATE 的两大易错场景:多表关联更新与误更新全表
UPDATE 最常见的坑不是语法不会,而是条件写漏导致全表被更新。我在测试环境就干过这种事:本来想更新某用户的状态,结果 WHERE 条件拼参数时没拼上,一整张表的状态都被改掉了。所以现在我的习惯是:
- 执行 UPDATE 之前先写等价的
SELECT COUNT(*)确认影响行数; - 生产环境的 UPDATE 尽量带上
LIMIT(不过 MySQL 对多表 UPDATE 的 LIMIT 支持有限,单表没问题); - 敏感操作前先开启事务,
UPDATE之后SELECT验证,再COMMIT。
多表关联更新是另一个高频场景。比如订单表要冗余客户等级,客户表的等级变了,订单表也要跟着改。MySQL 的语法是:
sql复制UPDATE orders o
JOIN customers c ON o.customer_id = c.id
SET o.customer_level = c.level
WHERE c.id = 1001;
这条 SQL 结合了 JOIN 和 UPDATE,写起来不复杂,但要注意:如果 JOIN 出来的结果存在一对多关系,被更新的表的某一行可能被更新多次,最后的值取决于 JOIN 顺序。我在商品分类和商品表做关联更新时就遇到过,同一个商品挂在多个分类下,更新后商品表的分类 ID 被后面的记录覆盖了。所以做这种操作之前,最好先单独跑一下 JOIN 查询,确认关联结果没有重复行。
2.3 DELETE 的安全删法:连表删除与保留最近 N 条
DELETE 的实战场景通常分成两类:清空历史数据和删除特定条件下的记录。清空整表推荐用 TRUNCATE TABLE,它不写逐行删除日志、速度极快,但无法回滚。这个差异必须记清楚:TRUNCATE 相当于直接把表重置,自增 ID 也会从头开始;DELETE FROM 删完后,自增 ID 不会重置。
连表删除的语法和 UPDATE 类似:
sql复制DELETE o
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.status = 'blocked';
这条 SQL 会把所有状态为 blocked 的客户的订单全部删掉。写这个的时候,我强烈建议先跑一次等价的 SELECT 确认结果集。之前有个项目就是因为没跑 SELECT 直接执行,把刚归档的数据删了,还得从备份恢复。
保留最近 N 条记录是删除场景里比较有趣的一个。比如日志表只保留每个用户最近 100 条操作记录,可以用 MySQL 8.0 的窗口函数:
sql复制DELETE FROM operation_log
WHERE (user_id, id) IN (
SELECT user_id, id FROM (
SELECT id, user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) AS rn
FROM operation_log
) t
WHERE t.rn > 100
);
MySQL 不允许在 DELETE 的子查询里直接引用目标表,所以中间套了一层子查询。窗口函数算出行号后,把超过 100 的行删掉,逻辑非常清晰。如果 MySQL 版本小于 8.0,就只能用 JOIN + 临时表的方式,稍显繁琐但不难。
3. 高频查询场景:SELECT 里那些"看起来对但其实不对"的写法和排查思路
3.1 去重:DISTINCT 的边界与 GROUP BY 的替代作用
SELECT DISTINCT 是最常见的去重方式,但它有个特点:它去重的是所有查询列的组合。如果你只关心某一列的不重复值,可以单独查这一列;但如果你想要"每个用户最新的一条记录"这种需求,DISTINCT 就无能为力了。
举一个真实案例:有一张用户登录日志表,我想知道每个用户最近一次登录时间。很多新人会写:
sql复制SELECT user_id, MAX(login_time)
FROM login_log
GROUP BY user_id;
这是对的。但如果你还想顺带查出登录 IP,直接加 login_ip 到 SELECT 里,在 MySQL 5.7 以及更早版本中不会报错但结果不可控(它拿到的 IP 可能不是最新那天的那条),在 MySQL 8.0 中则会直接报错。这就是 ONLY_FULL_GROUP_BY 模式在发挥作用。我建议:不要跟这个模式对着干,老老实实按聚合语义改写查询,否则线上数据出错很难查。
还有一个去重细节值得注意:DISTINCT 后面如果跟多个列,比如 SELECT DISTINCT user_id, login_ip FROM t,它的语义是 user_id 和 login_ip 组合起来去重,而不是单独对 user_id 去重。这个误区我在评审同事代码时看到过好几次,写的时候脑子里要清楚。
3.2 分页查询与深分页优化:LIMIT 的性能瓶颈在哪
分页查询大概是所有后端开发写得最多的 SQL。简单写法是:
sql复制SELECT * FROM orders
WHERE status = 'paid'
ORDER BY id DESC
LIMIT 20 OFFSET 0;
随着页码增大,OFFSET 越来越大,MySQL 会扫描并丢弃前 offset 行,效率急剧下降。比如 LIMIT 1000000, 20,它得先扫 1000020 行再丢掉前 1000000 行,代价很大。
我的优化方案通常有两种。
第一种:延迟关联(deferred join)。先用覆盖索引查出目标主键,再回表取完整数据:
sql复制SELECT o.*
FROM orders o
JOIN (
SELECT id
FROM orders
WHERE status = 'paid'
ORDER BY id DESC
LIMIT 20 OFFSET 1000000
) t ON o.id = t.id
ORDER BY o.id DESC;
这样内层查询只走索引,不碰数据行,外层再通过主键回表,性能提升非常明显。
第二种:基于游标的分页(keyset pagination)。记住上一页最后一条记录的 id,下一页用条件往下取:
sql复制SELECT * FROM orders
WHERE status = 'paid' AND id < 10012345
ORDER BY id DESC
LIMIT 20;
这种方案没有 OFFSET 的概念,翻页再深性能也稳定,非常适合 App 列表这种"永远只往前翻"的场景。缺点是无法跳页,所以在实际项目中,看产品需求选择合适的方案。
3.3 条件过滤:BETWEEN 和比较运算符的隐性坑
BETWEEN ... AND ... 看似直观,但因为是闭区间查询,容易漏掉边界考虑。比如查 3 月份的订单:
sql复制SELECT * FROM orders
WHERE create_time BETWEEN '2025-03-01' AND '2025-03-31';
问题来了:如果 create_time 是 datetime 类型,那么 2025-03-31 当天的所有订单都不会被查出来,因为 2025-03-31 12:00:00 大于 2025-03-31 00:00:00。正确做法是把右边界改成下个月的起始时刻:
sql复制SELECT * FROM orders
WHERE create_time >= '2025-03-01'
AND create_time < '2025-04-01';
这个写法在任何日期时间类型上都安全,而且更利于优化器走索引。这个坑我在按月统计报表时踩过不止一次,后来干脆统一用"大于等于左边界、小于右边界"的模式。
另一个容易出问题的是 NULL。WHERE col != 'x' 不会查出 col 为 NULL 的行,因为 NULL 参与比较的结果是 UNKNOWN,整个条件不成立。如果你确实要把 NULL 也算上,需要显式加 OR col IS NULL。这个细节在数据清洗时经常遇到,特别是从老系统导过来的数据,空值不规范,查询结果就和你预期不一致。
4. 聚合统计与分组:GROUP BY 的正确姿势和用错之后的典型报错
4.1 聚合函数的使用边界:COUNT、SUM、AVG 去重与 NULL 处理
聚合函数是报表统计的核心,但它们的行为有一些不仔细看文档发现不了的细节。
先说 COUNT(*) 和 COUNT(col) 的区别。COUNT(*) 统计行数,包括所有行;COUNT(col) 统计的是 col 列中非 NULL 的值的个数。这个差异在实际中容易导致上报的数字对不上。比如查订单数量时,如果某个订单的 deleted_at 是 NULL,COUNT(deleted_at) 就不会把它算进去,但它在业务上明明是一笔有效订单。所以统计有效订单数量时,我从来不用 COUNT(deleted_at),而是 COUNT(*) 配合 WHERE 条件。
SUM(col) 如果所有值都是 NULL,结果不是 0 而是 NULL。用 Java 的 Long 接收时,拆箱会直接 NPE。我习惯用 IFNULL(SUM(col), 0) 兜底。
AVG 也有类似的 NULL 处理逻辑,它只对非 NULL 值求平均。如果你想把 NULL 当作 0 参与平均,需要先 IFNULL(col, 0) 再求 AVG。
去重统计是另一个高频需求。比如统计某个日期内活跃用户数,用户可能一天登录多次,直接 COUNT(user_id) 会重复计算。正确写法是:
sql复制SELECT DATE(login_time), COUNT(DISTINCT user_id)
FROM login_log
GROUP BY DATE(login_time);
COUNT(DISTINCT col) 在去重的同时计数,语法简洁。数据量大的时候,这个性能不算特别好,但配合覆盖索引一般还能接受。
4.2 HAVING 与 WHERE 的分工:为什么 WHERE 不能替代 HAVING
WHERE 在分组前过滤行,HAVING 在分组后过滤组。这个知识点面试经常问,但实际开发中混用的情况也不少。
比如查"订单金额大于 1000 的客户":
sql复制SELECT customer_id, SUM(amount) AS total_amount
FROM orders
GROUP BY customer_id
HAVING total_amount > 1000;
如果你试图用 WHERE total_amount > 1000,MySQL 会直接报错:Unknown column 'total_amount' in 'where clause'。原因很简单:WHERE 执行的时候,分组和聚合还没发生,total_amount 这个别名还不存在。
但注意,能用 WHERE 过滤的条件尽量用 WHERE,比如"只统计已支付的订单":
sql复制SELECT customer_id, SUM(amount)
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
如果把 status='paid' 放进 HAVING,执行计划会先把所有订单分组、聚合,再丢掉未支付的分组,性能差很多。性能差异的实际感受是:数据量上了百万行之后,一个走了索引的 WHERE 过滤能毫秒级返回,而 HAVING 过滤可能要扫全表、好几百毫秒甚至更久。
4.3 分组排序与取每组前几条:窗口函数的实战用法
“取分组内排名第一/前 N 条”是个经典需求,MySQL 8.0 之前写起来非常难受,用变量或自连接绕来绕去。8.0 引入了窗口函数,直接清爽很多。
比如取每个分类下销量最高的商品:
sql复制SELECT category_id, product_id, sales
FROM (
SELECT category_id, product_id, sales,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY sales DESC) AS rn
FROM product_sales
) t
WHERE t.rn = 1;
ROW_NUMBER() 按分类分组、按销量降序生成序号,外层再过滤 rn=1。换成 RANK() 的话,销量并列时会保留并列名次,比如两个商品并列第一,都会返回。用哪个取决于业务到底要"唯一值"还是"并列都算"。
窗口函数还有一个常见用途就是前文提到的"保留每组最近 N 条",本质上是同一个思路:先开窗编号,再按条件删或查。窗口函数不能直接在 WHERE 里用,必须先套子查询,这个结构要记住。
4.4 字符串与日期处理:DATE_FORMAT、字符串拼接、类型转换的注意点
报表场景下,日期格式化是最高频的操作。比如按小时统计订单量:
sql复制SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_start,
COUNT(*)
FROM orders
GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00');
这里要说一个性能问题:对 create_time 做 DATE_FORMAT 后,索引基本就失效了,因为优化器无法用函数处理后的值和原始索引匹配。所以不要在索引列上使用函数。如果这个查询经常跑,建议加一个冗余字段,比如 hour_start,写入时就计算好,再在该字段上建索引。这是典型的用空间换时间。
字符串拼接推荐 CONCAT。如果某个字段是 NULL,CONCAT 的结果会变成 NULL,这是一个容易踩的坑。比如:
sql复制SELECT CONCAT(first_name, ' ', last_name) FROM users;
只要 last_name 是 NULL,整列结果就是 NULL。需要先用 IFNULL 处理:
sql复制SELECT CONCAT(IFNULL(first_name, ''), ' ', IFNULL(last_name, ''))
FROM users;
类型转换用 CAST 或 CONVERT。MySQL 在比较不同类型时会做隐式转换,比如字符串列和数字比较,会尝试把字符串转成数字。如果字符串列有索引,隐式转换会导致索引失效。这也是"字段类型设计要规范"的重要原因。
5. 表结构与索引查询:用最少的时间摸清一张表的家底
5.1 DESC 和 SHOW CREATE TABLE:看结构就用这两条
接手一张新表时,第一件事永远是看表结构。DESC table_name 可以快速看到字段名、类型、是否允许 NULL、默认值、主键等信息。它的输出比较简洁,适合快速浏览。
如果需要看更完整的信息,包括索引定义、字符集、分区、表注释,用 SHOW CREATE TABLE table_name,它会输出完整的建表语句。每次排查问题时,我都习惯把这两条都跑一遍:DESC 看字段,SHOW CREATE TABLE 看索引和表选项。
sql复制SHOW CREATE TABLE orders\G
在命令行和很多客户端里,\G 可以把输出按纵向展示。字段多的表,用这种方法读起来比横向表格舒服得多。
5.2 information_schema 三件套:按条件检索表、字段、索引
当你需要查"哪些表里有字段叫 user_id"时,DESC 就帮不上忙了。我经常用下面三条 SQL 快速定位,全部来自 information_schema 库。
按字段名找表:
sql复制SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE column_name = 'user_id'
AND table_schema = 'your_db';
查看某张表的所有索引:
sql复制SELECT table_name, index_name, column_name, seq_in_index
FROM information_schema.statistics
WHERE table_schema = 'your_db' AND table_name = 'orders';
查看某张表的大小(数据 + 索引):
sql复制SELECT table_name,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb
FROM information_schema.tables
WHERE table_schema = 'your_db'
ORDER BY total_mb DESC;
这三个查询在定位"数据去哪了""为什么某条查询这么慢""大表的索引情况"时特别有用。特别是最后一个,磁盘快满的时候,可以快速找到占用最大的表和索引,辅助决定是否要清理归档。
6. 慢 SQL 优化辅助:EXPLAIN 与常见性能陷阱排查思路
6.1 EXPLAIN 的核心输出怎么读:type、key、rows 一眼看出问题
写 SQL 如果只追求"结果对",那不算完。上线前用 EXPLAIN 看一眼执行计划,是保命的一步。
sql复制EXPLAIN SELECT o.* FROM orders o
WHERE o.customer_id = 1001
ORDER BY o.create_time DESC;
输出结果里,我最关注四列:
- type:访问类型。从好到差大致是 system > const > eq_ref > ref > range > index > ALL。看到 ALL 就说明全表扫描,SQL 有优化空间。
- key:实际使用的索引。如果为 NULL,说明没走索引。
- rows:预估扫描行数。这个数字越大,查询越慢。
- Extra:有很多值得注意的信息。比如出现
Using filesort意味着 ORDER BY 没法走索引,需要额外的排序操作;出现Using temporary意味着用了临时表,多半是 GROUP BY 或 DISTINCT 没有合适的索引支撑。
拿到 EXPLAIN 结果后,我的优化思路通常是:让 type 至少达到 range 或 ref,尽量避免 filesort 和 temporary。如果查询条件里的列没法用索引覆盖,就考虑加联合索引。
6.2 索引失效的常见原因:函数、隐式转换、最左前缀
索引失效是慢 SQL 的一大来源,排查时我会按这几个方向依次检查。
第一,索引列上使用函数。 我在前面提到过 DATE_FORMAT(create_time, ...),一旦套了函数,优化器基本放弃索引。改写为范围查询后,索引恢复。
第二,隐式类型转换。 举个例子:phone 列是 varchar 类型,查询条件是 WHERE phone = 13800138000,数字和字符串比较时,MySQL 会把字符串转成数字来比较,导致索引失效。正确做法是条件带上引号:WHERE phone = '13800138000'。
第三,违反最左前缀原则。 联合索引 (a, b, c),查询条件里如果没带 a,只带 b 和 c,索引就无法生效。这个规则挺多面试的人都背过,实际排查时看到联合索引用了却没完全用,也会出现"走了索引但效率不高"的中间状态。
第四,LIKE 模糊查询以通配符开头。 WHERE name LIKE '%张' 无法走索引,WHERE name LIKE '张%' 则可以。这是老生常谈,但依然有人在线上写出前一种。
第五,OR 连接非索引列。 WHERE a = 1 OR b = 2,如果 b 没有索引,整个查询可能退化成全表扫描。改成 UNION ALL 或为 b 添加索引是常见解法。
6.3 慢查询日志与 SHOW PROFILE:定位 SQL 到底慢在哪一步
当一条 SQL 明确很慢,但你又说不清 why 时,慢查询日志是第一手资料。在 MySQL 里可以动态开启:
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
这样持续超过 1 秒的 SQL 会被记录到日志文件。拿到具体 SQL 后,除了 EXPLAIN,还可以用 SHOW PROFILE 看更细的时间分布:
sql复制SET profiling = 1;
-- 再执行一次那条慢 SQL
SHOW PROFILES;
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;
这个方法能看到 SQL 执行过程中 Sending data、Sorting result、Creating sort index 等各个阶段耗时。Sending data 时间特别长时,多半是大量行从存储引擎返回给服务层,需要从减少扫描行数入手;Sorting result 时间特别长时,显然是排序没走索引。不要被"慢得没头绪"吓到,按这个链路拆解,一般四五步就能定位到根因。
7. 一套顺手可用的开发自检清单
最后这部分,我把写 SQL 时容易忽略的点整理成一份清单,结合我自己的经验说说每一条背后的道理。你可以直接复制到项目规范里,或者贴在工位上提醒自己。
| 检查项 | 说明 | 经验 |
|---|---|---|
| UPDATE/DELETE 前是否确认了 WHERE 条件 | 防止误更新全表 | 先跑 SELECT COUNT(*) 估算影响行数 |
| INSERT 批量是否分批 | 防止单条 SQL 过大 | 500-1000 条/批比较合适 |
| 索引列是否被函数包裹 | 函数会导致索引失效 | 改写为范围条件或增加冗余字段 |
| 日期范围是否用闭区间 | BETWEEN 会漏掉当天数据 | 改用 >= 左边界 AND < 右边界 |
| GROUP BY 是否遵循 ONLY_FULL_GROUP_BY | 避免结果不可控 | 所有非聚合列都要出现在 GROUP BY 里 |
| JOIN 结果是否有一对多风险 | 防止 UPDATE/DELETE 后数据被覆盖 | 执行前先跑 SELECT 确认 |
| 大分页是否用了 OFFSET | 深分页性能差 | 用延迟关联或 keyset 分页 |
| EXPLAIN 是否检查了 type 和 key | 确认执行计划 | 至少 range,避免 ALL 和 filesort |
| 字符串列是否被隐式转换 | 导致索引失效 | 查询条件类型与列类型保持一致 |
| 聚合结果是否可能为 NULL | 防止程序 NPE | 用 IFNULL 包裹 |
这张表不需要背,等你踩过几次坑自然就记住了。我之所以把它们列出来,是因为这些都是真实线上环境里出现过的问题——不是理论推演,是同事们在凌晨三点被叫起来处理的那种问题。
如果你现在刚把 MySQL 基础过完,我建议你把这份清单当作实操训练题:找一张自己项目里的表,按第七节的 EXPLAY 方法跑几条查询,再用第五节的信息 schema SQL 看一下索引覆盖情况,最后对照第六节检查一遍索引失效的常见场景。不用一次全做,但每做一遍,你对"这条 SQL 为什么这么写"的理解就会深一层。
