Группировка¶
Оператор GROUP BY объединяет кортежи с одинаковыми значениями в группы.
Затем к каждой группе можно применять агрегатные функции, например
COUNT() или SUM().
Сценарии использования¶
Используйте GROUP BY, когда нужно:
- посчитать количество кортежей по категориям
- найти сумму/среднее/максимум по группам
- получить статистику по определённым полям
См. также:
Примеры запросов¶
Создадим таблицу и добавим в неё данные, которые подходят для группировки:
CREATE TABLE customers (
id INTEGER NOT NULL,
name TEXT NOT NULL,
amount INTEGER,
PRIMARY KEY (id))
USING memtx DISTRIBUTED BY (id)
OPTION (TIMEOUT = 3.0);
INSERT INTO customers VALUES
(1, 'alice', 778),
(2, 'bob', 22),
(3, 'john', 62),
(4, 'bob', 990),
(5, 'alice', 24),
(6, 'bob', 22);
Использование без функций¶
Без агрегатных функций GROUP BY возвращает уникальные сочетания
значений. ORDER BY в примере ниже нужен только для фиксированного
порядка кортежей:
SELECT name, amount FROM customers
GROUP BY amount, name
ORDER BY amount, name;
Результат:
+-------+--------+
| name | amount |
+================+
| bob | 22 |
|-------+--------|
| alice | 24 |
|-------+--------|
| john | 62 |
|-------+--------|
| alice | 778 |
|-------+--------|
| bob | 990 |
+-------+--------+
(5 rows)
GROUP BY не заменяет ORDER BY, но меняет состав результата: два
кортежа ('bob', 22) превращаются в одну группу. Если нужен
предсказуемый порядок вывода, используйте ORDER BY.
В отличие от GROUP BY, агрегатная оконная
функция не сводит результат и не меняет
в нём количество строк. Если нужно вычислить агрегат по разделу,
но сохранить исходные кортежи, используйте выражение OVER (...).
Без ORDER BY порядок кортежей в результате запроса в Picodata не
гарантируется.
Группировка и фильтрация исходных строк¶
Сначала WHERE отбирает исходные кортежи, затем GROUP BY объединяет
оставшиеся кортежи в группы:
SELECT name, COUNT(*) AS count FROM customers
WHERE id != 3
GROUP BY name
ORDER BY name;
Запрос вернёт число кортежей для каждого имени:
+-------+-------+
| name | count |
+===============+
| alice | 2 |
|-------+-------|
| bob | 3 |
+-------+-------+
(2 rows)
Группировка и фильтрация итоговых групп¶
Для фильтрации уже сформированных групп используйте HAVING. В отличие
от WHERE, он применяется не к исходным кортежам, а к группам:
SELECT name, SUM(amount) AS total FROM customers
GROUP BY name
HAVING SUM(amount) > 100
ORDER BY name;
Результат:
+-------+-------+
| name | total |
+===============+
| alice | 802 |
|-------+-------|
| bob | 1034 |
+-------+-------+
(2 rows)
Правило простое: каждое неагрегированное выражение из SELECT должно
присутствовать в GROUP BY. Для агрегатов (COUNT, SUM и т.д.) это
не требуется.
Порядок обработки¶
Логический порядок обработки запроса с WHERE, GROUP BY, HAVING и
ORDER BY такой:
- Фильтрация исходных кортежей (
WHERE) - Группировка кортежей (
GROUP BY) - Фильтрация получившихся групп (
HAVING) - Сортировка итогового результата (
ORDER BY)
План исполнения запроса
EXPLAIN (RAW, FMT) SELECT name, amount FROM customers WHERE id !=3 GROUP BY amount,name HAVING sum(amount) > 100 ORDER BY name;
╭──────────────────────────╮
│ 1. Query (WHOLE STORAGE) │
╰──────────────────────────╯
SELECT
"customers"."amount" as "gr_expr_1",
"customers"."name" as "gr_expr_2",
sum (CAST ("customers"."amount" as int)) as "sum_1"
FROM
"customers"
WHERE
"customers"."id" <> CAST(3 AS int)
GROUP BY
"customers"."amount",
"customers"."name"
plan:
[0] SCAN TABLE customers (~983040 rows)
[0] USE TEMP B-TREE FOR GROUP BY
╭───────────────────╮
│ 2. Query (ROUTER) │
╰───────────────────╯
SELECT
"name",
"amount"
FROM
(
SELECT
"COL_1" as "name",
"COL_0" as "amount"
FROM
(
SELECT
"COL_0",
"COL_1",
"COL_2"
FROM
"_tmp_3176007081912592988_0136"
)
GROUP BY
"COL_0",
"COL_1"
HAVING
sum ("COL_2") > CAST(100 AS int)
)
ORDER BY
"name"
plan:
[0] SCAN TABLE _tmp_3176007081912592988_0136 (~1048576 rows)
[0] USE TEMP B-TREE FOR GROUP BY
[0] USE TEMP B-TREE FOR ORDER BY
См. также:
Перемещение данных¶
В распределённом запросе GROUP BY может потребоваться перемещение
данных: всё зависит от того, находятся ли нужные кортежи в одном или
нескольких бакетах.
Если планировщик доказывает, что запрос выполняется в пределах одного
бакета, перемещения данных не будет. Так бывает, когда WHERE
фиксирует ключ шардирования:
EXPLAIN (LOGICAL) SELECT id, SUM(amount) FROM customers WHERE id = 1 GROUP BY id;
Существенная часть плана
projection (customers.id::int -> id, sum(customers.amount::int::int)::decimal -> col_1)
group by (customers.id::int)
selection (customers.id::int = 1::int)
scan customers
См. также:
Важно не то, какая колонка указана в GROUP BY, а сколько бакетов
затрагивает запрос после фильтрации.
Если после фильтрации нужны данные из нескольких бакетов, используется один из вариантов перемещения данных:
- локальная вставка/материализация (данные в пределах одного узла)
- частичное/полное перемещение (данные расположены на разных узлах)
Один из таких вариантов — полное перемещение данных. Оно часто
появляется, когда группировка идёт по колонке, не входящей в ключ
распределения, а запрос не ограничен одним бакетом. В примере ниже
фильтр id <> 3 оставляет диапазон buckets = [1-16384], поэтому в
плане появляется motion [policy: full]:
EXPLAIN (LOGICAL) SELECT name, COUNT(*) AS count FROM customers
WHERE id <> 3
GROUP BY name;
Существенная часть плана
projection (gr_expr_1::string -> name, sum(count_1::int)::int -> count)
group by (gr_expr_1::string)
motion [policy: full, program: ReshardIfNeeded]
projection (customers.name::string -> gr_expr_1, count(*)::int -> count_1)
group by (customers.name::string)
selection (customers.id::int <> 3::int)
scan customers
См. также: