业务问“本周有多少不同客户”,查询系统拿到七个每日客户数后做求和,SQL可能完全合法,答案却回答成了客户日数。问题不在加法,而在聚合时丢失了重叠信息。本文讨论去重对象数的计算契约及近似加速边界;样例均为构造数据,未执行生产数据库或HLL性能实验。
先确定对象和集合,再给去重结果命名
转化率的分子分母契约要求明确合格对象与观察范围;去重人数同样需要这些前提。用户、账号、设备和企业客户并不是同一种对象。把设备标识去重后称为“人数”,或把两个系统的同名客户直接合并,都会让精确计算得到语义错误的数字。
建议冻结对象键、命名空间、身份映射版本、纳入事件、时间区间、业务时区、数据截止和权限范围。例如指标定义为:在指定自然周内至少发生一次有效咨询、且具有已确认企业客户ID的不同客户数量。同一客户咨询十次仍只计一次,未知身份单独报告。
跨租户的本地编号可能重复。若甲乙租户都有客户001,且业务定义中二者不是同一客户,就应使用带命名空间的对象身份;如果目标是集团统一客户,则必须先取得受控的统一映射。简单拼接字符串也要避免边界歧义,应采用有类型、可解码的复合键编码。
缺少客户ID不能统一填成一个“未知客户”后参与正式人数,否则所有缺失对象会被压成一个;也不能无提示排除。本文建议同时返回已识别客户去重数、缺失身份记录数和数据覆盖状态,是否允许发布总体结论由指标契约决定。
一个六行样例,拆开记录数、客户日数与客户数
构造六条有效咨询记录:第一天搜索渠道客户A、搜索渠道客户B、活动渠道客户A;第二天搜索渠道客户B、活动渠道客户C、活动渠道客户C。它们共有六条事件,第一天不同客户为A和B,第二天为B和C,因此每日去重数分别为2和2。
两天不同客户的集合是A、B、C,总数为3,不是每日去重数相加得到的4。4可以命名为客户日数,因为B在两个日期各贡献一次。搜索渠道的不同客户为A、B,活动渠道为A、C,两个渠道各为2;跨渠道总体仍为3,不能把多触点人数和当成总体客户数。
更细的日期加渠道分组,其计数是2、1、1、1,求和为5。它既不等于事件数6,也不等于客户日数4或总体客户数3。改变分组粒度后总数变化,暴露的是集合重叠信息被丢弃,而不是数据库的COUNT函数突然失灵。
仅知道两天各有2位客户,无法恢复两天总人数:两天完全相同则为2,完全不同则为4,本例是3。对任意两个集合,总体去重数等于两个数量之和减去交集数量;如果交集未知,不能让语言模型估计一个看似合理的重叠率来补全正式指标。
选择精确对象集,还是可合并的近似草图
精确路径是在目标筛选范围内对对象键重新去重,再计数;示例的关系运算是先保留符合资格的六行,再选出不同客户键,得到三行。可以维护按日的去重对象明细来减少重复事件,但周汇总仍要对跨日对象再去重。只有分组确实互斥时,分组计数才可直接相加。
当对象量大、查询频繁且业务允许近似时,可以保存HLL或HLL++草图。它保留用于估计基数的压缩统计状态,而不是客户名单。BigQuery把构建、合并和提取估计值分成不同函数,并说明APPROX_COUNT_DISTINCT的最终数值不支持同样的部分聚合与再聚合路径。[1]
Trino文档展示了按日保存HyperLogLog,再合并为周度估计的做法。[3] 核心区别是先合并草图、后提取一个估计数,而不是先提取七个日估计再求和。即使每个日估计都非常准确,相加也不会自动消除跨日重复。
精确去重适合需要对象名单、结算对账或边界不能容忍近似的任务;草图适合被明确标注的趋势探索和大规模重复汇总。不要仅因数据量大就默认近似,也不要把一次小样本上恰好相等的结果,当作算法已经变成精确计数。最终选择需比较目标负载下的成本与误差。
草图的分区与版本决定它能回答哪些问题
建议每份草图同时保存指标版本、对象身份版本、时间分区、分组维度、来源截止、完整性标记、引擎与序列化版本、输入类型和精度配置。二进制字段能读出来,不证明语义和格式都可合并。不同系统都叫HLL,也不能据此推定其字节格式、哈希与参数相容。
以BigQuery为例,官方函数要求合并草图的底层输入类型一致;不同精度合并时会降到参与合并的最低精度。[1] 因此建议写入时锁定配置,查询时校验清单,拒绝未经批准的混版。把字符串001和整数1当作同一对象与否,应在构建之前确定,而不是交给草图猜测。
分区还约束查询能力。若只保存每天全部客户的草图,后来询问某个产品线的客户数,无法从该草图中取出该产品线的对象;需要原始明细或预先按所需维度构建的状态。维度组合越多,维护与存储越高,应由真实查询需求驱动,避免无限生成组合。
授权也必须先于合并。一个已经混入所有部门的草图,不能通过隐藏部门名称变成只属于当前用户的结果。对行级权限变化频繁的指标,优先使用能够按当前授权过滤的明细路径,或维护经过验证的授权分区;聚合压缩不是权限隔离机制。
迟到可以补,删除不能靠从估计值减一解决
追加一个已存在客户的事件,不应增加集合基数;追加一个新客户才可能增加。对于兼容的集合草图,合并状态用于表示并集,但完整性仍依赖上游是否真的交齐。应记录每个预期日期与来源分区的就绪状态,不能把缺失分区当作空集合。
BigQuery文档明确指出不能从HLL++草图删除值。[2] 这意味着一条事件撤销、客户归属更正或身份映射调整,不能简单在旧估计数上减一:该客户可能还有其他合格事件,其他日期也可能出现,旧草图还保留着此前贡献。
可操作的办法是保留能够重建的受控明细,并按依赖关系重算受影响的日期和维度分区,再替换派生的周月结果。替换应使用新版本原子生效或等效的完整切换机制。已经混入旧分区的累计草图不能通过再合并新分区来移除旧贡献,必须从正确的分区集合重新构建。
滚动七天窗口同理:选择窗口内七份有效日草图重新合并,而不是从一个长期累计草图中减去最早一天的估计数。若原始明细已经过期、没有可重建的数据,必须说明无法精确修复的范围,不能为保持连续曲线偷偷保留已知错误。
分别验收集合语义、分区完整性和统计误差
第一层用前述六行固定样例验证语义:事件数6、两日客户日数4、整体客户数3、四个日期渠道格子的计数和5。追加任意一条重复事件,整体客户数仍为3;把同一输入拆成不同批次后合并,对象总体应保持一致。精确路径必须返回明确的预期值。
第二层检查完整性与恢复。故意缺少第二天分区,系统应报告不完整,不能把第一天的2显示为两天总体。再撤销第二天客户C的两条事件,精确总体应变为2;测试草图重建、旧版本退役及周汇总刷新。仅撤销其中一条C事件则仍为3,这个反例能发现错误的减一处理。
第三层才比较近似误差。在相同身份、资格、过滤和截止条件下,用精确去重作参照,按低中高基数、重复程度、日期和渠道覆盖分层。记录绝对误差;参照值大于零时再计算相对误差,真实空集单独验收。不要让多个分组的正负误差相互抵消后只报告总平均。
Trino对approx_distinct默认标准误差的说明是2.3%,同时明确这不是任意具体集合的误差上界。[4] 因此不能承诺每次查询都在2.3%以内。固定集合重复执行通常也不能形成新的独立统计样本;应覆盖多个有代表性的不同集合,并在实际引擎版本上验证误差分布。
把近似属性交给回答层,保留精确回查出口
若业务只想判断客户规模大致增长,近似结果可能足够;若要决定是否跨过严格的资格门槛,边界附近应转精确查询。具体误差预算必须来自用途,而不是由算法默认值决定。模型不能把“约一万名客户”自动改写成精确到个位的承诺。
返回结果建议携带指标版本、对象定义、时间范围、数据截止、精确或近似标记、引擎与精度、完整性状态,以及必要的误差说明。近似值的不确定性与数据缺失是两种不同问题,后者不能被前者的误差区间吸收。未到齐的数据不能标成已完整的近似总体。
上线对照应同时记录查询资源、延迟、草图构建和重建费用,以及精确回查的比例。若日常节省查询成本,却在身份规则频繁变化时大量重算,收益可能被抵消。对低频分析,直接精确计算可能更容易维护;对稳定高频指标,再考虑物化草图。
最终交付的是对象契约、可回放样例、兼容性清单、分区就绪证据、删除重建演练和分层误差报告。本文的六行例子只用于解释集合算术,不代表任何产品的HLL精度实测。让智能问数知道何时必须回到对象集合,比让它更快地相加一列数字更重要。
