-- ============================================================================ -- /smartor/serviceSubtask/getSfStatisticsCountDetails 慢查询优化(南华附一 nhfy) -- 2026-11-02 -- -- 【现象】满意度统计页点“查看详情”要等十几秒到二十几秒。 -- 请求:{ "configKey":"joyCount", "leavehospitaldistrictcodes":["40003514"], -- "serviceType":[6], "pageNum":1, "pageSize":20 } -- -- 【这条接口实际发了 5 条 SQL】 -- ServiceSubtaskServiceImpl#getSfStatisticsScriptDetails 在 type 为空时会跑两趟: -- type=1(语音) → getSfStatisticsScriptDetails + getSfStatisticsScriptItemDetails -- type=2(问卷) → getSfStatisticsScriptDetails + getSfStatisticsScriptItemDetails -- controller 上还有 PageUtils.startPageByPost(...),PageHelper 会对第一条再补一次 COUNT。 -- 每一条都在 service_subtask_detail(38 万行)上做「子任务 JOIN 题目明细 GROUP BY」。 -- -- 【根因:谓词把列包在函数里,索引全废】 -- 为了「出院病区为空回退到在院病区」,条件写成了 -- AND IFNULL(ss.leavehospitaldistrictcode, ss.hospitaldistrictcode) IN ('40003514') -- 列被 IFNULL 包住 → 优化器用不了任何索引,只能 orgid='20001001' 的 27864 条子任务全扫, -- 每条再去 detail 表按 sub_id 捞 11 行(≈30 万行随机读)。 -- EXPLAIN 实测:ss 走 idx_service_subtask_orgid_visit_time,rows=27864。 -- -- 【改法 1:谓词展开成等价的 OR,让索引可用】 -- AND (ss.leavehospitaldistrictcode IN ('40003514') -- OR (ss.leavehospitaldistrictcode IS NULL AND ss.hospitaldistrictcode IN ('40003514'))) -- 语义与 IFNULL 完全等价(4 个病区逐一比对明细行数:8873/95/4307/8550 全部一致)。 -- 只做这一步(不加索引):5.1s → 1.9s,执行计划变成 ref_or_null,rows 27864 → 3644。 -- -- 【改法 2:补两个索引,把范围再收窄】 -- 明细页固定按 (病区, service_type, type) 过滤,原来没有任何索引能覆盖这个组合 -- (service_subtask 上只有单列 index,或 visit_time 打头的复合索引)。 -- -- 离院病区(含空值回退)→ idx_ss_district_pair -- 在院病区 → idx_ss_hosp_dist_svc_type -- -- 【实测(192.168.99.192 / smartor_nhfy,同一条件下的单条 SQL)】 -- 离院病区 40003514:5120ms → 1593ms(冷) / 728ms(热) -- 在院病区 40003514:4750ms → 1854ms -- 明细选项统计 :6183ms → 629~1104ms -- 一次调用 5 条 SQL 合计:≈23s → ≈2s -- ============================================================================ -- ---------------------------------------------------------------------------- -- 【一】DDL(每个环境都要单独执行;MySQL 8 支持 Online DDL,不会锁表) -- ---------------------------------------------------------------------------- -- ALTER TABLE service_subtask -- ADD INDEX idx_ss_district_pair -- (leavehospitaldistrictcode, hospitaldistrictcode, service_type, type, del_flag), -- ADD INDEX idx_ss_hosp_dist_svc_type -- (hospitaldistrictcode, service_type, type, del_flag); -- 单独执行写法(已在 192.168.99.192 执行过,两条各 5~8 秒): -- CREATE INDEX idx_ss_district_pair ON service_subtask (leavehospitaldistrictcode, hospitaldistrictcode, service_type, type, del_flag); -- CREATE INDEX idx_ss_hosp_dist_svc_type ON service_subtask (hospitaldistrictcode, service_type, type, del_flag); -- 回滚: -- DROP INDEX idx_ss_district_pair ON service_subtask; -- DROP INDEX idx_ss_hosp_dist_svc_type ON service_subtask; -- ---------------------------------------------------------------------------- -- 【二】验证执行计划(执行 DDL 后应看到 key = idx_ss_district_pair / idx_ss_hosp_dist_svc_type) -- ---------------------------------------------------------------------------- -- EXPLAIN -- SELECT ssd.scriptid, COUNT(*) AS all_quantity -- FROM service_subtask_detail ssd -- INNER JOIN service_subtask ss ON ssd.sub_id = ss.id -- WHERE ss.del_flag = 0 AND ssd.del_flag = 0 -- AND ss.orgid = '20001001' AND ss.campusid = '30001002' -- AND (ss.leavehospitaldistrictcode IN ('40003514') -- OR (ss.leavehospitaldistrictcode IS NULL AND ss.hospitaldistrictcode IN ('40003514'))) -- AND ss.service_type IN (6) AND ss.type = '2' -- AND ssd.categoryid IN (62,1000000012,5,1000000015,465) -- GROUP BY ssd.scriptid; -- ---------------------------------------------------------------------------- -- 【三】等价性自检:换写法前后每个病区的明细行数必须一模一样 -- ---------------------------------------------------------------------------- -- SELECT w.code, -- (SELECT COUNT(*) FROM service_subtask ss JOIN service_subtask_detail ssd ON ssd.sub_id = ss.id -- WHERE ss.del_flag=0 AND ssd.del_flag=0 -- AND ss.orgid='20001001' AND ss.campusid='30001002' -- AND ss.service_type IN (6) AND ss.type='2' -- AND IFNULL(ss.leavehospitaldistrictcode, ss.hospitaldistrictcode) IN (w.code)) AS ifnull_rows, -- (SELECT COUNT(*) FROM service_subtask ss JOIN service_subtask_detail ssd ON ssd.sub_id = ss.id -- WHERE ss.del_flag=0 AND ssd.del_flag=0 -- AND ss.orgid='20001001' AND ss.campusid='30001002' -- AND ss.service_type IN (6) AND ss.type='2' -- AND (ss.leavehospitaldistrictcode IN (w.code) -- OR (ss.leavehospitaldistrictcode IS NULL AND ss.hospitaldistrictcode IN (w.code)))) AS or_rows -- FROM (SELECT '40003514' AS code UNION ALL SELECT '40003309' -- UNION ALL SELECT '40011024' UNION ALL SELECT '40003317') w; -- 实测:8873/8873、95/95、4307/4307、8550/8550 —— 完全一致。 -- ---------------------------------------------------------------------------- -- 【四】还没解决的两点(需要业务/前端配合,本次未动) -- ---------------------------------------------------------------------------- -- 1) type 为空时固定跑两趟(语音 + 问卷)。南华满意度只可能是问卷(type=2), -- 语音那一趟恒为 0 行。现在有索引后它只要 ~120ms,可以接受; -- 若要彻底省掉,让前端明确传 type,或后端按 serviceType 推断。 -- 2) PageHelper 的 COUNT 会对同一条重查询再跑一遍。这个结果集只有 19 行, -- 其实不需要分页。要省就得去掉 startPageByPost,接口返回结构会变,先不动。