Пошук уроків, статей та іншого контенту
Поєднаєте JOIN із WHERE, GROUP BY, HAVING і агрегатними функціями для формування звітів.
JOIN об’єднує рядки з кількох таблиць, а агрегатні функції узагальнюють отриманий набір:
COUNT() — кількість рядків;
SUM() — суму значень;
AVG() — середнє значення;
MIN() — мінімальне значення;
MAX() — максимальне значення.
Разом вони дають змогу формувати звіти: виручку за клієнтами, кількість замовлень за регіонами, середню вартість замовлення тощо.
Розглянемо таблиці клієнтів, замовлень і позицій замовлень:
CREATE TEMP TABLE customers (
id integer PRIMARY KEY,
name text NOT NULL,
region text NOT NULL,
is_active boolean NOT NULL
);
CREATE TEMP TABLE orders (
id integer PRIMARY KEY,
customer_id integer NOT NULL REFERENCES customers(id),
status text NOT NULL,
created_at date NOT NULL
);
CREATE TEMP TABLE order_items (
id integer PRIMARY KEY,
order_id integer NOT NULL REFERENCES orders(id),
product_name text NOT NULL,
quantity integer NOT NULL,
unit_price numeric(10, 2) NOT NULL
);
INSERT INTO customers (id, name, region, is_active)
VALUES
(1, 'Анна', 'Львів', true),
(2, 'Богдан', 'Київ', true),
(3, 'Олена', 'Львів', false),
(4, 'Дмитро', 'Одеса', true);
INSERT INTO orders (id, customer_id, status, created_at)
VALUES
(101, 1, 'paid', DATE '2026-01-10'),
(102, 1, 'cancelled', DATE '2026-01-15'),
(103, 2, 'paid', DATE '2026-01-20'),
(104, 3, 'paid', DATE '2026-01-25');
INSERT INTO order_items (
id,
order_id,
product_name,
quantity,
unit_price
)
VALUES
(1, 101, 'Клавіатура', 2, 700.00),
(2, 101, 'Миша', 1, 300.00),
(3, 102, 'Монітор', 1, 8000.00),
(4, 103, 'Навушники', 1, 900.00),
(5, 104, 'Монітор', 3, 500.00);Для запиту з JOIN, WHERE, GROUP BY і HAVING важливо розуміти логічний порядок обробки:
FROM і JOIN формують набір рядків.
WHERE відкидає окремі рядки.
GROUP BY об’єднує рядки в групи.
Агрегатні функції обчислюють значення для кожної групи.
HAVING відкидає цілі групи.
SELECT формує результат.
ORDER BY сортує результат.
Це логічний порядок, а не обов’язково порядок внутрішнього виконання оптимізатором PostgreSQL.
Припустімо, потрібно отримати звіт про активних клієнтів зі Львова та лише оплачені замовлення, створені у 2026 році.
SELECT
c.id AS customer_id,
c.name,
COUNT(DISTINCT o.id) AS orders_count,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.id
JOIN order_items AS oi
ON oi.order_id = o.id
WHERE c.region = 'Львів'
AND c.is_active = true
AND o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
GROUP BY
c.id,
c.name;Результат міститиме дані лише про клієнта Анна:
замовлень: 1;
виручка: 1700.00.
Умова WHERE застосовується до групування, тому вона відбирає окремі рядки замовлень і позицій до того, як починається підрахунок.
Вартість кожної позиції обчислюється так:
oi.quantity * oi.unit_priceЗагальна виручка клієнта обчислюється як:
SUM(oi.quantity * oi.unit_price)Оскільки одне замовлення може містити кілька позицій, для підрахунку замовлень потрібно використовувати:
COUNT(DISTINCT o.id)Якщо використати COUNT(*), буде пораховано позиції замовлень, а не самі замовлення.
Після об’єднання таблиць один клієнт може мати багато рядків:
один рядок на кожну позицію кожного замовлення;
кілька рядків можуть належати одному замовленню;
кілька замовлень можуть належати одному клієнту.
GROUP BY об’єднує ці рядки в групи.
SELECT
c.region,
COUNT(DISTINCT o.id) AS orders_count,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.id
JOIN order_items AS oi
ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY c.region;Тут кожна група відповідає одному регіону. PostgreSQL окремо підраховує кількість замовлень і загальну виручку для кожного регіону.
У SELECT можна вказувати:
стовпці, перелічені в GROUP BY;
агрегатні вирази.
Наприклад, цей запит некоректний:
SELECT
c.region,
c.name,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.id
JOIN order_items AS oi
ON oi.order_id = o.id
GROUP BY c.region;c.name не входить до GROUP BY і не є агрегатним виразом. PostgreSQL не знає, яке саме ім’я вибрати для групи з кількома клієнтами.
Правильний варіант — додати c.name до групування:
GROUP BY c.region, c.nameWHERE фільтрує окремі рядки, а HAVING — уже сформовані групи.
Наприклад, потрібно показати лише активних клієнтів зі Львова, чия виручка становить щонайменше 1000:
SELECT
c.id AS customer_id,
c.name,
COUNT(DISTINCT o.id) AS orders_count,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.id
JOIN order_items AS oi
ON oi.order_id = o.id
WHERE c.region = 'Львів'
AND c.is_active = true
AND o.status = 'paid'
GROUP BY
c.id,
c.name
HAVING SUM(oi.quantity * oi.unit_price) >= 1000
ORDER BY revenue DESC;У цьому запиті:
WHERE залишає лише потрібні замовлення та клієнтів;
GROUP BY створює групу для кожного клієнта;
SUM() обчислює виручку кожного клієнта;
HAVING залишає тільки групи з виручкою від 1000.
Псевдонім revenue зазвичай не можна використовувати в WHERE, оскільки WHERE обробляється до формування SELECT. Для переносимості також краще не покладатися на псевдонім у HAVING, а повторити агрегатний вираз:
HAVING SUM(oi.quantity * oi.unit_price) >= 1000INNER JOIN повертає лише рядки, для яких знайдено відповідність. LEFT JOIN зберігає всі рядки лівої таблиці, навіть якщо відповідності праворуч немає.
Потрібно показати всіх клієнтів, включно з тими, у кого немає оплачених замовлень у потрібний період:
SELECT
c.id AS customer_id,
c.name,
COUNT(DISTINCT o.id) AS paid_orders_count,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS revenue
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
AND o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
LEFT JOIN order_items AS oi
ON oi.order_id = o.id
GROUP BY
c.id,
c.name
ORDER BY revenue DESC;У результаті буде і клієнт Дмитро, хоча в нього немає відповідного замовлення:
paid_orders_count дорівнює 0;
revenue дорівнює 0.
Умова про замовлення розміщена в ON, тому вона визначає, які рядки приєднувати, але не вилучає клієнтів.
Такий запит виглядає схоже:
SELECT
c.id,
c.name,
COUNT(DISTINCT o.id) AS paid_orders_count
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
WHERE o.status = 'paid'
GROUP BY
c.id,
c.name;Для клієнта без замовлення o.status має значення NULL. Умова o.status = 'paid' не виконується, тому такий клієнт буде вилучений.
Фактично LEFT JOIN у цьому випадку починає поводитися як INNER JOIN.
Якщо потрібно зберегти рядки без відповідності, умову для правої таблиці слід розміщувати в ON:
LEFT JOIN orders AS o
ON o.customer_id = c.id
AND o.status = 'paid'Для групи без позицій:
SUM(oi.quantity * oi.unit_price)може повернути NULL, а не 0.
Щоб замінити NULL на нуль, використовуйте COALESCE:
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS revenueФункція COALESCE повертає перше значення, яке не є NULL.
Для кількості зазвичай зручно використовувати COUNT(DISTINCT o.id): якщо відповідних замовлень немає, результатом буде 0.
Наступний запит поєднує фільтрацію, групування, кілька агрегатів і фільтр для груп:
SELECT
c.id AS customer_id,
c.name,
COUNT(DISTINCT o.id) AS orders_count,
SUM(oi.quantity * oi.unit_price) AS revenue,
AVG(oi.quantity * oi.unit_price) AS average_item_value
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.id
JOIN order_items AS oi
ON oi.order_id = o.id
WHERE c.is_active = true
AND o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
GROUP BY
c.id,
c.name
HAVING COUNT(DISTINCT o.id) >= 1
AND SUM(oi.quantity * oi.unit_price) >= 1000
ORDER BY
revenue DESC,
c.name;Цей запит:
приєднує замовлення до клієнтів;
приєднує позиції до замовлень;
залишає активних клієнтів і оплачені замовлення;
групує дані за клієнтом;
рахує замовлення, виручку та середню вартість позиції;
залишає тільки групи з потрібною кількістю замовлень і виручкою;
сортує клієнтів за виручкою.
Некоректно:
WHERE SUM(oi.quantity * oi.unit_price) >= 1000WHERE не може фільтрувати результат агрегування. Для цього використовуйте HAVING:
HAVING SUM(oi.quantity * oi.unit_price) >= 1000Якщо замовлення має три позиції, COUNT(*) може порахувати його як три рядки.
Для кількості замовлень використовуйте:
COUNT(DISTINCT o.id)Умова для правої таблиці в WHERE вилучає рядки без відповідності. Якщо такі рядки потрібно зберегти, перенесіть умову в ON.
Кожен стовпець у SELECT, який не є аргументом агрегатної функції, має бути в GROUP BY.
Для звітів, де відсутні значення мають відображатися як нуль, використовуйте:
COALESCE(SUM(expression), 0)JOIN спочатку формує набір пов’язаних рядків.
WHERE фільтрує окремі рядки до групування.
GROUP BY об’єднує рядки в групи.
Агрегатні функції обчислюють значення для кожної групи.
HAVING фільтрує групи за агрегатними результатами.
Для підрахунку сутностей після JOIN часто потрібен COUNT(DISTINCT ...).
У LEFT JOIN умови для правої таблиці слід уважно розміщувати в ON або WHERE.
COALESCE допомагає замінити NULL на зручне для звіту значення, наприклад 0.