【问题标题】:How to make Django Queryset that selects records with max value within a group如何使 Django Queryset 在组内选择具有最大值的记录
【发布时间】:2015-11-28 08:42:53
【问题描述】:

这是我的 Django 课程:

class MyClass(models.Model):
    my_integer = models.IntegerField()
    created_ts = models.DateTimeField(default=datetime.utcnow, editable=False)

我想检索MyClass 的实例,这些实例对于my_integer 的每个唯一值具有最新的created_ts。我不知道该怎么做。

有人可以告诉我怎么做吗?

【问题讨论】:

  • 'latest' 表示只有一个。
  • "my_integer 的每个唯一值的最新 created_ts"。这意味着不止一个。
  • @SaqibAli 您的问题是“我想检索MyClass [...] 的实例”,但您接受的答案不检索MyClass 的实例。它返回字典。你甚至 commented 在另一个你想要类实例的答案上。
  • @SaqibAli 我也有完全相同的用例,但不知道该怎么做。你能说出你是否能够实现这一点,以及如何实现?

标签: python django django-models orm


【解决方案1】:

这会对你有所帮助

from django.db.models import Count, Max
MyClass.objects.values('my_integer').annotate(count=Count("my_integer"),latest_date=Max('created_ts'))

表格中的数据

  my_integer      created_ts
    -             -----------
    1 2015-09-08 20:05:51.144321+00:00
    1 2015-09-08 20:08:40.687936+00:00
    3 2015-09-08 20:08:58.472077+00:00
    2 2015-09-08 20:09:08.493748+00:00
    2 2015-09-08 20:10:20.906069+00:00

输出

[
    {'count': 2, 'latest_date': datetime.datetime(2015, 9, 8, 20, 8, 40, 687936, tzinfo=<UTC>), 'my_integer': 1},
    {'count': 2, 'latest_date': datetime.datetime(2015, 9, 8, 20, 10, 20, 906069, tzinfo=<UTC>), 'my_integer': 2},
    {'count': 1, 'latest_date': datetime.datetime(2015, 9, 8, 20, 8, 58, 472077, tzinfo=<UTC>), 'my_integer': 3}
]

