@@ -1836,6 +1836,21 @@ impl ExecutionPlan for HashJoinExec {
18361836
18371837 let batch_size = context. session_config ( ) . batch_size ( ) ;
18381838
1839+ // The join measures the work that its dynamic filters save for each
1840+ // row that they remove (see `RemovedRowWork`).
1841+ let removed_row_work = self
1842+ . dynamic_filter
1843+ . as_ref ( )
1844+ . filter ( |_| enable_dynamic_filter_pushdown)
1845+ . map ( |df| {
1846+ df. membership
1847+ . iter ( )
1848+ . chain ( df. bounds . iter ( ) )
1849+ . map ( |filter| Arc :: clone ( filter. removed_row_work ( ) ) )
1850+ . collect ( )
1851+ } )
1852+ . unwrap_or_default ( ) ;
1853+
18391854 // we have the batches and the hash map with their keys. We can how create a stream
18401855 // over the right that uses this information to issue new batches.
18411856 let right_stream = self . right . execute ( partition, context) ?;
@@ -1855,27 +1870,30 @@ impl ExecutionPlan for HashJoinExec {
18551870 . map ( |( _, right_expr) | Arc :: clone ( right_expr) )
18561871 . collect :: < Vec < _ > > ( ) ;
18571872
1858- Ok ( Box :: pin ( HashJoinStream :: new (
1859- partition,
1860- self . schema ( ) ,
1861- on_right,
1862- self . filter . clone ( ) ,
1863- self . join_type ,
1864- right_stream,
1865- self . random_state . random_state ( ) . clone ( ) ,
1866- join_metrics,
1867- column_indices_after_projection,
1868- self . null_equality ,
1869- HashJoinStreamState :: WaitBuildSide ,
1870- BuildSide :: Initial ( BuildSideInitialState { left_fut } ) ,
1871- batch_size,
1872- vec ! [ ] ,
1873- self . right . output_ordering ( ) . is_some ( ) ,
1874- build_accumulator,
1875- self . mode ,
1876- null_aware,
1877- self . fetch ,
1878- ) ) )
1873+ Ok ( Box :: pin (
1874+ HashJoinStream :: new (
1875+ partition,
1876+ self . schema ( ) ,
1877+ on_right,
1878+ self . filter . clone ( ) ,
1879+ self . join_type ,
1880+ right_stream,
1881+ self . random_state . random_state ( ) . clone ( ) ,
1882+ join_metrics,
1883+ column_indices_after_projection,
1884+ self . null_equality ,
1885+ HashJoinStreamState :: WaitBuildSide ,
1886+ BuildSide :: Initial ( BuildSideInitialState { left_fut } ) ,
1887+ batch_size,
1888+ vec ! [ ] ,
1889+ self . right . output_ordering ( ) . is_some ( ) ,
1890+ build_accumulator,
1891+ self . mode ,
1892+ null_aware,
1893+ self . fetch ,
1894+ )
1895+ . with_removed_row_work ( removed_row_work) ,
1896+ ) )
18791897 }
18801898
18811899 fn metrics ( & self ) -> Option < MetricsSet > {
@@ -8750,6 +8768,87 @@ mod tests {
87508768 Ok ( ( ) )
87518769 }
87528770
8771+ /// The join measures the work that its dynamic filter saves for each
8772+ /// row that the filter removes: its work for each probe row.
8773+ #[ tokio:: test]
8774+ async fn test_dynamic_filter_measures_removed_row_work ( ) -> Result < ( ) > {
8775+ let task_ctx = Arc :: new ( TaskContext :: default ( ) ) ;
8776+ let left = build_table (
8777+ ( "a1" , & vec ! [ 1 , 2 , 3 ] ) ,
8778+ ( "b1" , & vec ! [ 4 , 5 , 6 ] ) ,
8779+ ( "c1" , & vec ! [ 7 , 8 , 9 ] ) ,
8780+ ) ;
8781+ // `MIN_OBSERVED_ROWS` probe rows.
8782+ let rows = datafusion_physical_expr:: filter_stats:: MIN_OBSERVED_ROWS as i32 ;
8783+ let values: Vec < i32 > = ( 0 ..rows) . collect ( ) ;
8784+ let right = build_table ( ( "a2" , & values) , ( "b2" , & values) , ( "c2" , & values) ) ;
8785+ let on = vec ! [ (
8786+ Arc :: new( Column :: new_with_schema( "a1" , & left. schema( ) ) ?) as _,
8787+ Arc :: new( Column :: new_with_schema( "a2" , & right. schema( ) ) ?) as _,
8788+ ) ] ;
8789+ let dynamic_filter = HashJoinExec :: create_dynamic_filter ( & on) ;
8790+ let consumer: Arc < dyn PhysicalExpr > = Arc :: clone ( & dynamic_filter) as _ ;
8791+ // The consumer does not apply the filter here: the join sees all
8792+ // probe rows.
8793+ let right = Arc :: new ( FilterExecBuilder :: new ( consumer, right) . build ( ) ?) ;
8794+ let mut join = HashJoinExec :: try_new (
8795+ left,
8796+ right,
8797+ on,
8798+ None ,
8799+ & JoinType :: Inner ,
8800+ None ,
8801+ PartitionMode :: CollectLeft ,
8802+ NullEquality :: NullEqualsNothing ,
8803+ false ,
8804+ ) ?;
8805+ join. dynamic_filter = Some ( HashJoinExecDynamicFilter :: new (
8806+ Some ( Arc :: clone ( & dynamic_filter) ) ,
8807+ None ,
8808+ ) ) ;
8809+ let work = Arc :: clone ( dynamic_filter. removed_row_work ( ) ) ;
8810+ assert_eq ! ( work. ns_per_row( ) , None ) ;
8811+ let batches = common:: collect ( join. execute ( 0 , task_ctx) ?) . await ?;
8812+ assert_eq ! ( batches. iter( ) . map( |b| b. num_rows( ) ) . sum:: <usize >( ) , 3 ) ;
8813+ // The filter removed all but 3 probe rows, thus the join saw fewer
8814+ // than `MIN_OBSERVED_ROWS` rows: no measurement yet.
8815+ assert_eq ! ( work. ns_per_row( ) , None ) ;
8816+
8817+ // Without a consumer that filters, the join sees all probe rows.
8818+ let task_ctx = Arc :: new ( TaskContext :: default ( ) ) ;
8819+ let left = build_table (
8820+ ( "a1" , & vec ! [ 1 , 2 , 3 ] ) ,
8821+ ( "b1" , & vec ! [ 4 , 5 , 6 ] ) ,
8822+ ( "c1" , & vec ! [ 7 , 8 , 9 ] ) ,
8823+ ) ;
8824+ let right = build_table ( ( "a2" , & values) , ( "b2" , & values) , ( "c2" , & values) ) ;
8825+ let on = vec ! [ (
8826+ Arc :: new( Column :: new_with_schema( "a1" , & left. schema( ) ) ?) as _,
8827+ Arc :: new( Column :: new_with_schema( "a2" , & right. schema( ) ) ?) as _,
8828+ ) ] ;
8829+ let dynamic_filter = HashJoinExec :: create_dynamic_filter ( & on) ;
8830+ let mut join = HashJoinExec :: try_new (
8831+ left,
8832+ right,
8833+ on,
8834+ None ,
8835+ & JoinType :: Inner ,
8836+ None ,
8837+ PartitionMode :: CollectLeft ,
8838+ NullEquality :: NullEqualsNothing ,
8839+ false ,
8840+ ) ?;
8841+ join. dynamic_filter = Some ( HashJoinExecDynamicFilter :: new (
8842+ Some ( Arc :: clone ( & dynamic_filter) ) ,
8843+ None ,
8844+ ) ) ;
8845+ let batches = common:: collect ( join. execute ( 0 , task_ctx) ?) . await ?;
8846+ assert_eq ! ( batches. iter( ) . map( |b| b. num_rows( ) ) . sum:: <usize >( ) , 3 ) ;
8847+ let measured = dynamic_filter. removed_row_work ( ) . ns_per_row ( ) ;
8848+ assert ! ( measured. is_some_and( |ns| ns > 0.0 ) , "{measured:?}" ) ;
8849+ Ok ( ( ) )
8850+ }
8851+
87538852 /// The [`PartitionMode::Partitioned`] counterpart of
87548853 /// [`test_null_equal_dynamic_filter_keeps_probe_nulls_for_build_logical_null`].
87558854 ///
0 commit comments