人人都会AI编程

21.2 数据库性能优化:索引设计、SQL 优化、连接池调优

更新时间:2026-07-10

数据库是大多数企业应用的核心,也是性能问题最密集的一层。一个慢查询可能拖垮整个服务,而连接池配置不当则会让系统在流量高峰时完全不可用。本节围绕索引设计、SQL 优化和连接池调优三个最关键的环节,提供可直接落地的实践方案。

21.2.1 索引设计:让查询“看见”你的数据

1. 索引的基础原理

数据库索引类似书籍的目录,本质是一种有序的数据结构(InnoDB 中为 B+ 树)。它允许数据库引擎在 O(log n) 时间内定位到目标行,而不必全表扫描。但索引不是免费的:它占用磁盘空间,并会降低写入性能(INSERT/UPDATE/DELETE 时需要同时更新索引)。因此,索引设计永远是读与写的权衡

2. 常见索引类型与适用场景

  • 主键索引(聚簇索引):InnoDB 中的数据按主键顺序物理存储。主键应尽可能短且有序,推荐使用自增 ID 或有序 UUID,避免随机 UUID 导致页分裂。
  • 普通索引(二级索引):允许重复值,是最常用的查询加速手段。
  • 唯一索引:保证列值唯一,同时具备加速查询的功能,适合身份证号、邮箱等字段。
  • 联合索引:多列组合建立索引,可支持复杂条件的查询。最左前缀原则是关键——索引 (a,b,c) 能加速 a=?a=? AND b=?a=? AND b=? AND c=? 的查询,但无法单独靠 b 或 c 高效过滤。
  • 覆盖索引:查询所需的所有列都包含在索引中,无需回表读取完整数据行,性能提升极为明显。例如为 SELECT name, age FROM user WHERE email=? 建立 (email, name, age) 联合索引。

3. 索引设计的实用原则

  • 高频查询字段优先建索引:分析业务 SQL,给 WHERE、JOIN、ORDER BY、GROUP BY 中频繁出现的列加索引。
  • 选择区分度高的列:索引字段的基数(cardinality)越大,过滤效果越好。不要在性别、状态这种仅有几个值的字段上建单列索引。
  • 避免冗余和重复索引(a)(a, b) 是冗余的,前者可被后者覆盖;完全相同的索引更是浪费。
  • 控制索引数量:单表索引一般不建议超过 5-6 个。每个新增索引都会拖累写入并增加优化器选择负担。
  • 长字段使用前缀索引:对于 VARCHAR/TEXT 大字段,可只对前 N 个字符建索引,如 INDEX (description(100)),需确保前缀足够区分多数数据。
  • 定期分析并清理无用索引:利用 sys.schema_unused_indexes(MySQL 8.0+)或慢查询日志定位从未使用或低效的索引。

4. 真实案例:订单列表查询优化

慢查询:

SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID' 
ORDER BY create_time DESC LIMIT 10;

无合适索引时,数据库扫描数十万行。建立联合索引 (user_id, status, create_time) 后,查询可以按索引排序直接取前 10 条,耗时从数秒降至毫秒级。若将 SELECT * 改为只取必要列并用覆盖索引 (user_id, status, create_time, amount) 进一步优化,可连回表都省掉。

21.2.2 SQL 优化:写出高效的数据请求

1. 必备工具:EXPLAIN 解读

在任何优化前,先用 EXPLAIN(或 EXPLAIN ANALYZE 在 MySQL 8.0.18+)分析查询执行计划。关注的关键字段:

  • type:连接类型,从优到差依次为 consteq_refrefrangeindexALL。至少应达到 range 级别,出现 ALL(全表扫描)需立即优化。
  • key:实际使用的索引,若为 NULL 说明未走索引。
  • rows:预估扫描行数,越小越好。
  • ExtraUsing index 表示覆盖索引,性能良好;Using filesort 表示需要额外排序,可考虑索引优化;Using temporary 表示用到临时表,通常需要改写 SQL 或增加索引。

