dotnet/efcore · error · DbQueryConcurrencyException

SplitQueryConcurrentModification

SplitQueryConcurrentModification

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 (SplitQueryResultCoordinator.cs:104) as a DbQueryConcurrencyException. Split queries run the parent query and each related collection as separate SQL statements, correlating child rows to parents by key order. After the last parent is processed, if a child reader still has rows (HasNext == true), those rows belong to no parent, meaning the data was modified concurrently between the parent and child queries. EF throws rather than silently drop orphaned rows.

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 dbf9771522)

Solutions

  1. Re-execute the query (transient races often resolve on retry).
  2. Wrap the split query in a serializable or snapshot transaction so the read sees a stable point-in-time view.
  3. Switch the query to AsSingleQuery() (a single JOINed statement) which has no parent/child correlation window.
  4. Reduce the window between parent and child reads (fewer includes, smaller result sets).
  5. Apply an optimistic-retry policy around the read for concurrency-induced occurrences.

Example fix

// before - split query races against concurrent writes
var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList();
// may throw SplitQueryConcurrentModification if items are inserted concurrently

// after - snapshot transaction gives a stable view
using var tx = ctx.Database.BeginTransaction(System.Data.IsolationLevel.Snapshot);
var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList();
tx.Commit();

// or fall back to a single query (no correlation window)
var data = ctx.Orders.AsSingleQuery().Include(o => o.Items).ToList();
Defensive patterns

Strategy: retry

Validate before calling

// Run split queries under a snapshot/serializable transaction to avoid concurrent-modification races.
using var tx = ctx.Database.BeginTransaction(IsolationLevel.Snapshot);
var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList();
tx.Commit();
// Or prefer AsSingleQuery() for writes-prone environments.

Try / catch

// Retry transient split-query concurrent-modification failures.
for (int attempt = 0; ; attempt++) {
    try {
        using var tx = ctx.Database.BeginTransaction(IsolationLevel.Snapshot);
        var data = ctx.Orders.AsSplitQuery().Include(o => o.Items).ToList();
        tx.Commit();
        return data;
    } catch (DbQueryConcurrencyException ex) when (attempt < 3) {
        // SplitQueryConcurrentModification: data changed concurrently; retry
        await Task.Delay(100 * (attempt + 1));
    }
}

Prevention

When it happens

Trigger: Running an AsSplitQuery() with related collections while another transaction inserts child rows whose parent key is not among the (already-fetched) parents, or deletes parents, between the parent and child query executions. Fires at the end of enumeration when VerifyNoOrphanedChildRows is called (GroupBySplitQueryingEnumerable / SplitQueryingEnumerable end-of-enumeration paths).

Common situations: High-concurrency apps using split queries without transaction isolation; background jobs inserting children during a read; deleting parents mid-query; race between read and write under default (read committed / no snapshot) isolation.

Related errors


AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06). Data as JSON: /api/errors/e639080e51bb02e6. Report an issue: GitHub.