【问题标题】:How to speed up stacked queries in Django using caching如何使用缓存加速 Django 中的堆叠查询
【发布时间】:2021-12-24 20:49:06
【问题描述】:

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

  • 传感器类:存储不同的传感器
  • Entry 类:每当发生新数据输入时存储
  • 数据类:为每个条目存储数据对。

一般来说,使用该附加类是因为我对不同的传感器有不同的字段,并且我想将它们存储在一个数据库中,所以我是通过两种方法做到这一点的,一种用于获取数据的输入,另一种用于获取数据。传感器中的一个如下:

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),使用不同的方法,但到目前为止都没有真正奏效。

是否有一种方法可以在保存模型时自动将generateFields() 的结果存储在数据库中?或者可能只是保存反向查询self.dataFields.all() 的结果?我知道这是罪魁祸首,因为对于 5000 个条目,平均至少有 25000 个数据字段。

(感谢 Jacinator 的注释和改进)上面是 Jacinator 更改后的代码,但问题(和问题)仍然存在,因为缓存字段将加速该过程几乎 25-50 倍( !!)当我的实际数据集可能更大时(1~分钟运行查询不是真的可以接受),这很关键

【问题讨论】:

    标签: python django django-models optimization django-queryset


    【解决方案1】:

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

    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,000 个字段),但更新需要相当长的时间,在我的本地计算机上大约需要 23 分钟,在实时服务器上预计需要大约一两个小时.然而,这是一个一次性的过程,因为所有未来的条目都将自动添加,并且添加以下行会影响最小的性能:

                _ = newEntry.data
                newEntry.save()
    

    另一个是数据库大小,从 100.8 MB132.9 MB,显着增加了 31%,这对于一个比我大几个规模的数据库,但对于存储不如速度重要的中小型数据库来说,这是一个足够好的解决方案

    【讨论】:

      【解决方案2】:

      我想这就是我会考虑写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 now 之外的行应该没有问题。

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

      我对@9​​87654321@ 有点陌生,但我认为将其添加到这一行也会有所帮助。

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

      【讨论】:

      • 我会在这里尝试这些建议,但我怀疑它可能不是我想要的,但无论如何我都会尝试一下。主要问题是 self.dataFields.all() 只是被调用了太多次,所以我的数据库正在运行 25000(!)个查询,如果这些数据被缓存或存储,它不需要运行任何一个。不过,我非常感谢对代码的建议。
      • 这就是QuerySet.prefetch_related 的用途。我不是很熟悉,但我认为它会将一大堆查询合并为一个。
      • 我之前研究过,看不出它是如何应用的,但我会更深入地研究它并尝试一下。这里建议的更改确实将速度提高了一点,大约 5-10%(这很棒),但我确实觉得我需要预取才能工作或找到重大突破(因为我的数据集有时需要将这个函数运行到这个大小的 5-10 倍,这是有问题的。
      • 有趣的是,添加 entries = self.entriesList.filter(time__range=(toTime, fromTime)).order_by( "time").prefetch_related("dataFields") 会使查询速度降低 20%!
      • 那太糟糕了。我想我并不像我想象的那样理解这一点。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-25
      • 1970-01-01
      • 2014-01-31
      • 2013-07-27
      • 2021-08-25
      • 1970-01-01
      相关资源
      最近更新 更多