OriginChain docs
use the dashboard · explain

Explain and query plans.

A query that is slow is slow for a reason, and the plan is where the reason is written down. The console has a dedicated Explain page - Query → Explain in the sidebar - that runs your SQL, draws the operator tree, and points at the operator that burned the time.

explain is not in the workbench

The workbench once carried EXPLAIN and EXPLAIN ANALYZE buttons next to RUN. They were removed. Its action bar is now just SAVE and RUN, and its Plan results tab is a placeholder that never fills. Plans live here.

1

Run an explain.

  1. Check the instance in the top bar is the one you mean.
  2. Paste a SELECT into the SQL box. Leave the EXPLAIN keyword off - the page adds it. (If you do type it, it is not doubled up.)
  3. Leave Analyze ticked - it is on by default - and click RUN EXPLAIN.
SELECT o.id, c.name, c.country, o.amount_cents
  FROM shop.orders o
  JOIN shop.customers c ON o.customer = c.id
 WHERE o.status = 'paid'
 LIMIT 10
analyze executes the query

With Analyze on, the engine runs the query and annotates each operator with what it actually did. That is what makes the timings real - and it also means the query does real work on the instance. Keep a LIMIT on while you are iterating, and untick Analyze if you only want the shape.

Four numbers land above the tree the moment it returns. Read them in this order: hot operator first (that is where to look), then total time (is this worth fixing), then rows returned against operator count (how much work per row of output).

The console Explain page after a run: a SQL box with the Analyze checkbox ticked and a Run Explain button, a status line reading explain analyze with the round-trip time, and a strip of four tiles reading total time 44 ms, rows returned 10, operator count 5, and hot operator scan 22 ms.
The Explain page after a run. The hot-operator tile is the one to read first. Figures throughout this page come from a demo dataset.

Under the strip the page echoes back the query it explained, so a tree you screenshot or share always carries the SQL it came from.

2

Read the plan tree.

The tree is drawn top down: the operator that produces your final rows is at the top, and its inputs hang beneath it. Data flows the other way - bottom to top - so the way to read it is to start at the leaves and work up.

The join query above produces this shape:

limit(10)
  hash_join(on customer = id)
    filter(Eq { path: "status", value: String("paid") })
      scan(shop.orders)
    scan(shop.customers)
The rendered plan tree: a limit node at the top over a hash_join on customer equals id, which draws from a filter on status paid over a scan of shop.orders on one side and a scan of shop.customers on the other. Each card lists rows in, rows out, time and share, and the scan of shop.orders is outlined in accent and marked hot at 49 percent.
The same plan, rendered. The accent border marks the hot operator - here the full scan of shop.orders, at roughly half the total time.

The four operators in this plan.

Operator What it does
scan Reads a table. The detail line names the table and reports segs scanned and pruned - how many storage segments it had to open versus how many it managed to skip. A scan whose rows in equals its rows out read the whole table.
filter Applies a predicate to the rows below it. The detail line spells out the predicate it is enforcing. Its rows-in against rows-out is the selectivity: a big drop here means the filter did useful work, but it did it after the rows were already read.
hash_join Joins its two children on the equality shown in the detail line. It has two inputs, so it is the one node in this tree that branches.
limit Caps the rows that leave the plan. Sitting at the top, it is usually cheap - and that is worth noticing: a LIMIT 10 does not stop the work underneath it from happening.

Other query shapes produce other operators - the tree you get depends on what the planner chose for the query you gave it. The reading method below does not change.

The four fields on every card.

  • rows in - how many rows this operator was handed.
  • rows out - how many it passed on. The gap is the work it did.
  • time - self time. The console subtracts the children's time from the operator's own, so a parent is never credited with its children's cost. This is the field that makes the tree readable.
  • share - that self time as a percentage of the whole plan. The shares add up to 100%.
the hot operator is the one with the biggest self time

It gets the accent border and a hot badge, and it is repeated in the header strip. It is chosen on self time, not total - which is why it lands on the leaf that is genuinely expensive rather than on whatever happens to sit nearest the top of the tree.

3

Structure without running it.

Untick Analyze and the engine plans the query without executing it. You get the same tree with the same operators and detail lines, but no rows, no timings, no shares - and the header strip's timing tiles show a dash. Use it when the query is expensive, or when the only thing you want to know is "did it decide to join these two the way I expected".

