Linq 缓慢具体化复杂查询

Linq slowness materializing complex queries

我经常发现如果我在 Linq 查询中有太多连接(无论是使用 Entity Framework 还是 NHibernate)and/or 结果匿名的形状 class 太复杂, Linq 需要很长时间才能将结果集具体化为对象。

这是一个一般性问题,但这里有一个使用 NHibernate 的具体示例:

var libraryBookIdsWithShelfAndBookTagQuery = (from shelf in session.Query<Shelf>()
    join sbttref in session.Query<ShelfBookTagTypeCrossReference>() on
         shelf.ShelfId equals sbttref.ShelfId
    join bookTag in session.Query<BookTag>() on
         sbttref.BookTagTypeId equals (byte)bookTag.BookTagType
    join btbref in session.Query<BookTagBookCrossReference>() on
         bookTag.BookTagId equals btbref.BookTagId
    join book in session.Query<Book>() on
         btbref.BookId equals book.BookId
    join libraryBook in session.Query<LibraryBook>() on
         book.BookId equals libraryBook.BookId
    join library in session.Query<LibraryCredential>() on
         libraryBook.LibraryCredentialId equals library.LibraryCredentialId
    join lcsg in session
         .Query<LibraryCredentialSalesforceGroupCrossReference>()
          on library.LibraryCredentialId equals lcsg.LibraryCredentialId
    join userGroup in session.Query<UserGroup>() on
         lcsg.UserGroupOrganizationId equals userGroup.UserGroupOrganizationId
    where
         shelf.ShelfId == shelfId &&
         userGroup.UserGroupId == userGroupId &&
         !book.IsDeleted &&
         book.IsDrm != null &&
         book.BookFormatTypeId != null
    select new
    {
        Book = book,
        LibraryBook = libraryBook,
        BookTag = bookTag
    });

// add a couple of where clauses, then...
var result = libraryBookIdsWithShelfAndBookTagQuery.ToList();

我知道这不是查询执行,因为我在数据库上放了一个嗅探器,我可以看到查询花费了 0 毫秒,但代码执行该查询大约需要一秒钟并带回所有 11记录。

是的,这是一个过于复杂的查询,在 9 个表之间有 8 个连接,我可能可以将它重组为几个更小的查询。或者我可以将它变成一个存储过程 - 但这会有帮助吗?

我想了解的是,高性能查询和开始难以实现的查询之间的红线在哪里?幕后发生了什么?如果这是一个 SP,我随后在内存中将其平坦结果操作成正确的形状,这会有帮助吗?

编辑: 响应评论中的请求,这里是 SQL 发出的:

SELECT DISTINCT book4_.bookid                 AS BookId12_0_, 
                libraryboo5_.librarybookid    AS LibraryB1_35_1_, 
                booktag2_.booktagid           AS BookTagId15_2_, 
                book4_.title                  AS Title12_0_, 
                book4_.isbn                   AS ISBN12_0_, 
                book4_.publicationdate        AS Publicat4_12_0_, 
                book4_.classificationtypeid   AS Classifi5_12_0_, 
                book4_.synopsis               AS Synopsis12_0_, 
                book4_.thumbnailurl           AS Thumbnai7_12_0_, 
                book4_.retinathumbnailurl     AS RetinaTh8_12_0_, 
                book4_.totalpages             AS TotalPages12_0_, 
                book4_.lastpage               AS LastPage12_0_, 
                book4_.lastpagelocation       AS LastPag11_12_0_, 
                book4_.lexilerating           AS LexileR12_12_0_, 
                book4_.lastpageposition       AS LastPag13_12_0_, 
                book4_.hidden                 AS Hidden12_0_, 
                book4_.teacherhidden          AS Teacher15_12_0_, 
                book4_.modifieddatetime       AS Modifie16_12_0_, 
                book4_.isdeleted              AS IsDeleted12_0_, 
                book4_.importedwithlexile     AS Importe18_12_0_, 
                book4_.bookformattypeid       AS BookFor19_12_0_, 
                book4_.isdrm                  AS IsDrm12_0_, 
                book4_.lightsailready         AS LightSa21_12_0_, 
                libraryboo5_.bookid           AS BookId35_1_, 
                libraryboo5_.libraryid        AS LibraryId35_1_, 
                libraryboo5_.externalid       AS ExternalId35_1_, 
                libraryboo5_.totalcopies      AS TotalCop5_35_1_, 
                libraryboo5_.availablecopies  AS Availabl6_35_1_, 
                libraryboo5_.statuschangedate AS StatusCh7_35_1_, 
                booktag2_.booktagtypeid       AS BookTagT2_15_2_, 
                booktag2_.booktagvalue        AS BookTagV3_15_2_ 
FROM   shelf shelf0_, 
       shelfbooktagtypecrossreference shelfbookt1_, 
       booktag booktag2_, 
       booktagbookcrossreference booktagboo3_, 
       book book4_, 
       librarybook libraryboo5_, 
       library librarycre6_, 
       librarycredentialsalesforcegroupcrossreference librarycre7_, 
       usergroup usergroup8_ 
