liusheng
6 天以前 279d50c151d14d04cb6680b10c82bf32b72968cb
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
-- ============================================================================
-- /smartor/patinhosp/selectPatMedInhospList 慢 —— 一页诊断(把输出贴回来即可定位)
--
-- 现场入参:{"bedSort":1,"pageNum":1,"pageSize":20,
--           "deptcodeList":["40003378"],"hospitaldistrictcodeList":["40003380"],"cry":0}
--
-- 代码侧已经做完的三件事(不用再查):
--   ① del_flag / inhospstate 已改成字符串常量 → 索引首列能用上了
--   ② 分页 count 已改成手写 _COUNT(pagehelper 实测解析到 5.3.2,该机制生效),
--      不再把 60 列查询整个包一层重跑
--   ③ 【在院口径】cry=0 现在同时带 endtime is null 和 inhospstate = '0'
--      —— 这是迁出前 PatMedInhospServiceImpl 的行为,也是让索引第三列
--      (del_flag, orgid, inhospstate) 真正等值命中的前提
-- 如果【还是慢】,剩下的时间必然在这两处之一:
--   (甲) 在院口径选错了列 —— endtime IS NULL 命中的行数远大于真实在院人数
--        (HIS 导入的历史数据 endtime 常年为空,这一条最容易被忽略)
--   (乙) 索引没建 / 建了但用不上(类型或字符集不一致,join 键被隐式转换)
--
-- 跑法:整段贴进 mysql 客户端。每个 SELECT 的结果里客户端会打印
--       "1 row in set (0.0xx sec)",那个秒数就是我要的。
-- 需要替换的占位符:<ORGID>(= 登录用户的 orgid)、<DEPT>、<DISTRICT>。
-- ============================================================================
 
 
-- ----------------------------------------------------------------------------
-- 0) 版本(EXPLAIN ANALYZE 需要 8.0.17+;5.7 就走第 4 节的普通 EXPLAIN)
-- ----------------------------------------------------------------------------
SELECT VERSION() AS mysql_version, @@sql_mode AS sql_mode;
 
 
-- ----------------------------------------------------------------------------
-- 1) 索引现状 + 关键列类型/字符集(决定"索引能不能被用上")
-- ----------------------------------------------------------------------------
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;
 
-- ⚠️ 重点看两行:
--    b.patid  与 a.id  的 COLUMN_TYPE / COLLATION_NAME 是否【完全一致】
--    不一致 = join 键被隐式转换 = pat_archive 的主键索引用不上(见第 5 节)
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLLATION_NAME, IS_NULLABLE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND ( (TABLE_NAME = 'pat_med_inhosp'
         AND COLUMN_NAME IN ('inhospid','patid','del_flag','orgid','campusid',
                             'inhospstate','endtime','starttime','bed_no',
                             'deptcode','hospitaldistrictcode'))
     OR (TABLE_NAME = 'pat_archive'
         AND COLUMN_NAME IN ('id','del_flag','idcardno','age','age_unit','telcode','sex')) )
ORDER BY TABLE_NAME, COLUMN_NAME;
 
SELECT TABLE_NAME, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024) AS data_mb,
       ROUND(INDEX_LENGTH/1024/1024) AS index_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('pat_med_inhosp', 'pat_archive');
 
 
-- ----------------------------------------------------------------------------
-- 2) ★最关键:在院到底有多少行(两种口径对比)
--    如果 in_by_endtime 远大于 in_by_state,说明 endtime IS NULL 不是"在院",
--    那再怎么加索引也没救 —— 必须换成 inhospstate 口径(代码一行)。
-- ----------------------------------------------------------------------------
SELECT
    COUNT(*)                                                              AS total_rows,
    SUM(del_flag = '0')                                                   AS alive_rows,
    SUM(del_flag = '0' AND endtime IS NULL)                               AS in_by_endtime,
    SUM(del_flag = '0' AND inhospstate = '0')                             AS in_by_state,
    SUM(del_flag = '0' AND endtime IS NULL AND IFNULL(inhospstate,'') <> '0') AS conflict_endtime_null_but_not_state0,
    SUM(del_flag = '0' AND inhospstate = '0' AND endtime IS NOT NULL)     AS conflict_state0_but_has_endtime
FROM pat_med_inhosp;
 
-- 换成"该机构"再看一遍(口径分布可能因机构而异)
SELECT
    COUNT(*)                                                    AS org_alive_rows,
    SUM(endtime IS NULL)                                        AS org_in_by_endtime,
    SUM(inhospstate = '0')                                      AS org_in_by_state,
    SUM(endtime IS NULL AND IFNULL(inhospstate,'') <> '0')      AS org_conflict
FROM pat_med_inhosp
WHERE del_flag = '0' AND orgid = '<ORGID>';
 
-- 再看"该机构 + 该院区 + 该科室"(就是这次入参的过滤组合)
SELECT
    COUNT(*)                                                    AS scope_alive_rows,
    SUM(endtime IS NULL)                                        AS scope_in_by_endtime,
    SUM(inhospstate = '0')                                      AS scope_in_by_state
FROM pat_med_inhosp
WHERE del_flag = '0' AND orgid = '<ORGID>'
  AND hospitaldistrictcode = '<DISTRICT>' AND deptcode = '<DEPT>';
 
 
-- ----------------------------------------------------------------------------
-- 3) 分步计时(一条一条跑,看秒数落在哪一步)
-- ----------------------------------------------------------------------------
 
-- 3a 只扫 pat_med_inhosp,不 join、不 group by:这是"取数"的下限【新口径】
SELECT COUNT(*) AS c
FROM pat_med_inhosp b
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0';
 
