EF查询百万级数据的性能测试--多表链接复杂查询

相关文章:EF查询百万级数据的性能测试--单表查询html

1、原由 web

上次作的是EF百万级数据的单表查询,总结了一下,在200w如下的数据量的状况(Sql Server 2012),EF是可使用,可是因为查询条件过于简单,且是单表查询,EF只是负责生成Sql语句,对于一些简单的查询,生成Sql语句的时间能够基本忽略,因此不只没有发挥出EF的优点,并且这样的性能瓶颈基本能够说是和数据库彻底有关的,这个锅数据库得背(数据库:怪我了)。鉴于实际项目中可能是多表的链接查询,还有其余复杂的查询,一贯本着求真务实的思想的博主就趁此机会再次测试了一下EF的复杂的链接查询什么的。说实话,在测试以前我也不知道结果,只是为了本身之后用起来有个参考依据,也比老是听别人说EF性能不好,吓得都不敢用了要好。EF的性能到底有多差,或者说能够胜任什么样的场景,不吹不黑,咱们就一块儿来看看,也好在之后的实际项目选型的时候参考一下。ajax

2、关于不少ORM框架的对比测试sql

博主最近也看了很多关于ORM框架的测试,大多数都是增删改几千,几万条的数据,这样确实能够看出来性能的比较,可是实际项目中真的不多有这样的状况,一次增删改几千几万条数据的,咱们作项目服务的都是用户,按用户的一次请求为一次数据库上下文的操做,同一个上下文在这样的一次请求中基本不可能同时提交这么多的数据操做,有人说那要是成千上万的用户同时呢,那就要考虑并发了,就不是本文所要讨论的问题了。因此这些测试能代表结果,可是不能代表实际问题。另外在大多数对于EF的测试中,不少人忽略了EF对于实体的跟踪,好比:
数据库

这些属性虽然我不全知道是什么的东西,可是既然能够设置Enabled,就说明是对性能有影响的,并且数据量越多,相信影响也越大,但其余多数ORM应该都没有这些功能或者设置(我不知道,哈哈),因此对于增删改的操做,我以为当前状况下是彻底够用的,因此再也不探究增删改的性能(若是实在有朋友以为必要,博主再找机会)。EF的初衷,也能够说是不少ORM应该具有的出发点,就是从之前的很是不OO的数据操做方式,变成如今的OO的方式,就是为了解放开发人员写Sql查询操做数据库的方式,就是要用面向对象的思想来操做数据库,结果倒好,有些人又要回到之前写Sql语句,又要去回到解放前,这就比如 面向过程编程 效率很高速度很快,可是为何要提出面向对象编程,由于面向过程写起来累啊!很差维护啊!很差扩展啊!不方便啊,还有分层架构,不都是为了这吗,这些东西咱们应该是发挥它的优点,知道他在什么状况下用,什么状况下不用,而不是一直死死的抓住他的缺点说不行。固然,有不少状况下是不追求生产效率,只追求性能的,那就不说了。
编程

说了这么多,我也不是想证实什么,我只是想知道,我该什么状况下用EF,怎么用EF来发挥出他的优点,怎么能用好EF,应用到实际生产环境中。一句话,为何个人眼里常含泪水,由于我对EF爱的深沉。(斜眼笑)浏览器

 3、准备工做服务器

那确定是先建表结构和数据了,废话很少说,上图先。
网络

1.关系图架构

这是数据库的关系图,只有User和Role是多对多关系,其余的是一对多,另外都加了导航属性,博主事先用的是Code First,已经添加了导航属性,为的是能够在后来的测试中使用导航属性(EF会自动根据导航属性生成链接查询,能够由此来作测试),这里借用了Database First来从数据库生成了模型图,为的是你们可以清楚的看表之间的关系。

