Skip to content

Base CTID split part count on total size (heap + TOAST) - #50

Merged
teknogeek0 merged 2 commits into
mainfrom
fix/ctid-split-includes-toast
Jul 24, 2026
Merged

Base CTID split part count on total size (heap + TOAST)#50
teknogeek0 merged 2 commits into
mainfrom
fix/ctid-split-includes-toast

Conversation

@teknogeek0

Copy link
Copy Markdown
Collaborator

Problem

A migration stalled on a single huge table: heap ~3.3 GiB, TOAST ~936 GiB (99% of a ~942 GiB total), with a text primary key. With --split-tables-larger-than 50GB the table became one COPY job that serially detoasted the entire ~936 GiB — extremely slow.

Two different size metrics were used in the split path:

  1. Split decision compares pg_table_size(oid) (TOAST-inclusive) against the threshold, so the table is correctly chosen to be split.
  2. CTID fallback is taken because a text (or uuid) PK is not a single-column integer unique/PK key.
  3. CTID part-count math divided relpages (heap pages only, TOAST excluded) by pages-per-part. With a tiny heap relative to a huge TOAST, this rounds down to a single part.

So TOAST counted for whether to split but was invisible to how many CTID parts to make.

Fix

Base the CTID part count on table->bytes (total on-disk size, heap + TOAST), matching the key-based branch, then:

  • cap at --split-max-parts;
  • cap at relpages (cannot create more CTID page-ranges than there are heap pages);
  • distribute heap pages evenly across the parts.

A CTID range scan reads heap rows and detoasts each row on the fly, so splitting the heap page range parallelizes the TOAST reads. This also closes a latent case where relpages == 0 produced zero partitions.

Under --estimate-table-sizes, bytes is a heap-only estimate, so the change degrades gracefully to the previous heap-based behavior (no regression there).

Behavior change beyond TOAST-heavy tables

The CTID part count is now total-size-based for every CTID-split table, not only TOAST-dominant ones. For a plain table the total size ≈ heap size, so counts track the threshold more directly. This is reflected in the updated table_ctid_candidate grid in the unit test (splits into more parts than before for the same tiny test threshold).

Testing

Added a regression test to the existing split suite (tests/unit/4-list-table-split):

  • A physically TOAST-heavy table (table_toast_heavy) with no integer PK and STORAGE EXTERNAL (uncompressed → deterministic out-of-line size across PG 16/17/18): heap ~4 pages, TOAST ~6.4 MB.
  • With a 1 MB threshold the old heap-only math gives 1 part (no split); the fix gives ceil(6.5MB/1MB)=7, capped by --split-max-parts 2 to 2 parts. The pinned grid is verified identical on PG17 and PG18.

Pre-fix, the new assertion fails (the table reports a single part), proving it is a real regression test.

Check Result
make build (PG18) clean
make tests/unit (PG18) pass
make tests/unit (PG17) pass — pinned grid identical
citus_indent --check pass
make tests (full, PG18) pass

Docs: added a sentence to docs/concurrency.rst noting the CTID part count is derived from total on-disk size (heap + TOAST). No CLI/help changes, so no docs/include/*.rst regeneration.

When a table exceeds --split-tables-larger-than but has no single-column
integer unique/PK, pgcopydb splits it by CTID. The part count was
computed from table->relpages (heap pages only), so a table whose size
is dominated by TOAST became a single COPY job that serially detoasted
the whole table.

The split decision itself uses pg_table_size (TOAST-inclusive), so such
a table is correctly chosen for splitting but then gets one part. Base
the CTID part count on table->bytes (total on-disk size), matching the
key-based branch, then cap at --split-max-parts and at relpages (cannot
create more CTID page-ranges than heap pages), and distribute heap pages
across the parts. A CTID range scan detoasts each row it reads, so
splitting the heap page range parallelizes the TOAST reads.

Also fixes a latent case where relpages == 0 produced zero partitions.

Under --estimate-table-sizes, bytes is a heap-only estimate, so this
degrades to the previous heap-based behavior.
@teknogeek0
teknogeek0 merged commit 532ee5f into main Jul 24, 2026
96 checks passed
@teknogeek0
teknogeek0 deleted the fix/ctid-split-includes-toast branch July 24, 2026 20:48
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