Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document compiled queries #3502

Merged
merged 5 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion entity-framework/core/performance/advanced-performance-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,39 @@ Context pooling works by reusing the same context instance across requests. This

Context pooling is intended for scenarios where the context configuration, which includes services resolved, is fixed between requests. For cases where [Scoped](/aspnet/core/fundamentals/dependency-injection#service-lifetimes) services are required, or configuration needs to be changed, don't use pooling.

## Compiled queries

When EF receives a LINQ query tree for execution, it must first "compile" that tree, e.g. produce SQL from it. Because this task is a heavy process, EF caches queries by the query tree shape, so that queries with the same structure reuse internally-cached compilation outputs. This caching ensures that executing the same LINQ query multiple times is very fast, even if parameter values differ.

However, EF must still perform certain tasks before it can make use of the internal query cache. For example, your query's expression tree must be recursively compared with the expression trees of cached queries, to find the correct cached query. The overhead for this initial processing is negligible in the majority of EF applications, especially when compared to other costs associated with query execution (network I/O, actual query processing and disk I/O at the database...). However, in certain high-performance scenarios it may be desirable to eliminate it.

EF supports *compiled queries*, which allow the explicit compilation of a LINQ query into a .NET delegate. Once this delegate is acquired, it can be invoked directly to execute the query, without providing the LINQ expression tree. This technique bypasses the cache lookup, and provides the most optimized way to execute a query in EF Core. Following are some benchmark results comparing compiled and non-compiled query performance; benchmark on your platform before making any decisions. [The source code is available here](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Benchmarks/ContextPooling.cs), feel free to use it as a basis for your own measurements.

| Method | NumBlogs | Mean | Error | StdDev | Gen 0 | Allocated |
|--------------------- |--------- |---------:|---------:|---------:|-------:|----------:|
| WithCompiledQuery | 1 | 564.2 us | 6.75 us | 5.99 us | 1.9531 | 9 KB |
| WithoutCompiledQuery | 1 | 671.6 us | 12.72 us | 16.54 us | 2.9297 | 13 KB |
| WithCompiledQuery | 10 | 645.3 us | 10.00 us | 9.35 us | 2.9297 | 13 KB |
| WithoutCompiledQuery | 10 | 709.8 us | 25.20 us | 73.10 us | 3.9063 | 18 KB |

To used compiled queries, first compile a query with <xref:Microsoft.EntityFrameworkCore.EF.CompileAsyncQuery%2A?displayProperty=nameWithType> as follows (use <xref:Microsoft.EntityFrameworkCore.EF.CompileQuery%2A?displayProperty=nameWithType> for synchronous queries):

[!code-csharp[Main](../../../samples/core/Performance/Program.cs#CompiledQueryCompile)]

In this code sample, we provide EF with a lambda accepting a `DbContext` instance, and an arbitrary parameter to be passed to the query. You can now invoke that delegate whenever you wish to execute the query:
smitpatel marked this conversation as resolved.
Show resolved Hide resolved

[!code-csharp[Main](../../../samples/core/Performance/Program.cs#CompiledQueryExecute)]

Note that the delegate is thread-safe, and can be invoked concurrently on different context instances.
smitpatel marked this conversation as resolved.
Show resolved Hide resolved

### Limitations

* Compiled queries may only be used against a single EF Core model. Different context instances of the same type can sometimes be configured to use different models; running compiled queries in this scenario is not supported.
* When using parameters in compiled queries, use simple, scalar parameters. More complex parameter expressions - such as member/method accesses on instances - are not supported.

## Query caching and parameterization

When EF receives a LINQ query tree for execution, it must first "compile" that tree into a SQL query. Because this is a heavy process, EF caches queries by the query tree *shape*: queries with the same structure reuse internally-cached compilation outputs, and can skip repeated compilation. The different queries may still reference different *values*, but as long as these values are properly parameterized, the structure is the same and caching will function properly.
When EF receives a LINQ query tree for execution, it must first "compile" that tree, e.g. produce SQL from it. Because this task is a heavy process, EF caches queries by the query tree shape, so that queries with the same structure reuse internally-cached compilation outputs. This caching ensures that executing the same LINQ query multiple times is very fast, even if parameter values differ.

Consider the following two queries:

Expand Down
3 changes: 2 additions & 1 deletion entity-framework/core/performance/efficient-querying.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,5 @@ For more information, see the page on [async programming](xref:core/miscellaneou

## Additional resources

See the [performance section](xref:core/querying/null-comparisons#writing-performant-queries) of the null comparison documentation page for some best practices when comparing nullable values.
* See the [advanced performance topics page](xref:core/performance/advanced-performance-topics) for additional topics related to efficient querying.
* See the [performance section](xref:core/querying/null-comparisons#writing-performant-queries) of the null comparison documentation page for some best practices when comparing nullable values.
167 changes: 82 additions & 85 deletions samples/core/Benchmarks/AverageBlogRanking.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,109 +3,106 @@
using BenchmarkDotNet.Attributes;
using Microsoft.EntityFrameworkCore;

namespace Benchmarks
[MemoryDiagnoser]
public class AverageBlogRanking
{
[MemoryDiagnoser]
public class AverageBlogRanking
[Params(1000)]
public int NumBlogs; // number of records to write [once], and read [each pass]

[GlobalSetup]
public void Setup()
{
[Params(1000)]
public int NumBlogs; // number of records to write [once], and read [each pass]
using var context = new BloggingContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.SeedData(NumBlogs);
}

[GlobalSetup]
public void Setup()
#region LoadEntities
[Benchmark]
public double LoadEntities()
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var blog in ctx.Blogs)
{
using var context = new BloggingContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.SeedData(NumBlogs);
sum += blog.Rating;
count++;
}

#region LoadEntities
[Benchmark]
public double LoadEntities()
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var blog in ctx.Blogs)
{
sum += blog.Rating;
count++;
}

return (double)sum / count;
}
#endregion
return (double)sum / count;
}
#endregion

#region LoadEntitiesNoTracking
[Benchmark]
public double LoadEntitiesNoTracking()
#region LoadEntitiesNoTracking
[Benchmark]
public double LoadEntitiesNoTracking()
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var blog in ctx.Blogs.AsNoTracking())
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var blog in ctx.Blogs.AsNoTracking())
{
sum += blog.Rating;
count++;
}

return (double)sum / count;
sum += blog.Rating;
count++;
}
#endregion

#region ProjectOnlyRanking
[Benchmark]
public double ProjectOnlyRanking()
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var rating in ctx.Blogs.Select(b => b.Rating))
{
sum += rating;
count++;
}

return (double)sum / count;
}
#endregion
return (double)sum / count;
}
#endregion