2. 常见 SQL 优化规则

  • 避免 SELECT *:只查询需要的列,减少数据传输量,也更易利用覆盖索引。
  • 警惕隐式类型转换WHERE phone = 13800138000 如果 phone 是 VARCHAR 类型,会导致索引失效。传入参数类型必须与字段一致。
  • 负向条件慎用!=NOT INNOT EXISTS 常导致全表扫描。尝试用正向条件改写或结合业务逻辑避免。
  • LIKE 前置模糊查询LIKE '%keyword' 无法使用索引。如果必须做模糊搜索,考虑使用全文索引(FULLTEXT)或 Elasticsearch 等专用搜索引擎。
  • JOIN 优化:小表驱动大表,确保 JOIN 的关联列上有索引。避免超过三个表的 JOIN,复杂关联可拆分成多次查询在应用层组装。
  • 子查询优化:尽量将子查询改写为 JOIN,现代优化器有时能自动转换,但手写 JOIN 更具可控性。
  • 合理分页LIMIT 100000,10 会扫描并丢弃前 10 万行,性能极差。改进方法:
  • 使用游标分页:记录上一页最后一条记录的 ID,用 WHERE id > last_id LIMIT 10 加速。
  • 或采用延迟关联:先通过覆盖索引查出主键,再关联原表获取所需列。
  • 批量操作代替循环:多次单条 INSERT 会带来大量网络和事务开销,使用批量插入(一次插入多行)可提升数十倍性能。MyBatis 的 foreach 或 JPA 的 saveAll 针对少量数据可行,大批量建议直接使用 JDBC 批量或 LOAD DATA

3. 真实优化示例:统计报表查询重构

原始统计 SQL:

SELECT DATE(create_time), COUNT(*) FROM large_table 
WHERE status = 'FINISHED' GROUP BY DATE(create_time);

即使 status 有索引,全表分组计算依然很慢。优化手段:增加一个报表汇总表,通过定时任务或触发器每小时累加数据,查询直接读取汇总表,性能提升 100 倍以上。该思路体现了 以空间换时间业务预聚合 的常见优化策略。

21.2.3 连接池调优:把控数据库的“命脉”

1. 为什么需要连接池

数据库连接创建昂贵(TCP 握手、认证、线程分配)。应用中每次请求都新建连接再关闭,既低效又容易耗尽数据库资源。连接池复用一定数量的持久连接,让高频访问成为可能。Spring Boot 中默认集成 HikariCP,它是目前 Java 生态中性能最好的连接池。

2. HikariCP 关键参数调优

调整连接池参数前,务必先理解:池子大小并非越大越好。过多连接会导致数据库 CPU、内存竞争,上下文切换开销增加,反而降低吞吐。一个经验公式:

池大小 ≈ ((物理核数 × 2) + 有效磁盘数)

对于多数 Web 应用,10~30 个连接是合理起点。

核心配置项(application.yml):

spring:
  datasource:
    hikari:
      # 连接池最大连接数,默认 10
      maximum-pool-size: 20
      # 最小空闲连接数,默认与 maximum-pool-size 相同
      minimum-idle: 5
      # 连接超时时间(毫秒),默认 30000。超时未获取到连接则抛异常
      connection-timeout: 30000
      # 空闲连接最大存活时间,超过则会被释放
      idle-timeout: 600000
      # 连接最大生命周期,应比数据库的 wait_timeout 短几分钟,防止使用已断开的连接
      max-lifetime: 1800000
      # 连接测试查询(MySQL 8 驱动已不需要设置,会自动使用 ping)
      connection-test-query: SELECT 1

调优步骤

  1. 监控应用的实际并发请求量和数据库的 Threads_connected 指标。
  2. 压力测试时逐步增加 maximum-pool-size,观察 TPS 和响应时间。当吞吐不再增长甚至开始下降时,即为当前硬件下的最优池大小。
  3. 确认 max-lifetime 小于数据库 wait_timeout,避免半开连接错误。
  4. 针对连接泄漏保护,可设置 leak-detection-threshold(如 10000 毫秒),开发阶段快速发现未关闭的连接。

3. 连接池常见问题定位

  • 连接不够用:日志中出现 Connection is not available, request timed out after 30000ms。先检查是否有慢查询占用了连接长时间不归还;若无,再考虑增大池子或扩容数据库。
  • 连接泄漏:应用获取连接后未正确关闭(通常在 finally 中归还)。使用 Spring 的 @TransactionalJdbcTemplate 会自动管理连接,泄漏多见于手动从 DataSource.getConnection() 获取的场景。
  • 数据库超时断开:报 CommunicationsException: Communications link failure。检查 max-lifetime 是否超过数据库 wait_timeout,以及 MySQL 的 wait_timeout 是否过小(通常设为 8 小时即可)。

4. 验证调优效果

在 Spring Boot 中,HikariCP 的运行时状态可通过 /actuator/health 和自定义 Metrics 暴露。配置暴露后,可以在 Prometheus + Grafana 中监控 hikaricp_connections_activehikaricp_connections_pending 等指标,观察连接池的压力和等待情况,持续优化。

总结

数据库性能优化的核心在于让数据尽可能快地找到并返回,而且不给系统带来额外负担。索引设计从数据结构层面加速检索,SQL 优化确保每一次查询都高效,连接池调优则保障应用与数据库之间的通道顺畅。三项工作互为支撑,缺一不可。优先从 SQL 和索引入手(投入产出比最高),再配合连接池参数打磨,大部分性能问题都能迎刃而解。