Help us shape the future of Go! Take the 2026 Go Developer Survey

explain

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package explain turns what a database says about how it will run (or did run) a statement into one engine-neutral plan tree, works out where the time or the work goes, and says what looks wrong in plain words.

Four engines, six shapes of output, one tree:

Postgres  EXPLAIN (FORMAT JSON [, ANALYZE, BUFFERS])  ─► ParsePostgresJSON ─┐
Postgres  EXPLAIN [ANALYZE] (text, as psql shows it)  ─► ParsePostgresText ─┤
bytdb     EXPLAIN (Postgres-style text, no costs)     ─► ParsePostgresText ─┤
MySQL     EXPLAIN FORMAT=TREE / EXPLAIN ANALYZE       ─► ParseMySQLTree ────┼─► *Plan ─► Finalize
MySQL     EXPLAIN (the classic table), MariaDB too    ─► ParseMySQLTable ───┤      │    (metrics,
SQLite    EXPLAIN QUERY PLAN (id, parent, detail)     ─► ParseSQLite ───────┘      │     insights)
                                                                                   ▼
                                             TUI plan view · text · JSON · interactive HTML
                                             PDF · JPEG/PNG (picture.go) · Mermaid (mermaid.go)

THE TREE IS THE LOWEST COMMON DENOMINATOR, NOT THE UNION. A Node has the fields every consumer needs to draw and judge a step — its operation, the table and index it touches, estimated and actual rows, cost, time — typed, so the insight rules can be written once. Everything else an engine says about a step (a Postgres "Heap Blocks: exact=1471", a MySQL key_len) rides along as ordered Props: shown in the detail panel exactly as the engine worded it, never interpreted. Adding an engine means writing a parser that fills the typed fields it can; the views and rules then just work.

WHAT IS MISSING IS SAID, NOT FAKED. SQLite reports no costs and no rows; bytdb reports neither either; only Postgres and MySQL report per-step timings. A Plan records what it has (HasCost, HasEst, Analyzed), and the views pick the best metric that exists (see Metric) and label it, rather than drawing a confident bar chart out of numbers nobody measured.

Index

Constants

View Source
const (
	Postgres = "postgres"
	MySQL    = "mysql"
	SQLite   = "sqlite"
	Bytdb    = "bytdb"
)

Engine names, as the config's driver names them.

Variables

View Source
var (

	// PlanJS is the plan view's script, for a page that loads it as a file
	// (dbc web). It defines window.DbcPlan.
	//
	//go:embed assets/plan.js
	PlanJS string

	// PlanCSS is the plan view's stylesheet, scoped under .dbc-plan.
	//
	//go:embed assets/plan.css
	PlanCSS string
)

The interactive plan view is three assets:

plan.js    the view itself: DbcPlan.mount(root, doc, opts)
plan.css   its look, scoped under .dbc-plan
plan.html  the standalone page: a template that inlines the other two

