ArangoDB大数据集按时间步长聚合平均值的高效查询方案
你这个场景是典型的时序数据固定粒度聚合问题,不需要用到ArangoDB的图能力,之前方案跑不快核心是实现逻辑和索引没做对,按下面的方案调整完全可以达到<1秒的响应要求,并发场景下性能也足够稳定。
我曾在帖子《What's the best way to return a sample of data over the a period?》中提到过相关问题,当时给出的潜在方案虽然可行,但最终没能解决我现在的问题,需求也做了小幅调整。
问题描述
需要实现从大型数据集中快速返回按时间块拆分的聚合数据,例如从温度读数集合中,返回近24小时的逐小时平均读数。
示例场景:设有名为observations的集合,存储多设备采集的温度数据,采样频率为每秒1条,当前数据集规模达1.2亿条文档。每条文档包含deviceId、timestamp、temperature三个字段。
数据规模明细如下:
- 共
200台设备 - 单设备每小时产生
3,600条文档 - 单设备每日产生
86,400条文档 - 全设备每日产生
17,280,000条文档 - 全设备每周产生
120,960,000条文档
查询指定设备指定时间段的原始数据非常简单,查询语句如下:
FOR o IN observations FILTER o.deviceId = @deviceId FILTER o.timestamp >= @start AND o.timestamp <= @end RETURN o
核心难点在于聚合数据的查询效率。需要针对指定deviceId返回三类聚合结果:
- 近7天的逐日平均读数(从1728万条原始数据中返回7条结果)
- 近1天的逐小时平均读数(从8.64万条原始数据中返回24条结果)
- 近1小时的逐分钟平均读数(从3600条原始数据中返回60条结果)
注:实际场景中部分数据采样频率可能低于1秒/次,例如15秒/次、1分钟/次,部分时间段还可能存在数据缺失,上述规模为理想场景下的测算值。
已尝试的方案
曾尝试使用WINDOW函数实现(示例如下),但查询运行速度极慢,暂不确定是查询写法问题还是数据量过大导致,相关参考资料也较少。且该方案仍需实现按时间步长逐段取值的逻辑。
FOR o IN observations FILTER o.deviceId = @deviceId WINDOW DATE_TIMESTAMP(o.timestamp) WITH { preceding: "PT60M" } AGGREGATE temperature = AVG(o.temperature) RETURN { timestamp: o.timestamp, temperature }
也尝试过之前帖子中建议的基于时间戳取模的过滤方法,但该方案无法支持平均值计算,也不能适配数据缺失的场景(例如无对应精确时间戳的记录)。
还尝试过将数据拉出ArangoDB在库外计算过滤,但该方案性能极差,尤其是查询一周数据(1700万条)时完全无法正常运行。
后续尝试在AQL中实现分块遍历计算平均值的逻辑,功能可正常运行但性能不佳,在性能一般的设备上查询耗时约10秒,查询语句如下:
LET steps = 24 LET stepsRange = 0..23 LET diff = @end - @start LET interval = diff / steps LET filteredObservations = ( FOR o IN observations FILTER o.deviceId == @deviceId FILTER o.timestamp >= @start AND o.timestamp <= @end RETURN o ) FOR step IN stepsRange RETURN ( LET stepStart = start + (interval * step) LET stepEnd = stepStart + interval FOR f IN filteredObservations FILTER f.timestamp >= stepStart AND f.timestamp <= stepEnd COLLECT AGGREGATE temperature = AVG(f.temperature) RETURN { step, temperature } )
也尝试过结合WINDOW的变种写法,但均未取得理想效果。本身有关系型/文档数据库使用经验,对ArangoDB的图功能了解不多,不确定是否可以借助图能力提升查询效率。
需求目标
该查询需要支持多用户同时针对不同时间范围、不同设备发起并发请求,因此要求查询响应时长小于1秒。作为折中方案,若能实现每个时间块返回单条记录的效果也可接受。
解决方案
之前方案性能差的核心原因
- 缺少匹配查询模式的联合索引,所有过滤操作都退化为全集合扫描,IO开销极大
- 分块计算逻辑存在大量重复扫描:先把全量时间范围数据加载到内存,再按时间块循环遍历过滤,时间复杂度为O(n*steps),数据量越大耗时增长越快
- 函数使用场景错误:
WINDOW是为滑动窗口计算设计的,会逐行返回聚合结果,用于固定步长分桶统计会产生巨量无用计算 - 库外计算需要传输千万级别的原始数据,网络序列化和传输开销占总耗时的90%以上,性能必然不达标
最优方案:固定时间桶预聚合
这是时序聚合场景下性价比最高、性能最稳定的方案,上线后查询耗时可以稳定在几十毫秒级别,完全满足并发要求。
新建预聚合集合与索引
新建集合observation_aggregates存储预计算结果,文档字段包含:deviceId:设备IDgranularity:聚合粒度,取值为minute/hour/daybucketStart:时间桶起始时间戳(分钟粒度对齐到整分钟、小时对齐到整小时、天对齐到当日0点)sumTemperature:桶内所有温度读数总和count:桶内原始数据条数
给集合建跳过列表索引,字段顺序为["deviceId", "granularity", "bucketStart"],查询时可以直接走索引点查,不需要扫描任何无关文档。
预聚合数据写入
两种写入方式按需选择即可:- 同步写入:每次原始数据写入
observations时,用内置DATE_TRUNC函数计算三个粒度对应的时间桶,原子更新对应聚合文档的sumTemperature和count字段,实时性最高 - 批量写入:后台起定时任务,每分钟聚合上一分钟的原始数据写入分钟粒度桶、每小时聚合上一小时数据写入小时粒度桶、每天凌晨聚合前一天数据写入天粒度桶,对写入链路无影响,1分钟以内的延迟对查询场景完全无感知
时间桶对齐直接用内置函数实现,不需要自己写取模逻辑,天然适配非固定采样频率、数据缺失的场景:
// 分钟级桶对齐 LET minuteBucket = DATE_TRUNC(o.timestamp, "minute") // 小时级桶对齐 LET hourBucket = DATE_TRUNC(o.timestamp, "hour") // 天级桶对齐 LET dayBucket = DATE_TRUNC(o.timestamp, "day")- 同步写入:每次原始数据写入
查询逻辑
查询时直接访问预聚合集合,不需要扫描原始数据,以近24小时逐小时平均为例:FOR agg IN observation_aggregates FILTER agg.deviceId == @deviceId FILTER agg.granularity == "hour" FILTER agg.bucketStart >= DATE_SUBTRACT(DATE_NOW(), "PT24H") FILTER agg.bucketStart <= DATE_NOW() SORT agg.bucketStart ASC RETURN { timestamp: agg.bucketStart, temperature: agg.sumTemperature / agg.count }这类查询扫描的文档数和最终返回的结果数完全一致:近7天扫7条、近1天扫24条、近1小时扫60条,性能几乎不受数据总量增长影响。
过渡方案:实时查询优化(无需改动写入链路)
如果短期无法上线预聚合逻辑,按下面的方式调整,配合正确索引可以把现有10秒的查询耗时压缩到1秒以内:
- 首先给
observations集合建["deviceId", "timestamp"]联合跳过列表索引,这是所有过滤操作能走索引的基础 - 废弃嵌套循环分块的写法,直接用
COLLECT+DATE_TRUNC一次遍历完成分桶聚合,避免重复扫描数据,优化后的近24小时逐小时聚合语句如下:
这个写法只需要遍历一次指定时间范围内的原始数据,时间复杂度为O(n),配合索引性能会有数量级提升。如果遇到空桶,直接在应用层按时间步长补全默认值即可,不需要在数据库层面处理。FOR o IN observations FILTER o.deviceId == @deviceId FILTER o.timestamp >= @start AND o.timestamp <= @end COLLECT bucket = DATE_TRUNC(o.timestamp, "hour") AGGREGATE avgTemp = AVG(o.temperature) SORT bucket ASC RETURN { timestamp: bucket, temperature: avgTemp }
内容的提问来源于stack exchange,提问作者Adam K Dean

