EF查詢百萬級數據的性能測試--多表連接復雜查詢


相關文章:EF查詢百萬級數據的性能測試--單表查詢

一、起因 

上次做的是EF百萬級數據的單表查詢,總結了一下,在200w以下的數據量的情況(Sql Server 2012),EF是可以使用,但是由於查詢條件過於簡單,且是單表查詢,EF只是負責生成Sql語句,對於一些簡單的查詢,生成Sql語句的時間可以基本忽略,所以不僅沒有發揮出EF的優勢,而且這樣的性能瓶頸基本可以說是和數據庫完全有關的,這個鍋數據庫得背(數據庫:怪我了)。鑒於實際項目中多是多表的連接查詢,還有其他復雜的查詢,一向本着求真務實的思想的博主就趁此機會再次測試了一下EF的復雜的連接查詢什么的。說實話,在測試之前我也不知道結果,只是為了自己以后用起來有個參考依據,也比總是聽別人說EF性能很差,嚇得都不敢用了要好。EF的性能到底有多差,或者說可以勝任什么樣的場景,不吹不黑,我們就一起來看看,也好在以后的實際項目選型的時候參考一下。

二、關於很多ORM框架的對比測試

博主最近也看了不少關於ORM框架的測試,大多數都是增刪改幾千,幾萬條的數據,這樣確實可以看出來性能的比較,但是實際項目中真的很少有這樣的情況,一次增刪改幾千幾萬條數據的,我們做項目服務的都是用戶,按用戶的一次請求為一次數據庫上下文的操作,同一個上下文在這樣的一次請求中基本不可能同時提交這么多的數據操作,有人說那要是成千上萬的用戶同時呢,那就要考慮並發了,就不是本文所要討論的問題了。所以這些測試能表明結果,但是不能表明實際問題。另外在大多數對於EF的測試中,很多人忽略了EF對於實體的跟蹤,比如:

這些屬性雖然我不全知道是什么的東西,但是既然可以設置Enabled,就說明是對性能有影響的,而且數據量越多,相信影響也越大,但其他多數ORM應該都沒有這些功能或者設置(我不知道,哈哈),所以對於增刪改的操作,我覺得當前情況下是完全夠用的,所以不再探究增刪改的性能(如果實在有朋友覺得必要,博主再找機會)。EF的初衷,也可以說是很多ORM應該具備的出發點,就是從以前的非常不OO的數據操作方式,變成現在的OO的方式,就是為了解放開發人員寫Sql查詢操作數據庫的方式,就是要用面向對象的思想來操作數據庫,結果倒好,有些人又要回到以前寫Sql語句,又要去回到解放前,這就好比 面向過程編程 效率很高速度很快,但是為什么要提出面向對象編程,因為面向過程寫起來累啊!不好維護啊!不好擴展啊!不方便啊,還有分層架構,不都是為了這嗎,這些東西我們應該是發揮它的優勢,知道他在什么情況下用,什么情況下不用,而不是一直死死的抓住他的缺點說不行。當然,有很多情況下是不追求生產效率,只追求性能的,那就不說了。

說了這么多,我也不是想證明什么,我只是想知道,我該什么情況下用EF,怎么用EF來發揮出他的優勢,怎么能用好EF,應用到實際生產環境中。一句話,為什么我的眼里常含淚水,因為我對EF愛的深沉。(斜眼笑)

 三、准備工作

那肯定是先建表結構和數據了,廢話不多說,上圖先。

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的數據,其他兩個表按實際情況來,類型表沒有再細分,就這樣吧。

四、開始測試

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也花費了一些時間,可以看出來已經到的4、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語句的質量問題,我就不知道了。

五、關於時間概念

我們做的產品或者項目都是服務於用戶的,所以我們要以用戶的角度看待問題,那就是用戶的體驗問題。

1.關於頁面的響應時間,引用了網上的一點資料,百度的標准是3s以下,我們暫且定為2s以下,以Asp.Net Mvc為例 如果我們在控制器里拿數據並渲染到頁面上,拿數據時間應該在1s(1000ms)以下才可以。

2.現在越來越流行單頁面web應用,所以一般都是ajax請求異步拿數據,首先說明一點,拿數據最耗時的就是在數據庫里的查詢,傳輸時間也有,但是在現在這么高的帶寬下,完全可以忽略不計,但是說也是白說,大家還是以實際中的體驗來做標准吧

園子里的博客分頁應該是異步加載,就以此為例看看。

1.700ms左右的體驗:

2.300ms左右的體驗:

 


3.200ms左右的體驗: 

4.100ms左右的體驗:

具體體驗大家可以親自感受一下,谷歌瀏覽器調試工具可以設置當前網速,博主本着求真務實的思想,認為實際項目中如果不是非常非常注重用戶的體驗,我們的拿數據的時間可以控制在250ms以下也是可以接受的,100ms以下的時間已經是有點浪費了,在這里是給大家一個時間概念參考一下。

按着這個標准,我感覺EF在百萬級的數據下還是非常可以接受的,畢竟博主測試的都是自己的電腦,實際項目運行在服務器上,服務器的配置肯定是相當高的,肯定也會提高不少性能。

 六、總結

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的人提供一些幫助。

附:轉載請注明出處,樓主一個一個測試也是很不容易,感謝大家的支持。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM