Оптимизация работы с базой данных¶
Django всячески помогает разработчикам получить максимальную отдачу от используемой базы данных. В этом разделе собрана информация, которая пригодится при оптимизации вашего приложения.
Первым делом - профайлинг¶
В рамках общей практики программирования это само собой разумеется. Узнайте какие запросы вы делаете и сколько они вам стоят. Используйте QuerySet.explain(), чтобы понять, как конкретные QuerySets выполняются вашей базой данных. Вы также можете использовать внешний проект, например django-debug-toolbar, или инструмент, который напрямую контролирует вашу базу данных.
Вы можете оптимизировать скорость работы, или потребляемую память, или оба параметра. Иногда оптимизация одного пагубно влияет на другой параметр, но иногда можно улучшить оба. Так же нагрузка на базу данных может быть не так важна(для вас) как нагрузка на сервер(работы python). Вы сами определяете золотую средину, и не забывайте выполнить профайлинг всего этого, перед оптимизацией.
Не забывайте выполнять профайлинг после каждого изменения, чтобы удостоверится что изменения повысили производительность, и что это повышение стоит усложнения кода. Все советы ниже могут и не сработать в вашем случае, или даже понизить производительность.
Используйте стандартные техники оптимизации БД¶
…включая:
Индексы. Следует добавить в первую очередь, после того, когда вы определите через профайлинг, какие индексы требуются. Используйте
Field.db_indexилиMeta.index_togetherчтобы добавить индексы из Django. Следует добавить индексы полям, которые используются вfilter(),exclude(),order_by()и др., т.к. они ускоряют поиск по полям. Куда добавлять индексы - сложный вопрос, который зависит от БД и вашего приложения. Накладные расходы на поддержку индекса могут перевесить полученное ускорение при выполнении запросов.
Используйте правильные типы полей.
Предположим вы уже выполнили эти очевидные вещи. Остальная часть этого раздела расскажет, как оптимально использовать Django. Этот документ не описывает другие методы оптимизации, которые применимы для всех «тяжелых» операций, например, кэширование.
Понимание QuerySet¶
Понимание QuerySets - важная часть для написания эффективного простого кода. В частности:
Понимание выполнения QuerySet¶
Для избежания проблем с производительностью, важно понимать:
когда происходит вычисление.
Понимание кэширования атрибутов¶
Помимо кэширования всего QuerySet, существует кэширование результатов атрибутов объектов ORM. Как правило, атрибуты, которые не могут быть вызваны, будут кэшироваться. Например, если предположить, что пример модели блога:
>>> entry = Entry.objects.get(id=1)
>>> entry.blog # Blog object is retrieved at this point
>>> entry.blog # cached version, no DB access
Но в целом вызываемые атрибуты каждый раз вызывают поиск в БД:
>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all() # query performed
>>> entry.authors.all() # query performed again
Будьте внимательны читая код шаблонов - шаблонизатор не позволяет использовать скобки и автоматом вызывает функции и методы.
Будьте внимательны с собственными свойствами - вы должны самостоятельно реализовать кэширование, используя, например, декоратор cached_property.
Используйте шаблонный тэг with¶
Для использования кэширования в QuerySet можно использовать шаблонный тэг with.
Используйте iterator()¶
Если у вас очень много объектов, кэширование в QuerySet может использовать большой объем памяти. В этом случае может помочь iterator().
Используйте iterator()¶
QuerySet.explain() предоставляет подробную информацию о том, как база данных выполняет запрос, включая используемые индексы и объединения. Эти сведения могут помочь вам найти запросы, которые можно было бы переписать более эффективно, или определить индексы, которые можно добавить для повышения производительности.
Выполняйте задачи базы данных в базе данных, а не в Python¶
Например:
Самое простое: используйте filter и exclude для фильтрации данных в БД.
Используйте
объект F()для фильтрации по другим полям модели.Используйте annotate для выполнения агрегации в базе данных.
Если этого не достаточно для создания необходимого SQL:
Используйте RawSQL¶
Не совсем переносимый между разными БД, но очень мощный метод – RawSQL, который позволяет добавить SQL непосредственно в запрос. Если и этого вам не достаточно:
Используйте SQL¶
Используйте собственный SQL запрос для получения данных и загрузки в модели. Используйте django.db.connection.queries, чтобы понять что создает Django и начните с изменения этого запроса.
Получения объекта, используя уникальное или проиндексированное поле¶
Есть две причины использовать поле с unique или db_index в методе get(). Первая - запрос будет быстрее т.к. будет использовать индекс в базе данных. Вторая - запрос будет гораздо медленнее, если несколько объектов будут удовлетворять запросу, уникальное поле исключает такую ситуацию.
Итак, используя пример модели блога:
>>> entry = Entry.objects.get(id=10)
будет быстрее чем:
>>> entry = Entry.objects.get(headline="News Item Title")
т.к. id проиндексировано и уникально.
Следующий запрос будет скорее всего медленным:
>>> entry = Entry.objects.get(headline__startswith="News")
Во-первых headline не проиндексировано и база данных будет медленнее вычислять результат.
Во-вторых - запрос не гарантирует, что будет возвращен только один объект. Если несколько объектов удовлетворяют запросу, они все будут переданы с базы данных. Задержка может быть значительной при передачи сотен или тысяч объектов. Еще большую задержку получаем, если база данных находится на другом сервере.
Загружайте все данные сразу, если уверены, что будете использовать их.¶
Обращение несколько раз к базе данных для получения различных частей одного «массива» данных обычно менее эффективно, чем получение всех данных одним запросом. Это особенно важно для запросов, выполняемых в цикле, что может привести к большому количеству запросов. Поэтому:
Не получайте данные, которые вам не нужны¶
Используйте QuerySet.values() и values_list()¶
Если вам нужен dict или list значений, а не объекты моделей ORM, используйте values(). Это можно использовать для подмены объектов моделей в шаблоне - если атрибуты словаря совпадают с используемыми атрибутами моделей, все будет хорошо работать.
Используйте QuerySet.defer() и only()¶
Используйте defer() и only(), если есть колонки в базе данных, которые вы не будете использовать. Запомните, что если вы все же будете их использовать, ORM сделает дополнительный запрос для их получения, что уменьшит производительность.
Не будьте слишком агрессивны, откладывая поля без профилирования, поскольку базе данных приходится считывать с диска большую часть нетекстовых данных, не относящихся к типу VARCHAR, для одной строки результатов, даже если в конечном итоге она использует всего несколько столбцов. Методы defer() и only() наиболее полезны, когда вы можете избежать загрузки большого количества текстовых данных или для полей, для преобразования которых обратно в Python может потребоваться много обработки. Как всегда, сначала профилируйте, а затем оптимизируйте.
Используйте QuerySet.contains(obj)¶
… если вы хотите только узнать, находится ли obj в наборе запросов, а не if obj в наборе запросов.
Используйте QuerySet.count()¶
…вместо``len(queryset)``, если вам необходимо только количество объектов.
Используйте QuerySet.exists()¶
…если необходимо проверить есть ли результат, вместо if queryset.
Но:
Не злоупотребляйте contains(), count() и exists().¶
Если вам необходимы остальные данные из QuerySet, просто вычислите их.
Например, если предположить, что модель «Группа» имеет отношение «многие ко многим» к «Пользователь», оптимальным будет следующий код:
members = group.members.all()
if display_group_members:
if members:
if current_user in members:
print("You and", len(members) - 1, "other users are members of this group.")
else:
print("There are", len(members), "members in this group.")
for member in members:
print(member.username)
else:
print("There are no members in this group.")
Он оптимальный потому что:
Поскольку QuerySets являются ленивыми, запросы к базе данных не выполняются, если display_group_members имеет значение False.
Сохранение group.members.all() в переменнойmembers позволяет повторно использовать кэш результатов.
Строка
ifmembers:вызывает вызовQuerySet.__bool__(), что приводит к запуску запросаgroup.members.all()в базе данных. Если результатов нет, он вернет «False», в противном случае — «True».Строка
if current_user inmembers:проверяет, находится ли пользователь в кэше результатов, поэтому дополнительные запросы к базе данных не выполняются.Использование len(members) вызывает QuerySet.__len__(), повторно используя кэш результатов, поэтому запросы к базе данных, опять же, не выполняются.
Цикл formember перебирает кэш результатов.
В общей сложности этот код выполняет один или ноль запросов к базе данных. Единственная преднамеренная оптимизация — использование переменнойmembers. Использование QuerySet.exists() для if, QuerySet.contains() для in или QuerySet.count() для счетчика вызовет дополнительные запросы.
Используйте QuerySet.update() и delete()¶
Вместо загрузки данных в объекты, изменения значений и отдельного их сохранения, используйте SQL UPDATE запросы через QuerySet.update(). Аналогично используйте массовое удаление при возможности.
Однако учтите, что эти методы не вызывают save() или delete() объектов. Это означает, что логика добавленная вами в эти методы, не будет выполнена, учитывая обработчики сигналов от объектов.
Используйте значения ключей непосредственно¶
Если вам необходимо только значение внешнего ключа, используйте его, т.к. оно уже в объекте, который вы получили, вместо получения всего связанного объекта и использования первичного ключа. Например, делайте:
entry.blog_id
вместо:
entry.blog.id
Не сортируйте данные, если вам это не требуется¶
Сортировка требует ресурсы. Каждое поле, по которому производится сортировка, требует от базы данных дополнительных ресурсов. Если модель имеет сортировку по-умолчанию (Meta.ordering) и она вам не нужна, уберите её из запроса с помощью order_by() (без параметров).
Добавление индекса в вашу базу данных может улучшить производительность операции сортировки.
Используйте массовые методы¶
Используйте массовые методы, чтобы сократить количество операторов SQL.
Используйте общее добавление¶
При создании объектов, если возможно, используйте метод bulk_create() чтобы сократить количество SQL запросов. Например:
Entry.objects.bulk_create(
[
Entry(headline="This is a test"),
Entry(headline="This is only a test"),
]
)
…предпочтительнее чем:
Entry.objects.create(headline="This is a test")
Entry.objects.create(headline="This is only a test")
Заметим, что есть несколько предостережений к этому методу, убедитесь что этот метод подходит для всего случая.
Используйте общее добавление¶
При создании объектов, если возможно, используйте метод bulk_create() чтобы сократить количество SQL запросов. Например:
entries = Entry.objects.bulk_create(
[
Entry(headline="This is a test"),
Entry(headline="This is only a test"),
]
)
Следующий пример:
entries[0].headline = "This is not a test"
entries[1].headline = "This is no longer a test"
Entry.objects.bulk_update(entries, ["headline"])
…предпочтительнее чем:
entries[0].headline = "This is not a test"
entries[0].save()
entries[1].headline = "This is no longer a test"
entries[1].save()
Заметим, что есть несколько предостережений к этому методу, убедитесь что этот метод подходит для всего случая.
Используйте общее добавление¶
При создании объектов, если возможно, используйте метод bulk_create() чтобы сократить количество SQL запросов. Например:
my_band.members.add(me, my_friend)
…предпочтительнее чем:
my_band.members.add(me)
my_band.members.add(my_friend)
…где Bands и Artists связаны через многие-ко-многим.
При создании объектов, если возможно, используйте метод bulk_create() чтобы сократить количество SQL запросов. Например:
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create(
[
PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
],
ignore_conflicts=True,
)
…предпочтительнее чем:
my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)
Заметим, что есть несколько предостережений к этому методу, убедитесь что этот метод подходит для всего случая.
Используйте общее добавление¶
При создании объектов, если возможно, используйте метод bulk_create() чтобы сократить количество SQL запросов. Например:
my_band.members.remove(me, my_friend)
…предпочтительнее чем:
my_band.members.remove(me)
my_band.members.remove(my_friend)
…где Bands и Artists связаны через многие-ко-многим.
При удалении разных пар объектов из ManyToManyFields используйте delete() для выражения Q с несколькими through экземпляры модели для уменьшения количества SQL-запросов. Например:
from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
Q(pizza=my_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=mushroom)
).delete()
…предпочтительнее чем:
my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)
…где Bands и Artists связаны через многие-ко-многим.