数据模型设计:时间戳、维度列、指标列的选择策略

数据模型设计,说白了就是决定「哪些字段进 Druid,分别扮演什么角色」。这一步要是走偏了,后面调优再努力也白搭。我在好几个项目里都见过这种场景——业务方一股脑把所有字段都塞进去,结果查询慢、存储膨胀,最后还得回头重构。

今天咱们就把时间戳、维度列、指标列这三个角色彻底聊透,再深入 Roll-up 和预聚合的核心原理。嗯,这部分是 Druid 的「灵魂」,理解了它,你才算真正入门。

3.1 三列角色的定位与选择

Druid 的数据模型里,每一列都必须明确自己的身份。我习惯用一个简单的类比来理解:

  • 时间戳:数据的「身份证号」,决定了数据落在哪个时间桶里
  • 维度列:数据的「标签」,用来做过滤、分组、下钻
  • 指标列:数据的「数值」,用来做聚合计算

听起来简单吧?但实际选型时坑不少。

3.1.1 时间戳:唯一且必须

Druid 里每条记录都必须有一个时间戳。它决定了数据进入哪个时间分片(Segment)。

核心原则:时间戳必须是单调递增的,且精度要统一。

我曾经遇到过一个坑:业务方把时间戳精度设成了毫秒,但数据源里有一部分是秒级时间戳。结果摄入时 Druid 把秒级时间戳当成了毫秒级,时间直接偏移了 1000 倍。查询时数据全乱套了。

选择时间戳时,我建议你注意这几点:

  • 精度统一:要么全用毫秒,要么全用秒,别混用
  • 时区处理:Druid 默认 UTC,如果你的数据是北京时间,记得在查询时做时区转换
  • 不要用字符串:时间戳字段必须是 LONG 类型,别偷懒用字符串

3.1.2 维度列:精挑细选

维度列是 Druid 构建倒排索引和位图的基础。选多了,存储膨胀;选少了,查询灵活性不够。

我个人习惯遵循「二八原则」:

  • 高频过滤字段:比如用户 ID、城市、设备类型,这些一定要设为维度
  • 低基数优先:基数(唯一值数量)越低的维度,Druid 压缩效果越好
  • 高基数维度要谨慎:比如用户 ID 这种百万级基数的字段,会显著增加 Segment 大小

注意:不要把数值型字段设为维度,除非它真的用于过滤。比如「年龄」字段,如果只是用来做分组统计,应该设为指标列。

3.1.3 指标列:能少则少

指标列是 Druid 做预聚合的对象。每个指标列都会占用存储空间,而且 Roll-up 时会对它们进行聚合计算。

选择指标列时,我建议你问自己三个问题:

  1. 这个数值真的需要聚合吗?
  2. 聚合后还能还原业务含义吗?
  3. 有没有更高效的替代指标?

举个例子:如果你只需要统计「订单数量」,那就只设一个 COUNT 指标,别把「订单金额」也加进来。除非你真的需要 SUM(金额)。

3.2 Roll-up 与预聚合的核心原理

Roll-up 是 Druid 最核心的能力,也是它区别于传统 OLAP 引擎的关键。说白了,Roll-up 就是在摄入阶段对原始数据进行「预聚合」,把多条记录合并成一条。

3.2.1 Roll-up 的工作机制

假设你有这样一批原始数据:

时间戳 城市 设备 PV
2024-01-01 10:00:00 北京 iOS 1
2024-01-01 10:00:00 北京 iOS 1
2024-01-01 10:00:00 北京 Android 1

开启 Roll-up 后,Druid 会按「时间戳 + 所有维度列」做分组,对指标列做聚合。结果变成:

时间戳 城市 设备 PV
2024-01-01 10:00:00 北京 iOS 2
2024-01-01 10:00:00 北京 Android 1

你看,3 条数据变成了 2 条,存储直接省了 33%。

小技巧:Roll-up 的粒度由 granularitySpec 中的 queryGranularity 控制。如果你把粒度设为小时,那同一小时内相同维度的数据都会被合并。

3.2.2 预聚合的代价

