feat(heap): place inserts by clustered index order - #1
Draft
Komzpa wants to merge 82 commits into
Draft
Conversation
Komzpa
force-pushed
the
codex/clustered-write-insert-prototype
branch
from
April 29, 2026 17:23
411f79f to
c771090
Compare
Komzpa
force-pushed
the
codex/clustered-write-insert-prototype
branch
15 times, most recently
from
April 30, 2026 00:12
36f15f8 to
ff79bbf
Compare
Komzpa
force-pushed
the
codex/clustered-write-insert-prototype
branch
from
April 30, 2026 00:15
ff79bbf to
4a5adf8
Compare
…te-origin-master-check
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.
Summary
Adds clustered-write heap placement for tables that already have a valid clustered index.
When inserting into a clustered table, the heap now tries a bounded list of heap pages found through the remembered clustered index before falling back to the existing target block, FSM, and extension path.
indisclusteredindex OID in relcache asRelationGetClusteredIndex()CLUSTERto use, while limiting target-page probes to tuple-anchored btree keys(tile_id, osm_id)can still find the existingtile_idgroup for brand-new object IDs>= key, then<= key) so first-seen clustered keys are placed near adjacent ranges instead of input-order appendheap_multi_insertbatch, then groups tuples by discovered btree target block before entering the critical insertion loopRelationGetBufferForTuple()during multi-insert, and only lazily probes the index again for additional candidates if that preferred page is fullheap_multi_insertbatch for composite clustered btree indexes, with equality comparisons isolated in a resettable memory contextrd_amcachechunk, while verifying every hit with the clustered btree opfamily equality function(tile_id, osm_id)shapes benefitingheap_multi_insertbatch, then lets very hot equal-prefix batches use the regular bulk/FSM path instead of spending most of the write on low-value duplicate target probesheap_getattr/opfamily-equality pass while preparing the batchDatumcomparison, while still suppressing the lazy clustered-index lookup during insertion, so all-duplicate batches stay on the regular bulk/FSM path with less per-tuple overheadtext COLLATE "C"leading keys using the clustered btree equality function, so geohash-like duplicate-key COPY batches avoid prefix-cache allocation and clustered target probes tooheap_multi_insertreorders a batch, so COPY index maintenance and row triggers keep pointing at the correct heap tuplerd_clusteredindexas part of the relcache index-list state when retrying after mid-scan relcache invalidations, alongside primary-key and replica-identity indexesmark_index_clustered()changesindisclustered, so later inserts in all sessions refresh the remembered clustered index before using clustered-write placementheap_multi_insertbatch ordering when a clustered GiST opclass exposes the same sorted-build comparator; btree bulk inserts stay on the target-block path because benchmarking showed that was faster hereInvalidBlockNumberin FSM after the clustered-candidate list is exhausted; the real osm2pgsql run caught this as multi-GB FSM bloat before the fixsrc/tools/clustered_write_bench/, including a repeat-run wrapper that stores raw outputs and TSV summariesPG_BINDIRand recordsserver_version.txt, so benchmark runs can be tied to the build under test instead of accidentally using a system serverSINGLE_KEY_VALUES, matching both the left-prefix stress case and the experimental osm2pgsql generated-geohash shapetextclustering key withTEXT_KEY_VALUES, so geohash-like varlena-key changes can be smoke-tested before a full osm2pgsql/PostGIS runHEAP_FILLFACTOR_VALUES, so update locality and duplicate-key insert locality are measured separately instead of blaming every hot-key failure on page selectionHOT_TILE_FRACTION_VALUES, making large duplicate-key ranges and already-full equal-key pages visible in repeatable benchmarksHOT_TILE_COUNT_VALUES, so multi-hot COPY batches exercise cleanup paths that single-key hot tests misspsqloutputs and removing temporarypgdataafterUSE_TEMP_INSTANCE=trueruns unlessKEEP_TEMP_INSTANCE_DATA=trueis set${TMPDIR:-/tmp}instead ofOUTDIR, so long output paths do not trip PostgreSQL socket path limitsheap_block_spanandoutside_base_heap_block_spanin synthetic locality outputs and summaries, separating full mixed reserve/tail span from the compactness of rows that actually left the original clustered key rangerun_environment.txtfor synthetic benchmark runs, including git head, benchmark matrix settings, uptime/load, filesystem space, and selected PostgreSQL bindir, so noisy timing comparisons can be audited instead of treated as standalone truthmedian_elapsed_msalongside average/min/max timing summaries, so repeated synthetic runs expose outliers instead of hiding them in a single meanORDER_DIFF_BY_CLUSTER_KEY_VALUES, giving a cheap upper-bound check for micro-sort/buffered-insert ideasINSERT ... SELECTwith server-sideCOPY ... FROMviaCOPY_DIFF_FROM_FILE_VALUES, exercisingheap_insertandheap_multi_insertas distinct benchmark paths while cleaning up per-run staged TSV filesclustered_write_read_hotandwithout_cluster_metadata_read_hot, so micro-sort and overflow-placement experiments can be judged on read/write tradeoffs instead of locality counters aloneclustered_write_read_updated_hotandwithout_cluster_metadata_read_updated_hot, so moved-update read cost is visible separately from broader hot-tile readsHOT_UPDATE_FRACTION_VALUES, splitting locality intoupdate_hotandupdate_restwhen enabledUPDATES_BEFORE_INSERTS_VALUES, and records that this is diagnostic rather than a general fix because it moves locality damage from updates into hot inserts1, repeat3COPY-vs-INSERT result: integer COPY is often faster, but fillfactor90loses clustered locality, while generated text-key COPY is still noisy and can regress badly when unorderedheap_multi_insertpage packing inside clustered target boundaries, so a page selected for one clustered neighborhood is not silently filled with later tuples that targeted a different neighborhood90COPY locality recovered to83.97-84.75%inside the base range, while text-key COPY and ordered COPY remain separate cost follow-upsINSERT ... SELECTcost center without adding persistent target-block cache stateprefixCountSlots-> COPY target-cache shortcut because it improved several generated text-key COPY cases but regressed the fillfactor50unordered text-key path90COPY casesrun_environment.txt, gzip-compressing per-variant logs by default, and removing each variant'spgdataunlessKEEP_PGDATA=trueREAD_REPEATS, so cold-cache and scheduler outliers are visible without repeating the full importREAD_REPEATS=0and enable PostgreSQL slow-statement logging withPG_LOG_MIN_DURATION_MS, so append cost centers can be diagnosed without hand-editing benchmark clusterswork_memwithPG_WORK_MEM, after slow-statement diagnostics showed the ordered staged drain is sensitive to sort memoryheap_updatetuples on the regular FSM path, so clustered-write page selection remains an insertion policy and does not fight HOT/fillfactor/old-page locking constraintsBehavior
The normal heap placement path remains the fallback. If there is no clustered index, the clustered index is invalid/not ready, the selected pages cannot fit the tuple, or the catalog state does not satisfy
CLUSTER-style requirements, insertion behaves as before. Moved update tuples intentionally keep using the regular update/FSM placement path. Target-page probes currently require a plain btree key prefix from the tuple being inserted; GiST and other non-btree clustered indexes avoid fake unqualified placement probes.Partial clustered indexes are ignored defensively, matching the fact that PostgreSQL rejects them for
CLUSTER. Clustered placement is disabled during bootstrap processing so catalog initialization does not try to scan indexes whose heap files are still being created.GiST clustered indexes can still participate in
heap_multi_insertordering when their opclass exposes sorted-build sort support. This is batch ordering, not a per-tuple page probe.BRIN is not used directly in this patch: PostgreSQL's BRIN AM exposes
amgetbitmap, notamgettuple, andbrinhandleris not markedamclusterable. The benchmark has ause_brin=truemode so a future bitmap/range candidate path can be compared easily.Synthetic Benchmark
Ran the synthetic osm2pgsql-like diff benchmark on this branch after a fresh
tmp_installbuild:psql -v scale=1 -v use_brin=false -f src/tools/clustered_write_bench/osm2pgsql_diff.sql postgrespsql -v scale=1 -v use_brin=true -f src/tools/clustered_write_bench/osm2pgsql_diff.sql postgresStructured diff statement timings now emitted by the benchmark:
Locality summary:
A repeated
scale=1,use_brin=false,REPEATS=3wrapper run before the prefix-cache patch showed deterministic locality but noisy timings: clustered insert averaged708.46 mswith a648.43..806.90 msrange, while the control insert averaged131.75 mswith a112.41..152.19 msrange. After adding the per-batch leading-key target cache, the same wrapper shape measured clustered insert at618.40 msaverage (595.31..645.22 ms) and control insert at123.52 msaverage (97.52..137.78 ms), with unchanged locality. A verified temp-instance run usingUSE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=falserecordedPostgreSQL 19develinserver_version.txt; with the by-value hash cache it measured clustered insert at738.85 msaverage (633.56..839.51 ms) and control insert at162.86 msaverage (156.87..173.97 ms), again with clustered insert locality79.38%vs control0.00%. After making multi-insert precomputation use only a cheap first-candidate probe and leaving the full bounded search for the lazy full-page fallback, aREPEATS=5 SCALE_VALUES=1 BRIN_VALUES=falsetemp-instance run measured clustered insert at617.59 msaverage (473.09..785.07 ms) and control insert at131.90 msaverage (104.97..172.14 ms), with unchanged locality. Extending the per-batch leading-key cache to single-column btree indexes and runningREPEATS=5 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=truemeasured the generated-geohash-like shape at clustered insert497.63 msaverage (437.03..540.23 ms) vs control insert148.21 ms(128.68..192.37 ms), again with clustered insert locality79.38%vs control0.00%. A follow-up hot-duplicate run withREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9"kept the normal single-key case in the same range (484.39 msclustered insert vs155.14 mscontrol, locality79.38%vs0.00%) but exposed the duplicate-key cliff: withhot_tile_fraction=0.9, clustered insert averaged1926.53 msvs control140.53 ms, while locality was only10.02%vs0.00%and average drift improved modestly (3003.62vs3218.83blocks). Several attempted fixes for that cliff were benchmarked locally and rejected before commit, including repointing the remaining batch target group after a full preferred page, taking the backward edge for cheap first-candidate probes, target-run skipping, and prefix-hit caps. The kept hot-prefix pre-count guard measured better on the same verified temp-instance shape: normal single-key clustered insert averaged328.84 msvs87.05 mscontrol with locality still79.38%vs0.00%;hot_tile_fraction=0.9clustered insert averaged1110.25 msvs87.58 mscontrol, with locality still10.02%vs0.00%and average drift still3003.62vs3218.83blocks. After caching each tuple's prefix-count slot and splitting the hot locality report intoinsert_hot/insert_rest, a verified temp-instanceREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9 1"run measured normal single-key clustered insert at263.21 msvs59.58 mscontrol,hot_tile_fraction=0.9clustered insert at699.08 msvs43.35 mscontrol, and fully-hot1.0clustered insert at773.54 msvs45.67 mscontrol. After short-circuiting fully hot equal-prefix batches before clustered item allocation, a follow-upREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0.9 1"temp-instance run measuredhot_tile_fraction=0.9clustered insert at930.95 msvs72.87 mscontrol, and fully-hot1.0clustered insert at983.05 msvs66.57 mscontrol in the current noisy session; a post-regression scale0.1hot=1 smoke measured69.42 ms. After narrowing that path to by-value integer/OID all-equal batches and skipping the prefix cache allocation too, a verifiedREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9 1"temp-instance run measured normal single-key clustered insert at346.08 msvs94.77 mscontrol,hot_tile_fraction=0.9clustered insert at866.17 msvs59.85 mscontrol, and fully-hot1.0clustered insert at725.13 msvs42.07 mscontrol. A post-rebuild two-repeat check on hot0.9 1measured957.26 msand1005.07 ms, confirming the same broad improvement class under noise. The split locality makes the remaining cliff explicit: athot_tile_fraction=0.9, forced hot rows are only0.03%inside their base range, while the non-hot tail is99.95%inside.The composite-prefix path fixes the important diff-insert case: new object IDs under existing tile keys stay inside the original tile heap range about 79% of the time instead of 0% in the control. The structured timings make the write-side cost visible: clustered insert placement is slower than the control in this synthetic stress, while updates stay in the same broad range. A current scale
1, repeat3, single-key median run shows the hot duplicate-key path is no longer the main cost center: integer fillfactor90hot0.9clustered insert was165.92 msmedian versus120.71 mscontrol, and text fillfactor90was222.00 msversus175.87 ms. The ordinary non-hot single-key case still preserves83.97%to100%locality but costs roughly614-1205 msclustered insert median versus158-296 mscontrol, so future optimization should target clustered target-probe cost. Adding the optional BRIN index does not change locality yet, which matches the current contract because clustered placement does not consume BRIN/range candidates directly.A new text-key synthetic mode was added after the rejected varlena hash-cache experiment so future geohash-key changes can fail fast. The smoke command
USE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES="false true" HOT_TILE_FRACTION_VALUES="0 0.9"completed with gzip-compressed raw output and removed its temporarypgdata. On this run, integer and text keys had identical locality summaries, while text mode exposed extra write cost (39.25 msclustered insert vs22.30 msin the non-hot case). The hot case remained the known duplicate-key cliff: forced hot rows were only0.28%inside their base range, while the non-hot tail stayed99.00%inside.After switching the synthetic generated text key to
COLLATE "C"and extending the all-equal stop-spending guard to text prefixes, a before/after scale1hot1.0run measured fully hot text-key clustered insert at979.47 msbefore and810.16 msafter, with unchanged locality (0.03%inside the base range). A scale1hot0/0.9follow-up kept the same locality summaries for integer and text modes; text-key hot0.9measured853.77 msclustered insert with the non-hot tail still99.95%inside. This is still not a duplicate-key locality fix; it only stops spending extra clustered-index work on batches that cannot currently be placed locally.A fillfactor sweep then separated reserve-space effects from target-page selection. With
REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES=false HEAP_FILLFACTOR_VALUES="90 70 50" HOT_TILE_FRACTION_VALUES=0.9, lowering fillfactor from90to50improved moved-update locality from7.71%to99.28%inside the original key range, but forced-hot inserts only moved from0.03%to0.29%inside. The firstheap_block_spanmetric showed a long mixed span for hot rows, but a later outside-base span metric clarified the shape: after reserve restoration, the full span includes a few old-range reserve rows plus a compact tail. In a focused scale1, hot0.9, repeat2run, fillfactor90clusteredinsert_hothad fullheap_block_span=3773butoutside_base_heap_block_span=384, matching the control tail; at fillfactor50, clustered outside-base span was692versus control693.The kept single-row overflow fix pairs tail fallback with a stricter candidate-space check. Clustered candidates must now satisfy the normal target free-space threshold; when every equal-key neighbor is below that threshold, the insert tries the relation tail and extends from there instead of walking the global FSM. On
USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=1 BRIN_VALUES=false HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0.9 ORDER_DIFF_BY_CLUSTER_KEY_VALUES=false, the first version made clusteredinsert_hotspan match the control shape (384at fillfactor90,693at fillfactor50) instead of the previous3737/6596spans, but inserts were still expensive (1194.27 ms/1079.02 ms). The follow-up overflow-prefix cache remembers exhausted leading clustered keys and keeps appending locally, improving the same integer shape to70.85 ms/82.54 ms, close to the control at53.35 ms/55.03 ms. After extending that bounded cache to by-reference text prefixes, scale1generated-text hot inserts that were still around770-805 msdropped to63.07-112.31 msin the latest inline-cache sweep across the ff90/ff50 single/composite text-key matrix. The by-reference values are kept inline becauserd_amcachereset frees only its single palloc chunk; oversized by-reference keys skip the cache. Same-backend memory smokes covered both replacement and relcache invalidation: the invalidation stress dropped from1082680total /787256used bytes after warmup to1048576total /755272used bytes after30insert/invalidation cycles instead of growing.The newest reserve-threshold pass fixes a micro-sort-shaped regression caught by
ORDER_DIFF_BY_CLUSTER_KEY_VALUES=true: immediate tail-overflow activation after one below-fillfactor hit made sorted non-hot input fast but wrong, dropping clustered insert locality to20.48%inside the base range. Counting a small number of reserve hits before activating the cached tail target restored sorted scale1, hot0locality to84.75%at fillfactor90and100%at fillfactor50; unordered locality stayed at83.97%/100%. A hot0.9/1regression run kept write timings in the same broad range as the inline overflow-cache sweep. Hotinsert_hotspan remains a visible tradeoff after reserve restoration: a few hot rows can stay in the old clustered range while the rest append at the tail, so the splitinsert_hot/insert_restmetrics remain the source of truth rather than treating one span number as victory.The outside-base span follow-up makes that tradeoff less alarming: the overflow run is compact after it leaves the old range. The large full span should be read as "reserve rows plus compact tail", not as scattered tail placement.
A follow-up multi-hot COPY benchmark added
HOT_TILE_COUNT_VALUESand caught areal cleanup bug. The pre-fix scale
1, repeat3,hot_tile_fraction=0.9,hot_tile_count=1 8, COPY run crashed with SIGSEGVon the first eight-hot-key run while deleting the prefix-cache comparison
memory context. The fix makes the common cleanup tolerate the skip path having
already deleted that context. The repaired run completed with clustered COPY
insert medians of
63.17 msfor one hot key and86.04 msfor eight hot keys;the
hot_tile_count=128follow-up also completed and showed the non-skipmulti-hot overflow shape (
187.79 msclustered COPY median versus59.78 mscontrol, non-hot tail
96.85%inside the base range). Raw gzip outputs areunder
/home/kom/tmp/clustered-write-synthetic/multi-hot-cache-update-copy-fixed-20260501-033540/and
/home/kom/tmp/clustered-write-synthetic/multi-hot-overflow-cache-copy-20260501-033632/;the pre-fix crash log is under
/home/kom/tmp/clustered-write-synthetic/multi-hot-cache-update-copy-20260501-033322/.Real osm2pgsql/PostGIS Benchmark
Ran
src/tools/clustered_write_bench/run_osm2pgsql_georgia_bench.shwith Geofabrik Georgia, planet daily diff000/004/977.osc.gzsimplified byosmium merge-changes -s, PostgreSQL 18.3, PostGIS 3.6.3, and osm2pgsql 2.2.0.Write-side results from the three-way run:
Read/locality summary from the same run:
Follow-up real run after hardening the benchmark harness used planet daily diff
000/004/978.osc.gz, the same Georgia extract, stock osm2pgsql2.2.0,OSM2PGSQL_CACHE_MB=1024, andOSM2PGSQL_PROCS=2. The baseline was systemPostgreSQL
18.3with PostGIS3.6; patched rows used this branch'sPostgreSQL
19develinstall with locally built PostGIS3.7.0dev.Read/locality summary from the
978run:This run is deliberately documented as mixed: append time stayed roughly flat
and exact spatial reads improved in this pass, but initial import and bbox line
reads regressed. The stock-osm2pgsql locality aggregate is identical between
the baseline and patched rows, so this validates the real benchmark path and
PG19/PostGIS compatibility rather than proving a stock-osm2pgsql clustering
win.
The experimental clustered-import path is more promising on write time and
spatial reads in this run. Adding heap
fillfactor=90to those import tablesleft real reserve space (
reloptionsreported{autovacuum_enabled=off,fillfactor=90}) and reduced the compactness metricfrom
13/16/10p95 blocks to9/13/9for line/point/polygon, but it did notfix the long p95 heap span. A follow-up variant that rewrites the initial heap
by
osm2pgsql_cluster_key, osm_idbefore append fixes the heap-span diagnosis,but the first
CREATE TABLE AS SELECT *prototype silently turned the generatedkey into a plain text column. The current checked-in experiment moved that
ordering into the COPY path by staging rows in per-target temporary tables
created
LIKE ... INCLUDING GENERATED, then draining each target withORDER BY osm2pgsql_cluster_key, osm_id. The latest kept improvement deferscreating the clustered btree until after the initial staged drain, so initial
import does not pay btree maintenance while rows are already draining in key
order. A short create+append smoke confirmed all generated keys stay
non-NULL and the clustered btree marker survives. The next kept improvement
stops draining the staged table before every append DELETE: append-mode
DELETEs remove matching ids from the temporary staged table, and append staging
tables create a temporary delete-key index lazily before the first staged
DELETE for that target so those deletes do not full-scan a growing temp table.
In the current same-environment A/B, stock osm2pgsql on
patched PostgreSQL measured
1:58.33create and2:05.22append, while theclustered import measured
1:30.30create and2:13.45append. It used morespace (
2840 MBversus2727 MB), but p95 block/span tightened to2/2line,2/1point, and3/2polygon from stock7/2381,6/1002, and32/1208.Reads were mostly better or close (
627.029 msline bbox,954.597 mspolygon bbox,
184.525 msroads exact,1251.008 mspolygon exact), with onepoint bbox spike (
3163.663 ms) that needs a repeat before being treated as areal regression. A repeat of only
patched_clustered_importon the sameGeorgia
978inputs did not reproduce that point read spike (978.990 ms) andkept the same tight p95 block/span (
2/2,2/1,3/2). The repeat measured1:07.54create,2:51.11append,2840 MB,1146.925 msline bbox,1912.456 mspolygon bbox,293.356 msroads exact, and2053.134 mspolygon exact, so the honest claim remains locality plus plausible append
shape, with final read/write claims needing repeat medians. The repeat also
verified the real-workload cleanup path: logs were gzip-compressed and
pgdatawas removed with
KEEP_PGDATA=false. Small bounded in-memory sort windows weretested and rejected because they left key correlation poor and made diff append
much slower.
Adding
READ_REPEATS=3made the read comparison less dependent on one coldquery pass. On the same Georgia
978inputs, patched stock osm2pgsql measured1:47.47create,2:20.95append,2728 MB, warmed medians of130.545 mspoint bbox,
193.198 msline bbox,330.994 mspolygon bbox,76.100 msroads exact,
1852.406 mspolygon exact, and locality-summary median8123.631 ms, with p95 block/span7/2381,6/1002,32/1208. The stagedclustered import measured
1:21.41create,3:05.01append,2841 MB,warmed medians of
110.095 ms,200.126 ms,345.462 ms,46.813 ms, and1695.422 ms, locality-summary median6832.766 ms, and p95 block/span2/2,2/1,3/2. That is useful but mixed: tighter heap locality, fastercreate, and better point/exact warmed reads; roughly tied/slightly worse
line/polygon bbox; slower append in this run.
Creating the staged delete-key index lazily is a modest cleanup, not the final
append-cost fix. A same-input
READ_REPEATS=3run with the lazy index measured1:42.58create,2:46.88append,2841 MB, warmed medians of78.699 mspoint bbox,
163.190 msline bbox,281.257 mspolygon bbox,40.450 msroads exact, and
1382.557 mspolygon exact, with the same tight p95block/span
2/2,2/1,3/2. The appendReading input filesphase was163s, down from the previous repeated clustered run's181sbut still abovethe stock run's
131s. The real-workload cleanup removed the per-variant datadirectory with
KEEP_PGDATA=false; only the empty$WORKDIR/pgdataparentremained.
An append-only slow-statement diagnostic (
READ_REPEATS=0,PG_LOG_MIN_DURATION_MS=1000) confirmed the remaining clustered append cost ismostly the ordered staging path, not the temp delete-key index itself. During
append, statements above the
1sthreshold included staged COPY work, themiddle-table maintenance/index path, and the final staged drains; the largest
drain was polygon
INSERT ... SELECT ... ORDER BYat about46s. That makesthe drain the obvious next cost center, but not something to remove blindly.
A same-shape follow-up raised the benchmark cluster
work_memfrom64MBto256MBviaPGOPTIONS='-c work_mem=256MB'; the harness now exposes the samesetting as
PG_WORK_MEM=256MB. With the kept staged/lazy-index path, theappend-only Georgia
978run measured1:23.89create,2:10.95append,2840 MB, and reduced slow logged staged-drain time above1sfrom59.6sto
43.3s. A single-read follow-up measured1:18.84create,2:13.68append,
2840 MB, read timings of205.257 mspoint bbox,611.452 mslinebbox,
1418.884 mspolygon bbox,210.900 msroads exact, and1116.870 mspolygon exact, while preserving tight p95 block/span2/2line,2/1point, and3/2polygon. This is a practical benchmark/deployment knobrather than a new algorithmic fix.
Additional follow-up runs tested tempting variants and rejected them or kept them as caveats:
wayheap_multi_insertbatchdatum_image_hash()prefix cache8249line,7526point,6653.5polygonSo the checked-in osm2pgsql experiment uses a generated geometry-derived geohash
key, per-target ordered staging, staged DELETE cleanup with a temporary
delete-key index during append, and a clustered btree index created after the
initial staged drain; append runs keep using the existing clustered index.
PostgreSQL keeps GiST batch-sort support for opclasses where sorted-build order
is actually beneficial.
Rejected Experiments
The branch keeps negative results documented so the next pass does not circle
back into already-benchmarked dead ends:
0%inside base range), so current COPY batch prefix information must not be evicted blindly1, hot1.0check was about2016 ms2100 mson scale1, hot1.0, and risks COPY/index/trigger TID mapping correctness1.0scale0.1slower0.9/1.0scale0.1smokes slower without locality change; kept only the narrower by-value integer/OID and generatedtext COLLATE "C"all-equal guards after targeted scale1validationRecordAndGetPageWithFreeSpace()near the last attempted clustered page made scale1hot0.9/1.0inserts about2.2swith no hot-row locality gain3looked promising in 7/8 COPY cases, but repeat6fillfactor90recheck regressed integer unordered/ordered and text ordered, so the code was revertedhotPrefixTupleCount > ntuples / 2did not beat the kept all-hot-only guard and did not improve hot-row locality1hot0.9/1.0got slower with unchanged locality, so the branch keeps the simpler all-hot-only guard1temp-instance run kept locality unchanged but worsened single-key inserts to780.29 ms,1677.95 ms, and1294.90 msfor hot0,0.9, and1.03:14.39create,4:08.59append); target-page probing is better for this workloadway4:53.68create,5:30.27append)8192and65536row windows failed to repair generated-key correlation and made append4:33.82/5:11.02; a real replacement for the final rewrite needs a global/external sort1:54.96), but rejected becauseCREATE TABLE AS SELECT *does not preserve the generatedosm2pgsql_cluster_keycolumn for later appendsdatum_image_hash()prefix cache for varlena leading keys978writes regressed to2:10.79create /2:37.58append with unchanged locality, so the code change was reverted0NULL keys and0mismatches after append; real columns were plain), but Georgia978regressed to3:42.88create /4:18.57append with unchanged locality, so the checked-in patch stays with a stored generated real-table key2/2,2/1,3/2p95 block/span for line/point/polygon), but each append DELETE scanned the growing staging table and Georgia978append regressed to3:24.06; the kept variant adds a temporary delete-key index for append staging tablesheap_multi_insertbatch simplification1:59.29create,1:56.57append)1, hot0.9insert_hotspans unchanged (3737blocks at fillfactor90,6596at fillfactor50)1, hot0synthetic run: ordinary successful-prefix target caching made representative clustered inserts slower (313.52 -> 348.92 mscomposite int ff90,417.75 -> 465.84 mscomposite int ff50,284.92 -> 377.66 mscomposite text ff90,317.57 -> 398.11 mssingle text ff90) without locality gain; raw gzip output is under/home/kom/tmp/clustered-write-synthetic/single-row-target-cache-focused-20260430-230337/rd_amcache1, hot0, repeat3ordered/non-ordered matrix: locality stayed good, but write timings were mixed/noisy and several representative clustered inserts regressed (418.44 -> 614.21 mscomposite int ff50 unordered,353.75 -> 548.84 mscomposite text ff50 ordered,275.28 -> 559.61 mssingle text ff90 unordered); raw gzip output is under/home/kom/tmp/clustered-write-synthetic/overflow-metadata-order-nonhot-20260430-233011/8to41, repeat2, integer-key ordered/non-ordered matrix: sorted non-hot fillfactor50locality fell to81.92%inside the base range where threshold8had restored100%, fillfactor90sorted non-hot stayed below the kept84.75%result at81.91%, and hot span did not improve; raw gzip output is under/home/kom/tmp/clustered-write-synthetic/reserve-threshold4-focused-20260430-233816/1, repeat3, single-keyhot=0: locality stayed in the same shape (84.75%vs83.97%inside base range at fillfactor90,100%at fillfactor50), but clustered insert medians were mixed and remained far above control; raw gzip output is under/home/kom/tmp/clustered-write-synthetic/current-nonhot-ordered-median-20260501-014236/1, repeat2, single-keyhot=0experiment: locality did not change in the synthetic matrix, timings were mixed/noisy rather than a reliable win, and the policy is risky for genuinely sparse diffs where a singleton may be the only locality-preserving probe; raw gzip output and the reverted patch are under/home/kom/tmp/clustered-write-synthetic/skip-singleton-prefix-experiment-20260501-015018/The current lesson from the hot-prefix work is that duplicate-key batches need
either a genuinely local page-selection policy for the repeated key, enough
prefix grouping that the all-hot guard can keep overflow compact, or a clear
policy that refuses to spend old fillfactor reserve and starts compact overflow
when the clustered neighborhood is below the normal insert threshold. More
eager probes and extra per-tuple branches have repeatedly moved the cost curve
in the wrong direction.
Validation
origin/masterwas atf67dbd8398,maumaps/masterwas still at the branch fork point, and thisbranch merged cleanly with
origin/masterin a temporary worktree. Thefresh-master merge commit
c28180090fbuiltpostgres, passedgit diff --check, and passedmake -C src/test/regress check-tests TESTS='test_setup copy create_misc create_table create_index cluster'(
6/6tests). A scale0.02synthetic temp-instance smoke on that mergedbuild kept the intended locality shape: clustered insert
82.50%inside thebase range versus
0.00%for the control, with gzip raw output and temporarypgdataremoved under/home/kom/tmp/clustered-write-synthetic/fresh-master-smoke-20260501-181027/.make -C src/backend/access/heap hio.o heapam.o -j2make -C src/backend/commands repack.o -j2make -C src/backend/utils/cache relcache.o -j2make -C src/backend postgres -j2make -C src/test/regress check-tests TESTS='test_setup copy create_misc create_table create_index cluster'including a COPY batch reorder test that verifies secondary index lookups after clustered multi-insertpsql -v scale=1 -v use_brin=false -f src/tools/clustered_write_bench/osm2pgsql_diff.sql postgresafter the insert-only clustered placement guardrun_osm2pgsql_georgia_bench.shbash -n src/tools/clustered_write_bench/run_osm2pgsql_georgia_bench.shcmake --build build -j2for the experimental osm2pgsql checkout after applyingosm2pgsql_cluster_during_import.patchgit apply --check osm2pgsql_cluster_during_import.patchagainst a fresh osm2pgsql checkoutgit diff --checkfor code/docs; patch files contain normal context-line spaces, so the patch file itself is checked withgit apply --checkmake -C src/backend/access/heap hio.o heapam.o -j2,make -C src/backend postgres -j2, the same regression slice, and the synthetic osm2pgsql-like benchmark after reserving both TID and page-candidate budget for clustered btree equality/prefix range tailsbash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check,make -C src/backend/access/heap hio.o -j2,make -C src/backend postgres -j2, and the regression slice after making clustered candidates respect target free space and adding the ordered-diff benchmark knobUSE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false HEAP_FILLFACTOR_VALUES=90 HOT_TILE_FRACTION_VALUES=0.9 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true"before/after smoke for the ordered-diff knob and target-free-space tail policyUSE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=1 SCALE_VALUES=1 BRIN_VALUES=false HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0.9 ORDER_DIFF_BY_CLUSTER_KEY_VALUES=falseafter the compact overflow fixmake -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice, andgit diff --checkafter comparator cleanupmake -C src/backend/access/heap heapam.o -j2,make -C src/backend/utils/cache relcache.o -j2,make -C src/backend postgres -j2, regression slice, andgit diff --checkafter bounding clustered GiST sort memorymake -C src/backend/access/heap hio.o -j2,make -C src/backend postgres -j2, regression slice, andgit diff --checkafter bounding clustered candidate helper writesmake -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice, andgit diff --checkafter initializing GiST INCLUDE attributes for clustered batch sortuse_brin=falseanduse_brin=trueafter adding structured timing andbrin_enabledoutput;git diff --check;bash -n src/tools/clustered_write_bench/run_osm2pgsql_georgia_bench.shgit diff --check,make -C src/backend/access/heap hio.o heapam.o -j2,make -C src/backend postgres -j2, and regression slice after reusing precomputed clustered multi-insert target blocksbash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check, a live wrapper smoke withREPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false, and a repeatedREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=falserun showing stable locality but noisy timing rangesgit diff --check,make -C src/backend/access/heap heapam.o hio.o -j2,make -C src/backend postgres -j2, regression slice, and repeatedREPEATS=3 SCALE_VALUES=1 BRIN_VALUES=falseafter adding the per-batch leading-key target cachegit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice,USE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false, and verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=falseafter adding the by-value prefix hash cache and temp-instance benchmark modegit diff --check,make -C src/backend/access/heap hio.o heapam.o -j2,make -C src/backend postgres -j2, regression slice, and verifiedUSE_TEMP_INSTANCE=true ... REPEATS=5 SCALE_VALUES=1 BRIN_VALUES=falseafter making multi-insert batch target precomputation use cheap first-candidate probesgit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice,USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true", and verifiedUSE_TEMP_INSTANCE=true ... REPEATS=5 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=trueafter extending leading-key cache coverage to single-column clustered btree indexesgit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,make -C src/backend/access/heap hio.o heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2,USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9"smokes for the kept benchmark knob and the two rejected heap-placement experiments, and verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9"after keeping only the hot duplicate benchmark dimensiongit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2,USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9", verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9", and regression slice after adding the hot-prefix pre-count guardgit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2,USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9 1", verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9 1", and regression slice after removing the second hot-prefix recheck pass and splitting hot/rest insert localitygit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2,USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0.9 1", verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0.9 1",USE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0", regression slice, and a post-regression hot=1 smoke after short-circuiting fully hot equal-prefix batchesgit diff --checkandUSE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=1 SCALE_VALUES=0.02 BRIN_VALUES=false HOT_TILE_FRACTION_VALUES=0after bounding synthetic benchmark artifacts; the smoke left gzip-compressed raw output, logs, and TSV summaries while confirmingpgdatawas removedgit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, regression slice, verifiedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0 0.9 1", and post-rebuildREPEATS=2 ... HOT_TILE_FRACTION_VALUES="0.9 1"after bypassing prefix cache allocation for all-equal by-value batchesgit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice, andUSE_TEMP_INSTANCE=true ... REPEATS=1 SCALE_VALUES=0.1 BRIN_VALUES=false SINGLE_KEY_VALUES=true HOT_TILE_FRACTION_VALUES="0.9 1"after guarding raw prefix comparison withget_typbyval()bash -n src/tools/clustered_write_bench/run_osm2pgsql_georgia_bench.sh,git diff --check, locally built PostGIS3.7.0devsmoke on the PostgreSQL19develinstall, and fullpatched_stock+baseline_stockGeorgia/osm2pgsql runs on planet daily diff000/004/978.osc.gzafter hardening diff simplification and same-install PostGIS handlinggit diff --check,git apply --check osm2pgsql_cluster_during_import.patchagainst a clean osm2pgsql worktree,cmake --build build -j2in the experimental osm2pgsql checkout, fullpatched_clustered_importGeorgia/osm2pgsql runs before and after adding heapfillfactor=90, and a follow-up final-rewrite-by-key run confirmingreloptions,osm2pgsql_cluster_keycorrelation1, and improved heap-span localitygit diff --check,cmake --build build -j2, and two rejected fullpatched_clustered_importGeorgia/osm2pgsql runs with bounded in-memory pre-COPY sort windows of8192and65536rows; both were reverted from the experiment after failing to repair generated-key correlation and making append much slowergit diff --check,git apply --check osm2pgsql_cluster_during_import.patchagainst a clean osm2pgsql worktree,cmake --build build -j2, a full generated-preserving rewrite run, and a catalog/null-key check confirming stored generatedosm2pgsql_cluster_keycolumns survived append on all four OSM tablesgit diff --check, heap/backend builds, regression slice, install repair for PostGIS/hstore after regression refreshedtmp_install, and a rejected full Georgia978run for the varlenadatum_image_hash()prefix-cache experiment; the code change was reverted and logs were saved under978-prefix-image-hashgit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES="false true" HOT_TILE_FRACTION_VALUES="0 0.9",USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 BRIN_VALUES=true SINGLE_KEY_VALUES=true TEXT_KEY_VALUES=true, and a scale0.1integer/text comparison after adding the generated text-key synthetic benchmark modegit diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slice, before/afterUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES=true HOT_TILE_FRACTION_VALUES=1, and follow-upTEXT_KEY_VALUES="false true" HOT_TILE_FRACTION_VALUES="0 0.9"after adding the text all-equal clustered batch guard and generated text-key regression coveragegit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh, a scale0.02HEAP_FILLFACTOR_VALUES="90 70 50"smoke, andUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES=false HEAP_FILLFACTOR_VALUES="90 70 50" HOT_TILE_FRACTION_VALUES=0.9after adding the fillfactor benchmark dimensiongit diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,make -C src/backend/access/heap hio.o heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install, regression slice, long-OUTDIRsynthetic smoke, and scale1hot0.9span comparison after addingheap_block_spanand moving temp-instance sockets under${TMPDIR:-/tmp}git diff --check,make -C src/backend/access/heap hio.o, regression slice, and focused synthetic temp-instance sweeps after restoring bounded below-fillfactor clustered reserve use. Scale1, fillfactor90, non-hot inserts recovered83.97%inside the base clustered range versus0%control; hot0.9rest inserts recovered99.95%inside, while hot rows mostly follow control tail drift but keep a large span from the first reserve outlier.git diff --check,make -C src/backend/access/heap hio.o,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install, regression slice,USE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true" TEXT_KEY_VALUES=true HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES="0.9 1", and a same-backendCacheMemoryContextoverwrite smoke after caching by-reference text overflow prefixes.git diff --check,make -C src/backend/access/heap hio.o,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install, regression slice, relcache-invalidation memory smoke, oversized 2KB text-prefix smoke, andUSE_TEMP_INSTANCE=true ... REPEATS=2 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true" TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES="0 0.9 1"after moving by-reference cached prefixes inline inside the singlerd_amcachechunk.git diff --check,make -C src/backend/access/heap hio.o,make -C src/backend postgres,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install,USE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true" TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true",USE_TEMP_INSTANCE=true ... REPEATS=2 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true" TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES="0.9 1", a post-fixREPEATS=1ordered/hot smoke, and regression slice after delaying tail-overflow activation for short reserve-using prefix groups. A standaloneclusterregression invocation still fails because that test expectstest_setupto have createdtenk1; the full slice withtest_setuppasses.git diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh, a scale0.02temp-instance smoke, andUSE_TEMP_INSTANCE=true ... REPEATS=2 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES="false true" TEXT_KEY_VALUES=false HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0.9 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true"after addingoutside_base_heap_block_spanto distinguish compact tail overflow from mixed reserve/tail full span.git diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh, andUSE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 HOT_TILE_FRACTION_VALUES=0after addingrun_environment.txt; the smoke verified git head/matrix/load/df capture and temppgdatacleanup.git diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh, andUSE_TEMP_INSTANCE=true PG_BINDIR=/tmp/postgres-clustered-write/tmp_install/usr/local/pgsql/bin REPEATS=3 SCALE_VALUES=0.02 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES=false HEAP_FILLFACTOR_VALUES=90 HOT_TILE_FRACTION_VALUES=0 ORDER_DIFF_BY_CLUSTER_KEY_VALUES=falseafter addingmedian_elapsed_ms; the smoke verified correct median values, gzip raw output, and temppgdatacleanup.USE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 BRIN_VALUES=false SINGLE_KEY_VALUES=true TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES="0 0.9 1"for the current median hot/non-hot baseline, andUSE_TEMP_INSTANCE=true ... HOT_TILE_FRACTION_VALUES=0 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true"for the ordered-input non-hot check; both removed temppgdataand left gzip raw output under/home/kom/tmp/clustered-write-synthetic/current-*-median-*/.make -C src/backend/access/heap heapam.o,make -C src/backend postgres,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install, andUSE_TEMP_INSTANCE=true ... REPEATS=2 SCALE_VALUES=1 SINGLE_KEY_VALUES=true TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true"for the rejected singleton-prefix skip experiment; the experimental code was reverted andtmp_installwas rebuilt from the clean PR branch afterwards.bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check,USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 COPY_DIFF_FROM_FILE_VALUES="false true"smoke, andUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 SINGLE_KEY_VALUES=true TEXT_KEY_VALUES="false true" HEAP_FILLFACTOR_VALUES="90 50" HOT_TILE_FRACTION_VALUES=0 ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true" COPY_DIFF_FROM_FILE_VALUES="false true"after adding the COPY-vs-INSERT synthetic diff benchmark; both temp-instance runs removedpgdata, the focused run left 48 gzip raw files under/home/kom/tmp/clustered-write-synthetic/copy-vs-insert-single-key-20260501-020036/.git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2,USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 COPY_DIFF_FROM_FILE_VALUES=truesmoke, focused integer and text COPY boundary benchmarks, and regression slicetest_setup copy create_misc create_table create_index clusterafter stopping clustered COPY page packing at target boundaries; benchmark raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/copy-boundary-*-20260501-*/.git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2, regression slicetest_setup copy create_misc create_table create_index cluster, focused generated text-key COPY benchmark, and integer COPY smoke after hashing normalized text payloads for clustered COPY prefix-cache slots; text medians improved from1040.17/400.92/853.26/507.18 msto435.25/275.26/455.36/366.62 ms, raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/text-prefix-payload-hash-focused-20260501-022213/and/home/kom/tmp/clustered-write-synthetic/prefix-payload-hash-integer-smoke-20260501-022359/.git diff --check,make -C src/backend/access/heap hio.o -j2,make -C src/backend postgres -j2, regression slicetest_setup copy create_misc create_table create_index cluster, focused non-hotINSERT ... SELECTbenchmark, and hot0.9/1.0follow-up after making single-row clustered probes lazy; non-hot clustered insert medians fell into212.22-446.83 mswith unchanged83.97-100%locality, and hot medians stayed near control with the previous compact-tail shape. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/lazy-single-row-first-candidate-focused-20260501-023105/and/home/kom/tmp/clustered-write-synthetic/lazy-single-row-first-candidate-hot-20260501-023331/.git diff --check, a currentINSERT+COPYscale1, repeat2single-key matrix after the lazy single-row probe fix, and a rejected localMAX_BUFFERED_BYTES=262144COPY experiment; the larger buffer preserved locality but regressed integer fillfactor90COPY medians and generated text fillfactor90ordered COPY, so the code experiment was reverted and only docs were kept. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/current-insert-copy-after-lazy-single-row-20260501-023819/and/home/kom/tmp/clustered-write-synthetic/copy-buffer-256k-experiment-20260501-024252/.git diff --check,make -C src/backend/access/heap heapam.o -j2, fullmake -C src/test/regress check TESTS=clusterregression schedule (All 245 tests passed), focused scale1, repeat3copy-only matrix, and repeat6textff50ordered recheck after keeping same-target clustered COPY groups packed on fallback tail pages. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/copy-pack-same-target-20260501-024919/and/home/kom/tmp/clustered-write-synthetic/copy-pack-text-ff50-ordered-rerun-20260501-025142/.git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, a focused generated text-key COPY matrix, and a repeat6textff50unordered recheck for a rejected directprefixCountSlotstarget-cache shortcut; the shortcut improved textff90COPY medians but regressed textff50unordered to483.62 ms, so the code experiment was reverted and only docs were kept. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/direct-prefix-target-cache-text-copy-20260501-025740/and/home/kom/tmp/clustered-write-synthetic/direct-prefix-target-cache-text-ff50-unordered-rerun-20260501-025849/.git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, and a rejectedUSE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 ... COPY_DIFF_FROM_FILE_VALUES=truenon-hot COPY run for a lazy prefix-count allocation prototype; the prototype crashed with SIGSEGV on the first integer fillfactor90unordered COPY case, so the code was reverted and the install was rebuilt from the clean branch. Crash log and raw gzip output are under/home/kom/tmp/clustered-write-synthetic/lazy-prefix-count-nonhot-copy-20260501-030413/.USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 ... COPY_DIFF_FROM_FILE_VALUES=trueclean-install smoke after reverting the lazy prefix-count prototype; it completed, gzip-compressed raw output, and removed temporarypgdata. Raw gzip output is under/home/kom/tmp/clustered-write-synthetic/post-lazy-prefix-clean-copy-smoke-20260501-030920/.git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, a rejected scale1, repeat3COPY-only matrix, and a repeat6generated text-key unordered recheck for a prefix-count hash-slot reuse prototype. The narrower shortcut kept locality unchanged and improved some text cases in the first matrix, but integer fillfactor90regressed and the repeat6text unordered recheck measuredff90at410.60 msversus the kept324.91 ms, so the code was reverted and the install was rebuilt from the clean branch. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/reuse-prefix-hash-slot-copy-20260501-031244/and/home/kom/tmp/clustered-write-synthetic/reuse-prefix-hash-slot-text-unordered-rerun-20260501-031521/.git diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,make -C src/backend/access/heap heapam.o hio.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, a pre-fix multi-hot COPY crash reproduction, repaired scale1, repeat3,HOT_TILE_COUNT_VALUES="1 8"COPY benchmark,HOT_TILE_COUNT_VALUES=128overflow-shape follow-up, a final scale0.02hot-tile-count smoke after adding the benchmark axis, andmake -C src/test/regress check TESTS='test_setup copy create_misc create_table create_index cluster'(the make target ran the full parallel schedule; all245tests passed).git diff --check,make -C src/backend/access/heap heapam.o -j2,make -C src/backend postgres -j2,make install DESTDIR=/tmp/postgres-clustered-write/tmp_install -j2, a rejected scale1, repeat3,HOT_TILE_COUNT_VALUES=128COPY experiment loweringCLUSTERED_WRITE_MAX_PREFIX_TARGET_TUPLESfrom16to4, and a post-revert scale0.02COPY smoke; the prototype regressed unordered/ordered clustered COPY medians from140.79/86.61 msto200.88/129.12 ms, so the code was reverted and only the README note was kept.git diff --check,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.02 TEXT_KEY_VALUES="false true" COPY_DIFF_FROM_FILE_VALUES=truesmoke after adding hot read timings, plus a rejected btree leading-key sort prototype for skipped too-hot COPY prefixes. The prototype showed a possible unordered read win (12.92 msvs clean rerun15.70 ms) and a compact unordered tail, but did not beat the earlier clean write baseline (157.76 msvs140.79 ms) and added heap comparator complexity, so the code was reverted and only the read benchmark was kept. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/read-hot-smoke-20260501-035507/,/home/kom/tmp/clustered-write-synthetic/btree-prefix-sort-read-ab-proto-20260501-035531/, and/home/kom/tmp/clustered-write-synthetic/btree-prefix-sort-read-ab-clean-20260501-035714/.git diff --check, clean rebuild/install intotmp_install, regression slicetest_setup copy create_misc create_table create_index cluster,USE_TEMP_INSTANCE=true ... SCALE_VALUES=0.1 HOT_TILE_COUNT_VALUES=16 COPY_DIFF_FROM_FILE_VALUES=truesmoke, and a rejected moved-update clustered-placement prototype after addingread_updated_hot. The prototype kept lock ordering intact but regressed update medians to1191.28/1072.18 msclustered versus784.22/889.52 mscontrol without improving update locality enough, so the heap code was reverted and only the update-read benchmark and README note were kept. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/update-placement-proto-20260501-040654/and/home/kom/tmp/clustered-write-synthetic/update-read-clean-smoke-20260501-040857/.bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check,USE_TEMP_INSTANCE=true ... HOT_UPDATE_FRACTION_VALUES="0.75" UPDATES_BEFORE_INSERTS_VALUES="false true" SCALE_VALUES=0.02smoke, focused scale1, repeat3, fillfactor90/50hot-update matrix, and focused update-before A/B after adding hot-update and workload-order axes. The matrix showed fillfactor50keeps ordinary updates mostly local (95.13%clustered) but hot-biased updates still fail (update_hot0%clustered,15.63%control). Update-before improved clusteredupdate_hotto15.63%but regressed clusteredinsert_hotfrom28.31%to1.42%and insert median from204.08 msto345.64 ms, so it is recorded as a rejected standalone fix. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/hot-update-fillfactor-20260501-041447/,/home/kom/tmp/clustered-write-synthetic/update-before-ab-20260501-041913/, and/home/kom/tmp/clustered-write-synthetic/update-axis-final-smoke-20260501-042100/.git diff --check,make -s check-tests TESTS="test_setup copy create_misc create_table create_index cluster", and focused scale1, repeat3COPY benchmarks after skipping FSM lookup for batches already classified as too-hot for clustered target probing. This did not change locality (update_hotstill0%in the hot-update case), but improved dense COPY write cost in the measured cases: fillfactor50,hot_tile_count=128,hot_update_fraction=0.9clustered insert median204.08 ms->163.53 ms, and single-hot-key fillfactor90clustered insert median49.96 msversus38.81 mscontrol. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/skip-fsm-hot-prefix-proto-20260501-131256/and/home/kom/tmp/clustered-write-synthetic/skip-fsm-hot-prefix-single-20260501-131326/.bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check, a scale0.02temp-instance smoke, and two focused scale1, repeat2COPY matrices after addingUPDATE_PAYLOAD_REPEAT_VALUESplusavg_rows_measuredinlocality_summary.tsv. The hot-update candidate count is now visible (6272rows at scale1,hot_tile_count=128). With inserts first, clusteredupdate_hotstayed0%inside base for payload repeats8/16/32/64, while the control degraded with payload size (87.50%->15.63%). With updates first, payload8restored clusteredupdate_hotto87.50%, but clusteredinsert_hotfell from28.31%to0%, so payload size and phase order are diagnostic axes rather than standalone fixes. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/update-payload-axis-smoke-20260501-132633/,/home/kom/tmp/clustered-write-synthetic/update-payload-hot-matrix-20260501-132655/, and/home/kom/tmp/clustered-write-synthetic/update-payload-before-after-20260501-132803/.make -s -C src/backend/access/heap hio.o, backend rebuild/install,bash -n src/tools/clustered_write_bench/run_synthetic_bench.sh,git diff --check, a focused hot-update COPY A/B, a non-hot/hot COPY insert sweep, and a post-revert temp-instance smoke for a rejected prototype that disabled below-fillfactor clustered reserve use for all bulk/COPY clustered targets. The prototype proved the conflict: payload8recovered clusteredupdate_hotfrom0%to87.50%, and payload64to the control-like15.63%. It was still too broad: clusteredinsert_hotfell to0%inside base, and ordinary COPY insert locality also fell to0%for fillfactor90/50, with worse spans than control in several cases. The heap code was reverted and only README/PR notes were kept. Raw gzip outputs are under/home/kom/tmp/clustered-write-synthetic/no-bulk-reserve-proto-20260501-133332/,/home/kom/tmp/clustered-write-synthetic/no-bulk-reserve-nonhot-proto-20260501-133415/, and/home/kom/tmp/clustered-write-synthetic/post-no-bulk-reserve-revert-smoke-20260501-133741/.USE_TEMP_INSTANCE=true ... REPEATS=3 SCALE_VALUES=1 HOT_TILE_FRACTION_VALUES=0.9 HOT_TILE_COUNT_VALUES=128 HOT_UPDATE_FRACTION_VALUES=0.9 UPDATE_PAYLOAD_REPEAT_VALUES="8 64" ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true" COPY_DIFF_FROM_FILE_VALUES=trueafter the reserve experiments to retest ordered COPY specifically under hot insert+update pressure. Ordered input is not a non-hot cost fix, but in this hot-update shape it looks useful: payload8recovered clusteredupdate_hotfrom0%to86.88%, cut clustered insert median from350.89 msto107.58 ms, and improved broad hot reads (read_hotmedian22.02 ms->14.77 ms). Payload64recoveredupdate_hotfrom0%to15.50%and cut clustered insert median from249.40 msto129.00 ms. The explicit tradeoff is thatinsert_hotmostly moves to a compact tail (0.21%inside base), so this is a real osm2pgsql diff ordering direction to benchmark rather than a heap-locality win for new duplicate-key rows. Raw gzip output is under/home/kom/tmp/clustered-write-synthetic/hot-update-order-copy-ab-20260501-133942/.USE_TEMP_INSTANCE=true ... REPEATS=2 SCALE_VALUES=1 TEXT_KEY_VALUES=true HOT_TILE_FRACTION_VALUES=0.9 HOT_TILE_COUNT_VALUES=128 HOT_UPDATE_FRACTION_VALUES=0.9 UPDATE_PAYLOAD_REPEAT_VALUES="8 64" ORDER_DIFF_BY_CLUSTER_KEY_VALUES="false true" COPY_DIFF_FROM_FILE_VALUES=trueto check whether the ordered-COPY hot-update result transfers to generated text/geohash-like clustered keys. It does: payload8recovered clusteredupdate_hotfrom0%to86.88%, cut clustered insert median from370.74 msto134.47 ms, and improvedread_updated_hotfrom48.67 msto27.56 ms; payload64recoveredupdate_hotfrom0%to15.50%and cut insert median from347.72 msto135.43 ms. Broadread_hotwas noisier for text payload64, so the next real-osm2pgsql experiment should benchmark both append write time and read slices. Raw gzip output is under/home/kom/tmp/clustered-write-synthetic/hot-update-order-text-copy-ab-20260501-134352/.bash -n src/tools/clustered_write_bench/run_osm2pgsql_georgia_bench.sh,git diff --check, an append-only Georgia978slowlog run, and a follow-up single-read run after exposing the real benchmarkPG_WORK_MEMknob; the testedPGOPTIONS='-c work_mem=256MB'setting is now available asPG_WORK_MEM=256MB.Follow-ups
add a BRIN/range candidate path if PostgreSQL ever wants clustered-write placement from bitmap/range AMs rather than tuple scans
continue measuring the remaining clustered insert write cost against real osm2pgsql diffs; the current best direction is lazy or page-grouped candidate discovery rather than eager all-candidates-per-tuple work
continue reducing the remaining hot duplicate-key cost before claiming osm2pgsql diff imports are solved for heavily repeated geometry keys; the pre-count/short-circuit guards remove a large part of the cliff, but
HOT_TILE_FRACTION_VALUES="0.9"is still far slower than control for only a small hot-row locality wininvestigate a real overflow/range policy or prefix-grouped insertion path for massive duplicate-key diffs; the fillfactor and span sweeps show heap reserve helps moved updates but does not keep thousands of new rows local or compact when the original clustered key range is tiny
move the osm2pgsql experiment's final key-order rewrite into a global/external sort before COPY, then rerun the real Georgia + planet-diff matrix to see whether it keeps the locality/write win without the full rewrite and larger heap footprint
consider a more precise nearest-neighbour path for GiST opclasses that expose distance ordering
decide whether clustered writes should always follow
indisclusteredautomatically or be controlled by a table optionUpdated
osm2pgsql_cluster_during_import.patchfrom a post-import generated-key rewrite to an ordered staged-COPY experiment. COPY rows now go into per-target temporary tables createdLIKEthe real targetINCLUDING GENERATED, then drain into the real heap withORDER BY osm2pgsql_cluster_key, osm_idon sync. A short create+append smoke on system PG18/PostGIS verified generated keys stay stored/non-NULL and the clustered btree marker survives. The first per-target Georgia + planet daily diff978run on patched PG19/PostGIS 3.7 measured3:15.36create,3:10.19append,2793 MBdatabase size, reads of329.192 mspoint bbox /1140.398 msline bbox /1849.375 mspolygon bbox /182.531 msroads exact /3151.490 mspolygon exact, and p95 block/span3/2647.75line,3/1069point,3/1657polygon. Deferring the clustered btree until after the initial staged drain improved the same setup to2:10.01create,2:45.90append,2837 MB, reads of190.026 mspoint bbox /666.042 msline bbox /1106.330 mspolygon bbox /1890.598 msroads exact /1976.231 mspolygon exact, and p95 block/span3/2367.75line,3/1024point,3/1643polygon. This is better than the rejected single-temp-table fragment staging (6:06create,6:08append, long spans) and enough to keep deferred index creation, but still not good enough: write time remains worse than the previous generated-preserving rewrite target. Raw gzip logs are under/home/kom/tmp/clustered-write-osm2pgsql-ordered-deferred-index-20260501-143958/and/home/kom/tmp/clustered-write-osm2pgsql-ordered-target-stage-20260501-142615/; the rejected fragment-staging run is under/home/kom/tmp/clustered-write-osm2pgsql-ordered-stage-20260501-135818/.Rejected a plain-real-key osm2pgsql variant after benchmarking. It made the real
osm2pgsql_cluster_keya plain text column and kept the staging table generated, so the ordered drain could copy the already-computed key instead of recomputing it in the real table. Correctness passed on the Georgia978run (0NULL keys and0mismatches on point/line/polygon/roads; real columns were plain), but write time regressed to3:42.88create /4:18.57append with unchanged p95 block/span (3/2367.75,3/1024,3/1643). Reads were mixed: roads exact improved to462.948 ms, but bbox reads slowed and polygon exact regressed to3509.093 ms. The code experiment was reverted; only this negative result is documented. Raw gzip logs are under/home/kom/tmp/clustered-write-osm2pgsql-plain-key-stage-20260501-145922/.