文章目录
引言
LINQ(Language Integrated Query,语言集成查询)作为.NET平台的核心特性,提供了一种统一、直观且强大的方式来查询和操作各种数据源。在掌握了LINQ的基础语法后,我们需要深入了解其基本查询操作,这些操作构成了LINQ日常使用的基础。
本文将详细介绍LINQ中最常用的五种基本查询操作:过滤、投影、排序、分组和连接。无论您是处理内存中的集合、数据库表还是XML文档,这些操作都是必不可少的。通过本文的学习,您将能够熟练运用这些操作来实现各种数据查询需求。
为了使示例更加实用,我们将使用一个模拟电子商城的产品数据集合来演示各种查询操作。首先,让我们定义数据模型:
// 产品类 - 表示电子商城中的商品
public class Product
{
public int Id { get; set; } // 产品唯一标识
public string Name { get; set; } // 产品名称
public string Category { get; set; } // 产品类别
public decimal Price { get; set; } // 产品价格
public int Stock { get; set; } // 库存数量
public DateTime LaunchDate { get; set; } // 上市日期
public string[] Tags { get; set; } // 产品标签
public Supplier Supplier { get; set; } // 供应商
}
// 供应商类 - 表示产品的供应商
public class Supplier
{
public int Id { get; set; } // 供应商唯一标识
public string Name { get; set; } // 供应商名称
public string Country { get; set; } // 供应商所在国家
public int Rating { get; set; } // 供应商评级(1-5)
}
// 订单类 - 表示客户订单
public class Order
{
public int Id { get; set; } // 订单唯一标识
public int ProductId { get; set; } // 产品ID
public int Quantity { get; set; } // 购买数量
public DateTime OrderDate { get; set; } // 订单日期
public string CustomerName { get; set; } // 客户名称
}
接下来,我们初始化一些示例数据:
// 初始化供应商数据
List<Supplier> suppliers = new List<Supplier>
{
new Supplier { Id = 1, Name = "科技电子", Country = "中国", Rating = 4 },
new Supplier { Id = 2, Name = "环球数码", Country = "美国", Rating = 5 },
new Supplier { Id = 3, Name = "欧联电器", Country = "德国", Rating = 4 },
new Supplier { Id = 4, Name = "东方科技", Country = "日本", Rating = 3 },
new Supplier { Id = 5, Name = "南方电子", Country = "中国", Rating = 4 }
};
// 初始化产品数据
List<Product> products = new List<Product>
{
new Product {
Id = 1,
Name = "超薄笔记本电脑",
Category = "电脑",
Price = 6999.00M,
Stock = 25,
LaunchDate = new DateTime(2022, 3, 15),
Tags = new[] { "轻薄", "高性能", "商务" },
Supplier = suppliers[1] // 环球数码
},
new Product {
Id = 2,
Name = "专业游戏鼠标",
Category = "配件",
Price = 299.00M,
Stock = 150,
LaunchDate = new DateTime(2022, 5, 20),
Tags = new[] { "游戏", "高精度" },
Supplier = suppliers[0] // 科技电子
},
new Product {
Id = 3,
Name = "智能手机X1",
Category = "手机",
Price = 4999.00M,
Stock = 50,
LaunchDate = new DateTime(2022, 1, 10),
Tags = new[] { "5G", "高清拍摄", "快充" },
Supplier = suppliers[3] // 东方科技
},
new Product {
Id = 4,
Name = "机械键盘",
Category = "配件",
Price = 499.00M,
Stock = 100,
LaunchDate = new DateTime(2021, 11, 5),
Tags = new[] { "机械轴", "背光", "游戏" },
Supplier = suppliers[0] // 科技电子
},
new Product {
Id = 5,
Name = "4K高清显示器",
Category = "显示设备",
Price = 2499.00M,
Stock = 30,
LaunchDate = new DateTime(2022, 2, 25),
Tags = new[] { "4K", "HDR", "广色域" },
Supplier = suppliers[2] // 欧联电器
},
new Product {
Id = 6,
Name = "游戏主机Pro",
Category = "游戏设备",
Price = 3799.00M,
Stock = 20,
LaunchDate = new DateTime(2021, 12, 15),
Tags = new[] { "4K游戏", "高性能", "大容量" },
Supplier = suppliers[1] // 环球数码
},
new Product {
Id = 7,
Name = "无线蓝牙耳机",
Category = "音频设备",
Price = 799.00M,
Stock = 200,
LaunchDate = new DateTime(2022, 4, 10),
Tags = new[] { "无线", "降噪", "长续航" },
Supplier = suppliers[4] // 南方电子
},
new Product {
Id = 8,
Name = "智能手表",
Category = "穿戴设备",
Price = 1299.00M,
Stock = 75,
LaunchDate = new DateTime(2022, 3, 5),
Tags = new[] { "健康监测", "运动", "防水" },
Supplier = suppliers[3] // 东方科技
},
new Product {
Id = 9,
Name = "平板电脑",
Category = "电脑",
Price = 3499.00M,
Stock = 40,
LaunchDate = new DateTime(2022, 1, 20),
Tags = new[] { "轻薄", "触控笔", "高清屏幕" },
Supplier = suppliers[1] // 环球数码
},
new Product {
Id = 10,
Name = "便携式移动电源",
Category = "配件",
Price = 199.00M,
Stock = 300,
LaunchDate = new DateTime(2021, 10, 10),
Tags = new[] { "大容量", "快充", "小巧" },
Supplier = suppliers[4] // 南方电子
}
};
// 初始化订单数据
List<Order> orders = new List<Order>
{
new Order { Id = 1, ProductId = 1, Quantity = 2, OrderDate = new DateTime(2022, 4, 5), CustomerName = "张先生" },
new Order { Id = 2, ProductId = 3, Quantity = 1, OrderDate = new DateTime(2022, 4, 7), CustomerName = "李女士" },
new Order { Id = 3, ProductId = 5, Quantity = 1, OrderDate = new DateTime(2022, 4, 8), CustomerName = "王先生" },
new Order { Id = 4, ProductId = 2, Quantity = 3, OrderDate = new DateTime(2022, 4, 10), CustomerName = "赵女士" },
new Order { Id = 5, ProductId = 7, Quantity = 2, OrderDate = new DateTime(2022, 4, 12), CustomerName = "陈先生" },
new Order { Id = 6, ProductId = 3, Quantity = 1, OrderDate = new DateTime(2022, 4, 15), CustomerName = "林女士" },
new Order { Id = 7, ProductId = 4, Quantity = 2, OrderDate = new DateTime(2022, 4, 18), CustomerName = "黄先生" },
new Order { Id = 8, ProductId = 6, Quantity = 1, OrderDate = new DateTime(2022, 4, 20), CustomerName = "周女士" },
new Order { Id = 9, ProductId = 9, Quantity = 1, OrderDate = new DateTime(2022, 4, 22), CustomerName = "吴先生" },
new Order { Id = 10, ProductId = 10, Quantity = 5, OrderDate = new DateTime(2022, 4, 25), CustomerName = "郑女士" }
};
有了这些数据,我们就可以开始学习LINQ的基本查询操作了。
1. 过滤(Where)
过滤操作允许我们根据特定条件从数据源中选择元素,相当于SQL中的WHERE子句。在LINQ中,过滤主要通过Where
方法或查询语法中的where
关键字实现。
1.1 基本过滤
// 查找价格超过3000元的产品
// 查询语法
var expensiveProducts = from p in products
where p.Price > 3000
select p;
// 方法语法
var expensiveProducts2 = products.Where(p => p.Price > 3000);
// 输出结果
Console.WriteLine("价格超过3000元的产品:");
foreach (var product in expensiveProducts)
{
Console.WriteLine($"{product.Name} - ¥{product.Price}");
}
1.2 多条件过滤
过滤条件可以组合使用,通过逻辑运算符(&&,||,!)连接多个条件:
// 查找库存充足(大于50)且价格适中(低于1000元)的产品
// 查询语法
var affordableProducts = from p in products
where p.Stock > 50 && p.Price < 1000
select p;
// 方法语法
var affordableProducts2 = products.Where(p => p.Stock > 50 && p.Price < 1000);
// 输出结果
Console.WriteLine("\n库存充足且价格适中的产品:");
foreach (var product in affordableProducts)
{
Console.WriteLine($"{product.Name} - ¥{product.Price} - 库存:{product.Stock}");
}
1.3 嵌套属性过滤
我们可以基于嵌套属性或复杂条件进行过滤:
// 查找中国供应商提供的产品
// 查询语法
var chineseProducts = from p in products
where p.Supplier.Country == "中国"
select p;
// 方法语法
var chineseProducts2 = products.Where(p => p.Supplier.Country == "中国");
// 输出结果
Console.WriteLine("\n中国供应商提供的产品:");
foreach (var product in chineseProducts)
{
Console.WriteLine($"{product.Name} - 供应商:{product.Supplier.Name}");
}
1.4 集合属性过滤
我们也可以根据集合属性中的元素进行过滤:
// 查找标签中包含"游戏"的产品
// 查询语法
var gamingProducts = from p in products
where p.Tags.Contains("游戏")
select p;
// 方法语法
var gamingProducts2 = products.Where(p => p.Tags.Contains("游戏"));
// 输出结果
Console.WriteLine("\n游戏相关产品:");
foreach (var product in gamingProducts)
{
Console.WriteLine($"{product.Name} - 标签:{string.Join(", ", product.Tags)}");
}
1.5 日期过滤
日期类型也可以用于过滤条件:
// 查找2022年第一季度(1-3月)上市的产品
// 查询语法
var q1Products = from p in products
where p.LaunchDate >= new DateTime(2022, 1, 1) &&
p.LaunchDate <= new DateTime(2022, 3, 31)
select p;
// 方法语法
var q1Products2 = products.Where(p => p.LaunchDate >= new DateTime(2022, 1, 1) &&
p.LaunchDate <= new DateTime(2022, 3, 31));
// 输出结果
Console.WriteLine("\n2022年第一季度上市的产品:");
foreach (var product in q1Products)
{
Console.WriteLine($"{product.Name} - 上市日期:{product.LaunchDate.ToString("yyyy-MM-dd")}");
}
过滤操作的本质是对数据源进行条件筛选,返回符合条件的元素子集。在LINQ中,过滤操作支持丰富的条件表达式,可以满足各种复杂的筛选需求。
2. 投影(Select)
投影操作允许我们从数据源中选择特定的属性或转换数据形态,相当于SQL中的SELECT子句。在LINQ中,投影主要通过Select
方法或查询语法中的select
关键字实现。
2.1 基本投影
选择单个属性:
// 获取所有产品名称列表
// 查询语法
var productNames = from p in products
select p.Name;
// 方法语法
var productNames2 = products.Select(p => p.Name);
// 输出结果
Console.WriteLine("\n产品名称列表:");
foreach (var name in productNames)
{
Console.WriteLine(name);
}
2.2 创建匿名类型
选择多个属性并创建新的匿名类型:
// 创建简化的产品信息(只包含名称、价格和库存)
// 查询语法
var productInfos = from p in products
select new { p.Name, p.Price, p.Stock };
// 方法语法
var productInfos2 = products.Select(p => new { p.Name, p.Price, p.Stock });
// 输出结果
Console.WriteLine("\n产品简要信息:");
foreach (var info in productInfos)
{
Console.WriteLine($"{info.Name} - ¥{info.Price} - 库存:{info.Stock}");
}
2.3 数据转换
在投影过程中转换数据:
// 转换产品数据,包括计算销售额(价格 * 库存)
// 查询语法
var salesInfo = from p in products
select new {
p.Name,
p.Price,
p.Stock,
PotentialSales = p.Price * p.Stock
};
// 方法语法
var salesInfo2 = products.Select(p => new {
p.Name,
p.Price,
p.Stock,
PotentialSales = p.Price * p.Stock
});
// 输出结果
Console.WriteLine("\n产品潜在销售额:");
foreach (var info in salesInfo)
{
Console.WriteLine($"{info.Name} - 单价:¥{info.Price} - 库存:{info.Stock} - 潜在销售额:¥{info.PotentialSales}");
}
2.4 嵌套属性投影
投影可以包含嵌套属性:
// 获取产品及其供应商信息
// 查询语法
var productSupplierInfo = from p in products
select new {
ProductName = p.Name,
SupplierName = p.Supplier.Name,
SupplierCountry = p.Supplier.Country
};
// 方法语法
var productSupplierInfo2 = products.Select(p => new {
ProductName = p.Name,
SupplierName = p.Supplier.Name,
SupplierCountry = p.Supplier.Country
});
// 输出结果
Console.WriteLine("\n产品及供应商信息:");
foreach (var info in productSupplierInfo)
{
Console.WriteLine($"{info.ProductName} - 供应商:{info.SupplierName} ({info.SupplierCountry})");
}
2.5 集合属性投影
处理集合属性:
// 提取产品及其标签信息
// 方法语法(这种情况下方法语法更简洁)
var productTags = products.Select(p => new {
p.Name,
Tags = string.Join(", ", p.Tags),
TagCount = p.Tags.Length
});
// 输出结果
Console.WriteLine("\n产品标签信息:");
foreach (var item in productTags)
{
Console.WriteLine($"{item.Name} - 标签数量:{item.TagCount} - 标签:{item.Tags}");
}
2.6 投影到已有类型
除了匿名类型,也可以投影到已定义的类型:
// 定义一个视图模型类
public class ProductViewModel
{
public string ProductName { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
public string SupplierInfo { get; set; }
}
// 投影到已定义的类型
// 查询语法
var viewModels = from p in products
select new ProductViewModel {
ProductName = p.Name,
Price = p.Price,
Category = p.Category,
SupplierInfo = $"{p.Supplier.Name} ({p.Supplier.Country})"
};
// 方法语法
var viewModels2 = products.Select(p => new ProductViewModel {
ProductName = p.Name,
Price = p.Price,
Category = p.Category,
SupplierInfo = $"{p.Supplier.Name} ({p.Supplier.Country})"
});
投影操作的本质是从数据源中提取或转换数据,形成新的数据结构。这种操作不仅可以简化数据,还可以根据需要重塑数据,使其更适合后续处理或展示。
3. 排序(OrderBy/OrderByDescending)
排序操作允许我们根据一个或多个条件对结果集进行排序,相当于SQL中的ORDER BY子句。在LINQ中,排序主要通过OrderBy
、OrderByDescending
、ThenBy
和ThenByDescending
方法或查询语法中的orderby
关键字实现。
3.1 基本排序
// 按价格从低到高排序产品
// 查询语法
var productsByPrice = from p in products
orderby p.Price
select p;
// 方法语法
var productsByPrice2 = products.OrderBy(p => p.Price);
// 输出结果
Console.WriteLine("\n按价格升序排列的产品:");
foreach (var product in productsByPrice)
{
Console.WriteLine($"{product.Name} - ¥{product.Price}");
}
3.2 降序排序
// 按库存从高到低排序产品
// 查询语法
var productsByStockDesc = from p in products
orderby p.Stock descending
select p;
// 方法语法
var productsByStockDesc2 = products.OrderByDescending(p => p.Stock);
// 输出结果
Console.WriteLine("\n按库存降序排列的产品:");
foreach (var product in productsByStockDesc)
{
Console.WriteLine($"{product.Name} - 库存:{product.Stock}");
}
3.3 多级排序
// 先按类别升序,再按价格降序排序产品
// 查询语法
var productsByMultiple = from p in products
orderby p.Category, p.Price descending
select p;
// 方法语法
var productsByMultiple2 = products
.OrderBy(p => p.Category)
.ThenByDescending(p => p.Price);
// 输出结果
Console.WriteLine("\n按类别升序,价格降序排列的产品:");
foreach (var product in productsByMultiple)
{
Console.WriteLine($"{product.Category} - {product.Name} - ¥{product.Price}");
}
3.4 基于复杂条件的排序
// 按上市季度排序,然后按价格排序
// 方法语法(这里使用方法语法更简洁)
var productsByQuarterPrice = products
.OrderBy(p => (p.LaunchDate.Month - 1) / 3 + 1) // 计算季度
.ThenBy(p => p.Price);
// 输出结果
Console.WriteLine("\n按上市季度和价格排序的产品:");
foreach (var product in productsByQuarterPrice)
{
int quarter = (product.LaunchDate.Month - 1) / 3 + 1;
Console.WriteLine($"Q{quarter} {product.LaunchDate.Year} - {product.Name} - ¥{product.Price}");
}
3.5 自定义排序
通过实现自定义比较逻辑进行排序:
// 按产品名称长度排序
// 方法语法
var productsByNameLength = products.OrderBy(p => p.Name.Length);
// 输出结果
Console.WriteLine("\n按产品名称长度排序的产品:");
foreach (var product in productsByNameLength)
{
Console.WriteLine($"{product.Name} ({product.Name.Length}字符)");
}
LINQ的排序操作非常灵活,可以根据属性值、计算结果或自定义逻辑进行排序。多级排序允许我们在主要排序条件相同的情况下,使用次要条件进一步细化排序。
4. 分组(GroupBy)
分组操作允许我们根据一个或多个键将数据源中的元素分组,相当于SQL中的GROUP BY子句。在LINQ中,分组主要通过GroupBy
方法或查询语法中的group by
关键字实现。
4.1 基本分组
// 按产品类别分组
// 查询语法
var productsByCategory = from p in products
group p by p.Category;
// 方法语法
var productsByCategory2 = products.GroupBy(p => p.Category);
// 输出结果
Console.WriteLine("\n按类别分组的产品:");
foreach (var group in productsByCategory)
{
Console.WriteLine($"类别:{group.Key} (共{group.Count()}个产品)");
foreach (var product in group)
{
Console.WriteLine($" - {product.Name} - ¥{product.Price}");
}
Console.WriteLine(); // 添加空行分隔不同组
}
4.2 分组后投影
分组后我们通常需要对组进行进一步处理:
// 按供应商国家分组,计算每个国家的产品平均价格
// 查询语法
var priceByCountry = from p in products
group p by p.Supplier.Country into countryGroup
select new {
Country = countryGroup.Key,
AveragePrice = countryGroup.Average(p => p.Price),
ProductCount = countryGroup.Count()
};
// 方法语法
var priceByCountry2 = products
.GroupBy(p => p.Supplier.Country)
.Select(g => new {
Country = g.Key,
AveragePrice = g.Average(p => p.Price),
ProductCount = g.Count()
});
// 输出结果
Console.WriteLine("各国家产品平均价格:");
foreach (var item in priceByCountry)
{
Console.WriteLine($"{item.Country} - 平均价格:¥{item.AveragePrice:F2} - 产品数量:{item.ProductCount}");
}
4.3 多键分组
根据多个属性进行分组:
// 按供应商国家和产品类别双重分组
// 方法语法
var groupByCountryAndCategory = products
.GroupBy(p => new { Country = p.Supplier.Country, p.Category })
.OrderBy(g => g.Key.Country)
.ThenBy(g => g.Key.Category);
// 输出结果
Console.WriteLine("\n按国家和类别分组的产品:");
foreach (var group in groupByCountryAndCategory)
{
Console.WriteLine($"国家:{group.Key.Country},类别:{group.Key.Category} (共{group.Count()}个产品)");
foreach (var product in group)
{
Console.WriteLine($" - {product.Name}");
}
Console.WriteLine(); // 添加空行分隔不同组
}
4.4 分组聚合计算
对分组后的数据进行聚合计算:
// 计算各类别产品的价格统计信息
// 方法语法
var categoryStats = products
.GroupBy(p => p.Category)
.Select(g => new {
Category = g.Key,
MinPrice = g.Min(p => p.Price),
MaxPrice = g.Max(p => p.Price),
AveragePrice = g.Average(p => p.Price),
TotalValue = g.Sum(p => p.Price * p.Stock)
})
.OrderByDescending(x => x.TotalValue);
// 输出结果
Console.WriteLine("各类别产品价格统计:");
foreach (var stat in categoryStats)
{
Console.WriteLine($"类别:{stat.Category}");
Console.WriteLine($" 最低价格:¥{stat.MinPrice}");
Console.WriteLine($" 最高价格:¥{stat.MaxPrice}");
Console.WriteLine($" 平均价格:¥{stat.AveragePrice:F2}");
Console.WriteLine($" 库存总价值:¥{stat.TotalValue:F2}");
Console.WriteLine();
}
4.5 嵌套分组
可以创建层次化的分组结构:
// 先按供应商国家分组,再按产品类别分组
// 查询语法
var nestedGroups = from p in products
group p by p.Supplier.Country into countryGroup
select new {
Country = countryGroup.Key,
Categories = from p in countryGroup
group p by p.Category
};
// 输出结果
Console.WriteLine("\n嵌套分组:国家 -> 类别");
foreach (var countryGroup in nestedGroups)
{
Console.WriteLine($"国家:{countryGroup.Country}");
foreach (var categoryGroup in countryGroup.Categories)
{
Console.WriteLine($" 类别:{categoryGroup.Key} (共{categoryGroup.Count()}个产品)");
foreach (var product in categoryGroup)
{
Console.WriteLine($" - {product.Name}");
}
}
Console.WriteLine();
}
分组操作是数据分析中最有用的操作之一,它可以将数据按照共同特征组织在一起,并且可以对每个组执行聚合计算,从而获得数据的统计信息和分布特征。
5. 连接(Join)
连接操作允许我们基于键关系合并多个数据源的数据,相当于SQL中的JOIN操作。在LINQ中,连接主要通过Join
和GroupJoin
方法或查询语法中的join
关键字实现。
5.1 内连接(Inner Join)
内连接返回两个集合中键值匹配的所有元素对:
// 连接产品和订单数据
// 查询语法
var productOrders = from order in orders
join product in products
on order.ProductId equals product.Id
select new {
OrderId = order.Id,
ProductName = product.Name,
Price = product.Price,
Quantity = order.Quantity,
TotalAmount = product.Price * order.Quantity,
CustomerName = order.CustomerName,
OrderDate = order.OrderDate
};
// 方法语法
var productOrders2 = orders.Join(
products,
order => order.ProductId, // 订单集合的键选择器
product => product.Id, // 产品集合的键选择器
(order, product) => new { // 结果选择器
OrderId = order.Id,
ProductName = product.Name,
Price = product.Price,
Quantity = order.Quantity,
TotalAmount = product.Price * order.Quantity,
CustomerName = order.CustomerName,
OrderDate = order.OrderDate
}
);
// 输出结果
Console.WriteLine("订单详情:");
foreach (var order in productOrders)
{
Console.WriteLine($"订单ID:{order.OrderId} - 客户:{order.CustomerName} - 日期:{order.OrderDate.ToString("yyyy-MM-dd")}");
Console.WriteLine($" 产品:{order.ProductName} - 单价:¥{order.Price} - 数量:{order.Quantity} - 总金额:¥{order.TotalAmount}");
Console.WriteLine();
}
5.2 左外连接(Left Outer Join)
左外连接返回左侧集合的所有元素,无论它们是否在右侧集合中有匹配项。在LINQ中,左外连接通过GroupJoin
和SelectMany
组合实现:
// 左外连接:所有产品及其订单(包括没有订单的产品)
// 方法语法(这里用方法语法更清晰)
var productsWithOrders = products.GroupJoin(
orders,
product => product.Id, // 产品集合的键选择器
order => order.ProductId, // 订单集合的键选择器
(product, ordersGroup) => new { // 结果选择器
Product = product,
Orders = ordersGroup
})
.SelectMany(
x => x.Orders.DefaultIfEmpty(), // 如果没有订单,创建默认(null)订单
(productWithOrders, order) => new {
ProductName = productWithOrders.Product.Name,
Price = productWithOrders.Product.Price,
OrderId = order?.Id, // 可能为null,使用条件访问运算符
Quantity = order?.Quantity ?? 0,
CustomerName = order?.CustomerName
}
);
// 输出结果
Console.WriteLine("\n左外连接:所有产品及其订单");
foreach (var item in productsWithOrders)
{
if (item.OrderId.HasValue)
{
Console.WriteLine($"{item.ProductName} - 订单ID:{item.OrderId} - 客户:{item.CustomerName} - 数量:{item.Quantity}");
}
else
{
Console.WriteLine($"{item.ProductName} - 暂无订单");
}
}
5.3 组连接(Group Join)
组连接类似于左外连接,但它为左侧集合中的每个元素创建一个右侧匹配元素的组:
// 组连接:获取每个产品的所有订单
// 查询语法
var productWithOrderGroups = from product in products
join order in orders
on product.Id equals order.ProductId into orderGroup
select new {
ProductName = product.Name,
Price = product.Price,
Orders = orderGroup.ToList()
};
// 方法语法
var productWithOrderGroups2 = products.GroupJoin(
orders,
product => product.Id,
order => order.ProductId,
(product, ordersGroup) => new {
ProductName = product.Name,
Price = product.Price,
Orders = ordersGroup.ToList()
}
);
// 输出结果
Console.WriteLine("\n每个产品的所有订单:");
foreach (var item in productWithOrderGroups)
{
Console.WriteLine($"{item.ProductName} - ¥{item.Price} - 订单数量:{item.Orders.Count}");
if (item.Orders.Count > 0)
{
foreach (var order in item.Orders)
{
Console.WriteLine($" 订单ID:{order.Id} - 客户:{order.CustomerName} - 数量:{order.Quantity} - 日期:{order.OrderDate.ToString("yyyy-MM-dd")}");
}
}
else
{
Console.WriteLine(" 暂无订单");
}
Console.WriteLine();
}
5.4 多表连接
连接三个或更多集合:
// 连接产品、订单和供应商
// 查询语法
var orderDetails = from order in orders
join product in products
on order.ProductId equals product.Id
join supplier in suppliers
on product.Supplier.Id equals supplier.Id
select new {
OrderId = order.Id,
CustomerName = order.CustomerName,
OrderDate = order.OrderDate,
ProductName = product.Name,
Price = product.Price,
Quantity = order.Quantity,
SupplierName = supplier.Name,
SupplierCountry = supplier.Country
};
// 输出结果
Console.WriteLine("\n订单、产品和供应商详情:");
foreach (var detail in orderDetails)
{
Console.WriteLine($"订单ID:{detail.OrderId} - 客户:{detail.CustomerName} - 日期:{detail.OrderDate.ToString("yyyy-MM-dd")}");
Console.WriteLine($" 产品:{detail.ProductName} - 单价:¥{detail.Price} - 数量:{detail.Quantity}");
Console.WriteLine($" 供应商:{detail.SupplierName} ({detail.SupplierCountry})");
Console.WriteLine();
}
5.5 非等值连接(Non-equijoin)
非等值连接基于不是相等的条件(如范围比较)进行连接:
// 将产品与价格相近(差距在500元以内)的其他产品匹配
// 方法语法
var similarPriceProducts = from p1 in products
from p2 in products
where p1.Id != p2.Id && Math.Abs(p1.Price - p2.Price) <= 500
orderby p1.Name, p2.Name
select new {
Product1 = p1.Name,
Price1 = p1.Price,
Product2 = p2.Name,
Price2 = p2.Price,
PriceDifference = Math.Abs(p1.Price - p2.Price)
};
// 输出结果(限制输出数量以避免过多)
Console.WriteLine("\n价格相似的产品对(差距<=500元):");
foreach (var pair in similarPriceProducts.Take(10)) // 只显示前10对
{
Console.WriteLine($"{pair.Product1} (¥{pair.Price1}) 与 {pair.Product2} (¥{pair.Price2}) - 价差:¥{pair.PriceDifference}");
}
连接操作是处理关系型数据的核心功能,它允许我们从多个相关的数据集合中组合和关联数据,以便进行更复杂的查询和分析。LINQ的连接操作灵活且强大,能够实现各种类型的数据关联需求。
6. 综合示例:电子商城数据分析
下面是一个综合示例,结合了多种LINQ查询操作来分析我们的电子商城数据:
using System;
using System.Collections.Generic;
using System.Linq;
namespace LinqQueryOperationsDemo
{
// 使用前面定义的数据模型类
class Program
{
static void Main(string[] args)
{
// 初始化数据(使用前面的示例数据)
Console.WriteLine("========== 电子商城数据分析 ==========\n");
// 1. 月度销售报表:计算每个月的销售额
Console.WriteLine("1. 月度销售报表:");
var monthlySales = from order in orders
join product in products on order.ProductId equals product.Id
group new { order, product } by new { order.OrderDate.Year, order.OrderDate.Month } into g
orderby g.Key.Year, g.Key.Month
select new {
Year = g.Key.Year,
Month = g.Key.Month,
TotalAmount = g.Sum(x => x.product.Price * x.order.Quantity),
OrderCount = g.Count()
};
foreach (var month in monthlySales)
{
Console.WriteLine($"{month.Year}年{month.Month}月 - 订单数:{month.OrderCount} - 销售额:¥{month.TotalAmount:F2}");
}
// 2. 供应商产品分析:按供应商分组的产品数量、平均价格和库存价值
Console.WriteLine("\n2. 供应商产品分析:");
var supplierAnalysis = from product in products
group product by product.Supplier.Name into g
select new {
SupplierName = g.Key,
ProductCount = g.Count(),
AveragePrice = g.Average(p => p.Price),
TotalStockValue = g.Sum(p => p.Price * p.Stock)
};
foreach (var supplier in supplierAnalysis.OrderByDescending(s => s.TotalStockValue))
{
Console.WriteLine($"{supplier.SupplierName} - 产品数:{supplier.ProductCount} - 平均价格:¥{supplier.AveragePrice:F2} - 库存价值:¥{supplier.TotalStockValue:F2}");
}
// 3. 客户购买分析:每个客户的购买金额和购买商品
Console.WriteLine("\n3. 客户购买分析:");
var customerPurchases = from order in orders
join product in products on order.ProductId equals product.Id
group new { order, product } by order.CustomerName into g
select new {
CustomerName = g.Key,
TotalSpent = g.Sum(x => x.product.Price * x.order.Quantity),
ItemCount = g.Sum(x => x.order.Quantity),
PurchasedItems = g.Select(x => x.product.Name).ToList()
};
foreach (var customer in customerPurchases.OrderByDescending(c => c.TotalSpent))
{
Console.WriteLine($"{customer.CustomerName} - 总消费:¥{customer.TotalSpent:F2} - 购买商品数:{customer.ItemCount}");
Console.WriteLine($" 购买的商品:{string.Join(", ", customer.PurchasedItems)}");
}
// 4. 产品流行度分析:按销售量排序产品
Console.WriteLine("\n4. 产品流行度分析:");
var productPopularity = from product in products
join order in orders on product.Id equals order.ProductId into orderGroup
select new {
ProductName = product.Name,
Category = product.Category,
Price = product.Price,
TotalQuantitySold = orderGroup.Sum(o => o.Quantity),
OrderCount = orderGroup.Count(),
TotalRevenue = product.Price * orderGroup.Sum(o => o.Quantity)
};
foreach (var product in productPopularity.OrderByDescending(p => p.TotalQuantitySold))
{
Console.WriteLine($"{product.ProductName} ({product.Category}) - 价格:¥{product.Price}");
Console.WriteLine($" 总售出数量:{product.TotalQuantitySold} - 订单数:{product.OrderCount} - 总收入:¥{product.TotalRevenue:F2}");
}
// 5. 库存需求预测:基于销售和库存的分析
Console.WriteLine("\n5. 库存需求预测:");
var inventoryAnalysis = from product in products
join order in orders on product.Id equals order.ProductId into orderGroup
let totalSold = orderGroup.Sum(o => o.Quantity)
let restockNeeded = totalSold > 0 && (double)product.Stock / totalSold < 2.0 // 库存不足两倍销量
select new {
ProductName = product.Name,
CurrentStock = product.Stock,
TotalSold = totalSold,
StockToSalesRatio = totalSold > 0 ? (double)product.Stock / totalSold : double.MaxValue,
RestockRecommended = restockNeeded
};
var lowStockProducts = inventoryAnalysis.Where(p => p.RestockRecommended).OrderBy(p => p.StockToSalesRatio);
Console.WriteLine("建议补货的产品:");
foreach (var product in lowStockProducts)
{
Console.WriteLine($"{product.ProductName} - 当前库存:{product.CurrentStock} - 已售数量:{product.TotalSold} - 库存/销量比:{product.StockToSalesRatio:F2}");
}
}
}
}
这个综合示例展示了如何使用LINQ的基本查询操作来进行复杂的数据分析。通过组合过滤、投影、排序、分组和连接等操作,我们可以轻松地从原始数据中提取有价值的业务洞察。
总结
LINQ的基本查询操作为我们提供了一套强大而灵活的工具,用于处理各种数据查询和操作需求。通过合理组合这些基本操作,我们可以实现从简单到复杂的各种数据处理任务。
本文详细介绍了LINQ的五种核心查询操作:
- 过滤(Where):根据特定条件选择元素,能够处理各种简单和复杂的条件表达式。
- 投影(Select):从数据源中提取或转换数据,生成新的数据结构,可以简化或重塑数据。
- 排序(OrderBy/OrderByDescending):按照单个或多个条件对结果进行排序,支持升序、降序和多级排序。
- 分组(GroupBy):根据共同特征将数据分组,并可以对每个组执行聚合计算。
- 连接(Join):基于键关系合并多个数据源的数据,支持内连接、左外连接、组连接等多种连接类型。
通过综合示例,我们展示了如何将这些基本操作组合起来,构建复杂的数据分析应用。这些技术可以用于各种场景,从简单的数据筛选到复杂的业务智能分析。
LINQ的优势在于它将查询能力直接集成到了语言中,使得查询表达式变得简洁、类型安全且可组合。通过LINQ,我们可以用统一的语法处理不同类型的数据源,从而提高代码的可读性、可维护性和生产力。