September 4, 2020

Чуточку облегчаем жизнь проекту

Проблема в неоптимальном использовании queryset, что приводит к излишне тяжелым запросам, либо вовсе к появлению лишних запросов. За прошедшую неделю я встретил подобное несколько раз, а значит нам нужно поговорить.

Рассмотрим один пример (в примере нас интересует исключительно queryset exist_borrower):

В этом методе кверисет exist_borrower на ровном месте делает в базу три одинаковых запроса вида:

Не обязательно вчитываться, достаточно понимать что мы достаем из базы ВСЕ поля модели. Трижды.

Первый запрос формируется при вызове if exist_borrower. Да, вы можете сказать что при вызове exist_borrower.__bool__() - результат запроса кэшируется, и будете правы. Но далее по коду у exist_borrower дергаются методы .get() и .last() которые игнорируют этот кэш. В результате мы получаем три одинаковых запроса, где дергаются все поля модели.

Попробуем последовательно улучшить этот код.

Если нужно проверить что кверисет не пустой - следует всегда использовать метод .exists()

При выполнении exist_borrower.exists() получается такой запрос:

Гораздо лучше, правда?

Если еще приглядеться к коду, то можно заметить что нам достаточно всего один раз получить заемщика, и на весь метод получится лишь один запрос:

exist_borrower = Borrower.objects.filter(**params).first()

В принципе на этом варианте можно остановиться. Но если вы понимаете что данный метод очень часто используется, либо модель довольно тяжелая, то можно сразу получить лишь нужные поля:

В таком случае единственный запрос будет выглядеть так:

Выглядит гораздо лучше, чем три "толстых" запроса

Я умышленно не привожу в примере метод .only(), т.к. его использование требуется гораздо реже, требует от разработчика большей внимательности, и может сыграть злую шутку если неаккуратно с ним обращаться.

Еще немного

Если нужно проверить кверисет на пустоту - делаем queryset.exists()

Если нужно получить количество объектов в кверисете, то необходимо использовать только queryset.count() и никогда не следует делать len(queryset) (встречается и такое).

len(queryset) достает из базы объекты со всеми полями (тот самый большой запрос из начала заметки), а затем питоном получает их количество.

queryset.count() сразу спрашивает у базы количество объектов:

Не используйте обращение по индексу к объекту в кверисете. Обращение к объекту из списка по его какому-то магическому номеру - плохая практика.

Если вам нужен первый или последний объект в отсортированном кверисете - для этого есть методы .first() и .last(), они хорошо влияют на читаемость (к тому же queryset[0] необходимо оборачивать в блок try except).

Если из queryset нужно достать объект под определенным номером, скорее всего это значит что-то пошло не так в реализуемой логике. Попробуйте еще раз.


Если вы сомневаетесь в запросах которые генерируются ORM, или интересно посмотреть в каких ситуациях запросы кешируются, а в каких нет - можно запустить шелл с отладкой запросов:

./manage.py shell_plus --print-sql (это возможно благодаря батарейке django_extensions)

Все запросы в базу будут отображаться в консоли в момент выполнения.

Итоги

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

Также, если вам вдруг посчастливилось дорабатывать метод подобный тому что в примере - хорошим тоном будет навести в таком методе порядок.

Документация, чтобы освежить память:

https://djbook.ru/rel1.9/topics/db/queries.html про запросы на русском (django 1.9)

https://docs.djangoproject.com/en/2.0/topics/db/queries/ про запросы на англ. (django 2.0)