如何使用缓存加速 Django 中的堆叠查询

How to speed up stacked queries in Django using caching

目前在我的项目中,我有一层这样的模型:

一般来说,使用额外的 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")

虽然这不是完美的解决方案,但下面是我目前正在使用的方法(我仍在寻找更好的解决方案,但对于这种情况,我相信这可能对面临完全相同问题的其他人有用像我这里的情况。)

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 MB132.9 MB,显着增加了 31 % 对于比我的规模大几倍的数据库来说可能会有问题,但这是一个足够好的解决方案 small/medium 存储不如速度重要的数据库