索引失效场景复盘 + 项目规范
索引失效场景复盘
5 类索引失效场景+项目约束
索引列使用函数、运算、隐式类型转换
- 原理:索引存储的是原始字段值,运算后值发生变化,无法匹配索引树
- 错误示例:
where substr(phone,1,3)='138'、where phone = 13800000000(字符串手机号不加数字引号,隐式转换) - 项目规范:手机号、status、template_id 禁止在条件中做运算、截取
左模糊 / 全模糊 like 查询 like '%xxx' / like '%xxx%'
- 原理:B + 树前缀有序,后缀匹配无法走索引
- 业务方案:模糊检索手机号切换 ES 倒排索引;MySQL 仅允许
like '138%'右模糊
or 条件一侧字段无索引
- 原理:or 只要有一个条件无法走索引,整体触发全表扫描
- 规范:or 关联的所有字段统一建索引,复杂多条件拆分多条 SQL 合并结果
联合索引跳过最左前置字段(违背最左匹配)
- 示例:联合索引
(template_id,status,create_time)失效写法:where status=0,跳过首位 template_id - 强制规范:后台查询短信记录必须携带 template_id
数据倾斜,优化器主动放弃索引
- 场景:营销活动大量数据 status=0 待发送,符合条件数据占表绝大部分
- 解决方案:force index 强制指定索引、按时间拆分查询范围缩小数据量
总结:所有查询条件操作索引字段时,保证原始值匹配、不做转换、不跳过联合索引首列,规避全表扫描慢 SQL。
深分页专项优化
深分页底层缺陷
语法 limit offset, size,offset 代表偏移量。 MySQL 执行逻辑:先扫描 offset + size 条数据,丢弃前面 offset 条,只返回后面 size 条。 当 offset 数值很大(如 limit 10000,10),会扫描大量无用数据,IO 开销极高,页面越往后翻查询越慢,短信记录千万级表分页卡顿严重。
三种落地优化方案
主键书签分页(最推荐)
利用自增主键 id 定位起点,不用偏移量。
示例:上一页最后一条 id=10000
sql
select phone,template_id,status,create_time
from sms_record
where id > 10000 and status=0 limit 10
优势:直接走主键索引,无需扫描前置数据。
延迟关联
先只分页查询主键,再通过主键关联查询完整字段,减少回表开销。
sql
select t1.* from sms_record t1
inner join (select id from sms_record where status=0 limit 10000,10) t2
on t1.id = t2.id;
业务层限制
前端限制最大可查询页码,不允许用户查询几万页之后的数据,从业务源头规避深分页。
项目规范补充
禁止使用 select * 分页,只查询页面需要字段,配合覆盖索引进一步提速。
冗余索引识别与清理规范
冗余索引定义
如果已经存在联合索引 (a,b,c),那么单独建立的索引 (a) 就属于冗余索引。 联合索引最左匹配原则,(a,b,c) 本身就能单独命中 a 字段查询,额外建单列 a 索引完全重复,无作用。
短信项目实例
我们给短信记录表建立联合索引:(template_id, status, create_time) 如果再单独创建索引 (template_id),这条单列索引就是冗余索引。 因为仅根据 template_id 查询时,联合索引已经可以满足需求。
联合索引:
(template_id, status)单独创建
(template_id)单列索引属于冗余索引。仅按
template_id查询时,联合索引首字段即可满足查询,单列索引完全多余
索引过多、冗余索引带来的业务负面影响(短信营销场景)
- 拖慢写入性能:批量下发短信大量执行 insert,新增 / 修改数据时,所有索引都要同步更新,冗余索引会增加 IO 开销,降低发送吞吐;
- 占用磁盘空间:千万级短信记录表,多余索引会持续膨胀磁盘占用;
- 增加 SQL 解析耗时:MySQL 优化器需要遍历全部索引匹配最优方案,索引越多,筛选耗时越长
线上运维规范
定期工具查询冗余索引,在业务低峰期清理无用、冗余索引,平衡查询与写入性能。
大表分表实战方案
水平分表(按数据行拆分)
- 拆分逻辑 同一张表结构不变,按照某一列规则,把数据分散到多张子表,每张表存储一部分数据。
- 短信项目落地:按月拆分
- 表命名:
sms_record_202607、sms_record_202608 - 路由规则:根据短信
create_time创建月份,MyBatis-Plus 动态表名插件自动匹配对应分表
- 优势 单表数据量可控,索引体积变小,查询、写入速度大幅提升。
垂直分表(按字段拆分)
- 拆分逻辑 把一张宽表,拆分为两张表,主键一致,冷热字段分离。 短字段做主表,大文本、低频读取字段做附表。
- 短信项目落地
- 主表
sms_record:id、phone、template_id、status、create_time(短字段,页面常用) - 附表
sms_record_detail:record_id、sms_content、third_callback(短信内容、第三方回执大文本)
- 优势 减少单条数据存储体积,降低磁盘 IO,列表查询不需要加载大字段。
分表衍生问题配套解决方案
- 跨月分页、跨月统计麻烦 近 3 个月热数据直接查分表;半年以上冷数据归档至独立归档库。
- 报表统计复杂 通过定时任务预聚合发送量、成功失败量,存入 Redis 缓存,前端报表直接读缓存。
冷热数据分离配套方案
- 热数据:近 3 个月短信记录,存按月分表,支持实时查询;
- 冷数据:半年以上历史数据,归档归档库,不参与日常分页查询。
慢 SQL 线上排查流程
完整排查五步流程(短信报表超时场景标准流程)
- 开启慢查询日志捕获慢 SQL 运维提前配置 MySQL 慢日志:设置超时阈值(如 1s),自动记录所有执行超时、全表扫描的 SQL,短信报表、列表慢语句都会被日志捕获。
- 从慢日志定位超时 SQL 筛选接口对应执行耗时高、扫描行数巨大的目标 SQL。
- 使用 explain 分析执行计划 解析 SQL 索引使用、扫描行数、额外性能隐患,重点观察两处风险:
type=ALL:代表全表扫描,无有效索引;- Extra 字段出现
Using filesort/Using temporary,是严重性能损耗点。
- 针对性优化 SQL / 索引
- 出现
Using filesort:给排序字段 create_time 建立联合索引,避免磁盘排序; - 出现
Using temporary:拆分复杂 group by 统计 SQL,营销报表数据用定时任务预聚合存入 Redis; - 全表扫描:补充匹配业务条件的联合索引,规避索引失效场景。
- 出现
- 本地压测验证优化效果 对比优化前后执行耗时,确认查询速度大幅下降后,再上线发布。
两个关键隐患标识详解
Using filesort 文件排序
SQL 存在 order by 排序,但没有覆盖索引,MySQL 需要在内存 / 磁盘排序,千万级表排序极慢。
优化方案:联合索引包含 where 筛选字段 + 排序字段。
Using temporary 临时表
group by、distinct 多字段分组时,MySQL 创建临时表存放中间数据,占用大量内存 / 磁盘,报表统计极易触发。
优化方案:拆分复杂统计、预聚合数据缓存。
短信项目应急方案(活动高峰期报表阻塞)
活动期间临时下线实时报表,通过定时任务提前统计发送总量、成功率存入 Redis,前端直接读取缓存,避免大量慢 SQL 并发拖垮数据库。