简单说明一下:

  一个User对应多个Order;

  一个Order对应多个OrderDetail,对应一个City;

  一个OrderDetail至关于一个产品,对应一个产品类型Category。

 其中因为多对多的关系比较少见,且能够转化为两个 一对多的关系(Sql Server就是这么干的),因此此次暂时不作多对多的测试,应该和一对多差很少。

2.表数据

这里城市表 是如今项目中用的一个,由于以前就三个字段Id,Name,ParentId,而后要找其余数据就要递归查询,很浪费时间,后来想了想既然都是死数据,就一下给写进去,以后再用就不用查了。

附上City表的Sql文件,有须要的同窗能够带走:dbo.City.Table.zip

在某东首页复制的商品类型数据。。

3.数据量

用户表,订单表,订单明细表都是100w的数据,其余两个表按实际状况来,类型表没有再细分,就这样吧。

4、开始测试

1.关于Sql语句生成的时间

因为大多数人都说EF的性能瓶颈在生成Sql的时间和质量上,引用一位朋友的回答以下:

上边这条评论的第二条说的应该就是质量的问题,关于EF生成Sql语句有什么规则,或者怎样才能生成高质量的Sql,这个内容也是一个很值得研究的问题,咱们随后有时间研究。今天咱们就只针对生成Sql语句的时间上加以探究。

在网上搜索了一些资料,关于怎么测试EF生成Sql的时间,博主没有见到过相关的测试,可是怎样获取到生成的Sql语句仍是有办法的,因此,博主想了想,既然能获取到sql语句,那么这个获取的过程就能够做为生成Sql的时间,因为没有相关的资料说明,因此暂且用这样的方法来测,博主使用的两种比较笨的方法测试生成的时间,也但愿园友们若是有更好的方法能够告诉博主

1.ToString()方法

因为在IQueryable接口中重写了ToString()方法,因此博主试了一下,果然能获取到Sql语句,因此就用ToString()方法的执行时间当作生成Sql语句的时间。先来个简单的:

能够看出已经生成了Sql(注意:这里并无去数据库查询,只是生成了Sql)涉及到了最简的两个表的连接,那咱们接下来看生成所用的时间。

能够看出来,生成Sql的时间很是短,彻底能够忽略不计,可能博友以为Sql过于简单,不要紧,咱们再来几个复杂的

复杂语句一,涉及到了四个表的连接:

依旧不多时间,只是略比上一个Sql的时间长一点,毕竟复杂了一点。

复杂语句二,直接截图了,这里为了生成Sql语句的复杂,随便写了一些Linq,可能不是咱们平常想要的结果,只是为了复杂而已:

