Как ускорить суммированные запросы в 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 минут на моей машине и около часа или двух на живых серверах. Однако это одноразовый процесс, поскольку все последующие записи будут добавляться автоматически с минимальным эффектом производительности.
Другой вопрос - размер базы данных, хотя у меня нет точных цифр (я обновлю их, как только все настрою для справки), дополнительные данные приведут к значительному увеличению размера базы данных.