Entity Framework 8 AsSplitQuery benchmark




Date Added (UTC):

05 May 2024 @ 15:30

Date Updated (UTC):

05 May 2024 @ 15:30


.NET Version(s):

.NET 8

Tag(s):

#DBOperations #EntityFramework


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Columns;

public class Patient
{
    public int PatientId { get; set; }
    public string Name { get; set; }
    public List<Appointment> Appointments { get; set; } = new List<Appointment>();
}

public class Doctor
{
    public int DoctorId { get; set; }
    public string Name { get; set; }
    public List<Appointment> Appointments { get; set; } = new List<Appointment>();
}

public class Appointment
{
    public int AppointmentId { get; set; }
    public DateTime Date { get; set; }
    public int DoctorId { get; set; }
    public Doctor Doctor { get; set; }
    public int PatientId { get; set; }
    public Patient Patient { get; set; }
}

public class HospitalContext : DbContext
{
    public DbSet<Patient> Patients { get; set; }
    public DbSet<Doctor> Doctors { get; set; }
    public DbSet<Appointment> Appointments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"Server=localhost;Database=TestDB;Trusted_Connection=True;TrustServerCertificate=true;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Patient>().HasMany(p => p.Appointments).WithOne(a => a.Patient);
        modelBuilder.Entity<Doctor>().HasMany(d => d.Appointments).WithOne(a => a.Doctor);
    }
}

public static class DbInitializer
{
    public static void Initialize(HospitalContext context)
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        // Seed the database with dummy data
        var doctors = Enumerable.Range(1, 10).Select(i => new Doctor { Name = $"Dr. {i}" }).ToList();
        var patients = Enumerable.Range(1, 50).Select(i => new Patient { Name = $"Patient {i}" }).ToList();

        // Seed the database with dummy data using a fixed seed for consistency
        var seed = 123; // Fixed seed value
        var random = new Random(seed);

        var appointments = Enumerable.Range(1, 100).Select(i => new Appointment
        {
            Date = DateTime.Now.AddDays(random.Next(1, 365)),
            Doctor = doctors[random.Next(doctors.Count)],
            Patient = patients[random.Next(patients.Count)]
        }).ToList();

        context.Doctors.AddRange(doctors);
        context.Patients.AddRange(patients);
        context.Appointments.AddRange(appointments);
        context.SaveChanges();
    }
}

[MemoryDiagnoser]
[ReturnValueValidator(failOnError: true)]
[HideColumns(Column.AllocRatio, Column.RatioSD, Column.Median, Column.Gen0, Column.Gen1)]
[Config(typeof(Config))]
public class AsSplitQueryBenchmark
{
    private HospitalContext _context;

    [GlobalSetup]
    public void Setup()
    {
        _context = new HospitalContext();
        DbInitializer.Initialize(_context);
    }

    [Benchmark]
    public List<Appointment> LoadStandard()
    {
        return _context.Appointments
            .Include(a => a.Patient)
            .ThenInclude(p => p.Appointments) 
            .Include(a => a.Doctor)
            .ThenInclude(d => d.Appointments) 
            .ToList();
    }

    [Benchmark(Baseline = true)]
    public List<Appointment> LoadSplitQuery()
    {
        return _context.Appointments
            .Include(a => a.Patient)
            .ThenInclude(p => p.Appointments) 
            .Include(a => a.Doctor)
            .ThenInclude(d => d.Appointments) 
            .AsSplitQuery()
            .ToList();
    }

    private class Config : ManualConfig
    {
        public Config()
        {
            SummaryStyle =
                SummaryStyle.Default
                .WithRatioStyle(RatioStyle.Percentage)
                .WithTimeUnit(Perfolizer.Horology.TimeUnit.Millisecond);
        }
    }
}

// .NET 8 Lowered C# Code unavailable due to errors:
error CS0234: The type or namespace name 'EntityFrameworkCore' does not exist in the namespace 'Microsoft' (are you missing an assembly reference?)
error CS0246: The type or namespace name 'DbContext' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbSet<>' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbContextOptionsBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'ModelBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'Database' and no accessible extension method 'Database' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'SaveChanges' and no accessible extension method 'SaveChanges' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)

