Skip to content

[Bug] Z-order encoding of negative FLOAT and DOUBLE values is not order-preserving #9627

Description

@LuciferYang

Search before asking

  • I searched in the issues and found nothing similar.

Paimon version

master, 475be566f (2.1-SNAPSHOT).

Compute Engine

Flink and Spark. Reached from sys.compact with order_strategy => 'zorder', from write-path clustering, and from Spark's z-order sort, which calls the same two functions through SparkZOrderUDF.

Minimal reproduce step

Z-order clustering on a FLOAT or DOUBLE column. The transform that is supposed to make the bit pattern unsigned-comparable shifts by the wrong width:

public static ByteBuffer doubleToOrderedBytes(double val, ByteBuffer reuse) {
    ByteBuffer bytes = reuse(reuse, PRIMITIVE_BUFFER_SIZE);
    long lval = Double.doubleToLongBits(val);
    lval ^= ((lval >> (Integer.SIZE - 1)) | Long.MIN_VALUE);
    bytes.putLong(lval);
    return bytes;
}

lval is 64 bits, so the sign extension has to come from a shift of 63. Shifting right by 31 puts bits 31 through 61 of the pattern into the low half of the mask instead, so every value bit that lines up with a set bit there is inverted.

For negatives that means the magnitude is only partly inverted, and adjacent values come out reversed:

  • -2.5 encodes to 0x3ffbffff80080000
  • -2.5000000000000004, a strictly smaller value, encodes to 0x3ffbffff80080001

Positives are hit too, wherever two values differ at one of those bit positions:

  • 2.5 encodes to 0xc004000080080000
  • 2.5000000002328306, a strictly larger value, encodes to 0xc004000080000000

Walking 2.5 upward in steps of 2^19 ULPs, three of every five adjacent pairs come out inverted. floatToOrderedBytes widens to double and runs the same line, so floats are affected the same way.

What doesn't meet your expectations?

Values that sort one way have to encode the other way round for clustering to mean anything. With this transform, FLOAT and DOUBLE columns get a z-value ordering that is scrambled within each exponent range, so clustering and the min/max statistics computed over the sorted output do not help skipping. Results stay correct, since a z-value is only a sort key.

The same transform written correctly is a few files away, in SortUtil: putFloatNormalizedKey shifts an int by Integer.SIZE - 1 and putDoubleNormalizedKey shifts a long by Long.SIZE - 1. The z-order copy kept the int-sized constant on a long.

Anything else?

The existing tests testFloatOrdering and testDoubleOrdering only feed random.nextFloat() and random.nextDouble(), which land in [0, 1) and rarely differ at an affected bit, so nothing caught this.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions