perf: cache field parsers and prebuilt rows per query shape - #3753
Open
Iaotle wants to merge 1 commit into
Open
Conversation
Iaotle
force-pushed
the
perf/cache-field-metadata-per-query-shape
branch
from
August 18, 2026 11:10
0167057 to
9c67c03
Compare
Iaotle
marked this pull request as ready for review
August 18, 2026 11:21
Real workloads repeat a small set of result-set shapes, so addFields rebuilding the parser array and prebuilt empty row on every query is measurable overhead at high query rates. Cache both per shape, keyed on the types object, invalidated by a version counter that setTypeParser bumps. Only versioned types objects (TypeOverrides backed by the global pg-types module, or the module itself) use the cache; user-supplied types objects keep the uncached path. ~2.6x faster result processing for a cached 20-column single-row shape (7.3 -> 2.9 us per query in a addFields+parseRow microbenchmark).
Iaotle
force-pushed
the
perf/cache-field-metadata-per-query-shape
branch
from
August 18, 2026 11:29
9c67c03 to
2b4e8d7
Compare
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.
What
Result.addFieldscurrently callsgetTypeParserper column and rebuilds the prebuilt empty row object on every query. Real workloads repeat a small set of result-set shapes, so this work is almost always identical to what the previous query already computed. In a production Node service we profiled (~700–800 queries/s through pg + an ORM),addFields+getTypeParserself-time added up to ~9.6% of the main thread's on-CPU time — pure per-query overhead, since most queries return a single row.This PR caches the parser array and prebuilt row per query shape:
WeakMap), then on a shape signature ofname/dataTypeID/formatper field (NUL-separated — Postgres identifiers cannot contain NUL).TypeOverrides.setTypeParserbumps, soclient.setTypeParser(...)between queries applies to later queries exactly as before (unit-tested).TypeOverridesinstances backed by the global pg-types module (the standardClientpath — client.js injects itsTypeOverridesinto everyResult), and the global module itself. User-suppliedtypesobjects (per-querytypes, orTypeOverrideswrapping customuserTypes) keep the existing uncached path, since theirgetTypeParserbehavior can change without notice.Microbenchmark (
new Result+addFields+ oneparseRow, 20 columns, Node 26): 7.3µs → 2.9µs per query, ~2.6× faster result processing on repeated shapes.Global setTypeParser coverage
Registrations on the global module —
pg.types.setTypeParser(...),defaults.parseInt8, or a directrequire('pg-types')— all mutate the same module object, so itssetTypeParseris wrapped once (in result.js, at first pg load) to bump the same version counter. TheparseInt8integration test, which toggles the global parser between same-shape queries, passes. The one remaining bypass is code that saved a reference to the unwrappedsetTypeParserbefore pg was first required and calls it after queries have run; a version counter inside pg-types itself would close even that, and I'm happy to send that one-liner to brianc/node-pg-types as a follow-up if you want it.Tests
packages/pg/test/unit/client/field-metadata-cache-tests.js: shape reuse, distinct shapes, name/oid/format keying,setTypeParserinvalidation, per-instance isolation, custom-types bypass (both flavors), and a client-level test thatclient.setTypeParserbetween two same-shape queries applies to the second.pg.types.setTypeParserbetween two same-shape queries applies to the second.getaddrinfo ENOTFOUND localhost— fail identically on unmodified master).rowMode: 'array', named prepared statements, and livesetTypeParserinvalidation in both row modes.