The standalone page (HTML below — `dbc explain --open`, the TUI's `b`) and dbc web's Plan tab (which serves PlanJS and PlanCSS as files) run the SAME script, so the two cannot drift: a feature added to the view is in both. The page stays one self-contained file — no CDN, no web font, no second request — so it opens the same from a temp file, an email attachment, or an air-gapped machine.

WHY A TEMPLATE FILE AND NOT element/html/template. The page is mostly JavaScript — a tree layout, pan and zoom, a flame graph — and html/template would contextually escape inside the <script> blocks, rewriting code it has no business touching. So the assets are plain text, edited as what they are, and the Go side does a handful of substitutions, each escaped for the one context it lands in (see HTML).

Functions

func Aliases

func Aliases(stmt string) map[string]string

Aliases maps each alias a statement gives a table to the table's name: "FROM orders o JOIN users AS u" → {o: orders, u: users}. MySQL and SQLite name steps by alias ("Table scan on o"), and an index suggestion needs the table — CREATE INDEX … ON o would fail. A schema-qualified table is also reachable by its bare name ("users" → "public.users") unless two schemas' tables share it, in which case the bare name maps to neither.

It is a word walk, not a parse. A table reference starts after FROM, JOIN, UPDATE or INTO, or after a comma while a FROM list is open at the same parenthesis depth; the list closes at the clause words that end it (WHERE, GROUP, ORDER, …). A subquery's derived-table alias maps to nothing and is left alone.

func Bar

func Bar(frac float64, cells int) string

Bar draws frac (0–1) as a bar of at most cells cells, in eighth-cell steps, so a 3% share still shows as a sliver rather than nothing. A zero share draws nothing at all, which reads differently from "tiny" on purpose.

func Compare

func Compare(before, after *Plan) (text string, good bool)

Compare describes after against before by the best figure both have: measured time, else the planner's total cost. good says whether it went the right way (down, or within 1%). "" when there is nothing to compare — no before, or no figure the two share.

func Engine

func Engine(d string) string

Engine maps a config driver name (postgres, pg, sqlite3, mariadb, …) onto an engine constant.

func FmtCost

func FmtCost(c float64) string

FmtCost formats planner cost units compactly.

func FmtCount

func FmtCount(v float64) string

FmtCount formats a count in full with thousands separators: 36,650.

func FmtFactor

func FmtFactor(f float64) string

FmtFactor describes a misestimate: "×27 more" / "×25 fewer", "" when it is within 2× (planner estimates are rarely closer, and nobody acts on 1.3×).

func FmtKB

func FmtKB(kb float64) string

FmtKB formats a size given in kilobytes.

func FmtMetric

func FmtMetric(m Metric, v float64) string

FmtMetric formats a value of metric m.

func FmtMs

func FmtMs(ms float64) string

FmtMs formats milliseconds at a precision that suits the size: 840 µs, 21.6 ms, 3.42 s, 2m05s.

func FmtRows

func FmtRows(v float64) string

FmtRows formats a row count compactly: 950, 36.7k, 1.2M. Fractional per-loop averages below 10 keep one decimal ("0.7").

func PlanDir

func PlanDir() string

PlanDir is where plans opened in a browser are kept by default.

func RowsCell

func RowsCell(n *Node, analyzed bool) (rows, factor string)

RowsCell is the rows column for a step: actual rows (across loops) when the plan was analyzed, "~estimate" otherwise, and a misestimate marker when the two disagree by ×2 or more.

func SameSubject

func SameSubject(a, b *Plan) bool

SameSubject reports whether two plans explain the same statement on the same connection, give or take whitespace and case — the condition for showing one as the "before" of the other. A plan detected in a result (no statement) has no subject, so it compares with nothing.

func ScriptHash

func ScriptHash() string

ScriptHash is the CSP source for the standalone page's inline script, "'sha256-…'", for a server that sends the page (dbc web's "open as page"): script-src with this and nothing else runs the view and would run nothing injected beside it.

func Strip

func Strip(stmt string) (inner string, analyze, ok bool)

Strip removes a leading EXPLAIN clause, so "explain this" works on a statement the user already wrote as an EXPLAIN — dbc then sends its own, in the format it can read. analyze reports whether the clause asked for ANALYZE, which the caller honors. ok is false when stmt is not an EXPLAIN.

Every engine's spelling is recognized:

EXPLAIN [ANALYZE|ANALYSE] [VERBOSE] stmt           Postgres, MySQL
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) stmt       Postgres
EXPLAIN FORMAT=TREE stmt  /  EXPLAIN FORMAT = JSON  MySQL
EXPLAIN QUERY PLAN stmt                            SQLite
EXPLAIN EXTENDED | PARTITIONS stmt                 older MySQL

It walks words, not a grammar: after EXPLAIN it skips option words and one parenthesized option list, and the statement starts at the first word that is none of those. Comments before EXPLAIN are skipped the way the splitter skips them.

func TruncateCells

func TruncateCells(s string, w int) string

TruncateCells shortens s to at most w terminal cells, ending in "…".

func WrapWords

func WrapWords(s string, w int) []string

WrapWords wraps prose at word boundaries to w cells.

Types

type Document

type Document struct {
	*Plan
	Headline string   `json:"headline"`
	Metrics  []Metric `json:"metrics"`
}

Document is a plan as JSON consumers see it: the plan itself plus the few things every consumer would otherwise recompute — its headline and the metrics it can be viewed by. It is what `dbc explain -t json` prints and what the HTML view embeds.

