Skip to content

Add datafusion.execution.soft_max_bytes_per_output_file config parameter - #24924

Open
Tpt wants to merge 2 commits into
apache:mainfrom
Tpt:tpt/soft_max_bytes_per_output_bytes
Open

Tpt wants to merge 2 commits into
apache:mainfrom
Tpt:tpt/soft_max_bytes_per_output_bytes

Conversation

@Tpt

@Tpt Tpt commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Rationale for this change

Allows to roughly limit the size of parquet files when the number of rows is a hard-to-use estimator.
This is especially useful when a column contains potentially large blobs.

What changes are included in this PR?

  • Adds the config parameter
  • Adds an implementation aligned with datafusion.execution.soft_max_rows_per_output_file config parameter
  • Adds a test

@github-actions github-actions Bot added documentation Improvements or additions to documentation core Core DataFusion crate common Related to common crate datasource Changes to the datasource crate labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion v55.1.0 (current)
       Built [  63.636s] (current)
     Parsing datafusion v55.1.0 (current)
      Parsed [   0.038s] (current)
    Building datafusion v55.1.0 (baseline)
       Built [  63.650s] (baseline)
     Parsing datafusion v55.1.0 (baseline)
      Parsed [   0.038s] (baseline)
    Checking datafusion v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.919s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 130.481s] datafusion
    Building datafusion-common v55.1.0 (current)
       Built [  35.348s] (current)
     Parsing datafusion-common v55.1.0 (current)
      Parsed [   0.069s] (current)
    Building datafusion-common v55.1.0 (baseline)
       Built [  35.449s] (baseline)
     Parsing datafusion-common v55.1.0 (baseline)
      Parsed [   0.070s] (baseline)
    Checking datafusion-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.952s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ExecutionOptions.soft_max_bytes_per_output_file in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:894

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  73.424s] datafusion-common
    Building datafusion-datasource v55.1.0 (current)
       Built [  45.434s] (current)
     Parsing datafusion-datasource v55.1.0 (current)
      Parsed [   0.034s] (current)
    Building datafusion-datasource v55.1.0 (baseline)
       Built [  44.530s] (baseline)
     Parsing datafusion-datasource v55.1.0 (baseline)
      Parsed [   0.036s] (baseline)
    Checking datafusion-datasource v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.411s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  92.000s] datafusion-datasource
    Building datafusion-datasource-arrow v55.1.0 (current)
       Built [  44.486s] (current)
     Parsing datafusion-datasource-arrow v55.1.0 (current)
      Parsed [   0.012s] (current)
    Building datafusion-datasource-arrow v55.1.0 (baseline)
       Built [  43.293s] (baseline)
     Parsing datafusion-datasource-arrow v55.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-datasource-arrow v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.096s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  89.615s] datafusion-datasource-arrow
    Building datafusion-datasource-avro v55.1.0 (current)
       Built [  44.780s] (current)
     Parsing datafusion-datasource-avro v55.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-avro v55.1.0 (baseline)
       Built [  44.280s] (baseline)
     Parsing datafusion-datasource-avro v55.1.0 (baseline)
      Parsed [   0.012s] (baseline)
    Checking datafusion-datasource-avro v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.099s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  90.744s] datafusion-datasource-avro
    Building datafusion-datasource-parquet v55.1.0 (current)
       Built [  49.437s] (current)
     Parsing datafusion-datasource-parquet v55.1.0 (current)
      Parsed [   0.036s] (current)
    Building datafusion-datasource-parquet v55.1.0 (baseline)
       Built [  50.262s] (baseline)
     Parsing datafusion-datasource-parquet v55.1.0 (baseline)
      Parsed [   0.037s] (baseline)
    Checking datafusion-datasource-parquet v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.232s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 101.477s] datafusion-datasource-parquet
    Building datafusion-sqllogictest v55.1.0 (current)
       Built [  99.772s] (current)
     Parsing datafusion-sqllogictest v55.1.0 (current)
      Parsed [   0.022s] (current)
    Building datafusion-sqllogictest v55.1.0 (baseline)
       Built [ 100.603s] (baseline)
     Parsing datafusion-sqllogictest v55.1.0 (baseline)
      Parsed [   0.027s] (baseline)
    Checking datafusion-sqllogictest v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.118s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 203.637s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 3, 2026
@Tpt
Tpt force-pushed the tpt/soft_max_bytes_per_output_bytes branch 2 times, most recently from 20cc5e6 to 94e622b Compare September 3, 2026 18:48
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Sep 3, 2026
@codecov-commenter

codecov-commenter commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.74611% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.94%. Comparing base (140c7c5) to head (cb03a02).

Files with missing lines Patch % Lines
datafusion/core/src/datasource/listing/table.rs 89.77% 1 Missing and 8 partials ⚠️
datafusion/datasource/src/write/demux.rs 93.61% 2 Missing and 1 partial ⚠️
datafusion/datasource-arrow/src/file_format.rs 80.00% 1 Missing ⚠️
datafusion/datasource/src/write/orchestration.rs 96.29% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #24924    +/-   ##
========================================
  Coverage   81.93%   81.94%            
========================================
  Files        1136     1136            
  Lines      429152   429298   +146     
  Branches   429152   429298   +146     
