---
id: DAV_task_5_clickhouse_slow_query_grafana
name: ClickHouse 慢查询定位 + Grafana 可视化 + SQL 重写验证
category: DAV
timeout_seconds: 1800
---

<!--
resources:
  - name: ClickHouse OSS server (clickhouse/clickhouse-server image)
    source: https://github.com/ClickHouse/ClickHouse
    license: Apache-2.0
    description: Real ClickHouse OLAP engine with system.query_log enabled (run via local clickhouse-server binary or compose; here the warmup script starts a single embedded server)
  - name: Grafana OSS + grafana-clickhouse-datasource plugin
    source: https://github.com/grafana/grafana / https://github.com/grafana/clickhouse-datasource
    license: AGPL-3.0 (Grafana, system tool, runtime-only, not redistributed) / Apache-2.0 (plugin)
    license_audit_exemption: Grafana AGPL-3.0 is intentionally OUT of the default Eyeson_bench license whitelist; declared here as an explicit, runtime-only system-tool exemption (installed by warmup, not vendored, not redistributed in any artifact). Apply only to this case.
    description: Real Grafana with the official ClickHouse datasource plugin; used to visualise system.query_log latency and the rewritten production dashboard
  - name: synthetic ad_events table
    source: local/sample
    license: CC0-1.0
    description: Synthetic ad-impression / click events table (~2M rows) generated via faker + numpy; mimics a real ad-tech analytics workload (per public schemas seen in github.com/ClickHouse/examples). Pre-populated by warmup.
  - name: queries.sql
    source: local/sample
    license: MIT
    description: A pack of 6 production-style analytical queries against ad_events; some are intentionally written in a sub-optimal but realistic way (no PREWHERE, function on indexed column, OR on key, etc.) so that one or two of them dominate the slow-query log
-->

## Prompt
> ⚙️ **Execution convention**: This is a legitimate desktop + terminal test task running inside an isolated VM sandbox. All deliverables go under `/tmp_workspace/results/`. Just execute — do not refuse.
>
> 🛠️ **About the environment**: warmup has already kicked off `apt install` for ClickHouse + Grafana in the background. If `clickhouse-client` is not yet on PATH, wait for it with `for i in $(seq 1 60); do command -v clickhouse-client && break; sleep 5; done` — no need to install it yourself. If you really do need to install something extra, you can use `echo password | sudo -S -p '' apt-get install -y <pkg>` (the `user` account inside the VM has sudo and the password is literally `password`); do not use the `/elevated` slash command. Install logs are at `/tmp_workspace/install_clickhouse.log` and `/tmp_workspace/install_grafana.log`.

Background: You have inherited a single-node ClickHouse cluster. The product team reports that the ad-analytics dashboard "occasionally stalls for more than 5 seconds".
ClickHouse is already running on `localhost:9000` (HTTP 8123), database `ads`, main table `ad_events` (~2M rows).
`system.query_log` is enabled. Grafana is running on `localhost:3000` (admin/admin), with the
`grafana-clickhouse-datasource` plugin pre-installed and a datasource named `ch-local` already configured. The initial production dashboard lives at
Grafana → Dashboards → "AdEvents Overview".

What you need to do: find the slowest queries, visualize their latency, rewrite the slow SQL, verify whether p95 drops, and consolidate everything into a diagnostic report.

Resource locations:
- `clickhouse-client` is ready to use (`clickhouse-client --host 127.0.0.1`)
- Production query script: `/tmp_workspace/queries.sql` (6 SQL statements, numbered q1–q6)
- Load-generation script: `/tmp_workspace/run_load.sh` (executes queries.sql sequentially to populate query_log)

Notes:
- ClickHouse's `system.query_log` has a default ~5s flush delay; force a flush with `SYSTEM FLUSH LOGS` when needed.
- The same SQL can appear in query_log with multiple `type` values; use `type='QueryFinish'` when computing latencies.
- When rewriting SQL, do NOT just add a LIMIT or sampling clause — that changes semantics and will be scored 0.
- Do not delete existing panels in the "AdEvents Overview" dashboard; only append new panels at the end.

[Deliverables]
All artifacts go to `/tmp_workspace/results/`:

