Оптимизация запросов в Django ORM с помощью множественных объединений
В моем приложении я могу описать сущность, используя различные протоколы, причем каждый протокол является коллекцией различных признаков, а каждый признак позволяет использовать два различных признака. Протокол является коллекцией различных признаков, и каждый признак позволяет использовать два или более классов. Таким образом, описание - это набор выражений. Например, я хочу описать сущность "Джон" с протоколом "X", который включает в себя следующие два Черта и Классы:
Протокол АВС
Тракт 1: Рост
Доступные Классы : a. Короткий b. Средний c. Высокий
Тракт 2: Вес
Доступные Классы : a. Свет b. Средний c. Тяжелый
Описание Джона : Выражение 1: c. высокий, Выражение 2: b. средний
Моя спецификация модели (для простоты - только самое необходимое):
class Protocol(models.Model):
"""
A Protocol is a collection of Traits
"""
name = models.CharField()
class Trait(models.Model):
"""
Stores the Traits. Each Trait can have multiple Classes
"""
name = models.CharField()
protocol = models.ForeignKey(
Protocol,
help_text="The reference protocol of the trait",
)
class Class(models.Model):
"""
Stores the different Classes related to a Trait.
"""
name = models.CharField()
trait = models.ForeignKey(Trait)
class Description(models.Model):
"""
Stores the Descriptions. A description is a collection of Expressions.
"""
name = models.CharField()
protocol = models.ForeignKey(
Protocol,
help_text="reference to the protocol used to make the description;\
this will define which Traits will be available",
)
entity = models.ForeignKey(
Entity,
help_text="the Entity to which the description refers to",
)
class Expression(models.Model):
"""
Stores the expressions of entities related to a specific
Description. It refers to one particular Class (which is
then associated with a specific Trait)
"""
class = models.ForeignKey(Class)
description = models.ForeignKey(Description)
Следуя предыдущему примеру, допустим, я хочу найти все сущности, которые medium или tall (признак 1) и heavy (признак 2). Запрос, который я сейчас использую, выглядит следующим образом:
# This is the filter returned by the HTML form, which list
# all the available Classes for each Trait of the selected Protocol
filters = [
{'trait': 1, 'class': [2, 3]},
{'trait': 2, 'class': [6,]},
]
queryset = Description.objects.all()
for filter in filters:
queryset = queryset.filter(expression_set__class=filter["class"])
Проблема в том, что запрос выполняется медленно (у меня есть ATM ~1000 Описаний, описанных с помощью
протоколом из 40 признаков, каждый признак имеет от 2 до 5 классов). Требуется около двух
секунд для возврата результатов даже при фильтрации только по 5-6 выражениям.
Я пробовал использовать prefetch_related("expression_set")
или
prefetch_related("expression_set__class")
, но без существенного улучшения.
Вопрос в следующем: можете ли вы предложить способ улучшить производительность, или это просто реальность поиска в таком количестве таблиц?
Большое спасибо за уделенное время.
На мой взгляд, лучше использовать несколько функций. Это работает с той же скоростью, что и использование классов, если не быстрее. Проверьте этот вопрос. После того, как вы начнете использовать функции, вы можете попробовать использовать @cached_property(func, name=None)
:
Обычно метод экземпляра класса приходится вызывать более одного раза. Если эта функция дорогая, то это может быть расточительно.
Использование декоратора
cached_property
сохраняет значение, возвращаемое свойством; при следующем вызове функции для данного экземпляра она вернет сохраненное значение, а не будет вычислять его заново. Обратите внимание, что это работает только для методов, которые принимаютself
в качестве единственного аргумента, и что это меняет метод на свойство.
Рассмотрим типичный случай, когда представлению может потребоваться вызвать метод модели для выполнения некоторого вычисления, перед помещением экземпляра модели в контекст, где шаблон может вызвать метод еще раз:
# the model
class Person(models.Model):
def friends(self):
# expensive computation
...
return friends
# in the view:
if person.friends():
...
А в шаблоне у вас будет:
{% for friend in person.friends %}
Здесь friends()
будет вызван дважды. Поскольку экземпляр person
в представлении и шаблоне один и тот же, украшение метода friends()
с помощью @cached_property
может избежать этого:
from django.utils.functional import cached_property
class Person(models.Model):
@cached_property
def friends(self):
...
Актуальные вопросы и источники:
Для того, чтобы больше понять о запросах, можно использовать Django Debug Toolbar. Это полезно использовать, потому что трудно понять, как мы можем улучшить ситуацию, если мы не можем измерить текущее состояние (как это, похоже, и происходит).
В Django есть страница, посвященная оптимизации доступа к базе данных. На ней можно прочитать, например, что QuerySets являются ленивыми.
Поскольку OP исследовал Django ORM и не получил от него действительно хороших результатов, для улучшения производительности OP может попробовать использовать raw SQL запросы. Другими словами, написать свой собственный SQL для получения данных. Согласно документации
Django предоставляет вам два способа выполнения необработанных SQL запросов: вы можете использовать Manager.raw() для выполнения необработанных запросов и возврата экземпляров модели, или вы можете полностью избежать слоя модели и выполнять пользовательский SQL напрямую.
.
Дополнительно, ОП должен рассмотреть возможность использования какого-либо кэша, например MemCached. Согласно Alex Xu,
Кэш - это область временного хранения, которая сохраняет в памяти результат дорогих ответов или часто используемые данные, чтобы последующие запросы обслуживались быстрее. (...) Уровень кэша - это временный уровень хранения данных, гораздо более быстрый, чем база данных. Преимущества наличия отдельного яруса кэша включают более высокую производительность системы, возможность снижения нагрузки на базу данных и возможность независимого масштабирования яруса кэша.
.
Различные базы данных имеют различные причуды производительности, поэтому, не зная, с чем вы работаете, это немного выстрел в темноте, но вы пробовали Q-объекты?
from django.db.models import QuerySet
q = Q()
for filter in filters:
q |= Q(expression_set__class_in=filter['class'])
queryset.filter(q)
или просто предварительно вычислить значение фильтра (поскольку вы уже выполняете запрос):
filtr = []
for f in filters:
filtr += filter['class']
queryset.filter(expression_set__class_in=filtr)
или
queryset = Description.objects.filter(
expression__class__in=Class.objects.filter(pk__in=filtr)
)
(фильтр, построенный выше)
Я предполагаю, что вы изучили результаты анализатора запросов, чтобы убедиться, что вы не пропустили ни одного индекса и т.д...
Кэширование, вероятно, не очень хорошая идея (поскольку это близко к ad-hoc запросам, почти все будет пропуском кэша), но 1000 * 40 * 5, вероятно, помещается в памяти, поэтому может быть решением считать все в память, если это критичная по времени часть, которая выполняется часто.
Во-первых, вам следует избегать множественных объединений, агрегируя нужные фильтры заранее:
filters = [
{'trait': 1, 'class': [2, 3]},
{'trait': 2, 'class': [6,]},
]
queryset = Description.objects.all()
class_filter = []
for filter_entry in filters:
class_filter.append(filter_entry["class"])
queryset = queryset.filter(expression_set__class__in=class_filter)
Вторая проблема - сканирование текстовых значений. Используйте db_index=True
для поля Class.name.