Введение в тему оптимизации запросов в Django ORM
Ка��ие инструменты существуют в Django для работы с сырыми SQL запросами
Преимущества и недостатки использования сырых SQL запросов
Недостатки выполнения пользовательского SQL
Введение в тему оптимизации запросов в Django ORM
В документации Django предлагаются следующие методы оптимизации:
Индексация
Кеширование
Уменьшение количества запросов
Индексация
Ускоряет выполнение запросов за счет быстрого поиска записей по индексу.
Индексы — это специальная структура данных, сопоставляющая значения в одном или нескольких столбцах таблицы с соответствующими местоположениями на физическом накопителе, что позволяет базе данных быстро находить строки по конкретному запросу без необходимости сканирования всей таблицы. В Django можно создавать индексы как вручную, так и автоматически.

Кеширование
Сохраняет результаты выполнения запросов, благодаря чему можно быстро получать их при последующих запросах.
Django предоставляет несколько встроенных механизмов кэширования, таких как cache_page и cache_control. На проекте закупок данная оптимизация не применяется, так как кеширование происходит на уровне базы данных.
*сache_page кэширует результаты представления на определенное время, чтобы при последующих запросах к этому представлению они могли быть возвращены из кэша, не выполняя запрос к базе данных.
*cache_control позволяет управлять кэшированием на уровне HTTP-заголовков.

Уменьшение количества запросов
По умолчанию Django не загружает связанные объекты вместе с основным запросом, а использует ленивую загрузку и откладывает запрос в базу до обращения к связанным объектам.
Такой подход упрощает работу со связанными объектами, но так же может привести к проблеме N + 1, когда для каждой связанной сущности генерируется дополнительный запрос в базу.
Метод select_related() предназначен для оптимизации запросов, когда мы работаем со связями ForeignKey и OneToOneField. Вместо отдельного запроса для получения связанных данных при каждом обращении, select_related() делает все в одном запросе. Это позволяет избежать так называемой проблемы ‘N+1’, ускоряя доступ к связанным данным.
Для загрузки данных этот метод использует JOIN, в случае если в основной таблице записей много и они ссылаются на одни и те же данные в связанной таблице, в результирующей таблице данные будут повторяться, что может наоборот негативно повлиять на производительность.

