您當前的位置:首頁 > 農業

Django中的資料庫訪問最佳化——預載入關聯資料

作者:由 Fossen 發表于 農業時間:2018-05-02

Django的模型層提供了一套ORM系統,這使得我們無需學習SQL也能利用資料庫來儲存相關資料。一次query獲取所有需要的資料,往往比多次query分別取得資料要更高效。但由於django模型的資料庫檢索過程隱藏在後臺,不注意的話很容易導致多次檢索資料庫,浪費不必要的時間。因此充分理解django模型的query機制十分重要。

django官方文件給出了很多資料庫訪問最佳化的建議:Database access optimization。這些建議對於提升程式碼的效率十分有幫助,但其內容較多,本文只介紹其中的一種最佳化手段,預載入關聯資料。

模型中經常會用到外來鍵和多對多關係,但是queryset在獲取物件的資料時,如果不指定的話,則不會檢索關聯物件的資料。當你呼叫關聯物件時,queryset還會再一次訪問資料庫。因此當你迴圈多個物件並呼叫其外來鍵所關聯的物件時,django會不停的訪問資料庫,以獲取其所需的資料。

這樣說或許有點抽象,下面結合例子詳細解釋說明,例子使用以下的模型。

from

django。db

import

models

from

django。contrib。auth。models

import

User

class

Category

models

Model

):

name

=

models

CharField

‘分類’

max_length

=

16

class

Topic

models

Model

):

name

=

models

CharField

‘話題’

max_length

=

16

class

Article

models

Model

):

‘文章’

title

=

models

CharField

‘標題’

max_length

=

100

content

=

models

TextField

‘內容’

pub_date

=

models

DateTimeField

‘釋出日期’

category

=

models

ForeignKey

Category

topics

=

models

ManyToManyField

Topic

class

ArticleComment

BaseComment

):

‘文章評論’

content

=

models

TextField

‘評論’

article

=

models

ForeignKey

Article

related_name

=

‘comments’

先簡單看看查詢的機制,透過模型的 Manager構建一個QuerySet物件,QuerySet是懶載入,總是等到要用到結果時才去訪問資料庫。QuerySet在訪問資料庫時,實際上是使用SQL語句獲取結果的。我們可以透過logging檢視SQL語句,調整logging等級為DEBUG即可,我在另一篇文章中有介紹:如何檢視Django ORM執行的sql語句。或者檢視query屬性

print(QuerySet。query)

>>>

Article

objects

all

()

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

可以看到,一般的QuerySet只取出模型對應的表中的資料,但不會取得關聯表中的資料。這意味著只獲得外來鍵id,而非外來鍵所指向的資料,至於多對多關係則什麼不能獲得,因為多對多關係的資料實際都儲存在另一箇中間表裡。

select_related——預載入單個關聯物件

Article與Category用外來鍵關聯,是多對一關係,一篇文章只能屬於一個分類,一個分類可以包含多篇文章。獲取一篇文章的分類,即呼叫

Article。category

屬性。但由於文章中快取的僅僅只是文章分類的id

Article。category。id

,而非完整的Category物件,所以當使用文章的category屬性時,django會再次訪問資料庫,以檢索其內容。

如下,當用for迴圈列印文章分類

Article。category

時,每一次迴圈都會訪問一次資料庫。而且,文章的分類往往是重複的,同樣的分類可能在for中重複檢索了多次,這樣的用法顯然相當耗時。

# 訪問一次資料庫,獲得Article物件

for

a

in

Article

objects

all

():

# 訪問n次資料庫,每次迴圈都要重新檢索Category的資料

print

a

category

使用select_related則可以一次性獲取物件以及關聯的物件,只需訪問一次資料庫:

for

a

in

Article

objects

all

()

select_related

‘category’

):

# 已經快取了資料,不會再次訪問資料庫

print

a

category

再用query屬性看一下SQL語句,

select_related()

使用JOIN獲取了Category模型的資料。這樣就預先載入了外來鍵關聯的物件,再次呼叫關聯物件時就不會訪問資料庫了。

>>>

Article

objects

select_related

‘category’

# all()可以省略

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

`

blog_category

`

`

id

`

`

blog_category

`

`

name

`

FROM

`

blog_article

`

INNER

JOIN

`

blog_category

`

ON

`

blog_article

`

`

category_id

`

=

`

blog_category

`

`

id

`

);

獲取外來鍵的外來鍵只需用雙下劃線隔開就行,以此類推。比如:

ArticleComment。objects。select_related(‘article__category’)

可以同時預載入該評論歸屬的文章以及該文章歸屬的分類。

然而,為了避免由於加入多個關聯物件而導致的結果集太大,

select_related

僅限於獲取單值關係——外來鍵和一對一關係。

prefetch_related——預載入多個關聯物件

預載入多個關聯的物件時,需要使用prefetch_related,它分別查詢每一個關係,然後在Python中完成關聯物件間的連線。

接下來還是用Article與Category舉例,在資料庫的article表中,儲存了分類的id,但是在category表中,並沒有儲存下屬文章的id。要想獲取某分類下的文章,有兩種手段:

c

=

Category

objects

get

id

=

1

# 這兩種方法是等價的,都要訪問一次資料庫

c

article_set

all

()

Article

objects

filter

category

=

c

再看看查詢多個分類下的文章時的情況:

# 訪問1次資料庫,獲得分類

for

c

in

Category

objects

all

():

# 訪問n次資料庫,獲得文章

c

article_set

all

()

這種情況下不能使用select_related,因為有多個關聯物件時,需要用prefetch_related。這個方法會將所需的關聯物件全部載入至記憶體中,每次呼叫

c。article_set。all()

時將直接從快取中載入物件。

