Skip to content

Commit ae36b8d

Browse files
committed
Initial implementation for bucketization.
1 parent 74068d2 commit ae36b8d

13 files changed

+426
-141
lines changed

src/EFCore.Relational/Query/SqlNullabilityProcessor.cs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,7 @@ protected override Expression VisitExtension(Expression node)
135135
{
136136
// Create parameter for value if we didn't create it yet,
137137
// otherwise reuse it.
138-
if (expandedParameters.Count <= i)
139-
{
140-
var parameterName = Uniquifier.Uniquify(valuesParameter.Name, queryParameters, int.MaxValue);
141-
queryParameters.Add(parameterName, values[i]);
142-
var parameterExpression = new SqlParameterExpression(parameterName, values[i]?.GetType() ?? typeof(object), elementTypeMapping);
143-
expandedParameters.Add(parameterExpression);
144-
}
138+
ExpandParameterIfNeeded(valuesParameter.Name, expandedParameters, queryParameters, i, values[i], elementTypeMapping);
145139

146140
processedValues.Add(
147141
new RowValueExpression(
@@ -832,13 +826,7 @@ InExpression ProcessInExpressionValues(
832826
{
833827
// Create parameter for value if we didn't create it yet,
834828
// otherwise reuse it.
835-
if (expandedParameters.Count <= i)
836-
{
837-
var parameterName = Uniquifier.Uniquify(valuesParameter.Name, parameters, int.MaxValue);
838-
parameters.Add(parameterName, values[i]);
839-
var parameterExpression = new SqlParameterExpression(parameterName, values[i]?.GetType() ?? typeof(object), elementTypeMapping);
840-
expandedParameters.Add(parameterExpression);
841-
}
829+
ExpandParameterIfNeeded(valuesParameter.Name, expandedParameters, parameters, i, values[i], elementTypeMapping);
842830

843831
// Use separate counter, because we may skip nulls.
844832
processedValues.Add(expandedParameters[expandedParametersCounter++]);
@@ -857,6 +845,23 @@ InExpression ProcessInExpressionValues(
857845
throw new UnreachableException();
858846
}
859847
}
848+
849+
// Bucketization.
850+
if ((valuesParameter.TranslationMode ?? CollectionParameterTranslationMode) is ParameterTranslationMode.MultipleParameters)
851+
{
852+
// For provider to effectively disable bucketization, return always 1 from ParametersPadFactor.
853+
var padFactor = ParametersPadFactor(values.Count);
854+
var padding = (padFactor - (values.Count % padFactor)) % padFactor;
855+
for (var i = 0; i < padding; i++)
856+
{
857+
// Create parameter for value if we didn't create it yet,
858+
// otherwise reuse it.
859+
ExpandParameterIfNeeded(valuesParameter.Name, expandedParameters, parameters, values.Count + i, values[^1], elementTypeMapping);
860+
861+
// Use separate counter, because we may skip nulls.
862+
processedValues.Add(expandedParameters[expandedParametersCounter++]);
863+
}
864+
}
860865
}
861866
else
862867
{
@@ -1488,6 +1493,21 @@ protected virtual SqlExpression VisitJsonScalar(
14881493
protected virtual bool PreferExistsToInWithCoalesce
14891494
=> false;
14901495

1496+
/// <summary>
1497+
/// Gets the factor by which the parameters are padded when generating a parameterized collection
1498+
/// when using multiple parameters. This helps with query plan bloat.
1499+
/// </summary>
1500+
/// <param name="count">Number of value parameters are generated for.</param>
1501+
protected virtual int ParametersPadFactor(int count)
1502+
=> count switch
1503+
{
1504+
<= 5 => 1,
1505+
<= 150 => 10,
1506+
<= 750 => 50,
1507+
<= 2000 => 100,
1508+
_ => 200,
1509+
};
1510+
14911511
// Note that we can check parameter values for null since we cache by the parameter nullability; but we cannot do the same for bool.
14921512
private bool IsNull(SqlExpression? expression)
14931513
=> expression is SqlConstantExpression { Value: null }
@@ -2121,4 +2141,21 @@ private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool opera
21212141

21222142
private static bool IsLogicalNot(SqlUnaryExpression? sqlUnaryExpression)
21232143
=> sqlUnaryExpression is { OperatorType: ExpressionType.Not } && sqlUnaryExpression.Type == typeof(bool);
2144+
2145+
private static void ExpandParameterIfNeeded(
2146+
string valuesParameterName,
2147+
List<SqlParameterExpression> expandedParameters,
2148+
Dictionary<string, object?> parameters,
2149+
int index,
2150+
object? value,
2151+
RelationalTypeMapping typeMapping)
2152+
{
2153+
if (expandedParameters.Count <= index)
2154+
{
2155+
var parameterName = Uniquifier.Uniquify(valuesParameterName, parameters, int.MaxValue);
2156+
parameters.Add(parameterName, value);
2157+
var parameterExpression = new SqlParameterExpression(parameterName, value?.GetType() ?? typeof(object), typeMapping);
2158+
expandedParameters.Add(parameterExpression);
2159+
}
2160+
}
21242161
}

src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
1616
/// </summary>
1717
public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor
1818
{
19+
private const int MaxParameterCount = 2100;
20+
1921
/// <summary>
2022
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2123
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -294,7 +296,7 @@ private bool TryHandleOverLimitParameters(
294296
// SQL Server has limit on number of parameters in a query.
295297
// If we're over that limit, we switch to using single parameter
296298
// and processing it through JSON functions.
297-
if (values.Count > 2098)
299+
if (values.Count > MaxParameterCount)
298300
{
299301
if (_sqlServerSingletonOptions.SupportsJsonFunctions)
300302
{

test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,27 @@ public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Mul
215215
Assert.Equivalent(new[] { 2 }, result);
216216
}
217217

218+
[ConditionalFact]
219+
public virtual async Task Parameter_collection_Contains_parameter_bucketization()
220+
{
221+
var contextFactory = await InitializeAsync<TestContext>(
222+
onConfiguring: b => SetParameterizedCollectionMode(b, ParameterTranslationMode.MultipleParameters),
223+
seed: context =>
224+
{
225+
context.AddRange(
226+
new TestEntity { Id = 1 },
227+
new TestEntity { Id = 2 },
228+
new TestEntity { Id = 100 });
229+
return context.SaveChangesAsync();
230+
});
231+
232+
await using var context = contextFactory.CreateContext();
233+
234+
var ints = new[] { 2, 999, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 };
235+
var result = await context.Set<TestEntity>().Where(c => ints.Contains(c.Id)).Select(c => c.Id).ToListAsync();
236+
Assert.Equivalent(new[] { 2 }, result);
237+
}
238+
218239
protected class TestOwner
219240
{
220241
public int Id { get; set; }

0 commit comments

Comments
 (0)