HTML Diff
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>