Skip to content

ExplainSQL 0.1.1

Choose a tag to compare

@github-actions github-actions released this 06 Oct 13:31
· 33 commits to main since this release

Added

  • Ask the planner why it chose its plan: --why-not [TABLE] in connected
    mode, or y on a node in the viewer. The statement is planned again with
    the choice taken away (enable_seqscan = off, enable_nestloop = off) or
    with enough work_mem for a spill, and the plans are compared. The answer
    says whether an index can serve the condition at all and what keeps it
    out, how much more expensive the planner estimates the alternative, and,
    with --measure, whether the planner is right, or wrong because of a row
    misestimate or its cost settings. A cost setting such as
    random_page_cost = 1.1 is suggested only once the plan it leads to is
    measured better too. An existing index the planner did not use gets the
    reason found instead of the likely ones.
  • --measure and --runs N: measure each plan N times, after one run that
    only warms the cache, and compare the medians.
  • Comparisons report pages written to temporary files, and the JSON report
    says how two plans compare (change, basis).
  • explainsql diff BEFORE AFTER compares two plans of the same statement,
    node by node: scans that read their relation another way, joins with
    another method or order, other strategies, nodes that came or went,
    spills, misestimates, and nodes whose work changed beyond the noise, the
    most significant first, then the plan after with its changes marked. It
    matches nodes by the work they do, so partitions renamed by pruning or by
    another PostgreSQL version still match. Text, Markdown or JSON.
  • Plan shapes: an id for what makes a plan that plan, without its numbers,
    literal values or aliases, the same in JSON and text.
  • Reading every plan of an input: plans pasted one after the other, JSON
    arrays and documents, Markdown fences, psql results and log entries
    (parse_all). explainsql diff takes both plans from one input this way.
  • explainsql check, a gate for CI: plan files, or SQL files run against a
    database (-d), each checked against its findings (--fail-on) and
    against the plan locked for it in explainsql.lock (--update writes
    it). A plan fails when it is worse by pages, or by the estimated cost when
    not run; time alone, for the same pages, is a note. --strict fails any
    change of plan, and --prove tests the suggested fixes of the plans that
    failed. Exit codes 0, 1 and 2; text, Markdown, JSON or SARIF.
  • --fail-on SEVERITY: with a printed report, exit with 1 when a finding is
    at least that severe.
  • explainsql logs FILES: the plans auto_explain logged, and for each
    statement, which plans it got, when its plan changed, after how many runs
    and in which session, the median duration before and after, how the plan
    after compares and what changed, the costliest change first. Reads
    stderr logs with any line prefix, csvlog and jsonlog, plans in text or
    JSON. Statements are told apart by their query identifier, or by their
    text without literal values. A switch to a prepared statement's generic
    plan is named, with the values it ran with (PostgreSQL 16+) and the
    --params --bind command that tests it. sqlcommenter tags say where a
    statement comes from. --changed, --query, --trace, --since and
    --until narrow the report; text, Markdown or JSON.
  • parse_log: every auto_explain entry of a server log, with its time,
    process, user, database, application, duration, query identifier and
    parameters.
  • ES013, planner settings force the plan: the plan was made with an
    enable_* setting off, as left on in a session or set for a role or a
    database, or the planner used a node such a setting disables because it
    found no other way (Disabled: true from PostgreSQL 18, the disable cost
    before). Applications that plan with the defaults may get another plan.
  • I/O time, from plans captured with track_io_timing: among the
    statement's facts when it takes a tenth of the time or more, in the
    verdict when reading pages that were not in shared buffers took half of
    it (the cache was cold), and for each node in the viewer's details. It is
    compared with the time of every process, so that parallel plans read
    right.
  • --params in connected mode: how the plan of a statement with parameters
    ($1, or JDBC's ?) depends on their values. explainsql prepares the
    statement as an application does, tries values from the columns'
    statistics and common LIMIT and OFFSET row counts, and compares the
    custom plan each value gets with the generic plan, which PostgreSQL may
    switch to after five executions. With --measure, both plans run where
    they differ; the report says whether the generic plan does much worse for
    some value, whether PostgreSQL would switch to it, and what to do
    (plan_cache_mode = force_custom_plan, pgJDBC's prepareThreshold=0),
    and shows the generic plan with the values it does worst with. --bind N=VALUE gives a parameter's value.

Changed

  • ES003 and ES004 name the work_mem a spilled sort or hash needs, from
    what the plan shows, to set for the statement alone (SET LOCAL work_mem = '64MB' in its transaction), and say what it may take: each sort, hash
    and other operation that uses work_mem may take that much, in each
    process that runs it, in every session that runs the statement at once.
  • In connected mode, a run compared with the previous one says what changed
    in the plan, or that it is the same plan.
  • Before and after comparisons lead with pages, then temporary files, then
    time, and ignore differences under 10% (and 0.1 ms). Fewer pages but a
    slower run is a mixed result.
  • The README's demo is a recording of a real session: explainsql run
    against PostgreSQL on a slow query, asked why the planner uses no index,
    and testing the suggested index with HypoPG. cargo xtask demo --record
    records it again; cargo xtask demo draws it from the recording.

Fixed

  • Measuring a suggested index favored the index: the run without it often
    met a colder cache than the run with it, which followed the build that
    read the whole table, and any faster run counted as better. Both sides now
    run once first to warm the cache, pages decide before time, and a
    suggestion that is not better by more than the noise drops to low
    confidence.