【讨论】:

    【解决方案2】:

    试试这个;

    from django.db.models import Max
    
    MyClass.objects.values('my_integer').annotate(Max('created_ts'))
    

    【讨论】:

    • 这只会给我们my_integercreated_ts__max对象的值,而不是实际的实例。
    【解决方案3】:

    这是一个非常基本的方法。假设您拥有的数据量不是很大,这将正常工作。您可以通过覆盖 get_queryset 函数并仅返回过滤后的方式在视图中使用它。或者,如果您打算在任何地方使用它,也可以将它用作类的静态方法。

    values = MyClass.objects.order_by('-created_ts').all()
    
    filtered = []
    existing = []
    for value in values:
        if value.my_integer not in existing:
            existing.append(value.my_integer) 
            filtered.append(value)  
    

    由于列表是按最近的第一个排序的,因此它们将被添加到该整数的现有第一个。我用它做了一些基本的测试,但不多,所以可能有一两个缺陷。 用 sqlite 测试。

    编辑

    这是一个更快的版本。

    def iter_tools():
        import itertools
        qs = MyClass.objects.all()
        filtered = []
        group_by = itertools.groupby(qs, lambda x: x.my_integer)
        for x in group_by:
            filtered.append(sorted(x[1], key=lambda x: x.created_ts, reverse=True)[0])
        return filtered
    

    本质上,这是从数据库中获取所有对象,按整数对它们进行分组,然后根据时间戳对每个组进行排序,并从每个组中获取第一个。加快速度超出了我的技能,但我相信有一些方法。

    这是timeit 与之前在数据库中只有 6 个条目的对比:

    In[]: timeit.timeit(manual, number=1500)
    Out[]: 0.5577559471130371
    In[]: timeit.timeit(iter_tools, number=1500)
    Out[]: 0.39012885093688965
    -----------------------------------------------
    In[]: timeit.timeit(manual, number=5000)
    Out[]: 1.770777940750122
    In[]: timeit.timeit(iter_tools, number=5000)
    Out[]: 1.2411231994628906
    

    编辑 2: 我在数据库中创建了 60000 个对象以使用一些数据进行尝试。我使用 django-fixtureless 生成了数据,因此整数是完全随机的,并且所有整数上的时间戳都是每个对象的新 datetime.now()

    In[]: timeit.timeit(manual, number=1)
    Out[]: 11.946185827255249
    In[]: timeit.timeit(iter_tools, number=1)
    Out[]: 0.7811920642852783
    In[]: timeit.timeit(iter_tools, number=100)
    Out[]: 77.93837308883667
    In[]: MyClass.objects.all().count()
    Out[]: 60000
    

    关于数据库的注释: 在上面的示例中,我只是在本地机器上使用 sqlite3。我刚刚设置了一个快速的小型 mysql 服务器作为 vm,并收到了更好的速度结果。

    In[16]: MyClass.objects.all().count()
    Out[16]: 60000
    In[17]: timeit.timeit(iter_tools, number=100)
    Out[17]: 49.636733055114746
    In[18]: timeit.timeit(iter_tools, number=1)
    Out[18]: 0.4923059940338135
    

    无论哪种方式,您都会返回相同的对象。如果性能是一个问题,我建议使用 itertools one 或自定义 sql 查询。

    【讨论】:

    • 所以排序/分组不能由数据库使用 Django 查询集本地完成?我希望避免编写代码来做到这一点。我认为让数据库来做会更有效率。
    • 查看我的最新编辑,我用一个真正的 mysql 数据库发布了几次。但是要回答您的问题,我不知道有任何方法可以直接从数据库中执行此操作而无需自定义 SQL。但这并不意味着没有办法。我对 mysql 和 django 的组合相当缺乏经验,所以可能有一些方法可以做到这一点。然而,只用代码来做这件事可能比试图找到 django 的一些复杂的隐藏特性更容易。
    • 不要忘记,如果您非常频繁地创建这些对象,您还可以将查询集中的结果过滤为较近的时间。例如,如果您知道您需要的所有结果将在最后 5 分钟内的某个时间出现,您可以向 qs 添加一个过滤器,这将大大减少函数必须循环的对象数量。正确答案会因数据的大小、更新所有数据的频率、需要计算结果的频率以及需要完成的速度而有所不同。
    【解决方案4】:

    您可以进行原始查询:

    MyClass.objects.raw("""
    SELECT m1.id, m1.my_integer, m1.created_ts
    FROM app_myclass AS m1, (
        SELECT my_integer, MAX(created_ts) AS max_created_ts
        FROM app_myclass
        GROUP BY my_integer
    ) AS m2
    WHERE m1.my_integer = m2.my_integer AND m1.created_ts = m2.max_created_ts
    """))
    

    或者使用 Django ORM:

    MyClass.objects.filter(
        created_ts__in=MyClass.objects.values(
            "my_integer"
        ).annotate(
            created_ts=models.Max(
                "created_ts"
            )
        ).values_list("created_ts", flat=True)
    )
    

    请注意,这仅需要一个 SQL 请求,您可以通过在查询前后打印 len(django.db.connection.queries) 看到。

    但是,请注意,后一种解决方案仅在您的 created_ts 属性被保证是唯一的情况下才有效,这可能不是您的情况。

    如果您不愿意在 created_ts 上使用原始查询或索引,那么您可能应该按照其他答案的建议开始使用 PostgreSQL 及其 DISTINCT ON 功能。

    【讨论】:

      【解决方案5】:

      未经测试

      results = MyClass.objects.all().distinct('my_integer').order_by('created_ts')
      

      【讨论】:

      • 这给了我一个例外:NotImplementedError: DISTINCT ON fields is not supported by this database backend。仅供参考,我正在使用 mysql。
      【解决方案6】:
      MyClass.objects.order_by('my_integer', '-created_ts').distinct('my_integer')
      

      根据distinct,你需要在属性上调用distinct,顺序和order_by一样。因此,根据整数对元素进行排序,然后按反向时间戳对元素进行排序,并对它们调用 distinct,这将返回每个整数的最新实例。

      【讨论】:

      • 这给了我一个例外:NotImplementedError: DISTINCT ON fields is not supported by this database backend。仅供参考,我正在使用 mysql。
      • 参考这个,stackoverflow.com/questions/12402923/…mysql后端不支持distinct()操作。它的等价物(据我所知)MyClass.objects.values('my_integer', 'created_ts').distinct().order_by('-created_ts')。经过测试,它按预期工作。
      • 谢谢ianveshi。这让我更接近。但还是不太对。我需要获取符合此条件的 MyClass 的实际实例。您的解决方案仅将 created_ts 的值提供为有效的 my_integer 值。您必须假设实际上 MyClass 是一个比 my_integercreated_ts 复杂得多的类。我需要整件事。怎么样?
      • 不幸的是,由于我的笨拙,我知道实现这一目标的唯一方法是通过 postgresql 后端。对不起。
      • 再次检查此解决方案,因为此 QS 返回包含所有字段的实际实例。 User.objects.order_by('email', '-username').distinct('email') -> [, , ]。这为我们提供了按 email 分组的 Max username 的用户。以用户模型为例。
      猜你喜欢
      • 1970-01-01
      • 2012-05-18
      • 1970-01-01
      • 1970-01-01
      • 2017-10-03
      • 2022-11-07
      • 1970-01-01
      • 1970-01-01
      • 2019-04-13
      相关资源
      最近更新 更多