Appearance
投影查询 Select DTO
只查询需要的字段,减少数据传输,提升性能 60-80%
📖 目录
什么是投影查询
概念理解
投影查询(Projection Query) 是指使用 Select 只查询需要的字段,而非整个实体。
完整实体查询:
SELECT * FROM Products
→ 返回所有列 (Id, Name, Price, Description, CreatedAt, ...)
投影查询:
SELECT Id, Name, Price FROM Products
→ 只返回需要的列为什么需要投影
csharp
// ❌ 查询所有字段
var products = await context.Products.ToListAsync();
// SELECT * FROM Products
// 传输: 10KB/条 × 1000条 = 10MB
// ✅ 投影查询
var products = await context.Products
.Select(p => new { p.Id, p.Name, p.Price })
.ToListAsync();
// SELECT Id, Name, Price FROM Products
// 传输: 50B/条 × 1000条 = 50KB
// 减少: 99.5%性能提升:
- 网络传输减少 60-90%
- 内存占用减少 50-80%
- 序列化速度提升 30-60%
- 总体性能提升 40-70%
基础投影
1. 匿名类型投影
csharp
// 投影为匿名类型
var products = await context.Products
.Where(p => p.IsActive)
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
foreach (var product in products)
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}优点:
- ✅ 简单快速
- ✅ 适合临时查询
缺点:
- ❌ 不能作为返回值
- ❌ 无类型安全
2. 命名元组投影
csharp
// EF Core 8+ 支持
var products = await context.Products
.Select(p => (
Id: p.Id,
Name: p.Name,
Price: p.Price
))
.ToListAsync();
// 使用
foreach (var (Id, Name, Price) in products)
{
Console.WriteLine($"{Name}: ${Price}");
}3. 计算字段投影
csharp
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
OriginalPrice = p.Price,
DiscountedPrice = p.Price * 0.9m,
TaxAmount = p.Price * 0.13m,
FinalPrice = p.Price * 0.9m * 1.13m
})
.ToListAsync();生成的 SQL:
sql
SELECT
[Id],
[Name],
[Price] AS [OriginalPrice],
[Price] * 0.9 AS [DiscountedPrice],
[Price] * 0.13 AS [TaxAmount],
[Price] * 0.9 * 1.13 AS [FinalPrice]
FROM [Products]DTO 投影
1. 定义 DTO
csharp
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string CategoryName { get; set; }
}2. 投影到 DTO
csharp
var products = await context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
})
.ToListAsync();
// 可直接作为 API 返回值
return Ok(products);优点:
- ✅ 类型安全
- ✅ 可作为返回值
- ✅ 支持 IntelliSense
- ✅ 易于维护
3. API 控制器示例
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
{
var products = await _context.Products
.AsNoTracking()
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
})
.ToListAsync();
return Ok(products);
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(int id)
{
var product = await _context.Products
.AsNoTracking()
.Where(p => p.Id == id)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
})
.FirstOrDefaultAsync();
if (product == null)
return NotFound();
return Ok(product);
}
}嵌套对象投影
1. 嵌套 DTO
csharp
public class OrderDto
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public CustomerDto Customer { get; set; }
public List<OrderItemDto> Items { get; set; }
}
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class OrderItemDto
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}2. 投影嵌套对象
csharp
var orders = await context.Orders
.Where(o => o.OrderDate >= DateTime.Today.AddDays(-30))
.Select(o => new OrderDto
{
Id = o.Id,
OrderDate = o.OrderDate,
Customer = new CustomerDto
{
Id = o.Customer.Id,
Name = o.Customer.Name,
Email = o.Customer.Email
},
Items = o.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
ProductName = i.Product.Name,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList()
})
.ToListAsync();生成的 SQL:
sql
-- 查询 1: Orders + Customers
SELECT [o].[Id], [o].[OrderDate], [c].[Id], [c].[Name], [c].[Email]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[OrderDate] >= @date
-- 查询 2: OrderItems + Products
SELECT [i].[OrderId], [i].[ProductId], [p].[Name], [i].[Quantity], [i].[UnitPrice]
FROM [OrderItems] AS [i]
INNER JOIN [Products] AS [p] ON [i].[ProductId] = [p].[Id]
WHERE [i].[OrderId] IN (...)3. 扁平化投影
csharp
// 将嵌套结构扁平化
var orderItems = await context.OrderItems
.Where(oi => oi.Order.OrderDate >= DateTime.Today.AddDays(-30))
.Select(oi => new
{
OrderId = oi.Order.Id,
OrderDate = oi.Order.OrderDate,
CustomerName = oi.Order.Customer.Name,
ProductName = oi.Product.Name,
Quantity = oi.Quantity,
UnitPrice = oi.UnitPrice,
TotalAmount = oi.Quantity * oi.UnitPrice
})
.ToListAsync();条件投影
1. 条件字段
csharp
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
Status = p.IsActive ? "Available" : "Out of Stock",
PriceLevel = p.Price > 100 ? "Expensive" :
p.Price > 50 ? "Moderate" : "Cheap",
DiscountInfo = p.DiscountPercent > 0
? $"{p.DiscountPercent}% OFF"
: "No Discount"
})
.ToListAsync();2. 条件加载导航属性
csharp
var products = await context.Products
.Select(p => new
{
p.Id,
p.Name,
// 只在有评论时加载
ReviewCount = p.Reviews.Count(),
AvgRating = p.Reviews.Any()
? p.Reviews.Average(r => r.Rating)
: (decimal?)null,
LatestReview = p.Reviews
.OrderByDescending(r => r.CreatedAt)
.Select(r => new
{
r.Content,
r.Rating,
r.CreatedAt
})
.FirstOrDefault()
})
.ToListAsync();3. 动态投影
csharp
public async Task<List<object>> GetProductsAsync(string view = "summary")
{
IQueryable<Product> query = context.Products.AsNoTracking();
return view.ToLower() switch
{
"summary" => await query.Select(p => new
{
p.Id,
p.Name,
p.Price
}).ToListAsync<object>(),
"detail" => await query.Select(p => new
{
p.Id,
p.Name,
p.Price,
p.Description,
p.StockQuantity,
CategoryName = p.Category.Name
}).ToListAsync<object>(),
"full" => await query.Select(p => new
{
p.Id,
p.Name,
p.Price,
p.Description,
p.StockQuantity,
CategoryName = p.Category.Name,
Reviews = p.Reviews.Select(r => new
{
r.Rating,
r.Content,
r.CreatedAt
}).ToList()
}).ToListAsync<object>(),
_ => throw new ArgumentException("Invalid view type")
};
}性能优势
基准测试
csharp
public class ProjectionBenchmark
{
private readonly AppDbContext _context;
// 查询完整实体
[Benchmark]
public async Task<List<Product>> FullEntityQuery()
{
return await _context.Products
.AsNoTracking()
.ToListAsync();
}
// 投影查询
[Benchmark]
public async Task<List<ProductDto>> ProjectionQuery()
{
return await _context.Products
.AsNoTracking()
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
}
}性能数据
1000 条记录对比
| 指标 | 完整实体 | 投影查询 | 提升 |
|---|---|---|---|
| 查询时间 | 120ms | 70ms | 42% |
| 数据传输 | 5MB | 250KB | 95% |
| 内存占用 | 80MB | 15MB | 81% |
| 序列化时间 | 50ms | 10ms | 80% |
| 总耗时 | 170ms | 80ms | 53% |
不同场景对比
| 场景 | 记录数 | 完整实体 | 投影查询 | 提升 |
|---|---|---|---|---|
| 产品列表 | 100 | 30ms | 15ms | 50% |
| 产品列表 | 1,000 | 170ms | 80ms | 53% |
| 产品列表 | 10,000 | 1500ms | 600ms | 60% |
| 订单详情 | 100 | 50ms | 20ms | 60% |
| 报表统计 | 50,000 | 8s | 2.5s | 69% |
实际案例
电商网站产品列表页:
├─ 完整实体: 200ms, 100MB 内存
├─ 投影查询: 90ms, 18MB 内存
└─ 性能提升: 55%, 内存减少 82%
移动端 API:
├─ 完整实体: 500ms (3G 网络)
├─ 投影查询: 150ms (3G 网络)
└─ 性能提升: 70%, 流量减少 90%
报表系统:
├─ 完整实体: 超时 (100,000 条)
├─ 投影查询: 3s
└─ 性能提升: 从不可用到可用最佳实践
1. API 始终使用投影
csharp
// ✅ 推荐
[HttpGet]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts()
{
return await _context.Products
.AsNoTracking()
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
}
// ❌ 避免
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}2. 根据场景定义多个 DTO
csharp
// 列表视图
public class ProductListItemDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string ThumbnailUrl { get; set; }
}
// 详情视图
public class ProductDetailDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string CategoryName { get; set; }
public List<ReviewDto> Reviews { get; set; }
}
// 管理视图
public class ProductAdminDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}3. 使用 AutoMapper ProjectTo
bash
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjectioncsharp
// 配置映射
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Product, ProductDto>();
CreateMap<Order, OrderDto>();
}
}
// 使用
var products = await context.Products
.ProjectTo<ProductDto>(_mapper.ConfigurationProvider)
.ToListAsync();
// AutoMapper 自动生成最优的 Select 语句4. 避免在客户端投影
csharp
// ❌ 错误: 先查询所有字段,再在客户端投影
var products = await context.Products.ToListAsync();
var dtos = products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
}).ToList();
// ✅ 正确: 在数据库端投影
var dtos = await context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
})
.ToListAsync();5. 分页 + 投影
csharp
public async Task<PagedResult<ProductDto>> GetProductsAsync(
int pageNumber,
int pageSize)
{
var query = context.Products.AsNoTracking();
var totalCount = await query.CountAsync();
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
return new PagedResult<ProductDto>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
}⚠️ 注意事项
1. 投影不能用于更新
csharp
// ❌ 错误
var product = await context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
})
.FirstOrDefaultAsync();
product.Name = "New Name";
await context.SaveChangesAsync(); // ❌ 不会保存
// ✅ 正确: 查询完整实体
var product = await context.Products.FindAsync(id);
product.Name = "New Name";
await context.SaveChangesAsync();2. 复杂表达式可能无法翻译
csharp
// ❌ 可能无法翻译
var products = await context.Products
.Select(p => new
{
p.Id,
FormattedPrice = FormatPrice(p.Price) // 自定义方法
})
.ToListAsync();
// ✅ 正确: 使用可翻译的表达式
var products = await context.Products
.Select(p => new
{
p.Id,
FormattedPrice = "$" + p.Price.ToString("F2")
})
.ToListAsync();📚 延伸阅读
💡 小结
核心要点:
- ✅ 投影查询减少 60-90% 数据传输
- ✅ 性能提升 40-70%
- ✅ API 必须使用投影
- ✅ 定义多个 DTO 适配不同场景
- ✅ 配合 AsNoTracking 效果更佳
- ❌ 投影结果不能用于更新
使用原则:
API 返回 → 投影查询
只读展示 → 投影查询
需要修改 → 完整实体
大数据量 → 必须投影
小数据量 → 可选投影下一步:
- 学习 AsNoTracking 优化
- 掌握 避免 N+1 查询
- 理解 批量操作