assets/2026-07-01-医数智联系列开篇-医院数字化工作者的工具箱_2026-07-01_18-28-11.jpg

医数智联·第 13 期:SQL 性能优化 —— 让首页查询从 30 秒到 0.5 秒,EXPLAIN + 索引 + 重写

[!ABSTRACT] 核心摘要

  • 本期主题:SQL 性能优化——同样的查询,从 30 秒降到 0.5 秒
  • 核心结论:90% 的慢 SQL 都是「全表扫描」,只要找到「扫描原因」就能提速
  • 3 大核心工具:EXPLAIN(看执行计划)+ 索引(加速查询)+ 查询重写(避免反模式)
  • 5 大实战:单列索引、联合索引、覆盖索引、避免 SELECT *、EXPLAIN 解读
  • 典型效果:从「30 秒超时」到「0.5 秒返回」
  • 阅读时长:约 10 分钟

一、场景引入:30 秒的 SQL 是怎样的体验?

[!example] 一个真实的周三上午
院长:「上月骨科主诊错误率 Top 10 医生,马上给我。」

质管办小张跑 SQL:

1
2
3
4
5
6
7
8
SELECT a.责任医生, COUNT(*) AS 错误数
FROM 首页主表 a
LEFT JOIN 医生表 b ON a.责任医生工号 = b.医生工号
WHERE a.主诊错误标记 = 1
AND a.出院日期 >= '2026-06-01'
GROUP BY a.责任医生
ORDER BY 错误数 DESC
LIMIT 10;

跑了 30 秒没结果,Navicat 提示「查询超时」。

小张心急如焚,又不敢重启。

信息科同事路过,扫了一眼他的 SQL:

「你这表没建索引,where 里的字段全表扫描,500 万行当然慢。」

加了一个索引:

1
CREATE INDEX idx_出院日期_错误 ON 首页主表 (出院日期, 主诊错误标记);

0.5 秒返回结果。

这是医院慢 SQL 的典型故事。

[!quote] 狼叔的判断
“慢 SQL 不是程序员的锅,是’不会看 EXPLAIN’的代价。” 这一期,带你掌握 3 个核心技能——EXPLAIN、索引、查询重写,让 30 秒变 0.5 秒


二、工具原理:慢 SQL 的 3 大「罪魁祸首」

2.1 罪魁祸首 ①:全表扫描(Full Table Scan)

[!info] 全表扫描是什么?
数据库在执行查询时,逐行扫描整张表,每行都判断是否符合 WHERE 条件。

对于 500 万行的表,全表扫描要 30 秒+。

判断方法:EXPLAIN 结果里看到 type = ALL(MySQL) 或 Seq Scan(PostgreSQL)。

2.2 罪魁祸首 ②:索引失效

[!info] 索引失效是什么?
表上有索引,但查询写法让数据库用不上索引,退化成全表扫描。

常见原因:

  • 对索引列做函数运算:WHERE YEAR(出院日期) = 2026
  • 对索引列做类型转换:WHERE 病案号 = 123(病案号是 VARCHAR)
  • !=NOT IN:WHERE 状态 != '已处理'
  • LIKE '%xxx%'(前导模糊):WHERE 主诉 LIKE '%糖尿病%'
  • 复合索引不遵守最左前缀(详见 3.2)

2.3 罪魁祸首 ③:数据量过大

[!info] 数据量过大的表现

  • 单表超过 1000 万行(即使有索引,也会变慢)
  • JOIN 超过 3 张表(笛卡尔积爆炸)
  • SELECT *(返回不必要的列)
  • 排序字段无索引(ORDER BY 全表排序)

三、医院实战:5 大优化技巧

3.1 技巧一:EXPLAIN 看执行计划

[!example] 实战 ①:EXPLAIN 分析
EXPLAIN 是 SQL 优化的「听诊器」,告诉你数据库是怎么执行这条 SQL 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
-- MySQL 写法
EXPLAIN
SELECT a.责任医生, COUNT(*) AS 错误数
FROM 首页主表 a
WHERE a.主诊错误标记 = 1
AND a.出院日期 >= '2026-06-01'
GROUP BY a.责任医生
ORDER BY COUNT(*) DESC
LIMIT 10;

-- PostgreSQL 写法(更详细)
EXPLAIN ANALYZE
SELECT ...

MySQL EXPLAIN 输出关键字段:

字段 含义 优化目标
type 连接类型 const > eq_ref > ref > range > index > ALL(全表扫描,要避免)
key 实际使用的索引 NULL = 没用到索引
rows 扫描行数(估算) 越小越好
Extra 额外信息 看到 Using filesort / Using temporary 通常要优化

PostgreSQL EXPLAIN 输出关键字段:

