Skip to content
Back to skills

Clickhouse Autoresearch Campaign

ASecurity

Run a ClickHouse query optimization campaign on one git branch using pi-autoresearch, dynamic lanes and hypotheses, baseline result capture, correctness checks, and stagnation-aware lane/campaign review.

  • 40,048 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmentpythonc++bashsqlexpressgitdatabasedocumentation

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 5, 2026

npx -y skills add PostHog/posthog --skill clickhouse-autoresearch-campaign --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clickhouse Autoresearch Campaign?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clickhouse Autoresearch Campaign
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/posthog-clickhouse-autoresearch-campaign/badge)](https://www.skillsdirectory.com/skills/posthog-clickhouse-autoresearch-campaign)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clickhouse-autoresearch-campaign
description: Run a ClickHouse query optimization campaign on one git branch using pi-autoresearch, dynamic lanes and hypotheses, baseline result capture, correctness checks, and stagnation-aware lane/campaign review.
---

# ClickHouse Autoresearch Campaign

This skill packages the orchestration for optimizing one ClickHouse query on one git branch.

## Required reads

Before taking action, read `orchestration.md` (sibling of this file) completely. Treat it as the operating contract.

## Preconditions

This skill assumes:

- `pi-autoresearch` is installed and its tools are available
- the current directory is a git repository
- you have a target query or enough context to identify one
- the operator will provide or help configure `.clickhouse-autoresearch/adapter.json`

## Branch rule

One campaign = one git branch.

If the current branch is not a dedicated campaign branch yet, create one before initializing the workspace.

## Workspace rule

Use a single workspace at:

```text
.clickhouse-autoresearch/
```

The branch is the campaign boundary. The workspace is just the artifact layout.

If `autoresearch.config.json` exists in the current working directory, read its `workingDir` field and use that path as the workspace instead of the default above. Automated orchestrators (for example PostHog's `run_campaign.py`) initialize the workspace at `/tmp/autoresearch-campaign/` and write the config alongside it.

## Pre-initialized workspace detection

**Before doing anything in the Setup sequence, check whether the workspace has already been prepared by an external orchestrator.** If the resolved workspace contains **all** of:

- `adapter.json`
- `baseline/metrics.json`
- `query/original.sql`

…then the workspace is pre-initialized. In that case:

- **Skip the entire Setup sequence (steps 1–6).** Do not ask the operator for a target query, connection details, or anything else — the orchestrator has already supplied them.
- Jump directly to step 7 of the Setup sequence (read the baseline and seed the first lanes and hypotheses), then continue with steps 8–9 and the normal campaign loop.
- Operate headlessly: at no point prompt the operator for input. If a decision requires judgment, apply the skill's default guidance and record the choice in `state.json` / `autoresearch.md`.

Only fall back to the interactive Setup sequence below when the workspace is empty or partially initialized.

## Adapter capabilities (what you can and cannot run)

Campaign queries flow through the adapter configured in `adapter.json`. Every campaign script (`ch_capture_baseline.py`, `ch_run_candidate.py`, any ad-hoc probe) ultimately submits SQL through this adapter, and the adapter enforces what ClickHouse sees.

When `adapter.json` has `type: "coordinator"`, your SQL is routed to whichever ClickHouse the host-side coordinator is pointed at — typically a read-only test cluster or a local dev ClickHouse. Either way the cluster runs SQL under a profile that pins `readonly = 2`, so writes (INSERT, ALTER, CREATE, OPTIMIZE, SYSTEM, TRUNCATE, DROP, ATTACH, DETACH) will fail with a ClickHouse error. **Read `GET /v1/info` (or check `autoresearch.md` — the coordinator's prompt addendum is prepended there)** before issuing any predicate that depends on a specific `team_id`: the prompt addendum tells you which `team_id` the target cluster has data for, and you must rewrite team_id predicates to match it. For experiments treat every read-only statement form as available:

- `SELECT …` — arbitrary subqueries, CTEs, joins
- `WITH … SELECT …`
- `EXPLAIN …` — every variant ClickHouse supports. Use them before proposing rewrites:
  - `EXPLAIN SELECT …` (default: PLAN)
  - `EXPLAIN AST SELECT …`
  - `EXPLAIN SYNTAX SELECT …`
  - `EXPLAIN QUERY TREE SELECT …` (post-analyzer logical tree; invaluable on modern ClickHouse)
  - `EXPLAIN PIPELINE SELECT …` — processor-level pipeline, headers, expressions
  - `EXPLAIN ESTIMATE SELECT …` — per-part row/mark estimates before execution
  - `EXPLAIN PLAN indexes = 1, actions = 1, json = 1 SELECT …` — primary-key and skip-index use, JSON for machine parsing
  - `EXPLAIN TABLE OVERRIDE …`
- `SHOW …` — `SHOW CREATE TABLE events`, `SHOW COLUMNS FROM events`, `SHOW INDEX FROM events`, `SHOW SETTINGS ILIKE '%mark_cache%'`, etc.
- `DESCRIBE` / `DESC …`

**Timeout**: every submission is wrapped with `SETTINGS max_execution_time = 60`. Keep ad-hoc probes short. If the target query itself routinely exceeds 60s, use range narrowing (see Setup step 6) and only then start the campaign.

**Cluster scoping**: depends on the coordinator's `target`. The prompt addendum prepended to `autoresearch.md` tells you which `team_id` the target cluster has data for and asks you to rewrite team_id predicates accordingly. Read the addendum first.

**Profiling ClickHouse's perspective**:

After a run, the campaign scripts capture client-side `elapsed_ms`, `rows_read`, `bytes_read`, and the server-minted `query_id` from the proxy response (persisted as `query_id` in `runs/run-XXXX-*/metrics.json` and `baseline/metrics.json`). The `autoresearch` CH user can read four `system.*` profiling tables for its own queries (RESTRICTIVE row policies hide every other user's rows):

| Table                     | What's in it                                                                                                                             |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `system.query_log`        | One row per finished query: `query_duration_ms`, `read_rows`, `read_bytes`, `memory_usage`, `ProfileEvents` (map of low-level counters). |
| `system.query_thread_log` | Per-thread breakdown of the query: CPU time, wait time, peak memory per thread.                                                          |
| `system.text_log`         | Server-side log lines tagged with `query_id` (planner messages, mark-cache hits, merge-trigger).                                         |
| `system.trace_log`        | Sampled stack traces during query execution. Lit up by `query_profiler_real_time_period_ns`.                                             |

Look up a finished query's headline stats:

```sql
SELECT query_duration_ms, read_rows, read_bytes, memory_usage, ProfileEvents
FROM system.query_log
WHERE type = 'QueryFinish' AND query_id = '<query_id from metrics.json>'
ORDER BY event_time DESC
LIMIT 1
```

Useful `ProfileEvents` for query-perf work:

- `SelectedParts`, `SelectedRanges`, `SelectedMarks` — how much of the table the planner chose to read (lower = better-pruned predicates).
- `OSReadChars` vs `OSReadBytes` — disk-cache hit ratio.
- `RealTimeMicroseconds`, `UserTimeMicroseconds`, `SystemTimeMicroseconds` — wall vs CPU time (gap = waiting on I/O or locks).
- `Merge*`, `*Mark*`, `*PrimaryKey*` — index usage and compaction cost.

Per-thread to find tail latency:

```sql
SELECT thread_id, query_duration_ms, peak_memory_usage, ProfileEvents['OSReadBytes']
FROM system.query_thread_log
WHERE query_id = '<query_id>'
ORDER BY query_duration_ms DESC
```

Server-side log lines for a single query (planner choices, parts pruning, etc.):

```sql
SELECT event_time, level, logger_name, message
FROM system.text_log
WHERE query_id = '<query_id>'
ORDER BY event_time
```

Sampled stack traces (only useful if `query_profiler_real_time_period_ns > 0` was active for the run):

```sql
SELECT count(), arrayStringConcat(arrayMap(x -> demangle(addressToSymbol(x)), trace), '\n') AS frames
FROM system.trace_log
WHERE query_id = '<query_id>'
GROUP BY frames
ORDER BY count() DESC
LIMIT 20
```

`system.query_log` is readable under `readonly = 2`. Use it to compare ProfileEvents across iterations, not only wall-clock latency.

**Schema inspection** before proposing rewrites is free and often decisive:

```sql
SHOW CREATE TABLE events
DESCRIBE events
SELECT name, type, default_expression, codec_expression, is_in_primary_key, is_in_partition_key
FROM system.columns WHERE database = currentDatabase() AND table = 'events'
SELECT name, expr, type FROM system.data_skipping_indices WHERE table = 'events'
```

Use these to check the primary key, partitioning, codecs, and existing skip-indexes before hypothesizing that "adding an index" would help.

**ClickHouse source code** is bundled into the sandbox at `/opt/clickhouse`, pinned to a recent ClickHouse commit. This likely matches the behavior of the cluster you're querying, but it may lag or diverge from the version actually running in prod — treat it as a strong hint, not ground truth, and be alert to cases where observed behavior disagrees with what the source suggests. `tests/`, `docs/`, `website/`, `contrib/`, and `.git` are stripped; everything else — the engine, query analyzer, storage, functions — is readable. Use it to ground hypotheses in the actual implementation:

- `grep -rn "<FunctionName>" /opt/clickhouse/src/Functions/` — find the C++ implementation of a function you're using
- `ls /opt/clickhouse/src/Storages/MergeTree/` — MergeTree internals (skip indexes, parts, marks)
- `ls /opt/clickhouse/src/Interpreters/` — query-tree rewrites, analyzer passes, join engines
- `ls /opt/clickhouse/src/Processors/` — pipeline / QueryPlan steps (what `EXPLAIN PIPELINE` is describing)

Reading the source is often the fastest way to resolve questions like "does this function short-circuit?", "how does this skip-index decide which granules to read?", "what settings does this pass consult?". Prefer reading the source over guessing or inferring from documentation.

**What this means for hypothesis formation**: start every lane by running `EXPLAIN indexes = 1, actions = 1, json = 1 SELECT <original>` and inspecting the plan. Then propose rewrites that target concrete weaknesses (full table scan, skipped PREWHERE, missing primary-key usage, etc.). Do not guess.

## Setup sequence

1. Confirm or infer the target query and query identifier.
2. Create or verify the campaign branch.
3. Run:

```bash
python3 ../../scripts/ch_campaign_init.py --workspace .clickhouse-autoresearch --query-id <id>
```

Add optional flags as needed:

- `--query-file <path>`
- `--branch-name <name>`
- `--primary-metric latency_ms`
- `--metric-unit ms`
- `--direction lower`
- `--lane-stagnation-window <n>`
- `--campaign-stagnation-window <n>`
- `--max-total-iterations <n>`
- `--significant-improvement-pct <number>`
- `--repair-budget <n>`

4. Inspect the generated workspace.
5. Fill in or update:

- `.clickhouse-autoresearch/adapter.json`
- `.clickhouse-autoresearch/state.json`
- `.clickhouse-autoresearch/autoresearch.md`

6. Capture the baseline:

```bash
python3 ../../scripts/ch_capture_baseline.py --workspace .clickhouse-autoresearch
```

If the baseline times out, enter range narrowing (see `orchestration.md` § Timeout queries):

1. Copy `query/original.sql` to `query/narrowed.sql`
2. Halve the time range in `narrowed.sql` (leave `query/original.sql` untouched — it is the full-range reference used by the escalation check)
3. Retry against the narrowed file: `python3 ../../scripts/ch_capture_baseline.py --workspace .clickhouse-autoresearch --query-file query/narrowed.sql`
4. Repeat halving until the query completes in 1–10s
5. Record narrowing state in `state.json`: `{ "narrowed": true, "original_range": "...", "working_range": "..." }`

6. Read the baseline artifacts and seed the first lanes and hypotheses.
7. Initialize the autoresearch session against the configured primary metric.
8. Start the experiment loop using:

```bash
./.clickhouse-autoresearch/autoresearch.py
```

with correctness backpressure through:

```bash
./.clickhouse-autoresearch/autoresearch_checks.py
```

## Runtime responsibilities

During the campaign, you must:

- if the workspace was pre-initialized (see "Pre-initialized workspace detection"), never prompt the operator — assume headless operation and apply default guidance for every decision
- keep `query/current.sql` as the next candidate to test
- keep `query/best.sql` aligned with the best kept result
- maintain the lane / hypothesis / review notes
- update `state.json` after every experiment
- reflect after every experiment
- trigger lane review tactically
- trigger campaign review strategically
- preserve durable learning in `autoresearch.md` and `autoresearch.jsonl`
- maintain `out-of-scope-suggestions.md` — the agent's persistent record of schema-level changes it suspects would help but cannot apply in the sandbox (materialized columns, skip indexes, projections, table-engine swaps). The orchestrator harvests this file as a deliverable, so each entry must include the change, evidence, expected impact, and which queries would benefit
- classify every kept optimization as schema-level, query-generation, or query-specific
- if narrowed: after every `keep`, run an escalation check against the original time range and log the result
- if an escalation check succeeds, graduate to the full range (re-capture baseline, update correctness reference)

## What the scripts do vs what you do

The scripts do deterministic work:

- create the workspace
- invoke environment-specific commands from `adapter.json`
- capture baseline artifacts
- run candidate queries
- compare candidate results to the saved baseline result set
- emit `METRIC ...` lines

You do the reasoning:

- choose the active lane
- choose the hypothesis
- decide whether to repair a wrong-but-fast candidate
- decide when to integrate wins across lanes
- decide when a lane is exhausted
- decide when the campaign is exhausted

## Reasoning rules

Review separation, integration, correctness, and closure are defined in
`orchestration.md` (sibling of this file). Apply them as written.

Files in this skill

  • SKILL.md13.6 KB
  • orchestration.md14 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.