Recap: набір інструментів Django для величезної кількості даних
Коли отримується велика кількість даних, використання queryset.iterator()
гарантує те, що Django ні кешуватиме, ні вилучатиме всі результати в пам'ять. Це скорочує споживання пам'яті шляхом обробки одного запису за раз.
Ця оптимізація конфліктує з іншим інструментом: prefetch_related()
— функцією, що дозволяє Django розкласти пов'язані записи в один додатковий запит. Замість того, щоб робити виклик на кожен запис (1M у нашому випадку), можна зробити лише один додатковий запит. Однак, щоб цього досягти, Django треба заздалегідь перевірити всі результати.
Таким чином, неможливо використовувати queryset.iterator() та prefetch_related() разом.
Навіть якби це було можливо, то виявилось би неефективним для 1M записів...
Оптимізація #1: Попередня вибірка в групах
Оскільки prefetch_related()
залежить від того, щоб спочатку пройти через набір результатів, було б заманливо вилучати дані в групи по 1000 записів й виконувати попередню вибірку на них. На жаль, це сильно завантажує базу даних. А бази даних не люблять повтору того самого запиту знову і знову, відкидаючи всі попередні результати, щоб, наприклад, тільки вилучити записи 984000–985000.
За допомогою курсорної пагінації (cursor pagination) все поєднується. Курсорна пагінація вилучає тільки наступні 1000 записів з попередньої відомої точки. Для цього навіть існує пакет Django: django-cursor-pagination.
from cursor_pagination import CursorPaginator
def chunked_queryset_iterator(queryset, size, *, ordering=('id',)):
"""Розділяє набір запитів на частини
Це може бути використано замість queryset.iterator(), отже .prefetch_related() також працює.
.. примітка::
Упорядкування має однозначно ідентифікувати об'єкт, і бути в тому ж порядку (ASC/DESC).
"""
pager = CursorPaginator(queryset, ordering)
after = None
while True:
page = pager.page(after=after, first=size)
if page:
yield from page.items
else:
return
if not page.has_next:
break
# приймає останній елемент, наступна сторінка починається після цього.
after = pager.cursor(instance=page[-1])
Тепер результати можуть бути знову оброблені групами:
product = Product.objects.prefetch_related("stockrecords")
for product in chunked_queryset_iterator(products, 1000):
print("Product:", product.default_stockrecord)
print("Categories:", [c.id for c in product.categories.all()])
Оптимізація #2: Використання кешу попередньої вибірки
При використанні queryset.first()
Django виконує запит — навіть якщо дані були попередньо вибрані. Однак, queryset.all()[0]
використовує кеш попередньої вибірки. Це приводить нас до цієї додаткової оптимізації, відійшовши від стандартної логіки django-oscar:
class Product:
...
@cached_property
def default_stockrecord(self):
try:
# За допомогою .all()[0] читається кеш попередньої вибірки,
# використання .first() ігнорує це. В будь-якому випадку,
# всі наші продукти мають один stockrecord.
return self.stockrecords.all()[0]
except IndexError:
return None
Оптимізація #3: Попередня вибірка тільки тих полів, які потрібні
Django об'єкт Prefetch
допомагає встановити, які дані витягаються з бази даних. Коли queryset.values_list()
більше не вистачає, розгляньте наступне:
Product.objects.prefetch_related(
"stockrecords",
Prefetch("categories", queryset=Category.objects.only('id')),
)
Ще немає коментарів