type Insight

type Insight struct {
	Severity Severity `json:"severity"`
	NodeID   int      `json:"node"` // the step it is about; -1 for the plan as a whole
	Title    string   `json:"title"`
	Detail   string   `json:"detail,omitempty"`
	// Fix is the next step, in words; SQL, when there is one, is a statement
	// that would carry it out (a CREATE INDEX, an ANALYZE). It is offered to
	// copy or insert into the editor — never run on the user's behalf.
	Fix string `json:"fix,omitempty"`
	SQL string `json:"sql,omitempty"`
}

Insight is one finding about a plan, in words a user can act on.

type Kind

type Kind string

Kind classifies a step for coloring and reasoning.

const (
	KindScan     Kind = "scan"     // reads a whole table
	KindIndex    Kind = "index"    // reaches rows through an index
	KindJoin     Kind = "join"     // combines two inputs
	KindSort     Kind = "sort"     // orders rows
	KindAgg      Kind = "agg"      // groups, counts, deduplicates, windows
	KindLimit    Kind = "limit"    // stops early
	KindFilter   Kind = "filter"   // drops rows (MySQL's separate Filter step)
	KindHash     Kind = "hash"     // builds an in-memory table: Hash, Memoize, Materialize
	KindSubquery Kind = "subquery" // a subquery, CTE, or derived table
	KindSet      Kind = "set"      // UNION and friends
	KindParallel Kind = "parallel" // gathers the output of parallel workers
	KindModify   Kind = "modify"   // INSERT / UPDATE / DELETE / MERGE
	KindResult   Kind = "result"   // the statement itself, or a constant row
	KindOther    Kind = "other"
)

func Classify

func Classify(op string) Kind

Classify maps an operation name to its Kind. It is keyword matching over every engine's vocabulary at once — the names barely overlap, so one table serves all four, and a parser only calls it for steps it did not classify itself.

func (Kind) Glyph

func (k Kind) Glyph() string

Glyph is the one-cell symbol the views draw beside a step of this kind. Each is a single-width character from the BMP math and arrows blocks, which every terminal font carries — no emoji, whose width terminals disagree on.

type Metric

type Metric string

Metric is what a bar, a percentage, or a flame block's width measures.

const (
	MetricTime  Metric = "time"  // measured milliseconds (Analyzed plans)
	MetricCost  Metric = "cost"  // the planner's cost units
	MetricRows  Metric = "rows"  // rows each step handles
	MetricShape Metric = "shape" // no numbers at all: a heuristic by kind
)

func (Metric) Label

func (m Metric) Label() string

Label is how the views name the metric.

type Node

