先抛一个问题:你在终端里敲下一条SQL查询——SELECT * FROM user WHERE age > 18;,MySQL到底是怎么把你的意思变成磁盘上真实的数据扫描动作的?这条链路里牵涉到连接器、解析器、预处理器、优化器、执行器、存储引擎,每一层的设计都卡得很死。我早期做后端的时候,写完查询就把锅甩给数据库,直到一次线上慢查询把我折腾到凌晨两点,才发现自己对MySQL“执行”的认知停留在黑盒阶段。
这篇内容不讨论理论,就围绕一条查询从进入到返回结果的完整穿越过程展开。你不需要有DBA基础,只要写过SQL,跟着走一遍就能建立比较完整的执行链路认知。顺便还会把EXPLAIN怎么看、慢查询日志怎么开、索引为什么失灵这些高频问题一起解决掉。
1. 一条SQL在MySQL中的完整路线图
我习惯把MySQL的执行链路想象成公司里走个审批流程。一条查询从客户端进门,到最终把数据拿给你,中间要经过前台登记、部门助理核对、主管定方向、办事员跑腿这四层。对应到MySQL里,就是连接管理、解析与预处理、优化器决策、执行器调存储引擎取数。任何一层出了问题,你看到的就不是“查不到”就是“查得慢”。
很多同学在排查慢SQL时,习惯一上来就EXPLAIN看索引有没有生效,这当然没错。但如果对完整链路没有概念,很容易被几个表象带偏。比如同一条语句在测试环境跑得飞快,线上却慢成乌龟,这未必是索引问题,可能是连接数满了在排队,也可能是碎片化严重导致扫描的页变多。所以第一步,先把全景图刻在脑子里。
1.1 先记住两层分工:Server层与存储引擎层
虽然MySQL组件很多,但逻辑上只分成两大块:Server层和存储引擎层。这个观念很重要,后面所有问题都能归到这两层里解决。
Server层包含连接器、查询缓存、解析器、优化器、执行器,还有内置函数、存储过程、触发器和视图这些跨引擎功能。它的特点是“不碰数据文件”,只负责分析指令、制定方案、调度执行。
存储引擎层才是真正碰数据的地方。InnoDB是默认引擎,负责管理数据页、索引、事务、锁和崩溃恢复。你可以把Server层理解成总指挥,负责出主意;存储引擎层是施工队,负责干粗活。这样分层最大的好处是:换引擎不需要改动Server层逻辑,就像换个施工队,甲方不会受影响。
顺带说一句,正因为InnoDB和Server层的职责边界清晰,才会出现一个经典面试题:count(*)为什么这么慢?答案其实和引擎层的数据存储方式强相关——InnoDB没有像MyISAM那样单独维护一个总行数计数器,它必须根据MVCC可见性逐行判断,这个后面细说。
1.2 一条SELECT语句经过的主要节点
严格来说,一条普通SELECT会经过以下关卡,顺序不要记错:
- 客户端发起连接,MySQL通过连接器完成TCP握手和身份认证。
- 连接建立后,判断是否有权限,再接收SQL文本。
- 在MySQL 8.0之前的版本,先走查询缓存;8.0里这一步已经被彻底移除。
- 解析器对SQL做词法分析和语法分析,生成语法树。
- 预处理器检查表、列是否存在,处理权限和视图展开。
- 优化器决定用哪个索引、以什么顺序关联表,生成执行计划。
- 执行器按计划调用存储引擎接口,逐行读取并判断条件。
- 存储引擎从内存缓冲池或磁盘读取数据页,返回记录。
- 执行器把满足条件的行组织成结果集,最后返回客户端。
注意第4和第5在很多资料里被合并成“分析器”,这不影响理解,但在排查错误时你会碰到两个不同的报错阶段。语法错误(如少写逗号)大多数在解析器就挂了;而“表不存在”“列不存在”是由预处理器跑出来的,报错信息几乎都是ERROR 1054或ERROR 1146。
1.3 大白话版本:像过机场安检一样
如果觉得上述名词太干,可以换一个生活场景。你去乘飞机:连接器是柜台值机,确认你是本人且买了票——账号密码验证就干这个。查询缓存是老式快速通道,如果你今天已经贴过标签且航班没变,可以直接过——但这通道后来被拆了。解析器是安检员,扫描你有没有带违禁品——SQL语法错误就会在这里被拦下。预处理器是登机口地勤,再次核对你的登机牌上的姓名和航班号有没有写错——表和列不存在就在这一步曝光。优化器是航空调度中心,决定走哪条航线能省油——MySQL决定走哪个索引。
最好玩的是执行器,它就是个跑腿小哥。接到优化器给的路线图,往InnoDB那边跑一趟,拿到第一行,回来看看满不满足条件,满足就收进袋子,不满足继续跑下一趟,直到把整条路走完。记住这个画面,后面的执行细节都能对号入座。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接与校验环节:SQL还没开始分析,就已被卡了两道坎
很多人总觉得连接环节没什么可讲的,实际上生产环境里大量“查询超时”根本不是SQL本身的问题,而是卡在连接层。
2.1 连接器怎么验证身份,验证的到底是什么
当你在客户端执行mysql -u root -p,MySQL不会立即去跑你的查询,而是先由连接器接手。它会做三件事:建立TCP连接、校验用户名和密码、读取该账号的权限数据。
注意第三点,读取权限数据不是把user表里的host、Select_priv、Insert_priv这些字段一次性塞进会话里就完事。MySQL会把这些acl信息缓存到线程对象中,之后执行每条SQL时,还会持续做权限判断。这里有一个反直觉的坑:如果DBA通过GRANT或REVOKE修改了某个账号的权限,已经存在的连接不会立即生效,新的权限往往在下次重新连接时才读取。线上遇到过开发同学问“为什么我改了只读账号的权限,应用侧还是能写”,十有八九就是这个原因。
连接管理还有个容易被忽略的核心点:max_connections。默认值一般151,生产环境开几千很正常,但不要盲目调大。每个连接都会占用线程栈、缓存和内存,连接数拉满后,查询都在排队等线程,表现为连接超时、接口响应变慢。排查技巧很简单,执行一个命令:
sql复制SHOW PROCESSLIST;
重点看Command为Sleep的线程数量。如果很多空闲连接长期占着不释放,就得从连接池参数和wait_timeout下手了。
2.2 为什么查询缓存被MySQL 8.0“一锅端”
老版本的MySQL有一层查询缓存:同样的SQL文本,算一次结果集后把结果缓存起来,下次一模一样的SQL直接返回,理论上能省下解析、优化、执行一整套流程。
那为什么还要移除?问题出在缓存失效太频繁。只要涉及的表发生任何写操作,哪怕只更新了一行数据,这张表上的全部查询缓存都会被清空。对写多读少的业务来说,缓存命中率低也就罢了,清理缓存本身也是一种锁开销,反而让系统更慢。很多年前我维护过一套5.6的系统,业务方反馈“早高峰特别卡”,排查后发现查询缓存命中率常年不到2%,但每次UPDATE都要扫一遍对应表的缓存项做失效,白白浪费CPU。
因此在8.0里“一条查询从穿越到返回”的路径更清爽了,没有中间商赚差价。作为开发,也别想着依赖查询缓存来优化读性能,数据库缓存这层已经靠buffer pool和索引解决,真正的热点数据应该交给Redis这类外部缓存。
2.3 合理设置连接超时,避免查询卡死在门口
有一个非常经典的案例:应用报“SQL执行超时”,但DBA把这条SQL单独拎出来手动跑,只要几十毫秒。为什么?因为连接池里的连接已经断开,应用端还在傻傻地复用坏连接,或者连接数被耗尽,所有查询都在等待队列里排队。
我自己习惯做几件事:
- 把
wait_timeout设置成合理值(比如28800秒是默认,太长容易堆积Sleep连接,太短会让连接频繁重建)。 - 在连接池里配置
testOnBorrow或validationQuery,定期探活。 - 应用启动前用
SELECT 1做预热,避免连接首次建立时因为TCP握手、DNS解析慢而出现毛刺。
不要小看连接层,一大半“数据库好慢”的假象,其实是好汉连门都没进去。
3. 解析与预处理:SQL从字符串到语法树的第一次蜕变
过了连接这关,MySQL拿到的还只是一串字符。这时候解析器要上场了,它要做的是“断词”和“组句”。
3.1 词法分析与语法分析各干了什么
词法分析就是把SQL拆成一个个token。拿下面这条语句举例:
sql复制SELECT id, name FROM user WHERE age > 18;
MySQL会把它拆成SELECT、id、,、name、FROM、user、WHERE、age、>、18这些独立的单元。接着语法分析会根据MySQL的语法规则,把这些token组织成一棵抽象语法树(AST),内部叫作LEX和SELECT_LEX。这个过程像极了人读句子:先认字,再根据主谓宾结构理解意思。
你可能会好奇,这过程快吗?极快。MySQL对普通短查询的解析耗时通常在微秒到几十微秒级别,完全不是性能瓶颈。
但语法分析阶段如果发现你语句写错了,会直接报ERROR 1064 (42000): You have an error in your SQL syntax。这条报错看似简单,实际是在帮你指出“断词或组句失败”的位置。看到1064不要慌,大概率是多了个引号、少了逗号,或者在不该出现关键字的地方写了关键字。
3.2 预处理器:表和列不存在的“终极审判”
语法树生成后,预处理器会去做语义检查。它要回答几个问题:
- 表
user到底存不存在? id、name列存不存在?- 多表关联时,列名有没有歧义?
- 查询者有没有该表的访问权限?
- 视图是否需要展开成基础表的查询?
这一步的价值在于尽早拦截错误,不让带着错误语义的查询白白跑到存储引擎层。此外,MySQL在预处理阶段还会把一些标识符转为内部ID,方便后续优化器快速引用。比如你给表起名为user,存储引擎里它是test/user,内部还会有个table_id,后续所有阶段不再靠字符串匹配表名,而是靠这个ID,效率更高。
顺便提一句权限。很多人以为连接器已经完成了所有权限校验,其实连接时校验的是“你能不能登录”,而“能不能查这张表、这几个列”会在执行阶段或预处理阶段反复检查。不要为了省事把所有账号都配成all privileges,权限设太宽,预处理阶段拦不住恶意用户。
3.3 SQL注入就是钻了“信任”的空子
讲到解析和预处理,必须提一下常被热搜带出来的“SQL注入”。它为什么会被称为万能密码?核心原因就一句话:开发者在拼接SQL时,把用户的输入当成了“SQL代码的一部分”,而不是“一个值”。
一个典型的错误写法:
sql复制SELECT * FROM t_user WHERE username = 'admin' AND password = '123';
如果用户在密码框输入:
code复制123' OR '1'='1
拼出来的查询就变成:
sql复制SELECT * FROM t_user WHERE username = 'admin' AND password = '123' OR '1'='1';
由于OR '1'='1'恒为真,整条WHERE条件被绕过,攻击者不需要知道密码也能登录。这个过程中,MySQL解析器并不会报错,因为它拿到的是一条语法完全合法的SQL,只是在语义上被篡改成了“查询所有用户”。
那为什么参数化查询能防注入?因为执行过程换成:
sql复制SELECT * FROM t_user WHERE username = ? AND password = ?;
参数值不会再参与SQL语法解析,而是作为字符串被绑定。也就是说,用户就算输入' OR '1'='1,到了MySQL眼里也只是个普通字符串,不会变成“OR条件”的一部分。这一关是安全意识里最重要的一道闸门,永远不要相信用户的输入,拼SQL和拼HTML是两套完全不同的转义逻辑。
4. 优化器选路内幕:为什么明明有索引,它还是全表扫
解析与预处理完成后,MySQL拿到的是“已通过审计的语法树”,接下来就进入核心决策层:优化器。
4.1 一条查询理论上有很多种执行方案
遇到一个简单的单表查询,你可能觉得执行计划没什么好选的,走主键或者唯一索引就行。但一旦涉及多表关联、子查询、范围条件、排序分组,执行方案的数量会呈爆炸式增长。
举个例子,三张表关联时,关联顺序就有6种可能;如果每张表的访问路径都有索引扫描、全表扫描、范围扫描可选,那执行计划的分支就更多了。优化器要做的,是在众多方案里挑一个成本最低的,就像打车软件帮你比价,最后展示的是最划算的一条路。
MySQL默认使用的是基于成本的优化器(CBO),不是基于规则的写死逻辑。它会给每个方案算“钱”,然后挑最便宜的。这是我们理解优化器所有行为的总钥匙。
4.2 成本模型到底在算什么
优化器不是真的去跑一遍查询再计时,而是用估算模型算。简化一下,一条查询的总成本可以粗略看成:
- IO成本:要从磁盘或内存里读多少个数据页。
- CPU成本:要比较多少行记录、计算多少个表达式。
InnoDB从数据页读取一个页的成本有固定常数,每行记录的检测也有对应的CPU代价。这些数字可以通过mysql.engine_cost和mysql.server_cost查看。
真正影响优化器判断的另一个关键参数,是索引的区分度。InnoDB会为每个索引统计一个叫Cardinality的数值,简单理解就是“这个索引有多少个不同的值”。如果性别列只有“男、女、未知”三种值,Cardinality约等于3,选择性就非常差。此时假设表有100万行,查询条件要返回一半的数据,那用全表扫描反而比走索引再回表更快——因为走二级索引要读索引页,还要逐行回主键索引查完整行,来回折腾的IO远大于顺序扫一遍数据页。
这一点解释了很多初学者的世纪困惑:“我给列加了索引,为什么EXPLAIN里type还是ALL?”不是因为索引坏了,而是优化器拿小本本算了一笔账,觉得走索引赔本,不如全表扫。
可以通过以下命令查看索引基数:
sql复制SHOW INDEX FROM t_order;
注意Cardinality列。如果基数偏小或长期不更新,可以手动执行ANALYZE TABLE t_order;让统计信息刷新一下。
4.3 哪些情况会让索引真正“废掉”
除了基数太小,还有几类情况会让MySQL放弃使用索引,或者想用也用不上:
- 索引列参与函数运算:
WHERE DATE(create_time) = '2024-01-01',因为要对所有行的create_time先做函数计算,无法走索引,应当改成create_time >= '2024-01-01' AND create_time < '2024-01-02'。 - 隐式类型转换:比如索引列是varchar类型,查询条件写成
WHERE phone = 13800001111,MySQL会把字符串列转成数字再比较,导致索引失效。 - 不满足最左前缀原则:组合索引
(a, b, c),直接查b或c通常用不上索引。 - 模糊匹配以通配符开头:
LIKE '%abc'无法走索引,而LIKE 'abc%'可以。
但不要死记硬背“失效”二字,真实场景中,优化器会根据成本选择不失效的其它索引,或替代方案。看到慢查询,正确做法不是直接上FORCE INDEX,而是先理解优化器不走索引的账是怎么算的。
4.4 优化器会“看走眼”吗
会,而且生产环境不算罕见。最常见的原因是统计信息不准。InnoDB对索引基数的统计是采样估计,不是精确值,如果表数据频繁增删改,统计信息迟迟不更新,优化器可能拿着过时的“价目表”做决策。此外,查询条件很复杂时,计算范围估算也会偏差很大,比如你查age > 18,优化器按30%的比例估,实际却命中了80%。
针对这种情况,可以先用ANALYZE TABLE刷新统计。MySQL 8.0以后还支持直方图,它可以帮助优化器更准确理解数据分布。遇到极端但合理的场景,也可以考虑在SQL里用FORCE INDEX临时指定,但只能作为应急手段,永远不要成为常态——毕竟数据是会变的,写死的路径迟早过时。
5. 执行器与InnoDB的最后冲刺:真正干活的是这段
优化器定了执行计划,接下来就到执行器这个“跑腿小哥”出场。很多资料把它讲得很简单,其实这里面隐藏着SQL执行效率的秘密。
5.1 执行器如何逐行读取数据
执行器拿到计划后,并不会一次性把全表数据搬到内存里,而是使用迭代式访问模式。把执行计划想象成一个嵌套循环,每调用一次存储引擎接口,就返回一行记录,Server层再对这行做条件判断。
以这条查询为例:
sql复制SELECT id, name FROM t_user WHERE age > 18;
如果优化器决定用全表扫描,执行器做的事情是:
- 调用InnoDB接口,读取第一行记录。
- 判断
age > 18是否成立。 - 成立就把
id、name放入结果集;不成立就丢弃。 - 调用接口取下一行,重复这个过程。
- 直到取不到新行,循环结束。
如果是走索引查询,执行器会先按索引定位到第一行符合条件的记录,再读取。如果查询需要回表,那就还要用索引中的主键值,回到聚簇索引中再取一次完整行。这个“回来再取一次”的动作就叫回表,回表次数越多,性能越差。
5.2 覆盖索引和ICP,执行过程中两个关键的“减负”手段
既然回表费劲,最直接的优化思路就是让索引包含你需要的所有列。这时候二级索引上就有完整数据,查询不需要再回头找聚簇索引,这就是覆盖索引。EXPLAIN里Extra字段出现Using index时,就代表当前查询走了覆盖索引。我有个习惯:SELECT里不写*,要什么列就写什么列,既能减少网络传输量,也给覆盖索引创造机会。
另一个容易被忽略的机制是索引条件下推(ICP,Index Condition Pushdown)。MySQL 5.6之后引入,它能将一部分WHERE条件的判断下推到存储引擎层,在扫描二级索引时就先把不满足的行过滤掉,减少回表次数。举个典型例子:
sql复制SELECT * FROM t_user WHERE name LIKE '张%' AND age > 18;
如果在(name, age)上有组合索引,没有ICP时,MySQL从索引里找到name LIKE '张%'的记录后,还要回表读完整行,再判断age > 18。启用ICP后,age > 18这个条件在读取二级索引时就会被判断,不满足的直接跳过,回表次数显著降低。EXPLAIN的Extra字段出现Using index condition,就是ICP生效的信号。
执行器这一层的“Run Loop”其实非常机械,高下之分就看每次循环有没有做无用功。
5.3 LIMIT什么时候生效,慢查询日志什么时候记录
很多人误解LIMIT是“先把所有结果查出来再截断”,实际上执行器发现结果集已经攒够LIMIT数量后,会提前终断循环。这就是为什么带LIMIT 10的查询有时比不带LIMIT快几个数量级,尤其在排序处理得当的情况下。
关于耗时记录,慢查询日志是在Server层统一记录的。执行器完整跑完一条语句后,MySQL会根据实际耗时判断是否将它写入慢日志。因此排查慢SQL时,不只是看long_query_time,还要看是否开启了log_queries_not_using_indexes。这个参数会把没走索引的查询也记进日志,对于发现隐性问题很有帮助,代价是日志量大增,生产环境要控制好采样范围,避免日志系统先崩溃。
6. 手把手读懂EXPLAIN,把执行计划摊开看
会看EXPLAIN是排查慢查询的基本功,但很多同学只盯着type和key,这是不够的。EXPLAIN每一列都是一条线索,组合起来才能还原优化器的真实意图。
6.1 EXPLAIN输出核心字段速查
随便执行一条:
sql复制EXPLAIN SELECT u.id, u.name, o.order_no
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.age > 18;
模拟的输出大致长这样:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | u | ALL | NULL | 10000 | Using where |
| 1 | SIMPLE | o | ref | idx_user_id | 1 | NULL |
字段含义要这样理解:
type:访问类型,从好到差大致是system > const > eq_ref > ref > range > index > ALL。看到ALL是全表扫描;看到index是扫描了整棵索引树;看到range是范围扫描,通常比较理想;ref是非唯一索引等值匹配;const是主键或唯一索引等值匹配。key:实际用到的索引。NULL表示没走索引。rows:优化器预估要读取的行数,是成本估算产物,不是实际值。但它对判断“是否走了正确的索引”很有用。Extra:隐藏大量信息。Using where表示Server层对存储引擎返回的行做了过滤;Using index表示覆盖索引;Using filesort表示排序需要在内存或磁盘完成;Using temporary表示使用临时表,通常出现在GROUP BY或DISTINCT操作中。
很多人问我,EXPLAIN显示的rows不准怎么办?平常心看待,它是优化器根据统计信息得到的估算值,目的是比方案,不是报行数。要拿到真正执行行数,可以配合EXPLAIN ANALYZE(MySQL 8.0.18+)看实际值,它甚至会告诉你每一步耗时,是实战利器。
6.2 一个经典场景:组合索引到底怎么建
用一个我实际排查过的订单查询场景做推演。
表结构简化如下:
sql复制CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
KEY idx_status (status),
KEY idx_create_time (create_time)
) ENGINE=InnoDB;
业务SQL经常长这样:
sql复制SELECT * FROM t_order
WHERE status = 1
AND create_time > '2024-03-01 00:00:00'
ORDER BY create_time DESC
LIMIT 20;
优化器面前有三个选择:走idx_status过滤出所有status=1的行再排序;走idx_create_time按时间倒序读但还要过滤status;或者全表扫。
如果status=1的行占比很高,走idx_create_time可能更合适,因为它能利用索引天然有序的特性,避免额外排序。如果status=1的行占比很低,走idx_status效率更高,但ORDER BY就得额外处理排序,因为按status索引读出来的行并不是按时间排好的。
所以单列索引在这种组合条件下很尴尬。合理的做法是建组合索引(status, create_time),让优化器先按status筛,再按create_time排序扫描;如果查询要返回的列不多,还能设计成(status, create_time, user_id)之类的覆盖索引,让Extra显示Using index,连回表都省了。
这就是我经常说的:建索引不是“给where条件里的每一列都建一个”,而是要看查询条件、排序、回表成本结合起来设计。
6.3 实操时建议同时看几个参数
EXPLAIN只是第一步,真正做决策,我建议再补充两个动作:
SHOW INDEX FROM t_order;查看现有索引的基数。基数太低的索引,优化器大概率不用。SHOW TABLE STATUS LIKE 't_order'\G;查看表行数和数据长度。行数差距过大时(比如统计信息落后),先ANALYZE TABLE。
很多时候你以为“优化器选错”,其实是“你手里的统计信息太旧”,让优化器背了锅。
7. 线上最常遇到的几种“执行”问题排查实录
最后把平时会实际动手的排查流程整理一遍。这部分是我日常处理问题最依赖的“原子习惯”,比生记各种工具命令更有用。
7.1 针对慢查询:怎么开日志、怎么定位SQL
先确认慢查询日志有没有开:
sql复制SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
如果在生产环境临时开启,可以直接用SET GLOBAL,但注意所有已有连接不会立即生效,新连接才会读取:
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
long_query_time单位是秒,生产环境我一般调到1秒,低于这个值的SQL通常不值得为它大动干戈。等日志文件积累了内容,用自带工具mysqldumpslow或Percona Toolkit里的pt-query-digest分析。举个例子:
bash复制mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
按时间排序看前10条最慢的SQL。拿到慢SQL后的标准动作是:先EXPLAIN看计划,再根据计划决定加索引还是改写SQL。
7.2 几个容易被忽视的坑(速查表)
| 现象 | 排查思路 | 常见解法 |
|---|---|---|
| 明明有索引,type却是ALL | 检查统计信息是否过期、基数是否太低 | ANALYZE TABLE;评估组合索引 |
| 同一SQL有时快有时慢 | 看buffer pool命中率、是否有锁等待 | 加大innodb_buffer_pool_size;查锁等待 |
| count(*)在大表上一直慢 | 理解InnoDB MVCC,不能像MyISAM直接计数 | 维护汇总表或使用近似值 |
| 加索引后Insert变慢 | 索引过多导致写入维护成本升高 | 删除冗余/低频索引 |
| 查询量不大却CPU飙升 | 看是不是每秒都在解析相同SQL、排序多 | 检查慢日志和连接池;改写SQL |
关于count(*)特别说一句:很多人误以为count(*)和count(1)有差别,其实在MySQL 8.0的InnoDB里它们基本等价。真正影响性能的是InnoDB需要按事务可见性做判断。如果业务对行数要求是“差不多就行”,可以走EXPLAIN里的rows估算或者单独维护一张统计表。
7.3 我的三个排查习惯
踩过很多坑之后,我给自己定了三条规矩,分享给你:
第一,任何慢查询都不要只凭感觉改SQL,先用EXPLAIN看执行计划。没有执行计划的调优全是猜。第二,生产环境加索引之前,一定在测试环境跑一遍相同数据量的SQL,观察type、rows和实际耗时变化。第三,有空翻一下慢查询日志,不是等报警响了才看,而是主动做到“每周排查一次Top慢SQL”。长期坚持下来,你会发现很多索引缺陷和SQL坏味道在日常巡检中就暴露了,根本不会酿成线上事故。
执行链路这层知识最大的价值,不是让你能背出八股文,而是让你在数据变慢的第一时间,能在心里把链路从头到尾过一遍:是连接排队了,还是解析卡了,是优化器选错索引,还是回表太多?能快速定位到具体环节,问题就已经解决了一大半。
