惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
L
LangChain Blog
Jina AI
Jina AI
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
S
Secure Thoughts
T
The Exploit Database - CXSecurity.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
T
Threat Research - Cisco Blogs
量子位
G
GRAHAM CLULEY
腾讯CDC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
C
CERT Recently Published Vulnerability Notes
The Hacker News
The Hacker News
C
Cisco Blogs
Scott Helme
Scott Helme
Spread Privacy
Spread Privacy
宝玉的分享
宝玉的分享
V
V2EX
博客园 - 三生石上(FineUI控件)
T
Tor Project blog
P
Proofpoint News Feed
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V
Vulnerabilities – Threatpost
PCI Perspectives
PCI Perspectives
博客园_首页
L
LINUX DO - 最新话题
IT之家
IT之家
有赞技术团队
有赞技术团队
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Last Week in AI
Last Week in AI
The Cloudflare Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
美团技术团队
博客园 - 【当耐特】
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Security Archives - TechRepublic
Security Archives - TechRepublic
L
LINUX DO - 热门话题
AWS News Blog
AWS News Blog
S
Security Affairs
T
Tailwind CSS Blog

博客园 - 邓磊Lei

为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 .NET 进阶之路:异步、并发与内存管理的系统性认知 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战 如何在 rider 中配置 SonarQube
EF Core 查询性能黑洞:Include、投影与跟踪策略的边界
邓磊Lei · 2026-03-09 · via 博客园 - 邓磊Lei

很多团队把 EF Core 的性能问题归因于“ORM 天生慢”,但线上真实情况通常是:

  • 查询写法对 SQL 形态不敏感
  • 默认跟踪被滥用
  • 图省事一次 Include 到底

结果是接口能跑,但高峰时段 P95 持续抬高,数据库 CPU 和网络带宽一起被拖上去。

这篇文章聚焦一个目标:把 EF Core 查询从“能查到数据”升级到“可预测、可解释、可优化”。

1. 问题背景:列表页为什么越改越慢

一个典型场景:订单列表页需要展示订单、客户、明细、商品。

最直觉的写法是连续 Include

var orders = await db.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
        .ThenInclude(i => i.Product)
    .Where(o => o.CreatedAt >= from && o.CreatedAt < to)
    .OrderByDescending(o => o.CreatedAt)
    .Take(50)
    .ToListAsync();

这段代码看起来“很完整”,但常见后果是:

  • 结果集行数被笛卡尔放大
  • 应用端做了大量重复对象反序列化和跟踪
  • 实际只显示 6 个字段,却把整个对象图都拉回来了

2. 原理解析:EF Core 查询成本主要花在哪里

2.1 翻译成本

LINQ 先被转换为表达式树,再翻译成 SQL。复杂投影、方法调用、局部函数容易导致翻译退化或直接失败。

2.2 执行与网络成本

Include 深度越深,JOIN 越复杂,网络回包越大。很多慢查询不是数据库“算得慢”,而是“传得多”。

2.3 跟踪成本

默认跟踪模式会创建实体快照,便于更新,但纯读场景这部分是额外开销。

2.4 物化成本

即使 SQL 很快,应用层物化大量实体和导航属性也会抬高 CPU 与内存分配。

3. 示例代码:从全量实体到精准投影

先给出推荐的读模型查询写法:

public sealed record OrderListItemDto(
    long Id,
    string OrderNo,
    string CustomerName,
    decimal TotalAmount,
    int ItemCount,
    DateTime CreatedAt);

var query = db.Orders
    .AsNoTracking()
    .Where(o => o.CreatedAt >= from && o.CreatedAt < to)
    .OrderByDescending(o => o.CreatedAt)
    .Select(o => new OrderListItemDto(
        o.Id,
        o.OrderNo,
        o.Customer.Name,
        o.Items.Sum(i => i.Quantity * i.UnitPrice),
        o.Items.Count,
        o.CreatedAt));

var page = await query.Take(50).ToListAsync();

对于高频、形态固定的查询,可以进一步使用编译查询:

private static readonly Func<AppDbContext, DateTime, DateTime, int, IAsyncEnumerable<OrderListItemDto>>
    QueryOrderPage = EF.CompileAsyncQuery(
        (AppDbContext db, DateTime from, DateTime to, int take) =>
            db.Orders
              .AsNoTracking()
              .Where(o => o.CreatedAt >= from && o.CreatedAt < to)
              .OrderByDescending(o => o.CreatedAt)
              .Select(o => new OrderListItemDto(
                  o.Id,
                  o.OrderNo,
                  o.Customer.Name,
                  o.Items.Sum(i => i.Quantity * i.UnitPrice),
                  o.Items.Count,
                  o.CreatedAt))
              .Take(take));

var result = new List<OrderListItemDto>();
await foreach (var item in QueryOrderPage(db, from, to, 50))
{
    result.Add(item);
}

如果确实需要多个集合导航,优先评估 AsSplitQuery(),避免单 SQL 被放大成巨型 JOIN。

4. 工程实践建议

4.1 明确“查询分层”

  • 写模型:实体 + 跟踪,用于更新
  • 读模型:DTO 投影 + AsNoTracking()

不要让一个查询同时承担“展示”和“更新”职责。

4.2 评审清单必须落地

每个新查询上线前至少确认:

  • 是否只取了页面真正需要的字段
  • 是否误用了默认跟踪
  • 是否出现多集合 Include
  • ToQueryString() 生成 SQL 是否可读、可控

4.3 慢查询排查顺序

  1. 先看 SQL 形态(是否放大)
  2. 再看索引命中
  3. 最后看 EF 物化和跟踪开销

这个顺序能避免在错误方向上“调 ORM 参数”。

4.4 建立基线

对核心查询做固定数据量压测,记录:

  • 平均耗时
  • P95
  • 每请求分配内存
  • 数据库逻辑读

没有基线,优化结果只能靠感觉。

5. 总结

EF Core 查询优化的关键,不是“把 ORM 用得更花”,而是把查询职责拆清楚:

  • 读取就投影
  • 更新才跟踪
  • 复杂对象图按需拆分

真正的工程收益来自可预测性。你能解释每一个查询为什么快,团队才敢在业务增长时持续演进。