Skip to content

pqn reference

Facts about the pqn extension and its terminal tool, for lookup. To try it, see the quick start; to install it on your own server, see Install the pqn extension. pqn runs against PostgreSQL directly and needs no PgQueryNarrative server. It is not the pgquerynarrative CLI, and it is a different extension from pgquerynarrative, which calls the REST API.

The tool

pqn <command> [flags]. It logs in as you and stores no secret. Flags may come before or after the statement. A statement that contains a word starting with - goes in quotes, or after --; pqn run SELECT -1 and a -- comment after a word are read as SQL.

Command What it does Flags it reads
doctor Runs pqn_api.verify_setup(). Exits 1 on any blocking finding
top The statements that cost the most, from pg_stat_statements -n
plan Estimated plan and findings for one statement. Reads only, keeps nothing. A statement that writes cannot be planned: plan its SELECT statement, --bind
run Runs one read-only statement over the views you may see statement, -n
investigate Plans it, names what is wrong, proposes fixes from the plan and proves them statement, --queryid, --bind, --title, --no-record, --max-candidates, --max-proofs
prove Proves a rewrite you wrote: same rows, then faster --before, --after, --bind, --title, --no-record, --id
investigations Your recorded investigations -n
evidence <id> The evidence recorded for one investigation --id
version
Flag Meaning
--dsn Primary server. Default $PQN_DSN, then the libpq environment (PGHOST, PGUSER, PGSERVICE, ~/.pgpass)
--replica Optional second server. Default $PQN_REPLICA_DSN. Reads (plan, run, measuring) use it when set; the ledger and the workload statistics use the primary
--json Print JSON with snake_case field names
--timeout Give up after this long. Default 10 minutes
-n Rows to show (top, run, investigations). Default 20
statement --sql, --file, or the words after the command. Give it once; giving it twice is an error
--bind A value for $1, $2, … Repeat the flag once per placeholder. Without binds a statement with placeholders is planned but not executed
--queryid Investigate the statement pqn top showed under this id
--title A title for the ledger
--no-record Write nothing to the ledger
--max-candidates Most proposals to consider. Default 5
--max-proofs Most proposals to measure. Default 3
--before, --after The original and the rewritten statement (prove)
--id The investigation to add the proof to (prove), or to show (evidence)

Exit codes. 0 done; for investigate and prove it means a proposal was proven. 2 nothing was proven (the rewrite returned different rows, or was not fast enough). 1 an error, including a refused statement.

Verdicts

Verdict Rule
Proven Same rows (an order-independent fingerprint of every row) and at least 1.2 times faster
NotFaster Same rows, not 1.2 times faster
Different The rows differ. Never an improvement, however fast
Unverified Not compared: $n placeholders, a statement timeout, index DDL (review only), or both statements returned no rows. Two empty results are equal whatever the statements do

A verification on today's data, not a mathematical proof: the fingerprint is 128 bits (two independent 64-bit hashes), so agreement is probabilistic and not built to resist someone crafting colliding rows. See Verify result equivalence, which uses the same fingerprint. Times are server-side, planning plus execution, and are the fastest of two rounds that alternate the two statements. A proof prove() computes carries "source": "database"; one stored with record_evidence, as the replica flow does, carries "source": "client" whatever its payload says.

SQL API

Everything is in schema pqn_api. Each group includes the ones below it: pqn_admin ⊃ pqn_analyst ⊃ pqn_viewer. Nothing is executable by PUBLIC.

