1 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
-
<p>Как и любое средство повышения уровня абстракции, ORM может выдавать не самые эффективные запросы в некоторых случаях. К сожалению, по-другому быть не может: тонко настроенные SQL-запросы находятся на существенно более низком уровне. В Django ORM можно переписывать код запроса руками, но есть ли в этом необходимость? Вовсе нет.</p>
1
+
<p>Как и любое средство повышения уровня абстракции, ORM может выдавать не самые эффективные запросы в некоторых случаях. К сожалению, по-другому быть не может: тонко настроенные SQL-запросы находятся на существенно более низком уровне. В Django ORM можно переписывать код запрос�� руками, но есть ли в этом необходимость? Вовсе нет.</p>
2
<p>Для начала прочтем отрывок из документации к фреймворку:</p>
2
<p>Для начала прочтем отрывок из документации к фреймворку:</p>
3
<ol><li>Сначала<strong>измерьте</strong>. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода .explain(). Тут пригодится умение читать SQL и понимать то, о чем рассказывает через explain ваша СУБД</li>
3
<ol><li>Сначала<strong>измерьте</strong>. У любого QuerySet можно запросить описание предполагаемых запросов с помощью вызова метода .explain(). Тут пригодится умение читать SQL и понимать то, о чем рассказывает через explain ваша СУБД</li>
4
<li>Добавьте<strong>индексы</strong>. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в<a>документации</a></li>
4
<li>Добавьте<strong>индексы</strong>. SQL explain часто может сказать, что в каком-то месте делается "full scan" то есть полный перебор, а это означает, что где-то не хватает индексов. Стоит подумать о том, чтобы оные добавить. Подробности можно почитать в<a>документации</a></li>
5
<li>Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.</li>
5
<li>Делайте как можно больше работы силами СУБД. Аннотируйте, агрегируйте. СУБД знает, как это оптимизировать и как кэшировать результаты.</li>
6
<li>В крайнем случае пишите<strong>необходимый минимум</strong>SQL.</li>
6
<li>В крайнем случае пишите<strong>необходимый минимум</strong>SQL.</li>
7
<li>Знайте, как работают QuerySets и помните:</li>
7
<li>Знайте, как работают QuerySets и помните:</li>
8
</ol><ul><li><strong>Насколько</strong>QuerySets ленив - он не делает лишних запросов, пока вы не попросите</li>
8
</ol><ul><li><strong>Насколько</strong>QuerySets ленив - он не делает лишних запросов, пока вы не попросите</li>
9
<li><strong>В какой момент</strong>QuerySet вычисляется (финализируется)</li>
9
<li><strong>В какой момент</strong>QuerySet вычисляется (финализируется)</li>
10
<li><strong>Каким образом</strong>данные представлены в памяти</li>
10
<li><strong>Каким образом</strong>данные представлены в памяти</li>
11
<li><strong>Как</strong>QuerySet кеширует результаты запросов и подзапросов</li>
11
<li><strong>Как</strong>QuerySet кеширует результаты запросов и подзапросов</li>
12
</ul><p>Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдете до написания SQL.</p>
12
</ul><p>Следует помнить, что многие ситуации разрешаются задолго до того, как вы дойдете до написания SQL.</p>
13
<h2>Explain</h2>
13
<h2>Explain</h2>
14
<p>Один из ключевых приемов для анализа запросов это функция EXPLAIN. Он показывает, как база данных планирует выполнить ваш запрос. Он раскрывает такие важные детали, как использование индексов, порядок соединения таблиц и предполагаемую стоимость операций. В Django метод .explain() предоставляет удобный доступ к этой функциональности, помогая понять, почему некоторые запросы могут работать медленнее других и как их можно оптимизировать.</p>
14
<p>Один из ключевых приемов для анализа запросов это функция EXPLAIN. Он показывает, как база данных планирует выполнить ваш запрос. Он раскрывает такие важные детали, как использование индексов, порядок соединения таблиц и предполагаемую стоимость операций. В Django метод .explain() предоставляет удобный доступ к этой функциональности, помогая понять, почему некоторые запросы могут работать медленнее других и как их можно оптимизировать.</p>
15
<p>EXPLAIN выведет структуру запроса конкретной базы данных. Выше пример для Postgres, в другой базе вывод будет своим.</p>
15
<p>EXPLAIN выведет структуру запроса конкретной базы данных. Выше пример для Postgres, в другой базе вывод будет своим.</p>
16
<h2>Ограничение состава загружаемых данных</h2>
16
<h2>Ограничение состава загружаемых данных</h2>
17
<p>Довольно часто нам не требуются все поля модели - нужна всего пара полей из нескольких десятков. Тут пригодится метод .values_list(имена, полей), который возвращает новый QuerySet. Его элементами будут кортежи со значениями указанных полей в указанном же порядке. Такой QuerySet удобно обходить в цикле с одновременной распаковкой:</p>
17
<p>Довольно часто нам не требуются все поля модели - нужна всего пара полей из нескольких десятков. Тут пригодится метод .values_list(имена, полей), который возвращает новый QuerySet. Его элементами будут кортежи со значениями указанных полей в указанном же порядке. Такой QuerySet удобно обходить в цикле с одновременной распаковкой:</p>
18
<p>Если значений нужно не два и не три, то есть смысл использовать метод .values(имена, полей). При обходе итогового QuerySet он даст уже не кортежи, а словари с ключами, совпадающими с именами указанных полей.</p>
18
<p>Если значений нужно не два и не три, то есть смысл использовать метод .values(имена, полей). При обходе итогового QuerySet он даст уже не кортежи, а словари с ключами, совпадающими с именами указанных полей.</p>
19
<p>Однако ни кортежи, ни словари не дают воспользоваться методами модели, а в некоторых ситуациях это все-таки требуется. Если точно известно, какие поля понадобятся при работе с моделью и при вызове ее методов, подойдет метод .only(имена, полей): возращаемый им QuerySet выдает объекты модели, но в каждом объекте заполнены только указанные поля. Так мы можем использовать все возможности модели. Но следует помнить, что первое же обращение к полю, не указанному в вызове .only(), породит запрос. Он сработает для текущего объекта - запросит данные для этого поля и запомнит их на будущее:</p>
19
<p>Однако ни кортежи, ни словари не дают воспользоваться методами модели, а в некоторых ситуациях это все-таки требуется. Если точно известно, какие поля понадобятся при работе с моделью и при вызове ее методов, подойдет метод .only(имена, полей): возращаемый им QuerySet выдает объекты модели, но в каждом объекте заполнены только указанные поля. Так мы можем использовать все возможности модели. Но следует помнить, что первое же обращение к полю, не указанному в вызове .only(), породит запрос. Он сработает для текущего объекта - запросит данные для этого поля и запомнит их на будущее:</p>
20
<p>Использование .only() отлично показывает всю высокоуровневость ORM: можно просто обращаться к полям объекта и не думать, что и когда подгружается. Еще<em>view</em>выводит посты блога в виде списка диалогов, причем выводит тело только для пары первых пунктов. Это очень лаконичное решение с точки зрения кода, которое еще и не слишком нагружает базу.</p>
20
<p>Использование .only() отлично показывает всю высокоуровневость ORM: можно просто обращаться к полям объекта и не думать, что и когда подгружается. Еще<em>view</em>выводит посты блога в виде списка диалогов, причем выводит тело только для пары первых пунктов. Это очень лаконичное решение с точки зрения кода, которое еще и не слишком нагружает базу.</p>
21
<h2>Ранняя загрузка связанных данных</h2>
21
<h2>Ранняя загрузка связанных данных</h2>
22
<p>Проблема N+1 запросов - это классическая ловушка производительности при работе с ORM. Она возникает, когда код сначала делает один запрос для получения списка объектов, а затем для каждого объекта выполняет дополнительный запрос для получения связанных данных.</p>
22
<p>Проблема N+1 запросов - это классическая ловушка производительности при работе с ORM. Она возникает, когда код сначала делает один запрос для получения списка объектов, а затем для каждого объекта выполняет дополнительный запрос для получения связанных данных.</p>
23
<p>Например, мы открываем список из 100 постов блога, и для каждого поста нужно показать имя автора. Наивный подход приведет к тому, что сначала будет выполнен один запрос для получения постов, а затем для каждого поста будет выполнен отдельный запрос для получения информации об авторе. В итоге получится 101 запрос к базе данных (1 + 100), отсюда и название "N+1".</p>
23
<p>Например, мы открываем список из 100 постов блога, и для каждого поста нужно показать имя автора. Наивный подход приведет к тому, что сначала будет выполнен один запрос для получения постов, а затем для каждого поста будет выполнен отдельный запрос для получения информации об авторе. В итоге получится 101 запрос к базе данных (1 + 100), отсюда и название "N+1".</p>
24
<p>Вот как это выглядит в коде:</p>
24
<p>Вот как это выглядит в коде:</p>
25
<p>Здесь один запрос получает все посты, а затем для каждого поста запрашивается автор.</p>
25
<p>Здесь один запрос получает все посты, а затем для каждого поста запрашивается автор.</p>
26
<p>Нельзя сказать, что тут все плохо. Авторы подгружаются по мере необходимости, если мы хотим вывести список заголовков постов и показать автора первых двух постов, то это приемлемый код.</p>
26
<p>Нельзя сказать, что тут все плохо. Авторы подгружаются по мере необходимости, если мы хотим вывести список заголовков постов и показать автора первых двух постов, то это приемлемый код.</p>
27
<p>Если же все посты нужны вместе с авторами, нужно использовать метод .select_related(), который эквивалентен JOIN в SQL. Если при вызове метода не указывать аргументы, то будут присоединены все связанные таблицы. Если же указать имена связей, то указанные таблицы подгрузятся сразу, остальные - как обычно, по требованию. Это достаточно гибкое средство, особенно в сочетании с другими способами оптимизации:</p>
27
<p>Если же все посты нужны вместе с авторами, нужно использовать метод .select_related(), который эквивалентен JOIN в SQL. Если при вызове метода не указывать аргументы, то будут присоединены все связанные таблицы. Если же указать имена связей, то указанные таблицы подгрузятся сразу, остальные - как обычно, по требованию. Это достаточно гибкое средство, особенно в сочетании с другими способами оптимизации:</p>
28
<p>Обратите внимание, что первый запрос содержал только указанные поля - пусть даже и в двух таблицах - а также служебные поля вроде id и поля для связи таблиц. При этом с точки зрения наблюдателя post выглядит как цельный объект модели Post, а его атрибут .author - как цельный объект модели User. Все дополнительные данные прозрачно подгружаются по мере необходимости.</p>
28
<p>Обратите внимание, что первый запрос содержал только указанные поля - пусть даже и в двух таблицах - а также служебные поля вроде id и поля для связи таблиц. При этом с точки зрения наблюдателя post выглядит как цельный объект модели Post, а его атрибут .author - как цельный объект модели User. Все дополнительные данные прозрачно подгружаются по мере необходимости.</p>
29
<p>Кроме select_related(), в Django есть еще один похожий метод - prefetch_related(). С помощью обоих методов можно работать со связанными объектами, но немного в разных сценариях:</p>
29
<p>Кроме select_related(), в Django есть еще один похожий метод - prefetch_related(). С помощью обоих методов можно работать со связанными объектами, но немного в разных сценариях:</p>
30
<ul><li><p>Метод select_related() используется для отношений типа "один к одному" (OneToOne) и "многие к одному" (ForeignKey). Например, такое отношение есть между постом и автором в блоге, потому что каждый пост написан одним автором. Чтобы получить информацию об авторе каждого поста, можно добавить select_related() в тот же запрос к базе данных. Метод select_related() работает через выполнение SQL JOIN и включение полей связанного объекта в оператор SELECT.</p>
30
<ul><li><p>Метод select_related() используется для отношений типа "один к одному" (OneToOne) и "многие к одному" (ForeignKey). Например, такое отношение есть между постом и автором в блоге, потому что каждый пост написан одним автором. Чтобы получить информацию об авторе каждого поста, можно добавить select_related() в тот же запрос к базе данных. Метод select_related() работает через выполнение SQL JOIN и включение полей связанного объекта в оператор SELECT.</p>
31
</li>
31
</li>
32
<li><p>Метод prefetch_related() используется для отношений типа "один ко многим" и "многие ко многим" (ManyToManyField). Наглядный пример - пост в блоге, у которого может быть много комментариев (один ко многим), или теги постов (многие ко многим). Метод prefetch_related() выполняет отдельные запросы для каждого отношения и выполняет объединение уже в Python. Так мы заранее извлекаем связанные объекты, что помогает избежать проблемы N+1 запросов</p>
32
<li><p>Метод prefetch_related() используется для отношений типа "один ко многим" и "многие ко многим" (ManyToManyField). Наглядный пример - пост в блоге, у которого может быть много комментариев (один ко многим), или теги постов (многие ко многим). Метод prefetch_related() выполняет отдельные запросы для каждого отношения и выполняет объединение уже в Python. Так мы заранее извлекаем связанные объекты, что помогает избежать проблемы N+1 запросов</p>
33
</li>
33
</li>
34
</ul><p>Оба метода помогают избежать проблемы<em>N+1</em>, когда итерация по связанным объектам вызывает дополнительный запрос для каждого объекта. Но все таки один метод может быть более подходящим, чем другой - все зависит от типа отношения и конкретного случая.</p>
34
</ul><p>Оба метода помогают избежать проблемы<em>N+1</em>, когда итерация по связанным объектам вызывает дополнительный запрос для каждого объекта. Но все таки один метод может быть более подходящим, чем другой - все зависит от типа отношения и конкретного случая.</p>
35
<p>Проще говоря, оба метода позволяют сказать: "Я получаю данные и знаю, что еще мне понадобятся эти другие связанные данные, поэтому получи все сразу, а не ходи туда-сюда в базу данных".</p>
35
<p>Проще говоря, оба метода позволяют сказать: "Я получаю данные и знаю, что еще мне понадобятся эти другие связанные данные, поэтому получи все сразу, а не ходи туда-сюда в базу данных".</p>
36
<h2>Самостоятельная работа</h2>
36
<h2>Самостоятельная работа</h2>
37
<ol><li>На примере моделей из учебного проекта постройте несколько сложных запросов и понаблюдайте, какие запросы и в какой момент делает ORM.</li>
37
<ol><li>На примере моделей из учебного проекта постройте несколько сложных запросов и понаблюдайте, какие запросы и в какой момент делает ORM.</li>
38
<li>Попробуйте пооптимизировать запросы с помощью описанных в уроке средств.</li>
38
<li>Попробуйте пооптимизировать запросы с помощью описанных в уроке средств.</li>
39
</ol>
39
</ol>