Apache Druid Timeseries 原生查询完全指南:查询结构、参数详解与源码级实现原理
数据库OLAP大数据后端【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址https://gitcode.com/gh_mirrors/druid6/druid点击查看免费下载Apache Druid 的 Timeseries 查询Timeseries Query是原生native查询语言中最基础、最高效的查询类型之一它接收一个timeseries查询对象按指定时间粒度granularity对时间桶内的数据执行聚合并返回一组 JSON 对象每个对象代表一个时间桶的聚合结果。本文基于当前仓库中的 官方文档 展开结合processing模块的源码实现TimeseriesQuery.java、TimeseriesQueryEngine.java、TimeseriesQueryQueryToolChest.java深入讲解查询对象结构、七大核心字段、Grand Totals、空桶填充等关键行为帮助读者在真实数据上写出可运行、可调优的 Timeseries 查询并理解其底层执行机制。:::info Apache Druid 支持两种查询语言Druid SQL 与 原生查询。本文描述的是原生查询语言中的 Timeseries 查询类型关于 Druid SQL 在何种条件下会使用该查询类型请参阅 SQL 文档。 :::一、什么是 Timeseries 查询Timeseries 查询接收一个 timeseries 查询对象返回一个 JSON 对象数组其中每个对象代表该查询所请求的一个聚合结果。它天然面向按时间线聚合的场景例如统计某个数据源按天/小时/分钟维度的 PV、UV、求和、平均值等指标。与 GroupBy 不同Timeseries 查询不按任意维度分组只按时间粒度分桶因此执行路径更简单、通常更快是时间序列监控、报表、仪表盘类场景的首选。一个典型的 timeseries 查询对象如下{ queryType: timeseries, dataSource: sample_datasource, granularity: day, descending: true, filter: { type: and, fields: [ { type: selector, dimension: sample_dimension1, value: sample_value1 }, { type: or, fields: [ { type: selector, dimension: sample_dimension2, value: sample_value2 }, { type: selector, dimension: sample_dimension3, value: sample_value3 } ] } ] }, aggregations: [ { type: longSum, name: sample_name1, fieldName: sample_fieldName1 }, { type: doubleSum, name: sample_name2, fieldName: sample_fieldName2 } ], postAggregations: [ { type: arithmetic, name: sample_divide, fn: /, fields: [ { type: fieldAccess, name: postAgg__sample_name1, fieldName: sample_name1 }, { type: fieldAccess, name: postAgg__sample_name2, fieldName: sample_name2 } ] } ], intervals: [ 2012-01-01T00:00:00.000/2012-01-03T00:00:00.000 ] }二、Timeseries 查询的七个主要组成部分Timeseries 查询由以下主要属性构成property说明是否必填queryType该 String 必须始终为timeseries这是 Apache Druid 判断如何解释查询时首先查看的字段是dataSource定义要查询的数据源的 String 或 Object类似于关系数据库中的表。更多信息参见 DataSource是descending是否按降序返回结果。默认值为false升序否intervals表示 ISO-8601 时间区间的 JSON Object定义查询覆盖的时间范围是granularity定义查询结果分桶的时间粒度。参见 Granularities是filter维度过滤条件。参见 Filters否virtualColumns虚拟列 的 JSON 列表可在aggregations或postAggregations中引用否默认无aggregations聚合器列表。参见 Aggregations否postAggregations后聚合器列表。参见 Post Aggregations否limit限制结果数量的整数默认无限制否context用于修改查询行为的上下文参数包括 grand totals 与 empty bucket values 等适用于所有查询类型的通用参数参见 Context否从源码看queryType的解析对应 TimeseriesQuery.java 上的JsonTypeName(timeseries)注解Jackson 反序列化时据此将 JSON 映射为TimeseriesQuery对象其泛型结果为ResultTimeseriesResultValue即时间戳 聚合结果值的列表。2.1 各字段的源码级说明dataSource与intervals必填TimeseriesQuery构造函数接收DataSource与QuerySegmentSpec对应 JSON 中的intervals二者均为必填。intervals使用 ISO-8601 区间格式如2012-01-01T00:00:00.000/2012-01-03T00:00:00.000表示左闭右开的时间范围。descending可选默认 false决定结果按时间升序还是降序返回。构造函数通过BaseQuery继承该字段在引擎中会作为向量化游标VectorCursor的扫描方向参数传入见 TimeseriesQueryEngine.java。granularity必填控制结果如何按时间分桶。合法的取值包括预定义粒度all、second、minute、fifteen_minute、thirty_minute、hour、day、week、month、quarter、year以及自定义周期粒度如{type: period, period: P1M, origin: 2012-01-01T00:00:00Z}等。在 Java 构建器中未指定时默认值为Granularities.ALL见 Druids.java。filter可选用于在聚合前过滤原始行。支持selector、and、or、in、not、bound等所有 DimFilter 类型可任意嵌套组合。virtualColumns可选定义可供聚合器、后聚合器引用的虚拟列例如对原始列做表达式转换后再聚合。aggregations/postAggregations可选聚合器定义每个时间桶的度量指标如longSum、doubleSum、count、hyperUnique等后聚合器则在聚合结果之上做进一步计算如除法、比率、百分位数等。注意后聚合器的计算发生在结果合并阶段之后见下文第六节。limit可选默认无限制从源码看limit 0会被解释为不限制构造函数中this.limit (limit 0) ? Integer.MAX_VALUE : limit;且要求limit 0见 TimeseriesQuery.java。也就是说显式传0等同于不设 limit传负数会直接报错。三、查询语义与输出示例将上文示例拼合起来理解该查询会在2012-01-01至2012-01-03之间的每一天day粒度从sample_datasource返回 1 个数据点共 2 个数据点。每个数据点包含sample_fieldName1的 long 求和longSumsample_fieldName2的 double 求和doubleSum前两者相除/的 double 结果postAggregations中的sample_divide并且这些结果仅统计满足 filter 条件sample_dimension1 sample_value1且sample_dimension2 sample_value2或sample_dimension3 sample_value3的行。输出结果如下[ { timestamp: 2012-01-01T00:00:00.000Z, result: { sample_name1: some_value, sample_name2: some_value, sample_divide: some_value } }, { timestamp: 2012-01-02T00:00:00.000Z, result: { sample_name1: some_value, sample_name2: some_value, sample_divide: some_value } } ]每个结果对象由两部分组成timestamp该时间桶的起始时间经粒度对齐与result一个包含所有聚合/后聚合指标名称到值的映射。四、Grand Totals 总计行Druid 可以在 timeseries 结果集末尾追加一个额外的总计行grand totals。启用方式是在查询上下文中添加grandTotal: true例如{ queryType: timeseries, dataSource: sample_datasource, intervals: [ 2012-01-01T00:00:00.000/2012-01-03T00:00:00.000 ], granularity: day, aggregations: [ { type: longSum, name: sample_name1, fieldName: sample_fieldName1 }, { type: doubleSum, name: sample_name2, fieldName: sample_fieldName2 } ], context: { grandTotal: true } }总计行行为要点出现在结果数组的最后一行且没有时间戳timestamp为 null即使查询以descending降序模式运行总计行依然位于最后总计行中的后聚合结果基于总计行的聚合值计算即对全区间聚合值再做后聚合而不是各桶后聚合值之和。源码实现总计行由 TimeseriesQueryQueryToolChest.java 的mergeResults实现。其逻辑为遍历结果序列对每个聚合器用aggregatorFactory.combine(grandTotals[i], value)逐步累加总计值最后在序列末尾追加一个timestamp为 null、value 为总计映射的Result。上下文常量定义在 TimeseriesQuery.javaCTX_GRAND_TOTAL grandTotal。仓库测试 TimeseriesQueryRunnerTest.java 的testTimeseriesGrandTotal验证了在升序/降序下各桶与总计行含后聚合ADD_ROWS_INDEX_CONSTANT的结果正确性。五、Empty Bucket Values 空桶填充5.1 默认行为填充空桶默认情况下Druid 会用聚合函数的默认值填充 timeseries 查询结果中中间的空时间桶。例如对2012-01-01/2012-01-04区间、day粒度、使用 SUM 聚合器发起查询若2012-01-02没有任何数据Druid 返回[ { timestamp: 2012-01-01T00:00:00.000Z, result: { sample_name1: some_value } }, { timestamp: 2012-01-02T00:00:00.000Z, result: { sample_name1: NULL } }, { timestamp: 2012-01-03T00:00:00.000Z, result: { sample_name1: some_value } } ]注意完全落在数据区间之外的时间桶不会被填充默认值。也就是说空桶填充只作用于数据区间内部的空洞不会把查询区间两端都扩展到数据范围之外。5.2 关闭填充skipEmptyBuckets可以通过上下文标志skipEmptyBuckets禁用所有空桶填充。在该模式下Druid 会从结果中省略2012-01-02这个数据点{ queryType: timeseries, dataSource: sample_datasource, granularity: day, aggregations: [ { type: longSum, name: sample_name1, fieldName: sample_fieldName1 } ], intervals: [ 2012-01-01T00:00:00.000/2012-01-04T00:00:00.000 ], context : { skipEmptyBuckets: true } }源码实现上下文常量SKIP_EMPTY_BUCKETS skipEmptyBuckets定义于 TimeseriesQuery.java通过isSkipEmptyBuckets()读取。在 TimeseriesQueryEngine.java 中向量化路径processVectorized用emptyBucket标志跟踪当前桶是否有数据若emptyBucket skipEmptyBuckets则返回 null随后被filter(Objects::nonNull)过滤掉见 L215-L218非向量化路径processNonVectorized则在cursor.isDone()当前桶无任何行时直接返回 null见 L273-L276。另外从 SQL 层的翻译逻辑DruidQuery.java可以看到当查询粒度不是ALL或原本是 groupBy 但分组维度被移除时SQL 翻译器会自动向 context 写入skipEmptyBucketstrue以保证每个时间组返回一行的语义与聚合查询一致。六、源码级执行原理6.1 查询对象与构建器TimeseriesQuery继承自BaseQueryResultTimeseriesResultValue核心字段包括虚拟列、过滤条件、聚合器列表、后聚合器列表与 limit见 TimeseriesQuery.java。其中postAggregatorSpecs在构造时通过Queries.prepareAggregations(...)做依赖校验与排序确保后聚合器引用的字段先于其本身计算。在 Java 代码中除了直接构造 JSON还可以使用构建器 Druids.TimeseriesQueryBuilder 以链式 API 编程式构建例如TimeseriesQuery query Druids.newTimeseriesQueryBuilder() .dataSource(sample_datasource) .intervals(2012-01-01T00:00:00.000/2012-01-03T00:00:00.000) .granularity(Granularities.DAY) .descending(true) .filters(sample_dimension1, sample_value1) .aggregators( new LongSumAggregatorFactory(sample_name1, sample_fieldName1), new DoubleSumAggregatorFactory(sample_name2, sample_fieldName2) ) .context(ImmutableMap.of(TimeseriesQuery.CTX_GRAND_TOTAL, true)) .build();构建器默认granularity Granularities.ALL、descending false未指定 limit 时为 0即不限制。6.2 查询引擎向量化与非向量化两条路径TimeseriesQueryEngine.java 的process方法负责在单个 segment 上执行查询首先把过滤条件转换为 CNF 形式Filters.convertToCNFFromQueryContext然后根据三个条件决定是否走向量化路径adapter.canVectorize(filter, virtualColumns, descending)—— segment 存储适配器支持向量化VirtualColumns.shouldVectorize(...)—— 虚拟列支持向量化VectorGroupByEngine.canVectorizeAggregators(inspector, aggregatorSpecs)—— 所有聚合器都具备向量化实现。向量化路径processVectorized通过makeVectorCursor建立向量化游标配合VectorCursorGranularizer按粒度切分桶用AggregatorAdapters.aggregateVector批量处理一批行吞吐更高。非向量化路径processNonVectorized通过QueryRunnerHelper.makeCursorBasedQuery逐行推进游标cursor.advance()对每个聚合器调用aggregator.aggregate()最后用TimeseriesResultBuilder构建结果。无论哪条路径最终都会在引擎层应用limitif (limit Integer.MAX_VALUE) return result.limit(limit);见 L118-L123。6.3 结果合并、排序与后聚合在集群环境下每个 segment 会各自产出局部结果由 TimeseriesQueryQueryToolChest.java 的mergeResults统一合并合并前会临时移除后聚合器withPostAggregatorSpecs(ImmutableList.of())因为后聚合必须等全部聚合结果合并完成后再计算见 L127-L133使用 TimeseriesBinaryFn 作为合并函数对同一时间桶的聚合结果按聚合器语义如求和进行 combine排序比较器ResultGranularTimestampComparator.create(granularity, descending)依据粒度和descending决定结果顺序当granularity ALL且未开启skipEmptyBuckets、且非 bySegment 模式时若合并结果为空会返回一个零值结果各聚合器在空输入上的初始化值如 count 为 0以保持 SQL 语义兼容见 L155-L171后聚合在makePostComputeManipulatorFn中完成先将时间戳如设置了timestampResultField与未 finalize 的聚合值放入映射再逐个调用postAgg.compute(values)见 L509-L546。6.4 缓存策略Timeseries 查询默认可缓存isCacheable返回 true。缓存键由 TimeseriesQueryQueryToolChest.java 的computeCacheKey/computeResultLevelCacheKey构造包含descending、skipEmptyBuckets、granularity、filter、聚合器、虚拟列、limit以及结果级缓存特有的后聚合器、timestampResultField与grandTotal。这意味着这些字段的任何变化都会导致缓存键变化从而保证缓存命中语义的准确性。七、Druid SQL 与 Timeseries 的对应关系Druid SQL 的查询规划器在满足特定条件时会自动把 SQL 翻译为 Timeseries 原生查询见 DruidQuery.java 的toTimeseriesQuery()。翻译为 Timeseries 需要同时满足没有 having 过滤、没有窗口函数windowing、没有 subtotals最多只有一个分组维度且该维度必须是基于__time的时间粒度表达式FLOOR(__time TO ...)等否则退回 GroupBy排序只能是时间升序/降序TIME_ASCENDING/TIME_DESCENDING不能对指标列排序不支持 offset跳过行limit 为 0 时也拒绝翻译因为 0 会被引擎当作不限制。翻译时会把时间粒度的输出列名通过内部参数timestampResultFieldCTX_TIMESTAMP_RESULT_FIELD见 TimeseriesQuery.java传给引擎使时间桶结果能以该别名出现在结果中。因此形如SELECT FLOOR(__time TO DAY) AS d, SUM(m) FROM t WHERE ... GROUP BY 1 ORDER BY 1的 SQL 在底层往往就是一条 Timeseries 查询——这也是文档开头提示可参考 SQL 翻译文档 了解 Druid SQL 何时使用本查询类型的原因。八、使用建议与注意事项必填字段不可省略queryType、dataSource、intervals、granularity为必填省略intervals或granularity会导致查询无法解析。descending与limit的配合降序查询配合limit可高效获取最近 N 个时间桶的指标避免拉取全量再排序。空桶语义默认空桶会被填充为聚合器默认值数值型聚合器为 0 或 null视具体类型而定这可能让下游画图出现零点若希望结果只包含真实有数据的桶使用context: {skipEmptyBuckets: true}。Grand Total 的用途需要在时间序列下方展示整个区间汇总如总 PV、总营收时开启grandTotal: true且无需担心降序模式下总计行的位置。优先考虑向量化只要聚合器与过滤条件支持引擎会自动走向量化路径对于高吞吐查询应尽量使用支持向量化的聚合类型。借助 SQL在绝大多数场景下直接写 Druid SQLFLOOR(__time TO ...) GROUP BY即可自动获得 Timeseries 的执行计划不必手工构造原生 JSON原生 JSON 更适用于对执行细节有精确控制或通过 HTTP 直接调用的场景。延伸阅读原生查询总览数据源DataSource粒度Granularities过滤器Filters虚拟列Virtual Columns聚合器Aggregations后聚合器Post Aggregations查询上下文Query ContextDruid SQL 翻译说明源码查询对象 TimeseriesQuery.java、执行引擎 TimeseriesQueryEngine.java、结果处理与缓存 TimeseriesQueryQueryToolChest.java、Java 构建器 Druids.java测试行为验证 TimeseriesQueryRunnerTest.java、对象序列化 TimeseriesQueryTest.java赞分享数据库OLAP大数据后端【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址https://gitcode.com/gh_mirrors/druid6/druid点击查看免费下载相关推荐终极Rack查询参数解析指南从基础到高级配置的完整教程终极Rack查询参数解析指南从基础到高级配置的完整教程 Rack作为Ruby Web服务器接口的核心组件其QueryParser模块提供了强大的查询参数解析开发工具工作流自动化AI 技能/插件AI 应用人工智能Scientific Agent Skills 完整地图20大科学领域、163项技能与100数据库全解析Scientific Agent Skills 完整地图20大科学领域、163项技能与100数据库全解析 Scientific Agent Skills 是数据库数据分析OLAP大数据实时分析数据仓库后端如何用SpotifyRadar玩转Top榜单短/中/长期时间范围切换的完整攻略如何用SpotifyRadar玩转Top榜单短/中/长期时间范围切换的完整攻略 想搞清自己最近到底在循环哪些歌、沉迷哪位歌手 SpotifyRadar 这款数据库OLAP大数据后端上一篇如何快速掌握PowerSploit Set-DomainUserPasswordWindows域密码重置终极指南下一篇StyleGAN2-ADA对比分析如何用自适应判别器增强技术在小数据集上实现突破性效果 创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考