65.9K
CodeProject 正在变化。 阅读更多。
Home

使用基于图的查询优化 Entity Framework 查询性能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (14投票s)

2011年8月29日

CPOL

14分钟阅读

viewsIcon

97869

本文档介绍了一种新的、基于图的 Microsoft ADO.Net Entity Framework 查询的表达和执行方式。通过广泛的性能比较,我们表明,在表达能力和性能方面,基于图的查询(GBQ)轻松优于传统的 LINQ 查询。

引言

Microsoft ADO.Net Entity Framework (EF) 在查询复杂数据模型时存在一些限制,导致性能不佳和代码复杂(这本身就会导致软件维护问题)。

继承可能是 EF 中对性能影响最大的(负面)因素。对于非平凡的继承层次结构(使用 TPT),EF 查询的性能会显著下降。这一点众所周知,微软也正在努力解决(参见 EF 2011 年 6 月的 CTP 版本)。TPT 会导致非常复杂的 SQL 查询,这些查询需要大量资源来构造和执行。我们曾见过 EF 甚至无法构造出查询的例子。

另一个问题是,在 LINQ 中指定跨越实体图的查询很困难。例如,考虑以下结构:

image001.png

图 1. 具有图结构的实体模型。

包含所有实体的 LINQ 查询看起来会像这样:

  ObjectContext.OSet
     .Inlude("E00.E10.E20")
     .Inlude("E00.E10.E21")
     .Inlude("E00.E11.E22")
     .Inlude("E00.E11.E23")
     .Inlude("E00.A00.A10.B00")
     .Inlude("E00.A00.A11")
     .Inlude("E00.A00.A12")

跨越关联实体图的查询由从给定根(本例中为“E00”)到定义该图的最少路径集组成。这是一种相当冗长且易出错的定义查询包含内容的方式:它包含冗余信息,并且路径组件是无类型的。

如果查询包含继承和关联,情况会变得更糟,因为 LINQ 的表达能力不够。例如,下图显示了一个具有继承的实体模型。

image002.png

图 2. Depth3Assoc1 模型。1

在此图中,继承用虚线表示,关联用实线表示。

给定这样的图,就无法使用单个查询来获取数据了。原因是,我们无法指定从“E00”到“B00”的路径。也就是说,查询“ObjectContext.OSet.Include(“E00.A00.B00”)”将引发异常,因为“A00”和“B00”之间没有关联(只有从“A10”到“B00”的关联)。查询“ObjectContext.OSet.Include(“E00.A00.A10.B00”)”也无效,因为“A00”和“A10”之间的继承关系无法在 LINQ 查询中表达。

这些限制与复杂数据模型结合在一起,很容易导致性能不佳且难以理解和维护的查询。本文档简要介绍了一种用于查询复杂结构的替代查询机制。我们提供了深入的性能比较,表明该机制在性能和复杂性方面能迅速获得回报。

基于图的查询

基于图的查询(GBQ)是一种从关系数据库查询复杂结构的新方法。如果你的数据模型使用 TPT 继承,并且/或者你使用多个 SQL 查询来获取关联数据的图(如图 2 所示),那么 GBQ 可能是一个选择。图 3 展示了一个典型 GBQ 查询所涉及的表、关联和继承关系的示例。

image003.png

图 3. 复杂查询的模糊图表示。GBQ 旨在处理像这样的复杂查询。

与编写复杂的 SQL 查询(甚至更糟,是一系列 SQL 查询)以从数据库中获取所有数据不同,使用 GBQ,你可以定义你感兴趣的数据图的形状(即实体类型及其关系),然后调用此形状上的“Load()”方法。例如:

  var shape = new EntityGraphShape4SQL(ObjectContext)
      .Edge<O, E00>(x => x.E00Set)
      .Edge<E00, A00>(x => x.A00Set)
      .Edge<A10, B00>(x => x.B00Set);

此示例展示了如何定义一个强类型实体图形状,该形状涵盖 Depth3Assoc1 数据模型。现在,你可以像这样为所有所有者实体获取此形状中描述的所有数据:

  shape.Load<O>();

