Упорядочить набор запросов по переменному значению - PullRequest
0 голосов
/ 01 мая 2018

У меня есть следующая модель:

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

client - это имя клиента, которое может иметь такие значения, как facebook, google и т. Д.

Можно ли упорядочить набор запросов так, чтобы результат чередовал значения client? Я ожидаю что-то вроде этого:

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

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

Я использую django2.x и postgres, если это поможет.

EDIT:

Некоторая дополнительная информация / требования.

  • У меня около 10-20 разных клиентов
  • Запись также имеет created DateField. Если возможный результат также должен быть заказан по дате
  • Я хочу использовать нумерацию страниц для входа, поэтому решение должно использовать ORM Джанго

Ответы [ 3 ]

0 голосов
/ 02 мая 2018

Это не самый производительный способ, но работает:

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.

Этот код хорош, потому что мы по-прежнему работаем с Queryset, поэтому все остальные функции сортировки, упорядочивания, менеджеров, разбивки на страницы и другие будут работать.

0 голосов
/ 05 мая 2018

Поскольку вы используете Postgres, вы можете использовать Функции окна , которые выполняют вычисления для набора строк таблицы, которые так или иначе связаны с текущей строкой. Еще одна полезная информация заключается в том, что вы используете Django2.x, который поддерживает функции окна ( Django docs ), что позволяет добавить предложение 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 определяет, сколько элементов будет в каждой чередующейся группе.

приписка

Спасибо, что задали этот вопрос, потому что он помог мне лучше понять функции окна.

Удачного кодирования:)

0 голосов
/ 01 мая 2018

Может быть, следующее будет работать с модулем 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 '''  
...