电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

詳解Django ORM引發的數據庫N+1性能問題

瀏覽:154日期:2024-09-21 13:08:19

背景描述

最近在使用 Django 時,發現當調用 api 后,在數據庫同一個進程下的事務中,出現了大量的數據庫查詢語句。調查后發現,是由于 Django ORM 的機制所引起。

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

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

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

為了更清晰的表述問題,假設在數據庫有如下的表:

device 表,表示當前網絡中納管的物理設備。

interface 表,表示物理設備擁有的接口。

interface_extension 表,和 interface 表是一對一關系,由于 interface 屬性過多,用于存儲一些不太常用的接口屬性。

class Device(models.Model): name = models.CharField(max_length=100, unique=True) # 添加設備時的設備名 hostname = models.CharField(max_length=100, null=True) # 從設備中獲取的hostname ip_address = models.CharField(max_length=100, null=True) # 設備管理IPclass Interface(models.Model): device = models.ForeignKey(Device, on_delete=models.PROTECT, null=False,related_name=’interfaces’)) # 屬于哪臺設備 name = models.CharField(max_length=100) # 端口名 collect_status = models.CharField(max_length=30, default=’active’) class Meta: unique_together = ('device', 'name') # 聯合主鍵 class InterfaceExtension(models.Model): interface = models.OneToOneField( Interface, on_delete=models.PROTECT, null=False, related_name=’ex_info’) endpoint_device_id = models.ForeignKey( # 綁定了的終端設備 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, # 綁定了的終端設備的接口 null=True, blank=True)

簡單說一下之間的關聯關系,一個設備擁有多個接口,一個接口擁有一個拓展屬性。

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

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

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

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

DEBUG = Trueimport loggingl = 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 的功能是否打開,默認是關閉的:SHOW VARIABLES LIKE 'general_log%';# 將記錄功能打開,具體的 log 路徑會通過上面的命令顯示出來。SET GLOBAL general_log = ’ON’;

QuerySet

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

interfaces = Interface.objects.filter()[:5] # hit once databasefor interface in interfaces: print(’interface_name: ’, interface.name, ’device_name: ’, interface.device.name) # hit database again

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

但下面取接口所屬的設備名時,就會出現反復調用數據庫情況:當遍歷到一個接口,就會通過獲取的 device_id 去數據庫查詢 device_name. 對應的 raw sql 類似于:select name from device where id = {}.

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

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

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

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

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

select_related

在調用 select_related() 方法時,Queryset 會將所屬 Model 的外鍵關系,一起查詢。相當于 raw sql 中的 join . 一次將所有數據同時查詢出來。select_related() 主要的應用場景是:某個 model 中關聯了外鍵(多對一),或者有 1 對 1 的關聯關系情況。

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

interfaces = Interface.objects.select_related(’device’).filter()[:5] # hit once databasefor 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 是因為是非空外鍵。

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

ex_interfaces = InterfaceExtension.objects.select_related( ’endpoint_device_id’, ’endpoint_interface_id’).filter()[:5] # orex_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 的外鍵關系,可以通過:queryset.select_related(None) 來清空。

prefetch_related

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

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

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

interfaces = Interface.objects.prefetch_related(’device’).filter()[:5] # hit twice databasefor interface in interfaces: print(’interface_name: ’, interface.name, ’device_name: ’, interface.device.name) # don’t need to hit database again

換成 prefetch_related 后,sql 的執行邏輯變成這樣:

'SELECT * FROM interface ' 'SELECT * FROM device where device_id in (.....)' 然后通過 python 代碼將之間的關系組合起來。

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

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

執行邏輯也是:

'SELECT * FROM device' 'SELECT * FROM interface where device_id in (.....)' 然后通過 python 代碼將之間的關系組合起來。

如果換成多對多的關系,在第二步會變為 join 后在 in,具體可以直接嘗試。

但有一點需要注意,當使用的 QuerySet 有新的邏輯查詢時, prefetch_related 的結果不會生效,還是會去查詢數據庫:

如在查詢所有設備具有哪些接口上,增加一個條件,接口的狀態是 up 的接口

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

執行邏輯變成:

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

原因在于:之前的 prefetch_related 查詢,并不包含判斷 collect_status 的狀態。所以對于 QuerySet 來說,這是一個新的查詢。所以會重新執行。

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

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

執行邏輯變成:

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

可以通過 Prefetch 對象的 to_attr,來改變之間關聯關系的名稱:

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

可以看到通過 Prefetch,可以實現控制關聯那些有關系的對象。

最后,對于一些關聯結構較為復雜的情況,可以將 prefetch_related 和 select_related 組合到一起,從而控制查詢數據庫的邏輯。

比如,想要查詢全部接口的信息,及其設備名稱,以及拓展接口中綁定了對端設備和接口的信息。

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

執行邏輯如下:

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 的關系,所以使用 select_related 將其關聯起來。

第二三步:雖然 interface_extension 和 endpoint_device_id 和 endpoint_interface_id 是外鍵關系,如果繼續使用 select_related 則會進行 4 張表連續 join,將其換成 select_related,對于 interface_extension 外鍵關聯的屬性使用 in 查詢,因為interface_extension 表的屬性并不是經常使用的。

總結

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

