Как ускорить суммированные запросы в Django с помощью кэширования

В настоящее время в моем проекте есть слой моделей следующего вида:

  • Класс сенсора: в котором хранятся различные сенсоры
  • Класс записи: который хранит каждый раз, когда происходит новая запись данных
  • классData: который хранит пары данных для каждой записи.

В общем, этот дополнительный класс был использован, поскольку у меня есть разные поля для разных датчиков, и я хотел хранить их в одной базе данных, поэтому я использовал два метода, один для входа, чтобы получить данные для него, и один в датчике следующим образом:

class Data(models.Model):
    key = models.CharField(max_length=50)
    value = models.CharField(max_length=50)
    entry = models.ForeignKey(Entry, on_delete=CASCADE, related_name="dataFields")

    def __str__(self):
        return str(self.id) + "-" + self.key + ":" + self.value
class Entry(models.Model):
    time = models.DateTimeField()
    sensor = models.ForeignKey(Sensor, on_delete=CASCADE, related_name="entriesList")

    def generateFields(self, field=None):
        """
        Generate the fields by merging the Entry and data fields.

        Outputs a dictionary which contains all fields.
        """
        output = {"timestamp": self.time.timestamp()}
        for entry in self.dataFields.all():
            if field is not None:
                if entry.key != field:
                    continue
            try:
                value = float(entry.value)
                if math.isnan(value) == True:
                    return None
                else:
                    output[entry.key] = float(entry.value)
            except:
                pass
        return output
class Sensor(models.Model):
.
.
.
    def generateData(self, fromTime, toTime, field=None):
        fromTime = datetime.fromtimestamp(fromTime)
        toTime = datetime.fromtimestamp(toTime)
        entries = self.entriesList.filter(time__range=(toTime, fromTime)).order_by(
            "time"
        )
        output = []
        for entry in entries:
            value = entry.generateFields(field)
            if value is not None:
                output.append(value)
        return output

После того, как я попытался устранить проблемы с временем (так как выполнение этого запроса для ~5000-10000 записей заняло слишком много времени, почти 10 секунд!), я обнаружил, что большая часть времени (около 95%) была потрачена на метод для generateFields(), я рассматривал варианты кэширования (с помощью cached_property), используя различные методы, но пока ни один из них не сработал.

Существует ли метод автоматического сохранения результатов запроса generateFields() в базе данных при сохранении модели? Или, возможно, просто сохранять результаты обратного запроса self.dataFields.all()? Я могу понять, что это главный виновник, так как для 5000 записей в среднем 25000 полей данных, по крайней мере.

Я думаю, что именно так я бы рассмотрел вариант написания generateFields.

class Entry(models.Model):
    ...

    def generateFields(self, field=None):
        """
        Generate the fields by merging the Entry and data fields.

        Outputs a dictionary which contains all fields.
        """

        if field is None:
            field_set = self.dataFields.all()
        else:
            field_set = self.dataFields.filter(key=field)

        output = {"timestamp": self.time.timestamp()}

        for key, value in field_set.values_list("key", "value"):
            try:
                floated = float(value)
                isnan = math.isnan(floated)
            except (TypeError, ValueError):
                continue

            if isnan:
                return None
            else:
                output[key] = floated

        return output

Во-первых, я собираюсь избежать сравнения поля (если оно предоставлено) в Python. Я могу использовать queryset .filter, чтобы передать это SQL.

        if field is None:
            field_set = self.dataFields.all()
        else:
            field_set = self.dataFields.filter(key=field)

Во-вторых, я использую QuerySet.values_list для получения значений из записей. Я могу ошибаться (пожалуйста, поправьте меня, если это так), но я думаю, что это также передает получение атрибутов в SQL. Я не знаю, будет ли это быстрее, но подозреваю, что да.

        for key, value in field_set.values_list("key", "value"):

