4、数据库查询优化:索引优化、慢查询分析、连接池配置、读写分离
接口响应慢了,八成和数据库有关。这是后端优化的老规矩了。
说白了,你的业务逻辑再花哨,如果每次请求都要全表扫一遍,用户那边就是死等。我过去接手过一个报表系统,一个接口要跑十几秒,最后发现就是一条 SQL 没走索引。嗯,今天咱们就来聊聊怎么把这四块做好。
4.1 索引优化——别让数据库“硬扫”
为什么有些 SQL 就是慢?大部分情况是没走索引,或者走了错误的索引。数据库默认的 B+ 树索引,说白了就是一本电话簿的目录。你查一个人,按拼音翻目录快,还是从头翻到尾快?答案很明显。
4.1.1 索引选择的原则
我在项目中见过太多开发者,上来就给所有字段建索引。结果呢?写操作变慢了,索引文件比数据文件还大。这是典型的过度优化。
- 高选择性字段优先:比如用户 ID、订单号。性别这种字段,就两个值,索引意义不大。
- 复合索引最左前缀:比如 (a, b, c),查询条件里必须有 a 才能用到索引。b 或 c 单独查不走索引。
- 覆盖索引:如果查询的字段都在索引里,数据库就不需要回表了。速度会快很多。
EXPLAIN 看执行计划。如果 type 是 ALL,那就是全表扫描,必须优化。type 从好到差依次是:const > ref > range > index > ALL。
4.1.2 常见索引失效场景
你以为建了索引就万事大吉了?不一定。我曾经踩过一个坑:在查询时对索引列用了函数,比如 WHERE DATE(create_time) = '2024-01-01',结果索引完全没用到。
| 场景 | 示例 | 正确写法 |
|---|---|---|
| 对索引列使用函数 | WHERE DATE(create_time) = '2024-01-01' |
WHERE create_time >= '2024-01-01 00:00:00' |
| 隐式类型转换 | WHERE user_id = '123'(user_id 是数字) |
保持类型一致 |
| LIKE 以 % 开头 | WHERE name LIKE '%张三' |
避免,或用全文索引替代 |
| 复合索引未用最左列 | 索引 (a,b,c),查询只用到 b | 调整查询顺序或重建索引 |
4.2 慢查询分析——找到罪魁祸首
优化不能靠猜。你得知道哪些 SQL 确实慢。慢查询日志就是你的第一手证据。
4.2.1 开启慢查询日志
我个人习惯先把慢查询日志打开,设置一个合理的阈值。比如超过 1 秒就算慢。
-- 查看当前配置
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启并设置阈值(1秒)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
4.2.2 用 EXPLAIN 分析执行计划
拿到慢 SQL 之后,用 EXPLAIN 看它是怎么执行的。你问怎么看?我教你几个关键列:
- type:我之前说了,
ALL是灾难,eq_ref是好兄弟。 - rows:预估要扫描的行数。这个数字越大,SQL 越慢。
- Extra:看到
Using filesort或Using temporary就要警惕了。说明排序或分组没走索引。
rows 从 500 万降到了 200。原因就是加了一个联合索引,把 Using filesort 变成了 Using index。速度从 8 秒降到了 0.1 秒。
4.3 连接池配置——别让数据库“累死”
每次请求都新建一个数据库连接?那是开发新手干的事。连接池的存在,就是为了复用连接,减少三次握手的开销。
4.3.1 选择合适的连接池大小
很多人以为连接池越大越好。其实不是。数据库的并发处理能力是有限的。你想想看,1000 个连接同时压过来,CPU 全花在上下文切换上了。
# HikariCP 配置示例(Spring Boot)
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
| 参数 | 说明 | 建议值 |
|---|---|---|
| maximum-pool-size | 最大连接数 | 根据 CPU 核数 * 2 + 有效磁盘数 估算 |
| minimum-idle | 最小空闲连接 | 避免频繁创建,设为 5-10 |
| max-lifetime | 连接最大存活时间 | 小于数据库的 wait_timeout 即可 |
4.3.2 监控连接池状态
池子好不好用,得看指标。连接活跃数、等待数、超时次数。如果经常出现 ConnectionTimeoutException,要么池子太小,要么 SQL 执行太慢占着连接不放。
- 活跃连接数持续接近上限 → 考虑增加池大小或优化 SQL。
- 等待队列不断增长 → 检查是否有慢查询没释放资源。
4.4 读写分离——把压力分流
大部分系统都是读多写少。你不可能让读操作去抢写操作的时间片。我的建议是:主库负责写,从库负责读。这就叫读写分离。
4.4.1 实现方式
框架层面做路由。比如 ShardingSphere 或 MyCat,也可以自己在代码里配多个数据源。
// 简单的动态数据源路由思路(伪代码)
public class DataSourceRouter {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void setMaster() {
context.set("master");
}
public static void setSlave() {
context.set("slave");
}
public static String getCurrent() {
return context.get();
}
}
4.4.2 注意主从同步延迟
这里有个坑。我曾经遇到过,写完后立刻去读,结果从库还没同步数据,读到了旧数据。怎么处理?
- 强一致性场景:写完后强制走主库读。比如支付成功后查询订单状态。
- 最终一致性场景:读从库,接受短暂延迟。比如浏览历史记录、文章阅读量。
知识体系:数据库查询优化全流程
好,我们把上面四部分串起来。看下面这张图,会更清楚:
你看,这是一套组合拳。缺了哪一步,效果都会打折。好,今天就聊到这里。记住一句话:数据库优化,慢查是入口,索引是核心,连接池是保障,读写分离是升华。回去自己动手试试吧。