Appearance
LINQ 数据查询技术实践指南
LINQ (Language Integrated Query) 是 .NET 中用于查询数据的强大工具。它将查询功能直接集成到 C# 语言中,使得查询代码更加简洁、安全且易于维护。只要是实现了 IEnumerable<T> 或 IQueryable<T> 的对象,都可以使用 LINQ 进行查询。
1. 基础语法与核心关键字
LINQ 提供两种语法风格:查询表达式语法 (Query Syntax) 和 方法语法 (Method Syntax)。
1.1 核心关键字
在 LINQ 中,核心关键字的作用与 SQL 类似,但顺序有所不同:
from: 指定数据源和范围变量。where: 筛选条件。select: 指定返回的结果格式。group...by...into: 分组并将结果推入新范围。join: 关联查询。orderby: 排序。let: 引入临时变量。
1.2 查询语法示例
csharp
var query = from p in products
where p.Price > 10
orderby p.Name
select new { p.Name, p.Price };2. 关联查询 (Join) 实战
2.1 多表内连接 (Inner Join)
当需要关联三个及以上的表时,查询表达式语法比 Lambda 链式调用更直观:
csharp
var query = from user in Users
join userRole in UserRoles on user.Id equals userRole.UserId
join role in Roles on userRole.RoleId equals role.Id
join pr in PermissionRoles on role.Id equals pr.Role_Id
select new {
user.FullName,
RoleName = role.Name,
Permission = pr.Permission.Name
};2.2 链式左外连接 (Left Outer Join)
实现左连接的关键是 into 配合 DefaultIfEmpty()。如果需要连续左连多张表,可以按如下方式链式操作:
csharp
var query = from member in Members
join order in Orders on member.Id equals order.MemberId into gj1
from subOrder in gj1.DefaultIfEmpty()
join detail in OrderDetails on subOrder.Id equals detail.OrderId into gj2
from subDetail in gj2.DefaultIfEmpty()
select new {
member.Name,
OrderInfo = subOrder,
DetailInfo = subDetail
};2.4 集合扁平化 (SelectMany)
当一个对象包含集合属性,且需要将所有子集合展开为一个平坦序列时使用:
csharp
// 查询所有订单的明细项,并扁平化显示
var allDetails = orders.SelectMany(o => o.OrderDetails);
// 带过滤的扁平化
var filteredDetails = from o in orders
from d in o.OrderDetails
where d.Quantity > 5
select new { o.OrderNo, d.ProductName };3. 分组查询 (Group By) 高级场景
分组查询返回的是一个 IGrouping<K, T> 集合,其中包含 Key 和对应的元素列表。
3.1 常见聚合操作
- 基础统计:
group p by p.CategoryID into g后直接 select。 - 最大/最小值:
MaxPrice = g.Max(p => p.UnitPrice)。 - 平均值与求和:
AveragePrice = g.Average(p => p.UnitPrice)。 - Where 过滤组:在
into g之后使用where g.Count() > 10(相当于 SQL 的 HAVING)。
3.2 多列分组 (Multiple Columns)
通过匿名类实现多字段分组:
csharp
var groups = from p in products
group p by new { p.CategoryID, p.SupplierID } into g
select new {
g.Key.CategoryID,
g.Key.SupplierID,
Count = g.Count()
};3.3 表达式分组 (Expression Grouping)
按布尔表达式的结果(如“是否贵价”)进行分类:
csharp
var groups = from p in products
group p by new { IsExpensive = p.UnitPrice > 100 } into g
select g;4. 实用技巧与常见问题
4.1 Let 关键字与临时变量
let 允许在查询中存储子表达式的结果,避免重复计算:
csharp
var query = from p in products
let totalStockValue = p.UnitPrice * p.UnitsInStock
where totalStockValue > 500
select new { p.Name, Value = totalStockValue };4.2 Into 关键字与查询延续
into 可以将查询结果暂时存放,用于后续的进一步过滤或分组:
csharp
var query = from p in products
group p by p.CategoryID into g
where g.Count() > 5
select new { CategoryID = g.Key, ProductCount = g.Count() };4.3 查询数组中出现次数最多的数字
csharp
int[] values = { 1, 2, 5, 2, 3, 5, 5, 3, 4, 6, 3, 3 };
var mostFrequent = values.GroupBy(v => v)
.Select(g => new { g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count)
.First().Key;4.2 Any() vs Count()
- Any():只要找到第一个匹配项就返回,翻译为 SQL 的
EXISTS,性能极高。 - Count():会遍历整个序列,如果是数据库查询,会执行
COUNT(*),开销较大。 - 原则:仅判断是否存在时,永远优先使用
Any()。
5. 性能优化与注意事项
- 延迟执行 (Deferred Execution):LINQ 查询在
foreach迭代或调用ToList()、Count()等立即执行函数前,不会真正去数据库取数。 - IQueryable 与 IEnumerable 的边界:
- 在数据库操作中,务必在
IQueryable阶段完成所有的Where和Join过滤。 - 一旦调用
AsEnumerable()或ToList(),后续的操作将在内存中进行,可能导致全表扫描。
- 在数据库操作中,务必在
- 避免 N+1 查询:使用导航属性时,配合
.Include(x => x.Relation)进行预加载。
6. 为什么 LINQ 优于原生 SQL?
在 .NET 开发中,虽然 SQL 很强大,但 LINQ 提供了以下不可替代的优势:
- 强类型检查:在编译期即可发现字段名错误或类型不匹配,而 SQL 错误只能在运行时发现。
- 智能提示 (IntelliSense):编码时可实时查看字段和方法,极大提高效率。
- 可组合性:可以通过传递
IQueryable对象,在不同的业务层动态添加Where条件,实现极其灵活的查询构建。 - 安全性:天然防御 SQL 注入攻击。
- 多数据源统一:无论是 SQL Server、MySQL、XML 还是内存集合,语法高度一致。
