背景描述
最近在使用 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) # 设备管理IP class Interface(models.Model): device = models.ForeignKey(Device,> 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 的功能是否打开,默认是关闭的: SHOW VARIABLES LIKE "general_log%"; # 将记录功能打开,具体的 log 路径会通过上面的命令显示出来。 SET GLOBAL general_log = 'ON';
QuerySet
假如要通过 QuerySet 来查询,所有接口的所属设备的名称:
interfaces = Interface.objects.filter()[:5] # hit>select_related在调用
select_related()
方法时,Queryset 会将所属 Model 的外键关系,一起查询。相当于 raw sql 中的join
. 一次将所有数据同时查询出来。select_related()
主要的应用场景是:某个 model 中关联了外键(多对一),或者有 1 对 1 的关联关系情况。还拿上面的查找接口的设备名称举例的话:
interfaces = Interface.objects.select_related('device').filter()[:5] # hit> 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>prefetch_relatedprefetch_related 和 select_related 一样都是为了避免大量查询关系时的数据库调用。只不过为了避免多表 join 后产生的巨大结果集以及效率问题, 所以 select_related 比较偏向于外键(多对一)和一对一的关系。
而 prefetch_related 的实现方式则类似于之前 raw sql 的第二种,分开查询之间的关系,然后通过 python 代码,将其组合在一起。所以 prefetch_related 可以很好的支持一对多或者多对多的关系。
还是拿查询所有接口的设备名称举例:
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 的执行逻辑变成这样:
- "SELECT * FROM interface "
- "SELECT * FROM device where device_id in (.....)"
- 然后通过 python 代码将之间的关系组合起来。
如果查询所有设备具有哪些接口也是一样:
详解Django ORM引发的数据库N+1性能问题
扫一扫手机访问
