Skip to content

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(); 
// ❌ 不会执行 UPDATE

AsNoTrackingWithIdentityResolution ​

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 条记录):

方法时间内存相对性能
Tracked150ms50MB1.0x
AsNoTracking80ms25MB1.88x
AsNoTrackingWithIdentity100ms30MB1.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提升
产品列表10020ms12ms40%
产品列表1,00080ms45ms44%
产品列表10,000500ms250ms50%
订单报表50,0002000ms900ms55%
API 接口10015ms9ms40%

内存节省 ​

记录数跟踪查询AsNoTracking节省
1002MB0.8MB60%
1,00015MB6MB60%
10,000120MB45MB62%
100,0001.2GB450MB62%

📚 延伸阅读 ​


💡 小结 ​

核心要点:

  • ✅ AsNoTracking 提升性能 30-50%
  • ✅ 内存占用减少 50-60%
  • ✅ API、报表、搜索等只读场景必用
  • ✅ 需要修改实体时不能用
  • ✅ 可配置全局默认行为
  • ✅ 配合缓存效果更佳

使用原则:

只读查询 → AsNoTracking()
需要修改 → 跟踪查询
不确定 → 默认跟踪,后续优化

下一步:

  1. 学习 投影查询
  2. 掌握 避免 N+1 查询
  3. 理解 变更跟踪原理

基于 MIT 许可发布