【问题标题】:Order queryset by alternating value按交替值排序查询集
【发布时间】:2018-10-11 17:26:30
【问题描述】:

我有以下型号:

class Entry(models.Model):
    name = models.Charfield(max_length=255)
    client = models.Charfield(max_length=255)

client 是客户端的名称,可以有 facebookgoogle 等值。

是否可以对查询集进行排序以使结果交替client 的值? 我期望的是这样的:

Entry.objects.order_by('alternate client') --> 

| client   | name   |
| google   | robert |
| facebook | linda  |
| google   | kate   | 
| facebook | jack   |
| google   | nina   |
| facebook | pierre |    

如果有帮助,我正在使用 django2.x 和 postgres。

编辑:

一些附加信息/要求。

  • 我有大约 10 到 20 个不同的客户
  • 条目也有一个created DateField。如果可能,结果也应按日期排序
  • 我想对 Entry 使用分页,所以解决方案应该使用 django 的 ORM

【问题讨论】:

  • name 列的任何特定订单要求?它们可以按任何顺序排列吗?
  • 是的,它们可以按任何顺序排列。在实际示例中,我有一个 created 字段。如果可以降序排列就好了。
  • alternate 是什么?你只需要`Entry.objects.order_by('client').order_by('-name') 吗?
  • 不,我需要的结果 client 正在改变。对于这样的两个值:facebook,google,facebook,google,...
  • 你有多少clients

标签: django postgresql django-models django-queryset


【解决方案1】:

由于您使用 Postgres,您可以使用它的Window Functions,它对一组与当前行有某种关联的表行执行计算。另一个好的信息在于您使用支持窗口函数(Django docs)的 Django2.x,它允许将 OVER 子句添加到 Querysets

您的用例可以通过单一 ORM 查询来解决,例如:

from django.db.models.expressions import Window
from django.db.models.functions import RowNumber
from django.db.models import F

results = Entry.objects.annotate(row_number=Window(
    expression=RowNumber(),
    partition_by=[F('client')],
    order_by=F('created').desc())
).order_by('row_number', 'client')

for result in results:
    print('Id: {} - client: {} - row_number {}'.format(result.id, result.client, result.row_number))

输出:

Id: 12 - client: facebook - row_number 1
Id: 13 - client: google - row_number 1
Id: 11 - client: facebook - row_number 2
Id: 8 - client: google - row_number 2
Id: 10 - client: facebook - row_number 3
Id: 5 - client: google - row_number 3
Id: 9 - client: facebook - row_number 4
Id: 3 - client: google - row_number 4
Id: 7 - client: facebook - row_number 5
Id: 2 - client: google - row_number 5
Id: 6 - client: facebook - row_number 6
Id: 1 - client: google - row_number 6
Id: 4 - client: facebook - row_number 7

原始 SQL 如下所示:

SELECT 
"orm_entry"."id",
"orm_entry"."name",
"orm_entry"."client",
"orm_entry"."created",
ROW_NUMBER() OVER (PARTITION BY "orm_entry"."client" ORDER BY "orm_entry"."created" DESC) AS "row_number" 
FROM "orm_entry" 
ORDER BY "row_number" ASC, "orm_entry"."client" ASC

窗口函数被声明为一个聚合函数,后面跟着一个 OVER 子句,它准确地指示了行是如何分组的。应用窗口函数的行组称为“分区”。

您会注意到我们按 'client' 字段对行进行了分组,您可以得出结论,在我们的示例中,我们将拥有两个分区。第一个分区将包含所有 'facebook' 条目,第二个分区将包含所有 'google' 条目。在其基本形式中,分区与普通的聚合函数组没有什么不同:只是一组被某些标准视为“相等”的行,并且该函数将应用于所有这些行以返回单个结果。

在您的示例中,我们可以使用 row_number 窗口函数,它只返回从 1 开始的分区内当前行的索引。这有助于我在 order_by('row_number', 'client') 中建立交替输出。

其他信息:

如果你想实现这样的订单:

'facebook','facebook', 'google','google','facebook','facebook','google','google'

'facebook','facebook','facebook','google','google','google','facebook', 'facebook','facebook'

您需要对上一个查询进行一些与数学相关的小修改,例如:

GROUP_SIZE = 2
results = Entry.objects.annotate(row_number=Window(
    expression=RowNumber(),
    partition_by=[F('client')],
    order_by=F('created').desc())
).annotate(row_number=(F('row_number') - 1)/GROUP_SIZE + 1).order_by('row_number', 'client')

for result in results:
    print('Id: {} - client: {} - row_number {}'.format(result.id, result.client, result.row_number))

输出:

Id: 12 - client: facebook - row_number 1
Id: 11 - client: facebook - row_number 1
Id: 8 - client: google - row_number 1
Id: 13 - client: google - row_number 1
Id: 10 - client: facebook - row_number 2
Id: 9 - client: facebook - row_number 2
Id: 3 - client: google - row_number 2
Id: 5 - client: google - row_number 2
Id: 7 - client: facebook - row_number 3
Id: 6 - client: facebook - row_number 3
Id: 1 - client: google - row_number 3
Id: 2 - client: google - row_number 3
Id: 4 - client: facebook - row_number 4

您会注意到GROUP_SIZE 常量定义了每个交替组中的项目数。

附言

感谢您提出这个问题,因为它帮助我更好地理解了窗口函数。

编码愉快:)

【讨论】:

  • 非常好!谢谢,这正是我想要的。我还有一个问题。是否也可以通过这种方法获得这样的订单:“facebook,facebook,google,google,facebook,facebook,google,google”
  • 不客气。这是可能的,但需要对 ORM 查询进行一些小的修改。我更新了我的答案,请随时查看“附加信息”段落。
  • 这不仅对 Django 用户来说是一个很好的答案!谢谢。
  • 这个答案很棒,应该是公认的答案! +1
【解决方案2】:

这不是最高效的方式,但很有效:

from itertools import zip_longest
from django.db.models import Case, When

grouped_pks = []
for client in Entry.objects.values_list('client', flat=True).distinct():
    grouped_pks.append(
        Entry.objects.filter(client=client).values_list('pk', flat=True)
    )

alternated_pks = [
    x for x in
    filter(
        None,
        sum(zip_longest(*grouped_pks), ())
    )
]
alternated_pks_order = Case(
    *[
        When(pk=pk, then=position)
        for position, pk in enumerate(alternated_pks)
    ]
)

entries = Entry.objects.filter(pk__in=alternated_pks).order_by(alternated_pks_order)
for entry in entries:
    print('id: {} - client: {}'.format(entry.id, entry.client))

预期输出:

id: 8901 - client: google
id: 8890 - client: facebook
id: 8884 - client: google
id: 8894 - client: facebook
id: 8748 - client: google
id: 8891 - client: facebook
id: 8906 - client: google
id: 8909 - client: facebook
id: 8888 - client: google
id: 8895 - client: facebook
id: 8919 - client: google
id: 8910 - client: facebook
id: 8878 - client: google
id: 8896 - client: facebook
id: 8916 - client: google
id: 8902 - client: facebook
id: 8917 - client: google
id: 8885 - client: facebook
id: 8918 - client: google
id: 8903 - client: facebook
id: 8920 - client: google
id: 8886 - client: facebook
id: 8904 - client: facebook
id: 8905 - client: facebook
id: 8887 - client: facebook
id: 8911 - client: facebook
id: 8897 - client: facebook
id: 8912 - client: facebook
id: 8898 - client: facebook
id: 8899 - client: facebook
id: 8914 - client: facebook
id: 8900 - client: facebook
id: 8915 - client: facebook

这是python3的代码,但是如果你想和python 2一起使用,把zip_longest函数改成izip_longest

这段代码很好,因为我们仍然使用查询集,所以所有其他排序、排序、管理器、分页和其他东西仍然可以工作。

【讨论】:

  • 谢谢!这也适用于未知数量的客户吗?
  • 嗨@ilse2005,我刚刚更新了答案以支持它。逻辑和之前一样,但不是在zip_longest 上逐个传递,而是创建一个列表列表并使用解包* 符号传递参数。
【解决方案3】:

也许以下方法可以使用 numpy 模块

import numpy
queryset = Entry.objects.all()

''' catch the number of distinct Entry considering the client fields '''

queryset_client_entries = Entry.objects.distinct('client')

new_list = list(numpy.resize(queryset_client_entries, queryset.count())) 
''' An alternative list of clients depending on the length of queryset '''  

【讨论】:

  • 不确定我是否得到你的答案。使用 django 的 ORM 也很好,因为我想稍后使用分页,并且条目列表可能很长。
猜你喜欢
  • 2012-07-30
  • 2014-08-09
  • 1970-01-01
  • 2019-08-14
  • 1970-01-01
  • 2020-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多