// .NET 8 IL Code unavailable due to errors:
error CS0234: The type or namespace name 'EntityFrameworkCore' does not exist in the namespace 'Microsoft' (are you missing an assembly reference?)
error CS0246: The type or namespace name 'DbContext' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbSet<>' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbContextOptionsBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'ModelBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'Database' and no accessible extension method 'Database' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'SaveChanges' and no accessible extension method 'SaveChanges' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)

// .NET 8 Jit Asm Code unavailable due to errors:
error CS0234: The type or namespace name 'EntityFrameworkCore' does not exist in the namespace 'Microsoft' (are you missing an assembly reference?)
error CS0246: The type or namespace name 'DbContext' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbSet<>' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'DbContextOptionsBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS0246: The type or namespace name 'ModelBuilder' could not be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'Database' and no accessible extension method 'Database' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)
error CS1061: 'HospitalContext' does not contain a definition for 'SaveChanges' and no accessible extension method 'SaveChanges' accepting a first argument of type 'HospitalContext' could be found (are you missing a using directive or an assembly reference?)


Benchmark Description:


This benchmark setup is designed to measure the performance differences between executing a single complex query versus split queries when retrieving data involving multiple relationships in an Entity Framework Core (EF Core) context. Specifically, it focuses on the performance of loading complex object graphs from a database using EF Core, a common scenario in many applications. The benchmark is set against a hypothetical hospital database involving `Patient`, `Doctor`, and `Appointment` entities with relationships among them. ### General Setup Overview - **Entity Framework Core**: The version of EF Core is not explicitly mentioned, but it's assumed to be a relatively recent version since `AsSplitQuery` is a feature introduced in EF Core 5.0. - **Database**: The benchmark uses a SQL Server database, as indicated by the connection string in the `OnConfiguring` method of the `HospitalContext`. - **Data Seeding**: The `DbInitializer.Initialize` method seeds the database with dummy data for doctors, patients, and appointments to ensure there's enough data to make the benchmark meaningful. - **BenchmarkDotNet**: This benchmark uses BenchmarkDotNet, a powerful library for benchmarking .NET code. Specific configurations include: - **MemoryDiagnoser**: To diagnose memory allocations. - **ReturnValueValidator**: Ensures the methods return the expected results without errors. - **HideColumns**: Hides specific columns in the output for clarity. - **Config**: Custom configuration for the summary style and time unit. ### Benchmark Methods #### `LoadStandard` - **Purpose**: This method tests the performance of loading related data using EF Core's default behavior, which combines all related data into a single SQL query. - **Performance Aspect**: It measures how efficiently EF Core can handle complex object graphs in a single query, including potential issues like the N+1 problem or large Cartesian products. - **Expectations**: This method might be slower and consume more memory for complex queries due to the potential inefficiencies in handling all relations in a single roundtrip to the database. #### `LoadSplitQuery` - **Purpose**: This method evaluates the performance of loading related data using EF Core's `AsSplitQuery` method, which splits the single complex query into multiple simpler queries. - **Performance Aspect**: It focuses on the potential performance improvement by reducing the complexity of each query and avoiding large Cartesian products, at the cost of making multiple roundtrips to the database. - **Expectations**: This method is expected to perform better in scenarios where the single query approach suffers from inefficiencies. However, the improvement might vary based on the specific data model and the database's ability to handle multiple queries efficiently. ### Insights and Results Running these benchmarks should provide insights into the trade-offs between single query and split query approaches in EF Core. In scenarios with deeply nested relationships or large datasets, the split query approach might offer significant performance improvements by reducing the complexity of each individual query. However, the benefits need to be weighed against the increased number of database roundtrips. The specific results will depend on various factors, including the database schema, the size of the dataset, and the database server's performance. Generally, you would expect the `LoadSplitQuery` method to show better performance in terms of execution time for complex queries, with potential trade-offs in the number of database roundtrips and overall data retrieval strategy.


Benchmark Comments: