ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PostHog 实验查询运行器深入指南:漏斗评估表达式与统计参数配置

PostHog 实验查询运行器深入指南:漏斗评估表达式与统计参数配置 PostHog 实验查询运行器深入指南漏斗评估表达式与统计参数配置【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog导读本文聚焦 PostHog 实验Experiments后端查询运行器query runner中最核心的两个技术主题漏斗Funnel指标评估表达式的构建原理以及**可配置统计参数stats_config**的完整结构与校验规则。你将掌握aggregate_funnel_arrayUDF 的输入输出契约、步骤条件预计算的设计动机与实现方式、事件数组的构造细节、漏斗结果评估逻辑以及 Bayesian / Frequentist 两类统计方法的全部可调参数及默认值。文中所有结论均有仓库源码与测试用例佐证可直接用于理解实验漏斗查询的执行链路或在此基础上进行二次开发。关联文档products/experiments/backend/hogql_queries/README.md一、为什么实验漏斗查询需要表达式级文档说明PostHog 实验的漏斗指标在查询层面有一套不同于普通产品分析漏斗的特殊约束实验需要把每个用户的转化行为与实验曝光exposure和变体variant归属绑定在一起而完成这一逻辑的aggregate_funnel_arrayUDF 对输入格式有严格要求——它要求每个事件以元组数组的形式传入且每个元组必须携带时间戳、事件 ID、分组值breakdown value和命中的步骤编号。由于构造这种输入格式的 SQL 相对复杂README.md 用一整篇文档专门拆解了查询构造的各个部分。本文在此基础上结合 experiment_funnel_query_builder.py 与 base_query_utils.py 的源码实现还原完整的执行链路。二、漏斗步骤预计算把属性解析留在 SQL 层2.1 设计动机避免 UDF 内的属性解析问题当漏斗步骤带有属性过滤器时例如按wizard_step step_1过滤事件如果把这些过滤器直接塞进 UDF 内部解析会遇到属性解析property resolution层面的问题——尤其是嵌套属性、复杂操作符等场景。因此实验查询运行器要求步骤条件在指标事件查询metric events query中预先计算而不是在 UDF 内部解析。这一设计带来的三个核心收益在 README.md 中明确列出属性解析Property resolution复杂属性过滤器包括嵌套属性在 SQL 层面解析HogQL 类型系统在这里能正确工作性能Performance属性过滤在查询管线早期完成尽早缩小数据量兼容性Compatibility所有属性类型和操作符都可正常工作不受 UDF 自身能力限制。2.2 指标事件查询中的步骤条件预计算在漏斗指标的_get_metric_events_query()中每个步骤条件被计算为独立的布尔列。README 给出了完整示例SELECT events.timestamp, events.person_id AS entity_id, exposure_data.variant, events.event, events.uuid, events.properties, if(and(equals(events.event, $pageview), equals(events.properties.wizard_step, step_1)), 1, 0) AS step_0, if(and(equals(events.event, $pageview), equals(events.properties.wizard_step, step_2)), 1, 0) AS step_1 FROM events INNER JOIN exposure_data ON events.person_id exposure_data.entity_id WHERE ...源码印证FunnelStepBuilder仓库中负责生成这些步骤列的正是 funnel_step_builder.py 中的FunnelStepBuilder类。它支持两种构建模式见类注释 L27-L32布尔列模式build_boolean_columns用于纯事件漏斗所有步骤在单个查询中针对 events 表求值每个步骤是一个布尔表达式常量列模式build_constant_columns用于含数据仓库Data Warehouse源的UNION ALL查询每个子查询代表一个步骤活动步骤置 1、其余置 0。关键实现细节num_steps len(series) 1——步骤总数包含曝光步骤step_0L54-L55build_boolean_columns中step_0直接使用曝光条件表达式exposure_filter而step_1..N用if(step_filter, 1, 0)包裹L83-L99步骤过滤条件由event_or_action_to_filter()生成base_query_utils.py它统一处理EventsNode按事件名匹配与ActionsNode按动作 ID 匹配并叠加properties属性过滤。build_boolean_columns的 docstring 示例L71-L79 builder FunnelStepBuilder([ ... EventsNode(eventpageview), ... EventsNode(eventpurchase) ... ], team) exposure_filter parse_expr(event $feature_flag_called) columns builder.build_boolean_columns(exposure_filter) [col.alias for col in columns] [step_0, step_1, step_2]两条执行路径legacy3-CTE与 optimized单扫描FunnelQueryBuilder.build_funnel_query()会根据条件在两条路径间分派experiment_funnel_query_builder.py#L51-L79Legacy 路径build_funnel_query_legacy3 个 CTEexposures→metric_events→entity_metrics主要服务于预计算precomputed场景和含 DW 步骤的 UNION ALL 漏斗Optimized 路径build_funnel_query_optimized消除对 events 表的第二次扫描和中间 JOIN有序漏斗用 2 个 CTEbase_events→entity_metrics无序漏斗用 3 个 CTE额外增加first_exposures做时间过滤。在 legacy 路径的metric_eventsCTE 中WHERE 条件为(exposure_predicate OR funnel_steps_filter)随后build_funnel_step_columns()将步骤列注入 SELECTL287-L298。2.3 预计算路径从events表直接读取步骤数组值得一提的相关优化当启用了指标事件预计算时metric_eventsCTE 不再扫描 events 表而是读取experiment_metric_events_preaggregated表并通过arrayElement(t.steps, N)从打包的Array(UInt8)中解出每一步的布尔值。这一步的具体实现与数据流详见仓库内的姊妹文档 METRIC_EVENTS_PRECOMPUTATION.md。三、UDF 步骤条件构造把布尔列变成步骤编号预计算出的step_0、step_1等布尔列随后被funnel_evaluation_expr()消费。README 中展示了其核心形式multiply(1, metric_events.step_0), multiply(2, metric_events.step_1),在 base_query_utils.py 的funnel_evaluation_expr中这一逻辑以程序化方式生成L490-L490step_conditions [f{i 1} * {events_alias}.step_{i} for i in range(num_steps)]即第i个步骤条件 (i1) * step_i。目的为每个事件创建数值型步骤标识符逻辑如果事件匹配某步骤条件返回该步骤的编号1、2、3…否则返回 0结果每个事件被标记为满足了漏斗中的哪些步骤。注意这里的步骤编号从 1 开始step_0对应编号 1即曝光步骤这保证了arrayFilter(x - x 0, ...)能够恰好滤掉未命中任何步骤的事件。四、事件数组构造UDF 的主输入这是传给aggregate_funnel_array函数的主输入。查询部分将每个用户的全部事件转换为函数所需的格式——一个元组数组其中每个元素代表该用户的一个事件包含时间戳、事件标识以及该事件是否满足漏斗中的某些步骤。arraySort(t - t.1, groupArray(tuple( timestamp_float, -- 排序键时间戳 uuid, -- 事件标识 array(), -- 分组值空 无分组 arrayFilter(x - x 0, [...]) -- 该事件命中的步骤编号 )))在funnel_evaluation_expr的完整表达式中实际生成的是base_query_utils.py#L521-L529arraySort(t - t.1, arrayFilter( t - isNotNull(t.1) AND isNotNull(t.2), groupArray(tuple( toFloat(timestamp_field), uuid_field, array(), arrayFilter(x - x 0, [step_conditions]) )) ))各部分职责排序按时间戳t.1排序确保事件按时间先后排列过滤arrayFilter(x - x 0, [...])移除 0 与 NULL只保留真实命中的步骤编号空值防御外层arrayFilter(t - isNotNull(t.1) AND isNotNull(t.2), ...)过滤掉时间戳或 UUID 为空的事件示例结果[(1704110400.0, uuid1, , [1]), (1704110700.0, uuid2, , [2])]五、UDF 函数调用五个核心参数README 给出了aggregate_funnel_array的完整调用形态aggregate_funnel_array( 3, -- 漏斗步骤数 3600, -- 转化窗口1 小时 first_touch, -- 归因方式使用分组值的首次出现 ordered, -- 顺序事件必须按序发生 array(array()), -- 分组值空 - 无分组 events_array -- 上述预处理好的事件数据 )参数逐个解读参数示例值含义步骤数3漏斗中的步骤总数含曝光步骤时需相应增加转化窗口秒3600首尾步骤之间的最大时间间隔3600 秒 1 小时归因方式first_touch仅在启用分组breakdown时有意义实验场景通常不分组顺序模式orderedordered表示步骤 2 必须发生在步骤 1 之后、步骤 3 在步骤 2 之后分组值array(array())空值表示无分组事件数组events_array前述预处理后的事件数据源码中的默认值与细节转化窗口当指标未显式配置conversion_window/conversion_window_unit时funnel_evaluation_expr默认取3 年3 * 365 * 24 * 60 * 60秒base_query_utils.py#L478-L479确保选中时间段内的所有事件都被纳入顺序模式funnel_order_type未设置时默认orderedbase_query_utils.py#L495单位换算conversion_window_to_seconds()支持 SECOND / MINUTE / HOUR / DAY / WEEK / MONTH 六种单位其中 MONTH 按 30 天折算base_query_utils.py#L245-L258无序漏斗当funnel_order_type UNORDERED时UDF 不做曝光必须先于转化的时间约束因此 legacy 路径会在 JOIN 时附加AND metric_events.timestamp exposures.first_exposure_time的时间过滤experiment_funnel_query_builder.py#L159-L178optimized 路径则通过first_exposuresCTE INNER JOIN 实现同样的语义L443-L493。返回值每个用户的元组数组UDF 返回每个用户的元组数组每个分组值一个元素由于实验不关心分组值数组内只有一个元素元组结构如下step_reached完成的最高步骤0 索引因此2表示完成全部 3 个步骤breakdown_values分组属性值数组实验场景下为空conversion_times步骤间耗时数组[step1→step2, step2→step3]event_uuids每个步骤所用事件的 UUID 数组。注意funnel_evaluation_expr实际取用的是result.1最高步骤与result.4事件 UUID 数组通过arraySort(x - -x.1, ...)[1]选出最高完成步骤对应的结果元组base_query_utils.py#L501-L532同时把命中的首个事件 UUID 一并带出。六、结果评估完整漏斗判定最后一步是判断用户是否完成了完整漏斗返回 1或未完成返回 0。评估逻辑如下aggregate_funnel_array为每个用户返回一个元组数组每个分组值一个元素实验不关心分组值因此数组只有一个元素先过滤数组只保留漏斗完成的元素即满足step_reached num_steps - 1step_reached是 0 索引的若过滤后列表非空length 0返回 1否则返回 0。最终聚合查询中的体现这一是否完成判定最终体现在entity_metricsCTE 后的外层 SELECT 中experiment_funnel_query_builder.py#L253-L268SELECT entity_metrics.variant AS variant, count(entity_metrics.entity_id) AS num_users, countIf(entity_metrics.value.1 num_steps_minus_1) AS total_sum, countIf(entity_metrics.value.1 num_steps_minus_1) AS total_sum_of_squares FROM entity_metrics WHERE notEmpty(variant) GROUP BY entity_metrics.variant源码注释明确说明L256-L258漏斗评估的返回值是零索引的。到达第一步返回 0以此类推到达最后一步返回num_steps - 1。因此用value.1 num_steps - 1判定完整漏斗。此外为了渲染漏斗图查询还会追加step_counts列——统计到达每一步的用户数L300-L307tuple(countIf(value.1 1), countIf(value.1 2), ...) AS step_counts七、参数可配置性stats_config完整指南实验支持通过stats_config字段配置统计参数允许用户自定义 Bayesian 与 Frequentist 两类统计方法的行为。7.1 配置结构stats_config是一个按方法名分键的 JSON 对象{ bayesian: { ci_level: 0.95, difference_type: RELATIVE, prior_type: RELATIVE }, frequentist: { alpha: 0.05, difference_type: RELATIVE } }7.2 Bayesian 参数参数类型默认值说明ci_levelfloat0.95可信区间Credible interval水平取值必须在 0 与 1 之间difference_typestringRELATIVE差异计算方式RELATIVE为相对基线的百分比变化ABSOLUTE为相对基线的绝对差prior_typestringRELATIVE先验类型RELATIVE为相对基线的先验ABSOLUTE为绝对先验7.3 Frequentist 参数参数类型默认值说明alphafloat0.05显著性水平取值必须在 0 与 1 之间difference_typestringRELATIVE差异计算方式RELATIVE为相对基线的百分比变化ABSOLUTE为绝对差7.4 校验与默认值回退根据 README.md 与测试用例 test_stats_config.py配置遵循以下回退规则数值越界或非数值如alpha: 5.0、alpha: -0.5、ci_level: 1.5→ 回退到默认值L185-L230枚举值拼写错误或类型错误如difference_type: INVALID、difference_type: 123→ 回退到默认值L112-L150参数缺失→ 使用默认值null或空配置→ 全部使用默认值。测试还验证了配置确实影响统计输出ci_level从 0.90 调到 0.99 会实际改变可信区间宽度test_bayesian_ci_level_actually_affects_interval_widthL152-L182空配置None/{}/{frequentist: {}}下统计计算不会报错test_frequentist_defaults/test_bayesian_defaultsL51-L110数据不足时样本过小、基线总和为零、方差为零返回原始值而不产出统计推断L27-L31。7.5 服务层的补充校验除了统计数值本身的校验实验服务层 experiment_service.py 的validate_stats_config()还会校验stats_config的结构形态、method取值以及baseline_variant_key基线变体键L908-L924确保配置能够正确定位基线变体用于对比计算。八、延伸阅读METRIC_EVENTS_PRECOMPUTATION.md漏斗指标事件预计算的完整设计写路径、读路径、转化窗口延伸、与曝光预计算的差异LAZY_COMPUTATION.md惰性计算系统按天分窗、任务管理、TTLexperiment_funnel_query_builder.py漏斗查询构建器legacy 与 optimized 双路径funnel_step_builder.py步骤列构建布尔列 / 常量列base_query_utils.pyfunnel_evaluation_expr()与转换窗口工具funnel_validation.pyDW 漏斗配置校验必填字段、join key 一致性、复杂度上限最多 3 个 DW 步骤 / 2 张不同 DW 表测试佐证test_funnel_metric.py、test_stats_config.py。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进