В отличие от select_related, prefetch_related загружает связанные объекты отдельным запросом для каждого поля переданного в качестве параметра, кеширует их в памяти, и производит связывание объектов внутри python. Это позволяет избежать множества лишних запросов и помогает бороться с проблемой ‘N+1’.
Такой подход позволяет загружать объекты для ManyToMany полей и записи которые ссылаются на нашу таблицу через ForeignKey поле используя related_name.
hb=Province.objects.prefetch_related ('city_set').get (name__iexact=u"Hubei Province")
for city in hb.city_set.all():
city.nameЗапускаемые SQL-запросы:
SELECT Optimize_province.id, Optimize_province.name
FROM Optimize_province
WHERE Optimize_province.name LIKE Hubei Province;
SELECT Optimize_city.id, Optimize_city.name, Optimize_city.province_id
FROM Optimize_city
WHERE Optimize_city.province_id IN (1);Методы values() и values_list() служат для выборки только тех полей из модели, которые переданы в качестве параметров. values() возвращает результаты в виде словарей, а values_list() — в виде кортежей. Это позволяет уменьшить объем данных, получаемых из базы и тем самым ускорить обработку.
Метод annotate() позволяет добавить новое вычисляемое поле к каждой записи, а aggregate() вычисляет агрегатные значения, например сумму, среднее или количество записей для всего набора данных. Это позволяет избежать дополнительных запросов и выполнить вычисления на уровне БД, что значительно эффективнее.
Методы bulk_create() и bulk_update() служат для массового создания и массового обновления записей соответственно. Использование этих методов позволяет отправить в базу данных только один запрос для создания или обновления большого количества записей одной модели. Это значительно ускоряет процесс массового добавления или изменения данных.
К сожалению, в определенных местах проекта мощности DjangoORM все равно не достаточно. Увеличить производительность можно по средствам сырых SQL запросов. Для этого разработчики Django предлагают следующий инструментарий.
Какие инструменты существуют в Django для работы с сырыми SQL запросами
Метод менеджера raw()
Принимает необработанный SQL-запрос, выполняет его и возвращает экземпляр RawQuerySet, который можно перебирать так же, как обычный QuerySet, но он не обладает всеми теми же возможностями QuerySet. Аннотацию, фильтрацию и упорядочивание необходимо реализовывать в самом SQL запросе , соотвественно, на языке SQL. У метода есть удобное автоматическое сопоставление полей запроса с полями модели. Возможен поиск по индексу. Обладает «отложенными полями модели», когда поля не прописываются в запросе и будут загружаться исключительно по требованию.
Метод поддерживает только SELECT запросы.
Ниже примеры как выглядит вызов метода. Обратите внимание, на второй пример где демонстрируется перебор RawQuerySet и можно заметить в самом запросе аннотацию.
Person.objects.raw(
"""
SELECT first AS first_name,
last AS last_name,
bd AS birth_date,
pk AS id,
FROM some_other_table
"""
)people = Person.objects.raw( 'SELECT *, age(birth_date) AS age FROM myapp_person')
for p in people:
print("%s is %s." % (p.first_name, p.age))
# John is 37.
# Jane is 42.В целом этот метод хорошо применим для быстрого получения большого количества данных. Django ожидает, что оператор вернет набор строк из базы данных, ничего при этом не делая.
Для оператора SQL, переданного в .raw(), проверка не выполняется. Если запрос не возвращает строки, это приведет к ошибке.
При необходимости выполнить UPDATE, INSERT или DELETE запросы Django предлагает воспользоваться следующим инструментом.
Выполнение пользовательского SQL напрямую
Выполнение пользовательского SQL напрямую в Django позволяет разработчикам взаимодействовать с базой данных без использования моделей. Это может быть полезно в ситуациях, когда требуется выполнить сложные запросы или операции, которые тяжело описать встроенными функциями Django ORM.
Чтобы использовать соединение с базой данных необходимо вызвать connection.cursor(). Затем вызвать cursor.execute(sql, [params]), чтобы выполнить SQL.и cursor.fetchone(возвращает одну строку из результата запроса) или cursor.fetchall(возвращает все строки из результата запроса).
from django.db import connection
def my_custom_sql(self):
with connection.cursor as cursor:
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
row = cursor.fetchone
return rowПо умолчанию Python DB API возвращает результаты без имен полей, в джанго документации предлагается пример написания функции, которая может обрабатывать результат и возвращать dict(в примере ниже это функция "dictfetchall". Другой вариант - использовать collections.namedtuple() из стандартной библиотеки Python. namedtuple - это объект, похожий на кортеж, у которого есть поля, доступные при поиске по атрибутам; он индексируемый и повторяемый.
cursor.execute("SELECT id, parent_id FROM test LIMIT 2")
cursor.fetchall
# ((54360982, None), (54360880, None))
cursor.execute("SELECT id, parent_id FROM test LIMIT 2")
dictfetchall(cursor)
# ['parent_id': None, 'id': 54360982}, {'parent_id': None, 'id': 54360880}]
cursor.execute("SELECT id, parent_id FROM test LIMIT 2")
results = namedtuplefetchall(cursor)
results
# [Result(id=54360982, parent_id=None), Result(id=54360880, parent_id=None)]
results[0].id
# 54360982
results[0][0]
# 54360982Class RawSQL(sql, params, output_field)
Он так же помогает с необработанными выражениями SQL, которые можно встроить напрямую в фильтр или аннотацию. При данном способе мы получаем полноценный QuerySet и открываем возможность выразить сложную фильтрацию для определенного столбца. Класс RawSQL принимает три параметра: sql, params и output_field. Параметр sql представляет собой строку, содержащую SQL-запрос. Параметр params представляет собой список или кортеж, содержащий значения, которые будут подставлены в запрос. Параметр output_field представляет собой тип данных, который будет возвращён запросом.
Примеры демонтрируют простоту использования,как в аннотируемом поле, так и в фильтре.
form django.db.models.expressions import RawSQL
queryset.annotate(val=RawSQL('select col from sometable where othercol = %s', (params,)))
queryset.filter(id__in=RawSQL('select id from sometable where col = %s', (params,)))Extra(select=None, select_params=None, params=None, where=None, tables=None, order_by=None)
Иногда синтаксис запроса Django сам по себе не может легко выразить сложное предложение WHERE. Для этих крайних случаев Django предоставляет модификатор extra() QuerySet - ловушку для вставки определенных предложений в SQL, генерируемый QuerySet.
Аргумент select позволяет добавлять дополнительные поля в предложение SELECT. Это должен быть словарь, сопоставляющий имена атрибутов с предложениями SQL ,которые вычисляют значение этого атрибута. Для передачи параметров фрагментам SQL применяется select_params.
Аргумент params - это список любых дополнительных параметров, которые нужно заменить.
Where / tables – работают по такому же принципу что и селект, добавляет условия в WHERE/ таблицы в FROM.
order_by для extra() добавляет упорядочивание в результирующий набор запросов.
*В таких вставках мы можем вписывать атрибуты, о которых мы знаем, что они будут в основном запросе джанго.
Blog.objects.extra(
select={
"entry_count": "SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id"
},
)SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id)
AS entry_count
FROM blog_blog;Entry.objects.extra(select={'is_recent': "pub_date › '2006-01-01' "})SELECT blog_entry.*, (pub_date > '2006-01-01') AS is_recent
FROM blog_entry;Преимущества и недостатки использования сырых SQL запросов
Преимущества
Гибкость. Позволяет разработчикам писать сложные запросы, которые могут быть не доступны или сложны для реализации с использованием Django ORM.
Минимизация количества запросов. В один SQL запрос возможно включить сразу несколько операций, что может уменьшить количество обращений к базе данных и повысить производительность приложения.
Быстрая работа с большими объёмами данных.Использование raw SQL может быть более эффективным и быстрым, чем ORM, так как Django не нужно тратить время и ресурсы на формирование запроса в бд и обработку данных.
Недостатки
Отход от принципов ORM. ORM Django предоставляет абстракцию над базой данных, упрощая работу с данными и уменьшая количество кода, необходимого для взаимодействия с базой данных. Использование raw SQL отходит от этих принципов и может привести к потере некоторых преимуществ ORM.
Сложность отладки и тестирования. Поскольку raw SQL запросы могут быть более сложными и менее абстрактными, они могут затруднить отладку и тестирование вашего кода. Может потребоваться больше усилий для проверки правильности работы ваших запросов.
Уязвимость кода. Django никак не защищает и не проверяет SQL-запросы, об этом нужно заботиться отдельно, предостерегая код от SQL-инъекций.
* SQL-инъекция — это один из самых распространённых способов атаки на веб-приложения, использующие базы данных, позволяет злоумышленнику внедрить вредоносный SQL-код в запрос к базе данных.
Заключение
Подведем итоги: сырые SQL запросы это эффективный способ оптимизировать ORM запросы, НО далеко не все случаи подходят под такую оптимизацию. Зачастую это точечно узкие места проекта, где либо слишком сложная бизнес-логика, либо большие объемы данных, работать с которыми на SQL продуктивнее. Django ORM обладает хорошими мощностями для построения достойных SQL запросов, поэтому для использования RawSQL потребуютcя хорошие знания в области баз данных и в частности языка SQL. Прибегать к рассмотренным методам без крайней надобности не рекомендуется дабы не подрывать безопасность кода, не усложнять его читаемость и отладку, а также не отходить абстракций.
