Кеширование queryset.count в django
Как-то обнаружил, что у меня идут несколько одинаковых запросов вида SELECT COUNT(*) ...
. Оказалось (да, для меня это было новостью :) ), что метод queryset.count()
в джанго кешируется по особому. Но лучше начать рассказ издалека.
Как известно, объекты queryset
у ORM django являются “ленивыми”, а так же кешируются.
Т.е., преподолжим у нас такая модель:
class Item(models.Model):
name = models.CharField(max_length=50)
Тогда при создании запроса фактически обращения к БД не происходит (отсюда название lazy - “ленивый”):
items = Item.objects.all()
Оно происходит, когда мы непосредственно обращаемся к объектам из запроса, например в цикле:
for item in items:
print item.name
При исполнении инструкции for item in items:
был такой запрос к БД:
SELECT "main_item"."id", "main_item"."name" FROM "main_item";
При следующем обращении к объектам уже запроса к БД не будет, т.к. все объекты уже были “потроганы” и они попали в кэш. Т.е. этот код сделает только одно обращение к БД:
for item in items: # hit the database
print item.name
for item in items: # cache
print item.name
Тем не менее, есть некоторые нюансы, когда может произойти второй запрос к БД. Не буду дублировать документацию, чтобы не загромождать статью. Можно почитать здесь: https://docs.djangoproject.com/en/dev/topics/db/queries/#caching-and-querysets.
Теперь непосредственно про count
Зная, что queryset кешируется, мне казалось, что и .count() тоже кешируется. Но нет (точнее не всегда). Если вызываем метод count() до того, как исходный queryset попал в кеш, будет обращение к БД при каждом вызове count (данное обращение не ленивое, ведь count() возвращает число, а не другой queryset, как это делают all, filter, exclude):
items = Item.objects.all() # not hit DB
items.count() # hit DB
items.count() # hit DB
items.count() # hit DB
for item in items: # hit DB and put into cache
print item.name
Однако, если исходный queryset попал в кеш, то count уже не будет трогать БД:
items = Item.objects.all() # no DB hit
for item in items: # hit DB and put to cache
print item.name
items.count() # cache
items.count() # cache
items.count() # cache
Соответственно все это относится и к шаблонам django. В коде, который делал несколько одинаковых запросов SELECT COUNT(*) ...
, как раз были проверки вида:
{% if items.count %}
и просто вывод количества:
{{ items.count }}
При этом до этих строк не было обращения к самим объектам items. В итоге на каждой из этих строк шел запрос к БД.
Опять же, если до этого где-то был цикл, например такой:
{% for item in items %}
{{item.name}}
{% endfor %}
то {{ items.count }}
уже не обращался к БД.
Итак, варианты для избежания лишних запросов.
-
Если мы знаем, что где-то дальше будет перебор всех элементов из queryset, то вполне уместно использовать
len
.Python код:
len(items) # DB len(items) # cache for item in items: # cache # ...
или наоборот, что тоже верно:
for item in items: # DB # ... len(items) # cache len(items) # cache
Шаблон django:
{{ items|length }} # DB {{ items|length }} # cache {% if items|length %} # cache {% for item in items %} # cache
или наоборот:
{% for item in items %} # DB {{ items|length }} # cache {{ items|length }} # cache {% if items|length %} # cache
-
Если нужно только подсчитать количество, либо queryset, для которого нужно количество не совпадает с тем, который будет использоваться для доступа к элементам, то надо использовать count(). Но вызывать его лучше только единожды
Если в шаблоне нужно обратиться к count более одного раза, то вместо этого:
{{ items.count }} {{ items.count }}
надо либо во view, который генерит этот шаблон, добавить переменную items_count в контекст и в шаблоне использовать ее:
# views.py context['items_count'] = items.count() # template {{ items_count }} {{ items_count }}
либо можно использовать
{% with items.count as items_count %}
(не добавляя в контекст новых переменных из views.py):# template {% with items.count as items_count %} {{ items_count }} {{ items_count }} {% endwith %}
Конечно, в этой статье под словом “кеш” имеется в виду внутренний кеш queryset. Он никак не связан с кешированием.