Query Plan
An EXPLAIN tree with cost bars drawn from self cost, not total. Every root holds 100% of total by definition, so a total-cost bar makes the root look like the problem in every query ever profiled.
src/components/ui/query-plan.tsxAbout
From the component's own source, where it was written.
An EXPLAIN plan as a tree, with cost and row estimates.
Self cost is what drives the bar, not total. Every plan's root has 100% of the total cost by definition, so a bar drawn from total makes the root look like the problem in every query ever profiled.
The estimate-versus-actual ratio is computed and flagged past a threshold. That mismatch is the single most useful number in a plan — it means the statistics are stale, and it is why the planner picked a nested loop over a hash join and turned 40ms into 40 seconds.
Sequential scans on large row counts are marked. Not because a seq scan is wrong — on a small table it is optimal — but because a seq scan over two million rows is usually a missing index.
Install
npx astralyx-ui add query-plan
Usage
import { QueryPlan } from '@/components/ui/query-plan'
<QueryPlan plan={plan} />Composer
Change the props and the source below follows.
Hash Joinestimate off 82×
98,400 rowsest 1,200412.0ms
Seq Scanon ordersestimate off 2100×seq scan
2,100,000 rowsest 1,000380.0ms
Hash
4,180 rowsest 4,20022.0ms
Index Scan using users_pkeyon users
4,180 rowsest 4,20020.0ms
Estimates are far from actuals — usually stale statistics, and the reason the planner chose this shape.
<QueryPlan plan={plan} misestimateFactor={10} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
QueryPlan
Extends Omit<ComponentProps<'div'>, 'children'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| plan | PlanNode | required | { id, operation, relation?, estimatedRows?, actualRows?, cost?, actualMs?, children? }. |
| misestimateFactor | number | 10 | Ratio past which estimate-vs-actual is flagged. That mismatch is the most useful number in a plan — it means stale statistics, and it is why the planner chose a nested loop. |
| seqScanRows | number | 100_000 | Row count above which a sequential scan is marked. A seq scan on a small table is optimal; over two million rows it is usually a missing index. |
| locale | string | 'en-GB' | — |
| rowsLabel | ReactNode | 'rows' | — |
| estimateLabel | ReactNode | 'est' | — |
| misestimateLabel | ReactNode | 'estimate off' | — |
| seqScanLabel | ReactNode | 'seq scan' | — |
| misestimateNote | ReactNode | 'Estimates are far from actuals — usually stale statistics, and the reason the planner chose this shape.' | — |
Functions
| Function | Signature | Description |
|---|---|---|
| flattenPlan | (node: PlanNode, depth = 0, out: Row[] = []) => Row[] | — |
Types
PlanNode
An EXPLAIN plan as a tree, with cost and row estimates.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| operation | string | "Seq Scan", "Hash Join", "Index Scan using …". |
| relation? | string | — |
| estimatedRows? | number | Planner's estimate. |
| actualRows? | number | — |
| cost? | number | Total cost including children. |
| actualMs? | number | — |
| children? | PlanNode[] | — |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| bars | self cost | Total minus children — the node that is actually spending the time. |