WHERE  shelfbookt1_.shelfid = shelf0_.shelfid 
       AND booktag2_.booktagtypeid = shelfbookt1_.booktagtypeid 
       AND booktagboo3_.booktagid = booktag2_.booktagid 
       AND book4_.bookid = booktagboo3_.bookid 
       AND libraryboo5_.bookid = book4_.bookid 
       AND librarycre6_.libraryid = libraryboo5_.libraryid 
       AND librarycre7_.librarycredentialid = librarycre6_.libraryid 
       AND usergroup8_.usergrouporganizationid = 
           librarycre7_.usergrouporganizationid 
       AND shelf0_.shelfid = @p0 
       AND usergroup8_.usergroupid = @p1 
       AND NOT ( book4_.isdeleted = 1 ) 
       AND ( book4_.isdrm IS NOT NULL ) 
       AND ( book4_.bookformattypeid IS NOT NULL ) 
       AND book4_.lightsailready = 1 

编辑 2: 这是 ANTS Performance Profiler 的性能分析:

通常数据库 "good" 的做法是将大量联接或超常见联接放入视图中。 ORM 不会让您忽略这些事实,它们也不会补充数十年来花费在微调数据库上以高效地完成这些事情的时间。如果从更大的应用程序角度来看更有意义,请将这些连接重构为单个视图或几个视图。

NHibernate 应该优化查询并减少数据,以便 .Net 只需处理重要部分。但是,如果这些域对象天生就很大,那仍然是大量数据。此外,如果就行 returned 而言它是一个非常大的结果集,即使 DB 能够快速 return 集,也会有很多对象被实例化。将此查询重构为仅 return 包含您实际需要的数据的视图也将减少对象实例化开销。

另一个想法是不做 .ToList()。 Return 可枚举并让您的代码懒惰地使用数据。

你的问题很笼统,我会告诉你笼统的答案:)

  1. 如果您查询数据是为了阅读(而不是为了更新),请尝试使用匿名 classes。原因是 - 它们创建起来更轻松,它们没有导航属性。而你 select 只有你需要的数据!这是非常重要的规则。所以,尝试用这样的 smth 替换你的 select:

    select new { Book = new { book.Id, book.Name}, LibraryBook = new { libraryBook.Id, libraryBook.AnotherProperty}, BookTag = new { bookTag.Name} }

  2. 存储过程很好,当查询复杂,linq-provider生成无效代码时,可以用plain SQL或存储过程代替。这种情况并不常见,我认为这不是你的情况

  3. 运行 您的 sql-查询。它有多少行returns?它与结果的值相同吗?有时 linq 提供程序生成代码,select 一个实体 select 多行。当实体与另一个 selecting 实体具有一对多关系时,就会发生这种情况。例如:

class Book { int Id {get;set;} string Name {get;set;} ICollection<Tag> Tags {get;set;} } class Tag { string Name {get;set;} Book Book {get;set;} } ... dbContext.Books.Where(o => o.Id == 1).Select(o=>new {Book = o, Tags = o.Tags}).Single(); 我 Select 只有一本 ID = 1 的书,但提供商会生成代码,returns 行数等于标签数(entity framework 这样做)。

  1. 将复杂查询拆分为一组简单查询并在客户端加入。有时,您有许多条件的复杂查询,结果 sql 变得很糟糕。因此,您将大查询拆分为更简单的查询,在客户端获取每个查询和 join/filter 的结果。

最后,我建议您使用匿名 class 作为 select 的结果。

根据分析信息,CreateQuery 占总执行时间的 45%。但是,正如您提到的,当您直接执行查询时,查询花费了 0 毫秒。但这还不足以说存在性能问题,因为,

  1. 您是 运行 使用对执行时间有重大影响的探查器的查询。
  2. 当您使用分析器时,它会影响正在分析的每个代码,但不会影响 sql 执行时间(因为它发生在 SQL 服务器中),因此您可以看到其他所有内容与 SQL 语句相比更慢。

所以理想的情况是测量执行整个代码块需要多长时间,测量 SQL 查询和计算时间的时间,如果你这样做,你可能会得到不同的值。

但是,我并不是说 NH Linq to SQL 实现针对您提出的任何查询进行了优化,但是 NHibernate 中还有其他方法可以处理这些情况,例如 QueryOverAPI, CriteriaQueries、HQL 最后是 SQL.

  1. Where is that red line crossed between a query that is performant and one that starts to struggle with materialization. What's going on under the hood?

这是一个很难回答的问题,如果不了解 SQL 提供商的 NHibernate Linq 的详细知识,就很难提供准确的答案。您可以随时尝试提供的不同机制,看看哪一个最适合给定的场景。

  1. And would it help if this were a SP whose flat results I subsequently manipulate in memory into the right shape?

是的,使用 SP 可以使事情运行得非常快,但是使用 SP 会给您的代码库增加更多的维护问题。

我 100% 同意其他人表达的观点(关于它们是此处优化的两个部分,SQL 执行是一个很大的未知数,很可能是性能不佳的原因)。

可能会帮助您提高速度的解决方案的另一部分是预编译您的 LINQ 语句。我记得这是对我多年以前从事的一个小项目(高流量)的巨大优化……似乎它会导致您看到的客户端运行缓慢。说了这么多,虽然我还没有发现需要使用它们……所以首先要注意其他人的警告! :)

https://msdn.microsoft.com/en-us/library/vstudio/bb896297(v=vs.100).aspx

Don’t use Linq’s Join. Navigate!

在那post你可以看到:

As long as there are proper foreign key constraints in the database, the navigation properties will be created automatically. It is also possible to manually add them in the ORM designer. As with all LINQ to SQL usage I think that it is best to focus on getting the database right and have the code exactly reflect the database structure. With the relations properly specified as foreign keys the code can safely make assumptions about referential integrity between the tables.