5.4 EF Core 查询性能
EF Core 可以让数据访问更高效,但错误用法也很容易造成慢查询、N+1 查询或加载过多数据。性能优化的第一原则是让数据库做它擅长的筛选、排序和聚合。
学习目标
- 能使用投影查询减少加载字段。
- 能识别和避免 N+1 查询。
- 能正确使用分页和排序。
- 能理解跟踪查询和非跟踪查询的差异。
应用场景
- 列表页只需要展示少量字段。
- 每个任务要显示标签数量或创建人名称。
- 数据量增长后列表加载变慢。
- 只读接口不需要修改实体。
推荐写法:投影 DTO
var page = await dbContext.Todos
.AsNoTracking()
.Where(todo => todo.UserId == userId)
.OrderByDescending(todo => todo.CreatedAt)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(todo => new TodoListItemDto(
todo.Id,
todo.Title,
todo.IsCompleted,
todo.Tags.Count))
.ToListAsync(cancellationToken);
这会让数据库只返回页面需要的数据,而不是把完整实体和关系都加载到内存。
N+1 问题示例
var todos = await dbContext.Todos.ToListAsync(cancellationToken);
foreach (var todo in todos)
{
Console.WriteLine(todo.Tags.Count);
}
如果启用了懒加载,这类代码可能先查一次任务,再为每个任务查一次标签。推荐改为一次投影查询或显式 Include。
分页模型
public sealed record PageRequest(int Page = 1, int PageSize = 20);
public sealed record PageResult<T>(
IReadOnlyList<T> Items,
int Total,
int Page,
int PageSize);
重点难点
Include不是默认好事,列表页通常投影更合适。- 先
ToListAsync再Where会把过滤放到内存中执行。 - 分页必须有稳定排序,否则翻页可能重复或漏数据。
- 慢查询要结合生成 SQL、执行计划和索引一起看。
常见误区
| 误区 | 推荐做法 |
|---|---|
所有查询都 Include 全关系 | 按页面需要投影字段 |
| 先查全量再分页 | 在数据库侧 Skip / Take |
| 只看 C# 代码不看 SQL | 使用日志观察 EF Core 生成的 SQL |
练习
- 把一个返回实体列表的接口改成投影 DTO。
- 为任务列表增加分页,并返回总数。
- 打开 EF Core SQL 日志,观察修改前后的 SQL 差异。