Function Group Returns and notes
pqn_api.top(n = 20) analyst Statements by total time: queryid, query, calls, total_exec_time, mean_exec_time, rows
pqn_api.plan(query) analyst The estimated plan as JSON. Read only, and nothing it does is kept. A statement that writes (INSERT, UPDATE, DELETE, MERGE) is refused with a hint to plan its SELECT. $n placeholders need PostgreSQL 16
pqn_api.run(query, row_limit = 100) analyst {rows, columns, truncated}. One read-only statement over the exposed views; row_limit is clamped to 1–10000, and the answer is cut at 16 MB (truncated is then true). A column name that repeats gets a _2, _3 suffix. Makes the rest of its transaction read only
pqn_api.findings(plan) analyst Findings from a plan, as rules over the plan JSON
pqn_api.measure_pair(a, b, repeats = 2) analyst {equal, before, after, speedup, rounds}. Both fingerprints in one snapshot; repeats is clamped to 1–5. Refuses $n. Runs the statements read only, so one that writes fails, and leaves your transaction writable
pqn_api.investigate(query, title, queryid) analyst Plans, finds, and records a new investigation. investigation_id and findings in the result
pqn_api.prove(investigation, before, after, note) analyst The verdict, both plan costs and the measurement, recorded as a proof
pqn_api.record_investigation(query, queryid, title) analyst The new investigation's id. Each person may keep 256 MiB and 20000 rows in the ledger, counting record_evidence, investigate and prove too; past that the write is refused until an administrator removes old investigations
pqn_api.record_evidence(investigation, kind, payload) analyst The new evidence id. kind is lowercase letters and underscores, at most 40; the payload at most 1 MiB. Your own investigation only; an admin may write to anyone's
pqn_api.investigations(limit = 50) viewer Your investigations; an admin sees everyone's
pqn_api.evidence(investigation) viewer Its evidence, under the same rule
pqn_api.verify_setup() admin One row per check: level (BLOCK, WARN, INFO, OK), check_name, detail, fix
pqn_api.init() admin Creates the ledger, the exposure registry and the limits registry. Safe to repeat
pqn_api.expose(table, columns, view_name, scope = 'view'), pqn_api.expose_sql(…) admin Creates the view and registers it; expose_sql prints the statements instead. scope is view or full
pqn_api.unexpose(view_name), pqn_api.exposed() admin Removes a view and leaves pqn_owner exactly the access the remaining views on that table need; lists what is exposed
pqn_api.enroll(login, group = 'analyst', stmt_timeout = '15s'), pqn_api.enroll_sql(…) admin Puts a login into viewer, analyst or admin with limits; enroll_sql prints the statements. The timeout needs a unit: 15s, 500ms or 2min
pqn_api.enforce_limits() admin Cancels sessions that outlived their enrolled statement, lock or idle-in-transaction limit. Needs a superuser, or pg_read_all_stats and pg_signal_backend. A superuser's session cannot be cancelled without being one: it is reported with cancelled = false and the pass goes on
pqn_api.record_limit(login, stmt_ms, lock_ms = 2000, idle_ms = 10000) admin Called by the enroll script

explain_ms, exposed_path, has_params and ledger_room are helpers that only the extension's own roles can execute.

The Group column is who may execute a function. Some admin functions also change roles and grants, and a caller that is not a superuser needs those rights too. Membership in pqn_admin alone is enough for the read-only ones:

Function What a non-superuser needs beyond pqn_admin
verify_setup(), exposed(), record_limit(), expose_sql(), enroll_sql() Nothing. The two _sql functions only print statements
init() Membership in pqn_owner and pqn_ledger: the installer, not a day-to-day administrator
expose(), unexpose() Membership in pqn_owner with SET, and the right to grant on the table and its schema (own them, or hold GRANT OPTION). Otherwise run the printed script as someone who has it
enroll() CREATEROLE, ADMIN OPTION on the group being granted, and ADMIN on the login (a login the administrator created has it). Otherwise run the printed script as someone who has it
enforce_limits() A superuser, or membership in pg_read_all_stats and pg_signal_backend

A superuser needs nothing extra. Install the pqn extension shows how to set up a non-superuser administrator.

Roles and tables

Role Kind Holds
pqn_viewer, pqn_analyst, pqn_admin Group, granted to people The function grants above
pqn_owner NOLOGIN owner The curated views, plan, measure_pair, the registries. Reads the exposed columns, or the whole table for scope full
pqn_reader NOLOGIN owner run. Reads only the curated views
pqn_stats NOLOGIN owner top. Member of pg_monitor
pqn_ledger NOLOGIN owner The ledger, investigate, prove and the record_* functions
Object What it is
pqn.<view> A curated view over one exposed table, with the columns a DBA chose
pqn.exposed The exposure registry: schema, table, view, scope, columns
pqn.limits The statement timeout each person was enrolled with, where they cannot reach it
pqn_ledger.investigations, pqn_ledger.evidence The audit ledger, attributed to the real login. DROP EXTENSION keeps it
pqn_ledger.schema_migrations The ledger's version

Scope full lets planning and measuring see every column of the table (row counts can then reveal whether a value exists), while run still returns only the listed columns; pqn doctor warns about it. Limits, and why they are enforced from outside the session, are in Enforce the limits. The roles that own things, and why the extension holds no superuser privilege, are in Database roles.

See also

Quick start · Install the pqn extension · Investigate a slow query · Verify result equivalence · PostgreSQL extensions