python - Django + Django REST Framework + PostgreSQL查询和序列化非常慢-并非“N + 1”情况

标签 python django postgresql django-rest-framework

我喜欢Django+DRF的组合,我已经用了很长时间,但这个问题困扰了我一段时间。问题是,当存在多个或多个关系和嵌套对象时,查询+序列化将花费大量时间。StackOverflow中有很多类似的问题,通常都是某种形式的N+1问题(或未解决)。
例如
Django REST Framework Serialize extremely slow
Badly affecting performance in populating ManyToMany field values in rest api (using django rest framework)
Django REST framework is slow
另外,建议不要一次加载那么多对象。。可能适用于这里,虽然我需要所有的项目一次。
How to optimize the django rest serializer with method field?
在本例中,处理查询也是问题的一个重要部分,但查询并不太多,查询本身也很快。我正在使用prefetch_related来限制查询的数量和从DB查询中看到的内容一切看起来都很好..ish(?).I为每个prefetch_related属性+序列化对象的原始查询获取一个查询。在prefetch_related查询中有很多ID,但我想这是不可避免的,因为许多ID都是原始项目。
当这里所示的分析https://www.dabapps.com/blog/api-performance-profiling-django-rest-framework/时,我得到的结果是DB查找占用了大部分时间,而序列化也不太快。举个例子,我的本地PostgreSQL数据库中有一个“项目”的大约12k个“项”。所有“项目”都有1-5个“事件”,大多数“项目”也有1-2个“照片”。在我的笔记本电脑上获取和序列化这些数据大约需要22秒。我正在使用AWS EC2+RDS进行部署,那里的时间也差不多。在较大的“Item”集上,序列化时间的增长超过了DB查找时间,但是DB查找总是占用大部分时间。有了40k个条目,您将开始达到1分钟的执行时间,以及Nginx和堆栈其他部分的不同超时。
包含12k项的示例(下面是模型、序列化器和查询)

Database lookup               | 14.0292s
Serialization                 | 6.0076s
Django request/response       | 0.3488s
API view                      | 0.2170s
Response rendering            | 1.1092s

如果我把照片和事件放在外面,结果是
Database lookup               | 1.2447s
Serialization                 | 3.9668s
Django request/response       | 0.2435s
API view                      | 0.1320s
Response rendering            | 0.8495s

因此,相关字段占用了大部分时间(many=True)。我用于测试的配置文件是在序列化之前从queryset中生成list。因此,在序列化之前执行延迟查询。如果我不这样做,它不会改变整个结果,但是当用大约相同的时间序列化时,会计算数据库查找。
现在我的问题是,如果手动执行,所有查询都会很快完成(django.db.backends日志报告的时间差不多)。它们列在下面。所以,我相信SQL查询很快,但从Django的角度来看,DB查找非常慢。我错过了什么?或者我该如何继续调查?现在感觉它需要Django认真地将SQL查询结果转换为Django模型实例。那就意味着我的模特出了问题,对吧?
帮助调试此类案例的一般问题:
对于将相关项作为列表(many=True)的10-50k对象进行抓取和序列化的合理时间是多少?即使数据库查找会更好,6秒的序列化听起来也是错误的,不是吗?
SQL查询完成和DRF序列化开始之间会发生什么情况?有什么可以改进的吗?
最好不要从数据库中获取所有内容,而是使用values()。如果这样做了,还有办法利用DRF序列化器吗?我很喜欢他们。
最后,我可以转向缓存,但我假设如果处理正确的话,处理<100k对象不应该是一个问题。
设置:Python 2.7.13、Django 1.10.7、DRF 3.6.3
模型、视图和序列化程序的简化版本:
class List(models.Model):
    ... CharFields, DateTimeFields, ForeignKeys etc. ...

class Item(models.Model):
    list = models.ForeignKey(List, on_delete=models.CASCADE, db_index=True, null=True, related_name='items')
    deleted_at = models.DateTimeField(db_index=True, blank=True, null=True, default=None)
    created_by = models.ForeignKey(User, blank=False)
    project = models.ForeignKey('projects.Project', on_delete=models.CASCADE)
    ... other CharFields, DateTimeFields, ForeignKeys etc. ...

class Event(models.Model):
    item = models.ForeignKey(Item, on_delete=models.CASCADE, db_index=True, null=True, related_name='events')
    created_by = models.ForeignKey(User, blank=False)
    deleted_at = models.DateTimeField(db_index=True, blank=True, null=True, default=None)
    ... other CharFields, DateTimeFields, ForeignKeys etc. ...

class Photo(models.Model):
    item = models.ForeignKey(Item, on_delete=models.CASCADE, db_index=True, null=True, related_name='photos')
    created_by = models.ForeignKey(User, blank=False)
    deleted_at = models.DateTimeField(db_index=True, blank=True, null=True, default=None)
    ... other CharFields, DateTimeFields, ForeignKeys etc. ...


class PhotoSerializer(serializers.ModelSerializer):
    ... other CharFields, DateTimeFields, ForeignKeys etc. ...

class EventSerializer(serializers.ModelSerializer):
    createdBy = PrimaryKeyRelatedStringField(source='created_by', read_only=True)
    createdByFullName = serializers.CharField(source='created_by.get_full_name', read_only=True)
    ... other CharFields, DateTimeFields, ForeignKeys etc. ...

class ItemSerializer(serializers.ModelSerializer):
    listName = serializers.CharField(source='list.name', read_only=True)
    createdBy = PrimaryKeyRelatedStringField(source='created_by', read_only=True)
    createdByFullName = serializers.CharField(source='created_by.get_full_name', read_only=True)
    photos = PhotoSerializer(many=True, read_only=True)
    events = EventSerializer(many=True, required=False, allow_null=True, queryset=Event.objects.all())
    ... other fields ...


