指标引擎:让口径落到每一次查询

chihqiang chihqiang 2026-11-13 约 8 分钟 3176 字

指标定义得再清楚,如果每个页面各写一套查询,口径还是会散掉

上一篇聊了怎么设计指标:名称、口径、时间、维度、状态。但定义只是纸面上的共识,真正决定用户看到什么数字的,是执行这些定义的引擎。

同一句“支付率”,A 页面按自然日算、B 页面按滚动 24 小时算;A 页面排除了取消单、B 页面没排除——只要查询是各写各的,口径就一定会分叉。

指标引擎存在的意义,就是把“定义”变成“唯一的一次执行”。


一、引擎到底负责什么

一个指标引擎,核心只有四件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
指标定义
│
▼
① 注册:指标目录、时间字段、过滤条件、依赖关系
│
▼
② 翻译:逻辑字段 → 物理字段,条件、分组、排序
│
▼
③ 取数:按时间字段建桶,聚合计算
│
▼
④ 计算:间接指标、比率、差值、跨桶合并
│
▼
结果(汇总 / 趋势 / 分组 / 明细)

四件事缺一件,口径就会从某个缝里漏出去:

  • 没有注册:指标散落在各个查询里;
  • 没有翻译:前端直接拼物理字段,索引一变全挂;
  • 没有时间桶:所有指标被逼用同一个时间字段;
  • 没有统一计算:每个页面自己算比率,边界条件各不相同。

二、注册:把定义变成可执行的东西

指标注册不是“建个字典”,而是把一句业务定义拆成引擎能执行的参数:

注册项作用
唯一标识、中文名被引用、被展示
时间字段这个指标按哪个时间统计
过滤条件排除哪些单、哪些状态
取值方式计数、求和、去重、是否存在
是否支持下钻能不能从数字点到明细
依赖指标间接指标依赖哪些直接指标

注册带来三个直接好处:

  1. 新增指标不改引擎:只加一条注册,所有出口自动可用;
  2. 口径只有一处:过滤、时间、取值方式不会在页面之间漂移;
  3. 可测试:每个指标都能单独验证,不依赖页面。

指标分三类就够了:

  • 直接指标:一个聚合、一个时间字段、一个取值方式;
  • 间接指标:依赖若干直接指标,只做计算(比率、差值、求和、换算);
  • 指标目录:按业务域把指标分组,供查询和展示使用。

不需要第四类。类型越多,边界越模糊。


三、翻译:逻辑字段到物理字段

引擎对外暴露的应该是逻辑字段:店铺、渠道、品类、品牌、会员等级。内部再翻译成索引里的物理字段。

翻译层要覆盖四个地方:

  1. 过滤条件:等值、集合、范围、存在、模糊匹配;
  2. 分组字段:按什么拆,拆几层;
  3. 排序字段:按指标值还是按维度值;
  4. 明细字段:下钻和导出时展示哪些列。

只翻译过滤、不翻译分组和明细,就会出现“指标条件对、明细条件错”的经典问题。

翻译层还有两个必须遵守的规则:

  • 未知字段要报错,不能忽略:忽略等于放宽条件,数据会多出来;
  • 条件只能更严格,不能更宽松:取值集合为空、权限范围为空,都应该返回零行,而不是退化成不筛选。

这两条看起来是防错,实际是防“数据越权”。


四、时间桶:一个请求,多个时间

这是指标引擎和普通查询最不一样的地方。

普通查询只有一个时间范围;指标引擎要为每个指标自己的时间字段建时间桶:

1
2
3
按创建时间统计的指标  → 创建时间桶
按支付时间统计的指标 → 支付时间桶
按退款时间统计的指标 → 退款时间桶

一次请求里,引擎收集所有指标的时间字段,去重后分别建桶,再把结果按时间合并成一条时间轴。

为什么不能统一用一个时间字段?

  • 用创建时间算支付量,会把“上周下单、这周支付”的单算到上周;
  • 用支付时间算退款额,会把“这周退款、上周支付”的退款算到这周;
  • 每个指标的口径都会偏,但页面看起来“有数”,更难发现。

时间桶还有粒度问题:按小时、按天、按周、按月。粒度是查询参数,但边界必须由引擎统一计算,不能让每个页面自己算“上周是从哪天到哪天”。


五、条件下推:把范围推到查询层

如果只按时间桶过滤,查询引擎会先把整个索引扫一遍,再在内存里聚合。数据量一大,就会慢得没有道理。

