andygrove opened a new issue, #26130:
URL: https://github.com/apache/datafusion/issues/26130
### Is your feature request related to a problem or challenge?
`ListingTable` builds a scan's file groups before any pruning by file
statistics. `list_files_for_regular_scan` collects each file's statistics
(`datafusion.execution.collect_statistics` is on by default), and then
`FileGroup::split_files` sorts the files by path and cuts them into contiguous
chunks. The statistics are only used for pruning later, when each partition
opens its files.
When the files are clustered by a filtered column, for example Hive-style
date directories or files written in date order, every file that can match sits
in a few neighbouring groups. Only the partitions that own those groups have
rows to read.
A small reproduction in `datafusion-cli`, where each file holds a disjoint
range of `d`:
```sql
COPY (SELECT value AS d FROM generate_series(0, 9) ORDER BY random()) TO
'/tmp/skew/f0.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(10, 19) ORDER BY random()) TO
'/tmp/skew/f1.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(20, 29) ORDER BY random()) TO
'/tmp/skew/f2.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(30, 39) ORDER BY random()) TO
'/tmp/skew/f3.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(40, 49) ORDER BY random()) TO
'/tmp/skew/f4.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(50, 59) ORDER BY random()) TO
'/tmp/skew/f5.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(60, 69) ORDER BY random()) TO
'/tmp/skew/f6.parquet' STORED AS PARQUET;
COPY (SELECT value AS d FROM generate_series(70, 79) ORDER BY random()) TO
'/tmp/skew/f7.parquet' STORED AS PARQUET;
SET datafusion.execution.target_partitions = 4;
CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION '/tmp/skew/';
EXPLAIN FORMAT INDENT SELECT d FROM t WHERE d >= 20 AND d < 40;
```
```text
FilterExec: d@0 >= 20 AND d@0 < 40
DataSourceExec: file_groups={4 groups: [[tmp/skew/f0.parquet,
tmp/skew/f1.parquet], [tmp/skew/f2.parquet, tmp/skew/f3.parquet],
[tmp/skew/f4.parquet, tmp/skew/f5.parquet], [tmp/skew/f6.parquet,
tmp/skew/f7.parquet]]}, projection=[d], file_type=parquet, predicate=d@0 >= 20
AND d@0 < 40, pruning_predicate=d_null_count@1 != row_count@2 AND d_max@0 >= 20
AND d_null_count@1 != row_count@2 AND d_min@3 < 40, required_guarantees=[]
```
Only `f2` and `f3` can match, and both are in the second group, so the other
three partitions have nothing to read. The scan's statistics also still count
all 8 files (`Rows=Inexact(80)` with `datafusion.explain.show_statistics =
true`), even though at most 20 rows can match.
In a single process, FileStream work stealing (#21351) mostly hides this,
because an idle partition picks up files from its siblings. Engines that run
each partition as a separate task, such as Ballista and datafusion-distributed,
can't rely on that (see
`datafusion.execution.enable_file_stream_work_stealing`, #23294). For them the
skew decides how long the stage takes. In apache/datafusion-ballista#2551,
TPC-H Q6 over a `lineitem` table stored in `l_shipdate=...` directories and
registered without partition columns had 26 of 32 scan tasks read no rows,
while the other 6 read every matching row.
### Describe the solution you'd like
Prune files against the statistics `ListingTable` has already collected,
during listing and before the files are split into groups. This would sit next
to the existing Hive partition pruning. `scan_with_args_inner` already
separates the non-partition filters from the partition filters. Today it only
uses them to decide whether a limit can be applied while listing.
- A file is dropped only when its statistics prove that no row can match.
Files without usable statistics are kept.
- There is no extra I/O, since the statistics have already been collected.
- `split_files`, and `repartition_file_scans` later on, then spread the
remaining files over the partitions, so no new balancing logic is needed.
- The scan's statistics only count the remaining files, which gives the
optimizer better estimates.
- Scans with a declared `output_partitioning` keep their layout, since their
groups line up with partitions. #25437 covers pruning for the Range case.
### Describe alternatives you've considered
- Assign files to groups round-robin instead of in contiguous runs. This
spreads the matching files too, but every partition still has to check and skip
files that could have been dropped at planning time, and the statistics stay
unpruned.
- Prune in a physical optimizer rule once `FilterPushdown` has put the
predicate on the scan. This works for any `FileScanConfig`, and it is what
Ballista is adding in apache/datafusion-ballista#2558. The rule has to regroup
the files itself, though, and rules that run before `FilterPushdown`, such as
`JoinSelection`, still see unpruned statistics.
- Eager pruning at planning time (#24944, #25328) reads Parquet footers and
page indexes to sharpen estimates. It is opt-in and skips scans with more than
`eager_pruning_file_limit` files (256 by default). File-level pruning from
statistics that were already collected is a cheaper first step and would
complement it.
### Additional context
- apache/datafusion-ballista#2551 has the full TPC-H Q6 task metrics.
- datafusion-contrib/datafusion-distributed#759 is another scan skew report
from an engine that runs with work stealing off.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]