liusheng
5 天以前 fb89df539c6293a41933e445df25fa9b8047ba18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
-- ============================================================================
-- 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 项,改完请跑一次)
-- ============================================================================