如何使用缓存加速 Django 中的堆叠查询
How to speed up stacked queries in Django using caching
目前在我的项目中,我有一层这样的模型:
- Sensor class: 存储不同的传感器
- 条目 class: 每当有新的数据输入时就会存储
- Data class: 存储每个条目的数据对。
一般来说,使用额外的 class 是因为我对不同的传感器有不同的字段,我想将它们存储在一个数据库中,所以我是通过两种方法做到这一点的,一种用于输入获取它的数据,以及传感器中的一个如下:
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.
"""
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
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),使用不同的方法,但 none 到目前为止确实有效。
有没有一种方法可以在保存模型时自动将 generateFields()
的结果存储在数据库中?或者可能只是保存反向查询的结果 self.dataFields.all()
?我知道这是罪魁祸首,因为对于 5000 个条目,平均至少有 25000 个数据字段。
(感谢 Jacinator 的注释和改进)上面是 Jacinator 更改后的代码,但问题(和问题)仍然存在,因为缓存字段会使过程加快近 25-50次 (!!) 这在我的实际数据集可能更大时至关重要(1~ 分钟到 运行 一个查询是不可接受的)
我想这就是我考虑写作的方式 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 中的字段(如果提供)。我可以使用查询集 .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)
- 接下来,我将添加一个 属性 来生成值,并且 return 为:
@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,000 个字段)时,更新需要相当长的时间,在我的本地机器上大约需要 23 分钟,预计需要大约一两个小时实时服务器。但是,这是一次性过程,因为所有未来的条目都将自动添加,添加以下行对性能的影响最小:
_ = newEntry.data
newEntry.save()
另一个是数据库大小,从 100.8 MB 到 132.9 MB,显着增加了 31 % 对于比我的规模大几倍的数据库来说可能会有问题,但这是一个足够好的解决方案 small/medium 存储不如速度重要的数据库
目前在我的项目中,我有一层这样的模型:
- Sensor class: 存储不同的传感器
- 条目 class: 每当有新的数据输入时就会存储
- Data class: 存储每个条目的数据对。
一般来说,使用额外的 class 是因为我对不同的传感器有不同的字段,我想将它们存储在一个数据库中,所以我是通过两种方法做到这一点的,一种用于输入获取它的数据,以及传感器中的一个如下:
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.
"""
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
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),使用不同的方法,但 none 到目前为止确实有效。
有没有一种方法可以在保存模型时自动将 generateFields()
的结果存储在数据库中?或者可能只是保存反向查询的结果 self.dataFields.all()
?我知道这是罪魁祸首,因为对于 5000 个条目,平均至少有 25000 个数据字段。
(感谢 Jacinator 的注释和改进)上面是 Jacinator 更改后的代码,但问题(和问题)仍然存在,因为缓存字段会使过程加快近 25-50次 (!!) 这在我的实际数据集可能更大时至关重要(1~ 分钟到 运行 一个查询是不可接受的)
我想这就是我考虑写作的方式 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 中的字段(如果提供)。我可以使用查询集 .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)
- 接下来,我将添加一个 属性 来生成值,并且 return 为:
@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,000 个字段)时,更新需要相当长的时间,在我的本地机器上大约需要 23 分钟,预计需要大约一两个小时实时服务器。但是,这是一次性过程,因为所有未来的条目都将自动添加,添加以下行对性能的影响最小:
_ = newEntry.data
newEntry.save()
另一个是数据库大小,从 100.8 MB 到 132.9 MB,显着增加了 31 % 对于比我的规模大几倍的数据库来说可能会有问题,但这是一个足够好的解决方案 small/medium 存储不如速度重要的数据库