class ItemListAPIView(ListAPIView):
    model = Item
    serializer_class = ItemSerializer

    def get_queryset(self):
        return Item.objects.all().filter(project_id=...).filter(deleted_at__isnull=True).prefetch_related(
            'created_by',           # ID of user who created item
            'photos',               # Photo properties
            'event__created_by',    # Full name of the person who created the event
            'list',                 # Name of the related list
        )

来自具有14s DB查找结果的测试的示例查询:
django.db.backends: (0.196) SELECT "todo_item"."version", ... everything ... FROM "todo_item" WHERE ("todo_item"."project_id" = 1 AND "todo_item"."deleted_at" IS NULL) ORDER BY "todo_item"."created_at" DESC;
django.db.backends: (0.001) SELECT "auth_user"."id", ... everything ... FROM "auth_user" WHERE "auth_user"."id" IN (1, 2, ... some IDs ...);
django.db.backends: (0.148) SELECT "photos_photo"."version", ... everything ... FROM "photos_photo" WHERE ("photos_photo"."deleted_at" IS NULL AND "photos_photo"."item_id" IN (1, 2, ... lots of IDs... N)) ORDER BY "photos_photo"."created_at" DESC;
django.db.backends: (0.078) SELECT "events_event"."created_at", ... everything ... FROM "events_event" WHERE ("events_event"."deleted_at" IS NULL AND "events_event"."item_id" IN (1, 2, ... lots of IDs... N)) ORDER BY "events_event"."created_at" DESC, "events_event"."created_at" DESC; 
django.db.backends: (0.157) SELECT "todo_list"."created_at", ... FROM "todo_list" WHERE "todo_list"."id" IN (1, 2, ... lots of IDs... N)

更新1
@poz询问了计时测量:计时是用\timingEXPLAIN ANALYZE进行局部测试的。没有太详细的测量或调查,只是查询加起来不到1秒。与django.db.backends日志所建议的差不多。那些具有14秒DB查找时间的测试也在本地数据库中完成。套接字传输时间会影响,这是真的。物体很胖,但没有那么胖。总共有15个字段用于“Items”,而<10个字段用于“Events”和“Photos”。这会生成大量数据,但仍会生成足够的数据,以便在本地传输超过10秒,这听起来不太正确。不过,我可能错了。我会做更多的测试。谢谢你的回答!
更新2
为了检查传输的数据量是否减缓了速度,我使用psql从一个EC2实例运行相同的查询,并使用相同的DB数据运行rdsdb,然后将数据写入文件。SQL查询执行时间通常比我在本地设置时得到的时间要短。例如,对于项目,在EC2+RDS设置中,本地196ms的SELECT查询花费了65ms。数据传输加上文件写入,总共是131毫秒。文件写入是额外的,但我想确保我得到所有我期望的数据。
我测试了来自EC2实例的计时,命令来自如下:
time psql -f fat-query.psql --host=rds_host --port=5432 --username=user --dbname=dbname > output.txt

因此,在EC2+RDS设置中SQL查询速度更快(带数据传输),但整个数据查询+序列化的速度与在本地设置中一样慢。即使有很多数据需要移动,我仍然认为“问题”发生在“Django接收SQL查询的响应”和“DRF开始序列化”之间。

最佳答案

看来这不会像我希望的那样解决。由于仍然不知道与预取相关的Python JOIN为什么要花这么长时间,我用单独的queryset(尽管是相同的SQL查询)和适合这个用例的简单JOIN替换了prefetch_related
替换了GET“Items”中的Djangoprefetch_relatedJOIN和DRF序列化:
prefetch_related,但单独的查询集会产生相同的SQL查询
使用values,因为这次不需要对象
创建散列表,将“Item”ID作为通过单独查询获取的相关项的键
通过一次循环“items”将相关项连接到输出
在通过map循环“Items”的同时,还要使用字段名来匹配API规范
虽然正常的DRF序列化需要1分钟多一点的时间来序列化,例如50k个“项目”,但是用上面的6个步骤生成相同的输出大约需要10秒。
如果有人有足够的Django经验来指出Python中“default”prefetch+JOIN的行为为何如此糟糕,请告诉我。我知道使用prefetch_related会让事情更快地发生,但是花1分钟加入5万个项目是错误的。目前,我假设Django正在连接时克隆queryset,生成一些有趣的循环或查询,这些循环或查询没有显示在Django.db日志中。不知何故,我的数据并不符合Django所考虑的“最佳”用例。
我在谷歌群组中也问了同样的问题:https://groups.google.com/forum/#!topic/django-users/je2LmRRjI_I

关于python - Django + Django REST Framework + PostgreSQL查询和序列化非常慢-并非“N + 1”情况,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44461638/

相关文章:

Django admin - 实例需要有主键值才能使用多对多关系

postgresql - 更改数字在 psql 中的打印方式

postgresql - 将 postgres 数据库存储在数据文件夹以外的其他位置

Python - 如何避免调用祖 parent 构造函数(多重继承)

python - 替换所有高于阈值的 RGB 值

python - Django:查询ManyToManyField计数?

python - 使用 Django 从 Postgres 导出 JSON 时结果不一致

postgresql - 如何在 postgreSQL 中获取最近的日期?

python - Cython:将 C 缓冲区内存 View 返回给 Python

python - PyCharm 正在更改我的 Django 应用程序中的默认编码