add postgresql homework
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY, /* сделал первичные ключи в каждой таблице BIGINT, кол-во
|
||||
записей может быть большим */
|
||||
name VARCHAR(100) NOT NULL,
|
||||
age SMALLINT, -- Тип данных INT посчитал избыточным
|
||||
email VARCHAR(100),
|
||||
country VARCHAR(2), -- Идеально для ISO - кода страны
|
||||
is_active BOOLEAN,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE products (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(50),
|
||||
price DECIMAL(10,2) NOT NULL, -- Сделал тип данных для денежных операций
|
||||
in_stock BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE orders (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
order_date TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20), -- Читаемый статус нагляднее, не нужен справочник по кодам
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE order_items (
|
||||
order_id BIGINT,
|
||||
product_id BIGINT,
|
||||
quantity INT NOT NULL, /* Предположил, что типа данных SMALL INT возможно не
|
||||
хватит для обозначения количества */
|
||||
price_at_purchase DECIMAL(10,2) NOT NULL,
|
||||
|
||||
PRIMARY KEY(order_id, product_id),
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);
|
||||
|
||||
INSERT INTO users (id, name, age, email, country, is_active, created_at)
|
||||
VALUES
|
||||
(1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'),
|
||||
(2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'),
|
||||
(3, 'Carol', 27, 'carol@example.com', 'US', FALSE, '2024-03-01 15:20:00'),
|
||||
(4, 'Diana', 22, 'diana@GMAIL.com', 'US', TRUE, '2024-01-12 08:15:00'),
|
||||
(5, 'Evan', 35, 'EVAN@example.com', 'UK', TRUE, '2024-01-18 19:40:00'),
|
||||
(6, 'Farida', 29, 'farida@example.com', 'KZ', TRUE, '2024-02-20 11:05:00'),
|
||||
(7, 'George', 31, 'george+test@example.com', 'US', TRUE, '2024-03-05 16:45:00'),
|
||||
(8, 'Helen', 24, NULL, 'UK', FALSE, '2024-03-06 08:20:00'),
|
||||
(9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'),
|
||||
(10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00');
|
||||
|
||||
INSERT INTO products (id, name, category, price, in_stock)
|
||||
VALUES
|
||||
(1, 'iPhone 15', 'Электроника', 80000.00, TRUE),
|
||||
(2, 'Kindle', 'Электроника', 15000.00, TRUE),
|
||||
(3, 'Кроссовки Nike', 'Одежда', 9000.00, TRUE),
|
||||
(4, 'Кроссовки NoName', 'Одежда', 3000.00, FALSE),
|
||||
(5, 'Наушники Pro', 'Электроника', 12000.00, TRUE),
|
||||
(6, 'Фитнес-браслет', 'Гаджеты', 6000.00, TRUE),
|
||||
(7, 'Подарочная карта', 'Сервисы', 1000.00, TRUE);
|
||||
|
||||
INSERT INTO orders (id, user_id, order_date, status)
|
||||
VALUES
|
||||
(1, 1, '2025-01-10 12:00:00', 'paid'),
|
||||
(2, 3, '2025-01-12 09:30:00', 'cancelled'),
|
||||
(3, 1, '2025-01-15 18:20:00', 'paid'),
|
||||
(4, 2, '2025-01-20 11:05:00', 'created'),
|
||||
(5, 4, '2025-02-01 17:45:00', 'paid'),
|
||||
(6, 5, '2025-02-10 10:10:00', 'refunded'),
|
||||
(7, 7, '2025-03-01 09:00:00', 'paid'),
|
||||
(8, 9, '2025-03-05 19:30:00', 'paid');
|
||||
|
||||
INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase)
|
||||
VALUES
|
||||
(1, 1, 1, 78000.00),
|
||||
(1, 3, 2, 8500.00),
|
||||
(2, 2, 1, 14000.00),
|
||||
(3, 5, 1, 11000.00),
|
||||
(3, 7, 1, 1000.00),
|
||||
(4, 2, 1, 15000.00),
|
||||
(5, 6, 1, 6000.00),
|
||||
(6, 1, 1, 82000.00),
|
||||
(7, 3, 1, 9000.00),
|
||||
(8, 4, 2, 2800.00);
|
||||
@@ -0,0 +1,69 @@
|
||||
-- Задача 1 — Товары для продажи
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price,
|
||||
(price * 1.3) price_with_markup,
|
||||
(price * 1.5) price_with_vat
|
||||
FROM dmezentsev.products p
|
||||
WHERE in_stock = TRUE
|
||||
AND category != '%smartwatch%'
|
||||
AND price BETWEEN 100 AND 1300;
|
||||
|
||||
/* - Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему?
|
||||
|
||||
По мне, так BETWEEN короче и лаконичнее. Разве что с датами надо быть осторожнее :)
|
||||
*/
|
||||
|
||||
-- Задача 2 — Скидочная корзина: дешёвые или дорогие
|
||||
|
||||
SELECT name,
|
||||
price,
|
||||
(price * 0.85) price_with_discount
|
||||
FROM dmezentsev.products p
|
||||
WHERE (price < 100) OR (price > 1000);
|
||||
|
||||
/* - Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно?
|
||||
|
||||
Если в условии есть и OR и AND — скобки обязательны. Можно ошибиться с логикой выполнения,
|
||||
так как приоритет AND выше OR. Лучше перестраховаться
|
||||
и ставить скобки везде */
|
||||
|
||||
-- Задача 3 — Категории для рекламы
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM dmezentsev.products p
|
||||
WHERE category IN ('tablet', 'smartphone', 'mouse')
|
||||
AND price >= 100
|
||||
ORDER BY price DESC;
|
||||
|
||||
-- Задача 4 — Убрать iPhone из выборки
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dmezentsev.products p
|
||||
WHERE name NOT LIKE '%iPhone%';
|
||||
|
||||
-- Задача 5 — Поиск товаров по названию
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dmezentsev.products p
|
||||
WHERE name ILIKE '%pro%';
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dmezentsev.products p
|
||||
WHERE name ILIKE '%mx%';
|
||||
|
||||
/* - В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'.
|
||||
|
||||
Потому что = 'Pro' найдёт только точное совпадение с учётом регистра,
|
||||
а ILIKE '%pro%' найдёт любые вхождения "pro" в любом регистре
|
||||
*/
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Задача 1. Давность регистрации пользователей
|
||||
|
||||
SELECT name,
|
||||
CAST(created_at AS DATE) AS created_date,
|
||||
CURRENT_DATE - created_at::DATE AS days_since_registration,
|
||||
is_active
|
||||
FROM dmezentsev.users
|
||||
WHERE CURRENT_DATE - created_at::DATE > 180
|
||||
ORDER BY CURRENT_DATE - created_at::DATE DESC;
|
||||
|
||||
/* - Как бы ты использовал такое поле в реальном продукте
|
||||
(что можно делать с очень «старыми» пользователями)?
|
||||
|
||||
В реальности можно давать какие-то бонусы самым лояльным клиентам. Ну или
|
||||
наоборот, удалить их, если зарегистрированы давно, а активности никакой
|
||||
|
||||
- Почему удобнее сразу вычитать created_at::date, а не created_at как timestamp?
|
||||
|
||||
Если вычитать ::date, то получается число, удобно сравнивать, применять при
|
||||
фильтрации, в арифметических выражениях. Если timestamp, то интервал
|
||||
*/
|
||||
|
||||
-- Задача 2. Нормализация email и доменов
|
||||
|
||||
SELECT name,
|
||||
LOWER(email) AS email_clean,
|
||||
SUBSTRING(LOWER(email) FROM POSITION('@' IN email) + 1) AS domain
|
||||
FROM dmezentsev.users
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY SUBSTRING(email FROM POSITION('@' IN email) + 1), name;
|
||||
|
||||
/* - Зачем в реальных системах приводят email к нижнему регистру и выделяют домен отдельно?
|
||||
|
||||
email - адреса приводят к нижнему регистру, потому что в БД один и тот же
|
||||
почтовый ящик в разном регистре будет считаться двумя почтовыми ящиками, что
|
||||
приведет к дублям. Так же если пользователь введет свой почтовый ящик не в таком
|
||||
регистре, в котором ящик сохранен в БД, то он не сможет залогиниться
|
||||
Домен выделяют отдельно для анализа, маркетинга. Так же ради безопасности,
|
||||
чтобы блочить подозрительные домены
|
||||
*/
|
||||
|
||||
-- Задача 3. Сегменты по возрасту
|
||||
|
||||
SELECT name,
|
||||
age,
|
||||
CASE
|
||||
WHEN age < 25 THEN 'young'
|
||||
WHEN age BETWEEN 25 AND 35 THEN 'adult'
|
||||
ELSE 'senior'
|
||||
END AS age_group
|
||||
FROM dmezentsev.users
|
||||
ORDER BY age;
|
||||
|
||||
/* - Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения?
|
||||
|
||||
Вот тут точно не знаю, но мне кажется, что сегментация в SQL вернет результат.
|
||||
А если писать это в коде приложения, то выгрузка всех строк выльется в большой
|
||||
объем данных, которые нужно будет еще и обработать
|
||||
|
||||
- Что произойдёт, если в CASE забыть ветку ELSE
|
||||
(что получат пользователи, не попавшие ни в одно условие)?
|
||||
|
||||
Если забыть ELSE, то пользователи, которые не попадут ни под одно из условий,
|
||||
получат NULL
|
||||
*/
|
||||
|
||||
-- Задача 4. «Горячие» пользователи по дате и активности
|
||||
|
||||
SELECT
|
||||
name,
|
||||
created_at::DATE AS created_at_date,
|
||||
CURRENT_DATE - created_at::DATE AS days_since_registration,
|
||||
CASE
|
||||
WHEN is_active AND (CURRENT_DATE - created_at::DATE <= 30) THEN 'hot'
|
||||
ELSE 'regular'
|
||||
END AS hot_flag
|
||||
FROM dmezentsev.users
|
||||
WHERE is_active AND (CURRENT_DATE - created_at::DATE <= 30);
|
||||
|
||||
/* - Чем отличается фильтрация по дате в WHERE от простого CASE с меткой 'hot',
|
||||
но без фильтрации?
|
||||
|
||||
С фильтрацией в WHERE получим горячих с датой регистрации не больше 30 дней.
|
||||
Если без WHERE, то получим всех пользователей (и горячих, и обычных)
|
||||
|
||||
- Какие типы рассылок/кампаний логично запускать только на «горячих»?
|
||||
|
||||
Предположу, что горячим можно рассылать специальные предложения, чтобы
|
||||
дополнительно завлечь. Так же можно сделать рассылку по онбордингу, чтобы
|
||||
легче вкатиться в фичи сервиса
|
||||
*/
|
||||
|
||||
-- Задача 5. Качество данных по email
|
||||
|
||||
SELECT COUNT(*) AS total_users,
|
||||
COUNT(email) AS with_email,
|
||||
COUNT(*) FILTER (WHERE email IS NULL) AS without_email,
|
||||
COUNT(*) FILTER (WHERE email IS NULL)::NUMERIC
|
||||
/ COUNT(*) * 100 AS without_email_percent
|
||||
FROM dmezentsev.users;
|
||||
|
||||
/* - Почему для проверки «долей» и «заполненности» полей
|
||||
лучше использовать агрегаты в SQL, а не просто «на глаз» смотреть на таблицу?
|
||||
|
||||
Агрегаты гораздо легче воспринимать на глаз, чем смотреть всю таблицу.
|
||||
По сути, это уже готовый ответ на поставленный вопрос. Другое дело
|
||||
пытаться понять по всем данным, что вообще происходит.
|
||||
Особенно, если там миллионы строк.
|
||||
|
||||
- Какие действия по данным ты бы предложил продукту,
|
||||
если доля пользователей без email слишком большая?
|
||||
|
||||
Если бы доля юзеров, не заполняюших email, оказалась слишком большой, то я
|
||||
бы советовал сделать ввод email обязательным, ну или использовать email как
|
||||
логин. Как вариант, предложить какой-то бонус типа промокода
|
||||
*/
|
||||
|
||||
-- Задача 6. «Стаж» пользователя и его группа
|
||||
|
||||
SELECT name,
|
||||
CASE
|
||||
WHEN CURRENT_DATE - created_AT::DATE <= 30 THEN 'new'
|
||||
WHEN CURRENT_DATE - created_AT::DATE BETWEEN 31 AND 180 THEN 'middle'
|
||||
ELSE 'old'
|
||||
END AS tenure_group,
|
||||
CURRENT_DATE - created_AT::DATE AS days_since_registration,
|
||||
is_active
|
||||
FROM dmezentsev.users
|
||||
ORDER BY tenure_group,
|
||||
days_since_registration;
|
||||
|
||||
/* Чем сегментация по «стажу» отличается по смыслу от сегментации по возрасту?
|
||||
|
||||
Стаж - это время с момента регистрации. Думаю, такая сегментация необходима для того, чтобы
|
||||
по этим сегментам проводить какую-то свою аналитику, отличную для каждого из них.
|
||||
Например: стаж - аналитика лояльности, возраст - аналитика платежеспособности
|
||||
|
||||
- Если пересчитывать tenure_group прямо в SQL каждый день,
|
||||
какие плюсы и минусы по сравнению с хранением статического поля segment?
|
||||
|
||||
Плюс перерасчета стажа каждый день - это актуальность данных, которые потом
|
||||
будут использоваться аналитиками
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
-- Задача 1. Сколько товаров в каждой категории
|
||||
|
||||
SELECT category,
|
||||
count(id) AS products_count
|
||||
FROM dmezentsev.products
|
||||
GROUP BY category
|
||||
ORDER BY count(id) DESC;
|
||||
|
||||
/* - Что будет, если убрать ORDER BY — как изменится чтение результата
|
||||
для продукта/аналитика?
|
||||
|
||||
Если убрать сортировку, то строки в результирующей таблице будут выведены
|
||||
произвольно, а точнее это будет зависеть от плана выполнения запроса
|
||||
|
||||
- Если добавить в таблицу новый товар с категорией smartphone,
|
||||
как изменится строка для этой категории?
|
||||
|
||||
Если добавить новый товар с категорией, которая есть в таблице, то в поле
|
||||
products_count будет +1 значение по этой категории
|
||||
*/
|
||||
|
||||
-- Задача 2. Выручка по каждому заказу
|
||||
|
||||
SELECT order_id,
|
||||
SUM(quantity) AS total_quantity,
|
||||
SUM(quantity * price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.order_items
|
||||
GROUP BY order_id
|
||||
ORDER BY SUM(quantity * price_at_purchase) DESC;
|
||||
|
||||
/* - Что будет, если убрать GROUP BY и оставить SUM(quantity * price_at_purchase)?
|
||||
|
||||
Если убрать GROUP BY, то будет ошибка. Поле, запрашиваемое в SELECT и
|
||||
не участвующее в агрегирующей функции, должно быть в GROUP BY
|
||||
*/
|
||||
|
||||
-- Задача 3. Выручка по категориям товаров
|
||||
|
||||
SELECT p.category,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||
FROM dmezentsev.products p
|
||||
JOIN dmezentsev.order_items oi ON p.id = oi.product_id
|
||||
GROUP BY p.category
|
||||
ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC;
|
||||
|
||||
/* - Почему группировка идёт по p.category, а не по p.id?
|
||||
|
||||
Группировка по p.category, а не по p.id, потому что нам нужно узнать
|
||||
общую выручку по категориям, а не по товарам в категориях
|
||||
|
||||
- Если добавить в SELECT ещё p.name, что произойдёт с результатом и почему?
|
||||
|
||||
Если добавить в SELECT поле p.name, то запрос выдаст ошибку, что это
|
||||
поле должно быть в GROUP BY, так как не является агрегирующей функцией
|
||||
*/
|
||||
|
||||
-- Задача 4. Только оплаченные заказы и их выручка
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.orders o
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY o.id
|
||||
ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC;
|
||||
|
||||
/* - Чем результат этой задачи будет отличаться от задачи 2 (где статус не учитывался)?
|
||||
|
||||
В этом запросе в результирующую таблицу попадут только заказы со статусом 'paid'
|
||||
|
||||
- В какой реальной ситуации ты бы смотрел только на paid заказы, а не на все?
|
||||
|
||||
В реальной ситуации такая фильтрация была бы одной из основополагающих
|
||||
и отображающих метрик успешности бизнеса
|
||||
*/
|
||||
|
||||
-- Задача 5. Категории с выручкой больше 1000
|
||||
|
||||
SELECT p.category,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||
FROM dmezentsev.products p
|
||||
JOIN dmezentsev.order_items oi ON p.id = oi.product_id
|
||||
GROUP BY p.category
|
||||
HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000
|
||||
ORDER BY SUM(oi.quantity * oi.price_at_purchase) DESC;
|
||||
|
||||
/* - Почему нельзя написать WHERE category_revenue > 1000?
|
||||
|
||||
Условие в WHERE после GROUP BY не сработает, так как оно фильтрует данные до
|
||||
группировки
|
||||
|
||||
- Чем WHERE price > 1000 принципиально отличается от HAVING SUM(...) > 1000?
|
||||
|
||||
WHERE price > 1000 отличается от HAVING SUM(...) > 1000 тем, что в WHERE
|
||||
будет отфильтрованы товары с ценой больше 1000, а HAVING отфильтрует сгруппированные
|
||||
категории, у которых общая выручка больше 1000
|
||||
*/
|
||||
|
||||
-- Задача 6. Страны с достаточным числом активных пользователей
|
||||
|
||||
SELECT country,
|
||||
COUNT(id) AS active_users_count
|
||||
FROM dmezentsev.users
|
||||
WHERE is_active = TRUE
|
||||
GROUP BY country
|
||||
HAVING COUNT(id) >= 2
|
||||
ORDER BY COUNT(id) DESC;
|
||||
|
||||
/* - Почему фильтр по активности (is_active = true) логичнее писать в WHERE, а не в HAVING?
|
||||
|
||||
Логичнее условие писать в WHERE, потому что производительнее сперва будет
|
||||
отфильтровать нужные данные, а по ним уже потом проводить группировку
|
||||
|
||||
- Что покажет запрос, если в данных пока нет ни одной страны с active_users_count >= 2?
|
||||
|
||||
Если под условие в HAVING не попадет ни один агрегат, то запрос не выведет ни
|
||||
одной строки
|
||||
*/
|
||||
|
||||
-- Задача 7. Топ‑товары по количеству продаж
|
||||
|
||||
SELECT name AS product_name,
|
||||
SUM(quantity) AS total_quantity_sold
|
||||
FROM dmezentsev.products p
|
||||
JOIN dmezentsev.order_items oi ON p.id = oi.product_id
|
||||
GROUP BY name
|
||||
ORDER BY SUM(quantity) DESC
|
||||
LIMIT 3;
|
||||
|
||||
/* - Чем рейтинг по total_quantity_sold отличается от
|
||||
рейтинга по выручке (SUM(quantity * price_at_purchase))?
|
||||
|
||||
- В каких случаях бизнесу важнее смотреть на количество, а не на деньги?
|
||||
|
||||
В поле total_quantity_sold выводим товары, которые продавались чаще всего.
|
||||
SUM(quantity * price_at_purchase) вывел бы выручку наиболее часто продаваемых товаров
|
||||
Предположу, что кол-во важнее выручки в случае, когда важно завоевать место в нише.
|
||||
Как только увеличится база лояльных клиентов, обычно потом начинают поднимать цену
|
||||
*/
|
||||
|
||||
-- Задача 8. Заказы с «маленькой» выручкой
|
||||
|
||||
SELECT order_id,
|
||||
SUM(quantity * price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.order_items
|
||||
GROUP BY order_id
|
||||
HAVING SUM(quantity * price_at_purchase) < 100
|
||||
ORDER BY SUM(quantity * price_at_purchase);
|
||||
|
||||
/* - Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)?
|
||||
|
||||
Маленький по выручке заказ - это покупка чего-то дешевого (либо повседневного). Или же
|
||||
использование скидок/купонов.
|
||||
|
||||
- Что проще увидеть глазами: просто все заказы с суммой или
|
||||
именно отфильтрованные «микро‑заказы»?
|
||||
|
||||
В этом полезность фильтрации, чтобы не размывать взгляд на абсолютно все
|
||||
заказы, а оставить конкретные (< 100, например). И уже проанализировать их
|
||||
*/
|
||||
@@ -0,0 +1,207 @@
|
||||
-- Задание 1: Заказы + кто заказал
|
||||
|
||||
SELECT u.name AS user_name,
|
||||
u.country,
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
o.status
|
||||
FROM dmezentsev.users u
|
||||
LEFT JOIN dmezentsev.orders o ON o.user_id = u.id;
|
||||
|
||||
SELECT u.name AS user_name,
|
||||
u.country,
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
o.status
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON o.user_id = u.id;
|
||||
|
||||
/* - С какой таблицы логичнее начать: orders или users? Почему?
|
||||
|
||||
Логичнее начать с таблицы users. А дальше вариантами джойна выбирать, что
|
||||
нужно: если оставить только тех пользователей, у которых был хотя бы один заказ,
|
||||
то использовать INNER JOIN. Если вывести так же пользователей, у которых заказа
|
||||
не было, то LEFT JOIN
|
||||
|
||||
- Какой тип JOIN здесь уместнее: INNER или LEFT? Напиши запрос в обоих вариантах и посмотри,
|
||||
где пропадут строки.
|
||||
|
||||
При INNER JOIN пропадут три строки с пользователями, у которых не было ни одного заказа
|
||||
*/
|
||||
|
||||
-- Задание 2: Чек одного заказа
|
||||
|
||||
SELECT oi.order_id,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM dmezentsev.order_items oi
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
WHERE oi.order_id = 101;
|
||||
|
||||
/* - Что изменится, если эту фильтрацию order_id = 101 перенести в JOIN (после ON)?
|
||||
|
||||
При использовании INNER JOIN оба варианта (WHERE order_id = 101 или
|
||||
перенести в JOIN (после ON)) дадут одинаковый результат.
|
||||
|
||||
Но:
|
||||
LEFT JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
WHERE oi.order_id = 101
|
||||
Сначала делается джойн, потом фильтруются данные по условию. Вернутся строки,
|
||||
удовлетворяющие условию фильтрации
|
||||
|
||||
LEFT JOIN dmezentsev.products p ON oi.product_id = p.id AND oi.order_id = 101
|
||||
Тут возможно будет больше строк, так как вернутся строки из order_items c NULL
|
||||
в таблице products
|
||||
*/
|
||||
|
||||
-- Задание 3: Полный чек по всем заказам
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id;
|
||||
|
||||
/* - почему после JOIN заказ 3 появляется в нескольких строках?
|
||||
|
||||
Заказ 3 появился в двух строках, потому что при джойне кол-во строк может
|
||||
увеличиваться. Это связано с тем, что в этом заказе было два наименования,
|
||||
отсюда и увеличение строк
|
||||
|
||||
- Чем результат отличается от задания 2 (по количеству строк и информации)?
|
||||
|
||||
В этом запросе строк и информации больше, потому что информация запрошена
|
||||
по всему магазину, а не по конкретному заказу 101
|
||||
*/
|
||||
|
||||
-- Задание 4: Все пользователи + их заказы (включая тех, у кого заказов нет)
|
||||
|
||||
SELECT u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.status
|
||||
FROM dmezentsev.users u
|
||||
LEFT JOIN dmezentsev.orders o ON u.id = o.user_id;
|
||||
|
||||
/* - Чем этот запрос отличается от такого же, но с INNER JOIN?
|
||||
|
||||
Если бы тут был INNER JOIN, то пользователи без заказов не попали бы в
|
||||
результирующую таблицу
|
||||
*/
|
||||
|
||||
-- Задание 5: Пользователи без заказов
|
||||
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name
|
||||
FROM dmezentsev.users u
|
||||
LEFT JOIN dmezentsev.orders o ON u.id = o.user_id
|
||||
WHERE o.id IS NULL;
|
||||
|
||||
/* - Почему тут важно фильтровать именно по колонке из **`orders`**, а не из **`users`**?
|
||||
|
||||
Фильтрую по полю orders.id, потому что это первичный ключ таблицы,
|
||||
который подразумевает собой факт покупки. То есть если после джойна
|
||||
это поле IS NULL, то у пользователя не было ни одного заказа
|
||||
*/
|
||||
|
||||
-- Задание 6: Заказы, где есть Kindle
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
WHERE p.name LIKE '%Kindle%';
|
||||
|
||||
-- Задание 7: Товары и в каких заказах они покупались
|
||||
|
||||
SELECT p.name AS product_name,
|
||||
oi.order_id,
|
||||
oi.quantity
|
||||
FROM dmezentsev.products p
|
||||
LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id;
|
||||
|
||||
/* - Что будет в order_id, если товар ни разу не покупали?
|
||||
|
||||
Если бы товар не был ни разу куплен, то в поле order_id был NULL
|
||||
из-за использования LEFT JOIN
|
||||
|
||||
- Чем это отличается от варианта, если использовать INNER JOIN?
|
||||
|
||||
При использовании INNER JOIN товар, который ни разу не заказывали,
|
||||
не попал бы в итоговую таблицу
|
||||
*/
|
||||
|
||||
-- Задание 8: Только оплаченные заказы + их состав
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
WHERE o.status = 'paid';
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id AND o.status = 'paid'
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id;
|
||||
|
||||
/* - Что будет, если условие status = 'paid' написать
|
||||
в JOIN с orders (там же, где ON)? Изменится ли результат сейчас и почему?
|
||||
|
||||
Результат будет один и тот же, потому что используется INNER JOIN.
|
||||
Строки по условию o.status = 'paid' отсекутся в любом случае, что
|
||||
при WHERE, что на шаге JOIN
|
||||
*/
|
||||
|
||||
-- Задание 9: Заказы пользователя Alice
|
||||
|
||||
SELECT o.id AS order_id,
|
||||
o.order_date,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
WHERE u."name" = 'Alice';
|
||||
|
||||
/* - Если у Alice нет ни одного заказа, сколько строк вернёт твой запрос и почему?
|
||||
|
||||
Если бы у Alice не было заказов, то запрос не вернул ничего, так как
|
||||
не было бы ни одной строки, удовлетворяющей условию фильтрации
|
||||
*/
|
||||
|
||||
-- Задание 10: Заказы без товаров
|
||||
|
||||
SELECT
|
||||
o.id AS order_id,
|
||||
o.user_id,
|
||||
o.status
|
||||
FROM dmezentsev.orders o
|
||||
LEFT JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
WHERE oi.order_id IS NULL;
|
||||
|
||||
/* - Что будет, если вместо LEFT JOIN написать INNER JOIN — изменится ли результат и как?
|
||||
|
||||
Если изменить LEFT JOIN на INNER JOIN, то результирующих строк вообще не будет.
|
||||
INNER JOIN оставит только те заказы, у которых есть хотя бы одна запись в order_items.
|
||||
То есть при INNER JOIN колонка oi.order_id никогда не будет NULL
|
||||
*/
|
||||
@@ -0,0 +1,107 @@
|
||||
-- Задание 1. Активные и неактивные пользователи в одном списке
|
||||
SELECT id,
|
||||
name,
|
||||
CASE
|
||||
WHEN is_active THEN 'active'
|
||||
END AS user_status
|
||||
FROM dmezentsev.users
|
||||
WHERE is_active = TRUE
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
CASE
|
||||
WHEN NOT is_active THEN 'inactive'
|
||||
END AS user_status
|
||||
FROM dmezentsev.users
|
||||
WHERE is_active = FALSE
|
||||
|
||||
ORDER BY name;
|
||||
|
||||
/* - Что будет, если вместо UNION ALL написать UNION?
|
||||
|
||||
Кол-во строк при UNION и UNION ALL не изменится, потому что все строки
|
||||
в таблице users уникальны
|
||||
|
||||
- Зачем в результат добавлять колонку user_status, если флаг активности
|
||||
уже есть в таблице users?
|
||||
|
||||
Предположу, что добавляем колонку user_status только лишь для удобства
|
||||
понимания результирующей таблицы всем тем, кто далек от TRUE или FALSE.
|
||||
*/
|
||||
|
||||
-- Задание 2. Заказы до и после даты в одной ленте
|
||||
|
||||
SELECT
|
||||
id AS order_id,
|
||||
user_id,
|
||||
order_date,
|
||||
'old' AS period
|
||||
FROM dmezentsev.orders
|
||||
WHERE order_date < '2024-01-01'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
id AS order_id,
|
||||
user_id,
|
||||
order_date,
|
||||
'new' AS period
|
||||
FROM dmezentsev.orders
|
||||
WHERE order_date >= '2024-01-01'
|
||||
|
||||
ORDER BY order_date, order_id;
|
||||
|
||||
/* - Как ты по колонке period поймёшь, сколько заказов было
|
||||
до и после даты‑границы (словами, без SQL)?
|
||||
|
||||
Нужно посчитать количество строк, где в колонке period стоит
|
||||
значение 'old' — это и будет число заказов до 2024-01-01.
|
||||
А количество строк со значением 'new' — это число заказов начиная с 2024-01-01 и позже.
|
||||
|
||||
- Какой подход тебе кажется понятнее для новичка в команде:
|
||||
два SELECT + UNION ALL или один SELECT с CASE WHEN? Почему?
|
||||
|
||||
Вот совсем для новичка, пожалуй, вариант с UNION ALL выглядит понятнее.
|
||||
Два простых запроса, которые потом объединены в общую таблицу, к тому же
|
||||
их можно выполнить отдельно
|
||||
*/
|
||||
|
||||
-- Задание 3. Товары, которые покупали, и которые не покупали
|
||||
|
||||
SELECT DISTINCT
|
||||
p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
'ordered' AS product_status
|
||||
FROM dmezentsev.products p
|
||||
JOIN dmezentsev.order_items oi ON p.id = oi.product_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
'never_ordered' AS product_status
|
||||
FROM dmezentsev.products p
|
||||
LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id
|
||||
WHERE oi.product_id IS NULL
|
||||
|
||||
ORDER BY product_name;
|
||||
|
||||
/* - Почему в запросе на «не покупали» проверка IS NULL должна быть
|
||||
по полю из order_items, а не из products?
|
||||
|
||||
LEFT JOIN гарантирует, что все строки из products останутся в результате.
|
||||
Если для товара нет записей в order_items, то все колонки из order_items будут NULL.
|
||||
|
||||
- Какой сигнал для бизнеса даёт список с product_status = 'never_ordered'
|
||||
(1–2 идеи: что можно с этими товарами сделать)?
|
||||
|
||||
Список never_ordered даст понимание бизнесу, что либо товар не востребован
|
||||
(очень дорогой, непродающее описание), либо о товаре никто не знает (проблемы
|
||||
с рекламой и продвижением). То есть решив эти кейсы, ситуацию можно изменить
|
||||
в лучшую сторону.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Задание 1 — Спроектируй «карточку пользователя»
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country,
|
||||
created_at,
|
||||
email
|
||||
FROM dmezentsev.users u
|
||||
ORDER BY created_at DESC, name;
|
||||
|
||||
/* - Почему выбрал именно эти столбцы и зачем такая сортировка?
|
||||
|
||||
Сортировка нужна для того, чтобы вывести пользователей с датой регистрации по убыванию.
|
||||
Если даты бы совпадали, то была дополнительная сортировка по имени внутри одной даты
|
||||
*/
|
||||
|
||||
-- Задание 2 — Сегментация по странам (с выбором)
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dmezentsev.users u
|
||||
WHERE country = 'US'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dmezentsev.users u
|
||||
WHERE country = 'UK'
|
||||
ORDER BY name;
|
||||
|
||||
/* - Какой из двух запросов маркетологу полезнее и почему?
|
||||
|
||||
Предположу, что маркетологу выгоднее рассматривать пользователей из US,
|
||||
потому что их больше (5 US > 3 UK)
|
||||
*/
|
||||
|
||||
-- Задание 3 — Контакты для рассылки
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
email
|
||||
FROM dmezentsev.users u
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY name;
|
||||
|
||||
SELECT DISTINCT(LOWER(email)) email
|
||||
FROM dmezentsev.users u
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
/* - Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы?
|
||||
|
||||
Нужно приводить email к одному виду, потому что хранение одного и того же email
|
||||
в разных регистрах приведет к дублированию данных.
|
||||
По факту же один пользователь - один email
|
||||
*/
|
||||
|
||||
-- Задание 4 — Новички и активность
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
created_at
|
||||
FROM dmezentsev.users u
|
||||
WHERE created_at < '2024-03-11'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 2 OFFSET 1;
|
||||
|
||||
/* - В каком реальном сценарии тебе пригодится OFFSET?
|
||||
|
||||
Предположу, OFFSET понадобится, когда нужно убрать из выборки
|
||||
какой-то топ пользователей. Допустим, нужны все, кроме первой тройки
|
||||
*/
|
||||
Reference in New Issue
Block a user