最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

詳解Django ORM引發(fā)的數(shù)據(jù)庫N+1性能問題

 更新時(shí)間:2020年10月12日 09:24:52   作者:以終為始  
這篇文章主要介紹了詳解Django ORM引發(fā)的數(shù)據(jù)庫N+1性能問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

背景描述

最近在使用 Django 時(shí),發(fā)現(xiàn)當(dāng)調(diào)用 api 后,在數(shù)據(jù)庫同一個(gè)進(jìn)程下的事務(wù)中,出現(xiàn)了大量的數(shù)據(jù)庫查詢語句。調(diào)查后發(fā)現(xiàn),是由于 Django ORM 的機(jī)制所引起。

Django Object-Relational Mapper(ORM)作為 Django 比較受歡迎的特性,在開發(fā)中被大量使用。我們可以通過它和數(shù)據(jù)庫進(jìn)行交互,實(shí)現(xiàn) DDL 和 DML 操作.

具體來說,就是使用 QuerySet 對象來檢索數(shù)據(jù), 而 QuerySet 本質(zhì)上是通過在預(yù)先定義好的 model 中的 Manager 和數(shù)據(jù)庫進(jìn)行交互。

Manager 是 Django model 提供數(shù)據(jù)庫查詢的一個(gè)接口,在每個(gè) Model 中都至少存在一個(gè) Manager 對象。但今天要介紹的主角是 QuerySet ,它并不是關(guān)鍵。

為了更清晰的表述問題,假設(shè)在數(shù)據(jù)庫有如下的表:

device 表,表示當(dāng)前網(wǎng)絡(luò)中納管的物理設(shè)備。

interface 表,表示物理設(shè)備擁有的接口。

interface_extension 表,和 interface 表是一對一關(guān)系,由于 interface 屬性過多,用于存儲(chǔ)一些不太常用的接口屬性。

class Device(models.Model):
  name = models.CharField(max_length=100, unique=True) # 添加設(shè)備時(shí)的設(shè)備名
  hostname = models.CharField(max_length=100, null=True) # 從設(shè)備中獲取的hostname
  ip_address = models.CharField(max_length=100, null=True) # 設(shè)備管理IP

class Interface(models.Model):
  device = models.ForeignKey(Device, on_delete=models.PROTECT, null=False,related_name='interfaces')) # 屬于哪臺設(shè)備
  name = models.CharField(max_length=100) # 端口名
  collect_status = models.CharField(max_length=30, default='active')
  class Meta:
    unique_together = ("device", "name") # 聯(lián)合主鍵
    
class InterfaceExtension(models.Model):
  interface = models.OneToOneField(
    Interface, on_delete=models.PROTECT, null=False, related_name='ex_info')
    
  endpoint_device_id = models.ForeignKey( # 綁定了的終端設(shè)備
    Device, db_column='endpoint_device_id',
    on_delete=models.PROTECT, null=True, blank=True)
    
  endpoint_interface_id = models.ForeignKey(
    Interface, db_column='endpoint_interface_id', on_delete=models.PROTECT, # 綁定了的終端設(shè)備的接口
    null=True, blank=True)

簡單說一下之間的關(guān)聯(lián)關(guān)系,一個(gè)設(shè)備擁有多個(gè)接口,一個(gè)接口擁有一個(gè)拓展屬性。

在接口的拓展屬性中,可以綁定另一臺設(shè)備上的接口,所以在 interface_extension 還有兩個(gè)參考外鍵。

為了更好的分析 ORM 執(zhí)行 SQL 的過程,需要將執(zhí)行的 SQL 記錄下來,可以通過如下的方式:

  • 在 django settings 中打開 sql log 的日志
  • 在 MySQL 中打開記錄 sql log 的日志

django 中,在 settings.py 中配置如下內(nèi)容, 就可以在控制臺上看到 SQL 執(zhí)行過程:

DEBUG = True

import logging
l = logging.getLogger('django.db.backends')
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())

