diff --git a/dmezentsev/postgresql/homework/DDL_DML.sql b/dmezentsev/postgresql/homework/DDL_DML.sql new file mode 100644 index 0000000..3cb0128 --- /dev/null +++ b/dmezentsev/postgresql/homework/DDL_DML.sql @@ -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); \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Filtering.sql b/dmezentsev/postgresql/homework/Filtering.sql new file mode 100644 index 0000000..89e366c --- /dev/null +++ b/dmezentsev/postgresql/homework/Filtering.sql @@ -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" в любом регистре +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Functions.sql b/dmezentsev/postgresql/homework/Functions.sql new file mode 100644 index 0000000..4fb08ef --- /dev/null +++ b/dmezentsev/postgresql/homework/Functions.sql @@ -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? + +Плюс перерасчета стажа каждый день - это актуальность данных, которые потом +будут использоваться аналитиками + */ + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/GROUP_BY.sql b/dmezentsev/postgresql/homework/GROUP_BY.sql new file mode 100644 index 0000000..5d641a4 --- /dev/null +++ b/dmezentsev/postgresql/homework/GROUP_BY.sql @@ -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, например). И уже проанализировать их + */ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/JOIN.sql b/dmezentsev/postgresql/homework/JOIN.sql new file mode 100644 index 0000000..28f146b --- /dev/null +++ b/dmezentsev/postgresql/homework/JOIN.sql @@ -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 + */ diff --git a/dmezentsev/postgresql/homework/UNION.sql b/dmezentsev/postgresql/homework/UNION.sql new file mode 100644 index 0000000..9e2a3c8 --- /dev/null +++ b/dmezentsev/postgresql/homework/UNION.sql @@ -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 даст понимание бизнесу, что либо товар не востребован +(очень дорогой, непродающее описание), либо о товаре никто не знает (проблемы +с рекламой и продвижением). То есть решив эти кейсы, ситуацию можно изменить +в лучшую сторону. +*/ + + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/base.sql b/dmezentsev/postgresql/homework/base.sql new file mode 100644 index 0000000..b5833e4 --- /dev/null +++ b/dmezentsev/postgresql/homework/base.sql @@ -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 понадобится, когда нужно убрать из выборки +какой-то топ пользователей. Допустим, нужны все, кроме первой тройки + */