Skip to content

chore(bench): sort-key / read_in_order spike scaffolding (ClickStack Phase 3)#171

Open
Makisuo wants to merge 1 commit into
feat/clickstack-index-optimizationsfrom
feat/clickstack-sortkey-spike
Open

chore(bench): sort-key / read_in_order spike scaffolding (ClickStack Phase 3)#171
Makisuo wants to merge 1 commit into
feat/clickstack-index-optimizationsfrom
feat/clickstack-sortkey-spike

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #170 (uses the bench suite --table-map A/B harness from that PR).

What this is

Measurement scaffolding, not a production change. It builds throwaway shadow tables with candidate sort keys, backfills one busy org's window, and A/Bs them with the committed benchmark suite — to answer: can a time-bucketed primary key activate optimize_read_in_order and let us delete the two-stage list-query cutoff, without regressing service-scoped reads, attribute-bloom pruning, or compression?

Files (apps/api/scripts/spike/)

  • sortkey-variants.ts — candidate sort keys (t1/t2/t3, l1/l2/l3; OrgId always first for tenant scoping) + pure builders for CREATE TABLE AS shadow tables, per-day windowed backfill INSERTs, and EXPLAIN read_in_order probes.
  • emit-spike-sql.ts — dependency-free emitter (--mode setup|backfill|probe|drop), runs with bare bun in a fresh worktree; pipe its output at a dedicated ClickHouse.
  • SPIKE-SORTKEY.md — the runbook + explicit promote/reject decision criteria (list p50 −40% or read_rows −80%, service-scoped regression ≤ +25%, attr-bloom regression ≤ +25% read_rows, compression ≤ +15%).

Candidate keys

Shadow ORDER BY Trade-off
*_t1/l1 current key (rebuilt) control (like-aged)
*_t2/l2 (OrgId, toStartOfFiveMinutes(ts), ServiceName, ts) bucket + service locality
*_t3/l3 (OrgId, ts) pure time-first (upper bound)

Testing

10 spike unit tests (SQL-builder shape, OrgId-first invariant, day-windowing, emitter modes) + the 11 bench tests, all green; apps/api typecheck clean.

Running it needs a dedicated ClickHouse

Per the runbook — not a Tinybird branch (shared prod compute → noisy wall-time; lean on deterministic read_rows/read_bytes). Couldn't execute the live A/B in this environment.

🤖 Generated with Claude Code


Open in Devin Review

@pullfrog

pullfrog Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Your LLM provider API key was rejected. Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.

Update repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Open in Devin Review

label: "control — current key, freshly rebuilt (compare like-aged, not vs the aged prod table)",
partitionBy: "toDate(Timestamp)",
orderBy: "(OrgId, ServiceName, SpanName, toDateTime(Timestamp))",
tsColumn: "Timestamp",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Trace sort-key probe queries the wrong column, so the benchmark will always report the optimization as not working

The read-in-order probe orders by the raw nanosecond-precision column (ORDER BY Timestamp DESC at apps/api/scripts/spike/sortkey-variants.ts:144) instead of the second-precision expression (toDateTime(Timestamp)) that appears in every traces sort key, so ClickHouse cannot match the query ordering to the sort key and optimize_read_in_order will never engage for any traces variant.

Impact: The spike will falsely conclude that the candidate sort keys do not activate read-in-order for traces, producing misleading benchmark results and potentially discarding a valid optimization.

Sort-key expression mismatch between probe and ORDER BY

All three traces variants (traces_t1, traces_t2, traces_t3) define their sort key using toDateTime(Timestamp) — e.g. apps/api/scripts/spike/sortkey-variants.ts:48 has ORDER BY (OrgId, toStartOfFiveMinutes(toDateTime(Timestamp)), ServiceName, toDateTime(Timestamp)). The Timestamp column in the traces table is DateTime64(9) (nanosecond precision), confirmed at packages/domain/src/generated/clickhouse-schema.ts:36.

readInOrderProbeSQL at apps/api/scripts/spike/sortkey-variants.ts:138-146 generates ORDER BY ${v.tsColumn} DESC, which resolves to ORDER BY Timestamp DESC for traces. Since Timestamp is DateTime64(9) and the sort key uses toDateTime(Timestamp) (second precision), these are different orderings — multiple nanosecond-distinct values collapse to the same second. ClickHouse cannot prove monotonic compatibility, so it will not engage read_in_order.

