Skip to content

perf(db): fast-path conversions with 1 event-property breakdown#352

Open
ayushjhanwar-png wants to merge 1 commit into
mainfrom
perf/conversion-fast-path-breakdown
Open

perf(db): fast-path conversions with 1 event-property breakdown#352
ayushjhanwar-png wants to merge 1 commit into
mainfrom
perf/conversion-fast-path-breakdown

Conversation

@ayushjhanwar-png

@ayushjhanwar-png ayushjhanwar-png commented Jul 3, 2026

Copy link
Copy Markdown

Summary

  • Extends buildArrayPatternSql in conversion.service.ts to accept an optional breakdown param. Emits a split-scan variant (user_installs + user_finishes CTEs joined by resolved profile) instead of the single-scan groupArrayIf.
  • Gate in getConversion relaxed to allow ONE simple event-level property breakdown (not profile.*, not cohort, not custom event). All other paths (holds, cohorts, custom events, session group, TTC, multi-breakdown) unchanged.

Why not the single-scan shape

Grouping the existing single-scan user_events by (user, breakdown) pins finishes to the install's breakdown bucket → under-counts conversions when the deep-link event carries empty or different breakdown values.

Measured on shortreels 30-day installReferrer → deepLinkCaptured with ref_utm_source breakdown:

Variant Elapsed Rows read Bytes read Semantics
ASOF LEFT JOIN (baseline) 35.6s
Single-scan fast-path 13.4s 1.06 B 77 GiB ❌ apps.instagram.com at 6-10% (real: 22-93%)
Split-scan fast-path (this PR) 20.1s 613 M 38 GiB ✅ matches ASOF results

1.77× speedup with semantics preserved. Bytes read halves too — each split scan filters to ONE event name.

What the query looks like

WITH filtered_profiles AS (...),  -- reused from existing fast-path when filters present
     al AS (SELECT alias, argMax(profile_id, created_at) AS canonical FROM profile_aliases WHERE project_id = ? GROUP BY alias),
     user_installs AS (SELECT resolved_pid, <breakdown_expr> AS b_0, groupArray(created_at) AS opens
                       FROM events WHERE name = '<first>' AND date range AND profile_id != ''
                       GROUP BY resolved_pid, b_0
                       HAVING length(opens) > 0),
     user_finishes AS (SELECT resolved_pid, groupArray(created_at) AS finishes
                       FROM events WHERE name = '<last>' AND date range AND profile_id != ''
                       GROUP BY resolved_pid),
     user_events AS (SELECT ui.*, coalesce(uf.finishes, []) FROM user_installs ui LEFT JOIN user_finishes uf USING (resolved_pid)),
     per_user_per_day AS (arrayJoin over opens per user, keep b_0 threaded),
     agg AS (GROUP BY event_day, b_0 → total_first / conversions / conversion_rate_percentage)
SELECT event_day, b_0, total_first, conversions, conversion_rate_percentage
FROM (dense_rank OVER (ORDER BY _bucket_rate DESC))
WHERE _bucket_rank <= topNLimit
ORDER BY _bucket_rate DESC, event_day ASC

Query builder rewrite (chart.service.ts.getSelectPropertyKey) auto-swaps properties['key'] → materialized column ref when one exists — so this fast-path benefits directly from any openpanel-materialize-analysis decisions.

Companion: ref_utm_source materialized on events

Not in this PR, but same session did:

  • Manually triggered openpanel-materialize-analysis CronJob → picked events:ref_utm_source (cardinality 10, well under the 5000 cap)
  • Backfilled partitions 202607 (~14 min) and 202606 (~30 min) via ALTER TABLE events MATERIALIZE COLUMN ... IN PARTITION
  • Full write-up in docs/conversion-chart-perf.md (included in this PR)

Combined effect on the shortreels dashboard query: 35.6s → 20.1s (1.77× faster) end-to-end, once the API pod's 1h materialized-columns cache refreshes (or on kubectl rollout restart deployment openpanel-api -n prod).

Test plan

  • pnpm --filter db typecheck — no new errors in conversion.service.ts (pre-existing errors in unrelated files remain)
  • Manual SQL smoke test on prod ClickHouse — shortreels 30-day breakdown query, 20.1s wall-clock, output matches ASOF result shape
  • Reviewer to verify: existing conversion-without-breakdown queries unchanged (single-scan path preserved)
  • Reviewer to verify: gate correctly rejects profile.* / cohort / custom-event breakdowns → falls through to ASOF
  • Post-merge: watch system.query_log for conversion queries with 1 breakdown — expect wall-clock drop