Я изменил структуру блока try/except, но это имеет меньше отношения к увеличению скорости и больше к тому, чтобы сделать явным, какие ошибки ловятся и какие строки их вызывают.

            try:
                floated = float(value)
                isnan = math.isnan(floated)
            except (TypeError, ValueError):
                continue

Строки за пределами try/except теперь должны быть свободны от проблем.

            if isnan:
                return None
            else:
                output[key] = floated

Я немного не знаком с QuerySet.prefetch_related, но я думаю, что добавление его в эту строку также поможет.

        entries = self.entriesList.filter(time__range=(toTime, fromTime)).order_by(
            "time").prefetch_related("dataFields")

Хотя это не идеальное решение, ниже приведен подход, который я использую в настоящее время (я все еще ищу лучшее решение, но для данного случая я считаю, что это может быть полезно для других, кто сталкивается с такой же ситуацией, как и я здесь)

  • Я использую django-picklefield для хранения кэшированных данных, объявляя их как:
from picklefield.fields import PickledObjectField

.
.
.
class Entry(models.Model):
    time = models.DateTimeField()
    sensor = models.ForeignKey(Sensor, on_delete=CASCADE, related_name="entriesList")
    cachedValues = PickledObjectField(null=True)

  • Далее, я добавляю свойство, чтобы генерировать значение и возвращать его в виде:
    @property
    def data(self):
        if self.cachedValues is None:
            fields = self.generateFields()
            self.cachedValues = fields
            self.save()
            return fields
        else:
            return self.cachedValues

  • Обычно это поле устанавливается автоматически при добавлении новых данных, однако, поскольку у меня уже имеется большое количество данных, и я могу подождать, пока к ним получат доступ (поскольку будущий доступ будет намного быстрее), я решил быстро проиндексировать их, выполнив следующее:
def mass_set(request):
    clear = lambda: os.system("clear")
    entries = Entry.objects.all()
    length = len(entries)
    for count, entry in enumerate(entries):
        _ = entry.data
        clear()
        print(f"{count}/{length} done")

Наконец, ниже приведены бенчмарки для набора из 2230 полей, запущенные на моей локальной машине разработки, измеряя главный цикл таким образом:

    def generateData(self, fromTime, toTime, field=None):
        fromTime = datetime.fromtimestamp(fromTime)
        toTime = datetime.fromtimestamp(toTime)
        # entries = self.entriesList.filter().order_by('time')
        entries = self.entriesList.filter(time__range=(toTime, fromTime)).order_by(
            "time"
        )
        output = []
        totalTimeLoop = 0
        totalTimeGenerate = 0
        stage1 = time.time()
        stage2 = time.time()
        for entry in entries:
            stage1 = time.time()
            totalTimeLoop += stage1 - stage2
            # Value = entry.generateFields(field)
            value = entry.data
            stage2 = time.time()
            totalTimeGenerate += stage2 - stage1
            if value is not None:
                output.append(value)
        print(f"Total time spent generating the loop: {totalTimeLoop}")
        print(f"Total time spent creating the fields: {totalTimeGenerate}")
        return output

Перед:

  • Время генерации цикла: 0.1659650
  • Время генерации полей: 3.1726377

После:

  • Время генерации цикла: 0.1614456
  • Время генерации полей: 0.0032608

Примерно в тысячи раз уменьшилось время генерации полей, а в общем времени в 20 раз увеличилась скорость

Что касается минусов, то их в основном два:

Применяя его к моему набору данных в настоящее время (с 167 тысячами полей), требуется значительное время для обновления, он все еще обновляется, пока я пишу это, но я ожидаю около 15-20 минут на моей машине и около часа или двух на живых серверах. Однако это одноразовый процесс, поскольку все последующие записи будут добавляться автоматически с минимальным эффектом производительности.

Другой вопрос - размер базы данных, хотя у меня нет точных цифр (я обновлю их, как только все настрою для справки), дополнительные данные приведут к значительному увеличению размера базы данных.

Вернуться на верх