Skip to content

Commit 02d37a2

Browse files
authored
Optimize multiple consecutive LIMITs (#35384)
Fixes #35383
1 parent f163289 commit 02d37a2

21 files changed

+221
-162
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

+43-5
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression
654654
}
655655

656656
selectExpression.ApplyOffset(translation);
657-
selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(1))!);
657+
ApplyLimit(selectExpression, TranslateExpression(Expression.Constant(1))!);
658658

659659
return source;
660660
}
@@ -693,7 +693,7 @@ protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression s
693693
_queryCompilationContext.Logger.FirstWithoutOrderByAndFilterWarning();
694694
}
695695

696-
selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(1))!);
696+
ApplyLimit(selectExpression, TranslateExpression(Expression.Constant(1))!);
697697

698698
return source.ShaperExpression.Type != returnType
699699
? source.UpdateShaperExpression(Expression.Convert(source.ShaperExpression, returnType))
@@ -940,7 +940,7 @@ private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerK
940940
}
941941

942942
selectExpression.ReverseOrderings();
943-
selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(1))!);
943+
ApplyLimit(selectExpression, TranslateExpression(Expression.Constant(1))!);
944944

945945
return source.ShaperExpression.Type != returnType
946946
? source.UpdateShaperExpression(Expression.Convert(source.ShaperExpression, returnType))
@@ -1208,7 +1208,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
12081208
}
12091209

12101210
var selectExpression = (SelectExpression)source.QueryExpression;
1211-
selectExpression.ApplyLimit(TranslateExpression(Expression.Constant(_subquery ? 1 : 2))!);
1211+
ApplyLimit(selectExpression, TranslateExpression(Expression.Constant(_subquery ? 1 : 2))!);
12121212

12131213
return source.ShaperExpression.Type != returnType
12141214
? source.UpdateShaperExpression(Expression.Convert(source.ShaperExpression, returnType))
@@ -1258,11 +1258,49 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
12581258
_queryCompilationContext.Logger.RowLimitingOperationWithoutOrderByWarning();
12591259
}
12601260

1261-
selectExpression.ApplyLimit(translation);
1261+
ApplyLimit(selectExpression, translation);
12621262

12631263
return source;
12641264
}
12651265

1266+
private void ApplyLimit(SelectExpression selectExpression, SqlExpression limit)
1267+
{
1268+
var oldLimit = selectExpression.Limit;
1269+
1270+
if (oldLimit is null)
1271+
{
1272+
selectExpression.SetLimit(limit);
1273+
return;
1274+
}
1275+
1276+
if (oldLimit is SqlConstantExpression { Value: int oldConst } && limit is SqlConstantExpression { Value: int newConst })
1277+
{
1278+
// if both the old and new limit are constants, use the smaller one
1279+
// (aka constant-fold LEAST(constA, constB))
1280+
if (oldConst > newConst)
1281+
{
1282+
selectExpression.SetLimit(limit);
1283+
}
1284+
1285+
return;
1286+
}
1287+
1288+
if (oldLimit.Equals(limit))
1289+
{
1290+
return;
1291+
}
1292+
1293+
// if possible, use LEAST(oldLimit, limit); otherwise, use nested queries
1294+
if (_sqlTranslator.GenerateLeast([oldLimit, limit], limit.Type) is { } newLimit)
1295+
{
1296+
selectExpression.SetLimit(newLimit);
1297+
}
1298+
else
1299+
{
1300+
selectExpression.ApplyLimit(limit);
1301+
}
1302+
}
1303+
12661304
/// <inheritdoc />
12671305
protected override ShapedQueryExpression? TranslateTakeWhile(ShapedQueryExpression source, LambdaExpression predicate)
12681306
=> null;

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

+10
Original file line numberDiff line numberDiff line change
@@ -1924,6 +1924,16 @@ public void ApplyLimit(SqlExpression sqlExpression)
19241924
PushdownIntoSubquery();
19251925
}
19261926

1927+
SetLimit(sqlExpression);
1928+
}
1929+
1930+
/// <summary>
1931+
/// Sets a new limit of the <see cref="SelectExpression" /> to limit the number of rows returned in the result set.
1932+
/// </summary>
1933+
/// <param name="sqlExpression">An expression representing limit row count.</param>
1934+
[EntityFrameworkInternal]
1935+
public void SetLimit(SqlExpression sqlExpression)
1936+
{
19271937
Limit = sqlExpression;
19281938

19291939
if (Offset is null && Limit is SqlConstantExpression { Value: 1 })

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs

+16
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,22 @@ public override async Task SelectMany_correlated_with_outer_7(bool async)
10961096
AssertSql();
10971097
}
10981098

