Documentation
📚 Вопросы с собеседований
SQL задачи

📘 Банк задач для собеседований по SQL

В этом разделе собраны практические задачи, которые часто встречаются на собеседованиях.

SQL-песочница

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

SQL Playground: задача про конкурентов

Сначала выполни DDL и INSERT, потом замени код своим решением. Кнопка сброса полностью очищает БД и редактор.

Инициализация...
Что можно пробовать: Сначала выполни DDL/INSERT, затем пиши SELECT, CTE, COALESCE, ROW_NUMBER и проверяй результат.. Комментарий: 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_competitorcompetitor_idpriceparsing_date
152001472025-08-30
162002222025-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);
Показать результат
dtproductstock_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 (кол-во общих встреч)

  • user1
  • user2
  • количество общих комнат 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);
Показать результат
user1user2shared_meetings
123
132
232
Показать решение
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_id
  • name
  • department
  • id_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');
Показать результат
dttable_namestatusday
2025-09-15 14:10:00+03beta.customer_snapshotOK2025-09-15
2025-09-15 12:20:00+03delta.pricing_rulesOK2025-09-15
2025-09-15 11:45:00+03alpha.orders_dailyOK2025-09-15
2025-09-15 10:10:00+03epsilon.stock_levelsOK2025-09-15
2025-09-15 08:30:00+03gamma.product_catalogOK2025-09-15
2025-09-14 16:25:00+03gamma.product_catalogOK2025-09-14
2025-09-14 07:55:00+03delta.pricing_rulesOK2025-09-14
2025-09-13 06:40:00+03delta.pricing_rulesOK2025-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;