📘 Банк задач для собеседований по SQL
В этом разделе собраны практические задачи, которые часто встречаются на собеседованиях.
SQL-песочница
Снизу в редактор уже подставлен стартовый DDL для первой задачи. Выполни его, чтобы создать таблицу, а потом замени код своим запросом и смотри, что получается.
SQL Playground: задача про конкурентов
Сначала выполни DDL и INSERT, потом замени код своим решением. Кнопка сброса полностью очищает БД и редактор.
Ctrl+/ / Cmd+/. Выполнить: Ctrl+Enter / Cmd+EnterЗадача Конкуренты
Есть таблица competitors_prices со следующими данными:
- sku_id_competitor — идентификатор SKU конкурента
- competitor_id — идентификатор конкурента
- promo_price_competitor — промо-цена конкурента
- regular_price_competitor — регулярная цена конкурента
- parsing_date — дата парсинга
Нужно получить витрину, где для каждой комбинации (sku_id_competitor, competitor_id, parsing_date) остаётся только одно значение цены:
- если есть
promo_price_competitor→ берём её, - если промо отсутствует (
NULL) → берёмregular_price_competitor.
DDL и наполнение таблицы
CREATE TABLE IF NOT EXISTS public.competitors_prices (
sku_id_competitor INT,
competitor_id INT,
promo_price_competitor DECIMAL,
regular_price_competitor DECIMAL,
parsing_date DATE
);
INSERT INTO competitors_prices VALUES
(15, 200, 147.0, 174.0, DATE '2025-08-30'),
(16, 200, NULL, 222.0, DATE '2025-08-27'),
(15, 200, NULL , 174.0, DATE '2025-08-30');Показать результат
| sku_id_competitor | competitor_id | price | parsing_date |
|---|---|---|---|
| 15 | 200 | 147 | 2025-08-30 |
| 16 | 200 | 222 | 2025-08-27 |
Показать решение
WITH ranked AS (
SELECT
sku_id_competitor,
competitor_id,
promo_price_competitor,
regular_price_competitor,
parsing_date,
COALESCE(promo_price_competitor, regular_price_competitor) AS price,
ROW_NUMBER() OVER (
PARTITION BY sku_id_competitor, competitor_id, parsing_date
ORDER BY CASE WHEN promo_price_competitor IS NOT NULL THEN 1 ELSE 0 END DESC
) AS rn
FROM public.competitors_prices
)
SELECT
sku_id_competitor,
competitor_id,
price,
parsing_date
FROM ranked
WHERE rn = 1;Задача: Retail компания
Есть таблица движений товара на складе:
dttm- дата и время операцииproduct- товарamt- изменение остатка
Также есть стартовый остаток по каждому товару.
Нужно посчитать, сколько товара осталось на складе на конец каждого дня по формуле:
остаток на конец дня = стартовый остаток + все движения до конца этого дня
DDL и наполнение таблицы
DROP TABLE IF EXISTS stock_movements;
DROP TABLE IF EXISTS stock_balance;
CREATE TABLE stock_balance (
product TEXT,
start_balance INT
);
CREATE TABLE stock_movements (
dttm TIMESTAMP,
product TEXT,
amt INT
);
INSERT INTO stock_balance (product, start_balance) VALUES
('Бананы', 10),
('Апельсины', 20);
INSERT INTO stock_movements (dttm, product, amt) VALUES
('2022-05-16 12:01:15', 'Бананы', 5),
('2022-05-16 12:03:15', 'Апельсины', -5),
('2022-05-16 12:03:20', 'Апельсины', -4),
('2022-05-17 12:01:15', 'Бананы', 2),
('2022-05-17 12:03:15', 'Апельсины', -3),
('2022-05-18 12:01:15', 'Бананы', 3),
('2022-05-18 12:03:15', 'Апельсины', -1);Показать результат
| dt | product | stock_left |
|---|---|---|
| 2022-05-16 | Апельсины | 11 |
| 2022-05-16 | Бананы | 15 |
| 2022-05-17 | Апельсины | 8 |
| 2022-05-17 | Бананы | 17 |
| 2022-05-18 | Апельсины | 7 |
| 2022-05-18 | Бананы | 20 |
Показать решение
WITH movements AS (
SELECT
dttm::date AS dt,
dttm,
product,
SUM(amt) OVER (
PARTITION BY product
ORDER BY dttm
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS movement_total,
ROW_NUMBER() OVER (
PARTITION BY product, dttm::date
ORDER BY dttm DESC
) AS rn
FROM stock_movements
)
SELECT
m.dt,
m.product,
b.start_balance + m.movement_total AS stock_left
FROM movements AS m
JOIN stock_balance AS b
ON b.product = m.product
WHERE m.rn = 1
ORDER BY dt, product;Задача: Общие встречи пользователей
Есть таблица Встречи. В ней есть user_id и id встречи room_id. Надо найти все пары пользователей,
которые были на общих встречах более 1 раза и само кол-во общих встреч.
Представь, что два человека побыли на одной встрече, подружились,
а потом решили ходить вместе еще на другие встречи. Вот надо найти все такие пары.
Вывести user1, user2, shared_meetings (кол-во общих встреч)
user1user2- количество общих комнат
shared_meetings
DDL и наполнение таблицы
CREATE TABLE meetings (
user_id INT,
room_id INT
);
INSERT INTO meetings (user_id, room_id) VALUES
(1, 100),
(2, 100),
(1, 200),
(3, 200),
(2, 300),
(3, 300),
(1, 300),
(2, 400),
(3, 400),
(4, 400),
(1, 500),
(2, 500);Показать результат
| user1 | user2 | shared_meetings |
|---|---|---|
| 1 | 2 | 3 |
| 1 | 3 | 2 |
| 2 | 3 | 2 |
Показать решение
SELECT
m1.user_id AS user1,
m2.user_id AS user2,
COUNT(DISTINCT m1.room_id) AS shared_meetings
FROM meetings m1
JOIN meetings m2
ON m1.room_id = m2.room_id
AND m1.user_id < m2.user_id
GROUP BY m1.user_id, m2.user_id
HAVING COUNT(DISTINCT m1.room_id) > 1Задачи с собеседований МТС
1. Менеджеры, у которых в подчинении больше 5 сотрудников
Есть таблица:
Employee(id, name, department, managerId)Нужно написать SQL-запрос, который вернёт менеджеров, у которых в подчинении больше 5 сотрудников.
Показать решение
SELECT
managerId,
COUNT(*) AS employee_count
FROM Employee
WHERE managerId IS NOT NULL
GROUP BY managerId
HAVING COUNT(*) > 5;2. Курьеры, получившие премию за июнь 2021 года
У компании по доставке еды есть таблица заказов пеших курьеров:
date, courier_id, order_id, distance, travel_timeПример данных:
date courier_id order_id distance travel_time
2021-06-01 c1010 ord12 1.2 20
2021-06-01 c1010 ord13 0.5 10
2021-06-01 c1011 ord10 3.1 35
2021-06-02 c1011 ord116 1.4 30Таблица: orders
В конце каждого месяца компания выдаёт премию тем курьерам, у которых средняя скорость доставки выше средней скорости среди всех курьеров.
Нужно вывести курьеров, которые получили премию за июнь 2021 года.
Показать решение
WITH courier_speed AS (
SELECT
courier_id,
AVG(distance / travel_time) AS avg_speed
FROM orders
WHERE EXTRACT(YEAR FROM date) = 2021
AND EXTRACT(MONTH FROM date) = 6
GROUP BY courier_id
),
overall_speed AS (
SELECT
AVG(avg_speed) AS avg_speed_all
FROM courier_speed
)
SELECT
cs.courier_id,
cs.avg_speed
FROM courier_speed cs
CROSS JOIN overall_speed os
WHERE cs.avg_speed > os.avg_speed_all
ORDER BY cs.courier_id;Дополнительные задачи
Из интересного: спросили, что такое hint в БД.
Hint — это подсказка оптимизатору запроса (например, принудительно выбрать тип join, индекс, порядок доступа), когда нужно повлиять на план выполнения.
1) Топ-3 сотрудников по зарплате в каждом отделе
Есть таблица:
employees (
id INT PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(50),
salary DECIMAL(10,2),
fired_date DATE
)Задание: Для каждого отдела вывести трёх сотрудников с самой высокой зарплатой. Для каждого такого сотрудника также показать:
- его зарплату,
- среднюю зарплату по его отделу,
- разницу между его зарплатой и максимальной зарплатой в отделе.
Результат отсортировать по названию отдела и по убыванию зарплаты.
Показать решение
WITH ranked AS (
SELECT
name,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS avg_salary_per_department,
MAX(salary) OVER (PARTITION BY department) AS max_salary_per_department,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary DESC, id
) AS rn
FROM employees
WHERE fired_date IS NULL
)
SELECT
name,
department,
salary,
avg_salary_per_department,
salary - max_salary_per_department AS salary_max_diff
FROM ranked
WHERE rn <= 3
ORDER BY department, salary DESC, name;2) Количество двоек у учеников, у которых больше 10 пятёрок
Таблица:
student_marks (
name VARCHAR(100),
mark INT
)В таблице имена не повторяются между учениками (одно имя = один ученик). Нужно получить имя и количество двоек у тех учеников, у которых больше 10 пятёрок.
Показать решение
SELECT
name,
SUM(CASE WHEN mark = 2 THEN 1 ELSE 0 END) AS cnt_twos
FROM student_marks
GROUP BY name
HAVING SUM(CASE WHEN mark = 5 THEN 1 ELSE 0 END) > 10;3) Сотрудники с зарплатой выше, чем у руководителя
Атрибуты:
employee_idnamedepartmentid_master(employee_id руководителя)salary
Нужно выбрать имена сотрудников, получающих зарплату больше своих руководителей.
Показать решение
SELECT
e.name
FROM employees e
JOIN employees m
ON e.id_master = m.employee_id
WHERE e.salary > m.salary;4) Уровень сотрудника в иерархии
Есть таблица:
employees (
id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT REFERENCES employees(id) -- NULL для топ-менеджера
)Задание: Для каждого сотрудника вывести:
- его имя,
- уровень в иерархии (
0для топ-менеджера).
Отсортировать по уровню, затем по имени.
Показать решение
WITH RECURSIVE hierarchy AS (
SELECT
id,
name,
manager_id,
0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT
e.id,
e.name,
e.manager_id,
h.level + 1 AS level
FROM employees e
JOIN hierarchy h
ON e.manager_id = h.id
)
SELECT
name,
level
FROM hierarchy
ORDER BY level, name;Задача: Запрос с использованием SELECT
Есть таблица employees с полями:
- id
- name
- salary
- department_id
Нужно написать SQL-запрос, который возвращает имена сотрудников и их зарплаты для всех сотрудников с зарплатой выше средней по всей таблице.
Показать решение
SELECT
name,
salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);Задача: Оконные функции и подзапросы
В таблице employees напишите запрос, который возвращает:
- имя сотрудника
- зарплату
- ранг по зарплате внутри своего
department_id(нумерация по убыванию зарплаты).
Показать решение
SELECT
name,
salary,
department_id,
RANK() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS salary_rank
FROM employees;Задача Последняя загрузка по таблице за день
Есть таблица public.load_status со следующими данными: • dt — дата/время загрузки • table_name — имя таблицы/вьюхи • status — статус загрузки
Нужно построить выборку/витрину, где для каждой таблицы в рамках каждого календарного дня остаётся только одна строка — с максимальной dt (если в день было несколько загрузок). Отфильтровать последние 14 дней.
DROP TABLE IF EXISTS public.load_status;
CREATE TABLE public.load_status (
id bigserial PRIMARY KEY, -- уникальный ключ
dt timestamp with time zone NOT NULL, -- время загрузки
table_name text NOT NULL, -- имя таблицы/вьюхи
status text NOT NULL -- статус загрузки
);
INSERT INTO public.load_status (dt, table_name, status) VALUES
-- Таблица alpha — два прогона в один день
('2025-09-15 09:15:00+03', 'alpha.orders_daily', 'OK'),
('2025-09-15 11:45:00+03', 'alpha.orders_daily', 'OK'),
-- Таблица beta — три прогона в один день
('2025-09-15 07:20:00+03', 'beta.customer_snapshot', 'OK'),
('2025-09-15 09:55:00+03', 'beta.customer_snapshot', 'OK'),
('2025-09-15 14:10:00+03', 'beta.customer_snapshot', 'OK'),
-- Таблица gamma — один прогон сегодня, два вчера
('2025-09-15 08:30:00+03', 'gamma.product_catalog', 'OK'),
('2025-09-14 10:05:00+03', 'gamma.product_catalog', 'OK'),
('2025-09-14 16:25:00+03', 'gamma.product_catalog', 'OK'),
-- Таблица delta — разные даты
('2025-09-13 06:40:00+03', 'delta.pricing_rules', 'OK'),
('2025-09-14 07:55:00+03', 'delta.pricing_rules', 'OK'),
('2025-09-15 12:20:00+03', 'delta.pricing_rules', 'OK'),
-- Таблица epsilon — только один прогон
('2025-09-15 10:10:00+03', 'epsilon.stock_levels', 'OK');Показать результат
| dt | table_name | status | day |
|---|---|---|---|
| 2025-09-15 14:10:00+03 | beta.customer_snapshot | OK | 2025-09-15 |
| 2025-09-15 12:20:00+03 | delta.pricing_rules | OK | 2025-09-15 |
| 2025-09-15 11:45:00+03 | alpha.orders_daily | OK | 2025-09-15 |
| 2025-09-15 10:10:00+03 | epsilon.stock_levels | OK | 2025-09-15 |
| 2025-09-15 08:30:00+03 | gamma.product_catalog | OK | 2025-09-15 |
| 2025-09-14 16:25:00+03 | gamma.product_catalog | OK | 2025-09-14 |
| 2025-09-14 07:55:00+03 | delta.pricing_rules | OK | 2025-09-14 |
| 2025-09-13 06:40:00+03 | delta.pricing_rules | OK | 2025-09-13 |
Показать решение
WITH ranked AS (
SELECT
ls.dt,
ls.table_name,
ls.status,
ls.dt::date AS day,
ROW_NUMBER() OVER (
PARTITION BY ls.table_name, ls.dt::date
ORDER BY ls.dt DESC
) AS rn
FROM public.load_status AS ls
WHERE ls.dt >= current_date - INTERVAL '14 days'
)
SELECT
dt,
table_name,
status,
day
FROM ranked
WHERE rn = 1
ORDER BY dt DESC;Задача: Создание функции
Создайте функцию get_department_salary_summary(dept_id INT), которая принимает ID отдела и возвращает таблицу с:
- общим количеством сотрудников
- средней зарплатой в этом отделе.
Показать решение
CREATE OR REPLACE FUNCTION get_department_salary_summary(dept_id INT)
RETURNS TABLE (
employees_count INT,
avg_salary NUMERIC
)
AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(*) AS employees_count,
AVG(salary) AS avg_salary
FROM employees
WHERE department_id = dept_id;
END;
$$ LANGUAGE plpgsql;Задача: Поставщики в Лемана Про
Напишите запрос, который для всех поставщиков с типом (supplier_type) отличным от "WAREHOUSE" выдаст идентификатор поставщика и сумму заказов у этого поставщика. В запросе не использовать оконные функции.
order_id | store_id | item_id | supplier_id | price | quantity | dt
---------+----------+---------+-------------+-------+----------+------------------------
1 | 1 | 10001 | 1 | 20 | 1 | 2023-10-11 13:54:00.000
2 | 2 | 10001 | 2 | 30 | 2 | 2023-11-11 17:14:05.000
2 | 3 | 10002 | 2 | 100 | 3 | 2023-09-01 09:32:50.000
3 | 3 | 10002 | 3 | 130 | 1 | 2023-10-21 14:51:45.000
supplier_id | supplier_name | supplier_type
------------+---------------+--------------
1 | Supplier 1 | WAREHOUSE
2 | Supplier 2 | NULL
3 | Supplier 3 | SUPPLIER
4 | Supplier 4 | SUPPLIERПоказать решение
SELECT
o.supplier_id,
SUM(o.price * o.quantity) AS total_order_amount
FROM orders o
JOIN suppliers s
ON o.supplier_id = s.supplier_id
WHERE s.supplier_type <> 'WAREHOUSE'
OR s.supplier_type IS NULL
GROUP BY o.supplier_id;Задача: Яндекс Такси
Имеем таблицу RIDE с поездками пользователей на такси. Найти всех пользователей, которые совершали поездки два (или более) дня подряд.
| ride_id | user_id | tariff | dt | cost |
|--------:|--------:|---------|------------|-----:|
| 2305001 | 1001 | econom | 2023-05-02 | 340 |
| 2305002 | 1001 | comfort | 2023-05-03 | 500 |
| 2305003 | 2001 | econom | 2023-05-01 | 120 | 2023-05-01
| 2305004 | 2001 | econom | 2023-05-01 | 150 |
| 2305007 | 2001 | econom | 2023-05-02 | 999 |
| 2305097 | 2001 | econom | 2023-05-03 | 999 |
Показать решение
SELECT DISTINCT r1.user_id
FROM ride r1
JOIN ride r2
ON r1.user_id = r2.user_id
AND r2.dt = r1.dt + INTERVAL '1 day';Задача: Оптимизация JOIN больших таблиц
Вам необходимо выполнить запрос:
SELECT
o.id,
o.created_at,
SUM(oi.price * oi.quantity)
FROM orders o
JOIN order_items oi
ON o.id = oi.order_id
WHERE o.created_at >= DATE '2024-01-01'
GROUP BY o.id, o.created_at;Задание:
Предложите стратегию индексации для обеих таблиц, чтобы СУБД выполнила соединение через Nested Loop Join или Merge Join без полного сканирования (Full Scan) таблицы order_items.
Показать решение
-- Индекс для быстрого отбора заказов по дате
CREATE INDEX idx_orders_created_at_id
ON orders (created_at, id);
-- Индекс для быстрого поиска позиций заказа по ключу соединения
CREATE INDEX idx_order_items_order_id_inc
ON order_items (order_id)
INCLUDE (price, quantity);Почему это работает:
orders(created_at, id)позволяет быстро отфильтровать только нужные строки изorders;- после отбора заказов оптимизатор может идти в
order_itemsточечно поorder_id, а не читать всю таблицу; - индекс
order_items(order_id)подходит дляNested Loop Join, когда для каждой строки изordersищутся связанные строки вorder_items; - если оптимизатор выберет
Merge Join, наличие индексов по ключам соединения уменьшает стоимость сортировки и чтения.
Практический вывод:
- для этого запроса самый полезный индекс на
ordersэто(created_at, id), потому что сначала идёт фильтр по дате; - для
order_itemsдостаточно индекса по(order_id), аINCLUDE (price, quantity)в PostgreSQL помогает сделать чтение дешевле.
Вариант индексации с акцентом на Merge Join:
CREATE INDEX idx_orders_id
ON orders (id);
CREATE INDEX idx_order_items_order_id
ON order_items (order_id);Но без индекса по `created_at` таблицу `orders` всё равно может быть дорого фильтровать, поэтому в реальной задаче лучше оставить индекс `(created_at, id)`.Задача: CPI по инсталлам
Дана таблица в PostgreSQL с количеством установок приложения на каждый день и затратами на маркетинг:
CREATE TABLE user_installs (
date_ DATE PRIMARY KEY,
num_of_installs BIGINT,
ua_costs_usd BIGINT
);1. CPI за неделю
Нужно написать запрос, который выведет среднюю стоимость одного инсталла (CPI) за неделю с 2025-01-01 по 2025-01-07.
Показать решение
SELECT
SUM(ua_costs_usd)::numeric / NULLIF(SUM(num_of_installs), 0) AS cpi
FROM user_installs
WHERE date_ BETWEEN DATE '2025-01-01' AND DATE '2025-01-07';2. CPI последних 7 дней на каждую календарную дату
Нужно изменить запрос так, чтобы на каждый календарный день за весь 2025 год, включая даты, которых нет в user_installs, отображалась CPI последних 7 календарных дней.
Показать решение
WITH calendar AS (
SELECT generate_series(
DATE '2025-01-01',
DATE '2025-12-31',
INTERVAL '1 day'
)::date AS date_
),
installs_by_day AS (
SELECT
c.date_,
COALESCE(ui.num_of_installs, 0) AS num_of_installs,
COALESCE(ui.ua_costs_usd, 0) AS ua_costs_usd
FROM calendar c
LEFT JOIN user_installs ui
ON ui.date_ = c.date_
)
SELECT
date_,
SUM(ua_costs_usd) OVER (
ORDER BY date_
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)::numeric
/
NULLIF(
SUM(num_of_installs) OVER (
ORDER BY date_
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
),
0
) AS cpi_7d
FROM installs_by_day
ORDER BY date_;Задача: Клиенты без заказов за последние 90 дней
Нужно вернуть клиентов, у которых не было ни одного заказа за последние 90 дней.
CREATE TABLE customers (
customer_id BIGINT,
created_at TIMESTAMP
);
CREATE TABLE orders (
order_id BIGINT,
customer_id BIGINT,
order_dttm TIMESTAMP,
amount NUMERIC(18,2)
);Показать решение
SELECT
c.customer_id
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_dttm >= NOW() - INTERVAL '90 days'
AND o.order_dttm < NOW()
);Задача: Оставить только актуальную версию заказа
Известно, что по одному order_id может приходить несколько версий записи.
Нужно оставить только одну, самую актуальную по правилам:
- берём запись с максимальным
updated_at - если
updated_atодинаковый, приоритет уsource_system = 'core' - если и это одинаково, берём запись с максимальным
order_dttm
Нужно вернуть итоговый набор без дублей.
CREATE TABLE raw_orders (
order_id BIGINT,
user_id BIGINT,
order_dttm TIMESTAMP,
amount NUMERIC(18,2),
status TEXT,
updated_at TIMESTAMP,
source_system TEXT
);Показать решение
WITH ranked AS (
SELECT
ro.*,
ROW_NUMBER() OVER (
PARTITION BY ro.order_id
ORDER BY
ro.updated_at DESC,
CASE WHEN ro.source_system = 'core' THEN 0 ELSE 1 END,
ro.order_dttm DESC
) AS rn
FROM raw_orders ro
)
SELECT
order_id,
user_id,
order_dttm,
amount,
status,
updated_at,
source_system
FROM ranked
WHERE rn = 1;Задача: Оптимизация большой выборки в Greenplum
Есть запрос:
-- факт (очень большая)
fact_events (
event_id BIGINT,
user_id BIGINT,
event_dttm TIMESTAMP,
event_type TEXT,
amount NUMERIC
)
DISTRIBUTED BY (event_id);
-- пользователи
dim_users (
user_id BIGINT,
country TEXT,
is_active BOOLEAN
)
DISTRIBUTED RANDOMLY;
-- календарь
dim_dates (
dt DATE
)
DISTRIBUTED RANDOMLY;
SELECT
d.dt,
u.country,
COUNT(*) AS events_cnt,
SUM(f.amount) AS total_amount
FROM fact_events f
JOIN dim_users u
ON f.user_id = u.user_id
JOIN dim_dates d
ON DATE_TRUNC('day', f.event_dttm) = d.dt
WHERE u.is_active = true
AND d.dt BETWEEN DATE '2026-01-01' AND DATE '2026-01-31'
GROUP BY d.dt, u.country;Нужно предложить более эффективный вариант.
Показать решение
CREATE TEMP TABLE t_fact_events
DISTRIBUTED BY (user_id)
AS
SELECT
user_id,
event_id,
event_dttm::date AS dt,
amount
FROM fact_events
WHERE event_dttm >= TIMESTAMP '2026-01-01 00:00:00'
AND event_dttm < TIMESTAMP '2026-02-01 00:00:00';
CREATE TEMP TABLE t_dim_users
DISTRIBUTED BY (user_id)
AS
SELECT
user_id,
country
FROM dim_users
WHERE is_active = true;
SELECT
f.dt,
u.country,
COUNT(*) AS events_cnt,
SUM(f.amount) AS total_amount
FROM t_fact_events f
JOIN t_dim_users u
ON f.user_id = u.user_id
GROUP BY f.dt, u.country;Почему это лучше:
- фильтрация по дате применяется сразу к большой факт-таблице;
DATE_TRUNCубран из условияJOIN, вместо этого дата вычисляется заранее;- обе временные таблицы распределены по
user_id, поэтому join становится colocation join без лишнего motion; dim_datesздесь не нужна, так как диапазон дат уже задаётся фильтром.
Задача: Средний чек
Даны таблицы магазинов и чеков.Средний чек покупателя за последнюю неделю его активности
CREATE TABLE shop (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
city VARCHAR(255) NOT NULL
);
CREATE TABLE cheque (
uid UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMP NOT NULL DEFAULT now(),
"sum" DECIMAL(10, 2) NOT NULL,
shop_id INT NOT NULL,
customer_id BIGINT NOT NULL,
PRIMARY KEY (uid),
FOREIGN KEY (shop_id) REFERENCES shop (id)
);Показать решение
WITH last_purchase AS (
SELECT
customer_id,
MAX(created_at) AS last_purchase_at
FROM cheque
GROUP BY customer_id
),
weekly_avg AS (
SELECT
c.customer_id,
AVG(c."sum") AS avg_sum,
lp.last_purchase_at
FROM cheque c
JOIN last_purchase lp
ON lp.customer_id = c.customer_id
WHERE c.created_at >= lp.last_purchase_at - INTERVAL '7 days'
AND c.created_at <= lp.last_purchase_at
GROUP BY c.customer_id, lp.last_purchase_at
)
SELECT
customer_id,
avg_sum,
last_purchase_at::date AS last_purchase_date
FROM weekly_avg
ORDER BY customer_id;