PostHog HogQL 慢查询深度剖析:从 query_log_archive 定位、归因并根因分析用户与 AI 编写的任意 SQL
【免费下载链接】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
HogQLQuery 是 PostHog ClickHouse 慢查询报告中唯一一个“用户或 AI 想怎么写就怎么写”的分析桶,它的慢由因与产品级 insights(Trends、Funnels 等)截然不同:没有强制的日期范围、没有感知物化列的属性访问、可以任意 join。本文基于仓库中.agents/skills/generating-clickhouse-query-performance-reports/references/hogql-deep-dive.md的核心方法论,结合源码与可执行 SQL,讲清如何在posthog.query_log_archive中把慢 HogQL 扫出来、判断其中有多少是 AI 写的、并逐条做根因取证。
为什么 HogQLQuery 需要独立成桶
PostHog 会把进入 ClickHouse 的每条查询都打上结构化标签(写入system.query_log.log_comment,再由归档表展开为带lc_前缀的类型化列)。lc_query__kind = 'HogQLQuery'因此成为一个独立分析桶:与产品生成的查询(TrendsQuery、FunnelsQuery等)不同,HogQL 是任意的、由用户或 AI 撰写的 SQL。
它来自四个主要入口:
- Web 端的SQL 编辑器 / DataVisualization 节点(数据可视化里写自由 SQL 的画布节点);
/query/API:以personal_api_key(数据集成、API 消费者)或oauth方式直接投递 HogQL;- MCP server:外部 AI agent 通过 PostHog MCP 服务器发起的工具调用;
- Max assistant(PostHog 内置的 AI 助手)及其子工具。
在源码中可以看到这条执行链路的落点:HogQL 查询由 hogql_query_runner.py 中的HogQLQueryRunner承载,执行时显式传入query_type="HogQLQuery"(见 hogql_query_runner.py);execute_hogql_query()的默认query_type也会落到这个桶上(query.py)。
由于这是任意 SQL,它绕过了塑造产品 insights 的那层护栏:
- 没有强制的日期范围;
- 没有物化感知的属性访问(
properties.x走不带物化列优化的路径时是危险的); - 允许任意 join。
因此其慢因更五花八门,且在 OOM 与超时中占比显著偏高(异常码241/159,见下文异常码表)。
两张关键证据列:query与lc_query__query
分析 HogQL 慢查询时,归档表里有两列起着决定性作用:
| 列 | 含义 | 何时读它 |
|---|---|---|
query | 编译后的 ClickHouse SQL,真正执行的那份 | 分析执行层面:granule 裁剪、join 顺序、读了多少字节 |
lc_query__query | 用户或 AI 写的源 HogQL | 理解意图:它比编译后的 SQL 短得多、清楚得多 |
lc_query__query是意图层证据;query-patterns.md第 6 节的query_link配方(见 query-patterns.md)生成的共享 Metabase 链接默认就只 SELECTlc_query__query,方便读者直接点开看原始查询。需要执行细节时再扩宽 SELECT(例如把query一并带出)。
全景扫描:慢 HogQL 到底是谁发出的
先用一个聚合把“慢 HogQL 地图”铺开。下面这条 SQL 将慢集按产品、特性与访问方式分组,是报告的起点:
SELECT lc_product, lc_feature, lc_access_method, count() AS slow, uniqExact(team_id) AS teams, countIf(exception_code = 241) AS ooms, countIf(exception_code = 159) AS timeouts, round(avg(query_duration_ms)/1000) AS avg_s, formatReadableSize(sum(read_bytes)) AS total_read FROM posthog.query_log_archive WHERE event_time > now() - INTERVAL 14 DAY AND is_initial_query AND lc_query__kind = 'HogQLQuery' AND (query_duration_ms > 30000 OR exception_code IN (159,160,241)) GROUP BY lc_product, lc_feature, lc_access_method ORDER BY slow DESC LIMIT 40历史观测下,这张表有稳定的特征,读法如下:
- 主体是
product_analytics/query/personal_api_key:即数据集成与 API 消费者,它们是慢 HogQL 的大头; - 数量上的“噪声”另有其人:紧超时(tight-timeout)的 API 流量(平均约 13 秒、大多是超时)与
cache_warmup(后台 insight 刷新)在原始计数里反而占优; - 正确排序不看 count:按惯例用OOM 数与**集群小时(cluster-hours)**去加权,否则超时数量会伪装成真实算力消耗。
两条慢查询判定的关键约束(完整方法论见 SKILL.md):
- 必须带
is_initial_query = 1,避免分布式子查询被重复计数; - 慢集谓词是
query_duration_ms > 30000 OR exception_code IN (159,160,241),三个异常码含义如下:
| 异常码 | 含义 |
|---|---|
| 159 | TIMEOUT_EXCEEDED |
| 160 | TOO_SLOW |
| 241 | MEMORY_LIMIT_EXCEEDED |
识别 AI 编写的 HogQL:用lc_product+lc_feature,而不是ai_query_source
不存在单一布尔列“是否为 AI 写的”。归档里没有这种字段,正确的做法是从lc_product与lc_feature两个标签维度拼出证据:
| 信号 | 含义 |
|---|---|
lc_product = 'max_ai' | PostHog 的 Max 助手及其工具发出的查询(在ee/hogai/**中通过tags_context(product=Product.MAX_AI, ...)打标) |
lc_product = 'mcp'或lc_feature = 'mcp' | 外部 AI agent 经由 PostHog MCP 服务器发起的查询 |
lc_feature = 'posthog_ai' | AI 功能标签(存在,实践中较少见) |
推荐过滤条件:
lc_product IN ('max_ai','mcp') OR lc_feature IN ('mcp','posthog_ai')代码侧可验证的映射源头:
- 两个枚举定义在 query_tagging.py(
Product:MAX_AI = "max_ai"、MCP = "mcp",注释明言“queries originating through the MCP server (agent tool calls)”)与同文件的Feature枚举(POSTHOG_AI、MCP); - Max 侧的打标现场如 manage_memories.py、filter_session_recordings.py,均使用
tags_context(product=Product.MAX_AI, feature=Feature.POSTHOG_AI, ...); - 场景 → 产品/特性的兜底映射、以及“先场景 → 再 kind → 再查询结构 → 再 HogQL features → 最后 MCP 来源”的 fallback 顺序,都在 query_tagging.py。
千万不要用ai_query_source
ai_query_source这名字极具误导性。它在 ai_table_resolver.py 这类 LLM-analytics 解析器中被设置为dedicated_table/shared_table_fallback等取值,记录的是这次查询选择了哪张 AI events 表,与“这段 SQL 是否为 AI 所写”毫无关系。更关键的是,它没有被物化为归档里的lc_*列,在query_log_archive上按它过滤根本拿不到数据。
识别结果的边界与启发式信号
- 该方法标记的是**“在 AI/MCP 上下文中执行”的查询**。Max 起草、随后被人类保存并重新加载的 insight,会被重新标记为普通的
product_analytics,归档无从得知其 AI 出身——这类“AI 原创但被人类固化成 insight”的查询无法通过标签识别; - 一个有用的次级信号:AI 编写的 HogQL 往往在
lc_query__query里带着解释性的-- …注释(人很少给临时 SQL 写注释)。它只作为旁证,不是权威依据; - 标签体系的事实来源是
Product/Feature枚举与“节点类型 → 产品”映射(query_tagging.py),调用点分布在ee/hogai/**与 MCP server 中。
把上述过滤拼进慢集全景,就得到“AI 占了多慢”的视图:
SELECT lc_product, lc_feature, lc_access_method, count() AS slow, uniqExact(team_id) AS teams, countIf(exception_code = 241) AS ooms, countIf(exception_code = 159) AS timeouts, round(100 * countIf(exception_code = 241) / count()) AS oom_pct FROM posthog.query_log_archive WHERE event_time > now() - INTERVAL 14 DAY AND is_initial_query AND lc_query__kind = 'HogQLQuery' AND (lc_product IN ('max_ai','mcp') OR lc_feature IN ('mcp','posthog_ai')) AND (query_duration_ms > 30000 OR exception_code IN (159,160,241)) GROUP BY lc_product, lc_feature, lc_access_method ORDER BY slow DESC观测结果是 AI/MCP 的 HogQLOOM 与超时比例不成比例地高——仓库文档记录过一个单周案例:MCP-over-OAuth 桶里大约三分之一的慢查询直接 OOM。原因很直接:这些是雄心勃勃的分析查询,却完全没有产品级 insights 那套护栏约束(无强制日期、无物化感知、随意 join),所以拿 241/159 的概率远高于“被护栏约束成规范形状”的产品查询。
AI 与 ad-hoc HogQL 慢的六类高频根因
在慢集上读lc_query__query(源 HogQL,而非编译后的 SQL),反复出现的根因有这几类:
- 未物化的 JSON 提取:AI 直接写
JSONExtractString(properties, 'x')或properties.x去取事件/用户属性。关键在于JSONExtract*(...)这种函数调用形式绕过了物化列(细节见 investigation-playbook.md 与 materialization-analysis.md),导致每行都要完整读一遍 JSON blob。 events上的自 join / 交叉 join:例如在某个时间窗内按 person 做events e1 JOIN events e2,或拿每人聚合做CROSS JOIN。这会把扫描量直接乘上多倍。- 跨源 join:把数仓表(
postgres.*、vitally.*、s3(...))与 events 做 join。外源一侧没有任何 ClickHouse 索引,代价全部落在扫描与 shuffle 上。 - 没有或过宽的日期范围:ad-hoc SQL 常常漏写紧致的
timestamp过滤,等于扫全量历史。 - 按宽列排序/过滤的全量导出:即“函数包裹的排序键 / 过滤键”反模式(function-wrapped sort/filter key),让主键/分区无法裁剪,详见 investigation-playbook.md。
判读要点与配套文档:
- 判断物化问题前先对照“哪些列已物化”:仓库用
SHOW CREATE TABLE sharded_events交叉核对(materialization-analysis.md)。若物化列已存在但查询仍走 JSONExtract,通常是属性以JSONExtractString(properties, '$foo')形式被访问,形成ast.Call从而跳过visit_property_type(),而不是properties.$foo; - 单条查询级根因(bytes vs CPU vs duration、运行时成因、EXPLAIN 验证)属于 optimizing-clickhouse-and-hogql-queries 技能范畴。
单条 AI 查询取证:把源 HogQL 与编译后 SQL 放在同一行
在报告中引用任何一条慢查询时,都需要同时给出来源与执行形态,按query_id+event_date从归档表回溯(system.query_log只保留几小时,归档表才能覆盖多天窗口):
SELECT lc_query__query AS source_hogql, query AS compiled_sql, exception FROM posthog.query_log_archive WHERE query_id = '<id>' AND event_date = '<YYYY-MM-DD>' AND is_initial_query读法建议:
- 先用
lc_query__query理解 AI 想干什么(意图层),再切换到query检查执行层; - 慢因的判断不应停在“team X 慢”这种粒度,而要形成“为什么慢”的假设(如“时间过滤被函数包裹、granule 无法裁剪所以扫了全量历史”),再用
EXPLAIN验证——这正是根因取证 playbook 的职责范围; - 报告中的每个案例都应带着可点击的共享
query_link(编码规则在 query-patterns.md 第 6 节),保证读者能一键直达原始查询。
把这份 deep dive 放回报告流程
这份材料不是孤立文档,而是 ClickHouse 慢查询报告方法论中的固定一环:
- 在 SKILL.md 的标准工作流第 6 步,报告需要对用户侧查询做分桶,其中 “
HogQLQuery(任意用户/AI SQL)值得一次专门深潜,包括其中多少由 AI 撰写、以及它为什么慢”即指向本文; - 所有 SQL 均以
posthog.query_log_archive为数据源(Distributed 归档表、类型化lc_*列、约三周保留期),并通过hogli metabase:query --region <us|eu>执行;跨区域报告需分别对 US / EU 各跑一遍,因为两地的物化列与负载不同; - 报告落盘位置在私有仓库(或临时目录),公开仓库只承载方法与工具——读者在公开仓库中看到的是完整的 SQL 配方与分析路径。
检查清单
收尾时可用这张清单自查一份 HogQL 分析是否完整:
- 是否只用了
lc_query__query/query双列做意图层与执行层取证; - 判定 AI 出身是否只依赖
lc_product/lc_feature,而没有误用ai_query_source; - 是否区分了“AI 执行上下文”与“AI 原创”之间的边界(被人类保存的 AI insight 会重打标);
- 排序是否用 OOM 与集群小时加权,而非被紧超时噪声带偏;
- 每条慢因是否落到六类根因之一并给出可验证的假设,而不是停在统计层。
【免费下载链接】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),仅供参考