The same plan tree rendered with Analyze unticked: limit over hash_join over a filter and two scans, each card showing only the operator name and its detail line, with no rows, time or share figures and no hot-operator highlight.
Analyze off. Same shape, no measurements - and no hot operator, because nothing was timed.
4

Query stats - what to explain in the first place.

Query → Query stats aggregates every query the instance has served, grouped by fingerprint - the query with its literals replaced by ?, so a thousand runs with different parameter values collapse into one row. It is sorted by total time by default, which is exactly the right default: the query dominating your instance is rarely the slowest one, it is the merely-slow one you run constantly.

The Query stats page listing five query fingerprints with SQL, ASK and CYPHER language chips and columns for calls, p50, p95, p99, total, average rows and cache hit rate, sorted by total time, with a filter box and language filter above and an open link on each row.
Query stats. Fingerprints, percentiles, and total time - sorted so the load-dominant query is the first row.

What to look for:

  • total - the top row is where your instance's time goes. Start there, not at the bottom.
  • p95 and p99 far above p50 - the query is usually fine and occasionally terrible. That is a different problem from "uniformly slow", and it usually means a data-dependent path: some parameter values hit far more rows than others.
  • avg rows - a big average row count on a query nobody reads in full is a projection or LIMIT you should tighten.
  • cache - the hit rate, where the engine reports one.

Every column header sorts, the box at the top filters by text, and the All / SQL / Cypher / Ask buttons narrow by language. open at the end of a row loads that fingerprint's page in the workbench against the same instance.

this window is not forever

The registry lives in the engine's memory and holds the top 200 fingerprints by total time. The since timestamp on the right of the toolbar tells you how far back the numbers go - a restart resets them, so a suspiciously quiet instance may just have been restarted recently.

5

Slow queries - the individual runs.

Where Query stats aggregates, Query → Slow queries is the raw log: the most recent executions, newest first, with their real text and the literals intact. Drag the threshold slider up to hide everything under a given number of milliseconds - on a busy instance that is the difference between a wall of noise and the three runs you care about.

The Slow queries page with a threshold slider set to zero milliseconds and seven recent executions listed, each showing when it ran, a language chip, elapsed time, row count, the query text, and an explain link at the end of the SQL rows.
Slow queries. Each SQL row carries an explain link that opens the Explain page with that exact query already loaded and run.

Three details that matter:

  • The explain link is the whole point of the page - it opens the Explain page with that execution's SQL prefilled and already run, so you go from "this took too long" to its plan in one click.
  • Only SQL rows get that link. Cypher and natural-language rows show a dash - there is no plan visualiser for them.
  • Anything at or above 100 ms is called out in amber, independently of where you have the slider.

Like Query stats, this is an in-memory ring - the last 200 executions, cleared on engine restart. It is a live debugging tool, not an audit trail.

6

Turning a plan into a fix.

A plan tells you where the time went. What you do about it falls into two buckets: change the schema, or change the query.

Change the schema.

The signature to look for is a scan whose rows out are far larger than what the filter above it keeps. In the plan above, the scan reads every row of shop.orders and the filter immediately throws most of them away - and the scan's pruned=0 confirms it could not skip any storage segment on the way. The rows the query wanted were known up front; the engine just had no structure that let it find them without reading everything.

That is a secondary index on the filtered column. Indexes are part of the table's definition, so you add one on Data → Schema, in the table's secondary indexes section - not from the Explain page. Re-run the explain afterwards and compare: the same query should come back with a different leaf operator, and a smaller share on it.

Change the query.

  • The hot operator is a join. Filter harder before the join rather than after it - anything you can move into the WHERE of the larger side is work the join never has to do.
  • Rows out is much larger than what you use. Project the columns you actually need instead of everything, and make sure the LIMIT reflects what you will really read. Remember that a limit at the top of the tree does not make the work below it disappear.
  • p95 is far above p50 in Query stats but a one-off explain looks fine. You explained a cheap parameter value. Take a genuinely slow run from Slow queries - literals intact - and explain that one instead.
the loop

Query stats to find what costs you the most → Slow queries to grab a real slow execution of it → Explain to see which operator burned the time → change the schema or the query → explain it again and check the share moved. The Telemetry tab in the workbench closes the loop from the other end, showing the same per-fingerprint numbers next to the query you are editing.