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 снимает двусмысленность, сохраняет запросы дружелюбными к индексам и удерживает задержки под контролем даже при высокой скорости записи.