Unleash EF Core Performance With Compiled Queries

Unleash EF Core Performance With Compiled Queries

4 min read ·

Thank you to our sponsors who keep this newsletter free to the reader:

Have some C# and .NET experience, and want to get started building apps? Get introduced to the most common technologies like gRPC, Blazor, GraphQL, Cosmos DB, SignalR, .NET MAUI, and more in Apps and Services with .NET 7.

QuadSpinner Highlighter is an open-source Visual Studio extension that lets you highlight important objects and arbitrary texts to help you navigate your code more easily.

In this week's newsletter I want to introduce you to an interesting feature in EF Core called Compiled Queries.

If you have queries that you execute frequently in your application with a different set of parameters, it can be helpful to explicitly compile the query and reuse it throughout the lifetime of your application.

Compiled Queries are more performant than standard EF queries, because they can take advantage of some additional optimizations.

Let me show you how to use Compiled Queries, and how much performance improvement to expect!

How To Create Compiled Queries

Lets define a simple class that will represent out data model that we will use for writing EF queries:

public class Newsletter
    public long Id { get; init; }

    public string Title { get; init; }

    public int ReadTimeInMinutes { get; init; }

How would we write a simple query to fetch a Newsletter by the Id? I think you can do this in your sleep.

using var dbContext = new AppDbContext();

var newsletter = dbContext.Set<Newsletter>().FirstOrDefault(n => n.Id == id);

Now, how do we convert this query into a Complied Query?

There are a few steps involved:

  • Create a Compiled Query by calling EF.CompileQuery
  • Store the Compiled Query in a static field, so that it can be reused
  • Execute the database query using the Compiled Query

You can define the Compiled Query in a static field inside of the AppDbContext. And then expose a method that will accept an argument, and pass it to the Compiled Query to invoke it.

using Microsoft.EntityFrameworkCore;

public class AppDbContext
    private static Func<AppDbContext, long, Newsletter?> GetNewsletter =
            (dbContext, id) =>
                dbContext.Set<Newsletter>().FirstOrDefault(n => n.Id == id));

    public Newsletter? GetNewsletter(long id)
        return GetNewsletter(this, id);

This is how we would call the method which invokes the Compiled Query:

using var dbContext = new AppDbContext();

var newsletter = dbContext.GetNewsletter(id);

I ran some benchmarks, with the following setup:

  • EF Core 7
  • SQL Server 2022
  • Table with 10,000 records

The Compiled Query was consistently around 10% faster.

I also tried running a no-tracking query by calling AsNoTracking() and observed similar results.

Why Are Compiled Queries Faster?

So we can conclude that Compiled Queries are faster. But why is that?

Let's examine what happens when we execute an EF LINQ query. Before EF can convert the query into valid SQL that can be executed in the database, it needs to compile the query. The compiled query is cached and EF will be able to reuse that cached query. In some situations the query needs to be recompiled, introducing additional performance costs.

When we explicitly compile the query by calling EF.CompileQuery, we can utilize some optimization techniques that aren't available at runtime.

Note that Compiled Queries only improve the performance of the in-memory portion of executing an EF query. The round trip time and materializing results from the database remain unaffected.

Can We Make Compiled Queries Asynchronous?

I showed you how to write a synchronous Compiled Query. But due to performance considerations we almost always want to execute database queries asynchronously.

Here's how we can create an asynchronous Compiled Query:

using Microsoft.EntityFrameworkCore;

public class AppDbContext
    private static Func<AppDbContext, string, Task<Newsletter?>> GetByTitle =
            (AppDbContext context, string title) =>
                context.Set<Newsletter>().FirstOrDefault(c => c.Title == title));

    public async Task<Newsletter?> GetNewsletterByTitleAsync(string title)
        return await GetByTitle(this, title);

It's interesting that we aren't writing an asynchronous query in the expression passed to EF.CompileAsyncQuery. It will be converted to an asynchronous query during compilation.

Compiled Queries Aren't a Silver Bullet

You might be tempted to go and convert all of your EF queries into Compiled Queries, to squeeze out that last little bit of performance. I urge you not do it. Compiled Queries are a useful tool, but they aren't the solution to all your problems.

Instead, I think we should use Compiled Queries sparingly, only in situations where we really need to do these kinds of micro-optimizations.

Although Compiled Queries seem great, we can't deny they increase the complexity of our code considerably. If you think the slight performance improvement gained from using Compiled Queries justifies the increase in complexity, then by all means, you should use them. Otherwise, I would look for other ways to improve performance.

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 2,900+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Patreon Community: Join a community of 1,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
  4. Promote yourself to 53,000+ subscribers by sponsoring this newsletter.

Become a Better .NET Software Engineer

Join 53,000+ engineers who are improving their skills every Saturday morning.