Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
6ecd42b
Make filtered coalescing faster for primitive / byte types
Dandandan Dec 4, 2025
a8df36f
Make filtered coalescing faster for primitive types
Dandandan Dec 4, 2025
f20702b
Faster api
Dandandan Dec 4, 2025
124b4e3
Faster api
Dandandan Dec 4, 2025
79bd847
Faster api
Dandandan Dec 4, 2025
b2fc66f
Faster api
Dandandan Dec 4, 2025
0872a9b
Cleanup
Dandandan Dec 4, 2025
b7b3f18
Fix?
Dandandan Dec 4, 2025
7758889
optimize
Dandandan Dec 4, 2025
dcf4864
perf
Dandandan Dec 5, 2025
87626d1
comment
Dandandan Dec 5, 2025
7c46a72
Increase filter threshold
Dandandan Dec 6, 2025
6ee1f04
Adapt comment
Dandandan Dec 6, 2025
c39a455
More speed
Dandandan Dec 6, 2025
dc0c45e
Fmt
Dandandan Dec 6, 2025
d2b5d29
Don't collect
Dandandan Dec 6, 2025
18cf6fc
fix comments
Dandandan Dec 8, 2025
ddd0306
not unsafe
Dandandan Dec 9, 2025
82acfe1
not unsafe
Dandandan Dec 9, 2025
1acccc7
faster null handling
Dandandan Dec 9, 2025
bb025cf
not unsafe
Dandandan Dec 9, 2025
ca19422
Update arrow-select/src/filter.rs
Dandandan Dec 13, 2025
f718f2e
Update arrow-select/src/filter.rs
Dandandan Dec 13, 2025
b235243
Move / optimize
Dandandan Dec 15, 2025
ae995ba
Merge branch 'main' into coalesce_batches_filter
Dandandan Jan 8, 2026
e8919b1
docs: Update release schedule in README.md (#9111)
alamb Jan 8, 2026
46484ea
feat: add benchmarks for json parser (#9107)
Weijun-H Jan 8, 2026
3022aa6
chore: switch test from `bincode` to maintained `postcard` crate (RUS…
alamb Jan 8, 2026
4eb65a0
Speed up binary kernels (30% faster `and` and `or`), add `BooleanBuff…
alamb Jan 9, 2026
b2f9e42
[Variant] Optimize the object header generation logic in ObjectBuilde…
klion26 Jan 9, 2026
6bfd685
Update readme for geospatial crate (#9124)
paleolimbot Jan 9, 2026
0f994fa
Remove parquet arrow_cast dependency (#9077)
tustvold Jan 9, 2026
72c356a
Updated arrow-pyarrow to use pyo3 0.27, updated deprecated code warni…
hntd187 Jan 9, 2026
432b760
Fix clippy (#9130)
alamb Jan 10, 2026
a4dee8a
fix: display `0 secs` for empty DayTime/MonthDayNano intervals (#9023)
Jefffrey Jan 10, 2026
9927454
Fix IPC roundtripping dicts nested in ListViews (#9126)
brancz Jan 10, 2026
4a3ce6a
docs(parquet): add example for preserving dictionary encoding (#9116)
AndreaBozzo Jan 10, 2026
c587cf0
Add options to skip decoding `Statistics` and `SizeStatistics` in Par…
etseidl Jan 10, 2026
69dbab2
[arrow] Minimize allocation in GenericViewArray::slice() (#9016)
maxburke Jan 10, 2026
e28c305
bench: added to row_format benchmark conversion of 53 non-nested colu…
rluvaton Jan 10, 2026
077ad74
doc: fix link on FixedSizeListArray doc (#9033)
Jefffrey Jan 10, 2026
f7c430d
Docs: Add additional documentation and example for `make_array` (#9112)
alamb Jan 10, 2026
923c2b2
Change FlightSQLClient to return `FlightError` & cleanup code (#8916)
lewiszlw Jan 10, 2026
6d26fbc
Avoid overallocating arrays in coalesce primitives / views (#9132)
Dandandan Jan 10, 2026
91adb91
perf: optimize hex decoding in json (1.8x faster in binary-heavy) (#9…
Weijun-H Jan 10, 2026
d807503
Add nullif_kernel benchmark (#9089)
alamb Jan 11, 2026
db37aa1
perf: Avoid ArrayData allocation in PrimitiveArray::reinterpret_cast …
alamb Jan 11, 2026
6234ee0
Add `BooleanBufferBuilder::extend_trusted_len` (#9137)
Dandandan Jan 11, 2026
924e1fe
docs: Improve main README.md and highlight community (#9119)
alamb Jan 12, 2026
8485edf
fix: support cast from `Null` to list view/run encoded/union types (#…
Jefffrey Jan 12, 2026
9ee3cf1
docs(variant): fix VariantObject::get documentation to reflect Option…
mohit7705 Jan 12, 2026
684bc9f
Avoid clones in `make_array` for `StructArray` and `GenericByteViewAr…
alamb Jan 12, 2026
b41cd0d
Uncomment part of test_utf8_single_column_reader_test (#9148)
sdf-jkl Jan 13, 2026
66c1dae
Update ASF copyright year in NOTICE (#9145)
mohit7705 Jan 13, 2026
94317f7
fix:[9018]Fixed RunArray slice offsets (#9036)
manishkr Jan 13, 2026
717ed06
arrow-ipc: Add tests for nested dicts for Map and Union arrays (#9146)
brancz Jan 13, 2026
a064327
feat: add `reserve` to `Rows` (#9142)
rluvaton Jan 13, 2026
b8322ce
perf: improve calculating length performance for view byte array in r…
rluvaton Jan 13, 2026
ac1afae
[Parquet] perf: Create `PrimitiveArray`s directly rather than via `Ar…
alamb Jan 13, 2026
bf63ec5
doc: add example of RowFilter usage (#9115)
sonhmai Jan 13, 2026
5d5d1bf
[Parquet] perf: Create StructArrays directly rather than via `ArrayDa…
alamb Jan 13, 2026
9f43539
perf: improve field indexing in JSON StructArrayDecoder (1.7x speed u…
Weijun-H Jan 13, 2026
c3b76dc
Minor: try and avoid an allocation creating `GenericByteViewArray` fr…
alamb Jan 14, 2026
4bdccc2
Avoid a clone when creating `BooleanArray` from ArrayData (#9159)
alamb Jan 14, 2026
90c0a39
Avoid a clone when creating StringArray/BinaryArray from ArrayData (#…
alamb Jan 14, 2026
87fe9c8
perf: improve calculating length performance for `GenericByteArray` i…
rluvaton Jan 14, 2026
a9444d9
perf: improve calculating length performance for nested arrays in row…
rluvaton Jan 14, 2026
7d1223d
lint: remove unused function (fix clippy (#9178)
rluvaton Jan 14, 2026
91fe6f7
Add `TimestampWithOffset` canonical extension type (#8743)
serramatutu Jan 14, 2026
efeeded
feat: add null comparison handling in make_comparator (#9150)
Weijun-H Jan 14, 2026
b850d6f
docs: update examples in ArrowReaderOptions to use in-memory buffers …
AndreaBozzo Jan 14, 2026
2380b11
refactor: streamline date64 tests (#9165)
cht42 Jan 14, 2026
5678415
Improve `ArrowReaderBuilder::with_row_filter` documentation (#9153)
alamb Jan 14, 2026
4951e5b
add `#[inline]` to `BitIterator` `next` function (#9177)
rluvaton Jan 14, 2026
a4a49d1
Merge
Dandandan Jan 15, 2026
e78fa28
fix missing utf8 check for conversion from BinaryViewArray to StringV…
alamb Jan 14, 2026
e2ad508
Add find_nth_set_bit_position (#9151)
Dandandan Jan 15, 2026
414b3b9
Merge
Dandandan Jan 15, 2026
840653b
Merge
Dandandan Jan 15, 2026
a1d4097
Doc fix
Dandandan Jan 15, 2026
9773da7
Merge branch 'main' into coalesce_batches_filter
Dandandan Jan 16, 2026
bd65f64
Implement fallback copy_with_filter
alamb Jan 17, 2026
ec7ef9e
Merge pull request #13 from alamb/alamb/batches_filter_more
Dandandan Jan 17, 2026
e76a5d7
Fix nulls
Dandandan Jan 17, 2026
58190b8
Optimize
Dandandan Jan 17, 2026
be6b796
Use 0.5
Dandandan Jan 18, 2026
f0b98d6
Fix
Dandandan Jan 18, 2026
9aaca1f
Fix
Dandandan Jan 18, 2026
16f5d86
Merge branch 'main' into coalesce_batches_filter
alamb Jan 18, 2026
a27e4ab
Fix clippy
alamb Jan 18, 2026
6de89a3
Merge remote-tracking branch 'apache/main' into coalesce_batches_filter
alamb Jan 18, 2026
2b8711f
Avoid an Arc::clone
alamb Jan 19, 2026
12c526b
WIP byteview
Dandandan Jan 19, 2026
310404c
Merge pull request #16 from alamb/alamb/less_clone
Dandandan Jan 19, 2026
92b1b46
WIP byteview
Dandandan Jan 19, 2026
c9ae743
Revert "WIP byteview"
Dandandan Jan 19, 2026
5ae319f
WIP byteview
Dandandan Jan 19, 2026
bd4a1bc
Style
Dandandan Jan 19, 2026
99772cf
Remove slice for filterpredicate
Dandandan Jan 19, 2026
63443da
WIP
Dandandan Jan 20, 2026
b9efe0c
WIP
Dandandan Jan 20, 2026
cbaaf98
WIP
Dandandan Jan 20, 2026
0dbd786
WIP
Dandandan Jan 20, 2026
0a5a575
WIP
Dandandan Jan 20, 2026
3fbd560
Optimize
Dandandan Jan 20, 2026
d3cf7cb
Merge branch 'main' into coalesce_batches_filter
Dandandan Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions arrow-select/src/coalesce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
//!
//! [`filter`]: crate::filter::filter
//! [`take`]: crate::take::take
use crate::filter::filter_record_batch;
use crate::filter::{
FilterBuilder, FilterPredicate, IndexIterator, filter_record_batch,
is_optimize_beneficial_record_batch,
};
use arrow_array::types::{BinaryViewType, StringViewType};
use arrow_array::{Array, ArrayRef, BooleanArray, RecordBatch, downcast_primitive};
use arrow_schema::{ArrowError, DataType, SchemaRef};
Expand Down Expand Up @@ -211,7 +214,10 @@ impl BatchCoalescer {
/// Push a batch into the Coalescer after applying a filter
///
/// This is semantically equivalent of calling [`Self::push_batch`]
/// with the results from [`filter_record_batch`]
/// with the results from [`filter_record_batch`], but avoids
/// materializing the intermediate filtered batch.
///
/// [`filter_record_batch`]: crate::filter::filter_record_batch
///
/// # Example
/// ```
Expand All @@ -237,10 +243,101 @@ impl BatchCoalescer {
batch: RecordBatch,
filter: &BooleanArray,
) -> Result<(), ArrowError> {
// TODO: optimize this to avoid materializing (copying the results

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.

🎉

// of filter to a new batch)
let filtered_batch = filter_record_batch(&batch, filter)?;
self.push_batch(filtered_batch)
// We only support primitve now, fallback to filter_record_batch for other types
// Also, skip optimization when filter is not very selective

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.

Not sure if always better to take into account biggest_coalesce_batch_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.

Yeah, I do think it would be good to take into account biggest_coalesce_batch_size but maybe we can do so as a follow on PR

if batch
.schema()
.fields()
.iter()
.any(|field| !field.data_type().is_primitive())

@Dandandan Dandandan Jan 17, 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.

Because of the all-or-nothing check it has no effect on tpch/clickbench performance yet. I am not sure if we can somehow use the conventional filter kernel for non-supported arrays 🤔

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.

Yeah I am thinking about that as well

|| self

@Dandandan Dandandan Jan 17, 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.

I think benchmarks show it is better to just use the kernel always (so I think this condition needs to go).

.biggest_coalesce_batch_size
.map(|biggest_size| filter.true_count() > biggest_size)
.unwrap_or(false)
{
let batch = filter_record_batch(&batch, filter)?;

self.push_batch(batch)?;
return Ok(());
}

// Build an optimized filter predicate that chooses the best iteration strategy
let is_optimize_beneficial = is_optimize_beneficial_record_batch(&batch);
let selected_count = filter.true_count();

// Fast path: skip if no rows selected
if selected_count == 0 {
return Ok(());
}

// Fast path: if all rows selected, just push the batch
if selected_count == batch.num_rows() {
return self.push_batch(batch);
}

let (_schema, arrays, _num_rows) = batch.into_parts();

// Setup input arrays as sources
assert_eq!(arrays.len(), self.in_progress_arrays.len());
self.in_progress_arrays
.iter_mut()
.zip(&arrays)
.for_each(|(in_progress, array)| {
in_progress.set_source(Some(Arc::clone(array)));
});
Comment thread
Dandandan marked this conversation as resolved.

// Choose iteration strategy based on the optimized predicate
self.copy_from_filter(filter, is_optimize_beneficial, selected_count)?;

// Clear sources to allow memory to be freed
for in_progress in self.in_progress_arrays.iter_mut() {
in_progress.set_source(None);
}

Ok(())
}

/// Helper to copy rows at the given indices, handling batch boundaries efficiently
///
/// This method batches the index iteration to avoid per-row batch boundary checks.
fn copy_from_filter(
&mut self,
filter: &BooleanArray,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why getting BooleanArray and not BooleanBuffer? does nulls have meaning here?

is_optimize_beneficial: bool,
count: usize,
) -> Result<(), ArrowError> {
let mut remaining = count;
let mut offset = 0;

// We need to process the filter in chunks that fit the target batch size
while remaining > 0 {
let space_in_batch = self.target_batch_size - self.buffered_rows;
let to_copy = remaining.min(space_in_batch);

let sliced_filter = filter.slice(offset, to_copy);
let mut filter = FilterBuilder::new(&sliced_filter);

if is_optimize_beneficial {
filter = filter.optimize();
}
let chunk_predicate = filter.build();

// Copy all collected indices in one call per array
for in_progress in self.in_progress_arrays.iter_mut() {
in_progress.copy_rows_by_filter(&chunk_predicate)?;
}
Comment on lines +326 to +329

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A performance improvement you can do here is copy X columns at a time like I did and explained in 4:

the number 4 is a magic number, but you can pick other number like 2 to amortize the cost of boolean iterations


self.buffered_rows += to_copy;
offset += to_copy;
remaining -= to_copy;

// If we've filled the batch, finish it
if self.buffered_rows >= self.target_batch_size {
self.finish_buffered_batch()?;
}
}

Ok(())
}

/// Push all the rows from `batch` into the Coalescer
Expand Down Expand Up @@ -571,6 +668,15 @@ trait InProgressArray: std::fmt::Debug + Send + Sync {
/// Return an error if the source array is not set
fn copy_rows(&mut self, offset: usize, len: usize) -> Result<(), ArrowError>;

/// Copy rows at the given indices from the current source array into the in-progress array
fn copy_rows_by_filter(&mut self, filter: &FilterPredicate) -> Result<(), ArrowError> {
// Default implementation: iterate over indices from the filter
Comment thread
alamb marked this conversation as resolved.
Outdated
for idx in IndexIterator::new(filter.filter_array(), filter.count()) {

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 it strange that the default implementation copied with a different iteration strategy -- Upon review, it seems like this code should never be called

Therefore I would recommend making the default implementation panic

Another potential way to make this clearer would be add a method like supports_copy_rows_by_filter to avoid having to special case "is primitive"

self.copy_rows(idx, 1)?;
}
Ok(())
}

/// Finish the currently in-progress array and return it as an `ArrayRef`
fn finish(&mut self) -> Result<ArrayRef, ArrowError>;
}
Expand All @@ -579,6 +685,7 @@ trait InProgressArray: std::fmt::Debug + Send + Sync {
mod tests {
use super::*;
use crate::concat::concat_batches;
use crate::filter::filter_record_batch;
use arrow_array::builder::StringViewBuilder;
use arrow_array::cast::AsArray;
use arrow_array::{
Expand Down
100 changes: 100 additions & 0 deletions arrow-select/src/coalesce/primitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// under the License.

use crate::coalesce::InProgressArray;
use crate::filter::{FilterPredicate, IndexIterator, IterationStrategy, SlicesIterator};
use arrow_array::cast::AsArray;
use arrow_array::{Array, ArrayRef, ArrowPrimitiveType, PrimitiveArray};
use arrow_buffer::{NullBufferBuilder, ScalarBuffer};
Expand Down Expand Up @@ -92,6 +93,105 @@ impl<T: ArrowPrimitiveType + Debug> InProgressArray for InProgressPrimitiveArray
Ok(())
}

/// Copy rows by indices using a predicate
fn copy_rows_by_filter(&mut self, filter: &FilterPredicate) -> Result<(), ArrowError> {
self.ensure_capacity();

let s = self
.source
.as_ref()
.ok_or_else(|| {
ArrowError::InvalidArgumentError(
"Internal Error: InProgressPrimitiveArray: source not set".to_string(),
)
})?
.as_primitive::<T>();

let values = s.values();
let count = filter.count();

// Use the predicate's strategy for optimal iteration
match filter.strategy() {
IterationStrategy::SlicesIterator => {
// Copy values using slices
for (start, end) in SlicesIterator::new(filter.filter_array()) {
self.current.extend_from_slice(&values[start..end]);
}
// Copy nulls using slices
if let Some(nulls) = s.nulls().filter(|n| n.null_count() > 0) {

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 don't know if it would matter, but one difference with the filter kernel is that the filter kernel handles values in one loop and then nulls in a second (filter_bits). (as in a single loop that copies the values and then a second loop/iterator that copies the nulls, if any)

Doing so would keep the inner loop smaller and make it easier to reuse the null filtering code between kernels

However, this is also something we can do as a follow on PR / refactor when we add a second array type

for (start, end) in SlicesIterator::new(filter.filter_array()) {
let slice = nulls.slice(start, end - start);

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.

One thing we could also check is adding a nulls.append_buffer_sliced or something to avoid having to call "slice" on the buffer (and doing an atomic increment each time)

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.

Yes that would probably be faster

self.nulls.append_buffer(&slice);
}
} else {
self.nulls.append_n_non_nulls(count);
}
}
IterationStrategy::Slices(slices) => {

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 function needs some tests

I ran code coverage like this

cargo llvm-cov test --html -p arrow-buffer  -p arrow-select

And there appears to be no coverage
Image

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 still needs tests (it is still uncovered I think)

I can try and help find time to write some given how exciting this PR is

// Copy values using precomputed slices
for &(start, end) in slices {
self.current.extend_from_slice(&values[start..end]);
}
// Copy nulls using slices
if let Some(nulls) = s.nulls().filter(|n| n.null_count() > 0) {
for &(start, end) in slices {
let slice = nulls.slice(start, end - start);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note, null slices compute null count, so you might want to avoid that

self.nulls.append_buffer(&slice);
}
} else {
self.nulls.append_n_non_nulls(count);
}
}
IterationStrategy::IndexIterator => {
// Copy values and nulls for each index
if let Some(nulls) = s.nulls().filter(|n| n.null_count() > 0) {
for idx in IndexIterator::new(filter.filter_array(), count) {
if nulls.is_null(idx) {
self.nulls.append_null();
} else {
self.nulls.append_non_null();
}
self.current.push(values[idx]);
}
} else {
self.nulls.append_n_non_nulls(count);
for idx in IndexIterator::new(filter.filter_array(), count) {
self.current.push(values[idx]);
}
}
}
IterationStrategy::Indices(indices) => {
// Copy values and nulls using precomputed indices
if let Some(nulls) = s.nulls().filter(|n| n.null_count() > 0) {
for &idx in indices {
if nulls.is_null(idx) {
self.nulls.append_null();
} else {
self.nulls.append_non_null();
}
}
} else {
self.nulls.append_n_non_nulls(count);
}
self.current.extend(indices.iter().map(|&idx| values[idx]));
}
IterationStrategy::All => {
// Copy all values

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.

While trying to write a test to cover this code, I am pretty sure this is unreachable (as if the entire batch is selected, then the fast path above like this is executed:

        // Fast path: if all rows selected, just push the batch
        if selected_count == num_rows {
            return self.push_batch(batch);
        }

self.current.extend_from_slice(values);
if let Some(nulls) = s.nulls() {
self.nulls.append_buffer(nulls);
} else {
self.nulls.append_n_non_nulls(values.len());
}
}
IterationStrategy::None => {
// Nothing to copy
}
}

Ok(())
}

fn finish(&mut self) -> Result<ArrayRef, ArrowError> {
// take and reset the current values and nulls
let values = std::mem::take(&mut self.current);
Expand Down
64 changes: 50 additions & 14 deletions arrow-select/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,17 @@ impl Iterator for SlicesIterator<'_> {
///
/// This provides the best performance on most predicates, apart from those which keep
/// large runs and therefore favour [`SlicesIterator`]
struct IndexIterator<'a> {
pub struct IndexIterator<'a> {
remaining: usize,
iter: BitIndexIterator<'a>,
}

impl<'a> IndexIterator<'a> {
fn new(filter: &'a BooleanArray, remaining: usize) -> Self {
/// Creates a new [`IndexIterator`] from a [`BooleanArray`]
///
/// # Panics
/// Panics if `filter` has null values
pub fn new(filter: &'a BooleanArray, remaining: usize) -> Self {
assert_eq!(filter.null_count(), 0);
let iter = filter.values().set_indices();
Self { remaining, iter }
Expand Down Expand Up @@ -167,6 +171,21 @@ pub fn filter(values: &dyn Array, predicate: &BooleanArray) -> Result<ArrayRef,
filter_array(values, &predicate)
}

/// Determines if calling [FilterBuilder::optimize] is beneficial for the
/// given [`RecordBatch`].
pub fn is_optimize_beneficial_record_batch(record_batch: &RecordBatch) -> bool {

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 recommend putting this as a method on FilterBuilder so it is easier to discover

let num_cols = record_batch.num_columns();
if num_cols > 1 {
return true;
}
if num_cols == 1 {
return FilterBuilder::is_optimize_beneficial(
record_batch.schema_ref().field(0).data_type(),
);
}
false
}

/// Returns a filtered [RecordBatch] where the corresponding elements of
/// `predicate` are true.
///
Expand All @@ -182,13 +201,7 @@ pub fn filter_record_batch(
predicate: &BooleanArray,
) -> Result<RecordBatch, ArrowError> {
let mut filter_builder = FilterBuilder::new(predicate);
let num_cols = record_batch.num_columns();
if num_cols > 1
|| (num_cols > 0
&& FilterBuilder::is_optimize_beneficial(
record_batch.schema_ref().field(0).data_type(),
))
{
if is_optimize_beneficial_record_batch(record_batch) {
// Only optimize if filtering more than one column or if the column contains multiple internal arrays
// Otherwise, the overhead of optimization can be more than the benefit
filter_builder = filter_builder.optimize();
Expand Down Expand Up @@ -276,15 +289,22 @@ impl FilterBuilder {
}

/// The iteration strategy used to evaluate [`FilterPredicate`]
#[derive(Debug)]
enum IterationStrategy {
/// A lazily evaluated iterator of ranges
///
/// This determines how the filter will iterate over the selected rows.
/// The strategy is chosen based on the selectivity of the filter.
#[derive(Debug, Clone)]
pub enum IterationStrategy {
/// A lazily evaluated iterator of ranges (slices)
///
/// Best for high selectivity filters (> 80% of rows selected)
SlicesIterator,
/// A lazily evaluated iterator of indices
///
/// Best for low selectivity filters (< 80% of rows selected)
IndexIterator,
/// A precomputed list of indices
Indices(Vec<usize>),
/// A precomputed array of ranges
/// A precomputed array of ranges (start, end)
Slices(Vec<(usize, usize)>),
/// Select all rows
All,
Expand All @@ -295,7 +315,13 @@ enum IterationStrategy {
impl IterationStrategy {
/// The default [`IterationStrategy`] for a filter of length `filter_length`
/// and selecting `filter_count` rows
fn default_strategy(filter_length: usize, filter_count: usize) -> Self {
///
/// Returns:
/// - [`IterationStrategy::None`] if `filter_count` is 0
/// - [`IterationStrategy::All`] if `filter_count == filter_length`
/// - [`IterationStrategy::SlicesIterator`] if selectivity > 80%

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 you changed the threshold to 90%

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.

Yes

/// - [`IterationStrategy::IndexIterator`] otherwise
pub fn default_strategy(filter_length: usize, filter_count: usize) -> Self {
if filter_length == 0 || filter_count == 0 {
return IterationStrategy::None;
}
Expand Down Expand Up @@ -359,6 +385,16 @@ impl FilterPredicate {
pub fn count(&self) -> usize {
self.count
}

/// Returns the iteration strategy used by this [`FilterPredicate`]
pub fn strategy(&self) -> &IterationStrategy {
&self.strategy
}

/// Returns the underlying filter array
pub fn filter_array(&self) -> &BooleanArray {
&self.filter
}
}

fn filter_array(values: &dyn Array, predicate: &FilterPredicate) -> Result<ArrayRef, ArrowError> {
Expand Down
Loading