作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Anton是一名软件开发人员和技术顾问,在桌面分布式应用程序方面拥有10多年的经验.
17
在我的日常工作中,我使用实体框架. 它非常方便,但在某些情况下,它的性能很慢. 尽管有很多关于EF性能改进的好文章, 并给出了一些非常好的和有用的建议(e.g., avoid complex queries,在跳过和采取参数,使用视图,只选择需要的字段等.),当您需要使用complex时,就不能做那么多了 包含
在两个或多个领域,换句话说, 将数据连接到内存列表时.
让我们看看下面的例子:
var localData = getdatafrommapioruser ();
var query = from p in 上下文.价格
join s in 上下文.Securities on
p.安全Id equals s.安全Id
join t in localData on
新{.股票,p.TradedOn, p.价格SourceId } equals
New {t.股票,t.TradedOn, t.价格SourceId }
选择p;
var 结果 = query.ToList ();
上面的代码在EF 6中根本不起作用 做 在EF Core中,连接实际上是在本地完成的,因为我的数据库中有1000万条记录, 所有 它们中的一个被下载,所有的内存被消耗. This is not a bug in EF. It is expected. 然而,如果有办法解决这个问题,那不是很好吗? In this article, 我将用一种不同的方法做一些实验来解决这个性能瓶颈.
我将尝试不同的方法来实现这一点,从最简单的到更高级的. 在每个步骤中,我将提供代码和度量,例如花费的时间和内存使用情况. 注意,如果基准测试程序的运行时间超过10分钟,我将中断它的运行.
基准测试程序的代码位于下面 repository. It uses C#, .. 网。 Core, EF Core和PostgreSQL. 我用的是英特尔酷睿i5、8gb内存和一块固态硬盘.
用于测试的DB模式看起来像这样:
让我们从简单的开始.
var 结果 = new List<价格>();
使用(var 上下文 = CreateContext())
{
foreach (var TestData中的testElement)
{
结果.AddRange(上下文.价格.(在哪里
x => x.安全.股票行情自动收录器 == testElement.股票行情自动收录器 &&
x.TradedOn == testElement.TradedOn &&
x.价格SourceId == testElement.价格SourceId));
}
}
算法很简单:对于测试数据中的每个元素, 在数据库中找到合适的元素,并将其添加到结果集合中. 这段代码只有一个优点:它非常容易实现. 此外,它具有可读性和可维护性. 它明显的缺点是它是最慢的. 尽管所有三个列都有索引, 网络通信的开销仍然会造成性能瓶颈. Here 是 the metrics:
所以,对于一个大体积,大约需要一分钟. 内存消耗似乎是合理的.
现在让我们尝试向代码中添加并行性. 这里的核心思想是,在并行线程中访问数据库可以提高整体性能.
var 结果 = new ConcurrentBag<价格>();
var partitioner = Partitioner.Create(0, TestData.数);
平行.ForEach(partitioner, range =>
{
var subList = TestData.跳过(范围.Item1)
.把(范围.Item2 - range.Item1)
.ToList ();
使用(var 上下文 = CreateContext())
{
foreach (var testElement in subList)
{
var query = 上下文.价格.(在哪里
x => x.安全.股票行情自动收录器 == testElement.股票行情自动收录器 &&
x.TradedOn == testElement.TradedOn &&
x.价格SourceId == testElement.价格SourceId);
foreach (var el in query)
{
结果.添加(el);
}
}
}
});
It is interesting that, for sm所有er test data sets, 这种方法的工作速度比第一种解决方案慢, but for bigger samples, it is faster (approx. 2 times in this instance). 内存消耗有一点变化,但变化不大.
Let’s try another approach:
var 结果 = new List<价格>();
使用(var 上下文 = CreateContext())
{
var tickers = TestData.Select(x => x.股票行情自动收录器).Distinct().ToList ();
var 日期 = TestData.Select(x => x.TradedOn).Distinct().ToList ();
var ps = TestData.Select(x => x.价格SourceId)
.Distinct().ToList ();
var data = 上下文.价格
.(在哪里x => tickers.包含(x.安全.股票行情自动收录器) &&
日期.包含(x.TradedOn) &&
ps.包含(x.价格SourceId))
.Select(x => new {
x.价格SourceId,
价格= x,
股票行情自动收录器 = x.安全.股票,
})
.ToList ();
var lookup = data.ToLookup(x =>
$"{x.股票行情自动收录器}, {x.价格.TradedOn}, {x.价格SourceId}");
foreach (var el in TestData)
{
var key = $"{el.股票行情自动收录器}, {el.TradedOn}, {el.价格SourceId}";
结果.AddRange(lookup[key].Select(x => x.价格);
}
}
This approach is problematic. 执行时间非常依赖于数据. 它可能只检索所需的记录(在这种情况下,它将非常快), 但它可能会返回更多(甚至可能是100倍).
让我们考虑以下测试数据:
这里我查询股票行情自动收录器1在2018-01-01交易的价格和股票行情自动收录器2在2018-01-02交易的价格. 但是,实际上将返回4条记录.
The unique values for 股票行情自动收录器
是 股票行情自动收录器1
和 股票行情自动收录器2
. The unique values for TradedOn
是 2018-01-01
和 2018-01-02
.
因此,有四条记录匹配这个表达式.
这就是为什么需要进行本地复查,以及为什么这种方法是危险的. The metrics 是 as follows:
Awful memory consumption! 由于超时10分钟,具有大卷的测试失败.
让我们改变范式:让我们建立一个好的旧的 Expression
for each test data set.
var 结果 = new List<价格>();
使用(var 上下文 = CreateContext())
{
var baseQuery =从上下文中的p.价格
join s in 上下文.Securities on
p.安全Id equals s.安全Id
select new TestData()
{
股票行情自动收录器 = s.股票,
TradedOn = p.TradedOn,
价格SourceId = p.价格SourceId,
价格Object = p
};
var tradedOnProperty = typeof(TestData).GetProperty("TradedOn");
var priceSourceIdProperty =
typeof(TestData).GetProperty("价格SourceId");
var tickerProperty = typeof(TestData).GetProperty(“股票”);
var paramExpression =表达式.Parameter(typeof(TestData));
Expression wholeClause = null;
foreach (var td in TestData)
{
var elementClause =
Expression.需要说明(
Expression.= (
Expression.MakeMemberAccess(
paramExpression tradedOnProperty),
Expression.Constant(td.TradedOn)
),
Expression.需要说明(
Expression.= (
Expression.MakeMemberAccess(
paramExpression priceSourceIdProperty),
Expression.Constant(td.价格SourceId)
),
Expression.= (
Expression.MakeMemberAccess(
paramExpression tickerProperty),
Expression.Constant(td.股票行情自动收录器)
));
if (wholeClause == null)
wholeClause = elementClause;
其他的
wholeClause = Expression.OrElse (wholeClause elementClause);
}
var query = baseQuery.(在哪里
(Expression>)Expression.λ(
wholeClause, paramExpression)).Select(x => x.价格Object);
结果.AddRange(query);
}
生成的代码相当复杂. 构建表达式并不是一件容易的事情,它涉及到反思, 本身, is not that fast). 但是它可以帮助我们使用大量的 … (.. 和 .. 和 ..) OR (.. 和 .. 和 ..) OR (.. 和 .. 和 ..) ...
. These 是 the 结果s:
甚至比前两种方法都糟糕.
Let’s try one more approach:
我在数据库中添加了一个新表,用于保存查询数据. For each query I can now:
var 结果 = new List<价格>();
使用(var 上下文 = CreateContext())
{
上下文.数据库.BeginTransaction();
var reducedData = TestData.Select(x => new 分享dQueryModel()
{
价格SourceId = x.价格SourceId,
股票行情自动收录器 = x.股票,
TradedOn = x.TradedOn
}).ToList ();
//查询数据存储在共享表中
上下文.QueryData分享d.AddRange(reducedData);
上下文.SaveChanges();
var query = from p in 上下文.价格
join s in 上下文.Securities on
p.安全Id equals s.安全Id
join t in 上下文.QueryData分享d on
新{.股票,p.TradedOn, p.价格SourceId } equals
New {t.股票,t.TradedOn, t.价格SourceId }
选择p;
结果.AddRange(query);
上下文.数据库.RollbackTransaction();
}
Metrics first:
The 结果 is very good. 非常快. Memory consumption is also good. But the drawbacks 是:
但除此之外,这种方法还不错——快速且易读. 在本例中缓存了一个查询计划!
这里我将使用一个NuGet包 EntityFrameworkCore.MemoryJoin. 尽管它的名字中有Core这个词,但它也支持EF 6. It is c所有ed MemoryJoin, but in fact, 它将指定的查询数据作为VALUES发送到服务器,所有工作都在SQL服务器上完成.
Let’s check the code.
var 结果 = new List<价格>();
使用(var 上下文 = CreateContext())
{
//最好只选择需要的属性,以获得更好的性能
var reducedData = TestData.Select(x => new {
x.股票,
x.TradedOn,
x.价格SourceId
}).ToList ();
var queryable = 上下文.FromLocalList(reducedData);
var query = from p in 上下文.价格
join s in 上下文.Securities on
p.安全Id equals s.安全Id
join t in queryable on
新{.股票,p.TradedOn, p.价格SourceId } equals
New {t.股票,t.TradedOn, t.价格SourceId }
选择p;
结果.AddRange(query);
}
指标:
This looks awesome. 比之前的方法快三倍,这是迄今为止最快的方法. 3.5 seconds for 64K records! 代码简单易懂. 这适用于只读副本. 让我们检查为三个元素生成的查询:
SELECT "p"."价格Id",
"p"."Close价格",
"p"."Open价格",
"p"."价格SourceId",
"p"."安全Id",
"p"."TradedOn",
"t".“股票”,
"t"."TradedOn",
"t"."价格SourceId"
从 "价格" AS "p"
内连接"安全" AS "s" ON "p"."安全Id" = "s"."安全Id"
INNER JOIN
( SELECT "x"."string1" AS “股票”,
"x"."date1" AS "TradedOn",
铸造(“x”."long1"作为"价格SourceId"
从
( SELECT *
从(
值(1 @__gen_q_p0 @__gen_q_p1 @__gen_q_p2),
(2, @__gen_q_p3, @__gen_q_p4, @__gen_q_p5),
(3、@__gen_q_p6, @__gen_q_p7, @__gen_q_p8)
) AS __gen_query_data__ (id, string1, date1, long1)
)为“x”
) AS "t" ON (("s"."股票行情自动收录器" = "t".“股票”)
(“p”."价格SourceId" = "t"."价格SourceId")
As you can see, 这一次,实际值通过values构造从内存传递到SQL服务器. 这就达到了目的:SQL服务器成功地执行了快速连接操作并正确地使用了索引.
然而,也有一些缺点(你可以在我的 博客):
在我测试过的东西中,我肯定会选择MemoryJoin. 其他人可能会反对说这些缺点是无法克服的, 因为目前并不是所有的问题都能解决, 我们应该避免使用延期. 对我来说,这就像说你不应该用刀,因为你可能会割伤自己. 优化不是初级开发人员的任务,而是那些了解EF工作原理的人的任务. 为此,该工具可以显著提高性能. 谁知道? 也许有一天,微软的某个人会为动态值添加一些核心支持.
最后,这里还有一些图表来比较结果.
下面是执行操作所需时间的图表. MemoryJoin是唯一能在合理时间内完成这项工作的方法. 只有四种方法可以处理大容量:两种幼稚的实现, sh是d table, 和 MemoryJoin.
下一个图表是关于内存消耗的. 所有的方法都或多或少地证明了相同的数字,除了一个有多个 包含
. 这种现象在上面已经描述过了.
DBSet是一个抽象概念,它实际上是存储在表中的对象集合(通常是延迟加载的). 在DBSet上执行的操作实际上是通过SQL查询在实际数据库记录上执行的.
实体框架是一个对象关系映射框架, 它提供了一个标准接口,用于访问存储在(不同厂商的)关系数据库中的数据。.
代码优先方法意味着开发人员在创建实际的数据库之前首先创建模型类. 最大的优点之一是将数据库模型存储在源代码控制系统中.
Located in Tomsk, Tomsk Oblast, Russia
Member since December 8, 2014
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® 社区.