perf: don't manufacture an identity projection in ParquetSource - #24441
Open
Braedon-Wooding-Displayr wants to merge 1 commit into
Open
perf: don't manufacture an identity projection in ParquetSource#24441Braedon-Wooding-Displayr wants to merge 1 commit into
Braedon-Wooding-Displayr wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #24441 +/- ##
==========================================
+ Coverage 81.23% 81.28% +0.04%
==========================================
Files 1112 1112
Lines 390635 392081 +1446
Branches 390635 392081 +1446
==========================================
+ Hits 317350 318694 +1344
- Misses 54650 54675 +25
- Partials 18635 18712 +77 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`FileSource::projection()` returns `Option<&ProjectionExprs>`, but every
built-in source returned `Some(...)` unconditionally, and `ParquetSource::new`
manufactured an identity projection over the full table schema. Consumers in
`FileScanConfig` therefore always took the `Some` branch and did work
proportional to the schema width (projected schema, projection mapping,
statistics projection) even when the projection selected every column, in
order, under its own name.
`ParquetSource` now stores `Option<ProjectionExprs>` and leaves it `None`
until a projection is genuinely pushed down. `None` travels through the
morselizer and the per-file prepare stages to `DecoderProjection`, which
installs `ProjectionMask::all()` and no per-batch transform when the decoder's
own output already is the scan's output schema.
A concrete projection is still materialized where one is genuinely needed, and
per file rather than per partition: when partition or constant columns have to
be substituted as literals, when the file's schema does not match the table's
and casts or null fills have to land somewhere, and when the decoder's output
schema does not match the scan's for any other reason.
A pushdown that reproduces a source's own output is not a pushdown, and
detecting it belongs with the caller rather than with every source. Across the
sqllogictest corpus 871 of them were being performed, 663 of those into
ParquetSource. `projection_is_no_op` tests the incoming projection against the
source's current output, whether that output is the table schema or an existing
projection's aliases, and the callers of `FileSource::try_pushdown_projection`
skip the push when it holds. csv, arrow, avro and json are not converted here:
they still build an identity `SplitProjection` up front and still return
`Some(...)`, so they gain only the skipped pushdown, not the consumer-side
saving. Converting them is left for a follow-up.
`FileScanConfig::try_swapping_with_projection` reports success with the scan
unchanged rather than `Ok(None)`, so the caller still drops the redundant
`ProjectionExec`.
`FileScanConfig::partition_statistics` was not equivalent between its two
branches: `ProjectionExprs::project_statistics` recomputes `total_byte_size`
from the output schema, and the unprojected branch did not. Both branches now
recompute it, so a scan reports the same statistics whether or not a
projection was pushed.
## Benchmarks
The new `parquet_wide_scan` bench measures building the physical scan for an
unprojected parquet table, which is the work proportional to the table's width:
scan_construction/unprojected_1000_columns 456 us -> 7.0 us (-98.5%)
scan_construction/unprojected_10000_columns 4.61 ms -> 77.3 us (-97.9%)
scan_construction/unprojected_100000_columns 59.0 ms -> 1.26 ms (-97.8%)
End-to-end there is no measurable change, and the bench's `planning` and
`execution` groups are controls that show this rather than claim otherwise.
Physical planning of `SELECT *` is dominated by expanding the wildcard and
running the optimizer over one expression per column, and a full scan is
dominated by decoding, so the removed per-file `project_schema`, per-leaf mask
vector and per-batch `Projector` do not surface above the noise floor, measured
at roughly +/-10% by comparing the baseline binary against its own results.
Braedon-Wooding-Displayr
force-pushed
the
perf/no-identity-projection
branch
from
August 18, 2026 06:26
44b00e6 to
db5437b
Compare
Contributor
Author
|
Pushed some improvements to cover a missing line that we should cover the rest of the lines are just |
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.
FileSource::projection()returnsOption<&ProjectionExprs>, but every built-in source returnedSome(...)unconditionally, andParquetSource::newmanufactured an identity projection over the full table schema. Consumers inFileScanConfigtherefore always took theSomebranch and did work proportional to the schema width (projected schema, projection mapping, statistics projection) even when the projection selected every column, in order, under its own name.ParquetSourcenow storesOption<ProjectionExprs>and leaves itNoneuntil a projection is genuinely pushed down.Nonetravels through the morselizer and the per-file prepare stages toDecoderProjection, which installsProjectionMask::all()and no per-batch transform when the decoder's own output already is the scan's output schema.A concrete projection is still materialized where one is genuinely needed, and per file rather than per partition: when partition or constant columns have to be substituted as literals, when the file's schema does not match the table's and casts or null fills have to land somewhere, and when the decoder's output schema does not match the scan's for any other reason.
A pushdown that reproduces a source's own output is not a pushdown, and detecting it belongs with the caller rather than with every source. Across the sqllogictest corpus 871 of them were being performed, 663 of those into ParquetSource.
projection_is_no_optests the incoming projection against the source's current output, whether that output is the table schema or an existing projection's aliases, and the callers ofFileSource::try_pushdown_projectionskip the push when it holds, so csv, arrow, avro and json get the same benefit without being converted.FileScanConfig::try_swapping_with_projectionreports success with the scan unchanged rather thanOk(None), so the caller still drops the redundantProjectionExec.FileScanConfig::partition_statisticswas not equivalent between its two branches:ProjectionExprs::project_statisticsrecomputestotal_byte_sizefrom the output schema, and the unprojected branch did not. Both branches now recompute it, so a scan reports the same statistics whether or not a projection was pushed.Which issue does this PR close?
Happy to raise a bug for this, up to you? Just let me know / if it's a bug or a feature.
Rationale for this change
The new
parquet_wide_scanbench measures building the physical scan for an unprojected parquet table, which is the work proportional to the table's width:End-to-end there is no measurable change, and the bench's
planningandexecutiongroups are controls that show this rather than claim otherwise. Physical planning ofSELECT *is dominated by expanding the wildcard and running the optimizer over one expression per column, and a full scan is dominated by decoding, so the removed per-fileproject_schema, per-leaf mask vector and per-batchProjectordo not surface above the noise floor, measured at roughly +/-10% by comparing the baseline binary against its own results.The reason though is that I have a custom SQL command/node that handles a
SELECT *without having to expand the wildcard but this still results in relatively slow queries due to this physical overhead (as you can see 60ms is heavy! And we have upwards of 250k columns).What changes are included in this PR?
New benchmark + new tests + propagating None through scans.
Are these changes tested?
Yes!
Are there any user-facing changes?
We actually use None as a valid result from a projection from scans, this means that any consumer code of this (like analyzers and such) have to accept this as meaning an identity scan. This functionally doesn't seem like an issue.