在Django中计算具有特定值的相关对象的数量

时间:2015-10-02 07:47:18

标签: django database postgresql indexing denormalization

这是用于演示我的问题的简化模型:

class User(models.Model):
    username = models.CharField(max_length=30)
    total_readers = models.IntegerField(default=0)

class Book(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=100)

class Reader(models.Model):
    user = models.ForeignKey(User)
    book = models.ForeignKey(Book)

因此,我们有UsersBooksReadersUsers,他们已阅读Book)。因此,Reader基本上是BookUser之间的多对多关系。

现在让我们说,当前用户正在读书。现在,我想更新本书作者所有书籍的总读者人数:

# get the book (as an example pk=1)
book = Book.objects.get(pk=1)

# save Reader object for this user and this book
Reader(user=request.user, book=book).save()

# count and save the total number of readers for this author in all his books
book.author.total_readers = Reader.objects.filter(book__author=book.author).count()
book.author.save()

通过这样做,Django为PostgreSQL创建了一个LEFT OUTER JOIN查询,我们得到了预期的结果。但是,数据库表非常庞大,这已成为瓶颈。

在这个例子中,我们可以简单地在每个视图上将total_readers增加一个,而不是实际计算数据库行。然而,这只是一个简化的模型结构,我们不能在这里实现这一点。

我可以做的是在Reader模型中创建另一个名为book_author_id的字段。因此,我对数据进行非规范化,并且可以计算Reader对象,而不使PostgreSQL使用User表生成LEFT OUTER JOIN。 最后,我的问题是:是否可以创建某种数据库索引,以便PostgreSQL自动处理这种非规范化?或者我是否真的需要创建这个额外的模型字段并在那里冗余地存储作者的PK?

编辑 - 指出基本问题:我得到了几个很好的答案,适用于很多场景。但是,他们没有解决这个实际问题。我唯一想知道的是,是否有可能让PostgreSQL自动处理这种非规范化 - 例如通过创建某种数据库索引。

3 个答案:

答案 0 :(得分:2)

有时,此查询可以提供更好的服务:

book.author.total_readers = Reader.objects.filter(book__in=Book.objects.filter(author=book.author)).count()

这将生成带有子查询的查询,有时它将具有更好的性能,即使用join进行查询。您甚至可以进一步分别创建2个查询:

book.author.total_readers = Reader.objects.filter(book_id__in=Book.objects.filter(author=book.author).values_list('id', flat=True)).count()

这将生成2个查询,一个将检索该作者的所有书籍ID的列表,第二个将检索该列表中具有ID的书籍的读取次数。

答案 1 :(得分:1)

通过良好的设计解决此类瓶颈问题总是更好,可能还有一些缓存,而不是以您建议的方式复制数据。 total_readers字段是您应该生成而不是记录的数据。

class User(models.Model):
    username = models.CharField(max_length=30)

    @property
    def total_readers(self):
        cached_value = caching_client.get("readers_"+self.username, None)
        if cached_value is None:
            cached_value = self.readers()
            caching_client.set("readers_"+self.username, 
                                cached_value)
        return cached_value

    def readers(self):
        return Reader.objects.filter(book__author__user=self).count()

有些库通过装饰器进行缓存,但我觉得这是一种可以从明确看到的方式中受益的模式。您还可以将TTL附加到缓存,以确保该值不会超过TTL。您还可以在创建Reader对象时重新生成缓存。

实际上你可以通过声明一个m2m并通过关系定义来获得一些里程,但我没有经验。

答案 2 :(得分:1)

好的解决方案也可能是创建一些批处理任务,例如每小时运行一次并计算所有读数,但这样你最终会得不到实时刷新的读数。

您还可以创建芹菜任务,该任务将在创建读取后立即运行,以便为作者生成新值。这样你就不会有很长的响应时间和从创建阅读到计算它的延迟不会那么长。