type Node struct {
	ID int `json:"id"`

	// Op is the step as the engine names it ("Hash Join", "Seq Scan",
	// "SEARCH", "Index lookup"); Kind is what sort of step that is, which is
	// what the views color and the rules reason by.
	Op   string `json:"op"`
	Kind Kind   `json:"kind"`

	Relation string `json:"relation,omitempty"` // the table, when the step reads one
	Alias    string `json:"alias,omitempty"`    // what the statement calls it
	Index    string `json:"index,omitempty"`    // the index it uses, if any

	// Relationship is how the step feeds its parent when that is not the
	// plain input it is by default: "Inner"/"Outer" of a join, "SubPlan",
	// "InitPlan", "CTE recent". Shown, never interpreted.
	Relationship string `json:"relationship,omitempty"`

	// Props is everything else the engine said about the step, in its
	// order and its words: "Filter" → "(age > 30)".
	Props []Prop `json:"props,omitempty"`

	// Estimates — per execution of the step, as every engine reports them.
	HasCost     bool    `json:"has_cost,omitempty"`
	StartupCost float64 `json:"startup_cost,omitempty"`
	TotalCost   float64 `json:"total_cost,omitempty"`
	HasEst      bool    `json:"has_est,omitempty"`
	EstRows     float64 `json:"est_rows,omitempty"`
	Width       float64 `json:"width,omitempty"`

	// Actuals — Postgres and MySQL report time and rows as an average per
	// loop, so a step inside a nested loop that ran 4,000 times says
	// "0.001 ms, 1 row" and means four milliseconds and four thousand rows.
	// Finalize multiplies it out (TotalMs, RowsOut).
	HasActual     bool    `json:"has_actual,omitempty"`
	ActualStartMs float64 `json:"actual_start_ms,omitempty"`
	ActualMs      float64 `json:"actual_ms,omitempty"`
	ActualRows    float64 `json:"actual_rows,omitempty"`
	Loops         float64 `json:"loops,omitempty"`
	NeverExecuted bool    `json:"never_executed,omitempty"`

	// Typed extras the insight rules read. Each is zero when the engine did
	// not say, which every rule treats as "no evidence", never as "zero".
	RowsRemoved     float64 `json:"rows_removed,omitempty"` // discarded by a filter or recheck, per loop
	SortSpill       bool    `json:"sort_spill,omitempty"`   // a sort that went to disk
	SpillKB         float64 `json:"spill_kb,omitempty"`     // how much it wrote there
	Batches         float64 `json:"batches,omitempty"`      // hash / hash-aggregate batches (>1 = spilled)
	WorkersPlanned  float64 `json:"workers_planned,omitempty"`
	WorkersLaunched float64 `json:"workers_launched,omitempty"`
	SharedHit       float64 `json:"shared_hit,omitempty"`   // buffer pages found in cache
	SharedRead      float64 `json:"shared_read,omitempty"`  // pages read from disk (or the OS cache)
	TempBlocks      float64 `json:"temp_blocks,omitempty"`  // pages written to temp files
	TableRows       float64 `json:"table_rows,omitempty"`   // the whole table's size, when looked up
	FilteredPct     float64 `json:"filtered_pct,omitempty"` // MySQL's estimate of the rows a filter keeps, 0–100

	// Flags are engine facts the rules key off that have no typed field:
	// "filesort", "temporary", "join-buffer", "dependent", "correlated",
	// "auto-index", "temp-btree". Lowercase, stable, never shown raw.
	Flags []string `json:"flags,omitempty"`

	Children []*Node `json:"children,omitempty"`

	// Derived by Finalize — see there for how each is worked out.
	Depth    int     `json:"depth"`
	TotalMs  float64 `json:"total_ms,omitempty"` // inclusive, across every loop
	SelfMs   float64 `json:"self_ms,omitempty"`  // TotalMs less the children's
	SelfCost float64 `json:"self_cost,omitempty"`
	RowsOut  float64 `json:"rows_out,omitempty"` // rows the step produced, across every loop
	// Participants is how many processes share the step's loops: the
	// enclosing Gather's workers plus its leader for a step under one, 1
	// otherwise. Loops/Participants is how many times the step really ran.
	Participants float64 `json:"participants,omitempty"`
	// Misestimate is actual rows over estimated rows, per loop: 1 is spot on,
	// 27 is "27× more than planned", 0.04 is "25× fewer". 0 when either side
	// is unknown.
	Misestimate float64 `json:"misestimate,omitempty"`
	// Weights is Self for every metric the plan offers, precomputed so a
	// consumer of the JSON (the HTML view) sizes blocks exactly as the
	// terminal does without re-deriving the shape heuristic.
	Weights map[Metric]float64 `json:"weights,omitempty"`
	// contains filtered or unexported fields
}

Node is one step of a plan.

func (*Node) HasFlag

func (n *Node) HasFlag(f string) bool

HasFlag reports whether the step carries flag f.

func (*Node) Inclusive

func (n *Node) Inclusive(m Metric) float64

Inclusive is the step's metric including everything under it. It is computed as Self plus the children's Inclusive rather than read from the engine's own inclusive figure, so a flame block is always at least as wide as its children together — engines' inclusive numbers do not always add up (parallel workers, loops, subplans), and a child drawn wider than its parent would be nonsense.

func (*Node) Parent

func (n *Node) Parent() *Node

Parent returns the step's parent, nil for the root.

func (*Node) Prop

func (n *Node) Prop(key string) (string, bool)

Prop returns the value of the first prop named key.

func (*Node) RowsExamined

func (n *Node) RowsExamined() float64

RowsExamined is how many rows the step looked at across every loop: the ones it passed on plus the ones its filter threw away. For a scan this is the work it did, which RowsOut alone understates — a scan that reads 200,000 rows to return 10 "produced" 10.

