
PostHog 中 ClickHouse 与 HogQL 慢查询优化实战:从识别坏味道到在正确层级修复【免费下载链接】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 仓库中的技能文档 SKILL.md 为核心,系统讲解 ClickHouse/HogQL 查询优化的完整工作流:如何从 HogQL 提取真实执行的 ClickHouse SQL、如何识别FROM ... FINAL、JSONExtract、缺失 skip index、自连接、CTE 膨胀等常见坏味道,以及如何用EXPLAIN与测试集群实测验证重写效果,并最终把修复落在 printer、query runner 或迁移的正确层级。读完本文,你可以独立完成一条 PostHog 慢查询(无论来自 insight、query runner 还是手写sync_execute)的定位—诊断—实测—修复—测试闭环。适用范围:先确认你在正确的数据层PostHog 的后端查询分属多个存储层,这套技能只覆盖 ClickHouse 与 HogQL 查询(HogQL 最终编译为 ClickHouse SQL),不覆盖 Postgres / Django ORM(Model.objects.filter(...)、.raw(...)、RawSQL)——后者应改用 profiling-slow-api-endpoints 技能;personhog 客户端(personhog_client.*、get_personhog_client()、get_person_by_*)属于 gRPC/Postgres 路径,参见 personhog 客户端说明。判断慢查询在哪一层的完整分诊表:你看到的东西走向用什么execute_hogql_query(...)、HogQLQuery、*QueryRunner、insight、HogQL.ambr快照ClickHouse(经 HogQL printer)本技能sync_execute(...)、client.execute(...)、手写SELECT ... FROM events、迁移ClickHouse 直连(无 HogQL)本技能(第 2–5 步,跳过第 1 步)Model.objects.filter(...)、.raw(...)、queryset、应用库上的RawSQLPostgres via Django ORM不是本技能personhog_client.*、get_personhog_client()personhog(gRPC, Postgres)不是本技能两个重要的补充判断:被指向的是 coordinator / orchestrator / Celery task / Temporal workflow / management command 时,真正的 ClickHouse 查询通常还深一层(被分发的 activity、子 workflow 或apply_async目标)。要顺着分发追到构建 HogQL 的那一层;同一文件可能同时操作 Postgres(取活)和 HogQL(干活),应视为两个独立任务。生产代码里出现手写 ClickHouse SQL 要标记出来。printer 免费提供物化列替换、属性组分发、lazy join、team-id 守卫;手写 raw SQL 要么重新实现要么丢失这些能力。结构性修复是改用 HogQL,但工作量更大,应同时给出现在就地的局部修复 后续迁移到 HogQL两个选项。单团队 vs 多团队决定 raw SQL 是否情有可原:execute_hogql_query是团队作用域的(单团队,自动注入team_id守卫),因此单团队查询几乎总应该用 HogQL。手写单团队team_id X查询会漏掉物化、lazy join 和 team-id 守卫,视为坏味道。最典型的信号是手搓物化列查询(例如get_materialized_column_for_property(...)带JSONExtract兜底)——那是把 printer 的活手工做了一遍。跨团队 / 全局任务豁免(enrichment、billing rollup、找出所有满足 X 的团队这类没有team_id或team_id IN (...)的作业)无法使用execute_hogql_query,应保持 raw SQL 并在原地优化;此时手搓物化列查询是预期行为,不是坏味道。INSERT不豁免 SELECT 半边。HogQL 没有INSERT,正确做法是用 HogQL 构建SELECT→ 打印 → 拼接进INSERT INTO table printed_select(只有外层 wrapper 是 raw)。单团队的INSERT ... SELECT配手写SELECT(裸列、对properties用JSONExtract、手工team_id)同样是坏味道。真正 raw 无妨的场景:INSERT ... VALUES直插 Python 行、多团队INSERT ... SELECT、迁移、一次性脚本。优化整个团队的查询:先建完整清单当任务是优化 X 团队的所有查询时,必须先建全量清单,否则会漏。步骤:把团队解析为它拥有的路径,使用 establishing-code-ownership 技能。拥有路径横跨后端 Python和frontend/src/...,往往覆盖多个产品;不要悄悄缩小到单个产品或只查后端。在所有路径上找查询。多数是 Python(*QueryRunner、execute_hogql_query、裸sync_execute),但相当一部分在客户端构建后 POST 到/query。在frontend/中 grep:api.queryHogQL(...)、HogQLQueryString、hogql...模板NodeKind.HogQLQuery/kind: HogQLQuery带query:字符串会编译为 ClickHouse 的节点:DataTableNode、EventsQuery、TrendsQuery/InsightVizNode、PropertyFilterType.HogQL形如SELECT ... FROM events的字面量,或产品标记(如survey sent、$survey_id)同一个查询有时被实现两遍(后端 runner 前端 HogQL,常见于停滞的迁移)。两者都在范围内:后端 printer/函数修复触及不到手工构建的前端字符串。背景:一次读透的关键文件手册(概念模型):query-performance-optimization.md:发现与修复慢查询hogql-python.md:printer 流水线、如何驱动 HogQLclickhouse-queries-new-products.md:表 / runner 设计表结构,只需略读ORDER BY/PARTITION BY/INDEX/ 物化列(不必逐行):Events:posthog/models/event/sql.pySessions v3:sessions_v3.py(v2: sessions_v2.py)Persons:posthog/models/person/sql.py;覆盖:person_overrides/sql.pyCohorts(cohortpeople成员表):products/cohorts/backend/models/sql.py。Postgres 中的Cohort定义属于 Postgres 范畴(Step 0)其他表(app_metrics2、session_replay_events、log_entries、heatmaps、产品表):在 posthog/models/ 下各sql.py或创建它们的迁移中寻找从源码结构看,events 表的建表语句包含按toYYYYMM(timestamp)分区和ORDER BY (team_id, toDate(timestamp), event, timestamp, cityHash64(distinct_id), distinct_id, uuid)的排序键,定义于 posthog/models/event/sql.py,这是后文WHERE 必须覆盖排序键前缀这条规则的根据。HogQL 侧关键路径:查询入口 query.py;printer 为 clickhouse.py 与 base.py;辅助函数 utils.py;函数目录 posthog/hogql/functions/(聚合在 aggregations.py);schema 在 posthog/hogql/database/schema/。物化机制(自动把属性访问从JSONExtract重写走):注册表:ee/clickhouse/materialized_columns/columns.py 的get_materialized_columns(table)/get_enabled_materialized_columns(table),结果按所连 ClickHouse 缓存 15 分钟属性组:posthog/clickhouse/property_groups.pyprinter 替换逻辑:base.py 中的_get_materialized_property_source_for_property_type()/visit_property_type()(约 1260、1354 行附近),以及 clickhouse.py 的 ClickHouse 覆盖(约 412 行附近)。每次属性访问,printer 都会针对所连 ClickHouse 发出当前最优形式(直接列、属性组、DMAT 槽或JSONExtract兜底)这正是同一段 HogQL 在测试(稀疏物化)与生产(稠密物化)中打印结果不同的原因:查找是打到所连 ClickHouse 的。可以推断:凡是走 printer 路径的属性访问都应假设会物化,只有绕过 printer 的手写 SQL 才需要自己做物化列查找。集群拓扑(分片、副本、ingestion 节点 vs 数据节点):posthog/clickhouse/migrations/CLAUDE.md——在提任何迁移之前必读。第 1 步:拿到 ClickHouse SQL核心原则:从 ClickHouse SQL 出发工作,而不是 HogQL。HogQL 编译为 ClickHouse SQL,只在 HogQL 层面推理会掩盖 ClickHouse 实际执行的东西。拿到 SQL 后优化它,再把改动翻译回 HogQL 查询、query runner、printer 或迁移。若目标本身是 raw ClickHouse 查询,你已经有 SQL,直接跳到第 2 步。HogQL 有三种拿 SQL 的方式:Python 打印:execute_hogql_query()(query.py)返回的 response 中带有clickhouse字段(执行并返回 SQL);只要 SQL 不执行,用prepare_and_print_ast(..., dialectclickhouse)(utils.py);对已准备好的 AST 则用print_prepared_ast(utils.py)。快照:posthog/hogql_queries/test/__snapshots__/(及各产品测试目录)下的.ambr文件保存了代表性输入生成的 ClickHouse SQL。生产(信息量最大):通过 querying-production-databases-via-metabase 技能 查clusterAllReplicas(posthog, system, query_log)(超过约 4 小时的旧行查posthog.query_log_archive,约 22 天保留期,带类型化的lc_*列,优先用它)。过滤条件:is_initial_query、type QueryFinish、query_duration_ms 阈值。第 2 步:肉眼扫描常见坏味道在动用工具之前先目视 SQL。以下五类是最高频的已知坏形状;若要从一条具体慢查询的运行成本(字节 vs CPU vs 时长、高基数下钻、函数包裹的排序键、比率指标双扫描、溯源到代码、EXPLAIN)反推原因,见 investigation-playbook.md。坏味道一:FROM table FINALFINAL作用于 ReplacingMergeTree / CollapsingMergeTree / AggregatingMergeTree(person、groups、cohortpeople等)时,会强制对所读每个 part 做即时合并,按排序键行去重到最新版本。它破坏并行读、撑爆内存,随 part 数量增长恶化,在大型/分片表上几乎总是错的。改写方式:逐行argMax:SELECT properties FROM person FINAL WHERE ... id IN (...)变为SELECT argMax(properties, version) FROM person WHERE ... id IN (...) GROUP BY idLIMIT 1 BY配ORDER BY version DESC:每组取一行(要求 version 列单调)先过滤再 FINAL(若确有必要):让 WHERE 在排序键前缀上足够有选择性,减少被合并的 partPostHog 特有提示:按 CLAUDE.md,新的 person/group 访问应走 personhog(get_personhog_client),而不是裸person/groups。新代码里出现裸FROM person FINAL,通常想要的不是调优,而是迁到 personhog。来自实测的反例提醒:learnings 日志中记录了一个案例,对person FINAL套用教科书式argMax(properties, version)改写反而更慢——B 变体比 A 慢 46%、内存约 10 倍,而把argMax作用在窄的物化列pmat_$browser上(C)则快 4.6 倍、少读 33 倍字节。安全的改写层级是:(1) 有物化列就用物化列,(2)argMax只作用于你真正需要的窄列,(3) 最后才考虑对宽列argMax。详见 references/learnings.md。坏味道二:对 properties 做 JSON 操作对裸properties/person_properties/group_properties的JSONExtractString/Float(...)、JSONHas(...)会逐行解析 JSON blob:比直接物化列(mat_*/dmat_*)慢至约 100 倍,比属性组读取慢约 10 倍。printer 路径的查询(后端parse_select/execute_hogql_query/*QueryRunner,前端api.queryHogQL/hogql...):把每一个JSONExtract*(properties, X)替换为properties.X(非字符串包toFloat(...)/toInt(...))。必须全部转换,不要自己判断哪些已被物化——printer 在打印时做查找并发出最优形式(物化列、属性组、DMAT 槽、JSONExtract兜底)。properties.X绝不会比手写形式更差,且未来某列被物化后会自动受益。不要通过读迁移 / 注册表 /DESCRIBE去挑拣,那是在重新实现 printer 且大概率做错:物化未必来自迁移、属性组没有专门列、集合随环境和时间变化。部分转换只会让查询不一致且毫无收益。例外:绕过 printer 的 raw SQL(多团队sync_execute、迁移、手工构建的 temporal activity 字符串)——没有 printer,也没有properties.X语法,直接引用物化列。策略(仅该场景需要):直接物化列见 posthog/clickhouse/materialized_columns.py;属性组见 property_groups.py;DMAT 槽见 posthog/models/dmat_slot_assignments/ 以及 event/sql.py 中的EVENTS_TABLE_DYNAMICALLY_MATERIALIZED_COLUMNS()。新的 ClickHouse JSON 类型正在试用中,注意查看最近的迁移。两个配套要点:测试中可用 posthog/test/base.py 的materialized()上下文管理器为某个测试块物化属性(附带create_minmax_index、create_bloom_filter_index及小写变体),并用get_index_from_explain/get_indexes_from_explain(posthog/test/base.py,执行EXPLAIN PLAN indexes1, json1)断言 skip index 确实被使用,防止 printer 改动悄悄撤销索引命中。索引不被使用的常见原因:nullIf/wrapper 遮住了物化列、与字符串化的NULL比较、fixture 中缺列(用materialized(..., create_minmax_indexTrue))。.ambr快照里出现JSONExtract而源码用的是properties.X?那只是测试 fixture 的兜底(生产可能发出物化读)。不是 bug,不要修它。坏味道三:主键与 skip index确认WHERE覆盖ORDER BY前缀。events 排序键以(team_id, toDate(timestamp), event, ...)开头,因此没有文档化理由时(例如需要全部事件的 cohort 计算),非平凡的 events 查询都应过滤timestamp与event。坏味道四:events 自连接把events(或任何大表)与自己连接会加倍工作量并丢失主键有序性。改写为单遍 条件聚合:sumIf(amount, eventpurchase)、uniqIf(distinct_id, eventpageview)、uniqMapIf(...)。对关联行(转化前的第一个事件),对有序groupArray做arrayFilter/arrayFirst/ 窗口函数优于自连接。缺聚合函数就加到 aggregations.py。坏味道五:CTE 内联膨胀ClickHouse 的WITH name AS (SELECT ...)CTE 是内联而非物化的:被引用两次就执行两次,嵌套会指数放大。这是planner 行为怪异的最常见原因。在WITH ... AS MATERIALIZED落地之前(留意 ClickHouse 版本发布说明),改写为单遍条件聚合,或用FROM子查询强制单次执行。深入调查:从运行成本反推原因investigation-playbook.md 与上面的扫描坏味道方向相反:从一条具体慢查询的运行时成本回到成因。适用于从生产拉出来的查询(经 Metabase 查posthog.query_log_archive,最慢的真实案例胜过合成案例)。核心要点:永远取完整query文本 exception,不要取子串。关键证据几乎总在尾部:ORDER BY、WHERE时间边界、LIMIT、SETTINGS(max_threads、max_execution_time、max_memory_usage)。截断到 160 字符的列表预览会掩盖这些:SELECT query, exception, query_duration_ms, formatReadableSize(memory_usage) AS mem, formatReadableSize(read_bytes) AS read, read_rows FROM posthog.query_log_archive WHERE query_id id AND event_date YYYY-MM-DD AND is_initial_queryOOM(错误码 241)的exception会写明配置上限与爆掉的列,如Query memory limit exceeded: would use 42.01 GiB, maximum: 42.00 GiB (while reading column elements_chain)——这个列名就是直接线索。三个信号要一起读:读入字节数、CPU 时间、时长:读入字节是最稳定的工作量度量。按read_bytes DESC排序候选;同批次两条查询行数相近但字节差 ~100 倍,重的那条在解压宽列(几乎总是propertiesJSON blob)CPU 时间(ProfileEvents[OSCPUVirtualTimeMicroseconds])抓住字节数漏掉的:昂贵表达式、高基数聚合时长有铁律:永远不要信 API 请求的 duration(含排队、序列化、网络、应用开销)。报告时长必须取自query_log的query_duration_ms。时长高而字节/CPU 不高,通常意味着查询在等待(锁、并发上限、其他查询)——本身值得标记高频成因(按出现频率,仅为假设起点而非穷尽诊断):未物化的属性访问(JSONExtract)——极端读字节量的头号原因Sessions 连接(raw_sessions/sharded_sessions,如为取$session_duration)高基数下钻:对 URL 或 ID 的breakdown_value使分组爆炸,是用户侧TrendsQuery的主要 OOM 驱动高量事件作为漏斗/首步:$pageview会扫全部 pageview比率/多序列指标的双扫描:分子分母各跑一遍扫描和 person-overrides join全时段或跨月日期范围函数包裹的排序/过滤键破坏索引裁剪:例如coalesce(toTimeZone(timestamp, ...))而不是裸timestamp,使 ClickHouse 无法匹配主键,过滤退化为 Prewhere 逐行过滤(无 granule 裁剪),实际扫描远超请求窗口,在高max_threads下读宽列时是经典 OOM。留意对非空列的单参数coalesce()这类空操作包装溯源到发起方:lc_*列只存在于query_log_archive(是预解析的log_comment标签);system.query_log里同样信息未解析,需从log_commentJSON 中取(如JSONExtractString(log_comment, query_type))。这些标签(lc_query__kind、lc_route_id、lc_dashboard_id/lc_insight_id/lc_experiment_id/lc_cohort_id、lc_feature、lc_temporal__workflow_type、lc_dagster__job_name、lc_access_method、lc_api_key_label、lc_user_id)由 posthog/clickhouse/query_tagging.py 的QueryTags模型定义。逆向定位 SQL 时,grep 代码库找lc_query__kind值(如TrendsQuery)即可找到生成它的 query runner。第 3 步:跑 EXPLAINEXPLAIN不需要数据即可在 dev ClickHouse 上工作(planner 输出不依赖行):EXPLAIN PLAN indexes1, actions1, json1 SELECT ...:主键 skip index 使用EXPLAIN QUERY TREE SELECT ...:分析器后的逻辑树EXPLAIN PIPELINE SELECT ...:处理器流水线EXPLAIN ESTIMATE SELECT ...:逐 part 的行/mark 估算EXPLAIN SYNTAX SELECT ...:规范化 SQL按 ClickHouse EXPLAIN 官方文档,EXPLAIN 的变体不执行查询也不扫描表数据,可安全地指向任意规模的生产运行;但EXPLAIN ESTIMATE与indexes1会读主键索引 mark / part 元数据,属小额元数据读,非必要时优先用更轻的actions1/PIPELINE。最强技巧是嫌疑版本与修复版本并排 EXPLAIN 后 diff。对函数包裹键案例,比较ORDER BY coalesce(toTimeZone(timestamp, ...))与裸ORDER BY timestamp:Granules大幅下降(全历史缩到日期窗口)、时间边界从 Prewhere 移入主键条件,即可确认 wrapper 是成因。plan 上的读法:Granules: X:8192 行块在索引裁剪后剩多少。被包裹/不透明过滤的 granule 数远大于等价裸列过滤,差值就是浪费的扫描ReadType: InOrdervsDefault:events 按toDate(timestamp)排序(非天级),所以按完整timestamp排序仍会排序——不要仅凭日期排序就推断 in-order 读Prewhere filter vs 主键条件:时间边界在 Prewhere 里逐行求值、不裁剪 granule;在主键条件里则会skip index 有效性(如minmax_mat_*):各索引消掉了多少 granule字节 vs 行:granule 相近而字节差异巨大 列宽问题(又是 JSON blob)第 4 步:真实测量EXPLAIN展示意图;要确认重写更快,必须对代表性数据跑两个版本。本地 ClickHouse(正确性与 EXPLAIN,不用于计时——噪声太大):hogli dev:demo-data灌入合成数据,hogli db:ch打开客户端。本地(单节点)可以尝试 skip index / 物化列 / schema 变更,但先问用户,且记住生产是多节点的,结构性变更必须经 clickhouse-migrations 技能 走迁移;ALTER TABLE ... MATERIALIZE INDEX ...可对存量数据建新索引。读入字节(FORMAT JSON或本地system.query_log)是比墙钟时间更不嘈杂的代理指标。测试集群(计时):Metabase 前置的只读 team 2 数据快照,无嘈杂邻居。使用 querying-production-databases-via-metabase 技能。流程:先适配生产查询:换team_id、选重叠日期范围、替换或跳过依赖 team 2 没有的属性/功能的分支(需要判断)计时前先套用生产物化列:DESCRIBE table列出真实的pmat_*(events)/mat_*(persons、groups);把JSONExtract(...)换成它们,否则你在给 printer 永远不会发出的形状计时设SETTINGS use_uncompressed_cache0,取 5 次中位数,并从system.query_log读query_duration_ms/read_rows/read_bytes/memory_usage/ProfileEvents,不要读 Metabase 的请求耗时先测量再建议。任何重写都应在同一 team-2 适配查询上跑原始 vs 候选,报告前后query_duration_ms、read_bytes、memory_usage。没有数字的建议是猜测;无法测量时(集群不可用、无法适配、纯 schema 变更)要明说。集群只读,schema 变更原型在本地做。Autoresearch(powertool)用于困难案例:tools/query-performance-ai/ 将 pi-autoresearch 包装起来,在测试集群上循环优化。配置不轻(Docker sandbox、ANTHROPIC_API_KEY、Metabase DB ID),应由用户运行;见其 README 与 coordinator。第 5 步:把优化施加在爆炸半径最小的层级让 HogQL 发出更快的 ClickHouse SQL,按代价从低到高:Query runner(最便宜):若重写是另一条 HogQL 查询(聚合方式、join 顺序、CTE 改条件聚合),编辑posthog/hogql_queries/或products/*/backend/下的 runner,用.ambr做快照新增 HogQL 函数:加到 aggregations.py(或 functions/ 下合适的文件),用HogQLFunctionMeta(name, min_args, max_args, aggregateTrue)Printer 改动:printer 应自动应用的 SQL 级重写。_get_optimized_materialized_column_equals_operation(约 574 行,clickhouse.py)是模板。加一个快照测试 一个get_index_from_explain断言迁移:schema 变更(skip index、物化列、projection、引擎)。用 clickhouse-migrations 技能;生产是多分片/多副本、数据节点与 ingestion 角色分离,所以node_roles[...]、shardedTrue、is_alter_on_replicated_tableTrue都很要紧;绝不ON CLUSTER。干净范例:0250_property_values_lowercase_text_index.py团队特定启发式:不要投机性提交有些重写帮到一个团队却伤到另一个(某漏斗重写在步骤 1→2 大量流失时收益巨大,但当步骤 1 几乎匹配每个事件时反而变慢)。对这类不对称,应建议运行时启发式(统计每步事件数,比率有利时才应用),而不是投机性地提交;形态由用户决定。测试纪律与经验沉淀测试纪律:改 printer 规则 / query runner / 加函数时,用.ambr快照生成的 ClickHouse SQL,若收益依赖特定索引或重写,再加基于EXPLAIN的断言。修复后变绿不是证明——把改动关掉再跑,确认测试真的覆盖了你的路径。经验日志:references/learnings.md 是追加式案例库,记录了坏味道在真实测量中需要的修正,值得重点吸收的结论有:minmaxskip index 救不了混合内容字符串列上的无界点查:$session_id列同时含字面量null(每天数百万行)与其他非 UUID 值,每个 granule 的[min, max]区间宽到能容纳任何 UUIDv7,索引几乎不裁剪。无界单会话查询实测 16.5s / 24.4 亿行,而从 UUIDv7 内嵌的 48 位毫秒时间戳派生时间窗口后只要 503ms / 3570 万行(33 倍快、68 倍少行)。单实体查找(会话、trace 等)必须携带时间戳边界窗口函数输入不要预过滤:用 IN 子查询预过滤lagInFrame/leadInFrame的扫描,结果是双倍成本(~1.8 倍慢、2 倍读字节)——主导成本是解压JSON 解析properties,窗口排序本身很便宜,而 IN 子查询是同一张表的第二次全扫。不要为窗口分区预过滤需要重扫同一张表的谓词ClickHouse 分析器会裁剪未被外层引用的argMax(content):先拿不选 content 的变体对比read_bytes再下结论。真正贵的是对全历史argMax(metadata) GROUP BY document_id的哈希表内存;用key IN (SELECT DISTINCT id WHERE 页内谓词)把分组集合限界,是用一次廉价窄扫描换内存随请求页而非全团队历史增长的正确杠杆。注意正确性陷阱:report_id 过滤必须留在argMax之后,否则会重新浮现被重新分组的旧归属测量方法学:ClickHouse 26.x 的 query condition cache 会让同一谓词的重复运行只读上次命中的 granule,计时时必须use_query_condition_cache0;Metabase 会整体缓存相同 native query,5 次运行可能只有 1 次,每次加变化 nonce脱敏规范(公开仓库红线):learnings 条目严禁包含客户数据——不得出现原始 person/group/distinct_id 值、自定义属性名/值、team/org 名、行样本或精确客户规模。使用占位符(custom_property、team_id、bound_uuid)或描述形态(数千万行);PostHog 自己的 team 2 可以点名,其他 team ID 一律脱敏;$前缀的 PostHog 标准属性($browser、$os、$current_url)安全,客户自定义属性不安全。结语:一条可复用的优化闭环把上述内容串起来,PostHog 中优化一条 ClickHouse/HogQL 查询的完整闭环是:先按 Step 0 表确认存储层与团队作用域 → 用prepare_and_print_ast、.ambr快照或query_log_archive拿到真实 ClickHouse SQL → 目视五大坏味道(FINAL、JSONExtract、主键前缀、自连接、CTE)→ 用 bytes/CPU/duration 三信号与lc_*标签建立可证伪假设 → 并排 EXPLAIN diff 验证 → 在测试集群取 5 次中位数实测前后指标 → 按 runner → 新函数 → printer → 迁移的爆炸半径顺序施加修复 → 用.ambr快照 EXPLAIN断言固化,并把意外发现按脱敏规范追加到 learnings 日志。这套方法的精髓在于:一切结论必须建立在所连 ClickHouse 的真实 schema 与实测数字之上,而不是对 HogQL 源码的想象。【免费下载链接】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),仅供参考