Allow HOT updates when RUM index terms are unchanged (amcomparedatums, proposed PG API) by gburd · Pull Request #789 · documentdb/documentdb · GitHub
Skip to content

Allow HOT updates when RUM index terms are unchanged (amcomparedatums, proposed PG API) - #789

Open
gburd wants to merge 2 commits into
documentdb:mainfrom
gburd:hot-rum-amcomparedatums
Open

gburd wants to merge 2 commits into
documentdb:mainfrom
gburd:hot-rum-amcomparedatums

Conversation

@gburd

@gburd gburd commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Description

Lets updates to a document stay heap-only (HOT) when they leave the terms of
every index on the collection unchanged. This needs a PostgreSQL index AM
callback, amcomparedatums, proposed in CommitFest 5556 ("Expanding HOT
updates for expression and partial indexes",
https://commitfest.postgresql.org/patch/5556/). The callback is not in any
released PostgreSQL. On a server without it this PR compiles to a no-op:
the Makefile looks for the API in the server headers and builds the new code
only when it is there.

Every DocumentDB path index is a RUM index on the document column. Every
write changes that column, so today every update makes new entries in every
index, and leaves dead ones for VACUUM, even when the indexed paths did not
change. With amcomparedatums, PostgreSQL asks the index whether the new value
gives the same entries as the old. This PR implements it for the extended RUM
AM (rumcomparedatums in pg_documentdb_extended_rum/core/src/rumutil.c): it
extracts the terms of the old and new documents and compares them as RUM
compares them.

It returns false (not HOT) when:

  • the column stores per-item additional information (text indexes), or the
    index has attach / add-to columns;
  • the new document would set opclass metadata bits, such as a composite
    index's multi-key paths, that the old one did not set;
  • the index is an exclusion constraint, which is how DocumentDB builds unique
    indexes. A HOT update skips the constraint check, and the constraint
    compares values, not index entries (its terms are hashes). The proposed core
    patch does not call amcomparedatums for exclusion-constraint indexes; this
    guard is defence in depth. Consequence: collections with a secondary unique
    index get no HOT updates on the document column.
    A possible follow-up is
    a way for an AM to declare that "same entries" also means "same exclusion
    result" (true for DocumentDB's =?= over generated terms).

Commits:

  1. Detect the proposed server APIs at build time (Makefile.global greps the
    server headers and defines DOCUMENTDB_HAVE_AMCOMPAREDATUMS /
    DOCUMENTDB_HAVE_SUBATTR_SUPPORT). This commit is shared with the
    companion subattr PR.
  2. rumcomparedatums and the test rum_hot_update_tests.

Measured on a Debian 13 EC2 host (32 vCPU) with PostgreSQL REL_19_STABLE plus
the CF 5556 backport (debug, cassert). 200 documents, indexes a_1, composite
b_1_c_1 and h_hashed, fillfactor 10:

update before (stock PG19) with this PR
updateMany $inc of an unindexed field, 200 docs 0/200 HOT 200/200 HOT
updateOne $set of an unindexed field, 50 docs 0/50 HOT 50/50 HOT
updateMany $inc of an indexed field, 20 docs 0/20 HOT 0/20 HOT (correct)
updateMany on a collection with a unique index 0 HOT 0 HOT (exclusion guard)

Memory: the server calls amcomparedatums once per changed row. An earlier
version of the core patch ran it in the per-query memory context, so peak
backend memory grew with the number of rows updated. The current core patch
(v39, branch cf-5556)
runs it in a per-tuple context. This PR depends on that
fix.
Peak backend RSS (VmHWM), updateMany $inc over 60,000 documents with
indexes a_1, t_1 (40-element arrays) and composite b_1_c_1:

server VmHWM before → after the update
stock REL_19_STABLE (no CF 5556) 29 MB → 166 MB
CF 5556 core before the memory fix 27 MB → 518 MB
CF 5556 core with the memory fix 26 MB → 167 MB
CF 5556 core with the memory fix, rumcomparedatums compiled out 26 MB → 167 MB

With the memory fix, peak memory matches stock PG19, so the remaining growth
is baseline behaviour, not this PR. Stock PG19 makes 0 HOT updates on this
workload, and so does the patched server: fillfactor is 100 and the pages are
full. The HOT table above uses fillfactor 10. For a pure-core comparison,
updating 100k int[200] rows under a GIN index so that the GIN keys stay the
same: stock 238 MB, core before the fix 239 → 873 MB, core with the fix 225 →
225 MB.

Tests on the same host. "Branch" is the integration branch containing this PR,
the subattr PR and the alternate-AM EXPLAIN fixes:

  • make check-basic in extended_rum_tests: main 69/69, branch 71/71 (the 2
    new tests).
  • make check-basic in regress: main 125/125, branch 125/125.
  • This branch alone (commits 1+2), check-minimal with
    rum_hot_update_tests: 2/2 passed.
  • Builds with -Werror against patched PG19 (flag on, rumcomparedatums
    in the library) and against stock REL_19_STABLE (flag off, symbol absent).
  • citus_indent --check and scripts/validate_extension_file_state.sh pass.

The test checks HOT counts only as "all or none", so it passes on servers with
and without the API. On every server it also checks that index scans return
the same documents as sequential scans after updates that do and do not
change indexed paths, and that a unique index still rejects an update whose
terms collide with another document's (defaultUniqueIndexKeyhashOverride
plus truncated terms).

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring / code cleanup
  • DevOps / tooling
  • Test improvement
  • Other: ___________