更高效的做法是把时间范围下推到查询层,并且多个时间字段用“或”的关系取并集:

1
创建时间在区间内 或 支付时间在区间内 或 退款时间在区间内

为什么这样不会改变结果?

因为任何会进入某个时间桶的数据,必然在它自己的时间字段上落在区间内。先按并集缩小扫描范围,再在桶里聚合,结果和全量扫描一致。

这里有一个容易忽略的细节:明细下钻也要用同一套并集条件。如果指标用并集、明细只用创建时间,就会出现“指标里有这单、明细里查不到”的情况。


六、间接指标:依赖与计算顺序

间接指标是引擎里最容易出错的部分,因为它涉及“先算谁、后算谁”。

基本做法:

  1. 每个间接指标声明自己依赖哪些直接指标;
  2. 引擎先算被依赖的指标,再算依赖它的指标,顺序不能乱;
  3. 如果依赖成环,直接拒绝,而不是悄悄算错。

计算时要注意三个边界:

  • 分母为零:返回空或者零,不要返回无穷大或非法值;
  • 数据缺失:依赖的指标没有值时,按“零”还是“不可计算”处理,要统一;
  • 跨桶合并:比率类指标必须先把分子分母跨时间桶合并,再做除法,不能在每个桶里分别除再加权。

最后一条最容易被忽略:完成率、支付率这类指标,分子分母的时间字段可能不同。先除后加,和先加后除,结果是不一样的。


七、明细下钻:指标和明细必须同一套条件

“你这个数点开,明细怎么对不上?”——这是统计平台最常被质疑的一句话。

根因通常是:指标用聚合算,明细用检索查,两边条件写法不同。

引擎层的约束应该是:

  • 指标只有一个最外层过滤条件时,才支持明细下钻;
  • 多一层过滤的指标,直接标记“不支持”,而不是给一张对不上的明细;
  • 明细分页用游标,不用偏移量;
  • 明细只返回白名单字段,敏感信息不进明细。

与其给用户一份对不上的明细,不如明确告诉他“这个指标不支持看明细”。前者损失的是信任,后者只是少一个功能。


八、缓存与并发

指标查询通常很重,缓存能救急,但缓存错了比不缓存更糟。

几条原则:

  • 只缓存纯聚合查询:带明细、带游标的查询不缓存;
  • 缓存键要包含全部参数:指标、时间、过滤、分组、粒度,缺一个就会串数据;
  • 缓存时间要短:统计数据的“新鲜”比“省一次查询”更重要;
  • 并发相同的查询要合并:同一时刻多个页面请求同一个指标,只发一次查询。

并发控制同样重要:

  • 限制单个请求的聚合规模;
  • 限制时间跨度和返回条数;
  • 超时要能中断,而不是让用户干等。

九、可观测性:出了问题能定位

指标引擎的日志,要能回答三个问题:

  1. 这次查询慢在哪:扫描量、聚合规模、时间桶数量;
  2. 哪个指标或条件出了问题:指标名、时间范围、过滤条件;
  3. 这次失败该找谁:依赖没读到、参数非法、下游超时,错误码要能区分。

几个具体做法:

  • 每次查询打一行“形状”:时间字段数、指标数、聚合规模;
  • 单个指标耗时超过阈值,打慢查询日志;
  • 错误信息带上指标和条件,不要只返回“查询失败”。

没有这些日志,排查一个“数字不对”的问题,可能要花一整天。


十、常见坑

  1. 所有指标共用一个时间字段:口径错,但页面看起来正常;
  2. 只翻译过滤条件,不翻译分组和明细:指标和明细对不上;
  3. 比率在桶内先除再合并:跨桶后结果偏差;
  4. 分母为零返回无穷大:页面会显示异常值;
  5. 非法条件被静默忽略:数据变多,甚至越权;
  6. 缓存键漏了参数:换一个筛选还返回旧数据;
  7. 明细用偏移量分页:越翻越慢,还可能重复;
  8. 依赖成环没有检测:指标值随机变化,难以复现;
  9. 聚合规模不设上限:一次分组把服务打挂;
  10. 错误只返回“查询失败”:无法定位是哪个指标、哪个条件。

十一、结语

指标引擎不神秘,它就是把“定义”拆成四步:注册、翻译、取数、计算,然后保证每一步只有一种做法。

做到这一点,页面之间的数字才会一致;做不到,定义写得再漂亮,也只是贴在墙上的文档。

分享: Twitter Facebook
chihqiang

chihqiang

代码与生活的点滴记录