性能调优案例1:数据库查询优化,从SQL到C扩展

说实话,很多做C语言开发的朋友,一提到性能调优就盯着代码层面死磕——循环展开、内存对齐、指令流水线……这些当然重要。但我在实际项目中遇到过一类更头疼的问题:瓶颈根本不在C代码里,而在数据库查询上

你想想看,一个查询慢3秒,你C代码优化到极致也就省个几十毫秒。方向错了,全白干。今天这个案例,就是我从一条慢SQL入手,一路优化到C扩展的全过程。嗯,这里面的坑,我踩过不少。

问题现场:一个慢到离谱的查询

几年前我接手一个物联网数据平台。设备上报数据后,需要按时间窗口做聚合统计。业务逻辑不复杂,但有个接口响应时间经常超过8秒。用户反馈说“点一下查询,能去倒杯水再回来看结果”。

我第一反应是看C代码。翻了一遍,内存池、无锁队列、epoll事件驱动……写得挺漂亮。那问题在哪?

抓了个SQL日志,发现核心查询长这样:

SELECT 
    device_id,
    AVG(temperature) AS avg_temp,
    MAX(humidity) AS max_hum,
    COUNT(*) AS record_count
FROM sensor_data
WHERE ts >= '2024-01-01 00:00:00' 
  AND ts < '2024-01-08 00:00:00'
  AND device_id IN (1001, 1002, 1003, 1004, 1005)
GROUP BY device_id;

看着挺常规对吧?但执行计划一分析,问题就暴露了。

瓶颈定位:全表扫描与临时文件

用EXPLAIN ANALYZE一看,sensor_data表有800多万行,这个查询走了全表扫描。为什么?因为ts列虽然有索引,但device_id IN (...)这个条件导致优化器认为“回表成本太高,不如全扫”。

更致命的是,GROUP BY触发了磁盘临时表——内存不够,数据写到磁盘上了。你想想看,800万行数据在磁盘上做排序和聚合,不慢才怪。

核心结论: 这个查询的瓶颈不在C代码,而在数据库的执行计划。优化C代码最多省几十毫秒,但优化SQL能省几秒。

第一轮优化:从SQL层面下手

我个人的习惯是,遇到慢查询先做三件事:

  1. 看执行计划——是不是全表扫描?有没有用到索引?
  2. 看临时表——有没有Using temporary?有没有Using filesort?
  3. 看数据量——查询范围是不是太大了?

针对这个案例,我做了两个改动:

1. 复合索引

单列索引不够用,建一个复合索引:

CREATE INDEX idx_device_ts ON sensor_data(device_id, ts);

这样WHERE条件里的device_id INts范围查询就能走索引了。注意顺序——device_id在前,ts在后。因为等值条件在前、范围条件在后,索引利用率最高。

2. 分区表

数据量太大,按时间分区:

ALTER TABLE sensor_data 
PARTITION BY RANGE (TO_DAYS(ts)) (
    PARTITION p2024q1 VALUES LESS THAN (TO_DAYS('2024-04-01')),
    PARTITION p2024q2 VALUES LESS THAN (TO_DAYS('2024-07-01')),
    PARTITION p2024q3 VALUES LESS THAN (TO_DAYS('2024-10-01')),
    PARTITION p2024q4 VALUES LESS THAN (TO_DAYS('2025-01-01'))
);

分区后,查询只扫描相关分区,而不是整张表。效果立竿见影——查询时间从8秒降到了1.2秒。

小技巧: 分区键尽量和查询条件匹配。如果查询经常按周或月做范围扫描,按时间分区是最直接的选择。

第二轮优化:C扩展登场

1.2秒虽然能用了,但离“毫秒级响应”还有差距。而且随着数据增长,这个时间还会反弹。我决定把聚合计算从数据库里搬出来,用C扩展来做。

思路是这样的:

  • C扩展直接读取原始数据(通过共享内存或本地文件)
  • 用C语言做AVG、MAX、COUNT等聚合计算
  • 结果直接返回给应用层,不走SQL解析和网络开销

为什么这么做?因为数据库的聚合计算有大量额外开销:SQL解析、权限检查、锁竞争、网络传输……这些在C扩展里全都可以省掉。

核心代码片段

// 聚合计算的核心结构
typedef struct {
    int64_t device_id;
    double  sum_temp;
    double  max_temp;
    double  min_temp;
    int64_t count;
} agg_result_t;

// 批量处理函数
void aggregate_sensor_data(
    sensor_record_t *records,
    int64_t          record_count,
    agg_result_t    *results,
    int64_t         *result_count
) {
    // 用哈希表按device_id分组
    // 每个分组维护sum、max、min、count
    // 最后计算avg = sum / count
    
    // 这里省略了哈希表实现细节
    // 实际项目中我用的是开放寻址法
    // 配合内存池避免频繁malloc
}

这个C扩展跑起来后,聚合计算本身只用了不到50毫秒。加上数据读取和结果返回,总耗时控制在200毫秒以内。

注意: C扩展不是银弹。它适合计算密集、数据量大的场景。如果查询条件变化频繁、数据量小,SQL反而更灵活。我曾经在一个小项目里强行上C扩展,结果维护成本比收益还高——得不偿失。

优化效果对比

优化阶段 查询耗时 CPU使用率 内存占用 磁盘I/O
原始SQL(全表扫描) 8.2秒 45% 1.2GB 高(大量临时文件)
加复合索引+分区 1.2秒 22% 380MB
C扩展聚合 0.18秒 8% 120MB

从8.2秒到0.18秒,优化了45倍。说实话,我自己看到这个结果也挺意外的。但仔细想想,每一步都踩在了点子上:先解决数据库层面的瓶颈,再考虑C扩展。

核心逻辑流程图

下面这张图展示了整个优化路径和决策逻辑:

数据库查询优化路径 慢查询(8.2秒) 第一步:SQL层面优化 复合索引 + 分区表 达标? 结束(1.2秒) 第二步:C扩展聚合 共享内存 + 哈希分组 最终结果(0.18秒)

避坑指南

最后分享几个我踩过的坑:

  • 不要一上来就写C扩展。 我曾经在一个查询上直接写C扩展,结果发现加个索引就能解决。白忙活了两天。
  • 注意数据一致性。 C扩展直接读共享内存时,要处理好写操作和读操作的同步。我遇到过数据读到一半被覆盖的情况,排查了很久才发现是内存屏障没加。
  • 监控要跟上。 优化完不是终点。线上跑一段时间,看看有没有内存泄漏、CPU是否异常。我习惯在C扩展里加一些轻量级的统计日志。

这个案例给我的启发是:性能调优不能只盯着代码。有时候换个思路,从SQL到C扩展,一步步拆解,反而能走得更远。

公众号:蓝海资料掘金营,微信 deep3321