LOGGING = {
  'version': 1,
  'disable_existing_loggers': False,
  'filters': {
    'require_debug_false': {
      '()': 'django.utils.log.RequireDebugFalse'
    }
  },
  'handlers': {
    'mail_admins': {
      'level': 'ERROR',
      'filters': ['require_debug_false'],
      'class': 'django.utils.log.AdminEmailHandler'
    },'console': {
      'level': 'DEBUG',
      'class': 'logging.StreamHandler',
    },
  },
  'loggers': {
    'django.db': {
      'level': 'DEBUG',
      'handlers': ['console'],
    },
  }
}

或者直接在 MySQL 中配置:

# 查看記錄 SQL 的功能是否打開,默認(rèn)是關(guān)閉的:
SHOW VARIABLES LIKE "general_log%";

# 將記錄功能打開,具體的 log 路徑會(huì)通過上面的命令顯示出來。
SET GLOBAL general_log = 'ON';

QuerySet

假如要通過 QuerySet 來查詢,所有接口的所屬設(shè)備的名稱:

interfaces = Interface.objects.filter()[:5] # hit once database

for interface in interfaces: 
  print('interface_name: ', interface.name,
     'device_name: ', interface.device.name) # hit database again

上面第一句取前 5 條 interface 記錄,對應(yīng)的 raw sql 就是 select * from interface limit 5; 沒有任何問題。

但下面取接口所屬的設(shè)備名時(shí),就會(huì)出現(xiàn)反復(fù)調(diào)用數(shù)據(jù)庫情況:當(dāng)遍歷到一個(gè)接口,就會(huì)通過獲取的 device_id 去數(shù)據(jù)庫查詢 device_name. 對應(yīng)的 raw sql 類似于:select name from device where id = {}.

也就是說,假如有 10 萬個(gè)接口,就會(huì)執(zhí)行 10 萬次查詢,性能的消耗可想而知。算上之前查找所有接口的一次查詢,合稱為 N + 1 次查詢問題。

解決方式也很簡單,如果使用原生 SQL,通常有兩種解決方式:

  • 在第一次查詢接口時(shí),使用 join,將 interface 和 device 關(guān)聯(lián)起來。這樣僅會(huì)執(zhí)行一次數(shù)據(jù)庫調(diào)用。
  • 或者在查詢接口后,通過代碼邏輯,將所需要的 device_id 以集合的形式收集起來,然后通過 in 語句來查詢。類似于 SELECT name FROM device WHERE id in (....). 這樣做僅會(huì)執(zhí)行兩次 SQL。

具體選擇哪種,就要結(jié)合具體的場景,比如有無索引,表的大小具體分析了。

回到 QuerySet,那么如何讓 QuerySet 解決這個(gè)問題呢,同樣也有兩種解決方法,使用 QuerySet 中提供的 select_related() 或者 prefetch_related() 方法。

select_related

在調(diào)用 select_related() 方法時(shí),Queryset 會(huì)將所屬 Model 的外鍵關(guān)系,一起查詢。相當(dāng)于 raw sql 中的 join . 一次將所有數(shù)據(jù)同時(shí)查詢出來。select_related() 主要的應(yīng)用場景是:某個(gè) model 中關(guān)聯(lián)了外鍵(多對一),或者有 1 對 1 的關(guān)聯(lián)關(guān)系情況。

還拿上面的查找接口的設(shè)備名稱舉例的話:

interfaces = Interface.objects.select_related('device').filter()[:5] # hit once database

for interface in interfaces:
  print('interface_name: ', interface.name,
     'device_name: ', interface.device.name) # don't need to hit database again 

上面的查詢 SQL 就類似于:SELECT xx FROMinterface INNER JOIN device ON interface.device_id = device.id limit5,注意這里是 inner join 是因?yàn)槭欠强胀怄I。

select_related() 還支持一個(gè) model 中關(guān)聯(lián)了多個(gè)外鍵的情況:如拓展接口,查詢綁定的設(shè)備名稱和接口名稱:

ex_interfaces = InterfaceExtension.objects.select_related(
  'endpoint_device_id', 'endpoint_interface_id').filter()[:5] 

# or

