编辑
2026-01-31
C#
00

目录

🎯 核心差异解析
💡 执行位置的根本不同
🔥 表达式树 vs 委托
⚠️ 常见性能陷阱实战
🐛 陷阱1:意外的AsEnumerable()调用
🐛 陷阱2:混合查询的性能问题
🛠️ 性能优化最佳实践
💡 实践1:延迟执行的合理利用
💡 实践2:投影优化减少数据传输
💡 实践3:分批处理大量数据
🎯 核心要点总结

作为C#开发者,你是否遇到过这样的场景:查询数据库时程序突然变得异常缓慢?或者在处理大量数据时内存占用飙升?很可能你踩中了 IQueryableIEnumerable 的性能陷阱。

这两个接口看似相似,但在实际应用中差异巨大。一个不当的选择可能让你的应用性能下降10倍甚至更多。本文将深入剖析它们的本质区别,帮你避开常见陷阱,写出高性能的C#代码。

🎯 核心差异解析

💡 执行位置的根本不同

IQueryable:在数据源端执行(如数据库)

IEnumerable:在内存中执行

c#
using System; using System.Linq; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; namespace AppIQueryableVsIEnumerable { // 用户实体类 public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } } // 数据库上下文 public class AppDbContext : DbContext { public DbSet<User> Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 使用内存数据库进行演示 optionsBuilder.UseInMemoryDatabase("TestDb"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 添加一些测试数据 modelBuilder.Entity<User>().HasData( new User { Id = 1, Name = "张三", Age = 25 }, new User { Id = 2, Name = "李四", Age = 17 }, new User { Id = 3, Name = "王五", Age = 30 }, new User { Id = 4, Name = "赵六", Age = 16 }, new User { Id = 5, Name = "孙七", Age = 28 } ); } } internal class Program { static void Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; using var context = new AppDbContext(); context.Database.EnsureCreated(); Console.WriteLine("=== IQueryable vs IEnumerable 对比 ===\n"); // ❌ 危险做法:将查询结果转为IEnumerable // 这会将所有数据加载到内存中,然后在内存中执行过滤 Console.WriteLine("❌ 错误做法 (AsEnumerable):"); IEnumerable<User> usersEnum = context.Users.AsEnumerable() .Where(u => u.Age > 18) .Take(10); Console.WriteLine($"查询到 {usersEnum.Count()} 个成年用户"); foreach (var user in usersEnum) { Console.WriteLine($"- {user.Name}, 年龄: {user.Age}"); } Console.WriteLine("\n" + "=".PadRight(40, '=') + "\n"); // ✅ 正确做法:保持IQueryable // 这会在数据库层面执行过滤,只返回符合条件的数据 Console.WriteLine("✅ 正确做法 (IQueryable):"); IQueryable<User> usersQuery = context.Users .Where(u => u.Age > 18) .Take(10); Console.WriteLine($"查询到 {usersQuery.Count()} 个成年用户"); foreach (var user in usersQuery) { Console.WriteLine($"- {user.Name}, 年龄: {user.Age}"); } Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } } }

image.png 关键差异:第一种做法会将整个 Users 表加载到内存,然后在内存中筛选;第二种做法生成SQL在数据库端筛选,只返回需要的10条记录。

🔥 表达式树 vs 委托

c#
using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace AppIQueryableVsIEnumerable { public class PerformanceDemo { public void DemonstrateExpressionTrees() { var numbers = new[] { 1, 2, 3, 4, 5 }.AsQueryable(); // IQueryable使用表达式树 - 可以转换为SQL Expression<Func<int, bool>> queryableExpression = x => x > 3; var queryableResult = numbers.Where(queryableExpression); // IEnumerable使用委托 - 只能在内存执行 Func<int, bool> enumerableDelegate = x => x > 3; var enumerableResult = numbers.AsEnumerable().Where(enumerableDelegate); Console.WriteLine($"Queryable类型: {queryableResult.GetType()}"); Console.WriteLine($"Enumerable类型: {enumerableResult.GetType()}"); } } internal class Program { static void Main(string[] args) { PerformanceDemo demo = new PerformanceDemo(); demo.DemonstrateExpressionTrees(); Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } } }

image.png

⚠️ 常见性能陷阱实战

🐛 陷阱1:意外的AsEnumerable()调用

c#
// ❌ 性能陷阱示例 public async Task<List<Product>> GetExpensiveProducts() { using var context = new ShopContext(); // 这行代码会加载所有产品到内存! var products = await context.Products .AsEnumerable() // 💥 性能炸弹 .Where(p => p.Price > 1000) .ToListAsync(); return products; } // ✅ 性能优化版本 public async Task<List<Product>> GetExpensiveProductsOptimized() { using var context = new ShopContext(); // 在数据库层面筛选 var products = await context.Products .Where(p => p.Price > 1000) .ToListAsync(); return products; }

🐛 陷阱2:混合查询的性能问题

c#
using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace AppIQueryableVsIEnumerable { // 订单实体类 public class Order { public int Id { get; set; } public int CustomerId { get; set; } public string Status { get; set; } public decimal Amount { get; set; } public DateTime CreateDate { get; set; } } // 数据库上下文 public class OrderContext : DbContext { public DbSet<Order> Orders { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseInMemoryDatabase("OrderDb"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 添加测试数据 modelBuilder.Entity<Order>().HasData( new Order { Id = 1, CustomerId = 1, Status = "Completed", Amount = 100m, CreateDate = DateTime.Now.AddDays(-1) }, new Order { Id = 2, CustomerId = 1, Status = "Pending", Amount = 200m, CreateDate = DateTime.Now.AddDays(-2) }, new Order { Id = 3, CustomerId = 1, Status = "Completed", Amount = 150m, CreateDate = DateTime.Now.AddDays(-3) }, new Order { Id = 4, CustomerId = 1, Status = "Completed", Amount = -50m, CreateDate = DateTime.Now.AddDays(-4) }, // 无效金额 new Order { Id = 5, CustomerId = 2, Status = "Completed", Amount = 300m, CreateDate = DateTime.Now.AddDays(-1) }, new Order { Id = 6, CustomerId = 1, Status = "Cancelled", Amount = 80m, CreateDate = DateTime.Now.AddDays(-5) } ); } } // ❌ 错误的服务实现 public class OrderService { public async Task<decimal> GetOrderTotal(int customerId) { using var context = new OrderContext(); Console.WriteLine("❌ 错误做法:混合查询"); // 危险:部分在数据库,部分在内存 var orders = await context.Orders .Where(o => o.CustomerId == customerId) .ToListAsync(); // 提前物化查询,所有数据都加载到内存 Console.WriteLine($"从数据库加载了 {orders.Count} 条订单记录到内存"); // 后续计算在内存中进行 var validOrders = orders .Where(o => IsValidOrder(o)) // 自定义方法必须在内存执行 .ToList(); Console.WriteLine($"内存过滤后剩余 {validOrders.Count} 条有效订单"); var total = validOrders.Sum(o => o.Amount); return total; } private bool IsValidOrder(Order order) { // 复杂业务逻辑 return order.Status == "Completed" && order.Amount > 0; } } // ✅ 优化后的服务实现 public class OrderServiceOptimized { public async Task<decimal> GetOrderTotal(int customerId) { using var context = new OrderContext(); Console.WriteLine("✅ 正确做法:数据库层面过滤"); // 在数据库层面尽可能多的筛选 var totalAmount = await context.Orders .Where(o => o.CustomerId == customerId) .Where(o => o.Status == "Completed") // 在数据库执行 .Where(o => o.Amount > 0) // 在数据库执行 .SumAsync(o => o.Amount); // 直接在数据库计算总和 Console.WriteLine("直接在数据库层面完成筛选和计算"); return totalAmount; } // 如果确实需要复杂业务逻辑,分两步执行 public async Task<decimal> GetOrderTotalWithBusinessLogic(int customerId) { using var context = new OrderContext(); Console.WriteLine("✅ 混合方案:先数据库筛选,再业务逻辑"); // 步骤1:数据库层面基础筛选 var orders = await context.Orders .Where(o => o.CustomerId == customerId) .Where(o => o.Status == "Completed") // 能在数据库做的先做 .Where(o => o.Amount > 0) .ToListAsync(); Console.WriteLine($"数据库预筛选后加载 {orders.Count} 条记录"); // 步骤2:复杂业务逻辑(如果真的需要) var validOrders = orders.Where(o => ComplexBusinessRule(o)); return validOrders.Sum(o => o.Amount); } private bool ComplexBusinessRule(Order order) { // 这里可能有复杂的业务逻辑,无法转换为SQL // 比如调用外部API、复杂算法等 return order.CreateDate > DateTime.Now.AddDays(-30); } } // 主程序 internal class Program { static async Task Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; // 初始化数据库 using (var context = new OrderContext()) { context.Database.EnsureCreated(); } Console.WriteLine("=== IQueryable 查询优化示例 ===\n"); int customerId = 1; // 测试错误做法 var wrongService = new OrderService(); var result1 = await wrongService.GetOrderTotal(customerId); Console.WriteLine($"错误做法结果:{result1:C}"); Console.WriteLine("\n" + "=".PadRight(50, '=') + "\n"); // 测试正确做法 var correctService = new OrderServiceOptimized(); var result2 = await correctService.GetOrderTotal(customerId); Console.WriteLine($"正确做法结果:{result2:C}"); Console.WriteLine("\n" + "=".PadRight(50, '=') + "\n"); // 测试混合方案 var result3 = await correctService.GetOrderTotalWithBusinessLogic(customerId); Console.WriteLine($"混合方案结果:{result3:C}"); Console.WriteLine("\n按任意键退出..."); Console.ReadKey(); } } }

image.png

🛠️ 性能优化最佳实践

💡 实践1:延迟执行的合理利用

c#
public class DataService { private readonly AppDbContext _context; public DataService(AppDbContext context) { _context = context; } // ✅ 构建可复用的基础查询 public IQueryable<User> GetActiveUsers() { Console.WriteLine("🔧 构建基础查询:活跃用户"); return _context.Users .Where(u => u.IsActive) .Where(u => u.LastLoginDate > DateTime.Now.AddDays(-30)); } // ✅ 基于基础查询进行扩展 public async Task<List<User>> GetActiveAdminUsers() { Console.WriteLine("👑 查询活跃管理员用户"); var query = GetActiveUsers() // 复用基础查询 .Where(u => u.Role == "Admin") .OrderBy(u => u.Name); var result = await query.ToListAsync(); Console.WriteLine($"找到 {result.Count} 个活跃管理员"); return result; } // ✅ 按角色查询活跃用户 public async Task<List<User>> GetActiveUsersByRole(string role) { Console.WriteLine($"👤 查询角色为 {role} 的活跃用户"); var query = GetActiveUsers() .Where(u => u.Role == role) .OrderBy(u => u.LastLoginDate); var result = await query.ToListAsync(); Console.WriteLine($"找到 {result.Count}{role} 角色的活跃用户"); return result; } // ✅ 分页查询优化 public async Task<PaginatedResult<User>> GetActiveUsersPaginated(int page, int size) { Console.WriteLine($"📄 分页查询:第 {page} 页,每页 {size} 条"); var baseQuery = GetActiveUsers(); // 先计算总数 var total = await baseQuery.CountAsync(); Console.WriteLine($"总共有 {total} 个活跃用户"); // 然后获取分页数据 var users = await baseQuery .OrderBy(u => u.Name) // 分页需要排序 .Skip((page - 1) * size) .Take(size) .ToListAsync(); Console.WriteLine($"返回第 {page} 页的 {users.Count} 条记录"); return new PaginatedResult<User> { Items = users, TotalCount = total, Page = page, PageSize = size }; } // ✅ 复杂查询组合 public async Task<List<User>> GetRecentActiveUsers(int days = 7) { Console.WriteLine($"🕐 查询最近 {days} 天内活跃的用户"); var query = GetActiveUsers() .Where(u => u.LastLoginDate > DateTime.Now.AddDays(-days)) .OrderByDescending(u => u.LastLoginDate); var result = await query.ToListAsync(); Console.WriteLine($"找到 {result.Count} 个最近活跃用户"); return result; } }

image.png

💡 实践2:投影优化减少数据传输

c#
// ❌ 加载完整实体 public async Task<List<UserSummary>> GetUserSummaries() { using var context = new AppDbContext(); var users = await context.Users .Include(u => u.Orders) .Include(u => u.Profile) .ToListAsync(); // 加载大量不需要的数据 return users.Select(u => new UserSummary { Id = u.Id, Name = u.Name, OrderCount = u.Orders.Count }).ToList(); } // ✅ 使用投影只获取需要的数据 public async Task<List<UserSummary>> GetUserSummariesOptimized() { using var context = new AppDbContext(); var summaries = await context.Users .Select(u => new UserSummary { Id = u.Id, Name = u.Name, OrderCount = u.Orders.Count() // 在数据库层面计算 }) .ToListAsync(); return summaries; }

💡 实践3:分批处理大量数据

c#
public class BatchProcessor { private readonly AppDbContext _context; private const int BatchSize = 1000; // 每批处理1000条 public BatchProcessor(AppDbContext context) { _context = context; } // ✅ 标准分批处理方法 public async Task ProcessLargeDataSet() { Console.WriteLine("🚀 开始分批处理大数据集..."); var totalCount = await _context.LargeTable .Where(x => !x.IsProcessed) .CountAsync(); Console.WriteLine($"📊 总共需要处理 {totalCount} 条记录"); var processed = 0; var batchNumber = 1; while (processed < totalCount) { Console.WriteLine($"\n⚡ 处理第 {batchNumber} 批数据..."); // 获取一批未处理的数据 var batch = await _context.LargeTable .Where(x => !x.IsProcessed) .OrderBy(x => x.Id) .Take(BatchSize) .ToListAsync(); if (!batch.Any()) { Console.WriteLine("✅ 没有更多数据需要处理"); break; } Console.WriteLine($" 📦 本批加载了 {batch.Count} 条记录"); // 处理这批数据 await ProcessBatch(batch, batchNumber); processed += batch.Count; batchNumber++; Console.WriteLine($" ✅ 已处理 {processed}/{totalCount} 条记录 ({(double)processed / totalCount * 100:F1}%)"); // 清理DbContext避免内存累积 _context.ChangeTracker.Clear(); Console.WriteLine(" 🧹 已清理DbContext缓存"); await Task.Delay(100); } Console.WriteLine($"\n🎉 批处理完成!总共处理了 {processed} 条记录"); } // 处理单个批次 private async Task ProcessBatch(List<LargeEntity> batch, int batchNumber) { Console.WriteLine($" 🔄 开始处理第 {batchNumber} 批的 {batch.Count} 条记录..."); var startTime = DateTime.Now; foreach (var item in batch) { // 模拟业务处理逻辑 item.IsProcessed = true; item.ProcessedDate = DateTime.Now; item.Data = $"已处理_{item.Data}"; } await _context.SaveChangesAsync(); var duration = DateTime.Now - startTime; Console.WriteLine($" 💾 第 {batchNumber} 批保存完成,耗时: {duration.TotalMilliseconds:F0}ms"); } // ✅ 优化批处理:只更新ID,减少数据传输 public async Task ProcessLargeDataSetOptimized() { Console.WriteLine("\n🚀 使用优化批处理方法..."); var batchNumber = 1; var totalProcessed = 0; while (true) { Console.WriteLine($"\n⚡ 执行第 {batchNumber} 批优化处理..."); // 只获取需要处理的ID列表,减少数据传输 var idsToProcess = await _context.LargeTable .Where(x => !x.IsProcessed) .OrderBy(x => x.Id) .Take(BatchSize) .Select(x => x.Id) // 只选择ID .ToListAsync(); if (!idsToProcess.Any()) { Console.WriteLine("✅ 没有更多数据需要处理"); break; } Console.WriteLine($" 📋 获取了 {idsToProcess.Count} 个待处理ID"); // 批量更新这些记录 var entitiesToUpdate = await _context.LargeTable .Where(x => idsToProcess.Contains(x.Id)) .ToListAsync(); var startTime = DateTime.Now; foreach (var entity in entitiesToUpdate) { entity.IsProcessed = true; entity.ProcessedDate = DateTime.Now; entity.Data = $"已处理_{entity.Data}"; } await _context.SaveChangesAsync(); var duration = DateTime.Now - startTime; totalProcessed += entitiesToUpdate.Count; Console.WriteLine($" ✅ 第 {batchNumber} 批处理完成:{entitiesToUpdate.Count} 条记录,耗时: {duration.TotalMilliseconds:F0}ms"); batchNumber++; _context.ChangeTracker.Clear(); await Task.Delay(50); } Console.WriteLine($"\n🎉 优化批处理完成!总共处理了 {totalProcessed} 条记录"); } // ✅ 使用纯EF Core方式的批处理(兼容InMemory数据库) public async Task ProcessLargeDataSetWithEfCore() { Console.WriteLine("\n🚀 使用EF Core批量处理方法..."); var batchNumber = 1; var totalProcessed = 0; while (true) { Console.WriteLine($"\n⚡ 执行第 {batchNumber} 批EF Core处理..."); // 使用纯EF Core方式进行批量处理 var entitiesToProcess = await _context.LargeTable .Where(x => !x.IsProcessed) .OrderBy(x => x.Id) .Take(BatchSize) .ToListAsync(); if (!entitiesToProcess.Any()) { Console.WriteLine("✅ 没有更多数据需要处理"); break; } Console.WriteLine($" 📦 本批处理 {entitiesToProcess.Count} 条记录"); var startTime = DateTime.Now; // 批量更新 foreach (var entity in entitiesToProcess) { entity.IsProcessed = true; entity.ProcessedDate = DateTime.Now; entity.Data = $"已处理_{entity.Data}"; } // 批量保存 var affectedRows = await _context.SaveChangesAsync(); var duration = DateTime.Now - startTime; totalProcessed += affectedRows; Console.WriteLine($" ✅ 第 {batchNumber} 批处理完成:{affectedRows} 条记录,耗时: {duration.TotalMilliseconds:F0}ms"); batchNumber++; _context.ChangeTracker.Clear(); await Task.Delay(50); } Console.WriteLine($"\n🎉 EF Core批处理完成!总共处理了 {totalProcessed} 条记录"); } // ✅ 分页式批处理(更稳定的方法) public async Task ProcessLargeDataSetPaginated() { Console.WriteLine("\n🚀 使用分页式批处理方法..."); var totalCount = await _context.LargeTable.Where(x => !x.IsProcessed).CountAsync(); Console.WriteLine($"📊 总共需要处理 {totalCount} 条记录"); var totalPages = (int)Math.Ceiling((double)totalCount / BatchSize); var totalProcessed = 0; for (int page = 0; page < totalPages; page++) { Console.WriteLine($"\n⚡ 处理第 {page + 1}/{totalPages} 页..."); var entities = await _context.LargeTable .Where(x => !x.IsProcessed) .OrderBy(x => x.Id) .Skip(page * BatchSize) .Take(BatchSize) .ToListAsync(); if (!entities.Any()) { Console.WriteLine("✅ 当前页没有数据需要处理"); continue; } Console.WriteLine($" 📦 加载了 {entities.Count} 条记录"); var startTime = DateTime.Now; foreach (var entity in entities) { entity.IsProcessed = true; entity.ProcessedDate = DateTime.Now; entity.Data = $"已处理_{entity.Data}"; } var affectedRows = await _context.SaveChangesAsync(); totalProcessed += affectedRows; var duration = DateTime.Now - startTime; Console.WriteLine($" ✅ 第 {page + 1} 页处理完成:{affectedRows} 条记录,耗时: {duration.TotalMilliseconds:F0}ms"); Console.WriteLine($" 📈 总进度:{totalProcessed}/{totalCount} ({(double)totalProcessed / totalCount * 100:F1}%)"); _context.ChangeTracker.Clear(); await Task.Delay(50); } Console.WriteLine($"\n🎉 分页批处理完成!总共处理了 {totalProcessed} 条记录"); } // 📈 统计处理结果 public async Task ShowProcessingStats() { var total = await _context.LargeTable.CountAsync(); var processed = await _context.LargeTable.CountAsync(x => x.IsProcessed); var unprocessed = total - processed; Console.WriteLine($"\n📊 处理统计:"); Console.WriteLine($" 总记录数: {total}"); Console.WriteLine($" 已处理: {processed}"); Console.WriteLine($" 未处理: {unprocessed}"); Console.WriteLine($" 完成率: {(double)processed / total * 100:F1}%"); } // 🔄 重置所有数据为未处理状态 public async Task ResetAllData() { Console.WriteLine("🔄 重置所有数据为未处理状态..."); // 分批重置,避免一次性加载太多数据 var resetCount = 0; const int resetBatchSize = 2000; while (true) { var batch = await _context.LargeTable .Where(x => x.IsProcessed) .Take(resetBatchSize) .ToListAsync(); if (!batch.Any()) break; foreach (var entity in batch) { entity.IsProcessed = false; entity.ProcessedDate = null; entity.Data = entity.Data.Replace("已处理_", ""); } await _context.SaveChangesAsync(); resetCount += batch.Count; _context.ChangeTracker.Clear(); Console.WriteLine($" 已重置 {resetCount} 条记录..."); } Console.WriteLine($"✅ 总共重置了 {resetCount} 条记录"); } }

image.png

🎯 核心要点总结

通过本文的深入分析,我们可以总结出三个关键要点:

  1. 选择原则:数据库查询优先使用 IQueryable,内存操作使用 IEnumerable
  2. 性能意识:时刻关注查询的执行位置,避免不必要的数据传输和内存消耗
  3. 最佳实践:合理使用投影、分批处理、延迟执行等技术提升应用性能

掌握这些技巧不仅能显著提升你的应用性能,更能让你在面对复杂数据查询需求时游刃有余。记住,好的性能不是偶然,而是对技术细节的深度理解和精心设计的结果。


💭 互动讨论:你在项目中遇到过哪些 IQueryableIEnumerable 的性能问题?有什么独特的解决方案吗?

🔥 分享价值:如果这篇文章帮你解决了性能问题,请转发给更多需要的同行!

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!