dotnet/efcore · error · DbQueryConcurrencyException

The results of a split query could not be correlated because

Error message

The results of a split query could not be correlated because the data was modified concurrently while the query was executing. Re-execute the query, or execute it within a serializable or snapshot transaction to prevent concurrent modifications.

What it means

Thrown by SplitQueryResultCoordinator.VerifyNoOrphanedChildRows() when, after processing all parent rows, a split query's child data reader still has unread rows. This means a child row's parent key did not match any parent in the result set — which can only happen if data was modified between the parent and child query round-trips. EF treats this as a concurrency violation rather than silently dropping orphan rows (see issue #33826).

Source

Thrown at src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs:104

    /// <summary>
    ///     This is an internal API that supports the Entity Framework Core infrastructure and not subject to
    ///     the same compatibility standards as public APIs. It may be changed or removed without notice in
    ///     any release. You should only use it directly in your code with extreme caution and knowing that
    ///     doing so can result in application failures when updating to a new Entity Framework Core release.
    /// </summary>
    public void VerifyNoOrphanedChildRows()
    {
        foreach (var dataReaderContext in DataReaders)
        {
            // Split collection queries correlate child rows to parents by consuming, for each parent (in order), the leading child
            // rows whose parent key matches. HasNext == true here means the split reader is parked on a child row that didn't match
            // the last parent - and since every parent has now been processed, that row (and any after it) belongs to no parent in
            // the parent query's results. This can only happen if the data was modified concurrently between the execution of the
            // parent query and the child query, leaving orphan child rows that would otherwise be silently dropped (see #33826).
            if (dataReaderContext?.HasNext == true)
            {
                throw new DbQueryConcurrencyException(RelationalStrings.SplitQueryConcurrentModification);
            }
        }
    }
}

View on GitHub (pinned to 3a2006ef56)

Solutions

  1. Wrap the query in a serializable or snapshot transaction to ensure both round-trips see a consistent snapshot: using var tx = context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable).
  2. Re-execute the query — if the concurrent modification was transient, the retry may succeed.
  3. Switch to a single query (AsSingleQuery() or remove UseSplitQuery) so all data is fetched in one round-trip, eliminating the window for concurrent modification.
  4. Reduce the window by querying fewer rows or optimizing the query to execute faster.

Example fix

// before
var orders = context.Orders
    .AsSplitQuery()
    .Include(o => o.Items)
    .ToList(); // concurrent insert creates orphan child rows

// after
using var tx = context.Database
    .BeginTransaction(System.Data.IsolationLevel.Serializable);
var orders = context.Orders
    .AsSplitQuery()
    .Include(o => o.Items)
    .ToList();
tx.Commit();
Defensive patterns

Strategy: retry

Validate before calling

// Check if the transaction isolation level is appropriate for split queries
var isolation = context.Database.CurrentTransaction?.GetDbTransaction().IsolationLevel;
if (isolation is not null and not (System.Data.IsolationLevel.Serializable
    or System.Data.IsolationLevel.Snapshot))
{
    logger.LogWarning("Split query running under {Isolation} — concurrent modification risk", isolation);
}

Try / catch

// Retry pattern for transient concurrent modifications
const int maxRetries = 3;
for (var attempt = 0; attempt < maxRetries; attempt++)
{
    try
    {
        return await context.Orders.AsSplitQuery().Include(o => o.Items).ToListAsync();
    }
    catch (DbQueryConcurrencyException ex) when (attempt < maxRetries - 1)
    {
        logger.LogWarning(ex, "Split query concurrent modification, retrying (attempt {Attempt})", attempt + 1);
        await Task.Delay(100 * (attempt + 1));
        continue;
    }
}

Prevention

When it happens

Trigger: Using AsSplitQuery() or UseSplitQuery() with Include() for related collections. Between the parent query round-trip and the child query round-trip, a concurrent INSERT/UPDATE/DELETE changes the data so that child rows exist whose parent key no longer matches any parent row returned by the first query.

Common situations: High-concurrency environment where related entities are being inserted/deleted while a long-running split query iterates. Transaction isolation level is ReadUncommitted or ReadCommitted, allowing the child query to see rows inserted after the parent query's snapshot. Bulk import or data migration running in parallel with query workloads.

Related errors


AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11). Data as JSON: /api/errors/813e56becce7be4a. Report an issue: GitHub.