Skip to content

Commit

Permalink
Document compiled queries
Browse files Browse the repository at this point in the history
Closes dotnet#502
  • Loading branch information
roji committed Oct 21, 2021
1 parent 6cef60c commit bd5ebf7
Show file tree
Hide file tree
Showing 16 changed files with 425 additions and 490 deletions.
25 changes: 25 additions & 0 deletions entity-framework/core/performance/advanced-performance-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,31 @@ 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 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. This 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, up-front compilation of a LINQ query into a .NET delegate. Once this is done, the delegate can be invoked directly to execute the query, without providing the LINQ expression tree; this 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:

[!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.

## 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.
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

0 comments on commit bd5ebf7

Please sign in to comment.