Skip to content

[common] Widen hilbert index bytes beyond 8 dimensions - #9777

Draft
LuciferYang wants to merge 6 commits into
apache:masterfrom
LuciferYang:fix/hilbert-index-byte-width
Draft

[common] Widen hilbert index bytes beyond 8 dimensions#9777
LuciferYang wants to merge 6 commits into
apache:masterfrom
LuciferYang:fix/hilbert-index-byte-width

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

close #9776

HilbertIndexer.hilbertCurvePosBytes padded the index to a fixed 63 bytes. ConvertBinaryUtil.paddingToNByte keeps the first n bytes when the array is longer, so it drops low-order bytes off a big-endian value. An N-dimensional 63-bit index needs up to 63*N/8 + 1 bytes, the extra byte covering BigInteger's sign byte when the top bit is set, so 63 was too narrow from 8 dimensions upward and the sort key silently lost resolution.

At 8 dimensions this is worse than lost resolution. The index fills exactly 63 bytes, so a value in the top half of the space serializes to 64 bytes with a leading 0x00. Truncating that back to 63 leaves the zero in front, and the key then sorts below the untruncated key of a numerically smaller index. Clustering does not just get coarser, it gets the order backwards for half the space.

The width is now 63*N/8 + 1 at every dimension count. Nothing needs the old shape: all three consumers (HibertSorter in paimon-core, HilbertSorter in paimon-flink, SparkHilbertUDF) use the key as a transient sort key and strip it before writing, so no data file, manifest or index file carries it, and two keys are only ever compared within one sort where the dimension count is fixed. Ordering is unaffected for 1 to 7 dimensions, where 63 bytes never truncated: those keys just stop carrying dozens of leading zero bytes, so a 2-dimension key goes from 63 bytes to 16.

Tests

HilbertIndexerTest.testHighDimensionIndexKeepsAllBits asserts the widths (16 for 2 dimensions, 64 for 8, 71 for 9, 127 for 16) and that points differing only in low-order bits stay distinct at 9 and 16 dimensions.

testEightDimensionKeysOrderLikeTheirIndex is the one that pins the ordering bug: it spreads 24 points across the space, recomputes each index with HilbertCurve directly, and asserts that comparing the encoded keys unsigned agrees with comparing the indexes for every pair.

Verified on JDK 11: all 3 tests pass with the change; restoring the 63-byte width for 8 dimensions and nothing else makes testEightDimensionKeysOrderLikeTheirIndex fail on a concrete pair, alongside the width assertion. paimon-common builds clean with checkstyle and spotless.

hilbertCurvePosBytes padded the N-dimensional 63-bit hilbert index
to a fixed 63 bytes. That is only enough for up to 8 dimensions —
and even there it truncates the low byte for the half of the space
whose index has the top bit set (BigInteger's sign byte), e.g. any
row with a NULL order column. With 9+ order columns — trivially
configurable via the hilbert sorter and the Spark Hilbert UDF — the
padding dropped entire low-order bytes, collapsing distinct points
into the same sort key.

Keep the legacy 63-byte width for up to 8 dimensions so existing
index bytes stay stable, and use the full 63*N/8 + 1 width beyond
that. Hilbert keys are transient in all current consumers, so
nothing persists the legacy shape.

Assisted-by: GLM-5.3
@LuciferYang
LuciferYang marked this pull request as draft September 13, 2026 03:07
LuciferYang and others added 5 commits September 13, 2026 12:44
Keeping the legacy 63-byte width for up to 8 dimensions preserved a real
defect rather than compatibility: at exactly 8 dimensions the index fills 63
bytes, so the top half of the space carries BigInteger's sign byte and spills
to 64, and truncating back to 63 leaves a leading zero that makes a large
index sort BELOW a smaller one. The keys are transient sort keys in every
consumer, so there is nothing to stay byte-compatible with; use 63*N/8 + 1
everywhere, which also shrinks a 2-dimension key from 63 bytes to 16.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Sizes and inequalities are proxies for the property the width exists to
guarantee. Assert new BigInteger(1, key) equals the index at 8 and 9
dimensions, where the last byte is exactly what the old width dropped.

Co-Authored-By: Claude Code <noreply@anthropic.com>
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.

[Bug] Hilbert index silently truncates low-order bytes with 9+ order columns

1 participant