четверг, 23 июня 2011 г.

Уменьшаем кол-во запросов, которые генерируются Django ORM

Думаю, для многих уже совсем не секрет, что делать, чтобы уменьшить кол-во запросов, которые генерируются при помощи Django ORM. Однако, столкнувшись в очередной раз с необходимостью сбавить обороты и ускорить генерацию страницы я захотел вынести все эти методы в один пост.

Лирическое отступление: Для того, чтоб лучше понять как эти методы работают, я задам легкую схему для моделей, упоминаемых в статье. Во-первых, это будет встроенная auth.User, затем это будет модель статьи, которая будет версионироваться при помощи django-reversion. Иными словами, наш models.py будет выглядеть как-то так:

from django.db import models
from django.utils.translation import ugettext_lazy as _

import reversion


class Article(models.Model):

    STATE_NEW, STATE_SUBMITTED, STATE_REJECTED, STATE_ACCEPTED = range(1, 5)
    STATE_CHOICES = (
        (STATE_NEW, _('New')),
        (STATE_SUBMITTED, _('Submitted')),
        (STATE_REJECTED, _('Rejected)),
        (STATE_ACCEPTED, _('Accepted)),
    )

    title = models.CharField(_('title'), max_length=64)
    content = models.TextField(_('content'))
    state = models.PositiveIntegerField(_('state'), choices=STATE_CHOICES,
        default=STATE_NEW)

    writer = models.ForeignKey('auth.User', related_name='articles_writer',
        verbose_name=_('writer'))
    editor = models.ForeignKey('auth.User', blank=True, null=True,
        related_name='articles_editor', verbose_name=_('editor'))

reversion.register(Article)

Метод 1. Использование select_related вместо all

Здесь все очень просто. Если у моделей есть не-нулевые FK поля Вы явно захотите получать данные в них без дополнительных запросов к БД. Потому если писатель статьи должен быть явно указан на сайте, то считывайте статьи перед паджинацией как,

articles = Article.objects.select_related()

а не .all(). На самом деле метод .all() хорош как по мне только для "нишевых" (с малым кол-вом или вообще без связей) моделей. В остальных случаях надо смотреть по ситуации, может где-то .defer() после .select_related(*fields) пригодится, может где и .annotate() будет использован.

В целом с .select_related() я думаю всем все давным давно ясно, не забывайте только, что для не-нулевых FK-полей этот метод ничего не даст. И следующий код сгенерит в худшем случае Articles.objects.count() + 1 запросов (если все поля editor заполнены), вместо 1:

articles = Article.objects.select_related()
for article in articles:
    print(article.title, article.state, article.writer, article.editor)

Метод 2. Использование model.fk_id вместо model.fk.id

На самом деле, с первого взгляда кажется, что метод несколько бесполезен. Ну что там можно узнать про объект только по его идентификатору? Однако, смею вас уверить, это кажется только на первый взгляд. На самом деле во всяких filter или map и прочих lambda-функциях без него не обойтись.

Простой пример, необходимо найти все версии, оставленные редактором статьи. Что сразу приходит в голову? Использовать простой запрос,

article = Article.objects.get(pk=XXX)
versions = Version.objects.get_for_object(article).\
                           exclude(revision__user=None)
versions = article.editor and versions.filter(revision__user=article.editor) \
                          or versions.none()

И вроде все просто и безоблачно. Но только до поры до времени, для начала это +1 запрос к БД, а потом код переместиться в цикл или надо будет расширить параметры поиска искать не только ревизии редактора, но и какого-то системного пользователя.

Выход прост, изначально помнить, что редактор может принимать нулевое значение, а значит обращение к .editor спровацирует +1 запрос к БД. А значит используем .editor_id и точно знаем, что здесь искать кита мы не будем.

article = Article.objects.get(pk=XXX)
versions = Version.objects.get_for_object(article).\
                           exclude(revision__user=None)
versions = \
    article.editor_id and versions.filter(revision__user__pk=article.editor_id) \
                      or versions.none()

Метод 3. Предопределение FK значения

Сразу предупреждаю, пользуйтесь этим методом осторожно и только если уверены, что знаете что делаете :)

Помните пример из первого метода? Когда каждый запрос к полю редактора статьи стоил нам одного запроса. Нехорошо это, ведь и .select_related() нам тут не помощник. Но, не все так печально, а иначе очень уж просто. Надо всего лишь считать всех пользователей, а потом подставить необходимое значение в аттрибут ._editor_cache (примечание: на месте editor может быть любое название вашего FK-поля). А теперь еще раз и в коде:

articles = Article.objects.select_related('writer')
users = User.objects.all()
users = dict([(user.pk, user) for user in users])

for article in articles:
    article._editor_cache = users.get(article.editor_id)
    print(article.title, article.state, article.writer, article.editor)

И мы в итоге получаем гарантировано два запроса к БД, вместо Article.objects.сount() + 2 в худшем случае (опять же при условии, что все поля editor заполнены). Просто и изящно.

Однако надо помнить, что установив ручками значение в ._{{ fk_field }}_cache именно вы в ответе за него, а никак не Django. И если вдруг редактором статьи вместо Васи Пупкина станет Джон Доу, то вы знаете что делать :)

Метод 4. Ручное исполнение запросов

Я думаю, многие обрадовались и подумали, ну наконец-то про .raw(), ну наконец-то про SQL, к черту тот ORM, он и медленный и вообще сложно с ним. Но нет, поспешу разочаровать. Я лишь про то, что во многих случаях может пригодится явное преобразование QuerySet объектов в что-то более питоновское, как-то list и в дальнейшем использование именно питоновских функций для выборки данных, например filter(), а не метода .filter(). Взвесьте все за и против и помните, что QuerySet преобразованный в list есть не попросит к базе уже не обратиться, а обыкновенный еще как сможет.

Ну и про .raw() и .extra() не забывайте. Как говорится, есть моменты когда ваше SQL кунг-фу будет сильнее SQL кунг-фу Django ORM.

Метод 5. Или прочее

Сразу оговорюсь, что я не пытался упомянуть о всех методах уменьшения кол-ва запросов, а лишь хотел преподнести вам методы наиболее используемые мною при работе с Django. Для дальнейшего просветления я рекомендую вам прочитать раздел про оптимизацию доступа к базе данных в документации Django (версия для 1.3 или 1.2), ну или напрямую обращаться к Google или StackOverflow с теми же ключевыми словами, Django database optimization.

На сим прощаюсь и до новых встреч. Если появились вопросы или я где-то сглупил - милости прошу в комменты!

blog comments powered by Disqus