func (*Node) Self

func (n *Node) Self(m Metric) float64

Self is the step's own share of metric m — what it costs excluding its children. Always ≥ 0.

func (*Node) Summary

func (n *Node) Summary() string

Summary is the single most telling detail of the step, for the tree's inline hint.

func (*Node) Target

func (n *Node) Target() string

Target names what the step works on, as a reader would say it: "orders o", "orders using orders_status", "orders_status". "" for a step that touches no table.

func (*Node) Title

func (n *Node) Title() string

Title is the step's one-line name: its operation and what it works on.

type PictureOptions

type PictureOptions struct {
	// Metric sizes the bars and heat-colors the cards; "" is the plan's
	// best (Plan.Metric), and one the plan does not have falls back to it.
	Metric Metric
	// Palette colors the picture; the zero value is theme.Default(). dbc web
	// passes the light or dark palette the view is wearing, so what is sent
	// is what was on screen; the shell and the TUI pass the config's
	// plan_theme (or `dbc explain --theme`).
	Palette theme.Palette
	// Compare is the before/after line for a plan re-explained after a
	// change ("vs last: 3.4× faster"), shown in the header when set.
	Compare string
	// Scale is device pixels per CSS pixel; 0 means 2. A plan too big to
	// draw at it is drawn smaller (see maxPixels).
	Scale float64
}

PictureOptions choose how the picture looks. The zero value is the plan view's defaults: the plan's best metric, the dark dbc palette, 2×.

type Plan

type Plan struct {
	Engine    string `json:"engine"`
	Conn      string `json:"conn,omitempty"`      // connection name it ran on
	Statement string `json:"statement,omitempty"` // the statement explained
	Command   string `json:"command,omitempty"`   // the EXPLAIN actually sent, for the record

	// Analyzed: the statement was executed and every step carries measured
	// rows, loops and time. Measured: the statement was executed and timed
	// as a whole, on an engine that cannot time its steps (SQLite, bytdb) —
	// the root carries the total, the steps carry nothing.
	Analyzed bool `json:"analyzed"`
	Measured bool `json:"measured,omitempty"`

	PlanningMs  float64 `json:"planning_ms,omitempty"`
	ExecutionMs float64 `json:"execution_ms,omitempty"`
	ResultRows  float64 `json:"result_rows,omitempty"` // Measured: the rows the statement returned

	// Notes are facts about how the plan was obtained that a reader must
	// know to read it right ("ran inside a transaction that was rolled
	// back"). They are not findings about the plan; Insights are.
	Notes    []string  `json:"notes,omitempty"`
	Root     *Node     `json:"root"`
	Insights []Insight `json:"insights,omitempty"`

	// Metric is the best measure this plan has, which the views size bars
	// and flame blocks by unless the user picks another (see Metrics).
	Metric Metric `json:"metric"`

	// Raw is the engine's own output, verbatim — what "copy the plan"
	// copies, since that is what a DBA or a search engine recognizes.
	Raw string `json:"raw,omitempty"`
	// contains filtered or unexported fields
}

Plan is one explained statement.

func Detect

func Detect(r *model.Result, driver string) (*Plan, bool)

Detect recognizes a result grid that holds a plan — the output of an EXPLAIN the user ran themselves — and parses it, so the grid can offer "show as plan". driver names the engine the result came from. ok is false for any other result.

func ParseMySQLTable

func ParseMySQLTable(cols []string, rows [][]string) (*Plan, error)