Roll-up 不是免费的午餐。它带来的代价主要有两个:

  • 数据精度丢失:原始数据一旦聚合,就无法还原。比如你聚合了 PV,就再也查不到单条记录的详情了
  • 摄入延迟增加:Roll-up 需要在内存中做分组聚合,会消耗额外的 CPU 和内存

我曾经在一个实时流项目里遇到过这个问题:业务方要求秒级延迟,但 Roll-up 的聚合窗口设成了 60 秒。结果数据要等 60 秒才能看到,业务方直接炸了。后来我把窗口调小到 10 秒,虽然存储多了点,但延迟降下来了。

3.2.3 什么时候该用 Roll-up?

我总结了一个简单的决策矩阵:

场景 建议 原因
原始数据量极大(每天百亿级) 开启 Roll-up 存储和查询性能是首要目标
需要精确去重(如 UV) 谨慎开启 Roll-up 会丢失明细,去重可能不准
需要原始明细查询 关闭 Roll-up 保留每条原始记录
维度基数极高(如用户 ID) 关闭 Roll-up Roll-up 效果极差,反而增加开销

3.3 数据模型设计的最佳实践

说了这么多理论,咱们来点实际的。我把自己在多个项目里踩过的坑和总结的经验分享给你。

3.3.1 维度列的「瘦身」技巧

维度列是存储的大头。我见过最夸张的一个案例:业务方把「用户昵称」也设成了维度,结果 Segment 大小直接翻了 10 倍。

我的建议是:

  • 能用 ID 就别用名称:比如用「城市 ID」代替「城市名称」,基数不变但存储更小
  • 低基数维度优先:把高基数维度(如用户 ID)放到指标列里,用 HyperLogLog 做近似去重
  • 避免多值维度:如果一个字段有多个值(如标签),尽量拆成多个单值维度

3.3.2 指标列的「聚合函数」选择

Druid 支持多种聚合函数,不同函数对存储和性能的影响也不同:

聚合函数 适用场景 存储开销
COUNT / SUM 计数、求和 低
LONG_MIN / LONG_MAX 求最小值、最大值 低
DOUBLE_SUM 浮点数求和 中
HyperLogLog 近似去重(如 UV) 中
ThetaSketch 精确去重、集合运算 高

我个人习惯:能用 COUNT 就别用 SUM,能用 LONG 就别用 DOUBLE。浮点数聚合不仅慢,还容易有精度问题。

3.3.3 时间戳的「分段」策略

时间戳的分段粒度直接影响 Segment 的数量和大小。我建议你根据数据量和查询模式来选择:

  • 高频查询(分钟级):用小时分段,Segment 大小适中,查询快
  • 低频查询(天级):用天分段,Segment 数量少,管理方便
  • 超大流量(每天百亿+):用小时分段,甚至半小时分段,避免单个 Segment 过大

经验之谈:单个 Segment 的大小控制在 300MB-700MB 之间是最理想的。太小了查询时 IO 次数多,太大了内存压力大。

3.4 数据模型设计的完整流程

最后,我用一张流程图来总结数据模型设计的完整思路。这张图是我自己项目里用的,每次设计新模型时都会过一遍。

数据模型设计完整流程 1. 需求分析 2. 确定时间戳字段 3. 筛选维度列 基数 < 10万? 是 设为维度列 否 考虑指标列或排除 4. 确定指标列与聚合函数 5. 配置 Roll-up 与分段粒度

这张图的核心逻辑就是:先搞清楚业务需求,再确定时间戳,然后筛选维度列(注意基数判断),最后确定指标列和 Roll-up 配置。每一步都环环相扣,缺一不可。

我的习惯:每次设计新模型时,我都会先画一张类似的流程图,把每个字段的角色标清楚。这样既能避免遗漏,也能让团队其他人一目了然。

3.5 小结

数据模型设计是 Druid 使用的「地基」。地基没打好,上面的查询优化、性能调优都是空中楼阁。

记住三个核心原则:

  • 时间戳:精度统一,时区明确
  • 维度列:精挑细选,低基数优先
  • 指标列:能少则少,聚合函数选对

Roll-up 是 Druid 的「杀手锏」,但要用对地方。数据量大、查询频繁的场景,大胆用;需要明细查询、精确去重的场景,谨慎用。