【问题标题】:Loops introduced by ORM Reduces Performance drasticallyORM 引入的循环大大降低了性能
【发布时间】:2012-07-03 07:29:11
【问题描述】:

我有一张如下图所示的表格。它有固定和储蓄类型的账户。我需要更新用户 1 的所有帐户的状态。该用户有 10000 个帐户。本质上,逻辑将如以下 SQL 存储过程脚本所示。该脚本只需不到 1 秒即可执行(83 毫秒)。

但是当我使用 LINQ to SQL 将其转换为 ORM 时,需要 3 分钟以上(204814 毫秒)。它至少慢 240,000%

LINQ to SQL(或其他 ORM)中是否存在有助于克服这种性能损失的模式?

什么可以强制它一次性更新数据库?

注意:我知道从 LINQ 调用存储过程。我不认为这是 ORM,也不是我的选择。

手动存储过程脚本

DECLARE @UserID INT 
DECLARE @StatusForFixed VARCHAR(50)
DECLARE @StatusForSavings VARCHAR(50)

SET @UserID = 1
SET @StatusForFixed = 'FrozenFA11'
SET @StatusForSavings = 'FrozenSB22'

UPDATE BankAccount 
SET Status = 
        CASE 
            WHEN BankAccount.AccountType='Fixed' THEN @StatusForFixed
            WHEN BankAccount.AccountType='Savings' THEN @StatusForSavings
        END
 WHERE  AccountOwnerID=@UserID

LINQ 生成的代码示例

Note: This type of statements happen 10000 times

UPDATE [dbo].[BankAccount]
SET [Status] = @p3
WHERE [BankAccountID] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [3585]
-- @p3: Input NChar (Size = 10; Prec = 0; Scale = 0) [FrozenSB]

应用ORM后的代码

public class BankAccountAppService
{
    public RepositoryLayer.ILijosBankRepository AccountRepository { get; set; }

    public void FreezeAllAccountsForUser(int userId)
    {
        IEnumerable<DBML_Project.BankAccount> accounts = AccountRepository.GetAllAccountsForUser(userId);
        foreach (DBML_Project.BankAccount acc in accounts)
        {

            acc.Freeze();

        }
        AccountRepository.UpdateAccount();

    }

}

public class LijosSimpleBankRepository : ILijosBankRepository
{
    public System.Data.Linq.DataContext Context
    {
        get;
        set;
    }


    public List<DBML_Project.BankAccount> GetAllAccountsForUser(int userID)
    {
        IQueryable<DBML_Project.BankAccount> queryResultEntities = Context.GetTable<DBML_Project.BankAccount>().Where(p => p.AccountOwnerID == userID);
        return queryResultEntities.ToList();
    }

    public List<T> GetAllAccountsofType<T>() where T : DBML_Project.BankAccount
    {
        var query = from p in Context.GetTable<DBML_Project.BankAccount>().OfType<T>()
                    select p;

        List<T> typeList = query.ToList();
        return typeList;

    }

    public virtual void UpdateAccount()
    {
        Context.SubmitChanges();
    }

}

namespace DBML_Project
{

public  partial class BankAccount
{
    //Define the domain behaviors
    public virtual void Freeze()
    {
        //Do nothing
    }
}

public class FixedBankAccount : BankAccount
{

    public override void Freeze()
    {
        this.Status = "FrozenFA";
    }
}

public class SavingsBankAccount : BankAccount
{

    public override void Freeze()
    {
        this.Status = "FrozenSB";
    }
}  
}

参考

  1. Pass List as XElement to be used as XML Datatype parameter

