今天刚子不跟你扯理论直接上实战代码把 EF Core 复杂查询的几个核心技巧给你讲明白。顺便聊聊性能优化的几条铁律省得你下次再踩坑。先说核心EF Core 复杂查询的3个核心技巧处理复杂查询你只需要记住这几招关联查询用IncludeThenInclude一次性加载多级关联数据动态筛选用表达式树在运行时动态拼查询条件性能优化用AsNoTracking、Select投影、AsSplitQuery控制数据加载这些学会了90% 的复杂查询场景你都能搞定。刚子大白话写 EF Core 查询关键不是你写得多花哨而是你要知道它生成的 SQL 长啥样。把 EF Core 当成一个带类型安全的 SQL 生成器这才是正确心态。场景一多表关联查询Include ThenInclude基础用法加载关联数据例如我的博客系统一个 Blog 有多个 Post每个 Post 有一个 Author。// 加载 Blog、关联的 Post、每个 Post 的 Author var blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Author) .ToListAsync();这个查询会把三层数据一次加载出来生成的 SQL 是一个 JOIN 查询把三张表一次性查完。Include 也能过滤当然可以EF Core 支持在 Include 里对关联集合做过滤// 只加载今年发布的文章 var blogs await context.Blogs .Include(b b.Posts.Where(p p.PublishDate.Year DateTime.Now.Year)) .ToListAsync();多级关联要多个 ThenInclude如果需要加载更深层级的关联继续链式调用ThenInclude就行// Blog → Posts → Author → AuthorDetails var blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Author) .ThenInclude(a a.Details) .ToListAsync();划重点Include ThenInclude 链越长生成的 JOIN 越复杂。如果要加载多个集合导航属性注意笛卡尔积爆炸的问题。遇到这种情况可以用AsSplitQuery()把一个大查询拆成多个小查询。场景二动态查询表达式树为什么需要动态查询业务需求经常变用户按多个条件筛选但这些条件可能选也可能不选。用静态查询写一堆if太丑了还容易漏条件。动态查询的核心是用ExpressionFuncT, bool在运行时拼接查询条件。手写一个动态筛选器public async TaskListProduct SearchProductsAsync( string? name null, decimal? minPrice null, decimal? maxPrice null, int? categoryId null) { var query context.Products.AsQueryable(); if (!string.IsNullOrEmpty(name)) query query.Where(p p.Name.Contains(name)); if (minPrice.HasValue) query query.Where(p p.Price minPrice.Value); if (maxPrice.HasValue) query query.Where(p p.Price maxPrice.Value); if (categoryId.HasValue) query query.Where(p p.CategoryId categoryId.Value); return await query.ToListAsync(); }这样写没问题但条件越多代码越臃肿。更好的方式是用表达式树工具库或者自己封装一个PredicateBuilder。PredicateBuilder 的实现原理public static class PredicateBuilder { public static ExpressionFuncT, bool TrueT() { return f true; } public static ExpressionFuncT, bool FalseT() { return f false; } public static ExpressionFuncT, bool OrT( this ExpressionFuncT, bool expr1, ExpressionFuncT, bool expr2) { var invokedExpr Expression.Invoke(expr2, expr1.Parameters); return Expression.LambdaFuncT, bool( Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } public static ExpressionFuncT, bool AndT( this ExpressionFuncT, bool expr1, ExpressionFuncT, bool expr2) { var invokedExpr Expression.Invoke(expr2, expr1.Parameters); return Expression.LambdaFuncT, bool( Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } }用起来就很优雅了var predicate PredicateBuilder.TrueProduct(); if (!string.IsNullOrEmpty(name)) predicate predicate.And(p p.Name.Contains(name)); if (minPrice.HasValue) predicate predicate.And(p p.Price minPrice.Value); if (maxPrice.HasValue) predicate predicate.And(p p.Price maxPrice.Value); var products await context.Products .Where(predicate) .ToListAsync();划重点千万别把自定义方法塞进表达式树。EF Core 不认识你的MyUtil.IsAdult(x)整段逻辑会被静默跳过甚至退化为客户端求值——先查出全部数据再在内存里过滤。性能直接崩。场景三分页 排序 过滤分页是高频场景EF Core 配合 LINQ 写起来很顺手public async TaskPagedResultProduct GetPagedProductsAsync( int pageIndex 1, int pageSize 10, string? sortBy Id, string? sortDirection asc, string? searchTerm null) { var query context.Products.AsQueryable(); // 过滤 if (!string.IsNullOrEmpty(searchTerm)) query query.Where(p p.Name.Contains(searchTerm)); // 排序注意这里用了字符串反射生产环境建议用 switch 或字典映射 query sortDirection?.ToLower() desc ? query.OrderByDescending(GetSortExpression(sortBy)) : query.OrderBy(GetSortExpression(sortBy)); // 分页 var totalCount await query.CountAsync(); var items await query .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResultProduct { Items items, TotalCount totalCount, PageIndex pageIndex, PageSize pageSize }; }划重点分页查询必须在Skip和Take之前先做排序否则 EF Core 会抛异常。另外GetSortExpression这个函数要注意防止 SQL 注入最好用白名单映射。场景四分组与聚合查询按分类统计产品数量var categoryStats await context.Products .GroupBy(p p.CategoryId) .Select(g new { CategoryId g.Key, ProductCount g.Count(), AvgPrice g.Average(p p.Price), TotalRevenue g.Sum(p p.Price * p.SalesCount) }) .ToListAsync();这个查询 EF Core 会翻译成一条带GROUP BY的 SQL 语句直接在数据库端完成聚合计算性能很好。刚子大白话能用GroupBy就别自己写循环算数据库干这个比 C# 快多了。性能优化这 5 条铁律记住1. 只读查询用AsNoTracking()EF Core 默认会跟踪每个实体的变更这在只读场景下完全是浪费。var products await context.Products .AsNoTracking() .Where(p p.Price 100) .ToListAsync();加上AsNoTrackingEF Core 不会记录这些实体的状态变化内存占用和 CPU 开销都大幅降低。2. 只取需要的字段投影不要每次都Select *用投影只拿你真正需要的字段var productInfos await context.Products .Select(p new ProductDto { Id p.Id, Name p.Name, Price p.Price }) .ToListAsync();3. 用Select投影还能顺便加载关联数据var orderInfos await context.Orders .Select(o new { OrderId o.Id, CustomerName o.Customer.Name, TotalAmount o.Items.Sum(i i.Price * i.Quantity), ItemCount o.Items.Count() }) .ToListAsync();这种方式比Include更精准因为你只拿你需要的数据SQL 生成的 JOIN 也更精简。4. N1 问题用Include解决// ❌ 错误会触发 N1 次查询 // 场景获取所有订单并逐一输出客户名称 // 如果启用了延迟加载以下代码会导致 1 次查询获取订单 N 次查询获取每个订单的客户 var orders await context.Orders.ToListAsync(); foreach (var order in orders) { Console.WriteLine(order.Customer); // 每次访问都触发一次查询 } // ✅ 正确一次性预加载 // 场景获取所有订单及对应的客户仅需一次查询 var ordersWithCustomer await context.Orders .Include(o o.Customer) .ToListAsync();用Include显式预加载关联数据把原本 1N 次查询压成 1 次 JOIN 查询。5. 集合过多时用AsSplitQuery()如果一个查询包含多个集合导航属性默认的单查询模式会产生笛卡尔积爆炸。这时用AsSplitQuery()拆分成多个 SQLvar blogs await context.Blogs .Include(b b.Posts) .Include(b b.Comments) .AsSplitQuery() .ToListAsync();EF Core 会分别查询 Blog、Posts、Comments 三张表然后在内存中组装避免数据重复膨胀。复杂查询铁律场景推荐方案注意事项多表关联加载IncludeThenInclude链别太长注意笛卡尔积动态多条件筛选表达式树 / PredicateBuilder别塞自定义方法会被静默忽略只读数据查询AsNoTracking()Select投影减少内存开销避免 N1预加载 禁用延迟加载用Include一次搞定多集合查询AsSplitQuery()防笛卡尔积爆炸数据量大分页 索引Skip/Take前必须排序复杂聚合GroupBy/ 聚合函数EF Core 会翻译成 SQL实在搞不定原生 SQL (FromSqlRaw)最后手段别滥用刚子结语别把 EF Core 当成黑盒。你写出来的 LINQ 查询最终都会翻译成 SQL不理解 SQL你就写不出高效的 EF Core 查询。我刚学 EF Core 的时候也踩过 N1、笛卡尔积、客户端求值这些坑。后来我养成了一个习惯每个复杂查询都去检查生成的 SQL 长啥样。你可以用 EF Core 自带的日志功能optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging();看一眼生成的 SQL你就知道哪里写得不对了。刚子的经验写复杂查询的时候先想清楚“我要的数据结构是什么”再用 LINQ 去表达。把 EF Core 当成带类型安全的 SQL 生成器别把它当成万能魔法箱。如果你觉得这篇有用点个赞、转给还在被 EF Core 复杂查询折磨的兄弟。我是刚子一个写了六年 .NET 代码的程序员。咱们下回见原文链接写 EF Core 查询90% 的人第一步就错了刚子教你避开所有坑 - 码农刚子的开发笔记合集: C#/.NET开发者宝典 , C#/.NET 编程指南标签: EFCore, EF免责声明本内容来自平台创作者博客园系信息发布平台仅提供信息存储空间服务。好文要顶 关注我 收藏该文 微信分享码农刚子粉丝 - 61 关注 - 11加关注9« 上一篇 序列化 JSON 时崩了99% 是 EF 延迟加载惹的祸三种解法拿走不谢» 下一篇 推荐一个开箱即用的.NET权限管理平台Magic.NETposted 2026-04-22 08:02 码农刚子 阅读(944) 评论(9) 收藏 举报
写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑
今天刚子不跟你扯理论直接上实战代码把 EF Core 复杂查询的几个核心技巧给你讲明白。顺便聊聊性能优化的几条铁律省得你下次再踩坑。先说核心EF Core 复杂查询的3个核心技巧处理复杂查询你只需要记住这几招关联查询用IncludeThenInclude一次性加载多级关联数据动态筛选用表达式树在运行时动态拼查询条件性能优化用AsNoTracking、Select投影、AsSplitQuery控制数据加载这些学会了90% 的复杂查询场景你都能搞定。刚子大白话写 EF Core 查询关键不是你写得多花哨而是你要知道它生成的 SQL 长啥样。把 EF Core 当成一个带类型安全的 SQL 生成器这才是正确心态。场景一多表关联查询Include ThenInclude基础用法加载关联数据例如我的博客系统一个 Blog 有多个 Post每个 Post 有一个 Author。// 加载 Blog、关联的 Post、每个 Post 的 Author var blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Author) .ToListAsync();这个查询会把三层数据一次加载出来生成的 SQL 是一个 JOIN 查询把三张表一次性查完。Include 也能过滤当然可以EF Core 支持在 Include 里对关联集合做过滤// 只加载今年发布的文章 var blogs await context.Blogs .Include(b b.Posts.Where(p p.PublishDate.Year DateTime.Now.Year)) .ToListAsync();多级关联要多个 ThenInclude如果需要加载更深层级的关联继续链式调用ThenInclude就行// Blog → Posts → Author → AuthorDetails var blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Author) .ThenInclude(a a.Details) .ToListAsync();划重点Include ThenInclude 链越长生成的 JOIN 越复杂。如果要加载多个集合导航属性注意笛卡尔积爆炸的问题。遇到这种情况可以用AsSplitQuery()把一个大查询拆成多个小查询。场景二动态查询表达式树为什么需要动态查询业务需求经常变用户按多个条件筛选但这些条件可能选也可能不选。用静态查询写一堆if太丑了还容易漏条件。动态查询的核心是用ExpressionFuncT, bool在运行时拼接查询条件。手写一个动态筛选器public async TaskListProduct SearchProductsAsync( string? name null, decimal? minPrice null, decimal? maxPrice null, int? categoryId null) { var query context.Products.AsQueryable(); if (!string.IsNullOrEmpty(name)) query query.Where(p p.Name.Contains(name)); if (minPrice.HasValue) query query.Where(p p.Price minPrice.Value); if (maxPrice.HasValue) query query.Where(p p.Price maxPrice.Value); if (categoryId.HasValue) query query.Where(p p.CategoryId categoryId.Value); return await query.ToListAsync(); }这样写没问题但条件越多代码越臃肿。更好的方式是用表达式树工具库或者自己封装一个PredicateBuilder。PredicateBuilder 的实现原理public static class PredicateBuilder { public static ExpressionFuncT, bool TrueT() { return f true; } public static ExpressionFuncT, bool FalseT() { return f false; } public static ExpressionFuncT, bool OrT( this ExpressionFuncT, bool expr1, ExpressionFuncT, bool expr2) { var invokedExpr Expression.Invoke(expr2, expr1.Parameters); return Expression.LambdaFuncT, bool( Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } public static ExpressionFuncT, bool AndT( this ExpressionFuncT, bool expr1, ExpressionFuncT, bool expr2) { var invokedExpr Expression.Invoke(expr2, expr1.Parameters); return Expression.LambdaFuncT, bool( Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } }用起来就很优雅了var predicate PredicateBuilder.TrueProduct(); if (!string.IsNullOrEmpty(name)) predicate predicate.And(p p.Name.Contains(name)); if (minPrice.HasValue) predicate predicate.And(p p.Price minPrice.Value); if (maxPrice.HasValue) predicate predicate.And(p p.Price maxPrice.Value); var products await context.Products .Where(predicate) .ToListAsync();划重点千万别把自定义方法塞进表达式树。EF Core 不认识你的MyUtil.IsAdult(x)整段逻辑会被静默跳过甚至退化为客户端求值——先查出全部数据再在内存里过滤。性能直接崩。场景三分页 排序 过滤分页是高频场景EF Core 配合 LINQ 写起来很顺手public async TaskPagedResultProduct GetPagedProductsAsync( int pageIndex 1, int pageSize 10, string? sortBy Id, string? sortDirection asc, string? searchTerm null) { var query context.Products.AsQueryable(); // 过滤 if (!string.IsNullOrEmpty(searchTerm)) query query.Where(p p.Name.Contains(searchTerm)); // 排序注意这里用了字符串反射生产环境建议用 switch 或字典映射 query sortDirection?.ToLower() desc ? query.OrderByDescending(GetSortExpression(sortBy)) : query.OrderBy(GetSortExpression(sortBy)); // 分页 var totalCount await query.CountAsync(); var items await query .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResultProduct { Items items, TotalCount totalCount, PageIndex pageIndex, PageSize pageSize }; }划重点分页查询必须在Skip和Take之前先做排序否则 EF Core 会抛异常。另外GetSortExpression这个函数要注意防止 SQL 注入最好用白名单映射。场景四分组与聚合查询按分类统计产品数量var categoryStats await context.Products .GroupBy(p p.CategoryId) .Select(g new { CategoryId g.Key, ProductCount g.Count(), AvgPrice g.Average(p p.Price), TotalRevenue g.Sum(p p.Price * p.SalesCount) }) .ToListAsync();这个查询 EF Core 会翻译成一条带GROUP BY的 SQL 语句直接在数据库端完成聚合计算性能很好。刚子大白话能用GroupBy就别自己写循环算数据库干这个比 C# 快多了。性能优化这 5 条铁律记住1. 只读查询用AsNoTracking()EF Core 默认会跟踪每个实体的变更这在只读场景下完全是浪费。var products await context.Products .AsNoTracking() .Where(p p.Price 100) .ToListAsync();加上AsNoTrackingEF Core 不会记录这些实体的状态变化内存占用和 CPU 开销都大幅降低。2. 只取需要的字段投影不要每次都Select *用投影只拿你真正需要的字段var productInfos await context.Products .Select(p new ProductDto { Id p.Id, Name p.Name, Price p.Price }) .ToListAsync();3. 用Select投影还能顺便加载关联数据var orderInfos await context.Orders .Select(o new { OrderId o.Id, CustomerName o.Customer.Name, TotalAmount o.Items.Sum(i i.Price * i.Quantity), ItemCount o.Items.Count() }) .ToListAsync();这种方式比Include更精准因为你只拿你需要的数据SQL 生成的 JOIN 也更精简。4. N1 问题用Include解决// ❌ 错误会触发 N1 次查询 // 场景获取所有订单并逐一输出客户名称 // 如果启用了延迟加载以下代码会导致 1 次查询获取订单 N 次查询获取每个订单的客户 var orders await context.Orders.ToListAsync(); foreach (var order in orders) { Console.WriteLine(order.Customer); // 每次访问都触发一次查询 } // ✅ 正确一次性预加载 // 场景获取所有订单及对应的客户仅需一次查询 var ordersWithCustomer await context.Orders .Include(o o.Customer) .ToListAsync();用Include显式预加载关联数据把原本 1N 次查询压成 1 次 JOIN 查询。5. 集合过多时用AsSplitQuery()如果一个查询包含多个集合导航属性默认的单查询模式会产生笛卡尔积爆炸。这时用AsSplitQuery()拆分成多个 SQLvar blogs await context.Blogs .Include(b b.Posts) .Include(b b.Comments) .AsSplitQuery() .ToListAsync();EF Core 会分别查询 Blog、Posts、Comments 三张表然后在内存中组装避免数据重复膨胀。复杂查询铁律场景推荐方案注意事项多表关联加载IncludeThenInclude链别太长注意笛卡尔积动态多条件筛选表达式树 / PredicateBuilder别塞自定义方法会被静默忽略只读数据查询AsNoTracking()Select投影减少内存开销避免 N1预加载 禁用延迟加载用Include一次搞定多集合查询AsSplitQuery()防笛卡尔积爆炸数据量大分页 索引Skip/Take前必须排序复杂聚合GroupBy/ 聚合函数EF Core 会翻译成 SQL实在搞不定原生 SQL (FromSqlRaw)最后手段别滥用刚子结语别把 EF Core 当成黑盒。你写出来的 LINQ 查询最终都会翻译成 SQL不理解 SQL你就写不出高效的 EF Core 查询。我刚学 EF Core 的时候也踩过 N1、笛卡尔积、客户端求值这些坑。后来我养成了一个习惯每个复杂查询都去检查生成的 SQL 长啥样。你可以用 EF Core 自带的日志功能optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging();看一眼生成的 SQL你就知道哪里写得不对了。刚子的经验写复杂查询的时候先想清楚“我要的数据结构是什么”再用 LINQ 去表达。把 EF Core 当成带类型安全的 SQL 生成器别把它当成万能魔法箱。如果你觉得这篇有用点个赞、转给还在被 EF Core 复杂查询折磨的兄弟。我是刚子一个写了六年 .NET 代码的程序员。咱们下回见原文链接写 EF Core 查询90% 的人第一步就错了刚子教你避开所有坑 - 码农刚子的开发笔记合集: C#/.NET开发者宝典 , C#/.NET 编程指南标签: EFCore, EF免责声明本内容来自平台创作者博客园系信息发布平台仅提供信息存储空间服务。好文要顶 关注我 收藏该文 微信分享码农刚子粉丝 - 61 关注 - 11加关注9« 上一篇 序列化 JSON 时崩了99% 是 EF 延迟加载惹的祸三种解法拿走不谢» 下一篇 推荐一个开箱即用的.NET权限管理平台Magic.NETposted 2026-04-22 08:02 码农刚子 阅读(944) 评论(9) 收藏 举报