字段 含义 优化目标
Seq Scan 全表扫描 改为 Index Scan
Index Scan 索引扫描 理想
Bitmap Index Scan 位图索引扫描 多条件时常用
Sort 排序 尽量用索引排序
Hash Join / Nested Loop 连接方式 小表用 Nested Loop

3.2 技巧二:单列索引 vs 联合索引

[!example] 实战 ②:索引类型选择

3.2.1 单列索引

1
2
3
-- 适合:查询只用一个条件
CREATE INDEX idx_出院日期 ON 首页主表 (出院日期);
CREATE INDEX idx_主诊错误 ON 首页主表 (主诊错误标记);

3.2.2 联合索引(复合索引)

1
2
3
4
5
6
7
-- 适合:查询同时用多个条件
-- 联合索引的「最左前缀」原则:
-- WHERE a = 1 AND b = 2 AND c = 3 → 走索引
-- WHERE a = 1 → 走索引(只用前缀)
-- WHERE b = 2 → 不走索引(没用到 a)

CREATE INDEX idx_科室_错误_日期 ON 首页主表 (科室, 主诊错误标记, 出院日期);

3.2.3 覆盖索引(包含 SELECT 字段)

1
2
-- 如果查询只需要「责任医生」和「错误数」,索引直接包含这两个字段,不用回表
CREATE INDEX idx_覆盖_查询 ON 首页主表 (科室, 主诊错误标记, 出院日期, 责任医生);

[!TIP] 索引选择 3 原则

  1. 高频查询的 WHERE 字段建索引
  2. 高频 JOIN 的关联字段建索引
  3. 不要建太多索引(每多一个索引,INSERT/UPDATE 变慢 5-10%)

3.3 技巧三:避免 SELECT *

[!example] 实战 ③:避免 SELECT *

1
2
3
4
5
6
7
-- ❌ 错误:SELECT * 返回所有列,可能几 MB,慢
SELECT * FROM 首页主表 WHERE 出院日期 >= '2026-06-01';

-- ✅ 正确:只 SELECT 需要的列,几十 KB,快 10 倍
SELECT 病案号, 出院日期, 科室, 主诊ICD
FROM 首页主表
WHERE 出院日期 >= '2026-06-01';

3.4 技巧四:重写慢 SQL

[!example] 实战 ④:重写慢 SQL 的 5 种模式

模式 ①:对索引列避免函数

1
2
3
4
5
6
7
-- ❌ 慢:对索引列用函数,索引失效
SELECT * FROM 首页主表
WHERE YEAR(出院日期) = 2026 AND MONTH(出院日期) = 6;

-- ✅ 快:范围查询,走索引
SELECT * FROM 首页主表
WHERE 出院日期 >= '2026-06-01' AND 出院日期 < '2026-07-01';

模式 ②:对索引列避免类型转换

1
2
3
4
5
-- ❌ 慢:病案号是 VARCHAR,传入数字,索引失效
SELECT * FROM 首页主表 WHERE 病案号 = 123;

-- ✅ 快:传字符串
SELECT * FROM 首页主表 WHERE 病案号 = '123';

模式 ③:用 EXISTS 替代 IN

1
2
3
4
5
6
7
8
9
10
-- 慢:大数据量 IN 慢
SELECT * FROM 首页主表
WHERE 责任医生工号 IN (SELECT 医生工号 FROM 医生表 WHERE 科室 = '骨科');

-- 快:大数据量 EXISTS 更快
SELECT * FROM 首页主表 a
WHERE EXISTS (
SELECT 1 FROM 医生表 b
WHERE b.医生工号 = a.责任医生工号 AND b.科室 = '骨科'
);

模式 ④:用 JOIN 替代子查询

1
2
3
4
5
6
7
8
-- 慢:相关子查询
SELECT a.*, (SELECT 医生姓名 FROM 医生表 b WHERE b.医生工号 = a.责任医生工号) AS 姓名
FROM 首页主表 a;

-- 快:JOIN 一次性关联
SELECT a.*, b.医生姓名 AS 姓名
FROM 首页主表 a
LEFT JOIN 医生表 b ON a.责任医生工号 = b.医生工号;

模式 ⑤:小表驱动大表(Nested Loop)

1
2
3
4
5
-- ✅ 正确:小表(医生表)驱动大表(首页主表)
SELECT *
FROM 首页主表 a
INNER JOIN 医生表 b ON a.责任医生工号 = b.医生工号
WHERE b.科室 = '骨科';

3.5 技巧五:分页查询优化

[!example] 实战 ⑤:深分页优化
场景:LIMIT 100000, 20 —— 跳过 10 万行取 20 行,慢到爆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- ❌ 慢:LIMIT 100000, 20 仍然要扫描前 10 万行
SELECT * FROM 首页主表
ORDER BY 出院日期 DESC
LIMIT 100000, 20;

