bios000's Blog

关于ORM的一些Tips

2023/02/28

前言

开发中的大部分功能本质都是对数据库的操作,之前一直遇到 nyconnnections 这个错误估计也是代码质量不高导致,
所以对数据库的重复操作次数以及连接数太多导致的。自己通过django对数据库的操作主要是通过ORM,所以掌握关于ORM能够减少查询的tips确实能够帮助提升很大一部分性能。
在这里赞美GPT,给了非常多的优化方案

关于查询

当你查询单个主对象或主对象列表并需要在模板
或其它地方中使用到每个对象的关联对象信息时,请一定记住使用select_related和prefetch_related一次性获取所有对象信息,从而提升数据库查询效率,避免重复查询。

这部分的优化场景,主要是针对关联查询的两个方法的示例

#select_related

1
2
3
4
5
6
7
8
9
10
11
12
task_config_item_record = TaskConfigItem.objects.filter(
task__user=user_id,
task__status=1,
task__show_dashboard=1
).select_related('template', 'task_config')

url_list = [
{
"task_name": tci.task.name,
tci.template.name: get_payload(tci.task_config.key, tci.template.payload)
} for tci in task_config_item_record[:30]
]

当在一对一或者一对多的使用场景中,可以用select_related来一次性获取住对象及相关对象的信息
会有一些其他的使用情况 #selected_related_demo

prefetch_related的使用方法

对于多对多的数据表关联信息查询一般不回用select_related方法,这样做是避免做JOIN操作造成最后的表非常大

1
2
3
4
5
# 文章列表及每篇文章的tags对象名字信息
Article.objects.all().prefetch_related('tags__name')

# 获取id=13的文章对象同时,获取其相关tags信息
Article.objects.prefetch_related('tags').get(id=13)

其他利用场景 #prefetch_related_demo

in_bulk

#in_bulk
in_bulk() 接受一个 id 值列表并返回一个字典,将每个 id 映射到具有该 id 的对象实例。如果不将列表传递给 in_bulk()
方法,则将返回所有对象。
假设我只想检索 id 为 1 和 4 的学生,我可以这样做。

1
2
3
4
5
6
>>> 
students = Student.objects.in_bulk([1, 4])
>>> students[1].name
'Regina Johnson'
>>> students[4].name
'Jessie Smith'

关于聚合查询

annotate

#annotate
主要用于跨表查询,作用于queryset,其实就是给Queryset添加一个属性,Queryset中的每个对象都会有那么一个属性,属性可以是模型中的字段
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 品牌模型
class Brand(models.Model):
name = models.CharField(max_length=20, unique=True, verbose_name='名称')
location = models.CharField(max_length=30, default='中国', blank=True, verbose_name='品牌所属地区')
logo = models.ImageField(null=True, blank=True, verbose_name='Logo图片')
create_time = models.DateField(auto_now_add=True, verbose_name='创建时间')


# 商品模型
class Goods(models.Model):
name = models.CharField(max_length=50, verbose_name='名称')
sales = models.IntegerField(default=0, null=True, blank=True, verbose_name='销量')
comments = models.IntegerField(default=0, null=True, blank=True, verbose_name='评价数')
brand = models.ForeignKey(Brand, null=True, on_delete=models.PROTECT, verbose_name='所属品牌')
create_time = models.DateField(auto_now_add=True, verbose_name='创建日期')

商品模型中 brand字段就是外键,关联的品牌Brand
当我想查询每个品牌下对应商品的数量时,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查询每个品牌下的商品数量,结果为一个queryset,并且每个对象都有一个num_count属性,这个属性的值就是每个品牌下的商品数量。
result = Brand.objects.annotate(num_count=Count('goods')) # Sun(类型小写)
result[0].num_count # 获取num_count属性

# 与values合用,相当于按values的值进行分组聚合
result = Brand.objects.values('name').annotate(num_count=Count('goods'))
# 结果:
< QuerySet[{'name': 'oppo', 'num_count': 0}, {'name': '华为', 'num_count': 3}, {'name': '小米', 'num_count': 0}, {
'name': '苹果', 'num_count': 3}] >
# 对应的sql

SELECT
`tb_brand`.`name`, COUNT(`tb_goods`.`id`) AS`num_count`
FROM `tb_brand` LEFT OUTER JOIN `tb_goods`
ON(`tb_brand`.`id` = `tb_goods`.`brand_id`) GROUP
BY `tb_brand`.`name` ORDER BY
NULL

# 查询每个品牌下商品的销量
result = Brand.objects.values('name').annotate(sale_count=Sum('goods__sales')) # 类名小写__字段


关于Antenna中的聚合查询

1
2
message_date_count = message_record.annotate(date=TruncDate('create_time')).values('date').
annotate(count=Count('id')).order_by('date')

使用annotate()方法计算每天消息的数量,使用values()方法来指定查询的字段,使用TruncDate()[[TruncDate]]
方法来截取日期,使用Count()方法进行聚合计算,最后使用order_by()方法排序。

aggregate

#aggregate
也是作用于queryset,对某列进行如下操作:

  • Sum:求和
  • Count:数量
  • Avg:平均
  • Max:求最大值
  • Min:求最小值
  • Variance:计算方差
  • StdDev:计算标准差
1
2
3
4
#对商品销量取平均值,求和
result = Goods.objects.all().aggregate(avg=Avg('sales'), sum=Sum('sales'))
#结果:
{'avg': 3.625, 'sum': 29}

关于创建

对于for循环的数据对象进行保存,那就会导致每次循环都要保存一次数据,这样会增加开销,例如

1
2
3
4
5
6
7
8
9
10
11
12
if nums.isdigit() and int(nums) > 0:
for i in range(int(nums)):
device = Device(
category=category,
seat=seat_obj,
asset_code='',
asset_num='V{}-{}'.format(category.name, str(i).zfill(4)), # V类型-0001编号
use_info='',
operator=operator,
op_type=1
)
device.save() # 每次save()的时候都会访问一次数据库。导致性能问题

针对于这种情况我们选择先批量创建对象,最后使用bulk_create一次保存到数据库
#bulk_create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
device_obj_list = []
for i in range(int(nums)):
device_obj_list.append(
Device(
category=category,
seat=seat_obj,
asset_code='---',
asset_num='{}-xxxx'.format(category.name), # 类型-xxxx
use_info='---',
operator=operator,
op_type=1
)
)
Device.objects.bulk_create(device_obj_list) # 使用django.db.models.query.QuerySet.bulk_create()批量创建对象,减少SQL查询次数
messages.info(request, '批量添加{}条数据完成!'.format(nums))
CATALOG
  1. 1. 前言
  2. 2. 关于查询
    1. 2.1. select_related 的使用方法
    2. 2.2. prefetch_related的使用方法
    3. 2.3. in_bulk
    4. 2.4. 关于聚合查询
      1. 2.4.1. annotate
      2. 2.4.2. aggregate
  3. 3. 关于创建