ParseMySQLTable reads the classic EXPLAIN table (id, select_type, table, type, possible_keys, key, rows, filtered, Extra — and MariaDB's ANALYZE, which adds r_rows and r_filtered). It is the only format every MySQL and MariaDB version speaks, so it is the fallback when the tree format is not available.

The table is flat: one row per table access, grouped by the SELECT (id) it belongs to, in join order. It becomes a tree the only way it honestly can — each SELECT a subtree, its tables under a nested-loop join in the order MySQL reads them (MySQL joins are nested loops, outer table first, unless Extra says a hash join). Nothing here invents a cost: the table has none, so the plan offers rows, not cost, as its metric.

func ParseMySQLTree

func ParseMySQLTree(text string) (*Plan, error)

ParseMySQLTree reads MySQL's tree format: EXPLAIN FORMAT=TREE (8.0.16+) and EXPLAIN ANALYZE (8.0.18+), which is the same tree with measurements. Unlike Postgres, MySQL returns the whole tree as ONE value with embedded newlines, and a statement with a dependent subquery gets a second tree at the top level:

-> Filter: (u.id < 50)  (cost=10.1 rows=49) (actual time=0.03..0.2 rows=49 loops=1)
    -> Index range scan on u using PRIMARY over (id < 50)  (cost=10.1 rows=49) …
-> Select #2 (subquery in projection; dependent)
    -> Aggregate: max(o.total)  (cost=4148 rows=1) (actual time=29.8..29.8 rows=1 loops=49)

Nesting is four columns per level. Several top-level trees are gathered under a synthetic "Query" root, so every view still has one tree to draw.

func ParsePostgresJSON

func ParsePostgresJSON(data []byte) (*Plan, error)

ParsePostgresJSON reads the output of EXPLAIN (FORMAT JSON …): a one-element array holding {"Plan": {...}, "Planning Time": …, "Execution Time": …}.

It decodes into maps rather than a struct because the key set is open: every Postgres version and node type adds keys ("Cache Hits" for Memoize, "Presorted Key" for Incremental Sort), and a struct would silently drop the ones it did not list. The keys the rules need are lifted into typed Node fields; the rest become Props in the engine's order (see pgPropOrder).

func ParsePostgresText

func ParsePostgresText(lines []string, engine string) (*Plan, error)

ParsePostgresText reads EXPLAIN's text format — what psql prints, and what bytdb emits (it speaks the same format, without costs). engine names which of the two it came from.

The format's structure is its indentation. A step's own detail lines sit two columns right of where its name starts, and so do the arrows of its children; a subplan is announced by a label line at the detail column just before its arrow:

col 0   Hash Join  (cost=…)                  ◄ root; its text starts at 0
col 2     Hash Cond: (o.user_id = u.id)      ◄ detail of the node at text col 0
col 2     ->  Seq Scan on orders o (…)       ◄ child; its text starts at col 6
col 8           Filter: (…)                  ◄ detail of the node at text col 6
col 2     SubPlan 1                          ◄ label for the next child
col 2     ->  Aggregate (…)
col 0   Planning Time: 0.4 ms                ◄ summary, once back at col 0

So each line is attached by column alone: a detail or label line at column i belongs to the most recent step whose text starts left of i, and an arrow at column c is a child of the most recent step whose text starts left of c. A stack of open steps, popped past anything at or right of the column, finds both in one pass.

func ParseSQLite

func ParseSQLite(cols []string, rows [][]string) (*Plan, error)

ParseSQLite reads EXPLAIN QUERY PLAN: rows of (id, parent, notused, detail), where parent names the row a row hangs under (0 = the top). It is the sparest of the formats — no costs, no row counts, only what SQLite will do:

SEARCH o USING INDEX orders_status (status=?)
SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
USE TEMP B-TREE FOR GROUP BY
USE TEMP B-TREE FOR ORDER BY

Two rows side by side that each read a table are a nested-loop join — the first is the outer loop, each later one runs once per row of those before it — but SQLite lists them as flat siblings. They are gathered under a "Nested loop" step here, because "these two are one join, outer first" is exactly what a reader of the flat list has to know and is easy to miss. Nothing else is restructured: the temp B-trees stay where SQLite put them.

func (*Plan) FileName

func (p *Plan) FileName(ext string) string

FileName is the name a saved or downloaded rendering of the plan takes: plan-<conn>-<yyyymmdd-hhmmss>.<ext>, or plan-<stamp>.<ext> with no connection.

func (*Plan) Finalize

func (p *Plan) Finalize()

Finalize numbers the steps, links parents, derives every computed field and runs the insight rules. Parsers call it last; it is idempotent, so a caller that edits a plan (the db layer adding table sizes) calls it again.

Time is the subtle part. Engines report a step's time as an average per loop, INCLUSIVE of its children, so:

TotalMs(n) = ActualMs(n) × Loops(n)        (÷ workers inside a Gather)
SelfMs(n)  = TotalMs(n) − Σ TotalMs(child)  clamped at 0

The clamp matters: a Postgres InitPlan, a CTE referenced twice, or a parallel child whose per-worker average does not add up can make the naive difference negative, and a negative bar is worse than a slightly generous one.

func (*Plan) Findings

func (p *Plan) Findings() (crit, warn int)

Findings counts the plan's critical and warning insights — the ones worth acting on; info-level notes are not counted.

func (*Plan) HTML

func (p *Plan) HTML() (string, error)

HTML renders the plan as a self-contained interactive page: a tidy-tree graph of the steps with heat-colored cards and data-flow edges, a flame (icicle) view, the insights with copyable fixes, and a detail panel for the selected step. It references nothing on the network — no CDN, no web font — so it opens the same from a temp file, an email attachment, or an air-gapped machine.

The plan travels as the Document JSON (see JSON) inside a <script type="application/json"> block, parsed by the page at load. That keeps one source of truth for what a plan contains: the page reads the same document `dbc explain -t json` prints, weights included, so its bars and blocks are sized exactly as the terminal's.

func (*Plan) Headline

func (p *Plan) Headline() string

Headline is the plan's one-line summary: engine, how it was obtained, the totals, the size.

func (*Plan) JPEG

func (p *Plan) JPEG(opt PictureOptions) ([]byte, error)

JPEG renders the picture as a JPEG. Quality 90 keeps text edges clean; the flat fills compress well enough that higher buys nothing visible.

func (*Plan) JSON

func (p *Plan) JSON() ([]byte, error)

JSON renders the plan as an indented Document.

func (*Plan) Mermaid

func (p *Plan) Mermaid() string

Mermaid renders the plan as a Mermaid flowchart — the form a plan takes in a GitHub or GitLab comment, a wiki page, or a Notion doc, which draw a ```mermaid block as a diagram without anyone installing anything.

flowchart BT                        ┌───────────────┐
  n0["Hash Join<br/>…"]             │ n0 Hash Join  │  ← the root on top
  n1["Seq Scan · orders<br/>…"]     └───────▲───────┘
  n1 -->|"36.7k rows"| n0                   │ 36.7k rows
                                    ┌───────┴───────┐
                                    │ n1 Seq Scan   │
                                    └───────────────┘

WHY BOTTOM-TO-TOP. Every other view draws the root on top and the data flowing up into it. In Mermaid an edge A --> B puts B after A in the flow's direction, so "BT" with child --> parent gives the same picture — root on top, arrows pointing the way the rows go — without the reversed arrowheads a TD chart would need.

WHAT A NODE SAYS. The same lines the graph's cards carry: the operation and what it works on, the step's most telling detail, rows, and its share of the metric the plan is best measured by. Heat and findings become classDefs (see mermaidClass) so a renderer that draws colors shows where the time goes, and one that does not still has the words.

ESCAPING. Labels are double-quoted, and inside one Mermaid reads its own entity codes: #quot; for a quote, and #<decimal>; for anything else. A plan's text is the user's SQL — full of quotes, <, >, # and | — so every character Mermaid or its HTML labels could read as syntax is written as a code (see mermaidText). The result never breaks the chart, whatever the statement holds.

func (*Plan) Metrics

func (p *Plan) Metrics() []Metric

Metrics lists the measures this plan can be viewed by, best first.

func (*Plan) Node

func (p *Plan) Node(id int) *Node

Node returns the step with the given ID, or nil.

func (*Plan) Nodes

func (p *Plan) Nodes() []*Node

Nodes returns every step in preorder — a parent before its children, children in the order the engine listed them — so Nodes()[i].ID == i.

func (*Plan) PDF

func (p *Plan) PDF(opt PictureOptions) ([]byte, error)

PDF renders the picture (see picture.go) as a one-page PDF, the page sized to the picture so nothing is scaled or split: a wide plan makes a wide page, which a viewer fits to its window and a printer to its paper.

WHY A RASTER INSIDE A PDF, NOT VECTOR PDF. A vector page would need the text laid out a second time with PDF's own font machinery — embedding and subsetting the Go fonts as CID fonts to reach beyond Latin-1 — to draw the same picture the raster already is. At the 2× scale the picture is drawn at, the page is sharp on screen and on paper; what it gives up is selecting the text, and the statement and findings are one "copy as text" away in every dbc view. So the PDF is a thin, dependency-free wrapper, written by hand below, around the tested renderer.

The file, object by object:

1 Catalog ─► 2 Pages ─► 3 Page ─┬─► 4 content stream: "q W 0 0 H 0 0 cm /Im0 Do Q"
                                └─► 5 Image XObject (RGB, Flate + PNG "Up" predictor)
6 Info (Title, Creator, CreationDate)
xref table, trailer

The image is Flate-compressed after the PNG "Up" filter (each row stored as its difference from the row above). The picture is mostly flat fills, so most rows are all zeros after the filter and compress to almost nothing; plain Flate on raw pixels is several times larger.

func (*Plan) PNG

func (p *Plan) PNG(opt PictureOptions) ([]byte, error)

PNG renders the picture as a PNG: lossless, and for a picture of flat fills and text usually smaller than the JPEG too.

func (*Plan) Picture

func (p *Plan) Picture(opt PictureOptions) (*image.RGBA, error)

Picture draws the plan. See the top of this file for what is on it.

func (*Plan) ResolveAliases

func (p *Plan) ResolveAliases(stmt string)

ResolveAliases fills in the table behind each step that names only an alias, from the statement's FROM and JOIN clauses, then re-runs the rules so their suggestions can name real tables.

func (*Plan) Share

func (p *Plan) Share(n *Node, m Metric) float64

Share is the step's Self as a fraction (0–1) of the whole plan's.

func (*Plan) Text

func (p *Plan) Text(o TextOptions) string

Text renders the plan as an indented tree with a column of numbers and a bar per step — the headless `dbc explain` output, the "copy plan as text" clipboard payload, and what the assistant is shown.

Hash Join · (o.user_id = u.id)             36,650 rows          8.4 ms  31% ███▏
├─ Bitmap Heap Scan · orders o             50,000 rows          6.2 ms  23% ██▎
│  └─ Bitmap Index Scan · orders_status    50,000 rows          0.9 ms   3% ▎
└─ Hash                                    15,659 rows          2.3 ms   9% ▉

The bar and percentage are each step's OWN share (self time, self cost), so they add up to 100% down the column and point straight at the step that matters; the tree lines carry the nesting.

func (*Plan) WriteFile

func (p *Plan) WriteFile(dir, ext string, data []byte) (string, error)

WriteFile saves one rendering of the plan (the page, a PDF, a JPEG, the Mermaid source) into dir under the same what-and-when name WriteHTML uses, with ext as its extension, and returns its path. 0600 for the reason WriteHTML gives: every rendering quotes the statement.

func (*Plan) WriteHTML

func (p *Plan) WriteHTML(dir string) (string, error)

WriteHTML saves the interactive page into dir (created if need be) under a name that says what and when — plan-<conn>-<yyyymmdd-hhmmss>.html — and returns its path. The file is 0600: a plan quotes the statement and its literals, which are the user's business, and dir is usually the shared temp directory.

type Prop

type Prop struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

Prop is one key/value line of a step's detail.

type Severity

type Severity string

Severity grades an insight.

const (
	SevInfo Severity = "info"
	SevWarn Severity = "warn"
	SevCrit Severity = "crit"
)

func (Severity) Glyph

func (s Severity) Glyph() string

Glyph is the one-cell marker the views draw for a severity.

type TextOptions

type TextOptions struct {
	Width    int    // total line width; 0 = 100
	Metric   Metric // "" = the plan's best
	Color    bool   // ANSI colors, for a terminal
	Insights bool   // append the findings
}

TextOptions shape Text.

type TreeLine

type TreeLine struct {
	Node   *Node
	Prefix string // "│  ├─ " and friends
}

TreeLine is one step with the tree-drawing prefix that goes before it.

func TreeLines

func TreeLines(root *Node) []TreeLine

TreeLines flattens the tree in preorder with box-drawing prefixes, the way `tree` draws a directory — each child's line says whether more siblings follow (├─) or it is the last (└─), and the columns to its left carry its ancestors' continuations (│).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL