Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 87 additions & 17 deletions src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,11 @@ static void UpdateLimit(SelectExpression selectExpression)
innerShaperExpression);
}

AddJoin(JoinType.OuterApply, ref innerSelectExpression, out _);
// Single-result (to-one) joins never increase result cardinality, so the outer entity's
// identifiers are already sufficient to uniquely identify rows. We don't need to add the
// inner's identifiers to our own; doing so would cause unnecessary reference table JOINs,
// ORDER BY columns, and projections in split collection queries (#29182).
AddJoin(JoinType.OuterApply, ref innerSelectExpression, out _, isToOneJoin: true);
var offset = _clientProjections.Count;
var count = innerSelectExpression._clientProjections.Count;

Expand Down Expand Up @@ -3018,7 +3022,8 @@ private void AddJoin(
JoinType joinType,
ref SelectExpression innerSelect,
out bool innerPushdownOccurred,
SqlExpression? joinPredicate = null)
SqlExpression? joinPredicate = null,
bool isToOneJoin = false)
{
innerPushdownOccurred = false;
// Try to convert Apply to normal join
Expand Down Expand Up @@ -3099,7 +3104,8 @@ private void AddJoin(
joinType == JoinType.CrossApply ? JoinType.InnerJoin : JoinType.LeftJoin,
ref innerSelect,
out innerPushdownOccurred,
joinPredicate);
joinPredicate,
isToOneJoin);

return;
}
Expand Down Expand Up @@ -3153,24 +3159,37 @@ private void AddJoin(
innerPushdownOccurred = true;
}

// If the caller knows this is a to-one join (e.g. SingleResult in ApplyProjection), or if the
// join predicate equates all of the inner's identifier columns (PK) to outer columns, then the
// join doesn't increase cardinality — the outer identifiers are already sufficient. Skipping the
// inner's identifiers avoids unnecessary JOINs, ORDER BY columns, and projections in split
// collection queries (#29182).
if (!isToOneJoin && joinPredicate is not null && innerSelect._identifier.Count > 0)
{
isToOneJoin = AllInnerIdentifiersInPredicate(joinPredicate, innerSelect._identifier);
}

if (_identifier.Count > 0 && innerSelect._identifier.Count > 0)
{
switch (joinType)
if (!isToOneJoin)
{
case JoinType.LeftJoin or JoinType.OuterApply:
_identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)));
break;
switch (joinType)
{
case JoinType.LeftJoin or JoinType.OuterApply:
_identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)));
break;

case JoinType.RightJoin:
var nullableOuterIdentifier = _identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)).ToList();
_identifier.Clear();
_identifier.AddRange(nullableOuterIdentifier);
_identifier.AddRange(innerSelect._identifier);
break;
case JoinType.RightJoin:
var nullableOuterIdentifier = _identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)).ToList();
_identifier.Clear();
_identifier.AddRange(nullableOuterIdentifier);
_identifier.AddRange(innerSelect._identifier);
break;

default:
_identifier.AddRange(innerSelect._identifier);
break;
default:
_identifier.AddRange(innerSelect._identifier);
break;
}
}
}
else
Expand All @@ -3185,7 +3204,7 @@ private void AddJoin(
var joinTable = joinType switch
{
JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate!),
JoinType.LeftJoin => new LeftJoinExpression(innerTable, joinPredicate!),
JoinType.LeftJoin => new LeftJoinExpression(innerTable, joinPredicate!, prunable: isToOneJoin),
JoinType.RightJoin => new RightJoinExpression(innerTable, joinPredicate!),
JoinType.CrossJoin => new CrossJoinExpression(innerTable),
JoinType.CrossApply => new CrossApplyExpression(innerTable),
Expand Down Expand Up @@ -3219,6 +3238,57 @@ static void GetPartitions(SelectExpression select, SqlExpression sqlExpression,
}
}

