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 即可
提醒:我建议你压测后再决定连接池大小。别只看理论公式。实际场景中,数据库的瓶颈往往在磁盘 I/O 或锁上。

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 注意主从同步延迟

这里有个坑。我曾经遇到过,写完后立刻去读,结果从库还没同步数据,读到了旧数据。怎么处理?

  • 强一致性场景:写完后强制走主库读。比如支付成功后查询订单状态。
  • 最终一致性场景:读从库,接受短暂延迟。比如浏览历史记录、文章阅读量。
注意:不要所有查询都走从库。如果从库压力也很大,或者主从延迟超过 1 秒,你的接口依然会慢。合理分配,监控延迟曲线。

知识体系:数据库查询优化全流程

好,我们把上面四部分串起来。看下面这张图,会更清楚:

数据库查询优化核心流程 1. 慢查询分析 2. 索引优化 3. 连接池配置 4. 读写分离 5. 持续监控 & 调优 说明: 流程从慢查询分析入手,找到问题 SQL。然后针对性地做索引优化。 同时调整连接池配置、实施读写分离。最后通过监控验证效果,形成闭环。

你看,这是一套组合拳。缺了哪一步,效果都会打折。好,今天就聊到这里。记住一句话:数据库优化,慢查是入口,索引是核心,连接池是保障,读写分离是升华。回去自己动手试试吧。