Commit 247744a
perf(cohorts): re-key the cohort summary MVs for the criteria that read them (#458)
* perf(cohorts): re-key the cohort summary MVs for the criteria that read them
profile_event_summary_mv is ordered (project_id, profile_id, name,
event_date), and the property variant similarly with property_key after
name. Cohort criteria filter on name, a date range and optionally a
property, then GROUP BY profile_id. They never filter profile_id, so with
profile_id second the usable key prefix ends at project_id and every
criterion reads the project's entire slice of the MV however narrow the
criterion is. Cohorts run one such query per criterion, on a schedule.
Measured on a ~100M-row summary MV: a criterion that can only prune on
project_id reads 103,985,861 rows (10.9s); the same criterion with name
and event_date in the key prefix reads 115,141 rows (0.11s). Reproduced
from scratch on a seeded 373K-row MV: 373,500 rows read vs 28,348.
A sort key cannot be altered in place, so migration 20 creates
replacement MVs keyed for the consumer:
event_profile_summary_mv
(project_id, name, event_date, profile_id)
event_property_profile_summary_mv
(project_id, name, property_key, property_value, event_date, profile_id)
The SELECT bodies, the identity filter and the aggregate columns are
copied verbatim, so the new tables hold exactly the same rows as the old
ones and only the physical order differs. Verified locally: both old and
new MVs receive identical row counts from the same inserts.
populate: false, with history backfilled by a companion script that is
deliberately not a numbered migration so it cannot run inside the
migration container. These are AggregatingMergeTree tables, so re-running
a populated range double counts; the script is month-partition-aligned
with an explicit --replace retry path and requires --until to avoid
double counting the window the live trigger already covers.
The old MVs are left in place and keep receiving inserts, so this is
reversible by pointing cohort.service back at them. Dropping them is a
one-line follow-up migration once the new ones are verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(cohorts): run the summary MV backfill as an idempotent migration
The backfill was a hand-run script: it appended to the MVs, so it needed
an exact --until to avoid double counting rows the live trigger had
already written, and it refused to run without one. It also mixed
topologies, resolving its target to the node-local <mv>_replicated while
reading from the Distributed events.
Make it migration 21 and rebuild rather than append. Each month's
partition is dropped and rebuilt from events, which is idempotent by
construction: an evicted migration pod is resumed by running again, and a
month that was double counted heals on the next pass. That in turn removes
the need to know when the MVs were created, so no operator input is
required and a small deployment gets working cohorts with no flags.
The bound is now read per month, straight after that month's drop. A run
can take hours and the current month is dropped at the end of it, so a
single bound taken at the start would delete every row the trigger wrote
during the run and then decline to rebuild them. Boundary strings keep
milliseconds for the same reason: created_at is DateTime64(3), and
truncating the final batch to whole seconds drops the events in the
boundary second after their trigger rows are gone.
Reads and writes both go through the Distributed tables, so one run from
one node covers every shard. DROP PARTITION is the exception, since the
Distributed engine rejects partitioning, so it goes ON CLUSTER against the
local table.
Rebuilding a large events table unattended is still not wanted, so it
steps aside above COHORT_BACKFILL_MAX_EVENTS rows and prints the manual
command. The same file remains directly executable for supervised runs.
Verified on standalone 26.1.3 and a keeper-backed 2-shard cluster 25.3:
fresh install no-ops, history plus live-trigger overlap matches ground
truth computed from events for both tables, one run writes both shards,
re-runs from either node are stable, and a deliberately double-counted
month returns to exact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(cohorts): drop the size guard, rebuild newest month first
The COHORT_BACKFILL_MAX_EVENTS threshold and its --force escape hatch are
gone. The constant was an invented number standing in for "how long will
this take", which row count is a poor proxy for, and it made the automatic
path conditional on a guess.
Months now rebuild newest first. Cohort criteria mostly use relative
timeframes, so the recent months are the ones that make cohorts correct
again, and an interrupted run leaves the useful end done.
Also drop the caveat about cohorts under-counting mid-rebuild needing
attention: the cohortRefresh cron already recomputes every non-static
cohort every 30 minutes, so it resolves on its own.
Re-verified on standalone 26.1.3 and a 2-shard cluster 25.3 from empty
databases: both tables match ground truth computed from events, re-runs
from either node are stable, and a deliberately double-counted month
returns to exact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent a62e387 commit 247744a
5 files changed
Lines changed: 531 additions & 4 deletions
File tree
- packages/db
- code-migrations
- src
- clickhouse
- services
Lines changed: 143 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
0 commit comments