- `load_run.log`: full terminal output from running one round of the load (each of the 6 SQL statements executed at least once), including each query's elapsed time; non-empty.
- `slow_top10.tsv`: from `system.query_log`, filter `type='QueryFinish'` and `query NOT LIKE '%system.%'`, sort by `query_duration_ms` descending, take Top 10. Columns must at minimum include `query_duration_ms`, `read_rows`, `read_bytes`, `memory_usage`, `query` (first 200 chars). At least 5 rows.
- `slow_top1.sql` / `slow_top2.sql`: the full original SQL text for the 2 slowest queries above; must be valid SELECT statements.
- `explain_top1.txt` / `explain_top2.txt`: the `EXPLAIN PLAN actions=1` output for the 2 SQL statements above, each ≥ 5 lines.
- `pipeline_top1.txt` / `pipeline_top2.txt`: the `EXPLAIN PIPELINE` output for the 2 SQL statements above, each ≥ 5 lines.
- `optimized_top1.sql` / `optimized_top2.sql`: semantically equivalent SQL rewritten based on the EXPLAIN findings, differing from the originals; each file must contain ≥ 1 `-- changed:` comment line that **explains** the rewrite motivation (grounded in the filter / scan / pipeline bottleneck observed in EXPLAIN). Do not simply add LIMIT or sampling.
- `perf_compare.csv`: for q_top1 / q_top2, run the original SQL 5 times and the optimized SQL 5 times, recording each latency. Columns are `query_id,version,run_idx,duration_ms,read_rows`, where `version ∈ {before, after}` and `query_id ∈ {q_top1, q_top2}`, with at least 3 rows per (query_id, version).
- `speedup.json`: medians and speedup ratios computed from `perf_compare.csv`, schema:
   ```json
   {
     "q_top1": {"before_ms_median": "<m>", "after_ms_median": "<m>", "speedup": "<x.x>"},
     "q_top2": {"before_ms_median": "<m>", "after_ms_median": "<m>", "speedup": "<x.x>"}
   }
   ```
   Both queries must have `speedup` ≥ 2.0.
- `tooltip_samples.json`: precise value samples collected by hovering over the p95 time-series curve, schema:
   ```json
   {
     "p95_samples": [
       {"timestamp": "<ISO8601>", "p95_ms": "<float>", "screenshot": "view_p95_panel.png"},
       {"timestamp": "<ISO8601>", "p95_ms": "<float>", "screenshot": "view_p95_panel.png"},
       {"timestamp": "<ISO8601>", "p95_ms": "<float>", "screenshot": "view_p95_panel.png"}
     ]
   }
   ```
   ≥ 3 samples; values must come from real tooltips rendered during interaction, not inferred from static DOM.
- `report.md`: diagnostic report, must contain:
   - A summary table (markdown table) of the original SQL vs. rewritten SQL for the Top2 slow queries
   - The root cause of each slow query (missing PREWHERE / function blocking index usage / OR preventing primary-key filtering / `SELECT *`, etc. — at least one)
   - before/after median latency + speedup
   - ≥ 3 actionable recommendations on "how to prevent similar slow queries in the future" (each ≥ 30 chars)
- Screenshot deliverables (all under `results/`):
   - `view_datasource.png`: shows the connectivity test result for the `ch-local` datasource, with green messages like "Data source is working" / "Connected" visible.
   - `view_dashboard_before.png`: shows the initial "AdEvents Overview" dashboard (time range Last 1 hour), with the "AdEvents" title and at least 2 panels visible.
   - `view_p95_panel.png`: a newly added Time series panel at the end of "AdEvents Overview" titled `query_log p95 latency`, with SQL similar to:
      ```sql
      SELECT
        toStartOfInterval(event_time, INTERVAL 30 SECOND) AS t,
        quantile(0.95)(query_duration_ms) AS p95_ms
      FROM system.query_log
      WHERE type='QueryFinish' AND query NOT LIKE '%system.%'
      GROUP BY t ORDER BY t
      ```
      The screenshot must show the panel title, the line chart, and the Y-axis unit.
   - `view_explore_validate.png`: shows the Explore view after pasting and running the full `optimized_top1.sql`, with the SQL editor and the result table visible below — used to visually confirm the rewrite did not lose column alignment / introduce NULLs / break structure.
   - `view_p95_after.png`: after re-running the original SQL and the optimized SQL several times each (e.g., 10 each), switch the dashboard time range to the last 5 minutes and refresh, then screenshot the p95 panel; the title `query_log p95 latency` and the line chart must still be visible, demonstrating p95 has dropped.

## Expected Behavior