1099+
public override async Task SelectMany_with_multiple_Take(bool async)
1100+
{
1101+
// Cosmos client evaluation. Issue #17246.
1102+
await AssertTranslationFailed(() => base.SelectMany_with_multiple_Take(async));
1103+
1104+
AssertSql();
1105+
}
1106+
1107+
public override async Task Select_with_multiple_Take(bool async)
1108+
{
1109+
// Cosmos client evaluation. Issue #17246.
1110+
await AssertTranslationFailed(() => base.Select_with_multiple_Take(async));
1111+
1112+
AssertSql();
1113+
}
1114+
10991115
public override async Task FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(bool async)
11001116
{
11011117
// Cosmos client evaluation. Issue #17246.

test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs

+14
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,20 @@ from o in ss.Set<Order>().Where(o => c.CustomerID.Length >= o.CustomerID.Length)
14411441
AssertEqual(e.o, a.o);
14421442
});
14431443

1444+
[ConditionalTheory]
1445+
[MemberData(nameof(IsAsyncData))]
1446+
public virtual Task SelectMany_with_multiple_Take(bool async)
1447+
=> AssertQuery(
1448+
async,
1449+
ss => ss.Set<Customer>().SelectMany(c => c.Orders.OrderBy(o => o.OrderID).Take(5).Take(3)));
1450+
1451+
[ConditionalTheory]
1452+
[MemberData(nameof(IsAsyncData))]
1453+
public virtual Task Select_with_multiple_Take(bool async)
1454+
=> AssertQuery(
1455+
async,
1456+
ss => ss.Set<Customer>().OrderBy(o => o.CustomerID).Take(5).Take(3));
1457+
14441458
[ConditionalTheory]
14451459
[MemberData(nameof(IsAsyncData))]
14461460
public virtual Task FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(bool async)

test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyNoTrackingQuerySqlServerTest.cs

+5-7
Original file line numberDiff line numberDiff line change
@@ -300,17 +300,15 @@ public override async Task Skip_navigation_order_by_single_or_default(bool async
300300
"""
301301
SELECT [s0].[Id], [s0].[Name]
302302
FROM [EntityOnes] AS [e]
303-
OUTER APPLY (
304-
SELECT TOP(1) [s].[Id], [s].[Name]
303+
LEFT JOIN (
304+
SELECT [s].[Id], [s].[Name], [s].[LeftId]
305305
FROM (
306-
SELECT TOP(1) [e0].[Id], [e0].[Name]
306+
SELECT [e0].[Id], [e0].[Name], [j].[LeftId], ROW_NUMBER() OVER(PARTITION BY [j].[LeftId] ORDER BY [e0].[Id]) AS [row]
307307
FROM [JoinOneSelfPayload] AS [j]
308308
INNER JOIN [EntityOnes] AS [e0] ON [j].[RightId] = [e0].[Id]
309-
WHERE [e].[Id] = [j].[LeftId]
310-
ORDER BY [e0].[Id]
311309
) AS [s]
312-
ORDER BY [s].[Id]
313-
) AS [s0]
310+
WHERE [s].[row] <= 1
311+
) AS [s0] ON [e].[Id] = [s0].[LeftId]
314312
""");
315313
}
316314

test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs

+5-7
Original file line numberDiff line numberDiff line change
@@ -299,17 +299,15 @@ public override async Task Skip_navigation_order_by_single_or_default(bool async
299299
"""
300300
SELECT [s0].[Id], [s0].[Name]
301301
FROM [EntityOnes] AS [e]
302-
OUTER APPLY (
303-
SELECT TOP(1) [s].[Id], [s].[Name]
302+
LEFT JOIN (
303+
SELECT [s].[Id], [s].[Name], [s].[LeftId]
304304
FROM (
305-
SELECT TOP(1) [e0].[Id], [e0].[Name]
305+
SELECT [e0].[Id], [e0].[Name], [j].[LeftId], ROW_NUMBER() OVER(PARTITION BY [j].[LeftId] ORDER BY [e0].[Id]) AS [row]
306306
FROM [JoinOneSelfPayload] AS [j]
307307
INNER JOIN [EntityOnes] AS [e0] ON [j].[RightId] = [e0].[Id]
308-
WHERE [e].[Id] = [j].[LeftId]
309-
ORDER BY [e0].[Id]
310308
) AS [s]
311-
ORDER BY [s].[Id]
312-
) AS [s0]
309+
WHERE [s].[row] <= 1
310+
) AS [s0] ON [e].[Id] = [s0].[LeftId]
313311
""");
314312
}
315313