Summary by CodeRabbit

  • New Features

    • Faster conversion charts now support one event-property breakdown in the optimized path, helping top-N breakdown results load more efficiently.
    • Improved handling for conversion performance analysis and tracking notes in the documentation.
  • Bug Fixes

    • Conversion queries now preserve intended results when using supported breakdowns, with more consistent conversion attribution and ranking.

Extends buildArrayPatternSql to accept an optional breakdown param and
emits a split-scan variant (user_installs + user_finishes CTEs joined by
resolved profile). Grouping a single-scan by (user, breakdown) pins
finishes to the install's breakdown bucket and under-counts (measured
6-10% for apps.instagram.com vs. correct 22-93%). Split scans preserve
the "convert on the user, not on the utm-tagged finish" semantic.

Prod-measured on shortreels 30-day installReferrer→deepLinkCaptured
with ref_utm_source breakdown: 35.6s (ASOF path) → 20.1s (this fast
path) = 1.77x. Bytes read halves too (77 GiB → 38 GiB) because each
split scan filters to one event name.

Gate: only fires when breakdown is a simple event-level property (not
profile.*, not cohort, not custom event). All other paths (holds,
cohorts, custom events, session group, TTC, multi-breakdown) unchanged.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends the ClickHouse-based conversion fast path to support a single event-level property breakdown by generating a split-scan SQL variant, widens fast-path eligibility checks accordingly, and adds documentation tracking conversion-chart performance investigations and shipped optimizations.

Changes

Conversion fast-path breakdown support

Layer / File(s) Summary
Breakdown-aware SQL generation
packages/db/src/services/conversion.service.ts
buildArrayPatternSql gains breakdown/topNLimit params and a new split-scan branch building user_installs/user_finishes CTEs, grouping conversions by b_0, and applying dense-rank top-N filtering.
Fast-path eligibility and response typing
packages/db/src/services/conversion.service.ts
canUseArrayPath now allows up to one simple event-level property breakdown via isSimpleEventPropertyBreakdown; getConversion passes breakdown/topNLimit through, comments are updated, and response data typing gains an index signature.
Performance investigation docs
docs/conversion-chart-perf.md
Documents the 2026-07-04 update (ref_utm_source materialization, breakdown fast path), prior 2026-05-13 root-cause findings, fixes shipped, tests run, next steps, and diagnostic SQL snippets.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant getConversion
  participant buildArrayPatternSql
  participant ClickHouse

  Client->>getConversion: request conversion with breakdown
  getConversion->>getConversion: canUseArrayPath (breakdowns.length <= 1)
  alt eligible fast path
    getConversion->>buildArrayPatternSql: breakdown, topNLimit
    buildArrayPatternSql->>buildArrayPatternSql: build split-scan CTEs
    buildArrayPatternSql->>ClickHouse: execute breakdown-grouped query
    ClickHouse-->>getConversion: top-N breakdown rows
  else not eligible
    getConversion->>ClickHouse: fall back to ASOF path
    ClickHouse-->>getConversion: conversion rows
  end
  getConversion-->>Client: conversion response
Loading

Suggested reviewers: harshitsaamu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clear and specific: it summarizes the DB conversion fast path and support for one event-property breakdown.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/conversion-fast-path-breakdown

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
docs/conversion-chart-perf.md (1)

111-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add language identifiers to fenced code blocks.

Static analysis (markdownlint MD040) flags these fences as missing a language hint (e.g., sql for the `ORDER BY` snippets, text for the ProfileEvents dump).

