教程|Django與資料庫交互中你需要的九個知識點

教程|Django與數據庫交互中你需要的九個知識點

對於開發人員來說,雖然對象映射關係(ORMs)確實非常的有用,但是數據庫的訪問也是有成本的(例如:計算成本與時間成本)。那些沉浸於數據庫開發的研究人員經常會發現,一點小小的改變,就可以換得性能上的巨大提升。

本文福利:私信回覆【PDF】可獲取Python電子書一套

在此篇文章中,我將分享在 Django 中使用數據庫的 9 個小技巧。

1. 過濾器聚合

在 Django 2.0 之前,如果你想得到“用戶總數”、“活躍用戶總數”等信息時,你不得不使用條件表達式。

教程|Django與數據庫交互中你需要的九個知識點

Django 2.0 中,通過在聚合函數中添加一個過濾器使這種問題變得更加簡單、容易處理:

教程|Django與數據庫交互中你需要的九個知識點

怎麼樣? 看起來很簡潔明瞭吧。 如果你使用的是 PostgreSQL 的話,若要進行這種查詢,查詢程序如下:

教程|Django與數據庫交互中你需要的九個知識點

可以看到,在第二個查詢語句中,使用 FILTER(WHERE) 過濾語句。

2. 將查詢結果變為 namedtuples 形式

我個人非常喜歡 namedtuples。 從 Django 2.0 之後,ORM 也同樣青睞於 namedtuples。

在 Django 2.0 中, 在 value_list 方法中添加了一個名為 named 的屬性。將 name 設置為 True,查詢結果將以 namedtuples 的形式展示。

教程|Django與數據庫交互中你需要的九個知識點

3. 自定義函數

Django 的 ORM 功能強大且內容豐富。但是卻不可能緊跟所有的數據庫供應商的步伐。幸運的是, ORM 允許用戶根據自身需要開發自定義的函數。

假設我們有一個報告模型,其中包含了一個記錄“持續時間”(duration)的值。現在,我們想要找到所有報告的平均持續時間:

教程|Django與數據庫交互中你需要的九個知識點

看上去還不錯的樣子, 但是一個平均值包含的信息有點少。下面讓我們計算一下標準差:

教程|Django與數據庫交互中你需要的九個知識點

為什麼錯誤了? PostgreSQL 不支持對間隔類型字段數據進行標準差計算。我們需要先將間隔變為數字,然後再用 STDDEV_POP 去計算標準差。

一個可行的解決辦法就是從持續時間中提取數據:

教程|Django與數據庫交互中你需要的九個知識點

那麼問題來了,我們如何在 Django 中實現上述操作? 我想你已經猜到了, 答案就是:自定義的函數:

教程|Django與數據庫交互中你需要的九個知識點

我們定義的函數調用形式如下:

教程|Django與數據庫交互中你需要的九個知識點

需要注意的是,我們在調用 Epoch 自定義函數時使用了 F 表達式。

這個可能是所有小知識點中,最簡單但卻最重要的小知識點。 正所謂:人非聖賢,孰能無過。我們不能對每種邊緣情況面面俱到,所以我們需要設置邊界條件。

與 Tornado, asyncio 或者 Node 這些非阻塞程序服務不同, Django 經常會用到同步工作進程。這也就意味著,當一個用戶的操作程序運行很長時間時,工作進程處於阻塞狀態,其他人都無法使用,直到此人的操作程序運行完畢。

我想在實際情況活中, 沒有人會只使用一個工作進程來運行 Django。但是我們仍舊希望一個查詢進程不會佔用太多的資源和時間。

在很多 Django 應用程序中,大部分時間都用在了等待數據庫查詢結果上。所以,在查詢語句中設置一個超時屬性是一個很好的開端。

在 wsgi.py 中,我們設置了一個全局超時變量,如下程序所示:

教程|Django與數據庫交互中你需要的九個知識點

為什麼要使用 wsgi.py 呢? 因為這樣一來,它只會影響工作進程,而不會進程之外的查詢與 cron 等任務。

希望你使用的是持久數據庫鏈接, 這樣一來,每個鏈接的設置就不會增加額外的系統開銷。

我們也可以在用戶這一級上設置超時:

教程|Django與數據庫交互中你需要的九個知識點

額外提一點: 網絡也是一大耗時的地方,所以當你調用遠程服務器的時候,記得設置超時提醒:

教程|Django與數據庫交互中你需要的九個知識點

5. 限制

與上一條在邊緣情況設置超時相似,我們在某些情況下需要設置限制。有時候,我們想讓用戶產生報表,然後將其倒入到電子表格中。而在所有的的產品中,這些行為通常被認為是異常行為。