【问题讨论】:

    标签: c# linq-to-sql


    【解决方案1】:

    您正在比较两个截然不同的场景:

    1:在SQL server本地运行脚本,基于单集的UPDATE

    2:通过网络获取 10,000 条记录,更新每条记录,单独提交每条记录

    您可以通过将SubmitChanges() 推迟到一批 10,000 而不是 10,000 批 1 来提高 2 a bit,但是这仍然涉及向两个方向发送 10,000 条记录的详细信息,以及所有开销(例如,SubmitChanges() 可能仍然选择通过 10,000 个单独的调用来执行此操作)。

    基本上,基于对象的工具不适合针对记录进行批量更新。如果 SP 有效,请使用 SP。也许通过数据上下文调用 SP,只是为了方便它添加方法/参数/等。

    【讨论】:

    • 谢谢。你能解释一下为什么你说“SubmitChanges() 可能仍然选择通过 10,000 个单独的调用来做到这一点”。这其中的主导因素是什么?什么可以强制它进行批量更新?
    • @Lijo 简单吗?需要在并发等方面分别验证每个(在直接 TSQL 方法中不是问题)。一些 ORM 批次;有些没有。
    • 您认为下面的方法会有所帮助吗? stackoverflow.com/questions/11306344/…
    【解决方案2】:

    您仍然可以从您的应用程序中执行您的存储过程/自定义 SQL 脚本。您甚至可以在 Linq-to-sql 模型中映射过程,这样您就无需手动打开连接和创建命令。

    我不确定 Linq-to-sql 是否总是在与数据库的单独往返中执行每个修改命令,但我猜它确实如此(至少在大多数情况下)。 EF总是这样做。 NHibernate 对此类操作有更好的支持,因为它具有命令批处理功能。

    您在这里展示的不是批量更新(单个命令更新大量记录)——大多数 ORM 总是会单独更新每条记录——这就是这些工具的工作方式。如果您加载记录并在循环中修改每个记录,则与用于加载记录的原始查询的关系将丢失。现在,您的应用程序中有 10.000 条加载的记录必须更新。无法进行批量更新,因为您必须将 10.000 项更改从应用程序移动到数据库。

    如果您想进行批量更新,您应该使用直接 SQL 或实现一些逻辑,从 Linq-to-sql 进行更新,而不是加载记录并在应用程序中更新它们。检查this article 或在 Linq-to-sql 中搜索 Bulk / Batch 更新。

    【讨论】:

    【解决方案3】:

    这是因为 Linq to SQL 首先从服务器加载数据,然后单独更新每条记录,其中包括数据查询/传输到客户端,更新每条记录的请求。而在 SP 情况下,只调用 SP 直接在服务器上执行更新查询,它不包括数据获取和每条记录的更新。批量更新记录

    【讨论】:

      【解决方案4】:

      我做的另一种方法是将对象值作为 XML 数据类型传递给存储过程。但是当记录数超过 1000 时会出现超时异常(大约 25 秒后)。是不是因为 xml 文件太大?

      注意:1000 条记录大约需要 5 秒

         public virtual void UpdateBankAccountUsingParseXML_SP(System.Xml.Linq.XElement inputXML)
          {
              string connectionstring = "Data Source=.;Initial Catalog=LibraryReservationSystem;Integrated Security=True;Connect Timeout=600";
              var myDataContext = new DBML_Project.MyDataClassesDataContext(connectionstring);
              myDataContext.ParseXML(inputXML);
      
          }
      
          public void FreezeAllAccountsForUser(int userId)
          {
              List<DTOLayer.BankAccountDTOForStatus> bankAccountDTOList = new List<DTOLayer.BankAccountDTOForStatus>(); 
      
              IEnumerable<DBML_Project.BankAccount> accounts = AccountRepository.GetAllAccountsForUser(userId);
              foreach (DBML_Project.BankAccount acc in accounts)
              {
                  string typeResult = Convert.ToString(acc.GetType());
                  string baseValue = Convert.ToString(typeof(DBML_Project.BankAccount));
      
                  if (String.Equals(typeResult, baseValue))
                  {
                      throw new Exception("Not correct derived type");
                  }
      
                  acc.Freeze();
      
                  DTOLayer.BankAccountDTOForStatus presentAccount = new DTOLayer.BankAccountDTOForStatus();
                  presentAccount.BankAccountID = acc.BankAccountID;
                  presentAccount.Status = acc.Status;
                  bankAccountDTOList.Add(presentAccount);
      
              }
      
      
      
              IEnumerable<System.Xml.Linq.XElement> el = bankAccountDTOList.Select(x =>
                              new System.Xml.Linq.XElement("BankAccountDTOForStatus",
                                new System.Xml.Linq.XElement("BankAccountID", x.BankAccountID),
                                new System.Xml.Linq.XElement("Status", x.Status)
                              ));
      
              System.Xml.Linq.XElement root = new System.Xml.Linq.XElement("root", el);
      
      
              AccountRepository.UpdateBankAccountUsingParseXML_SP(root);
              //AccountRepository.Update();
      
          }
      

      存储过程

      ALTER PROCEDURE [dbo].[ParseXML] (@InputXML xml)
      AS
      BEGIN
      
      DECLARE @MyTable TABLE (RowNumber int, BankAccountID int, StatusVal varchar(max))
      
      INSERT INTO @MyTable(RowNumber, BankAccountID,StatusVal)
      
      SELECT ROW_NUMBER() OVER(ORDER BY c.value('BankAccountID[1]','int') ASC) AS Row,
          c.value('BankAccountID[1]','int'),
          c.value('Status[1]','varchar(32)')
      FROM
          @inputXML.nodes('//BankAccountDTOForStatus') T(c);
      
      
      DECLARE @Count INT
      SET @Count = 0
      
      DECLARE @NumberOfRows INT
      SELECT @NumberOfRows = COUNT(*) FROM @MyTable
      
      
      WHILE @Count < @NumberOfRows
      BEGIN
      
      
      
            SET @Count = @Count + 1
      
           DECLARE @BankAccID INT
           DECLARE @Status VARCHAR(MAX)
      
           SELECT @BankAccID = BankAccountID 
           FROM @MyTable
           WHERE RowNumber = @Count
      
           SELECT @Status = StatusVal 
           FROM @MyTable
           WHERE RowNumber = @Count
      
      
           UPDATE BankAccount 
           SET Status= @Status
           WHERE BankAccountID = @BankAccID
      
      END
      
      
      END
      
      
      GO
      

      【讨论】:

      • 从 TDS 更改为 XML 可能会增加带宽...但可能会减少往返。复杂的。测量一下看看。
      • 比较时间,XML 似乎稍微快一些。但挑战在于超时异常。这种行为是预期的吗? 1000 条记录大约需要 5 秒。
      • 坦率地说,我不知道你为什么要把一些东西从“工作得很好,很快”变成“勉强工作,慢慢地”
      • 您有一个需要 1 秒的现有 SP。你为什么要远离它?
      • @MarcGravell 我试图从现有的存储过程中获取业务逻辑。你认为这种尝试值得吗?
      猜你喜欢
      • 2016-07-19
      • 1970-01-01
      • 1970-01-01
      • 2018-08-14
      • 1970-01-01
      • 1970-01-01
      • 2020-05-29
      • 2021-04-05
      • 1970-01-01
      相关资源
      最近更新 更多