ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PostHog 查询性能优化实战:PostgreSQL 与 ClickHouse 双引擎的规模化调优指南

PostHog 查询性能优化实战:PostgreSQL 与 ClickHouse 双引擎的规模化调优指南 PostHog 查询性能优化实战PostgreSQL 与 ClickHouse 双引擎的规模化调优指南【免费下载链接】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/posthogPostHog 在规模化运行中能否保持快速响应直接关系到产品体验。本文以 PostHog 工程手册中的查询性能优化文档 为核心系统梳理其两大存储引擎PostgreSQL 与 ClickHouse的查询性能最佳实践从编码规范、索引设计、慢查询定位与修复到索引回收、锁规避等生产级实操并结合当前仓库的真实源码与迁移文件进行佐证。读完本文你将掌握一套可直接复用的 PostHog 数据库调优方法论。存储引擎选型什么时候用 PostgreSQL什么时候用 ClickHousePostHog 同时使用两种不同类型的数据库它们面向完全不同的访问模式。理解二者的边界是性能优化的第一步PostgreSQL行式存储的 OLTP 数据库主要用于以可预测的查询条件访问和查询数据集。它更可能是你的最佳选择如果访问数据集时的查询模式是可预测的数据集规模预计不会超过 1 TB数据集需要频繁变更DELETE/UPDATE查询模式需要在多个表之间进行 JOIN。ClickHouse列式存储的 OLAP 数据库用于存储大规模数据集并对其执行分析型查询。它更可能是你的最佳选择如果访问数据集时的查询模式是不可预测的数据集规模预计会增长到 1 TB 以上数据集不需要频繁变更DELETE/UPDATE查询模式不需要跨多表 JOIN。从源码结构看这一分工在仓库中体现得非常清晰PostgreSQL 侧由 Django ORM 管理 posthog/models 目录下的模型承担团队、用户、Person、事件定义等业务数据存储而 ClickHouse 侧则由 posthog/clickhouse 目录承载大规模事件分析查询两者职责边界明确。PostgreSQL 查询优化编码最佳实践对于使用 Django 的应用层PostHog 的工程实践总结了 7 条核心编码规范只请求需要的字段SELECT name, surname优于SELECT *后者仅在少数边界场景下有用。只请求需要的行在查询末尾使用LIMIT条件。尽可能避免显式事务如果无法避免务必保持事务短小——事务会锁住正在处理的数据表并可能导致死锁强烈不建议在应用热路径中使用事务。尽可能避免JOIN。避免使用子查询子查询是嵌入在另一条 SQL 语句某个子句中的SELECT语句写起来更简单但JOIN通常能被数据库引擎优化得更好。使用合适的数据类型并非所有类型占用空间相同使用具体类型时还应按存储内容限制其大小。例如VARCHAR(4000)与VARCHAR(40)完全不同。应始终根据字段将要存储的内容来调整避免在数据库中占用不必要空间并应在应用代码中强制该限制避免查询报错。仅在必要时使用LIKE运算符如果你确切知道要找什么请使用运算符。注对于 Django 应用PostHog 目前依赖 Django ORM 作为数据与关系数据库之间的接口。虽然此时不直接编写 SQL 查询但上述最佳实践仍应予以考虑。打印执行查询的调试技巧在 Django 中若想以DEBUG模式运行并打印已执行的查询可以执行from django.db import connection print(connection.queries)对于单条查询可以执行print(Model.objects.filter(nametest).query)索引设计如果你以编程方式对某列进行排序ordering、排序sorting或分组grouping那么很可能应该在该列上建立索引。注意事项索引会拖慢表的写入速度并占用磁盘空间请务必删除未使用的索引。复合索引在需要针对多个非条件列进行查询优化时非常有用。关于单列索引和多列索引的更多信息可参阅 PostgreSQL 官方文档。从源码看 PostHog 的索引实践PostHog 在真实迁移中大量使用复合索引来支撑高频查询路径。例如在 posthog/migrations/0532_taxonomy_unique_on_project.py 中就为propertydefinition构建了包含coalesce(project_id, team_id)、type、group_type_index与query_usage_30_day降序排序的多列索引index_property_def_query_proj直接服务属性定义的检索与热度排序查询。如何发现慢查询在生产环境中查找并调试慢查询有几个可选方案AWS Console Aurora 和 RDS Performance insightsAWS 托管的性能洞察面板pganalyze query performance专业的 Postgres 查询性能分析工具。如何修复慢查询修复慢查询通常是一个3 步过程定位生成慢查询的代码位置将堆栈跟踪stacktrace作为查询注释query comments附加通常有助于将查询映射到代码。用EXPLAIN重新执行查询获取查询计划EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)查询计划并不容易阅读——它信息量巨大更接近机器可解析而非人类可读。Postgres Explain Viewer 2pev2是简化阅读查询计划的工具它以水平树展示每个节点对应查询计划中的一个节点包含时序信息、计划时间与实际时间的误差量并为“成本最高costliest”或“估算偏差bad estimate”等有趣节点提供徽章标记。修复查询修复后应当生成成本更低的EXPLAIN计划。从源码看「查询注释」的落地方式文档建议将堆栈跟踪作为查询注释附加这一实践在 ClickHouse 客户端中已有对应实现在 posthog/clickhouse/client/execute.py 中sync_execute会通过get_caller_source()捕获调用方的源文件与行号并连同查询标签一起写入log_commentJSON 格式使得每条查询都能从 ClickHouse 的system.query_log中反查到对应的应用代码位置。如何减少 IO索引需要 IO通过移除未使用的索引可以减少部分 IO。检查写入 IO例如用以下 SQLSELECT total_time, blk_write_time, calls, query FROM pg_stat_statements ORDER BY (blk_write_time) DESC LIMIT 10;SELECT 也可能产生写入 IO由于 MVCC 机制PostgreSQL 中的 SELECT 查询在特定场景下如 HOT 更新、同步复制、页面清理同样可能触发磁盘写入。移除外键字段上未使用的索引假设你在team_id、person_id上建了复合索引。如果team_id和person_id是 Django 外键Django 会自动为team_id和person_id各自创建独立索引。但根据 PostgreSQL 多列索引文档复合索引可以同时覆盖team_id与person_id的查询因此我们可以通过添加db_indexFalse来避免额外建立这两个索引。这一点在仓库中有直接印证在 posthog/models/person/person.py#L429-L436 中PersonDistinctId.team显式声明了db_indexFalse而复合外键(team_id, person_id)的约束在数据库层手动管理既避免了冗余单列索引又利用了分区裁剪。移除外键字段不要立即移除外键字段——这是向后不兼容的操作。应先做一次弃用deprecation流程让收益先落地先获得不再有索引和约束的好处再逐步移除。操作步骤将例如foreign_key_field重命名为__deprecated_foreign_key_field并添加db_columnforeign_key_field使得模型外部的引用必须使用完整限定名保留该字段是为了让 Django 不会尝试创建删除迁移等待一个发布周期的字段弃用期在下个发布版本中彻底移除字段并提示用户通过弃用版本进行升级以保证运行中的代码兼容。原文档注记TODO——想办法让 SELECT 查询不再请求该字段即最终能够真正 drop 列。查找并移除未使用的索引如何知道索引是否被使用可以执行类似下面的 SQLSELECT s.schemaname, s.relname AS tablename, s.indexrelname AS indexname, pg_relation_size(s.indexrelid) AS index_size FROM pg_catalog.pg_stat_user_indexes s JOIN pg_catalog.pg_index i ON s.indexrelid i.indexrelid WHERE s.idx_scan 0 -- has never been scanned ORDER BY pg_relation_size(s.indexrelid) DESC;如果索引确实未被使用可以通过移除db_indexFalse即恢复为默认建索引行为配合删除对应索引声明并运行./manage.py makemigration来安全移除。这会生成一个迁移但如果你查看./manage.py sqlmigrate的输出会发现它可能不是并发CONCURRENTLY删除索引而是一次阻塞性操作。要解决这个问题需要修改迁移使用SeparateDatabaseAndState让 Django 在状态层面跟踪模型的数据库结构同时允许我们自行控制索引的创建方式使用RemoveIndexConcurrently以非阻塞方式删除索引。PostHog 仓库中有两个非常典型的真实案例案例一外键自动索引的非并发删除0212 迁移在 posthog/migrations/0212_alter_persondistinctid_team.py 中原生成的AlterField迁移会执行阻塞式的DROP INDEX如文件注释中展示的sqlmigrate输出。工程团队将其改写为SeparateDatabaseAndStatestate_operations中声明db_indexFalse保持 Django 状态同步database_operations中使用DROP INDEX CONCURRENTLY IF EXISTS执行真正的非阻塞删除。注释还指出django.contrib.postgres.operations.RemoveIndexConcurrently似乎只对显式索引生效对ForeignKey自动生成的索引并不适用因此这里改用RunSQL。案例二大规模索引调整0532 迁移在 posthog/migrations/0532_taxonomy_unique_on_project.py 中迁移以atomic False声明这是并发索引操作的前提先后使用RemoveIndexConcurrently移除 4 个冗余的project_id单列索引再用AddIndexConcurrently创建基于coalesce(project_id, team_id)的新复合索引全程不阻塞线上读写。ee/migrations/0035_conversation_slack_index.py中则展示了另一条路径用SeparateDatabaseAndState配合手写CREATE UNIQUE INDEX CONCURRENTLY创建部分唯一索引partial unique index并同时保证 Django 状态与原始 SQL 同步。避免相关表上的锁例如在批量插入bulk insert时可能需要从被引用表中选出大量主键。当我们并不真正关心这些关联约束时可以指定db_constraintFalse如果正在更新已有字段则需要同步生成必要的迁移。这一实践在仓库中同样有据可查PersonDistinctId的team字段使用on_deletemodels.DO_NOTHING, db_constraintFalse团队删除由人工处理可能跨数据库person字段也使用db_constraintFalse其复合外键约束在数据库层手动管理见 posthog/models/person/person.py#L431-L436。此外 posthog/models/user_facet_settings.py 中team外键同样组合使用db_constraintFalse, db_indexFalse。ClickHouse 查询优化如何发现慢查询在生产环境中查找并调试慢查询有以下几个可选方案GrafanaClickHouse queries - by endpoint仪表盘提供了可靠性与性能维度的拆分视图。高频使用且缓慢/不可靠的端点往往暗示其背后的查询存在问题。PostHoginstance/status仪表盘在instance/status的内部指标页面下可以找到各种指标与查询日志。如果你是 staff 用户还可以通过点击或复制自己的查询来分析查询。分析输出包含查询运行时间Query runtime读取的行数 / 字节数Number of rows read / Bytes read内存使用量Memory usedCPU、时间与内存的火焰图Flamegraphs for CPU, time and memory这些信息对定位查询为什么变慢非常有用。Metabase如果以上仪表盘提供的查询粒度不够细可以使用 Metabase 查询。ClickHouse 的system表例如system.query_log提供了大量用于识别和诊断慢查询的有用信息。从源码看system.query_log的定位价值在 PostHog 中已被工程化如 posthog/clickhouse/client/execute.py 通过log_comment注入来源文件与行号正是为了能在system.query_log中快速把慢查询映射回代码调用点。如何修复慢查询修复 ClickHouse 慢查询的方法与技巧可参阅 PostHog 工程手册中的 ClickHouse 专题文档clickhouse 手册 相关的工程实践。小结一套可落地的性能优化流程综合文档与仓库实践PostHog 的查询性能优化可以沉淀为一条可重复执行的流水线选对引擎OLTP 可预测查询走 PostgreSQLOLAP 大规模分析走 ClickHouse写对查询只取所需字段与行、避免事务与子查询、选择合适数据类型、善用复合索引找对慢查询生产环境用 Performance Insights / pganalyze / Grafana /system.query_log/ 内部指标面板定位问题查询修对慢查询用EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) pev2 分析执行计划定位代码调用点针对性改写持续治理 IO 与索引周期性清理未使用索引用pg_stat_user_indexes排查以SeparateDatabaseAndStateRemoveIndexConcurrently非阻塞迁移以db_indexFalse/db_constraintFalse消除冗余索引与不必要的锁。这套方法论既有文档层面的最佳实践又有仓库中真实迁移文件如0212、0532、0035与模型定义如PersonDistinctId的代码级印证可直接用于 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创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表