You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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返回三类聚合结果:

  1. 近7天的逐日平均读数(从1728万条原始数据中返回7条结果)
  2. 近1天的逐小时平均读数(从8.64万条原始数据中返回24条结果)
  3. 近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秒。作为折中方案,若能实现每个时间块返回单条记录的效果也可接受。


解决方案

之前方案性能差的核心原因

  1. 缺少匹配查询模式的联合索引,所有过滤操作都退化为全集合扫描,IO开销极大
  2. 分块计算逻辑存在大量重复扫描:先把全量时间范围数据加载到内存,再按时间块循环遍历过滤,时间复杂度为O(n*steps),数据量越大耗时增长越快
  3. 函数使用场景错误:WINDOW是为滑动窗口计算设计的,会逐行返回聚合结果,用于固定步长分桶统计会产生巨量无用计算
  4. 库外计算需要传输千万级别的原始数据,网络序列化和传输开销占总耗时的90%以上,性能必然不达标

最优方案:固定时间桶预聚合

这是时序聚合场景下性价比最高、性能最稳定的方案,上线后查询耗时可以稳定在几十毫秒级别,完全满足并发要求。

  1. 新建预聚合集合与索引
    新建集合observation_aggregates存储预计算结果,文档字段包含:

    • deviceId:设备ID
    • granularity:聚合粒度,取值为minute/hour/day
    • bucketStart:时间桶起始时间戳(分钟粒度对齐到整分钟、小时对齐到整小时、天对齐到当日0点)
    • sumTemperature:桶内所有温度读数总和
    • count:桶内原始数据条数
      给集合建跳过列表索引,字段顺序为["deviceId", "granularity", "bucketStart"],查询时可以直接走索引点查,不需要扫描任何无关文档。
  2. 预聚合数据写入
    两种写入方式按需选择即可:

    • 同步写入:每次原始数据写入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")
    
  3. 查询逻辑
    查询时直接访问预聚合集合,不需要扫描原始数据,以近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秒以内:

  1. 首先给observations集合建["deviceId", "timestamp"]联合跳过列表索引,这是所有过滤操作能走索引的基础
  2. 废弃嵌套循环分块的写法,直接用COLLECT+DATE_TRUNC一次遍历完成分桶聚合,避免重复扫描数据,优化后的近24小时逐小时聚合语句如下:
    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
    }
    
    这个写法只需要遍历一次指定时间范围内的原始数据,时间复杂度为O(n),配合索引性能会有数量级提升。如果遇到空桶,直接在应用层按时间步长补全默认值即可,不需要在数据库层面处理。

内容的提问来源于stack exchange,提问作者Adam K Dean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:27:25