使用Django從資料庫中隨機取N條記錄的不同方法及其效能實測

熊鐸發表於2013-12-06

【宣告】:本文中的實驗僅限於特定資料庫和特定框架。不同資料庫,資料庫伺服器的效能,甚至同一個資料庫的不同配置都會影響到同一段程式碼的效能。具體情況請在自己的生產環境進行測試。

這裡(stackoverflow)有一篇關於使用Django隨機獲取記錄的討論。主要意思是說

這樣獲取2個記錄會導致效能問題,原因如下:

“ 對於有著相當多數量記錄的表來說,這種方法異常糟糕。這會導致一個 ORDER BY RAND() 的SQL查詢。舉個栗子,這裡是MYSQL是如何處理這個查詢的(其他資料庫的情況也差不多),想象一下當一個表有十億行的時候會怎樣:

  1. 為了完成ORDER BY RAND() ,需要一個RAND()列來排序
  2. 為了有RAND()列,需要一個新表,因為現有的表沒有這個列。
  3. 為了這個新表,mysql建立了一個帶有新列的,新的臨時表,並且將已有的一百萬行資料複製進去。
  4. 當其新建完了,他如你所要求的,為每一行執行RAND()函式來填上這個值。是的,你派mysql建立一百萬個隨機數,這要點時間:)
  5. 幾個小時或幾天後,當他幹完這活,他要排序。是的,你排mysql去排序一個一百萬行的,最糟糕的表(說他最糟糕是因為排序的鍵是隨機的)。
  6. 幾天或者幾星期後,當排序完了,他忠誠地將你實際需要的可憐的兩行抓出來返回給你。做的好。;)

注意:只是稍微說一句,得注意到mysql一開始會試著在記憶體中建立臨時表。當記憶體不夠了,他將會把所有東西放在硬碟上,所以你會因為近乎於整個過程中的I/O瓶頸而雪上加霜。

懷疑者可以去看看python程式碼引起的查詢語句,確認是ORDER BY RAND(), 然後去Google下”order by rand()”(帶上引號)。

一個更好的方式是將這個耗費嚴重的查詢換成3個耗費更輕的:

如上Manganeez所說的方法,相應的獲取n條記錄的程式碼應該如下:

基於Python程式碼應該簡潔優雅的想法,如上的程式碼似乎可以寫成:

就效能問題,請教了stackoverflow上的大神 (雖然被踩和被教育了=。=)

Record.objects.count() 將被轉換成一個相當輕量級的SQL請求:

Record.objects.all()[0]也會被轉換成一個十分輕量級的SQL請求

Querying all 是一個耗費十分嚴重的請求

通常情況下Django會不顯示其他的結果,這樣你不會真正的獲取到所有的記錄。

任何時候你將一個Queryset轉換成list的時候,將是資源消耗嚴重的時候。

如果我沒錯的話,在這個例子裡,sample方法將把Queryset轉換成list。

這樣如果你result = random.sample(Record.objects.all(),n) 這樣做的話,全部的Queryset將會轉換成list,然後從中隨機選擇。

想象一下如果你有十億行的資料。你是打算把它儲存在一個有百萬元素的list中,還是願意一個一個的query?

在上邊Yeo的回答中,freakish回覆道:.count的效能是基於資料庫的。而Postgres的.count為人所熟知的相當之慢。

某人說過,要知道梨子的滋味,就得變革梨子,親口嘗一嘗。

測試環境:

  • Win8.1 pro x64
  • Wampserver2.4-x64 (apache2.4.4 mysql5.6.12 php5.4.12)
  • Python2.7.5
  • Django1.4.6

在一個已有的測試project中新建一個app,資料庫是MYSQL:

在models.py中新增模型:

新增一萬行資料:

15分鐘以後我得到了這個MYSQL表。真的,不騙你,真的是15分鐘。看了記錄才知道 每次save都要呼叫一次insert和一次update。。。。下次一定用SQL語句初始化。。。。

先寫了個指令碼 在manage.py shell中呼叫了下 結果讓我震驚了。我表示不敢相信 又寫了view 並在settings.py中新增了顯示SQL Query語句的log

這裡是寫的view:

執行結果如下,第一行是頁面顯示的時間,後邊是Queryset實際呼叫的SQL語句

令人難以置信的,在10000行的MYSQL表中 方法1的效率是最高的。無論是結果上看(12ms)還是SQL語句的執行時間上看(9ms)方法1甩了其他方法一大截

即便資料量增加到21萬,方法1也會比其他兩種方法快:

資料量再次提升至百萬級別 1066768條資料

第三種方法所用時間長到令人無法接受(19.65秒)。但有意思的是 SQL語句所花費的時間“只有”3.6秒。而大部分的時間都用在python上了。

既然第二種方法和第三種方法都需要random.sample 一個百萬個資料的list,那就是說,有大量的時間花費在將SELECT到的結果轉化為django物件的過程中了。

此後將不再測試第三種方法

最後,資料量增加到5,195,536個

隨著表中資料行數的增加,兩個方法的所用的時間都到了一個完全不能接受的程度。兩種方法所用的時間也幾乎相同。

 

值得注意的是,Mysql資料庫有一個特點是,對於一個大表,OFFSET越大,查詢時間越長。或許有其他方法可以在offset較大的時候加快select的速度,然而django明顯沒有做到。如果能夠減少這種消耗,方法2明顯會優於方法1。

附上三種方法資料量和SQL時間/總時間的資料圖表:

table

20131205220333

總時間

 

最後總結,Django下,使用mysql資料庫,資料量在百萬級以下時,使用

來獲取隨機記錄序列,效能不會比

差。

相關文章