Rowkery Docs

Working with data

Query plans

Press ⌘E in a query tab to see how the database plans to run a statement, with the slowest step marked and, when Rowkery spots a likely fix, a hint with the SQL for it.

Show a plan

Put the caret in a statement, or select it, and press ⌘E (Explain statement). The plan replaces the results in a card titled Plan, and the header reads Estimate.

An estimated plan of a query on orders and order_items. The header reads Estimate, with Copy as text and Run with timings. A hint says the seq scan on orders reads about 84,000 rows to keep about 4 and offers CREATE INDEX ON public.orders (customer_id) with Copy. Each step shows its own cost as a bar and its expected rows.

An estimate runs nothing, not even a write: the database only says how it would go about it. Safe mode treats it as a read, so ⌘E on a DELETE shows its plan without asking.

⌘E works on one statement at a time. With several selected, Rowkery says Explain works on one statement at a time. If the statement already starts with EXPLAIN, it asks you to remove it and press ⌘E again.

On a table tab, ⌘E is Edit SQL instead. Explain isn't in the menu bar; find it in ⌘K or in the key strip beside Run (⌘E plan).

Run with timings

On Postgres, Run with timings in the plan header (or Explain with timings in ⌘K) runs the statement once and shows how long each step really took and how many rows it really returned. The header then reads Ran in with the time and the row count, and the button becomes Run again.

A timed plan: the header reads Ran in 43 ms, 183 rows. A hint suggests an index with a Copy button. The Seq Scan on orders is tinted as the slowest step and open, showing its Filter, Removed and Read details. Some steps carry tags such as 20× off, and a loop shows 2 → 3 × 61.

A timed plan of a read runs the read. A timed plan of a write goes through safe mode like any other write:

  • With Confirm writes, it stops first and shows the statement with Not run in the header. ⇧⌘↵ runs it and Esc drops it.
  • With Read-only, it's refused.
  • When it does run, it runs inside a transaction that is always rolled back, or inside a savepoint if you have your own transaction open in the tab. You see the timings, and your data stays as it was.
A DELETE on orders in the editor. Below it, the bar reads DELETE on orders isn't run yet, prd · storefront / shop · Confirm writes is on for this connection, with Cancel and Run buttons, and the results header reads Not run.

Esc or ⌘. cancels a timed plan that's taking too long, the same as a query. The next plan you ask for replaces the one shown.

Reading a plan

Each row is one step of the plan, indented under the step that uses its output. The columns are:

ColumnEstimateWith timings
StepThe kind of step and what it works on, such as Seq Scan on ordersThe same
OwnOwn cost: the step's share of the planner's cost, leaving out the steps under itOwn time: the time spent in the step itself, in ms
RowsRows, expected: how many rows the planner thinks the step returnsRows, expected → actual, with the number of loops when the step ran more than once, such as 2 → 3 × 61

The step that costs the most is tinted and opens by itself. Click any step to open its details.

Step details

DetailWhat it tells you
Index Cond, Recheck CondThe condition used to look up rows in an index, and the one checked again on each row found
Hash Cond, Merge Cond, Join FilterHow a join matches rows
FilterA condition checked on each row after reading it
RemovedRows the filter threw away, across all loops
Rechecked awayRows dropped on the recheck
SortHow the step sorted and how much memory or disk it used
HashHow many batches a hash used and its peak memory
SpilledHow much the step wrote to disk because it didn't fit in memory
ReadPages read, and how many came from disk rather than memory (with timings)
WorkersParallel workers launched

Bad guesses

In a timed plan, a step whose row guess was off by 10 times or more, and by at least 100 rows across its loops, gets a tag such as 20× off. A bad guess often leads the planner to pick the wrong join or scan, so these are worth a look.

Hints

Rowkery looks at the steps that carry at least a tenth of the plan's cost or time, heaviest first, and shows at most one hint above the plan. It checks for these, in this order:

What it foundThe hintSuggested SQL
A scan through a table of 10,000+ rows that keeps under 1% of them, filtering on a column no index starts with"Seq scan on orders reads … rows to keep …. orders.customer_id has no index."CREATE INDEX ON …
A row guess off by 1,000 rows or more (with timings)"Postgres expected … rows from orders and got …. Its statistics may be out of date."ANALYZE …
A sort that spilled to disk"The sort wrote … to disk. A larger work_mem keeps it in memory."SET work_mem = …
A hash or hash aggregate that spilled, or a hash join split into batches"The hash join split into … batches on disk. A larger work_mem keeps it in one."SET work_mem = …
A full scan repeated more than 100 times, once per row of a join (with timings)"… is scanned in full … times, once per row from the other side of the join."CREATE INDEX ON … when a filter column has no index
A filter that wraps an indexed column in a function, such as lower(email)"lower(email) can't use … Index lower(email), or compare email directly."CREATE INDEX ON … (lower(email))

Rowkery never runs a hint's SQL. Click Copy to copy it, then decide whether to run it yourself. Building an index on a large table takes time and locks, and a bigger work_mem costs memory on the server.

SQLite

On SQLite, ⌘E shows the steps of EXPLAIN QUERY PLAN: which tables SQLite scans or searches and which indexes it uses. SQLite gives no costs or times, so there are no own times, no expected rows and no Run with timings.

It hints at two things: a full scan of a table with 10,000 rows or more ("If you filter or join on one of its columns, an index on it lets SQLite go straight to the rows."), and a sort SQLite does itself for ORDER BY ("An index on the ORDER BY columns returns them already in order."). These hints have no SQL to copy.

Copy a plan

Copy as text in the plan header copies the plan as indented text, one line per step, to paste into a ticket or a chat.