ex_interfaces = InterfaceExtension.objects.select_related(
  'endpoint_device_id').select_related('endpoint_interface_id').filter()[:5]

上面的 SQL 類似于:

SELECT XXX FROM interface_extension LEFT OUTER JOIN device ON (interface_extension.endpoint_device_id=device.id) 
LEFT OUTER JOIN interface ON (interface_extension.endpoint_interface_id=interface.id)
LIMIT 5

這里由于是可空外鍵,所以是 left join.

如果想要清空 QuerySet 的外鍵關(guān)系,可以通過:queryset.select_related(None) 來清空。

prefetch_related

prefetch_related 和 select_related 一樣都是為了避免大量查詢關(guān)系時(shí)的數(shù)據(jù)庫調(diào)用。只不過為了避免多表 join 后產(chǎn)生的巨大結(jié)果集以及效率問題, 所以 select_related 比較偏向于外鍵(多對一)和一對一的關(guān)系。

而 prefetch_related 的實(shí)現(xiàn)方式則類似于之前 raw sql 的第二種,分開查詢之間的關(guān)系,然后通過 python 代碼,將其組合在一起。所以 prefetch_related 可以很好的支持一對多或者多對多的關(guān)系。

還是拿查詢所有接口的設(shè)備名稱舉例:

interfaces = Interface.objects.prefetch_related('device').filter()[:5] # hit twice database

for interface in interfaces:
  print('interface_name: ', interface.name,
     'device_name: ', interface.device.name) # don't need to hit database again

換成 prefetch_related 后,sql 的執(zhí)行邏輯變成這樣:

  1. "SELECT * FROM interface "
  2. "SELECT * FROM device where device_id in (.....)"
  3. 然后通過 python 代碼將之間的關(guān)系組合起來。

如果查詢所有設(shè)備具有哪些接口也是一樣:

devices = Device.objects.prefetch_related('interfaces').filter()[:5] # hit twice database
for device in devices:
  print('device_name: ', device.name,
     'interface_list: ', device.interfaces.all())

執(zhí)行邏輯也是:

  1. "SELECT * FROM device"
  2. "SELECT * FROM interface where device_id in (.....)"
  3. 然后通過 python 代碼將之間的關(guān)系組合起來。

如果換成多對多的關(guān)系,在第二步會(huì)變?yōu)?join 后在 in,具體可以直接嘗試。

但有一點(diǎn)需要注意,當(dāng)使用的 QuerySet 有新的邏輯查詢時(shí), prefetch_related 的結(jié)果不會(huì)生效,還是會(huì)去查詢數(shù)據(jù)庫:

如在查詢所有設(shè)備具有哪些接口上,增加一個(gè)條件,接口的狀態(tài)是 up 的接口

devices = Device.objects.prefetch_related('interfaces').filter()[:5] # hit twice database
for device in devices:
  print('device_name: ', device.name,
     'interfaces:', device.interfaces.filter(collect_status='active')) # hit dababase repeatly

執(zhí)行邏輯變成:

  • "SELECT * FROM device"
  • "SELECT * FROM interface where device_id in (.....)"
  • 一直重復(fù) device 的數(shù)量次: "SELECT * FROM interface where device_id = xx and collect_status='up';"
  • 最后通過 python 組合到一起。

原因在于:之前的 prefetch_related 查詢,并不包含判斷 collect_status 的狀態(tài)。所以對于 QuerySet 來說,這是一個(gè)新的查詢。所以會(huì)重新執(zhí)行。

可以利用 Prefetch 對象 進(jìn)一步控制并解決上面的問題:

devices = Device.objects.prefetch_related(
  Prefetch('interfaces', queryset=Interface.objects.filter(collect_status='active'))
  ).filter()[:5] # hit twice database
for device in devices:
  print('device_name: ', device.name, 'interfaces:', device.interfaces) 

執(zhí)行邏輯變成:

  • "SELECT * FROM device"
  • "SELECT * FROM interface where device_id in (.....) and collect_status = 'up';"
  • 最后通過 python 組合到一起。

