LINQ 高级查询与性能
大约 13 分钟约 3850 字
LINQ 高级查询与性能
简介
LINQ(Language Integrated Query)是 C# 内置的数据查询语言,支持对集合、数据库、XML 等数据源进行统一查询。掌握 LINQ 高级操作(分组、联接、分区、聚合)和性能优化技巧,是写出高效数据处理代码的关键。
LINQ 分为两大类:LINQ to Objects(操作内存中的 IEnumerable<T>)和 LINQ to Entities/SQL(操作 IQueryable<T>,翻译为 SQL)。两者的执行机制完全不同,优化策略也截然不同。理解延迟执行、表达式树翻译和查询计划是掌握 LINQ 的核心。
特点
延迟执行详解
理解迭代器与延迟执行
// ==========================================
// LINQ 的延迟执行本质
// ==========================================
// LINQ to Objects 基于 IEnumerable<T> 的迭代器模式
// 查询不是立即执行的,而是在 foreach 时才"拉动"数据
// 示例 — 观察延迟执行
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 定义查询(不执行!)
var query = numbers.Where(n =>
{
Console.WriteLine($"过滤: {n}");
return n > 2;
});
Console.WriteLine("查询已定义,开始迭代");
// 迭代时才执行
foreach (var n in query)
{
Console.WriteLine($"结果: {n}");
}
// 输出:
// 查询已定义,开始迭代
// 过滤: 1
// 过滤: 2
// 过滤: 3 → 结果: 3
// 过滤: 4 → 结果: 4
// 过滤: 5 → 结果: 5
// ==========================================
// 延迟执行的陷阱
// ==========================================
// 陷阱 1: 修改数据源后查询结果改变
var list = new List<int> { 1, 2, 3 };
var query = list.Where(x => x > 1); // 延迟执行
list.Add(4); // 修改数据源
Console.WriteLine(query.Count()); // 3(包含 4!)
// 解决: 立即物化
var materialized = list.Where(x => x > 1).ToList();
list.Add(5);
Console.WriteLine(materialized.Count); // 3(不受影响)
// 陷阱 2: 多次枚举导致重复计算
var expensiveQuery = items
.Where(x => ExpensiveFilter(x)) // 每次枚举都执行
.Select(x => ExpensiveTransform(x));
var count = expensiveQuery.Count(); // 第一次遍历
var first = expensiveQuery.First(); // 第二次遍历!
var list2 = expensiveQuery.ToList(); // 第三次遍历!
// 解决: 物化一次
var cached = items
.Where(x => ExpensiveFilter(x))
.Select(x => ExpensiveTransform(x))
.ToList(); // 只遍历一次
// 陷阱 3: 闭包捕获循环变量
var funcs = new List<Func<int>>();
for (int i = 0; i < 5; i++)
funcs.Add(() => i); // 闭包捕获的是变量 i,不是值
// C# 5+ 已修复 foreach 的闭包问题
var actions = new List<Action>();
foreach (var item in items)
actions.Add(() => Console.WriteLine(item)); // 安全
// 但 for 循环仍需注意
var funcs2 = new List<Func<int>>();
for (int i = 0; i < 5; i++)
{
int local = i; // 创建局部变量
funcs2.Add(() => local); // 安全
}高级查询操作
分组与聚合
/// <summary>
/// GroupBy + 聚合函数
/// </summary>
var orders = new List<Order> { /* ... */ };
// 按类别分组统计
var categoryStats = orders
.GroupBy(o => o.Category)
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
TotalAmount = g.Sum(o => o.Amount),
AvgAmount = g.Average(o => o.Amount),
MaxOrder = g.MaxBy(o => o.Amount)
});
// 多字段分组
var multiGroup = orders
.GroupBy(o => new { o.Category, o.Year })
.Select(g => new { g.Key.Category, g.Key.Year, Total = g.Sum(o => o.Amount) });
// 分桶统计(价格区间)
var priceRanges = products
.GroupBy(p => p.Price switch
{
< 100 => "低价",
< 500 => "中价",
_ => "高价"
});
// ==========================================
// 分组后的进一步操作
// ==========================================
// 每组取前 N 条
var topPerCategory = orders
.GroupBy(o => o.Category)
.Select(g => g.OrderByDescending(o => o.Amount).Take(3));
// 分组后过滤(Having 等价)
var bigCategories = orders
.GroupBy(o => o.Category)
.Where(g => g.Count() > 10 && g.Sum(o => o.Amount) > 10000)
.Select(g => new { Category = g.Key, OrderCount = g.Count() });
// 分组后投影为字典
var categoryDict = orders
.GroupBy(o => o.Category)
.ToDictionary(
g => g.Key,
g => g.OrderByDescending(o => o.Amount).First());
// ToLookup — 一键多值字典
var ordersByCustomer = orders.ToLookup(o => o.CustomerId);
foreach (var group in ordersByCustomer)
{
Console.WriteLine($"客户 {group.Key}: {group.Count()} 个订单");
}联接操作
/// <summary>
/// Join / GroupJoin / Zip
/// </summary>
// 内连接
var result = users.Join(orders,
user => user.Id,
order => order.UserId,
(user, order) => new { user.Name, order.Amount });
// 左连接(GroupJoin + SelectMany)
var leftJoin = users.GroupJoin(orders,
user => user.Id,
order => order.UserId,
(user, orderList) => new
{
user.Name,
Orders = orderList.DefaultIfEmpty()
})
.SelectMany(x => x.Orders.Select(o => new
{
x.Name,
Amount = o?.Amount ?? 0
}));
// Zip — 按位置合并两个序列
var names = new[] { "张三", "李四", "王五" };
var scores = new[] { 90, 85, 92 };
var combined = names.Zip(scores, (name, score) => $"{name}: {score}分");
// ==========================================
// 联接的高级用法
// ==========================================
// 多键联接
var result2 = orders.Join(products,
order => new { order.Category, order.ProductId },
product => new { product.Category, product.Id },
(order, product) => new { order.OrderId, product.Name });
// 交叉联接(笛卡尔积)
var crossJoin = colors.SelectMany(
c => sizes,
(color, size) => new { Color = color, Size = size });
// 自联接(树形结构查询)
var treeNodes = new List<TreeNode>();
var children = treeNodes.Join(treeNodes,
parent => parent.Id,
child => child.ParentId,
(parent, child) => new { Parent = parent.Name, Child = child.Name });
// 左连接的简洁写法
var leftJoinSimple = from user in users
join order in orders on user.Id equals order.UserId into orderGroup
from o in orderGroup.DefaultIfEmpty()
select new { user.Name, OrderAmount = o?.Amount ?? 0 };分区与排序
/// <summary>
/// Skip/Take 分页,OrderBy 排序
/// </summary>
// 分页查询
var page = products
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.Name)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.ToList();
// 多条件动态排序
IQueryable<Product> query = dbContext.Products;
query = sortBy switch
{
"price" => desc ? query.OrderByDescending(p => p.Price) : query.OrderBy(p => p.Price),
"name" => desc ? query.OrderByDescending(p => p.Name) : query.OrderBy(p => p.Name),
_ => query.OrderBy(p => p.Id)
};
// Top N
var top3 = scores.OrderByDescending(s => s.Value).Take(3);
// 去重
var uniqueNames = users.Select(u => u.City).Distinct().OrderBy(c => c);
// ==========================================
// 分区高级用法
// ==========================================
// 分块处理 — 每批处理 N 条
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int size)
{
while (source.Any())
{
yield return source.Take(size);
source = source.Skip(size);
}
}
// .NET 6+ 内置 Chunk
var batches = items.Chunk(100);
foreach (var batch in batches)
{
await ProcessBatchAsync(batch);
}
// TakeLast / SkipLast (.NET 6+)
var last5 = items.TakeLast(5);
var exceptLast3 = items.SkipLast(3);
// 滑动窗口
public static IEnumerable<IEnumerable<T>> SlidingWindow<T>(
IEnumerable<T> source, int windowSize)
{
return source.Zip(
source.Skip(windowSize - 1),
(first, last) => source.Take(windowSize));
}集合操作
/// <summary>
/// 集合运算:Union, Intersect, Except
/// </summary>
var team1 = new[] { 1, 2, 3, 4, 5 };
var team2 = new[] { 4, 5, 6, 7, 8 };
var all = team1.Union(team2); // { 1,2,3,4,5,6,7,8 }
var common = team1.Intersect(team2); // { 4,5 }
var diff = team1.Except(team2); // { 1,2,3 }
// 按属性去重
var unique = products
.GroupBy(p => p.SKU)
.Select(g => g.First());
// SequenceEqual — 比较两个序列
bool same = list1.SequenceEqual(list2);
// ==========================================
// 自定义比较器的集合操作
// ==========================================
var set1 = new[] { "apple", "BANANA", "cherry" };
var set2 = new[] { "Apple", "banana", "DATE" };
// 忽略大小写的并集
var unionIgnoreCase = set1.Union(set2, StringComparer.OrdinalIgnoreCase);
// ["apple", "BANANA", "cherry", "DATE"]
// Concat vs Union
// Concat: 保留重复元素
var concat = set1.Concat(set2);
// ["apple", "BANANA", "cherry", "Apple", "banana", "DATE"]
// Union: 去重
var union = set1.Union(set2);
// ["apple", "BANANA", "cherry", "Apple", "banana", "DATE"]性能优化
延迟执行 vs 立即执行
/// <summary>
/// 理解延迟执行
/// </summary>
// 延迟执行 — 多次迭代会多次计算
var filtered = products.Where(p => p.Price > 100); // 此时没有执行
var count = filtered.Count(); // 第一次遍历
var list = filtered.ToList(); // 第二次遍历!
// 正确做法 — 先物化再使用
var filteredList = products.Where(p => p.Price > 100).ToList(); // 一次遍历
var count2 = filteredList.Count;
var list2 = filteredList; // 不再遍历
// 常见陷阱:在循环中多次查询
foreach (var id in ids)
{
var user = users.FirstOrDefault(u => u.Id == id); // O(n) 每次遍历
}
// 优化:先建索引
var userDict = users.ToDictionary(u => u.Id); // O(n) 一次
foreach (var id in ids)
{
_ = userDict.TryGetValue(id, out var user); // O(1)
}LINQ 性能优化清单
// ==========================================
// 1. 避免 Where + ElementAt 的 O(n^2)
// ==========================================
// 不好: 先 Where 再 FirstOrDefault → O(n)
var item = list.Where(x => x.Id == targetId).FirstOrDefault();
// 好: 直接 FirstOrDefault → O(n) 但只遍历一次
var item2 = list.FirstOrDefault(x => x.Id == targetId);
// ==========================================
// 2. Any 优于 Count
// ==========================================
// 不好: Count() 遍历整个序列
if (list.Count(x => x.IsActive) > 0) { /* ... */ }
// 好: Any() 找到第一个就返回
if (list.Any(x => x.IsActive)) { /* ... */ }
// ==========================================
// 3. 避免在 Where 中使用方法调用
// ==========================================
// 不好: 每次迭代都调用 GetHashCode(假设方法有开销)
var filtered = items.Where(x => x.ComputeHash() > 1000);
// 好: 先计算再过滤
var precomputed = items.Select(x => new { Item = x, Hash = x.ComputeHash() })
.Where(x => x.Hash > 1000)
.Select(x => x.Item);
// ==========================================
// 4. 避免不必要的 Select 投影
// ==========================================
// 如果只需要 Count,不要先 Select 再 Count
// 不好
var count = items.Select(x => x.Id).Distinct().Count();
// 好(效果相同但更直观)
var count2 = items.Select(x => x.Id).Distinct().Count();
// ==========================================
// 5. 使用 Span/Ranges 替代 LINQ(热路径)
// ==========================================
// 对于数组操作,直接使用 for 循环比 LINQ 快
var arr = new int[10000];
// LINQ 版本(慢)
var sum = arr.Where(x => x > 0).Sum();
// 手动循环版本(快 2-5 倍)
int sum2 = 0;
foreach (var x in arr)
{
if (x > 0) sum2 += x;
}
// Span 版本(最快)
int sum3 = 0;
var span = arr.AsSpan();
for (int i = 0; i < span.Length; i++)
{
if (span[i] > 0) sum3 += span[i];
}EF Core LINQ 优化
// ==========================================
// LINQ to Entities 的性能注意事项
// ==========================================
// 1. 只 Select 需要的字段
// 不好: 查询整个实体
var users = dbContext.Users.ToList(); // SELECT *
// 好: 只查询需要的字段
var names = dbContext.Users
.Where(u => u.IsActive)
.Select(u => new { u.Id, u.Name })
.ToList(); // SELECT Id, Name FROM Users WHERE IsActive = 1
// 2. 避免 N+1 查询
// 不好: 每个订单都查一次用户
var orders = dbContext.Orders.ToList();
foreach (var order in orders)
{
var user = dbContext.Users.Find(order.UserId); // N 次查询!
}
// 好: 一次查询加载关联数据
var ordersWithUsers = dbContext.Orders
.Include(o => o.User)
.ToList();
// 更好: 投影,不加载完整实体
var orderDtos = dbContext.Orders
.Select(o => new OrderDto
{
OrderId = o.Id,
CustomerName = o.User.Name,
Total = o.TotalAmount
})
.ToList();
// 3. 避免 Client-side Evaluation
// 不好: 在客户端执行过滤
var filtered = dbContext.Orders
.Where(o => MyCustomFunction(o.Amount)) // 在客户端执行!
.ToList();
// 好: 确保过滤在数据库执行
var filtered2 = dbContext.Orders
.Where(o => o.Amount > 1000)
.ToList();
// 4. Split Queries (.NET 5+)
var ordersWithDetails = dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery() // 避免笛卡尔积爆炸
.ToList();
// 5. 使用 AsNoTracking 提高只读查询性能
var readOnly = dbContext.Users
.AsNoTracking()
.Where(u => u.IsActive)
.ToList();PLINQ 并行查询
/// <summary>
/// PLINQ — 并行处理 CPU 密集型查询
/// </summary>
// 简单并行
var results = data.AsParallel()
.Where(x => ExpensiveFilter(x))
.Select(x => Transform(x))
.ToList();
// 控制并行度
var results2 = data.AsParallel()
.WithDegreeOfParallelism(4)
.WithCancellation(cancellationToken)
.Select(x => HeavyComputation(x))
.OrderBy(x => x.Id)
.ToList();
// ForAll — 无序并行处理
data.AsParallel()
.Where(x => x.IsValid)
.ForAll(x => Process(x)); // 不保证顺序
// ==========================================
// PLINQ 的注意事项
// ==========================================
// 1. PLINQ 只适合 CPU 密集型操作
// 2. 数据量小时 PLINQ 的调度开销反而更大(建议 > 1000 条)
// 3. PLINQ 不保证顺序,除非使用 AsOrdered()
// 4. PLINQ 的异常会被包装为 AggregateException
// 5. 带有副作用的操作不适合 PLINQ
// AsOrdered — 保持原始顺序
var orderedResults = data.AsParallel()
.AsOrdered()
.Where(x => ExpensiveFilter(x))
.Select(x => Transform(x))
.ToList(); // 结果顺序与原始顺序一致
// ==========================================
// PLINQ vs Parallel.ForEach vs Task.WhenAll
// ==========================================
// PLINQ: 适合查询/转换/过滤(有返回值)
// Parallel.ForEach: 适合执行操作(无返回值)
// Task.WhenAll: 适合异步 IO 操作实际应用 — 规格化查询构建器
/// <summary>
/// 动态查询构建器 — 根据条件组合查询
/// </summary>
public class QueryBuilder<T> where T : class
{
private IQueryable<T> _query;
private readonly List<Expression<Func<T, bool>>> _filters = new();
private Expression<Func<T, object>>? _orderBy;
private bool _orderDescending;
private int _skip;
private int _take;
public QueryBuilder(IQueryable<T> query) => _query = query;
public QueryBuilder<T> Where(Expression<Func<T, bool>> predicate)
{
_filters.Add(predicate);
return this;
}
public QueryBuilder<T> OrderBy(
Expression<Func<T, object>> keySelector,
bool descending = false)
{
_orderBy = keySelector;
_orderDescending = descending;
return this;
}
public QueryBuilder<T> Paginate(int page, int pageSize)
{
_skip = (page - 1) * pageSize;
_take = pageSize;
return this;
}
public IQueryable<T> Build()
{
foreach (var filter in _filters)
_query = _query.Where(filter);
if (_orderBy != null)
_query = _orderDescending
? _query.OrderByDescending(_orderBy)
: _query.OrderBy(_orderBy);
if (_take > 0)
_query = _query.Skip(_skip).Take(_take);
return _query;
}
}
// 使用
var result = new QueryBuilder<Product>(dbContext.Products)
.Where(p => p.IsActive)
.Where(p => p.Price > 100)
.OrderBy(p => p.CreatedAt, descending: true)
.Paginate(page: 1, pageSize: 20)
.Build()
.ToList();优点
缺点
总结
LINQ 高级操作的核心:GroupBy 分组聚合、Join/GroupJoin 关联、Skip/Take 分页、Distinct 去重。性能关键:理解延迟执行、避免重复遍历、大集合先建字典索引。CPU 密集场景用 PLINQ 并行加速。简单查询用方法语法,复杂查询可读性优先。
核心原则:
- 延迟执行是双刃剑 — 理解何时执行,避免重复遍历
- 物化要及时 — 多次使用的查询先 ToList()
- Select 只需要的字段 — 特别是数据库查询
- Any 优于 Count — 检查是否存在不需要遍历全部
- 热路径用 for 循环 — 性能敏感场景避免 LINQ
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道"为什么这样写"和"在什么边界下不能这样写"。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 用 Count() > 0 判断是否存在元素(应用 Any())。
- 在 foreach 中 LINQ 查询导致闭包捕获问题。
- 忘记 AsNoTracking 导致 EF Core 只读查询性能差。
- N+1 查询问题未使用 Include 或投影。
- PLINQ 用于 IO 密集型操作(应用异步)。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 学习
IAsyncEnumerable<T>的 LINQ 扩展。 - 研究表达式树(Expression Tree)的构建和翻译。
适用场景
- 当你准备把《LINQ 高级查询与性能》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
- 用 EF Core 的日志输出查看实际生成的 SQL。
- 检查是否存在 N+1 查询和客户端评估。
复盘问题
- 如果把《LINQ 高级查询与性能》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《LINQ 高级查询与性能》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《LINQ 高级查询与性能》最大的收益和代价分别是什么?
