Skip to content

Filter pushdown on float columns drops rows with NaN or -0.0 #101

Open

Description

@darshil929

Describe the bug

Some filters on double and float columns return fewer rows from an Iceberg table than DataFusion returns for the same data. The missing rows are the ones that hold NaN or -0.0. The query succeeds, but those rows aren't in the result.

The two sides compare these values differently:

  • DataFusion treats NaN as equal to itself and greater than any other value, and treats -0.0 as equal to 0.0. SELECT CAST('NaN' AS DOUBLE) > 1.0, CAST('NaN' AS DOUBLE) = CAST('NaN' AS DOUBLE), CAST('-0.0' AS DOUBLE) = 0.0 returns true for all three.
  • iceberg-rust's InclusiveMetricsEvaluator decides that a data file holding only NaN can't match >, >=, = or IN, and NaN is left out of a file's upper bound. iceberg-rust also orders floats with total_cmp, so to it -0.0 and 0.0 are different values.

scalar_value_to_datum passes float literals through as Datum::double, and the comparison reaches the scan unchanged. So the scan skips files that contain rows DataFusion would match. Those files are never read, and filtering again in DataFusion can't bring the rows back.

To Reproduce

Using the sqllogictest engine, with one INSERT per data file:

CREATE TABLE default.default.t (id INT NOT NULL, x DOUBLE);
INSERT INTO default.default.t VALUES (1, CAST('NaN' AS DOUBLE));
INSERT INTO default.default.t VALUES (2, 0.5), (3, CAST('NaN' AS DOUBLE));
INSERT INTO default.default.t VALUES (4, 2.0);
INSERT INTO default.default.t VALUES (5, CAST('-0.0' AS DOUBLE));
INSERT INTO default.default.t VALUES (6, 0.0);
CREATE TABLE m AS SELECT * FROM default.default.t;

SELECT id FROM <table> WHERE <filter> returns:

Filter Iceberg table t MemTable copy m
x > 1.0 4 1, 3, 4
x = CAST('NaN' AS DOUBLE) none 1, 3
x = 0.0 6 5, 6
x >= 0.0 2, 3, 4, 6 1, 2, 3, 4, 5, 6

The same table returns 1, 3, 4 for x + 0 > 1.0, which isn't pushed down.

Which rows go missing also depends on how they're laid out in files. Row 3 is skipped because its file's upper bound is 0.5. The same NaN in a file with 2.0 comes back.

Expected behavior

t returns the same rows as m.

I can think of two ways to get there:

  1. Stop pushing down comparisons and IN lists that have a float literal. That's the simplest, but float columns lose file pruning.
  2. Push a looser predicate that still covers every row DataFusion could match, and leave the exact check to DataFusion. For example, x > c becomes x > c OR x IS NAN and x = NaN becomes x IS NAN. Zero literals would cover both signs: x = 0.0 becomes x = -0.0 OR x = 0.0, x >= 0.0 becomes x >= -0.0, and x <= -0.0 becomes x <= 0.0.

Let me know which you'd prefer.

Additional context

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