Appearance
查询管道与 LINQ 翻译
概述
EF Core 的查询管道(Query Pipeline)是将 LINQ 表达式转换为 SQL 语句的核心引擎。理解这个翻译过程对于编写高效查询、排查性能问题和避免常见陷阱至关重要。
查询执行流程
C# LINQ 查询
↓
[1] 表达式树(Expression Tree)
↓
[2] 查询预处理(规范化/优化)
↓
[3] LINQ 翻译器(转换为 SQL AST)
↓
[4] SQL 生成器(生成具体 SQL)
↓
[5] 参数化与缓存
↓
发送到数据库执行表达式树基础
什么是表达式树?
csharp
// 普通委托 - 直接执行
Func<Product, bool> func = p => p.Price > 100;
// 表达式树 - 可分析的结构
Expression<Func<Product, bool>> expr = p => p.Price > 100;
// 查看表达式树结构
Console.WriteLine(expr.Body); // (p.Price > 100)
Console.WriteLine(expr.Parameters); // [p]EF Core 如何使用表达式树
csharp
// LINQ 查询被编译为表达式树
var query = context.Products
.Where(p => p.Price > 100) // Expression<Func<Product, bool>>
.OrderBy(p => p.Name) // Expression<Func<Product, string>>
.Select(p => new { p.Id, p.Name }); // Expression<Func<Product, DTO>>
// EF Core 遍历表达式树,翻译成 SQL
string sql = query.ToQueryString();翻译过程详解
阶段 1: 查询预处理
csharp
// 原始查询
var query = context.Products
.Where(p => p.Price > 100 && p.CategoryId == 1)
.Where(p => p.StockQuantity > 0); // 多个 Where
// EF Core 自动合并条件
// 等价于:
var optimized = context.Products
.Where(p => p.Price > 100 && p.CategoryId == 1 && p.StockQuantity > 0);阶段 2: LINQ 方法翻译
支持的 LINQ 方法
csharp
// ✅ 过滤
.Where(p => p.Price > 100)
.Where(p => p.Name.Contains("Laptop"))
// ✅ 投影
.Select(p => new { p.Id, p.Name })
.Select(p => p.Name.ToUpper())
// ✅ 排序
.OrderBy(p => p.Price)
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.Name)
// ✅ 分页
.Skip(10).Take(20)
// ✅ 聚合
.Count()
.Sum(p => p.Price)
.Average(p => p.Price)
.Min(p => p.Price)
.Max(p => p.Price)
// ✅ 分组
.GroupBy(p => p.CategoryId)
// ✅ 连接
.Join(context.Categories,
p => p.CategoryId,
c => c.Id,
(p, c) => new { Product = p, Category = c })
// ✅ 包含
.Include(p => p.Category)
.ThenInclude(c => c.Products)
// ✅ 去重
.Distinct()
// ✅ 条件判断
.FirstOrDefault(p => p.Price > 100)
.SingleOrDefault(p => p.Id == 1)
.Any(p => p.Price > 1000)
.All(p => p.Price > 0)无法翻译的方法
csharp
// ❌ 自定义方法
.Where(p => IsExpensive(p.Price)) // ⚠️ 无法翻译
bool IsExpensive(decimal price) => price > 1000;
// ❌ .NET 特定方法
.Where(p => p.ToString().StartsWith("A")) // ⚠️ ToString 可能无法翻译
// ❌ 客户端集合操作
.Select(p => MyHelper.FormatPrice(p.Price)) // ⚠️ 自定义辅助方法
// ❌ 某些 DateTime 方法
.Where(p => p.CreatedAt.DayOfWeek == DayOfWeek.Monday) // ⚠️ 部分支持阶段 3: SQL 生成
csharp
// C# LINQ
var query = context.Products
.Where(p => p.Price > 100 && p.CategoryId == 1)
.OrderByDescending(p => p.Price)
.Skip(10)
.Take(20)
.Select(p => new { p.Id, p.Name, p.Price });
// 生成的 SQL (SQL Server)
/*
SELECT [t].[Id], [t].[Name], [t].[Price]
FROM (
SELECT [p].[Id], [p].[Name], [p].[Price],
ROW_NUMBER() OVER(ORDER BY [p].[Price] DESC) AS [row]
FROM [Products] AS [p]
WHERE ([p].[Price] > 100.0) AND ([p].[CategoryId] = 1)
) AS [t]
WHERE [t].[row] > 10 AND [t].[row] <= 30
*/翻译规则与限制
客户端评估 vs 服务端评估
EF Core 8+ 的行为
csharp
// EF Core 8+ 默认禁止隐式客户端评估
// ❌ 错误: 会抛出异常
var query = context.Products
.Where(p => IsExpensive(p.Price)); // 💥 InvalidOperationException
// ✅ 正确: 显式切换到客户端
var query = context.Products
.AsEnumerable() // 明确标记: 从这里开始在客户端执行
.Where(p => IsExpensive(p.Price));
// ✅ 更好: 重写为可翻译的表达式
var query = context.Products
.Where(p => p.Price > 1000); // 可以在服务器端执行强制客户端评估(谨慎使用)
csharp
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
// ⚠️ 允许客户端评估(不推荐,会影响性能)
options.ConfigureWarnings(warnings =>
warnings.Log(RelationalEventId.ClientEvaluationWarning));
});可翻译的 .NET 方法
csharp
// ✅ 字符串方法
p.Name.ToLower()
p.Name.ToUpper()
p.Name.Trim()
p.Name.Contains("search")
p.Name.StartsWith("A")
p.Name.EndsWith("z")
p.Name.Substring(0, 5)
p.Name.Replace("old", "new")
p.Name.Length
// ✅ 数学方法
Math.Abs(p.Price)
Math.Round(p.Price, 2)
Math.Floor(p.Price)
Math.Ceiling(p.Price)
Math.Pow(p.BasePrice, 2)
// ✅ DateTime 方法
DateTime.Now
DateTime.UtcNow
p.CreatedAt.Year
p.CreatedAt.Month
p.CreatedAt.Day
p.CreatedAt.Hour
p.CreatedAt.AddDays(7)
p.CreatedAt.AddMonths(1)
p.CreatedAt.AddYears(1)
// ✅ 类型转换
Convert.ToInt32(p.SomeValue)
Convert.ToString(p.Price)
Convert.ToDecimal(p.IntValue)
// ✅ EF.Functions (数据库特定函数)
EF.Functions.Like(p.Name, "%search%")
EF.Functions.FreeText(p.Description, "keyword") // SQL Server 全文搜索
EF.Functions.ILike(p.Name, "%search%") // PostgreSQL 不区分大小写不可翻译的操作
csharp
// ❌ 自定义方法调用
.Where(p => CustomLogic(p.Price))
// ❌ 委托调用
.Where(p => someDelegate(p))
// ❌ 反射操作
.Where(p => p.GetType().Name == "Product")
// ❌ 大多数格式化方法
.Where(p => p.Price.ToString("C") == "$100.00")
// ❌ 复杂的 LINQ 嵌套
.Select(p => items.Where(i => i.ProductId == p.Id).ToList())
// ❌ 客户端集合操作后尝试返回 IQueryable
.Select(p => new {
Products = context.Products.Where(x => x.CategoryId == p.Id).ToList() // ⚠️ 会在客户端执行
})复杂查询翻译
Join 查询
csharp
// LINQ Join
var query = from p in context.Products
join c in context.Categories on p.CategoryId equals c.Id
where c.Name == "Electronics"
select new { p.Name, c.Name as Category };
// 生成的 SQL
/*
SELECT [p].[Name], [c].[Name] AS [Category]
FROM [Products] AS [p]
INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id]
WHERE [c].[Name] = N'Electronics'
*/GroupBy 聚合
csharp
// LINQ GroupBy
var query = context.Products
.GroupBy(p => p.CategoryId)
.Select(g => new
{
CategoryId = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
MaxPrice = g.Max(p => p.Price)
});
// 生成的 SQL
/*
SELECT [p].[CategoryId] AS [CategoryId],
COUNT(*) AS [Count],
AVG([p].[Price]) AS [AvgPrice],
MAX([p].[Price]) AS [MaxPrice]
FROM [Products] AS [p]
GROUP BY [p].[CategoryId]
*/子查询
csharp
// LINQ 子查询
var query = context.Categories
.Select(c => new
{
c.Name,
ProductCount = c.Products.Count(),
ExpensiveProducts = c.Products.Count(p => p.Price > 1000)
});
// 生成的 SQL
/*
SELECT [c].[Name],
(SELECT COUNT(*) FROM [Products] AS [p] WHERE [c].[Id] = [p].[CategoryId]) AS [ProductCount],
(SELECT COUNT(*) FROM [Products] AS [p] WHERE ([c].[Id] = [p].[CategoryId]) AND ([p].[Price] > 1000.0)) AS [ExpensiveProducts]
FROM [Categories] AS [c]
*/Any/All 翻译
csharp
// Any - EXISTS
var hasExpensive = context.Products.Any(p => p.Price > 1000);
/*
SELECT CASE
WHEN EXISTS (SELECT 1 FROM [Products] AS [p] WHERE [p].[Price] > 1000.0)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
*/
// All - NOT EXISTS ... NOT
var allInStock = context.Products.All(p => p.StockQuantity > 0);
/*
SELECT CASE
WHEN NOT EXISTS (SELECT 1 FROM [Products] AS [p] WHERE [p].[StockQuantity] <= 0)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END
*/性能优化技巧
1. 避免 N+1 查询
csharp
// ❌ 错误: N+1 查询
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
var customer = await context.Customers.FindAsync(order.CustomerId); // N 次查询
}
// ✅ 正确: 使用 Include
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync(); // 1 次查询2. 使用投影减少数据传输
csharp
// ❌ 加载所有列
var products = await context.Products.ToListAsync();
// ✅ 只选择需要的列
var products = await context.Products
.Select(p => new { p.Id, p.Name, p.Price })
.ToListAsync();3. 避免在 Where 中使用客户端方法
csharp
// ❌ 客户端评估
var query = context.Products
.Where(p => FormatPrice(p.Price).Contains("99"));
string FormatPrice(decimal price) => price.ToString("F2");
// ✅ 服务端评估
var query = context.Products
.Where(p => EF.Functions.Like(SqlFunctions.StringConvert((double)p.Price), "%99%"));4. 使用 AsNoTracking 提升读取性能
csharp
// 只读查询,不需要变更跟踪
var products = await context.Products
.AsNoTracking()
.Where(p => p.Price > 100)
.ToListAsync();5. 预编译查询(高频查询)
csharp
// 预编译查询
private static readonly Func<AppDbContext, int, Task<Product?>> _getProductById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
// 使用(性能提升 10-20%)
var product = await _getProductById(context, productId);调试翻译问题
查看生成的 SQL
csharp
// 方法 1: ToQueryString
var query = context.Products.Where(p => p.Price > 100);
string sql = query.ToQueryString();
Console.WriteLine(sql);
// 方法 2: 日志记录
options.LogTo(Console.WriteLine, LogLevel.Information);
// 方法 3: MiniProfiler
// 安装 MiniProfiler.EntityFrameworkCore 包识别客户端评估警告
csharp
// 配置警告
options.ConfigureWarnings(warnings =>
{
warnings.Throw(RelationalEventId.QueryClientEvaluationWarning); // 抛出异常
// 或
warnings.Log(RelationalEventId.QueryClientEvaluationWarning); // 仅记录日志
});常见翻译失败及解决方案
问题 1: 自定义方法
csharp
// ❌ 失败
.Where(p => CalculateDiscount(p.Price) > 10)
// ✅ 修复: 内联逻辑
.Where(p => p.Price * 0.9m > 10)问题 2: DateTime 比较
csharp
// ❌ 可能失败
.Where(p => p.CreatedAt.Date == DateTime.Today)
// ✅ 修复: 使用范围查询
var today = DateTime.Today;
var tomorrow = today.AddDays(1);
.Where(p => p.CreatedAt >= today && p.CreatedAt < tomorrow)问题 3: 字符串比较
csharp
// ❌ 区分大小写(取决于数据库排序规则)
.Where(p => p.Name == "laptop")
// ✅ 明确指定
.Where(p => EF.Functions.ILike(p.Name, "laptop")) // PostgreSQL
// 或
.Where(p => p.Name.ToLower() == "laptop") // 通用高级翻译特性
自定义函数映射
csharp
// 定义 CLR 方法
public static class DbFunctions
{
[DbFunction("SOUNDEX", IsBuiltIn = true)]
public static string SoundEx(string input)
=> throw new NotSupportedException();
}
// 在查询中使用
var query = context.Products
.Where(p => DbFunctions.SoundEx(p.Name) == DbFunctions.SoundEx("Laptop"));
// 生成的 SQL
// WHERE SOUNDEX([p].[Name]) = SOUNDEX(N'Laptop')标量函数映射
csharp
// 数据库函数
// CREATE FUNCTION dbo.CalculateTax(@price DECIMAL(18,2))
// RETURNS DECIMAL(18,2) AS BEGIN RETURN @price * 0.08 END
// C# 映射
[DbFunction("CalculateTax", Schema = "dbo")]
public static decimal CalculateTax(decimal price)
=> throw new NotSupportedException();
// 使用
var query = context.Products
.Select(p => new {
p.Name,
Price = p.Price,
Tax = DbFunctions.CalculateTax(p.Price)
});表值函数映射
csharp
// 数据库 TVF
// CREATE FUNCTION dbo.GetProductsByCategory(@categoryId INT)
// RETURNS TABLE AS RETURN SELECT * FROM Products WHERE CategoryId = @categoryId
// C# 映射
public IQueryable<Product> GetProductsByCategory(int categoryId)
=> FromExpression(() => GetProductsByCategory(categoryId));
// 注册
modelBuilder.HasDbFunction(typeof(AppDbContext).GetMethod(nameof(GetProductsByCategory)));
// 使用
var products = await context.GetProductsByCategory(1).ToListAsync();最佳实践
✅ 推荐做法
优先使用可翻译的 LINQ
csharp// ✅ 服务端执行 .Where(p => p.Price > 100)必要时显式切换客户端
csharp// ✅ 明确标记 .AsEnumerable() .Where(p => CustomMethod(p))定期审查生成的 SQL
csharp// 开发环境始终记录 SQL if (env.IsDevelopment()) { options.LogTo(Console.WriteLine, LogLevel.Information); }使用预编译查询优化热点路径
csharpprivate static readonly Func<...> _compiledQuery = EF.CompileQuery(...);
❌ 避免的错误
不要在循环中执行查询
csharp// ❌ N+1 问题 foreach (var id in ids) { var item = await context.Items.FindAsync(id); } // ✅ 批量加载 var items = await context.Items .Where(i => ids.Contains(i.Id)) .ToListAsync();不要忽略客户端评估警告
csharp// ❌ 隐藏警告 options.ConfigureWarnings(w => w.Ignore(...)); // ✅ 解决根本原因不要在投影中执行子查询
csharp// ❌ 性能差 .Select(c => new { c.Name, Orders = c.Orders.ToList() // 每个分类都执行查询 }) // ✅ 使用 Include 或 Join
总结
翻译能力速查表
| LINQ 方法 | 是否支持 | 备注 |
|---|---|---|
| Where | ✅ | 完全支持 |
| Select | ✅ | 支持匿名类型和 DTO |
| OrderBy | ✅ | 支持 ThenBy |
| Skip/Take | ✅ | 生成分页 SQL |
| Join | ✅ | INNER JOIN |
| GroupBy | ✅ | 聚合查询 |
| Include | ✅ | LEFT JOIN |
| Any/All | ✅ | EXISTS 子查询 |
| First/FirstOrDefault | ✅ | TOP 1 |
| Count/Sum/Avg | ✅ | 聚合函数 |
| Distinct | ✅ | DISTINCT |
| Concat/Union | ✅ | UNION ALL / UNION |
| Contains | ✅ | IN 子句 |
| 自定义方法 | ❌ | 需切换到客户端 |
核心要点
- 理解表达式树: LINQ 被编译为表达式树,EF Core 遍历树结构翻译为 SQL
- 知道什么能翻译: 标准 LINQ 方法大部分支持,自定义方法不支持
- 监控生成的 SQL: 使用 ToQueryString 或日志查看实际 SQL
- 避免隐式客户端评估: EF Core 8+ 会抛出异常,必须显式切换
- 优化查询性能: 使用投影、AsNoTracking、预编译查询等技术