Repository navigation
Conversation
This was referenced May 2, 2026
Closed
adriangb
force-pushed
the
adrian/wide-schema-perf
branch
3 times, most recently
from
June 8, 2026 18:56
97e0b4b to
0173350
Compare
adriangb
marked this pull request as ready for review
June 8, 2026 18:57
adriangb
force-pushed
the
adrian/wide-schema-perf
branch
from
June 8, 2026 19:50
0173350 to
41d7b4a
Compare
Contributor
|
are we still keen on this PR? i'm not as familiar with the context but can help review if needed |
adriangb
force-pushed
the
adrian/wide-schema-perf
branch
from
August 8, 2026 19:23
41d7b4a to
20382c4
Compare
adriangb
added a commit
to pydantic/datafusion
that referenced
this pull request
Aug 9, 2026
Building an `ArrowReaderMetadata` walks every leaf of the parquet schema to produce the arrow `Schema` + dremel field levels. For wide-schema files this O(N_columns) walk runs on every file open, once per query, even though the result is identical across queries for the same file. Cache it on `CachedParquetMetaData` (which already lives in the file metadata cache): - `arrow_reader_metadata()` lazily builds the base `ArrowReaderMetadata` via `parquet_to_arrow_schema_and_field_levels` + `ArrowReaderMetadata::from_field_levels` and memoises it in a `OnceLock`; warm hits are a cheap `Arc`-bump clone. - `coerced_arrow_reader_metadata()` memoises a single post-coercion build keyed by the supplied schema's `Arc` identity (the common case: every file coerces to the same table schema). - `CachedParquetFileReader` overrides `AsyncFileReader::get_arrow_reader_metadata` to serve both from cache, falling back to a fetch + `try_new` on miss. Depends on the arrow-rs primitives in apache/arrow-rs#9882 (`from_field_levels`, `parquet_to_arrow_schema_and_field_levels`, `ArrowReaderOptions` accessors, `AsyncFileReader::get_arrow_reader_metadata`). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
adriangb
added a commit
to pydantic/datafusion
that referenced
this pull request
Aug 9, 2026
This PR uses parquet API that only exists on apache/arrow-rs#9882, which has not merged and so is in no parquet release. Without a patch the workspace does not compile and CI cannot say anything useful about the change. The patch points at a throwaway branch that is the parquet 59.2.0 release tag plus apache#9882's four commits — deliberately not arrow-rs `main`, which has post-release changes (notably `arrow_schema::Metadata` replacing `HashMap<String, String>`) that DataFusion has not absorbed and which break `datafusion-common` before parquet is even reached. Revert this commit once apache#9882 has merged and DataFusion has bumped to a parquet release containing it. Green CI on this PR means "the change works against that API", not "this is mergeable". Co-Authored-By: Claude Opus 5 <[email protected]>
`parquet_column` previously did an O(N) linear scan over all parquet leaf columns to find the leaf belonging to a given root field. For wide schemas (hundreds/thousands of columns) this is called once per column, making per-file statistics setup O(N^2). Precompute a `root_to_first_leaf` map on `SchemaDescriptor` at construction so `parquet_column` is O(1). Expose it via `SchemaDescriptor::root_first_leaf_index`. Update the `memory_size` tests to account for the extra cache field. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add building blocks so callers (e.g. a metadata-caching reader) can construct an `ArrowReaderMetadata` from precomputed parts instead of re-walking every parquet leaf on each file open: - `parquet_to_arrow_schema_and_field_levels`: produce the arrow `Schema` and `FieldLevels` in a single schema walk. - `ArrowReaderMetadata::from_field_levels`: build an `ArrowReaderMetadata` from already-computed `(Schema, FieldLevels)`. - Public accessors on `ArrowReaderOptions` (`virtual_columns`, `supplied_schema`, `skip_arrow_metadata`) so a cache-aware reader can decide whether its cached arrow view applies. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a low-overhead `StatisticsConverter` constructor that takes an already-resolved `(arrow field, parquet leaf index)` pair, skipping the name-based `parquet_column` lookup that `try_new` performs. Lets callers that have precomputed the logical->leaf mapping build converters in O(1). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a `get_arrow_reader_metadata` method to `AsyncFileReader` whose default implementation delegates to `ArrowReaderMetadata::try_new`, and route `load_async` through it. Cache-aware readers can override it to return a precomputed/cached `ArrowReaderMetadata` (built via `from_field_levels`) instead of re-walking the parquet schema per open. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
adriangb
force-pushed
the
adrian/wide-schema-perf
branch
from
September 28, 2026 21:49
20382c4 to
6cc09fa
Compare
Contributor
|
Is this PR still useful? Should I try and find time to review it? Are there benchmarks we can run to show it performing? |
Contributor
Author
|
I think hold off for now, I should invest some time into it before you do |
alamb
marked this pull request as draft
October 9, 2026 19:38
Contributor
Thanks, converting to draft for now |
adriangb
added a commit
to pydantic/datafusion
that referenced
this pull request
Oct 16, 2026
…on (apache#26355) ## Which issue does this PR close? - Part of apache#21968. This revives apache#21987 by @adriangb, which was closed while stale and never reviewed. The two commits are his, rebased onto current `main`. The fix was never carried into apache#22829 or apache#22830, and the quadratic lookup is still on `main`. ## Rationale for this change Planning a query against a wide-schema Parquet table takes time quadratic in the number of columns, even when the query reads only one or two of them. `DFParquetMetadata::statistics_from_parquet_metadata` resolves every field in the logical file schema to a Parquet leaf column. Both branches resolve each field with an O(N) lookup: - When the file has statistics, it calls `StatisticsConverter::try_new`. That does a linear `Schema::column_with_name`, then calls `parquet_column`, which does a linear `Fields::find` and a linear scan for the first leaf under the matched root. - When the file has no statistics, it calls `parquet_column` directly. This happens once per field, so the cost is O(N²) per file, on every query plan, for every file in the table. ## What changes are included in this PR? - A new crate-private `LeafResolver` (`datafusion/datasource-parquet/src/leaf_resolver.rs`). It builds two maps once per file: Arrow field name to root index, and Parquet root index to first leaf index. Both call sites use it in place of the per-field linear lookups, which makes the statistics pass O(N) per file. - `LeafResolver` gives the same results as `parquet_column`, including its tie-breaks: the first Arrow field with a given name wins, and the lowest leaf index for a root wins. When a leaf exists, it builds the converter with `StatisticsConverter::from_column_index`, which yields the same converter `try_new` would (same leaf index, same Arrow field, same physical type). - A field with no Parquet leaf (a nested type, or a name missing from the file schema) falls back to `StatisticsConverter::try_new`. This reproduces its exact error for an unknown column, and it is needed because `from_column_index` cannot express "this column has no Parquet leaf". - The `parquet_metadata_statistics` benchmark now also covers 1024 columns, because the quadratic term only shows up past a few hundred columns. Rebasing onto `main` conflicted in `statistics_from_parquet_metadata`. The resolution keeps every change made on `main` since apache#21987, including the omitted-null-count handling and the untrusted min/max order checks, and changes only how each field's converter is built. **Known limitation:** schemas with many nested fields still take the O(N) fallback for those fields. apache/arrow-rs#9882 proposes a constructor that takes an `Option<usize>` leaf, which would remove the fallback. Flat wide schemas never take it. ## What is the testing strategy for this PR? - Seven unit tests in `leaf_resolver.rs`. Five of them check, for every field name plus some absent names, that `LeafResolver::leaf_index` returns exactly what `parquet_column` returns and that `LeafResolver::converter` matches `StatisticsConverter::try_new` (same leaf index, same Arrow field, same error text). The tie-break rules are the risky part: getting either backwards would silently attribute one column's statistics to another, and nothing in the existing suite covers duplicate names. The other two check specific leaf indices, and that a duplicated name uses the first field's type. The schemas cover: - primitives, a nested struct, a nested list, and a duplicate field name with a different type from the first - a primitive Arrow field over a multi-leaf Parquet group (lowest leaf wins) - a Parquet root with no leaves (an empty group) - more Arrow fields than Parquet roots - an empty schema - No `.slt` test: this changes how columns are looked up, not the statistics produced, so the unit tests compare directly against `parquet_column` and `try_new`. - Existing suites: `-p datafusion-datasource-parquet --lib`, `-p datafusion --test parquet_integration`, and the `parquet_statistics`, `listing_table_statistics`, `parquet_sorted_statistics` and `parquet_nested_schema_pruning` sqllogictest files. ### Benchmarks `wide_schema` (1024 vs 8 columns × 256 files), run locally on an AMD Ryzen 9 9950X3D. These are Criterion medians, `main` (e732212) against this branch: | Query | main | this PR | change | |---|---:|---:|---:| | Q01 wide | 694.5 ms | 522.9 ms | −24.7% | | Q02 wide | 528.0 ms | 350.3 ms | −33.7% | | Q03 wide | 529.2 ms | 353.4 ms | −33.2% | | Q04 wide | 617.2 ms | 445.5 ms | −27.8% | | Q01–Q04 narrow | — | — | within ±0.5% | For every wide query the confidence intervals do not overlap. The two sides ran one after the other on the same machine, not interleaved. ## Are there any user-facing changes? No. `LeafResolver` is crate-private and the statistics produced are unchanged. Planning against wide-schema Parquet tables is faster. --- This PR was prepared with AI assistance (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Adrian Garcia Badaracco <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]>
alamb
pushed a commit
to rxdrccq/datafusion
that referenced
this pull request
Oct 16, 2026
…on (apache#26355) ## Which issue does this PR close? - Part of apache#21968. This revives apache#21987 by @adriangb, which was closed while stale and never reviewed. The two commits are his, rebased onto current `main`. The fix was never carried into apache#22829 or apache#22830, and the quadratic lookup is still on `main`. ## Rationale for this change Planning a query against a wide-schema Parquet table takes time quadratic in the number of columns, even when the query reads only one or two of them. `DFParquetMetadata::statistics_from_parquet_metadata` resolves every field in the logical file schema to a Parquet leaf column. Both branches resolve each field with an O(N) lookup: - When the file has statistics, it calls `StatisticsConverter::try_new`. That does a linear `Schema::column_with_name`, then calls `parquet_column`, which does a linear `Fields::find` and a linear scan for the first leaf under the matched root. - When the file has no statistics, it calls `parquet_column` directly. This happens once per field, so the cost is O(N²) per file, on every query plan, for every file in the table. ## What changes are included in this PR? - A new crate-private `LeafResolver` (`datafusion/datasource-parquet/src/leaf_resolver.rs`). It builds two maps once per file: Arrow field name to root index, and Parquet root index to first leaf index. Both call sites use it in place of the per-field linear lookups, which makes the statistics pass O(N) per file. - `LeafResolver` gives the same results as `parquet_column`, including its tie-breaks: the first Arrow field with a given name wins, and the lowest leaf index for a root wins. When a leaf exists, it builds the converter with `StatisticsConverter::from_column_index`, which yields the same converter `try_new` would (same leaf index, same Arrow field, same physical type). - A field with no Parquet leaf (a nested type, or a name missing from the file schema) falls back to `StatisticsConverter::try_new`. This reproduces its exact error for an unknown column, and it is needed because `from_column_index` cannot express "this column has no Parquet leaf". - The `parquet_metadata_statistics` benchmark now also covers 1024 columns, because the quadratic term only shows up past a few hundred columns. Rebasing onto `main` conflicted in `statistics_from_parquet_metadata`. The resolution keeps every change made on `main` since apache#21987, including the omitted-null-count handling and the untrusted min/max order checks, and changes only how each field's converter is built. **Known limitation:** schemas with many nested fields still take the O(N) fallback for those fields. apache/arrow-rs#9882 proposes a constructor that takes an `Option<usize>` leaf, which would remove the fallback. Flat wide schemas never take it. ## What is the testing strategy for this PR? - Seven unit tests in `leaf_resolver.rs`. Five of them check, for every field name plus some absent names, that `LeafResolver::leaf_index` returns exactly what `parquet_column` returns and that `LeafResolver::converter` matches `StatisticsConverter::try_new` (same leaf index, same Arrow field, same error text). The tie-break rules are the risky part: getting either backwards would silently attribute one column's statistics to another, and nothing in the existing suite covers duplicate names. The other two check specific leaf indices, and that a duplicated name uses the first field's type. The schemas cover: - primitives, a nested struct, a nested list, and a duplicate field name with a different type from the first - a primitive Arrow field over a multi-leaf Parquet group (lowest leaf wins) - a Parquet root with no leaves (an empty group) - more Arrow fields than Parquet roots - an empty schema - No `.slt` test: this changes how columns are looked up, not the statistics produced, so the unit tests compare directly against `parquet_column` and `try_new`. - Existing suites: `-p datafusion-datasource-parquet --lib`, `-p datafusion --test parquet_integration`, and the `parquet_statistics`, `listing_table_statistics`, `parquet_sorted_statistics` and `parquet_nested_schema_pruning` sqllogictest files. ### Benchmarks `wide_schema` (1024 vs 8 columns × 256 files), run locally on an AMD Ryzen 9 9950X3D. These are Criterion medians, `main` (e732212) against this branch: | Query | main | this PR | change | |---|---:|---:|---:| | Q01 wide | 694.5 ms | 522.9 ms | −24.7% | | Q02 wide | 528.0 ms | 350.3 ms | −33.7% | | Q03 wide | 529.2 ms | 353.4 ms | −33.2% | | Q04 wide | 617.2 ms | 445.5 ms | −27.8% | | Q01–Q04 narrow | — | — | within ±0.5% | For every wide query the confidence intervals do not overlap. The two sides ran one after the other on the same machine, not interleaved. ## Are there any user-facing changes? No. `LeafResolver` is crate-private and the statistics produced are unchanged. Planning against wide-schema Parquet tables is faster. --- This PR was prepared with AI assistance (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Adrian Garcia Badaracco <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Part of #9722 and apache/datafusion#21968.
Rationale for this change
Two per-file costs in
parquetscale with schema width even when a reader touches only a few columns.1.
parquet_columnresolves a name with two linear scans. FirstFields::findover the arrow schema, then a scan over the parquet leaves to find the first one descended from the matched root. The second scan is pure waste: the root→first-leaf mapping is fixed for a givenSchemaDescriptorand can be computed once when it is built.2. Every file open re-walks every parquet leaf to build the arrow
Schemaand dremel field levels (ArrowReaderMetadata::try_new). A reader that already caches per-file metadata has no way to cache the derived arrow view alongside it, so the walk repeats on every open of the same file.Scope — what this PR does not do
It does not make
parquet_columnO(1), and it does not remove the O(N²) that callers hit when resolving every column of a file.Fields::findlives inarrow-schemaand is still a linear scan with string comparisons;StatisticsConverter::try_newcalls it twice (once directly, once insideparquet_column). Those two scans dominate, and eliminating them needs name indexing onFields, which is not attempted here.This PR removes the third scan — the cheapest of the three, being a sweep over a flat
Vec<usize>— and the repeated schema walks. Callers that want the quadratic term gone must hoist the name lookup themselves; apache/datafusion#21987 does exactly that, and no longer depends on this PR (it builds onStatisticsConverter::from_column_index, released in 59.1.0). So this PR should be judged on its own merits, not as the fix for that workload.What changes are included in this PR?
Four commits, each green on its own:
parquet_column—SchemaDescriptorprecomputes aroot_to_first_leafmap, exposed asroot_first_leaf_index. Costs 8 bytes per root field, reflected in the updatedmemory_sizeexpectations (+40 bytes for the 5-root test schema).parquet_to_arrow_schema_and_field_levels(one schema walk →(Schema, FieldLevels)),ArrowReaderMetadata::from_field_levels, and publicArrowReaderOptionsaccessors.StatisticsConverter::from_arrow_field— constructor taking an already-resolved(field, Option<leaf index>). Overlaps withfrom_column_index(introduce StatisticsConverter::from_column_index #9540, merged since this PR was opened) but is strictly more expressive:from_column_indextakes a bareusizeand so cannot represent a column with no parquet leaf — nested types, or schema evolution — which is precisely the case that forces callers back onto the O(N)try_new.AsyncFileReader::get_arrow_reader_metadata— new trait method, default delegating totry_new, withload_asyncrouted through it, so a cache-aware reader can return a fully-builtArrowReaderMetadataand skip the per-leaf walk.Are these changes tested?
Existing parquet tests pass with
--all-features, including thememory_sizeexpectations updated for the newSchemaDescriptorfield.parquet_column's behaviour is unchanged and is covered by the existing suite — the new map reproduces its tie-break (lowest leaf index for a root wins).Are there any user-facing changes?
New public API only, no breaking changes.
SchemaDescriptorgrows by 8 bytes per root field, which is visible throughParquetMetaData::memory_size— about 0.65% for a 1024-column schema.