有時會遇到一個用戶想要獲取從一大早開始的所有銷售數據,這種情況是很常見的。同樣常見的情況是,當第一次查詢正在處理且卡頓時,用戶經常會打開另一個窗口,重複進行查詢請求。

這就是限制的原因

下面,讓我們設置一個限制,用於限制一個查詢請求的返回結果不超過 100 行數據。

教程|Django與數據庫交互中你需要的九個知識點


bad example 是一個反面例子。在其的代碼中,你已經將所有查詢的結果取出來並存進了內存,但是你卻只顯示了 100 行數據。 也許你應該參考一下 good example 中代碼。在此代碼中, Django 使用 SQL 中的限制語句,從而保證了只提取 100 行數據。

現在我們可以安心的說:我們添加了限制要求,用戶行為在計劃當中,一切盡在掌握。但我們仍舊有一個問題:用戶想要的是所有的銷售數據,但是我們只給了他 100 行數據,他就會認為數據庫中一共就只有 100 行數據,這是不對的想法。

和嚴格返回前 100 行數據不同,當我們的數據查超過 100 行數據的時候,我們會拋出一個異常:

教程|Django與數據庫交互中你需要的九個知識點

上面的代碼能夠正常使用,但是其中也加入了其他的查詢代碼。我們是否能夠做的更加漂亮呢? 我覺得應該是可以的:

教程|Django與數據庫交互中你需要的九個知識點

可以看出了,我們截取的不是 100 行數據,而是 101 行數據。如果 101 行數據存在,我們就可以確信數據庫中的數據大於 100 行。話句話說,只有我們能夠獲取到 LIMIT+1 行數據時,我們就可以確定,數據庫中的函數行數大於 LIMIT 行。記住 LIMIT+1 這個小竅門,有時候還挺有用的。

6. 選擇並更新時出現的問題

這是一個挺難得問題。由於數據的鎖機制, 我們過去經常會在半夜的時候得到交易超時錯誤。我們的代碼中,交易操作代碼的常見模式如下:

教程|Django與數據庫交互中你需要的九個知識點

因為交易的操作經常會涉及一些用戶和產品的屬性,所以我們經常會使用 select_realted 語句來強制關聯並保存一些查詢語句。

更新一個交易就會涉及到獲取鎖的問題,從而確保此交易目前沒有被別的其他程序操作。

現在你是否看到了問題的關鍵呢? 沒有? 我們也沒有。

在晚上的時候,我們會運行一些 ETL(Extract, Transform, Load)進程來維護產品與用戶表格。這些 ETL 進程會對錶格進行更新和插入操作,所以他們也會獲取到使用表格的鎖。

現在,你知道問題出現在哪裡了吧? 當需要執行 select_realted 語句中的 select_for_update 語句時,Django 需要去獲取其所查詢的所有表格的鎖。

我們用來實現交易的代碼也會嘗試去取的交易表格和 用戶、產品、類別的表格的鎖。半夜的時候,一旦 ETL 進程獲取到了最後三個表格的鎖,將其鎖定,交易就會失敗。

現在我們對問題的由來已經有了深入的瞭解,那麼接下來的問題就是尋找方法來鎖定唯一必要的表格——交易表格。幸運的是,在 Django 2.0 中, select_for_update 語句中引入了一個新的參數。

教程|Django與數據庫交互中你需要的九個知識點

可以看到,在 select_for_update 語句中,引入了 of 參數。使用 of 參數,我們可以指定想要鎖住哪些表格。self 是一個特殊的關鍵字,表示我們想要鎖住我們正在使用的模型,在此種情況下,就是指交易表。

現在,這些功能僅限於 PostgreSQL 和 Oracle 使用。

7. 外鍵索引 FK indexes

當我們創建一個模型時, Django 會為所有的外鍵創建一個 B-tree 索引,B-tree 索引的開銷會很大,而且有些時候根本沒有必要。

典型的例子就是 M2M 關係模型,即 多對多關係模型:

教程|Django與數據庫交互中你需要的九個知識點

在上面的模型中, Django 會在後臺創建兩個索引,一個針對用戶,一個針對群組。 在 M2M 模型中,另一個常見的模式就是使用兩個字段來作為唯一的外鍵。在這種情況下,一個用戶只能是一個群組的成員:

教程|Django與數據庫交互中你需要的九個知識點

這個 unique_together 將會在兩個字段上創建一個索引,這樣一來,我們的模型就會擁有兩個字段,三個索引。

由於我們使用這個模型的應用場景問題,很多時候,我們可以忽略這兩個外鍵而單單使用這個唯一約束的外鍵 unique_together:

教程|Django與數據庫交互中你需要的九個知識點

刪除冗餘索引將使插入和更新速度更快,此外,也會使我們的數據庫現在變得更輕,這總歸算是一件好事。

8. 組合索引中列的順序

