2025, Dec 14 15:02
Первая запись за 24 часа в Django QuerySet: быстрый и корректный способ
.first() на Django QuerySet тормозит при поиске первой записи за 24 часа. Покажем, как ускорить запрос с явным order_by по time_epoch и индексом.
Производительность Django QuerySet часто кажется предсказуемой, пока не вмешивается скрытое значение по умолчанию. Типичный кейс: быстро растущая таблица получает новые строки каждые несколько секунд, и вам нужна самая ранняя запись за последние 24 часа. Запрос выглядит тривиальным, но с .first() или Min('id') он может выполняться секундами, тогда как альтернативы — мгновенно. Причина — в неявной сортировке и в том, как база данных задействует индексы.
Обзор проблемы
У вас есть модель, куда записи приходят непрерывно, и поле с меткой времени проиндексировано. Нужно найти самую раннюю запись за последние 24 часа. Получить последнюю строку — мгновенно, а вот первую при стандартных приёмах — неожиданно долго.
from django.db import models
class EventRow(models.Model):
time_epoch = models.PositiveBigIntegerField(db_index=True)
Наивные запросы, которые вроде бы должны отработать быстро, вместо этого занимают секунды:
import time
from django.db.models import Min
cutoff_epoch = int(time.time()) - 24 * 60 * 60
first_row = EventRow.objects.filter(time_epoch__gte=cutoff_epoch).first()
first_row_id_min = EventRow.objects.filter(time_epoch__gte=cutoff_epoch).aggregate(Min('id'))
Для сравнения, получение минимальной метки времени — мгновенное; более того, даже выгрузка всех подходящих id в Python с последующим min локально в этом сценарии ощущается быстрой:
fast_ts_min = EventRow.objects.filter(time_epoch__gte=cutoff_epoch).aggregate(Min('time_epoch'))
unsafe_fast = EventRow.objects.filter(time_epoch__gte=cutoff_epoch)[0] # не гарантирует, что это самая ранняя запись по времени
python_min = min(list(EventRow.objects.filter(time_epoch__gte=cutoff_epoch)
.values_list('id', flat=True)))
Что на самом деле происходит
Суть — в неявной сортировке. Когда вы вызываете .first() без явного order_by, Django применяет сортировку по умолчанию по первичному ключу (id). В результате база должна одновременно выполнить фильтр по time_epoch и найти наименьший id среди отфильтрованных строк. Индексы на id и time_epoch есть, но такая связка мешает планировщику запросов использовать их эффективно для вашей задачи.
Напротив, агрегат Min('time_epoch') хорошо сочетается с индексом по time_epoch и выполняется почти мгновенно. Аналогично, срез unsafe_fast кажется быстрым, потому что вообще не гарантирует порядок: возвращается произвольная подходящая строка, а это не то же самое, что «самая ранняя по времени».
Чистое решение
Сделайте порядок явным и согласованным с фильтром и индексируемым полем. Если нужна самая ранняя запись за последние 24 часа, отсортируйте по метке времени и возьмите первую строку:
import time
cutoff_epoch = int(time.time()) - 24 * 60 * 60
first_by_ts = (
EventRow.objects
.filter(time_epoch__gte=cutoff_epoch)
.order_by('time_epoch')
.first()
)
Так вы используете существующий индекс по time_epoch и избегаете неявной сортировки по id. На практике в этом сценарии запрос возвращается практически мгновенно.
Почему это важно
Неявное поведение при построении запросов способно подорвать производительность, особенно на больших таблицах с интенсивными вставками. Полагаться на значения по умолчанию — значит рисковать случайно отсортировать по «не тому» полю, сбить базу с толку и превратить простой, дружелюбный к индексам проход в дорогую операцию. Итог — многосекундные задержки там, где ожидался простой поиск.
Выводы
Всегда задавайте порядок, который соответствует вашей бизнес-логике «первого» или «последнего». Если задача про время — явно сортируйте по метке времени. Не полагайтесь на неявную сортировку через .first() и помните: быстрый доступ по индексу среза вроде queryset[0] — это не «самое раннее по времени». Для быстрых агрегатов выбирайте поле, по которому есть индекс и которое согласовано с вашим условием фильтра.
Осознанный order_by снимает двусмысленность, сохраняет запросы дружелюбными к индексам и удерживает задержки под контролем даже при высокой скорости записи.