-- ============================================================================
|
-- POST /smartor/patinhosp/selectPatMedInhospList 慢查询诊断 + 索引脚本
|
--
|
-- 现场入参(在院视图):
|
-- {"bedSort":1,"pageNum":1,"pageSize":20,
|
-- "deptcodeList":["40003378"],"hospitaldistrictcodeList":["40003380"],"cry":0}
|
--
|
-- 调用链(已逐行核对):
|
-- InhospVisitDataController#selectPatMedInhosplis
|
-- -> InhospVisitFacade#selectPatMedInhospList
|
-- -> DefaultInhospVisitDataService#selectPatMedInhospList
|
-- -> PatMedInhospMapper.xml <select id="selectPatMedInhospList">
|
--
|
-- ⚠️ 注意:迁移后这条链【不再经过】smartor 的 PatMedInhospServiceImpl#selectPatMedInhospList,
|
-- 因此那里面的 cry 归一化(cry=0 -> inhospstate='0')在这条接口上【已经不生效了】。
|
-- 这一点同时是"慢"的根因之一,见下面【第 4 节】。
|
--
|
-- 执行顺序:第 1 步看现状 → 第 2 步(二选一)→ 第 3 步 EXPLAIN 验证。
|
-- ADD INDEX 走 online DDL(不阻塞读写),但大表仍会吃 IO,建议业务低峰执行。
|
-- ============================================================================
|
|
|
-- ----------------------------------------------------------------------------
|
-- 第 1 节:这条入参实际执行的两条 SQL(由 MyBatis 动态标签展开 + PageHelper 改写)
|
-- ----------------------------------------------------------------------------
|
--
|
-- ① 数据查询:<select id="selectPatMedInhospList">(cry=0 走 endtime is null 分支;
|
-- bedSort 传字符串 "1" 不匹配 XML 里的 "asc"/"desc",故落到 <otherwise> 默认排序)
|
--
|
-- SELECT a.age, a.telcode, a.sex, b.inhospid, b.nurse_id, ...(共 60+ 列)..., a.idcardno
|
-- FROM pat_med_inhosp b
|
-- JOIN pat_archive a ON a.id = b.patid
|
-- WHERE 1=1
|
-- AND a.del_flag = 0 -- ← 数字常量比 char 列,见第 4 节 F1
|
-- AND b.del_flag = 0 -- ← 同上,且它正是 idx_inhosp_optimize 的首列
|
-- AND a.idcardno IS NOT NULL
|
-- AND b.endtime IS NULL -- ← 在院判定(等价条件,无法用现有索引)
|
-- AND b.hospitaldistrictcode IN ('40003380')
|
-- AND b.deptcode IN ('40003378')
|
-- AND b.orgid = '<登录用户 orgid>' -- 由 @AddOrgId 注入
|
-- [AND b.campusid = '<登录用户 campusid>'] -- 院区权限未放开时由 @AddOrgId 注入
|
-- GROUP BY b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex -- ← 见第 4 节 F3
|
-- ORDER BY b.starttime DESC, b.bed_no ASC
|
-- LIMIT 0, 20
|
--
|
-- ② 总数查询:PageHelper 自动生成。因为有 GROUP BY,isSimpleCount() 为 false,
|
-- CountSqlParser 会【把整条查询包一层】而不是改写成 count(0)(已核对 pagehelper 5.3.x
|
-- 字节码常量 "select count(" + ") from ( " + ") tmp_count"):
|
--
|
-- SELECT count(0) FROM ( <① 去掉 ORDER BY 后的完整 60 列语句> ) tmp_count
|
--
|
-- => 每次翻页 = 同一条重量级查询跑两遍。这是"慢"的第二个根因,见第 4 节 F2。
|
--
|
-- ③ 另外一条:fillTagname 不在这条链上(那是 smartor 侧 ServiceImpl 的逻辑),可排除。
|
|
|
-- ----------------------------------------------------------------------------
|
-- 第 1 步:先看现状 —— 现有索引、慢日志、以及这条 SQL 的真实执行计划
|
-- ----------------------------------------------------------------------------
|
|
-- 1.1 pat_med_inhosp / pat_archive 现有索引
|
-- 重点看 idx_inhosp_optimize 的真实列顺序(代码注释里写的是 (del_flag, orgid, inhospstate))
|
SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME, CARDINALITY, NON_UNIQUE
|
FROM information_schema.STATISTICS
|
WHERE TABLE_SCHEMA = DATABASE()
|
AND TABLE_NAME IN ('pat_med_inhosp', 'pat_archive')
|
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;
|
|
-- 1.2 关掉的条件到底是不是索引列、列类型对不对(del_flag / inhospstate 是 char 还是 int,
|
-- 直接决定下面 F1 / F4 要不要改)
|
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE
|
FROM information_schema.COLUMNS
|
WHERE TABLE_SCHEMA = DATABASE()
|
AND TABLE_NAME IN ('pat_med_inhosp', 'pat_archive')
|
AND COLUMN_NAME IN ('inhospid', 'patid', 'del_flag', 'orgid', 'campusid',
|
'inhospstate', 'endtime', 'starttime', 'bed_no',
|
'deptcode', 'hospitaldistrictcode');
|
|
-- 1.3 表体量 + 在院占比(判断"全表扫描"到底贵不贵)
|
SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH / 1024 / 1024, 1) AS data_mb,
|
ROUND(INDEX_LENGTH / 1024 / 1024, 1) AS index_mb
|
FROM information_schema.TABLES
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('pat_med_inhosp', 'pat_archive');
|
|
SELECT COUNT(*) AS total,
|
SUM(endtime IS NULL) AS in_hosp_by_endtime,
|
SUM(inhospstate = '0') AS in_hosp_by_state,
|
-- 两种"在院"定义不一致的行数:>0 就说明【不能】随手把 inhospstate='0' 当作
|
-- endtime IS NULL 的替代条件,F2 只能走"加索引"那条路
|
SUM(CASE WHEN endtime IS NULL AND IFNULL(inhospstate, '') <> '0' THEN 1 ELSE 0 END) AS in_hosp_def_conflict
|
FROM pat_med_inhosp
|
WHERE del_flag = '0';
|
|
-- 1.4 直接 EXPLAIN 这条语句(把 orgid 换成现场值)
|
EXPLAIN
|
SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime, b.endtime
|
FROM pat_med_inhosp b
|
JOIN pat_archive a ON a.id = b.patid
|
WHERE a.del_flag = 0
|
AND b.del_flag = 0
|
AND a.idcardno IS NOT NULL
|
AND b.endtime IS NULL
|
AND b.hospitaldistrictcode IN ('40003380')
|
AND b.deptcode IN ('40003378')
|
AND b.orgid = '<换成现场 orgid>'
|
GROUP BY b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex
|
ORDER BY b.starttime DESC, b.bed_no ASC
|
LIMIT 0, 20;
|
--
|
-- 预期(改之前):b 表 type=ref/range 或 ALL,key 只有 idx_inhosp_optimize 的
|
-- (del_flag, orgid) 前缀能用到(或完全用不上);
|
-- rows 约等于"该机构全部历史住院记录数",Extra 里出现 Using where; Using temporary; Using filesort。
|
-- 改之后要点:b 表 rows 应降到"该机构在院人数"量级(数千),且 key 命中下面的新索引 / 现有索引全前缀。
|
|
|
-- ----------------------------------------------------------------------------
|
-- 第 2 节:两种修法 —— 【本次采用修法 A】,修法 B 留作备选
|
-- ----------------------------------------------------------------------------
|
|
-- 修法 A(本次采用,零语义风险):给"在院"补一个能定位的索引。
|
-- 现有 idx_inhosp_optimize 的第三个等值列是 inhospstate,但本接口不传这个条件,
|
-- 只能用上 (del_flag, orgid) 前缀 = 该机构全部历史住院记录,等于没走索引。
|
-- 前提:F1 的 del_flag = '0' 已改(否则首列用不上,这条索引也白建)。
|
ALTER TABLE pat_med_inhosp
|
ADD INDEX idx_pmi_del_org_end (del_flag, orgid, endtime);
|
|
-- 可选加强:把两个 IN 条件也放进索引,命中行不用回表就能判掉(索引更宽,写放大更明显)
|
-- ALTER TABLE pat_med_inhosp
|
-- ADD INDEX idx_pmi_del_org_end_dept (del_flag, orgid, endtime, deptcode, hospitaldistrictcode);
|
|
-- 出院 / 转科 / 预入院 三个视图(cry=1/2/3,order by b.endtime desc)用这条:
|
-- 等值列 (del_flag, orgid, inhospstate) 全命中,且 endtime 可以顺带避免 filesort 的排序键
|
ALTER TABLE pat_med_inhosp
|
ADD INDEX idx_pmi_del_org_state_end (del_flag, orgid, inhospstate, endtime);
|
|
|
-- 修法 B(本次不采用,留作备选):不加索引,cry=0 时补 inhospstate='0',让现有
|
-- idx_inhosp_optimize 的 (del_flag, orgid, inhospstate) 前缀全部等值命中 ——
|
-- 其实这才是【迁移前的行为】:smartor 的 PatMedInhospServiceImpl#selectPatMedInhospList
|
-- 在 cry=0 时会 setInhospstate("0"),迁到 ruoyi-inhospvisit 后这条链直接调 Mapper,
|
-- 这段归一化丢了。
|
-- ⚠️ 之所以没选它:它会让"endtime 为空但 inhospstate<>'0'"的行静默消失,
|
-- 是否安全取决于数据,得先看第 1.3 节的 in_hosp_def_conflict。
|
-- 若将来确认 = 0,这一条比建索引更省(见 PatMedInhospMapper.xml 的
|
-- <if test="cry != null and cry == 0 "> 分支)。
|
|
|
-- ----------------------------------------------------------------------------
|
-- 第 3 节:验证(第 2 步做完后重跑第 1.4 节)
|
-- ----------------------------------------------------------------------------
|
-- ① b 表 type = ref,key = idx_pmi_del_org_end(或 idx_inhosp_optimize),rows 数千级;
|
-- ② Using temporary; Using filesort 仍会有(GROUP BY + 床位排序导致,见 F3),
|
-- 但因为参与排序的行数已经从"全机构历史"降到"本部门在院",可以接受;
|
-- ③ 接口耗时:count + data 两条都按这个计划走,整体应降到几十毫秒级。
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【回滚】
|
-- ----------------------------------------------------------------------------
|
-- ALTER TABLE pat_med_inhosp DROP INDEX idx_pmi_del_org_end;
|
-- ALTER TABLE pat_med_inhosp DROP INDEX idx_pmi_del_org_state_end;
|
|
|
-- ============================================================================
|
-- 第 4 节:代码侧顺带发现的问题(按影响排序)
|
-- ✅ 已改:F1 / F2 / F4 ⏸ 未改(等确认):F3 / F5 / F6
|
--
|
-- F1 ✅ 已改 b.del_flag = 0 / a.del_flag = 0:数字常量比 char 列,MySQL 会把【列】转
|
-- DOUBLE,该条件只能过滤、不能定位。idx_inhosp_optimize 的首列就是 del_flag,
|
-- 等于首列白给。已改成 = '0'。本仓库已有同类结论,见
|
-- sql/sltdMydTotalByDimension_index_20260924.sql 第 117-134 行:"代码侧必须同步修正
|
-- del_flag 的写法,否则索引只能用上一半"。
|
-- (列是 int 时写 '0' 也安全:常量侧转换,索引照样可用。)
|
--
|
-- F2 ✅ 已改 PageHelper 的 count:有 GROUP BY 时不能简化,只能包一层 tmp_count,
|
-- 等于每次翻页把这条 60 列查询再跑一遍。已在
|
-- ruoyi-inhospvisit/.../PatMedInhospMapper.xml 补 selectPatMedInhospList_COUNT
|
-- (pagehelper 5.3.x 的 PageInterceptor 支持 countSuffix="_COUNT" 的手写 count,
|
-- 已核对字节码常量:genCountMsId / getExistedMappedStatement / executeManualCount),
|
-- 并把 FROM/WHERE 抽成 selectPatMedInhospListFromWhere 片段给两条语句共用,
|
-- 保证 count 与列表永远是同一套条件。
|
--
|
-- F3 ⏸ 未改 GROUP BY b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex 恒等、不去重:
|
-- a.id 是 pat_archive 主键、b.patid 只关联到一行,每个 join 结果行自成分组。
|
-- 它只换来 Using temporary + Using filesort。删它之前要先确认 pat_archive.id 唯一;
|
-- 确认后删掉,F2 的 count 还会自动变成简单的 count(0)。
|
--
|
-- F4 ✅ 已改 cry=1/2/3 分支原来是 b.inhospstate=1 / =2 / =3(数字常量 vs char 列),同 F1,
|
-- 出院 / 转科 / 预入院 三个视图的索引也用不上,已改成 '1' / '2' / '3'。
|
--
|
-- F5 ⏸ 未改 bedSort 在这条链上没有归一化。前端传 bedSort:1(数字),Jackson 落到 String
|
-- 是 "1",而 XML 只认 "asc"/"desc",所以【按床位排序实际没生效】,落回默认
|
-- order by starttime desc, bed_no asc。归一化函数 normalizeBedSort 目前只在
|
-- smartor 的 ServiceImpl 里,而这条链已经不走它了。
|
--
|
-- F6 ⏸ 未改 date_format(b.endtime,'%y%m%d') >= date_format(?, '%y%m%d') 这类写法
|
-- (出院/入院时间范围过滤,本入参不涉及)对列用了函数,索引必失效。等价改法:
|
-- >= date_format(?, '%y%m%d') -> b.endtime >= DATE(?)
|
-- <= date_format(?, '%y%m%d') -> b.endtime < DATE(?) + INTERVAL 1 DAY
|
--
|
-- 回归检查:python sql\_probe\verify_patinhosp_list_count.py(17 项,改完请跑一次)
|
-- ============================================================================
|