具有多個列的索引成為組合索引。在 B-tree 組合索引中,第一列使用樹結構索引。在第一層的樹葉中我們為第二層創建一個新的樹結構,以此類推。

索引中列的順序非常重要。按照之前所說的例子,我們首先會得到一個群組的樹,對於每一個群組,會創建一個針對用戶的樹。

B-tree 組合索引的的經驗法則是讓二級索引的值儘可能的小。換句話說就是值越多的列排在第一位。

在的我們例子中,可以認為用戶的數量大於群組的數量,所以將用戶列排在首位,這樣會使得基於群組的第二級索引更小。

教程|Django與數據庫交互中你需要的九個知識點

但這只是一個經驗法則,對此我們應該持有懷疑的態度。最終的索引應該根據具體問題具體分析。此處主要想說的是,我們要注意到隱式索引以及組合索引中列順序的重要性。

9. BRIN 索引(Block Range INdexes)

B-tree 索引結構像一顆樹結構。對於隨機訪問而言,查找單個值的成本是樹的高度加一。 這使得 B-tree 索引很適合那些唯一約束和某些範圍查找。

B-tree 索引的缺點在於樹的大小,B-tree 可能變得非常的大。

通常人們可能認為已經別無他法,但是數據庫為某些特定的情況提供了其他類型的索引。

從 Django 1.11 開始,在創建模型建立索引的時候,提供了一個 Meta 參數,這給了我們提供了一個探索其他類型索引的機會。

PostgreSQL 有一個非常有用的索引類型,叫做 BRIN(Block Range Index)。在某些情況下, BRIN 索引要比 B-tree 索引更加有效。

下面的一段內容參考自官方文檔:

BRIN 被設計用於處理非常大的表格,其中某些列與他們在表格內的物理位置有一些自相關性。

要理解這句話的含義,我們就應該首先了解 BRIN 索引的原理。顧名思義,BRIN 索引會在表格的相鄰的塊區域之間建立一個小型的索引。這個索引很小,它可以確定某個值不在這個塊索引範圍之內或者這個值可能在這個塊索引範圍之內。

下面我們簡單的舉一個例子來說明 BRIN 索引的工作原理以幫助我們理解。

可以看到,這些數排成一列,自成一塊。然後我們每三個相鄰的塊組成一個區域。對於每一個區域保存其中的最大值與最小值。

教程|Django與數據庫交互中你需要的九個知識點

有了這些索引,讓我們先嚐試一下尋找數值 5 的位置:

  • - [1-3] 5 肯定不包含其中
  • - [4-6] 5 可能包含其中
  • - [7-9] 5 肯定不包含其中

可以看到,使用這個索引,我們可以將我們的查找範圍縮小到 [4-6] 這個區域之間。

下面我們另舉一個例子,其中,元素的位置隨機排列,分塊後的區域如圖所示,然後取每個區域的最大值與最小值。

教程|Django與數據庫交互中你需要的九個知識點

同樣,我們嘗試尋找 5 所在的位置:

  • - [2-9] 5 可能包含其中
  • - [1-7] 5 可能包含其中
  • - [3-8] 5 可能包含其中

可以看出來,這樣的索引不但毫無用處,因為它並不能幫我縮小查找範圍,而且還使得我們查找了更多的數據,因為我們需要為整個表格建立索引。

讓我們重溫一下官方文檔中的描述:

... 其中某些列與他們在表格內的物理位置有一些自相關性

這是 BRIN 索引的關鍵,為了更好的利用這一點,我們希望,每列中的值都基本上已經排序好了或者其在磁盤的位置都是聚類好的。

現在回到 Django,我們有哪些常被索引的字段,並且最有可能在磁盤上自然排好序呢?沒錯,需要的是 auto_now_add 參數。使用

在 Django 模型中一個非常常見的模式如下:

教程|Django與數據庫交互中你需要的九個知識點

當我們將 auto_now_add 參數設置為 True 時,Django 將自動使用當前行創建時的時間填充該值。created 字段也是查詢時的一個非常好的候選字段,所以也經常被索引。

下面我們為 created 字段添加 BRIN 索引:

教程|Django與數據庫交互中你需要的九個知識點

為了瞭解索引大小的差異,我創建了一個約有兩百萬行的表,並在磁盤上按照日期字段自然排列:

- B-tree 索引的大小為 37 MB

- BRIN 索引的大小為 49 KB

是的, 你沒有看錯。

當我們創建一個索引的時候,除了索引的大小外,我們還需要考慮其他很多因素。但是,在 Django 1.11 所支持的索引中,我麼可以非常容易的將新型的索引整合到我們的應用中,使得我們的程序更加輕便且快速。

教程|Django與數據庫交互中你需要的九個知識點


分享到:


相關文章: