MySQL优化器基于成本估算默认选择全表扫描,强制索引后触发索引下推(ICP)和有序回表(MRR),但扫描行数仅由索引最左前缀决定,ICP不减少扫描量。优化器因局部数据倾斜误判,实际有效数据仅31行。最优方案是创建覆盖索引,避免扫描与回表。
本次分析使用的是业务表 contract_company_info(合同分公司明细表),核心表结构及索引信息如下:
CREATE TABLE IF NOT EXISTS `contract_company_info` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '分公司明细表主键', `delete_flag` smallint(2) NOT NULL DEFAULT 0 COMMENT '数据状态,0正常,1删除', `contract_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合同编号', `project_code` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联项目号', `update_time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE, -- 核心联合索引(本次分析重点) KEY `idx_contract_company` (`contract_code`,`company_code`,`delete_flag`) USING BTREE, KEY `idx_contract_oppo` (`contract_code`,`opportunity_code`,`delete_flag`), KEY `idx_company_code` (`company_code`)) ENGINE=InnoDB AUTO_INCREMENT=1686295 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合同分公司明细表';
contract_code > company_code > delete_flag本次优化分析的核心查询SQL,业务需求为:根据指定合同号、有效数据状态,查询合同关联的项目编码。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
SELECT contract_code, project_code FROM contract_company_info WHERE delete_flag = 0 AND contract_code IN ('ACCS20022962N', 'ACCS20024734W');未加任何强制索引时,MySQL优化器默认选择了全表扫描:
1 SIMPLE contract_company_info ALL idx_contract_company,idx_contract_oppo 799303 Using where
idx_contract_company、idx_contract_oppoMySQL基于成本优化器(CBO) 进行决策,核心逻辑如下:
idx_contract_company 不包含查询字段 project_code,走索引需回表查询EXPLAIN SELECT contract_code, project_code FROM contract_company_info FORCE INDEX(idx_contract_company)WHERE delete_flag = 0 AND contract_code IN ('ACCS20022962N', 'ACCS20024734W');强制索引后,完整的执行计划及各字段解析如下:
| 字段名称 | 字段值 | 详细说明 |
|---|---|---|
| id | 1 | 查询执行顺序,单条简单查询,无关联子查询 |
| select_type | SIMPLE | 简单查询,无子查询、UNION、派生表 |
| table | contract_company_info | 本次查询的数据表 |
| type | range | 索引范围扫描,IN条件命中索引区间,优于全表扫描 |
| possible_keys | idx_contract_company | 优化器可选用的索引 |
| key | idx_contract_company | 本次实际生效的联合索引 |
| key_len | 259 | 仅命中索引首列 contract_code,未命中后续字段,严格遵循最左前缀原则 |
| ref | NULL | 无常量等值匹配,属于范围扫描场景 |
| rows | 404811 | 优化器仅根据索引前缀估算的扫描行数,不受 delete_flag、ICP 影响 |
| Extra | Using index condition; Rowid-ordered scan | Using index condition:触发索引下推ICP,引擎层过滤数据,减少回表;Rowid-ordered scan:MRR有序回表优化,随机IO转顺序IO |
IN 查询被优化为索引范围扫描,命中二级索引,替代了全表扫描。
仅使用了索引最左前缀 contract_code 一列,计算依据如下:
结论:delete_flag 未参与索引范围裁剪,仅靠 contract_code 确定扫描区间。
优化器仅根据索引前缀contract_code估算的扫描行数,与 delete_flag、索引下推无关,仅代表需遍历的索引总行数。
delete_flag=0,减少回表次数通过真实计数SQL,验证索引扫描行数与有效数据行数的巨大差异,解释优化器误判的根源。
SELECT COUNT(*) FROM contract_company_info WHERE contract_code IN ('ACCS20022962N', 'ACCS20024734W');实测结果:694501 条(真实索引扫描总行数,优化器估算40万,存在采样偏差)
SELECT COUNT(*) FROM contract_company_info WHERE delete_flag = 0 AND contract_code IN ('ACCS20022962N', 'ACCS20024734W');实测结果:31 条(最终有效的业务数据)
MySQL采用基于成本的优化器(CBO, Cost-Based Optimizer),执行计划的选择完全由成本估算结果决定,并非“索引一定比全表快”的固定规则。优化器会分别计算不同执行路径的总成本,最终选择成本最低的方案。
针对当前查询,优化器会分别计算“走idx_contract_company索引”和“全表扫描”两条路径的总成本:
优化器的成本估算依赖全局统计信息,无法感知字段间的局部关联分布,导致本次场景出现决策偏差:
EXPLAIN输出的rows字段,是优化器基于统计信息估算的需要扫描的记录条数,并非最终返回给客户端的结果行数。其估算严格遵循最左前缀原则,仅由能用于索引区间裁剪的字段决定。
当执行计划为type=ALL时,rows值为表的预估总行数,来自InnoDB的元数据统计信息:
当执行计划走二级索引时,rows值仅由索引最左连续前缀字段的过滤性估算得出,非连续前缀的过滤条件不参与行数估算。
结合本次强制索引场景(idx_contract_company,key_len=259):
本次强制索引场景下,优化器估算rows=404811,而实测contract_code条件匹配的真实行数为694501,存在明显偏差,原因在于:
核心规则:EXPLAIN的rows是“索引扫描预估行数”,仅由能裁剪索引区间的连续前缀字段决定。
当前索引 (contract_code,company_code,delete_flag),查询跳过了中间 company_code,delete_flag 属于非连续索引字段:
MySQL优化器仅依赖全局统计信息,无法识别局部数据倾斜:
| 执行方案 | 索引扫描行数 | 回表次数 | 核心特性 | 性能评级 |
|---|---|---|---|---|
| 默认全表扫描 | 80万行 | 0 | 顺序IO、内存遍历,无索引优化 | 一般 |
| 原索引+ICP+MRR | 69万行 | 31次 | 索引层过滤、有序回表,减少无效IO | 良好 |
| 优化后覆盖索引 | 31行 | 0次 | 精准区间扫描、纯索引查询、零开销 | 最优 |
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述