Начну немного издалека. На протяжении всего времени моей работы с джанго, я как-то не очень задумывался о производительности приложений. Мне везло(хотя это как посмотреть), проекты были не очень большие, нагрузка относительно маленькая, да и конфигурация сервера не давала усомниться в том что пользователь получит ответ на запрос в приемлемые сроки даже в "часы пик"(по местным меркам). Но вот случилось, что я столкнулся с проблемой оптимизации работы своих проектов.
Сразу в голове закрутились мысли о умном кеше(до сих пор крутятся), оптимизации кода, оптимизации структуры базы данных ну и конечно оптимизация запросов к этой базе. Принял решение внедрять изменения постепенно и с проверкой, и со сбором локальной статистики "как было - как стало". Первым делом решил оптимизировать запросы к базе. Конечно был вставлен в нужные места вызов 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. Я сам продолжаю его только постигать чего и вам советую.
А вы как им пользуетесь? Очень инетресно.

По идее M2MManager мог бы кешировать зарос к count, чтобы не вымучивать всё это.
Нужно посмотреть, что он на самом деле делает. ;)
Ну как бы конечно мог, но дело то не в кешировании, а в самом получении количества. Суть то этого ухищрения чтобы не плодить лишние запрос count совсем. Наверно можно было бы по-извращаться и заставить автоматически делать подобное для всех M2M полей в модели. Но это уже перебор:)
По идее M2MManager мог бы кешировать зарос к count, чтобы не вымучивать всё это.
Нужно посмотреть, что он на самом деле делает. ;)
У меня на 100000 объектах и 300 подобъектов у каждого (вида m2m), и получении данных о 100 объектов и количестве подобъектов "неоптимизированные" 100 и 1 запрос работают быстрее, чем один, который с count(*). БД postgres, памяти много, оптимизированная.
дак подзапрос, это тоже запрос! :)
Ну по крайней мере он выполняется inplace, и есть надежда на хорошую оптимизацию со стороны СУБД