EXPLAIN (GENERIC_PLAN): plan a normalized statement, no bound values required #6
Loading…
Reference in New Issue
No description provided.
Delete Branch "hns33/examples:explain-generic-plan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
EXPLAINtoday wants a statement it can actually run. Give it a normalizedstatement instead — the form the plan cache keeps, with the literals stripped out
and replaced by
$1or a JDBC?— and analysis stops you cold withthere is no parameter $1. That is exactly the statement a DBA has in hand afterpulling a slow query out of
statement_history, and today the only way to see itsplan is to guess the original constants and paste them back in.
This series adds one option,
EXPLAIN (GENERIC_PLAN) <normalized SQL>, that plansthe statement the way openGauss plans it the first time it is prepared: the
parameters stay parameters, estimates come from the type's default selectivity, and
nothing is executed. The plan you see is the generic plan the engine would build
for that statement — not an approximation of it.
Nothing about ordinary
EXPLAINchanges. The feature is reached only through thenew option, and the
?spelling only lights up behind a GUC that ships off.Why it doesn't work today, and what each patch does about it
A normalized statement is unexplainable for three unrelated reasons. They are
independent, so the series fixes them independently.
Analysis will not type an unbound
$n. The plain analysis path has noparameter hooks, so
$1has no type and it errors. — On theGENERIC_PLANpath we install openGauss's own variable-parameter hooks
(
parse_variable_parametersbeforetransformTopLevelStmt,check_variable_parametersafter). These are the hooksPREPAREand theextended-query protocol already use, so
$1survives as aParamand its typeis inferred from context, the same way it would be at prepare time.
The planner would fold the parameter or refuse it. With no bound value there
is nothing to fold
$1into, and the default cursor options invite a customplan. — We plan with
CURSOR_OPT_GENERIC_PLANandboundParams = NULL. The flagalready exists in
parsenodes_common.hand the custom-vs-generic decisionalready honours it; with no values supplied the parameters cannot collapse to
constants and the result is a generic plan by construction.
?is not a parameter token in openGauss.?is an operator character(jsonb
?,?|,?&, hstore, geometry), so there is no "bare?→ parameter"rule to lean on. — One GUC-gated lexer rule maps a standalone
?to a positionalparameter, reusing the scanner's own placeholder counter. flex's longest match
keeps every multi-character
?-operator intact.Everything past analysis — rewrite, the planner, plan rendering,
$nprinting,every
FORMAT— is reused unchanged.Patch series
es.generic_planonExplainState; thegeneric_planoption keyword (explainOptionIsEnabled(stmt->options, "generic_plan"));has_qmark_paramonExplainStmtwithcopyfuncs/equalfuncssupport?placeholder and GUC?→positional-param rule inscan.l; scanner placeholder counter;?/$n/:namemixing detection; theenable_qmark_paramGUC and its backing field inknl_session_attr_sql.htransformExplainStmt; theANALYZE/GENERIC_PLANmutual exclusion;CURSOR_OPT_GENERIC_PLAN+NULLboundParams inExplainOneQuery; inferred parameter types carried onto theExplainState; the threeVERBOSEattribute linesexplain_generic_planadded toparallel_schedule; self-containedsql/+expected/filesThe split is deliberate: 0001 is inert scaffolding, 0002 is a lexer change that
stands on its own and stays dormant until its GUC is set, 0003 is the actual
behaviour, 0004 is the test. Each applies and builds on top of the previous.
The option and the GUC
GENERIC_PLANis an ordinary booleanEXPLAINoption, so it composes withVERBOSE,COSTS,FORMAT, and the rest. A repeated option takes its last value,so analysis and planning never disagree about whether this is a generic plan. It is
mutually exclusive with
ANALYZE/PERFORMANCE— a generic plan has no bound valuesand cannot be executed, so there are no real rows or timings to report. That
rejection is raised in
transformExplainStmtearly and again inExplainQuerydefensively, so both paths agree.
Turn it on and a standalone
?becomes a positional parameter for the currentsession. Off, the lexer is byte-for-byte what it was.
What it looks like
The
?form is the same plan; only the scanner input differs:Parameter Typesis worth calling out: variable-parameter analysis already producesthe inferred type vector and records it on the
Query, so underVERBOSEwe justprint it. An operator can confirm that
$2was read astextand not, say,integerdirectly, instead of reverse-engineering it from the plan. No extraanalysis cost.
Type inference
There are no new type rules here. Inference is whatever the variable-parameter hooks
already do for
PREPARE, which means the generic plan an admin sees is the genericplan the engine builds for the same statement. The short version:
col = $1,$1 = col,col IN ($1,$2),col BETWEEN $1 AND $2$1::TT(an explicit cast wins)f($1)INSERT ... VALUES ($1)SELECT $1,$1 IS NULL$1 = $2(both unknown) is not an error: openGauss coerces the unknown-unknowncomparison to
textand plans both astext, matching ordinary unknown-literalhandling. Parameters are dense — skip
$1and use$2and analysis errors, exactlyas a prepared statement would.
Compatibility and the one trade-off
The invasive-looking part is
scan.l, so be precise about its blast radius:enable_qmark_param = off(the default) there is no change to any session.A grep of the diff for behaviour outside the new rule turns up nothing; the rule
is guarded on the GUC.
jsonb ? textisshadowed by the placeholder rule for that session. This is the only semantic
trade-off and it is contained:
?|and?&are multi-character and flex'slongest match leaves them alone, and a session that needs single-char
?eitherleaves the GUC off or writes
jsonb_exists(col, 'key').?is a parameter only when it stands alone. Written tight against an operator(
col=?) flex fuses=?into one operator token — the same longest-match rulethat protects
?|/?&. Writecol = ?. This is spelled out indocs/OPERATIONS.md.?,$nand:namecannot be mixed in one statement; mixing is rejected throughthe scanner's existing placeholder-conflict detection, so we don't invent a second
numbering scheme.
The two
EXPLAINpaths that bypass direct-query planning are guarded so a plan thatis not generic can never be labelled
Generic Plan: true:EXECUTEruns a cachedplan the plan cache may resolve to a custom plan (rejected with a hint), and
CREATE MODELis planned with bound parameters (rejected).CREATE TABLE ASalreadyrequires
ANALYZEand so errors on the mutual exclusion first;DECLARE CURSORfalls through to the direct-query path and is planned generically.
Testing
Two layers, both in the tree.
Integration (
scripts/run_tests.sh, groups A–M against a running instance):the
?and$nspellings, tight-operatorcol=?handling, jsonb?/?|/?&disambiguation, the
ANALYZE+GENERIC_PLANrejection,?/$nmixing rejection,EXECUTEnot being mislabelled, and a full regression group (L1–L5) assertingthat ordinary
EXPLAIN/PREPARE/EXECUTEare untouched.Upstream regression (
explain_generic_plan, registered inparallel_schedule, run throughpg_regress --use-existing):Build/apply is scripted end to end:
scripts/apply_to_upstream.shapplies the fourpatches to a clean checkout,
scripts/build.shbuilds debug,scripts/run_regress.shruns the registered test. Logs are captured under
build/.Follow-ups I did not do here
?, soSET enable_qmark_param = onhas to happen in an earlier round-trip than the
EXPLAINit governs — you cannotset it and use
?in the same multi-statement string. That is a consequence ofwhere the rule lives, not a bug, but it surprises people; it is worth a line in the
client-driver docs if we ever wire this into a JDBC helper.
then run it with these values" that is a different feature and a different option,
not an
ANALYZEvariant of this one.Lineage
EXPLAIN (GENERIC_PLAN)for$nis the design PostgreSQL 16 settled on —variable-parameter inference,
CURSOR_OPT_GENERIC_PLAN, no bound values. openGaussinherits that machinery, so on that baseline this change is small: it adds the JDBC
?spelling behind a default-off GUC, and surfaces the inferredParameter Typesunder
VERBOSE. The jsonb?conflict is handled the only honest way — keep theGUC off by default and let longest-match protect the multi-character operators.
Step 1:
From your project repository, check out a new branch and test the changes.Step 2:
Merge the changes and update on Gitea.