эффективность django __in поиска для наборов запросов - PullRequest
7 голосов
/ 12 января 2012

У меня сложная модель базы данных, настроенная в Django, и я должен выполнить ряд вычислений на основе данных фильтра. У меня есть объект Test, объект TestAttempt и объект UserProfile (с внешним ключом для тестирования и внешним ключом для пользовательского профиля). Существует метод, который я запускаю на TestAttempt, который вычисляет оценку теста (основываясь на количестве предоставленных пользователем вариантов по сравнению с правильными ответами, связанными с каждым тестом). И затем другой метод, который я использую для Test, который вычисляет среднюю оценку теста на основе каждого из связанных с ним TestAttempt. Но иногда мне нужно только среднее значение на основе предоставленного подмножества связанных TestAttempt, которые связаны с определенным набором UserProfiles. Таким образом, вместо того, чтобы вычислять средний балл теста для определенного теста следующим образом:

[x.score() for x in self.test_attempts.all()]

и затем усреднение этих значений. Я делаю запрос, как это:

[x.score() for x in self.test_attempts.filter(profile__id__in=user_id_list).all()]

где user_id_list - это определенное подмножество идентификаторов UserProfile, для которого я хочу найти среднюю оценку теста в виде списка. Мой вопрос таков: если user_id_list действительно является полным набором UserProfile (поэтому фильтр будет возвращать то же, что и self.test_attempts.all()), и большую часть времени это будет иметь место, стоит ли платить за проверку этот случай, а если так, то не выполнить фильтр вообще? или поиск __in достаточно эффективен, так что даже если user_id_list содержит всех пользователей, будет эффективнее запустить фильтр. Кроме того, мне нужно беспокоиться о том, чтобы сделать результирующий test_attempts отличным ()? или они не могут найти дубликаты со структурой моего набора запросов?

РЕДАКТИРОВАТЬ: Для тех, кто заинтересован в просмотре необработанного SQL-запроса, это выглядит без фильтра:

SELECT "mc_grades_testattempt"."id", "mc_grades_testattempt"."date", 
"mc_grades_testattempt"."test_id", "mc_grades_testattempt"."student_id" FROM 
"mc_grades_testattempt" WHERE "mc_grades_testattempt"."test_id" = 1

и это с фильтром:

SELECT "mc_grades_testattempt"."id", "mc_grades_testattempt"."date", 
"mc_grades_testattempt"."test_id", "mc_grades_testattempt"."student_id" FROM 
"mc_grades_testattempt" INNER JOIN "mc_grades_userprofile" ON 
("mc_grades_testattempt"."student_id" = "mc_grades_userprofile"."id") WHERE 
("mc_grades_testattempt"."test_id" = 1  AND "mc_grades_userprofile"."user_id" IN (1, 2, 3))

обратите внимание, что массив (1,2,3) является просто примером

Ответы [ 3 ]

2 голосов
/ 12 января 2012
  1. Короткий ответ - эталонный тест.Проверьте это в разных ситуациях и измерьте нагрузку.Это будет лучший ответ.

  2. Здесь не может быть дубликатов.

  3. Действительно ли сложно проверить наличие двух ситуаций?Вот гипотетический код:

    def average_score(self, user_id_list=None):
        qset = self.test_attempts.all()
        if user_id_list is not None:
            qset = qset.filter(profile__id__in=user_id_list)
        scores = [x.score() for x in qset]
        # and compute the average
    
  4. Я не знаю, что делает метод score, но вы не можете вычислить среднее значение на уровне БД?Это даст вам гораздо более заметное повышение производительности.

  5. И не забывайте о кешировании.

2 голосов
/ 18 ноября 2012

Используйте Аннотация вместо циклического перебора наборов запросов, который создает новый удар по базе данных для каждого элемента в user_id_list и выполняет среднее значение в python.

ms = MyModel.objects.annotate(Avg('some_field'))
ms[0].avg__some_field # prints the average for that instance

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

2 голосов
/ 12 января 2012

Из того, что я понимаю в документации, все запросы строятся до того, как они фактически используются. Так, например, test_attempts.all() генерирует код SQL один раз, и когда вы выполняете запрос, фактически получаете данные, делая что-то вроде .count(), for t in test_attempts.all(): и т. Д., Он выполняет запрос к базе данных и возвращает объект Queryset или просто объект, если вы использовали get (). Имея это в виду, количество вызовов в базу данных будет точно таким же, а фактический вызов будет другим. Как показано в отредактированном сообщении, необработанные запросы отличаются, но оба они генерируются одинаковым образом, до того, как Django получит доступ к данным. С точки зрения Django они оба будут созданы одинаковым образом, а затем выполнены в базе данных. На мой взгляд, было бы лучше не проверять ситуацию all (), так как вам нужно выполнить ДВА запроса, чтобы определить это. Я считаю, что вы должны работать с имеющимся у вас кодом и пропустить проверку сценария all (), который вы описываете как наиболее распространенный случай. Большинство современных механизмов баз данных выполняют запросы таким образом, что добавленные объединения не влияют на показатели производительности, так как они в любом случае обрабатывают запросы в оптимальной последовательности.

...