Understanding Performance Implications of First vs Single in EF Core with .NET 8 in SQL Server

Understanding Performance Implications of First vs Single in EF Core with .NET 8 in SQL Server

This post explores the impact of choosing between First and Single methods in EF Core on performance when expecting a single result, emphasizing practical guidance for developers.

Understanding First and Single

To grasp their impact on performance, let's clarify the roles of First and Single in EF Core:

  • First fetches the first element. It raises an exception if the sequence is empty.
  • Single asserts that a sequence must contain precisely one element. It throws an exception if the sequence is empty or if more than one element is found.

These methods are foundational for querying data, with distinct behaviors that influence how we interact with database records.

Execution Plan

Both First and Single generate similar SQL queries under the hood with the difference in the number of records returned by the top expression.

First

context.MyEntities.First(x => x.ExternalId == externalId);
SELECT TOP(1) [m].[Id], [m].[ExternalId]
FROM [MyEntities] AS [m]
WHERE [m].[ExternalId] = @__externalId_0

Single

context.MyEntities.Single(x => x.ExternalId == externalId);
SELECT TOP(2) [m].[Id], [m].[ExternalId]
FROM [MyEntities] AS [m]
WHERE [m].[ExternalId] = @__externalId_0

Because the top operation is executed on the result of the where clause, the performance should be the same when expecting a single record.

Client-side Overhead

Suprisingly, EF Core uses System.Linq.Enumerable.Single with SingleQueryingEnumerable<T> for both First and Single operations when the results are read from the database.

public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
    TSource? single = source.TryGetSingle(out bool found);
    if (!found)
    {
        ThrowHelper.ThrowNoElementsException();
    }

    return single!;
}

private static TSource? TryGetSingle<TSource>(this IEnumerable<TSource> source, out bool found)
{
    if (source is null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (source is IList<TSource> list)
    {
        switch (list.Count)
        {
            case 0:
                found = false;
                return default;
            case 1:
                found = true;
                return list[0];
        }
    }
    else
    {
        using (IEnumerator<TSource> e = source.GetEnumerator())
        {
            if (!e.MoveNext())
            {
                found = false;
                return default;
            }

            TSource result = e.Current;
            if (!e.MoveNext())
            {
                found = true;
                return result;
            }
        }
    }

    found = false;
    ThrowHelper.ThrowMoreThanOneElementException();
    return default;
}

This extension always verify for more than one element in the list but because EF Core generates a TOP(1) with IQueryable<TSource>.First, the MoveNext returns false and can't throw an exception in this case.

What are the performance implications of choosing one method over another?

To provide a clearer understanding, I've conducted a benchmark analysis comparing the methods within a real-world context. This involved the instantiation of a DbContext to maintain a clean and isolated state, explicitly avoiding the use of caching mechanisms.

[Benchmark]
public async Task<MyEntity> SingleByGuid()
{
    await using var context = CreateContext();
    return await context.MyEntities.SingleAsync(e => e.ExternalId == _randomExternalId);
}

[Benchmark(Baseline = true)]
public async Task<MyEntity> FirstByGuid()
{
    await using var context = CreateContext();
    return await context.MyEntities.FirstAsync(e => e.ExternalId == _randomExternalId);
}

The outcomes consistently show that the First method is slightly faster, indicating there may be a small overhead that has gone unnoticed in my analysis.

Method Mean Error StdDev Ratio Allocated
Single 1.812 ms 0.0630 ms 0.1735 ms 1.02 81 KB
First 1.810 ms 0.0843 ms 0.2265 ms 1.00 80.91 KB

Conclusion

In practice, the performance difference between using First and Single when querying a column expected to be unique in SQL Server with EF Core is minimal. The choice between the two should be guided more by the semantics of your application's requirements rather than performance concerns. Single is more semantically appropriate for enforcing uniqueness, while First could be slightly faster in scenarios where every microsecond counts.