# 訪問2次資料庫,獲得分類與文章

for

c

in

Category

objects

prefetch_related

‘article_set’

):

# 直接呼叫快取,不再訪問資料庫

c

article_set

all

()

為什麼是兩次?第一步檢索分類,第二步檢索所屬的文章,使用SELECT和IN語句查詢,相當於:

>>>

# prefetch_related

>>>

Category

objects

prefetch_related

‘article_set’

SELECT

`

blog_category

`

`

id

`

`

blog_category

`

`

name

`

`

blog_category

`

`

number

`

FROM

`

blog_category

`

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

category_id

`

IN

1

2

3

。。。

);

。。。

>>>

# no prefetch_related

>>>

c

=

Category

objects

all

()

>>>

a

=

Article

objects

filter

category__in

=

c

>>>

print

c

SELECT

`

blog_category

`

`

id

`

`

blog_category

`

`

name

`

`

blog_category

`

`

number

`

FROM

`

blog_category

`

args

=

()

。。。

>>>

print

a

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

category_id

`

IN

SELECT

`

blog_category

`

`

id

`

FROM

`

blog_category

`

。。。

多對多關係也是類似的情況,以Article和Topic為例,使用該方法也能預載入關聯物件,

Article。objects。prefetch_related(‘topics’)

Prefetch——進一步控制預載入操作

Prefetch可以用於進一步控制預載入時的操作,例如,下面的程式碼使用Prefetch將分類下的文章限制為id大於5的文章:

>>>

from

django。db。models

import

Prefetch

>>>

c

=

Category

objects

prefetch_related

‘article_set’

get

id

=

2

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

category_id

`

IN

2

);

>>>

c

article_set

count

()

# 不需訪問資料庫

11

>>>

qs

=

Article

objects

filter

id__gt

=

5

>>>

c

=

Category

objects

prefetch_related

Prefetch

‘article_set’

queryset

=

qs

))

get

id

=

2

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

id

`

>

5

AND

`

blog_article

`

`

category_id

`

IN

2

));

>>>

c

article_set

count

()

# 結果與前一個不一樣了

7

除此之外,還可以用

to_attr

引數指定預載入結果為初始物件的屬性,這樣就不會覆蓋原來的Manager,

to_attr

指定的屬性將預載入的結果儲存在列表中。

>>>

c

=

Category

objects

prefetch_related

Prefetch

‘article_set’

queryset

=

qs

to_attr

=

‘aidgt5’

))

get

id

=

2

SELECT

`

blog_article

`

`

id

`

`

blog_article

`

`

title

`

`

blog_article

`

`

content

`

`

blog_article

`

`

pub_date

`

`

blog_article

`

`

category_id

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

id

`

>

5

AND

`

blog_article

`

`

category_id

`

IN

2

));

>>>

c

article_set

count

()

# 執行SQL語句,因為沒有快取該queryset

SELECT

COUNT

*

AS

`

__count

`

FROM

`

blog_article

`

WHERE

`

blog_article

`

`

category_id

`

=

2

11

>>>

len

c

aidgt5

# 已快取,無需訪問資料庫

7

預載入效能對比

使用select_related和prefetch_related能大大減少訪問資料庫的次數,但這對效能有多大提升呢?我們依然沒有一個直觀上的印象。接下來將透過實際執行程式碼,對比非預載入和預載入在效率上的區別。(其實是因為我不會分析演算法複雜度,只能對比實際執行時間了。。。)

def

articles_retrieve_no_prefetch

():

for

a

in

Article

objects

all

():

print

a

category

\n

a

topics

all

())

def

articles_retrieve_prefetch

():

for

a

in

Article

objects

all

()

\

select_related

‘category’

\

prefetch_related

‘topics’

):

print

a

category

\n

a

topics

all

())

上面兩個函式分別定義了簡單的資料庫查詢,以及使用了預載入的資料庫查詢,並列印其結果。

測試方法為兩個函式分別執行100次,並統計單次執行所耗費的時間。具體的統計函式將放在後面,硬體和軟體配置就不贅述了,直接上結果,其中平均值為函式單次執行所花費的時間,單位為s。

很明顯,預載入的效能更高。

Django中的資料庫訪問最佳化——預載入關聯資料

django的模型雖然簡單易用,但是不能淺嘗輒止,要深入理解其背後的原理,併合理使用查詢方法。否則很容易執行許多次不必要的資料庫訪問,造成嚴重的效能浪費。

因此,資料庫訪問最佳化至關重要,本文僅僅只是介紹了一種最佳化方法,更多的最佳化方法請參考django官方文件:Database access optimization。

下面給出詳細的統計函式,如果有興趣可以在自己的專案中跑一下,

‘’‘run this uder Django project’‘’

import

time

statistics

from

django。utils

import

timezone

from

blog。models

import

Article

def

count_run_time

func

):

start

=

time

perf_counter

()

func

()

end

=

time

perf_counter

()

return

end

-

start

def

statistic_run_time

func

n

):

data

=

count_run_time

func

for

i

in

range

n

)]

mean

=

statistics

mean

data

sd

=

statistics

stdev

data

xbar

=

mean

return

data

mean

sd

max

data

),

min

data

)]

def

compare_articles_retrieve_time

n

):

result1

=

statistic_run_time

articles_retrieve_no_prefetch

n

result2

=

statistic_run_time

articles_retrieve_prefetch

n

print

‘對比

\t

no prefetch

\t

prefetch’

print

‘平均值

\t

result1

1

],

\t

result2

1

])

print

‘標準差

\t

result1

2

],

\t

result2

2

])

print

‘最大

\t

result1

3

],

\t

result2

3

])

print

‘最小

\t

result1

4

],

\t

result2

4

])

標簽: article  blog  category  id  related