The logs variants are unaffected because tsColumn is TimestampTime (already DateTime), which matches the sort key expression directly.

Fix: either change tsColumn for traces variants to toDateTime(Timestamp), or add a dedicated probeOrderByExpr field to SortKeyVariant that carries the exact expression from the sort key.

Prompt for agents
The readInOrderProbeSQL function at sortkey-variants.ts:138-146 generates ORDER BY using v.tsColumn, which for traces is the raw DateTime64(9) column 'Timestamp'. But all traces sort keys use toDateTime(Timestamp) (second-precision cast). ClickHouse's optimize_read_in_order requires the query ORDER BY to match the sort key expression exactly, so the probe will always show a false negative for traces.

The fix needs to ensure the probe's ORDER BY expression matches what's in the sort key. Options:
1. Add a new field to SortKeyVariant (e.g. probeOrderByExpr) that carries the exact timestamp expression from the sort key (toDateTime(Timestamp) for traces, TimestampTime for logs), and use it in readInOrderProbeSQL.
2. Change tsColumn for traces variants from 'Timestamp' to 'toDateTime(Timestamp)'. This would also affect backfillSQL where it's used in WHERE clauses, but toDateTime(Timestamp) >= toDateTime('...') still works correctly for filtering.

Option 1 is cleaner because it separates the backfill windowing column from the probe ordering expression. The test at sortkey-variants.test.ts:54 that checks for 'ORDER BY Timestamp DESC' would also need updating to expect 'ORDER BY toDateTime(Timestamp) DESC'.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Measurement tooling to decide whether a time-bucketed primary key can replace
the two-stage list-query cutoff (read_in_order). Ships NO production change.

- spike/sortkey-variants.ts: candidate sort keys (t1/t2/t3, l1/l2/l3, OrgId
  always first) + shadow-table / backfill / read_in_order-probe SQL builders.
- spike/emit-spike-sql.ts: dependency-free emitter (setup | backfill | probe |
  drop) — runs with bare bun in a fresh worktree.
- spike/SPIKE-SORTKEY.md: runbook + explicit promote/reject decision criteria.

Drives the A/B via `bench suite --table-map` against a dedicated ClickHouse.
Tests: 10 spike + 11 bench.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Makisuo Makisuo force-pushed the feat/clickstack-sortkey-spike branch from efc5a48 to 8909dd6 Compare July 3, 2026 22:57

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +138 to +147
export function readInOrderProbeSQL(v: SortKeyVariant, opts: { orgId: string; limit?: number }): string {
const limit = opts.limit ?? 50
return (
`EXPLAIN actions = 1, indexes = 1\n` +
`SELECT * FROM ${v.name}\n` +
`WHERE OrgId = ${lit(opts.orgId)}\n` +
`ORDER BY ${v.tsColumn} DESC\n` +
`LIMIT ${limit}`
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Probe query uses raw Timestamp (DateTime64) but sort keys use toDateTime(Timestamp) — read_in_order may not engage for traces

The readInOrderProbeSQL function emits ORDER BY Timestamp DESC (sortkey-variants.ts:144), but all three traces sort-key candidates use toDateTime(Timestamp) (a DateTime, seconds-precision) in their ORDER BY — e.g. traces_t3 has (OrgId, toDateTime(Timestamp)) at sortkey-variants.ts:56. ClickHouse's optimize_read_in_order requires the query's ORDER BY to match a suffix of the sort key. Whether CH recognizes Timestamp (DateTime64) as monotonically equivalent to toDateTime(Timestamp) (DateTime) is version-dependent.

This is intentional — the probe tests the real production query pattern (see packages/query-engine/src/ch/queries/traces.ts:512 which confirms production queries use ORDER BY Timestamp DESC). But if CH doesn't recognize the monotonic relationship, the probe will show no read_in_order for ANY traces variant (including t2/t3), making the spike inconclusive. The SPIKE-SORTKEY.md procedure (line 38) expects to see read_in_order on t2/t3 but not t1 — if it's absent on all three, the operator should check whether using ORDER BY toDateTime(Timestamp) DESC in the probe (or Timestamp directly in the sort key) changes the result before concluding the sort key doesn't help.

Logs variants don't have this issue: TimestampTime is already a plain DateTime and appears directly in the sort key.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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