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
- Variables
- func Aliases(stmt string) map[string]string
- func Bar(frac float64, cells int) string
- func Compare(before, after *Plan) (text string, good bool)
- func Engine(d string) string
- func FmtCost(c float64) string
- func FmtCount(v float64) string
- func FmtFactor(f float64) string
- func FmtKB(kb float64) string
- func FmtMetric(m Metric, v float64) string
- func FmtMs(ms float64) string
- func FmtRows(v float64) string
- func PlanDir() string
- func RowsCell(n *Node, analyzed bool) (rows, factor string)
- func SameSubject(a, b *Plan) bool
- func ScriptHash() string
- func Strip(stmt string) (inner string, analyze, ok bool)
- func TruncateCells(s string, w int) string
- func WrapWords(s string, w int) []string
- type Document
- type Insight
- type Kind
- type Metric
- type Node
- func (n *Node) HasFlag(f string) bool
- func (n *Node) Inclusive(m Metric) float64
- func (n *Node) Parent() *Node
- func (n *Node) Prop(key string) (string, bool)
- func (n *Node) RowsExamined() float64
- func (n *Node) Self(m Metric) float64
- func (n *Node) Summary() string
- func (n *Node) Target() string
- func (n *Node) Title() string
- type PictureOptions
- type Plan
- func Detect(r *model.Result, driver string) (*Plan, bool)
- func ParseMySQLTable(cols []string, rows [][]string) (*Plan, error)
- func ParseMySQLTree(text string) (*Plan, error)
- func ParsePostgresJSON(data []byte) (*Plan, error)
- func ParsePostgresText(lines []string, engine string) (*Plan, error)
- func ParseSQLite(cols []string, rows [][]string) (*Plan, error)
- func (p *Plan) FileName(ext string) string
- func (p *Plan) Finalize()
- func (p *Plan) Findings() (crit, warn int)
- func (p *Plan) HTML() (string, error)
- func (p *Plan) Headline() string
- func (p *Plan) JPEG(opt PictureOptions) ([]byte, error)
- func (p *Plan) JSON() ([]byte, error)
- func (p *Plan) Mermaid() string
- func (p *Plan) Metrics() []Metric
- func (p *Plan) Node(id int) *Node
- func (p *Plan) Nodes() []*Node
- func (p *Plan) PDF(opt PictureOptions) ([]byte, error)
- func (p *Plan) PNG(opt PictureOptions) ([]byte, error)
- func (p *Plan) Picture(opt PictureOptions) (*image.RGBA, error)
- func (p *Plan) ResolveAliases(stmt string)
- func (p *Plan) Share(n *Node, m Metric) float64
- func (p *Plan) Text(o TextOptions) string
- func (p *Plan) WriteFile(dir, ext string, data []byte) (string, error)
- func (p *Plan) WriteHTML(dir string) (string, error)
- type Prop
- type Severity
- type TextOptions
- type TreeLine
Constants ¶
const ( Postgres = "postgres" MySQL = "mysql" SQLite = "sqlite" Bytdb = "bytdb" )
Engine names, as the config's driver names them.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
Engine maps a config driver name (postgres, pg, sqlite3, mariadb, …) onto an engine constant.
func FmtFactor ¶
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 FmtMs ¶
FmtMs formats milliseconds at a precision that suits the size: 840 µs, 21.6 ms, 3.42 s, 2m05s.
func FmtRows ¶
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 ¶
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 ¶
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 ¶
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 ¶
TruncateCells shortens s to at most w terminal cells, ending in "…".
Types ¶
type Document ¶
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" )
type Metric ¶
type Metric string
Metric is what a bar, a percentage, or a flame block's width measures.
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"`
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) Inclusive ¶
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) RowsExamined ¶
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 ¶
Self is the step's own share of metric m — what it costs excluding its children. Always ≥ 0.
func (*Node) Summary ¶
Summary is the single most telling detail of the step, for the tree's inline hint.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Findings counts the plan's critical and warning insights — the ones worth acting on; info-level notes are not counted.
func (*Plan) HTML ¶
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 ¶
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) Mermaid ¶
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) Nodes ¶
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 ¶
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) 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 ¶
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 ¶
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 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 ¶
TreeLine is one step with the tree-drawing prefix that goes before it.