可以通過 Prefetch 對象的 to_attr,來改變之間關(guān)聯(lián)關(guān)系的名稱:

devices = Device.objects.prefetch_related(
  Prefetch('interfaces', queryset=Interface.objects.filter(collect_status='active'), to_attr='actived_interfaces')
  ).filter()[:5] # hit twice database
for device in devices:
  print('device_name: ', device.name, 'interfaces:', device.actived_interfaces) 

可以看到通過 Prefetch,可以實(shí)現(xiàn)控制關(guān)聯(lián)那些有關(guān)系的對象。

最后,對于一些關(guān)聯(lián)結(jié)構(gòu)較為復(fù)雜的情況,可以將 prefetch_related 和 select_related 組合到一起,從而控制查詢數(shù)據(jù)庫的邏輯。

比如,想要查詢?nèi)拷涌诘男畔?,及其設(shè)備名稱,以及拓展接口中綁定了對端設(shè)備和接口的信息。

queryset = Interface.objects.select_related('ex_info').prefetch_related(
      'ex_info__endpoint_device_id', 'ex_info__endpoint_interface_id')

執(zhí)行邏輯如下:

  • SELECT XXX FROM interface LEFT OUTER JOIN interface_extension ON (interface.id=interface_extension .interface_id)
  • SELECT XXX FROM device where id in ()
  • SELECT XXX FROM interface where id in ()
  • 最后通過 python 組合到一起。

第一步, 由于 interface 和 interface_extension 是 1 對 1 的關(guān)系,所以使用 select_related 將其關(guān)聯(lián)起來。

第二三步:雖然 interface_extension 和 endpoint_device_id 和 endpoint_interface_id 是外鍵關(guān)系,如果繼續(xù)使用 select_related 則會(huì)進(jìn)行 4 張表連續(xù) join,將其換成 select_related,對于 interface_extension 外鍵關(guān)聯(lián)的屬性使用 in 查詢,因?yàn)閕nterface_extension 表的屬性并不是經(jīng)常使用的。

總結(jié)

在這篇文章中,介紹了 Django N +1 問題產(chǎn)生的原因,解決的方法就是通過調(diào)用 QuerySet 的 select_related 或 prefetch_related 方法。

對于 select_related 來說,應(yīng)用場景主要在外鍵和一對一的關(guān)系中。對應(yīng)到原生的 SQL 類似于 JOIN 操作。

對于 prefetch_related 來說,應(yīng)用場景主要在多對一和多對多的關(guān)系中。對應(yīng)到原生的 SQL 類似于 IN 操作。

通過 Prefetch 對象,可以控制 select_related 和 prefetch_related 和那些有關(guān)系的對象做關(guān)聯(lián)。

最后,在每個(gè) QuerySet 可以通過組合 select_related 和 prefetch_related 的方式,更改查詢數(shù)據(jù)庫的邏輯。

參考

https://docs.djangoproject.com/en/3.1/ref/models/querysets/]

