Skip to content

fix: validate Parquet ordering across row groups - #27163

Open
rank-p wants to merge 2 commits into
apache:mainfrom
rank-p:fix/parquet-row-group-ordering
Open

rank-p wants to merge 2 commits into
apache:mainfrom
rank-p:fix/parquet-row-group-ordering

Conversation

@rank-p

@rank-p rank-p commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Related to lakehq/sail#2730.

Rationale for this change

Parquet sort declarations apply to individual row groups. Using the first group's declaration for the whole file can remove a required sort. For groups [1, 4] and [2, 3], ORDER BY a ASC NULLS FIRST LIMIT 2 returned [1, 4] instead of [1, 2].

What changes are included in this PR?

Keep only the common leading sort keys whose row-group boundaries are ordered. Use typed bounds that account for NULLs, comparison order, and schema mapping. Missing or incompatible metadata leaves the required sort in place.

What is the testing strategy for this PR?

  • Query regressions in core/tests/parquet/ordering.rs fail on the old code and pass with the fix, including ORDER BY ... LIMIT.
  • Query and metadata tests cover valid multi-group files, NULLs, multiple keys, missing metadata, projections, and floating-point order.
  • 301 Parquet crate tests, 13,535 extended Rust tests, and 546 SQL test files pass. Full-workspace Clippy passes with all features enabled.

Are there any user-facing changes?

Correct ordering and row selection when reading Parquet files whose row groups are sorted but the whole file is not. No public API changes.

AI assistance: Codex.

@github-actions github-actions Bot added core Core DataFusion crate datasource Changes to the datasource crate labels Nov 6, 2026
@xudong963
xudong963 self-requested a review November 6, 2026 14:05
@alamb
alamb requested a review from zhuqi-lucas November 7, 2026 12:14
@alamb

alamb commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

@zhuqi-lucas could you perhaps help review this PR (or help find someone that could)?

@zhuqi-lucas zhuqi-lucas 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.

Thanks @rank-p for the fix, and @alamb for the ping. I also ran a few row group layouts locally (overlapping groups, ties on the first key, NULLs in a later group, DESC and mixed ASC/DESC keys, strings, timestamps, floats with and without NaN): the inferred ordering and the results were all correct.

|| group
.column(column_index)
.statistics()
.and_then(ParquetStatistics::nan_count_opt)

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.

Files whose float statistics have no nan_count (e.g. written by pyarrow 23, which I checked locally) used to keep a float sort key and now lose it, so the SortExec comes back for them. That seems right since NaNs are not in min/max, but it may be worth a line in the upgrade guide.

@rank-p

rank-p commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Hi @xudong963, I see that you self-requested a review, are you able to take a look at this?

@alamb alamb 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.

Thank you for this PR @rank-p

Can we please break this PR up into smaller pieces so we can more easily undertand the implications of the changes?

This looks to me like it will disable floating point statisics in some cases, for example, as well as potentially try to handle statistics for nested strucutres.

Also the tests are really hard to follow as they have a lot of repeated boilerplate that obscures what is desired

}

#[tokio::test]
async fn test_parquet_unordered_row_groups_require_sort() -> Result<()> {

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.

I found these tests very hard to read -- is there some way we can convert them to mostly/all sqllogictests?

Or at least insta snapshot tests?

for example, a test that shows:

  1. Input (with relevant sorting)
  2. Query that calls for sorting
  3. Verifificion of the correct results and plan


// Find the column in the arrow schema
let (index, _) = arrow_schema.column_with_name(name)?;
/// Refine boundaries still tied on earlier keys. A strict earlier boundary

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.

this code feels much more complicated than necessary to me.

Can you add some exampels of what it is actually doing?

return Ok(None);
};
let resolver = LeafResolver::new(parquet_schema, &physical_schema);
let mut ordering = Vec::with_capacity(sorting_columns.len());

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.

is this code to handle sorting of nested columns new? Is it needed for this PR? If so, I think we should have end to end slt/sql examples that show what is going on

Some(PhysicalSortExpr::new(expr, options))
})
.collect()
fn float_bounds_exclude_zero(

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.

CHanging the sorting statistics for floating point columns seems like a substantial new behavior as well

@rank-p
rank-p force-pushed the fix/parquet-row-group-ordering branch from 7ae491b to 817963e Compare November 13, 2026 11:13
@rank-p

rank-p commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @alamb, thanks a lot for the review, I really appreciate it. I scoped this PR down.

The core logic in metadata.rs is now:

  • Get all sorting_columns from row_groups (check if they are equal)
  • Iterate through row_groups, check if bounds are sorted.
  • If bounds are equal --> decide sorting decision with the next column

If statistics are unknown the file is labeled as not ordered. This could be the case if null counts are missing.

One question from my side: For the snapshot tests, is this how you are usually doing this?
Concretely, this tests would fail e.g. if the default plan is changed and instead of TopK the plan would use something else. Not sure if this is a problem or an accepted maintenance cost.

Sqllogictest can't test this behavior as it can't write a file like this (sorting_columns specified but row_groups not in order). Which makes me think if this is even worth fixing at all.

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

Labels

core Core DataFusion crate datasource Changes to the datasource crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ORDER BY can return wrong results across individually sorted Parquet row groups

3 participants