-- ✅ 快 ①:子查询 + ID 限定
SELECT * FROM 首页主表
WHERE 出院日期 < (
SELECT 出院日期 FROM 首页主表
ORDER BY 出院日期 DESC
LIMIT 100000, 1
)
ORDER BY 出院日期 DESC
LIMIT 20;

-- ✅ 快 ②:用主键 ID 限定(最快)
SELECT * FROM 首页主表
WHERE 主键ID > 100020 -- 假设上一页最后 ID 是 100020
ORDER BY 主键ID ASC
LIMIT 20;

四、避坑指南:5 个让索引「失效」的常见错误

[!warning] 雷区 ①:用 != / <> / NOT IN
症状:WHERE 状态 != '已处理' 全表扫描。
解决:=IN;或者考虑 WHERE 状态 IN ('待处理', '处理中')

[!warning] 雷区 ②:LIKE '%关键词%'(前导模糊)
症状:WHERE 主诉 LIKE '%糖尿病%' 全表扫描。
解决:用后置模糊:WHERE 主诉 LIKE '糖尿病%'(走索引)。

[!warning] 雷区 ③:对索引列做运算
症状:WHERE DATE(出院日期) = '2026-06-15' 索引失效。
解决:用范围查询:WHERE 出院日期 >= '2026-06-15' AND 出院日期 &lt; '2026-06-16'

[!warning] 雷区 ④:联合索引不遵守最左前缀
症状:联合索引 (科室, 出院日期),查询 WHERE 出院日期 = '2026-06-15' 不走索引。
解决:查询条件顺序要和索引一致,或者调整索引顺序。

[!warning] 雷区 ⑤:OR 让索引失效
症状:WHERE 科室 = '骨科' OR 主诊ICD = 'I10' 全表扫描。
解决:UNION ALL:

1
2
3
SELECT * FROM 首页主表 WHERE 科室 = '骨科'
UNION ALL
SELECT * FROM 首页主表 WHERE 主诊ICD = 'I10';

五、SQL 性能优化清单(实战模板)

[!TIP] 上线前的 5 步自查

Step 1:用 EXPLAIN 看执行计划

  • 是否有 ALL / Seq Scan?
  • rows 估算是否过大?

Step 2:检查 WHERE 字段是否建索引

  • 高频查询字段有没有索引?
  • 联合索引顺序是否正确?

Step 3:检查是否有 SELECT * / 函数 / 类型转换

Step 4:检查表数据量

  • 超过 1000 万行?考虑分区表或归档

Step 5:跑一遍实际耗时

  • 5 秒内:可接受
  • 5-30 秒:需优化
  • 30 秒:必须优化


六、模块总结:数据库 7 期学完了,你获得了什么?

[!ABSTRACT] 数据库模块 7 期回顾

期号 主题 核心技能
07 MySQL 基础 10 个 SQL:SELECT/WHERE/GROUP BY/HAVING/LIMIT/INSERT/UPDATE/DELETE
08 MySQL 进阶 JOIN/子查询/窗口函数,5 大常用查询
09 首页主诊错误率 完整分析 5 步流程
10 DRG 入组异常 5 类异常筛查 SQL
11 PostgreSQL 入门 JSONB + 全文检索 + GIS
12 PostgreSQL 进阶 GIN 索引 + zhparser + PostGIS
13 SQL 性能优化 EXPLAIN + 索引 + 查询重写

学完这 7 期,你能独立完成:

  • 80% 的医院日常数据查询
  • 首页质控、DRG 入组的完整 SQL 报告
  • 用 EXPLAIN 排查慢 SQL
  • PostgreSQL 科研病历库搭建

下个模块预告:Python 自动化(共 8 期,见开篇规划)


[!TIP] 互动
如果你觉得这篇「医数智联·第 13 期」对你有用,请点赞、在看、转发给科里的同事。

文中 EXPLAIN 解读手册 + 索引设计清单 + SQL 重写模板,我已经整理成「SQL 性能优化工具包」。点击「阅读原文」可直接下载

我是白衣狼,一个用数字化工具把医院管理做「轻」的实战派。咱们下期见。


[!quote] 数据库模块结语
“SQL 是医院数据的’普通话’——会 SQL,你就能和任何医院系统’对话’。”

MySQL + PostgreSQL 双栈并存,既能跑业务(HIS 用 MySQL),又能做科研(PostgreSQL),这就是 2026 年医院数据人的标配

下一模块 Python,我们把 SQL 的结果「自动化」起来——自动报表、自动校验、自动发邮件。敬请期待。