Also applies to: 352-365

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/conversion-chart-perf.md` around lines 111 - 121, Add language
identifiers to the fenced code blocks in the markdown snippet so it passes
markdownlint MD040. Update the `ORDER BY` examples in the conversion chart doc
to use an appropriate fence like `sql`, and ensure the other fenced dumps in the
same section (including the ProfileEvents example referenced by the comment) use
a matching language tag such as `text`.

Source: Linters/SAST tools

packages/db/src/services/conversion.service.ts (1)

293-459: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Significant SQL duplication between the breakdown and non-breakdown branches.

The breakdown branch (372-457) and the existing single-scan branch (460-514) repeat the same al alias-resolution CTE text, WITH ${filteredProfilesCte} prefix, and per_user_per_day arrayJoin pattern almost verbatim, with only b_0 threading and the split install/finish scans differing. A future fix to alias resolution or the day-bucketing logic would need to be applied in both places, risking drift.

Consider extracting the shared fragments (e.g., the al CTE text, the day-bucketing arrayJoin block) into local string constants/helpers reused by both branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/services/conversion.service.ts` around lines 293 - 459, The
breakdown and non-breakdown paths in conversion.service.ts duplicate the same
SQL fragments, especially the alias-resolution CTE (`al`), the `WITH
${filteredProfilesCte}` prefix, and the per_user_per_day day-bucketing logic.
Extract these shared pieces into local constants or helper builders inside the
conversion query method, then reuse them in both the breakdown branch and the
single-scan branch so future changes to alias resolution or date bucketing only
need to be made once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/src/services/conversion.service.ts`:
- Around line 584-592: The comment references a nonexistent helper, so update
the array-aggregate path note in conversion.service to match the actual
implementation. Replace the mention of buildArrayPatternWithBreakdownSql with
the real branch inside buildArrayPatternSql, and make it clear that the one
event-level breakdown case is handled by that function rather than a separate
helper. Keep the wording aligned with the surrounding fast-path comments so
readers can find the correct logic quickly.

---

Nitpick comments:
In `@docs/conversion-chart-perf.md`:
- Around line 111-121: Add language identifiers to the fenced code blocks in the
markdown snippet so it passes markdownlint MD040. Update the `ORDER BY` examples
in the conversion chart doc to use an appropriate fence like `sql`, and ensure
the other fenced dumps in the same section (including the ProfileEvents example
referenced by the comment) use a matching language tag such as `text`.

In `@packages/db/src/services/conversion.service.ts`:
- Around line 293-459: The breakdown and non-breakdown paths in
conversion.service.ts duplicate the same SQL fragments, especially the
alias-resolution CTE (`al`), the `WITH ${filteredProfilesCte}` prefix, and the
per_user_per_day day-bucketing logic. Extract these shared pieces into local
constants or helper builders inside the conversion query method, then reuse them
in both the breakdown branch and the single-scan branch so future changes to
alias resolution or date bucketing only need to be made once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3796333d-df75-4f48-8950-914c40ecbce2

📥 Commits

Reviewing files that changed from the base of the PR and between 09ff2c6 and 8fe863c.

📒 Files selected for processing (2)
  • docs/conversion-chart-perf.md
  • packages/db/src/services/conversion.service.ts

Comment on lines +584 to +592
// Array-aggregate fast path. 3-7x speedup vs the multi-CTE ASOF JOIN
// path below. Two shapes:
// - No breakdown: single-scan groupArrayIf (buildArrayPatternSql)
// - One event-level breakdown: split installs/finishes CTEs
// (buildArrayPatternWithBreakdownSql) — 1.77x measured on prod
// shortreels 30-day query.
// Everything unsupported (holds, cohorts, custom events, session group,
// TTC, profile.* filters, profile.* / cohort breakdowns, multi-breakdown)
// falls through to the ASOF path unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Comment references a function that doesn't exist.

The comment says the breakdown path uses buildArrayPatternWithBreakdownSql, but the implementation is a branch inside buildArrayPatternSql (see line 370) — there's no separate function by that name in this file. This could mislead future readers searching for it.

✏️ Suggested fix
-    //   - One event-level breakdown: split installs/finishes CTEs
-    //     (buildArrayPatternWithBreakdownSql) — 1.77x measured on prod
-    //     shortreels 30-day query.
+    //   - One event-level breakdown: split installs/finishes CTEs
+    //     (buildArrayPatternSql's `breakdown` branch) — 1.77x measured on
+    //     prod shortreels 30-day query.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Array-aggregate fast path. 3-7x speedup vs the multi-CTE ASOF JOIN
// path below. Two shapes:
// - No breakdown: single-scan groupArrayIf (buildArrayPatternSql)
// - One event-level breakdown: split installs/finishes CTEs
// (buildArrayPatternWithBreakdownSql) — 1.77x measured on prod
// shortreels 30-day query.
// Everything unsupported (holds, cohorts, custom events, session group,
// TTC, profile.* filters, profile.* / cohort breakdowns, multi-breakdown)
// falls through to the ASOF path unchanged.
// Array-aggregate fast path. 3-7x speedup vs the multi-CTE ASOF JOIN
// path below. Two shapes:
// - No breakdown: single-scan groupArrayIf (buildArrayPatternSql)
// - One event-level breakdown: split installs/finishes CTEs
// (buildArrayPatternSql's `breakdown` branch) — 1.77x measured on
// prod shortreels 30-day query.
// Everything unsupported (holds, cohorts, custom events, session group,
// TTC, profile.* filters, profile.* / cohort breakdowns, multi-breakdown)
// falls through to the ASOF path unchanged.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/services/conversion.service.ts` around lines 584 - 592, The
comment references a nonexistent helper, so update the array-aggregate path note
in conversion.service to match the actual implementation. Replace the mention of
buildArrayPatternWithBreakdownSql with the real branch inside
buildArrayPatternSql, and make it clear that the one event-level breakdown case
is handled by that function rather than a separate helper. Keep the wording
aligned with the surrounding fast-path comments so readers can find the correct
logic quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant