-
Notifications
You must be signed in to change notification settings - Fork 259
perf: improve to-one relational filters #4235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
cca604d
to
befe230
Compare
CodSpeed Performance ReportMerging #4235 will not alter performanceComparing Summary
|
@@ -453,6 +453,14 @@ impl<'a> Select<'a> { | |||
self | |||
} | |||
|
|||
pub fn join<J>(mut self, join: J) -> Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add documentation
@@ -344,6 +344,20 @@ impl<'a> Table<'a> { | |||
|
|||
self | |||
} | |||
|
|||
pub fn join<J>(self, join: J) -> Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add documentation
insta::assert_snapshot!( | ||
run_query!(&runner, r#"{ findManyAlbum(where: { Tracks: { some: { OR:[{ MediaType: {is: { Name: { equals: "MediaType1" }}}}, { Genre: { is: { Name: { equals: "Genre2" }}}}]}}}) { Title }}"#), | ||
run_query!(&runner, r#"{ findManyAlbum(where: { Tracks: { some: { OR:[{ MediaType: {is: { Name: { equals: "MediaType1" }}}}, { Genre: { is: { Name: { equals: "Genre2" }}}}]}}}, orderBy: { Title: asc }) { Title }}"#), | ||
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"Album3"},{"Title":"Album4"},{"Title":"Album5"}]}}"### | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ordering is needed because joins messes up the natural order
insta::assert_snapshot!( | ||
run_query!(&runner, r#"{ findManyGenre(where: { Tracks: { some: {} }}) { Name }}"#), | ||
run_query!(&runner, r#"{ findManyGenre(where: { Tracks: { some: {} }}, orderBy: { Name: asc }) { Name }}"#), | ||
@r###"{"data":{"findManyGenre":[{"Name":"Genre1"},{"Name":"Genre2"},{"Name":"Genre3"}]}}"### | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ordering is needed because joins messes up the natural order
@@ -31,8 +31,9 @@ pub(crate) async fn update_one_with_selection( | |||
return get_single_record(conn, model, &filter, &selected_fields, &[], ctx).await; | |||
} | |||
|
|||
let update = build_update_and_set_query(model, args, Some(&selected_fields), ctx) | |||
.so_that(build_update_one_filter(record_filter).aliased_condition_from(None, false, ctx)); | |||
let cond = FilterBuilder::without_top_level_joins().visit_filter(build_update_one_filter(record_filter), ctx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like for all mutations, we don't render filters with top-level joins because we can't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all copy & pasted stuff that comes with the filter refactoring. So no changes in this file.
field: &RelationFieldRef, | ||
alias: &str, | ||
parent_alias: Option<&str>, | ||
ctx: &Context<'_>, | ||
) -> AliasedJoin { | ||
let (left_fields, right_fields) = if rf.is_inlined_on_enclosing_model() { | ||
(rf.scalar_fields(), rf.referenced_fields()) | ||
} else { | ||
( | ||
rf.related_field().referenced_fields(), | ||
rf.related_field().scalar_fields(), | ||
) | ||
}; | ||
|
||
let right_table_alias = format!("{}_{}", join_prefix, rf.related_model().name()); | ||
|
||
let related_model = rf.related_model(); | ||
let pairs = left_fields.into_iter().zip(right_fields); | ||
|
||
let on_conditions: Vec<Expression> = pairs | ||
.map(|(a, b)| { | ||
let a_col = match previous_join { | ||
Some(prev_join) => Column::from((prev_join.alias.to_owned(), a.db_name().to_owned())), | ||
None => a.as_column(ctx), | ||
}; | ||
let join_columns: Vec<Column> = field | ||
.join_columns(ctx) | ||
.map(|c| c.opt_table(parent_alias.map(ToOwned::to_owned))) | ||
.collect(); | ||
|
||
let b_col = Column::from((right_table_alias.clone(), b.db_name().to_owned())); | ||
let related_table = field.related_model().as_table(ctx); | ||
let related_join_columns: Vec<_> = ModelProjection::from(field.related_field().linking_fields()) | ||
.as_columns(ctx) | ||
.map(|col| col.table(alias.to_owned())) | ||
.collect(); | ||
|
||
a_col.equals(b_col).into() | ||
}) | ||
.collect::<Vec<_>>(); | ||
let join = related_table | ||
.alias(alias.to_owned()) | ||
.on(Row::from(related_join_columns).equals(Row::from(join_columns))); | ||
|
||
AliasedJoin { | ||
alias: right_table_alias.to_owned(), | ||
data: related_model | ||
.as_table(ctx) | ||
.alias(right_table_alias) | ||
.on(ConditionTree::And(on_conditions)), | ||
alias: alias.to_owned(), | ||
data: Join::Left(join), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of the refactorings of this function is just a nicest way to express the same thing.
match alias { | ||
Some(alias) => self.table(alias.to_string(None)), | ||
None => self, | ||
fn visit_relation_filter_select(&mut self, filter: RelationFilter, ctx: &Context<'_>) -> Select<'static> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untouched function except to accommodate for the new shared state and apis that come with it
query-engine/connectors/sql-query-connector/src/filter/visitor.rs
Outdated
Show resolved
Hide resolved
}; | ||
(conditions.not(), Some(output_joins)) | ||
} | ||
RelationCondition::ToOneRelatedRecord if self.should_render_join() && !filter.field.is_list() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ to_one: { ... } }
now uses a LEFT JOIN
.
// If the relation is not inlined and we can use joins, then we join the relation and check whether the related linking fields are null. | ||
// | ||
// ```sql | ||
// SELECT "Parent"."id" FROM "Parent" | ||
// LEFT JOIN "Child" AS "j1" ON ("j1"."parentId" = "Parent"."id") | ||
// WHERE "j1"."parentId" IS NULL OFFSET; | ||
// ``` | ||
if self.should_render_join() { | ||
let alias = self.next_alias(AliasMode::Join); | ||
|
||
let conditions: Vec<_> = ModelProjection::from(filter.field.related_field().linking_fields()) | ||
.as_columns(ctx) | ||
.map(|col| col.aliased_col(Some(alias.flip(AliasMode::Join)), ctx)) | ||
.map(|c| c.aliased_col(Some(alias), ctx)) | ||
.map(|c| c.is_null()) | ||
.map(Expression::from) | ||
.collect(); | ||
|
||
let nested_conditions = self | ||
.nested_filter | ||
.aliased_condition_from(Some(alias.flip(AliasMode::Join)), false, ctx) | ||
.invert_if(condition.invert_of_subselect()); | ||
|
||
let conditions = selected_identifier | ||
.clone() | ||
.into_iter() | ||
.fold(nested_conditions, |acc, column| acc.and(column.is_not_null())); | ||
|
||
let join = related_table | ||
.alias(alias.to_string(Some(AliasMode::Join))) | ||
.on(Row::from(related_join_columns).equals(Row::from(join_columns))); | ||
let join = compute_one2m_join( | ||
&filter.field, | ||
alias.to_string(None).as_str(), | ||
parent_alias_string.as_deref(), | ||
ctx, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is the most important change of this function. When traversing a { to_one: null }
, we're now using a join if we can.
query-engine/connectors/sql-query-connector/src/filter/visitor.rs
Outdated
Show resolved
Hide resolved
let sub_select = Select::from_table(relation_table) | ||
.columns(columns) | ||
.and_where(columns_not_null); | ||
fn visit_scalar_list_filter(&mut self, filter: ScalarListFilter, ctx: &Context<'_>) -> ConditionTree<'static> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untouched function
AggregationFilter::Sum(filter) => aggregate_conditions(*filter, alias, reverse, |x| sum(x).into(), ctx), | ||
AggregationFilter::Min(filter) => aggregate_conditions(*filter, alias, reverse, |x| min(x).into(), ctx), | ||
AggregationFilter::Max(filter) => aggregate_conditions(*filter, alias, reverse, |x| max(x).into(), ctx), | ||
fn scalar_filter_aliased_cond( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Untouched function
pub(crate) struct AliasedJoin { | ||
// Actual join data to be passed to quaint | ||
pub(crate) data: JoinData<'static>, | ||
pub(crate) data: Join<'static>, | ||
// Alias used for the join. eg: LEFT JOIN ... AS <alias> | ||
pub(crate) alias: String, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An AliasedJoin
now also holds the type of join. Makes it easier to fold on a list of joins.
let (filter, filter_joins) = self | ||
.filter | ||
.map(|f| f.aliased_condition_from(None, false, ctx)) | ||
.unwrap_or(ConditionTree::NoCondition); | ||
.map(|f| FilterBuilder::with_top_level_joins().visit_filter(f, ctx)) | ||
.unwrap_or((ConditionTree::NoCondition, None)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we gather the filter and the associated new joins with it.
let joined_table = if let Some(filter_joins) = filter_joins { | ||
filter_joins | ||
.into_iter() | ||
.fold(joined_table, |acc, join| acc.join(join.data)) | ||
} else { | ||
joined_table | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These joins are then added here to the top-level part of the SELECT
AST.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feedback provided on a call together. This looks good from my perspective.
6af97ef
to
53e8b3b
Compare
Overview
fixes prisma/prisma#14688
fixes prisma/prisma#17879
fixes prisma/prisma#7894
LEFT JOIN
s when traversing to-one relations. Note: when rendering joins for mutations, we always need at least one sub-select before we can render nested joins.where
directives on related records prisma#15417SQL differences
Consider this schema:
to-one -> to-one
Before 👇
After 👇
some -> to-one (inlined)
Before 👇
After 👇
some -> every -> to-one (inlined)
Before 👇
After 👇
Benchmarks
(source)