(https://docs.djangoproject.com/en/3.1/ref/models/querysets/)

https://medium.com/better-programming/django-select-related-and-prefetch-related-f23043fd635d

https://stackoverflow.com/questions/39669553/django-rest-framework-setting-up-prefetching-for-nested-serializers

[https://medium.com/@michael_england/debugging-query-performance-issues-when-using-the-django-orm-f05f83041c5f

到此這篇關(guān)于詳解Django ORM引發(fā)的數(shù)據(jù)庫N+1性能問題的文章就介紹到這了,更多相關(guān)Django ORM 數(shù)據(jù)庫N+1性能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Python 壓縮打包文件/文件夾的方法

    Python 壓縮打包文件/文件夾的方法

    本文主要介紹了Python?壓縮打包文件/文件夾的方法,分兩種類型處理,打包文件是需要傳入文件的路徑,打包文件夾是傳入文件夾的路徑,感興趣的可以了解一下
    2023-12-12
  • Python實(shí)現(xiàn)火柴人的設(shè)計(jì)與實(shí)現(xiàn)

    Python實(shí)現(xiàn)火柴人的設(shè)計(jì)與實(shí)現(xiàn)

    火柴人(Stick Figure)是一種極簡風(fēng)格的圖形,通常由簡單的線段和圓圈組成,卻能生動(dòng)地表達(dá)人物的姿態(tài)和動(dòng)作,本文旨在介紹如何使用Python實(shí)現(xiàn)火柴人的設(shè)計(jì)與繪制,通過編程的方式,讓讀者了解火柴人背后的基本原理和實(shí)現(xiàn)方法,需要的朋友可以參考下
    2024-10-10
  • python數(shù)組如何添加整行或整列

    python數(shù)組如何添加整行或整列

    這篇文章主要介紹了python數(shù)組如何添加整行或整列問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • Django添加feeds功能的示例

    Django添加feeds功能的示例

    這篇文章主要介紹了Django添加feeds功能的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-08-08
  • 利用Python腳本實(shí)現(xiàn)ping百度和google的方法

    利用Python腳本實(shí)現(xiàn)ping百度和google的方法

    最近在做SEO的時(shí)候,為了讓發(fā)的外鏈能夠快速的收錄,想到了利用ping的功能,google和百度都有相關(guān)的ping介紹,有興趣的朋友可以去看看相關(guān)的知識。下面這篇文章主要介紹了利用Python腳本實(shí)現(xiàn)ping百度和google的方法,需要的朋友可以參考借鑒,一起來看看吧。
    2017-01-01
  • Python實(shí)現(xiàn)求笛卡爾乘積的方法

    Python實(shí)現(xiàn)求笛卡爾乘積的方法

    這篇文章主要介紹了Python實(shí)現(xiàn)求笛卡爾乘積的方法,結(jié)合實(shí)例形式分析了Python計(jì)算笛卡爾乘積的原理與實(shí)現(xiàn)技巧,需要的朋友可以參考下
    2017-09-09
  • Python實(shí)現(xiàn)SVN的目錄周期性備份實(shí)例

    Python實(shí)現(xiàn)SVN的目錄周期性備份實(shí)例

    這篇文章主要介紹了Python實(shí)現(xiàn)SVN的目錄周期性備份,實(shí)例分析了Python實(shí)現(xiàn)SVN周期性備份的原理與實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-07-07
  • python和C語言混合編程實(shí)例

    python和C語言混合編程實(shí)例

    這篇文章主要介紹了python和C語言混合編程實(shí)例,文中開發(fā)了一個(gè)tcp端口ping程序來介紹混合編程,需要的朋友可以參考下
    2014-06-06
  • python的常見命令注入威脅

    python的常見命令注入威脅

    不過下面可是我們開發(fā)產(chǎn)品初期的一些血淋淋的案例,更多的安全威脅可以看看北北同學(xué)的《python hack》PPT,里面提及了不只命令執(zhí)行的威脅,那些都是我們親身經(jīng)歷的代碼
    2013-02-02
  • Python中的復(fù)制、淺拷貝與深拷貝解讀

    Python中的復(fù)制、淺拷貝與深拷貝解讀

    這篇文章主要介紹了Python中的復(fù)制、淺拷貝與深拷貝解讀,對于可變對象,賦值是最簡單省事的,如b=a,意思是直接使得a指向b代表的對象,兩者id一樣,指向同一個(gè)對象,一個(gè)修改,另一個(gè)也隨之變化,需要的朋友可以參考下
    2023-11-11

最新評論

慈利县| 玛多县| 孝义市| 建阳市| 汕头市| 昌吉市| 奎屯市| 子洲县| 景洪市| 罗定市| 保亭| 全州县| 长乐市| 施甸县| 烟台市| 滕州市| 伊宁县| 洛浦县| 广河县| 抚宁县| 西充县| 普宁市| 大庆市| 雷州市| 桐梓县| 荥经县| 麻城市| 樟树市| 景德镇市| 赤峰市| 芦山县| 昆山市| 芒康县| 禹州市| 乌兰察布市| 阿拉善盟| 武安市| 上犹县| 株洲县| 咸丰县| 九龙城区|