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.tsx

About

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.

Props
tsx
<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'>.

PropTypeDefaultDescription
planPlanNoderequired{ id, operation, relation?, estimatedRows?, actualRows?, cost?, actualMs?, children? }.
misestimateFactornumber10Ratio 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.
seqScanRowsnumber100_000Row 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.
localestring'en-GB'—
rowsLabelReactNode'rows'—
estimateLabelReactNode'est'—
misestimateLabelReactNode'estimate off'—
seqScanLabelReactNode'seq scan'—
misestimateNoteReactNode'Estimates are far from actuals — usually stale statistics, and the reason the planner chose this shape.'—

Functions

FunctionSignatureDescription
flattenPlan(node: PlanNode, depth = 0, out: Row[] = []) => Row[]—

Types

PlanNode

An EXPLAIN plan as a tree, with cost and row estimates.

FieldTypeDescription
idstring—
operationstring"Seq Scan", "Hash Join", "Index Scan using …".
relation?string—
estimatedRows?numberPlanner's estimate.
actualRows?number—
cost?numberTotal cost including children.
actualMs?number—
children?PlanNode[]—

Behaviour

TopicDetailDescription
barsself costTotal minus children — the node that is actually spending the time.