数据模型设计:时间戳、维度列、指标列的选择策略
数据模型设计,说白了就是决定「哪些字段进 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 时会对它们进行聚合计算。
选择指标列时,我建议你问自己三个问题:
- 这个数值真的需要聚合吗?
- 聚合后还能还原业务含义吗?
- 有没有更高效的替代指标?
举个例子:如果你只需要统计「订单数量」,那就只设一个 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 数据模型设计的完整流程
最后,我用一张流程图来总结数据模型设计的完整思路。这张图是我自己项目里用的,每次设计新模型时都会过一遍。
这张图的核心逻辑就是:先搞清楚业务需求,再确定时间戳,然后筛选维度列(注意基数判断),最后确定指标列和 Roll-up 配置。每一步都环环相扣,缺一不可。
我的习惯:每次设计新模型时,我都会先画一张类似的流程图,把每个字段的角色标清楚。这样既能避免遗漏,也能让团队其他人一目了然。
3.5 小结
数据模型设计是 Druid 使用的「地基」。地基没打好,上面的查询优化、性能调优都是空中楼阁。
记住三个核心原则:
- 时间戳:精度统一,时区明确
- 维度列:精挑细选,低基数优先
- 指标列:能少则少,聚合函数选对
Roll-up 是 Druid 的「杀手锏」,但要用对地方。数据量大、查询频繁的场景,大胆用;需要明细查询、精确去重的场景,谨慎用。