LINQ 語法應用

AdSense

前言

使用 CustomersOrders 兩個表,逐步了解幾個 LINQ 的實際語法應用,包含:Join (連接)、Grouping (分組)、Ordering (排序) 和 Aggregates (聚合)。

示範資料表

Customers (客戶) 表

Id Name Country
1 Allen Taiwan
2 Betty USA
3 Carol Taiwan
4 David UK

Orders (訂單) 表

Id CustomerId Country ProductName OrderDate Amount
101 1 Taiwan Laptop 2024/01/15 30000
102 1 Taiwan Mouse 2024/09/16 1200
103 2 USA Keyboard 2024/01/20 2500
104 3 Taiwan Laptop 2024/09/25 30000
105 3 Taiwan Monitor 2024/10/26 8000
106 5 USA Speaker 2024/02/01 3500

1. 內部連接 (Inner Join)

在 LINQ 中,join 關鍵字用來執行內部連接,join 只會回傳兩個集合中都有符合條件的資料。在這個例子中,只會選取在 CustomersOrders 表中都能找到對應 CustomerId 的資料。

LINQ 查詢語法

1
2
3
4
5
6
7
8
9
var innerJoinResult = from c in customers
join o in orders
on c.Id equals o.CustomerId
select new
{
CustomerName = c.Name, // 可以指定屬姓名
o.ProductName, // 也可以不指定屬姓名
o.Amount
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
SELECT c.Name, o.ProductName, o.Amount
FROM Customers c
INNER JOIN Orders o ON c.Id = o.CustomerId;

查詢結果

David (Id=4) 和 Speaker (CustomerId=5) 的資料都不會出現在結果中,因為 David 沒有下單,而 SpeakerCustomerIdCustomers 表中找不到對應資料。

CustomerName ProductName Amount
Allen Laptop 30000
Allen Mouse 1200
Betty Keyboard 2500
Carol Laptop 30000
Carol Monitor 8000

2. 左連接 (Left Join)

左連接會回傳左邊集合 (Customers) 的所有資料,即使在右邊集合 (Orders) 中沒有找到符合的項目。如果沒有匹配的資料,右邊的欄位會顯示為 null

LINQ 沒有直接的關鍵字來做左連接,但你可以透過 into + DefaultIfEmpty() 來模擬這個行為。如果右邊集合找不到相符的元素,則使用 DefaultIfEmpty() 產生一個空集合。

LINQ 查詢語法

1
2
3
4
5
6
7
8
9
10
var leftJoinResult = from c in customers
join o in orders
on c.Id equals order.CustomerId into customerOrders
from order in customerOrders.DefaultIfEmpty()
select new
{
CustomerName = c.Name,
ProductName = order?.ProductName, // 如果沒有匹配,ProductName 會是 null
Amount = order?.Amount ?? 0 // 如果沒有匹配,Amount 會是 0
};

from order in customerOrders.DefaultIfEmpty() 這個步驟中,如果 customerOrders 這個分組裡有資料,就會取出實際的 order 物件,如果沒有資料,DefaultIfEmpty() 會回傳一個包含 null 的集合,確保 order 在沒有匹配訂單時會是 null,而不會產生錯誤。

查詢結果

David 的資料被保留下來了,且他的 ProductNameAmount 都是空值 (因為沒有下單)。

CustomerName ProductName Amount
Allen Laptop 30000
Allen Mouse 1200
Betty Keyboard 2500
Carol Laptop 30000
Carol Monitor 8000
David 0

3. 分組 (Grouping)

接下來我們來看看如何對 Orders 表進行分組,並對分組後的資料進行聚合運算。

依單一屬性分組 (Group by Single Property)

我們想計算每個客戶總共下了多少訂單金額。

LINQ 查詢語法

1
2
3
4
5
6
7
8
var groupedByCustomer = from order in orders
group order by order.CustomerId into g
select new
{
CustomerId = g.Key,
TotalAmount = g.Sum(o => o.Amount),
OrderCount = g.Count()
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
SELECT CustomerId, SUM(Amount) AS TotalAmount, Count(*)
FROM Orders
GROUP BY CustomerId;

查詢結果

CustomerId TotalAmount OrderCount
1 31200 2
2 2500 1
3 38000 2
5 3500 1

依多個屬性分組 (Group by Multiple Properties)

假設我們想計算每個國家中,每個客戶所下的訂單總額。

LINQ 查詢語法

1
2
3
4
5
6
7
8
var groupedByCountryAndCustomer = from order in orders
group order by new { order.Country, order.CustomerId } into g
select new
{
Country = g.Key.Country,
CustomerId = g.Key.CustomerId,
TotalAmount = g.Sum(o => o.Amount)
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
SELECT Country, CustomerId, SUM(Amount) AS TotalAmount
FROM Orders
GROUP BY Country, CustomerId;

查詢結果

Country CustomerId TotalAmount
Taiwan 1 31200
USA 2 2500
Taiwan 3 38000
USA 5 3500

4. 分組後排序 (Ordering Groups)

你可以根據分組後的一個或多個屬性來進行排序。這在 SQL 語法上是直接在 GROUP BY 後接 ORDER BY,而在 LINQ 查詢語法中則是在 group by 後接 orderby

LINQ 查詢語法

1
2
3
4
5
6
7
8
9
var groupedData = from order in orders
group order by new { order.Country, order.CustomerId } into g
orderby g.Key.Country, g.Key.CustomerId
select new
{
Country = g.Key.Country,
CustomerId = g.Key.CustomerId,
TotalAmount = g.Sum(o => o.Amount)
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
4
SELECT Country, CustomerId, SUM(Amount) AS TotalAmount
FROM Orders
GROUP BY Country, CustomerId
ORDER BY Country, CustomerId;

LINQ 方法語法 (Method Syntax):

1
2
3
4
5
6
7
8
9
10
var groupedData = orders
.GroupBy(order => new { order.Country, order.CustomerId })
.OrderBy(g => g.Key.Country)
.ThenBy(g => g.Key.CustomerId)
.Select(g => new
{
Country = g.Key.Country,
CustomerId = g.Key.CustomerId,
TotalAmount = g.Sum(o => o.Amount)
});

5. 條件式聚合運算 (Conditional Aggregates)

在 SQL 中,我們經常使用 CASE WHEN 語法在聚合運算中加入條件判斷。

範例一

在 LINQ 中,我們可以使用 Where() 來實現類似 SQL 中 CASE WHEN 的條件判斷,這樣可以針對不同條件進行聚合計算。

LINQ 查詢語法

假設我們想計算每個客戶訂單中,金額大於 10000 的總額,以及金額小於等於 10000 的總額。

1
2
3
4
5
6
7
8
var conditionalAggregate = from order in orders
group order by order.CustomerId into g
select new
{
CustomerId = g.Key,
HighValueOrdersTotal = g.Where(row => row.Amount > 10000).Sum(row => row.Amount),
LowValueOrdersTotal = g.Where(row => row.Amount <= 10000).Sum(row => row.Amount)
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
4
5
SELECT CustomerId,
Sum(CASE WHEN Amount > 10000 THEN Amount ELSE 0 END) AS HighValueOrdersTotal,
Sum(CASE WHEN Amount <= 10000 THEN Amount ELSE 0 END) AS LowValueOrdersTotal
FROM Orders
GROUP BY CustomerId;

查詢結果

CustomerId HighValueOrdersTotal LowValueOrdersTotal
1 30000 1200
2 0 2500
3 30000 8000
5 0 3500

範例二

也可以使用三元運算子 ? : 來實現類似 SQL 中 CASE WHEN 的條件判斷

LINQ 查詢語法

假設我們想計算每個客戶訂單中,6/30以前 的總額,以及7/1以後 的總額。

1
2
3
4
5
6
7
8
var conditionalAggregate = from order in orders
group order by order.CustomerId into g
select new
{
CustomerId = g.Key,
Before0630 = g.Sum(o => o.OrderDate < '2024/07/01' ? o.Amount : 0),
After0701 = g.Sum(o => o.OrderDate >= '2024/07/01' ? o.Amount : 0)
};

這段程式碼等同於以下的 SQL 語法:

1
2
3
4
5
SELECT CustomerId,
SUM(CASE WHEN OrderDate < '2024/07/01' THEN Amount ELSE 0 END) AS Before0630,
SUM(CASE WHEN OrderDate >= '2024/07/01' THEN Amount ELSE 0 END) AS After0701
FROM Orders
GROUP BY CustomerId;

查詢結果

CustomerId Before0630 After0701
1 30000 1200
2 2500 0
3 0 38000
5 3500 0