指标定义得再清楚,如果每个页面各写一套查询,口径还是会散掉
上一篇聊了怎么设计指标:名称、口径、时间、维度、状态。但定义只是纸面上的共识,真正决定用户看到什么数字的,是执行这些定义的引擎。
同一句“支付率”,A 页面按自然日算、B 页面按滚动 24 小时算;A 页面排除了取消单、B 页面没排除——只要查询是各写各的,口径就一定会分叉。
指标引擎存在的意义,就是把“定义”变成“唯一的一次执行”。
一、引擎到底负责什么
一个指标引擎,核心只有四件事:
1 | 指标定义 |
四件事缺一件,口径就会从某个缝里漏出去:
- 没有注册:指标散落在各个查询里;
- 没有翻译:前端直接拼物理字段,索引一变全挂;
- 没有时间桶:所有指标被逼用同一个时间字段;
- 没有统一计算:每个页面自己算比率,边界条件各不相同。
二、注册:把定义变成可执行的东西
指标注册不是“建个字典”,而是把一句业务定义拆成引擎能执行的参数:
| 注册项 | 作用 |
|---|---|
| 唯一标识、中文名 | 被引用、被展示 |
| 时间字段 | 这个指标按哪个时间统计 |
| 过滤条件 | 排除哪些单、哪些状态 |
| 取值方式 | 计数、求和、去重、是否存在 |
| 是否支持下钻 | 能不能从数字点到明细 |
| 依赖指标 | 间接指标依赖哪些直接指标 |
注册带来三个直接好处:
- 新增指标不改引擎:只加一条注册,所有出口自动可用;
- 口径只有一处:过滤、时间、取值方式不会在页面之间漂移;
- 可测试:每个指标都能单独验证,不依赖页面。
指标分三类就够了:
- 直接指标:一个聚合、一个时间字段、一个取值方式;
- 间接指标:依赖若干直接指标,只做计算(比率、差值、求和、换算);
- 指标目录:按业务域把指标分组,供查询和展示使用。
不需要第四类。类型越多,边界越模糊。
三、翻译:逻辑字段到物理字段
引擎对外暴露的应该是逻辑字段:店铺、渠道、品类、品牌、会员等级。内部再翻译成索引里的物理字段。
翻译层要覆盖四个地方:
- 过滤条件:等值、集合、范围、存在、模糊匹配;
- 分组字段:按什么拆,拆几层;
- 排序字段:按指标值还是按维度值;
- 明细字段:下钻和导出时展示哪些列。
只翻译过滤、不翻译分组和明细,就会出现“指标条件对、明细条件错”的经典问题。
翻译层还有两个必须遵守的规则:
- 未知字段要报错,不能忽略:忽略等于放宽条件,数据会多出来;
- 条件只能更严格,不能更宽松:取值集合为空、权限范围为空,都应该返回零行,而不是退化成不筛选。
这两条看起来是防错,实际是防“数据越权”。
四、时间桶:一个请求,多个时间
这是指标引擎和普通查询最不一样的地方。
普通查询只有一个时间范围;指标引擎要为每个指标自己的时间字段建时间桶:
1 | 按创建时间统计的指标 → 创建时间桶 |
一次请求里,引擎收集所有指标的时间字段,去重后分别建桶,再把结果按时间合并成一条时间轴。
为什么不能统一用一个时间字段?
- 用创建时间算支付量,会把“上周下单、这周支付”的单算到上周;
- 用支付时间算退款额,会把“这周退款、上周支付”的退款算到这周;
- 每个指标的口径都会偏,但页面看起来“有数”,更难发现。
时间桶还有粒度问题:按小时、按天、按周、按月。粒度是查询参数,但边界必须由引擎统一计算,不能让每个页面自己算“上周是从哪天到哪天”。
五、条件下推:把范围推到查询层
如果只按时间桶过滤,查询引擎会先把整个索引扫一遍,再在内存里聚合。数据量一大,就会慢得没有道理。
更高效的做法是把时间范围下推到查询层,并且多个时间字段用“或”的关系取并集:
1 | 创建时间在区间内 或 支付时间在区间内 或 退款时间在区间内 |
为什么这样不会改变结果?
因为任何会进入某个时间桶的数据,必然在它自己的时间字段上落在区间内。先按并集缩小扫描范围,再在桶里聚合,结果和全量扫描一致。
这里有一个容易忽略的细节:明细下钻也要用同一套并集条件。如果指标用并集、明细只用创建时间,就会出现“指标里有这单、明细里查不到”的情况。
六、间接指标:依赖与计算顺序
间接指标是引擎里最容易出错的部分,因为它涉及“先算谁、后算谁”。
基本做法:
- 每个间接指标声明自己依赖哪些直接指标;
- 引擎先算被依赖的指标,再算依赖它的指标,顺序不能乱;
- 如果依赖成环,直接拒绝,而不是悄悄算错。
计算时要注意三个边界:
- 分母为零:返回空或者零,不要返回无穷大或非法值;
- 数据缺失:依赖的指标没有值时,按“零”还是“不可计算”处理,要统一;
- 跨桶合并:比率类指标必须先把分子分母跨时间桶合并,再做除法,不能在每个桶里分别除再加权。
最后一条最容易被忽略:完成率、支付率这类指标,分子分母的时间字段可能不同。先除后加,和先加后除,结果是不一样的。
七、明细下钻:指标和明细必须同一套条件
“你这个数点开,明细怎么对不上?”——这是统计平台最常被质疑的一句话。
根因通常是:指标用聚合算,明细用检索查,两边条件写法不同。
引擎层的约束应该是:
- 指标只有一个最外层过滤条件时,才支持明细下钻;
- 多一层过滤的指标,直接标记“不支持”,而不是给一张对不上的明细;
- 明细分页用游标,不用偏移量;
- 明细只返回白名单字段,敏感信息不进明细。
与其给用户一份对不上的明细,不如明确告诉他“这个指标不支持看明细”。前者损失的是信任,后者只是少一个功能。
八、缓存与并发
指标查询通常很重,缓存能救急,但缓存错了比不缓存更糟。
几条原则:
- 只缓存纯聚合查询:带明细、带游标的查询不缓存;
- 缓存键要包含全部参数:指标、时间、过滤、分组、粒度,缺一个就会串数据;
- 缓存时间要短:统计数据的“新鲜”比“省一次查询”更重要;
- 并发相同的查询要合并:同一时刻多个页面请求同一个指标,只发一次查询。
并发控制同样重要:
- 限制单个请求的聚合规模;
- 限制时间跨度和返回条数;
- 超时要能中断,而不是让用户干等。
九、可观测性:出了问题能定位
指标引擎的日志,要能回答三个问题:
- 这次查询慢在哪:扫描量、聚合规模、时间桶数量;
- 哪个指标或条件出了问题:指标名、时间范围、过滤条件;
- 这次失败该找谁:依赖没读到、参数非法、下游超时,错误码要能区分。
几个具体做法:
- 每次查询打一行“形状”:时间字段数、指标数、聚合规模;
- 单个指标耗时超过阈值,打慢查询日志;
- 错误信息带上指标和条件,不要只返回“查询失败”。
没有这些日志,排查一个“数字不对”的问题,可能要花一整天。
十、常见坑
- 所有指标共用一个时间字段:口径错,但页面看起来正常;
- 只翻译过滤条件,不翻译分组和明细:指标和明细对不上;
- 比率在桶内先除再合并:跨桶后结果偏差;
- 分母为零返回无穷大:页面会显示异常值;
- 非法条件被静默忽略:数据变多,甚至越权;
- 缓存键漏了参数:换一个筛选还返回旧数据;
- 明细用偏移量分页:越翻越慢,还可能重复;
- 依赖成环没有检测:指标值随机变化,难以复现;
- 聚合规模不设上限:一次分组把服务打挂;
- 错误只返回“查询失败”:无法定位是哪个指标、哪个条件。
十一、结语
指标引擎不神秘,它就是把“定义”拆成四步:注册、翻译、取数、计算,然后保证每一步只有一种做法。
做到这一点,页面之间的数字才会一致;做不到,定义写得再漂亮,也只是贴在墙上的文档。