#region CalculateInDatabase
[Benchmark(Baseline = true)]
public double CalculateInDatabase()
#region ProjectOnlyRanking
[Benchmark]
public double ProjectOnlyRanking()
{
var sum = 0;
var count = 0;
using var ctx = new BloggingContext();
foreach (var rating in ctx.Blogs.Select(b => b.Rating))
{
using var ctx = new BloggingContext();
return ctx.Blogs.Average(b => b.Rating);
sum += rating;
count++;
}
#endregion

public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
return (double)sum / count;
}
#endregion

#region CalculateInDatabase
[Benchmark(Baseline = true)]
public double CalculateInDatabase()
{
using var ctx = new BloggingContext();
return ctx.Blogs.Average(b => b.Rating);
}
#endregion

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True");
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

public void SeedData(int numblogs)
{
Blogs.AddRange(
Enumerable.Range(0, numblogs).Select(
i => new Blog
{
Name = $"Blog{i}", Url = $"blog{i}.blogs.net", CreationTime = new DateTime(2020, 1, 1), Rating = i % 5
}));
SaveChanges();
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True");

public class Blog
public void SeedData(int numblogs)
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public DateTime CreationTime { get; set; }
public int Rating { get; set; }
Blogs.AddRange(
Enumerable.Range(0, numblogs).Select(
i => new Blog
{
Name = $"Blog{i}", Url = $"blog{i}.blogs.net", CreationTime = new DateTime(2020, 1, 1), Rating = i % 5
}));
SaveChanges();
}
}

public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public DateTime CreationTime { get; set; }
public int Rating { get; set; }
}
}
2 changes: 1 addition & 1 deletion samples/core/Benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0-rc.1.21452.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="6.0.0-rc.1.21452.10" />
</ItemGroup>
Expand Down
80 changes: 80 additions & 0 deletions samples/core/Benchmarks/CompiledQueries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.EntityFrameworkCore;

[MemoryDiagnoser]
public class CompiledQueries
{
private static readonly Func<BloggingContext, IAsyncEnumerable<Blog>> _compiledQuery
= EF.CompileAsyncQuery((BloggingContext context) => context.Blogs.Where(b => b.Url.StartsWith("http://")));

private BloggingContext _context;

[Params(1, 10)]
public int NumBlogs { get; set; }

[GlobalSetup]
public async Task Setup()
{
using var context = new BloggingContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
await context.SeedDataAsync(NumBlogs);

_context = new BloggingContext();
}

[Benchmark]
public async ValueTask<int> WithCompiledQuery()
{
var idSum = 0;

await foreach (var blog in _compiledQuery(_context))
{
idSum += blog.Id;
}

return idSum;
}

[Benchmark]
public async ValueTask<int> WithoutCompiledQuery()
{
var idSum = 0;

await foreach (var blog in _context.Blogs.Where(b => b.Url.StartsWith("http://")).AsAsyncEnumerable())
{
idSum += blog.Id;
}

return idSum;
}

[GlobalCleanup]
public ValueTask Cleanup() => _context.DisposeAsync();

public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True")
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

public async Task SeedDataAsync(int numBlogs)
{
Blogs.AddRange(Enumerable.Range(0, numBlogs).Select(i => new Blog { Url = $"http://www.someblog{i}.com"}));
await SaveChangesAsync();
}
}

public class Blog
{
public int Id { get; set; }
public string Url { get; set; }
}
}
Loading