// Checks whether all of the inner select's identifier columns appear as equality terms in the
// join predicate. If so, the join is to-one from the outer's perspective (each outer row matches
// at most one inner row), meaning the inner identifiers are redundant for row identification.
static bool AllInnerIdentifiersInPredicate(
SqlExpression joinPredicate,
List<(ColumnExpression Column, ValueComparer Comparer)> innerIdentifiers)
{
Span<bool> matched = innerIdentifiers.Count <= 8
? stackalloc bool[innerIdentifiers.Count]
: new bool[innerIdentifiers.Count];

MatchIdentifiersInPredicate(joinPredicate, innerIdentifiers, matched);

foreach (var m in matched)
{
if (!m)
{
return false;
}
}

return true;

static void MatchIdentifiersInPredicate(
SqlExpression expression,
List<(ColumnExpression Column, ValueComparer Comparer)> identifiers,
Span<bool> matched)
{
switch (expression)
{
case SqlBinaryExpression { OperatorType: ExpressionType.Equal } binary:
for (var i = 0; i < identifiers.Count; i++)
{
if (!matched[i]
&& (identifiers[i].Column.Equals(binary.Left)
|| identifiers[i].Column.Equals(binary.Right)))
{
matched[i] = true;
}
}

break;

case SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binary:
MatchIdentifiersInPredicate(binary.Left, identifiers, matched);
MatchIdentifiersInPredicate(binary.Right, identifiers, matched);
break;
}
}
}

static SqlExpression? TryExtractJoinKey(SelectExpression outer, SelectExpression inner, bool allowNonEquality)
{
if (inner.Limit != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ public override async Task Delete_entity_with_auto_include(bool async)
"""
DELETE FROM [c]
FROM [Context30572_Principal] AS [c]
LEFT JOIN [Context30572_Dependent] AS [c0] ON [c].[DependentId] = [c0].[Id]
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -532,18 +532,8 @@ public override async Task Delete_with_LeftJoin(bool async)

AssertSql(
"""
@p='0'
@p1='100'
DELETE FROM [o]
FROM [Order Details] AS [o]
LEFT JOIN (
SELECT [o0].[OrderID]
FROM [Orders] AS [o0]
WHERE [o0].[OrderID] < 10300
ORDER BY [o0].[OrderID]
OFFSET @p ROWS FETCH NEXT @p1 ROWS ONLY
) AS [o1] ON [o].[OrderID] = [o1].[OrderID]
WHERE [o].[OrderID] < 10276
""");
}
Expand All @@ -554,18 +544,8 @@ public override async Task Delete_with_LeftJoin_via_flattened_GroupJoin(bool asy

AssertSql(
"""
@p='0'
@p1='100'
DELETE FROM [o]
FROM [Order Details] AS [o]
LEFT JOIN (
SELECT [o0].[OrderID]
FROM [Orders] AS [o0]
WHERE [o0].[OrderID] < 10300
ORDER BY [o0].[OrderID]
OFFSET @p ROWS FETCH NEXT @p1 ROWS ONLY
) AS [o1] ON [o].[OrderID] = [o1].[OrderID]
WHERE [o].[OrderID] < 10276
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ FROM [JoinOneToTwo] AS [j0]
WHERE [e1].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s].[Id], [s0].[OneId], [s0].[TwoId]
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s0].[OneId]
""");
}

Expand All @@ -59,7 +59,7 @@ FROM [EntityOneEntityTwo] AS [e2]
WHERE [e3].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId]
""");
}

Expand Down Expand Up @@ -90,7 +90,7 @@ FROM [EntityOneEntityTwo] AS [e4]
WHERE [e3].[Id] = @p
) AS [s1] ON [s].[Id] = [s1].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId0], [s1].[TwoSkipSharedId0]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[OneSkipSharedId0]
""");
}

Expand Down Expand Up @@ -121,7 +121,7 @@ FROM [JoinTwoToThree] AS [j]
INNER JOIN [EntityThrees] AS [e4] ON [j].[ThreeId] = [e4].[Id]
) AS [s1] ON [s].[Id] = [s1].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s0].[Id], [s1].[ThreeId], [s1].[TwoId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s1].[ThreeId]
""");
}

Expand Down Expand Up @@ -153,7 +153,7 @@ FROM [JoinTwoToThree] AS [j]
WHERE [e4].[Id] IN (13, 11)
) AS [s1] ON [s].[Id] = [s1].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s0].[Id], [s1].[ThreeId], [s1].[TwoId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s1].[ThreeId]
""");
}

Expand Down Expand Up @@ -193,7 +193,7 @@ public override async Task Load_collection_using_Query_with_join(bool async)
"""
@p='3'
SELECT [s].[Id], [s].[CollectionInverseId], [s].[Name], [s].[ReferenceInverseId], [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id0], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId], [s2].[Id], [s2].[Name], [s1].[CollectionInverseId], [s1].[Name0], [s1].[ReferenceInverseId]
SELECT [s].[Id], [s].[CollectionInverseId], [s].[Name], [s].[ReferenceInverseId], [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId], [s2].[Id], [s2].[Name], [s1].[Id0], [s1].[CollectionInverseId], [s1].[Name0], [s1].[ReferenceInverseId]
FROM [EntityOnes] AS [e]
INNER JOIN (
SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[OneSkipSharedId], [e0].[TwoSkipSharedId]
Expand All @@ -216,7 +216,7 @@ FROM [EntityOneEntityTwo] AS [e5]
WHERE [e6].[Id] = @p
) AS [s2] ON [s].[Id] = [s2].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id0], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s2].[OneSkipSharedId]
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ FROM [JoinOneToTwo] AS [j0]
WHERE [e1].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s].[Id], [s0].[OneId], [s0].[TwoId]
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s0].[OneId]
"""
: """
@p='3'
Expand All @@ -52,7 +52,7 @@ FROM [JoinOneToTwo] AS [j0]
WHERE [e1].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s].[Id], [s0].[OneId], [s0].[TwoId]
ORDER BY [e].[Id], [s].[OneId], [s].[TwoId], [s0].[OneId]
""");
}

Expand All @@ -78,7 +78,7 @@ FROM [EntityOneEntityTwo] AS [e2]
WHERE [e3].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId]
""");
}

