1 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Базы данных призваны не только хранить и накапливать данные. И даже если добавить к хранению выдачу данных по запросу, останется не упомянутой ещё одна обязанность, возлагаемая на СУБД:<em>анализ накопленных данных</em>.</p>
1
<p>Базы данных призваны не только хранить и накапливать данные. И даже если добавить к хранению выдачу данных по запросу, останется не упомянутой ещё одна обязанность, возлагаемая на СУБД:<em>анализ накопленных данных</em>.</p>
2
<p>Представим классический пример предметной области - "Книжный магазин". Главной сущностью в проекте, работающем в данной области, будет "книга":</p>
2
<p>Представим классический пример предметной области - "Книжный магазин". Главной сущностью в проекте, работающем в данной области, будет "книга":</p>
3
<p>Когда имеешь дело с некоторыми товарами, часто приходится вычислять<em>суммарную цену</em>наборов товаров и<em>среднюю цену</em>товаров набора. Python - язык достаточно выразительный, он позволяет ту же сумму посчитать прямо на месте:</p>
3
<p>Когда имеешь дело с некоторыми товарами, часто приходится вычислять<em>суммарную цену</em>наборов товаров и<em>среднюю цену</em>товаров набора. Python - язык достаточно выразительный, он позволяет ту же сумму посчитать прямо на месте:</p>
4
<p>Средняя цена вычисляется ненамного сложнее. Однако будет ли такое решение оправданным?</p>
4
<p>Средняя цена вычисляется ненамного сложнее. Однако будет ли такое решение оправданным?</p>
5
<p>ORM честно запросит все книги из базы и поместит данные каждой книги в объект класса Book. А затем в коде потребуется только цена - это уже выглядит как лишняя работа. И СУБД тоже потратит лишние ресурсы на загрузку всех столбцов таблицы, вместо того, чтобы достать ровно один. Когда дело касается крупного магазина книг, подобное использование БД неприемлемо.</p>
5
<p>ORM честно запросит все книги из базы и поместит данные каждой книги в объект класса Book. А затем в коде потребуется только цена - это уже выглядит как лишняя работа. И СУБД тоже потратит лишние ресурсы на загрузку всех столбцов таблицы, вместо того, чтобы достать ровно один. Когда дело касается крупного магазина книг, подобное использование БД неприемлемо.</p>
6
<blockquote><p>Строго говоря, Django ORM умеет запрашивать только часть данных. Как это делается, будет рассказано в последующем уроке про эффективную работу с БД.</p>
6
<blockquote><p>Строго говоря, Django ORM умеет запрашивать только часть данных. Как это делается, будет рассказано в последующем уроке про эффективную работу с БД.</p>
7
</blockquote><p>Ещё один минус подобной обработки на стороне Python заключается в том, что мы при этом не используем часть возможностей СУБД. Как уже было сказано, подсчёт сумм и средних значений - часто встречающиеся задачи. Поэтому большинство СУБД умеет выполнять такой анализ данных на своей стороне и сам язык SQL содержит средства для описания того, что же СУБД должна вычислить или, как ещё говорят, выполнить<em>агрегацию</em>. И разработчики СУБД вкладывают много сил в то, чтобы агрегация работала быстро. Осталось научиться описывать агрегацию с использованием Django ORM.</p>
7
</blockquote><p>Ещё один минус подобной обработки на стороне Python заключается в том, что мы при этом не используем часть возможностей СУБД. Как уже было сказано, подсчёт сумм и средних значений - часто встречающиеся задачи. Поэтому большинство СУБД умеет выполнять такой анализ данных на своей стороне и сам язык SQL содержит средства для описания того, что же СУБД должна вычислить или, как ещё говорят, выполнить<em>агрегацию</em>. И разработчики СУБД вкладывают много сил в то, чтобы агрегация работала быстро. Осталось научиться описывать агрегацию с использованием Django ORM.</p>
8
<h2>Агрегация и агрегирующие функции</h2>
8
<h2>Агрегация и агрегирующие функции</h2>
9
<p>Для того чтобы получить уже агрегированные данные, нужно воспользоваться методом .aggregate(), вызвав его у имеющегося менеджера или QuerySet. Этот метод принимает в качестве параметров так называемые<a>агрегирующие функции</a>. Функций этих достаточно много, но все они используются примерно одинаково, поэтому рассмотрим для примера функцию Avg:</p>
9
<p>Для того чтобы получить уже агрегированные данные, нужно воспользоваться методом .aggregate(), вызвав его у имеющегося менеджера или QuerySet. Этот метод принимает в качестве параметров так называемые<a>агрегирующие функции</a>. Функций этих достаточно много, но все они используются примерно одинаково, поэтому рассмотрим для примера функцию Avg:</p>
10
<p>Если аргументы указываются как позиционные, то имена для ключей генерирует Django ORM на основе имени поля и имени агрегирующей функции. Аргументов можно указать сразу несколько и генерируемые имена не дадут запутаться:</p>
10
<p>Если аргументы указываются как позиционные, то имена для ключей генерирует Django ORM на основе имени поля и имени агрегирующей функции. Аргументов можно указать сразу несколько и генерируемые имена не дадут запутаться:</p>
11
<p>Как можно заметить, каждый запрос на агрегацию возвращает не сами книги, а только итоговый результат. Таким образом со стороны Python никаких промежуточных объектов создавать не приходится!</p>
11
<p>Как можно заметить, каждый запрос на агрегацию возвращает не сами книги, а только итоговый результат. Таким образом со стороны Python никаких промежуточных объектов создавать не приходится!</p>
12
<p>Вернёмся к учебному проекту, который моделирует платформу для ведения блогов. Никаких "цен" в этом проекте нет, но задачи для анализа найдутся. Предположим, что нужно для каждой записи в блоге некоторого автора узнать количество комментариев. Агрегация на первый взгляд не подходит: сами посты тоже нужны. Можно решить задачу "в лоб", написав:</p>
12
<p>Вернёмся к учебному проекту, который моделирует платформу для ведения блогов. Никаких "цен" в этом проекте нет, но задачи для анализа найдутся. Предположим, что нужно для каждой записи в блоге некоторого автора узнать количество комментариев. Агрегация на первый взгляд не подходит: сами посты тоже нужны. Можно решить задачу "в лоб", написав:</p>
13
<p>Такое решение имеет своё собственное название - "N+1 запросов" - поскольку будет выполнен один запрос N постов, а затем N запросов комментариев к каждому. Легко представить, насколько это неэффективно.</p>
13
<p>Такое решение имеет своё собственное название - "N+1 запросов" - поскольку будет выполнен один запрос N постов, а затем N запросов комментариев к каждому. Легко представить, насколько это неэффективно.</p>
14
<p>Для того чтобы для каждой возвращаемой сущности<em>вычислить</em>некоторое значение в рамках одного запроса, Django ORM предоставляет механизм<em>аннотирования</em>.</p>
14
<p>Для того чтобы для каждой возвращаемой сущности<em>вычислить</em>некоторое значение в рамках одного запроса, Django ORM предоставляет механизм<em>аннотирования</em>.</p>
15
<h2>Аннотирование</h2>
15
<h2>Аннотирование</h2>
16
-
<p>Процесс, при котором к каждому объекту из выборки применяется агрегирующая функция, назвается аннотированием. Он описывается вызовом метода .annotate() применительно к менеджеру или QuerySet. Этот метод принимает те же агрегирующие функции, а возвращает метод QuerySet, объекты которого будут всё теми же экземплярами класса модели, но каждый объект будет иметь<em>дополнительные атрибуты</em>. Каждый атрибут будет хранить результат соответствующей агрегации<strong>относительно текущего объекта</strong>. Например, .aggregate(Count('postcomment')) подсчитает количество<strong>всех</strong>комментариев, а .annotate(Count('postcomment')) даст количество комментариев<strong>к каждому</strong>посту. Так выглядит подсчёт количества тегов, которыми помечен каждый пост:</p>
16
+
<p>Процесс, при котором к каждому объекту из выборки применяется агрегирующая функция, назвается аннотированием. Он описывается вызовом метода .annotate() применительно к менеджеру или QuerySet. Этот метод принимает те же агрегирующие функции, а возв��ащает метод QuerySet, объекты которого будут всё теми же экземплярами класса модели, но каждый объект будет иметь<em>дополнительные атрибуты</em>. Каждый атрибут будет хранить результат соответствующей агрегации<strong>относительно текущего объекта</strong>. Например, .aggregate(Count('postcomment')) подсчитает количество<strong>всех</strong>комментариев, а .annotate(Count('postcomment')) даст количество комментариев<strong>к каждому</strong>посту. Так выглядит подсчёт количества тегов, которыми помечен каждый пост:</p>
17
<p>Здесь новый атрибут получил имя "tags__count", но имя можно было указать вручную, как и в случае обычной агрегации.</p>
17
<p>Здесь новый атрибут получил имя "tags__count", но имя можно было указать вручную, как и в случае обычной агрегации.</p>
18
<h2>Аннотирование и дубликаты в выдаче</h2>
18
<h2>Аннотирование и дубликаты в выдаче</h2>
19
<p>Если вы уже имеете некоторый опыт в SQL, вы можете задаться вопросом: а не добавляет ли OUTER JOIN, который можно заметить в примере выше, в выборку дублирующиеся элементы, если присовокупляемые сущности соотносятся с текущей как "многие к одному"? Добавляет! Более того, агрегация в таких случаях даёт неверные результаты, так как учитывает и повторяющиеся строки. И тем больше дублей вы увидите, чем больше разных связей "многие к одному" задействуете (и даже одну и ту же, но несколько раз).</p>
19
<p>Если вы уже имеете некоторый опыт в SQL, вы можете задаться вопросом: а не добавляет ли OUTER JOIN, который можно заметить в примере выше, в выборку дублирующиеся элементы, если присовокупляемые сущности соотносятся с текущей как "многие к одному"? Добавляет! Более того, агрегация в таких случаях даёт неверные результаты, так как учитывает и повторяющиеся строки. И тем больше дублей вы увидите, чем больше разных связей "многие к одному" задействуете (и даже одну и ту же, но несколько раз).</p>
20
<p>Увы, в общем виде эту проблему не решить. Но конкретно агрегирующая функция Count имеет опцию distinct=True, которая убирает дублирование, пока вы используете только этот вид аннотаций и каждый Count используете с distinct=True.</p>
20
<p>Увы, в общем виде эту проблему не решить. Но конкретно агрегирующая функция Count имеет опцию distinct=True, которая убирает дублирование, пока вы используете только этот вид аннотаций и каждый Count используете с distinct=True.</p>
21
<h2>Агрегация аннотированных значений</h2>
21
<h2>Агрегация аннотированных значений</h2>
22
<p>Аннотирование позволяет добавить вычислимые данные к каждому элементу запроса, а это значит, что можно выполнить итоговую агрегацию с использованием этих значений. Получение среднего количества тегов среди всех постов будет выглядеть так:</p>
22
<p>Аннотирование позволяет добавить вычислимые данные к каждому элементу запроса, а это значит, что можно выполнить итоговую агрегацию с использованием этих значений. Получение среднего количества тегов среди всех постов будет выглядеть так:</p>
23
<p>Преимущество такого подхода в том, что база данных выполняет все вычисления на своей стороне одним запросом, что гораздо эффективнее, чем делать несколько отдельных запросов или обрабатывать данные на стороне Python.</p>
23
<p>Преимущество такого подхода в том, что база данных выполняет все вычисления на своей стороне одним запросом, что гораздо эффективнее, чем делать несколько отдельных запросов или обрабатывать данные на стороне Python.</p>
24
<p>Поля, добавляемые аннотированием, можно использовать не только для агрегации, но и для фильтрации с сортировкой, и даже в последующих аннотациях - везде, где можно использовать обычные поля.</p>
24
<p>Поля, добавляемые аннотированием, можно использовать не только для агрегации, но и для фильтрации с сортировкой, и даже в последующих аннотациях - везде, где можно использовать обычные поля.</p>