Skip to content

Commit ea4ba3a

Browse files
authored
perf(layout): lower zoned pruning predicates once (#10428)
Only compute lowered stats predicate once. Avoids relowering on every dynamic version update --------- Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent 85e7acf commit ea4ba3a

2 files changed

Lines changed: 94 additions & 12 deletions

File tree

‎vortex-layout/src/layouts/zoned/pruning.rs‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,23 @@ impl PruningState {
102102
Some(
103103
async move {
104104
let zone_map = zone_map.await?;
105-
let initial_mask =
106-
zone_map.prune(&predicate, &session).map_err(|err| {
107-
err.with_context(format!(
105+
let lowered = zone_map.lower_stats(predicate.clone()).and_then(
106+
|lowered_predicate| {
107+
let mask =
108+
zone_map.prune_lowered(&lowered_predicate, &session)?;
109+
Ok((lowered_predicate, mask))
110+
},
111+
);
112+
let (lowered_predicate, initial_mask) = lowered.map_err(|err| {
113+
err.with_context(format!(
108114
"While evaluating pruning predicate {} (derived from {})",
109115
predicate, expr
110116
))
111-
})?;
117+
})?;
118+
112119
Ok(Arc::new(PruningResult {
113120
zone_map,
114-
predicate,
121+
lowered_predicate,
115122
dynamic_updates,
116123
latest_result: RwLock::new((0, initial_mask)),
117124
session,
@@ -191,7 +198,7 @@ impl PruningState {
191198

192199
pub(super) struct PruningResult {
193200
zone_map: ZoneMap,
194-
predicate: BoundExpression,
201+
lowered_predicate: BoundExpression,
195202
dynamic_updates: Option<DynamicExprUpdates>,
196203
latest_result: RwLock<(u64, Mask)>,
197204
session: VortexSession,
@@ -219,17 +226,17 @@ impl PruningResult {
219226

220227
trace!(
221228
version,
222-
predicate = %self.predicate,
229+
predicate = %self.lowered_predicate,
223230
"recomputing pruning mask"
224231
);
225232

226233
let next_mask = self
227234
.zone_map
228-
.prune(&self.predicate, &self.session)
235+
.prune_lowered(&self.lowered_predicate, &self.session)
229236
.map_err(|err| {
230237
err.with_context(format!(
231238
"While evaluating pruning predicate {}",
232-
self.predicate
239+
self.lowered_predicate
233240
))
234241
})?;
235242
*guard = (version, next_mask.clone());

‎vortex-layout/src/layouts/zoned/zone_map.rs‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,26 @@ impl ZoneMap {
145145
&self,
146146
predicate: &BoundExpression,
147147
session: &VortexSession,
148+
) -> VortexResult<Mask> {
149+
let predicate = self.lower_stats(predicate.clone())?;
150+
self.prune_lowered(&predicate, session)
151+
}
152+
153+
/// Apply a predicate that [`Self::lower_stats`] already lowered to this zone map's
154+
/// stats table.
155+
///
156+
/// Lowering depends only on the column and stats-table dtypes, so callers that evaluate
157+
/// the same predicate many times lower it once and call this function for each evaluation.
158+
pub(super) fn prune_lowered(
159+
&self,
160+
predicate: &BoundExpression,
161+
session: &VortexSession,
148162
) -> VortexResult<Mask> {
149163
let mut ctx = session.create_execution_ctx();
150164
let num_zones = self.array.len();
151-
let predicate = self.lower_stats(predicate.clone())?;
152165

153166
let array = self.array.clone().into_array();
154-
let applied = array.apply_bound(&predicate)?;
167+
let applied = array.apply_bound(predicate)?;
155168

156169
if !contains_row_count(&applied) {
157170
return null_as_false_mask(applied, &mut ctx);
@@ -162,7 +175,8 @@ impl ZoneMap {
162175
null_as_false_mask(substituted, &mut ctx)
163176
}
164177

165-
fn lower_stats(&self, predicate: BoundExpression) -> VortexResult<BoundExpression> {
178+
/// Rewrite the stats references in `predicate` into expressions over the stats table.
179+
pub(super) fn lower_stats(&self, predicate: BoundExpression) -> VortexResult<BoundExpression> {
166180
let binder = ZoneMapStatsBinder { zone_map: self };
167181
bind_stats(predicate, &binder)
168182
}
@@ -377,6 +391,8 @@ fn row_count_array(zone_len: u64, row_count: u64, num_zones: usize) -> VortexRes
377391
mod tests {
378392
use std::num::NonZeroUsize;
379393
use std::sync::Arc;
394+
use std::sync::atomic::AtomicI32;
395+
use std::sync::atomic::Ordering;
380396

381397
use rstest::rstest;
382398
use vortex_array::IntoArray;
@@ -408,6 +424,7 @@ mod tests {
408424
use vortex_array::expr::BoundExpression;
409425
use vortex_array::expr::Expression;
410426
use vortex_array::expr::cast;
427+
use vortex_array::expr::dynamic;
411428
use vortex_array::expr::gt;
412429
use vortex_array::expr::gt_eq;
413430
use vortex_array::expr::is_not_null;
@@ -417,6 +434,8 @@ mod tests {
417434
use vortex_array::expr::not_eq;
418435
use vortex_array::expr::root;
419436
use vortex_array::expr::stats::Stat;
437+
use vortex_array::scalar::ScalarValue;
438+
use vortex_array::scalar_fn::fns::operators::CompareOperator;
420439
use vortex_array::stats::all_nan;
421440
use vortex_array::stats::all_non_nan;
422441
use vortex_array::stats::all_non_null;
@@ -1076,4 +1095,60 @@ mod tests {
10761095
&mut SESSION.create_execution_ctx()
10771096
);
10781097
}
1098+
1099+
#[test]
1100+
fn lowered_dynamic_predicate_tracks_rhs_updates() -> VortexResult<()> {
1101+
let max = Max.bind(NumericalAggregateOpts::skip_nans());
1102+
let min = Min.bind(NumericalAggregateOpts::skip_nans());
1103+
let zone_map = ZoneMap::try_new(
1104+
PType::I32.into(),
1105+
StructArray::from_fields(&[
1106+
(
1107+
max.to_string(),
1108+
PrimitiveArray::new(buffer![5i32, 6i32, 7i32], Validity::AllValid).into_array(),
1109+
),
1110+
(
1111+
min.to_string(),
1112+
PrimitiveArray::new(buffer![1i32, 2i32, 3i32], Validity::AllValid).into_array(),
1113+
),
1114+
])?,
1115+
Arc::new([max, min]),
1116+
3,
1117+
10,
1118+
)?;
1119+
let ctx = &mut SESSION.create_execution_ctx();
1120+
1121+
let threshold = Arc::new(AtomicI32::new(5));
1122+
let rhs = Arc::clone(&threshold);
1123+
let expr = dynamic(
1124+
CompareOperator::Gt,
1125+
move || Some(ScalarValue::from(rhs.load(Ordering::Relaxed))),
1126+
PType::I32.into(),
1127+
true,
1128+
root(),
1129+
);
1130+
1131+
// Lower once, as the zoned reader does, and evaluate the lowered predicate after each
1132+
// update of the dynamic value.
1133+
let lowered = zone_map.lower_stats(falsify(&expr, PType::I32.into()))?;
1134+
1135+
// A > 5 => A.max <= 5
1136+
let mask = zone_map.prune_lowered(&lowered, &SESSION)?;
1137+
assert_arrays_eq!(
1138+
mask.into_array(),
1139+
BoolArray::from_iter([true, false, false]),
1140+
ctx
1141+
);
1142+
1143+
// A > 6 => A.max <= 6
1144+
threshold.store(6, Ordering::Relaxed);
1145+
let mask = zone_map.prune_lowered(&lowered, &SESSION)?;
1146+
assert_arrays_eq!(
1147+
mask.into_array(),
1148+
BoolArray::from_iter([true, true, false]),
1149+
ctx
1150+
);
1151+
1152+
Ok(())
1153+
}
10791154
}

0 commit comments

Comments
 (0)