Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Technical Portfolio
GitHub & GitVerse Pages

Глава 8. Группировка и агрегация

SQL Lab in JupyterLab

Data & BI Analyst
Pandas ver. 3.0.5: порог усечения строк уменьшен до 20
SQLAlchemy ver. 2.0.52: подключение создано
JupySQL ver. 0.11.1: подключен через SQLAlchemy Engine

Концепция группировки

При наличии 599 клиентов, имеющих более 16000 записей об аренде, просмотрев необработанные данные, невозможно определить какие клиенты взяли напрокат больше всего фильмов:

Loading...
Loading...

Вместо этого мы можем попросить сервер базы данных сгруппировать данные с помощью предложения group_by для группировки данных о прокате по идентификатору клиента:

Loading...
Loading...

Результирующий набор содержит по одной строке для каждого отдельного значения в столбце customer_id. Причина меньшего результирующего набора в том, что некоторые клиенты брали напрокат более одного фильма.

Чтобы увидеть, сколько фильмов было взято напрокат каждым клиентом, можно использовать агрегатную функцию count(*) в предложении select для подсчета количества строк в каждой группе:

Loading...
Loading...

Чтобы определить, какие клиенты взяли напрокат больше всего фильмов, добавим предложение order by:

Loading...
Loading...

При группировке данных может потребоваться отфильтровать нежелательные данные из набора результатов на основе групп данных (а не необработанных данных). Поскольку предложение group by выполняется после того, как вычислено предложение where, добавить условия фильтрации к предложению where для этой цели нельзя.

Например, вот к чему приводит попытка отфильтровать клиентов, бравших напрокат менее 40 фильмов:

