Skip to content

Commit ae7a264

Browse files
committed
feat(optimizer): support forced nested-loop joins
1 parent 420540e commit ae7a264

16 files changed

Lines changed: 256 additions & 109 deletions

File tree

docs/features.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,15 @@ ORDER BY user_id;
5757
```
5858

5959
For ORM queries, enable both `orm` and `spill`, then call `force_spill()` before
60-
`group_by()` or `distinct()` so the option is passed when the aggregate plan is
61-
built:
60+
building the projection and aggregate plan:
6261

6362
```rust,ignore
6463
let grouped = database.bind(|ctx| {
6564
ctx.from::<Order>()?
65+
.force_spill()?
6666
.project_tuple(|e| {
6767
Ok(vec![e.column(Order::user_id())?, e.count_all()?])
6868
})?
69-
.force_spill()?
7069
.group_by(|e| e.column(Order::user_id()))?
7170
.order_by(Order::user_id())?
7271
.finish()
@@ -78,6 +77,33 @@ an external sort on the group keys and executes a streaming aggregate. The
7877
hint is intended for grouped aggregates or `DISTINCT`; an aggregate without
7978
group keys already uses constant-size accumulator state.
8079

80+
### Nested-loop Join Hint
81+
82+
Use `FORCE_NEST_LOOP_JOIN` to select the nested-loop implementation for joins
83+
in the current `SELECT` query block:
84+
85+
```sql
86+
SELECT /*+ FORCE_NEST_LOOP_JOIN */ orders.id, users.name
87+
FROM orders
88+
JOIN users ON orders.user_id = users.id;
89+
```
90+
91+
ORM queries can select the same implementation before adding joins:
92+
93+
```rust,ignore
94+
let joined = database.bind(|ctx| {
95+
ctx.from::<Order>()?
96+
.force_nested_loop()
97+
.inner_join::<User, _>(|e| {
98+
e.column(Order::user_id())?.eq(e.column(User::id())?)
99+
})?
100+
.finish()
101+
})?;
102+
```
103+
104+
It avoids Hash Join's build-side tuple materialization, but may perform
105+
substantially more work on large inputs.
106+
81107
### User-Defined Function: `features = ["macros"]`
82108
```rust
83109
scala_function!(TestFunction::test(LogicalType::Integer, LogicalType::Integer) -> LogicalType::Integer => |v1: DataValue, v2: DataValue| {

src/binder/aggregate.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,14 @@ impl<T: Transaction, A: AsRef<[(&'static str, DataValue)]>> Binder<'_, '_, T, A>
4848
children: LogicalPlan,
4949
agg_calls: Vec<ScalarExpression>,
5050
groupby_exprs: Vec<ScalarExpression>,
51-
force_spill: bool,
5251
) -> Result<LogicalPlan, DatabaseError> {
5352
self.context.step(QueryBindStep::Agg);
5453
Ok(AggregateOperator::build(
5554
children,
5655
agg_calls,
5756
groupby_exprs,
5857
false,
59-
force_spill,
58+
self.force_spill,
6059
))
6160
}
6261

src/binder/distinct.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ impl<T: Transaction, A: AsRef<[(&'static str, DataValue)]>> Binder<'_, '_, T, A>
2727
&mut self,
2828
children: LogicalPlan,
2929
select_list: Vec<ScalarExpression>,
30-
force_spill: bool,
3130
) -> Result<LogicalPlan, DatabaseError> {
3231
self.context.step(QueryBindStep::Distinct);
3332

@@ -36,7 +35,7 @@ impl<T: Transaction, A: AsRef<[(&'static str, DataValue)]>> Binder<'_, '_, T, A>
3635
vec![],
3736
select_list,
3837
true,
39-
force_spill,
38+
self.force_spill,
4039
))
4140
}
4241

src/binder/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,8 @@ impl<'a, T: Transaction> BinderContext<'a, T> {
561561
pub struct Binder<'a, 'parent, T: Transaction, A: AsRef<[(&'static str, DataValue)]>> {
562562
pub(crate) context: BinderContext<'a, T>,
563563
pub(crate) args: &'a A,
564+
pub(crate) force_spill: bool,
565+
pub(crate) force_nested_loop: bool,
564566
with_pk: Option<TableName>,
565567
pub(crate) parent: Option<&'parent BinderContext<'a, T>>,
566568
}
@@ -574,6 +576,8 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(&'static str, DataValue)]>> Binder<
574576
Binder {
575577
context,
576578
args,
579+
force_spill: false,
580+
force_nested_loop: false,
577581
with_pk: None,
578582
parent,
579583
}

src/binder/parser.rs

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,7 @@ where
13641364
self.binder.bind_table_ref_sql(from, self.arena)?,
13651365
JoinCondition::None,
13661366
JoinType::Cross,
1367+
self.binder.force_nested_loop,
13671368
)
13681369
}
13691370
plan
@@ -1424,7 +1425,6 @@ where
14241425
group_by: &GroupByExpr,
14251426
having: Option<&Expr>,
14261427
orderby: Option<&[OrderByExpr]>,
1427-
force_spill: bool,
14281428
) -> Result<BindPlanAggregated<'s, 'a, 'b, 'arena, T, A>, DatabaseError> {
14291429
let group_by = with_query_bind_step!(self.binder, QueryBindStep::Agg, {
14301430
match group_by {
@@ -1453,22 +1453,16 @@ where
14531453
})
14541454
})
14551455
.transpose()?;
1456-
self.aggregate(
1457-
group_by,
1458-
having,
1459-
orderby,
1460-
force_spill,
1461-
|binder, arena, orderby| {
1462-
let OrderByExpr { expr, options, .. } = orderby;
1463-
with_query_bind_step!(binder, QueryBindStep::Sort, {
1464-
SortField::new(
1465-
binder.bind_expr(expr, arena)?,
1466-
options.asc.is_none_or(|asc| asc),
1467-
options.nulls_first.unwrap_or(false),
1468-
)
1469-
})
1470-
},
1471-
)
1456+
self.aggregate(group_by, having, orderby, |binder, arena, orderby| {
1457+
let OrderByExpr { expr, options, .. } = orderby;
1458+
with_query_bind_step!(binder, QueryBindStep::Sort, {
1459+
SortField::new(
1460+
binder.bind_expr(expr, arena)?,
1461+
options.asc.is_none_or(|asc| asc),
1462+
options.nulls_first.unwrap_or(false),
1463+
)
1464+
})
1465+
})
14721466
}
14731467
}
14741468

@@ -1480,9 +1474,8 @@ where
14801474
pub(crate) fn distinct_sql(
14811475
self,
14821476
distinct: Option<&Distinct>,
1483-
force_spill: bool,
14841477
) -> Result<BindPlanDistinct<'s, 'a, 'b, 'arena, T, A>, DatabaseError> {
1485-
self.distinct(matches!(distinct, Some(Distinct::Distinct)), force_spill)
1478+
self.distinct(matches!(distinct, Some(Distinct::Distinct)))
14861479
}
14871480
}
14881481

@@ -2624,27 +2617,40 @@ impl<'a, 'parent, T: Transaction, A: AsRef<[(&'static str, DataValue)]>> Binder<
26242617
"QUALIFY is not supported".to_string(),
26252618
));
26262619
}
2627-
let force_spill = optimizer_hint
2628-
.as_ref()
2629-
.is_some_and(|hint| hint.text.trim().eq_ignore_ascii_case("FORCE_AGG_SPILL"));
2620+
let has_hint = |expected: &str| {
2621+
optimizer_hint.as_ref().is_some_and(|hint| {
2622+
hint.text
2623+
.split(|char: char| char.is_ascii_whitespace() || char == ',')
2624+
.any(|hint| hint.eq_ignore_ascii_case(expected))
2625+
})
2626+
};
2627+
let force_spill = has_hint("FORCE_AGG_SPILL");
2628+
let force_nested_loop = has_hint("FORCE_NEST_LOOP_JOIN");
26302629
if force_spill && !cfg!(feature = "spill") {
26312630
return Err(DatabaseError::UnsupportedStmt(
26322631
"FORCE_AGG_SPILL requires the `spill` feature".to_string(),
26332632
));
26342633
}
2635-
Ok(self
2636-
.build_plan(arena)
2637-
.from_sql(from)?
2638-
.select_list_from_sql(projection)?
2639-
.where_sql(selection.as_ref())?
2640-
.aggregate_sql(group_by, having.as_ref(), orderby, force_spill)?
2641-
.having()?
2642-
.window()?
2643-
.distinct_sql(distinct.as_ref(), force_spill)?
2644-
.order_by()?
2645-
.project()?
2646-
.select_into_sql(into.as_ref())?
2647-
.finish())
2634+
let previous_options = (self.force_spill, self.force_nested_loop);
2635+
self.force_spill = force_spill;
2636+
self.force_nested_loop = force_nested_loop;
2637+
let result = (|| {
2638+
Ok(self
2639+
.build_plan(arena)
2640+
.from_sql(from)?
2641+
.select_list_from_sql(projection)?
2642+
.where_sql(selection.as_ref())?
2643+
.aggregate_sql(group_by, having.as_ref(), orderby)?
2644+
.having()?
2645+
.window()?
2646+
.distinct_sql(distinct.as_ref())?
2647+
.order_by()?
2648+
.project()?
2649+
.select_into_sql(into.as_ref())?
2650+
.finish())
2651+
})();
2652+
(self.force_spill, self.force_nested_loop) = previous_options;
2653+
result
26482654
}
26492655

26502656
/// FIXME: temp values need to register BindContext.bind_table
@@ -3154,6 +3160,54 @@ mod tests {
31543160
Ok(())
31553161
}
31563162

3163+
#[test]
3164+
fn force_nest_loop_join_marks_join_operator() -> Result<(), DatabaseError> {
3165+
let tables = build_t1_table()?;
3166+
let plan =
3167+
tables.plan("select /*+ FORCE_NEST_LOOP_JOIN */ c1, c3 from t1 join t2 on c1 = c3")?;
3168+
let join = plan
3169+
.childrens
3170+
.iter()
3171+
.find_map(|plan| match &plan.operator {
3172+
Operator::Join(operator) => Some(operator),
3173+
_ => None,
3174+
})
3175+
.expect("query should contain a join");
3176+
3177+
assert!(join.force_nested_loop);
3178+
Ok(())
3179+
}
3180+
3181+
#[cfg(feature = "spill")]
3182+
#[test]
3183+
fn optimizer_hints_can_be_combined() -> Result<(), DatabaseError> {
3184+
let tables = build_t1_table()?;
3185+
let plan = tables.plan(
3186+
"select /*+ FORCE_AGG_SPILL, FORCE_NEST_LOOP_JOIN */ c1, count(c3) \
3187+
from t1 join t2 on c1 = c3 group by c1",
3188+
)?;
3189+
let aggregate_plan = plan
3190+
.childrens
3191+
.iter()
3192+
.find(|plan| matches!(plan.operator, Operator::Aggregate(_)))
3193+
.expect("query should contain an aggregate");
3194+
let Operator::Aggregate(aggregate) = &aggregate_plan.operator else {
3195+
unreachable!()
3196+
};
3197+
let join = aggregate_plan
3198+
.childrens
3199+
.iter()
3200+
.find_map(|plan| match &plan.operator {
3201+
Operator::Join(operator) => Some(operator),
3202+
_ => None,
3203+
})
3204+
.expect("aggregate input should contain a join");
3205+
3206+
assert!(aggregate.force_spill);
3207+
assert!(join.force_nested_loop);
3208+
Ok(())
3209+
}
3210+
31573211
#[cfg(feature = "copy")]
31583212
#[test]
31593213
fn test_copy_file_format_options() -> Result<(), DatabaseError> {

0 commit comments

Comments
 (0)