AI Disclosure

  • No AI tools were used substantially in this PR
  • AI tools were used to assist with this PR (please complete below)
    • Tool(s) used: claude-opus-5-5 via the pi coding agent
    • How it was used: Drafted the code, tests and descriptions; ran the
      builds, tests and measurements on the EC2 host; reviewed the RUM metadata
      and exclusion-constraint cases. A human reviewed and tested the result.

Checklist

  • I can explain every change in this PR if asked during review
  • I have tested these changes locally
  • I have added or updated tests as appropriate
  • All commits are signed off (DCO: git commit -s)
  • I have read the CONTRIBUTING.md guidelines

Disclosure

This work was created with an even blend of human and AI contributions. AI was
used to make content edits, such as changes to scope, information, and ideas.
AI was used to make new content, such as text, images, analysis, and ideas. AI
was prompted for its contributions, or AI assistance was enabled. AI-generated
content was reviewed and approved. The following model(s) or application(s)
were used: claude-opus-5-5 (via the pi coding agent). (AIA HAb CeNc Hin R claude-opus-5-5 v1.0)

gburd added 2 commits October 16, 2026 13:09
A patch series proposed for PostgreSQL (CommitFest 5556, "Expanding HOT
updates for expression and partial indexes") adds two server APIs that
DocumentDB can use so that updates to a document stay heap-only (HOT) when
they leave the document's index entries unchanged:

- amcomparedatums, an optional index AM callback that says whether a new
  column value would give the same index entries as the old one; and
- SupportRequestSubattr, a planner support request that says which parts
  of a structured value a function reads or may change.

Neither is in a released PostgreSQL.  Detect them from the server's headers,
not from its version, and define DOCUMENTDB_HAVE_AMCOMPAREDATUMS and
DOCUMENTDB_HAVE_SUBATTR_SUPPORT so code using them builds only where they
exist.  Nothing uses the definitions yet.

Proposal: https://commitfest.postgresql.org/patch/5556/

Assisted-by: Claude Opus 5.5 (claude-opus-5-5) via pi coding agent; human
reviewed and tested.  AIA HAb CeNc Hin R claude-opus-5-5 v1.0

Signed-off-by: Greg Burd <greg@burd.me>
Every DocumentDB index on a document path is a RUM index on the bson
document column itself, with the path held in operator class options:

    CREATE INDEX ... USING documentdb_rum
        (document bson_rum_single_path_ops (path = 'a.b'))

PostgreSQL can only update a row as heap-only (HOT), making no new index
entries, if no indexed column changed.  Since every write changes the
document column, every update to a collection with any path index has made
new entries in every one of its indexes, including those whose terms did
not change, and left dead entries for VACUUM.  That is true of updateOne
and findAndModify, which compute the new document in C and write it back
whole, and of updateMany.

A proposed PostgreSQL index AM callback, amcomparedatums, lets the index
answer instead: would the new value give the same entries as the old?
Implement it for RUM.  A RUM index stores, for each row, the keys its
operator class extracts and scans see the row only through them, so the
entries are the same if extractValue gives the same keys, compared as RUM
compares them.  For DocumentDB that means the terms for the paths an index
covers, so an update that leaves those paths alone keeps every index's
entries and can be HOT.

The callback is conservative where an entry holds more than its key: it
returns false for columns with per-item additional information (text
indexes, which store positions) and for indexes with attach or add-to
columns.  It also returns false if the new document would set opclass
metadata bits, such as a composite index's multi-key paths, that the old
one did not: insertion records those in the metapage, and skipping it must
not lose them.

Same entries are not enough for an exclusion constraint, which is how
DocumentDB builds unique indexes: a HOT update skips the constraint check,
and the constraint compares the new values, not the entries (its terms are
hashes).  The server does not call amcomparedatums for an exclusion
constraint's index; the callback refuses such an index anyway.  So an
update to a collection with a secondary unique index is not HOT.

The callback is only built when the server has the API (see the previous
commit); documentdb_rum loaded from the public rum library does not get
it.  The new test checks HOT counts, which depend on the server, only as
"all or none", and checks on every server that index scans find the same
documents as sequential scans after updates that do and do not change
indexed paths, and that a unique index still rejects an update whose
terms collide with another document's.

Proposal: https://commitfest.postgresql.org/patch/5556/

Assisted-by: Claude Opus 5.5 (claude-opus-5-5) via pi coding agent; human
reviewed and tested.  AIA HAb CeNc Hin R claude-opus-5-5 v1.0

Signed-off-by: Greg Burd <greg@burd.me>
@gburd
gburd marked this pull request as ready for review October 17, 2026 09:30
@gburd
gburd requested a review from a team as a code owner October 17, 2026 09:30

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant