Appearance
预编译查询 Compiled Queries
目录
什么是预编译查询
概念理解
预编译查询(Compiled Queries) 是将 LINQ 查询预先编译为可重用的委托,避免每次执行时重新解析和编译查询表达式树。
csharp
// ❌ 普通查询: 每次都编译
var products = await context.Products
.Where(p => p.Price > 100)
.ToListAsync(); // ← 每次都要编译查询
// ✅ 预编译查询: 编译一次,多次使用
private static readonly Func<AppDbContext, decimal, Task<List<Product>>>
GetExpensiveProducts = EF.CompileAsyncQuery(
(AppDbContext context, decimal minPrice) =>
context.Products.Where(p => p.Price > minPrice)
);
// 使用预编译查询
var products = await GetExpensiveProducts(context, 100); // ← 跳过编译,直接执行工作原理
mermaid
graph TB
A[普通查询] --> B[解析表达式树]
B --> C[生成 SQL]
C --> D[执行查询]
D --> E[返回结果]
F[预编译查询] --> G{已编译?}
G -->|首次| H[解析并编译]
G -->|后续| I[直接使用]
H --> J[缓存编译结果]
J --> I
I --> K[执行查询]
K --> L[返回结果]EF Core 查询编译过程
普通查询执行流程:
┌─────────────────────────────────────┐
│ 1. 创建 LINQ 表达式树 │
│ 2. 遍历表达式树 │
│ 3. 解析查询方法(Where, Select等) │
│ 4. 转换为 SQL 表达式 │
│ 5. 参数化处理 │
│ 6. 生成最终 SQL │
│ 7. 执行 SQL │
│ 8. 映射结果到对象 │
└─────────────────────────────────────┘
↑
每次执行都要重复 1-8 步!
预编译查询执行流程:
┌─────────────────────────────────────┐
│ 首次: │
│ 1. 编译查询(步骤 1-6) │
│ 2. 缓存编译结果 │
│ │
│ 后续: │
│ 1. 从缓存获取编译结果 │
│ 2. 执行 SQL(跳过 1-6) │
│ 3. 映射结果到对象 │
└─────────────────────────────────────┘
↑
节省编译时间!为什么需要预编译查询
查询编译的开销
EF Core 每次执行 LINQ 查询时,都需要进行以下操作:
- 表达式树遍历: 分析 LINQ 表达式结构
- 查询转换: 将 LINQ 转换为 SQL
- 参数提取: 处理查询中的变量和参数
- SQL 生成: 生成目标数据库的 SQL 语句
- 缓存查找: 检查是否已有编译缓存
对于高频执行的查询,编译开销可能占总执行时间的 20-40%!
性能影响示例
csharp
// 场景: Web API 中频繁调用的查询
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
// ❌ 普通查询: 每次请求都编译
[HttpGet("expensive/{minPrice}")]
public async Task<ActionResult<List<Product>>> GetExpensive(decimal minPrice)
{
var products = await _context.Products
.Where(p => p.Price > minPrice)
.ToListAsync();
return Ok(products);
}
// 假设:
// - 查询编译时间: 2ms
// - 查询执行时间: 8ms
// - 每秒 100 次请求
//
// 总时间: (2ms + 8ms) × 100 = 1000ms/秒
// 编译占比: 2ms / 10ms = 20% ← 浪费!
}
// ✅ 预编译查询优化后
public class ProductsController : ControllerBase
{
private static readonly Func<AppDbContext, decimal, Task<List<Product>>>
GetExpensiveProducts = EF.CompileAsyncQuery(
(AppDbContext context, decimal minPrice) =>
context.Products.Where(p => p.Price > minPrice)
);
[HttpGet("expensive/{minPrice}")]
public async Task<ActionResult<List<Product>>> GetExpensive(decimal minPrice)
{
var products = await GetExpensiveProducts(_context, minPrice);
return Ok(products);
}
// 优化后:
// - 查询编译时间: 0ms(跳过编译)
// - 查询执行时间: 8ms
// - 每秒 100 次请求
//
// 总时间: 8ms × 100 = 800ms/秒
// 性能提升: 20% 🚀
}何时使用预编译查询
✅ 适合使用:
- 高频执行的查询(API 端点)
- 循环中的查询
- 定时任务中的重复查询
- 热点代码路径
❌ 不适合使用:
- 只执行一次的查询
- 动态构建的复杂查询
- 原型开发阶段
- 性能不敏感的后台任务
如何使用预编译查询
基本语法
同步查询
csharp
// 定义预编译查询
private static readonly Func<AppDbContext, int, Product> GetProductById =
EF.CompileQuery((AppDbContext context, int id) =>
context.Products.FirstOrDefault(p => p.Id == id)
);
// 使用
var product = GetProductById(context, 1);异步查询
csharp
// 定义预编译查询
private static readonly Func<AppDbContext, int, Task<Product>> GetProductByIdAsync =
EF.CompileAsyncQuery((AppDbContext context, int id) =>
context.Products.FirstOrDefaultAsync(p => p.Id == id)
);
// 使用
var product = await GetProductByIdAsync(context, 1);完整示例
示例 1: 简单过滤
csharp
public class ProductQueries
{
// 按价格范围查询
public static readonly Func<AppDbContext, decimal, decimal, Task<List<Product>>>
GetProductsByPriceRange = EF.CompileAsyncQuery(
(AppDbContext context, decimal minPrice, decimal maxPrice) =>
context.Products
.Where(p => p.Price >= minPrice && p.Price <= maxPrice)
.OrderByDescending(p => p.Price)
.ToList()
);
// 按分类查询
public static readonly Func<AppDbContext, int, Task<List<Product>>>
GetProductsByCategory = EF.CompileAsyncQuery(
(AppDbContext context, int categoryId) =>
context.Products
.Where(p => p.CategoryId == categoryId)
.OrderByDescending(p => p.CreatedAt)
.ToList()
);
// 搜索产品
public static readonly Func<AppDbContext, string, Task<List<Product>>>
SearchProducts = EF.CompileAsyncQuery(
(AppDbContext context, string searchTerm) =>
context.Products
.Where(p => p.Name.Contains(searchTerm)
|| p.Description.Contains(searchTerm))
.Take(50)
.ToList()
);
}
// 使用示例
public class ProductService
{
private readonly AppDbContext _context;
public async Task<List<Product>> GetElectronicsAsync()
{
return await ProductQueries.GetProductsByCategory(_context, 1);
}
public async Task<List<Product>> SearchLaptopsAsync()
{
return await ProductQueries.SearchProducts(_context, "laptop");
}
}示例 2: 包含关联数据
csharp
// ⚠️ 注意: CompileQuery 不支持 Include
// 解决方案: 手动加载关联数据或使用投影
// 方式 1: 分别查询
public static readonly Func<AppDbContext, int, Task<Customer>> GetCustomerWithOrders =
EF.CompileAsyncQuery(
(AppDbContext context, int customerId) =>
context.Customers
.FirstOrDefault(c => c.Id == customerId)
);
public async Task<Customer> GetCustomerFullDataAsync(int customerId)
{
var customer = await GetCustomerWithOrders(_context, customerId);
if (customer != null)
{
// 手动加载订单
await _context.Entry(customer)
.Collection(c => c.Orders)
.LoadAsync();
}
return customer;
}
// 方式 2: 使用投影(推荐)
public static readonly Func<AppDbContext, int, Task<CustomerDto>> GetCustomerDto =
EF.CompileAsyncQuery(
(AppDbContext context, int customerId) =>
context.Customers
.Where(c => c.Id == customerId)
.Select(c => new CustomerDto
{
Id = c.Id,
Name = c.Name,
OrderCount = c.Orders.Count,
TotalSpent = c.Orders.Sum(o => o.TotalAmount)
})
.FirstOrDefault()
);示例 3: 分页查询
csharp
// 分页查询(返回总数需要使用 OUT 参数或单独查询)
public static readonly Func<AppDbContext, string, int, int, Task<List<Product>>>
GetPagedProducts = EF.CompileAsyncQuery(
(AppDbContext context, string searchTerm, int skip, int take) =>
context.Products
.Where(p => string.IsNullOrEmpty(searchTerm)
|| p.Name.Contains(searchTerm))
.OrderByDescending(p => p.CreatedAt)
.Skip(skip)
.Take(take)
.ToList()
);
// 使用
var products = await GetPagedProducts(_context, "laptop", 0, 10);示例 4: 批量更新
csharp
// EF Core 8+ 批量操作也可以预编译
public static readonly Func<AppDbContext, decimal, Task<int>> IncreasePrices =
EF.CompileAsyncQuery(
(AppDbContext context, decimal increaseRate) =>
context.Products
.Where(p => p.IsActive)
.ExecuteUpdate(setters => setters
.SetProperty(p => p.Price, p => p.Price * increaseRate))
);
// 使用
var affectedRows = await IncreasePrices(_context, 1.1m);
Console.WriteLine($"更新了 {affectedRows} 条记录");高级用法
1. 在仓储模式中使用
csharp
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
// 静态字段: 所有实例共享
private static readonly Func<AppDbContext, int, Task<Product>> GetById =
EF.CompileAsyncQuery(
(AppDbContext context, int id) =>
context.Products.FirstOrDefaultAsync(p => p.Id == id)
);
private static readonly Func<AppDbContext, Task<List<Product>>> GetAll =
EF.CompileAsyncQuery(
(AppDbContext context) =>
context.Products.OrderByDescending(p => p.CreatedAt).ToList()
);
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await GetById(_context, id);
}
public async Task<List<Product>> GetAllAsync()
{
return await GetAll(_context);
}
}2. 泛型预编译查询
csharp
public class QueryCompiler<TContext> where TContext : DbContext
{
private static readonly ConcurrentDictionary<string, Delegate> _compiledQueries
= new();
public static Func<TContext, int, Task<TEntity>> CreateGetByIdQuery<TEntity>(
Expression<Func<TEntity, int>> keySelector) where TEntity : class
{
var cacheKey = $"GetById_{typeof(TEntity).Name}";
return (Func<TContext, int, Task<TEntity>>)_compiledQueries.GetOrAdd(
cacheKey,
_ => EF.CompileAsyncQuery(
(TContext context, int id) =>
context.Set<TEntity>()
.FirstOrDefaultAsync(e => EF.Property<int>(e, "Id") == id)
)
);
}
}
// 使用
var getProduct = QueryCompiler<AppDbContext>.CreateGetByIdQuery<Product>(p => p.Id);
var product = await getProduct(_context, 1);3. 组合查询条件
csharp
// 对于复杂条件,可以预编译多个简单查询并组合
public static class ProductQueries
{
public static readonly Func<AppDbContext, int?, decimal?, DateTime?, Task<List<Product>>>
GetFilteredProducts = EF.CompileAsyncQuery(
(AppDbContext context, int? categoryId, decimal? minPrice, DateTime? startDate) =>
{
var query = context.Products.AsQueryable();
if (categoryId.HasValue)
query = query.Where(p => p.CategoryId == categoryId.Value);
if (minPrice.HasValue)
query = query.Where(p => p.Price >= minPrice.Value);
if (startDate.HasValue)
query = query.Where(p => p.CreatedAt >= startDate.Value);
return query.OrderByDescending(p => p.CreatedAt).ToList();
}
);
}
// 使用: 灵活组合条件
var electronics = await ProductQueries.GetFilteredProducts(
_context,
categoryId: 1,
minPrice: null,
startDate: null);
var expensiveRecent = await ProductQueries.GetFilteredProducts(
_context,
categoryId: null,
minPrice: 100,
startDate: DateTime.Today.AddMonths(-1));性能分析与基准测试
基准测试代码
csharp
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class QueryCompilationBenchmark
{
private AppDbContext _context;
private static readonly Func<AppDbContext, int, Task<Product>> GetProductCompiled =
EF.CompileAsyncQuery(
(AppDbContext context, int id) =>
context.Products.FirstOrDefaultAsync(p => p.Id == id)
);
[GlobalSetup]
public void Setup()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("Server=...;Database=BenchmarkTest;...")
.Options;
_context = new AppDbContext(options);
}
[Benchmark]
public async Task<Product> RegularQuery()
{
return await _context.Products
.FirstOrDefaultAsync(p => p.Id == 1);
}
[Benchmark]
public async Task<Product> CompiledQuery()
{
return await GetProductCompiled(_context, 1);
}
[Benchmark]
public async Task<List<Product>> RegularQuery_List()
{
return await _context.Products
.Where(p => p.Price > 100)
.ToListAsync();
}
[Benchmark]
public async Task<List<Product>> CompiledQuery_List()
{
return await GetExpensiveProducts(_context, 100);
}
}
// 运行基准测试
var summary = BenchmarkRunner.Run<QueryCompilationBenchmark>();典型测试结果
BenchmarkDotNet v0.13.12
Method | Mean | Error | StdDev | Gen0 | Allocated |
----------------------- |----------:|--------:|--------:|-------:|----------:|
RegularQuery | 2.850 ms | 0.056ms | 0.065ms | 0.9766 | 8.2 KB |
CompiledQuery | 2.100 ms | 0.042ms | 0.045ms | 0.9766 | 8.2 KB |
RegularQuery_List | 5.200 ms | 0.103ms | 0.120ms | 2.9297 | 24.5 KB |
CompiledQuery_List | 4.350 ms | 0.087ms | 0.095ms | 2.9297 | 24.5 KB |
结论:
- 单次查询: 预编译快 26% (2.85ms → 2.10ms)
- 列表查询: 预编译快 16% (5.20ms → 4.35ms)
- 内存分配: 相同实际场景性能
csharp
// 场景: Web API 高并发查询
public class PerformanceTest
{
private readonly HttpClient _client;
private readonly ILogger _logger;
public async Task SimulateHighConcurrencyAsync()
{
var tasks = new List<Task>();
var sw = Stopwatch.StartNew();
// 模拟 1000 个并发请求
for (int i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(async () =>
{
var response = await _client.GetAsync("/api/products/expensive/100");
return await response.Content.ReadFromJsonAsync<List<Product>>();
}));
}
await Task.WhenAll(tasks);
sw.Stop();
_logger.LogInformation($"完成 1000 个请求耗时: {sw.ElapsedMilliseconds}ms");
_logger.LogInformation($"平均每个请求: {sw.ElapsedMilliseconds / 1000.0}ms");
}
}
// 测试结果:
// 普通查询: 1000 请求 / 3200ms = 312 req/s
// 预编译查询: 1000 请求 / 2500ms = 400 req/s
// 吞吐量提升: 28% 🚀性能影响因素
| 因素 | 影响程度 | 说明 |
|---|---|---|
| 查询复杂度 | 高 | 越复杂的查询,编译开销越大 |
| 执行频率 | 高 | 频率越高,收益越明显 |
| 参数数量 | 中 | 参数越多,编译越慢 |
| 关联数据 | 中 | Include 增加编译时间 |
| 数据库类型 | 低 | 不同数据库编译时间略有差异 |
.NET 8/9/10 新特性
.NET 8: 改进的查询编译
csharp
// .NET 8 优化了复杂查询的编译性能
// 对于包含多个 Where/OrderBy 的查询,编译速度提升 15-25%
var products = await _context.Products
.Where(p => p.Price > 100)
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.Name)
.ToListAsync();
// .NET 8 内部优化:
// - 更快的表达式树遍历
// - 改进的缓存策略
// - 减少不必要的内存分配.NET 9: 增强的编译查询诊断
csharp
// .NET 9 添加查询编译诊断信息
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.EnableDetailedErrors()
.LogTo(Console.WriteLine, LogLevel.Debug);
});
// 输出详细的编译信息:
// info: Compiling query model for 'Products.Where(p => p.Price > 100)'
// info: Query compilation took 2.5ms
// info: Using cached compiled query (cache hit).NET 10: 智能查询优化(预览)
预计特性:
- 自动检测并优化热点查询
- 基于 AI 的查询性能预测
- 更智能的查询计划缓存
- 运行时编译优化建议
最佳实践与陷阱
最佳实践
1. 识别热点查询
csharp
// 监控慢查询日志
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.LogTo((eventId, logLevel) =>
{
if (eventId.Id == RelationalEventId.CommandExecuted.Id)
return logLevel >= LogLevel.Warning; // 记录慢查询
return false;
}, Console.WriteLine);
});
// 分析日志,找出高频查询
// 将这些查询转换为预编译形式2. 合理组织预编译查询
csharp
// ✅ 推荐: 集中管理
public static class CompiledQueries
{
// Product 相关
public static readonly Func<...> GetProductById = ...;
public static readonly Func<...> GetProductsByCategory = ...;
// Customer 相关
public static readonly Func<...> GetCustomerById = ...;
public static readonly Func<...> GetCustomerOrders = ...;
}
// ❌ 避免: 分散在各处
public class SomeService
{
private static readonly Func<...> MyQuery = ...; // 难以管理
}3. 使用静态只读字段
csharp
// ✅ 正确: static readonly
public static readonly Func<AppDbContext, int, Task<Product>> GetProduct =
EF.CompileAsyncQuery(...);
// ❌ 错误: 每次调用都重新编译
public Task<Product> GetProduct(AppDbContext ctx, int id)
{
var compiled = EF.CompileAsyncQuery(...); // 💥 失去预编译意义!
return compiled(ctx, id);
}4. 权衡利弊
csharp
// 决策矩阵:
//
// 查询频率 \ 复杂度 | 简单 | 中等 | 复杂
// ------------------|-------|-------|------
// 低频(每天几次) | ❌ | ❌ | ⚠️
// 中频(每小时几次) | ❌ | ⚠️ | ✅
// 高频(每秒几次) | ✅ | ✅ | ✅
//
// 建议: 优先预编译高频查询常见陷阱
陷阱 1: 过度使用
csharp
// ❌ 错误: 为只执行一次的查询使用预编译
public async Task InitializeDatabaseAsync()
{
// 这个查询只执行一次,不需要预编译
var count = await _context.Products.CountAsync();
if (count == 0)
{
// 种子数据也只插入一次
await SeedDataAsync();
}
}陷阱 2: 动态查询无法预编译
csharp
// ❌ 错误: 动态构建的查询无法预编译
public async Task<List<Product>> SearchAsync(Dictionary<string, object> filters)
{
var query = _context.Products.AsQueryable();
// 动态添加条件
if (filters.ContainsKey("name"))
query = query.Where(p => p.Name.Contains(filters["name"].ToString()));
if (filters.ContainsKey("price"))
query = query.Where(p => p.Price > Convert.ToDecimal(filters["price"]));
// 💥 无法预编译,因为条件是动态的
return await query.ToListAsync();
}
// ✅ 解决: 使用预定义的查询组合
public static readonly Func<AppDbContext, string, decimal, Task<List<Product>>>
SearchWithFilters = EF.CompileAsyncQuery(
(AppDbContext context, string name, decimal minPrice) =>
context.Products
.Where(p => string.IsNullOrEmpty(name) || p.Name.Contains(name))
.Where(p => minPrice <= 0 || p.Price > minPrice)
.ToList()
);陷阱 3: 忘记异步版本
csharp
// ❌ 错误: 在异步上下文中使用同步版本
public async Task<Product> GetProductAsync(int id)
{
// 💥 会阻塞线程
return GetProductById(_context, id);
}
// ✅ 正确: 使用异步版本
public async Task<Product> GetProductAsync(int id)
{
return await GetProductByIdAsync(_context, id);
}陷阱 4: 生命周期问题
csharp
// ❌ 错误: DbContext 生命周期问题
public class ProductService : IDisposable
{
private AppDbContext _context;
private static readonly Func<AppDbContext, int, Task<Product>> GetProduct =
EF.CompileAsyncQuery(...);
public async Task<Product> GetByIdAsync(int id)
{
return await GetProduct(_context, id);
}
public void Dispose()
{
_context?.Dispose(); // ⚠️ 预编译查询仍然引用已释放的 context
}
}
// ✅ 正确: 通过依赖注入管理生命周期
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<Product> GetByIdAsync(int id)
{
return await GetProduct(_context, id);
}
}调试技巧
1. 验证是否使用了预编译
csharp
// 启用详细日志
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information);
});
// 观察日志:
// 首次执行: "Compiling query..."
// 后续执行: "Using cached query plan" ← 确认预编译生效2. 性能对比测试
csharp
public class CompilationTest
{
public async Task ComparePerformanceAsync()
{
var sw = new Stopwatch();
// 测试普通查询
sw.Start();
for (int i = 0; i < 100; i++)
{
await _context.Products.FirstOrDefaultAsync(p => p.Id == 1);
}
sw.Stop();
Console.WriteLine($"普通查询: {sw.ElapsedMilliseconds}ms");
// 测试预编译查询
sw.Restart();
for (int i = 0; i < 100; i++)
{
await GetProductCompiled(_context, 1);
}
sw.Stop();
Console.WriteLine($"预编译查询: {sw.ElapsedMilliseconds}ms");
}
}总结
核心要点
- 预编译查询: 编译一次,多次使用
- 性能提升: 15-30%,高频场景更明显
- 适用场景: 热点查询、API 端点、循环查询
- 使用方式:
EF.CompileAsyncQuery - 注意事项: 静态只读、合理组织、避免过度使用
性能对比
| 查询类型 | 首次执行 | 后续执行 | 适用场景 |
|---|---|---|---|
| 普通查询 | 编译+执行 | 编译+执行 | 低频查询 |
| 预编译查询 | 编译+执行 | 仅执行 | 高频查询 |
使用建议
决策流程:
1. 识别高频查询(监控日志)
2. 评估查询复杂度
3. 创建预编译查询
4. 性能测试验证
5. 持续监控效果代码模板
csharp
// 标准模板
public static class CompiledQueries
{
// GetById
public static readonly Func<TContext, int, Task<TEntity>> GetById =
EF.CompileAsyncQuery(
(TContext context, int id) =>
context.Set<TEntity>().FirstOrDefaultAsync(e => e.Id == id)
);
// List
public static readonly Func<TContext, Task<List<TEntity>>> GetAll =
EF.CompileAsyncQuery(
(TContext context) =>
context.Set<TEntity>().OrderByDescending(e => e.CreatedAt).ToList()
);
// Filtered
public static readonly Func<TContext, TParam, Task<List<TEntity>>> GetByFilter =
EF.CompileAsyncQuery(
(TContext context, TParam param) =>
context.Set<TEntity>()
.Where(e => e.SomeProperty == param)
.ToList()
);
}