test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs

+68-56
Original file line numberDiff line numberDiff line change
@@ -760,14 +760,10 @@ public override async Task Project_single_element_from_collection_with_OrderBy_T
760760
AssertSql(
761761
"""
762762
SELECT (
763-
SELECT TOP(1) [o0].[CustomerID]
764-
FROM (
765-
SELECT TOP(1) [o].[CustomerID], [o].[OrderID]
766-
FROM [Orders] AS [o]
767-
WHERE [c].[CustomerID] = [o].[CustomerID]
768-
ORDER BY [o].[OrderID]
769-
) AS [o0]
770-
ORDER BY [o0].[OrderID])
763+
SELECT TOP(1) [o].[CustomerID]
764+
FROM [Orders] AS [o]
765+
WHERE [c].[CustomerID] = [o].[CustomerID]
766+
ORDER BY [o].[OrderID])
771767
FROM [Customers] AS [c]
772768
""");
773769
}
@@ -828,14 +824,10 @@ public override async Task Project_single_element_from_collection_with_OrderBy_T
828824
AssertSql(
829825
"""
830826
SELECT (
831-
SELECT TOP(1) [o0].[CustomerID]
832-
FROM (
833-
SELECT TOP(1) [o].[CustomerID], [o].[OrderID]
834-
FROM [Orders] AS [o]
835-
WHERE [c].[CustomerID] = [o].[CustomerID]
836-
ORDER BY [o].[OrderID]
837-
) AS [o0]
838-
ORDER BY [o0].[OrderID])
827+
SELECT TOP(1) [o].[CustomerID]
828+
FROM [Orders] AS [o]
829+
WHERE [c].[CustomerID] = [o].[CustomerID]
830+
ORDER BY [o].[OrderID])
839831
FROM [Customers] AS [c]
840832
WHERE [c].[CustomerID] = N'ALFKI'
841833
""");
@@ -869,14 +861,10 @@ public override async Task Project_single_element_from_collection_with_multiple_
869861
AssertSql(
870862
"""
871863
SELECT (
872-
SELECT TOP(1) [o0].[CustomerID]
873-
FROM (
874-
SELECT TOP(2) [o].[CustomerID], [o].[OrderID], [o].[OrderDate]
875-
FROM [Orders] AS [o]
876-
WHERE [c].[CustomerID] = [o].[CustomerID]
877-
ORDER BY [o].[OrderID], [o].[OrderDate] DESC
878-
) AS [o0]
879-
ORDER BY [o0].[OrderID], [o0].[OrderDate] DESC)
864+
SELECT TOP(1) [o].[CustomerID]
865+
FROM [Orders] AS [o]
866+
WHERE [c].[CustomerID] = [o].[CustomerID]
867+
ORDER BY [o].[OrderID], [o].[OrderDate] DESC)
880868
FROM [Customers] AS [c]
881869
""");
882870
}
@@ -892,14 +880,10 @@ await base
892880
AssertSql(
893881
"""
894882
SELECT (
895-
SELECT TOP(1) [o0].[c]
896-
FROM (
897-
SELECT TOP(2) CAST(LEN([o].[CustomerID]) AS int) AS [c], [o].[OrderID], [o].[OrderDate]
898-
FROM [Orders] AS [o]
899-
WHERE [c].[CustomerID] = [o].[CustomerID]
900-
ORDER BY [o].[OrderID], [o].[OrderDate] DESC
901-
) AS [o0]
902-
ORDER BY [o0].[OrderID], [o0].[OrderDate] DESC)
883+
SELECT TOP(1) CAST(LEN([o].[CustomerID]) AS int)
884+
FROM [Orders] AS [o]
885+
WHERE [c].[CustomerID] = [o].[CustomerID]
886+
ORDER BY [o].[OrderID], [o].[OrderDate] DESC)
903887
FROM [Customers] AS [c]
904888
""");
905889
}
@@ -911,14 +895,10 @@ public override async Task Project_single_element_from_collection_with_multiple_
911895
AssertSql(
912896
"""
913897
SELECT (
914-
SELECT TOP(1) [o0].[CustomerID]
915-
FROM (
916-
SELECT TOP(2) [o].[CustomerID], [o].[OrderDate]
917-
FROM [Orders] AS [o]
918-
WHERE [c].[CustomerID] = [o].[CustomerID]
919-
ORDER BY [o].[CustomerID], [o].[OrderDate] DESC
920-
) AS [o0]
921-
ORDER BY [o0].[CustomerID], [o0].[OrderDate] DESC)
898+
SELECT TOP(1) [o].[CustomerID]
899+
FROM [Orders] AS [o]
900+
WHERE [c].[CustomerID] = [o].[CustomerID]
901+
ORDER BY [o].[CustomerID], [o].[OrderDate] DESC)
922902
FROM [Customers] AS [c]
923903
""");
924904
}
@@ -930,15 +910,11 @@ public override async Task Project_single_element_from_collection_with_OrderBy_o
930910
AssertSql(
931911
"""
932912
SELECT COALESCE((
933-
SELECT TOP(1) [s].[OrderID]
934-
FROM (
935-
SELECT TOP(1) [o0].[OrderID], [p].[ProductName]
936-
FROM [Order Details] AS [o0]
937-
INNER JOIN [Products] AS [p] ON [o0].[ProductID] = [p].[ProductID]
938-
WHERE [o].[OrderID] = [o0].[OrderID]
939-
ORDER BY [p].[ProductName]
940-
) AS [s]
941-
ORDER BY [s].[ProductName]), 0)
913+
SELECT TOP(1) [o0].[OrderID]
914+
FROM [Order Details] AS [o0]
915+
INNER JOIN [Products] AS [p] ON [o0].[ProductID] = [p].[ProductID]
916+
WHERE [o].[OrderID] = [o0].[OrderID]
917+
ORDER BY [p].[ProductName]), 0)
942918
FROM [Orders] AS [o]
943919
WHERE [o].[OrderID] < 10300
944920
""");
@@ -953,17 +929,15 @@ public override async Task Project_single_element_from_collection_with_OrderBy_o
953929
"""
954930
SELECT [s0].[OrderID], [s0].[ProductID], [s0].[Discount], [s0].[Quantity], [s0].[UnitPrice]
955931
FROM [Orders] AS [o]
956-
OUTER APPLY (
957-
SELECT TOP(1) [s].[OrderID], [s].[ProductID], [s].[Discount], [s].[Quantity], [s].[UnitPrice]
932+
LEFT JOIN (
933+
SELECT [s].[OrderID], [s].[ProductID], [s].[Discount], [s].[Quantity], [s].[UnitPrice]
958934
FROM (
959-
SELECT TOP(1) [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice], [p].[ProductName]
935+
SELECT [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [p].[ProductName]) AS [row]
960936
FROM [Order Details] AS [o0]
961937
INNER JOIN [Products] AS [p] ON [o0].[ProductID] = [p].[ProductID]
962-
WHERE [o].[OrderID] = [o0].[OrderID]
963-
ORDER BY [p].[ProductName]
964938
) AS [s]
965-
ORDER BY [s].[ProductName]
966-
) AS [s0]
939+
WHERE [s].[row] <= 1
940+
) AS [s0] ON [o].[OrderID] = [s0].[OrderID]
967941
WHERE [o].[OrderID] < 10250
968942
""");
969943
}
@@ -1336,6 +1310,44 @@ WHERE CAST(LEN([c].[CustomerID]) AS int) >= CAST(LEN([o].[CustomerID]) AS int)
13361310
""");
13371311
}
13381312

1313+
public override async Task SelectMany_with_multiple_Take(bool async)
1314+
{
1315+
await base.SelectMany_with_multiple_Take(async);
1316+
1317+
AssertSql(
1318+
"""
1319+
SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate]
1320+
FROM [Customers] AS [c]
1321+
INNER JOIN (
1322+
SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate]
1323+
FROM (
1324+
SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o].[CustomerID] ORDER BY [o].[OrderID]) AS [row]
1325+
FROM [Orders] AS [o]
1326+
) AS [o0]
1327+
WHERE [o0].[row] <= 3
1328+
) AS [o1] ON [c].[CustomerID] = [o1].[CustomerID]
1329+
""");
1330+
}
1331+
1332+
public override async Task Select_with_multiple_Take(bool async)
1333+
{
1334+
await base.Select_with_multiple_Take(async);
1335+
1336+
AssertSql(
1337+
"""
1338+
@p0='3'
1339+
@p='5'
1340+
1341+
SELECT TOP(@p0) [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region]
1342+
FROM (
1343+
SELECT TOP(@p) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
1344+
FROM [Customers] AS [c]
1345+
ORDER BY [c].[CustomerID]
1346+
) AS [c0]
1347+
ORDER BY [c0].[CustomerID]
1348+
""");
1349+
}
1350+
13391351
public override async Task FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(bool async)
13401352
{
13411353
await base.FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(async);

0 commit comments

Comments
 (0)