Решение медленного запроса с помощью Foreignkey с isnull = False и order_by в Django ListView - PullRequest
0 голосов
/ 27 апреля 2020

У меня есть Django ListView , который позволяет разбивать страницы на «активных» людей.

(упрощенные) модели:

class Person(models.Model):
    name = models.CharField()
    # ...
    active_schedule = models.ForeignKey('Schedule', related_name='+', null=True, on_delete=models.SET_NULL)

class Schedule(models.Model):
    field = models.PositiveIntegerField(default=0)
    # ...
    person = models.ForeignKey(Person, related_name='schedules', on_delete=models.CASCADE)

Таблица Person содержит почти 700 000 строк, а таблица «Расписание» содержит чуть более 2 000 000 строк (в среднем на каждого сотрудника приходится 2-3 записи в расписании, хотя у многих их нет, а у многих больше). Для «активного» Персона установлен active_schedule ForeignKey, из которых в любой момент времени может быть около 5.000.

Предполагается, что ListView отображает все активные персоны, отсортированные по field в расписании (и некоторые другие условия, которые, кажется, не имеют значения для этого случая).

Запрос затем становится:

Person.objects
    .filter(active_schedule__isnull=False)
    .select_related('active_schedule')
    .order_by('active_schedule__field')

В частности, order_by в связанном поле делает этот запрос ужасно медленным (что is: это занимает около секунды, что слишком медленно для веб-приложения).

Я надеялся, что условие filter выберет 5000 записей, которые затем станут относительно легко сортируемыми. Но когда я запускаю объяснение для этого запроса, он показывает, что база данных (Postgres) работает с большим количеством строк:

Gather Merge  (cost=224316.51..290280.48 rows=565366 width=227)
  Workers Planned: 2
  ->  Sort  (cost=223316.49..224023.19 rows=282683 width=227)
        Sort Key: exampledb_schedule.field
        ->  Parallel Hash Join  (cost=89795.12..135883.20 rows=282683 width=227)
              Hash Cond: (exampledb_person.active_schedule_id = exampledb_schedule.id)
              ->  Parallel Seq Scan on exampledb_person  (cost=0.00..21263.03 rows=282683 width=161)
                    Filter: (active_schedule_id IS NOT NULL)
              ->  Parallel Hash  (cost=67411.27..67411.27 rows=924228 width=66)
                    ->  Parallel Seq Scan on exampledb_schedule  (cost=0.00..67411.27 rows=924228 width=66)

Я недавно изменил модели, чтобы они были такими. В предыдущей версии у меня была модель с ~ 5000 активными людьми. Выполнение order_by на этом маленьком столе было значительно быстрее! Я надеюсь достичь той же скорости с текущими моделями.

Я попытался получить только те поля, которые необходимы для Listview (используя values), что немного помогает, но не сильно. Я также попытался установить значение related_name на active_schedule и подойти к проблеме из расписания, но это не имеет значения. Я попытался поставить db_index на Schedule.field, но это, кажется, только делает вещи медленнее. Условные запросы также не помогли (хотя я, вероятно, не пробовал все возможности). Я в растерянности.

Оператор SQL, сгенерированный запросом ORM:

SELECT 
    "exampledb_person"."id", 
    "exampledb_person"."name", 
    ...
    "exampledb_person"."active_schedule_id", 
    "exampledb_person"."created", 
    "exampledb_person"."updated", 
    "exampledb_schedule"."id", 
    "exampledb_schedule"."person_id", 
    "exampledb_schedule"."field", 
    ...
    "exampledb_schedule"."created", 
    "exampledb_schedule"."updated" 
FROM 
    "exampledb_person" 
INNER JOIN 
    "exampledb_schedule" 
ON ("exampledb_person"."active_schedule_id" = "exampledb_schedule"."id") 
WHERE 
    "exampledb_person"."active_schedule_id" IS NOT NULL 
ORDER BY 
    "exampledb_schedule"."field" ASC

(Некоторые поля были опущены для простоты.)

Можно ли ускорить этот запрос, или я должен вернуться к использованию специальной модели для активного человека?

РЕДАКТИРОВАТЬ: Когда я изменяю запрос, только для сравнения / тестирования, для сортировки по полю без индекса на Person запрос одинаково показан. Однако, если я добавлю индекс к этому полю, запрос будет быстрым! Мне пришлось попробовать это, поскольку оператор SQL действительно показывает, что он упорядочен по "exampledb_schedule"."field" - поле без индекса, но, как я уже сказал: добавление индекса к полю не имеет значения.

РЕДАКТИРОВАТЬ: Полагаю, стоит также отметить, что при попытке выполнить более простой запрос сортировки непосредственно по расписанию, либо по индексируемому полю, либо нет, это НАМНОГО быстрее. Например, для этого теста я добавил индекс к Schedule.field, а затем быстро выполняется следующий запрос:

Schedule.objects.order_by('field')

Где-то здесь лежит решение ...

1 Ответ

0 голосов
/ 28 апреля 2020

Комментарии @ guarav и мои правки указали мне направление решения, которое какое-то время смотрело мне в лицо ...

Предложение фильтра в моих вопросах - filter(active_schedule__isnull=False) - похоже, делает недействительными индексы базы данных. Я не знал об этом и надеялся, что эксперт по базам данных укажет мне это направление.

Решение состоит в том, чтобы отфильтровать по Schedule.field, что равно 0 для неактивных записей Person и> 0 для активных. :

Person.objects
    .select_related('active_schedule')
    .filter(active_schedule__field__gte=1)
    .order_by('active_schedule__field')

Этот запрос правильно использует индексы и является быстрым (20 мс против ~ 1000 мс).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...