时间明显变长,可是依旧不到1ms,附上生成的Sql语句,够复杂了吧。    

 1 SELECT 
 2     [Project7].[C1] AS [C1], 
 3     [Project7].[Work] AS [Work], 
 4     [Project7].[C2] AS [C2]
 5     FROM ( SELECT 
 6         [Project6].[Work] AS [Work], 
 7         1 AS [C1], 
 8         [Project6].[C1] AS [C2]
 9         FROM ( SELECT 
10             [Project3].[Work] AS [Work], 
11             (SELECT 
12                 MAX([Project5].[Amount]) AS [A1]
13                 FROM ( SELECT 
14                     [Extent11].[Id] AS [Id], 
15                     [Extent11].[Amount] AS [Amount], 
16                     [Filter4].[UserId] AS [UserId]
17                     FROM   (SELECT [Project4].[UserId] AS [UserId], [Project4].[FullName] AS [FullName], [Project4].[UserName] AS [UserName1], [Extent10].[Work] AS [Work]
18                         FROM   (SELECT 
19                             [Extent6].[UserId] AS [UserId], 
20                             [Extent7].[FullName] AS [FullName], 
21                             [Extent8].[UserName] AS [UserName], 
22                             (SELECT 
23                                 SUM([Extent9].[TotalPrice]) AS [A1]
24                                 FROM [dbo].[OrderDetail] AS [Extent9]
25                                 WHERE [Extent6].[Id] = [Extent9].[OrderId]) AS [C1]
26                             FROM   [dbo].[Order] AS [Extent6]
27                             INNER JOIN [dbo].[City] AS [Extent7] ON [Extent6].[CityId] = [Extent7].[Id]
28                             INNER JOIN [dbo].[User] AS [Extent8] ON [Extent6].[UserId] = [Extent8].[Id] ) AS [Project4]
29                         LEFT OUTER JOIN [dbo].[User] AS [Extent10] ON [Project4].[UserId] = [Extent10].[Id]
30                         WHERE [Project4].[C1] > cast(500 as decimal(18)) ) AS [Filter4]
31                     LEFT OUTER JOIN [dbo].[User] AS [Extent11] ON [Filter4].[UserId] = [Extent11].[Id]
32                     WHERE ([Filter4].[FullName] LIKE @p__linq__0 ESCAPE N'~') AND (([Filter4].[UserName1] = @p__linq__1) OR (([Filter4].[UserName1] IS NULL) AND (@p__linq__1 IS NULL))) AND (([Project3].[Work] = [Filter4].[Work]) OR (([Project3].[Work] IS NULL) AND ([Filter4].[Work] IS NULL)))
33                 )  AS [Project5]) AS [C1]
34             FROM ( SELECT 
35                 [Distinct1].[Work] AS [Work]
36                 FROM ( SELECT DISTINCT 
37                     [Extent5].[Work] AS [Work]
38                     FROM   (SELECT 
39                         [Extent1].[UserId] AS [UserId], 
40                         [Extent2].[FullName] AS [FullName], 
41                         [Extent3].[UserName] AS [UserName], 
42                         (SELECT 
43                             SUM([Extent4].[TotalPrice]) AS [A1]
44                             FROM [dbo].[OrderDetail] AS [Extent4]
45                             WHERE [Extent1].[Id] = [Extent4].[OrderId]) AS [C1]
46                         FROM   [dbo].[Order] AS [Extent1]
47                         INNER JOIN [dbo].[City] AS [Extent2] ON [Extent1].[CityId] = [Extent2].[Id]
48                         INNER JOIN [dbo].[User] AS [Extent3] ON [Extent1].[UserId] = [Extent3].[Id] ) AS [Project1]
49                     LEFT OUTER JOIN [dbo].[User] AS [Extent5] ON [Project1].[UserId] = [Extent5].[Id]
50                     WHERE ([Project1].[C1] > cast(500 as decimal(18))) AND ([Project1].[FullName] LIKE @p__linq__0 ESCAPE N'~') AND (([Project1].[UserName] = @p__linq__1) OR (([Project1].[UserName] IS NULL) AND (@p__linq__1 IS NULL)))
51                 )  AS [Distinct1]
52             )  AS [Project3]
53         )  AS [Project6]
54     )  AS [Project7]
55     ORDER BY [Project7].[Work] ASC

复杂语句三,再来一个看看,用到了分页。

此次因为比较复杂,因此生成Sql也花费了一些时间,能够看出来已经到的四、5ms左右,可是生成的Sql确比上次的少。   

