Skip to content

投影查询 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 条记录对比 ​

指标完整实体投影查询提升
查询时间120ms70ms42%
数据传输5MB250KB95%
内存占用80MB15MB81%
序列化时间50ms10ms80%
总耗时170ms80ms53%

不同场景对比 ​

场景记录数完整实体投影查询提升
产品列表10030ms15ms50%
产品列表1,000170ms80ms53%
产品列表10,0001500ms600ms60%
订单详情10050ms20ms60%
报表统计50,0008s2.5s69%

实际案例 ​

电商网站产品列表页:
├─ 完整实体: 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.DependencyInjection
csharp
// 配置映射
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 返回 → 投影查询
只读展示 → 投影查询
需要修改 → 完整实体
大数据量 → 必须投影
小数据量 → 可选投影

下一步:

  1. 学习 AsNoTracking 优化
  2. 掌握 避免 N+1 查询
  3. 理解 批量操作

基于 MIT 许可发布