knowledge: two-arm EXPLAIN (ANALYZE) attribution when the fast arm never executed (2 folded into #73/#52) - #74
Open
dch0202-rsquare wants to merge 3 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Knowledge flush — 3 insight(s)
Queue drained: 3 pending candidates from 2 session files
(
ab5516dc-…×2,f0eef7c4-…×1). Outcome: 1 ingested here, 2 folded intoopen PRs, so this PR carries a single new page.
Verified best-practice
C1 — attributing a query cancellation to the access-technology layer that emitted it
Claim.
ERROR: canceling statement due to user requestdoes not name itssender; identify the layer by matching the statement's measured duration against
each timeout configured in the application, and read a duration that matches no
configured value and varies run to run as "no timeout applies on this path".
Sources checked and what they establish.
https://github.com/postgres/postgres/blob/083ac033419f690758508e08c1736089384bbee8/src/backend/tcop/postgres.c— fetched and read directly.ProcessInterruptsemits four cancellation messages:"canceling statement due to lock timeout","canceling statement due to statement timeout","canceling autovacuum task", and — as the fall-through, guarded byif (!DoingCommandRead)—"canceling statement due to user request". ("canceling statement due to conflict with recovery"is not one of them: it is emitted byProcessRecoveryConflictInterruptwithERRCODE_T_R_SERIALIZATION_FAILURE.) Lock timeout usesERRCODE_LOCK_NOT_AVAILABLE; statement timeout and user request both useERRCODE_QUERY_CANCELED(57014), so SQLSTATE cannot separate a server-side timeout from an external cancel and the message text must.https://mybatis.org/mybatis-3/configuration.html—defaultStatementTimeout: "Sets the number of seconds the driver will wait for a response from the database"; valid values "Any positive integer"; default "Not Set (null)".https://github.com/mybatis/mybatis-3/blob/master/src/main/java/org/apache/ibatis/executor/statement/BaseStatementHandler.javaand.../StatementUtil.java— fetched and read.setStatementTimeoutresolvesmappedStatement.getTimeout(), elseconfiguration.getDefaultStatementTimeout(), callsstmt.setQueryTimeout(queryTimeout)when either is non-null, thenStatementUtil.applyTransactionTimeout(...), which — given a non-null transaction timeout — lowers the statement to it wheneverqueryTimeoutisnull, is0(JDBC's "no limit" sentinel), or exceeds the transaction's remaining time. MyBatis therefore bounds a statement with noConnectionHolderin the picture — the exact contrast withJdbcTemplate, whoseDataSourceUtils.applyTimeoutapplies the transaction timeout only "if any" and otherwise falls back to aqueryTimeoutdefaulting to-1.Confidence: verified (framework source read from the actual files and now
cited by commit SHA —
REL_17_STABLEis a branch, not a tag, and the MyBatisfiles were first read at
master, so the citations were re-pinned to083ac03/273ec65after the cross-check flagged the "pinned" wording asfalse; the measured durations are field evidence from one production service).
C2 — a two-arm
EXPLAIN (ANALYZE)comparison whose fast arm never executed the pipelineClaim. Before attributing a slowdown to factor X from a two-arm plan
comparison, check the fast arm for
never executed; if its expensive subtreenever ran, X and "did rows flow" both moved and neither is attributable.
Sources checked.
https://github.com/postgres/postgres/blob/083ac033419f690758508e08c1736089384bbee8/src/backend/commands/explain.c(lines 1841–1888) — fetched and grepped. The(actual time=… rows=… loops=…)line is emitted only underif (es->analyze && planstate->instrument && planstate->instrument->nloops > 0); theelse if (es->analyze)branch appends" (never executed)"only inEXPLAIN_FORMAT_TEXT, and in other formats emitsActual Rows/Actual Loopsof0unconditionally plusActual Startup Time/Actual Total Timeof0.0only whenes->timing. Two consequences the page now states precisely: the marker is not a biconditional onnloops == 0(thatelse ifalso fires whenplanstate->instrumentis NULL), and a JSON/YAML harness must key onActual Loops == 0rather than on the time, which may be absent.https://www.postgresql.org/docs/current/using-explain.html— checked; it does not documentnever executedat all (its only nearby text coversSubplans Removedfor partition pruning).https://www.postgresql.org/docs/current/ddl-partitioning.html— the one official page that does name it, found via the cross-check: "Some may be shown as(never executed)if they were pruned every time." Added to the page's sources so the claim rests on docs as well as on C source.Correction applied to the harvested candidate. The candidate's directive said
to recover a client-cancelled arm's duration from
pg_stat_statements.total_time.That is wrong on two counts and was rewritten before ingest:
https://www.postgresql.org/docs/current/pgstatstatements.html— statistics are updated "at their respective end phase, and only for successful operations". A statement cancelled by the client (or bystatement_timeout) contributes nothing tocalls/total_exec_time. Corroborated by pganalyze ("called from theExecutorEndhook … no aborted or timed-out query metrics are stored") and by PostgreSQL BUG #14901 "Canceled queries missing from pg_stat_statements".total_exec_time(withcalls,mean_exec_time);total_timeis the pre-PostgreSQL-13 name.Second correction, from the adversarial cross-check. The rewrite initially
offered
auto_explainas a third recovery route. That is false for the samereason:
contrib/auto_explain/auto_explain.cinstallsexplain_ExecutorEndasExecutorEnd_hook(line 255) and logs only inside that function (lines 368–389),so a statement cancelled mid-execution raises
ERRORand never reachesExecutorEnd— the page would have offered, as an alternative to re-running, atool that only works if you re-run. Verified independently by reading
auto_explain.cfrom a local clone ofpostgres/postgresat083ac03.The page now directs recovery via re-running without the client deadline, with
pg_stat_activity.query_start(https://www.postgresql.org/docs/current/monitoring-stats.html— "Time when the currently active query was started") for reading the number
mid-flight, and records both the
pg_stat_statementsand theauto_explainblind spots as edge cases pointing at the shared
ExecutorEndcause.Confidence: verified (server source read from the file and cited by commit
SHA
083ac03; four official doc pages quoted; the 4-arm timings are fieldevidence, and the page states explicitly that the field record kept only a
binary "did rows flow", not per-arm row counts).
C3 — a mutation harness whose restore step re-stamps the original mtime
Claim. A byte-length-preserving mutation restored with
shutil.copy2leavesCPython's
(mtime, size)cache key unchanged, so a subsequent run loads theprevious iteration's bytecode.
Sources checked.
https://docs.python.org/3/library/shutil.html—copyfilecopies "the contents (no metadata)";copycopies data and permission mode, and "Other metadata, like the file's creation and modification times, is not preserved";copy2is "Identical tocopy()except thatcopy2()also attempts to preserve file metadata" and "usescopystat()to copy the file metadata". Socopy2is precisely the mtime-restoring member of the family.(mtime, size)invalidation mechanism was already verified and cited on the existing wiki page (docs.python.org/3/reference/import.html, PEP 552,py_compile), including a 2026-08-04 local reproduction.Confidence: verified for the
shutilbehaviour (official docs);field-tested for the batch-vs-solo GREEN/RED reproduction.
Existing-layer check
Routed via
INDEX.md→databases(the artifact under change is a SQLstatement and its plan) with a cross-read of
backendanddebugging.Pages read: databases-query-optimization-reading-execution-plans, backend-python-language-bytecode-cache-staleness, debugging-methodology-hypothesis-testing
Also read, from open PR branches, and therefore deliberately not in the line
above (the gate resolves that line against this checkout):
backend-java-jpa-raw-jdbc-inside-a-jpa-transaction, which PR #73 adds andwhich does not exist on
main;backend-common-reliability-timeouts-and-retries,which does exist on
mainand which #73 modifies; and PR #52's revision ofbackend-python-language-bytecode-cache-staleness. The fullwiki/databases/index.mdand the mergedwiki/qa/page list were scanned forcategory fit.
Overlaps and what was done.
databases-query-optimization-reading-execution-plansnever executedcontent and no two-arm attribution content. Per one-case-per-page it stays as-is; added arelated:link and one edge-case row routing onward, and the new page links backdebugging-methodology-hypothesis-testingInstead of; no conflictbackend-python-language-bytecode-cache-stalenessshutil.copy2was missing from its mtime-preserving list. Folded (see below)Conflicts flagged: none. Nothing in the merged layer contradicts these
directives.
Related links added both ways:
reading-execution-plans⇄comparing-two-execution-plans.One link the new page originally carried,
qa-deliverables-quantitative-claims-in-a-published-document, was removed:it exists only on PR #51 and would have been dangling on
main.Open-PR check
Listed with
gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"→ 17 open heads (#73, #72, #69, #68, #66, #64, #62, #61, #58, #57, #56, #55,
#52, #51, #50, #49, #47). Four are fork heads, so each was fetched as
refs/pull/<n>/headand diffed againstorigin/mainoverwiki/.backend/java/jpa/raw-jdbc-inside-a-jpa-transaction.md— same production incident, carrying the same measured durations (10,012 / 151,558 / 163,489 ms)databases/indexing/{index-selection, trigram-index-short-patterns}anddatabases/index.md; no open head touchesdatabases/query-optimization/backend/python/language/bytecode-cache-staleness.mdFolds pushed (both heads are on this account's fork, so the additions land on
the branch under review rather than as a sibling PR):
knowledge/dch0202-rsquare-co-kr-20260810-163633, commit "knowledge: fold MyBatis timeout layer + duration fingerprinting into raw-jdbc page" (+12/−1). Adds three edge-case rows (MyBatis as a third access technology bounded independently of the holder chain; duration-fingerprinting against configured values;user requestvsstatement timeoutsharing SQLSTATE 57014), oneInstead ofrow, three sources and the 120,010 ms MyBatis measurement. Body 91 → 99 lines (limit 120).knowledge/dch0202-rsquare-20260807-100149, commit "knowledge: fold shutil.copy2 mtime-restore case into bytecode-cache-staleness" (+5/−1). Adds theshutil.copy2edge-case row with thecopyfile/os.utimereplacement, theshutilsource, and the batch-vs-solo reproduction. Body 63 → 66 lines.No sibling duplicate PR was opened for either.
Routing decision
databases/query-optimization→ new pagecomparing-two-execution-plans.md(iddatabases-query-optimization-comparing-two-execution-plans)databasesowns it overdebugging.query-optimizationalready holdsreading-execution-plans; the new case — attributing a difference between two plans — is a distinct trigger from reading one plan, soAGENTS.mdrule 1 (one case per page) makes it a new page rather than a mergebackend/jpabackend/languageNo new category was created.
query-optimizationcovers the case;databases/index.mdgained the routing line, and
log.mdgained oneingestand onededupentry.Page checks on the new page: 84 body lines (limit 120); no banned vague
qualifiers in directives; all
related:ids and inline[page-id]referencesresolve against this checkout.
Cross-Check
Cross-Check: independent adversarial review (headless
claudeCLI, separateprocess, instructed to refute and to verify every claim against primary sources)
returned BLOCK on the first pass and its findings were applied in full before
this PR was opened.
The reviewer had no network tools available, so it verified by blobless shallow
clone and read from disk —
postgres/postgres@083ac033,mybatis/mybatis-3@
273ec650,python/cpython@219768ff, plus PostgreSQL's owndoc/src/sgml/sources andREL_12_STABLE/REL_13_STABLEfor the versionboundary. That is a stronger substrate than the WebFetch summaries used on the
first pass, and it is what caught the
es->timingguard.What it confirmed: the
never executedmechanism, thepg_stat_statementssuccessful-operations-only rule, the
total_time→total_exec_timeboundary atPostgreSQL 13, the shared SQLSTATE 57014, the MyBatis precedence chain, the
shutilmetadata semantics, the 18.05× arithmetic, zero dangling page ids, andevery mechanical
AGENTS.mdrule.What it broke, and what changed as a result:
auto_explaincannot capture a cancelled statement (ExecutorEnd-only), yet the page recommended itExecutorEndcausees->timingcorrection was still uncommitted;HEADcarried the wrong wordingREL_17_STABLE(a branch) andmasteros.utime, which the same page shows is insufficient.pyc/ unconditional purge)⟺ nloops == 0overstated (the branch also fires wheninstrumentis NULL)log.md, and herenever executedexistedddl-partitioning.htmladded to sourcesStatementUtil.java; the narrowing description dropped thenull/0armsmain)Not fixed, and why:
raw-jdbc-inside-a-jpa-transaction.mdcites pgjdbc atblob/master/. That citation is pre-existing content of PR #73, not part of thisfold, so it is left for that PR's own review rather than rewritten here.
The reviewer had no network, so it could not check the 17-open-head list or the
fold pushes. Those were re-verified with
ghon the pass that opened this PR:gh pr list --state openreturns exactly 17knowledge/*heads; a per-PRfile scan of every open PR returns no head touching
databases/query-optimization/, so C2'snewverdict holds; and both fold headscarry their fix commits on the fork
(
…-20260810-163633=b3e7d3b,…-20260807-100149=b71d931).One defect found on that pass and fixed here.
log.md's newingestentrystill ended its recovery list with "or
auto_explain" — the exact claim theHIGH finding removed from the page — while the same entry went on to explain
that
auto_explainshares theExecutorEndblind spot. A reader of the logwould have taken the retracted advice. The clause now names only the two working
routes. Independently re-confirmed before the fix:
auto_explain.cat083ac03has its single
ereport(line 431) insideexplain_ExecutorEnd, which is thelast function in the file, so there is no non-
ExecutorEndlogging path.Decision Log
Intent. Drain the three queued
★ Insightcandidates into the wiki as onereviewed PR, with the open-PR check applied first so that sibling flushes do not
pile up duplicate pages (the failure mode recorded in #39 and in the #17–#40 and
#42/#43 consolidations).
Decisions and the alternatives rejected.
databases/query-optimizationreading-execution-plansAGENTS.mdrule 1: that page's case is reading one plan; attributing a difference between two is a separate trigger. Merging would have pushed a 60-line page past its case boundary(mtime, size)mechanism. A sibling page would have been the exact duplicate this skill's step 2b′ exists to preventpg_stat_statements[추정]— the harvested number was most likely recovered from a later completed run and mis-attributed to the cancelled one at write-up timerelated:toqa-deliverables-quantitative-claims-in-a-published-documentremovedmain; if #51 is rejected or renamed the link dangles. Cross-PR links wait until the target mergesWhere a reviewer should look hardest.
comparing-two-execution-plans.mddirective 4 and the twoExecutorEndedge-case rows — this is the part that was wrong twice and rewritten twice.reading-execution-plans(the oneAGENTS.mdjudgment call here).b3e7d3bon knowledge: captured-call argument completeness, pg_trgm short-pattern degeneration, raw JDBC inside a JPA transaction #73,b71d931on knowledge: 4 verified insights — surviving-mutant triage, source-text wiring assertions, query state vs fetch state, python text-io encoding #52.