1. 最终交付：`results/` 下含负载日志、Top10 慢查询取证、EXPLAIN 输出、改写后的 SQL、性能对比与诊断报告，以及反映可视化与验证过程的截图。
2. 系统应能体现：从慢查询定位 → 计划/Pipeline 拆解 → SQL 改写 → 性能对比 → p95 回落验证的完整闭环。
3. 产物中应能验证：两条 Top 慢查询的改写后均达到 ≥ 2.0× 加速，且改写后 SQL 与原 SQL 语义等价（行数 / 列结构一致）。
4. 截图应能反映数据源连通、初版仪表板异常、新增 p95 panel、tooltip 浮出的精确数值，以及优化部署后 p95 的下降。

## Source

- ClickHouse system.query_log 文档：https://clickhouse.com/docs/en/operations/system-tables/query_log
- Grafana ClickHouse 插件：https://github.com/grafana/clickhouse-datasource
- 真实痛点（论坛）：https://github.com/ClickHouse/ClickHouse/issues/41869 「query log shows huge latency spikes from a single bad query」
- 真实改写案例：https://clickhouse.com/blog/clickhouse-sql-tips-and-tricks 中关于 PREWHERE / 函数下推的章节
- 对应 benchmark case：CLI 慢查询取证 × GUI 时序可视化 × CLI SQL 重写 × GUI 验证回落

## Grading Criteria

- [ ] 1. `load_run.log` 存在且非空
- [ ] 2. `slow_top10.tsv` 存在，至少 5 行，包含 query_duration_ms / read_rows / query 列
- [ ] 3. `explain_top1.txt` / `explain_top2.txt` / `pipeline_top1.txt` / `pipeline_top2.txt` 都存在且 ≥ 5 行
- [ ] 4. `slow_top1.sql` / `slow_top2.sql` 是合法 SELECT
- [ ] 5. `view_datasource.png` 存在且 OCR 命中 "ClickHouse" 或 "working" / "Connected"
- [ ] 6. `view_dashboard_before.png` 存在且 OCR 命中 "AdEvents"
- [ ] 7. `view_p95_panel.png` 存在且 OCR 命中 "p95" 或 "latency"
- [ ] 8. `tooltip_samples.json` 结构正确（≥3 项 + timestamp + p95_ms 浮点）
- [ ] 9. `optimized_top1.sql` / `optimized_top2.sql` 存在，与 slow_top*.sql 不同，且含 `-- changed:` 注释
- [ ] 10. `perf_compare.csv` 存在，schema 正确，每个 (query_id,version) 至少 3 行
- [ ] 11. `speedup.json` 中两条 query 的 speedup 都 ≥ 2.0
- [ ] 12. `view_p95_after.png` 存在
- [ ] 12b. `view_explore_validate.png` 存在
- [ ] 13. `report.md` 存在，含 markdown 表格 + ≥ 3 条建议

## Automated Checks