對于 select_related 來說,應用場景主要在外鍵和一對一的關系中。對應到原生的 SQL 類似于 JOIN 操作。

對于 prefetch_related 來說,應用場景主要在多對一和多對多的關系中。對應到原生的 SQL 類似于 IN 操作。

通過 Prefetch 對象,可以控制 select_related 和 prefetch_related 和那些有關系的對象做關聯。

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

參考

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

到此這篇關于詳解Django ORM引發的數據庫N+1性能問題的文章就介紹到這了,更多相關Django ORM 數據庫N+1性能內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Django
相關文章:
主站蜘蛛池模板: 镀锌方管,无缝方管,伸缩套管,方矩管_山东重鑫致胜金属制品有限公司 | 三价铬_环保铬_环保电镀_东莞共盈新材料贸易有限公司 | POS机办理_个人POS机免费领取 - 银联POS机申请首页 | jrs高清nba(无插件)直播-jrs直播低调看直播-jrs直播nba-jrs直播 上海地磅秤|电子地上衡|防爆地磅_上海地磅秤厂家–越衡称重 | 等离子空气净化器_医用空气消毒机_空气净化消毒机_中央家用新风系统厂家_利安达官网 | 机器视觉检测系统-视觉检测系统-机器视觉系统-ccd检测系统-视觉控制器-视控一体机 -海克易邦 | 篮球架_乒乓球台_足球门_校园_竞技体育器材_厂家_价格-沧州浩然体育器材有限公司 | 合肥升降机-合肥升降货梯-安徽升降平台「厂家直销」-安徽鼎升自动化科技有限公司 | 大通天成企业资质代办_承装修试电力设施许可证_增值电信业务经营许可证_无人机运营合格证_广播电视节目制作许可证 | 山东臭氧发生器,臭氧发生器厂家-山东瑞华环保设备 | 洛阳永磁工业大吊扇研发生产-工厂通风降温解决方案提供商-中实洛阳环境科技有限公司 | 丙烷/液氧/液氮气化器,丙烷/液氧/液氮汽化器-无锡舍勒能源科技有限公司 | 净化板-洁净板-净化板价格-净化板生产厂家-山东鸿星新材料科技股份有限公司 | 政府园区专业委托招商平台_助力企业选址项目快速落地_东方龙商务集团 | 胶辊硫化罐_胶鞋硫化罐_硫化罐厂家-山东鑫泰鑫智能装备有限公司 意大利Frascold/富士豪压缩机_富士豪半封闭压缩机_富士豪活塞压缩机_富士豪螺杆压缩机 | 杭州高温泵_热水泵_高温油泵|昆山奥兰克泵业制造有限公司 | 水厂自动化-水厂控制系统-泵站自动化|控制系统-闸门自动化控制-济南华通中控科技有限公司 | 热处理炉-退火炉-回火炉设备厂家-丹阳市电炉厂有限公司 | EFM 022静电场测试仪-套帽式风量计-静电平板监测器-上海民仪电子有限公司 | 临沂招聘网_人才市场_招聘信息_求职招聘找工作请认准【马头商标】 | 压力控制器,差压控制器,温度控制器,防爆压力控制器,防爆温度控制器,防爆差压控制器-常州天利智能控制股份有限公司 | 酒吧霸屏软件_酒吧霸屏系统,酒吧微上墙,夜场霸屏软件,酒吧点歌软件,酒吧互动游戏,酒吧大屏幕软件系统下载 | 楼承板-钢筋楼承板-闭口楼承板-无锡优贝斯楼承板厂 | 冷却塔改造厂家_不锈钢冷却塔_玻璃钢冷却塔改造维修-广东特菱节能空调设备有限公司 | 吉祥新世纪铝塑板_生产铝塑板厂家_铝塑板生产厂家_临沂市兴达铝塑装饰材料有限公司 | 屏蔽泵厂家,化工屏蔽泵_维修-淄博泵业 | 黑龙江「京科脑康」医院-哈尔滨失眠医院_哈尔滨治疗抑郁症医院_哈尔滨精神心理医院 | ET3000双钳形接地电阻测试仪_ZSR10A直流_SXJS-IV智能_SX-9000全自动油介质损耗测试仪-上海康登 | 森旺-A级防火板_石英纤维板_不燃抗菌板装饰板_医疗板 | 学习安徽网| 自动配料系统_称重配料控制系统厂家 | 干法制粒机_智能干法制粒机_张家港市开创机械制造有限公司 | 灌装封尾机_胶水灌装机_软管灌装封尾机_无锡和博自动化机械制造有限公司 | 车辆定位管理系统_汽车GPS系统_车载北斗系统 - 朗致物联 | 高柔性拖链电缆_卷筒电缆_耐磨耐折聚氨酯电缆-玖泰特种电缆 | 杭州|上海贴标机-百科| 振动筛-交叉筛-螺旋筛-滚轴筛-正弦筛-方形摇摆筛「新乡振动筛厂家」 | ICP备案查询_APP备案查询_小程序备案查询 - 备案巴巴 | 云南成考网_云南成人高考报名网| 不锈钢闸阀_球阀_蝶阀_止回阀_调节阀_截止阀-可拉伐阀门(上海)有限公司 | 杭州可当科技有限公司—流量卡_随身WiFi_AI摄像头一站式解决方案 |