镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Count a reused parameterized collection once when checking the SQL Server parameter limit - #39634

Open
benedict-odonovan wants to merge 1 commit into
dotnet:mainfrom
benedict-odonovan:fix/39631-reused-collection-parameter-count
Open

benedict-odonovan wants to merge 1 commit into
dotnet:mainfrom
benedict-odonovan:fix/39631-reused-collection-parameter-count

Conversation

@benedict-odonovan

Copy link
Copy Markdown
Contributor

Fixes #39631

  • I've read the guidelines for contributing and seen the walkthrough
  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team
  • The code builds and tests pass locally (also verified by our automated build checks)
  • Commit messages follow this format
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

Summary

When the same captured collection is used more than once in a query, SQL Server falls back from expanded parameters to OPENJSON far below the 2,100-parameter limit:

var ids = Enumerable.Range(1, 600).ToList();

context.Blogs
    .Where(b => ids.Contains(b.Id))
    .Select(b => new
    {
        b.Id,
        First = b.Posts.Where(p => ids.Contains(p.BlogId)).Select(p => p.Title).FirstOrDefault(),
        Second = b.Posts.Where(p => ids.Contains(p.BlogId)).Select(p => p.Rating).FirstOrDefault(),
        Third = b.Posts.Where(p => ids.Contains(p.BlogId)).Count()
    });
// 10.0.12: @ids1 ... @ids600, shared by all four IN lists
// 11.0.0:  OPENJSON(@ids) in all four places

OPENJSON here costs more than it saves. In the application from the issue, a 950-item list is used in the outer WHERE and in three correlated subqueries. After the fallback, server CPU for that statement went from 24 ms to 176 ms per execution, because each subquery parses the JSON again.

Cause.

  • SqlNullabilityProcessor expands each collection parameter once and reuses those parameters at every occurrence, through _collectionParameterExpansionMap. That map is keyed by SqlParameterExpression equality (name, type and type mapping). The bucket padding grows the same shared list, so it ends up at the largest size any occurrence needs.
  • Make type mapping generic so that it can create value comparers for NativeAOT #38440 changed ParametersCounter to add the expanded size of a MultipleParameters collection on every occurrence. So the list above counts as 4 × 600 = 2,400, which is over MaxParameterCount (2,098), although only 600 parameters are sent.
  • The ..._same_parameter tests in PrimitiveCollectionsQuerySqlServerTest were changed in Make type mapping generic so that it can create value comparers for NativeAOT #38440 to assert OPENJSON. In 10.0 they asserted expanded parameters.

Change

  • ParametersCounter now counts each collection parameter once, keyed the same way as _collectionParameterExpansionMap. A collection that gets separate expansions, because it's compared to columns with different type mappings, is still counted once per expansion.
  • Each occurrence's size includes its own bucket padding, and the counter keeps the largest size seen per parameter. A VALUES occurrence isn't padded and an IN occurrence is, so a list used both ways is counted at its padded size whichever comes first.
  • Parameter mode is unchanged: one parameter per collection.

Tests

  • The ..._same_parameter and ..._same_parameter_different_property tests in PrimitiveCollectionsQuerySqlServerTest and PrimitiveCollectionsQueryOldSqlServerTest go back to expecting expanded parameters. They now also check how many are sent, e.g. @extra1000 but no @extra1001. Id and Int have the same type mapping, so the different_property tests share one expansion.
  • New Same_parameter_with_different_type_mappings_over_parameter_limit: 1,100 values compared to a datetime and a datetime2 column get two expansions (2,200 parameters), so the query still falls back. If the counter is keyed by name only, this test fails with SQL Server's "too many parameters" error.
  • New Parameter_collection_reused_with_and_without_padding_over_parameter_limit: 1,901 values used in Count (1,901 parameters) and in Contains (padded to 2,000), plus 91 values padded to 100, is 2,100 parameters and must fall back. If only the first occurrence is counted, this test fails with the same SQL Server error.

Locally, all SQL Server query tests pass (20,910 passed, 0 failed), and the primitive-collections suites pass with the two new tests. Without the change, the four reused-collection tests fail because the queries fall back to OPENJSON.

…rver parameter limit

SqlNullabilityProcessor expands each collection parameter once and shares the
expanded parameters across occurrences, but ParametersCounter added the
expanded size on every occurrence, so a list used several times fell back to
OPENJSON far below the 2,100-parameter limit.

Fixes dotnet#39631
@benedict-odonovan
benedict-odonovan requested a review from a team as a code owner November 13, 2026 15:20
Copilot AI balanced review requested due to automatic review settings November 13, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A critical unresolved test mismatch remains because the JSON-type fixture still expects OPENJSON for same-parameter cases affected by the new collection-counting behavior.

1 open issue

Review effort: Lite

Comment on lines +469 to +483
// Occurrences share one expansion, padded to the largest bucket any of them needs.
var parameters = parametersDecorator.GetAndDisableCaching();
var count = ((IEnumerable?)parameters[sqlParameterExpression.Name])?.Cast<object?>().Count() ?? 0;
Count += count;

if (bucketization)
{
var elementTypeMapping = (RelationalTypeMapping)sqlParameterExpression.TypeMapping!.ElementTypeMapping!;
Count += bucketizationPadding(count, elementTypeMapping);
count += bucketizationPadding(count, elementTypeMapping);
}

var counted = _expandedParameterCounts.GetValueOrDefault(sqlParameterExpression);
if (count > counted)
{
Count += count - counted;
_expandedParameterCounts[sqlParameterExpression] = count;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL Server: a parameterized collection used several times in one query falls back to OPENJSON well below the parameter limit (11.0 regression)

2 participants