========================================
+ Hits       351633   351770   +137     
+ Misses      56475    56472     -3     
- Partials    21044    21056    +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

@Tpt,

Thanks for working on this. I like the goal of making output file sizing more practical when row counts are a poor proxy for size, especially for blob-heavy datasets.

I found one issue that I think needs to be addressed before merging. The new byte limit is currently based on Arrow RecordBatch memory size rather than the number of serialized bytes written to the output file. For Parquet, compression can make those two values very different, so the current behavior can rotate files much earlier than the documented output-file byte target suggests.

I also left one small test coverage suggestion for the new config option.

part_idx += 1;
}
row_counts[next_send_steam] += rb.num_rows();
bytes_counts[next_send_steam] += rb.get_array_memory_size();

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 think this needs to measure serialized output bytes rather than the Arrow allocation size. get_array_memory_size() gives us the pre-serialization RecordBatch size, but Parquet defaults to zstd(3), so a highly compressible blob batch could be hundreds of MiB in memory and only a small fraction of that on disk.

In that case, the demuxer would rotate before the next batch and could create much smaller files than the configured soft_max_bytes_per_output_file suggests. That also seems particularly relevant to the blob use case this option is intended to help with.

Could we move the accounting to a point where serialized or emitted bytes are known, and feed that back to the demuxer if needed? If the intent is instead to use input memory size as an estimate, I think the option and docs should be narrowed to make that explicit.

It would also be useful to add a regression test using a large, highly compressible binary column with Parquet compression enabled. The current parameterized test uses UNCOMPRESSED, so it would not catch this difference.

@Tpt Tpt Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow reply, got trapped by other things and thank you so much for the review

This is a great point. Done. I used a Arc<AtomicUsize> to record and report back the output size without blocking the code. An other approach might be to have an other channel giving back the written size but:

  • either the demuxer block on it and we lose must of the concurrency and pipelining advantage (I guess very much a no go)
  • we don't block and we get the same behavior of Arc<AtomicUsize> with more overhead and code

The downside of Arc<AtomicUsize> is indeed that the reporting might lag quite a lot. I have updated the documentation to highlight this.

Comment thread datafusion/common/src/config.rs Outdated
/// number of rows written is not roughly divisible by the soft max
pub soft_max_rows_per_output_file: ConfigNonZeroUsize, default = non_zero_usize_default(50000000)

/// Target number of bytes in output files when writing multiple.

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.

Could we also add a SET datafusion.execution.soft_max_bytes_per_output_file = 0 error case in set_variable.slt? ConfigNonZeroUsize already rejects zero, but adding coverage through the public SQL configuration path would match the existing row-limit coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Allows to roughly limit the size of parquet files when the number of rows is a hard-to-use estimator
@Tpt
Tpt force-pushed the tpt/soft_max_bytes_per_output_bytes branch from 94e622b to f9054bc Compare September 16, 2026 17:52
@Tpt
Tpt force-pushed the tpt/soft_max_bytes_per_output_bytes branch from f9054bc to cb03a02 Compare September 16, 2026 18:04

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

@Tpt,

Thanks for working on this. I like the direction of adding a byte-based soft limit, especially for workloads where row count is a poor proxy for file size.

I found one blocking issue with how the byte count is measured for Parquet. The current accounting is based on Arrow memory size rather than the encoded output size, which can cause highly compressible data to rotate files far too early. I also left one small suggestion to add SQL-path coverage for rejecting zero.

Once the byte accounting reflects serialized or emitted bytes, this should line up much better with the option's documented behavior.

&write_id,
part_idx,
&file_extension,
single_file_output,

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.

get_array_memory_size() measures the pre-serialization Arrow allocation rather than the number of bytes actually written to the output file. This is especially noticeable with Parquet, since the default compression is zstd(3). A highly compressible blob batch could occupy hundreds of MiB or even GiB in Arrow memory while producing a very small Parquet row group.

In that case, the demuxer can rotate before each following batch and create much smaller files than the configured soft_max_bytes_per_output_file target. That seems to conflict with the documented output-file-byte semantics and also affects the large-blob use case this option is intended to help with.

Could we move the accounting to a serialized or emitted-byte boundary, with feedback to the demuxer if needed? Alternatively, the option would need to be narrowed and renamed to make it clear that it is based on input memory size.

It would also be useful to add a regression test using a large, highly compressible Binary column with compressed Parquet. The current parameterized test forces UNCOMPRESSED, so it would not catch this difference.

/// engines should set this to `false`: the coordinated fallback is then
/// disabled for left-emitting multi-partition joins, which instead fail
/// with a resource-exhaustion error under memory pressure rather than
/// deadlocking. Single-partition and non-left-emitting joins are

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.

Could we also add the matching SET datafusion.execution.soft_max_bytes_per_output_file = 0 error case in set_variable.slt? ConfigNonZeroUsize already rejects zero, but adding coverage through the public SQL configuration path would keep this consistent with the sibling row-limit setting.

@Tpt

Tpt commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

@kosiew thank you for having a look again at this MR. I am a bit confused, your new comments seems to repeat the ones from your previous review and skip the changes pushed yesterday cb03a02

But maybe I am missing something, sorry if it's the case

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

Labels

auto detected api change Auto detected API change common Related to common crate core Core DataFusion crate datasource Changes to the datasource crate documentation Improvements or additions to documentation sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants