EF Core 与 MySQL:查询优化详解

本文将详细讲解EF Core与MySQL的查询优化,包括使用AsNoTracking提高查询性能,使用Include和ThenInclude进行贪婪加载,使用Select进行投影查询、原始SQL查询,使用索引优化查询,其他优化技巧如分页、批量操作和查询编译,性能监控和诊断工具的使用。

1. 使用 AsNoTracking 提高查询性能

基本用法

// 常规查询(会跟踪实体变更)
var products = context.Products
    .Where(p => p.Price > 100)
    .ToList();

// 使用 AsNoTracking(不跟踪实体变更,性能更好)
var products = context.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToList();

应用场景

  • 只读查询操作

  • 数据展示场景

  • 报表生成

  • 大数据量查询

全局配置

// 在DbContext中配置全局不跟踪
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

// 或者针对特定查询启用跟踪
var products = context.Products
    .AsTracking() // 显式启用跟踪
    .Where(p => p.Price > 100)
    .ToList();

2. 使用 Include 和 ThenInclude 进行贪婪加载

基本用法

// 加载单个关联实体
var blogs = context.Blogs
    .Include(b => b.Posts) // 加载Posts集合
    .ToList();

// 加载多层关联实体
var blogs = context.Blogs
    .Include(b => b.Posts)
        .ThenInclude(p => p.Comments) // 加载Posts下的Comments
    .Include(b => b.Author) // 加载单个Author
    .ToList();

// 加载多个关联实体
var blogs = context.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Tags)
    .ToList();

过滤包含的关联数据

// 只加载符合条件的关联数据(EF Core 5.0+)
var blogs = context.Blogs
    .Include(b => b.Posts.Where(p => p.IsPublished))
    .Include(b => b.Tags.OrderBy(t => t.Name).Take(5))
    .ToList();

// 使用字符串方式包含(动态查询场景)
var blogs = context.Blogs
    .Include("Posts.Comments")
    .ToList();

性能考虑

// 避免过度包含(N+1查询问题)
// 错误方式:会产生N+1查询
var blogs = context.Blogs.ToList();
foreach (var blog in blogs)
{
    var posts = context.Posts.Where(p => p.BlogId == blog.Id).ToList();
    // 处理posts...
}

// 正确方式:使用Include一次性加载所有关联数据
var blogs = context.Blogs
    .Include(b => b.Posts)
    .ToList();
foreach (var blog in blogs)
{
    // 直接访问blog.Posts,不会产生额外查询
}

3. 使用 Select 进行投影查询

基本投影

// 只选择需要的字段
var productInfo = context.Products
    .Where(p => p.Price > 100)
    .Select(p => new 
    {
        p.Id,
        p.Name,
        p.Price,
        CategoryName = p.Category.Name // 关联实体字段
    })
    .ToList();

// 转换为DTO对象
var productDtos = context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price,
        CategoryName = p.Category.Name
    })
    .ToList();

条件投影

var products = context.Products
    .Select(p => new 
    {
        p.Id,
        p.Name,
        PriceCategory = p.Price > 100 ? "Expensive" : "Affordable",
        HasStock = p.Stock > 0
    })
    .ToList();

集合投影

var blogSummaries = context.Blogs
    .Select(b => new 
    {
        b.Id,
        b.Title,
        PostCount = b.Posts.Count(),
        LatestPost = b.Posts
            .OrderByDescending(p => p.CreatedDate)
            .Select(p => new { p.Title, p.CreatedDate })
            .FirstOrDefault()
    })
    .ToList();

4. 原始 SQL 查询

基本查询

// 使用FromSqlRaw执行原始SQL查询
var products = context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0} AND Stock > {1}", 100, 0)
    .ToList();

// 使用参数化查询防止SQL注入
var minPrice = 100;
var minStock = 0;
var products = context.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice} AND Stock > {minStock}")
    .ToList();

与LINQ结合使用

// 原始SQL查询后继续使用LINQ
var expensiveProducts = context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > 100")
    .Where(p => p.Stock > 0)
    .OrderByDescending(p => p.Price)
    .ToList();

执行非查询SQL

// 执行更新、删除等操作
var affectedRows = context.Database.ExecuteSqlRaw(
    "UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}", 
    categoryId);

// 使用存储过程
var products = context.Products
    .FromSqlRaw("EXEC GetExpensiveProducts @minPrice = {0}", 100)
    .ToList();

5. 使用索引优化查询

在模型中定义索引

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 创建单列索引
    modelBuilder.Entity<Product>()
        .HasIndex(p => p.Name);
    
    // 创建唯一索引
    modelBuilder.Entity<Product>()
        .HasIndex(p => p.Sku)
        .IsUnique();
    
    // 创建复合索引
    modelBuilder.Entity<Product>()
        .HasIndex(p => new { p.CategoryId, p.Price });
    
    // 创建筛选索引(MySQL 8.0+)
    modelBuilder.Entity<Product>()
        .HasIndex(p => p.Price)
        .HasFilter("[Price] > 100");
}

在迁移中创建索引

// 创建迁移后,可以自定义索引
migrationBuilder.CreateIndex(
    name: "IX_Products_CategoryId_Price",
    table: "Products",
    columns: new[] { "CategoryId", "Price" },
    filter: "Price > 100");

使用索引提示(MySQL 8.0+)

// 强制使用特定索引
var products = context.Products
    .FromSqlRaw("SELECT * FROM Products USE INDEX (IX_Products_Price) WHERE Price > 100")
    .ToList();

监控查询性能

// 启用MySQL慢查询日志
// 在my.cnf或my.ini中添加:
// slow_query_log = 1
// slow_query_log_file = /var/log/mysql/mysql-slow.log
// long_query_time = 2

// 使用EXPLAIN分析查询
var explainResult = context.Database.ExecuteSqlRaw(
    "EXPLAIN SELECT * FROM Products WHERE Price > 100");

6. 其他优化技巧

分页优化

// 使用Keyset分页(基于值的分页)
var lastPrice = 100;
var lastId = 50;
var products = context.Products
    .Where(p => p.Price > lastPrice || (p.Price == lastPrice && p.Id > lastId))
    .OrderBy(p => p.Price)
    .ThenBy(p => p.Id)
    .Take(20)
    .ToList();

// 传统分页(适用于小数据集)
var pageNumber = 2;
var pageSize = 20;
var products = context.Products
    .OrderBy(p => p.Name)
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize)
    .ToList();

批量操作优化

// 使用AddRange批量添加
var products = new List<Product>();
// 添加多个产品到列表
context.Products.AddRange(products);
context.SaveChanges();

// 使用ExecuteUpdate批量更新(EF Core 7.0+)
context.Products
    .Where(p => p.CategoryId == 1)
    .ExecuteUpdate(p => p.SetProperty(x => x.Price, x => x.Price * 1.1m));

// 使用ExecuteDelete批量删除(EF Core 7.0+)
context.Products
    .Where(p => p.Stock == 0)
    .ExecuteDelete();

查询编译优化

// 使用编译查询(适用于频繁执行的查询)
private static readonly Func<ApplicationDbContext, int, IEnumerable<Product>> 
    GetProductsByCategory = 
        EF.CompileQuery((ApplicationDbContext context, int categoryId) => 
            context.Products.Where(p => p.CategoryId == categoryId));

// 使用编译查询
var products = GetProductsByCategory(context, 1).ToList();

连接池优化

// 在连接字符串中配置连接池
var connectionString = "server=localhost;database=efcoredb;user=root;password=yourpassword;Pooling=true;MinimumPoolSize=5;MaximumPoolSize=100;ConnectionTimeout=30;";

7. 性能监控和诊断

启用EF Core日志

// 在DbContext配置中启用敏感数据日志记录和详细错误
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
        .EnableSensitiveDataLogging() // 仅开发环境
        .EnableDetailedErrors() // 仅开发环境
        .LogTo(Console.WriteLine, LogLevel.Information); // 记录SQL查询
}

使用MiniProfiler监控性能

// 安装MiniProfiler.EntityFrameworkCore
services.AddMiniProfiler(options => 
{
    options.RouteBasePath = "/profiler";
    options.ColorScheme = StackExchange.Profiling.ColorScheme.Auto;
}).AddEntityFramework();

分析查询性能

// 使用MySQL的EXPLAIN分析查询
var query = context.Products.Where(p => p.Price > 100);
var sql = query.ToQueryString(); // 获取生成的SQL
Console.WriteLine(sql);

// 或者在数据库直接执行EXPLAIN
var explainResult = context.Database.ExecuteSqlRaw(
    "EXPLAIN SELECT * FROM Products WHERE Price > 100");

总结

本教程详细介绍了EF Core与MySQL的查询优化技巧,包括:

  1. 使用AsNoTracking提高只读查询性能

  2. 使用Include和ThenInclude正确加载关联数据,避免N+1查询问题

  3. 使用Select投影查询减少数据传输量

  4. 使用原始SQL查询处理复杂场景

  5. 使用索引优化查询性能

  6. 其他优化技巧如分页、批量操作和查询编译

  7. 性能监控和诊断工具的使用

优化查询性能是一个持续的过程,需要结合实际应用场景和数据库特性进行调整。建议定期分析慢查询日志,使用EXPLAIN分析查询计划,并根据结果调整索引和查询方式。

记住,最好的优化往往是基于实际性能分析而不是盲目猜测。在生产环境中,始终使用性能监控工具来识别和解决瓶颈问题。在这个系列的最后,会单独详细的写一篇EF Core与MySQL的日志和调试详解。