RuntimeError: (pymysql.err.ProgrammingError) (1111, 'Invalid use of group function')
[SQL: SELECT
    customer_id,
    COUNT(*)
FROM rental
WHERE COUNT(*) >= 40
GROUP BY customer_id;]
(Background on this error at: https://sqlalche.me/e/20/f405)

Вы не можете обратиться к агрегатной функции count(*) в предложении where потому что во время вычисления предложения where группы еще не были сгенерированы. Вместо этого вы должны поместить условия группового фильтра в предложение having:

Loading...
Loading...
Loading...
Loading...

В таблице rental колонка return_date содержит дату и время возврата диска. Если покупатель еще не принес фильм обратно в прокат, в эту ячейку записывается NULL.

Loading...
Loading...

Агрегатные функции

Агрегатные функции выполняют определенные операции над всеми строками в группе. Распространенные агрегатные функции:

ФункцияОписание
max()Возвращает максимальное значение в наборе
min()Возвращает минимальное значение в наборе
avg()Возвращает усредненное значение в наборе
sum()Возвращает сумму значений в наборе
count()Возвращает количество значений в наборе

Вот как выглядит запрос, в котором используются все распространенные агрегатные функции для анализа данных по прокату фильмов:

Loading...
Loading...

Неявная и явная группировка

В предыдущем примере каждое значение, возвращаемое запросом, генерируется агрегатной функцией. Поскольку предложения group by в запросе нет, существует единственная неявная группа (все строки в таблице payment).

Однако в большинстве случаев требуется получить дополнительные столбцы вместе со столбцами, генерируемыми агрегатными функциями. Например, можно расширить предыдущий запрос не для всех клиентов одновременно, а для каждого клиента.

Для каждого такого запроса нужно вместе с пятью агрегатными функциями выполнить выборку customer_id:

RuntimeError: (pymysql.err.OperationalError) (1140, "In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'sakila.payment.customer_id'; this is incompatible with sql_mode=only_full_group_by")
[SQL: SELECT
    customer_id,
    MAX(amount) max_amt,
    MIN(amount) min_amt,
    AVG(amount) avg_amt,
    SUM(amount) tot_amt,
    COUNT(*) num_payments
FROM payment;]
(Background on this error at: https://sqlalche.me/e/20/e3q8)

Однако, мы получим сообщение об ошибке.

Этот запрос не выполняется, потому что в нем не указано явно как должны быть сгруппированы данные. Следовательно, в запрос нужно добавить предложение group by чтобы указать к какой группе строк следует применять агрегатные функции:

Loading...
Loading...

При включении предложения group by сервер понимает, что надо сначала сгруппировать строки, имеющие одинаковое значение в столбце customer_id, а затем применить к каждой из 599 групп агрегатные функции.

Подсчет различных значений

При использовании функции count() для определения количества членов в каждой группе у вас есть выбор: подсчитать все элементы группы или подсчитать только различные значения столбца среди всех элементов группы.

Рассмотрим пример, в котором функция count() и столбец customer_id используются двумя разными способами:

Loading...
Loading...

При указании ключевого слова distinct функция count() проверяет значения столбца для каждого члена группы, находя и удаляя дубликаты, а не просто подсчитывает количество значений в группе.

Использование выражений

Наряду с использованием столбцов в качестве аргументов агрегатных функций можно исопользовать и выражения.

Например, можно найти максимальное количество дней между моментом, когда фильм был взят напрокат и последующим его возвратом:

Loading...
Loading...

Функция datediff() используется для вычисления количества дней между датой возврата фильма и датой взятия его напрокат для каждой аренды фильма, а функция max() возвращает максимальное найденное значение.

Обработка значений null

При выполнении агрегации (на самом деле – любого вида числовых вычислений) всегда следует учитывать как на результат вычислений могут повлиять значения null.

Для понимания построим простую таблицу для хранения числовых данных и заполним ее множеством {1, 3, 5}:

Loading...

Рассмотрим запрос, выполняющий пять агрегатных функций:

Loading...
Loading...

Теперь добавим в таблицу значение NULL и снова выполним тот же запрос:

Loading...
Loading...
Loading...
Loading...

Функции sum(), max() и avg() возвращают те же значения – игнорируют любые встречающиеся NULL. Функция count(*) возвращает значение 4, поскольку таблица number_tbl сейчас содержит четыре строки. А функция count(val) по-прежнему возвращает значение 3.

Дело в том, что count(*) подсчитывает количество строк, тогда как count(val) подсчитывает количество значений в столбце val и игнорирует любые обнаруженные в нем значения NULL.

Генерация групп

Из этого раздела узнаем как группировать данные по одному или нескольким столбцам, как группировать данные с помощью выражений и как создавать сводки внутри группы.

Группировка по одному столбцу

Группы из одного столбца – самый простой и наиболее часто используемый тип группировки.

Если, например, хотим найти количество фильмов, связанных с каждым актером, нужна группировка по единственному столбцу film_actor.actor_id:

Loading...
Loading...

Этот запрос генерирует 200 групп, по одной для каждого актера, а затем суммирует количество фильмов для каждого участника группы.

Многостолбцовая группировка

В некоторых случаях может потребоваться создавать группы, охватывающие более одного столбца.

Расширяя предыдущий пример, представим, что для каждого актера хотим найти общее количество фильмов с разными рейтингами (G, PG, ...). Вот как этого добиться:

Loading...
Loading...

Эта версия запроса генерирует 996 групп, по одной для каждой комбинации “актер/рейтинг фильма”, полученной путем соединения таблицы film_actor с таблицей film.

Группировка с помощью выражений

Для группировки можно использовать не только данные столбцов, но и значения, генерируемые выражениями.

Рассмотрим запрос, который группирует прокат по годам:

Loading...
Loading...

В этом запросе применено простое выражение, которое использует функцию extract() чтобы вернуть из даты только год для соответствующей группировки строк в таблице rental.

Loading...
Loading...

Генерация итоговых данных

В разделе Многостолбцовая группировка приведен пример, в котором подсчитывается количество фильмов для каждой комбинации “актер/рейтинг фильма”. Допустим, вместе с общим количеством для каждой комбинации нужно получить и общее количество для каждого отдельного актера.

Можно выполнить дополнительный запрос и объединить результаты. Но лучше использовать конструкцию with rollup:

Loading...
Loading...

Теперь в результирующем наборе имеется 201 дополнительная строка по одной для каждого из 200 различных актеров и одна общая (для всех актеров вместе).

В столбце rating для итоговых значений для 200 актеров предоставляется значение NULL (в Pandas NaN), поскольку выполняется накопление по всем рейтингам.

Для строки общего итога (первая строка вывода) значение NULL (NaN) предоставлено как для столбца actor_id так и для столбца rating. Сумма в первой строке вывода равна 5462, что соответствует числу строк в таблице film_actor.

Loading...
Loading...

Если помимо итогов по актерам хотите подсчитать итоги по рейтингу, можно использовать конструкцию with cube, которая будет генерировать итоговые строки для всех возможных комбинаций столбцов группировки.

Однако конструкция with cube недоступна в MySQL версии 8.0.

Условия группового фильтра

В главе 4 Фильтрация мы познакомились с типами условий фильтрации и узнали как их использовать в предложении where. При группировке данных также можно применить фильтрующее условие к данным после того как были сгенерированы группы.

Типы условий фильтрации должны быть размещены в предложении having.

Loading...
Loading...

Этот запрос имеет два условия фильтрации: одно – в предложении where, которое отфильтровывает любые фильмы с рейтингом отличным от G или PG, и еще одно – в предложении having, которое отфильтровывает всех актеров, снявшихся менее чем в 10 фильмах.

Таким образом, один из фильтров действует на данные до* их группировки, а другой – после того, как группы были созданы.

Если поместите оба фильтра в предложение where, то получите сообщение об ошибке:

RuntimeError: (pymysql.err.ProgrammingError) (1111, 'Invalid use of group function')
[SQL: SELECT
    fa.actor_id,
    f.rating,
    COUNT(*)
FROM film_actor fa
    INNER JOIN film f ON fa.film_id = f.film_id
WHERE f.rating IN ('G', 'PG')
    AND COUNT(*) > 9
GROUP BY fa.actor_id, f.rating;]
(Background on this error at: https://sqlalche.me/e/20/f405)

Этот запрос не работает, потому что нельзя включать агрегатную функцию в предложение where. Это связано с тем, что фильтры в предложении where вычисляются до группировки, поэтому сервер еще не в состоянии выполнять какие-либо функции для групп.


Упражнения

Упражнение 8.1

Создайте запрос, который подсчитывает количество строк в таблице payment.

Loading...
Loading...

Упражнение 8.2

Измените запрос из упражнения 8.1 так, чтобы подсчитать количество платежей, произведенных каждым клиентом. Выведите идентификатор клиента и общую уплаченную сумму для каждого клиента.

Loading...
Loading...

Упражнение 8.3

Измените запрос из упражнения 8.2 включив в него только тех клиентов, у которых имеется не менее 40 выплат.

Loading...
Loading...

Вариация 8.3 для тренировки:

Loading...
Loading...