3、数据库优化实战:索引优化(复合索引、覆盖索引)、查询优化(避免SELECT *、分页查询)、连接池配置

各位同行,咱们直接切入正题。SCADA系统跑得慢,十有八九是数据库在拖后腿。风场的数据量有多大?一台风机一天就能产生几万条记录,一个风场几十台风机,一年下来数据量轻松上亿。你想想看,如果查询还像查小表那样写,不卡才怪。

这一章,我就把压箱底的数据库优化三板斧掏出来。索引怎么建、查询怎么写、连接池怎么配,咱们一个一个说清楚。

3.1 索引优化:别让数据库做全表扫描

索引这东西,说白了就是书的目录。没有目录,你要找「偏航角」这个数据,就得从第一页翻到最后一页。这就是全表扫描,慢得让人抓狂。

但索引也不是越多越好。我见过一个项目,运维兄弟给每个字段都加了索引,结果写入速度慢得像蜗牛。为什么?因为每次插入数据,数据库都要更新所有索引,开销巨大。

3.1.1 复合索引:多条件查询的利器

风场最常见的查询是什么?「查某台风机、某段时间、某个参数」。比如:

SELECT timestamp, wind_speed, power 
FROM scada_data 
WHERE turbine_id = 'T01' 
  AND timestamp BETWEEN '2024-01-01' AND '2024-01-02';

如果只给 turbine_id 建索引,数据库会先找到 T01 的所有记录,然后再逐条过滤时间范围。如果 T01 有100万条记录,这一步就够呛。

正确的做法是建一个复合索引:

CREATE INDEX idx_turbine_time ON scada_data(turbine_id, timestamp);

这里有个关键点:最左前缀原则。索引的字段顺序很重要。我把 turbine_id 放前面,timestamp 放后面。这样查询时,数据库能先定位到 T01,再在 T01 的数据范围内快速找到时间区间。

核心原则:复合索引中,等值条件(=)的字段放前面,范围条件(BETWEEN、>、<)的字段放后面。

我在项目中遇到过一个问题:有个同事把索引建成了 (timestamp, turbine_id)。结果查询时,数据库先按时间范围扫了一大片数据,再过滤风机ID,效率反而更差。嗯,顺序搞反了,效果天差地别。

3.1.2 覆盖索引:让查询飞起来

什么叫覆盖索引?简单说,就是查询需要的所有字段,都在索引里。数据库不用回表查原始数据,直接从索引里拿结果。

举个例子,我们经常查风机的实时功率:

SELECT turbine_id, timestamp, power 
FROM scada_data 
WHERE turbine_id = 'T05' 
  AND timestamp = '2024-06-01 12:00:00';

如果索引只包含 turbine_idtimestamp,数据库查到索引后,还得根据主键去数据文件里拿 power 字段。这叫「回表」,多了一次IO操作。

优化方案:把 power 也加到索引里。

CREATE INDEX idx_cover ON scada_data(turbine_id, timestamp, power);

这样,索引本身已经包含了所有需要的数据。数据库直接扫描索引就完事了,速度能快一个数量级。

避坑指南:我曾经在一个高频查询场景里用了覆盖索引,查询时间从800ms降到了15ms。但要注意,覆盖索引会增加存储空间。如果表有几十个字段,别一股脑全塞进去。只覆盖最频繁的查询即可。

3.2 查询优化:写SQL的坏习惯得改

很多运维兄弟写SQL很随意,觉得能跑就行。但在大数据量下,一个坏习惯就能让数据库崩溃。我见过最典型的问题有两个。

3.2.1 别再用 SELECT * 了

你想想看,SCADA表里可能有几十个字段:风速、功率、桨距角、偏航角、温度、振动... 如果你写 SELECT *,数据库就得把所有字段都读出来。哪怕你只需要两个字段,它也得把整行数据从磁盘搬到内存。

更糟糕的是,如果表结构变了(比如新增了一个大字段),SELECT * 会无差别地把新字段也拉出来,查询时间直接翻倍。

正确的做法:

