Skip to content

Commit 4fc9dc1

Browse files
committed
Better sort pushdown for DF
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent f793584 commit 4fc9dc1

1 file changed

Lines changed: 173 additions & 9 deletions

File tree

vortex-datafusion/src/persistent/source.rs

Lines changed: 173 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::cmp::Ordering;
45
use std::fmt::Formatter;
56
use std::ops::Range;
67
use std::sync::Arc;
78
use std::sync::Weak;
89

910
use datafusion_common::Result as DFResult;
11+
use datafusion_common::ScalarValue;
1012
use datafusion_common::config::ConfigOptions;
13+
use datafusion_datasource::PartitionedFile;
1114
use datafusion_datasource::TableSchema;
1215
use datafusion_datasource::file::FileSource;
1316
use datafusion_datasource::file_scan_config::FileScanConfig;
@@ -17,9 +20,11 @@ use datafusion_physical_expr::EquivalenceProperties;
1720
use datafusion_physical_expr::PhysicalExprRef;
1821
use datafusion_physical_expr::PhysicalSortExpr;
1922
use datafusion_physical_expr::conjunction;
23+
use datafusion_physical_expr::expressions::Column;
2024
use datafusion_physical_expr::projection::ProjectionExprs;
2125
use datafusion_physical_expr_adapter::DefaultPhysicalExprAdapterFactory;
2226
use datafusion_physical_expr_common::physical_expr::fmt_sql;
27+
use datafusion_physical_expr_common::sort_expr::LexOrdering;
2328
use datafusion_physical_plan::DisplayFormatType;
2429
use datafusion_physical_plan::PhysicalExpr;
2530
use datafusion_physical_plan::SortOrderPushdownResult;
@@ -200,7 +205,7 @@ pub struct VortexSource {
200205
natural_split_ranges: Arc<DashMap<Path, Arc<[Range<u64>]>>>,
201206
expression_convertor: Arc<dyn ExpressionConvertor>,
202207
pub(crate) vortex_reader_factory: Option<Arc<dyn VortexReaderFactory>>,
203-
pub(crate) ordered: bool,
208+
pub(crate) sort_order: Option<LexOrdering>,
204209
vx_metrics_registry: Arc<dyn MetricsRegistry>,
205210
file_metadata_cache: Option<Arc<dyn FileMetadataCache>>,
206211
/// Options controlling scan planning and execution behavior.
@@ -235,7 +240,7 @@ impl VortexSource {
235240
vortex_reader_factory: None,
236241
vx_metrics_registry: Arc::new(DefaultMetricsRegistry::default()),
237242
file_metadata_cache: None,
238-
ordered: false,
243+
sort_order: None,
239244
options: VortexTableOptions::default(),
240245
}
241246
}
@@ -362,7 +367,8 @@ impl VortexSource {
362367
metrics_registry: Arc::clone(&self.vx_metrics_registry),
363368
layout_readers: Arc::clone(&self.layout_readers),
364369
natural_split_ranges: Arc::clone(&self.natural_split_ranges),
365-
has_output_ordering: !base_config.output_ordering.is_empty() || self.ordered,
370+
has_output_ordering: !base_config.output_ordering.is_empty()
371+
|| self.sort_order.is_some(),
366372
expression_convertor: Arc::clone(&self.expression_convertor),
367373
file_metadata_cache: self.file_metadata_cache.clone(),
368374
projection_pushdown: self.options.projection_pushdown,
@@ -414,16 +420,46 @@ impl FileSource for VortexSource {
414420
return Ok(SortOrderPushdownResult::Unsupported);
415421
}
416422

417-
if eq_properties.ordering_satisfy(order.iter().cloned())? {
418-
let mut this = self.clone();
419-
this.ordered = true;
423+
let Some(sort_order) = LexOrdering::new(order.iter().cloned()) else {
424+
return Ok(SortOrderPushdownResult::Unsupported);
425+
};
426+
427+
let mut this = self.clone();
428+
this.sort_order = Some(sort_order);
420429

430+
if eq_properties.ordering_satisfy(order.iter().cloned())? {
421431
return Ok(SortOrderPushdownResult::Exact {
422432
inner: Arc::new(this) as Arc<dyn FileSource>,
423433
});
424434
}
425435

426-
Ok(SortOrderPushdownResult::Unsupported)
436+
Ok(SortOrderPushdownResult::Inexact {
437+
inner: Arc::new(this) as Arc<dyn FileSource>,
438+
})
439+
}
440+
441+
fn reorder_files(&self, mut files: Vec<PartitionedFile>) -> Vec<PartitionedFile> {
442+
let Some((col_idx, descending)) = self
443+
.sort_order
444+
.as_ref()
445+
.and_then(|sort_order| sort_reorder_key(sort_order, &self.table_schema))
446+
else {
447+
return files;
448+
};
449+
450+
files.sort_unstable_by(|a, b| {
451+
match (file_min_value(a, col_idx), file_min_value(b, col_idx)) {
452+
(Some(va), Some(vb)) => {
453+
let cmp = va.partial_cmp(vb).unwrap_or(Ordering::Equal);
454+
if descending { cmp.reverse() } else { cmp }
455+
}
456+
(Some(_), None) => Ordering::Less,
457+
(None, Some(_)) => Ordering::Greater,
458+
(None, None) => Ordering::Equal,
459+
}
460+
});
461+
462+
files
427463
}
428464

429465
fn fmt_extra(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
@@ -542,13 +578,35 @@ impl FileSource for VortexSource {
542578
}
543579
}
544580

581+
fn sort_reorder_key(sort_order: &LexOrdering, table_schema: &TableSchema) -> Option<(usize, bool)> {
582+
let first = sort_order.first();
583+
let col = first.expr.downcast_ref::<Column>()?;
584+
let col_idx = table_schema.table_schema().index_of(col.name()).ok()?;
585+
Some((col_idx, first.options.descending))
586+
}
587+
588+
fn file_min_value(file: &PartitionedFile, col_idx: usize) -> Option<&ScalarValue> {
589+
file.statistics
590+
.as_ref()?
591+
.column_statistics
592+
.get(col_idx)?
593+
.min_value
594+
.get_value()
595+
}
596+
545597
#[cfg(test)]
546598
mod tests {
599+
use std::sync::Arc;
600+
547601
use arrow_schema::DataType;
548602
use arrow_schema::Field;
549603
use arrow_schema::Schema;
604+
use datafusion_common::ColumnStatistics;
550605
use datafusion_common::ScalarValue;
606+
use datafusion_common::Statistics;
551607
use datafusion_common::config::ConfigOptions;
608+
use datafusion_common::stats::Precision;
609+
use datafusion_datasource::PartitionedFile;
552610
use datafusion_datasource::file_scan_config::FileScanConfigBuilder;
553611
use datafusion_execution::object_store::ObjectStoreUrl;
554612
use datafusion_expr::Operator;
@@ -601,6 +659,10 @@ mod tests {
601659
PhysicalSortExpr::new_default(expr)
602660
}
603661

662+
fn sort_column_desc(name: &str, index: usize) -> PhysicalSortExpr {
663+
sort_column(name, index).desc()
664+
}
665+
604666
fn sort_test_schema() -> Arc<Schema> {
605667
Arc::new(Schema::new(vec![
606668
Field::new("a", DataType::Int32, false),
@@ -636,10 +698,48 @@ mod tests {
636698
.downcast_ref::<VortexSource>()
637699
.ok_or_else(|| anyhow::anyhow!("expected VortexSource"))?;
638700

639-
assert!(source.ordered);
701+
assert!(source.sort_order.is_some());
640702
Ok(())
641703
}
642704

705+
fn assert_inexact_source(
706+
result: SortOrderPushdownResult<Arc<dyn FileSource>>,
707+
) -> anyhow::Result<VortexSource> {
708+
let SortOrderPushdownResult::Inexact { inner } = result else {
709+
anyhow::bail!("expected inexact sort pushdown");
710+
};
711+
712+
Ok(inner
713+
.downcast_ref::<VortexSource>()
714+
.ok_or_else(|| anyhow::anyhow!("expected VortexSource"))?
715+
.clone())
716+
}
717+
718+
fn file_with_min(path: &str, min: Option<i32>) -> PartitionedFile {
719+
let column_stats = min.map_or_else(ColumnStatistics::new_unknown, |min| {
720+
let value = ScalarValue::Int32(Some(min));
721+
ColumnStatistics {
722+
min_value: Precision::Exact(value.clone()),
723+
max_value: Precision::Exact(value),
724+
..ColumnStatistics::new_unknown()
725+
}
726+
});
727+
let statistics = Statistics {
728+
num_rows: Precision::Exact(1),
729+
total_byte_size: Precision::Absent,
730+
column_statistics: vec![column_stats, ColumnStatistics::new_unknown()],
731+
};
732+
733+
PartitionedFile::new(path, 1).with_statistics(Arc::new(statistics))
734+
}
735+
736+
fn file_names(files: &[PartitionedFile]) -> Vec<&str> {
737+
files
738+
.iter()
739+
.map(|file| file.object_meta.location.as_ref())
740+
.collect()
741+
}
742+
643743
#[test]
644744
fn try_pushdown_sort_returns_exact_when_ordering_is_satisfied() -> anyhow::Result<()> {
645745
let schema = sort_test_schema();
@@ -655,7 +755,71 @@ mod tests {
655755
anyhow::bail!("expected exact sort pushdown")
656756
}
657757
}
658-
assert!(!source.ordered);
758+
assert!(source.sort_order.is_none());
759+
Ok(())
760+
}
761+
762+
#[test]
763+
fn try_pushdown_sort_returns_inexact_and_keeps_order() -> anyhow::Result<()> {
764+
let schema = sort_test_schema();
765+
let source = sort_test_source(Arc::clone(&schema));
766+
let order = vec![sort_column("a", 0)];
767+
let eq_properties = EquivalenceProperties::new(schema);
768+
769+
let updated_source =
770+
assert_inexact_source(source.try_pushdown_sort(&order, &eq_properties)?)?;
771+
772+
let sort_order = updated_source
773+
.sort_order
774+
.as_ref()
775+
.ok_or_else(|| anyhow::anyhow!("expected sort order for file reordering"))?;
776+
assert_eq!(sort_order.first().expr.to_string(), "a@0");
777+
Ok(())
778+
}
779+
780+
#[test]
781+
fn reorder_files_sorts_by_min_statistics_ascending() -> anyhow::Result<()> {
782+
let schema = sort_test_schema();
783+
let source = sort_test_source(Arc::clone(&schema));
784+
let order = vec![sort_column("a", 0)];
785+
let eq_properties = EquivalenceProperties::new(schema);
786+
let updated_source =
787+
assert_inexact_source(source.try_pushdown_sort(&order, &eq_properties)?)?;
788+
789+
let reordered = updated_source.reorder_files(vec![
790+
file_with_min("middle", Some(50)),
791+
file_with_min("missing", None),
792+
file_with_min("small", Some(10)),
793+
file_with_min("large", Some(100)),
794+
]);
795+
796+
assert_eq!(
797+
file_names(&reordered),
798+
vec!["small", "middle", "large", "missing"]
799+
);
800+
Ok(())
801+
}
802+
803+
#[test]
804+
fn reorder_files_sorts_by_min_statistics_descending() -> anyhow::Result<()> {
805+
let schema = sort_test_schema();
806+
let source = sort_test_source(Arc::clone(&schema));
807+
let order = vec![sort_column_desc("a", 0)];
808+
let eq_properties = EquivalenceProperties::new(schema);
809+
let updated_source =
810+
assert_inexact_source(source.try_pushdown_sort(&order, &eq_properties)?)?;
811+
812+
let reordered = updated_source.reorder_files(vec![
813+
file_with_min("middle", Some(50)),
814+
file_with_min("missing", None),
815+
file_with_min("small", Some(10)),
816+
file_with_min("large", Some(100)),
817+
]);
818+
819+
assert_eq!(
820+
file_names(&reordered),
821+
vec!["large", "middle", "small", "missing"]
822+
);
659823
Ok(())
660824
}
661825

0 commit comments

Comments
 (0)