性能调优案例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万行数据在磁盘上做排序和聚合,不慢才怪。
第一轮优化:从SQL层面下手
我个人的习惯是,遇到慢查询先做三件事:
- 看执行计划——是不是全表扫描?有没有用到索引?
- 看临时表——有没有Using temporary?有没有Using filesort?
- 看数据量——查询范围是不是太大了?
针对这个案例,我做了两个改动:
1. 复合索引
单列索引不够用,建一个复合索引:
CREATE INDEX idx_device_ts ON sensor_data(device_id, ts);
这样WHERE条件里的device_id IN和ts范围查询就能走索引了。注意顺序——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毫秒以内。
优化效果对比
| 优化阶段 | 查询耗时 | CPU使用率 | 内存占用 | 磁盘I/O |
|---|---|---|---|---|
| 原始SQL(全表扫描) | 8.2秒 | 45% | 1.2GB | 高(大量临时文件) |
| 加复合索引+分区 | 1.2秒 | 22% | 380MB | 中 |
| C扩展聚合 | 0.18秒 | 8% | 120MB | 低 |
从8.2秒到0.18秒,优化了45倍。说实话,我自己看到这个结果也挺意外的。但仔细想想,每一步都踩在了点子上:先解决数据库层面的瓶颈,再考虑C扩展。
核心逻辑流程图
下面这张图展示了整个优化路径和决策逻辑:
避坑指南
最后分享几个我踩过的坑:
- 不要一上来就写C扩展。 我曾经在一个查询上直接写C扩展,结果发现加个索引就能解决。白忙活了两天。
- 注意数据一致性。 C扩展直接读共享内存时,要处理好写操作和读操作的同步。我遇到过数据读到一半被覆盖的情况,排查了很久才发现是内存屏障没加。
- 监控要跟上。 优化完不是终点。线上跑一段时间,看看有没有内存泄漏、CPU是否异常。我习惯在C扩展里加一些轻量级的统计日志。
这个案例给我的启发是:性能调优不能只盯着代码。有时候换个思路,从SQL到C扩展,一步步拆解,反而能走得更远。