SELECT 
    [Project3].[Id] AS [Id], 
    [Project3].[UserName] AS [UserName], 
    [Project3].[Name] AS [Name], 
    [Project3].[Amount] AS [Amount], 
    [Project3].[C1] AS [C1]
    FROM ( SELECT 
        [Project2].[Id] AS [Id], 
        [Project2].[UserName] AS [UserName], 
        [Project2].[Amount] AS [Amount], 
        [Project2].[Name] AS [Name], 
        [Project2].[C1] AS [C1]
        FROM ( SELECT 
            [Project1].[Id] AS [Id], 
            [Extent5].[UserName] AS [UserName], 
            [Extent5].[Amount] AS [Amount], 
            [Extent6].[Name] AS [Name], 
            (SELECT 
                COUNT(1) AS [A1]
                FROM [dbo].[OrderDetail] AS [Extent7]
                WHERE [Project1].[Id] = [Extent7].[OrderId]) AS [C1]
            FROM    (SELECT 
                [Extent1].[Id] AS [Id], 
                [Extent1].[UserId] AS [UserId], 
                [Extent1].[CityId] AS [CityId], 
                [Extent2].[FullName] AS [FullName], 
                [Extent3].[UserName] AS [UserName], 
                (SELECT 
                    SUM([Extent4].[TotalPrice]) AS [A1]
                    FROM [dbo].[OrderDetail] AS [Extent4]
                    WHERE [Extent1].[Id] = [Extent4].[OrderId]) AS [C1]
                FROM   [dbo].[Order] AS [Extent1]
                INNER JOIN [dbo].[City] AS [Extent2] ON [Extent1].[CityId] = [Extent2].[Id]
                INNER JOIN [dbo].[User] AS [Extent3] ON [Extent1].[UserId] = [Extent3].[Id] ) AS [Project1]
            LEFT OUTER JOIN [dbo].[User] AS [Extent5] ON [Project1].[UserId] = [Extent5].[Id]
            LEFT OUTER JOIN [dbo].[City] AS [Extent6] ON [Project1].[CityId] = [Extent6].[Id]
            WHERE ([Project1].[C1] > cast(500 as decimal(18))) AND ([Project1].[FullName] LIKE @p__linq__0 ESCAPE N'~') AND (([Project1].[UserName] = @p__linq__1) OR (([Project1].[UserName] IS NULL) AND (@p__linq__1 IS NULL)))
        )  AS [Project2]
        WHERE ([Project2].[Amount] > cast(50 as decimal(18))) AND ([Project2].[Amount] < cast(500 as decimal(18)))
    )  AS [Project3]
    ORDER BY [Project3].[Amount] DESC
    OFFSET 28 ROWS FETCH NEXT 14 ROWS ONLY

2.和数据库的时间对比

这是博主又想到的一个笨方法,就是点击按钮的时候记下当前的时间,而后去数据库的Profile里边获取监视到的开始时间,由于这里考虑的网络传输Sql语句的时间,可是因为是本机传送,因此应该不会耗费不少时间,那么咱们就来对比一下,也就能够大体估算出生成sql语句所用的时间了。以下图:

下面来看统计结果:

预期结果为差值大于后边的生成sql的时间(确定的啊),里边有两次时间为负,多是其余缘由致使的 客户端开始时间记录产生的偏差,从这里能够看出 ,由于生成sql的时间必然要小于差值,因此生成sql的时间仍是很短的。

再来看一张图:

从下边的结果能够看出,传输时间相对于生成sql的时间仍是挺长的,这也再一次说明了,EF生成sql语句的时间很短,几乎能够忽略。因此EF的性能瓶颈能够排除在生成的sql语句时间长上。

2.查询数据

下面咱们就根据实际的业务须要查询一波数据,看看结果到底怎么样。代码以下:

需求1:查询最近六个月下单的用户的部分信息(用户名,余额,下单日期),并按照下单日期排序进行分页(涉及到两个100w数据表的连接User表和Order表)

生成sql语句,中规中矩。

 1 SELECT 
 2     [Project1].[UserId] AS [UserId], 
 3     [Project1].[UserName] AS [UserName], 
 4     [Project1].[Amount] AS [Amount], 
 5     [Project1].[OrderDate] AS [OrderDate]
 6     FROM ( SELECT 
 7         [Extent1].[UserId] AS [UserId], 
 8         [Extent1].[OrderDate] AS [OrderDate], 
 9         [Extent2].[UserName] AS [UserName], 
10         [Extent2].[Amount] AS [Amount]
11         FROM  [dbo].[Order] AS [Extent1]
12         INNER JOIN [dbo].[User] AS [Extent2] ON [Extent1].[UserId] = [Extent2].[Id]
13         WHERE [Extent1].[OrderDate] > @p__linq__0
14     )  AS [Project1]
15     ORDER BY [Project1].[OrderDate] DESC
16     OFFSET 2000 ROWS FETCH NEXT 20 ROWS ONLY 

