查詢語法是 C#/VB 提供的**聲明式簡化寫法**,用於表達 LINQ 查詢。編譯器會將其轉譯為擴充方法呼叫 (Lambda 運算式),提供完整 IntelliSense 與編譯期檢查。
// 查詢語法
var query = from p in people
where p.LastName.StartsWith("G")
orderby p.FirstName
select p;
// 等價方法語法
var query = people
.Where(p => p.LastName.StartsWith("G"))
.OrderBy(p => p.FirstName)
.Select(p => p);
from p in people // p 為範圍變數,代表 people 中每個 Person 物件
from o in orders // 多來源可串聯 (交叉聯結)
join d in departments on p.DeptId equals d.Id // 內部聯結
from c in p.Children.DefaultIfEmpty() // 左外部聯結
where p.Age > 18 && p.IsActive
where p.Tags.Contains("vip") // 集合包含
where new[] {1,2,3}.Contains(p.Type) // IN 條件
orderby p.LastName, p.FirstName descending
orderby p.Salary descending, p.HireDate
// 完整物件投影
select p
// 單一欄位投影 → IEnumerable
select p.FirstName
// 匿名型別投影 → IEnumerable<匿名型別>
select new { p.Id, FullName = p.FirstName + " " + p.LastName, p.Email }
// 具名型別投影 (物件初始化器)
select new PersonDto {
Id = p.Id,
FullName = p.FirstName + " " + p.LastName,
Email = p.Email
}
// 計算欄位投影
select new {
p.Id,
TotalRevenue = p.OrderDetails.Sum(od => od.UnitPrice * od.Quantity)
}
// 簡單分組
from p in people
group p by p.DeptId into g
select new { DeptId = g.Key, Count = g.Count(), AvgAge = g.Average(p => p.Age) }
// 多鍵分組
group p by new { p.DeptId, p.JobGrade } into g
select new { g.Key.DeptId, g.Key.JobGrade, Count = g.Count() }
from p in people
let fullName = p.FirstName + " " + p.LastName
let orderCount = p.Orders.Count()
where orderCount > 5
select new { p.Id, fullName, orderCount }
from p in people
join d in departments on p.DeptId equals d.Id
select new { p.Name, DeptName = d.Name }
// 群組聯結 (左外部聯結效果)
from d in departments
join p in people on d.Id equals p.DeptId into dp
from p in dp.DefaultIfEmpty()
select new { d.Name, EmpName = p != null ? p.Name : "無員工" }
查詢語法建構 IQueryable<T>,**延遲執行**,最終轉譯為 SQL 在資料庫執行。
// 僅建構查詢,未執行
var query = from p in db.Products
where p.UnitPrice > 99
select p;
// 觸發執行:ToList(), ToArray(), foreach, Count(), First()
var list = query.ToList(); // 立即執行 SQL
// 完整實體 → SELECT * (所有欄位)
var q1 = from p in db.Products where p.UnitPrice > 99 select p;
// 投影 DTO → 只 SELECT 指定欄位
var q2 = from p in db.Products
where p.UnitPrice > 99
select new ProductDto { Id = p.ProductId, Name = p.ProductName, Price = p.UnitPrice };
// 進階:計算欄位下推資料庫
var q3 = from p in db.Products
where p.UnitPrice > 99
select new {
p.ProductId,
p.ProductName,
p.UnitPrice,
TotalRevenue = p.OrderDetails.Sum(od => od.UnitPrice * od.Quantity)
};
| 類型 | 操作 | 行為 |
|---|---|---|
| 延遲 (Deferred) | Where, Select, OrderBy, GroupBy, Join, Skip, Take | 建構查詢計畫,不執行 |
| 立即 (Immediate) | ToList(), ToArray(), ToDictionary(), ToLookup() | 立即執行,回傳記憶體集合 |
| 立即 | First(), FirstOrDefault(), Single(), SingleOrDefault() | 立即執行,回傳單一元素 |
| 立即 | Count(), Any(), All(), Sum(), Average(), Max(), Min() | 立即執行,回傳純量 |
| 立即 | foreach 迴圈 | 開始迭代時執行 |
var baseQuery = db.Products.Where(p => p.UnitPrice > 50);
// 動態加入條件
if (categoryId.HasValue)
baseQuery = baseQuery.Where(p => p.CategoryId == categoryId);
if (inStockOnly)
baseQuery = baseQuery.Where(p => p.UnitsInStock > 0);
var result = baseQuery.ToList(); // 單一 SQL 執行
var page = query
.OrderBy(p => p.ProductName)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.ToList();
var list = await query.ToListAsync();
var count = await query.CountAsync();
var first = await query.FirstOrDefaultAsync();
| 陷阱 | 錯誤現象 | 解決 |
|---|---|---|
| 多次列舉 | foreach 兩次執行兩次 SQL | 先 .ToList() 緩存 |
| N+1 查詢 | 導航屬性觸發大量 SQL | .Include() 或 投影選取 |
| 客戶端評估 | 無法轉譯 SQL 的運算在記憶體執行 | 改寫為可轉譯運算、或明確 AsEnumerable() |
| 匿名型別跨方法 | 無法作為回傳型別 | 定義 DTO/Record 型別 |
public record ProductDto(int Id, string Name, decimal Price);
var query = from p in db.Products
where p.UnitPrice > 99
select new ProductDto(p.ProductId, p.ProductName, p.UnitPrice);