数据库层面优化:索引优化策略、SQL语句重构、读写分离架构、分库分表方案、连接池参数调优

各位同学,咱们直接进入正题。数据库优化,说白了就是跟慢查询死磕。我做了十几年高并发系统,见过太多系统被数据库拖垮的案例。今天这一章,我把压箱底的经验掏出来,咱们一个一个说。

一、索引优化策略

索引这东西,用好了是神器,用不好就是累赘。我个人习惯,上线前必做一次索引评审。

1. 复合索引的字段顺序

这是最容易被忽视的点。你想想看,复合索引就像一本电话簿——先按姓氏排,再按名字排。如果你查“叫张三的人”,姓氏是精确匹配,名字是范围查询,那索引效率就高。反过来,如果你跳过姓氏直接查名字,索引就废了。

核心原则:等值条件放前面,范围条件放后面。

-- 错误示范
CREATE INDEX idx_status_time ON orders(status, create_time);
-- 查询:WHERE status = 1 AND create_time > '2024-01-01'
-- 这样写没问题,但如果查询是 WHERE create_time > '2024-01-01',索引就失效了

-- 正确做法
-- 根据实际查询模式,把最常用的等值条件放前面

2. 覆盖索引

我在项目中遇到过这样一个场景:一个查询要返回10个字段,但索引只覆盖了3个。结果每次查询都要回表,性能直接掉了80%。

解决办法?把查询需要的字段都塞进索引里。这叫覆盖索引,查询时不需要回表,速度飞快。

-- 假设查询经常需要:user_id, order_id, amount, status
-- 创建覆盖索引
CREATE INDEX idx_cover ON orders(user_id, order_id, amount, status);
-- 这样查询时,索引本身就能提供所有数据,无需回表

小技巧:用 EXPLAIN 看 Extra 列,如果显示 "Using index",说明用上了覆盖索引。如果显示 "Using index condition",说明还有回表操作。

3. 索引下推

MySQL 5.6 引入的特性,很多人不知道。简单说,就是存储引擎在索引层面先过滤掉不符合条件的行,减少回表次数。

嗯,这里要注意:索引下推只对二级索引有效。我见过有人建了索引,但查询还是很慢,一查发现是没开启索引下推。默认是开启的,但有些老版本或者特殊配置会关掉。

-- 检查是否开启
SHOW VARIABLES LIKE 'optimizer_switch';
-- 找到 index_condition_pushdown=on 就是开启了

二、SQL语句重构

SQL写得好不好,直接决定数据库的生死。我见过最离谱的,一个查询跑了30秒,重构后变成0.1秒。

1. 避免SELECT *

这个我说过无数次了。你想想看,你只需要两个字段,却把整行数据都拉回来,网络传输、内存占用都是浪费。

-- 错误
SELECT * FROM users WHERE id = 100;

-- 正确
SELECT id, name, email FROM users WHERE id = 100;

2. 分页优化

传统的 LIMIT OFFSET 分页,越往后越慢。为什么?因为数据库要扫描前面所有的行,然后扔掉。

我曾经优化过一个后台报表,翻到第100页要等5秒。改成游标分页后,秒开。

-- 传统分页(越往后越慢)
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 100000;

-- 游标分页(推荐)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;

3. 避免隐式类型转换

这个坑我踩过。字段是 VARCHAR 类型,传参传了个数字,MySQL 会做隐式转换,索引就失效了。

-- 错误(phone是varchar类型)
SELECT * FROM users WHERE phone = 13800138000;

-- 正确
SELECT * FROM users WHERE phone = '13800138000';

避坑指南:我曾经因为这个问题,导致一个核心查询从毫秒级变成秒级。排查了半天,最后发现是传参类型不对。从那以后,我要求团队所有SQL参数必须显式指定类型。

三、读写分离架构

高并发系统,读写分离是标配。说白了,就是把写操作(INSERT/UPDATE/DELETE)发给主库,读操作(SELECT)发给从库。

1. 主从延迟问题

这是读写分离最大的坑。你刚写完数据,立刻去读,结果从库还没同步过来,读到的还是旧数据。

我个人的解决方案:

  • 强制读主库:对于实时性要求高的场景,比如支付结果查询,直接读主库
  • 延迟容忍:对于报表、统计类查询,可以接受几秒的延迟
  • 缓存兜底:用Redis缓存刚写入的数据,避免直接读从库

2. 负载均衡策略

从库多了,怎么分配读请求?轮询?随机?最少连接?

我建议用最少连接数策略。因为每个从库的负载不一样,轮询可能导致某些从库过载。

-- 以ProxySQL为例
-- 配置查询路由规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT', 1, 1);
-- 这样所有SELECT请求都路由到从库组(hostgroup 1)

四、分库分表方案

当单表数据量超过千万,或者QPS超过几千,就该考虑分库分表了。

1. 垂直分库

按业务模块拆分。比如用户库、订单库、商品库。这个比较简单,但要注意跨库事务的问题。

2. 水平分表

按某个字段取模分片。最常见的是按用户ID分片。

-- 分片规则:user_id % 16
-- 查询时带上分片键
SELECT * FROM orders_8 WHERE user_id = 12345;
-- 如果不带分片键,就要全库扫描,性能极差

核心原则:分片键必须出现在查询条件中,否则就是灾难。

3. 分库分表中间件

我推荐 ShardingSphere 或 MyCat。ShardingSphere 更轻量,支持多种分片策略。

-- ShardingSphere 配置示例
sharding:
  tables:
    orders:
      actualDataNodes: ds$->{0..1}.orders_$->{0..15}
      tableStrategy:
        inline:
          shardingColumn: user_id
          algorithmExpression: orders_$->{user_id % 16}

五、连接池参数调优

连接池是数据库的第一道防线。参数调不好,再好的SQL也白搭。

1. 核心参数

参数 说明 推荐值
maximumPoolSize 最大连接数 CPU核心数 * 2 + 1
minimumIdle 最小空闲连接 maximumPoolSize的10%-20%
connectionTimeout 连接超时时间 30000ms(30秒)
idleTimeout 空闲连接超时 600000ms(10分钟)
maxLifetime 连接最大存活时间 1800000ms(30分钟)

2. 连接数不是越多越好

很多人以为连接数越大越好,其实不然。连接数过多,数据库上下文切换开销巨大,反而降低性能。

我记得有一次,一个团队把连接池设成200,结果数据库CPU直接飙到100%。我让他们降到50,性能反而提升了30%。

调优建议:从CPU核心数 * 2开始,逐步增加,观察数据库的活跃连接数和响应时间。找到拐点,那就是最佳值。

3. 连接池监控

HikariCP 提供了丰富的监控指标。我习惯在监控面板上加上这几个指标:

  • 活跃连接数
  • 等待获取连接的线程数
  • 连接获取平均耗时
  • 连接超时次数

一旦发现等待线程数持续增加,或者连接获取耗时变长,就要考虑扩容或者优化SQL了。

// Spring Boot 配置 HikariCP
spring:
  datasource:
    hikari:
      maximum-pool-size: 50
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: MyPool
      # 开启监控
      metrics-enabled: true

好了,数据库层面的优化就讲到这里。下一章咱们聊聊缓存策略,那可是高并发系统的另一大杀器。