代码以下:

查询结果以下:

能够看出来表现很不错,时间大概在70ms左右,是很是能够接受的。至于这里为何生成sql的时间长了,那是由于在生成sql的前边作了一次Count查询,因此这里的生成sql的时间是无效的。前边已经证实过生成sql的时间是能够忽略不计的。

需求2:查询最近六个月订单总金额大于1000的订单,获取用户和订单详情的部分信息,并按照下单日期排序进行分页(涉及到三个100w数据表的连接User表和Order表、OrderDetail表)


生成的sql:

 1 SELECT 
 2     [Project4].[Id] AS [Id], 
 3     [Project4].[UserName] AS [UserName], 
 4     [Project4].[Amount] AS [Amount], 
 5     [Project4].[OrderDate] AS [OrderDate], 
 6     [Project4].[C1] AS [C1], 
 7     [Project4].[C2] AS [C2]
 8     FROM ( SELECT 
 9         [Project3].[Id] AS [Id], 
10         [Project3].[OrderDate] AS [OrderDate], 
11         [Project3].[UserName] AS [UserName], 
12         [Project3].[Amount] AS [Amount], 
13         [Project3].[C1] AS [C1], 
14         [Project3].[C2] AS [C2]
15         FROM ( SELECT 
16             [Project2].[Id] AS [Id], 
17             [Project2].[OrderDate] AS [OrderDate], 
18             [Project2].[UserName] AS [UserName], 
19             [Project2].[Amount] AS [Amount], 
20             [Project2].[C1] AS [C1], 
21             (SELECT 
22                 SUM([Extent5].[TotalPrice]) AS [A1]
23                 FROM [dbo].[OrderDetail] AS [Extent5]
24                 WHERE [Project2].[Id] = [Extent5].[OrderId]) AS [C2]
25             FROM ( SELECT 
26                 [Project1].[Id] AS [Id], 
27                 [Project1].[OrderDate] AS [OrderDate], 
28                 [Extent3].[UserName] AS [UserName], 
29                 [Extent3].[Amount] AS [Amount], 
30                 (SELECT 
31                     COUNT(1) AS [A1]
32                     FROM [dbo].[OrderDetail] AS [Extent4]
33                     WHERE [Project1].[Id] = [Extent4].[OrderId]) AS [C1]
34                 FROM   (SELECT 
35                     [Extent1].[Id] AS [Id], 
36                     [Extent1].[UserId] AS [UserId], 
37                     [Extent1].[OrderDate] AS [OrderDate], 
38                     (SELECT 
39                         SUM([Extent2].[TotalPrice]) AS [A1]
40                         FROM [dbo].[OrderDetail] AS [Extent2]
41                         WHERE [Extent1].[Id] = [Extent2].[OrderId]) AS [C1]
42                     FROM [dbo].[Order] AS [Extent1] ) AS [Project1]
43                 LEFT OUTER JOIN [dbo].[User] AS [Extent3] ON [Project1].[UserId] = [Extent3].[Id]
44                 WHERE ([Project1].[OrderDate] > @p__linq__0) AND ([Project1].[C1] > cast(1000 as decimal(18)))
45             )  AS [Project2]
46         )  AS [Project3]
47     )  AS [Project4]
48     ORDER BY [Project4].[OrderDate] DESC
49     OFFSET 2080 ROWS FETCH NEXT 20 ROWS ONLY

查询结果:

查询用了330ms左右,仍是能够接受的。

需求3:查询订单总价格大于1000的数据,并按时间降续排列,取前10000条的用户的部分信息,而且对着10000条按帐户余额排序,再进行分页处理。

这里能够说是链接了四个表的(User表,Order表,OrderDetail表,City表),其中三个表都是100w的数据


