【问题标题】:Counting the number of related objects with a certain value in Django计算Django中具有一定值的相关对象的数量
【发布时间】:2015-12-30 09:32:03
【问题描述】:

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

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 自动处理这种非规范化 - 例如通过创建某种数据库索引。

【问题讨论】:

    标签: django database postgresql indexing denormalization


    【解决方案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 并通过关系定义获得一些好处,但我没有这方面的经验。

    【讨论】:

    • 好主意,但对我们不起作用。有数百万用户/作者。缓存数百万个条目可能不是一个好主意...
    • 数以百万计的条目只是 MB。您也可以只缓存最常用的数据。缓存不是所有问题的 100% 解决方案,但它可能会充分缓解您的问题,使其消失(目前)。
    • 我设法让新闻网站使用缓存技巧在 50 毫秒内呈现几乎所有页面。我将 html 代码片段存储在命名空间中,使每个页面主要连接来自缓存的字符串。如果字符串不可用,它将由网络测功机重新生成。几乎所有幕后工作人员都保持缓存“温暖”。结果是对数据库最频繁的查询是更新页面命中数。
    【解决方案2】:

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

    您还可以创建 celery 任务,该任务将在创建 read 后立即运行,以为作者生成新值。这样您就不会有很长的响应时间,并且从创建读取到计数的延迟也不会那么长。

    【讨论】:

    • 此解决方案可能会改善用户体验,但是,我们的目标是减少数据库服务器上的负载 - 这不会通过使用异步进程来实现。无论如何,谢谢。
    • 您还可以监控异步进程,这样一位作者就不会有 2 个进程在队列中。此外,队列在任务之间可能会有一些延迟,并且可能会降低性能,因此随着时间的推移负载会降低。
    【解决方案3】:

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

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

    这将生成带有子查询的查询,有时它会比带有连接的查询具有更好的性能。您甚至更进一步,最终分别创建了 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 的书籍的阅读计数。

    【讨论】:

    • 我们在不同的模型/问题上使用这种方法。然而,在这种情况下,它不会让事情变得更容易。该示例仅包括描述问题所需的最少部分。实际上,它还有更多功能,这使得这种方法不太实用。
    • 因此,如果还有其他内容会对有效答案产生影响,请在您的问题中进行说明,以便答案可以更加具体并直接与您的问题相关。
    猜你喜欢
    • 2020-04-05
    • 1970-01-01
    • 1970-01-01
    • 2022-07-03
    • 2015-10-26
    • 1970-01-01
    • 2021-07-25
    • 2021-08-21
    • 1970-01-01
    相关资源
    最近更新 更多