Skip to main content

mz-query-tracing

Debug SQL execution time via distributed tracing (OpenTelemetry / Tempo). Trigger: "why is this query slow", "where is the time going", "this SELECT takes forever", or latency breakdown for SQL statement. Also tracing queries, span analysis, Tempo traces, trace IDs, opentelemetry_filter.

Source facts

Repository
MaterializeInc/materialize
Last source activity
May 7, 2026 at 16:53
Detected SKILL.md language
English
Stars
6,383
Forks
515

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
mz-query-tracing
description
Debug SQL execution time via distributed tracing (OpenTelemetry / Tempo). Trigger: "why is this query slow", "where is the time going", "this SELECT takes forever", or latency breakdown for SQL statement. Also tracing queries, span analysis, Tempo traces, trace IDs, opentelemetry_filter.
# Query Tracing Skill Use distributed tracing (OpenTelemetry / Tempo) to understand where time is spent during SQL statement execution in a local Materialize instance. For background on Materialize's tracing infrastructure (span levels, filtering, `#[tracing::instrument]`, distributed context propagation), see `doc/developer/tracing.md`. ## Important: Use Optimized Builds **Always trace `--optimized` builds.** Debug builds introduce significant artifacts — unoptimized code, extra debug checks, and inflated self-times that don't reflect production behavior. Tracing a debug build can be misleading because time may appear in places that are negligible in release builds. ```bash bin/environmentd --optimized --monitoring ``` The `--optimized` build takes longer to compile but produces traces that accurately represent where time is actually spent. ## Prerequisites The local monitoring stack (Tempo + Grafana + Prometheus) and `environmentd` with tracing enabled must be running. All network calls require `dangerouslyDisableSandbox`. **Important: Start the monitoring stack _before_ environmentd.** If Tempo is not running when environmentd starts sending spans, those spans are lost — they will not be retroactively captured. ### 1. Start the monitoring stack ```bash bin/mzcompose --find monitoring run default ``` Wait for Tempo to be ready: ```bash # Poll until Tempo is accepting requests for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code}" http://localhost:3200/ready | grep -q 200 && break sleep 5 done ``` ### 2. Start environmentd with tracing ```bash bin/environmentd --optimized --monitoring ``` Or with the trace filter pre-set: ```bash bin/environmentd --optimized --reset --monitoring -- --system-parameter-default='opentelemetry_filter=debug' ``` ### 3. Verify both services are running Before tracing, confirm the stack is healthy: ```bash # Tempo accepting traces? curl -s -o /dev/null -w "%{http_code}" http://localhost:3200/ready # environmentd listening? lsof -tiTCP:6875 -sTCP:LISTEN ``` ### 4. Set the trace filter at runtime (if not set at startup) Connect as `mz_system` and set the OpenTelemetry filter: ```bash psql -U mz_system -h localhost -p 6877 materialize -c "ALTER SYSTEM SET opentelemetry_filter = 'debug';" ``` `debug` is a good starting filter. For even more detail, use `trace` (but expect much more data). You can also target specific modules: ``` ALTER SYSTEM SET opentelemetry_filter = 'mz_adapter::coord=trace,debug'; ``` ## Workflow: Trace a Query ### Step 1: Run the query and capture the trace ID Use `psql` with `emit_trace_id_notice` enabled. **Important:** Use separate `-c` flags for SET and the query, since DDL statements (CREATE, DROP, ALTER) cannot run inside an implicit transaction block: ```bash psql -U materialize -h localhost -p 6875 materialize \ -c "SET emit_trace_id_notice = true;" \ -c "<YOUR SQL STATEMENT HERE>;" ``` The output will contain NOTICE lines with trace IDs — one per statement. The SET statement gets its own trace ID; the second trace ID is the one you want: ``` SET NOTICE: trace id: fd2f69eb059e4823d8d48a87ccab8f6f NOTICE: trace id: 984bd8c69f99703243d350e14b02caea CREATE TABLE ``` The trace ID for the query is the **last** one before the statement result. To extract the trace ID programmatically: ```bash OUTPUT=$(psql -U materialize -h localhost -p 6875 materialize \ -c "SET emit_trace_id_notice = true;" \ -c "CREATE TABLE foo (id INT);" 2>&1) TRACE_ID=$(echo "$OUTPUT" | grep "trace id:" | tail -1 | sed 's/.*trace id: //') echo "Trace ID: $TRACE_ID" ``` ### Step 2: Wait for spans to be flushed Spans are batched before export (default 5s delay, configured in `--opentelemetry-sched-delay`). Wait ~10 seconds after the query completes. ### Step 3: Fetch the trace from Tempo Query the Tempo HTTP API on port 3200. Retry if needed — spans may still be in flight: ```bash sleep 10 # Retry loop — first attempt often gets "trace not found" for i in $(seq 1 5); do RESP=$(curl -s http://localhost:3200/api/traces/$TRACE_ID) if echo "$RESP" | grep -q "batches"; then echo "$RESP" > /tmp/claude-1000/trace.json echo "Trace fetched ($(echo "$RESP" | wc -c) bytes)" break fi echo "Trace not ready, retrying... ($i/5)" sleep 5 done ``` ### Step 4: Analyze the trace The Tempo API returns traces in OTLP JSON format. **Important:** Span IDs and parent span IDs are base64-encoded, not hex. The analysis script handles this. Use the `trace_tree.py` script in this skill directory to analyze traces: ```bash python3 .claude/skills/mz-query-tracing/trace_tree.py /tmp/claude-1000/trace.json "My Query" ``` ## Interpreting Results - **Self-time** is where actual work happens. A span with 100ms duration but 0ms self-time is just a wrapper — its children do all the work. - **`group_commit_apply::append_fut`** — Persist append operations, often dominant for DDL. Time here is spent writing to the durable log. - **`catalog::transact_inner`** — Catalog transaction processing. High self-time means catalog state manipulation. - **`oracle::write_ts` / `oracle::apply_write`** — Timestamp oracle calls to CockroachDB. Each is a round-trip. - **`consensus::compare_and_set` / `consensus::scan`** — Persist consensus operations (also CockroachDB round-trips). - **`coord::check_consistency`** — Post-DDL consistency check. Self-time here is pure computation. - **Sequential vs. concurrent children** — children with overlapping time ranges are concurrent; non-overlapping are sequential. - **Code locations** in `[src/...]` brackets let you jump directly to the source code. - For spans not listed above, **read the source code** at the location shown in the `[src/...]` bracket to understand what the span does and why it's taking time. ## Tips - **Use `--optimized` builds.** Debug builds inflate self-times with unoptimized code paths and extra debug checks (e.g., `check_consistency` may appear much larger than in production). Always trace optimized builds for meaningful results. - **Start monitoring before environmentd.** Spans emitted before Tempo is ready are silently dropped. If you restart environmentd, make sure Tempo is still running. - If traces are empty or incomplete, the `opentelemetry_filter` may be too restrictive. Try `debug` or even `trace`. - The first fetch after a query often returns `trace not found`. The default batch delay is 5s, but ingestion adds latency. Always use a retry loop. - Tempo retains traces for only 15 minutes by default (configured in `misc/monitoring/tempo.yml`). Fetch traces promptly. - To search for recent traces: `curl -s "http://localhost:3200/api/search?limit=10"` - You can also view traces in Grafana at http://localhost:3000 by searching for the trace ID in the Tempo datasource explore view. - For comparing before/after a code change, trace the same query on both builds and compare the self-time rankings side by side.
View on GitHub