-- 3g ★对照:同一条,但去掉 inhospstate(= 改造前的口径)
--    3a 与 3g 的秒数差 = "补上 inhospstate 到底救回来多少"。
--    如果 3g 还是几秒而 3a 是毫秒级,说明病因就是它,到此为止;
SELECT COUNT(*) AS c
FROM pat_med_inhosp b
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL;
 
-- 3b 加上 join pat_archive(判 a.del_flag / a.idcardno):join 贵不贵
SELECT COUNT(*) AS c
FROM pat_med_inhosp b
JOIN pat_archive a ON a.id = b.patid
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL;
 
-- 3c 改造【前】的 count(60 列 + GROUP BY 整个包一层)—— 这就是被优化掉的那条
SELECT COUNT(0) AS c FROM (
  SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime
  FROM pat_med_inhosp b
  JOIN pat_archive a ON a.id = b.patid
  WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
    AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
    AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL
  GROUP BY b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex
) tmp_count;
 
-- 3d 数据查询【去掉 GROUP BY 和 ORDER BY】:纯取 20 行要多久
SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime
FROM pat_med_inhosp b
JOIN pat_archive a ON a.id = b.patid
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL
LIMIT 20;
 
-- 3e 数据查询【加 GROUP BY,不加 ORDER BY】:group by 的代价
SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime
FROM pat_med_inhosp b
JOIN pat_archive a ON a.id = b.patid
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL
GROUP BY b.inhospid, a.age, a.age_unit, a.telcode, a.id, a.sex
LIMIT 20;
 
-- 3f 现状全貌(= 接口真正执行的那条):再加 ORDER BY
SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime
FROM pat_med_inhosp b
JOIN pat_archive a ON a.id = b.patid
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL
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 20;
 
-- 读法:
--   3g 慢、3a 快      -> 病因就是"在院口径",本次补 inhospstate='0' 已解决,收工
--   3a 慢             -> 取数就走错路了(索引缺失 / 类型字符集不一致),看第 1、2、5 节
--   3a 快、3b 慢      -> join 有问题(patid/id 类型或字符集不一致,看第 5 节)
--   3b 快、3e 慢      -> GROUP BY 的代价;可以确认"GROUP BY 是空操作"后删掉
--   3e 快、3f 慢      -> ORDER BY 的 filesort 代价;可考虑让索引顺序覆盖排序
--   3c 明显慢于 3b    -> 说明这次的手写 count 改造确实省掉了大头
 
 
-- ----------------------------------------------------------------------------
-- 4) 执行计划(MySQL 5.7 用普通 EXPLAIN;8.0.17+ 直接用 EXPLAIN ANALYZE 看真实耗时)
-- ----------------------------------------------------------------------------
EXPLAIN
SELECT a.age, a.telcode, a.sex, b.inhospid, b.patid, b.deptcode, b.bed_no, b.starttime
FROM pat_med_inhosp b
JOIN pat_archive a ON a.id = b.patid
WHERE b.del_flag = '0' AND b.orgid = '<ORGID>'
  AND b.hospitaldistrictcode = '<DISTRICT>' AND b.deptcode = '<DEPT>'
  AND b.endtime IS NULL AND b.inhospstate = '0' AND a.del_flag = '0' AND a.idcardno IS NOT NULL
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 20;
 
-- 8.0.17+ 才有:直接给出每个算子的实际行数/耗时,比 EXPLAIN 有用得多
-- EXPLAIN ANALYZE
-- SELECT ... (同上面这条)
 
-- 看 EXPLAIN 时只盯 4 列:
--   table  : 第一行是不是 b(如果第一行是 a,说明优化器反着驱动,最坏情况)
--   type   : ALL / index = 全扫(问题);ref / range = 走索引(正常)
--   key    : 命中的索引名;NULL = 没用索引
--   rows   : 估算扫描行数(几万以上就是全机构历史,说明口径/索引仍不对)
 
 
-- ----------------------------------------------------------------------------
-- 5) join 键一致性(第 1 节看不明白就跑这条,直接给出"会不会隐式转换"的结论)
-- ----------------------------------------------------------------------------
SELECT
    b_col.COLUMN_TYPE   AS b_patid_type,
    b_col.COLLATION_NAME AS b_patid_collation,
    a_col.COLUMN_TYPE   AS a_id_type,
    a_col.COLLATION_NAME AS a_id_collation,
    CASE
        WHEN a_col.COLUMN_TYPE = b_col.COLUMN_TYPE
         AND IFNULL(a_col.COLLATION_NAME,'') = IFNULL(b_col.COLLATION_NAME,'')
        THEN '✅ 一致,join 可以用 a.id 主键'
        ELSE '❌ 不一致 -> join 键被隐式转换,pat_archive 可能被反复全扫'
    END AS verdict
FROM (SELECT COLUMN_TYPE, COLLATION_NAME FROM information_schema.COLUMNS
      WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pat_med_inhosp' AND COLUMN_NAME = 'patid') b_col
JOIN (SELECT COLUMN_TYPE, COLLATION_NAME FROM information_schema.COLUMNS
      WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pat_archive' AND COLUMN_NAME = 'id') a_col;
 
 
-- ============================================================================
-- 要贴回来的东西(如果补了 inhospstate 之后还是慢,按这个顺序,最省事的三项就够):
--   ① 第 3 节 3a 和 3g 的秒数(这两个一对比就知道病因是不是"在院口径")
--   ② 第 2 节第一组的四个数字(total_rows / in_by_endtime / in_by_state / conflict...)
--   ③ 第 4 节 EXPLAIN 的 table / type / key / rows 四列
--   ④ 需要时再补第 1 节索引清单与第 5 节的 join 键一致性结论
-- ============================================================================