news 2026/6/25 17:18:47

写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
写 EF Core 查询,90% 的人第一步就错了:刚子教你避开所有坑

今天刚子不跟你扯理论,直接上实战代码,把 EF Core 复杂查询的几个核心技巧给你讲明白。顺便聊聊性能优化的几条铁律,省得你下次再踩坑。


先说核心:EF Core 复杂查询的3个核心技巧

处理复杂查询,你只需要记住这几招:

  1. 关联查询:用Include+ThenInclude一次性加载多级关联数据
  2. 动态筛选:用表达式树在运行时动态拼查询条件
  3. 性能优化:用AsNoTrackingSelect投影、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?太丑了,还容易漏条件。

动态查询的核心是用Expression<Func<T, bool>>在运行时拼接查询条件。

手写一个动态筛选器

public async Task<List<Product>> 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 Expression<Func<T, bool>> True<T>() { return f => true; } public static Expression<Func<T, bool>> False<T>() { return f => false; } public static Expression<Func<T, bool>> Or<T>( this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); return Expression.Lambda<Func<T, bool>>( Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters); } public static Expression<Func<T, bool>> And<T>( this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var invokedExpr = Expression.Invoke(expr2, expr1.Parameters); return Expression.Lambda<Func<T, bool>>( Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters); } }

用起来就很优雅了:

var predicate = PredicateBuilder.True<Product>(); 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 Task<PagedResult<Product>> 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 PagedResult<Product> { Items = items, TotalCount = totalCount, PageIndex = pageIndex, PageSize = pageSize }; }

划重点:分页查询必须在SkipTake之前先做排序,否则 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();

加上AsNoTracking,EF 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. N+1 问题用Include解决

// ❌ 错误:会触发 N+1 次查询 // 场景:获取所有订单,并逐一输出客户名称 // 如果启用了延迟加载,以下代码会导致 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显式预加载关联数据,把原本 1+N 次查询压成 1 次 JOIN 查询。

5. 集合过多时用AsSplitQuery()

如果一个查询包含多个集合导航属性,默认的单查询模式会产生笛卡尔积爆炸。这时用AsSplitQuery()拆分成多个 SQL:

var blogs = await context.Blogs .Include(b => b.Posts) .Include(b => b.Comments) .AsSplitQuery() .ToListAsync();

EF Core 会分别查询 Blog、Posts、Comments 三张表,然后在内存中组装,避免数据重复膨胀。


复杂查询铁律

场景推荐方案注意事项
多表关联加载Include+ThenInclude链别太长,注意笛卡尔积
动态多条件筛选表达式树 / PredicateBuilder别塞自定义方法,会被静默忽略
只读数据查询AsNoTracking()+Select投影减少内存开销
避免 N+1预加载 + 禁用延迟加载Include一次搞定
多集合查询AsSplitQuery()防笛卡尔积爆炸
数据量大分页 + 索引Skip/Take前必须排序
复杂聚合GroupBy/ 聚合函数EF Core 会翻译成 SQL
实在搞不定原生 SQL (FromSqlRaw)最后手段,别滥用

刚子结语

别把 EF Core 当成黑盒。你写出来的 LINQ 查询最终都会翻译成 SQL,不理解 SQL,你就写不出高效的 EF Core 查询。

我刚学 EF Core 的时候,也踩过 N+1、笛卡尔积、客户端求值这些坑。后来我养成了一个习惯:每个复杂查询都去检查生成的 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.NET

posted @ 2026-04-22 08:02 码农刚子 阅读(944) 评论(9) 收藏 举报

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/25 17:18:45

Python多核并发实战:绕过GIL的4种生产级方案

Python 3.14 并不存在——截至2024年&#xff0c;CPython 官方最新稳定版本是3.12.6&#xff08;2024年8月发布&#xff09;&#xff0c;3.13 正处于 beta 阶段&#xff08;预计2024年10月正式发布&#xff09;&#xff0c;而3.14 尚未进入官方开发路线图&#xff0c;也未在 py…

作者头像 李华
网站建设 2026/6/25 17:18:23

NewTab Redirect! 终极指南:5个场景彻底重塑你的浏览器工作流

NewTab Redirect! 终极指南&#xff1a;5个场景彻底重塑你的浏览器工作流 【免费下载链接】NewTab-Redirect NewTab Redirect! is an extension for Google Chrome which allows the user to replace the page displayed when creating a new tab. 项目地址: https://gitcode…

作者头像 李华
网站建设 2026/6/25 17:18:03

大数据量 Excel 导出性能优化:SXSSFWorkbook 流式写入实战

大数据量 Excel 导出性能优化&#xff1a;SXSSFWorkbook 流式写入实战 一、问题背景 导出10万行数据到 Excel 时&#xff0c;常见的性能问题&#xff1a;问题原因后果内存溢出&#xff08;OOM&#xff09;所有行对象同时存在堆内存中服务崩溃导出慢&#xff08;20秒&#xff09…

作者头像 李华
网站建设 2026/6/25 17:09:50

LMXCMS 1.4 SQL注入漏洞实战审计:从原理到修复

1. 项目概述&#xff1a;从一次实战审计看LMXCMS 1.4的注入风险最近在整理一些老版本CMS的审计案例&#xff0c;LMXCMS 1.4这个版本进入了我的视野。它虽然不算特别主流&#xff0c;但在一些特定场景下仍有使用&#xff0c;其代码结构清晰&#xff0c;对于学习代码审计和漏洞挖…

作者头像 李华
网站建设 2026/6/25 17:06:40

HeidiSQL 12.20 发布:修复多项问题,新增 SQLite 默认值关键字支持!

HeidiSQL 12.20 修复与新增功能亮点 HeidiSQL 12.20 正式发布&#xff0c;带来了一系列更新。在修复方面&#xff0c;解决了在 mysql.proc 中显示 MySQL 存储过程和函数大小写的问题&#xff0c;让显示更加准确&#xff1b;还修复了 macOS 上 SelectUserNode 无法找到新创建具有…

作者头像 李华
网站建设 2026/6/25 17:03:55

4G 报警器和传统有线报警器比,哪个更靠谱?

鱼塘、果园、仓库、养殖场……户外场所装报警器&#xff0c;有线和无线到底怎么选&#xff1f;这篇文章从安装、可靠性、成本、维护四个维度说清楚。一、先上结论维度4G 无线报警器传统有线报警器安装难度磁吸贴装&#xff0c;几分钟搞定需要布线、打孔、接电源&#xff0c;半天…

作者头像 李华