业务上我们用Redis缓存热数据,Java用JIT缓存热代码,而MySQL 用 Buffer Pool 缓存索引页。为了消除磁盘随机IO,无论是读一行还是改一行,innoDB都会把整个16kb页加载到内存中。
写代码的程序员根据业务热点决定缓存信息,JVM会分析CPU执行最多的代码块,MySQL则把热判断交给了优化器。
一张表就是一片森林,由一堆B+树组成。 主键树=主键id+整行数据: SELECT * FROM table WHERE id=1; 二级树=索引值+主键id: SELECT id FROM table WHERE poi_id=?;
因此一条SQL的执行顺序首先是确定数据来源,你要走哪片森林【from table】,索引主要针对【where】,【select】牵扯回表,【order by】关乎是否需要利用内存或磁盘文件排序。
客户端发送SQL。服务端中,连接器核对身份;分析器识别关键字和语法;优化器基于扫描成本选择索引(树)和表顺序(森林);执行器校验权限,调用存储引擎的接口。
优化器预估成本的手段分为统计采样和索引潜水。
前者随机抽取N个数据页,看这几页的数据分布,乘以总页数,估算全局基数。基数越大说明字段区分度越高,过滤越高效。既然采样,就说明有误差,优化器很可能会选错索引。
后者对于WHERE IN(1,2,3)这种确定范围的查询,会扫描B+树,找到起点和终点,计算两点之间的大概页数。
优化器基于执行成本做决策。 成本=IO成本(读页数)+CPU成本(计算/排序)。 数据量决定预估手段。如果为了选索引要把所有树走一遍,那查询还没开始IO已经爆了
存储引擎层,即InnoDB,负责把数据从磁盘搬到Buffer Pool(内存),执行逻辑可以类比为git管理。内存就是工作区,我们在idea修改代码,mysql修改数据,是脏页,还没存盘。git add 是把代码放到暂存区,在这里是redo log buffer。
如果没有暂存区,每插入一条数据mysql就要往磁盘写一次,磁盘io是毫秒级的,写一万次磁盘程序可能跑几分钟,而把记录写入内存是纳秒级。
业务中下发数据前会记录spu快照,idea本地 git commit 记录代码快照,mysql则在 redo log物理存档,记录"在第X页偏移量Y处改了Z",即使断电重启也能重放恢复。
远程推送 git push 则在 Binlog磁盘,这是逻辑存档,记录"执行UPDATE语句",用于主从复制和回滚。最终落盘,好比CD部署,后台线程把Buffer Pool的脏页数慢慢刷回.ibd数据文件。
刷盘策略取决数据安全与性能取舍,0-1-2不再赘述。redo log在引擎层,binlog在Server层,执行器操作以上。
因此分析慢sql的思路是顺着设计"自底向上"的视角,按照物理层→引擎层→逻辑/服务层→应用层顺序排查。我们不是上来就分析具体语句,而是先看表空间,防止因为业务量大覆盖多。你可以拆成两个维度去理解:高并发吞吐和高频更新与删除。这俩维度决定内存与磁盘的健康状况。
空间分析除了容量和binlog空间,还有有俩指标叫增长率和【表空洞容量】。后者指删除数据后,不会把申请空间还给操作系统,而是标记可复用,有新数据直接插,不用二次申请。跟CMS垃圾回收器的标记清除算法一个意思。你不做压缩整理,空闲列表全是小碎块,塞不进大对象。
这俩指标针对的就输高吞吐和高频变化:
如果单位时间内进入数据库的请求非常高,意味着内存的数据页频繁修改,脏页变多,后台线程需要拼命刷盘。如果刷盘速度<写入速度,buffer pool 被占满,新请求进来没有空闲可用,必须强制等待,导致性能卡顿。
如果数据不是写完不动了,而是反复修改、覆盖、删除,就会出现稀疏页。对内存,页密度会下降:产生大量碎片和空洞,而不是满当当的有效数据。原本16kb能存100条订单,现在因为碎片,只能存30条,那么为了读取同样数据量,你需要加载更多物理页。对磁盘:字段从A变成AAAAAA,当前页塞不下,就会发生页分裂,一页变两页,空间利用率减半。
如果磁盘满了或碎片太高,机械磁盘磁头疯狂跳跃(随机io),ssd的写放大也会增加。这是物理层面的极限,sql再优化也没用。定时删除数据也解决不了的话,那就只能拆表。
表空间没问题才关注SQL。以表纬度分析,可能找出共因,要查看最耗时语句的扫描行数。这里依然关注的是吞吐,不能直接看工具里AI给出的explain,因为同条语句的数据量也会不一样。而扫描行数是比执行时间更客观的指标,时间受机器负载影响,扫描反应了复杂度,优化器根据扫描成本选择索引。
接着单独执行这条最耗时的sql,看返回数据量(实际)并explain执行计划(优化器的预测),二者差异可能是突破点。你会发现复现往往比日志速度快很多,索引选择也正确。除了buffer pool预热缓存外,还因为环境是隔离的,手动跑sql没有并发锁竞争。因此指标会提供lock_time和row_sent
最后是代码执行层,拿着sql的traceid找到对应代码,从火焰图里最耗时的类下手。(注意sql案发时间,trace平台可能有时效性,如果id为空,只能去日志淘金)
在代码层分析调用频率、事务范围、业务模型。能拆接口参数就不要拆SQL,除非拆不了。比如,mapper接口只传了spuid列表,可调用方不知道spuid在库表里是作为entityid存在,还是relationid存在,所以用了or查询。那么只能在sql拆成union,这样可以命中两个索引。
未完待续。。。