Appearance
AsNoTracking 与无跟踪查询
EF Core 最重要的性能优化技巧之一
📖 目录
什么是跟踪查询
跟踪查询的工作原理
默认情况下,EF Core 会跟踪所有查询到的实体:
csharp
// 跟踪查询(默认行为)
var product = await context.Products.FindAsync(1);
// EF Core 内部:
// 1. 从数据库读取数据
// 2. 创建 Product 对象
// 3. 保存原始值快照
// 4. 开始跟踪变化
product.Price = 99.99m; // 修改被跟踪
await context.SaveChangesAsync();
// UPDATE Products SET Price = 99.99 WHERE Id = 1跟踪的开销
跟踪查询的内存占用:
┌─────────────────────────────────────┐
│ 实体数据 (当前值) │
│ + 原始值快照 │
│ + 状态信息 │
│ + 属性元数据 │
│ = 2x 内存占用 │
└─────────────────────────────────────┘
跟踪查询的 CPU 开销:
┌─────────────────────────────────────┐
│ 1. 创建快照 │
│ 2. DetectChanges 对比 │
│ 3. 维护状态字典 │
│ 4. 生成更新 SQL │
└─────────────────────────────────────┘AsNoTracking 的作用
基本用法
csharp
// 无跟踪查询
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// EF Core 内部:
// 1. 从数据库读取数据
// 2. 创建 Product 对象
// 3. 不保存快照,不跟踪
// 4. 直接返回
// 尝试修改不会保存到数据库
var product = products.First();
product.Price = 99.99m;
await context.SaveChangesAsync();
// ❌ 不会执行 UPDATEAsNoTrackingWithIdentityResolution
csharp
// EF Core 5.0+ 新增
var products = await context.Products
.AsNoTrackingWithIdentityResolution()
.Include(p => p.Category)
.ToListAsync();
// 特点:
// - 不跟踪变化
// - 但保证同一实体只创建一个实例
// - 适合需要导航属性但不修改的场景对比:
| 方法 | 跟踪 | 身份解析 | 性能 | 适用场景 |
|---|---|---|---|---|
| 默认 | ✅ | ✅ | 基准 | 需要修改 |
AsNoTracking() | ❌ | ❌ | +30-50% | 只读,无导航 |
AsNoTrackingWithIdentityResolution() | ❌ | ✅ | +20-40% | 只读,有导航 |
性能对比
基准测试
csharp
public class TrackingBenchmark
{
private readonly AppDbContext _context;
[Benchmark]
public async Task<List<Product>> TrackedQuery()
{
return await _context.Products.ToListAsync();
}
[Benchmark]
public async Task<List<Product>> UntrackedQuery()
{
return await _context.Products.AsNoTracking().ToListAsync();
}
[Benchmark]
public async Task<List<Product>> UntrackedWithIdentityQuery()
{
return await _context.Products
.AsNoTrackingWithIdentityResolution()
.ToListAsync();
}
}典型结果(10,000 条记录):
| 方法 | 时间 | 内存 | 相对性能 |
|---|---|---|---|
| Tracked | 150ms | 50MB | 1.0x |
| AsNoTracking | 80ms | 25MB | 1.88x |
| AsNoTrackingWithIdentity | 100ms | 30MB | 1.5x |
实际项目数据
电商网站产品列表页:
├─ 跟踪查询: 200ms, 80MB 内存
├─ AsNoTracking: 110ms, 35MB 内存
└─ 性能提升: 45%, 内存减少 56%
报表系统:
├─ 跟踪查询: 500ms, 200MB 内存
├─ AsNoTracking: 250ms, 90MB 内存
└─ 性能提升: 50%, 内存减少 55%
API 接口:
├─ 跟踪查询: 50ms, 10MB 内存
├─ AsNoTracking: 30ms, 4MB 内存
└─ 性能提升: 40%, 内存减少 60%使用场景
✅ 应该使用 AsNoTracking
1. API 返回数据
csharp
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
{
var products = await context.Products
.AsNoTracking()
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
return Ok(products);
}2. 报表和统计
csharp
// 销售报表
var report = await context.Orders
.AsNoTracking()
.Where(o => o.OrderDate >= startDate)
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
TotalOrders = g.Count(),
TotalAmount = g.Sum(o => o.TotalAmount)
})
.ToListAsync();3. 下拉列表选项
csharp
// 分类列表
var categories = await context.Categories
.AsNoTracking()
.Select(c => new { c.Id, c.Name })
.ToListAsync();
// 绑定到下拉框
ViewBag.Categories = new SelectList(categories, "Id", "Name");4. 搜索功能
csharp
public async Task<List<Product>> SearchProductsAsync(string searchTerm)
{
return await context.Products
.AsNoTracking()
.Where(p => p.Name.Contains(searchTerm))
.Take(50)
.ToListAsync();
}5. 导出功能
csharp
// 导出 Excel/CSV
public async Task<byte[]> ExportProductsAsync()
{
var products = await context.Products
.AsNoTracking()
.ToListAsync();
// 转换为 Excel
return ExcelExporter.Export(products);
}❌ 不应该使用 AsNoTracking
1. 需要修改实体
csharp
// ❌ 错误
var product = await context.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id);
product.Price = 99.99m;
await context.SaveChangesAsync();
// ❌ 不会保存!
// ✅ 正确
var product = await context.Products
.FirstOrDefaultAsync(p => p.Id == id);
product.Price = 99.99m;
await context.SaveChangesAsync();
// ✅ 会保存2. 工作单元模式
csharp
public async Task UpdateProductPriceAsync(int productId, decimal newPrice)
{
// 需要跟踪以检测变化
var product = await context.Products.FindAsync(productId);
if (product != null)
{
product.Price = newPrice;
// ChangeTracker 自动检测到变化
await context.SaveChangesAsync();
}
}3. 并发控制
csharp
public async Task UpdateWithConcurrencyAsync(int productId, decimal newPrice)
{
// 需要跟踪以进行并发检查
var product = await context.Products.FindAsync(productId);
try
{
product.Price = newPrice;
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// 处理并发冲突
}
}全局配置
设置默认跟踪行为
csharp
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
// 所有查询默认无跟踪
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});效果:
csharp
// 现在所有查询都默认无跟踪
var products = await context.Products.ToListAsync();
// 等同于 AsNoTracking()
// 如需跟踪,显式指定
var product = await context.Products
.AsTracking()
.FirstOrDefaultAsync(p => p.Id == id);根据环境配置
csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
if (environment.IsDevelopment())
{
// 开发环境: 默认跟踪(便于调试)
options.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll);
}
else
{
// 生产环境: 默认无跟踪(性能优先)
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
});注意事项
1. 导航属性加载
csharp
// ❌ 错误: AsNoTracking 后 Include 可能有问题
var order = await context.Orders
.AsNoTracking()
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
// 如果 Order 和 OrderItem 都引用同一个 Product
// 会创建两个 Product 实例
// ✅ 正确: 使用 AsNoTrackingWithIdentityResolution
var order = await context.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);2. 不能直接修改
csharp
var product = await context.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == id);
// ❌ 直接修改不会保存
product.Price = 99.99m;
await context.SaveChangesAsync(); // 无效果
// ✅ 方法 1: Attach 后标记修改
context.Products.Attach(product);
context.Entry(product).State = EntityState.Modified;
await context.SaveChangesAsync();
// ✅ 方法 2: 重新查询
var trackedProduct = await context.Products.FindAsync(id);
trackedProduct.Price = 99.99m;
await context.SaveChangesAsync();3. 变更检测
csharp
// 跟踪查询: 自动检测变化
var product = await context.Products.FindAsync(1);
product.Price = 99.99m;
// DetectChanges 自动调用
await context.SaveChangesAsync();
// 无跟踪查询: 不会检测变化
var product = await context.Products
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == 1);
product.Price = 99.99m;
// 没有任何变化检测
await context.SaveChangesAsync(); // 不执行 UPDATE最佳实践
1. API 接口统一使用 AsNoTracking
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
{
return await _context.Products
.AsNoTracking() // ✅ 始终添加
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _context.Products
.AsNoTracking() // ✅ 始终添加
.Where(p => p.Id == id)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.FirstOrDefaultAsync();
if (product == null)
return NotFound();
return product;
}
}2. 读写分离
csharp
public class ProductService
{
private readonly AppDbContext _readContext; // 无跟踪
private readonly AppDbContext _writeContext; // 跟踪
public ProductService(
IDbContextFactory<AppDbContext> factory)
{
_readContext = factory.CreateDbContext();
_writeContext = factory.CreateDbContext();
}
// 读操作
public async Task<List<Product>> GetProductsAsync()
{
return await _readContext.Products
.AsNoTracking()
.ToListAsync();
}
// 写操作
public async Task UpdateProductAsync(Product product)
{
_writeContext.Products.Update(product);
await _writeContext.SaveChangesAsync();
}
}3. 缓存配合
csharp
public class CachedProductService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _context;
public async Task<List<Product>> GetActiveProductsAsync()
{
const string cacheKey = "active_products";
// 尝试从缓存获取
if (_cache.TryGetValue(cacheKey, out List<Product> products))
{
return products;
}
// 查询数据库(无跟踪)
products = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// 存入缓存(5 分钟)
_cache.Set(cacheKey, products, TimeSpan.FromMinutes(5));
return products;
}
}4. 批量操作优化
csharp
// ❌ 慢: 跟踪所有实体
for (int i = 0; i < 10000; i++)
{
var product = await context.Products.FindAsync(i);
// 处理...
}
// ✅ 快: 无跟踪查询
for (int i = 0; i < 10000; i += 1000)
{
var products = await context.Products
.AsNoTracking()
.Where(p => p.Id >= i && p.Id < i + 1000)
.ToListAsync();
// 处理...
// 清除跟踪(虽然无跟踪,但好习惯)
context.ChangeTracker.Clear();
}📊 性能优化检查清单
使用 AsNoTracking 前确认:
- [ ] 是否需要修改实体?
- [ ] 是否只需要读取数据?
- [ ] 是否是 API 返回数据?
- [ ] 是否是报表或统计?
- [ ] 是否是下拉列表选项?
- [ ] 是否是搜索功能?
- [ ] 是否是导出功能?
如果以上任一为"是",使用 AsNoTracking()!
💡 性能提升总结
典型场景性能对比
| 场景 | 记录数 | 跟踪查询 | AsNoTracking | 提升 |
|---|---|---|---|---|
| 产品列表 | 100 | 20ms | 12ms | 40% |
| 产品列表 | 1,000 | 80ms | 45ms | 44% |
| 产品列表 | 10,000 | 500ms | 250ms | 50% |
| 订单报表 | 50,000 | 2000ms | 900ms | 55% |
| API 接口 | 100 | 15ms | 9ms | 40% |
内存节省
| 记录数 | 跟踪查询 | AsNoTracking | 节省 |
|---|---|---|---|
| 100 | 2MB | 0.8MB | 60% |
| 1,000 | 15MB | 6MB | 60% |
| 10,000 | 120MB | 45MB | 62% |
| 100,000 | 1.2GB | 450MB | 62% |
📚 延伸阅读
💡 小结
核心要点:
- ✅ AsNoTracking 提升性能 30-50%
- ✅ 内存占用减少 50-60%
- ✅ API、报表、搜索等只读场景必用
- ✅ 需要修改实体时不能用
- ✅ 可配置全局默认行为
- ✅ 配合缓存效果更佳
使用原则:
只读查询 → AsNoTracking()
需要修改 → 跟踪查询
不确定 → 默认跟踪,后续优化下一步: