Repository navigation
perf(hogql): reuse shared database in runners that bypassed it - #90965
Conversation
HogQLQuery, EventsQuery, SessionsQuery, GroupsQuery, ActorsQuery, InsightActorsQuery, SessionsTimelineQuery, and PropertyValuesQuery called execute_hogql_query without a context, so the executor built its own HogQL database instead of reusing the runner's shared one. Thread context=build_hogql_context() through those call sites so one build serves every call in a run. Generated-By: PostHog Desktop Task-Id: 298f3504-9b69-4cb1-bb9d-908e0b4610d5
|
😎 Merged successfully - details. |
🤖 CI report
|
hogql_database_build_duration_seconds counts builds but cannot say
which call path requested them, so redundant executor-side builds are
invisible in aggregate. Add hogql_database_build_total{trigger} and
label the query-execution seams (shared, executor, printer,
runner_context, shared_kill_switch); all other callers count as
direct.
Generated-By: PostHog Desktop
Task-Id: 298f3504-9b69-4cb1-bb9d-908e0b4610d5
👀 Auto-assigned reviewersThese soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:
Soft owners come from each directory's |
🦔 PostHog Review reviewed this pull requestFound 0 must fix, 2 should fix, 1 consider. Published 3 findings (view the review). Resolved comments: 1 fixed, 1 already settled |
|
Reviews (1): Last reviewed commit: "perf(hogql): attribute database builds t..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR extends the “shared HogQL database per runner run” optimization beyond insight runners to additional HogQL query runners that previously rebuilt the database inside the executor/printer, reducing redundant schema builds during a single calculation. It also adds a Prometheus counter to attribute database builds by the call path that triggered them, making regressions/stragglers visible in aggregate.
Changes:
- Thread a shared
HogQLContext(backed byQueryRunner.shared_database) into more runners’execute_hogql_querycalls to avoid executor/printer rebuilding the database within the same run. - Add
hogql_database_build_total{trigger=...}attribution via a new counter incremented inDatabase.create_for(trigger=...), and threadtriggerthrough executor/printer/shared/runner-context build sites. - Add regression tests that assert a single
Database.create_forper calculation for multiple runners and ensure shared-db SQL matches the executor-built SQL when modifiers reshape schema.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| posthog/hogql/query.py | Adds trigger="executor" when the executor builds a database, improving build attribution. |
| posthog/hogql/printer/utils.py | Adds trigger="printer" when the printer must build its own database. |
| posthog/hogql/editor_assist_metrics.py | Introduces HOGQL_DATABASE_BUILD_TOTAL counter (exported as hogql_database_build_total). |
| posthog/hogql/database/database.py | Adds trigger parameter to Database.create_for and increments the new counter per build. |
| posthog/hogql_queries/query_runner.py | Attributes shared-db builds (shared, shared_kill_switch) and runner-context builds (runner_context); exposes build_hogql_context() helper. |
| posthog/hogql_queries/hogql_query_runner.py | Passes shared context when not using a direct connection (connectionId is None). |
| posthog/hogql_queries/events_query_runner.py | Threads shared context into main events query and session-recordings check to avoid double builds. |
| posthog/hogql_queries/sessions_query_runner.py | Threads shared context into sessions execution path. |
| posthog/hogql_queries/sessions_timeline_query_runner.py | Switches manual HogQLContext(...) construction to build_hogql_context(...). |
| posthog/hogql_queries/property_values_query_runner.py | Switches manual HogQLContext(...) construction to build_hogql_context(...) for event/person paths. |
| posthog/hogql_queries/groups/groups_query_runner.py | Switches manual context to build_hogql_context(globals=...) while preserving required globals. |
| posthog/hogql_queries/actors_query_runner.py | Threads shared context into execution and validation/printing path to avoid redundant builds. |
| posthog/hogql_queries/insights/insight_actors_query_runner.py | Threads shared context into insight actors execution. |
| posthog/hogql_queries/test/test_query_runner.py | Adds parameterized test asserting one Database.create_for per calculation across multiple runners. |
| posthog/hogql_queries/test/test_hogql_query_runner.py | Adds test asserting shared-db SQL matches executor-built SQL when query-level modifiers reshape schema. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The persons list now touches the shared-database kill switch, whose instance setting costs one cold-cache query in tests. Generated-By: PostHog Desktop Task-Id: 298f3504-9b69-4cb1-bb9d-908e0b4610d5
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
The shared per-run database is built from self.modifiers, but the executor was still handed self.query.modifiers or self.modifiers. When a caller sets both constructor and query modifiers, the schema is built from one set while the SQL is printed and reported for another. Pass self.modifiers so the printer and shared database always agree. Add a regression test that catches the reported modifiers diverging from the schema the SQL was built against. Generated-By: PostHog Desktop Task-Id: d8a05cb6-9f8f-4e00-8ce3-37018a0bbb98
to_query() called replace_filters() without a database, so a HogQL query
carrying a {filters} placeholder plus dashboard/insight filters built one
throwaway database there (the "direct" trigger) and then the shared database
in _calculate. That is two full schema builds for a common SQL-insight shape,
the exact redundancy this PR removes elsewhere.
Pass the shared database into replace_filters when there is no connection id,
so the filtered path builds the schema once. replace_filters uses the database
only to classify the query's FROM tables (events/sessions/logs/traces/groups/
persons), which is invariant to user and modifiers for those core tables, so
the generated SQL is unchanged. Connection-id queries keep the per-call build,
since their schema is the external connection's.
Add a filtered-query case to the build-count test, which asserts one
Database.create_for call; it fails without the threading.
Generated-By: PostHog Desktop
Task-Id: ed4c0bb6-29a7-4129-bd13-0a8ecdc8dffb
thmsobrmlr
left a comment
There was a problem hiding this comment.
✅ (only checked product analytics related files)
The shared database built its own UserAccessControl inside the system-table access check, re-loading the RBAC rows the runner had already fetched for its cache fingerprint. Pass the runner's snapshot into both shared_database build paths so one instance serves the fingerprint and the schema, as the executor's own build already did. Generated-By: PostHog Desktop Task-Id: 298f3504-9b69-4cb1-bb9d-908e0b4610d5
There was a problem hiding this comment.
Approved.
This is an internal query-execution performance optimization (reusing an already-existing shared-database mechanism across more runners), not schema/data-model, API-contract, billing, auth, or CI/deploy territory, so my own read of the diff suffices. The riskiest part — making sure the shared build matches what a per-call build would have produced, including when query-level modifiers reshape the schema — is covered by new targeted tests (build-once assertions per runner, SQL-equivalence test under modifiers), and no reviewer bot or human raised an unresolved must-fix.
- Author wrote 8% of the modified lines and has 84 merged PRs in these paths (familiarity MODERATE).
- 👍 on the PR from greptile-apps[bot].
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 79L, 13F substantive, 167L/16F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (167L, 16F, single-area, perf) |
| stamphog 2.0.0b4 | .stamphog/policy.yml @ aa36e22 · reviewed head aa36e22 |
mariusandra
left a comment
There was a problem hiding this comment.
Found one blocking authorization-state issue.
ActorsQueryRunner.run() assigns self.user = user before delegating to QueryRunner.run(). The base runner therefore sees no identity change and never calls _on_user_changed(), which clears _shared_database and _user_access_control. Before this PR, the actors execution path built a fresh database per calculation; context=self.build_hogql_context() at posthog/hogql_queries/actors_query_runner.py:204 now makes a second run reuse the first user's access-controlled schema.
Please let the base class own the user transition, or explicitly invalidate before the preassignment, and add a regression test that runs one ActorsQueryRunner as two users and asserts the database/access-control snapshot rebuilds. The new build-count tests otherwise pass locally (8/8).
ActorsQueryRunner.run() assigned self.user before delegating to the base run(), so the base saw no identity change and never dropped the shared database or access-control snapshot. A second run as a different user reused the first user's access-controlled schema. Perform the transition with invalidation instead of a bare assignment, keeping this runner's take-the-run-user-verbatim semantics including None. Generated-By: PostHog Desktop Task-Id: 298f3504-9b69-4cb1-bb9d-908e0b4610d5
A new stamphog review started for this PR — the fresh verdict replaces this approval.
|
@mariusandra Fixed in a4ccd8a — thanks for catching this.
Regression test as requested: I checked the other runners for the same pattern — |
There was a problem hiding this comment.
Not approved yet — waiting on the conditions below.
Re-add the stamphog label to request another review once you have addressed this.
Gates denied due to an unresolved CHANGES_REQUESTED review from mariusandra identifying a real authorization/access-control bug (reusing a stale user's access-controlled database across users in ActorsQueryRunner), and nothing in later comments shows it was fixed.
- Author wrote 7% of the modified lines and has 84 merged PRs in these paths (familiarity MODERATE).
- 👍 on the PR from greptile-apps[bot].
- Unresolved CHANGES_REQUESTED review from @mariusandra: ActorsQueryRunner sets self.user before delegating to the base runner, so the shared database/access-control cache isn't invalidated on user change, potentially leaking one user's access-controlled schema to another user's query.
- This is security-sensitive (access control) territory with an active blocking review, so it cannot be auto-approved.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✗ | changes requested by: mariusandra |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 86L, 13F substantive, 195L/16F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (195L, 16F, single-area, perf) |
| stamphog 2.0.0b4 | .stamphog/policy.yml @ a4ccd8a · reviewed head a4ccd8a |
mariusandra
left a comment
There was a problem hiding this comment.
Second review complete. The original cross-user invalidation issue is fixed.
I also added two follow-up safeguards in 7a47931: conflicting modifier precedence now gets a rollout-safe cache namespace, and a matching preloaded access-control snapshot survives the Actors runner's first user transition.
Focused regression tests, Ruff, formatting, preflight, and repo-wide mypy all pass. No remaining merge-blocking findings.
|
/trunk merge |
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Marius Andra <marius.andra@gmail.com>
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Marius Andra <marius.andra@gmail.com>
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Marius Andra <marius.andra@gmail.com>
Problem
Building the per-team HogQL database is a large share of query compile time, and eight query runners rebuild it inside the executor on every calculation. These runners power SQL insights, the events explorer, person and group lists, session timelines, and property-value dropdowns.
#88183 added a shared per-run database on the runner base class, but only the insight runners (trends, funnels, retention, paths) use it. The existing
hogql_database_build_duration_secondshistogram counts builds and their duration, but cannot attribute a build to the call path that requested it, so redundant builds are invisible in aggregate.Changes
context=self.build_hogql_context()intoexecute_hogql_query, so one shared build serves every call in a run.*columns built the database twice per run (main query plus the recordings check); they now build once.hogql_database_build_total{trigger}attributes every build to its call path:shared,executor,printer,runner_context,shared_kill_switch, ordirect(all other callers). After deploy,executorandprinterrates drop andsharedrises; watch it withsum(rate(hogql_database_build_total[5m])) by (trigger).timingspayload now recordsbuild_shared_databasein place of the executor'screate_hogql_databaseentry.HOGQL_SHARED_INSIGHT_DATABASE_ENABLEDinstance setting covers these call sites; disabling it restores per-call builds (visible as theshared_kill_switchtrigger).triggerparameter threading is mechanical.How did you test this code?
TestRunnersBuildDatabaseOnce(parameterized over 7 runners) asserts oneDatabase.create_forcall per calculation, made through the shared path. All 7 cases fail on master and pass on this branch, so it catches any runner losing the threading.test_calculate_with_query_modifiers_matches_unthreaded_executor_sqlcatches the shared database diverging from the executor's own build when query-level modifiers reshape the schema (e.g.sessionTableVersion).test_insight_actors_query_runner.pythat reproduce identically on unmodified master in this environment (the local person-on-events default differs from the snapshot environment) - unrelated to this diff.hogli ci:preflight --fixpass locally.Automatic notifications
Docs update
None - no user-facing behavior change.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
build_shared_databasetiming so it fails without the threading.Created with PostHog Desktop