Apache Druid distinctCount 聚合器扩展实战指南:配置加载、查询用法与源码原理

发布时间:2026/9/23 4:05:13
Apache Druid distinctCount 聚合器扩展实战指南:配置加载、查询用法与源码原理 数据库OLAP大数据后端【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址https://gitcode.com/gh_mirrors/druid6/druid点击查看免费下载导读本文围绕 Apache Druid 的druid-distinctcount扩展系统讲解如何加载该扩展、在 Timeseries / TopN / GroupBy 三类查询中使用distinctCount聚合器计算去重计数典型场景如 UV 统计并深入剖析其基于 Bitmap 的实现原理、两个必须满足的前置分区条件以及使用限制。读完本文你将能够正确配置并安全使用该聚合器避免因分区与粒度设置不当导致的计数错误。该扩展位于仓库的 extensions-contrib/distinctcount 模块对应官方文档为 docs/development/extensions-contrib/distinctcount.md。一、加载 druid-distinctcount 扩展druid-distinctcount是 Apache Druid 的 contrib 扩展与其它扩展一样需要通过扩展加载列表启用。官方文档指出使用前需在扩展加载列表中包含druid-distinctcount具体加载方式参见 加载扩展说明。在实际部署中通常在common.runtime.properties中通过druid.extensions.loadList属性声明druid.extensions.loadList[druid-distinctcount]也可以与其它扩展一起列出例如druid.extensions.loadList[postgresql-metadata-storage, druid-hdfs-storage, druid-distinctcount]从仓库的 extensions-contrib/distinctcount/pom.xml 可以看到该模块的artifactId为druid-distinctcount依赖druid-processing、guava、jackson、fastutil等组件均为 provided 或 test 作用域并通过 DistinctCountDruidModule.java 注册了distinctCount这一聚合器 JSON 类型名public static final String DISTINCT_COUNT distinctCount; ... new SimpleModule(DistinctCountModule).registerSubtypes( new NamedType(DistinctCountAggregatorFactory.class, DISTINCT_COUNT) )这意味着在查询 JSON 的aggregations数组中只要将type设为distinctCountJackson 反序列化即可定位到 DistinctCountAggregatorFactory.java。二、使用前的两个关键前置条件务必遵守官方文档明确强调使用distinctCount聚合器前必须完成以下两步否则结果可能错误按单一维度做 Hash 分区使用基于单维度的 hash 分区规格按该维度例如visitor_id对数据进行分区确保该维度上具有同一值的所有行都落入同一个 segment。因为该聚合器只在单个 segment 内做去重若相同键分散到多个 segment会导致重复计数over count。保证 queryGranularity 能被 segmentGranularity 整除即查询粒度必须能整除分段粒度例如查询粒度为day而分段粒度为month时day能整除month反之若查询粒度比分段粒度更粗且无法整除结果会出错。官方原文的表述是make sure queryGranularity is divided exactly by segmentGranularity or else the result will be wrong。在 Druid 的摄取配置中Hash 分区可通过partitionsSpec与partitionDimensions实现例如使用type: hashed的分区规格并将partitionDimensions设为[visitor_id]。Hashed 分区的基本思路是先选定 segment 数量再按分区维度哈希把行均匀分布到各 segment相关背景可参考 Hadoop 摄取文档中的 Hashed 分区说明 与 本地批量任务文档。三、三种查询类型中的 distinctCount 用法以下三个示例完整继承自官方文档分别演示 Timeseries、TopN、GroupBy 查询中的distinctCount聚合器配置。三者均对visitor_id字段做去重计数输出命名为uv。1. Timeseries 查询{ queryType: timeseries, dataSource: sample_datasource, granularity: day, aggregations: [ { type: distinctCount, name: uv, fieldName: visitor_id } ], intervals: [ 2016-03-01T00:00:00.000/2013-03-20T00:00:00.000 ] }2. TopN 查询{ queryType: topN, dataSource: sample_datasource, dimension: sample_dim, threshold: 5, metric: uv, granularity: all, aggregations: [ { type: distinctCount, name: uv, fieldName: visitor_id } ], intervals: [ 2016-03-06T00:00:00/2016-03-06T23:59:59 ] }3. GroupBy 查询{ queryType: groupBy, dataSource: sample_datasource, dimensions: [sample_dim], granularity: all, aggregations: [ { type: distinctCount, name: uv, fieldName: visitor_id } ], intervals: [ 2016-03-06T00:00:00/2016-03-06T23:59:59 ] }三个示例的公共参数含义如下type聚合器类型固定为distinctCount对应源码中注册的 JSON 类型名name输出结果中的字段名如uvfieldName要去重计数的维度字段如visitor_id该字段必须是维度字段因为聚合器内部通过DimensionSelector读取它见下文源码分析。从 DistinctCountAggregatorFactory.java 的构造器可以看出它接受三个 JSON 属性name、fieldName和可选的bitmapFactoryname与fieldName均通过Preconditions.checkNotNull强制非空JsonCreator public DistinctCountAggregatorFactory( JsonProperty(name) String name, JsonProperty(fieldName) String fieldName, JsonProperty(bitmapFactory) BitMapFactory bitMapFactory ) { Preconditions.checkNotNull(name); Preconditions.checkNotNull(fieldName); ... this.bitMapFactory bitMapFactory null ? DEFAULT_BITMAP_FACTORY : bitMapFactory; }其中bitmapFactory未指定时默认使用RoaringBitMapFactory。如果需要对底层位图实现做精细调优可以在聚合器配置中显式指定例如{ type: distinctCount, name: uv, fieldName: visitor_id, bitmapFactory: { type: roaring } }bitmapFactory支持的类型由 BitMapFactory.java 中的 Jackson 子类型注册定义共有三种type 值实现类底层位图roaring默认RoaringBitMapFactoryRoaringBitmapFactory见 RoaringBitMapFactory.javajavaJavaBitMapFactoryBitSetBitmapFactory见 JavaBitMapFactory.javaconciseConciseBitMapFactoryConciseBitmapFactory见 ConciseBitMapFactory.java四、源码实现原理Bitmap 驱动的单段去重理解distinctCount的实现才能明白为何它有上述前置条件与限制。整个扩展只有 10 个 Java 源文件逻辑非常聚焦。1. 聚合核心DistinctCountAggregatorDistinctCountAggregator.java 是核心实现它在factorize时通过DimensionSelector拿到维度值每次aggregate()把当前行对应的维度值在 segment 字典中的整数索引写入MutableBitmap位图public void aggregate() { IndexedInts row selector.getRow(); for (int i 0, rowSize row.size(); i rowSize; i) { int index row.get(i); mutableBitmap.add(index); } }去重结果就是位图中置位比特的数量public Object get() { return mutableBitmap.size(); }注意这里的关键点位图去重的是字典索引整数而不是原始字符串本身。由于同一 segment 内同一个维度值必然映射到同一个字典索引位图的“集合去重”语义天然成立——这也正解释了为什么必须用单一维度 hash 分区把相同键的所有行收敛到同一个 segment一旦相同键落入不同 segment每个 segment 各自维护一个独立位图最终合并阶段无法再对原始值去重。2. 合并逻辑为何是求和从 DistinctCountAggregatorFactory.java 可以看到该聚合器的combine()与getCombiningFactory()均按数值求和处理public Object combine(Object lhs, Object rhs) { ... return ((Number) lhs).longValue() ((Number) rhs).longValue(); } Override public AggregatorFactory getCombiningFactory() { return new LongSumAggregatorFactory(name, name); }也就是说单个 segment 内是精确的位图去重而跨 segment 的结果合并是“各 segment 去重计数之和”无法消除跨 segment 重复。这也是官方文档提醒“否则可能重复计数”的根源。getIntermediateType()与getResultType()均为LONGgetMaxIntermediateSize()为 8 字节位图只在内存计算期间存活最终以 long 形式输出。3. Buffer 聚合与空值兜底在需要缓冲式聚合如 groupBy 的批处理时使用 DistinctCountBufferAggregator.java它通过Int2ObjectMapMutableBitmap按 buffer 位置维护一组WrappedRoaringBitmapaggregate()后将位图大小写入ByteBuffer中的 long 位置init()时置 0同样只返回 long 值。当字段不是维度或无法构造DimensionSelector时工厂会回退到 NoopDistinctCountAggregator.java及对应的NoopDistinctCountBufferAggregator直接返回 0保证查询不会因缺失维度而报错。4. 缓存键与模块注册该聚合器的查询结果缓存键由 getCacheKey() 生成包含类型标识、fieldName与bitmapFactory的字符串表示其中类型字节0x10定义于 processing 模块的 AggregatorUtil.java 的DISTINCT_COUNT_CACHE_KEY与其它聚合器共享同一套缓存键协议。五、使用限制与注意事项官方文档明确列出了distinctCount在使用中的两类限制与 groupBy 一起使用每个 segment 内 groupBy 键的数量不应超过maxIntermediateRows这是 groupBy 查询的中间结果行数上限可通过查询 context 调整。一旦超过结果将不正确。这是因为缓冲聚合需要为每个 groupBy 键维护独立的位图集合超出上限后会导致部分键的位图被截断或丢弃。与 topN 一起使用numValuesPerPasstopN 单遍扫描处理的候选值数量不应设置过大。该值过大时distinctCount会为大量候选维度值各维护一份位图占用大量内存可能导致 JVM 内存溢出OOM。从DistinctCountBufferAggregator的Int2ObjectMapMutableBitmap结构可以直观看到内存占用与“候选键数量 × 位图大小”成正比。此外结合前文源码结论还需重申两点使用纪律必须按去重维度做单一维度 Hash 分区否则合并阶段的求和会重复计数必须保证 queryGranularity 能被 segmentGranularity 整除否则跨粒度聚合的数值会失真。六、测试验证与延伸阅读仓库为扩展提供了三类查询的完整测试可直接作为行为参考DistinctCountTimeseriesQueryTest.java构造三条包含不同visitor_id的记录断言 Timeseries 查询得到UV 3DistinctCountGroupByQueryTest.java验证 groupBy 场景下的去重计数DistinctCountTopNQueryTest.java验证 topN 场景下的去重计数。如果你希望了解 Druid 提供的其它去重相关聚合方案例如基于基数估计的近似去重可以继续阅读 聚合器文档而distinctCount本身的特点是单 segment 内精确去重代价是需要严格的分区与粒度配合并受限于 groupBy / topN 的中间行数与内存参数。在实践中请务必先完成第一节的分区与粒度设计再在生产环境大规模使用。赞分享数据库OLAP大数据后端【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址https://gitcode.com/gh_mirrors/druid6/druid点击查看免费下载相关推荐Apache Druid DistinctCount 聚合器扩展详解精确去重计数UV的原理、安装与三种查询实战Apache Druid DistinctCount 聚合器扩展详解精确去重计数UV的原理、安装与三种查询实战 本篇技术指南围绕 Apache Druid数据库数据分析OLAP大数据实时分析数据仓库后端DrawerKit动画原理深度剖析如何实现流畅的抽屉过渡效果DrawerKit动画原理深度剖析如何实现流畅的抽屉过渡效果 DrawerKit是一个优秀的iOS自定义视图控制器转场库它能够让你的应用实现类似AppleApache Druid RabbitMQ Firehose 扩展实战指南从配置、原理到源码解析Apache Druid RabbitMQ Firehose 扩展实战指南从配置、原理到源码解析 导读 本文围绕 Apache Druid当前仓库版本 0.数据库数据分析OLAP大数据实时分析数据仓库后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询