Робота з масивними датасетами в Django

4 хв. читання

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')),
)
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.8K
Приєднався: 6 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація

Читайте також: prefetch_related, try except else, _.first