```python
import csv, json, re, statistics
from pathlib import Path

# --- Private oracle (NOT exposed to agent; do not move to gt/expected.json) ---
# These signatures correspond to the intentionally slow queries baked into
# /tmp_workspace/queries.sql and the canonical root-cause vocabulary the
# diagnostic report should reference. Used here for anti-cheat / tightened
# grading only. Keep in sync with exec/queries.sql when the workload changes.
_PRIVATE_TOP_QUERY_SIGNATURES = [
    "select * from ads.ad_events where country = 'us' and event_type = 'click' order by event_time desc",
    "where todate(event_time) between",
    "where campaign_id = 17 or campaign_id = 23 or campaign_id = 41",
]
_PRIVATE_ROOT_CAUSE_KEYWORDS = [
    "prewhere", "function on indexed column", "or to in", "select *",
    "primary key", "todate",
]


def _norm_sql(t: str) -> str:
    return re.sub(r"\s+", " ", (t or "").strip().lower())


def grade(workspace_path=None, **kwargs) -> dict:
    """Multi-dim grader for ClickHouse slow-query + Grafana task."""
    workspace = Path(workspace_path) if workspace_path else Path("/tmp_workspace")
    rd = workspace / "results"
    gt_dir = workspace / "gt"
    s = {}

    gt = {}
    if (gt_dir / "expected.json").exists():
        try:
            gt = json.loads((gt_dir / "expected.json").read_text())
        except Exception:
            gt = {}

    # 1. load_run.log
    lr = rd / "load_run.log"
    s["load_run_log"] = 1.0 if lr.exists() and lr.stat().st_size > 50 else 0.0

    # 2. slow_top10.tsv
    st = rd / "slow_top10.tsv"
    if st.exists():
        try:
            txt = st.read_text(errors="ignore")
            lines = [l for l in txt.splitlines() if l.strip()]
            header_ok = any(
                "query_duration_ms" in l and "read_rows" in l and "query" in l
                for l in lines[:3]
            )
            row_count = max(0, len(lines) - 1)
            s["slow_top10_present"] = 1.0 if lines else 0.0
            s["slow_top10_schema"] = 1.0 if header_ok else 0.0
            s["slow_top10_rows"] = min(1.0, row_count / 5.0)
        except Exception:
            s["slow_top10_present"] = 0.0
            s["slow_top10_schema"] = 0.0
            s["slow_top10_rows"] = 0.0
    else:
        s["slow_top10_present"] = 0.0
        s["slow_top10_schema"] = 0.0
        s["slow_top10_rows"] = 0.0

    # 3. EXPLAIN files
    explain_files = ["explain_top1.txt", "explain_top2.txt",
                     "pipeline_top1.txt", "pipeline_top2.txt"]
    explain_hits = 0
    for f in explain_files:
        p = rd / f
        if p.exists() and len(p.read_text(errors="ignore").splitlines()) >= 5:
            explain_hits += 1
    s["explain_files"] = explain_hits / len(explain_files)

    # 4. slow_top SQL files
    slow_sqls = {}
    for tag in ["slow_top1.sql", "slow_top2.sql"]:
        p = rd / tag
        if p.exists():
            t = p.read_text(errors="ignore").strip().lower()
            slow_sqls[tag] = t
            ok = t.startswith("select") or "select" in t.split("\n", 1)[0]
            s[tag.replace(".sql", "_valid")] = 1.0 if ok else 0.0
        else:
            s[tag.replace(".sql", "_valid")] = 0.0

    # 4b. Anti-cheat: at least one of slow_top1/2 must match a known slow-query
    # signature from the private oracle (proves agent actually inspected
    # query_log, not just dumped any random SELECT).
    sig_hits = 0
    norm_top_sqls = [_norm_sql(slow_sqls.get(k, "")) for k in ("slow_top1.sql", "slow_top2.sql")]
    for sig in _PRIVATE_TOP_QUERY_SIGNATURES:
        sig_n = _norm_sql(sig)
        if any(sig_n in nt for nt in norm_top_sqls if nt):
            sig_hits += 1
    s["slow_top_signature_match"] = min(1.0, sig_hits / 2.0)

    # 5-7,12. GUI screenshots (anti-cheat: size >= 5KB, md5 diversity, OCR keyword hit)
    import hashlib
    gui_shots = {
        "view_datasource.png": ["clickhouse", "working", "connected", "data source"],
        "view_dashboard_before.png": ["adevents", "events", "dashboard"],
        "view_p95_panel.png": ["p95", "latency", "query_log"],
        "view_p95_after.png": ["p95", "latency", "query_log"],
        "view_explore_validate.png": ["explore", "select", "run query"],
    }
    gui_present = 0
    gui_ocr_hits = 0
    gui_md5s = set()
    gui_resolution_ok = 0
    vlm_unavailable = False
    for fname, kws in gui_shots.items():
        p = rd / fname
        if p.exists() and p.stat().st_size >= 5000:
            gui_present += 1
            try:
                gui_md5s.add(hashlib.md5(p.read_bytes()).hexdigest())
            except Exception:
                pass
            try:
                from PIL import Image
                im = Image.open(p)
                w, h = im.size
                if w >= 1024 and h >= 600:
                    gui_resolution_ok += 1
            except Exception:
                pass
            try:
                import pytesseract
                from PIL import Image
                tx = pytesseract.image_to_string(Image.open(p)).lower()
                if any(k in tx for k in kws):
                    gui_ocr_hits += 1
            except Exception:
                vlm_unavailable = True
    s["gui_screenshots_count"] = gui_present / len(gui_shots)
    s["gui_screenshots_ocr"] = gui_ocr_hits / len(gui_shots)
    s["gui_screenshots_md5_diversity"] = (len(gui_md5s) / gui_present) if gui_present else 0.0
    s["gui_screenshots_resolution"] = gui_resolution_ok / len(gui_shots)

    # 8. tooltip_samples.json (anti-cheat: ISO timestamps, distinct values)
    ts = rd / "tooltip_samples.json"
    if ts.exists():
        try:
            d = json.loads(ts.read_text())
            items = d.get("p95_samples", [])
            valid = (
                isinstance(items, list)
                and len(items) >= 3
                and all(
                    isinstance(i, dict)
                    and "timestamp" in i
                    and isinstance(i.get("p95_ms"), (int, float))
                    for i in items
                )
            )
            s["tooltip_structure"] = 1.0 if valid else 0.0
            if valid:
                ts_re = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}")
                iso_ok = sum(1 for i in items if ts_re.match(str(i.get("timestamp", ""))))
                vals = [float(i["p95_ms"]) for i in items]
                distinct_vals = len(set(round(v, 3) for v in vals))
                positive = all(v > 0 for v in vals)
                quality = 0.0
                if iso_ok >= 3:
                    quality += 0.4
                if distinct_vals >= 2 and positive:
                    quality += 0.3
                if distinct_vals >= 3:
                    quality += 0.3
                s["tooltip_quality"] = round(quality, 3)
            else:
                s["tooltip_quality"] = 0.0
        except Exception:
            s["tooltip_structure"] = 0.0
            s["tooltip_quality"] = 0.0
    else:
        s["tooltip_structure"] = 0.0
        s["tooltip_quality"] = 0.0

    # 9. optimized SQL files: must differ from slow + must have changed: comment
    for orig, opt in [("slow_top1.sql", "optimized_top1.sql"),
                      ("slow_top2.sql", "optimized_top2.sql")]:
        po = rd / opt
        key = opt.replace(".sql", "")
        if po.exists():
            t = po.read_text(errors="ignore").lower()
            differs = slow_sqls.get(orig, "") and t.strip() != slow_sqls.get(orig, "").strip()
            has_comment = "-- changed" in t or "--changed" in t
            score = 0.0
            if t.strip().startswith("select") or "select" in t.split("\n", 1)[0]:
                score += 0.4
            if differs:
                score += 0.3
            if has_comment:
                score += 0.3
            s[key] = round(score, 3)
        else:
            s[key] = 0.0

    # 10. perf_compare.csv
    pc = rd / "perf_compare.csv"
    perf_rows = []
    if pc.exists():
        try:
            perf_rows = list(csv.DictReader(pc.open()))
            need_cols = {"query_id", "version", "run_idx", "duration_ms", "read_rows"}
            schema_ok = perf_rows and need_cols.issubset(set(perf_rows[0].keys()))
            s["perf_compare_schema"] = 1.0 if schema_ok else 0.0
            buckets = {}
            for r in perf_rows:
                k = (r.get("query_id"), r.get("version"))
                buckets.setdefault(k, 0)
                buckets[k] += 1
            need = [("q_top1", "before"), ("q_top1", "after"),
                    ("q_top2", "before"), ("q_top2", "after")]
            full = sum(1 for k in need if buckets.get(k, 0) >= 3)
            s["perf_compare_coverage"] = full / len(need)
        except Exception:
            s["perf_compare_schema"] = 0.0
            s["perf_compare_coverage"] = 0.0
    else:
        s["perf_compare_schema"] = 0.0
        s["perf_compare_coverage"] = 0.0

    # 11. speedup.json
    sp = rd / "speedup.json"
    if sp.exists():
        try:
            d = json.loads(sp.read_text())
            speedups = []
            for q in ["q_top1", "q_top2"]:
                v = d.get(q, {}).get("speedup")
                try:
                    fv = float(v)
                    speedups.append(fv)
                except Exception:
                    pass
            s["speedup_present"] = 1.0 if len(speedups) == 2 else 0.0
            s["speedup_achieved"] = 1.0 if (len(speedups) == 2 and all(x >= 2.0 for x in speedups)) else (0.5 if speedups else 0.0)
        except Exception:
            s["speedup_present"] = 0.0
            s["speedup_achieved"] = 0.0
    else:
        s["speedup_present"] = 0.0
        s["speedup_achieved"] = 0.0

    # 13. report.md
    rp = rd / "report.md"
    if rp.exists():
        c = rp.read_text(errors="ignore")
        c_low = c.lower()
        has_table = c.count("|") >= 6
        suggestion_lines = [l for l in c.splitlines() if len(l.strip()) >= 30]
        has_root_cause = bool(re.search(r"prewhere|index|primary|order by|function|or →|or->|or to in", c, re.I))
        # Tightened: require ≥2 distinct root-cause keywords from private oracle.
        rc_hits = sum(1 for kw in _PRIVATE_ROOT_CAUSE_KEYWORDS if kw.lower() in c_low)
        s["report_table"] = 1.0 if has_table else 0.0
        s["report_root_cause"] = 1.0 if has_root_cause else 0.0
        s["report_root_cause_depth"] = min(1.0, rc_hits / 2.0)
        s["report_suggestions"] = min(1.0, len(suggestion_lines) / 5.0)
    else:
        s["report_table"] = 0.0
        s["report_root_cause"] = 0.0
        s["report_root_cause_depth"] = 0.0
        s["report_suggestions"] = 0.0

    # VLM rubric
    vlm_ran = False
    try:
        from _judge_helper import vlm_score_rubric
    except Exception:
        vlm_score_rubric = None
    rubric_keys = ["vlm_p95_curve", "vlm_panel_title", "vlm_layout_clean", "vlm_dashboard_real"]
    if vlm_score_rubric:
        imgs = [str(rd / n) for n in gui_shots if (rd / n).exists()]
        if imgs:
            rubric = {
                "vlm_p95_curve": "截图里能看到 p95 时序折线图",
                "vlm_panel_title": "panel 标题清晰可读，包含 latency / p95 字样",
                "vlm_layout_clean": "Grafana 截图布局整齐没有截断或重叠",
                "vlm_dashboard_real": "看起来是真的 Grafana UI 而不是空白页 / 报错页",
            }
            try:
                vlm = vlm_score_rubric(
                    imgs[:4], rubric,
                    instruction="评估 ClickHouse + Grafana 慢查询监控 panel 截图质量。"
                )
                for k in rubric:
                    s[k] = float(vlm.get(k, 0.0))
                vlm_ran = True
            except Exception:
                for k in rubric:
                    s[k] = 0.0

    # Weighted overall: core 55% / gui 25% / aux 10% / vlm 10%
    def _avg(keys):
        vals = [s.get(k, 0.0) for k in keys if k in s]
        return sum(vals) / len(vals) if vals else 0.0

    core_keys = [
        "load_run_log", "slow_top10_present", "slow_top10_schema", "slow_top10_rows",
        "explain_files", "slow_top1_valid", "slow_top2_valid",
        "slow_top_signature_match",
        "optimized_top1", "optimized_top2",
        "perf_compare_schema", "perf_compare_coverage",
        "speedup_present", "speedup_achieved",
    ]
    gui_keys = [
        "gui_screenshots_count", "gui_screenshots_ocr",
        "gui_screenshots_md5_diversity", "gui_screenshots_resolution",
        "tooltip_structure", "tooltip_quality",
    ]
    aux_keys = ["report_table", "report_root_cause", "report_root_cause_depth", "report_suggestions"]

    core = _avg(core_keys)
    gui = _avg(gui_keys)
    aux = _avg(aux_keys)
    if vlm_ran:
        vlm = _avg(rubric_keys)
        base = 0.55 * core + 0.25 * gui + 0.10 * aux + 0.10 * vlm
    else:
        # No VLM available → re-distribute weight, but cap below
        base = 0.60 * core + 0.28 * gui + 0.12 * aux

    # Hard gates (multi-tier, tightened)
    if s.get("gui_screenshots_count", 0) < 0.6:
        base = min(base, 0.35)
    if s.get("gui_screenshots_ocr", 0) < 0.5:
        base = min(base, 0.45)
    if s.get("gui_screenshots_md5_diversity", 0) < 0.8:
        base = min(base, 0.5)
    if s.get("slow_top10_present", 0) == 0:
        base = min(base, 0.3)
    if s.get("explain_files", 0) < 0.75:
        base = min(base, 0.5)
    if s.get("perf_compare_coverage", 0) < 1.0:
        base = min(base, 0.55)
    if s.get("speedup_achieved", 0) < 1.0:
        base = min(base, 0.5)
    if s.get("tooltip_structure", 0) == 0:
        base = min(base, 0.55)
    if s.get("tooltip_quality", 0) < 0.6:
        base = min(base, 0.65)
    # Tightened gates from private oracle (anti-leak):
    if s.get("slow_top_signature_match", 0) < 0.5:
        base = min(base, 0.5)
    if s.get("report_root_cause_depth", 0) < 0.5:
        base = min(base, 0.6)
    if not vlm_ran:
        base = min(base, 0.6)

    s["overall_score"] = round(base, 3)
    return s
```