查询了十次,咱们来看查询时间

已经1s多的时间,能够说是有点慢了。(注意,这里在查询出来以前先是按日期排序再取10000条,这个排序是很耗费性能的,这里也是一个咱们之后须要优化的地方)可是,对,说到可是了,因而乎,楼主把生成的sql语句复制到数据库中直接查询,结果也是很长的。

因此说,这应该是数据库方面的问题的,这里确定不是EF生成sql语句的时间问题,前边已经说明过了,至因而不是EF生成的sql语句的质量问题,我就不知道了。

5、关于时间概念

咱们作的产品或者项目都是服务于用户的,因此咱们要以用户的角度看待问题,那就是用户的体验问题。

1.关于页面的响应时间,引用了网上的一点资料,百度的标准是3s如下,咱们暂且定为2s如下,以Asp.Net Mvc为例 若是咱们在控制器里拿数据并渲染到页面上,拿数据时间应该在1s(1000ms)如下才能够。

2.如今愈来愈流行单页面web应用,因此通常都是ajax请求异步拿数据,首先说明一点,拿数据最耗时的就是在数据库里的查询,传输时间也有,可是在如今这么高的带宽下,彻底能够忽略不计,可是说也是白说,你们仍是以实际中的体验来作标准吧

园子里的博客分页应该是异步加载,就以此为例看看。

1.700ms左右的体验:

2.300ms左右的体验:

 


3.200ms左右的体验: 

4.100ms左右的体验:

具体体验你们能够亲自感觉一下,谷歌浏览器调试工具能够设置当前网速,博主本着求真务实的思想,认为实际项目中若是不是很是很是注重用户的体验,咱们的拿数据的时间能够控制在250ms如下也是能够接受的,100ms如下的时间已是有点浪费了,在这里是给你们一个时间概念参考一下。

按着这个标准,我感受EF在百万级的数据下仍是很是能够接受的,毕竟博主测试的都是本身的电脑,实际项目运行在服务器上,服务器的配置确定是至关高的,确定也会提升很多性能。

 6、总结

1.EF能够说是不存在生成sql语句时间长方面的瓶颈,至于生成sql语句的质量,可能真的有性能影响,可是这些东西也是开发人员写的,因此这个锅EF仍是不能背,还应该是开发人员的锅。

2.对于简单的链接查询,EF生成的sql语句应该不存在质量问题,应该和开发人员写的差很少,可是对于复杂的查询,EF确实生成了一大堆的sql语句,可是开发人员面对这么复杂的查询,还不必定能写出来呢(反正我如今是写不出来),即便花费一上午写了出来,那么再花费一下午调试,一天过去了,这时候你对大家经理说,我考虑到性能问题,不想用自动生成的sql语句。那么你基本能够卷铺盖走人了。(哈哈),因此基于这个角度,我以为仍是乖乖用生成的sql查询吧。

3.对于百万级以上的数据,表链接最好控制在3个之内,我这里不是针对EF,是针对全部在座的数据库。(请自动脑补星爷电影里的桥段)

4.本文只作测试功能,可能会有一些误差,你们用时仍是请以实际项目为准。毕竟有博友几百万的数据链接查询也一样高效:

 5.关于怎么用EF写出高效的查询,我相信这也是一个很值得研究的话题,之后有时间的话博主还会继续研究,关于这方面但愿你们也踊跃为博主提供一些资料,也但愿有作DBA的朋友提出一些sql语句方面的优化建议,毕竟博主也是只能一个个试来试去。

 6.仍是那句话,我只是想知道,我该什么状况下用EF,怎么用EF来发挥出他的优点,怎么能用好EF,应用到实际生产环境中。也为更多的喜欢EF的人和不了解EF的人提供一些帮助。

附:转载请注明出处,楼主一个一个测试也是很不容易,感谢你们的支持。 

相关文章
相关标签/搜索