Как сделать Django Queryset, который выбирает записи с максимальным значением внутри группы

Вот мой класс Django:

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

Я хотел бы получить экземпляры MyClass, у которых есть самое последнее created_ts для каждого уникального значения my_integer. Я не могу понять, как это сделать.

Может кто-нибудь показать мне, как это сделать?

6 ответов

Это поможет вам

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}
]
</utc></utc></utc>


Вы можете выполнить необработанный запрос:

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, как это было предложено другими ответами.


Это действительно простой способ сделать это. Предполагая, что объем данных у вас не очень большой, это будет работать прилично. Вы можете использовать это в своих представлениях, переопределив функцию 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.

Edit

Вот гораздо более быстрая версия.

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

По сути, это работает с получением всех ваших объектов из вашего db, группируя их по целому числу, затем сортируя каждую группу на основе метки времени и получая только первую из каждой группы. Ускорение этого еще больше выходит за рамки моих навыков, но я уверен, что есть некоторые способы.

Вот timeit этого в сравнении с предыдущим, и только 6 записей в db:

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, либо собственный SQL-запрос.


MyClass.objects.order_by('my_integer', '-created_ts').distinct('my_integer')

Согласно distinct, вам нужно вызывать разные атрибуты в том же порядке, что и в order_by. Следовательно, упорядочивайте элементы на основе целого числа, а затем в обратную временную метку и вызывайте на них разные, что возвращает последний экземпляр для каждого целого.


Попробуйте это;

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


непроверенных

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

licensed under cc by-sa 3.0 with attribution.