Волшебный метод extra

Начну немного издалека. На протяжении всего времени моей работы с джанго, я как-то не очень задумывался о производительности приложений. Мне везло(хотя это как посмотреть), проекты были не очень большие, нагрузка относительно маленькая, да и конфигурация сервера не давала усомниться в том что пользователь получит ответ на запрос в приемлемые сроки даже в "часы пик"(по местным меркам). Но вот случилось, что я столкнулся с проблемой оптимизации работы своих проектов.

Сразу в голове закрутились мысли о умном кеше(до сих пор крутятся), оптимизации кода, оптимизации структуры базы данных ну и конечно оптимизация запросов к этой базе. Принял решение внедрять изменения постепенно и с проверкой, и со сбором локальной статистики "как было - как стало". Первым делом решил оптимизировать запросы к базе. Конечно был вставлен в нужные места вызов select_related, что немного улучшило ситуацию, но не сильно. Тут мой взор пал на метод extra у джанговского QuerySet. Я давно слышал и читал о его магическом действии и способности расширить возможности джанговского ORM, но как-то руки не доходили попробовать. Но попробовав, я понял всю его мощь и необходимость в использовании.

И так лучше показать его возможности на небольшом примере, который иллюстрирует наиболее, на мой взгляд, распространенный юз кейс extra. Допустим у нас есть такая модель:

class Entry( models.Model ):
    title = models.CharField( max_length = 150 )

    comments = models.ManyToManyField( Comment )

Модель простая и понятная. Всё в ней хорошо. Теперь я хочу вывести некоторое количество этих записей в шаблоне ну и количество комментариев у каждой. Пишу в лоб так:

{% for entry in entries %}
    <p>{{entry.title}}</p>
    <p>Comments count: {{entry.comments.count}}</p>
{% endfor %}

Ну казалось бы что может быть проще. У M2MManager есть хороший метод count, который возвращает количество "соединенных" объектов. НО! Да большое такое "но" и громкое:) Каждый вызов этого метода - это запрос к базе. Каждый. Т.е. если я хочу вывести на странице 10 записей, то ко всему прочему заимею 10 запросов, которые всего-навсего выводят маленькую цифру и всё. Так вот, если бы в джанго не было бы ORM (о ужас!), то написав вполне не сложный запрос, мы бы эту проблему решили (а может и не заимели бы вовсе, поскольку сразу этот вариант бы учли). А раз мы пользуемся благами орм, значит должны плясать под его дудку. Но дудка у него не простая и мелодию разнообразить все-таки можно. Вот тут на передний план и выходит метод extra, точнее его подмножество extra(select={}).

И так теперь наши entries получаются немного хитрее чем objects.all():

entries = Entry.objects.all().extra( select = { "comment_count" : "SELECT COUNT(*) FROM myapp_entry_comments as ec WHERE ec.entry_id = myapp_entry.id" } )

Да тут конечно нужно немого больше усилий, чем просто вызвать count. Но оно того стоит. Теперь немого подробнее про запрос. Моё приложение называет myapp и имена таблиц соответствующие моделям стандартные и сгенерированы джангой. Поскольку таблица myapp_entry и так будет в WHERE у внешнего запроса, поэтому я не её не упоминаю, а пишу лишь myapp_entry_comments которая не явного существует для поддержания связи многий-ко-многим.

Примерно такой запрос SQL получается в сумме:

SELECT myapp_entry.*, (SELECT COUNT(*) FROM myapp_entry_comments as ec WHERE ec.entry_id = myapp_entry.id) FROM myapp_entry

Доступ к результату под-запросу будет осуществляться через атрибут comment_count у соответствующего entry:

{{entry.comment_count}}

После применения в некоторых местах похожего приема удалось уменьшить количество запросов в 2(!) раза. Так что игра стоит свеч.

Это только часть возможностей которые предоставляет метод extra. Я сам продолжаю его только постигать чего и вам советую.

А вы как им пользуетесь? Очень инетресно.

Комментарии 5

  1. qewerty написал:

    По идее M2MManager мог бы кешировать зарос к count, чтобы не вымучивать всё это.

    Нужно посмотреть, что он на самом деле делает. ;)

    Оставлен 14 Декабрь 2007 в 03:04
  2. Александр Кошелев написал:

    Ну как бы конечно мог, но дело то не в кешировании, а в самом получении количества. Суть то этого ухищрения чтобы не плодить лишние запрос count совсем. Наверно можно было бы по-извращаться и заставить автоматически делать подобное для всех M2M полей в модели. Но это уже перебор:)

    Оставлен 14 Декабрь 2007 в 21:31
  3. qewerty написал:

    По идее M2MManager мог бы кешировать зарос к count, чтобы не вымучивать всё это.

    Нужно посмотреть, что он на самом деле делает. ;)

    Оставлен 31 Декабрь 2007 в 05:22
  4. Большой Лис написал:

    дак подзапрос, это тоже запрос! :)

    Оставлен 04 Июнь 2008 в 14:14
  5. Александр Кошелев написал:

    Ну по крайней мере он выполняется inplace, и есть надежда на хорошую оптимизацию со стороны СУБД

    Оставлен 04 Июнь 2008 в 18:52