Что вызывает неэффективность при разборе этого QuerySet в кортежи? - PullRequest
0 голосов
/ 28 мая 2019

В приложении django я пытаюсь проанализировать набор запросов, представляющий отдельные значения временных рядов x из n датчиков, в кортежи (t, x1, x2 ... x n ), а затем в объект json в формате, указанном здесь Google-диаграммами: https://developers.google.com/chart/interactive/docs/gallery/linechart

Нет значения используются в качестве заполнителей, если для заданной отметки времени от конкретного датчика не было зарегистрировано никакого значения

Время загрузки страницы является значительным для QuerySet с ~ 6500 строками (~ 3 секунды, запускается локально)

Это значительно дольше на сервере

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

Профилирование указывает, что 99,9% времени тратится на _winapi.WaitForSingleObject (что я не могу интерпретировать), а ручное профилирование с таймером указывает, что виновником на стороне сервера является цикл while, который выполняет итерацию по QuerySet и группам значения в кортежи (строка 23 в моем примере кода)

Результаты выглядят следующим образом:

базовое получение (заняло 5 мс)

запрашиваемые данные (заняло 0 мс)

разделить данные по датчику (заняло 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).

Очевидно, что это не вся проблема, поскольку она учитывает только часть кажущегося времени загрузки, но я думаю, что это хорошее место для начала.

Ответы [ 2 ]

0 голосов
/ 28 мая 2019

У вас есть запросы, работающие в цикле. Вы можете использовать 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 Документы

0 голосов
/ 28 мая 2019

Раздел «запрашиваемые данные» принимает 0 мс, поскольку этот раздел строит запрос, а не выполняет запрос к базе данных.

Запрос выполняется, когда он попадает в эту строку: while data_idx < len(raw_data):, потому что для вычисления длины iterable он должен его оценить.

Так что, возможно, это не цикл, который занимает большую часть времени, это, вероятно, выполнение и оценка запроса. Вы можете оценить запрос перед основным циклом, поместив набор запросов в list(), это позволит вашему time_marker отобразить, сколько времени фактически требуется для выполнения запроса.

Вам нужен набор запросов, оцененный для модели? В качестве альтернативы вы можете использовать .values() или .values_list() для возврата фактического списка значений, который пропускает сериализацию результатов запроса в объекты Model. Делая это, вы также избегаете необходимости возвращать все столбцы из базы данных, вы возвращаете только те, которые вам нужны.

Вы можете потенциально удалить объединение таблиц в этом запросе SensorDatum.objects.filter(sensor__datalogger__device_name=device_name).order_by('timestamp', 'sensor'), денормализовав вашу схему (если возможно), чтобы иметь поле device_name на датчике.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...