Добавить в корзинуПозвонить
Найти в Дзене
Павлин Шарит

Кеширование на уровне 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

Кеширование на уровне 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