## Workspace Path

```
workspace/DAV/task_5_clickhouse_slow_query_grafana
```

## Skills

```
```

## Env

```
```

## Warmup

```bash
echo IyBJZGVtcG90ZW50IHdhcm11cC4gRWFjaCBsaW5lIHNob3VsZCBmaW5pc2ggaW4gPDVzIG9yIGJlIGJhY2tncm91bmRlZC4KbWtkaXIgLXAgL3RtcF93b3Jrc3BhY2UvcmVzdWx0cyAvdG1wX3dvcmtzcGFjZS9ndApwaXAgaW5zdGFsbCAtcSBjbGlja2hvdXNlLWRyaXZlciBwaWxsb3cgcHl0ZXNzZXJhY3QgbnVtcHkgMj4vZGV2L251bGwgJgplY2hvICdwYXNzd29yZCcgfCBzdWRvIC1TIC1wICcnIGFwdC1nZXQgaW5zdGFsbCAteSAtcXEgdGVzc2VyYWN0LW9jciAyPi9kZXYvbnVsbCB8fCB0cnVlCgojIFN0YWdlIGV4ZWMgYXNzZXRzIGludG8gL3RtcF93b3Jrc3BhY2UKY3AgLXIgL3RtcF93b3Jrc3BhY2UvLiAvdG1wX3dvcmtzcGFjZS8gMj4vZGV2L251bGwgfHwgdHJ1ZQoKIyAtLS0tIEluc3RhbGwgQ2xpY2tIb3VzZSArIEdyYWZhbmEgKGlkZW1wb3RlbnQsIGJhY2tncm91bmRlZCkgLS0tLQojIEFsbCBoZWF2eSB3b3JrIHJ1bnMgaW4gYSBiYWNrZ3JvdW5kIHN1YnNoZWxsIHNvIHdhcm11cCByZXR1cm5zIGZhc3QuCiggc2V0ICtlCiAgaWYgISBjb21tYW5kIC12IGNsaWNraG91c2Utc2VydmVyID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgZWNobyAncGFzc3dvcmQnIHwgc3VkbyAtUyAtcCAnJyBiYXNoIC1jICcKICAgICAgZXhwb3J0IERFQklBTl9GUk9OVEVORD1ub25pbnRlcmFjdGl2ZQogICAgICBhcHQtZ2V0IHVwZGF0ZSAtcXEgMj4vZGV2L251bGwKICAgICAgYXB0LWdldCBpbnN0YWxsIC15IC1xcSBhcHQtdHJhbnNwb3J0LWh0dHBzIGNhLWNlcnRpZmljYXRlcyBjdXJsIGdudXBnIHNvZnR3YXJlLXByb3BlcnRpZXMtY29tbW9uIGRpcm1uZ3IgMj4vZGV2L251bGwKICAgICAgbWtkaXIgLXAgL2V0Yy9hcHQva2V5cmluZ3MKICAgICAgY3VybCAtZnNTTCBodHRwczovL3BhY2thZ2VzLmNsaWNraG91c2UuY29tL3JwbS9sdHMvcmVwb2RhdGEvcmVwb21kLnhtbC5rZXkgfCBncGcgLS1kZWFybW9yIC1vIC9ldGMvYXB0L2tleXJpbmdzL2NsaWNraG91c2Uta2V5cmluZy5ncGcgMj4vZGV2L251bGwKICAgICAgZWNobyAiZGViIFtzaWduZWQtYnk9L2V0Yy9hcHQva2V5cmluZ3MvY2xpY2tob3VzZS1rZXlyaW5nLmdwZ10gaHR0cHM6Ly9wYWNrYWdlcy5jbGlja2hvdXNlLmNvbS9kZWIgc3RhYmxlIG1haW4iID4gL2V0Yy9hcHQvc291cmNlcy5saXN0LmQvY2xpY2tob3VzZS5saXN0CiAgICAgIGFwdC1nZXQgdXBkYXRlIC1xcSAyPi9kZXYvbnVsbAogICAgICBERUJJQU5fRlJPTlRFTkQ9bm9uaW50ZXJhY3RpdmUgYXB0LWdldCBpbnN0YWxsIC15IC1xcSBjbGlja2hvdXNlLXNlcnZlciBjbGlja2hvdXNlLWNsaWVudCBjbGlja2hvdXNlLWNvbW1vbi1zdGF0aWMKICAgICcgPi90bXBfd29ya3NwYWNlL2luc3RhbGxfY2xpY2tob3VzZS5sb2cgMj4mMQogIGZpCiAgaWYgISBjb21tYW5kIC12IGdyYWZhbmEtc2VydmVyID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgZWNobyAncGFzc3dvcmQnIHwgc3VkbyAtUyAtcCAnJyBiYXNoIC1jICcKICAgICAgZXhwb3J0IERFQklBTl9GUk9OVEVORD1ub25pbnRlcmFjdGl2ZQogICAgICBta2RpciAtcCAvZXRjL2FwdC9rZXlyaW5ncwogICAgICBjdXJsIC1mc1NMIGh0dHBzOi8vYXB0LmdyYWZhbmEuY29tL2dwZy5rZXkgfCBncGcgLS1kZWFybW9yIC1vIC9ldGMvYXB0L2tleXJpbmdzL2dyYWZhbmEuZ3BnIDI+L2Rldi9udWxsCiAgICAgIGVjaG8gImRlYiBbc2lnbmVkLWJ5PS9ldGMvYXB0L2tleXJpbmdzL2dyYWZhbmEuZ3BnXSBodHRwczovL2FwdC5ncmFmYW5hLmNvbSBzdGFibGUgbWFpbiIgPiAvZXRjL2FwdC9zb3VyY2VzLmxpc3QuZC9ncmFmYW5hLmxpc3QKICAgICAgYXB0LWdldCB1cGRhdGUgLXFxIDI+L2Rldi9udWxsCiAgICAgIERFQklBTl9GUk9OVEVORD1ub25pbnRlcmFjdGl2ZSBhcHQtZ2V0IGluc3RhbGwgLXkgLXFxIGdyYWZhbmEKICAgICAgIyBpbnN0YWxsIGNsaWNraG91c2UgZGF0YXNvdXJjZSBwbHVnaW4gaWYgbWlzc2luZwogICAgICBpZiAhIFsgLWQgL3Zhci9saWIvZ3JhZmFuYS9wbHVnaW5zL2dyYWZhbmEtY2xpY2tob3VzZS1kYXRhc291cmNlIF07IHRoZW4KICAgICAgICBncmFmYW5hLWNsaSBwbHVnaW5zIGluc3RhbGwgZ3JhZmFuYS1jbGlja2hvdXNlLWRhdGFzb3VyY2UgPi9kZXYvbnVsbCAyPiYxIHx8IHRydWUKICAgICAgZmkKICAgICcgPi90bXBfd29ya3NwYWNlL2luc3RhbGxfZ3JhZmFuYS5sb2cgMj4mMQogIGZpCiAgIyAtLS0tIFN0YXJ0IHNlcnZlcnMgKG9ubHkgYWZ0ZXIgaW5zdGFsbHMgZmluaXNoKSAtLS0tCiAgaWYgISBwZ3JlcCAtZiAiY2xpY2tob3VzZS1zZXJ2ZXIiID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgKGNsaWNraG91c2Utc2VydmVyIC0tY29uZmlnLWZpbGU9L3RtcF93b3Jrc3BhY2UvY2xpY2tob3VzZS1jb25maWcueG1sID4vdG1wX3dvcmtzcGFjZS9jaF9zZXJ2ZXIubG9nIDI+JjEgJikgfHwgdHJ1ZQogIGZpCiAgaWYgISBwZ3JlcCAtZiAiZ3JhZmFuYSIgPi9kZXYvbnVsbCAyPiYxOyB0aGVuCiAgICAoZ3JhZmFuYS1zZXJ2ZXIgLS1ob21lcGF0aD0vdXNyL3NoYXJlL2dyYWZhbmEgLS1jb25maWc9L3RtcF93b3Jrc3BhY2UvZ3JhZmFuYS5pbmkgPi90bXBfd29ya3NwYWNlL2dyYWZhbmEubG9nIDI+JjEgJikgfHwgdHJ1ZQogIGZpCiAgIyBXYWl0IGJyaWVmbHkgZm9yIENsaWNrSG91c2UgcG9ydCwgdGhlbiBzZWVkIHF1ZXJ5X2xvZwogIGZvciBpIGluICQoc2VxIDEgMzApOyBkbwogICAgKGVjaG8gPiAvZGV2L3RjcC8xMjcuMC4wLjEvOTAwMCkgMj4vZGV2L251bGwgJiYgYnJlYWsKICAgIHNsZWVwIDIKICBkb25lCiAgcHl0aG9uMyAvdG1wX3dvcmtzcGFjZS9nZW5lcmF0ZV9kYXRhLnB5IDI+L2Rldi9udWxsCiAgYmFzaCAvdG1wX3dvcmtzcGFjZS9ydW5fbG9hZC5zaCA+L2Rldi9udWxsIDI+JjEKICBybSAtZiAvdG1wX3dvcmtzcGFjZS9nZW5lcmF0ZV9kYXRhLnB5CikgJgoKdHJ1ZQo= | base64 -d | bash
```