掌握LINQ五大查询操作:从过滤到连接

引言

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中,过滤操作支持丰富的条件表达式,可以满足各种复杂的筛选需求。

过滤Where
基本条件过滤
多条件组合
嵌套属性过滤
集合属性过滤
日期条件过滤

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})"
});

投影操作的本质是从数据源中提取或转换数据,形成新的数据结构。这种操作不仅可以简化数据,还可以根据需要重塑数据,使其更适合后续处理或展示。

投影Select
单一属性选择
匿名类型创建
数据计算与转换
嵌套属性处理
集合属性处理
投影到具名类型

3. 排序(OrderBy/OrderByDescending)

排序操作允许我们根据一个或多个条件对结果集进行排序,相当于SQL中的ORDER BY子句。在LINQ中,排序主要通过OrderByOrderByDescendingThenByThenByDescending方法或查询语法中的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的排序操作非常灵活,可以根据属性值、计算结果或自定义逻辑进行排序。多级排序允许我们在主要排序条件相同的情况下,使用次要条件进一步细化排序。

排序
升序OrderBy
降序OrderByDescending
多级排序ThenBy/ThenByDescending
复杂条件排序
自定义排序逻辑

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();
}

分组操作是数据分析中最有用的操作之一,它可以将数据按照共同特征组织在一起,并且可以对每个组执行聚合计算,从而获得数据的统计信息和分布特征。

分组GroupBy
简单分组
分组并投影
多键分组
分组聚合计算
嵌套分组

5. 连接(Join)

连接操作允许我们基于键关系合并多个数据源的数据,相当于SQL中的JOIN操作。在LINQ中,连接主要通过JoinGroupJoin方法或查询语法中的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中,左外连接通过GroupJoinSelectMany组合实现:

// 左外连接:所有产品及其订单(包括没有订单的产品)
// 方法语法(这里用方法语法更清晰)
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的连接操作灵活且强大,能够实现各种类型的数据关联需求。

连接Join
内连接Inner Join
左外连接Left Outer Join
组连接Group Join
多表连接
非等值连接

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的五种核心查询操作:

  1. 过滤(Where):根据特定条件选择元素,能够处理各种简单和复杂的条件表达式。
  2. 投影(Select):从数据源中提取或转换数据,生成新的数据结构,可以简化或重塑数据。
  3. 排序(OrderBy/OrderByDescending):按照单个或多个条件对结果进行排序,支持升序、降序和多级排序。
  4. 分组(GroupBy):根据共同特征将数据分组,并可以对每个组执行聚合计算。
  5. 连接(Join):基于键关系合并多个数据源的数据,支持内连接、左外连接、组连接等多种连接类型。

通过综合示例,我们展示了如何将这些基本操作组合起来,构建复杂的数据分析应用。这些技术可以用于各种场景,从简单的数据筛选到复杂的业务智能分析。

LINQ的优势在于它将查询能力直接集成到了语言中,使得查询表达式变得简洁、类型安全且可组合。通过LINQ,我们可以用统一的语法处理不同类型的数据源,从而提高代码的可读性、可维护性和生产力。

进一步学习资源

  1. Microsoft官方文档:LINQ查询操作
  2. LINQ操作符完整参考
  3. LINQ示例项目
  4. LINQPad - LINQ查询和C#/F#/VB测试工具
  5. LINQ to Entities与Entity Framework
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰茶_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值