如果你更倾向于仅为单个所有者对象加载数据,那么给定主键,你可以发出以下命令:

  var owner = new O { Id  = <my owner id> };
  shape.Load(owner);

形状定义中的边可以从继承层次结构中的任何位置开始。这样,你就可以轻松地表达从某个子类型到另一个实体的关联。例如,要表达“A10”与“B00”有关联(参见图 2),我们定义以下边:

.Edge<A10, B00>(x => x.B00Set)

正如你所见,实体图形状简洁且声明式,易于定义(和维护)你的查询。此外,形状是可以在程序中任何地方使用的对象(查看 http://riaservicescontrib.codeplex.com/wikipage?title=EntityGraph 以了解更多关于实体图的信息)。最重要的是,GBQ 使用形状定义中的信息来生成非常高效的 SQL 查询。因此,根据形状定义从 SQL 数据库获取数据非常高效。本文档通过表明它轻松优于传统的 EF LINQ 查询来演示其效率。

GBQ 是 EF 的一个扩展。在查询时,它通过生成 T-SQL 并自行具体化结果集来完全绕过 EF。生成的实体会被附加到 EF 对象上下文中,以便由 EF 进一步管理。这意味着 GBQ 不会取代 Entity Framework。相反,它旨在与 EF 协同工作,并且仅在 EF 查询不满足要求时使用。GBQ 使用 EF 模型的元模型来分析类型、存储映射等,以生成 T-SQL 和具体化结果数据。

测试

为了对 GBQ 的性能进行基准测试,我们测量了其在不同复杂度的不同数据模型和不同数据量下的性能。我们将结果与相应的 EF 查询进行了比较。

数据模型

GBQ 旨在提高 TPT 继承模型和复杂关联结构下的性能。为了测试此类模型下 GBQ 的性能,我们创建了 5 个复杂度递增的 EF 模型。每个数据模型都定义了一个以实体类型“E00”为根的继承层次结构。继承层次结构被构建成一个二叉树(除了“Depth5Wide”数据模型,其叶子节点数量超过 2^(深度-1))。模型深度递增,从深度 3(包含 4 个具体类型和 3 个抽象基类型)到 6(包含 32 个具体类型和 31 个抽象基类型)。模型有一个“拥有”实体类型“O”,该类型与“E00”定义了关联。最简单的模型(Depth3)如下所示:

image005.png

图 4. Depth3 模型。

为了也对具有关联的数据模型的性能进行基准测试,我们为每个模型创建了两种版本:一个**有**关联的模型和一个**没有**关联的模型。对于两个具有关联的模型,我们甚至创建了具有 1、2 和 3 个关联的变体。因此,总共有 14 个不同的数据模型。下图显示了最简单的具有 3 个关联的继承模型(Depth3Assoc3)。

image006.png

图 5. Depth3Assoc3 模型。

在此图中,关联用实线表示,而虚线表示继承。这些关联非常简单。实际上,这种简单的关联结构并不现实。因此,GBQ 的性能提升在我们将在下面展示的场景中会更加显著。

关联有两个重要方面:

  1. 它们本身形成一个继承层次结构,以实体类型“A00”为根。
  2. “A00”的子类型本身也具有关联。

这一点很重要,因为像这样的数据结构无法使用 Include 机制在单个 EF 查询中涵盖。也就是说,你可以定义一个查询,如 ObjectContext.E00Set.Include(“A00Set”),但你无法表示为 A10 类型的元素也包含 B00Set。

查询

我们将 GBQ 查询与两种形式的 EF 查询进行比较。

EFSingleQuery

第一种形式是单个查询,其形式如下:

ObjectContext.E00Set.Where(x => x.OId == selectedEntityId)

此查询获取给定所有者 ID 为“selectedEntityId”的“E00”类型的元素。我们以这样的方式填充数据库,使得每个所有者对象只有一个 E00 对象。此查询的结果是一个从“E00”派生的类型的单个元素。EF 负责连接相应的表(在 TPT 中,每个类型都有自己的数据库表),并实例化一个正确的 E00 子类型。具有关联的模型查询形式如下:

ObjectContext.E00Set.Where(x => x.OId == selectedEntityId).Include("A00Set")

这将不仅获取并实例化一个 E00 子类型的实例,还会获取关联的 A00 类型的实体。正如我们已经指出的,这种 Include 机制的表达能力不足以指示也应该获取与 A10 实体关联的 B00 实体。因此,此查询对于具有关联的数据模型是不完整的。

EFMultiQuery

第二种 EF 查询形式是多查询。

单个 EF 查询的一个问题是,它在处理复杂的继承层次结构时性能不佳。这是因为 EF 生成了一个跨越 TPT 层次结构所有表的非常复杂的 SQL 查询。另一种方法是将单个查询分解为多个查询,每个查询对应一个具体类型。每个查询的形式如下:

ObjectContext.E00Set.OfType<ConcreteType>().Where(x => x.OId == selectedEntityId)

ConcreteType 表示“E00”的一个具体子类型。对于“Depth3”数据模型,我们将得到 4 个查询:

  ObjectContext.E00Set.OfType<E20>().Where(x => x.OId == selectedEntityId);
  ObjectContext.E00Set.OfType<E21>().Where(x => x.OId == selectedEntityId);
  ObjectContext.E00Set.OfType<E22>().Where(x => x.OId == selectedEntityId);
  ObjectContext.E00Set.OfType<E23>().Where(x => x.OId == selectedEntityId);

直到 EF 4.0,这比单个查询快得多,尽管对于较大的继承层次结构,性能有所下降。自 EF 2011 年 6 月的 CTP 版本发布以来,单个查询快了很多(尽管对于较大的继承层次结构,性能再次下降)。对于具有关联的模型,我们在每个查询中附加“.Include("A00Set")”。为了也获取 B00 实体,我们执行了“E00”、“A10”和“B00”实体之间的连接:

  var q1 = ObjectContext.OSet.Where(o => o.Id == SelectEntityId)
    .Join(ObjectContext.E00Set, o => o.Id, e00 => e00.OId, (o, e00) => e00)
    .Join(ObjectContext.A00Set.OfType<A10>(), e00 => e00.Id, a10 => a10.E00Id, (e00, a10) => a10)
    .Join(ObjectContext.B00Set, a10 => a10.Id, b00 => b00.A10Id, (a10, b00) => b00);
  q1.ToList();

观察此查询对于这个简单数据模型的复杂性。还要注意,当需要获取更多关联数据时(例如,对于 Depth3Assoc2 和 Depth3Assoc3 数据模型),表达此类查询确实会变得非常麻烦。我们已经尝试过替代查询,但这些查询性能更差,而且更冗长。根据 此处此处 的帖子,似乎没有好的解决方案。我欢迎任何能改进上述查询的替代方案。

GraphBasedQuery

GBQ 查询包括定义要从数据库中获取的实体图的形状,然后调用此形状上的“Load()”方法。对于没有关联的模型,这看起来像:

  var shape = new EntityGraphShape4SQL(ObjectContext)
                .Edge<O, E00>(x => x.E00Set);
  shape.Load();

对于具有关联的模型,我们只需将关联作为边添加到图形状中:

  var shape = new EntityGraphShape4SQL(ObjectContext)
                  .Edge<O, E00>(x => x.E00Set)
                  .Edge<E00, A00>(x => x.A00Set)
                  .Edge<A10, B00>(x => x.B00Set);
  shape.Load();

观察我们如何表达“A10”类型的实体应该获取所有“B00”。

数据量

我们为 10 种不同数据量进行了测试,其中“E00”实体的数量从 100 到 1,000 不等。对于每个 E00 的具体子类型,我们创建了相同数量的实例。对于每个 E00 实体,我们创建了 75 个 A00 实例(25 个 A10 实例、25 个 A11 实例和 25 个 A12 实例)。对于 A10 的每个实例,我们创建了 5 个 B00 实例。同样,我们创建了 5 个 C00 实例和 5 个 D00 实例。对于每个数据模型和数据量的组合,我们生成一个单独的数据库。因此,我们的测试跨越 140 个不同的数据库。数据库中的对象数量从 Depth3 模型(见图 4)的 400 个到 Depth6Assoc3 模型(见图 6)的超过 540,000 个不等。表的数量从 8 到 71。

image005a-small.png

图 6. Depth6Assoc3 模型。

测试执行

对于 14 个数据模型中的每一个,我们都有三种不同的查询。这些查询针对每个数据量执行。这意味着我们有 420 个查询结果。

我们为三种查询类型分别运行了独立的基准测试。这为每个数据量提供了 14 个不同的查询。一个查询针对特定数据量的组合构成一个基准测试。这使得每个运行有 140 个不同的基准测试。基准测试的结果构成一个样本。每次运行包含执行 14,000 个基准测试,产生相同数量的样本。平均而言,每个基准测试执行 100 次。

每个基准测试都从 140 个测试集中随机选择。每个测试都包括查询一个“所有者”对象的数据。为了最大限度地减少结果缓存的影响,我们为每个基准测试运行随机选择一个所有者对象。

对于每个基准测试,我们测量完成时间。完成后,我们记录每个基准测试的平均执行时间。根据这些平均数字,我们创建了稍后将讨论的性能图。

EF 在第一次执行查询时需要相当多的资源。为了尽量减少此启动时间在基准测试结果中的影响,我们忽略了每个基准测试第一次运行的时间。GBQ 没有特殊的初始化。为了防止数据自动加载,我们将“LazyLoadingEnabled”标志设置为“false”。

EF 和 GBQ 都有查询计划缓存机制,该机制通过缓存编译后的查询来提高性能。如果禁用此机制,EF 的性能会立即下降,而 GBQ 的性能则不会。因此,我们保持查询计划缓存开启,以使 GBQ 的性能比较更具挑战性。

我们使用 EF 4.1 2011 年 6 月 CTP 版本进行基准测试。此版本包含对 TPT 查询的重大性能改进。

我们在 Dell Latitude E6520 Essential 笔记本电脑上运行基准测试,该笔记本电脑配备 Intel Core I7-2720QM (2.2 GHz) CPU 和 8 GB 内存。在这台计算机上,我们安装了 64 位 Windows 7、Visual Studio 2010 SP1 和 Entity Framework 2011 年 6 月 CTP 版本。对于基准测试,我们不太关心测试运行完成所需的绝对时间,而是关心不同查询、不同数据量和数据模型之间的相对时间差异。最终,我们想研究的是,在什么样的数据模型复杂度/数据量大小时,GBQ 开始显现优势。

测试结果

仅继承的数据模型

本节介绍查询仅继承数据模型的结果。图表显示 GBQ 轻松优于其他两种查询形式。自 EF 4.1 2011 年 6 月 CTP 版本发布以来,EF 单查询在所有测试的数据模型中都比 EF 多查询快得多。在之前的 EF 版本中,情况正好相反。当继承层次结构增长时,两种查询类型的性能都会显著下降。值得注意的是,EFSingleQuery 查询的性能在较大的继承层次结构中会显著下降,以至于与 EFMultiQuery 查询的性能相当。GBQ 的性能对此类数据模型复杂度依赖性要小得多。

Depth 3 模型

image007.png

image008.png

Depth4 模型

image009.png

image010.png

Depth 5 模型

image011.png

image012.png

Depth5Wide 模型

image013.png

image014.png

Depth6 模型

image015.png

image016.png

具有继承和关联的数据模型

如前所述,EF 无法在单个查询中表达获取所有关联。因此,下面的 EFSingleQuery 查询返回不完整的结果。这些查询仅用于说明,在下面的图中包含它们。它们的计时不能用于与其他两种查询类型的有效比较。

下面是 Depth3Assoc1 模型对应的 EFMultiQuery。请注意,需要使用 join 查询来获取与 A10 实体关联的 B00 实体。如果还需要获取更多关联,查询的复杂性会显著增加。另外请注意,我们为每个具体子类型“E20”、“E21”、“E22”和“E23”都有单独的查询。每种都是代码重复的一种形式。当具体类型数量增加时,查询数量也随之增加。

  ObjectContext.E00Set.OfType<E20>().Include("A00Set").Where(x => x.OId == selectedEntityId).ToList();
  ObjectContext.E00Set.OfType<E21>().Include("A00Set").Where(x => x.OId == selectedEntityId).ToList();
  ObjectContext.E00Set.OfType<E22>().Include("A00Set").Where(x => x.OId == selectedEntityId).ToList();
  ObjectContext.E00Set.OfType<E23>().Include("A00Set").Where(x => x.OId == selectedEntityId).ToList();
  var q1 = ObjectContext.OSet.Where(o => o.Id == SelectEntityId)
    .Join(ObjectContext.E00Set, o => o.Id, e00 => e00.OId, (o, e00) => e00)
    .Join(ObjectContext.A00Set.OfType<A10>(), e00 => e00.Id, a10 => a10.E00Id, (e00, a10) => a10)
    .Join(ObjectContext.B00Set, a10 => a10.Id, b00 => b00.A10Id, (a10, b00) => b00);
  q1.ToList();

下面是相应的 GBQ 查询:

  var entity = new O { Id = SelectEntityId };

  var shape = new EntityGraphShape4SQL(ObjectContext)
      .Edge<O, E00>(x => x.E00Set)
      .Edge<E00, A00>(x => x.A00Set)
      .Edge<A10, B00>(x => x.B00Set);
  
  shape.Load(entity);

在此查询中,您只需通过枚举其边来定义关联实体图的形状。请注意,边可以在子类型上定义(例如,从“A10”到“B00”)。此外,与 EF 查询不同,GBQ 查询对于本节中使用的数据模型保持不变。相应的 EF 查询需要进行调整,以添加子类型的附加查询,并为附加关联添加新的 join。

Depth3Assoc1 模型

image017.png

image018.png

Depth3Assoc2 模型

image019.png

image020.png

Depth3Assoc3 模型

image021.png

image022.png

Depth4Assoc1 模型

image023.png

image024.png

Depth5Assoc1 模型

image025.png

image026.png

Depth5WideAssoc1 模型

image027.png

image028.png

Depth6Assoc1 模型

image029.png

image030.png

Depth6Assoc2 模型

image031.png

image032.png

Depth6Assoc3 模型

image033.png

image034.png

摘要

GBQ 是一种绕过 EF 查询机制的替代数据库查询方法。由于生成的实体会被附加到 EF 上下文,因此 GBQ 可以与 EF 一起使用。GBQ 通常在 EF 查询性能不足和/或 EF 查询变得过于复杂时使用。

本报告中的性能图表清楚地表明,GBQ 查询在性能方面很快占据优势。如前所述,EFSingleQuery 查询在关联模型中的性能无法与 EFMultiQuery 和 EFGraphQuery 查询进行比较,因为它们返回的结果不完整。

数据模型和相应的查询仍然相对简单。对于包含许多关联的更复杂数据模型(例如,参见图 3),GBQ 变得特别有吸引力。在这种情况下,EF 的查询性能确实会成为一个问题,而表达和维护这些查询也是如此。

QBG 查询的结果实体会被附加到 EF 对象上下文中。由于我们无法访问对象上下文的任何内部 API,因此我们被迫使用“AttachTo”方法。如果 GBQ 能够以较低级别访问对象上下文,预计可以获得轻微的性能提升。

本报告比较了 GBQ 查询与 EF 查询的性能。我欢迎对改进我所使用的 EF 查询的任何建议。

有关 EntityGraph 和基于图的查询的更多信息,请访问 http://entitygraph.codeplex.com

[1] 本文档中的数据模型使用以下命名方案:Depth<x> 表示“E00”的继承层次结构的深度。Assoc<y> 表示数据模型从“E00”到“A00”有了一个关联。数字“y”表示关联的数量(例如,1 表示从“A10”到“B00”的单个关联,而 2 表示第二个关联,从“A11”到“C00”。

使用基于图的查询改进 Entity Framework 查询性能 - CodeProject - 代码之家
© . All rights reserved.