-- ============================================================================ -- smartor_sltd 数据库 满意度维度统计接口性能优化 —— 索引脚本 -- 生成日期: 20260924 -- 目标接口: POST /smartor/serviceSubtaskDetail/sltdMydTotalByDimension -- (dimensiontType=1 → ServiceSubtaskMapper.sltdMydTotalByDimension -- dimensiontType=2 → ServiceSubtaskMapper.sltdMydTotalByYQ,同样受益) -- 目标 SQL: smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml:3050 -- -- 【问题定位】(线上库实测:MySQL 5.7.34,service_subtask_detail 102,249 行 / -- service_task 153 行,两张表除主键外没有任何二级索引) -- -- EXPLAIN 结果显示 4 个访问路径全部是 type=ALL 全表扫描 + Block Nested Loop: -- id=2 DERIVED service_subtask_detail ALL rows=101778 -- period 时间段子查询 -- id=3 DERIVED service_subtask_detail ALL rows=101778 -- dimension 维度子查询 -- id=1 PRIMARY t (service_task) ALL rows=153 -- id=1 PRIMARY d (service_subtask_detail) ALL rows=101778 -- Using join buffer -- -- 即:主查询里 d.taskid = t.taskid 没有索引,102,778 行的明细表被当成 BNL 内表 -- 反复全表扫描,这是接口慢的根本原因;两个派生表也各自全表扫描一次。 -- ============================================================================ -- ---------------------------------------------------------------------------- -- 【执行前检查】确认当前确实没有这些索引(正常应只看到 PRIMARY) -- ---------------------------------------------------------------------------- -- SHOW INDEX FROM service_subtask_detail; -- SHOW INDEX FROM service_task; -- SELECT COUNT(*) FROM service_subtask_detail; -- 约 10 万行,加索引约需 10~60 秒 -- ---------------------------------------------------------------------------- -- 【步骤 1】service_subtask_detail 增加 3 个索引 -- -- 1) idx_ssd_taskid_flag_dim_ct (taskid, del_flag, dimension, create_time) -- 用途:主查询的 JOIN 条件 d.taskid = t.taskid -- + d.del_flag = '0' + d.dimension = dim.dimension(等值) -- + d.create_time 区间(范围)。 -- 效果:由「全表扫描 10 万行」变为「按 taskid 精确定位约 700 行」。 -- -- 2) idx_ssd_ct_flag (create_time, del_flag) -- 用途:period 时间段子查询 -- WHERE del_flag='0' AND create_time >= ? AND create_time < ? -- 效果:create_time 走范围扫描;del_flag 虽在索引第二位但仍可做 -- Index Condition Pushdown 过滤,不必回表,省掉 10 万行回表。 -- (注意:del_flag 必须放第二位,把 create_time 放第一位才能用上范围扫描) -- -- 3) idx_ssd_flag_dim (del_flag, dimension) -- 用途:dim 维度子查询 -- SELECT DISTINCT dimension FROM service_subtask_detail -- WHERE del_flag='0' AND dimension IS NOT NULL -- 效果:索引覆盖该子查询,可走松散索引扫描(Using index for group-by), -- dimension 只有 16 个不同值,基本瞬间返回。 -- -- MySQL 5.7 的 ALTER TABLE 加二级索引支持 ONLINE DDL(不锁表、不阻塞读写), -- 下面显式声明 ALGORITHM=INPLACE, LOCK=NONE;三条索引放在同一条 ALTER 里, -- 只需扫描一次表,比分开执行快。若中途报错不支持,去掉这两个选项重试即可。 -- ---------------------------------------------------------------------------- ALTER TABLE service_subtask_detail ADD INDEX idx_ssd_taskid_flag_dim_ct (taskid, del_flag, dimension, create_time), ADD INDEX idx_ssd_ct_flag (create_time, del_flag), ADD INDEX idx_ssd_flag_dim (del_flag, dimension), ALGORITHM = INPLACE, LOCK = NONE; -- ---------------------------------------------------------------------------- -- 【步骤 2】service_task 增加 1 个索引 -- -- 4) idx_st_type_flag (service_type, del_flag) -- 用途:LEFT JOIN service_task t ON t.service_type IN (6,14) AND t.del_flag='0' -- 效果:service_task 只有 153 行,收益有限(153 行 → 46 行), -- 但能让该表也走索引、避免作为 BNL 外表全扫,属于顺带优化。 -- ---------------------------------------------------------------------------- ALTER TABLE service_task ADD INDEX idx_st_type_flag (service_type, del_flag), ALGORITHM = INPLACE, LOCK = NONE; -- ---------------------------------------------------------------------------- -- 【步骤 3】更新统计信息(加完索引后执行一次,让优化器选对索引) -- ---------------------------------------------------------------------------- ANALYZE TABLE service_subtask_detail; ANALYZE TABLE service_task; -- ---------------------------------------------------------------------------- -- 【验证】执行下面两条 EXPLAIN,关键看: -- * 不再出现 type=ALL 全表扫描(除派生表外) -- * d 行出现 type=ref / key=idx_ssd_taskid_flag_dim_ct -- * service_task 行 key=idx_st_type_flag -- ---------------------------------------------------------------------------- -- EXPLAIN -- SELECT p.period, dim.dimension, IFNULL(ROUND(SUM(d.score), 2), 0) AS totalScore -- FROM (SELECT DISTINCT DATE_FORMAT(d.create_time, '%Y-%m') AS period -- FROM service_subtask_detail d -- WHERE d.del_flag = '0' AND d.create_time >= '2025-01-01' AND d.create_time < '2026-12-31') p -- CROSS JOIN (SELECT DISTINCT dimension FROM service_subtask_detail -- WHERE del_flag = '0' AND dimension IS NOT NULL) dim -- LEFT JOIN service_task t ON t.service_type IN (6, 14) AND t.del_flag = '0' -- LEFT JOIN service_subtask_detail d -- ON d.taskid = t.taskid AND d.del_flag = '0' AND d.dimension = dim.dimension -- AND DATE_FORMAT(d.create_time, '%Y-%m') = p.period -- AND d.create_time >= '2025-01-01' AND d.create_time < '2026-12-31' -- GROUP BY p.period, dim.dimension ORDER BY p.period, dim.dimension; -- ---------------------------------------------------------------------------- -- 【回滚】如需撤销,执行以下语句 -- ---------------------------------------------------------------------------- -- ALTER TABLE service_subtask_detail DROP INDEX idx_ssd_taskid_flag_dim_ct; -- ALTER TABLE service_subtask_detail DROP INDEX idx_ssd_ct_flag; -- ALTER TABLE service_subtask_detail DROP INDEX idx_ssd_flag_dim; -- ALTER TABLE service_task DROP INDEX idx_st_type_flag; -- ============================================================================ -- 【重要】代码侧必须同步修正 del_flag 的写法,否则索引只能用上一半 -- -- ServiceSubtaskMapper.xml 中写的是: d.del_flag = 0 -- 但 del_flag 的列类型是 char(10)(取值 '0' / '1')。 -- MySQL 在「字符串列 = 数字常量」时会按 DOUBLE 隐式转换比较, -- 导致该条件无法作为索引的等值条件使用(只能过滤,不能定位)。 -- 上面 3 个索引里 del_flag 都是等值列,改成下面这样才算真正生效: -- -- d.del_flag = 0 → d.del_flag = '0' -- del_flag = 0 → del_flag = '0' -- t.del_flag = 0 → t.del_flag = '0' -- -- 适用范围(同一文件内同样问题的查询,建议一起改): -- * sltdMydTotalByDimension (3050 行附近) -- * sltdMydTotalByYQ (3097 行附近) -- * mydTrend (3030 行附近) -- 语义影响:'0' 与 0 的匹配结果在本库中完全一致(del_flag 实测只有 '0'、'1' -- 两种取值,无 NULL、无空串),改成字符串比较不会改变业务结果。 -- -- 另:本接口两条 SQL 的 LEFT JOIN service_task t 后面没有跟任何关联条件 -- (ON 里只有 t 自己的过滤条件),等价于先对 service_task 做一次过滤再交叉 -- 连接,本身写法不规范;本次只加索引、不动 SQL,避免影响返回结构。 -- ============================================================================