用批处理使EF Core查询速度提升 3倍

EF Core是一个及其出色的 ORM(对象关系映射)工具,非常适合构建 .NET 应用程序。但它和其他工具一样,如果使用不当,可能影响性能。

在这里向你展示一个简单的方法,可以提升接近4倍的性能。这也不是说你一定会得到相同的性能提升,但理解其核心肯定会对查询速度有改善。

存在性能问题的查询

本文用以下示例来解释这个概念。 它取自一个真正的生产应用程序,但进行了简化。

使用一个InvoiceService来获取给定公司的发票集合。这些发票可能来自第三方API或某些其他持久化存储。现在还缺乏详细的发票行信息,所以需要查询数据库来填补缺失的行信息。

下面的LINQ查询本身并没有太大问题。它通过一次数据库查询返回某个发票的所有行。

但因为我们要对发票集合进行轮询,会多次查询储存行的数据表。所以这段代码有更多的性能提升空间。

ini 复制代码
app.MapGet("invoices/{companyId}", (
    long companyId,
    InvoiceService invoiceService,
    AppDbContext dbContext) =>
{
    IEnumerable<Invoice> invoices = invoiceService.GetForCompanyId(
        companyId,
        take: 10);

    var invoiceDtos = new List<InvoiceDto>();
    foreach (var invoice in invoices)
    {
        var invoiceDto = new InvoiceDto
        {
            Id = invoice.Id,
            CompanyId = invoice.CompanyId,
            IssuedDate = invoice.IssuedDate,
            DueDate = invoice.DueDate,
            Number = invoice.Number
        };

        var lineItemDtos = await dbContext
            .LineItems
            .Where(li => invoice.LineItemIds.Contains(li.Id))
            .Select(li => new LineItemDto
            {
                Id = li.Id,
                Name = li.Name,
                Price = li.Price,
                Quantity = li.Quantity
            })
            .ToArrayAsync();

        invoiceDto.LineItems = lineItemDtos;

        invoiceDtos.Add(invoiceDto);
    }

    return invoiceDtos;
})

解决这个问题的方案也很直接。就是提前查询所有行项目,而不是为每张发票获取行信息。

使用批处理

查询方法是相同的,但重构为只查询一次行项目。这意味着只有一次数据库访问。

最终设计包含三个组成部分:

  1. 将LineItemIds映射到一个HashSet中,这样可以去除重复项。
  2. 通过单次数据库往返查询所有LineItems。
  3. 创建一个LineItemDto字典以便快速查找。

一旦我们有了字典,我们就可以遍历发票并分配行项目。填充行项目变成了字典查找(成本低)而不是数据库查询(成本高)。

在使用这个解决方案之前,还要考虑一件事情, 即一次能从数据库加载多少记录?

假设每张发票平均包含约20个行项目,我们只获取十张发票。在最坏的情况下(所有行项目都是唯一的),我们从数据库加载约200个行项目。大多数数据库可以处理这种负载。但如果你正在读取成千上万行,情况又不一样了, 需要考虑其他方案,例如分页查询。

ini 复制代码
app.MapGet("invoices/{companyId}", (
    long companyId,
    InvoiceService invoiceService,
    AppDbContext dbContext) =>
{
    IEnumerable<Invoice> invoices = invoiceService.GetForCompanyId(
        companyId,
        take: 10);

    HashSet<long> lineItemIds = invoices
        .SelectMany(invoice => invoice.LineItemIds)
        .ToHashSet();

    var lineItemDtos = await context
        .LineItems
        .Where(li => lineItemIds.Contains(li.Id))
        .Select(li => new LineItemDto
        {
            Id = li.Id,
            Name = li.Name,
            Price = li.Price,
            Quantity = li.Quantity
        })
        .ToListAsync();

    Dictionary<long, LineItemDto> lineItemsDictionary =
        lineItemDtos.ToDictionary(keySelector: li => li.Id);

    var invoiceDtos = new List<InvoiceDto>();
    foreach (var invoice in invoices)
    {
        var invoiceDto = new InvoiceDto
        {
            Id = invoice.Id,
            CompanyId = invoice.CompanyId,
            IssuedDate = invoice.IssuedDate,
            DueDate = invoice.DueDate,
            Number = invoice.Number,
            LineItems = invoice
                .LineItemIds
                .Select(li => lineItemsDictionary[li])
                .ToArray()
        };

        invoiceDtos.Add(invoiceDto);
    }

    return invoiceDtos;
})

会快多少?

批处理版本看起来会更快, 真是这样吗?

在第一个版本中,有N个查询(每张发票一个),而在批处理版本中,只有一个查询。

以下是使用BenchmarkDotNet得到的基准测试结果:

foreach版本平均耗时1913.3微秒(us), 批处理版本平均耗时558.6微秒。

批处理版本快了3.42倍。这是使用本地SQL数据库的结果。

如果查询远程数据库,批处理版本应该会更快,因为网络往返时间的影响。当有N个查询(foreach版本)时,这种时间会快速累积。

总结

这个方法的好处在于其简单性和效率。通过批处理数据库查询,显著减少了对数据库的访问次数。这通常是最大的性能瓶颈之一。

但也要理解,这种方法并不是万能的。

EF Core提供了许多功能和优化方法,但如何有效使用它们取决于开发者。

最后,永远记得进行基线测试。在这个案例中看到的改进是通过基线测试量化的。没有适当的测量,很容易做出那些无意中降低性能的, 事与愿违的修改。

相关推荐
程序员张31 分钟前
SpringBoot计时一次请求耗时
java·spring boot·后端
程序员岳焱6 小时前
Java 与 MySQL 性能优化:Java 实现百万数据分批次插入的最佳实践
后端·mysql·性能优化
麦兜*7 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
大只鹅7 小时前
解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题
spring boot·后端·elasticsearch
ai小鬼头7 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
IT_10248 小时前
Spring Boot项目开发实战销售管理系统——数据库设计!
java·开发语言·数据库·spring boot·后端·oracle
bobz9658 小时前
动态规划
后端
stark张宇8 小时前
VMware 虚拟机装 Linux Centos 7.9 保姆级教程(附资源包)
linux·后端
亚力山大抵9 小时前
实验六-使用PyMySQL数据存储的Flask登录系统-实验七-集成Flask-SocketIO的实时通信系统
后端·python·flask
超级小忍9 小时前
Spring Boot 中常用的工具类库及其使用示例(完整版)
spring boot·后端