Skip to content

[common] Order decimal z-values through the signed long transform - #9626

Open
LuciferYang wants to merge 3 commits into
apache:masterfrom
LuciferYang:fix/zindexer-decimal-order
Open

[common] Order decimal z-values through the signed long transform#9626
LuciferYang wants to merge 3 commits into
apache:masterfrom
LuciferYang:fix/zindexer-decimal-order

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Purpose

close #9625

ZIndexer's DECIMAL branch fed Decimal.toUnscaledBytes(), a minimal two's-complement array, into the fixed 8-byte z-value buffer through byteTruncateOrFill. That call left-aligns and zero-pads, and z-values are compared unsigned, so the encoding was not order-preserving. On DECIMAL(20,2), -1.00 sorted above 1.00, the longer 100.00 sorted below 1.00, and 0.00 became eight zero bytes, which is the all-zero null sentinel.

Decimals now get a fixed-width, sign-flipped big-endian two's-complement encoding of the unscaled value. The width is sized to the column's precision (8 bytes for precision up to 18, up to 16 for DECIMAL(38, x)), enough to hold any value the precision allows. That makes the key order-preserving under the unsigned comparison a z-value gets, and it keeps full resolution: small separated values, their negatives, and values past the long range all get distinct keys. A decimal column reserves that width in the z-value instead of a flat 8 bytes.

This only changes the transient sort key used while clustering, so nothing on disk changes format.

An earlier revision narrowed the unscaled value into a single long, first by clamping to the long bounds and then by a precision-based right shift. Both preserved order but lost resolution: a shift wide enough to fit the DECIMAL(38, 18) range maps every value below about 18.44 to one key, so a price or rate column on that schema stops contributing clustering information while still paying the rewrite cost. The fixed-width encoding avoids that.

Tests

TestZOrderByteUtil.testZIndexerDecimalOrdering builds a two-column DECIMAL(20, 2) ZIndexer and checks ordering by value, separation from the null sentinel, and a large magnitude against small positives and negatives.

TestZOrderByteUtil.testZIndexerHighPrecisionDecimalClustering walks a strictly ascending DECIMAL(38, 18) sequence covering negatives, sub-unit fractions, small integers, and a value past Long.MAX_VALUE, and asserts each z-key is strictly greater than the previous one, with null below all. It fails against the earlier shift revision, where the small values shared a single key.

mvn -pl paimon-common -Dtest=TestZOrderByteUtil test on JDK 11: 16 tests, 0 failures.

The DECIMAL branch wrote Decimal.toUnscaledBytes(), a variable-length
two's-complement array, into the fixed 8-byte z-value buffer, which is
compared unsigned. On DECIMAL(20,2) that put -1.00 above 1.00, put
100.00 below 1.00 because it is longer, and made 0.00 encode to the
all-zero null sentinel.

Encode the unscaled value with longToOrderedBytes, like the other
numeric types. Unscaled values wider than a long are clamped rather
than narrowed, so the mapping stays non-decreasing; the lower bound
stops one above Long.MIN_VALUE, which would encode to the sentinel.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decimal ordering problem has end-to-end value, but the proposed saturation introduces a clustering regression for supported high-precision decimal columns. The existing ordering test passes with this patch yet misses collisions between ordinary separated values.

: decimal.toBigDecimal()
.unscaledValue()
.max(MIN_UNSCALED)
.min(MAX_UNSCALED)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve high-precision decimal clustering instead of saturating at long bounds. For DECIMAL(38,18), ordinary values 10, 20 and 90 already exceed Long.MAX_VALUE after unscaling. With the second clustering column fixed at 0, an isolated base/head probe produces three distinct z-keys on the base but exactly the same key with this change. ZorderSorter uses the key directly, so the decimal column stops contributing clustering information throughout this range. Use a sortable fixed-width decimal encoding or a monotonic projection retaining high-order range information, and test separated values beyond both long bounds, rather than only their order relative to zero/null.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate submission caused by an ambiguous API response. Please use the original review: #9626 (review)

@JingsongLi
JingsongLi dismissed their stale review September 10, 2026 05:45

Duplicate of review 5163156610; the original changes-requested review and its substantive comment remain active.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requirement fit: SUPPORTED. Implementation: FINDINGS.

Replacing saturation fixes separation across some large magnitudes, but the fixed precision-based shift still removes clustering information for ordinary wide-decimal values. The remaining P2 is inline.

An exact base/head JDK 8 probe checked actual ZIndexer keys and the core ZorderSorter. For DECIMAL(38,18), 10,001 values in [0,10] at .001 increments went from 9,962 distinct keys to one. A 4,000-row repeated 1/2/3/10 workload changed from four value groups to mixed sorter output. These are real key/sorter checks, not a measured table-query performance multiplier. The PR description also still describes the earlier saturation implementation and should be updated with the final approach.

// a long without reaching Long.MIN_VALUE (whose z-value is the null sentinel). Compact
// decimals (p <= 18) always fit, so the shift is 0.
final int unscaledShift =
Math.max(0, BigInteger.TEN.pow(decimalType.getPrecision()).bitLength() - 63);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve decimal clustering resolution in ordinary value ranges

For DECIMAL(38,18), this shift is 64, so every nonnegative value below 18.446744073709551616 maps to long 0: 0/1/2/3/10 all have the same Z-key when the other column is fixed. Decimal.isCompact() depends on declared precision, so none escape via the compact branch. An exact base/head probe reduced 10,001 values in [0,10] from 9,962 keys to one, and the actual core ZorderSorter changed a repeated 1/2/3/10 input from four groups into mixed output because it compares only this key. Price/rate columns using a wide decimal schema therefore lose useful clustering while still paying the rewrite cost. Please use a monotonic representation with useful relative resolution (or a wider sortable decimal encoding), and test small separated values and negative counterparts as well as values beyond long bounds. The 10/20/90 test crosses the new coarse buckets and misses this regression.

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] Decimal z-order values are not order-preserving and zero collides with the null sentinel

2 participants