add postgresql homework

This commit is contained in:
dmezentsev
2026-05-17 14:56:01 +03:00
parent c0f5f0dc5e
commit 19563dcfa3
7 changed files with 846 additions and 0 deletions
@@ -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?
Плюс перерасчета стажа каждый день - это актуальность данных, которые потом
будут использоваться аналитиками
*/
+160
View File
@@ -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, например). И уже проанализировать их
*/
+207
View File
@@ -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
*/
+107
View File
@@ -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 даст понимание бизнесу, что либо товар не востребован
(очень дорогой, непродающее описание), либо о товаре никто не знает (проблемы
с рекламой и продвижением). То есть решив эти кейсы, ситуацию можно изменить
в лучшую сторону.
*/
+73
View File
@@ -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 понадобится, когда нужно убрать из выборки
какой-то топ пользователей. Допустим, нужны все, кроме первой тройки
*/