.agents/skills/query-performance/SKILL.md
Turn "this query looks expensive" into numbers a reviewer can act on. The output is a verdict per clause — change, keep, or caller's call — each attached to a measurement, plus the negative results so nobody retries them.
ORDER BY, and anything paginated, where order decides which rows land on the page —
the comparison must include that order; compare order-independently only when the result genuinely
has no defined order. A faster query that answers a different question is not an optimization.
(Equivalence holds between the candidate and its variants. A candidate is often meant to change
results versus main; main is the cost reference, not a result reference.)The failure mode of this work is a confident mechanism the engine does not implement.
IN (SELECT …) are often built eagerly and
appear only as a literal (x in 35297-element set) with no read node — so counting
ReadFromMergeTree nodes undercounts work, and a dominant subquery can be invisible.EXPLAIN executes scalar and set subqueries to resolve index conditions: it is neither free nor
a timing proxy.main — see rendering.md.environments.md.main, ≥5 runs each, plan captured per call site, shape
recorded alongside. The delta between them is the cost of the change, and it is a finding in its
own right — often the one that matters most.instrumentation.md. Isolate the suspect subquery and measure
end-to-end; a gap between them is itself a finding.Put the four measured dimensions side by side — latency (p50, p90, p95, min), peak memory, CPU time, scanned/read (parts, granules, marks, rows) — for every shape and every call site, and decide from the whole set:
Make the scan numbers do their job: they should explain the latency, CPU and memory you measured. If they do not — scan is flat but memory doubled, or rows fell but CPU rose — you have not found the mechanism yet, and the verdict is not ready.
Whether you are reviewing a PR or opening one, justify every claim with a before/after table — one row per variant (or per revision), one column per measured dimension:
| variant | p50 ms | p90 ms | p95 ms | CPU ms | peak MiB | parts | granules (marks) | rows read |
|---|---|---|---|---|---|---|---|---|
main | … | … | … | … | … | … | … | … |
| candidate (before) | … | … | … | … | … | … | … | … |
| with the change (after) | … | … | … | … | … | … | … | … |
The scan columns are not optional: they are what makes the other three explainable, so carry the pruning evidence into the table rather than only the totals.
One table per call site and per shape, with the shape named (entity counts, run count). A verdict without its table is an opinion, and prose alone hides exactly the trade a reviewer needs to see.
Lead with what the change itself costs against main, per call site — a tuning delta of a few percent
does not outrank the endpoint getting materially more expensive, and if that cost is not acceptable,
say so and name the lever that would actually move it.
Then per clause: change (the replacement, why, before → after table), keep (what you tried and the number that killed it), or caller's call (both options, trade named). Then the equivalence evidence and the caveats. Do not include optimizations you did not measure, or a mechanism you did not verify.
opik-backend (clickhouse.md, testing.md) — DAO and ClickHouse conventions, plus the
Testcontainers rules including "one ClickHouse-migrating test class per mvn invocation".