Создание характеристик для большого количества товаров

Доброе время суток!

Задача: создать в базе данных значения характеристик (10-15) для большого количества (15k+) товаров.

Проблема состоит в слишком долгой работе функции add_item_specs, которая добавляет в БД значения характеристик. Немного сокращенный код выглядит так:

# models.py

class Item(models.Model):
    brand = models.CharField(max_length=250, blank=True, default="")
    code = models.CharField(max_length=10, db_index=True, blank=True)
    ...

class SpecItem(models.Model):
    """
    Характеристики товара
    """
    code = models.CharField(max_length=20, verbose_name="Код характеристики", unique=True)
    title = models.CharField(max_length=200, verbose_name='Наименование характеристики')
    ...

class SpecValue(models.Model):
    """
    Занчение характеристики
    """
    item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='spec_value')
    spec_item = models.ForeignKey('SpecItem', on_delete=models.CASCADE, related_name='value')
    value = models.CharField(max_length=150, verbose_name="Значение", null=True)
# services.py
def add_item_specs(items: dict):
    # ВЫБИРАЮ ТОВАРЫ ИЗ БД ДЛЯ КОТОРЫХ БУДУ ДОБАВлЯТЬ ХАР-КИ
    items_in_db = Item.objects.filter(code__in=items.keys()) 
    count_items = items_in_db.count()
    # ДЛЯ ПРОПУСКА УЖЕ СУЩЕСТВУЮЩИХ ЗНАЧЕНИЙ ХАР-К
    exists_spec_values = SpecValue.objects.all().values_list('item', 'spec_item__code')
    all_specs = SpecItem.objects.all()
    spec_objects = list()
    for item in items_in_db:
        for spec in items[item.code]["Features"]:
            if (item.pk, spec['FeatureCode']) in exists_spec_values:
                # ПРОПУСКАЮ СУЩЕСТВУЮЩИЕ
                continue
            spec_item = all_specs.get(code=spec['FeatureCode'])
            spec_value = SpecValue(
                item=item,
                spec_item=spec_item,
                value=spec['FeatureValue'] if spec['FeatureValue'] is not None else ""
                )
            spec_objects.append(spec_value)
        count_items -= 1
        # Данное сообщение выводится примерно 1-2 раза в секунду 
        print(f"Добавлены характеристики для {item.title} \n\t>> Осталось => {count_items}")
    print(f'SpecValue.objects.bulk_create(spec_objects) => {len(spec_objects)=} значений характеристик заливается в БД')
    SpecValue.objects.bulk_create(spec_objects)

То есть, еще до записи в БД код работает почти 2,5 часа. Есть ли возможность ускорить данный код?


Ответы (2 шт):

Автор решения: Anton Balmakov

Вы можете попробовать разбить количество товаров на куски по 3-5 к и делать всё это с помощью Threading

→ Ссылка
Автор решения: Андрей Коробейников

Примерно так стал выглядеть мой код после того как я пошел по пути подсказанным Антоном.

class CreateObjects(Thread):
    def __init__(self, items_butch, data, all_specs, exists_spec_values, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.items_butch = items_butch
        self.data = data
        self.all_specs = all_specs
        self.exists_spec_values = exists_spec_values
        self.__result = list()

    def run(self):
        self.iterate_data()

    def get_result(self):
        return self.__result

    def iterate_data(self):
        for item in self.items_butch:
            self.__result.extend(self.create_obj(item))

    def create_obj(self, item):
        return [
            SpecValue(
                item=item,
                spec_item=self.all_specs.get(code=spec['FeatureCode']),
                value=spec['FeatureValue'] if spec['FeatureValue'] is not None else ""
                )
            for spec in self.data[item.code]["Features"] 
            if (item.pk, spec['FeatureCode']) not in self.exists_spec_values
        ]


def chunks_generator(lst: list, n: int) -> list:
    """
    Создание пачек из списка, по n штук
    """
    for i in range(0, len(lst), n):
        yield lst[i : i + n]


def add_item_specs(items: dict):
    items_in_db = Item.objects.filter(code__in=items.keys())
    exists_spec_values = set(SpecValue.objects.all().values_list('item', 'spec_item__code'))
    all_specs = SpecItem.objects.all()
    spec_objects = list()
    threads = list()
    for items_butch in chunks_generator(items_in_db, 700):
        print(f"Добавляем в обработку список на {len(items_butch)} обьектов ", flush=True)
        threads.append(
            CreateObjects(
                items_butch=items_butch,
                data=items,
                all_specs=all_specs,
                exists_spec_values=exists_spec_values
                )
            )
    
    for th in threads:
        th.start()

    print("Ждем создания объектов!", flush=True)

    for th in threads:
        th.join()

    for th in threads:
        spec_objects.extend(th.get_result())

    print(f'SpecValue.objects.bulk_create(spec_objects) => {len(spec_objects)=} значений характеристик заливается в БД')
    SpecValue.objects.bulk_create(spec_objects)

Честно говоря, я думал над этим ранее, но не реализовал, так как не мог даже представить что будет такой буст! В итоге то что ранее обрабатывалось за 2.5 - 3 часа (и это еще до записи в БД) прошло за 5.5 минут вместе с внесением в базу данных!!!

Спасибо, задача решена!

→ Ссылка