【问题标题】:What is causing inefficiency when parsing this QuerySet into tuples?将此 QuerySet 解析为元组时导致效率低下的原因是什么?
【发布时间】:2019-05-28 02:44:47
【问题描述】:

在 django 应用程序中,我试图将 Queryset 解析为元组 (t, x1, x2 . .. xn),然后转换成 json 对象,格式由此处的谷歌图表指定:https://developers.google.com/chart/interactive/docs/gallery/linechart

如果没有记录来自特定传感器的给定时间戳的值,则使用值作为占位符

页面加载时间对于约 6500 行(约 3 秒,在本地运行)的 QuerySet 来说很重要

在服务器上明显更长

http://54.162.202.222/pulogger/simpleview/?device=test

分析表明 99.9% 的时间花在 _winapi.WaitForSingleObject(我无法解释)上,使用计时器进行手动分析表明服务器端的罪魁祸首是遍历 QuerySet 和组的 while 循环元组中的值(我的代码示例中的第 23 行)

结果如下:

基本获取(耗时 5 毫秒)

查询数据(耗时 0ms)

按传感器拆分数据(耗时 981 毫秒)

准备好列标签/类型(耗时 0 毫秒)

准备好的 json(耗时 27 毫秒)

创建上下文(耗时 0 毫秒)

为了完整起见,定时函数如下:

def print_elapsed_time(ref_datetime, description):
    print('{} (took {}ms)'.format(description, floor((datetime.now()-ref_datetime).microseconds/1000)))
    return datetime.now()

执行处理和生成视图的代码如下:

def simpleview(request):
    time_marker = datetime.now()
    device_name = request.GET['device']
    device = Datalogger.objects.get(device_name=device_name)

    sensors = Sensor.objects.filter(datalogger=device).order_by('pk')
    sensor_count = len(sensors)  # should be no worse than count() since already-evaluated and cached.  todo: confirm

    #assign each sensor an index for the tuples (zero is used for time/x-axis)
    sensor_indices = {}
    for idx, sensor in enumerate(sensors, start=1):
        sensor_indices.update({sensor.sensor_name:idx})

    time_marker = print_elapsed_time(time_marker, 'basic gets')

    # process data into timestamp-grouped tuples accessible by sensor-index ([0] is timestamp)
    raw_data = SensorDatum.objects.filter(sensor__datalogger__device_name=device_name).order_by('timestamp', 'sensor')
    data = []
    data_idx = 0

    time_marker = print_elapsed_time(time_marker, 'queried data')

    while data_idx < len(raw_data):
        row_list = [raw_data[data_idx].timestamp]
        row_list.extend([None]*sensor_count)
        row_idx = 1

        while data_idx < len(raw_data) and raw_data[data_idx].timestamp == row_list[0]:
            row_idx = sensor_indices.get(raw_data[data_idx].sensor.sensor_name)
            row_list[row_idx] = raw_data[data_idx].value
            data_idx += 1
        data.append(tuple(row_list))

    time_marker = print_elapsed_time(time_marker, 'split data by sensor')

    column_labels = ['Time']
    column_types = ["datetime"]
    for sensor in sensors:
        column_labels.append(sensor.sensor_name)
        column_types.append("number")

    time_marker = print_elapsed_time(time_marker, 'prepared column labels/types')

    gchart_json = prepare_data_for_gchart(column_labels, column_types, data)


    time_marker = print_elapsed_time(time_marker, 'prepared json')


    context = {
        'device': device_name,
        'sensor_count': sensor_count,
        'sensor_indices': sensor_indices,
        'gchart_json': gchart_json,
    }

    time_marker = print_elapsed_time(time_marker, 'created context')

    return render(request, 'pulogger/simpleTimeSeriesView.html', context)

我是 python 新手,所以我认为我在某处使用过的操作/集合选择不佳。除非我是盲人,否则它应该在 O(n) 内运行。

显然这不是全部问题,因为它只占表面加载时间的一部分,但我认为这是一个很好的起点。

【问题讨论】:

  • 在尝试了一些进一步的测试之后,问题似乎是在row_idx = sensor_indices.get(raw_data[data_idx].sensor.sensor_name) 中对raw_data[data_idx].sensor.sensor_name 的评估所以这是我处理django 对象的方式的问题。我的理解是,在每次评估中,它都会通过索引(快速)跳转到相关的 QuerySet 元素(SensorDatum),检索关联的 Sensor 对象(可能很慢),然后获取其 sensor_name(快速)。最好的解决方案是去规范化,在插入行时在 SensorDatum 的字段中复制 sensor_name?
  • 如果您事先将查询集转换为列表,这可能是最好的。喜欢raw_data = list(SensorDatum.objects.filter(sensor__datalogger__device_name=device_name).order_by('timestamp', 'sensor'))
  • @ruddra 如何与外键字段交互?对应的列表元素会变成某种对象还是只是与其主键对应的 int?
  • 不,您将按原样使用它。但不是多次评估查询集,而是评估一次。希望它会提高性能,但我不确定。更多信息可以在这里找到:docs.djangoproject.com/en/2.1/ref/models/querysets/…
  • 使用 all(), filter() 不会评估查询集。调用len()listget()、切片查询集等。

标签: python django python-3.x performance django-models


【解决方案1】:

“查询数据”部分需要 0 毫秒,因为该部分正在构建查询,而不是针对数据库执行查询。

当查询到达这一行时正在执行查询:while data_idx &lt; len(raw_data):,因为要计算 iterable 的长度,它必须对其进行评估。

所以大部分时间可能不是循环,它可能是查询执行和评估。您可以通过将查询集包装在 list() 中来在主循环之前评估查询,这将允许您的 time_marker 显示查询实际执行所需的时间。

需要将查询集评估为模型吗?或者,您可以使用.values().values_list() 返回一个实际的值列表,从而跳过将查询结果序列化为Model 对象。通过这样做,您还避免了返回数据库中的所有列,您只返回您需要的那些。

您可以通过非规范化架构(如果可能)在传感器上具有 device_name 字段来删除此查询 SensorDatum.objects.filter(sensor__datalogger__device_name=device_name).order_by('timestamp', 'sensor') 中的表连接。

【讨论】:

  • 谢谢,我没有意识到调用长度导致重新评估。
【解决方案2】:

您有在循环下运行的查询。您可以使用select_related预先缓存相关对象。

示例

raw_data = SensorDatum.objects.filter(
    sensor__datalogger__device_name=device_name
).order_by(
    'timestamp',
    'sensor'
).select_related('sensor') # this will fetch and cache sensor objects and will prevent further db queries in the loop

参考:select_related Django 2.1 Docs

【讨论】:

  • 谢谢。我已经对模型进行了非规范化,但会尝试使用此方法。
猜你喜欢
  • 2020-12-23
  • 2010-09-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-28
  • 1970-01-01
  • 2010-10-01
相关资源
最近更新 更多