Expand Down Expand Up @@ -109,7 +109,7 @@ FROM [EntityOneEntityTwo] AS [e4]
WHERE [e3].[Id] = @p
) AS [s1] ON [s].[Id] = [s1].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId0], [s1].[TwoSkipSharedId0]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[OneSkipSharedId0]
""");
}

Expand Down Expand Up @@ -140,7 +140,7 @@ FROM [JoinTwoToThree] AS [j]
INNER JOIN [EntityThrees] AS [e4] ON [j].[ThreeId] = [e4].[Id]
) AS [s1] ON [s].[Id] = [s1].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s0].[Id], [s1].[ThreeId], [s1].[TwoId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s1].[ThreeId]
""");
}

Expand Down Expand Up @@ -172,7 +172,7 @@ FROM [JoinTwoToThree] AS [j]
WHERE [e4].[Id] IN (13, 11)
) AS [s1] ON [s].[Id] = [s1].[TwoId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s0].[Id], [s1].[ThreeId], [s1].[TwoId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s0].[OneSkipSharedId], [s0].[TwoSkipSharedId], [s1].[ThreeId]
""");
}

Expand Down Expand Up @@ -212,7 +212,7 @@ public override async Task Load_collection_using_Query_with_join(bool async)
"""
@p='3'
SELECT [s].[Id], [s].[CollectionInverseId], [s].[ExtraId], [s].[Name], [s].[ReferenceInverseId], [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id0], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId], [s2].[Id], [s2].[Name], [s1].[CollectionInverseId], [s1].[ExtraId], [s1].[Name0], [s1].[ReferenceInverseId]
SELECT [s].[Id], [s].[CollectionInverseId], [s].[ExtraId], [s].[Name], [s].[ReferenceInverseId], [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId], [s2].[Id], [s2].[Name], [s1].[Id0], [s1].[CollectionInverseId], [s1].[ExtraId], [s1].[Name0], [s1].[ReferenceInverseId]
FROM [EntityOnes] AS [e]
INNER JOIN (
SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[ReferenceInverseId], [e0].[OneSkipSharedId], [e0].[TwoSkipSharedId]
Expand All @@ -235,7 +235,7 @@ FROM [EntityOneEntityTwo] AS [e5]
WHERE [e6].[Id] = @p
) AS [s2] ON [s].[Id] = [s2].[TwoSkipSharedId]
WHERE [e].[Id] = @p
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s].[Id], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s1].[Id0], [s2].[OneSkipSharedId], [s2].[TwoSkipSharedId]
ORDER BY [e].[Id], [s].[OneSkipSharedId], [s].[TwoSkipSharedId], [s1].[Id], [s1].[OneSkipSharedId], [s1].[TwoSkipSharedId], [s2].[OneSkipSharedId]
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ FROM [ManyMN_DB] AS [m2]
WHERE [m3].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[ManyN_Id]
WHERE [m].[Id] = @p
ORDER BY [m].[Id], [s].[Id0], [s].[Id], [s0].[Id]
ORDER BY [m].[Id], [s].[Id0]
""",
//
"""
Expand Down Expand Up @@ -95,7 +95,7 @@ FROM [ManyMN_DB] AS [m2]
WHERE [m3].[Id] = @p
) AS [s0] ON [s].[Id] = [s0].[ManyM_Id]
WHERE [m].[Id] = @p
ORDER BY [m].[Id], [s].[Id0], [s].[Id], [s0].[Id]
ORDER BY [m].[Id], [s].[Id0]
""");
}
}
Loading
Loading