Skip to content

Commit 7674831

Browse files
Optimize IN LIST for byte view arrays
1 parent a8edfdb commit 7674831

7 files changed

Lines changed: 848 additions & 19 deletions

File tree

datafusion/physical-expr/benches/in_list_strategy.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,11 @@ fn random_string(rng: &mut StdRng, len: usize) -> String {
166166
fn strings_with_shared_prefix(
167167
rng: &mut StdRng,
168168
count: usize,
169-
prefix_len: usize,
169+
prefix: &str,
170+
discriminator: char,
170171
) -> Vec<String> {
171-
let prefix = random_string(rng, prefix_len);
172172
(0..count)
173-
.map(|_| format!("{}{}", prefix, random_string(rng, 8))) // prefix + random 8-char suffix
173+
.map(|_| format!("{prefix}{}{discriminator}", random_string(rng, 7)))
174174
.collect()
175175
}
176176

@@ -275,12 +275,14 @@ fn bench_string_shared_prefix<A>(
275275
.wrapping_add(prefix_len as u64 * 0x4444);
276276
let mut rng = StdRng::seed_from_u64(seed);
277277

278-
// Generate IN list with a shared prefix.
279-
let haystack = strings_with_shared_prefix(&mut rng, list_size, prefix_len);
278+
// Use the same prefix and equal-length, disjoint suffixes for both pools so
279+
// misses exercise length/prefix collisions rather than immediate rejection.
280+
let prefix = random_string(&mut rng, prefix_len);
281+
let haystack = strings_with_shared_prefix(&mut rng, list_size, &prefix, 'h');
280282

281283
// Generate non-matching strings with the same prefix to keep misses close
282284
// to the matching set.
283-
let non_match_pool = strings_with_shared_prefix(&mut rng, 100, prefix_len);
285+
let non_match_pool = strings_with_shared_prefix(&mut rng, 100, &prefix, 'm');
284286

285287
// Generate array with controlled match rate
286288
let values: A = (0..ARRAY_SIZE)
@@ -313,19 +315,29 @@ fn bench_string_mixed_lengths<A>(
313315
name: &str,
314316
list_size: usize,
315317
match_rate: f64,
318+
inline_rate: f64,
316319
to_scalar: fn(String) -> ScalarValue,
317320
) where
318321
A: Array + FromIterator<Option<String>> + 'static,
319322
{
320323
let seed = 0xABCD_EF01_u64.wrapping_add(list_size as u64 * 0x5555);
321324
let mut rng = StdRng::seed_from_u64(seed);
322325

323-
// Mixed lengths: some short (<= 12), some long (> 12)
324-
let lengths = [4, 8, 12, 16, 20, 24];
326+
let inline_lengths = [4, 8, 12];
327+
let long_lengths = [16, 20, 24];
328+
let inline_count = ((list_size as f64 * inline_rate).round() as usize)
329+
.max(1)
330+
.min(list_size - 1);
325331

326332
// Generate IN list with mixed lengths
327333
let haystack: Vec<String> = (0..list_size)
328-
.map(|_| {
334+
.map(|idx| {
335+
let inline = idx < inline_count;
336+
let lengths = if inline {
337+
&inline_lengths
338+
} else {
339+
&long_lengths
340+
};
329341
let len = *lengths.choose(&mut rng).unwrap();
330342
random_string(&mut rng, len)
331343
})
@@ -337,6 +349,11 @@ fn bench_string_mixed_lengths<A>(
337349
Some(if !haystack.is_empty() && rng.random_bool(match_rate) {
338350
haystack.choose(&mut rng).unwrap().clone()
339351
} else {
352+
let lengths = if rng.random_bool(inline_rate) {
353+
&inline_lengths
354+
} else {
355+
&long_lengths
356+
};
340357
let len = *lengths.choose(&mut rng).unwrap();
341358
random_string(&mut rng, len)
342359
})
@@ -602,6 +619,7 @@ fn bench_utf8(c: &mut Criterion) {
602619
&format!("mixed_len/list={list_size}/match={match_pct}%"),
603620
list_size,
604621
match_pct as f64 / 100.0,
622+
0.5,
605623
to_scalar,
606624
);
607625
}
@@ -694,6 +712,23 @@ fn bench_utf8view(c: &mut Criterion) {
694712
&format!("mixed_len/list={list_size}/match={match_pct}%"),
695713
list_size,
696714
match_pct as f64 / 100.0,
715+
0.5,
716+
to_scalar,
717+
);
718+
}
719+
}
720+
721+
// Strongly skewed mixed lists exercise routing near the all-inline and
722+
// all-long boundaries while retaining both representations.
723+
for inline_pct in [2, 98] {
724+
for match_pct in MATCH_RATES {
725+
bench_string_mixed_lengths::<StringViewArray>(
726+
c,
727+
"utf8view",
728+
&format!("mixed_len/inline={inline_pct}%/list=64/match={match_pct}%"),
729+
64,
730+
match_pct as f64 / 100.0,
731+
inline_pct as f64 / 100.0,
697732
to_scalar,
698733
);
699734
}

datafusion/physical-expr/src/expressions/in_list.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use datafusion_common::{
3737
use datafusion_expr::{ColumnarValue, expr_vec_fmt};
3838

3939
mod array_static_filter;
40+
mod byte_view_filter;
4041
mod frozen_set;
4142
mod primitive_filter;
4243
mod result;
@@ -215,7 +216,7 @@ impl InListExpr {
215216
expr,
216217
list,
217218
negated,
218-
Some(instantiate_static_filter(array)?),
219+
Some(instantiate_static_filter(array, &expr_data_type)?),
219220
))
220221
}
221222

@@ -242,7 +243,7 @@ impl InListExpr {
242243

243244
// Try to create a static filter if all list expressions are constants
244245
let static_filter = match try_evaluate_constant_list(&list, schema)? {
245-
Some(in_array) => Some(instantiate_static_filter(in_array)?),
246+
Some(in_array) => Some(instantiate_static_filter(in_array, &expr_data_type)?),
246247
None => None, // Non-constant expressions, fall back to dynamic evaluation
247248
};
248249

@@ -3576,6 +3577,23 @@ mod tests {
35763577
)?
35773578
);
35783579

3580+
// Utf8View in_array, Utf8View and Dict(Utf8View) needles
3581+
let utf8view_in =
3582+
Arc::new(StringViewArray::from(vec!["a", "b", "c"])) as ArrayRef;
3583+
let utf8view_needle =
3584+
Arc::new(StringViewArray::from(vec!["a", "d", "b"])) as ArrayRef;
3585+
assert_eq!(
3586+
expected,
3587+
eval_in_list_from_array(
3588+
Arc::clone(&utf8view_needle),
3589+
Arc::clone(&utf8view_in),
3590+
)?
3591+
);
3592+
assert_eq!(
3593+
expected,
3594+
eval_in_list_from_array(wrap_in_dict(utf8view_needle), utf8view_in)?
3595+
);
3596+
35793597
// Struct in_array, Struct needle: multi-column join
35803598
let struct_fields = Fields::from(vec![
35813599
Field::new("c0", DataType::Utf8, true),

0 commit comments

Comments
 (0)