-- 错误
SELECT * FROM scada_data WHERE turbine_id = 'T03';

-- 正确
SELECT turbine_id, timestamp, wind_speed, power 
FROM scada_data 
WHERE turbine_id = 'T03';

只取你需要的字段。这不仅是规范问题,更是性能问题。

3.2.2 分页查询:别用 LIMIT OFFSET

风场SCADA系统经常要做历史数据查询,比如「查看T01风机过去一年的数据,每页显示100条」。很多人的第一反应是:

SELECT * FROM scada_data 
WHERE turbine_id = 'T01' 
ORDER BY timestamp 
LIMIT 100 OFFSET 10000;

这个写法有个致命问题:OFFSET 10000 意味着数据库要先扫描前10000条记录,然后扔掉,只返回后面的100条。你想想看,如果用户翻到第100页,数据库就要扫描100万条记录再扔掉。这不是浪费是什么?

我建议用「游标分页」或者「键集分页」:

-- 记住上一页最后一条记录的时间戳
SELECT turbine_id, timestamp, wind_speed, power 
FROM scada_data 
WHERE turbine_id = 'T01' 
  AND timestamp > '2024-05-15 12:00:00'  -- 上一页最后一条的时间
ORDER BY timestamp 
LIMIT 100;

这种方式不需要扫描无用的数据,直接定位到目标位置。性能提升非常明显。

注意:游标分页要求排序字段是唯一的。如果时间戳有重复,可以加上主键作为辅助排序条件,比如 ORDER BY timestamp, id

3.3 连接池配置:别让数据库连接成为瓶颈

SCADA系统通常有多个采集服务、多个Web应用同时访问数据库。如果每个请求都新建一个数据库连接,那开销太大了。TCP三次握手、认证、权限检查... 一套流程下来,几十毫秒就没了。

连接池的作用就是复用连接。说白了,就是提前创建好一批连接放在池子里,谁用谁拿,用完归还。

我常用的连接池是 HikariCP(Spring Boot 默认的),配置起来很简单:

# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      max-lifetime: 1800000

这里有几个关键参数,我解释一下:

参数 说明 建议值
maximum-pool-size 最大连接数 CPU核心数 × 2 + 1(经验公式)
minimum-idle 最小空闲连接 5~10,避免频繁创建连接
connection-timeout 等待连接的超时时间 20000ms(20秒)
max-lifetime 连接的最大存活时间 1800000ms(30分钟)

这里有个坑:最大连接数不是越大越好。我曾经在一个项目里把连接池设成了100,结果数据库CPU直接飙到100%。为什么?因为连接数太多,数据库的上下文切换开销太大了。你想想看,100个线程同时抢CPU,每个线程都在做无谓的等待。

正确的做法是:根据数据库服务器的CPU核心数来算。比如4核的数据库,最大连接数设为 4×2+1=9 左右。如果并发确实高,可以适当调大,但别超过20。

个人经验:我习惯在连接池配置里加上 connection-test-query: SELECT 1。这样每次从池子里拿连接时,都会验证连接是否有效。风场网络偶尔会抖动,这个配置能避免拿到「死连接」。

3.4 本章知识体系

为了让你更直观地理解这三板斧的关系,我画了一张图:

数据库优化实战:三大核心方向 数据库 性能优化 索引优化 复合索引 · 覆盖索引 查询优化 避免SELECT * · 分页优化 连接池配置 HikariCP · 参数调优 最左前缀原则 避免回表查询 只取所需字段 游标分页替代OFFSET 连接复用 参数调优 核心目标 减少IO次数 · 降低CPU开销 · 提升并发能力

这三板斧是相辅相成的。索引优化让查询更快,查询优化让索引发挥最大作用,连接池配置则保证了高并发下的稳定性。缺一不可。

好了,这一章的内容就到这里。记住,数据库优化不是一蹴而就的,需要根据实际业务场景不断调整。你可以在测试环境先模拟风场的数据量,跑一遍慢查询日志,看看哪些SQL最耗时,然后对症下药。

专注资料整理