使用 prefetch_related 优化 Django Queryset 多对多 for 循环

Posted

技术标签:

【中文标题】使用 prefetch_related 优化 Django Queryset 多对多 for 循环【英文标题】:Optimize Django Queryset Many to Many for loop with prefetch_related 【发布时间】:2021-10-28 08:49:50 【问题描述】:

我看到了一个在 4.5 秒内执行 64 个查询的函数,所以我必须优化以获得更好的性能。

问题是 prefetch_related 没有像我预期的那样工作,所以我为 for 循环上的每次迭代都有一个新的查询集。

这是模型。

class Carrera(models.Model):
'''Carreras de cada sede'''
codigo = models.CharField(max_length=5, unique=True)
nombre = models.CharField(max_length=300)

def __str__(self):
    return f"self.codigo - self.nombre"


class EstandarProducto(models.Model):
''''''
costo_unitario_uf = models.DecimalField(max_digits=20, decimal_places=5, default=0)
cantidad = models.IntegerField(default=0)
total_uf = models.DecimalField(max_digits=20, decimal_places=5, default=0)
recinto = models.ForeignKey(Recinto, related_name='estandares_producto', on_delete=models.CASCADE)
producto = models.ForeignKey(
    Producto, related_name='estandares_producto', on_delete=models.PROTECT)
proveedor = models.ForeignKey(
    Proveedor, related_name='estandares_producto', on_delete=models.CASCADE, blank=True, null=True)
carreras = models.ManyToManyField(Carrera, related_name="estandares_productos", blank=True)

这是我的观点:

class getEstandarPorRecinto(APIView):

@query_debugger
def get(self, request, format=None):
    id_recinto = request.GET.get('recinto')
    id_sede = request.GET.get('sede')
    estandares = EstandarProducto.objects.select_related('recinto','producto','proveedor').prefetch_related(Prefetch('carreras')).filter(recinto=id_recinto)
    inventario = InventarioProducto.objects.select_related('inventario').filter(inventario__sede=id_sede)
    num_salas = SedeRecinto.objects.select_related('sede').filter(sede=id_sede, recinto=id_recinto)[0].numero_salas
    productos_en_inventario = inventario.values() 
    lista_enviar = []

    for estandarProducto in estandares:
        carreras = []
        for carrera in estandarProducto.carreras.values():
            print("Codigo: " ,carrera.get("codigo"))
            carreras.append(carrera.get("codigo"))
        cantidad_inventario = 0
        año_compra = ""
        id_inventario_producto = ""
        print("Carreras: ",carreras)
        for producto in productos_en_inventario: 
            if producto.get("id") == estandarProducto.producto.id: 
                cantidad_inventario = producto.get("cantidad") 
                año_compra = producto.get("ultimo_año_reposicion") 
                id_inventario_producto = producto.get("id") 

        
        estandar_json = 
            "id_producto": estandarProducto.producto.id, 
            "id_estandar_producto": estandarProducto.id, 
            "codigo": estandarProducto.producto.codigo, 
            "carreras": carreras, 
            "categoria": estandarProducto.producto.categoria, 
            "nombre": estandarProducto.producto.nombre, 
            "descripcion": estandarProducto.producto.descripcion, 
            "cantidad_esperada": estandarProducto.cantidad,
            "proveedor_id": estandarProducto.proveedor.id, 
            "proveedor": estandarProducto.proveedor.nombre, 
            "frecuencia_reposicion": estandarProducto.producto.frecuencia_reposicion, 
            "inventario_producto_id": id_inventario_producto, 
            "costo_un_uf": estandarProducto.costo_unitario_uf, 
            "total_uf": estandarProducto.total_uf, 
            "tipo_presupuesto": estandarProducto.producto.tipo_presupuesto, 
            "inventario": cantidad_inventario, 
            "ultimo_año_compra": año_compra, 
            "numero_salas": num_salas, 
            "a_comprar": estandarProducto.cantidad*num_salas 
        
        
        lista_enviar.append(estandar_json)
    data = lista_enviar
    return Response(data, status.HTTP_200_OK)

问题就出来了:

estandares = EstandarProducto.objects.select_related('recinto','producto','proveedor').prefetch_related(Prefetch('carreras')).filter(recinto=id_recinto)

for carrera in estandarProducto.carreras.values():
    print("Codigo: " ,carrera.get("codigo"))
    carreras.append(carrera.get("codigo"))

如何在单个查询中获取所有 carreras.codigo,然后附加到列表 carreras[] 上?

【问题讨论】:

【参考方案1】:

您可以使用values_list 并设置flat True 来给出列表输出

例如:

carreras  = estandares.values_list("carreras__codigo", flat=True)

【讨论】:

【参考方案2】:

docs中所述:

任何暗示不同数据库查询的后续链接方法将忽略以前缓存的结果,并使用新的数据库查询检索数据。

这意味着values() 调用实质上使您的预取不可用,并且将导致您的for 循环访问数据库以满足values() 调用。在这种情况下,只需使用all() 并从实例本身获取codigo,如下所示:

    for estandarProducto in estandares:
        carreras = []
        for carrera in estandarProducto.carreras.all():
            carreras.append(carrera.codigo)

但您似乎只需要codigo 自己,所以@pouria farhadi 的回答会很好。

【讨论】:

我仍然得到相同数量的两个答案的查询。 你能更新你当前的变化吗?如果可能的话,添加你计算查询的方式 对不起,我重新加载错误,我使用你的代码并且知道我在 0.62 秒内收到了 6 个查询!非常感谢!

以上是关于使用 prefetch_related 优化 Django Queryset 多对多 for 循环的主要内容,如果未能解决你的问题,请参考以下文章

ORM数据库查询优化only与defer(select_related与prefetch_related)

转 实例具体解释DJANGO的 SELECT_RELATED 和 PREFETCH_RELATED 函数对 QUERYSET 查询的优化

Django框架详细介绍---ORM相关操作---select_related和prefetch_related函数对 QuerySet 查询的优化

Django查询优化之select_related和prefetch_related

详解Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化

转 实例详解Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化