Кеширование на уровне Django ORM - где оно стреляет в ногу
У каждого QuerySet есть встроенный кеш результатов - _result_cache.
QuerySet ленивый - в БД не идет, пока его не начнут вызывать. При первой полной итерации запрос выполняется, результат складывается в кеш инстанса. Вторая итерация того же инстанса - уже из памяти
Один и тот же запрос дважды это два разных QuerySet
# два запроса в БД
print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])
# один запрос
entries = Entry.objects.all()
print([e.headline for e in entries])
print([e.pub_date for e in entries])
Кеш живет на инстансе QuerySet, не на запросе. Каждый вызов .all() создает новый инстанс с пустым кешем - даже если SQL идентичный
Не все обращения заполняют кеш
qs = Entry.objects.all()
print(qs) # __repr__ - НЕ кеширует
print(qs[5]) # SELECT с LIMIT/OFFSET - запрос
print(qs[5]) # еще один запрос
Заполняют кеш:
[entry for entry in qs] # полная итерация
list(qs) # материализация
bool(qs) # if qs:
entry in qs # проверка вхождения
print(qs) дергает repr, который берет первые 20 + 1 элемент (REPR_OUTPUT_SIZE + 1, в SQL это LIMIT 21) и не считает это полной оценкой. Кеш пустой
С индексацией - пока QuerySet не выполнен, qs[5] идет в БД с LIMIT/OFFSET. После list() - из кеша
Связанные объекты в кеш не попадают сами
entries = Entry.objects.all()
for e in entries:
print(e.blog.name) # +1 запрос на каждую запись, N+1
entries = Entry.objects.select_related('blog')
for entry in entries:
print(entry.blog.name) # из join
Базовый кеш тянет только поля самой модели. FK кешируется на инстансе после первого обращения - e.blog второй раз для того же entry уже из памяти. Для другого entry - снова запрос. Решается select_related для FK/OneToOne и prefetch_related для M2M и обратных связей
cached_property + QuerySet работает не как ожидаешь
class User(models.Model):
@cached_property
def followers(self):
return User.objects.filter(followed_by=self)
cached_property запоминает то, что вернула функция - один и тот же инстанс QuerySet на все обращения. Полная итерация - заполнит кеш QuerySet, второй проход уже из памяти. Это работает
Ломается на чейнинге и индексации:
user.followers[0] # запрос
user.followers[0] # еще запрос
user.followers.filter(active=True) # новый QuerySet
Любая .filter(), .order_by() создает новый QuerySet с пустым кешем. Хочешь кешировать именно данные - оборачивай в list()
@cached_property
def followers(self):
return list(User.objects.filter(followed_by=self))
iterator() кеш отключает совсем
qs.iterator(chunk_size=2000) - стрим без кеша. Повторная итерация = повторный запрос. By design, но забыть легко
Что вынести
- Сохрани QuerySet в переменную если используешь больше одного раза
- На связанные объекты - select_related/prefetch_related
- В cached_property оборачивай в list() если хочешь данные, а не запрос
- Перед оптимизацией смотри реальные SQL - django-debug-toolbar и connection.queries полезнее интуиции
Это встроенный кеш на уровне инстанса - живет в рамках одного запроса. Шарить между запросами и процессами - это уже Redis и совершенно другая история
Поддержать на Boosty
Посмотреть на Youtube
У каждого QuerySet есть встроенный кеш результатов - _result_cache.
QuerySet ленивый - в БД не идет, пока его не начнут вызывать. При первой полной итерации запрос выполняется, результат складывается в кеш инстанса. Вторая итерация того же инстанса - уже из памяти
Один и тот же запрос дважды это два разных QuerySet
# два запроса в БД
print([e.headline for e in Entry.objects.all()])
print([e.pub_date for e in Entry.objects.all()])
# один запрос
entries = Entry.objects.all()
print([e.headline for e in entries])
print([e.pub_date for e in entries])
Кеш живет на инстансе QuerySet, не на запросе. Каждый вызов .all() создает новый инстанс с пустым кешем - даже если SQL идентичный
Не все обращения заполняют кеш
qs = Entry.objects.all()
print(qs) # __repr__ - НЕ кеширует
print(qs[5]) # SELECT с LIMIT/OFFSET - запрос
print(qs[5]) # еще один запрос
Заполняют кеш:
[entry for entry in qs] # полная итерация
list(qs) # материализация
bool(qs) # if qs:
entry in qs # проверка вхождения
print(qs) дергает repr, который берет первые 20 + 1 элемент (REPR_OUTPUT_SIZE + 1, в SQL это LIMIT 21) и не считает это полной оценкой. Кеш пустой
С индексацией - пока QuerySet не выполнен, qs[5] идет в БД с LIMIT/OFFSET. После list() - из кеша
Связанные объекты в кеш не попадают сами
entries = Entry.objects.all()
for e in entries:
print(e.blog.name) # +1 запрос на каждую запись, N+1
entries = Entry.objects.select_related('blog')
for entry in entries:
print(entry.blog.name) # из join
Базовый кеш тянет только поля самой модели. FK кешируется на инстансе после первого обращения - e.blog второй раз для того же entry уже из памяти. Для другого entry - снова запрос. Решается select_related для FK/OneToOne и prefetch_related для M2M и обратных связей
cached_property + QuerySet работает не как ожидаешь
class User(models.Model):
@cached_property
def followers(self):
return User.objects.filter(followed_by=self)
cached_property запоминает то, что вернула функция - один и тот же инстанс QuerySet на все обращения. Полная итерация - заполнит кеш QuerySet, второй проход уже из памяти. Это работает
Ломается на чейнинге и индексации:
user.followers[0] # запрос
user.followers[0] # еще запрос
user.followers.filter(active=True) # новый QuerySet
Любая .filter(), .order_by() создает новый QuerySet с пустым кешем. Хочешь кешировать именно данные - оборачивай в list()
@cached_property
def followers(self):
return list(User.objects.filter(followed_by=self))
iterator() кеш отключает совсем
qs.iterator(chunk_size=2000) - стрим без кеша. Повторная итерация = повторный запрос. By design, но забыть легко
Что вынести
- Сохрани QuerySet в переменную если используешь больше одного раза
- На связанные объекты - select_related/prefetch_related
- В cached_property оборачивай в list() если хочешь данные, а не запрос
- Перед оптимизацией смотри реальные SQL - django-debug-toolbar и connection.queries полезнее интуиции
Это встроенный кеш на уровне инстанса - живет в рамках одного запроса. Шарить между запросами и процессами - это уже Redis и совершенно другая история
Поддержать на Boosty
Посмотреть на Youtube