diff --git a/dmedvedev/10.1.Transactions.sql b/dmedvedev/10.1.Transactions.sql new file mode 100644 index 0000000..730d7f2 --- /dev/null +++ b/dmedvedev/10.1.Transactions.sql @@ -0,0 +1,61 @@ +--Задание 1. Оформление заказа +BEGIN; + + INSERT INTO orders + (user_id, order_date, status) + VALUES + (10, '2025-01-15', 'paid'); + + INSERT INTO order_items + (order_id, product_id, quantity, price_at_purchase) + VALUES + (LASTVAL(), 6, 1, 4800), + (LASTVAL(), 7, 1, 5200); + + UPDATE products + SET in_stock = FALSE + WHERE id IN (6, 7); + + COMMIT; + +/*Что в данных пойдёт не так, если часть шагов выполнится, а часть нет? +В таком случае транзакция не будет закрыта. Я начал бегин, но произошла ошибка внутри транзакции и поэтому dbeaver дальше ничего выполнять не будет пока я не сделаю откат, либо не скажу завершить. + +Почему оформление заказа — типичный пример «либо всё, либо ничего»? +Потому что в этом случае происходят изменения записей сразу в нескольких таблицах одновременно. И либо меняются все значения, либо вообще ничего не меняется. +*/ + +--Задание 2. Ошибка посередине +/*- Сценарий: + - заказ создали; + - одну позицию заказа добавили; + - на второй позиции выяснилось, что товара нет на складе. +- Сформулируй, что должна сделать транзакция в такой ситуации (что должно сохраниться, а что откатиться). + +ОТВЕТ: В таком случае все будет зависеть от бизнес-логики данного "магазина". Если отсутствие второго заказа является причиной для отклонения выполнения заказа, то должен быть откат. +А как по мне, то здесь правильным решением будет частичная запись без отката. + +Какие «битые» заказы можно получить без отката (ROLLBACK)? +В таком случае можно получить: создан заказ, оплачен, а второго товара даже нет в наличие; появится неполный заказ с ошибками и это большая проблема. + +Как такие ошибки повлияют на отчёты и витрины по выручке? +Такие ошибки могут криво влиять (будет искажаться информация) потому что заказ может быть оплачен, хотя его продать мы и не можем. +*/ + +--Задание 3. Один UPDATE как транзакция +/*- Возьми простое действие в магазине: + - смена `email` пользователя в `users`; + - изменение цены товара в `products`. +- Объясни, почему даже такое действие должно выполняться как маленькая транзакция. + +ДАЖЕ ТАКОЕ МАЛЕНЬКОЕ ДЕЙСТВИЕ ДОЛЖНО ВЫПОЛНЯТЬСЯ ОТДЕЛЬНО ПОТОМУ ЧТО ОНО МОЖЕТ ВЛИЯТЬ НА ДРУГИЕ ПОКАЗАТЕЛИ В ДРУГИХ ТАБЛИЦАХ, СООТВЕТСТВЕННО МНОГОЕ МОЖЕТ СБИТЬСЯ. +ТАКЖЕ СТОИТ ПОНИМАТЬ, ЧТО ЧЕМ МЕНЬШЕ ДЕЙСТВИЙ В ТРАНЗАКЦИИ, ТЕМ ОНА ПРОЩЕ И ПОНЯТНЕЕ. ПРОЩЕ ОТКАТИТЬСЯ НАЗАД В ТАКОМ СЛУЧАЕ, ЧЕМ КОГДА ТРАНЗАКЦИЯ БОЛЬШАЯ. +**❓ Ответь на вопросы:** + + +- Что может случиться, если во время изменения цены пропадёт соединение с базой? +ЗДЕСЬ ЕСТЬ НЕСКОЛЬКО ВАРИАНТОВ, ЕСЛИ БЫЛ АВТОКОММИТ ИЛИ ОБЫЧНЫЙ КОММИТ. В ПЕРВОМ СЛУЧАЕ МОГЛА УСПЕТЬ ЗАВЕРШИТЬСЯ ТРАНЗАКЦИЯ НА СТОРОНЕ БД ИЛИ ДАЖЕ НЕ НАЧАТЬСЯ, О РЕЗУЛЬТАТЕ МЫ УЗНАЕМ ТОЛЬКО ЕСЛИ СДЕЛАЕМ СЕЛЕКТ, ЛИБО НЕ ЗАВЕРШИТЬСЯ И ОНА ОТКАТИТСЯ НА ПЕРВОНАЧАЛЬНОЕ СОСТОЯНИЕ. А ЕСЛИ БЫЛ ОБЫКНОВЕННЫЙ РУЧНОЙ КОММИТ, ТО ЗДЕСЬ ТАКЖЕ СТОИТ РАЗДЕЛЯТЬ НА ВЫПОЛНИЛСЯ ИЛИ НЕ ВЫПОЛНИЛСЯ КОММИТ (ЕСЛИ ДА, ТО ИЗМЕНЕНИЯ СОХРАНИЛИСЬ, ИНАЧЕ НЕТ). + +- Почему хорошо, что база сама откатывает незавершённые изменения? +ПОТОМУ ЧТО ЕСЛИ БЫ ОНА НЕ ОТКАТЫВАЛА, ТО ЦЕЛОСТНОСТЬ ТРАНЗАКЦИИ НЕ СОБЛЮДАЛОСЬ БЫ, А ТРАНЗАКЦИЯ КАК РАЗ И НУЖНА ДЛЯ ЦЕЛОСТНОСТИ. +*/ \ No newline at end of file diff --git a/dmedvedev/10.2.Blocks.sql b/dmedvedev/10.2.Blocks.sql new file mode 100644 index 0000000..0065c56 --- /dev/null +++ b/dmedvedev/10.2.Blocks.sql @@ -0,0 +1,33 @@ +--Задание 1. Чтение и чтение +--Сформулируй, что делают две одновременные сессии, каждая выполняет SELECT * FROM users WHERE id = 1;. +--Эти две сессии из делают следующее: запрашивают данные из таблицы users, фильтруют по id и далее выводят все столбцы. + +--Почему оба запроса могут выполниться сразу, без ожидания? +-- Потому что этим сессиям нечего ожидать, они берут данные и читают их, здесь нет никаких действий по изменению данных. + +--Задание 2. Два UPDATE одного пользователя +--Опиши, что будет делать вторая сессия, пока первая транзакция не закончилась. +--Вторая сессия будет ждать пока первая транзакция завершится. + +--Почему нельзя разрешить обоим UPDATE пройти одновременно? +--В таком случае не будет ясно какой именно будет конечный результат, нельзя допускать эту коллизию, все должно быть последовательно. + +--Какой email в итоге останется у Alice и почему порядок COMMIT важен? +--В данном случае в конечном счете у Алисы будет второй имэйл, либо получит ошибку (если ее откатят). + +--Задание 3. SELECT ... FOR UPDATE +--что будет с SELECT * FROM users WHERE id = 2; +--просто прочитает данные. Боб лишь заблокирован для изменений. + +--что будет с UPDATE users SET country = 'FR' WHERE id = 2;. +--В таком случае БД зависнет (blocked) и будет ждать пока первая транзакция не исполнится. + +--Задание 4. Дедлок на двух пользователях +--Объясни своими словами, почему возникает дедлок и что сделает база с одной из транзакций. +--Потому что обе транзакции держат пользователей заблокированными. БД решает конфликт и отменяют одну из них. + +--Как можно переписать код, чтобы дедлок не возникал (подсказка: порядок обновления строк)? +--Код надо писать так чтобы не было такого пересечения, в данном случае пересекаются id товаров, поэтому можно переписать запрос таким образом, что было последовательность запросов соответствовала возрастающему id. + +--Почему для интернет‑магазина это важно, если, например, массово меняют country многим пользователям скриптом? +--потому что в момент замены БД может заблокировать всех пользователей, пока не завершится транзакция, а это может быть миллион строк, соответственно, другая сессия может обратиться к одному из пользователей и получится дэдлок. Поэтому если и обновлять, то лучше партиями и с увеличением id. \ No newline at end of file diff --git a/dmedvedev/10.3.Optimization of select.sql b/dmedvedev/10.3.Optimization of select.sql new file mode 100644 index 0000000..51da339 --- /dev/null +++ b/dmedvedev/10.3.Optimization of select.sql @@ -0,0 +1,58 @@ +Задача 1 — Одинаковые ли запросы? +Напиши, будут ли эти два запроса давать одинаковый результат? Объясни почему. +Результат будет одинаковый потому что фильтрация по одинаковым параметрам, однако время выполнения запроса разное (фактическое), возможно потому что БД дольше работает с числами, нежели чем с текстом (я имею ввиду внутри фильтрации). +Но мне кажется, раз в современных СУБД есть оптимизатор, то он сам должен оптимально выбрать порядок и в таком случае разницы в выполнении мы не должны видеть. + + +Задача 2 — JOIN в разном порядке +Будут ли результаты одинаковыми? +будут одинаковые + +Какой таблицей оптимизатор начнёт чаще всего? Почему? +Начнет с меньшей, где можно отфильтровать данные быстрее. + +Задача 3 — Первое знакомство с EXPLAIN +Какой тип сканирования использует база (Seq Scan, Index Scan)? +Seq Scan +Сколько строк она прочитала? +Прочитано 10 строк, 9 удалено по условию +Сколько времени это заняло? +0.067сек + +Задача 4 — Сравни два плана +Почему второй запрос может быть быстрее первого? +Потому что в запросе на вывод участвуют не все колонки. + +Задача 5 — JOIN с EXPLAIN +С какой таблицы начала читать база? Почему именно с неё? +Начала читать с users потому что есть условие where где отфильтровываются некоторые строки и поэтому БД понимает что выгоднеее начать с users, тк таблица меньше. + +Задача 6 — Найди узкое место +❓ Найди как минимум 2 проблемы, которые замедляют запрос. Как их исправить? +Первая проблема это использование функций внутри WHERE, здесь функция LOWER, вторая - LIKE с %, когда невозможно использовать индекс, а третья - группировка по name, что может в конечном счете исказить данные. + +Задача 7 — Оптимизируй JOIN +В каком порядке база будет читать таблицы? Почему? Можно ли как-то ускорить этот запрос? +Сначала читается users как самая маленькая после фильтрации, потом orders, order_items и products + +Задача 8 — Сравни подходы +Запусти оба с EXPLAIN ANALYZE и сравни время выполнения. +Первый будет работать быстрее потому что выводит меньше колонк, соответственно ест меньше памяти. + +Задача 9 — Найди и исправь +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; + +CREATE INDEX users_lower_country_idx ON users (LOWER(country)); +SELECT id, name, age +FROM users +WHERE + created_at >= '2026-01-01' AND created_at < '2027-01-01' + AND LOWER(country) = 'россия' + AND age > 25; + +Исправлено: Добавлен индекс, который приведет все строки данного столбца в нижний регистр и дальнейший поиск будет производиться только по индексу. + diff --git a/dmedvedev/10.4.Explain.sql b/dmedvedev/10.4.Explain.sql new file mode 100644 index 0000000..d4ddfc0 --- /dev/null +++ b/dmedvedev/10.4.Explain.sql @@ -0,0 +1,156 @@ +--Задача 1 — Читаем план + +EXPLAIN +SELECT name, email +FROM users +WHERE id = 1; + +Какой тип сканирования использует база? +--SeqScan - сканирование всей таблицы + +Что означает строка `Index Cond: (id = 1)`? +-- Строка означает фильтрацию столбца по индексу, который есть в столбце id. + +Почему база использует индекс, а не читает всю таблицу? +--Потому что по полю есть индекс и таким образом оптимизатор ускоряет чтение. +---------- + +--Задача 2 — Seq Scan vs Index Scan +Почему в первом запросе используется Index Scan, а во втором может быть Seq Scan (если нет индекса на age)? +--Потому что Seq Scan сканирует таблицу полностью и ищет значения, а вот Index Scan нет необходимости сканировать всю таблицу, он отсекает по индексу. +---------- + +--Задача 3 — Замеряем время +1. Запиши **Execution Time** +--0.023 мс + +2. Определи тип сканирования (Seq Scan или Index Scan) +--Seq Scan + +3. Сколько строк вернул запрос? +--вернул 3 строки +---------- + +--Задача 4 — До и после индекса +Запиши Execution Time и тип сканирования. +--Index Scan, 0,06 мс после создания индекса. + +Изменился ли тип сканирования? +-- да, изменился. + +Стал ли запрос быстрее? На сколько? +--Да, в 21 раза + +Почему индекс помог? +--Потому что не пришлось искать по всей таблице. + +---------- + +--Задача 5 — Анализируем JOIN + +Какой тип JOIN использует база (Hash Join, Nested Loop, Merge Join)? +--Hash + +С какой таблицы база начала чтение? +-- с users + +Сколько времени занял весь запрос? +--execution time 0.032ms +---------- + +--Задача 6 — Почему медленно? +1. Выполни запрос и посмотри на план +--посмотрел + +2. Использует ли база индекс? +--нет, используется seq scan + +3. Почему НЕТ? (даже если индекс на email есть) +--Думаю, потому что также используется функция LOWER внутри WHERE + +4. Как исправить запрос, чтобы индекс заработал? +CREATE INDEX idx_email ON users(LOWER(email)) +---------- + +--Задача 7 — Функции убивают индексы +Какой запрос работает быстрее? +--тот где нет дополнительной математики в условии + +Почему первый запрос НЕ может использовать индекс на `age`? +--потому что появилась функция + +Как правильно переписать запрос А? +-- убрать функцию из условия: +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 35; + +---------- + +--Задача 8 — Диагностика медленного запроса +1. Почему функция `EXTRACT()` — плохая идея? +--потому что она мешает оптимизатору применить индекс. + +2. Как переписать условие WHERE без функции? +--добавлением граничных условий что-то по типу created_at>='2026-01-01' AND created_at<'2027-01-01' + +3. Будет ли использоваться индекс на `created_at` в текущем виде? +--нет, по описанным выше условиям + +---------- + +--Задача 9 — SELECT * против конкретных колонок +Какой запрос работает быстрее? +-- запрос Б, там где выбираются не все столбцы + +Почему второй может быть эффективнее (даже если тип сканирования одинаковый)? +--потому что объем обработанной информации меньше + +В каких ситуациях разница будет особенно заметна? +--когда вся таблица изначально состоит из множества столбцов (более 10), а вт втором запросе мы берем только пару И когда количество строк очень большой. + +---------- + +--Задача 10 — Оптимизируем JOIN с условием +1. Выполни запрос и посмотри на план +посмотрел + +2. В каком порядке база читает таблицы? +БД читает таблицы в порядке, который является наименее затратным. Сначала идет отфильтровывание по условию,далее создается хэш таблица и хэш соединение, соединение по индексу. + +3. Сколько строк проходит через каждый JOIN? +У меня здесь только 1 hash join - там 5 строк. Но сразу скажу, что одно из условий я удалил, так что у меня осталось только одно условие с category. + +4. Можно ли оптимизировать? (Подсказка: подумай про индексы на `country` и `category`) +Я добавил индекс на category, поменялось количество hash join, стало 2. +Но сканирование не изменилось на индекс. Видимо, это связано с тем, что оптимизатору проще проходить по всей таблице, чем искать по индексу ввиду того, что таблица сама по себе не большая. + +---------- + +--Задача 11 — Найди и исправь +Основной запрос + +EXPLAIN ANALYZE + +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; + +Оптимизированный + +EXPLAIN ANALYZE + +SELECT u.name, o.order_date, u.age +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.name LIKE '%иван%' + AND o.order_date >= '2024-01-01' + AND o.order_date < '2024-02-01' + AND u.age > 25; + +По моим данным ожидаемое кол-во строк = 0, поэтому и запросы отрабатываются быстро = 0.03 мс. + + + diff --git a/dmedvedev/10.5.Porydok.sql b/dmedvedev/10.5.Porydok.sql new file mode 100644 index 0000000..a6ba4d7 --- /dev/null +++ b/dmedvedev/10.5.Porydok.sql @@ -0,0 +1,284 @@ +- **Задача 1** — Запрос с группировкой + + SELECT country, COUNT(*) AS users_count + FROM users + WHERE is_active = true + GROUP BY country + HAVING COUNT(*) > 1 + ORDER BY users_count DESC; + + **❓ Вопросы:** + + 1. Когда применится `WHERE` — до или после `GROUP BY`? + До группировки, потому что это фильтрация строк + 2. Когда применится `HAVING` — до или после `GROUP BY`? + После группировки, потому что это фильтрация групп + 3. Можно ли использовать алиас `users_count` в `HAVING`? + Нельзя, тк HAVING применяется до SELECT + 4. Можно ли использовать алиас `users_count` в `ORDER BY`? + Можно, тк ORDER BY применяется после SELECT + + +- **Задача 2** — JOIN с фильтрацией + + SELECT u.name, o.order_date, o.status + FROM users u + JOIN orders o ON u.id = o.user_id + WHERE u.country = 'Россия' AND o.status = 'completed' + ORDER BY o.order_date DESC; + + **✅ Задание:**  + + - Опиши порядок выполнения своими словами (как будто объясняешь другу-новичку). + На самом первом этапе идет формирование таблицы данных из фром и джойна, далее данные отфильтровываются в WHERE и выполняется вычисление выражение черезе SELECT, в конечном счете строки фильруются по order_date через ORDER BY + +- **Задача 3** — Найди ошибку + + Дан запрос: + + SELECT country, AVG(age) AS avg_age + FROM users + GROUP BY country + HAVING country = 'Россия'; + + **❓ Вопросы:** + + 1. Запрос работает, но написан **неэффективно**. Почему? + Потому что фильтрация по "Россия" идет в самом конце запроса, когда можно было на первоначальном этапе сделать через  WHERE, чтобы не читать всю таблицу. Плюс можно добавить индекс по столюцу country + 2. Перепиши его правильно, используя WHERE. + SELECT + country, + AVG(age) AS avg_age + FROM users + WHERE country = 'RU' + GROUP BY country; + 3. Объясни, почему твой вариант быстрее. + Объяснил в 1ом вопросе. + +- **Задача 4** — Исправь запрос + + SELECT category, AVG(price) AS avg_price + FROM products + GROUP BY category + HAVING category = 'Электроника' AND AVG(price) > 20000; + + **✅ Задание:** + + 1. Перепиши запрос так, чтобы часть условий была в `WHERE`. + SELECT category, AVG(price) AS avg_price + FROM products + WHERE category = 'Электроника' + GROUP BY category + HAVING AVG(price) > 20000; + 2. Объясни, почему это быстрее. + Потому что сразу происходит фильтрация строк и дальнейшая группировка и фильтрация уже не затрагивает ненужные строки (чем раньше фильтрация, тем лучше) + 3. Нарисуй схему: сколько строк будет обработано в каждом случае. + Если не исполбзовать WHERE , то ан этапе группировке на входе будет 7 строк , которые далее отфильтуются. + А если сразу использовать WHERE, то на входе в группировку будет всего три строки + +- **Задача 5** — Исправь все ошибки + + SELECT + country, + COUNT(*) AS users_count + FROM users + WHERE users_count > 1 + GROUP BY country + ORDER BY country; + + **✅ Задание:**  + + - Найди ошибку и исправь запрос. + + SELECT + country, + COUNT(*) AS users_count + FROM users + --WHERE users_count > 1 + GROUP BY country + HAVING COUNT(*) > 1 --добавлена фильтрация по группам + ORDER BY country; +- **Задача 6** — Полный цикл выполнения + + SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + WHERE u.is_active = true + GROUP BY u.country + HAVING COUNT(o.id) > 1 + ORDER BY orders_count DESC + LIMIT 3; + + **✅ Задание:**  + + - Опиши ПОЛНЫЙ порядок выполнения (все 7 шагов) и объясни, что происходит на каждом. + 1. Формирование таблицы данных из FROM и JOIN + 2. Фильтрация строк WHERE + 3. Группировка строк GROUP BY + 4. Фильтрация групп HAVING + 5. Вывод столбцов SELECT + 6. Сортировка ORDER BY + 7. Лимитирование выхода строк LIMIT + +- **Задача 7** — Оптимизация + + Сравни два запроса: + + -- Запрос А + SELECT category, COUNT(*) + FROM products + GROUP BY category + HAVING category = 'Электроника'; + + -- Запрос Б + SELECT category, COUNT(*) + FROM products + WHERE category = 'Электроника' + GROUP BY category; + + **❓ Вопросы:** + + 1. Какой запрос быстрее? Почему? + Запрос 2 быстрее потому что на этапе WHERE сразу отфильтровываются ненужные строки. + + 2. Нарисуй таблицу: на каком шаге сколько строк обрабатывается в каждом запросе. + На этапе FROM всего 7 строк. Далее если смотреть по запросу А они превращаются в 4 строки и на HAVING в 1. + Если смотреть на запрос Б, то после FROM остается 3 строки, содержащие данные только о категории 'Электроника', далее они просто группируются в одну. + + 3. Выполни оба с `EXPLAIN ANALYZE` и сравни `Execution Time.` + Запрос А - 0.04 мс + Запрос Б - 0.03 мс + + + +- **Задача 8** — Многоэтапная фильтрация + + SELECT + u.name, + COUNT(o.id) AS orders_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' + GROUP BY u.name + HAVING COUNT(o.id) >= 2 + ORDER BY orders_count DESC; + + **❓ Вопросы:** + + 1. Когда применяются условия `WHERE`? + Для фильтрации строк + 2. Когда применяется условие `HAVING`? + Для фильтрации групп + 3. В чём разница между фильтрацией по `o.status` в `WHERE` и по `COUNT(o.id)` в `HAVING`? + В  WHERE фильтрация подразумевает выполненные заказы, а в HAVING подразумевает более 2х штук. Соответственно получается, что сначала мы фильтруем выполненные заказы, а потом людей у которых заказов более 2х штук. + + +- **Задача 9** — JOIN и ORDER BY + + SELECT + u.name, + p.name AS product_name, + p.price + FROM users u + JOIN orders o ON u.id = o.user_id + JOIN order_items oi ON o.id = oi.order_id + JOIN products p ON oi.product_id = p.id + WHERE u.country = 'Россия' + ORDER BY p.price DESC + LIMIT 5; + + **❓ Вопросы:** + + 1. На каком этапе применяется `ORDER BY`? + После FROM, JOIN, WHERE, SELECT и до LIMIT. + + 2. Почему `LIMIT` идёт после `ORDER BY`, а не до? + В таком случае нарушалась бы логика использования этих двух функций. Например, нужно вывести топ-3 (или топ-3 наименьшие) позиции из списка. И если лимит стоял бы до сортировки, то мы могли бы вывести совсем не топ значения. Данная последовательность помогает сначала сделать сортировка вверх или вниз и потом вывести нужные строки. + + 3. Что будет, если поменять их местами (технически невозможно, но подумай концептуально)? + В таком случае нарушалась бы логика использования этих двух функций. Например, нужно вывести топ-3 (или топ-3 наименьшие) позиции из списка. И если лимит стоял бы до сортировки, то мы могли бы вывести совсем не топ значения. Данная последовательность помогает сначала сделать сортировка вверх или вниз и потом вывести нужные строки. + +- **Задача 10** — Напиши запрос с нуля + + ✅ Тебе нужно получить: + + - Список категорий товаров + - Количество товаров в каждой категории + - Среднюю цену товаров в категории + - Только те категории, где средняя цена > 15000 + - Только те категории, где товаров >= 2 + - Отсортировать по средней цене (от большей к меньшей) + - Взять только топ-3 + + **✅ Задание:** + + 1. Напиши запрос + + SELECT + p.category AS category, + COUNT(*) AS count_product_in_category, + AVG(price) AS avg_price_product_in_category + FROM products p + GROUP BY p.category + HAVING AVG(price) > 15000 AND COUNT(*) >= 2 + ORDER BY avg_price_product_in_category DESC + LIMIT 3; + + 2. Опиши порядок выполнения + 1. Формируется таблица из FROM + 2. Группировка GROUP BY (Так как строки фильтровать не нужно) + 3. Фильтрация групп HAVING + 4. Вывод значений SELECT + 5. Сортировка ORDER BY + 6. Лимитирование LIMIT + + 3. Объясни, почему использовал `WHERE` или `HAVING` для каждого условия + Использовал только HAVING потому что по условию нужно фильтровать именно группы, а не строки. + + 4. Выполни с `EXPLAIN ANALYZE` и посмотри план + 1. чтение таблицы products + 2. Группировка с фильтрацией групп (hash aggregate) + 3. Сортировка Sort key + 4. LIMIT + + +- **Задача 11** — Подзапросы и порядок + + SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count + FROM users u + WHERE u.country = 'Россия' + ORDER BY orders_count DESC; + + **❓ Вопросы:** + + 1. Когда выполняется подзапрос в `SELECT`? + Подзапрос в SELECT вычисляется после того, как отработали секции FROM и WHERE + + 2. Можно ли использовать алиас `orders_count` в `WHERE`? + Нет, потому что WHERE не видит алиасы + + 3. Можно ли использовать его в `ORDER BY`? + Да, потому что order by выполняется после SELECT + + 4. Перепиши этот запрос через `JOIN`. Какой вариант быстрее? Проверь с `EXPLAIN ANALYZE`! + SELECT + u.name, + u.age, + COUNT(o.id) AS orders_count + FROM users u + LEFT JOIN orders o ON o.user_id = u.id + WHERE u.country = 'Россия' + GROUP BY u.id, u.name, u.age + ORDER BY orders_count DESC; + + Второй запрос через JOIN выполняется быстрее (0.13 и 0.1 мс соответственно) \ No newline at end of file diff --git a/dmedvedev/10.6.Logic_and_Physics_joins.sql b/dmedvedev/10.6.Logic_and_Physics_joins.sql new file mode 100644 index 0000000..fe6faa4 --- /dev/null +++ b/dmedvedev/10.6.Logic_and_Physics_joins.sql @@ -0,0 +1,192 @@ +- **Задача 1** — Когда какой метод? + + ✅ Заполни таблицу: + + | **Ситуация** | **Какой JOIN выберет база?** | **Почему?** | + | --- | --- | --- | + | 10 пользователей, 1 млн заказов, есть индекс на orders.user_id | Nested Loop | Потому что есть индекс по одному полю и одна таблица мееьше другой сильно | + | 100 000 пользователей, 100 000 заказов, нет индексов | Hash Join | Нет ни индексов, ни сортировки | + | Обе таблицы отсортированы по ключу, есть индексы | Merge Join | Потому что есть и индексы и сортировка | + | Маленькая таблица (5 строк) соединяется с большой (1 млн строк), нет индекса | Hash Join | Потому что нет индекса | + + +- **Задача 2** — Размер таблиц имеет значение + + ✅ Создай тестовые данные: + + + -- Маленькая таблица (10 строк) + CREATE TABLE small_users AS + SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + + -- Большая таблица (100 000 строк) + CREATE TABLE big_orders AS + SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; + + + ✅ Теперь выполни: + + + -- Вариант 1: Без индекса + EXPLAIN ANALYZE + SELECT u.name, COUNT(o.id) + FROM small_users u + JOIN big_orders o ON u.id = o.user_id + GROUP BY u.name; + + -- Вариант 2: С индексом + CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); + + EXPLAIN ANALYZE + SELECT u.name, COUNT(o.id) + FROM small_users u + JOIN big_orders o ON u.id = o.user_id + GROUP BY u.name; + + **❓ Вопросы:** + + 1. Какой метод `JOIN` используется в каждом случае? + В обоих случаях у меня показало Merge Join, просто там где индекс - там Scan Index. + 2. Почему с индексом база может выбрать `Nested Loop`? + Потому что будет маленькая таблица, иногда проще заджойнить большую и маленькую Nested Loop. + 3. Насколько быстрее стало с индексом? + В 2,5 раза. + +- **Задача 3** — Влияние сортировки + + ✅ Выполни три варианта: + + -- Вариант 1: Без ORDER BY + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + JOIN orders o ON u.id = o.user_id; + + -- Вариант 2: С ORDER BY по ключу соединения + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + JOIN orders o ON u.id = o.user_id + ORDER BY u.id, o.user_id; + + -- Вариант 3: С ORDER BY по другому полю + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + JOIN orders o ON u.id = o.user_id + ORDER BY o.order_date; + + **❓Вопросы:** + + 1. Изменился ли тип `JOIN` во втором варианте? + В первом Hash, во втором - Merge. + 2. Появился ли `Merge Join`? + Да. + 3. Почему `ORDER BY` может повлиять на выбор `JOIN`? + Потому что появляется метод при соединении (при условии наличия order by), упрощающий join. + +- **Задача 4** — Найди узкое место + + ✅ Дан медленный запрос: + + SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date + FROM users u + JOIN orders o ON u.id = o.user_id + JOIN order_items oi ON o.id = oi.order_id + JOIN products p ON oi.product_id = p.id + WHERE u.country = 'Россия'; + + **✅ Задание:** + + 1. Выполни с `EXPLAIN ANALYZE` + 2. Посмотри, какие типы `JOIN` используются + Nested Loop and Hash Join. + 3. Есть ли `Seq Scan`? Если да — где? + Да, в трех местах. При чтении таблицы users, orders и products. + 4. Предложи, какие индексы нужно создать + Можно создать 3 индекса + 5. Создай индексы и проверь, изменились ли типы `JOIN` + Изменились, но не сильно, в основном все Hash, один внешний Nested + +- **Задача 5** — Многоступенчатый JOIN + + EXPLAIN ANALYZE + SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id + LEFT JOIN order_items oi ON o.id = oi.order_id + WHERE u.country = 'Россия' + GROUP BY u.name; + + **✅ Задание:** + + 1. Посмотри на план выполнения + 2. Какие методы `JOIN` используются на каждом уровне? + Hash right join 2 раза + 3. Почему для первого `JOIN` один метод, а для второго — другой? + У меня оба одинаковые, вероятнее всего из-за неверных индексов или чего-то другого. + 4. Сколько строк проходит через каждый `JOIN`? + По одной + +- **Задача 6** — `LEFT JOIN` vs `INNER JOIN` + + ✅ Сравни два запроса: + + -- Вариант A: INNER JOIN + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + INNER JOIN orders o ON u.id = o.user_id; + + -- Вариант B: LEFT JOIN + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + LEFT JOIN orders o ON u.id = o.user_id; + + **❓ Вопросы:** + + 1. Одинаковый ли тип физического JOIN используется? + Hash and Hash Right + 2. Есть ли разница в Execution Time? + Не сильно , скачет то в одну, то в другую сторону + 3. Почему LEFT JOIN может быть медленнее? + + +- **Задача 7** — Порядок таблиц в JOIN + + ✅ Сравни: + + -- Вариант 1: users JOIN orders + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM users u + JOIN orders o ON u.id = o.user_id; + + -- Вариант 2: orders JOIN users + EXPLAIN ANALYZE + SELECT u.name, o.order_date + FROM orders o + JOIN users u ON o.user_id = u.id; + + **❓ Вопросы:** + + 1. Изменился ли тип `JOIN`? + нет + 2. Изменился ли порядок чтения таблиц в плане? + нет + 3. Одинаковое ли `Execution Time`? + Да + 4. Почему оптимизатор может выбрать разный порядок? + потому чтт ему будет так проще соединять таблицы и читать данные + diff --git a/dmedvedev/11.0 Pet project DDL DML.sql b/dmedvedev/11.0 Pet project DDL DML.sql new file mode 100644 index 0000000..05c768f --- /dev/null +++ b/dmedvedev/11.0 Pet project DDL DML.sql @@ -0,0 +1,230 @@ +--Создание таблиц +CREATE TABLE pet_users ( + id INT PRIMARY KEY NOT NULL, + name VARCHAR(50) NOT NULL, + age INT, + email VARCHAR(100) NOT NULL, + registration_date DATE NOT NULL +); + +CREATE TABLE pet_courses ( + id INT PRIMARY KEY NOT NULL, + title VARCHAR(100) NOT NULL, + category VARCHAR(50) NOT NULL, + created_at DATE NOT NULL +); + +CREATE TABLE pet_lessons( + id INT PRIMARY KEY NOT NULL, + course_id INT NOT NULL, + FOREIGN KEY (course_id) REFERENCES pet_courses (id), + title VARCHAR(50), + duration_min INT NOT NULL + ); + +CREATE TABLE pet_enrollments ( + id INT PRIMARY KEY NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES pet_users (id), + course_id INT NOT NULL, + FOREIGN KEY (course_id) REFERENCES pet_courses (id), + enrolled_at DATE NOT NULL +); + + +CREATE TABLE pet_lesson_views( + id INT PRIMARY KEY NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES pet_users (id), + lesson_id INT NOT NULL, + FOREIGN KEY(lesson_id) REFERENCES pet_lessons (id), + viewed_at TIMESTAMPTZ NOT NULL +); + +--Добавление данных в таблицы +INSERT INTO pet_users ( + id, + name, + age, + email, + registration_date +) +VALUES + (1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), + (2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), + (3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), + (4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), + (5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), + (6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), + (7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), + (8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), + (9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), + (10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), + (11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), + (12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), + (13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), + (14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), + (15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), + (16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), + (17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), + (18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), + (19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), + (20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +INSERT INTO pet_courses ( + id, + title, + category, + created_at +) +VALUES + (1, 'SQL для начинающих', 'data', '2023-01-01'), + (2, 'Python для анализа данных', 'programming', '2023-01-15'), + (3, 'BI с нуля', 'business', '2023-03-01'), + (4, 'Excel для аналитиков', 'business', '2023-02-01'), + (5, 'Основы машинного обучения', 'data', '2023-03-15'); + +INSERT INTO pet_lessons ( + id, + course_id, + title, + duration_min +) +VALUES + (1, 1, 'SELECT и FROM', 10), + (2, 1, 'JOIN', 15), + (3, 1, 'WHERE и фильтрация', 12), + (4, 1, 'Агрегации и GROUP BY', 18), + (5, 1, 'Подзапросы', 25), + (6, 2, 'Pandas основы', 20), + (7, 2, 'NumPy', 15), + (8, 2, 'Визуализация данных', 22), + (9, 3, 'Основы BI', 12), + (10, 3, 'Метрики и дашборды', 18), + (11, 3, 'Power BI введение', 20), + (12, 4, 'Формулы в Excel', 15), + (13, 4, 'Сводные таблицы', 18), + (14, 4, 'Графики и диаграммы', 14), + (15, 5, 'Введение в ML', 20), + (16, 5, 'Линейная регрессия', 30), + (17, 5, 'Классификация', 28), + (18, 5, 'Кластеризация', 25); + +INSERT INTO pet_enrollments ( + id, + user_id, + course_id, + enrolled_at +) +VALUES + (1, 1, 1, '2023-01-15'), + (2, 1, 2, '2023-02-01'), + (3, 2, 1, '2023-01-20'), + (4, 3, 2, '2023-03-05'), + (5, 4, 3, '2023-04-01'), + (6, 6, 1, '2023-01-06'), + (7, 6, 2, '2023-01-10'), + (8, 6, 4, '2023-02-05'), + (9, 7, 1, '2023-01-13'), + (10, 7, 3, '2023-03-10'), + (11, 8, 1, '2023-01-22'), + (12, 8, 2, '2023-01-22'), + (13, 8, 3, '2023-01-22'), + (14, 9, 4, '2023-02-12'), + (15, 10, 1, '2023-04-16'), + (16, 11, 2, '2023-04-21'), + (17, 12, 1, '2023-01-16'), + (18, 13, 2, '2023-01-19'), + (19, 13, 4, '2023-02-20'), + (20, 14, 1, '2023-01-09'), + (21, 14, 2, '2023-01-09'), + (22, 14, 3, '2023-01-09'), + (23, 14, 4, '2023-02-01'), + (24, 14, 5, '2023-03-20'), + (25, 15, 1, '2023-01-11'), + (26, 15, 3, '2023-03-05'), + (27, 16, 2, '2023-02-16'), + (28, 16, 4, '2023-02-16'), + (29, 17, 3, '2023-03-22'), + (30, 17, 5, '2023-03-25'), + (31, 18, 1, '2023-04-02'), + (32, 19, 1, '2023-04-11'), + (33, 19, 2, '2023-04-11'), + (34, 20, 2, '2023-04-19'), + (35, 5, 5, '2023-03-20'), + (36, 1, 4, '2023-02-10'), + (37, 2, 3, '2023-03-15'), + (38, 3, 1, '2023-02-25'), + (39, 4, 2, '2023-03-10'), + (40, 6, 3, '2023-03-08'), + (41, 7, 2, '2023-02-01'), + (42, 9, 1, '2023-02-15'), + (43, 12, 3, '2023-03-10'), + (44, 15, 2, '2023-02-01'), + (45, 18, 4, '2023-04-05'); + +INSERT INTO pet_lesson_views ( + id, + user_id, + lesson_id, + viewed_at +) +VALUES + (1, 1, 1, '2023-01-16 10:00:00'), + (2, 1, 2, '2023-01-16 10:15:00'), + (3, 1, 3, '2023-01-17 09:00:00'), + (4, 1, 4, '2023-01-17 09:30:00'), + (5, 1, 4, '2023-01-17 11:00:00'), + (6, 1, 4, '2023-01-18 10:00:00'), + (7, 1, 6, '2023-02-02 14:00:00'), + (8, 1, 12, '2023-02-15 10:00:00'), + (9, 1, 13, '2023-02-15 10:30:00'), + (10, 2, 1, '2023-01-21 09:00:00'), + (11, 2, 9, '2023-03-16 10:00:00'), + (12, 2, 10, '2023-03-16 10:30:00'), + (13, 3, 6, '2023-03-06 12:00:00'), + (14, 3, 1, '2023-02-26 11:00:00'), + (15, 3, 2, '2023-02-26 11:20:00'), + (16, 4, 9, '2023-04-02 10:00:00'), + (17, 4, 10, '2023-04-03 10:00:00'), + (18, 4, 6, '2023-03-12 14:00:00'), + (19, 4, 7, '2023-03-13 09:00:00'), + (20, 5, 15, '2023-03-21 10:00:00'), + (21, 5, 16, '2023-03-22 11:00:00'), + (22, 6, 1, '2023-01-07 10:00:00'), + (23, 6, 2, '2023-01-07 10:15:00'), + (24, 6, 3, '2023-01-08 09:00:00'), + (25, 6, 4, '2023-01-08 09:30:00'), + (26, 6, 5, '2023-01-09 10:00:00'), + (27, 6, 6, '2023-01-11 14:00:00'), + (28, 6, 7, '2023-01-12 10:00:00'), + (29, 6, 8, '2023-01-13 11:00:00'), + (30, 6, 12, '2023-02-06 09:00:00'), + (31, 6, 13, '2023-02-06 09:30:00'), + (32, 6, 14, '2023-02-07 10:00:00'), + (33, 6, 9, '2023-03-09 11:00:00'), + (34, 7, 1, '2023-01-14 10:00:00'), + (35, 7, 2, '2023-01-14 10:20:00'), + (36, 7, 3, '2023-01-15 09:00:00'), + (37, 7, 9, '2023-03-11 14:00:00'), + (38, 7, 10, '2023-03-11 14:30:00'), + (39, 9, 12, '2023-02-13 10:00:00'), + (40, 9, 13, '2023-02-13 10:30:00'), + (41, 9, 1, '2023-02-16 11:00:00'), + (42, 12, 1, '2023-01-17 10:00:00'), + (43, 12, 2, '2023-01-17 10:20:00'), + (44, 12, 1, '2023-04-10 11:00:00'), + (45, 12, 3, '2023-04-10 11:30:00'), + (46, 12, 9, '2023-03-12 10:00:00'), + (47, 13, 6, '2023-01-20 14:00:00'), + (48, 13, 7, '2023-01-21 10:00:00'), + (49, 13, 12, '2023-02-21 09:00:00'), + (50, 14, 1, '2023-01-09 08:00:00'), + (51, 14, 2, '2023-01-09 08:15:00'), + (52, 14, 3, '2023-01-10 10:00:00'), + (53, 14, 4, '2023-01-10 10:30:00'), + (54, 14, 5, '2023-01-11 09:00:00'), + (55, 14, 6, '2023-01-10 14:00:00'), + (56, 14, 7, '2023-01-11 10:00:00'), + (57, 14, 8, '2023-01-12 11:00:00'), + (58, 14, 9, '2023-01-10 08:00:00'); \ No newline at end of file diff --git a/dmedvedev/11.1 Pet project Activity users.sql b/dmedvedev/11.1 Pet project Activity users.sql new file mode 100644 index 0000000..cb731cd --- /dev/null +++ b/dmedvedev/11.1 Pet project Activity users.sql @@ -0,0 +1,82 @@ +--Активность пользователей + +--CREATE MATERIALIZED VIEW user_activity_summary AS + +--CTE кол-во курсов на которые записан пользователь +WITH enrollment_count AS ( + SELECT + pe.user_id, + COUNT(*) AS enrollment_count + FROM pet_enrollments pe + GROUP BY pe.user_id +), +--CTE просмотренные уникальные уроки пользователем (кол-во) +viewed_lessons_count AS ( + SELECT + plv.user_id, + DENSE_RANK() OVER( + PARTITION BY plv.user_id + ORDER BY plv.lesson_id + ) AS viewed_lessons_count + FROM pet_lesson_views plv +), + +--CTE дата и время последнего просмотра урока +last_view AS ( + SELECT + plv.user_id, + LAST_VALUE(plv.viewed_at) OVER ( + PARTITION BY plv.user_id + ORDER BY plv.viewed_at + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_view + FROM pet_lesson_views plv +), + +--CTE дата и время первого просмотра урока +first_view AS ( + SELECT + plv.user_id, + FIRST_VALUE(plv.viewed_at) OVER ( + PARTITION BY plv.user_id + ORDER BY plv.viewed_at + ) AS first_view + FROM pet_lesson_views plv +), + +--CTE кол-во курсов доступных пользователю +lesson_user_all_open AS ( + SELECT + pe.user_id, + pe.course_id, + COUNT(*) AS lesson_user_all_open + FROM pet_enrollments pe + LEFT JOIN pet_lessons pl ON pl.course_id = pe.course_id + GROUP BY pe.user_id, pe.course_id + ORDER BY user_id, course_id +) + +SELECT + pu.id AS user_id, + pu.name AS name, + ec.enrollment_count AS enrollment_count, + MAX(vlc.viewed_lessons_count) AS viewed_lessons_count, + last_view, + first_view, + ROUND(MAX(vlc.viewed_lessons_count) * 100.0 / lesson_user_all.lesson_user_all_open2, 2) || '%' AS engagement_rate + +FROM pet_users pu +LEFT JOIN enrollment_count ec ON pu.id = ec.user_id +LEFT JOIN viewed_lessons_count vlc ON pu.id = vlc.user_id +LEFT JOIN last_view lv ON pu.id = lv.user_id +LEFT JOIN first_view fv ON pu.id = fv.user_id +LEFT JOIN ( + SELECT + user_id, + SUM(luao.lesson_user_all_open) AS lesson_user_all_open2 + FROM lesson_user_all_open luao + GROUP BY user_id +) AS lesson_user_all ON lesson_user_all.user_id = pu.id + +GROUP BY pu.id, ec.enrollment_count, last_view, first_view, lesson_user_all.lesson_user_all_open2 +ORDER BY pu.id; \ No newline at end of file diff --git a/dmedvedev/11.2 Pet project Effective courses.sql b/dmedvedev/11.2 Pet project Effective courses.sql new file mode 100644 index 0000000..e1e8b7e --- /dev/null +++ b/dmedvedev/11.2 Pet project Effective courses.sql @@ -0,0 +1,82 @@ +CREATE MATERIALIZED VIEW course_summary AS + +--кол-во уникальных пользователей в рамках одного курса (ready) +WITH unique_users AS ( + SELECT + pe.course_id, + COUNT(DISTINCT pe.user_id) AS unique_users + FROM pet_enrollments pe + GROUP BY pe.course_id + ORDER BY pe.course_id +), + +--общее кол-во уроков + lesson_count AS ( + SELECT + pl.course_id, + COUNT(*) AS lesson_count + FROM pet_lessons pl + GROUP BY pl.course_id + ORDER BY pl.course_id + +), + +--суммарное число просмотров уроков по курсу +total_views AS ( + SELECT + pc.id, + COUNT(*) AS total_views + FROM pet_courses pc + JOIN pet_lessons pl ON pc.id = pl.course_id + JOIN pet_lesson_views plv ON pl.id = plv.lesson_id + GROUP BY pc.id + ORDER BY pc.id + +), + +--дата и время первого просмотра любого урока курса +first_viewed_at AS ( + SELECT + pc.id, + FIRST_VALUE(plv.viewed_at) OVER ( + PARTITION BY pc.id + ORDER BY plv.viewed_at + ) AS first_viewed_at + FROM pet_courses pc + JOIN pet_lessons pl ON pc.id = pl.course_id + JOIN pet_lesson_views plv ON pl.id = plv.lesson_id +), + +--дата и время последнего просмотра любого урока курса +last_viewed_at AS ( + SELECT + pc.id, + LAST_VALUE(plv.viewed_at) OVER ( + PARTITION BY pc.id + ORDER BY plv.viewed_at + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_viewed_at + FROM pet_courses pc + JOIN pet_lessons pl ON pc.id = pl.course_id + JOIN pet_lesson_views plv ON pl.id = plv.lesson_id +) + +SELECT + pc.id AS course_id, + pc.title AS course_title, + uu.unique_users, + lc.lesson_count, + tv.total_views, + ROUND(tv.total_views * 1.0 / uu.unique_users, 2) AS avg_views_per_user, + first_viewed_at, + last_viewed_at + +FROM pet_courses pc +LEFT JOIN unique_users uu ON pc.id = uu.course_id +LEFT JOIN lesson_count lc ON lc.course_id = pc.id +LEFT JOIN total_views tv ON tv.id = pc.id +LEFT JOIN first_viewed_at fva ON fva.id = pc.id +LEFT JOIN last_viewed_at lva ON lva.id = pc.id + +GROUP BY pc.id, uu.unique_users, lc.lesson_count, tv.total_views, first_viewed_at, last_viewed_at +ORDER BY course_id; \ No newline at end of file diff --git a/dmedvedev/11.3 Pet project platform_summary.sql b/dmedvedev/11.3 Pet project platform_summary.sql new file mode 100644 index 0000000..dc557c9 --- /dev/null +++ b/dmedvedev/11.3 Pet project platform_summary.sql @@ -0,0 +1,20 @@ +CREATE MATERIALIZED VIEW platform_summary AS + +WITH stats AS ( + SELECT + (SELECT COUNT(*) total_users FROM pet_users pu) AS total_users, + (SELECT COUNT(*) total_courses FROM pet_courses pc) AS total_courses, + (SELECT COUNT(*) total_lessons FROM pet_lessons pl) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM pet_lesson_views plv) AS users_with_views, + (SELECT COUNT(*) FROM pet_lesson_views plv) AS total_views +) + +SELECT + total_users, + total_courses, + total_lessons, + users_with_views, + + ROUND((total_lessons * 1.0 / total_courses), 2) AS avg_lessons_per_course, + ROUND((total_views * 1.0 / total_lessons), 2) AS avg_views_per_lesson +FROM stats; \ No newline at end of file diff --git a/dmedvedev/11.4 Pet project Cohort_analysis.sql b/dmedvedev/11.4 Pet project Cohort_analysis.sql new file mode 100644 index 0000000..ac0f2c1 --- /dev/null +++ b/dmedvedev/11.4 Pet project Cohort_analysis.sql @@ -0,0 +1,38 @@ +CREATE MATERIALIZED +VIEW user_cohort_analysis AS + +SELECT + TO_CHAR(pu.cohort_month, 'YYYY-MM') AS month, + COUNT(DISTINCT pu.id) AS users_in_cohort, + COUNT(DISTINCT pe.user_id) AS enrolled_users, + COUNT(DISTINCT plv.user_id) AS active_users, + ROUND(COUNT(DISTINCT plv.user_id) * 100.0 / COUNT(DISTINCT pu.id), 2) || '%' AS retention_rate, + ROUND(COUNT(DISTINCT pe.id) * 1.0 / COUNT(DISTINCT pu.id), 2) AS avg_courses_per_user, + ( + SELECT + ROUND(AVG(table_first_viewed_at.first_viewed_at::date - pu2.registration_date::date), 2) + FROM pet_users pu2 + JOIN ( + SELECT + plv2.user_id, + MIN(plv2.viewed_at) AS first_viewed_at + FROM pet_lesson_views plv2 + GROUP BY plv2.user_id + ) AS table_first_viewed_at ON pu2.id = table_first_viewed_at.user_id + WHERE DATE_TRUNC('month', pu2.registration_date) = pu.cohort_month + ) AS avg_time_to_first_view + +FROM ( + SELECT + *, + DATE_TRUNC('month', registration_date) AS cohort_month + FROM pet_users +) AS pu + LEFT JOIN pet_enrollments pe ON pu.id = pe.user_id + LEFT JOIN pet_lesson_views plv ON pu.id = plv.user_id + +GROUP BY pu.cohort_month +ORDER BY pu.cohort_month; + + + diff --git a/dmedvedev/11.5 Pet project Lesson_popularity_ranking.sql b/dmedvedev/11.5 Pet project Lesson_popularity_ranking.sql new file mode 100644 index 0000000..58f402e --- /dev/null +++ b/dmedvedev/11.5 Pet project Lesson_popularity_ranking.sql @@ -0,0 +1,64 @@ +CREATE MATERIALIZED VIEW lesson_popularity_ranking AS + +SELECT + pl.id AS lesson_id, + pl.title AS lesson_title, + pc.id AS course_id, + pc.title AS course_title, + COALESCE(total_views_lesson.total_views, 0) AS total_views, + COALESCE(table_unique_viewers.unique_v, 0) AS unique_viewers, + ROUND(AVG(views_one_user_per_one_lesson.views_per_viewer), 2) AS avg_views_per_viewer, --также это можно было посчитать total_views/unique_viewers + DENSE_RANK() OVER ( + PARTITION BY pc.id + ORDER BY total_views_lesson.total_views DESC NULLS LAST + ) AS popularity_rank, + + ROUND(COALESCE(table_unique_viewers.unique_v, 0) * 100.0 / NULLIF(table_enrolled_users.enrolled_to_course, 0), 2) || '%' AS completion_rate + + FROM pet_lessons pl + + LEFT JOIN ( + SELECT + lesson_id, + COUNT(*) AS total_views + FROM pet_lesson_views + GROUP BY lesson_id + ) AS total_views_lesson + ON total_views_lesson.lesson_id = pl.id + + LEFT JOIN pet_courses pc + ON pc.id = pl.course_id + + LEFT JOIN ( + SELECT + lesson_id, + COUNT(DISTINCT user_id) AS unique_v + FROM pet_lesson_views plv + GROUP BY plv.lesson_id + ) AS table_unique_viewers + ON table_unique_viewers.lesson_id = pl.id + + LEFT JOIN ( + SELECT + lesson_id, + user_id, + COUNT(*) AS views_per_viewer + FROM pet_lesson_views plv + GROUP BY user_id, lesson_id + ORDER BY user_id, lesson_id + ) AS views_one_user_per_one_lesson + ON views_one_user_per_one_lesson.lesson_id = pl.id + + LEFT JOIN ( + SELECT + course_id, + COUNT(DISTINCT user_id) AS enrolled_to_course + FROM pet_enrollments pe + GROUP BY course_id + ) AS table_enrolled_users + ON table_enrolled_users.course_id = pc.id + + + + GROUP BY pl.id, pl.title, pc.id, pc.title, total_views_lesson.total_views, table_unique_viewers.unique_v, table_enrolled_users.enrolled_to_course + ORDER BY popularity_rank; \ No newline at end of file diff --git a/dmedvedev/11.6 Pet project Conversion_funnel.sql b/dmedvedev/11.6 Pet project Conversion_funnel.sql new file mode 100644 index 0000000..cc048d6 --- /dev/null +++ b/dmedvedev/11.6 Pet project Conversion_funnel.sql @@ -0,0 +1,156 @@ +CREATE MATERIALIZED +VIEW conversion_funnel AS + +-- CTE какое кол-во человек записалось на курс +WITH + + enrolled_users + AS + ( + SELECT + course_id, + user_id, + COUNT(*) OVER (PARTITION BY course_id) AS enrolled_count + FROM pet_enrollments pe + ), + + -- СТЕ на начавших смотреть курс + started_users + AS + ( + SELECT + eu.course_id, + COUNT(DISTINCT plv.user_id) AS started + FROM enrolled_users eu -- Базой берем записанных пользователей + LEFT JOIN pet_lessons pl + ON eu.course_id = pl.course_id + LEFT JOIN pet_lesson_views plv + ON plv.lesson_id = pl.id AND plv.user_id = eu.user_id + -- Проверяем, что смотрел именно записанный юзер + GROUP BY eu.course_id + ), + + + --СТЕ на кол-во просмотров курса и прогресс по курсу + user_progress + AS + ( + SELECT + eu.course_id, + eu.user_id, + COUNT(DISTINCT plv.lesson_id) * 1.0 / count_lesson_in_course.count_lesson_in_course AS progress + FROM enrolled_users eu -- Базой берем записанных пользователей + LEFT JOIN pet_lessons pl + ON pl.course_id = eu.course_id + LEFT JOIN pet_lesson_views plv + ON plv.lesson_id = pl.id AND plv.user_id = eu.user_id -- Просмотры могут быть NULL, если записался, но не смотрел + LEFT JOIN ( + SELECT + pl.course_id, + COUNT(*) AS count_lesson_in_course + FROM pet_lessons pl + GROUP BY pl.course_id + ) AS count_lesson_in_course + ON count_lesson_in_course.course_id = eu.course_id + GROUP BY eu.course_id, eu.user_id, count_lesson_in_course.count_lesson_in_course + ), + + main_table + AS + ( + SELECT + pc.id AS course_id, + pc.title AS course_title, + 'registered' AS stage_name, + 0 AS stage_order, + (SELECT COUNT(*) + FROM pet_users pu) AS users_count, + ROUND(100, 2) || '%' AS conversion_from_previous, + ROUND(100, 2) || '%' AS conversion_from_start + + FROM pet_courses pc + + UNION ALL + + SELECT + pc.id AS course_id, + pc.title AS course_title, + 'enrolled' AS stage_name, + 1 AS stage_order, + COALESCE(eu.enrolled_count, 0), + ROUND(COALESCE(eu.enrolled_count, 0) * 100.0 / NULLIF((SELECT COUNT(*) FROM pet_users), 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(eu.enrolled_count, 0) * 100.0 / NULLIF((SELECT COUNT(*) FROM pet_users), 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + LEFT JOIN (SELECT course_id, MAX(enrolled_count) AS enrolled_count + FROM enrolled_users + GROUP BY course_id) eu + ON eu.course_id = pc.id + + + UNION ALL + + SELECT + pc.id AS course_id, + pc.title AS course_title, + 'started' AS stage_name, + 2 AS stage_order, + COALESCE(su.started, 0), + ROUND(COALESCE(su.started, 0) * 100.0 / NULLIF(eu.enrolled_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(su.started, 0) * 100.0 / NULLIF((SELECT COUNT(*) FROM pet_users), 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + LEFT JOIN (SELECT course_id, MAX(enrolled_count) AS enrolled_count + FROM enrolled_users + GROUP BY course_id) eu + ON eu.course_id = pc.id + LEFT JOIN started_users su + ON su.course_id = pc.id + + UNION ALL + + SELECT + pc.id AS course_id, + pc.title AS course_title, + 'mid_progress' AS stage_name, + 3 AS stage_order, + (SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 0.5), + ROUND((SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 0.5) * 100.0 / NULLIF(su.started, 0), 2) || '%' AS conversion_from_previous, + ROUND((SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 0.5) * 100.0 / NULLIF((SELECT COUNT(*) FROM pet_users), 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + LEFT JOIN started_users su + ON su.course_id = pc.id + + UNION ALL + + SELECT + pc.id AS course_id, + pc.title AS course_title, + 'completed' AS stage_name, + 4 AS stage_order, + (SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 1), + ROUND((SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 1) * 100.0 / NULLIF((SELECT COUNT(*) FROM user_progress up WHERE up.course_id = pc.id AND up.progress >= 0.5), 0), 2) || '%' AS conversion_from_previous, + ROUND((SELECT COUNT(*) + FROM user_progress up + WHERE up.course_id = pc.id AND up.progress >= 1) * 100.0 / NULLIF((SELECT COUNT(*) FROM pet_users), 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + ) +-- main select +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start + +FROM main_table +ORDER BY course_id, stage_order; \ No newline at end of file diff --git a/dmedvedev/11.7 Pet Project Additional tasks.sql b/dmedvedev/11.7 Pet Project Additional tasks.sql new file mode 100644 index 0000000..ba5f05a --- /dev/null +++ b/dmedvedev/11.7 Pet Project Additional tasks.sql @@ -0,0 +1,436 @@ +-- 1. Задание 1: Анализ drop-off точек +-- Используя conversion_funnel, найди курсы с самым большим процентом отвала между этапами enrolled → started (WHERE stage_name IN (...)) +WITH drop_off AS ( + SELECT + course_id, + stage_name, + users_count, + LAG(users_count) OVER ( + PARTITION BY course_id + ORDER BY stage_name + ) AS prev + FROM conversion_funnel + WHERE stage_name IN ('enrolled', 'started') +) +SELECT + course_id, + ROUND((1 - users_count * 1.0 / NULLIF(prev, 0)) * 100, 2) || '%' AS percent_of_drop +FROM drop_off +WHERE stage_name = 'started' +ORDER BY (1 - users_count * 1.0 / NULLIF(prev, 0)) DESC; + + +-- ОТВЕТ: Самые большие проценты отвала между enrolled и started замечены на курсе 1 и 3 (62 и 67% соответственно). + +-- 2. Задание 2: Топ-3 урока с повторными просмотрами +-- Используя lesson_popularity_ranking, найди уроки, где avg_views_per_viewer > 1 (пользователи пересматривают) +SELECT * +FROM lesson_popularity_ranking +WHERE avg_views_per_viewer > 1; + +--Ответ: Это урок 1 'select и from' и урок 4 'агрегации и group by', больше уроков нет с где avg_views_per_viewer > 1. + +-- 3. Задание 3: Лучшая когорта по retention +-- Из user_cohort_analysis выбери месяц с максимальным retention_rate и проанализируй, почему (можно объединить с другими витринами) +SELECT * +FROM user_cohort_analysis; + +-- ОТВЕТ: По данному запросу это 2 когорты - январь и февраль 2023 года (по 75%). +-- К сожалению, сейчас не могу понять с какими витринами можно ее объединить... + + +-- 4. Задание 4: Анализ плана выполнения +-- Запусти `EXPLAIN ANALYZE` на всех витринах. Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT. +-- Перепиши запросы чтобы достигнуть минимального времени выполнения. +-- Напиши что сделал, почему, и насколько быстрее стал запрос + +*** ACTIVITY USERS *** +Стоимость я писал прогнозную, в конце указал реальные до/после. В Activity users у меня проблема с чтением таблиц, как я вижу оптимизатор показывает, что в какой-то момент он читает 3625 строк, хочу если подумать, таблица та не большие по факту, а когда будет рабочий проект, то там строк будет намного больше и из-за таких же ошибок как у меня сейчас, строк может стать в сотни раз больше. Причем итоговая стоимость составляет 380. +Сам я не смог придумать что можно было бы оптимизировать, обратился к ИИ, он предлагает оптимизировать таблицу, которую оптимизатор читает аж 3 раза (я с этим согласен полностью, вижу что в 3х СТЕ обращаюсь к одной и той же таблице). Также еще говорит, что у меня бесполезный GROUP BY в конце. Теперь я попробую это оптимизировать самостоятельно без использования ИИ. +После сокращения 3х СТЕ до одной получилась стоимость 364, лучше, но все равно как-будто много, однако, максимальное количество строк уже не 3625, а 210. +Также я сам увидел бесполезную сортировку внутри СТЕ, убрал ее, стоимость стала 356. +Потом я понял, что зря вообще использовал оконные функции DENSE_RANK, FIRST_VALUE, LAST_VALUE, можно обойтись агрегатными функциями, я сделал замену и получил стоимость 342. +Я убрал подзапрос из конца и сделал его проще в СТЕ, теперь нет подзапроса. +По итогу стоимость теперь 325. А фактическая 0.183, а изначально было 2.174 (снижение более чем в 10 раз). + +ОПТИМИЗИРОВАННЫЙ КОД +EXPLAIN ANALYZE +--Активность пользователей +CREATE MATERIALIZED VIEW user_activity_summary AS + +--CTE кол-во курсов на которые записан пользователь +WITH enrollment_count AS ( + SELECT + pe.user_id, + COUNT(*) AS enrollment_count + FROM pet_enrollments pe + GROUP BY pe.user_id +), + +--CTE просмотренные уникальные уроки пользователем (кол-во), last view and first view +viewed_lessons_count_max_min AS ( + SELECT + plv.user_id, + COUNT(DISTINCT plv.lesson_id) AS viewed_lessons_count, + MIN(plv.viewed_at) AS first_view, + MAX(plv.viewed_at) AS last_view + + FROM pet_lesson_views plv + GROUP BY plv.user_id +), + + +--CTE кол-во доступных уроков пользователю +lesson_user_all AS ( + SELECT + pe.user_id, + COUNT(pl.id) AS lesson_user_all_open + FROM pet_enrollments pe + LEFT JOIN pet_lessons pl ON pl.course_id = pe.course_id + GROUP BY pe.user_id + +) +--main запрос +SELECT + pu.id AS user_id, + pu.name AS name, + ec.enrollment_count AS enrollment_count, + vlcmm.viewed_lessons_count, + vlcmm.first_view, + vlcmm.last_view, + ROUND(vlcmm.viewed_lessons_count * 100.0 / lua.lesson_user_all_open, 2) || '%' AS engagement_rate + +FROM pet_users pu +LEFT JOIN enrollment_count ec ON pu.id = ec.user_id +LEFT JOIN viewed_lessons_count_max_min vlcmm ON pu.id = vlcmm.user_id +LEFT JOIN lesson_user_all lua ON lua.user_id = pu.id +ORDER BY pu.id; + +*** EFFECTIVE COURSES *** +actual time моего кода составляет 0.992, количество строк при сортировке максимум 1022. +Для начала убрал ненужные сортировки в СТЕ. +Для поиска первых и последних времен просмотра уроков в курсе убрал из использования оконные функции, заменил на агрегаты MIN, MAX и объединил эти 2 СТЕ в одну плюс к ним также внес еще одну СТЕ внутрб, которая ссылалась на pet_lesson_views. +Почему-то у меня в главном селекте была группировка, убрал ее. +По итогу фактическое время стало 0.120, а максимальное количество строк 510. + +--Эффективность курсов +--ОПТИМИЗИРОВАННЫЙ КОД +CREATE MATERIALIZED VIEW course_summary AS + +--кол-во уникальных пользователей в рамках одного курса (ready) +WITH unique_users AS ( + SELECT + pe.course_id, + COUNT(DISTINCT pe.user_id) AS unique_users + FROM pet_enrollments pe + GROUP BY pe.course_id +), + +--общее кол-во уроков по курсу +lesson_count AS ( + SELECT + pl.course_id, + COUNT(*) AS lesson_count + FROM pet_lessons pl + GROUP BY pl.course_id + +), + +--дата и время первого и последнего просмотра любого урока курса, суммарное число просмотров уроков по курсу +first_last_viewed_at AS ( + SELECT + pl.course_id, + MIN(plv.viewed_at) AS first_viewed_at, + MAX(plv.viewed_at) AS last_viewed_at, + COUNT(plv.id) AS total_views + FROM pet_lessons pl + LEFT JOIN pet_lesson_views plv ON pl.id = plv.lesson_id + GROUP BY pl.course_id +) + +SELECT + pc.id AS course_id, + pc.title AS course_title, + COALESCE(uu.unique_users, 0), + COALESCE(lc.lesson_count, 0), + COALESCE(flva.total_views, 0), + ROUND(COALESCE(flva.total_views, 0) * 1.0 / NULLIF(uu.unique_users, 0), 2) AS avg_views_per_user, + flva.first_viewed_at, + flva.last_viewed_at + +FROM pet_courses pc +LEFT JOIN unique_users uu ON pc.id = uu.course_id +LEFT JOIN lesson_count lc ON pc.id = lc.course_id +LEFT JOIN first_last_viewed_at flva ON pc.id = flva.course_id + +ORDER BY course_id; + +*** PLATFORM SUMMARY *** +У меня и так маленький базовый запрос, поэтому я попытался его оптимизировать как знаю, но думаю, что это не ведет к чему-то более эффективному. +actual time примерно одинаковое . +execution time тоже скачет везде , но как-будто одинаковое. +По итогу я убрал СТЕ и все поместил внутрь одного селекта. У меня как и было 8 Seq Scan, так и осталось. + +EXPLAIN ANALYZE +SELECT + (SELECT COUNT(*) total_users FROM pet_users pu) AS total_users, + (SELECT COUNT(*) total_courses FROM pet_courses pc) AS total_courses, + (SELECT COUNT(*) total_lessons FROM pet_lessons pl) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM pet_lesson_views plv) AS users_with_views, + ROUND(((SELECT COUNT(*) total_lessons FROM pet_lessons pl) * 1.0 / (SELECT COUNT(*) total_courses FROM pet_courses pc)), 2) AS avg_lessons_per_course, + ROUND(((SELECT COUNT(*) FROM pet_lesson_views plv) * 1.0 / (SELECT COUNT(*) total_lessons FROM pet_lessons pl)), 2) AS avg_views_per_lesson; + +НО по итогу я зашел в ИИ и он сказал, что для решения такой задачи можно использовать CROSS JOIN для которого кстати как я теперь понимаю не нужно соединение по индексу через ON. +КОНЕЧНЫЙ ВАИАНТ СЛЕДУЮЩИЙ, В НЕМ УЖЕ 5 РАЗ POSTGRE ЧИТАЮТ ТАБЛИЦЫ, А НЕ 8. Execution time упало на 30%. + +EXPLAIN ANALYZE +SELECT + tu.total_users, + tc.total_courses, + tl.total_lessons, + fplv.users_with_views, + ROUND((tl.total_lessons * 1.0 / tc.total_courses), 2) AS avg_lessons_per_course, + ROUND((fplv.total_views * 1.0 / tl.total_lessons), 2) AS avg_views_per_lesson +FROM + (SELECT COUNT(*) AS total_users FROM pet_users pu) tu +CROSS JOIN + (SELECT COUNT(*) AS total_courses FROM pet_courses pc) tc +CROSS JOIN + (SELECT COUNT(*) AS total_lessons FROM pet_lessons pl) tl +CROSS JOIN + (SELECT + COUNT(DISTINCT user_id) AS users_with_views, + COUNT(*) AS total_views + FROM pet_lesson_views plv + ) fplv; + +*** COHORT ANALYSIS *** +В данном случае исходный код показывал Execution time 0.45, а после преобразований 0.33. Вижу проблему множественного обращения к одним и тем же таблицам. +Теперь код реализован через СТЕ, сначала считаются метрика по зарегист пользователям, потом по записавшимся, потом по перввым просмотрам (эти же пользователи берутся как активные в дальнейших расчетах). +Далее делаем таблицу под каждого пользователя в другой СТЕ. +И в конечном запросе собираем все в когорты. + +ИТОГОВЫЙ КОД: +--CREATE MATERIALIZED VIEW user_cohort_analysis AS +EXPLAIN ANALYZE + +WITH +user_registraion AS ( + SELECT + pu.id AS user_id, + TO_CHAR(DATE_TRUNC('month', pu.registration_date),'YYYY-MM') AS MONTH + FROM pet_users pu +), + +user_enrollments_summary AS ( + SELECT + pe.user_id AS user_id, + COUNT(DISTINCT pe.course_id) AS unique_courses_count + FROM pet_enrollments pe + GROUP BY user_id +), + +first_viewed AS ( + SELECT + plv.user_id AS user_id, + MIN(viewed_at) AS first_viewed_date_for_each_user + FROM pet_lesson_views plv + GROUP BY user_id +), + +user_metrics AS ( + SELECT + pu.id AS user_id, + DATE_TRUNC('month', pu.registration_date) AS MONTH, + CASE WHEN ur.user_id IS NOT NULL THEN 1 ELSE 0 END AS registration_user, + CASE WHEN ues.user_id IS NOT NULL THEN 1 ELSE 0 END AS enrolled_user, + CASE WHEN fv.user_id IS NOT NULL THEN 1 ELSE 0 END AS active_user, + COALESCE(ues.unique_courses_count, 0) AS courses_count, + fv.first_viewed_date_for_each_user::date - registration_date AS differens_between_reg_and_fv + FROM pet_users pu + LEFT JOIN user_registraion ur ON pu.id = ur.user_id + LEFT JOIN user_enrollments_summary ues ON pu.id = ues.user_id + LEFT JOIN first_viewed fv ON pu.id = fv.user_id +) + +SELECT + TO_CHAR(user_metrics.MONTH, 'YYYY-MM') AS cohort_month, + COUNT(DISTINCT user_metrics.user_id) AS users_in_cohort, + SUM(user_metrics.enrolled_user) AS enrolled_users, + SUM(user_metrics.active_user) AS active_users, + ROUND(SUM(user_metrics.registration_user * 100.0) / SUM(user_metrics.enrolled_user), 2) || '%' AS retention_rate, + ROUND(SUM(user_metrics.courses_count) / SUM(user_metrics.enrolled_user), 2) AS avg_courses_per_user, + AVG(user_metrics.differens_between_reg_and_fv) AS avg_time_to_first_view +FROM user_metrics +GROUP BY cohort_month +ORDER BY cohort_month; + +*** LESSON POPULARITY RANKING*** + +Вижу раздутость кода. Колчиество строк достигает 1850. В одном запросе множество подзапросов, для начала лучше все закинуть в разные СТЕ. +По исходному коду Execution time от 0.4 до 1.8, по оптимизированному 0.27-0.4. +Я обращался к одной таблице 3 раза, а также переписал запрос и в конце не нужна группировка. +Но проблему 1850 строк не смог решить. Вообще возможно ли ее решить? + +WITH +lesson_views_summary AS ( + SELECT + lesson_id, + COUNT(*) AS total_views, + COUNT(DISTINCT user_id) AS unique_v, + COUNT(*) * 1.0 / COUNT(DISTINCT user_id) AS views_one_user_per_one_lesson + FROM pet_lesson_views + GROUP BY lesson_id +), + +table_enrolled_users AS ( + SELECT + course_id, + COUNT(DISTINCT user_id) AS enrolled_to_course + FROM pet_enrollments pe + GROUP BY course_id +) + + +SELECT + pl.id AS lesson_id, + pl.title AS lesson_title, + pc.id AS course_id, + pc.title AS course_title, + COALESCE(lvs.total_views, 0) AS total_views, + COALESCE(lvs.unique_v, 0) AS unique_viewers, + COALESCE(ROUND(lvs.views_one_user_per_one_lesson, 2), 0) AS avg_views_per_viewer, --также это можно было посчитать total_views/unique_viewers + DENSE_RANK() OVER ( + PARTITION BY pc.id + ORDER BY lvs.total_views DESC NULLS LAST) AS popularity_rank, + + ROUND(COALESCE(lvs.unique_v, 0) * 100.0 / NULLIF(teu.enrolled_to_course, 0), 2) || '%' AS completion_rate + + FROM pet_lessons pl + LEFT JOIN lesson_views_summary lvs ON lvs.lesson_id = pl.id + LEFT JOIN pet_courses pc ON pc.id = pl.course_id + LEFT JOIN table_enrolled_users teu ON teu.course_id = pc.id + + ORDER BY popularity_rank; + + +*** CONVENSION FUNNEL *** +В моем запросе выполняется порядка 7 SubPlan, execution time 60 ms. +Я в своем новом запросе создал СТЕ, которая один раз считает количество пользователей на платформе. +Далее 1 раз считаем кол-во уроков в каждом курсе, собираем все метрики на каждого юсера на курсе. И строим воронку. +У оптимизированного запроса execution time составляет 0.8 ms. + +--CREATE MATERIALIZED VIEW conversion_funnel AS + +--Считаем общее число пользователей на платформе +WITH total_platform_users AS ( + SELECT COUNT(*) AS total_users FROM pet_users +), + +--Считаем количество уроков в каждом курсе +lessons_per_course AS ( + SELECT + course_id, + COUNT(*) AS total_lessons + FROM pet_lessons + GROUP BY course_id +), + +-- Собираем ВСЕ метрики каждого студента на курсе за один проход +user_course_progress AS ( + SELECT + pe.course_id, + pe.user_id, + COUNT(DISTINCT plv.lesson_id) * 1.0 / NULLIF(lpc.total_lessons, 0) AS progress + FROM pet_enrollments pe + LEFT JOIN pet_lessons pl ON pl.course_id = pe.course_id + -- Строго проверяем просмотры только для этого записанного пользователя + LEFT JOIN pet_lesson_views plv ON plv.lesson_id = pl.id AND plv.user_id = pe.user_id + LEFT JOIN lessons_per_course lpc ON lpc.course_id = pe.course_id + GROUP BY pe.course_id, pe.user_id, lpc.total_lessons +), + +-- Получаем метрики студентов по курсу +course_funnel_aggregates AS ( + SELECT + course_id, + COUNT(DISTINCT user_id) AS enrolled_count, + -- Начал смотреть = посмотрел хотя бы 1 урок (прогресс > 0) + COUNT(DISTINCT CASE WHEN progress > 0 THEN user_id END) AS started_count, + -- Прошел половину = прогресс >= 50% + COUNT(DISTINCT CASE WHEN progress >= 0.5 THEN user_id END) AS mid_count, + -- Завершил = прогресс >= 100% + COUNT(DISTINCT CASE WHEN progress >= 1.0 THEN user_id END) AS completed_count + FROM user_course_progress + GROUP BY course_id +), + +-- Строим воронку +main_table AS ( +-- Зарегистрированы + SELECT + pc.id AS course_id, pc.title AS course_title, 'registered' AS stage_name, 0 AS stage_order, + tpu.total_users AS users_count, + '100.00%' AS conversion_from_previous, + '100.00%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + + UNION ALL + +--Записались на курс + SELECT + pc.id AS course_id, pc.title AS course_title, 'enrolled' AS stage_name, 1 AS stage_order, + COALESCE(cfa.enrolled_count, 0) AS users_count, + ROUND(COALESCE(cfa.enrolled_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.enrolled_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Начали смотреть + SELECT + pc.id AS course_id, pc.title AS course_title, 'started' AS stage_name, 2 AS stage_order, + COALESCE(cfa.started_count, 0) AS users_count, + ROUND(COALESCE(cfa.started_count, 0) * 100.0 / NULLIF(cfa.enrolled_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.started_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Прошли половину курса + SELECT + pc.id AS course_id, pc.title AS course_title, 'mid_progress' AS stage_name, 3 AS stage_order, + COALESCE(cfa.mid_count, 0) AS users_count, + ROUND(COALESCE(cfa.mid_count, 0) * 100.0 / NULLIF(cfa.started_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.mid_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Полностью завершили курс + SELECT + pc.id AS course_id, pc.title AS course_title, 'completed' AS stage_name, 4 AS stage_order, + COALESCE(cfa.completed_count, 0) AS users_count, + ROUND(COALESCE(cfa.completed_count, 0) * 100.0 / NULLIF(cfa.mid_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.completed_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id +) + +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start +FROM main_table +ORDER BY course_id, stage_order; diff --git a/dmedvedev/2.1.theory_bd_indexes.sql b/dmedvedev/2.1.theory_bd_indexes.sql new file mode 100644 index 0000000..fa83b9c --- /dev/null +++ b/dmedvedev/2.1.theory_bd_indexes.sql @@ -0,0 +1,203 @@ +- Задание 1: По каким полям делать индексы + + Возьми таблицы из пет проекта “Онлайн магазин”. + + **Ответь:** + + 1. По каким полям, как тебе кажется, реальный продукт‑менеджер или аналитик будет чаще всего: + - искать (`WHERE`), + по странам пользователя, по категориям техники, стомиость, возраст пользователей, статус заказа, активность пользователей. + - сортировать (`ORDER BY`), + количество заказов, стоимость, возраст пользователей, дата заказа. + - группировать (`GROUP BY`), + по пользователям, по возрасту пользователей, по дате заказа (когорты), по статусу заказа, по категориям. + - соединять (`JOIN`)? + по u.id, o.user_id, p.id, oi.order_id, oi.product_id + 2. Для каждого поля, которое ты назвал, укажи: «индекс нужен / индекс не нужен» и коротко объясни почему (часто фильтруем, редко фильтруем, мало разных значений и т.п.). + + по странам пользователя ДА + по категориям техники ДА + стомиость ДА + возраст пользователей НЕТ + статус заказа ДА + активность пользователей ДА + дата заказа ДА + по пользователям НЕТ + + по u.id ДА + o.user_id ДА + p.id ДА + oi.order_id ДА + oi.product_id ДА + + + +- Задание 2. Свяжи запрос и индекс + + **Придумай и напиши по одному SQL‑запросу для ситуаций:** + + 1. Найти пользователя по `email`. + SELECT + name, + email + FROM users + WHERE email = ''; + Здесь лучше всего подойдет индекс по email. Ускорит WHERE. + + 2. Показать всех активных пользователей (`is_active = true`), зарегистрированных за последний год (`created_at`). + SELECT + name , + created_at + FROM users + WHERE is_active = TRUE AND created_at > now() - interval '1 year'; + Здесь лучше всего подойдет индекс по is_active, created_at. Ускорит WHERE. + + 3. Показать все товары категории `'electronics'`, отсортированные по `price` от низкой к высокой. + SELECT + name , + price + FROM products + WHERE category = 'electronics' + ORDER BY price ASC; + Здесь лучше всего подойдет индекс по category, price. Ускорит WHERE и ORDER BY. + + 4. Показать все заказы конкретного пользователя за последние 30 дней по `user_id` и `order_date`. + SELECT + id, + o.id AS order_id, + order_date + FROM users u + LEFT JOIN orders o ON o.user_id = u.id + WHERE order_date > now() - interval '30 days' AND u.id = ; + Здесь лучше всего подойдет индекс по order_date, o.user_id, u.id. Ускорит JOIN и WHERE. + + + 5. Для каждого заказа показать товары в нём (используя `order_items`). + SELECT + o.id AS order_id, + p.name AS product_name + FROM orders o + JOIN order_items oi ON o.id = oi.order_id + JOIN products p ON p.id = oi.product_id; + Здесь лучше всего подойдут индексы по o.id, oi.order_id, p.id, oi.product_id. Ускорит JOIN. + + **После этого:** + + 1. Для **каждого** запроса предложи **один индекс**, который поможет его ускорить. + 2. Объясни словами: по каким полям индекс, в каком порядке (если их несколько) и какие части запроса он ускорит (`WHERE`, `JOIN`, `ORDER BY`). + + ***(Важно: сейчас не писать `CREATE INDEX` — только понять, какие индексы логичны.)*** + +- Задание 3. Порядок столбцов в индексе + + Смотри на таблицу `orders`. + + **Запросы:** + + 1. Все заказы пользователя за период: + + WHERE user_id = ? AND order_date BETWEEN ? AND ? + + 2. Все заказы за период (без фильтра по пользователю): + + WHERE order_date BETWEEN ? AND ? + + 3. Все заказы пользователя вообще: + + WHERE user_id = ? + + + **Вопросы:** + + 1. Какой **один** составной индекс по полям `user_id` и `order_date` ты бы сделал (в каком порядке)? + CREATE INDEX idx_user_od ON orders(user_id, order_date) + + 2. Для каких из трёх запросов этот индекс будет работать особенно хорошо, а для каких — хуже? Объясни. + Для первого особенно хорошо, потому что будет по двух индексам. + Для остальных хуже потому что они фильтруют по одному индексу. + + 3. Сравни варианты: + - один индекс `(user_id, order_date)` + плюс: работает быстрее если сортируем по обоим столбцам + минус: не работает вообще если сортировать по order_date + + - два индекса: `(user_id)` и `(order_date)` + плюс: гибкость + минус: требует больше места, иногда делает lookup + + Напиши, в чём плюс и минус каждого подхода. + +- Задание 4. Лишние индексы в наших таблицах + + **Для каждой таблицы:** + 1. users(...) + 2. products(...) + 3. orders(...) + 4. order_items(...) + + СКОРЕЕ НУЖЕН + u.id - связь по джойну и GROUP BY + u.age - можно применить в WHERE и ORDER BY + u.country - можно применить в WHERE и GROUP BY + u.is_active - можно применить в WHERE и GROUP BY + p.id - связь по джойну и GROUP BY + p.category - можно применить в WHERE и GROUP BY + p.price - можно применить в WHERE и ORDER BY + p.in_stock - можно применить в WHERE и GROUP BY + o.id - связь по джойну + o.user_id - связь по джойну и GROUP BY + o.status - можно применить в WHERE и GROUP BY + oi.order_id - связь по джойну и GROUP BY + oi.product_id - связь по джойну и GROUP BY + oi.price_at_purchase - можно применить в WHERE и ORDER BY + o.order_date - можно применить в WHERE и ORDER BY и GROUP BY + u.created_at - можно применить в WHERE и ORDER BY и GROUP BY + НА САМОМ ДЕЛЕ ИНДЕКСОВ ОЧЕНЬ МНОГО И ЛУЧШЕ КОНЕЧНО ИХ СОКРАТИТЬ, ЧТОБЫ БД МОГЛА БЫСТРЕЕ ВЫДАВАТЬ РЕЗУЛЬТАТ, ПОЭТОМУ ВЫБОР ДОБАВЛЕНИЯ ИЛИ УДАЛЕНИЯ ИНДЕКСА БУДЕТ ЗАВИСЕТЬ ОТ ПРОИЗВОДСТВЕННЫХ ЗАДАЧ. + + + СКОРЕЕ НЕ НУЖЕН + oi.quantity - не имеет смысла + p.name - не имеет смысла + u.email - все различны + u.name - не имеет смысла + + Почему слишком много индексов замедляет INSERT, UPDATE и DELETE? + Индексы это средства для быстрого чтения данных, в этом они помогают. Но когда нам необзодимо добавить данные в таблицу, обновить или удалить, то в таком случае, БД должна будет и обратиться к индексам и их изменить тоже, поэтому слишком большое количество индексов в системе, которая предназначена для постоянного добавления/обновления данных, является негативным фактором. + + **Сделай две группы столбцов:** + + - «Скорее нужен индекс» + - «Скорее не нужен индекс» + + И для **каждого** столбца, который ты туда попал, коротко напиши причину. + + **В конце ответь текстом:** почему слишком много индексов замедляет INSERT, UPDATE и DELETE? + +- Задание 5. Что должно быть уникальным + 1. В таблице `users` выбери поля, которые должны быть уникальными в реальном проекте (например, `email`). + id, email + 2. В таблице `products` выбери поле, которое в реальном магазине часто делают уникальным (например, какой‑то артикул — можешь придумать колонку `sku`, даже если её нет). + Например, это может быть поле name (например, айфон 15 про 128 гб серый) или как ты сказал артикул (sku) + 3. Для каждого выбранного поля напиши: + - что будет, если уникальности не будет (какие реальные проблемы могут возникнуть); + В таком случае будет коллизия, задвоение или ошибка, например, человек покупает товар с определенным артикулом, в БД попадают данные о покупке товара с этим артикулом и случайно в заказ попадет два товара с одинаковым артиклом. + + - почему вместо обычного индекса логичен **уникальный** индекс. + Чтобы случайно нельзя было добавить другой товар с таким же индексом. Разные индексы будут создавать уникальность для уникального товара. + +- Задание 6. Где индексы помогут, а где нет + + Для каждого запроса оцени: «индекс нужен / не нужен», и если нужен — по каким полям: + + 1. SELECT * FROM users WHERE id = 100; нужен по полю id + 2. SELECT * FROM users WHERE email = 'user@example.com'; нужен по полю email + 3. SELECT * FROM products WHERE price BETWEEN 100 AND 500; нужен по price + 4. SELECT * FROM products WHERE name LIKE '%phone%'; не нужен по name + 5. SELECT * FROM orders WHERE status = 'DELIVERED'; нужен по status + 6. SELECT * FROM order_items WHERE order_id = 123; нужен по order_id + + **Ответ — текстом:** + В чём выгода индекса и где он не поможет + + Индекс помогает искать данные быстрее (индекс это как содержание в книге) за счет плана в виде дерева. + Индекс не поможет в тех столбцах, где уникальность данных мала, а также где есть сортировка по части строки, например "%abc" или если индекс не по функции . diff --git a/dmedvedev/2.2.theory_bd_partitions.sql b/dmedvedev/2.2.theory_bd_partitions.sql new file mode 100644 index 0000000..c434d57 --- /dev/null +++ b/dmedvedev/2.2.theory_bd_partitions.sql @@ -0,0 +1,170 @@ +- Задание 1. Какой вид партиционирования выбрать + + **Ответ текстом.** + + 1. Таблица `orders(id, user_id, order_date, status)` — очень большая (10 лет истории). + - Какой вид партиционирования (RANGE / LIST / HASH) ты выберешь и по какому полю? + ДУМАЮ МОЖНО СДЕЛАТЬ 2 ПАРТИЦИИ, ПЕРВАЯ RANGE ПО ПОЛЯМ USER_ID И ORDER_DATE - RANGE. И ВТОРАЯ ПАРТИЦИЯ ПО USER_ID И STATUS - LIST + + - Какие типы запросов выиграют от этого выбора? Приведи 3 примера запросов. + + + 2. Таблица `users(id, name, age, email, country, is_active, created_at)` — много пользователей по разным странам. + - Выбери вид партиционирования (можно «не партиционировать») и обоснуй. + МОЖНО ПО АЙДИ И СТРАНЕ - LIST. ПО АЙДИ И CREATE_AT - RANGE. COUNTRY И IS_ACTIVE - ЧТОБЫ ПОНИМАТЬ В КАКОЙ СТРАНЕ БОЛЬШЕ АКТИВНЫХ ПОЛЬЗОВАТЕЛЕЙ - МОЖЕТ HASH? + + - Если ты выбрал LIST по `country`, какие плюсы и минусы у такого решения? + Огромное количество стран - минус. + + 3. Таблица логов `user_logs(id, user_id, created_at, event_type, payload)` — миллионы записей в день. + - Какой вид партиционирования по какому полю логичен (RANGE / HASH) и почему? + USER_ID и EVENT_TYPE по LIST или по HASH. + +- Задание 2. Размер партиций + + **Ответ текстом.** + + 1. Для `orders` с партиционированием по `order_date` придумай три варианта: + - партиции по годам; + - по месяцам; + - по дням. + 2. Для каждого варианта напиши: + - примерно сколько будет партиций за 10 лет; + по годам - 10 * 1 = 10, по месяцам - 10 * 12 = 120, по дням 366 * 2 + 365 * 8 = 3652. + + - плюсы и минусы (удаление старых данных, поддержка, скорость запросов). + + 3. Выбери один вариант и обоснуй: почему он оптимален для большого онлайн‑магазина. + Для онлайн магазина лучше подойдет по месяцам, чтобы понимать сезонность товаров. + +- Задание 3. RANGE‑партиции по дате заказов + + **Уточнение**: партиционировать уже существующую таблицу муторно, проще создать новую + + **Сделать в PostgreSQL:** + + - создать партиционированную таблицу `orders`, разбитую методом RANGE по полю `order_date`; + CREATE TABLE orderz ( + id INT GENERATED ALWAYS AS IDENTITY, + user_id INT NOT NULL, + order_date DATE NOT NULL, + status TEXT NOT NULL, + PRIMARY KEY (id, order_date) + ) PARTITION BY RANGE(order_date); + + - создать несколько партиций (например, по годам или другим диапазонам дат), чтобы вставка строк с разными датами автоматически попадала в нужную партицию; + CREATE TABLE orderz_2023 PARTITION OF orderz + FOR VALUES FROM ('2023-01-01') TO ('2024-01-01'); + + CREATE TABLE orderz_2024 PARTITION OF orderz + FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); + + CREATE TABLE orderz_2025 PARTITION OF orderz + FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); + + CREATE TABLE orderz_before_2023 PARTITION OF orderz + FOR VALUES FROM ('2000-01-01') TO ('2023-01-01'); + - вставить тестовые данные с разными датами; + INSERT INTO orderz (user_id, order_date, status) + VALUES + (1, '2023-04-06', 'paid'), + (2, '2024-07-17', 'decline'), + (3, '2007-11-27', 'error'); + + - Написать несколько запросов выборки (за один год, за несколько лет, за весь период) + + SELECT * + FROM orderz o + WHERE order_date BETWEEN ('2023-01-01') AND ('2023-12-31'); + + SELECT * + FROM orderz o + WHERE order_date BETWEEN ('2022-01-01') AND ('2024-12-31'); + + SELECT * + FROM orderz o; + + - Объяснить, в каких случаях БД будет читать только часть партиций, а в каких — все. + Если мы выставляем определенное условие соответствующее одной партиции, то чтение будет по этой же партиции, если выставляем по нескольким, то чтение по нескольким. + +- Задание 4. LIST‑партиции по стране пользователей + + **Сделать в PostgreSQL:** + + - создать партиционированную таблицу `users`, разделённую методом LIST по полю `country`; + CREATE TABLE userz ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + age INT, + email TEXT NOT NULL, + country TEXT NOT NULL, + is_active BOOLEAN NOT NULL, + created_at TIMESTAMP NOT NULL + ) PARTITION BY LIST (country); + + - определить несколько партиций для разных стран или групп стран и вставить тестовые данные так, чтобы строки автоматически распределялись по этим партициям; + + CREATE TABLE userz_ru PARTITION OF userz + FOR VALUES IN ('RU'); + + CREATE TABLE userz_us PARTITION OF userz + FOR VALUES IN ('US'); + + CREATE TABLE userz_other PARTITION OF userz + FOR VALUES IN (DEFAULT); + - написать запросы выборки: + - по конкретной стране (например, только пользователи из одной страны); + SELECT * + FROM userz u + WHERE country = 'RU'; + + - по условию, не связанному с `country` (например, все активные пользователи); + SELECT * + FROM userz u + WHERE status = 'is_active'; + + - Объясни, для какого запроса PostgreSQL может обращаться только к одной/нескольким партициям, а для какого придётся просматривать все партиции. + Первый запрос обращается именно к одной партиции, а второй уже ко всем, потому что вто второй нет условия по партиции 'RU'. + +- Задание 5. HASH‑партиции по user_id в логах + + **Сделать в PostgreSQL:** + + - создать таблицу `user_logs`, партиционированную методом HASH по полю `user_id`, с несколькими партициями; + CREATE TABLE user_logz ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL, + event_type TEXT NOT NULL, + payload JSONB + ) PARTITION BY HASH (user_id); + + CREATE TABLE user_logz_p0 PARTITION OF user_logz + FOR VALUES WITH (MODULUS 4, REMAINDER 0); + + CREATE TABLE user_logz_p1 PARTITION OF user_logz + FOR VALUES WITH (MODULUS 4, REMAINDER 1); + + CREATE TABLE user_logz_p2 PARTITION OF user_logz + FOR VALUES WITH (MODULUS 4, REMAINDER 2); + + CREATE TABLE user_logz_p3 PARTITION OF user_logz + FOR VALUES WITH (MODULUS 4, REMAINDER 3); + + - заполнить таблицу тестовыми строками с разными значениями `user_id`; + INSERT INTO user_logz (user_id, created_at, event_type, payload) + VALUES (10, now(), 'login', '{"ip": "127.0.0.1"}'); + + SELECT * FROM user_logz + WHERE created_at >= now() - interval '1 day'; + + - написать запрос: + - выборки всех логов по конкретному `user_id`; + SELECT * FROM user_logz WHERE user_id = 10; + + - выборки всех логов за период по полю `created_at` (без фильтра по `user_id`); + SELECT * FROM user_logz + WHERE created_at >= now() - interval '1 day'; + + - Объясни какой запрос позволит серверу обратиться только к одной партиции, а какой заставит сканировать все партиции и почему. + Первый запрос обращается к одной партиции потому что есть условие по партиционному полю, второй именно ко всем таблицам, потому что нет условия по партиции. diff --git a/dmedvedev/2.3.theory_bd_shard.sql b/dmedvedev/2.3.theory_bd_shard.sql new file mode 100644 index 0000000..40e4ea8 --- /dev/null +++ b/dmedvedev/2.3.theory_bd_shard.sql @@ -0,0 +1,45 @@ +- Задание 1. Выбор ключа шардирования + + У тебя есть сервис онлайн‑магазина с таблицами: + + - `users` + - `orders` + + Нужно: + + - Выбрать, по какому полю ты будешь **шардировать пользователей** и объяснить, почему именно этот ключ, а не другие + Я БЫ ХОТЕЛ ШАРДИРОВАТЬ ПОЛЬЗОВАТЕЛЕЙ ПО АЙДИШНИКАМ, ИМЕННО АЙДИШНИК ЭТО ЧИСЛО, ПОЭТОМУ МОЖНО ШАРДИРОВАТЬ ПО ХЭШУ, ПЛЮС КО ВСЕМУ АЙДИ НЕ ПОВТОРЯЕТСЯ. + + - Описать, как при выбранном ключе будут распределяться пользователи по шардам и какие типы запросов станут выполняться быстрее, а какие, наоборот, станут сложнее. + ЕСЛИ ШАРДОВ 4, ТО ПЕРВЫЙ ПОЛЬЗОВАТЕЛЕЙ В 1ЫЙ, ВТОРОЙ ВО 2ОЙ, ТРЕТИЙ В 3ИЙ, ЧЕТВЕРТЫЙ В 0, ПЯТЫЙ В 1ЫЙ, ШЕСТОЙ ВО 2ОЙ, СЕДБМОЙ В 3ИЙ, ВОСБМОЙ В 0. + + Задание 2. Добавление нового шарда + + Ты шардируешь пользователей по правилу `user_id % 4` (4 шарда). Через пару лет один шард сильно перегружен, и ты хочешь перейти на `user_id % 8` (8 шардов). + + Нужно: + + - Описать, какие проблемы возникают при смене количества шардов (что происходит с существующими пользователями и их данными); + КАК МНЕ КАЖЕТСЯ НЕОБХОДИМО БУДЕТ ЗАНОВО ВЗЯТЬ СПИСОК ВСЕХ ПОЛЬЗОВАТЕЛЕЙ (КОТОРЫЕ УЖЕ В ШАРДАХ) И РАСКИДАТЬ ПО НОВЫМ РАЗНЫМ ШАРДАМ. + + - Предложить общий план «перешардирования» (как переносить данные, как не потерять консистентность, что делать с новыми запросами в момент миграции). + ПОЛУЧИТЬ ДАННЫЕ ИЗ СТАРЫХ ШАРДОВ, НЕ УДАЛЯЯ НИ ДАННЫЕ, НИ ШАРДЫ, РАСКИДАТЬ ДАННЫЕ ПО НОВЫМ ШАРДАМ (СЕРВЕРАМ) И УДАЛИТЬ СТАРЫЕ ШАРДЫ; + ПО ИДЕЕ ЗАПРОСЫ ВО ВРЕМЯ МИГРАЦИИ ТАК И БУДУТ ОБРАЩАТЬСЯ К СТАРЫМ ШАРДАМ, ВПЛОТЬ ДО МОМЕНТА, ПОКА ОНИ НЕ БУДУТ УДАЛЕНЫ. + +НОВЫЙ ОТВЕТ: РАЗВЕРНУТЬ НОВЫЕ ШАРДЫ ПАРАЛЛЕЛЬНО СТАРЫМ И НАСТРОИТЬ ПАРАЛЛЕЛЬНУЮ МИГРАЦИЮ ДАННЫЕ В СТАРЫЕ И НОВЫЕ ШАРДЫ. ДАЛЕЕ БЕРЕМ ДАННЫЕ ИЗ СТАРЫХ ШАРДОВ И ЗАПУСКАЕМ ПРОЦЕСС КОПИРОВАНИЯ В НОВЫЕ ШАРДЫ. ПОСЛЕ ПЕРЕНОСА НАМ НУЖНО УДОСТОВЕРИТЬСЯ ЧТО ДАННЫЕ АНАЛОГИЧНО И ТАМ И ТАМ. ЕСЛИ ЕСТЬ РАСХОЖДЕНИЯ, ТО НУЖНО ИХ ДОПЕРЕНЕСТИ. +ПОТОМ НУЖНО ВЫКЛЮЧИТЬ ЗАПИСЬ В СТАРЫЕ ШАРДЫ И ОСТАВИТЬ ЗАПИСЬ НА НОВЫХ. + +- Задание 3. Шардинг против партиционирования + + Есть одна большая база PostgreSQL с таблицей `orders`, и она начала тормозить. Команда обсуждает два варианта: сделать партиционирование в рамках одной базы или шардирование по нескольким базам/серверям. + + Нужно: + + - описать, чем принципиально отличается партиционирование от шардирования (где данные находятся и кто ими управляет); + ШАРДЫ НАХОДЯИСЯ НА РАЗНЫХ СЕРВЕРАХ, А ПАРТИЦИИ НА ОДНОМ, СООТВЕТСТВЕННО СКОРОСТЬ БУДЕТ БЫСТРЕЕ ПРИ ШАРДЕ. ПРИ ШАРДИРОВАНИИ ВОЗНИКАЕТ ПРОБЛЕМА СОЕДИНЕНИЯ ТАБЛИЦ, ПРИ ПАРТИЦИОНИРОВАНИИ ТАКОГО НЕТ. + + - придумать пример, когда **достаточно** партиционирования и шардирование не нужно; + НЕБОЛЬШАЯ ТАБЛИЦА В СОТНИ ТЫСЯЧ СТРОК НЕ ТРЕБУЕТ ШАРДИРОВАНИЯ. + + - придумать пример, когда без шардирования уже не обойтись, даже если партиции есть. + КОЛИЧЕСТВО СТРОК СЛИШКОМ БОЛЬШОЕ, СКОРОСТЬ УПАЛА, ПОЭТОМУ ЛУЧШЕ РАСКИДАТЬ НА РАЗНЫЕ СЕРВЕРА ДАННЫЕ. \ No newline at end of file diff --git a/dmedvedev/2.4.theory_bd_replications.sql b/dmedvedev/2.4.theory_bd_replications.sql new file mode 100644 index 0000000..056e2d7 --- /dev/null +++ b/dmedvedev/2.4.theory_bd_replications.sql @@ -0,0 +1,94 @@ +- Задание 1. Где и зачем понадобится репликация + + На примере онлайн‑магазина описать 3 конкретные ситуации, в которых репликация базы данных помогает: + + - защититься от падения сервера; + НОЧЬЮ СЕРВЕР ПАДАЕТ ИЗ-ЗА СКАЧКА НАПРЯЖЕНИЯ, САЙТ ПАДАЕТ, ИДЕТ ПОТЕРЯ ЗАКАЗОВ. + БЕЗ РЕПЛИКАЦИИ МОЖНО ЗАПУСТИТЬ ВОССТАНОВЛЕНИЕ , НО ЧАСТЬ ЗАКАЗОВ БУДЕТ ВСЕ РАВНО ПОТЕРЯНА, ПОСТРАДАЕТ РЕПУТАЦИ, УЙДЕТ ВРЕМЯ НА ВОССТАНОВЛЕНИЕ ИНФОРМАЦИИ. + С РЕПЛИКАЦИЕЙ МЕНЯЕТСЯ ПОТОК ДАННЫХ, МАСТЕР СТАНОВИТСЯ РЕПЛИКОЙ, А РЕПЛИКА МАСТЕРОМ - ДАННЫЕ ПИШУТСЯ НА ЗАПАСНОЙ СЕРВЕР, В ДАННОМ СЛУЧАЕ ПОТЕРИ ДАННЫХ МИНИМАЛЬНЫ. + + - разгрузить чтение; + НАБЛЮДАЕТСЯ НАПЛЫВ КЛИЕНТОВ, НАПРИМЕР ИЗ-ЗА СКИДОК. + БЕЗ РЕПЛИКАЦИИ ОДИН СЕРВЕР ОБРАБОТЫВАЕТ ВСЕ, ЗАПРОСЫ ТОРМОЗЯТ РАБОТУ СИСТЕМЫ, САЙТ ПАДАЕТ. + С РЕПЛИКАЦИЕЙ МОЖНО РАСПРЕДЕЛЕИТЬ ОБЯЗАННОСТИ, НАПРИМЕР, КАКИЕ-ТО СЕРВЕРЫ ТОЛЬКО ЧИТАЮТ, КАКИЕ-ТО ЗАПИСЫВАЮТ. + + - делать отчёты без влияния на основную базу; + ОТДЕЛУ НУЖЕН 1 РАЗ В НЕДЕЛЮ ОТЧЕТ ПО ПРОДАЖАМ И ТП. + БЕЗ РЕПЛИКАЦИИ СЕРВЕР ЗАГРУЖЕН ДАННЫМИ И САЙТ ТОРМОЗИТ У КЛИЕНТОВ. + С РЕПЛИКАЦИЕЙ ОТЧЕТ ФОРМИРУЕТСЯ НА РЕПЛИКЕ, А КЛИЕНТЫ РАБОТАЮТ ЧЕРЕЗ МАСТЕРА. + + - обновлять версию СУБД без простоя и т.п. + МЕНЯЕТСЯ ВЕРСИЯ СУБД ПО КАКИМ-ТО ПРИЧИНАМ. + БЕЗ РЕПЛИКАЦИИ ПРИДЕТСЯ ОСТАНОВИТЬ БД И ЖДАТЬ МИГРАЦИИ, А ЭТО ЗНАЧИТ ПОТЕРЯ ТРАФИКА И ЗАКАЗОВ. + С РЕПЛИКАЦИЕЙ ВСЕ ПРОЩЕ, ДЕЛАЕМ РЕПЛИКУ НА НОВОЙ ВЕРСИИ , НАСТРАИВАЕМ МИГРАЦИЮ С МАСТЕРА НА НОВУЮ РЕПЛИКУ. ДЕЛАЕМ НОВУЮ РЕПЛИКУ МАСТЕРОМ, СТАРЫЙ МАСТЕР СТАНОВИТСЯ БЭКОМ. + + Для каждой ситуации объяснить: что было бы без репликации и как её наличие меняет картину. + +- Задание 2. Сравнение синхронной и асинхронной репликации + + Сформулировать своими словами различия между синхронной и асинхронной репликацией: + Асинхронная подразумевает "параллельную" запись между мастером и слэйвом, а синхронная когда слэйв делает запись , а мастер в это время ждет пока слэйв подтвердит запись. + + - как ведёт себя запись при нормальной работе; + ПРИ АСИНХРОННОЙ ЗАПИСЬ ИДЕТ В ЖУРНАЛ, ДАЛЕЕ НА МАСТЕР И ТОЛЬКО ПОТОМ НА СЛЭЙВЫ. СУЩЕСТВУЕТ ЗАДЕРЖКА И ПРОБЛЕМА ПОТЕРИ ДАННЫХ. + ПРИ СИНХРОННОЙ ЗАПИСЬ ИДЕТ В ЖУРНАЛ. ДАЛЕЕ МАСТЕР И СЛЭЙВЫ ЗАПИСЫВАЮТ ПАРАЛЛЕЛЬНО. + + - что происходит при падении мастера; + АСИНХРОННАЯ. ДАННЫЕ МОГУТ ИСЧЕЗНУТЬ. + СИНХРОННАЯ. СЛЭЙВ СМОЖЕТ ЗАМЕНИТЬ МАСТЕРА. + + - какие риски потери/отставания данных есть. + ПРИ АСИНХРОННОЙ ЕСТЬ РИСК ПОТЕРИ ДАННЫХ. + ПРИ СИНХРОННОЙ РИСКИ ПОТЕРИ МИНИМАЛЬНЫ, ТАК КАК МАСТЕР ЖДЕТ ОТВЕТА ОТ СЛЭЙВА И В ЛЮБОЙ ЧЕГО СЛЭЙВ МОЖЕТ ЗАМЕНИТЬ МАСТЕРА. + + После этого выбрать тип репликации для двух систем: платёжный сервис и аналитическая витрина, и обосновать выбор. + ПЛАТЕЖНЫЙ СЕРВИС СИНХРОННАЯ, ЧТОБЫ ВО ВСЕХ БД БЫЛИ ОДИНАКОВЫЕ ДАННЫЕ В ЛЮБОЙ МОМЕНТ ВРЕМЕНИ. + ДЛЯ ВИТРИНЫ НЕ ТАК ВАЖНО ЧТОБЫ ДАННЫЕ БЫЛИ И ТАМ И ТАМ ОДНОВРЕМЕННО. АСИНХРОННАЯ. + +- Задание 3. Логическая и физическая репликация + + Объяснить, чем логическая репликация отличается от физической: + + - на каком уровне передаются изменения; + В ФИЗИЧЕСКОЙ ВСЕ ПЕРЕДАЕТСЯ НА УРОВНЕ БАЙТОВ, А В ЛОГИЧЕСКОЙ НА УРОВНЕ СТРОК И ТАБЛИЦ. + + - насколько гибко можно выбирать, какие таблицы и данные реплицировать; + ФИЗИЧЕСКАЯ РЕПЛИКАЦИЯ НЕ ПОДРАЗУМЕВАЕТ ВЫБОР ТАБЛИЦ (ГИБКОСТЬ ОТСУТСТВУЕТ), В ЛОГИЧЕСКОЙ МОЖНО ВЫБИРАТЬ (МАКСИМАЛЬНАЯ ГИБКОСТЬ). + + - как это влияет на возможность миграций между версиями/схемами. + С ФИЗИЧЕСКОЙ ЭТО БУДЕТ ТРУДНО ДЕЛАТЬ МИГРАЦИЮ МЕЖДУ РАЗНЫМИ ВЕРСИЯМИ, А В ЛОГИЧЕСКОЙ ПРОСТО ПОТОМУ ЧТО ПЕРЕНОСИТЬ ТАБЛИЦЫ МОЖНО КОМАНДАМИ, ОДНАКО НУЖНО СЛЕДИТЬ ЗА СХЕМАМИ В МАСТЕРЕ И НА РЕПЛИКЕ, ЧТОБЫ ОНИ СОВПАДАЛИ, ИНАЧЕ ПРИ ПЕРЕНОСЕ МОЖЕТ ВОЗНИКНУТЬ ОШИБКА. + + Придумать по одному сценарию, где логическая репликация удобнее, и где физическая проще и надёжнее. + ФИЗИЧЕСКАЯ КОГДА НУЖНО СОЗДАТЬ СЕРВЕР ДУБЛЕР, А ЛОГИЧЕСКАЯ КОГДА НУЖНЫ НЕ ВСЕ ДАННЫЕ , А ЛИШЬ ДАННЫЕ ДЛЯ ПОСТРОЕНИЯ ВИТРИН. + +- Задание 4. Схема один мастер — несколько реплик для чтения + + Представить, что есть один главный сервер (мастер) и две реплики только для чтения. + + Нужно описать: + + - какие типы запросов в интернет‑магазине должны ходить строго на мастер, а какие можно смело отправлять на реплики; + НА РЕПЛИКИ МОЖНО ЧТЕНИЕ ВИТРИН, НА МАСТЕРЕ ЗАПРОСЫ "РЕАЛЬНОГО ВРЕМЕНИ", НАПРИМЕР, ЗАКАЗЫ ПОЛЬЗОВАТЕЛЕЙ. + + - какие проблемы могут возникнуть из‑за того, что реплики иногда отстают на несколько секунд; + В МОМЕНТЕ МОЖЕТ ВОЗНИКНУТЬ СИТУАЦИЯ, ЧТО ДАННЫЕ НЕ СХОДЯТСЯ, ПРОБЛЕМА СХОДИМОСТИ. + + - как в коде можно уменьшить риск багов из‑за отставания (например, после критичных операций записи). + + +- Задание 5. Мульти‑мастер: когда это оправдано + + Рассмотреть схему, где несколько узлов базы могут принимать запись (multi‑master). + + Нужно: + + - перечислить главные плюсы по сравнению со схемой «один мастер — много реплик»; + СКОРОСТЬ, ВСЕ УЗЛЫ МОГУТ ДЕЛАТЬ ЗАПИСЬ. + В ОСНОВНОМ МИНУСЫ КОГДА ВЕДЕТСЯ ОДНА БД НЕСКОЛЬКИМИ МАСТЕРАМИ. + + - перечислить основные сложности: конфликты, согласованность, сложность настройки; + ДА, КОГДА НЕСКОЛЬКО МАСТЕРОВ МОГУТ ВОЗНИКАТЬ КОНФЛИКТЫ. ПРИ РЕГИСТРАЦИИ ПОЛЬЗОВАТЕЛЯ ОДИН МАСТЕР ДЕЛАЕТ ЗАПИСЬ УНИКАЛЬНОГО АЙДА, ДРУГОЙ МАСТЕР НЕ ЗНАЕТ О ПЕРВОЙ ЗАПИСИ И ТОЖЕ ПРОИЗВОДИТ ТОЖЕ САМОЕ, НО АЙДИШНИК УЖЕ ЗАДАН И ЗДЕСЬ МОЖЕТ ПОЯВИТЬСЯ ДУБЛИКАТ. + + - придумать пример системы, где мульти‑мастер даёт реальную пользу, и пример, где такая схема только усложнит архитектуру без заметного выигрыша. + МУЛЬТИ МАСТЕРЫ ДАЮТ РЕАЛЬНУЮ ПОЛЬЗУ В ПРОЕКТАХ РАЗБРОСАННЫХ ПО ВСЕМУ МИРУ, НАПРИМЕР ЭТО СЕТЬ МАГАЗИНОВ, В КАЖДОМ МАГАЗИНЕ БУДЕТ СВОЙ МАСТЕР, КАЖДЫЙ МАСТЕР ВЕДЕТ СВОЙ WAL ЖУРНАЛ И ЗАПИСЫВАЕТ ТУДА ДАННЫЕ, ПОТОМ ПРОСТО ПРОИСХОДИТ СИНХРОНИЗАЦЯ МЕЖДУ МАСТЕРАМИ. + УСЛОЖНИТ ТАКАЯ СХЕМА АРХИТЕКТУРУ В ОДНОМ ИНТЕРНЕТ-МАГАЗИНЕ, НАПРИМЕР, С ОДНИМ СЛАДОМ, МОЖЕТ СОЗДАВАТЬСЯ КОНФЛИКТЫ. \ No newline at end of file diff --git a/dmedvedev/2.5.theory_bd_b_tree_hash_index.sql b/dmedvedev/2.5.theory_bd_b_tree_hash_index.sql new file mode 100644 index 0000000..7138732 --- /dev/null +++ b/dmedvedev/2.5.theory_bd_b_tree_hash_index.sql @@ -0,0 +1,104 @@ +- Задание 1: + + **Дана таблица:** + + users(id PK, email, age, created_at) + + + **И индекс:** + + CREATE INDEX idx_users_email ON users(email); + + + **Ответь для каждого варианта:** + + - будет ли использоваться этот индекс; + - если нет — что нужно изменить (индекс или запрос). + + **Варианты:** + + 1. SELECT * FROM users WHERE email = 'test@example.com'; // будет, менять ничего не нужно + 2. SELECT * FROM users WHERE LOWER(email) = 'test@example.com'; // не будет, нужно изменить запрос, а именно убрать функцию LOWER + 3. SELECT * FROM users WHERE email LIKE 'alex%'; // будет, все норм + 4. SELECT * FROM users WHERE email LIKE '%@gmail.com'; // не будет, так как ищем оканчание строки. не знаю что тут можно поменять, как-будто ничего + 5. SELECT * FROM users ORDER BY email LIMIT 10; // будет, все норм + 6. SELECT * FROM users WHERE age BETWEEN 20 AND 30; // не будет +- Задание 2: + + **Дана таблица заказов:** + + orders( + id PK, + user_id, + status, + created_at, + amount + ); + + **К каждому запросу нужно сказать, что индекс не нужен/бесполезен):** + + 1. История заказов пользователя за период: + + SELECT * + FROM orders + WHERE user_id = 123 + AND created_at BETWEEN '2025-01-01' AND '2025-02-01' + ORDER BY created_at; + + + 2. Ежедневная агрегация по статусу: + + SELECT date_trunc('day', created_at) AS day, + status, + count(*) + FROM orders + GROUP BY day, status; + + + 3. Проверка наличия хотя бы одного «висящего» заказа: + + SELECT 1 + FROM orders + WHERE status = 'pending' + LIMIT 1; + + **Напиши `CREATE INDEX ...` и объясни, почему такой порядок колонок** +1. CREATE INDEX idx_orders_user_id_created_at ON orders (user_id, created_at) + ТАК ЭФФЕКТИВНЕЕ ПОТОМУ ЧТО СНАЧАЛА НАХОДИТСЯ ПОЛЬЗОВАТЕЛЕЙ, А ПОТОМ ДИАПАЗОН ЕГО ДАТ. ИНАЧЕ ПРИШЛОСЬ БЫ СНАЧАЛА ФИЛЬТРОВАТЬ ДИАПАЗОН ДАТ, А ПОТОМ НЕ НУЖНЫХ ПОЛЬЗОВАТЕЛЕЙ. + +2. В ДАННОМ СЛУЧАЕ ИНДЕКС НЕ НУЖЕН ПОТОМУ ЧТО НЕТ УСЛОВИЯ В КОДЕ, ПОСТГРЕ ПРИДЕТСЯ В ЛЮБОМ СЛУЧАЕ СМОТРЕТЬ ВСЮ ТАБЛИЦУ. + +3. CREATE INDEX idx_status_peding ON orders (status) WHERE status = 'pending' + ЗДЕСЬ МОЖНО ИСПОЛЬЗОВАТЬ ИНДЕКС ТОЛЬКО ПО ОДНОМУ ПОЛЮ "pending" ЧТОБЫ НЕ ИНДЕКСИРОВАТЬ ВСЕ ЗНАЧЕНИЯ ПО ДАННОМУ СТОЛБЦУ status + + +- Задание 3: + + Та же таблица `orders` + + **Выбери тип индекса и объясни почему выбрал такой для вариантов:** + + 1. SELECT * FROM orders WHERE id = 100500; // hash , возможное соединение таблиц + 2. SELECT * FROM orders WHERE status = 'paid'; // спорен + 3. SELECT * FROM orders WHERE created_at BETWEEN ...; // только B-tree из-за диапазона + 4. SELECT * FROM orders WHERE user_id = 123; // hash, возможное соединение таблиц + + **Вопросы:** + + - Где hash имел бы смысл? + - Где обязательно B‑tree? + - Где индекс вообще спорен? +- Задание 4: + + **Исходные индексы:** + + CREATE INDEX idx_orders_user_created ON orders(user_id, created_at); + CREATE INDEX idx_orders_user ON orders(user_id); + + **Задача:** + + - объяснить, зачем нужен первый; + ЧТОБЫ ОДНОВРЕМЕННО ИСКАТЬ ПОЛЬЗОВАТЕЛЕЙ В СОВОКУПНОСТИ С ДАТОЙ СОЗДАНИЯ. И ТАКЖЕ СОРТИРОВАТЬ ПО ДАТЕ СОЗДАНИЯ ПОЛЬЗОВАТЛЕЙ . + + - нужен ли второй или его можно удалить; + ВТОРОЙ НЕ НУЖЕН, ПОТОУ ЧТО ПЕРВЫЙ МОЖЕТ РАБОТАТЬ ПО ЛЕВОМУ ПРЕФИКСУ. ВТОРОЙ ИНДЕКС ЛИШЬ ЗАНИМАЕТ ЛИШНЕЕ МЕСТО. \ No newline at end of file diff --git a/dmedvedev/2.6.theory_bd_acid_base.sql b/dmedvedev/2.6.theory_bd_acid_base.sql new file mode 100644 index 0000000..9591278 --- /dev/null +++ b/dmedvedev/2.6.theory_bd_acid_base.sql @@ -0,0 +1,40 @@ +- **Задание 1. Интернет‑банк** + + **В интернет‑банке** пользователь переводит 1000 ₽ с карты А на карту Б. + + Во время операции связь на секунду пропадает, запрос дублируется, и на сервер приходят два очень похожих запроса перевода подряд. + + **Нужно:** + + - указать, какие свойства ACID здесь особенно важны и почему; + СОГЛАСОВАННОЙ ЧЕРЕЗ УНИКАЛЬНЫЙ КЛЮЧ ОБНАРУЖИВАЕТ ПОВТОРНЫЙ ЗАПРОС, А АТОМАРНОСТЬ ГАРАНТИРУЕТ, ЧТО ЭТА НЕУДАВШАЯСЯ ПОПЫТКА НЕ ИЗМЕНИТ БАЛАНС + - описать, какие проблемы могут возникнуть, если этих свойств не будет (например, нет атомарности или изолированности). + ЕСЛИ ЭТИХ СВОЙСТВ НЕ БУДЕТ, ТО ПРОИЗОЙДУТ ОШИБКИ, ДВА ОДИНАКОВЫХ ЗАПРОСА ИЛИ НЕИСПОЛНЕНИЕ ЗАПРОСА ВООБЩЕ. + +- **Задание 2. Лента новостей и лайки** + + **В социальной сети:** + + - пользователь ставит лайк под постом; + - у его друга этот лайк появляется в ленте только через 5–10 секунд; + - иногда счётчик лайков на разных устройствах на пару секунд показывает разные значения. + + **Нужно:** + + - решить, ближе ли эта система по поведению к ACID или к BASE и почему + ЭТА СИСТЕМА БЛИЖЕ К БЭЙС. У ДАННОЙ СИСТЕМЫ НЕТ СТРОГИХ ОГРАНИЧЕНИЙ. + +- **Задание 3. Заказы в интернет‑магазине** + + **В интернет‑магазине:** + + - при оформлении заказа запись о заказе и списание товара со склада делаются в **одной транзакции**; + - статистика «топ‑товары недели» обновляется раз в несколько минут на отдельном сервисе, и пару минут может показывать старые данные. + + **Нужно:** + + - указать, какая часть системы ведёт себя по принципам ACID, а какая — по принципам BASE; + ЗАПИСЬ О ЗАКАЗЕ И СПИСАНИЕ ТОВАРА - ЭТО ACID. + СТАТИСТИКА - BASE. + - объяснить, почему такое разделение логично: где нам важна немедленная строгая точность, а где допускается небольшое отставание и eventual consistency. + В ПЕРВОМ СЛУЧАЕ НУЖЕН ACID потому что требуются свойства ACID, а во втором эти свойства необзяательны, нет ничего страшного в том, что данная БД обновится чуть позже. \ No newline at end of file diff --git a/dmedvedev/2.7.theory_bd_isolation_levels.sql b/dmedvedev/2.7.theory_bd_isolation_levels.sql new file mode 100644 index 0000000..f94e175 --- /dev/null +++ b/dmedvedev/2.7.theory_bd_isolation_levels.sql @@ -0,0 +1,87 @@ +- Задание 1: Разбери ситуацию + + У тебя две транзакции работают с таблицей `accounts`: + + **Транзакция A:** + + BEGIN; + SELECT balance FROM accounts WHERE id = 1; -- balance = 100 + -- ждёт 10 секунд + SELECT balance FROM accounts WHERE id = 1; -- balance = ??? + COMMIT; + + **Транзакция B (выполняется параллельно):** + + BEGIN; + UPDATE accounts SET balance = 200 WHERE id = 1; + COMMIT; + + **Вопросы:** + + 1. Какой баланс увидит транзакция A во втором SELECT на уровне **Read Committed**? + УВИДИТ 200 + 2. Какой баланс увидит транзакция A во втором SELECT на уровне **Repeatable Read**? + УВИДИТ 100 + 3. Объясни, почему результаты разные. + ПОТОМУ ЧТО УРОВНИ ИЗОЛЯЦИИ РАЗНЫЕ, В ПЕРВОМ СЛУЧАЕ ВИДИМ ТОЛЬКО ЗАКОММИЧЕННЫЕ ДАННЫЕ, А ВО ВТОРОМ ТЕ ДАННЫЕ, КОТОРЫЕ БЫЛИ В САМОМ НАЧАЛЕ ТРАЗАКЦИИ. + +- Задание 2: Практика + + Создай таблицу: + + CREATE TABLE test_isolation (id INT PRIMARY KEY, value INT); + INSERT INTO test_isolation VALUES (1, 100); + + Открой **две** вкладки в DBeaver. + + **В первой вкладке:** + + BEGIN; + SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; + SELECT value FROM test_isolation WHERE id = 1; + + **Во второй вкладке:** + + BEGIN; + UPDATE test_isolation SET value = 200 WHERE id = 1; + COMMIT; + + **Обратно в первой вкладке:** + + SELECT value FROM test_isolation WHERE id = 1; + COMMIT; + + **Вопрос:** Какое значение ты увидел во втором SELECT? Почему? + УВИДЕЛ 100, ПОТОМУ ЧТО dbeaver В ПЕРВОЙ ТРАНЗАКЦИИ ПОДДЕРЖИВАЕТ Repeatable Read И ТРАНЗАКЦИЯ НЕ БЫЛА ЗАКОММИЧЕНА. + + Повтори эксперимент с уровнем **Read Committed** вместо **Repeatable Read**. Что изменилось? + В ДАННОМ СЛУЧАЕ ИЗМЕНИЛСЯ ТИП ИЗОЛЯЦИИ И ТК ВТОРАЯ ТРАЗАКЦИЯ БЫЛА ЗАКОМИЧЕНА, ТО ВЫВОД ТРЕТЬЕГО ЗАПРОСА ПОКАЗАЛ ИМЕННО ПОСЛЕДНЕЕ ЗАКОМИЧЕННОЕ. + +- Задание 3: Найди ошибку + + Вася написал код для списания денег со счёта: + + BEGIN; + SET TRANSACTION ISOLATION LEVEL READ COMMITTED; + + -- Проверяем баланс + SELECT balance FROM accounts WHERE id = 1; -- balance = 100 + + -- Думаем 5 секунд (пользователь вводит сумму) + + -- Списываем деньги + UPDATE accounts SET balance = balance - 50 WHERE id = 1; + + COMMIT; + + **Проблема:** Пока Вася думал 5 секунд, другая транзакция списала 80 руб. Теперь баланс стал отрицательным + + **Вопросы:** + + 1. Почему так произошло? + ПОТОМУ ЧТО ВНУТРЬ ТРАНЗАКЦИИ ВОРВОЛАСЬ ДРУГАЯ, А UPDATE БЕРЕТ ЗНАЧЕНИЕ НА ДАННЫЙ МОМЕНТ. ЧТОБЫ ЭТОГО ИЗБЕЖАТЬ ВАСЕ НАДО БЫЛО В СВОЕЙ ТРАНЗАКЦИИ НАПИСТАЬЬ FOR UPDATE И ЕГО ЗНАЧЕНИЕ БЫ ЗАМОРОЗИЛОСЬ. + + 2. Какой уровень изоляции поможет избежать этой проблемы? + Serializable + 3. Как ещё можно решить эту проблему без смены уровня изоляции? (подсказка: проверяй баланс перед UPDATE) + КАК ВЫШЕ СКАЗАЛ МОЖНО ИСПОЛЬЗОВАТЬ FOR UPDATE. А ТАКЖЕ УСЛОВИЕ, ЧТО БАЛАНС НЕ МОЖЕТ ОПУСКАТЬСЯ НИЖЕ НУЛЯ, ИНАЧЕ ОТКЛОНЕНИЕ ТРАНЗАКЦИИ. \ No newline at end of file diff --git a/dmedvedev/2.8.theory_bd_CAP.sql b/dmedvedev/2.8.theory_bd_CAP.sql new file mode 100644 index 0000000..bed68ed --- /dev/null +++ b/dmedvedev/2.8.theory_bd_CAP.sql @@ -0,0 +1,40 @@ +- Задание 1: Определи тип базы + + Для каждого сценария выбери, какой тип базы подходит (CA, CP, или AP): + + 1. **Банковские переводы** — критично важно, чтобы все видели правильный баланс. + + **Какой тип?** + СР + + 2. **Лента новостей в Instagram** — если пост появится с задержкой в 5 секунд, ничего страшного. + + **Какой тип?** + АР + + 3. **Интернет-магазин на WordPress** — небольшой сайт на одном сервере. + + **Какой тип?** + СА + + 4. **Система логирования для миллиардов событий** — важна скорость записи, данные могут появиться с задержкой. + + **Какой тип?** + АР + +- Задание 2: Что выбрать? + + Ты делаешь **систему бронирования авиабилетов**. На рейс осталось **1 место**. + + Два пользователя **одновременно** пытаются его забронировать на разных серверах (Москва и Париж). + + **Вопросы:** + + 1. Какой тип базы выберешь: **CP** или **AP**? Почему? + СР. ПОТОМУ ЧТО В ДАННОМ СЛУЧАЕ ВАЖНА СОГЛАСОВАННОСТЬ, НЕЛЬЗЯ ПРОДАТЬ 2 БИЛЕТА, ЕСЛИ В НАЛИЧИЕ ВСЕГО ОДИН. + + 2. Что произойдёт, если выберешь **CP**? + ПРОИЗОЙДЕТ ПРОДАЖА ОДНОГО БИЛЕТА, ВТОРОМУ ПОЛЬЗОВАТЕЛЮ ВЫДАСТ ОШИБКУ. + + 3. Что произойдёт, если выберешь **AP**? + В ТАКОМ СЛУЧАЕ БУДЕТ ДОСТУПНОСТЬ, НО НЕ БУДЕТ СОГЛАСОВАННОСТИ. МОЖЕТ ПРОИЗОЙТИ СИТУАЦИЯ ПРОДАЖИ ДВУХ БИЛЕТОВ НА ОДНО МЕСТО, НЕЛЬЗЯ ТАКОЕ ДОПУСТИТЬ. \ No newline at end of file diff --git a/dmedvedev/4.1.greenplum_zaplosi.sql b/dmedvedev/4.1.greenplum_zaplosi.sql new file mode 100644 index 0000000..5b3b9ab --- /dev/null +++ b/dmedvedev/4.1.greenplum_zaplosi.sql @@ -0,0 +1,90 @@ +### Задача 1: + +SELECT * FROM orders ORDER BY total DESC LIMIT 10; + +Таблица `orders` содержит **10 млн строк**, распределена по `user_id`. + +- 📌 Что произойдёт? Почему это может тормозить? + - **Визуализация** + + Segment 1 → 2M строк + Segment 2 → 2M строк + Segment 3 → 2M строк + Segment 4 → 2M строк + Segment 5 → 2M строк + + ▼ + Gather to master + ▼ + LIMIT 10 применяется только тут + + +**Задание:** + +- Предложи способ ускорить выборку топ-N с предварительной агрегацией/фильтрацией. + ЕСЛИ РАБОТАТЬ ПО ТАКОМУ ЗАПРОСУ, ТО КАЖДЫЙ СЛЭЙВ СНАЧАЛА ВЫДЕЛИТ 10 ТОП ЗНАЧЕНИЙ И ОТПРАВИТ НА МАСТЕР, ПОЛУЧИТСЯ 50 СТРОК. + В ИДЕАЛЕ ЕСЛИ ДЕЛАЕМ ORDER BY ПО total, ТО НУЖНО СДЕЛАТЬ ИНДЕКС ПО ПОЛЮ total, ЧТОБЫ НЕ ЧИТАЛИСЬ ВСЕ ТАБЛЛИЦЫ, А ТАКЖЕ ВМЕСТО * УКАЗАТЬ НЕОБХОДИМЫЕ СТОЛБЦЫ. + +### Задача 2: + +SELECT + user_id, + total, + RANK() OVER (PARTITION BY region_id ORDER BY total DESC) as rnk +FROM orders; + +- 📌 Что не так? Какие издержки? + - **Визуализация:** + + ``` + orders распределены по user_id + ▼ + окно работает по region_id → данные перегоняются между сегментами! + ``` + + +**Вопрос:** + +- Как лучше распределить таблицу, если часто используешь оконные по `region_id`? + ДУМАЮ В ЭТОМ СЛУЧАЕ МОЖНО ПОМЕНЯТЬ destributed by по region_id . + +### Задача 3: + +INSERT INTO top_users +SELECT user_id, SUM(total) as total_spent +FROM orders +GROUP BY user_id; + +Но `top_users DISTRIBUTED BY (user_id)`, а `orders DISTRIBUTED BY (order_id)`. + +**Что произойдёт?** + +- 📌 Подсказка: + + Greenplum будет пересылать строки между сегментами. + + - **Визуализация:** + + ``` + Segment A: SUM(user_id=1) → отправить в другой сегмент (там находится user_id=1 в top_users) + Segment B: SUM(user_id=2) → ... + ``` + + +**Задание:** + +- Предложи, как перераспределить `orders`, чтобы вставка была без motion. + В ТАКОМ СЛУЧАЕ ЛУЧШЕ ПЕРЕДЕЛАТЬ РАСПРЕДЕЛЕНИЕ orders НА DISTRIBUTED BY (user_id) ПО ОБЕИМ ТАБЛИЦАМ. + +### Задача 4: + +Ты пишешь: + +EXPLAIN SELECT u.name, o.total +FROM users u +JOIN orders o ON u.user_id = o.user_id; + +**Вопросы:** + +- Какие ключевые слова ты хочешь увидеть в плане? + ПОКА ЗАТРУДНЯЮСЬ ОТВЕТИТЬ НА ДАННЫЙ ВОПРОС, ЕЩЕ НЕ РАЗОБРАЛСЯ В ТЕМЕ ДО КОНЦА. \ No newline at end of file diff --git a/dmedvedev/4.2.greenplum_distr_by.sql b/dmedvedev/4.2.greenplum_distr_by.sql new file mode 100644 index 0000000..24909f2 --- /dev/null +++ b/dmedvedev/4.2.greenplum_distr_by.sql @@ -0,0 +1,95 @@ +## Задача 1. + +CREATE TABLE users ( + user_id INT, + name TEXT +) DISTRIBUTED BY (user_id); + +CREATE TABLE orders ( + order_id INT, + user_id INT, + amount NUMERIC +) DISTRIBUTED BY (order_id); + +**Запрос:** + +SELECT u.name, o.amount +FROM users u +JOIN orders o ON u.user_id = o.user_id; + +📌 Вопрос: будет ли `JOIN` локальным? + +📌 Что делает Greenplum? + +- Подсказка в виде визуализации: + + ``` + Segment 1: Segment 2: Segment 3: + users(user_id=1) users(user_id=2) users(user_id=3) + orders(order_id=100, user_id=2) + orders(order_id=200, user_id=1) + + JOIN требует: + users.user_id = orders.user_id + НО orders распределена по order_id + + ❌ → Redistribute Motion: перегон строк между сегментами + ``` + + +**Задание:** + +Перераспредели `orders` по `user_id`, чтобы JOIN стал локальным. + ALTER TABLE orders SET DISTRIBUTED BY (user_id); + +## Задача 2. + +CREATE TABLE logs ( + id SERIAL, + user_agent TEXT, + ts TIMESTAMP +) DISTRIBUTED RANDOMLY; + +📌 Логи не участвуют в `JOIN` — можно распределять случайно. + +- Визуализация: + + ``` + Segment 1: 1/3 строк + Segment 2: 1/3 строк + Segment 3: 1/3 строк + + ✅ Отлично сбалансировано, нет связи с другими таблицами + ``` + + +**Задание:** + +Почему `DISTRIBUTED BY` не подходит для этой таблицы? + ПОТОМУ ЧТО ЛОГИ НЕ УЧАСТВУЮТ В ПОСЛЕДУЮЩИХ ДЖОЙНАХ, СООТВЕТСТВЕННО motion МЕЖДУ СЕГМЕНТАМИ НЕ БУДЕТ. + +## Задача 3. + +CREATE TABLE events ( + event_type TEXT, + ts TIMESTAMP +) DISTRIBUTED BY (event_type); + +📌 Но `event_type = 'click'` — 95% строк + +- Подсказка в виде визуализации: + + ``` + Segment 1 (click) → 95% данных + Segment 2 (view) → 3% + Segment 3 (hover) → 2% + + ❌ Segment 1 перегружен, тормозит всё + ``` + + +**Задание:** + +Как это исправить? Приведи пример хорошего ключа. + В ДАННОМ СЛУЧАЕ БУДЕТ ПЕРЕГРУЗ ПЕРВОГО СЕГМЕНТА, ОСТАЛЬНЫЕ СЕРВЕРА БУДУТ ПРОСТАИВАТЬ В ОЖИДАНИИ - ПРОБЛЕМА ОПТИМИЗАЦИИ. + РЕШЕНИЕМ БУДЕТ ЯВЛЯТЬСЯ ЛИБО РАНДОМИЗИРОВАНИЕ ПО СЕГМЕНТАМ - КАК МНЕ КАЖЕТСЯ ЭТО РЕШЕНИЕ, НО НЕ САМОЕ ЛУЧШЕЕ (ALTER TABLE events SET DISTRIBUTED RANDOMLY), А ТАКЖЕ ДВОЙНОЙ СОСТАВНОЙ КЛЮЧ ПО ИВЕНТАМ И ВРЕМЕНИ (ALTER TABLE events SET DISTRIBUTED BY (event_type, ts)). \ No newline at end of file diff --git a/dmedvedev/4.3.greenplun_optimization_explain.sql b/dmedvedev/4.3.greenplun_optimization_explain.sql new file mode 100644 index 0000000..a81c501 --- /dev/null +++ b/dmedvedev/4.3.greenplun_optimization_explain.sql @@ -0,0 +1,53 @@ +## Задача 1: + +EXPLAIN ANALYZE +SELECT user_id, COUNT(*) +FROM orders +GROUP BY user_id; + +📌 `orders` распределена по `order_id`. + +Что происходит: + +``` +Segment: + Seq Scan on orders + ▼ + Redistribute Motion (по user_id) + ▼ + HashAggregate (GROUP BY user_id) +``` + +💣 **Проблема:** Motion перед агрегированием — значит данные идут по сети между сегментами. + +### Задание: + +1. Что будет, если `orders DISTRIBUTED BY user_id`? + В таком случае таблица orders будет распределена по user_id между сегментами, соответственно счет будет внутри сегментов и не будет motion между сегментами. +2. Как меняется `EXPLAIN ANALYZE` + Не будет Redistribute Motion (по user_id) + +## Задача 2: + +SELECT user_id, COUNT(*) +FROM orders +GROUP BY user_id; + +📌 `orders` содержит 10 млн заказов, и 90% из них у `user_id = 1`. + +Что происходит: + +``` +Segment 1: user_id = 1 → 9 млн строк +Segment 2: остальные user_id → 1 млн строк +Segment 3–N: почти пусто + +❗ Segment 1 перегружен → весь запрос тормозит +``` + +### Задание: + +1. Как это называется? + Это называется "перекос" +2. Как избежать? + Сделать составной ключ по двум аргументам, либо разбить сегменты рандомно. \ No newline at end of file diff --git a/dmedvedev/4.4.greenplum_external_tables.sql b/dmedvedev/4.4.greenplum_external_tables.sql new file mode 100644 index 0000000..9cf7ebc --- /dev/null +++ b/dmedvedev/4.4.greenplum_external_tables.sql @@ -0,0 +1,52 @@ +## Задача 1: + +У тебя в Hive есть таблица `sales (id, amount)` + +Ты хочешь читать её из Greenplum, **не копируя**. + +CREATE EXTERNAL TABLE hive_sales ( + id INT, + amount NUMERIC +) +LOCATION ('pxf://hive.sales?PROFILE=hive') +FORMAT 'CUSTOM' (FORMATTER='pxfwritable_import'); + +Greenplum ↔ PXF ↔ Hive Metastore + │ + ▼ + HDFS файлы (ORC, Parquet...) + +### ❓ Вопросы: + +1. Где реально хранятся данные? + ЖАННЫЕ ХРАНЯТСЯ В ХРАНИЛИЩЕ HIVE. +2. Можно ли писать обратно в Hive? + НЕТ, НЕЛЬЗЯ. +3. Что если структура таблицы в Hive изменилась? + В ТАКОМ СЛУЧАЕ ПРИ ЗАПРОСЕ МОГУТ БЫТЬ ОШИБКИ ИЛИ ПУСТЫЕ СТОЛБЦЫ СО ЗНАЧЕНИЯМИ, ТАК КАК GREENPLUM НИКАКИМ ОБРАЗОМ НЕ СМОЖЕТ УСЗНАТЬ КАКИЕ ИЗМЕНЕНИЯ ПРОИЗОШЛИ. ПОЭТОМУ НЕОБХОДИМО ПЕРЕСОЗДАТЬ ВНЕШНЮЮ ТАБЛИЦУ ПУТЕМ УДАЛЕНИЯ И СОЗДАНИЯ. + +## Задача 2: + +Есть файл `s3://my-data-bucket/orders.parquet`. Нужно читать его как таблицу. + +CREATE EXTERNAL TABLE s3_orders ( + order_id INT, + amount NUMERIC +) +LOCATION ('pxf://my-data-bucket/orders.parquet?PROFILE=s3') +FORMAT 'CUSTOM' (FORMATTER='parquet'); + +### 🔍 Визуализация: + +Greenplum ↔ PXF ↔ S3 + ▼ + Чтение .parquet + +### ❓ Вопросы: + +1. Данные копируются в Greenplum? + НЕТ, ДАННЫЕ НЕ КОПИРУЮТСЯ. +2. Что быстрее: EXTERNAL или `CREATE TABLE AS SELECT`? + ВСЕ ЗАВИСИТ ОТ ЦЕЛЕЙ. + ДЛЯ РАЗОВОГО ИСПОЛЬЗОВАНИЯ МОЖНО ВЫБРАТЬ EXTERNAL, СОЗДАНИЕ ТАБЛИЦЫ БУДЕТ ПРАКТИЧЕСКИЕ МОМЕНТАЛЬНЫМ ПОТОМУ ЧТО СОЗДАЕТСЯ ПРОСТО ССЫЛКА НА ТАБЛИЦУ, А ВОТ ЧТЕНИЕ БУДЕТ НЕ ОЧЕНЬ ЭФФЕКТИВНЫМ. + ДЛЯ МНОГОКРАТНОГО ИСПОЛЬЗОВАНИЯ ЛУЧШЕ ВЫБРАТЬ CTAS, ХОТЬ МЫ И ДОЛГО КОПИРУЕМ (СОЗДАЕМ) ТАБЛИЦУ, ЗАТО ПОСЛЕДУЮЩИЕ ЧАСТЫЕ ОБРАЩЕНИЯ БУДУТ ПРОИСХОДИТЬ ГОРАЗДО БЫСТРЕЕ ЧЕМ ПРИ EXTERNAL. \ No newline at end of file diff --git a/dmedvedev/4.5.greenplum_pet_project.sql b/dmedvedev/4.5.greenplum_pet_project.sql new file mode 100644 index 0000000..ee08a26 --- /dev/null +++ b/dmedvedev/4.5.greenplum_pet_project.sql @@ -0,0 +1,256 @@ +--Добавление raw-таблиц в GREENPLUM +CREATE TABLE raw.pet_users +( + id INT, + name VARCHAR(50), + age INT, + email VARCHAR(100), + registration_date DATE +); + +CREATE TABLE raw.pet_courses +( + id INT, + title VARCHAR(100), + category VARCHAR(50), + created_at DATE +); + +CREATE TABLE raw.pet_lessons +( + id INT, + course_id INT, + title VARCHAR(50), + duration_min INT +); + +CREATE TABLE raw.pet_enrollments +( + id INT, + user_id INT, + course_id INT, + enrolled_at DATE +); + +CREATE TABLE raw.pet_lesson_views +( + id INT, + user_id INT, + lesson_id INT, + viewed_at TIMESTAMPTZ +); + +--Создание core-таблиц (измерений - dimension) в GREENPLUM +-- Измерение пользователей +CREATE TABLE core.dim_user +( + user_id INT PRIMARY KEY, + name TEXT, + age INT, + email TEXT, + registration_date DATE +); + +-- Измерение курсов +CREATE TABLE core.dim_course +( + course_id INT PRIMARY KEY, + title TEXT, + category TEXT +); + +-- Измерение уроков +CREATE TABLE core.dim_lesson +( + lesson_id INT PRIMARY KEY, + title TEXT, + duration_min INT, + course_id INT REFERENCES core.dim_course(course_id) +); + +-- Опционально: измерение времени +CREATE TABLE core.dim_date +( + date DATE PRIMARY KEY, + year INT, + month INT, + day INT, + weekday TEXT +); + +--Создание core-таблиц (факты - fact table) в GREENPLUM + +CREATE TABLE core.fact_lesson_views +( + user_id INT REFERENCES core.dim_user(user_id), + lesson_id INT REFERENCES core.dim_lesson(lesson_id), + course_id INT REFERENCES core.dim_course(course_id), + viewed_at TIMESTAMP +); + +CREATE TABLE core.enrollments +( + user_id INT REFERENCES core.dim_user(user_id), + course_id INT REFERENCES core.dim_course(course_id), + enrolled_at TIMESTAMP +); + +--ETL переброска данных из raw в core в GREENPLUM +--переброска для dim_user +INSERT INTO core.dim_user + (user_id, name, age, email, registration_date) +SELECT DISTINCT + id::INT, + name::TEXT, + age::INT, + email::TEXT, + registration_date::DATE +FROM raw.pet_users; + +--переброска для dim_course +INSERT INTO core.dim_course + (course_id, title, category) +SELECT DISTINCT + id::INT, + title::VARCHAR(100), + category::VARCHAR(50) +FROM raw.pet_courses; + +--переброска для dim_lesson +INSERT INTO core.dim_lesson + (lesson_id, title, duration_min, course_id) +SELECT DISTINCT + id::INT, + title::VARCHAR(50), + duration_min::INT, + course_id::INT +FROM raw.pet_lessons; + +--переброска для dim_date +--ЗДЕСЬ Я ВЫБРАЛ ПОДТЯГИВАНИЕ ДАТ ИЗ ТАБЛИЦЫ ПРОСМОТРОВ УРОКОВ +INSERT INTO core.dim_date + (date, YEAR, MONTH, DAY, weekday) +SELECT DISTINCT + viewed_at::date, + EXTRACT(YEAR FROM viewed_at), + EXTRACT(MONTH FROM viewed_at), + EXTRACT(DAY FROM viewed_at), + EXTRACT(ISODOW FROM viewed_at) +FROM raw.pet_lesson_views; + +--переброска для enrollments +INSERT INTO core.enrollments + (user_id, course_id, enrolled_at) +SELECT + user_id::INT, + course_id::INT, + enrolled_at::DATE +FROM raw.pet_enrollments; + +--переброска для fact_lesson_views +--ЗДЕСЬ ПО ИДЕЕ ДОЛЖЕН БЫЛ ПОДТЯНУТЬ АЙДИШНИК КУРСА, НО ЕГО НЕТ В ИСХОДНОЙ ТАБЛИЦЕ raw.pet_lesson_views, ПРАВИЛЬНО ЛИ Я СДЕЛАЛ, ЧТО ПРОСТО НЕ ДОБАВИЛ ЕГО В ЗАПРОС?????? +INSERT INTO core.fact_lesson_views + (user_id, lesson_id, viewed_at) +SELECT + user_id::INT, + lesson_id::INT, + viewed_at::TIMESTAMPTZ +FROM raw.pet_lesson_views; + +--ВИТРИНА dm.lesson_popularity_summary +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary AS + +SELECT + flv.lesson_id AS lesson_id, + dl.title AS lesson_title, + dc.course_id AS course_id, + dc.title AS course_title, + COUNT(*) AS total_views, + COUNT(DISTINCT user_id) AS unique_users, + MIN(viewed_at) AS first_view, + MAX(viewed_at) AS last_view +FROM core.fact_lesson_views flv +LEFT JOIN core.dim_lesson dl ON flv.lesson_id = dl.lesson_id +LEFT JOIN core.dim_course dc ON dl.course_id = dc.course_id +GROUP BY flv.lesson_id, dl.title, dc.course_id, dc.title +ORDER BY lesson_id; + +--ВИТРИНА dm.inactive_users_summary +CREATE MATERIALIZED VIEW dm.inactive_users_summary AS +SELECT + du.user_id AS user_id, + du.name AS user_name, + du.email, + du.age, + COUNT(DISTINCT e.course_id) AS enrollments_count, + du.registration_date +FROM core.dim_user du +LEFT JOIN core.enrollments e ON du.user_id = e.user_id +LEFT JOIN core.fact_lesson_views flv ON du.user_id = flv.user_id +WHERE flv.user_id IS NULL +GROUP BY du.user_id, du.name, du.email, du.age, du.registration_date; + +--ВИТРИНА dm.course_completion_rate +CREATE MATERIALIZED VIEW dm.course_completion_rate AS +SELECT + du.user_id AS user_id, + du.name AS user_name, + e.course_id AS course_id, + ( + SELECT + dc.title + FROM core.dim_course dc + WHERE e.course_id = dc.course_id + ) AS course_title, + COUNT(DISTINCT dl.lesson_id) AS lessons_in_course, + COUNT(DISTINCT flv.lesson_id) AS lessons_viewed, + ROUND(COUNT(DISTINCT flv.lesson_id) * 1.0 / NULLIF(COUNT(DISTINCT dl.lesson_id), 0), 2) AS completion_rate +FROM core.dim_user du + +LEFT JOIN core.enrollments e ON du.user_id = e.user_id +LEFT JOIN core.dim_lesson dl ON e.course_id = dl.course_id +LEFT JOIN core.fact_lesson_views flv ON dl.lesson_id = flv.lesson_id AND du.user_id = flv.user_id +GROUP BY du.user_id, du.name, e.course_id, course_title; + + +--✅ Задание 10: Проверь витрины +ДАННЫЕ ВЫВОДЯТСЯ, ОНИ КОРРЕКТНЫ +SELECT * FROM dm.lesson_popularity_summary LIMIT 10; + +SELECT * FROM dm.lesson_popularity_summary LIMIT 10; +lesson_id lesson_title course_id course_title total_views unique_users first_view last_view +2 JOIN 1 SQL для начинающих 6 6 2023-01-07 10:15:00.000 2023-02-26 11:20:00.000 +3 WHERE и фильтрация 1 SQL для начинающих 5 5 2023-01-08 09:00:00.000 2023-04-10 11:30:00.000 +6 Pandas основы 2 Python для анализа данных 6 6 2023-01-10 14:00:00.000 2023-03-12 14:00:00.000 +7 NumPy 2 Python для анализа данных 4 4 2023-01-11 10:00:00.000 2023-03-13 09:00:00.000 +9 Основы BI 3 BI с нуля 6 6 2023-01-10 08:00:00.000 2023-04-02 10:00:00.000 +14 Графики и диаграммы 4 Excel для аналитиков 1 1 2023-02-07 10:00:00.000 2023-02-07 10:00:00.000 +15 Введение в ML 5 Основы машинного обучения 1 1 2023-03-21 10:00:00.000 2023-03-21 10:00:00.000 +16 Линейная регрессия 5 Основы машинного обучения 1 1 2023-03-22 11:00:00.000 2023-03-22 11:00:00.000 +1 SELECT и FROM 1 SQL для начинающих 9 8 2023-01-07 10:00:00.000 2023-04-10 11:00:00.000 +4 Агрегации и GROUP BY 1 SQL для начинающих 5 3 2023-01-08 09:30:00.000 2023-01-18 10:00:00.000 + +SELECT * FROM dm.inactive_users_summary LIMIT 10; +user_id user_name email age enrollments_count registration_date +8 Henry henry@mail.com 32 3 2023-01-20 +16 Paul paul@mail.com 29 2 2023-02-15 +18 Rita rita@mail.com 27 2 2023-04-01 +17 Quinn quinn@gmail.com 24 2 2023-03-20 +20 Tina tina@mail.com 25 1 2023-04-18 +10 Jack jack@mail.com 27 1 2023-04-15 +19 Sam sam@gmail.com 30 2 2023-04-10 +11 Kelly kelly@gmail.com 23 1 2023-04-20 +15 Oscar oscar@gmail.com 33 3 2023-01-10 + +SELECT * FROM dm.course_completion_rate LIMIT 10; +user_id user_name course_id course_title lessons_is_course lesson_viewed completion_rate +2 Bob 1 SQL для начинающих 5 1 0.20 +2 Bob 3 BI с нуля 3 2 0.67 +3 Charlie 1 SQL для начинающих 5 2 0.40 +3 Charlie 2 Python для анализа данных 3 1 0.33 +4 Diana 2 Python для анализа данных 3 2 0.67 +4 Diana 3 BI с нуля 3 2 0.67 +6 Frank 1 SQL для начинающих 5 5 1.00 +6 Frank 2 Python для анализа данных 3 3 1.00 +6 Frank 3 BI с нуля 3 1 0.33 +6 Frank 4 Excel для аналитиков 3 3 1.00 \ No newline at end of file diff --git a/dmedvedev/agregation.sql b/dmedvedev/agregation.sql new file mode 100644 index 0000000..4c3f633 --- /dev/null +++ b/dmedvedev/agregation.sql @@ -0,0 +1,131 @@ +--task 1 +--Задача 1. Сколько товаров в каждой категории +SELECT p.category, COUNT (*) AS products_count +FROM products p + +GROUP BY category +ORDER BY products_count DESC; + +/*Что будет, если убрать ORDER BY — как изменится чтение результата для продукта/аналитика? +Аналитик может пропустить товар с самым большим значением или с самым малым + +Если добавить в таблицу новый товар с категорией smartphone, как изменится строка для этой категории? +Тогда количество увеличится на 1 +*/ + +--таск 2 +--Задача 2. Выручка по каждому заказу + +SELECT order_id, SUM(quantity) AS total_quantity, SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items oi + +GROUP BY order_id +ORDER BY order_revenue DESC; + +/*Что будет, если убрать GROUP BY и оставить SUM(quantity * price_at_purchase)? +Будет ошибка, так как order_id нигде не задействовано + +Почему нельзя просто добавить в SELECT ещё одну колонку (например, product_id) без добавления её в GROUP BY или агрегат? +Потому что при использовании агрегатных функций всегда что-то должно по чему-то группироваться, в данном случае идет группировка по order_id, если добавить что-то другое в селект, то нужно будет это тоже использовать в GROUP BY +*/ + +--таск 3 +--Задача 3. Выручка по категориям товаров +SELECT category, SUM (oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON oi.product_id = p.id +GROUP BY p.category +ORDER BY category_revenue DESC; + +/*Почему группировка идёт по p.category, а не по p.id? +Задача была группировать по категориям, поэтому так. Если бы задача была группировать по айдишникам продуктов, то да, делали бы так. + +Если добавить в SELECT ещё p.name, что произойдёт с результатом и почему? +Выдаст ошибку, потому что p.name дальше не используется ни в агрегате ни в группировке. +*/ + +--таск 4 +--Задача 4. Только оплаченные заказы и их выручка +SELECT COUNT(order_id), SUM (oi.quantity * oi.price_at_purchase) AS order_revenue +FROM order_items oi +JOIN orders o ON oi.order_id = o.id +WHERE status = 'paid' +ORDER BY order_revenue DESC; + +/* +Чем результат этой задачи будет отличаться от задачи 2 (где статус не учитывался)? +Во-первых здесь в выдаче одна строчка, во-вторых во второй задаче суммировались все заказы, даже не оплаченные. + +В какой реальной ситуации ты бы смотрел только на paid заказы, а не на все? +Когда необходимо подбить сколько именно денег пришло на счет за определенный период. +*/ + +--таск 5 +--Задача 5. Категории с выручкой больше 1000 (HAVING) +SELECT category, SUM (oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON oi.product_id = p.id +GROUP BY p.category +HAVING SUM (oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY category_revenue DESC; + +/* Почему нельзя написать WHERE category_revenue > 1000? +Потому что where фильтрует только строки, а не группы + +Чем WHERE price > 1000 принципиально отличается от HAVING SUM(...) > 1000? + +Принципиально отличается тем что именно фильтруется и тем когда это делается, +WHERE ставится до GROUP BY, а HAVING уже после GROUP BY +*/ + +--таск 6 +--Задача 6. Страны с достаточным числом активных пользователей +SELECT country, COUNT(*) AS active_users_count +FROM users u +WHERE is_active IS TRUE +GROUP BY country +HAVING COUNT(*) >= 2 +ORDER BY active_users_count DESC; + +/* +Почему фильтр по активности (is_active = true) логичнее писать в WHERE, а не в HAVING? +Потому что WHERE проверяет каждую строку, а у HAVING другая функция + +Что покажет запрос, если в данных пока нет ни одной страны с active_users_count >= 2? +Запрос покажет пустую таблицу без данных +*/ + +--таск 7 +--Задача 7. Топ‑товары по количеству продаж +SELECT p.name product_name, SUM(quantity) AS total_quantity_sold +FROM order_items oi +JOIN products p ON oi.product_id = p.id +GROUP BY product_name +ORDER BY SUM(quantity) DESC +LIMIT 3; + +/* +Чем рейтинг по total_quantity_sold отличается от рейтинга по выручке (SUM(quantity * price_at_purchase))? +Тем что мы видим самые продаваемые товары, которые имеют наибольшую популярность и здесь появляются вопросы , а почему так? продаем слишком дешево или товары слишком классные? +В любом случае для бизнеса лучше всего поднять цену на такие товары, чтобы они приносили больше прибыли. + +В каких случаях бизнесу важнее смотреть на количество, а не на деньги? +Я думаю что количество всегда важнее, а лучше и количество и выручка. Именно количество дает понимание оборачиваемости по продукту и его востребованности. +Бывают товары с большой стоимостью и маленьким количеством продаж относительно других, такие товары неликвидны. +*/ + +--таск 8 +--Задача 8. Заказы с «маленькой» выручкой +SELECT order_id, SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items oi +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY SUM(quantity * price_at_purchase); + +/* +Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? +Заниженная цена, неликвидность товара. + +Что проще увидеть глазами: просто все заказы с суммой или именно отфильтрованные «микро‑заказы»? +Отфильтрованные микро-заказы. +*/ \ No newline at end of file diff --git a/dmedvedev/base_sql.sql b/dmedvedev/base_sql.sql new file mode 100644 index 0000000..4dbdab0 --- /dev/null +++ b/dmedvedev/base_sql.sql @@ -0,0 +1,66 @@ +--TASK 1 +SELECT id, name, country, created_at, email +FROM users; + +SELECT id, name, country, created_at, email +FROM users +ORDER BY created_at DESC, name ASC; +/* Почему выбрал именно эти столбцы и зачем такая сортировка? + +эти столбцы содержать основную информацию о пользователях, +а сортировка поможет выявить сначала новых пользователей, +а потом сортировку по алфавиту внутри одной даты */ + +--TASK 2 +--пользователи из страны US (id,name,country) с сортировкой по имени +SELECT id, name, country +FROM users +WHERE country = 'US' +ORDER BY name ASC; + +--пользователи из страны UK +SELECT id, name, country, email, is_active, created_at +FROM users +WHERE country = 'UK'; + +/* Какой из двух запросов маркетологу полезнее и почему? + +второй запрос будет более информативным ввиду наличия информации +по дате регистрации и активности, даже возможно перегруженным */ + +--TASK 3 +SELECT id, name, email +FROM users +WHERE email IS NOT NULL +ORDER BY name ASC; + +SELECT DISTINCT LOWER(email) AS email_lower +FROM users; + +/* Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? + +Это необходимо ввиду того, что многие пользователи при регистрации могут использовать +верхний регистр, который в свою очередь никак не влияет на самом деле на сам адрес, +но может влиять при обработке данных. +А дубликаты убираются ввиду возможной повторной регистрации пользователей, +что в свою очередь тоже может повлиять на анализ информации по пользователям. */ + +-- TASK 4 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-18' +ORDER BY created_at DESC +LIMIT 3; + +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-18' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; + +/* В каком реальном сценарии тебе пригодится OFFSET? + +OFFSET может пригодиться когда например нужно убрать пользователей, +которые зарегистрировались сегодня, а сама подписка, например, с завтрашнего дня считается. +Или когда данные нужно смотреть партиями или же когда необходимо подгрузить данные по-странично, +то есть на первой странице может быть 10 человек, на второй еще 10 */ \ No newline at end of file diff --git a/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql b/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql new file mode 100644 index 0000000..670ce89 --- /dev/null +++ b/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql @@ -0,0 +1,104 @@ +### Задача 1: **Создай таблицу** page_views: + +- user_id UInt64 +- url String +- view_time DateTime + +**Условия:** + +- Движок: MergeTree +- ORDER BY (user_id, view_time) +- PARTITION BY toYYYYMM(view_time) + +CREATE TABLE page_views ( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +### Задача 2: **Создай таблицу** users: + +- id UInt64 +- name String +- updated_at DateTime + +**Условия:** + +- Движок: ReplacingMergeTree(updated_at) +- ORDER BY id + +CREATE TABLE users ( + id UInt64, + name String, + updated_at DateTime +) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +### Задача 3: **Создай таблицу** region_sales: + +- region String +- date Date +- sales UInt64 + +**Условия:** + +- Движок: SummingMergeTree +- PARTITION BY toYYYYMM(date) +- ORDER BY (region, date) +- Вставь данные с повторами (region, date) + +CREATE TABLE region_sales ( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO region_sales (region, date, sales) +VALUES + ('RU', '2026-08-11', 150), + ('FR', '2026-08-10', 100), + ('RU', '2026-08-10', 100), + ('FR', '2026-08-09', 75), + ('RU', '2025-07-05', 175); + +### Задача 4: **Создай таблицу** user_metrics: + +- region String +- avg_age_state AggregateFunction(avg, UInt8) +- active_users_state AggregateFunction(uniq, UInt64) + +**Условия:** + +- Движок: AggregatingMergeTree +- ORDER BY region +- Вставь данные +- Читай их через avgMerge() и uniqMerge() + +CREATE TABLE user_metrics ( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +INSERT INTO user_metrics (region, avg_age_state, active_users_state) +SELECT 'RU', avgState(toUInt8(22)), uniqState(toUInt64(73)) UNION ALL +SELECT 'FR', avgState(toUInt8(33)), uniqState(toUInt64(54)) UNION ALL +SELECT 'RU', avgState(toUInt8(44)), uniqState(toUInt64(10)) UNION ALL +SELECT 'FR', avgState(toUInt8(55)), uniqState(toUInt64(15)) UNION ALL +SELECT 'RU', avgState(toUInt8(22)), uniqState(toUInt64(17)); + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; \ No newline at end of file diff --git a/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql b/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql new file mode 100644 index 0000000..85b88e7 --- /dev/null +++ b/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql @@ -0,0 +1,102 @@ +# Задачи на отработку: + +### **Задача 1.** + +На каждой ноде(узел) есть таблица logs, в которой пишутся события. + +CREATE TABLE logs ( + dt DateTime, + user_id UInt32, + event_type String +) ENGINE = MergeTree +ORDER BY dt; + +1. Создай распределённую таблицу dist_logs поверх logs с помощью ENGINE = Distributed. + +CREATE TABLE dist_logs ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'dmedvedev', + 'logs', + rand() +); + +2. Объясни, зачем нужен параметр rand() в Distributed(...). + + ЭТО КЛЮЧ ШАРДИРОВАНИЯ, ОН ПОЗВОЛЯЕТ РАСПРЕДЕЛИТЬ ДАННЫЕ РАНДОМНО И РАВНОМЕРНО ПО РАЗЛИЧНЫМ СЕРВЕРАМ. + + +### **Задача 2.** + +Есть таблица events(user_id, event_type, ts) + +CREATE TABLE events +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = MergeTree +ORDER BY (ts, user_id); + +1. Создай VIEW top_active_users, который показывает топ-100 пользователей по числу событий. + +CREATE VIEW top_active_users AS +SELECT + user_id, + COUNT(*) AS total_logs_user +FROM events +GROUP BY user_id +ORDER BY total_logs_user DESC +LIMIT 100; + +2. Объясни, почему VIEW не подходит, если нам нужно часто показывать это в дашборде с миллионами строк. + + ПОТОМУ ЧТО ПРЕДСТАВЛЕНИЮ VIEW НЕ ОБЛАДАЕТ СВОЙСТВОМ ХРАНЕНИЯ ИНФОРМАЦИИ, ПОСТОЯННО БУДУТ ПРОИСХОДИТЬ ТАКИЕ ПОВТОРНЫЕ ЗАПРОСЫЫ, + КОТОРЫЕ БУДУТ ЗАСОРЯТЬ ПАМЯТЬ, ДЛЯ ТАКИХ ЗАДАЧ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО МАТЕРИАЛИЗОВАННОЕ ПРЕДСТАВЛЕНИЕ. + + +### **Задача 3.** + +Таблица raw_events содержит "сырые" клики по сайту. + +CREATE TABLE raw_events +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE = MergeTree +ORDER BY (event_time, user_id); + +1. Создай Materialized View mv_clicks_per_day, который считает число кликов по дням. + +CREATE MATERIALIZED VIEW mv_clicks_per_day +ENGINE = SummingMergeTree +ORDER BY = user_id +AS +SELECT + toDate(event_time) AS DAY, + count(*) AS count_of_enent +FROM raw_events; + +2. В движке используй SummingMergeTree, а исходник — raw_events. +3. Проверь, что при INSERT INTO raw_events данные появляются и в mv_clicks_per_day. + +INSERT INTO raw_events (event_time, user_id, event_type) +VALUES + ('2026-01-01', 1, 'abc'), + ('2026-02-01', 2, 'hdf'), + ('2026-02-03', 3, 'hhj'), + ('2026-03-04', 4, 'sdf'), + ('2026-01-01', 5, 'gf'); + +CREATE MATERIALIZED VIEW mv_clicks_per_day +ENGINE = SummingMergeTree +ORDER BY (DAY) +AS +SELECT + toDate(event_time) AS DAY, + toUInt64(1) AS count_of_event +FROM raw_events +GROUP BY DAY; diff --git a/dmedvedev/clickhouse/3.CH_type_of_date.sql b/dmedvedev/clickhouse/3.CH_type_of_date.sql new file mode 100644 index 0000000..01025a4 --- /dev/null +++ b/dmedvedev/clickhouse/3.CH_type_of_date.sql @@ -0,0 +1,95 @@ +# Задачи на отработку: + +### Задача 1. + +Есть "грязная" таблица с данными из внешней системы: + +CREATE TABLE raw_data ( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +**Подзадачи:** + +1. Преобразуй `balance` в `Decimal(10, 2)` — безопасно. +2. Преобразуй `created_at` в `DateTime64(3)`, даже если возможны ошибки. +3. Верни только строки, в которых оба преобразования прошли успешно. + SELECT + user_id, + toDecimal100rNull(balance, 2), + toDateTime640rNull(created_at, 3) + FROM raw_data + WHERE toDecimal640rNull(balance, 2) AND toDateTime640rNull(created_at, 3) IS NOT NULL; + +4. В чём разница между: + - CAST(balance AS Decimal(10, 2)) -- не поддерживает NULL вообще, при исполнении такой строки, если на входе не число, то запрос упадет в ошибку. + - toDecimal64OrNull(balance, 2) -- если на входе не число, то на выходе будет NULL + - toDecimal64OrZero(balance, 2) -- если на входе не число, то на выходе будет 0 + +### **Задача 2:** + +Есть таблица: + +CREATE TABLE events ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +); + +**Подзадачи:** + +1. Преобразуй **`event_date`** в **`Date`**, игнорируя некорректные значения. +2. Извлеки час из **`event_time`** (с обработкой ошибок). +3. Собери **`DateTime`** из валидных **`event_date`** и **`event_time`**. + + SELECT + event_id, + toDate320rNull(event_date), + toTime640rNull(event_time) + FROM events + WHERE toDate320rNull(event_date) AND toTime640rNull(event_time) IS NOT NULL; + +### **Задача 3:** + +Есть таблица с JSON-данными: + +CREATE TABLE user_activity ( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +); + +**Задания:** + +1. Преобразуй JSON-строку в массив (**`Array(String)`**). +2. Найди пользователей, совершивших действие "purchase". +3. Подсчитай количество действий у каждого пользователя + SELECT + user_id, + JSONExtract(actions, 'Array(String)'), + lenght(JSONExtract(actions, 'Array(String)')) + FROM user_activity + WHERE has(JSONExtract(actions, 'Array(String)'), 'purchase'); + +### **Задача 4:** + +Есть таблица: + +CREATE TABLE sensor_data ( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +); + +**Задания:** + +1. Преобразуй **`status`** в строку (**`String`**). +2. Посчитай среднее значение **`value`** для каждого **`status`**. +3. Отфильтруй строки, где **`value`** нельзя преобразовать в **`Decimal(10,2)`**. + + SELECT + toString(status), + AVG(value) + FROM sensor_data + WHERE toDecimal640rNull(toString(value), 2) IS NOT NULL + GROUP BY toString(status); \ No newline at end of file diff --git a/dmedvedev/clickhouse/4.CH_syntaxis.sql b/dmedvedev/clickhouse/4.CH_syntaxis.sql new file mode 100644 index 0000000..0f42980 --- /dev/null +++ b/dmedvedev/clickhouse/4.CH_syntaxis.sql @@ -0,0 +1,230 @@ +# Задачи на отработку: + +### **Задача 1.** + +Есть таблица `users`: + +| user_id | name | age | country | is_active | +| --- | --- | --- | --- | --- | +| 1 | Ivan | 30 | RU | 1 | +| 2 | Anna | 25 | RU | 1 | +| 3 | Petr | 45 | RU | 0 | +| 4 | Olga | 35 | RU | 1 | +| 5 | John | 55 | US | 1 | +| 6 | Alice | 42 | US | 0 | +| 7 | Tom | 38 | US | 1 | +| 8 | Lucas | 28 | BR | 1 | +| 9 | Maria | 60 | BR | 1 | +| 10 | João | 27 | BR | 0 | +| 11 | Kenji | 33 | JP | 1 | +| 12 | Yuki | 29 | JP | 0 | +| 13 | Chen | 40 | CN | 1 | +| 14 | Li | 48 | CN | 1 | +| 15 | Fatima | 31 | AE | 1 | +| 16 | Ahmed | 36 | AE | 1 | +| 17 | George | 24 | US | 1 | +| 18 | Nina | 34 | RU | 1 | +| 19 | Max | 50 | DE | 1 | +| 20 | Sophie | 22 | DE | 0 | + +CREATE TABLE users ( + user_id UInt64, + name String, + age UInt32, + country String, + is_active UInt32 +) +ENGINE = MergeTree() +ORDER BY (age, country); + +1. Получи 10 самых старших **активных** пользователей. + SELECT + user_id, + name, + age + FROM users + WHERE is_active = 1 + ORDER BY age DESC + LIMIT 10; + +2. Выведи пользователей из `country = 'RU'`, отсортированных по имени. + SELECT + user_id, + name + FROM users + WHERE country = 'RU' + ORDER BY name; + +3. Что произойдёт, если написать `LIMIT 10` **без `ORDER BY`**? + В таком случае CH покажет 10 произвольных неотсортированных строк + +4. Посчитай количество пользователей по странам. + SELECT + country, + COUNT(*) + FROM users + GROUP BY country; + +5. Для каждой страны выведи минимальный и максимальный возраст. + SELECT + country, + MIN(age), + MAX(age) + FROM users + GROUP BY country; + +6. Отсортируй страны по убыванию количества пользователей. + SELECT + country, + COUNT(*) + FROM users + GROUP BY country + ORDER BY COUNT(*) DESC; + +📌 Дополнительно: + +- Используй `count()`, `min()`, `max()`, `sum()` +- Попробуй `uniqExact(country)` — что он делает? + uniqExact выводит количество уникальных значений, это тоже самое что и DISTINCT + +### **Задача 2.** + +Есть таблица `events`: + +| user_id | event_time | event_type | +| --- | --- | --- | +| 1 | 2025-07-17 09:00:00 | click | +| 1 | 2025-07-17 09:05:00 | view | +| 1 | 2025-07-17 09:10:00 | purchase | +| 2 | 2025-07-17 10:00:00 | click | +| 2 | 2025-07-17 10:05:00 | click | +| 2 | 2025-07-17 11:00:00 | view | +| 3 | 2025-07-16 12:00:00 | view | +| 3 | 2025-07-16 12:30:00 | click | +| 3 | 2025-07-17 08:00:00 | purchase | +| 4 | 2025-07-17 13:00:00 | click | +| 4 | 2025-07-17 14:00:00 | view | +| 4 | 2025-07-17 15:00:00 | view | +| 5 | 2025-07-16 20:00:00 | view | +| 5 | 2025-07-17 01:00:00 | click | +| 5 | 2025-07-17 06:00:00 | purchase | +| 6 | 2025-07-17 07:30:00 | view | +| 6 | 2025-07-17 08:45:00 | click | +| 7 | 2025-07-16 23:59:59 | click | +| 7 | 2025-07-17 00:01:00 | purchase | +| 7 | 2025-07-17 10:10:00 | view | + +CREATE TABLE events ( + user_id UInt64, + event_type String, + event_time DateTime64(3) +) +ENGINE = MergeTree() +ORDER BY (event_time, user_id); + +INSERT INTO dmedvedev.events (user_id, event_time, event_type) VALUES +(1, '2025-07-17 09:00:00', 'click'), +(1, '2025-07-17 09:05:00', 'view'), +(1, '2025-07-17 09:10:00', 'purchase'), +(2, '2025-07-17 10:00:00', 'click'), +(2, '2025-07-17 10:05:00', 'click'), +(2, '2025-07-17 11:00:00', 'view'), +(3, '2025-07-16 12:00:00', 'view'), +(3, '2025-07-16 12:30:00', 'click'), +(3, '2025-07-17 08:00:00', 'purchase'), +(4, '2025-07-17 13:00:00', 'click'), +(4, '2025-07-17 14:00:00', 'view'), +(4, '2025-07-17 15:00:00', 'view'), +(5, '2025-07-16 20:00:00', 'view'), +(5, '2025-07-17 01:00:00', 'click'), +(5, '2025-07-17 06:00:00', 'purchase'), +(6, '2025-07-17 07:30:00', 'view'), +(6, '2025-07-17 08:45:00', 'click'), +(7, '2025-07-16 23:59:59', 'click'), +(7, '2025-07-17 00:01:00', 'purchase'), +(7, '2025-07-17 10:10:00', 'view'); + + +1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`). + SELECT + user_id, + COUNT() OVER (PARTITION BY user_id ORDER BY user_id) + FROM events; + +2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)` + SELECT + user_id, + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) + FROM events; + +3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли? + + Посчитается, поддерживается. + SELECT + user_id, + COUNT(*) OVER ( + PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) + FROM events; + +4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа. + SELECT + user_id, + event_time + FROM events + WHERE event_time <= NOW() - INTERVAL 24 HOUR; + +5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий. + WITH + + recent_events AS ( + SELECT + user_id, + event_type, + event_time + FROM events + WHERE event_time >= NOW() - INTERVAL 24 HOUR + ) + + SELECT + user_id, + COUNT(*) + FROM recent_events + GROUP BY user_id + ORDER BY COUNT(*) DESC + LIMIT 5; + +6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе. + WITH + + recent_events AS ( + SELECT + user_id, + event_type, + event_time + FROM events + WHERE event_time >= NOW() - INTERVAL 24 HOUR + ), + + top_users AS( + SELECT + user_id, + COUNT(*) + FROM recent_events + GROUP BY user_id + ORDER BY COUNT(*) DESC + LIMIT 5 + ) + + SELECT + user_id, + CASE + WHEN event_type IN ('click', 'view') THEN 'interaction' + WHEN event_type IN ('purchase') THEN 'conversion' + ELSE 'unknown' + END AS category + FROM recent_events re INNER JOIN top_users tu ON re.user_id = tu.user_id; + +📌 Вопрос: поддерживает ли ClickHouse **многоуровневые** или **рекурсивные** CTE? + МНОГОУРОВНЕВОСТЬ ЕСТЬ, А РЕКУРСИВНОСТИ НЕТ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/5.CH_joins.sql b/dmedvedev/clickhouse/5.CH_joins.sql new file mode 100644 index 0000000..6c7f420 --- /dev/null +++ b/dmedvedev/clickhouse/5.CH_joins.sql @@ -0,0 +1,167 @@ +# Задачи на отработку: + +### **Задача 1.** + +Таблица `products` + +| product_id | name | +| --- | --- | +| 101 | iPhone 14 | +| 102 | Galaxy S22 | +| 103 | Pixel 7 | + +Таблица `prices` + +| product_id | price | region | +| --- | --- | --- | +| 101 | 999 | US | +| 101 | 1049 | EU | +| 102 | 850 | US | +| 102 | 899 | CA | +| 103 | 799 | JP | +1. Сделай `ANY LEFT JOIN`, чтобы получить цену **любой** из регионов. + SELECT + pr.product_id, + p.region, + pr.name, + p.price + FROM prices p + ANY LEFT JOIN products pr ON p.product_id = pr.product_id; + ЗДЕСЬ МЫ ПОЛУЧИМ НА ВЫХОДЕ 5 СТРОК. + А ЕСЛИ БЫ ДЖОЙНИЛИ ТАБЛИЦЫ НАОБОРОТ, ТО ПОЛУЧИЛИ БЫ 3 СТРОКИ. + +2. Что будет, если в таблице `prices` несколько цен на один `product_id`? + ВСЕ БУДЕТ ЗАВИСЕТЬ ОТ СОЕДИНЕНИЯ ТАБЛИЦ. ЕСЛИ МЫ ДЖОЙНИМ К ТАБЛИЦЕ С product_id И НА КАЖДОЫЙ ЕСТЬ НЕСКОЛЬКО ЦЕН, ТО В ТАКОМ СЛУЧАЕ КЛИКХАУС ВОЗЬМЕТ ПЕРВОЕ ЗНАЧЕНИЕ С ДИСКА И ПРИСВОИТ ЕГО К product_id, А ОСТАЛЬНЫЕ ЗНАЧЕНИЯ ВЫБРОСИТ. + +3. Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают? + ИЗ-ЗА ПРИМЕНЕНИЯ ОЛЛ ЛЕФТ ДЖОЙН КЛИКХАУС НЕ СМОЖЕТ ВЫБРОСИТЬ НЕНУЖНЫЕ СТРОКИ И УЧТЕТ ВСЕ. В НАШЕМ ПРИМЕРЕ СТРОК СТАНЕТ 5. + ДАННЫЕ МОГУТ ПЕРЕГРУЖАТЬ ПАМЯТЬ И ТОРМОЗИТЬ СИСТЕМУ. МЕТРИКИ МОГУТ СЧИТАТЬСЯ ИСКАЖЕННЫМИ. + +### **Задача 2.** + +Таблица `ticks` + +| time | symbol | price | +| --- | --- | --- | +| 2025-07-17 10:00:00 | AAPL | 190.5 | +| 2025-07-17 10:01:00 | AAPL | 191.2 | +| 2025-07-17 10:02:30 | AAPL | 192.0 | +| 2025-07-17 10:05:00 | GOOG | 2850.0 | + +Таблица `orders` + +| time | symbol | size | +| --- | --- | --- | +| 2025-07-17 10:00:30 | AAPL | 50 | +| 2025-07-17 10:02:00 | AAPL | 75 | +| 2025-07-17 10:03:00 | AAPL | 20 | +| 2025-07-17 10:04:00 | GOOG | 15 | +1. Сделай `ASOF JOIN`, чтобы каждой сделке найти **предыдущую цену**. + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM orders o + ASOF LEFT JOIN ticks t ON o.symbol = t.symbol AND o.time >= t.time; + +2. Почему нельзя использовать обычный `JOIN` по `time = time`? + КАК МИНИМУМ ПОТОМУ ЧТО ВРЕМЯ ПО СДЕЛКЕ И ВРЕМЯ ПО ТИКЕТУ НЕ СОВПАДАЕТ. + +3. Сделай выборку только по `symbol = 'AAPL'`. + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM orders o + ASOF LEFT JOIN ticks t ON o.symbol = t.symbol AND o.time >= t.time + WHERE o.symbol = 'AAPL'; + +ИЛИ + + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM + ( + SELECT * FROM orders WHERE o.symbol = 'AAPL' + ) AS o + ASOF LEFT JOIN + ( + SELECT * FROM ticks WHERE o.symbol = 'AAPL' + ) AS t + ON o.symbol = t.symbol AND o.time >= t.time; + +### **Задача 3.** + +Таблица `users` + +| user_id | name | +| --- | --- | +| 1 | Alice | +| 2 | Bob | +| 3 | Charlie | +| 4 | Diana | + +Таблица `payments` + +| payment_id | user_id | amount | +| --- | --- | --- | +| 1001 | 1 | 500 | +| 1002 | 1 | 1500 | +| 1003 | 3 | 1200 | +| 1004 | 4 | 900 | +1. Верни всех пользователей, у которых **есть платежи** (через `WHERE user_id IN (...)`). + SELECT + u.user_id + FROM users u + WHERE user_id IN (SELECT user_id FROM payments); + +2. Добавь условие: сумма платежа больше 1000. + SELECT + u.user_id + FROM users u + WHERE user_id IN (SELECT user_id FROM payments WHERE amount > 1000); + +3. Почему это называют `SEMI JOIN`? Как это оптимизируется в ClickHouse? + Потому что это неполноценное соединение , а только проверяющее факт наличия того или иного условия. + Вообще по идее нужно было бы написать LEFT SEMI JOIN , в кликхаус видя конструкцию IN (SELECT...) уже сам понимает что это SEMI JOIN. + +### **Задача 4.** + +Таблица `big_logs` (упрощённый фрагмент) + +| user_id | event_time | event_type | +| --- | --- | --- | +| 1 | 2025-07-17 08:00:00 | login | +| 2 | 2025-07-17 08:05:00 | view | +| 3 | 2025-07-17 08:10:00 | purchase | +| 1 | 2025-07-17 08:20:00 | logout | +| 2 | 2025-07-17 08:30:00 | click | + +Таблица `users` + +| user_id | name | +| --- | --- | +| 1 | Alice | +| 2 | Bob | +| 3 | Charlie | +1. Сделай `JOIN`, чтобы добавить `name` к каждому логу. + SELECT + bl.user_id, + bl.event_time, + bl.event_type, + u.name + FROM big_logs bl ANY LEFT JOIN users u ON bl.user_id = u.user_id; + +2. Почему `users` нужно ставить **в правую часть JOIN**? + ПОТОМУ ЧТО ЭТА ТАБЛИЦА ЯВЛЯЕТСЯ МАЛЕНЬКОЙ И МОЖНО СКАЗАТЬ СПРАВОЧНОЙ, А ЛОГИ БУДУТ РАСТИ И РАСТИ. И ЕСЛИ ЕЕ ЗАГРУЗИТЬ В ОПЕРАТИВНУЮ ПАМЯТЬ, ТО МЫ МОЖЕМ РАНО ИЛИ ПОЗДНО ПОЛУЧИТЬ ОШИБКУ. + +3. Как выбрать `ANY` или `ALL` — если `user_id` уникален? + ЕСЛИ МЫ УВЕРЕНЫ НА 100% ЧТО ОН УНИКАЛЕН ТО НУЖНО ВАЫБИРАТЬ ANY , В ТАКОМ СЛУЧАЕ СОХРАНЯЮТСЯ РЕСУРСЫ ПАМЯТИ. ALL ЖЕ ВЫБИРАЕТСЯ КОГДА НЕОБХОДИМО ДЕКАРТОВО ПРОИЗВЕДЕНИЕ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/6.CH_indexes_partitions.sql b/dmedvedev/clickhouse/6.CH_indexes_partitions.sql new file mode 100644 index 0000000..d3aa19b --- /dev/null +++ b/dmedvedev/clickhouse/6.CH_indexes_partitions.sql @@ -0,0 +1,111 @@ +# Задачи на отработку: + +### **Задача 1.** + +Таблица `sales` + +| id | region | sale_date | amount | +| --- | --- | --- | --- | +| 1 | RU | 2024-05-01 | 100.0 | +| 2 | RU | 2024-05-15 | 120.0 | +| 3 | US | 2024-06-01 | 90.0 | +| 4 | US | 2024-07-03 | 150.0 | +| 5 | DE | 2024-07-04 | 160.0 | +1. Создай таблицу с `PARTITION BY toYYYYMM(sale_date)`. + CREATE TABLE IF NOT EXISTS sales ( + id UInt8, + region String, + sale_sate DateTime64, + amount Float64 + ) + ENGINE = MergeTree() + PARTITION BY toYYYYMM(sale_date) + ORDER BY (sale_date, region); + + +2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`. + УДАЛИЛАСЬ ПАРТИЦИЯ + +3. Что произойдет, если не указывать `PARTITION BY`? + ЕСЛИ МЫ ГОВОРИМ ПРО ПРОЦЕСС СОЗДАНИЯ, ТО В ТАКОМ СЛУЧАЕ ВСЕ ДАННЫЕ БУДУТ НАХОДИТЬСЯ ПО УМОЛЧАНИЮ В ОДНОЙ ПАРТИЦИИ. + +### **Задача 2.** + +Есть таблица `events`, и ты хочешь логически хранить данные **по типу события**. + +| id | event_type | event_date | +| --- | --- | --- | +| 1 | login | 2024-06-01 | +| 2 | click | 2024-06-01 | +| 3 | click | 2024-06-02 | +| 4 | purchase | 2024-06-03 | +| 5 | login | 2024-06-04 | +1. Создай таблицу с `PARTITION BY event_type`. + CREATE TABLE IF NOT EXISTS sales ( + id UInt8, + event_type String, + event_date Date + ) + ENGINE = MergeTree() + PARTITION BY event_type + ORDER BY (event_type, event_date); + +2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`). +3. Чем такой подход может быть опасен (подумай про кардинальность значений)? + СЕЙЧАС СОБЫТИЙ ПО ФАКТУ 3, ЭТО МАЛО И УДАЛЕНИЕ БУДЕТ ПРОИСХОДИТЬ БЫСТРО, А ЕСЛИ ПАРТИЦИЙ БУДЕТ 1000, ТО В ТАКОМ СЛУЧАЕ СИСТЕМА БУДЕТ ТОРМОЗИТЬ. + +### **Задача 3.** + +Ты хочешь разбить таблицу `logs` на 100 партиций по `user_id`, чтобы равномерно распределить объём данных. + +| id | user_id | log_time | +| --- | --- | --- | +| 1 | 123 | 2024-07-17 10:00:00 | +| 2 | 456 | 2024-07-17 10:01:00 | +| 3 | 789 | 2024-07-17 10:02:00 | +1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`. + CREATE TABLE IF NOT EXISTS logs ( + id UInt32, + user_id UInt32, + log_time DateTime64 + ) + ENGINE = MergeTree() + PARTITION BY cityHash64(user_id) % 100 + ORDER BY (user_id, log_time); + +2. Что ты выиграешь с точки зрения баланса? + Я СБАЛАНСИРОВАННО РАЗДЕЛЮ ВСЕ ЛОГИ ПО ПАРТИЦИЯМ ПО УНИКАЛЬНОМУ ЮЗЕР АЙДИ + +3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`? + ПОТОМУ ЧТО В ТАКОМ СЛУЧАЕ ПРИДЕТСЯ ВСЕ РАВНО СКАНИРОВАТЬ ВСЕ ПАРТИЦИИ, ТО ЕСТЬ ВСЮ БАЗУ ДАННЫХ, ВВИДУ ТОГО, ЧТО АЙДИШНИК 456 КЛИКХАУЦС НЕ СМОЖЕТ ПОДСТАВИТЬ В + cityHash64(user_id) ДЛЯ РАСЧЕТА В ХЭЩ ФУНКЦИИ. ПОЭТОМУ ОПТИМИЗАТОРУ НЕОБХОДИМО БУДЕТ ЗАГЛЯНУТЬ В 100 ФАЙЛОВ НА ДИСКЕ . + +### **Задача 4.** + +В таблице `visits` данные зависят от региона и времени. + +| id | region | visit_date | +| --- | --- | --- | +| 1 | RU | 2024-06-01 | +| 2 | RU | 2024-06-15 | +| 3 | US | 2024-06-03 | +| 4 | US | 2024-07-01 | +| 5 | RU | 2024-07-05 | +1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`. + CREATE TABLE IF NOT EXISTS visits ( + id UInt8, + region String, + visit_date Date + ) + ENGINE = MergeTree() + PARTITION BY (region, toYYYYMM(visit_date)) + ORDER BY (visit_date, id); + +2. Как будет выглядеть имя партиции? + "RU-202406" + "US-202406" + "US-202407" + "RU-202407" + +3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? + НЕТ КОНЕЧНО, ТАК КАК ЭТИ ДАННЫЕ НАХОДЯТСЯ В ДВУХ ПАРТИЦИЯХ, ТО НЕОБХОДИМО БУДЕТ С ПОМОЩЬЮ УДАЛЯТЬ ЧЕРЕЗ "RU-202407". \ No newline at end of file diff --git a/dmedvedev/clickhouse/7.CH_indexes.sql b/dmedvedev/clickhouse/7.CH_indexes.sql new file mode 100644 index 0000000..809c46b --- /dev/null +++ b/dmedvedev/clickhouse/7.CH_indexes.sql @@ -0,0 +1,115 @@ +# Задачи на отработку: + +### **Задача 1.** + +**Таблица:** `logs(log_id UInt64, ts DateTime)` + +| log_id | ts | +| --- | --- | +| 1 | 2024-06-01 10:00:00 | +| 2 | 2024-06-01 11:00:00 | +| 3 | 2024-06-02 12:00:00 | +| 4 | 2024-06-03 13:00:00 | +| 5 | 2024-06-04 14:00:00 | +1. Создай `ORDER BY ts`, добавь `minmax`индекс: + + ALTER TABLE logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2; + + ГОТОВО + +2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'` + ГОТОВО + +3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`). + НЕТ ПРАВ ДОСТУПА + +4. Что будет, если `ORDER BY log_id`, а не `ts`? + ТОГДА ДАННЫЕ НА ДИСКЕ БУДУТ ОТСОРТИРОВАНЫ ПО log_id, а первичный и вторичный индексы по ts перестанут помогать в поиске данных. + + +### **Задача 2.** + +**Таблица:** `clicks(id UInt64, country String)` + +| id | country | +| --- | --- | +| 1 | RU | +| 2 | US | +| 3 | RU | +| 4 | DE | +| 5 | RU | +1. Создай таблицу с `ORDER BY id`, добавь индекс: + + ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + + ДОБАВИЛ + +2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`. + ВЫПОЛНИЛ + +3. Посмотри `EXPLAIN` — индекс используется? + + ИСПОЛЬЗУЕТСЯ + + Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dmedvedev.clicks) + Indexes: + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_country + Description: set GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + +4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений? + ОН НЕ БУДЕТ РАБОТАЬ, ТАК КАК МЫ ИНДЕКСЕ set МЫ УСТАНОВИЛИ ПОРОГ УНИКАЛЬНЫХ ЗНАЧЕНИЙ В 1000 ШТУК. + В ТАКИХ СЛУЧАЯХ НУЖНО БЫЛО БЫ ВОСПОЛЬЗОВАТЬСЯ БЛУМ ФИЛЬТРОМ. + +### **Задача 3.** + +**Таблица:** `users(id UInt64, email String)` + +| id | email | +| --- | --- | +| 1 | alice@mail.com | +| 2 | bob@mail.com | +| 3 | charlie@mail.com | +| 4 | dave@mail.com | +| 5 | eve@example.com | +1. Добавь индекс: + + ALTER TABLE users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + ДОБАВИЛ + +2. Проверь поиск по точному email: + SELECT * FROM users WHERE email = 'alice@mail.com'; + + РАБОТАЕТ + 1 alice@mail.com + + Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dmedvedev.users) + Indexes: + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_email + Description: bloom_filter GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + +3. Почему этот индекс **не ускоряет** поиск по `LIKE '%mail%'`? + ПОТОМУ ЧТО ФИЛЬТР БЛУМА НЕ МОЖЕТ ИСКАТЬ ДАННЫЕ КОТОРЫЕ СОДЕРЖАТЬСЯ ВНУТРИ КАКИХ-ТО СТРОК, ЕМУ НУЖНО ПОНИМАТЬ КАКОЕ БУДЕТ НАЧАЛО, А ЗДЕСЬ ОНО НЕ УКАЗАНО. + ДЛЯ ТАКОГО ПОИСКА НУЖНО ПРИМЕНЯТЬ МОДЕРНИЗИРОВАННЫЙ ФИЛЬТР БЛУМА ПО ТОКЕНАМ. + +4. Что произойдёт при `LIKE 'alice%'`? + В ТАКОМ СЛУЧАЕ ОБЫКНОВЕННЫЙ ФИЛЬТР БЛУМА СРАБОТАЕТ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/8.CH_compretion.sql b/dmedvedev/clickhouse/8.CH_compretion.sql new file mode 100644 index 0000000..c1ab8fe --- /dev/null +++ b/dmedvedev/clickhouse/8.CH_compretion.sql @@ -0,0 +1,26 @@ +### **Задача 1.** + +У тебя таблица `logs`с длинными строками — сообщения занимают много места. + +| id | level | message | created_at | +| --- | --- | --- | --- | +| 1 | INFO | "User logged in" | 2024-07-01 10:00:00 | +| 2 | WARN | "Request took too long" | 2024-07-01 10:05:00 | +| 3 | ERROR | "Unable to connect to server" | 2024-07-01 10:10:00 | +1. Создай таблицу `logs` с колонкой `message CODEC(ZSTD(3))`. +2. Добавь `created_at CODEC(Delta, ZSTD)` — это улучшит хранение по времени. +3. Посмотри статистику сжатия: + + SELECT + column, + codec_expression, + compression_codec + FROM system.columns + WHERE table = 'logs'; + + ОТВЕТ: + name type compression_codec + id UInt64 - + LEVEL String - + message String CODEC(ZSTD(3)) + created_at DateTime CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/dmedvedev/clickhouse/9.CH_pet_project.sql b/dmedvedev/clickhouse/9.CH_pet_project.sql new file mode 100644 index 0000000..abad31c --- /dev/null +++ b/dmedvedev/clickhouse/9.CH_pet_project.sql @@ -0,0 +1,283 @@ +Этап 1. Загрузка тестовых данных и создание таблиц + +CREATE TABLE dmedvedev.core_dim_course +ON CLUSTER clickhouse_cluster ( + course_id UInt32, + title String, + category String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +CREATE TABLE dmedvedev.core_dim_lesson +ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + title String, + duration_min UInt32, + course_id UInt32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_lesson', + '{replica}' +) +ORDER BY lesson_id; + +CREATE TABLE dmedvedev.core_dim_user ON CLUSTER clickhouse_cluster( + user_id UInt32, + name String, + age UInt32, + email String, + registration_date DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dmedvedev.core_enrollments ON CLUSTER clickhouse_cluster( + user_id UInt32, + course_id UInt32, + enrolled_at DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_enrollments', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dmedvedev.core_fact_lesson_views ON CLUSTER clickhouse_cluster ( + user_id UInt32, + course_id UInt32, + lesson_id UInt32, + viewed_at DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_fact_lesson_views', + '{replica}' +) +ORDER BY user_id; + +--Этап 2. Задание 2. +--СОЗДАЕМ МАТ ПРЕДСТАВЛЕНИЕ mv_lesson_popularity_summary +--для начала создаем таблицу в кликхаусе, реплицируемую с движком ReplicatedAggregatingMergeTree +CREATE TABLE dmedvedev.dm_lesson_popularity_summary ON CLUSTER clickhouse_cluster +( + lesson_id UInt32, + lesson_title String, + course_id UInt32, + course_title String, + total_views SimpleAggregateFunction(sum, UInt64), + unique_users AggregateFunction(uniq, UInt32), + first_view SimpleAggregateFunction(min, DateTime), + last_view SimpleAggregateFunction(max, DateTime) +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_lesson_popularity_summary', + '{replica}') +ORDER BY (lesson_id, lesson_title, course_id, course_title); + +--далее создаем мат представление, считывает новые данные, делает джойны и кладет данные в dm_lesson_popularity_summary +-- +CREATE MATERIALIZED VIEW dmedvedev.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_lesson_popularity_summary AS +SELECT + flv.lesson_id AS lesson_id, + dl.title AS lesson_title, + dc.course_id AS course_id, + dc.title AS course_title, + COUNT(*) AS total_views, + uniqState(flv.user_id) AS unique_users, + MIN(flv.viewed_at) AS first_view, + MAX(flv.viewed_at) AS last_view +FROM dmedvedev.core_fact_lesson_views AS flv +LEFT JOIN dmedvedev.core_dim_lesson AS dl ON flv.lesson_id = dl.lesson_id +LEFT JOIN dmedvedev.core_dim_course AS dc ON dl.course_id = dc.course_id +GROUP BY lesson_id, lesson_title, course_id, course_title; + +-- помещаем данные в dm_lesson_popularity_summary +INSERT INTO dmedvedev.dm_lesson_popularity_summary +SELECT + flv.lesson_id AS lesson_id, + dl.title AS lesson_title, + dc.course_id AS course_id, + dc.title AS course_title, + COUNT(*) AS total_views, + uniqState(flv.user_id) AS unique_users, + MIN(flv.viewed_at) AS first_view, + MAX(flv.viewed_at) AS last_view +FROM dmedvedev.core_fact_lesson_views AS flv +LEFT JOIN dmedvedev.core_dim_lesson AS dl ON flv.lesson_id = dl.lesson_id +LEFT JOIN dmedvedev.core_dim_course AS dc ON dl.course_id = dc.course_id +GROUP BY lesson_id, lesson_title, course_id, course_title; + +ЗАПРОС (ТОП 5 самых просматриваемых уроков уникальными пользователями): +SELECT + lesson_id, + lesson_title, + total_views, + uniqMerge(unique_users) AS unique_users +FROM dmedvedev.dm_lesson_popularity_summary dlps +GROUP BY lesson_id, lesson_title, course_id, course_title, total_views, first_view, last_view +ORDER BY unique_users DESC +LIMIT 5; + + +--Задание 3. СОЗДАЕМ МАТ ПРЕДСТАВЛЕНИЕ mv_inactive_users_summary +--для начала создаем таблицу в кликхаусе, реплицируемую с движком ReplicatedAggregatingMergeTree +CREATE TABLE dmedvedev.dm_inactive_users_summary ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + email String, + age UInt32, + registrated_courses_count AggregateFunction(uniqExact, UInt32), + registration_date DateTime +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_inactive_users_summary', + '{replica}' +) +ORDER BY (user_id, name, email, age, registration_date); + +--создаем само мат представление, которое перехватывает данные из таблицы фактов и помещает в кликхаус dmedvedev.dm_inactive_users_summary +CREATE MATERIALIZED VIEW dmedvedev.mv_inactive_users_summary ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_inactive_users_summary +AS +SELECT + ce.user_id, + cdu.name, + cdu.email, + cdu.age, + uniqExactState(ce.course_id) AS registrated_courses_count, + cdu.registration_date +FROM core_enrollments ce +LEFT JOIN core_dim_user cdu ON ce.user_id = cdu.user_id +GROUP BY ce.user_id, cdu.name, cdu.email, cdu.age, cdu.registration_date; + +-- помещаем данные в dm_inactive_users_summary +INSERT INTO dmedvedev.dm_inactive_users_summary +SELECT + ce.user_id, + cdu.name, + cdu.email, + cdu.age, + uniqExactState(ce.course_id) AS registrated_courses_count, + cdu.registration_date +FROM core_enrollments ce +LEFT JOIN core_dim_user cdu ON ce.user_id = cdu.user_id +GROUP BY ce.user_id, cdu.name, cdu.email, cdu.age, cdu.registration_date; + +--читаем данные из dm_inactive_users_summary +SELECT + user_id, + name, + email, + age, + uniqExactMerge(registrated_courses_count) AS registrated_courses_count, + registration_date +FROM dmedvedev.dm_inactive_users_summary +GROUP BY user_id, name, email, age, registration_date; + +ЗАПРОС (ученики зарегистрировавшиеся только на 1 курс): +SELECT + user_id, + name, + email, + age, + uniqExactMerge(registrated_courses_count) AS registrated_courses_count, + registration_date +FROM dmedvedev.dm_inactive_users_summary +GROUP BY user_id, name, email, age, registration_date +HAVING registrated_courses_count = 1; + +--Задание 4. создаем витрину course_completion_rate +--для начала создаем таблицу в кликхаус с движком +CREATE TABLE dmedvedev.dm_course_completion_rate ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + course_id UInt32, + course_title String, + lessons_in_course SimpleAggregateFunction(max, UInt32), + lessons_viewed AggregateFunction(uniqExact, UInt32) +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id, name, course_title); + +--далее создаем мат представление +CREATE MATERIALIZED VIEW dmedvedev.mv_course_completion_rate ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_course_completion_rate +AS +SELECT + cflv.user_id AS user_id, + cdu.name AS name, + cdl.course_id AS course_id, + cdc.title AS course_title, + max(count_lesson_in_course.total_leeson) AS lessons_in_course, + uniqExactState(cflv.lesson_id) AS lessons_viewed +FROM core_fact_lesson_views cflv +LEFT JOIN core_dim_user cdu ON cflv.user_id = cdu.user_id +LEFT JOIN core_dim_lesson cdl ON cflv.lesson_id = cdl.lesson_id +LEFT JOIN core_dim_course cdc ON cdl.course_id = cdc.course_id +LEFT JOIN ( + SELECT course_id, uniqExact(lesson_id) AS total_leeson + FROM dmedvedev.core_dim_lesson cdm + GROUP BY course_id +) AS count_lesson_in_course ON count_lesson_in_course.course_id = cdl.course_id +GROUP BY cflv.user_id, cdl.course_id, cdu.name, cdc.title; + +--вставляем данные +INSERT INTO dmedvedev.dm_course_completion_rate +SELECT + cflv.user_id AS user_id, + cdu.name AS name, + cdl.course_id AS course_id, + cdc.title AS course_title, + max(count_lesson_in_course.total_leeson) AS lessons_in_course, + uniqExactState(cflv.lesson_id) AS lessons_viewed +FROM core_fact_lesson_views cflv +LEFT JOIN core_dim_user cdu ON cflv.user_id = cdu.user_id +LEFT JOIN core_dim_lesson cdl ON cflv.lesson_id = cdl.lesson_id +LEFT JOIN core_dim_course cdc ON cdl.course_id = cdc.course_id +LEFT JOIN ( + SELECT course_id, uniqExact(lesson_id) AS total_leeson + FROM dmedvedev.core_dim_lesson cdm + GROUP BY course_id +) AS count_lesson_in_course ON count_lesson_in_course.course_id = cdl.course_id +GROUP BY cflv.user_id, cdl.course_id, cdu.name, cdc.title; + + +ЗАПРОС: ученики просмотревшие какой-либо курс менее чем на половину +SELECT + user_id, + name, + course_id, + course_title, + max(lessons_in_course) AS total_lessons, + -- Объединяем просмотры: + uniqExactMerge(lessons_viewed) AS viewed_lessons, + IF(total_lessons > 0, CAST(viewed_lessons AS Float64) / total_lessons, 0.0) AS completion_rate +FROM dmedvedev.dm_course_completion_rate +GROUP BY user_id, name, course_id, course_title +HAVING completion_rate <= 0.5 +ORDER BY user_id, course_id; + +--ЭТАП 4. Тестирование и валидация +В ДАННОМ ЗАДАНИИ Я НЕ ПРОИЗВОДИЛ СРАВНЕНИЕ ТАБЛИЦ ИЗ ГП И КХ, ТАК КАК ТАКБЛИЦЫ +С ОДИНАКОВЫМИ НАЗВАНИЯМИ ПРОСТО НАПРОСТО СЧИТАЮТ РАЗНЫЕ ПОКАЗАТЕЛИ. +ПОЧЕМУ ТАК ПРОИЗОШЛО? ПОТОМУ ЧТО В ЗАДАНИИ КХ ДАНЫ СОВСЕМ ДРУГИЕ ПОКАЗАЛИТЕ, ПОЭТОМУ ТАБЛИЦЫ РАЗЛИЧАЮТСЯ ПО НАПОЛНЕНИЮ. + +ЧТО Я ЗАМЕТИЛ ПРИ СОЗДАНИИ МАТ ПРЕДСТАВЛЕНИЙ В КХ? +ТО ЧТО В ГП МАТ ПРЕДСТАВЛЕНИЯ ЭТО ОТДЕЛЬНАЯ ТАБЛИЦА, КОТОРАЯ ХРАНИТ ОПРЕДЕЛЕННЫЙ РЕЗУЛЬТАТ, ОНА НЕ ЯВЛЕТСЯ ДИНАМИЧЕСКОЙ, +ЕЕ НЕОБХОДИМО ОБНОВЛЯТЬ ПРИ ПОСТУПЛЕНИИ НОВЫХ ДАННЫХ, СООТВЕТСТВЕННО ВЕСЬ АНАЛИЗ ДАННЫХ ГП ДЕЛАЕТ ПО ВСЕМ ДАННЫМ ЗАПРОСА. +КХ В ОТЛИЧИЕ ОТ ГП РАБОТАЕТ ИНАЧЕ, ОН ПОДХВАТЫВАЕТ ЛИШЬ НОВЫЕ ДАННЫЕ, ПРИЧЕМ ЭТИ НОВЫЕ ДАННЫЕ МОГУТ ПОСТУПАТЬ ТОЛЬКО ИЗ ТАБЛИЦЫ ФАКТОВ, +ЕСЛИ ОНИ ПОСТУПЯТ ИЗ ТАБЛИЦ ИЗМЕРЕНИЙ, ТО НИКАКИХ РАСЧЕТОМ КХ ДЕЛАТЬ НЕ БУДЕТ. +ТАКЖЕ СТОИТ ОТМЕТИТЬ, ЧТО В КХ ПОЯВЛЯЕТСЯ ЦЕЛЕВАЯ ТАБЛИЦА, В КОТОРУЮ МАТ ПРЕДСТАВЛЕНИЕ И ПРОИЗВОДИТ ЗАПИСЬ ДАННЫХ И ПОТОМ +В ЗАПРОСЕ МЫ ОБРАЩАЕМСЯ ИМЕННО К ЦЕЛЕВОЙ ТАБЛИЦЕ, А НЕ К МАТ ПРЕДСТАВЛЕНИЮ (МАТ ПРЕДСТАВЛЕНИЕ ЭТО ВСЕГО ЛИШЬ ТРИГГЕР, КОТОРЫЕ УЛАВЛИВАЕТ НОВЫЕ ДАННЫЕ ИЗ ТАБЛИЦЫ ФАКТОВ). diff --git a/dmedvedev/ddl.sql b/dmedvedev/ddl.sql new file mode 100644 index 0000000..fcb53ed --- /dev/null +++ b/dmedvedev/ddl.sql @@ -0,0 +1,39 @@ +CREATE TABLE users +( + id INT PRIMARY KEY NOT NULL, + name VARCHAR(100) NOT NULL, + age INT, + email VARCHAR(100), + country VARCHAR(2), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE products +( + id INT PRIMARY KEY NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10,2) NOT NULL, + in_stock BOOLEAN +); + +CREATE TABLE orders +( + id SERIAL PRIMARY KEY NOT NULL, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE order_items +( + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + 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) +); \ No newline at end of file diff --git a/dmedvedev/dml.sql b/dmedvedev/dml.sql new file mode 100644 index 0000000..4621dda --- /dev/null +++ b/dmedvedev/dml.sql @@ -0,0 +1,42 @@ +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, TRUE), + (2, 'Kindle', 'Электроника', 15000, TRUE), + (3, 'Кроссовки Nike', 'Одежда', 9000, TRUE), + (4, 'Кроссовки NoName', 'Одежда', 3000, FALSE), + (5, 'Наушники Pro', 'Электроника', 12000, TRUE), + (6, 'Фитнес‑браслет', 'Гаджеты', 6000, TRUE), + (7, 'Подарочная карта', 'Сервисы', 1000, 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), + (1, 3, 2, 8500), + (2, 2, 1, 14000), + (3, 5, 1, 11000), + (3, 7, 1, 1000), + (4, 2, 1, 15000), + (5, 6, 1, 6000), + (6, 1, 1, 82000), + (7, 3, 1, 9000), + (8, 4, 2, 2800); \ No newline at end of file diff --git a/dmedvedev/filtering.sql b/dmedvedev/filtering.sql new file mode 100644 index 0000000..d7e8e37 --- /dev/null +++ b/dmedvedev/filtering.sql @@ -0,0 +1,44 @@ +-- TASK 1 +--Задача 1 — Товары для продажи +SELECT name, category, price, price * 1.3 AS price_with_markup, price * 1.5 AS price_with_vat +FROM products +WHERE in_stock IS TRUE AND category != 'smartwatch' AND price BETWEEN 100 AND 1300; +/* Что лучше читается `BETWEEN 100 AND 1300` или `price >= 100 AND price <= 1300` и почему? + +Читается лучше выражение с between потому что сразу видно и понятно внутри какого диапазона мы находимся, +а когда используются знаки сравнения, то необходимо вчитываться и вдумываться, какой знаки что именно он обозначает*/ + +--TASK 2 +-- Задача 2 — Скидочная корзина: дешёвые или дорогие +SELECT name, price, price * 0.85 AS price_with_discount +FROM products +WHERE price < 100 OR price > 1000; +/* Почему важно явно писать скобки в условии с **`OR`**, даже если кажется, что и так понятно? + +потому что скобки защищают от логических ошибок */ + +--TASK 3 +--Задача 3 — Категории для рекламы +SELECT id, name, category, price +FROM products +WHERE category IN ('tablet', 'smartphone', 'mouse') AND price < 1000 +ORDER BY price DESC; + +--TASK 4 +-- Задача 4 — Убрать iPhone из выборки +SELECT name, category, price +FROM products +WHERE name NOT LIKE '%iPhone%'; + +--TASK 5 +-- Задача 5 — Поиск товаров по названию +SELECT * +FROM products +WHERE name ILIKE '%pro%'; + +SELECT * +FROM products +WHERE name ILIKE '%mx%'; + +/* так лучше потому что для магазина с поисковой строкой не должно быть важно какой регистр используют люди для поиска товара, + в любом случае, должен находиться релевантный товар для человека*/ \ No newline at end of file diff --git a/dmedvedev/function: date.sql b/dmedvedev/function: date.sql new file mode 100644 index 0000000..e4a603c --- /dev/null +++ b/dmedvedev/function: date.sql @@ -0,0 +1,109 @@ +--task 1 +--Задача 1. Давность регистрации пользователей +SELECT name, created_at::date AS created_date, CURRENT_DATE - created_at::date AS date_since_registration +FROM users u +WHERE CURRENT_DATE - created_at::date > 180 +ORDER BY date_since_registration DESC + +/*Как бы ты использовал такое поле в реальном продукте (что можно делать с очень «старыми» пользователями)? +В зависимости от их активности можно предложить те или иные плюшки. + +Почему удобнее сразу вычитать created_at::date, а не created_at как timestamp? +Потому что CURRENT_DATE - это просто текущая дата без учета времени +*/ + +--task 2 +--Задача 2. Нормализация email и доменов +SELECT + name, + LOWER(email) AS email_clean, + substring(LOWER(email) FROM POSITION('@' IN email) +1) AS domain +FROM users +WHERE email IS NOT NULL +ORDER BY domain, name +/* +Зачем в реальных системах приводят email к нижнему регистру и выделяют домен отдельно? +Возможно потому что люди могут писать с разным регистром, и если бы это учитывалось, то одинаковые почтовые ящики с разными регистрами были бы совершенно разными. +А системе не нужны дубликаты с разными регистрами. + +Что будет, если по ошибке не отфильтровать email IS NOT NULL (что получишь в domain)? +Получу NULL,человек не заполнил ячейку имэйла при регистрации. +*/ + +--task 3 +--Задача 3. Сегменты по возрасту +SELECT + name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 and 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + END AS age_group +FROM users +ORDER BY age + +/* +Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? +Возможно, Так проще работать с данными + +Что произойдёт, если в CASE забыть ветку ELSE (что получат пользователи, не попавшие ни в одно условие)? +У них появятся значения NULL +*/ + +--task 4 +--Задача 4. «Горячие» пользователи по дате и активности +SELECT + name, + created_at::date AS created_date, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN (is_active IS TRUE OR CURRENT_DATE - created_at::date <= 30) THEN 'hot' + ELSE 'regular' + END AS hot_flag + +FROM users +WHERE (is_active IS TRUE OR CURRENT_DATE - created_at::date <= 30) +/*Чем отличается фильтрация по дате в WHERE от простого CASE с меткой 'hot', но без фильтрации? +Последовательностью выполнения, различными функциями (в одном фильтрация идет, в другом просто присваивание определенному условию наименования в дополнительном столбце) + +Какие типы рассылок/кампаний логично запускать только на «горячих»? + +*/ + +--task 5 +--Задача 5. Качество данных по email +SELECT + COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) - COUNT(email) AS without_email, + (COUNT(*) - COUNT(email)) * 100.0 / COUNT(*) AS dolya +FROM users + +/*Почему для проверки «долей» и «заполненности» полей лучше использовать агрегаты в SQL, а не просто «на глаз» смотреть на таблицу? +То ли я туп, то ли вопрос с подковыркой для меня. Если значений может быть миллион, то как еще это делать, если не через агрегаты? + +Какие действия по данным ты бы предложил продукту, если доля пользователей без email слишком большая? +Как минимум при регистрации сделать поле "имэйл" обязательным для заполнения. +*/ + +--task 6 +--Задача 6. «Стаж» пользователя и его группа +SELECT + name, + CURRENT_DATE - created_at::date AS says_since_registration, + CASE + WHEN CURRENT_DATE - created_at::date <= 30 THEN 'new' + WHEN CURRENT_DATE - created_at::date > 180 THEN 'old' + ELSE 'middle' + END AS tenure_group, + is_active +FROM users +ORDER BY tenure_group, says_since_registration + +/* +Чем сегментация по «стажу» отличается по смыслу от сегментации по возрасту? + +Если пересчитывать tenure_group прямо в SQL каждый день, какие плюсы и минусы по сравнению с хранением статического поля segment? +Можно будет понимать в динамике сколько старичков и сколько молодых, можно будет видеть приросты пользователей за каждый день. +*/ diff --git a/dmedvedev/joins.sql b/dmedvedev/joins.sql new file mode 100644 index 0000000..dfeb3bc --- /dev/null +++ b/dmedvedev/joins.sql @@ -0,0 +1,210 @@ +-- task 1 +--✅ Задача: покажи все заказы и информацию о пользователе, который их сделал. + +SELECT + o.id AS order_id, + o.order_date AS order_date, + o.status AS status, + u.name AS user_name, + u.country AS country +FROM orders o +INNER JOIN users u ON u.id = o.user_id; + +SELECT + o.id AS order_id, + o.order_date AS order_date, + o.status AS status, + u.name AS user_name, + u.country AS country +FROM orders o +LEFT JOIN users u ON u.id = o.user_id; + + +/* - С какой таблицы логичнее начать: `orders` или `users`? Почему? +Логичнее начать с orders потому что мы сразу отсекаем пользователей, которые не делали заказ. + +Написал в обоих, INNER JOIN покажет только те заказы, у которых есть пользователи. А LEFT JOIN покажет все заказы, даже там где нет пользователя. +- Какой тип JOIN здесь уместнее: `INNER` или `LEFT? Напиши запрос в обоих вариантах и посмотри, где пропадут строки. + */ + + +-- task 2 +--✅ Задача: покажи состав заказа 101 (что в нём купили). + +SELECT + o.id AS order_id, + p.name AS product_name, + oi.quantity AS quantity, + oi.price_at_purchase AS price_at_purchase +FROM order_items oi +INNER JOIN orders o ON o.id = oi.order_id +INNER JOIN products p ON p.id = oi.product_id +WHERE order_id = 101; + +/* +Что изменится, если эту фильтрацию order_id = 101 перенести в JOIN (после ON)? + +как я понял, в таком случае , заказ 101 привязывается ко всем товарам потому +что таблицы не соединены между собой +*/ + +--task 3 +--✅ Задача: выведи все заказы и товары внутри заказов (как “чек по магазину”). + +SELECT + o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity AS quantity, + oi.price_at_purchase AS price_at_purchase +FROM orders o +INNER JOIN order_items oi ON o.id = oi.order_id +INNER JOIN products p ON oi.product_id = p.id +INNER JOIN users u ON u.id = o.user_id; + +/***❓ Ответь на вопросы:** + +- почему после JOIN заказ `3` появляется в нескольких строках; +потому что внутри заказа 3 несколько позиций товаров + +- чем результат отличается от задания 2 (по количеству строк и информации). +во второй задаче нужно было вывести информацию только по 101 заказу, здесь по всем. +и плюс указали кто купил и какое количество товаров +*/ + +--task 4 +--✅ Задача: вывести список всех пользователей и их заказы. +SELECT + u.name AS user_name, + o.id AS order_id, + o.status AS status +FROM users u +LEFT JOIN orders o ON o.user_id = u.id; + +/* +**✅ После выполнения:** +Найди глазами пользователя без заказов (у него **`order_id`** будет **`NULL`**); +Таких имен найдено 3 штуки + +Чем этот запрос отличается от такого же, но с **`INNER JOIN`**? +Иннер джоин в таком случае бы просто удалил из выдачи данные с NULL, +то есть показал бы только пользователей с заказами, а остальных бы скрыл +*/ + +--task 5 +--✅ Задача: найти пользователей, у которых нет ни одного заказа. + +SELECT + u.id AS user_id, + u.name AS user_name +FROM users u +LEFT JOIN orders o ON o.user_id = u.id +WHERE o.user_id IS NULL; + +/* **❓ Ответь на вопросы:** +Почему тут важно фильтровать именно по колонке из **`orders`**, а не из **`users`**? + +Фильтровать нужно по orders потому что в принципе в системе есть люди, +которые имеют и не имеют заказы, соответственно при выдаче запроса мы увидим , +что некоторые пользователи не имеют заказов, поэтому и фильтруем по заказам, +чтобы получить имена +*/ + +--task 6 +--✅ Задача: найти все заказы, где встречается товар Kindle. + +SELECT + oi.order_id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity AS quantity +FROM order_items oi +JOIN orders o ON o.id = oi.order_id +JOIN users u ON o.user_id = u.id +JOIN products p ON p.id = oi.product_id +WHERE p.name = 'Kindle'; + +/*Ответь на вопросы:** +- Почему заказ `2` встречается в выдаче один раз? + +Потому что внутри 2го заказа только 1 товар +*/ + +--task 7 +--✅ Задача: вывести все товары и информацию о том, в каких заказах они +--встречаются (если товар не покупали — тоже показать). + +SELECT + p.name AS product_name, + oi.order_id AS order_id, + oi.quantity AS quantity +FROM products p +LEFT JOIN order_items oi ON p.id = oi.product_id +ORDER BY p.name; + +/*❓ Ответь на вопросы: +что будет в order_id, если товар ни разу не покупали? +- в order_id будет NULL если товар ни разу не покупали + +чем это отличается от варианта, если использовать INNER JOIN? +если использовать иннер, то в таком случае мы могли бы потерять из выборки +те заказы, которые не покупали */ + +--task 8 +--Задание 8: Только оплаченные заказы + их состав + +SELECT + oi.order_id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity AS quantity, + oi.price_at_purchase AS price_at_purchase +FROM order_items oi +JOIN orders o ON o.id = oi.order_id +JOIN products p ON p.id = oi.product_id +JOIN users u ON u.id = o.user_id +WHERE o.status = 'paid'; + +/**❓ Ответь на вопросы:** +- что будет, если условие **`status = 'paid'`** написать в **`JOIN`** с **`orders`** (там же, где **`ON`**)? Изменится ли результат сейчас и почему? + +*/ + +--task 9 +--Задание 9: Заказы пользователя Alice (все товары внутри) + +SELECT + oi.order_id AS order_id, + o.order_date AS order_date, + p.name AS product_name, + oi.quantity AS quantity, + oi.price_at_purchase AS price_at_purchase +FROM order_items oi +JOIN orders o ON o.id = oi.order_id +JOIN products p ON p.id = oi.product_id +JOIN users u ON u.id = o.user_id +WHERE u.name = 'Alice'; + +/**❓ Ответь на вопросы:** +- если у Alice нет ни одного заказа, сколько строк вернёт твой запрос и почему? +В таком случае вернет 0 строк.*/ + +--task 10 +--Задание 10: Заказы без товаров (проверка целостности) + +SELECT + oi.order_id AS order_id, + o.user_id AS user_id, + o.status AS status, + oi.product_id AS product +FROM orders o +LEFT JOIN order_items oi ON o.id = oi.order_id +LEFT JOIN products p ON p.id = oi.product_id +WHERE oi.product_id IS NULL; + +/* +**❓ Ответь на вопросы:** +- Что будет, если вместо **`LEFT JOIN`** написать **`INNER JOIN`** — изменится ли результат и как. + +В таком случае, пропаду пустые строки +*/ \ No newline at end of file diff --git a/dmedvedev/python/1.variables.ipynb b/dmedvedev/python/1.variables.ipynb new file mode 100644 index 0000000..4fbc64f --- /dev/null +++ b/dmedvedev/python/1.variables.ipynb @@ -0,0 +1,275 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d44be848", + "metadata": {}, + "source": [ + "\n", + "- **Задача 1. Анкета ученика**\n", + " \n", + " Сделай программу‑анкету.\n", + " \n", + " Требования:\n", + " \n", + " 1. Спросить имя ученика.\n", + " 2. Спросить город.\n", + " 3. Спросить школу (номер или название).\n", + " 4. Вывести три строки:\n", + " \n", + " ```python\n", + " Тебя зовут <имя>\n", + " Ты живешь в городе <город>\n", + " Ты учишься в школе <школа>\n", + " ```\n", + " \n", + " - Подсказка:\n", + " \n", + " Нужен трижды `input` и трижды использование переменных в `print`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0d1a8013", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Тебя зовут дима\n", + "Ты живешь в городе уфа\n", + "Ты из 60 школы\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "city = input('Из какого ты города?')\n", + "school = input('В какой школе ты учишься или учился?')\n", + "\n", + "print(f'Тебя зовут {name}')\n", + "print(f'Ты живешь в городе {city}')\n", + "print(f'Ты из {school} школы');" + ] + }, + { + "cell_type": "markdown", + "id": "56c0a52f", + "metadata": {}, + "source": [ + "- **Задача 2. Визитка**\n", + " \n", + " Сделай программу «визитка».\n", + " \n", + " Требования:\n", + " \n", + " 1. Спросить имя.\n", + " 2. Спросить фамилию.\n", + " 3. Спросить любимый предмет.\n", + " 4. Вывести одну «визитку» блоком:\n", + " \n", + " ```python\n", + " Твоя визитка:\n", + " Имя: <имя>\n", + " Фамилия: <фамилия>\n", + " Любимый предмет: <предмет>\n", + " ```\n", + " \n", + " - Подсказка:\n", + " \n", + " Используем несколько `print`, где часть текста — в кавычках, часть — переменные.\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4d883f02", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Твоя визитка:\n", + "Имя: 1\n", + "Фамилия: 2\n", + "Любимый предмет: 3\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "surname = input('Какая у тебя фамилия?')\n", + "favorite_subject = input('Какой твой любимый предмет?')\n", + "\n", + "print('Твоя визитка:')\n", + "print(f'Имя: {name}')\n", + "print(f'Фамилия: {surname}')\n", + "print(f'Любимый предмет: {favorite_subject}')" + ] + }, + { + "cell_type": "markdown", + "id": "52df089f", + "metadata": {}, + "source": [ + "- **Задача 3. Повторное использование имени**\n", + " \n", + " Сделай программу, которая несколько раз обращается по имени.\n", + " \n", + " Требования:\n", + " \n", + " 1. Спросить имя.\n", + " 2. Вывести три строки:\n", + " \n", + " ```python\n", + " Привет, <имя>\n", + " Как дела, <имя>?\n", + " Пока, <имя>\n", + " ```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c69260b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Привет Дима.\n", + "Как дела, Дима?\n", + "Пока, Дима.\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "print(f'Привет {name}.')\n", + "print(f'Как дела, {name}?')\n", + "print(f'Пока, {name}.')\n" + ] + }, + { + "cell_type": "markdown", + "id": "c507de55", + "metadata": {}, + "source": [ + "- **Задача 4. Мини‑диалог**\n", + " \n", + " Нужно сделать диалог, где часть информации запоминается.\n", + " \n", + " Требования:\n", + " \n", + " 1. Спросить имя.\n", + " 2. Вывести: `Привет, <имя>`.\n", + " 3. Спросить: `Что ты любишь делать?`.\n", + " 4. Вывести две строки:\n", + " \n", + " ```python\n", + " <имя>, здорово, что ты любишь <занятие>\n", + " Удачи тебе, <имя>!\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "cfe2e0ce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Привет, Дима.\n", + "Дима, здорово, что ты люибшь плавать.\n", + "Удачи тебе, Дима!\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "print(f'Привет, {name}.')\n", + "favorite_do = input('Что ты любишь делать?')\n", + "print(f'{name}, здорово, что ты люибшь {favorite_do}.')\n", + "print(f'Удачи тебе, {name}!')\n" + ] + }, + { + "cell_type": "markdown", + "id": "1cb76c4f", + "metadata": {}, + "source": [ + "- **Задача 5. План на день**\n", + " \n", + " Сделай программу, которая помогает составить план.\n", + " \n", + " Требования:\n", + " \n", + " 1. Спросить, что пользователь сделает утром.\n", + " 2. Спросить, что сделает днём.\n", + " 3. Спросить, что сделает вечером.\n", + " 4. Вывести блок:\n", + " \n", + " ```python\n", + " Твой план на день:\n", + " Утром: <утро>\n", + " Днём: <день>\n", + " Вечером: <вечер>\n", + " ```" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "5319e498", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Твой план на день:\n", + "Утром: готовлюсь к работе\n", + "Днем: сижу в офисе\n", + "Вечером: курю кальян\n" + ] + } + ], + "source": [ + "will_do_morning = input('Что ты делаешь утром?')\n", + "will_do_day = input('Что ты делаешь днем?')\n", + "will_do_evening = input('Что ты делаешь вечером?')\n", + "\n", + "print(f'Твой план на день:\\nУтром: {will_do_morning}\\nДнем: {will_do_day}\\nВечером: {will_do_evening}');" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/10.structure_of_data_dict.ipynb b/dmedvedev/python/10.structure_of_data_dict.ipynb new file mode 100644 index 0000000..a5ee4da --- /dev/null +++ b/dmedvedev/python/10.structure_of_data_dict.ipynb @@ -0,0 +1,341 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1cc2906", + "metadata": {}, + "source": [ + "- Задача 1. Описание файла\n", + " \n", + " Создай словарь `file_info`, в котором будут:\n", + " \n", + " - имя файла;\n", + " - формат;\n", + " - размер.\n", + " \n", + " Выведи:\n", + " \n", + " - весь словарь;\n", + " - отдельно имя файла;\n", + " - отдельно формат." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "659422a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name_file': '123.py', 'format': 'py', 'size': '128kB'}\n", + "123.py\n", + "py\n" + ] + } + ], + "source": [ + "file_info = {'name_file':'123.py', 'format':'py', 'size':'128kB'}\n", + "print(file_info)\n", + "print(file_info['name_file'])\n", + "print(file_info['format'])" + ] + }, + { + "cell_type": "markdown", + "id": "fbabd675", + "metadata": {}, + "source": [ + "- Задача 2. Обновление данных о таблице\n", + " \n", + " Создай словарь с данными о таблице:\n", + " \n", + " - `name`\n", + " - `rows`\n", + " - `source`\n", + " \n", + " Потом:\n", + " \n", + " - измени количество строк;\n", + " - выведи словарь до изменения;\n", + " - выведи словарь после изменения." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c5f7bc4f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name': '123', 'rows': 122, 'source': '123.csv'}\n", + "{'name': '123', 'rows': 123, 'source': '123.csv'}\n" + ] + } + ], + "source": [ + "dict = {'name':'123', 'rows':122, 'source':'123.csv'}\n", + "print(dict)\n", + "dict['rows'] = 123\n", + "print(dict)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "ccdc112d", + "metadata": {}, + "source": [ + "- Задача 3. Добавь новые метаданные\n", + " \n", + " Создай словарь с двумя ключами:\n", + " \n", + " - `table_name`\n", + " - `rows_count`\n", + " \n", + " Потом добавь:\n", + " \n", + " - `file_format`\n", + " - `load_date`\n", + " \n", + " Выведи итоговый словарь." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a6ca1f4f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'table_name': '123', 'rows_count': 24, 'file_format': 'csv', 'load_date': '12-12-2024'}\n" + ] + } + ], + "source": [ + "dict = {'table_name':'123', 'rows_count':24}\n", + "dict['file_format'] = 'csv'\n", + "dict['load_date'] = '12-12-2024'\n", + "print(dict)" + ] + }, + { + "cell_type": "markdown", + "id": "8cf5540f", + "metadata": {}, + "source": [ + "- Задача 4. Удали лишние поля\n", + " \n", + " Создай словарь с 4 ключами, например с метаданными файла или таблицы.\n", + " \n", + " Потом:\n", + " \n", + " - удали один ключ через `del`;\n", + " - удали другой ключ через `pop()`;\n", + " - выведи словарь после каждого удаления." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "11d837f9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'rows_count': 24, 'file_format': 'csv', 'load_date': '12-12-2024'}\n", + "{'file_format': 'csv', 'load_date': '12-12-2024'}\n" + ] + } + ], + "source": [ + "dict = {'table_name':'123', 'rows_count':24, 'file_format':'csv', 'load_date':'12-12-2024'}\n", + "del dict['table_name']\n", + "print(dict)\n", + "dict.pop('rows_count')\n", + "print(dict)" + ] + }, + { + "cell_type": "markdown", + "id": "11a31707", + "metadata": {}, + "source": [ + "- Задача 5. Проверь ключ в схеме\n", + " \n", + " Создай словарь с описанием записи, например:\n", + " \n", + " - `id`\n", + " - `name`\n", + " - `city`\n", + " \n", + " Проверь:\n", + " \n", + " - есть ли ключ `\"name\"`;\n", + " - есть ли ключ `\"email\"`.\n", + " \n", + " Выведи результат каждой проверки." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "af86882d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "False\n" + ] + } + ], + "source": [ + "dict = {'id':123, 'name':'dimasta', 'city':'ufa'}\n", + "print('name' in dict)\n", + "print('email' in dict)" + ] + }, + { + "cell_type": "markdown", + "id": "68232ebc", + "metadata": {}, + "source": [ + "- Задача 6. Перебор словаря с метаданными\n", + " \n", + " Создай словарь с данными о датасете:\n", + " \n", + " - имя;\n", + " - формат;\n", + " - количество строк.\n", + " \n", + " С помощью цикла:\n", + " \n", + " - выведи все ключи;\n", + " - выведи все значения;\n", + " - выведи пары ключ-значение." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "9f3a514a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "name\n", + "format\n", + "rows_count\n", + "dimasta\n", + "csv\n", + "123\n", + "name dimasta\n", + "format csv\n", + "rows_count 123\n" + ] + } + ], + "source": [ + "dict = {'name':'dimasta', 'format':'csv', 'rows_count':123}\n", + "for key in dict:\n", + " print(key)\n", + "for value in dict.values():\n", + " print(value)\n", + "for key,value in dict.items():\n", + " print(key, value)" + ] + }, + { + "cell_type": "markdown", + "id": "53b175f5", + "metadata": {}, + "source": [ + "- Задача 7. Мини-метаданные из ввода\n", + " \n", + " Сделай программу, которая спрашивает у пользователя:\n", + " \n", + " - имя файла;\n", + " - формат файла;\n", + " - количество строк;\n", + " - источник данных.\n", + " \n", + " Сохрани всё в словарь.\n", + " \n", + " После этого выведи:\n", + " \n", + " - весь словарь;\n", + " - отдельно имя файла;\n", + " - отдельно источник;\n", + " - все пары ключ-значение через цикл." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "7494cee0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name': '123', 'format': '123', 'rows_count': 123, 'source': '123'}\n", + "123\n", + "123\n", + "name 123\n", + "format 123\n", + "rows_count 123\n", + "source 123\n" + ] + } + ], + "source": [ + "dict['name'] = input('vvedite nazvanie faila')\n", + "dict['format'] = input('vvedite nazvanie formata')\n", + "dict['rows_count'] = int(input('vvedite koli4estvo strok'))\n", + "dict['source'] = input('vvedite isto4nik data')\n", + "print(dict)\n", + "print(dict['name'])\n", + "print(dict['source'])\n", + "for key, value in dict.items():\n", + " print(key, value)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/11.tuples.py b/dmedvedev/python/11.tuples.py new file mode 100644 index 0000000..16b095b --- /dev/null +++ b/dmedvedev/python/11.tuples.py @@ -0,0 +1,111 @@ +""" +Задача 1. Координаты файла в партиции +Создай кортеж из 3 значений: + год; + месяц; + день. +Выведи: + весь кортеж; + первый элемент; + последний элемент; + длину кортежа. +""" + +a = (2024, 12, 1) +print(a) +print(a[0]) +print(a[-1]) +print(len(a)) + +""" +Задача 2. Описание партиции +Создай кортеж с данными партиции, например: ("2025", "03", "orders") +Выведи: + сам кортеж; + отдельно первый элемент; + отдельно второй элемент; + отдельно третий элемент. +Требования: + получить значения по индексам. + """ + +a = ("2025", "03", "orders") +print(a) +print(a[0]) +print(a[1]) +print(a[2]) + +""" +Задача 3. Уникальные города пользователей +Создай список городов, в котором есть повторы. +Преобразуй его в множество. +Выведи: + исходный список; + множество; + количество элементов в списке; + количество элементов в множестве. +Смысл задачи: + показать, что множество убирает повторы. +""" +city = ['ufa', 'moscow', 'spb', 'moscow', 'saratov', 'moscow'] +unique_city = set(city) +print(city) +print(unique_city) +print(len(city)) +print(len(unique_city)) + + +"""Задача 4. Уникальные форматы файлов +Создай множество с форматами файлов, например: + csv + json + parquet + csv +Потом: + добавь новый формат; + удали один формат; + выведи множество после каждого шага. +""" + +format = {'csv', 'json', 'parquet', 'csv'} +print(format) #{'parquet', 'csv', 'json'} +format.add('csv') #add format csv +format.add('exe') #add format exe +print(format) #{'parquet', 'csv', 'json', 'exe'} +format.remove('json') +print(format) #{'exe', 'parquet', 'csv'} + +""" +Задача 5. Проверка уникальных источников +Создай список источников данных, в котором некоторые источники повторяются. +С помощью множества: + получи только уникальные источники; + проверь, есть ли в множестве "api"; + выведи результат проверки. +""" + +source = ['csv', 'csv', 'json', 'json', 'xls'] +uniq_source = set(source) +print('api' in uniq_source) + +""" +Задача 6. Мини-анализ уникальных значений +Спроси у пользователя 5 раз название города, источника или формата файла. +Сохраняй ответы в список. +После этого: + выведи весь список; + создай из него множество; + выведи множество уникальных значений; + выведи количество всех ответов; + выведи количество уникальных значений. +""" +i = 1 +cities = [] +for i in range(1, 6): + a = input(f'vvedite nazvanie goroda #{i}') + cities.append(a) +print(cities) +uniq_cities = set(cities) +print(uniq_cities) +print(f'koli4estvo vseh otvetov {len(cities)}') +print(f'koli4estvo uniq zna4enii {len(uniq_cities)}') \ No newline at end of file diff --git a/dmedvedev/python/12.mutable_and_immutable_types.ipynb b/dmedvedev/python/12.mutable_and_immutable_types.ipynb new file mode 100644 index 0000000..2cbf860 --- /dev/null +++ b/dmedvedev/python/12.mutable_and_immutable_types.ipynb @@ -0,0 +1,311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ad33a980", + "metadata": {}, + "source": [ + "Задача 1. Две переменные — один список\n", + "Есть список колонок:\n", + "columns= [\"id\",\"event_time\",\"amount\"]\n", + "Нужно:\n", + " создать переменную columns_for_export и присвоить ей columns;\n", + " добавить в columns_for_export колонку \"load_dt\";\n", + " вывести:\n", + " columns\n", + " columns_for_export" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f5d2c49a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['id', 'event_time', 'amount', 'load_dt']\n", + "['id', 'event_time', 'amount', 'load_dt']\n" + ] + } + ], + "source": [ + "columns = [\"id\",\"event_time\",\"amount\"]\n", + "columns_for_expert = columns\n", + "columns_for_expert.append(\"load_dt\")\n", + "print(columns)\n", + "print(columns_for_expert)" + ] + }, + { + "cell_type": "markdown", + "id": "e3cc1bdb", + "metadata": {}, + "source": [ + "почему изменились обе переменные: обе переменные изменились потому что мы используем изменяемую переменную списка - list, данная переменная изменяемая и columns_for_expert ссылается на columns, сами переменные это не переменные, а указатели на них в памяти, поэтому если изменить одну, то и вторая поменяется\n", + "\n", + "какой тип здесь используется: используется список list\n", + "\n", + "он изменяемый или неизменяемый: изменяемый" + ] + }, + { + "cell_type": "markdown", + "id": "1422c7e6", + "metadata": {}, + "source": [ + "Задача 2. Строка и список — разное поведение\n", + "Есть переменные:\n", + " table_name=\"orders\"\n", + " metrics= [\"rows\",\"errors\",\"duration\"]\n", + "Нужно:\n", + " создать table_name_copy = table_name;\n", + " создать metrics_copy = metrics;\n", + " к table_name добавить строку \"_raw\";\n", + " в metrics_copy добавить элемент \"latency\";\n", + "вывести все 4 переменные:\n", + " table_name\n", + " table_name_copy\n", + " metrics\n", + " metrics_copy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7acdbe0d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "orders_raws\n", + "orders\n", + "['rows', 'errors', 'duration', 'latency']\n", + "['rows', 'errors', 'duration', 'latency']\n" + ] + } + ], + "source": [ + "table_name = \"orders\"\n", + "metrics = [\"rows\",\"errors\",\"duration\"]\n", + "table_name_copy = table_name\n", + "metrics_copy = metrics\n", + "table_name = (\"orders\" + \"_raws\")\n", + "metrics_copy.append(\"latency\")\n", + "print(table_name)\n", + "print(table_name_copy)\n", + "print(metrics)\n", + "print(metrics_copy)" + ] + }, + { + "cell_type": "markdown", + "id": "ade754e4", + "metadata": {}, + "source": [ + "почему строка изменилась не так, как список: потому что строка в данном случае это переменная и это неизменяемый тип данных, каждая переменная хранится в одной ячейке памяти и получается, что у нас две переменные и каждая хранится по отдельности, в отличие от списка, который в данном случае хранится как указатель на ячейку в памяти и изменение одного списка порождает изменение и в другом, так как это одна и та же ячейка в памяти\n", + "\n", + "какой объект оказался неизменяемым: строка\n", + "\n", + "какой объект оказался изменяемым: список" + ] + }, + { + "cell_type": "markdown", + "id": "07e7c0e3", + "metadata": {}, + "source": [ + "Задача 3. Словарь конфигурации пайплайна\n", + "Есть конфиг:\n", + "pipeline_config= {\n", + "\"source\":\"postgres\",\n", + "\"target\":\"s3\",\n", + "\"batch_size\":1000\n", + "}\n", + "Нужно:\n", + " создать current_config = pipeline_config;\n", + " изменить в current_config значение batch_size на 5000;\n", + " добавить ключ \"mode\" со значением \"append\";\n", + " вывести:\n", + " pipeline_config\n", + " current_config" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9b4fb54b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'source': 'postgres', 'target': 's3', 'batch_size': '5000', 'mode': 'append'}\n", + "{'source': 'postgres', 'target': 's3', 'batch_size': '5000', 'mode': 'append'}\n" + ] + } + ], + "source": [ + "pipeline_config = {\n", + " \"source\":\"postgres\",\n", + " \"target\":\"s3\",\n", + " \"batch_size\":1000\n", + "}\n", + "current_config = pipeline_config\n", + "current_config[\"batch_size\"] = \"5000\"\n", + "current_config[\"mode\"] = \"append\"\n", + "print(pipeline_config)\n", + "print(current_config)" + ] + }, + { + "cell_type": "markdown", + "id": "d41c940e", + "metadata": {}, + "source": [ + "почему исходный словарь тоже изменился: потому что словарь это изменяемый тип данных и когда мы его записываем в переменную, то он лишь хранится в виде указателя на ячейку памяти, соответственно, когда мы присваиваем другой переменной этот же словарь, то он ссылкается на ту же ячейку в памяти и любое изменение того или иного словаря влечет изменение и другого словаря\n", + "\n", + "является ли словарь изменяемым типом: да, является\n", + "\n", + "чем это может быть опасно в ETL-задачах: тем что если мы например захотим как-то проанализировать данные и например добавим какую-то колонку, то соответственно эта колонка появится в обоих словарях, что может привести к ошибкам или искажениям данных, поэтому необходимо при такой работе создавать копию словаря, чтобы оставить исходный словарь без изменений" + ] + }, + { + "cell_type": "markdown", + "id": "7af4f14d", + "metadata": {}, + "source": [ + "Задача 4. Функция меняет исходные данные\n", + "Есть список статусов:\n", + "statuses= [\"new\",\"validated\",\"loaded\"]\n", + "\n", + "Нужно:\n", + " вывести список до изменений;\n", + " заменить первый элемент на \"processed\";\n", + " добавить в конец \"archived\";\n", + " вывести список после изменений.\n", + "Требования:\n", + " использовать обращение по индексу;\n", + " использовать append();\n", + " вывести список до и после изменений." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5344d3d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['new', 'validated', 'loaded']\n", + "['processed', 'validated', 'loaded', 'archived']\n" + ] + } + ], + "source": [ + "statuses = [\"new\",\"validated\",\"loaded\"]\n", + "print(statuses)\n", + "statuses[0] = \"processed\"\n", + "statuses.append(\"archived\")\n", + "print(statuses)" + ] + }, + { + "cell_type": "markdown", + "id": "a276419e", + "metadata": {}, + "source": [ + "почему список изменился: список это изменяемый тип данных и его можно менять. когда мы изменили значение и добавили новое , то список изменяется внутри ячейки памяти\n", + "\n", + "какой элемент был заменён: самый первый\n", + "\n", + "какой элемент был добавлен: самый последний - \"archived\"" + ] + }, + { + "cell_type": "markdown", + "id": "5ea9f452", + "metadata": {}, + "source": [ + "Задача 5. Подготовка списка колонок без порчи исходника\n", + "Есть список:\n", + " raw_columns= [\"id\",\"user_id\",\"price\"]\n", + "Нужно подготовить список для витрины:\n", + " в новый список добавить \"load_dt\";\n", + " в новый список добавить \"source_name\".\n", + "Но при этом исходный raw_columns менять нельзя.\n", + "Что нужно сделать:\n", + " создать новый список mart_columns;\n", + " добавить в него новые поля;\n", + " вывести:\n", + " raw_columns\n", + " mart_columns" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3e4dbd80", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['id', 'user_id', 'price']\n", + "['id', 'user_id', 'price', 'load_dt', 'source_name']\n" + ] + } + ], + "source": [ + "raw_columns = [\"id\",\"user_id\",\"price\"]\n", + "mart_columns = raw_columns.copy()\n", + "mart_columns.append(\"load_dt\")\n", + "mart_columns.append(\"source_name\")\n", + "print(raw_columns)\n", + "print(mart_columns)" + ] + }, + { + "cell_type": "markdown", + "id": "bd0f9b9c", + "metadata": {}, + "source": [ + "почему нельзя просто написать mart_columns = raw_columns: потому что в таком случае оба списка будут ссылаться на одну ячейку памяти и изменение одно из списков в таком случае влечет изменение второго\n", + "\n", + "что произойдёт, если потом изменить mart_columns: если мы напишем mart_columns = raw_columns, то оба списка изменятся, это нам не нужно\n", + "\n", + "почему в DE важно не портить исходные структуры: потому что это может вызвать ошибку или исказить анализ данных" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/13.error_and_exception_handling.ipynb b/dmedvedev/python/13.error_and_exception_handling.ipynb new file mode 100644 index 0000000..e144a29 --- /dev/null +++ b/dmedvedev/python/13.error_and_exception_handling.ipynb @@ -0,0 +1,231 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9b88d6f7", + "metadata": {}, + "source": [ + "Задача 1. Безопасное преобразование числа\n", + "Пользователь вводит число с клавиатуры.\n", + "Нужно:\n", + " попытаться преобразовать ввод в int;\n", + " если получилось — вывести:\n", + " само число;\n", + " его квадрат;\n", + " его удвоенное значение;\n", + " если не получилось — вывести сообщение об ошибке." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "66c30856", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "44\n", + "1936\n", + "88\n" + ] + } + ], + "source": [ + "a = input(\"vvedite 4islo s klaviaturi\")\n", + "try:\n", + " a = int(a)\n", + " print(a)\n", + " print(a ** 2)\n", + " print(a * 2)\n", + "except:\n", + " print(\"error\")" + ] + }, + { + "cell_type": "markdown", + "id": "7be390bd", + "metadata": {}, + "source": [ + "Задача 2. Деление чисел с разными ошибками\n", + "Напиши программу, которая:\n", + " просит пользователя ввести два числа;\n", + " первое число — делимое;\n", + " второе число — делитель;\n", + "пытается:\n", + " преобразовать оба значения в float;\n", + " выполнить деление;\n", + "обрабатывает ошибки:\n", + " если введено не число;\n", + " если попытались делить на ноль.\n", + "Если всё прошло успешно, вывести результат деления." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "259db03e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.0\n" + ] + } + ], + "source": [ + "a = input(\"vvedite pervoe 4islo - delimoe\")\n", + "b = input(\"vvedite vtoroe 4islo - delitel\")\n", + "try:\n", + " a = float(a)\n", + " b = float(b)\n", + " print(a / b)\n", + "except ValueError:\n", + " print(\"nevernii type of data\")\n", + "except ZeroDivisionError:\n", + " print(\"delenie na 0\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "654327c9", + "metadata": {}, + "source": [ + "Задача 3. Обработка списка строк с числами\n", + "Дан список строк:\n", + "values= [\"10\",\"20\",\"abc\",\"40\",\"\",\"15\"]\n", + "\n", + "Нужно пройти по списку и для каждого элемента:\n", + " попытаться преобразовать его в число;\n", + " если получилось — вывести:\n", + " исходное значение;\n", + " число;\n", + " квадрат числа;\n", + " если не получилось — вывести сообщение, что элемент не удалось преобразовать.\n", + "В конце дополнительно вывести:\n", + " сколько элементов успешно обработано;\n", + " сколько элементов вызвали ошибку." + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "15ba90bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "common text - 10\n", + "number - 10\n", + "sqr number - 100\n", + "common text - 20\n", + "number - 20\n", + "sqr number - 400\n", + "ne ydalos preobrazovat abc\n", + "common text - 40\n", + "number - 40\n", + "sqr number - 1600\n", + "ne ydalos preobrazovat \n", + "common text - 15\n", + "number - 15\n", + "sqr number - 225\n", + "uspeshno obrabotano 4\n", + "oshibok sdelano 2\n" + ] + } + ], + "source": [ + "values = [\"10\",\"20\",\"abc\",\"40\",\"\",\"15\"]\n", + "success = 0\n", + "errors = 0\n", + "for i in values:\n", + " try:\n", + " a = int(i)\n", + " print(f\"common text - {i}\")\n", + " print(f\"number - {a}\")\n", + " print(f\"sqr number - {a**2}\")\n", + " success += 1\n", + " except ValueError:\n", + " print(f\"ne ydalos preobrazovat {i}\")\n", + " errors += 1\n", + "print(f\"uspeshno obrabotano {success}\")\n", + "print(f\"oshibok sdelano {errors}\")" + ] + }, + { + "cell_type": "markdown", + "id": "51101218", + "metadata": {}, + "source": [ + "Задача 4. Использование else и finally\n", + "Попроси пользователя ввести целое число.\n", + "Программа должна:\n", + " в блоке try попытаться преобразовать ввод в int;\n", + " в except обработать ошибку ввода;\n", + " в else вывести:\n", + " число;\n", + " его куб;\n", + " сообщение, что операция выполнена успешно;\n", + "в finally вывести:\n", + " \"Программа завершила попытку обработки\"." + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "f2a891bd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "27\n", + "операция выполнена успешно\n", + "программа завершила попытку обработки\n" + ] + } + ], + "source": [ + "a = input(\"введите целое число\")\n", + "try:\n", + " a = int(a)\n", + "except ValueError:\n", + " print(\"ошибка ввода\")\n", + "else:\n", + " print(a)\n", + " print(a**3)\n", + " print(\"операция выполнена успешно\")\n", + "finally:\n", + " print(\"программа завершила попытку обработки\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/14.date_and_time.ipynb b/dmedvedev/python/14.date_and_time.ipynb new file mode 100644 index 0000000..5d6daa5 --- /dev/null +++ b/dmedvedev/python/14.date_and_time.ipynb @@ -0,0 +1,263 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "901c685d", + "metadata": {}, + "source": [ + "Задача 1. Текущая дата и время + разбор на части\n", + "Напиши программу, которая:\n", + " получает текущую дату и время;\n", + " выводит:\n", + " полную дату и время;\n", + " отдельно год;\n", + " отдельно месяц;\n", + " отдельно день;\n", + " отдельно часы;\n", + " отдельно минуты;\n", + " дополнительно выводит дату в формате:\n", + " YYYY-MM-DD\n", + " DD.MM.YYYY" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "a9776c66", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Полная текущая дата и время - 2026-10-09 11:53:36.376962\n", + "Год - 2026\n", + "Месяц - 10\n", + "День - 9\n", + "Часы - 11\n", + "Минуты - 53\n", + "Формат вывода YYYY-MM-DD - 2026-10-09\n", + "Формат вывода DD.MM.YYYY - 09.10.2026\n" + ] + } + ], + "source": [ + "from datetime import datetime\n", + "now = datetime.now()\n", + "print(f\"Полная текущая дата и время - {now}\")\n", + "print(f\"Год - {now.year}\")\n", + "print(f\"Месяц - {now.month}\")\n", + "print(f\"День - {now.day}\")\n", + "print(f\"Часы - {now.hour}\")\n", + "print(f\"Минуты - {now.minute}\")\n", + "print(f\"Формат вывода YYYY-MM-DD - {now.strftime(\"%Y-%m-%d\")}\")\n", + "print(f\"Формат вывода DD.MM.YYYY - {now.strftime(\"%d.%m.%Y\")}\")" + ] + }, + { + "cell_type": "markdown", + "id": "69b619b5", + "metadata": {}, + "source": [ + "Задача 2. Работа с конкретной датой\n", + "Создай вручную дату события:\n", + "2025-12-31 18:45:00\n", + "Нужно:\n", + " вывести эту дату;\n", + " вывести отдельно:\n", + " год;\n", + " месяц;\n", + " день;\n", + " часы;\n", + " минуты;\n", + " создать ещё одну дату:\n", + " 2026-01-05 09:00:00\n", + " посчитать разницу между этими двумя датами;\n", + " вывести:\n", + " полную разницу;\n", + " количество дней." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "af9e0816", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Дата - 2025-12-31\n", + "Год - 2025\n", + "Месяц - 12\n", + "День - 31\n", + "Часы - 18\n", + "Минуты - 45\n", + "Полная разница между датами '-5 days, 9:45:00'\n", + "Разница в количестве дней -5\n" + ] + } + ], + "source": [ + "from datetime import datetime, timedelta\n", + "\n", + "date_event_str = \"2025-12-31 18:45:00\"\n", + "date_event = datetime.strptime(date_event_str, \"%Y-%m-%d %H:%M:%S\")\n", + "print(f\"Дата - {date_event.strftime(\"%Y-%m-%d\")}\") \n", + "print(f\"Год - {date_event.year}\")\n", + "print(f\"Месяц - {date_event.month}\")\n", + "print(f\"День - {date_event.day}\")\n", + "print(f\"Часы - {date_event.hour}\")\n", + "print(f\"Минуты - {date_event.minute}\")\n", + "\n", + "another_date_event_str = \"2026-01-05 09:00:00\"\n", + "another_date_event = datetime.strptime(another_date_event_str, \"%Y-%m-%d %H:%M:%S\")\n", + "differences = date_event - another_date_event\n", + "print(f\"Полная разница между датами '{differences}'\")\n", + "print(f\"Разница в количестве дней {differences.days}\")" + ] + }, + { + "cell_type": "markdown", + "id": "470d379a", + "metadata": {}, + "source": [ + "Задача 3. Прибавление и вычитание времени\n", + "Напиши программу, которая:\n", + " получает текущую дату;\n", + " считает:\n", + " дату через 7 дней;\n", + " дату через 30 дней;\n", + " дату 14 дней назад;\n", + " выводит все результаты;" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "060745f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-10-09 14:11:42.487216\n", + "2026-10-16 14:11:42.487216\n", + "2026-11-08 14:11:42.487216\n", + "2026-09-25 14:11:42.487216\n" + ] + } + ], + "source": [ + "from datetime import datetime, timedelta\n", + "now = datetime.now()\n", + "in_7_days = now + timedelta(days=7)\n", + "in_30_days = now + timedelta(days=30)\n", + "before_14_days = now - timedelta(days=14)\n", + "print(now)\n", + "print(in_7_days)\n", + "print(in_30_days)\n", + "print(before_14_days)" + ] + }, + { + "cell_type": "markdown", + "id": "9c82c9de", + "metadata": {}, + "source": [ + "Задача 4. Преобразование строк в даты\n", + "Дан список строк с датами:\n", + "dates = [\"2026-03-30\",\"2026-03-25\",\"2026-02-15\",\"2026-01-01\"]\n", + "Нужно:\n", + " пройти по списку;\n", + " каждую строку превратить в дату;\n", + " для каждой даты вывести:\n", + " исходную строку;\n", + " полученную дату;\n", + " день;\n", + " месяц;\n", + " год;\n", + " посчитать, сколько дней прошло от этой даты до текущей даты." + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "25874736", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Исходная строка - 2026-03-30\n", + "Преобразовання строка в дату - 2026-03-30 00:00:00\n", + "День - 30\n", + "Месяц - 3\n", + "Год - 2026\n", + "На сегодняшний день прошло - 193 дня\n", + "\n", + "Исходная строка - 2026-03-25\n", + "Преобразовання строка в дату - 2026-03-25 00:00:00\n", + "День - 25\n", + "Месяц - 3\n", + "Год - 2026\n", + "На сегодняшний день прошло - 198 дня\n", + "\n", + "Исходная строка - 2026-02-15\n", + "Преобразовання строка в дату - 2026-02-15 00:00:00\n", + "День - 15\n", + "Месяц - 2\n", + "Год - 2026\n", + "На сегодняшний день прошло - 236 дня\n", + "\n", + "Исходная строка - 2026-01-01\n", + "Преобразовання строка в дату - 2026-01-01 00:00:00\n", + "День - 1\n", + "Месяц - 1\n", + "Год - 2026\n", + "На сегодняшний день прошло - 281 дня\n", + "\n" + ] + } + ], + "source": [ + "from datetime import datetime, timedelta\n", + "dates_str = [\"2026-03-30\",\"2026-03-25\",\"2026-02-15\",\"2026-01-01\"]\n", + "now = datetime.now()\n", + "for date in dates_str:\n", + " dates = datetime.strptime(date, \"%Y-%m-%d\")\n", + " print(f\"Исходная строка - {date}\")\n", + " print(f\"Преобразовання строка в дату - {dates}\")\n", + " print(f\"День - {dates.day}\")\n", + " print(f\"Месяц - {dates.month}\")\n", + " print(f\"Год - {dates.year}\")\n", + " differences = now - dates\n", + " print(f\"На сегодняшний день прошло - {differences.days} дня\\n\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/15.functions.ipynb b/dmedvedev/python/15.functions.ipynb new file mode 100644 index 0000000..a7bb261 --- /dev/null +++ b/dmedvedev/python/15.functions.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3759f336", + "metadata": {}, + "source": [ + "Задача 1. Сообщение о запуске ETL\n", + "Нужно написать функцию show_etl_start, которая принимает имя джобы и выводит сообщение о ее запуске.\n", + "Что нужно сделать:\n", + " создать функцию show_etl_start(job_name);\n", + " внутри вывести строку:Запуск ETL: <имя ETL>;\n", + " вызвать функцию для:\n", + " \"orders_load\"\n", + " \"users_load\"\n", + "\n", + "Ожидаемый вывод:\n", + " Запуск ETL: orders_load\n", + " Запуск ETL: users_load" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "ad2441e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Запуск ETL: orders_load\n", + "Запуск ETL: users_load\n" + ] + } + ], + "source": [ + "def show_etl_start(job_name):\n", + " print(f\"Запуск ETL: {job_name}\")\n", + "\n", + "show_etl_start(\"orders_load\")\n", + "show_etl_start(\"users_load\")" + ] + }, + { + "cell_type": "markdown", + "id": "7f37ba98", + "metadata": {}, + "source": [ + "Задача 2. Нормализация имени таблицы\n", + "Нужно написать функцию normalize_table_name, которая принимает имя таблицы и возвращает его:\n", + " без пробелов по краям;\n", + " в нижнем регистре.\n", + "Что нужно сделать:\n", + " создать функцию normalize_table_name(table_name);\n", + " использовать strip() и lower();\n", + " сохранить результат в переменную;\n", + " вывести результат для значений:\n", + " \" Orders \"\n", + " \" USERS \"\n", + " \"Payments\"\n", + "Пример вызова функции:\n", + " result = normalize_table_name(\" Orders \")\n", + " print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "e5777c5a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "orders\n", + "users\n", + "payments\n" + ] + } + ], + "source": [ + "def normalize_table_name(table_name):\n", + " return table_name.strip().lower()\n", + "\n", + "result_orders = normalize_table_name(\" Orders \")\n", + "result_users = normalize_table_name(\" USERS \")\n", + "result_payments = normalize_table_name(\"Payments\")\n", + "\n", + "print(result_orders)\n", + "print(result_users)\n", + "print(result_payments)" + ] + }, + { + "cell_type": "markdown", + "id": "efa9fae5", + "metadata": {}, + "source": [ + "Задача 3. Проверка корректной суммы\n", + "Нужно написать функцию is_valid_amount, которая принимает сумму и возвращает:\n", + " True, если сумма больше 0;\n", + " False, если сумма меньше или равна 0.\n", + "Что нужно сделать:\n", + " создать функцию is_valid_amount(amount);\n", + " проверить значения:\n", + " 100\n", + " 0\n", + " -25" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "09665391", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "False\n", + "False\n" + ] + } + ], + "source": [ + "def is_valid_amount(amount):\n", + " return amount > 0\n", + "\n", + "print(is_valid_amount(100))\n", + "print(is_valid_amount(0))\n", + "print(is_valid_amount(-25))" + ] + }, + { + "cell_type": "markdown", + "id": "d99423c2", + "metadata": {}, + "source": [ + "Задача 4. Построение имени витрины\n", + "Нужно написать функцию build_mart_name, которая принимает:\n", + " название сущности;\n", + " слой.\n", + "И возвращает строку в формате:\n", + " _\n", + "Что нужно сделать:\n", + " создать функцию build_mart_name(entity_name, layer);\n", + " проверить на примерах:\n", + " (\"orders\", \"dm\")\n", + " (\"users\", \"core\")\n", + " (\"payments\", \"stg\")" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "d3c6683e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "dm_orders\n", + "core_users\n", + "stg_payments\n" + ] + } + ], + "source": [ + "def build_mart_name(entity_name, layer):\n", + " return f\"{layer}_{entity_name}\"\n", + "\n", + "print(build_mart_name(\"orders\", \"dm\"))\n", + "print(build_mart_name(\"users\", \"core\"))\n", + "print(build_mart_name(\"payments\", \"stg\"))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/16.varuables_on_default.ipynb b/dmedvedev/python/16.varuables_on_default.ipynb new file mode 100644 index 0000000..6a8c698 --- /dev/null +++ b/dmedvedev/python/16.varuables_on_default.ipynb @@ -0,0 +1,190 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "669999d9", + "metadata": {}, + "source": [ + "Задача 1. Лог запуска процесса\n", + "Напиши функцию log_job_start, которая принимает:\n", + " job_name\n", + " level=\"INFO\"\n", + " env=\"dev\"\n", + "Функция должна выводить строку:\n", + " [INFO] Job load_users started in dev" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "2af8c0ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[INFO] Job load_users started in dev\n" + ] + } + ], + "source": [ + "def log_job_start(job_name, level = \"INFO\", env = \"dev\"):\n", + " print(f\"[{level}] Job {job_name} started in {env}\")\n", + "\n", + "log_job_start(\"load_users\")" + ] + }, + { + "cell_type": "markdown", + "id": "60ce459d", + "metadata": {}, + "source": [ + "Задача 2. Подключение к источнику\n", + "Напиши функцию connect_source, которая принимает:\n", + " source_name\n", + " host=\"localhost\"\n", + " port=5432\n", + " user=\"etl_user\"\n", + "Функция должна возвращать словарь с настройками подключения." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d73d7ab5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'source_name': 'dwh', 'host': 'localhost', 'port': 5432, 'user': 'etl_user'} \n" + ] + } + ], + "source": [ + "def connect_source(source_name, host = \"localhost\", port = 5432, user = \"etl_user\"):\n", + " connecting_configuration = {\n", + " \"source_name\" : source_name,\n", + " \"host\" : host,\n", + " \"port\" : port,\n", + " \"user\" : user\n", + " }\n", + " return connecting_configuration" + ] + }, + { + "cell_type": "markdown", + "id": "e667db79", + "metadata": {}, + "source": [ + "Задача 3. Построение SELECT-запроса\n", + "Напиши функцию build_select_query, которая принимает:\n", + " table_name\n", + " columns=None\n", + "Логика:\n", + " если columns=None, функция должна вернуть:\n", + " SELECT * FROM table_name\n", + " если список колонок передан, собрать их через запятую." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "4b710ebe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SELECT id, user_id, email FROM orders'" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def build_select_query(table_name, columns = None):\n", + " if columns is None:\n", + " return f\"SELECT * FROM {table_name}\"\n", + " else:\n", + " columns_str = \", \".join(columns)\n", + " return f\"SELECT {columns_str} FROM {table_name}\"\n", + "\n", + "build_select_query(\"orders\")\n", + "build_select_query(\"orders\", [\"id\", \"user_id\", \"email\"])\n" + ] + }, + { + "cell_type": "markdown", + "id": "ba059bd5", + "metadata": {}, + "source": [ + "Задача 4. Проверка обязательных полей\n", + "Напиши функцию validate_row, которая принимает:\n", + " row — словарь;\n", + " required_fields=None\n", + "Логика:\n", + " если required_fields=None, использовать [\"id\"];\n", + " проверить, есть ли все нужные поля;\n", + " если какого-то поля нет, вернуть строку:\n", + " Missing field: email\n", + " если всё хорошо, вернуть:\n", + " Row is valid" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "ceb7c30b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Row is valid'" + ] + }, + "execution_count": 94, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def validate_row(row, required_fields = None):\n", + " if required_fields is None:\n", + " required_fields = [\"id\"]\n", + " for field in required_fields:\n", + " if field not in row:\n", + " return f\"Missing field: {field}\"\n", + " return \"Row is valid\"\n", + "\n", + "validate_row({\"id\": 1, \"email\":2}, [\"id\", \"email\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/2.variable_types.ipynb b/dmedvedev/python/2.variable_types.ipynb new file mode 100644 index 0000000..8310312 --- /dev/null +++ b/dmedvedev/python/2.variable_types.ipynb @@ -0,0 +1,405 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7f86619f", + "metadata": {}, + "source": [ + "- Задача 1. Определи типы\n", + " \n", + " Создай 5 переменных разных типов:\n", + " \n", + " - строка;\n", + " - целое число;\n", + " - дробное число;\n", + " - логическое значение;\n", + " - `None`.\n", + " \n", + " Выведи значение каждой переменной и её тип с помощью `type()`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4e3b13ed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "abc \n", + "1 \n", + "1.0 \n", + "False \n", + "None \n" + ] + } + ], + "source": [ + "var_1 = 'abc'\n", + "var_2 = 1\n", + "var_3 = 1.0\n", + "var_4 = False\n", + "var_5 = None\n", + "\n", + "print(var_1, type(var_1))\n", + "print(var_2, type(var_2))\n", + "print(var_3, type(var_3))\n", + "print(var_4, type(var_4))\n", + "print(var_5, type(var_5))" + ] + }, + { + "cell_type": "markdown", + "id": "d480c9e9", + "metadata": {}, + "source": [ + "- Задача 2. Строки\n", + " \n", + " Создай две строковые переменные:\n", + " \n", + " - имя;\n", + " - фамилия.\n", + " \n", + " Выведи:\n", + " \n", + " 1. имя;\n", + " 2. фамилию;\n", + " 3. полное имя;\n", + " 4. длину полного имени через `len()`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7dc68e3b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dima\n", + "Medvedev\n", + "Dima Medvedev\n", + "13\n" + ] + } + ], + "source": [ + "name = 'Dima'\n", + "surname = 'Medvedev'\n", + "full_name = f'{name} {surname}'\n", + "print(name)\n", + "print(surname)\n", + "print(f'{name} {surname}')\n", + "print(len(f'{name} {surname}'))" + ] + }, + { + "cell_type": "markdown", + "id": "ad4ee370", + "metadata": {}, + "source": [ + "- Задача 3. Числа\n", + " \n", + " Создай две переменные с числами.\n", + " \n", + " Выведи результаты операций:\n", + " \n", + " - сложение;\n", + " - вычитание;\n", + " - умножение;\n", + " - деление.\n", + " \n", + " После этого выведи тип результата деления." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "fbc4fd93", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n", + "-1\n", + "2\n", + "0.5\n", + "\n" + ] + } + ], + "source": [ + "a = 1\n", + "b = 2\n", + "print(a + b)\n", + "print(a - b)\n", + "print(a * b)\n", + "print(a / b)\n", + "print(type(a / b))" + ] + }, + { + "cell_type": "markdown", + "id": "0d0e9f19", + "metadata": {}, + "source": [ + "- Задача 4. Скобки и порядок действий\n", + " \n", + " Создай две переменные с результатами выражений:\n", + " \n", + " - одно выражение без скобок;\n", + " - второе похожее выражение, но со скобками.\n", + " \n", + " Выведи оба результата и покажи, что они отличаются." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fe383ba3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "24\n", + "18\n" + ] + } + ], + "source": [ + "a = 3\n", + "b = 5\n", + "vir_1 = a * (b + a)\n", + "vir_2 = a * b + a\n", + "print(vir_1)\n", + "print(vir_2)" + ] + }, + { + "cell_type": "markdown", + "id": "538b91f6", + "metadata": {}, + "source": [ + "- Задача 5. Преобразование типов\n", + " \n", + " Создай переменные:\n", + " \n", + " - строку `\"25\"`;\n", + " - строку `\"3.14\"`;\n", + " - число `100`;\n", + " - дробное число `7.5`.\n", + " \n", + " Нужно:\n", + " \n", + " - строку `\"25\"` преобразовать в `int`;\n", + " - строку `\"3.14\"` преобразовать в `float`;\n", + " - число `100` преобразовать в `str`;\n", + " - дробное число `7.5` преобразовать в `str`.\n", + " \n", + " Выведи новые значения и их типы." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "925a2bd7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25 3.14 100 7.5 \n" + ] + } + ], + "source": [ + "a = '25'\n", + "b = '3.14'\n", + "c = 100\n", + "d = 7.5\n", + "a = int(a)\n", + "b = float(b)\n", + "c = str(c)\n", + "d = str(d)\n", + "print(a, type(a), b, type(b), c, type(c), d, type(d))" + ] + }, + { + "cell_type": "markdown", + "id": "91428133", + "metadata": {}, + "source": [ + "- Задача 6. Что возвращает `input()`\n", + " \n", + " Спроси у пользователя:\n", + " \n", + " - возраст;\n", + " - рост.\n", + " \n", + " Сразу после ввода выведи тип обеих переменных.\n", + " \n", + " После этого:\n", + " \n", + " - возраст преобразуй в `int`;\n", + " - рост преобразуй в `float`;\n", + " \n", + " и снова выведи типы." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "de601ce7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "28, , 178, \n", + "28, , 178.0, \n" + ] + } + ], + "source": [ + "age = input('Сколько тебе лет?')\n", + "height = input('Какой у тебя рост?')\n", + "print(f'{age}, {type(age)}, {height}, {type(height)} ')\n", + "age = int(age)\n", + "height = float(height)\n", + "print(f'{age}, {type(age)}, {height}, {type(height)} ')" + ] + }, + { + "cell_type": "markdown", + "id": "a079eff1", + "metadata": {}, + "source": [ + "- Задача 7. Мини-калькулятор\n", + " \n", + " Спроси у пользователя два числа.\n", + " \n", + " Нужно:\n", + " \n", + " - преобразовать их в нужный тип;\n", + " - вывести сумму;\n", + " - вывести разность;\n", + " - вывести произведение;\n", + " - вывести деление.\n", + " \n", + " Дополнительно выведи тип каждого результата." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "cb8021fa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Сумма: 6.0, \n", + "Разность: -1.0, \n", + "Произведение: 8.75, \n", + "Деление: 0.71, \n" + ] + } + ], + "source": [ + "num_1 = input('Назови первое число:')\n", + "num_2 = input('Назови второе число:')\n", + "num_1 = float(num_1)\n", + "num_2 = float(num_2)\n", + "sum = num_1 + num_2\n", + "dif = num_1 - num_2\n", + "div = num_1 / num_2\n", + "mult = num_1 * num_2\n", + "print(f'Сумма: {sum}, {type(sum)}\\nРазность: {dif}, {type(dif)}\\nПроизведение: {mult}, {type(mult)}\\nДеление: {round(div, 2)}, {type(div)}')\n" + ] + }, + { + "cell_type": "markdown", + "id": "5be5b6cf", + "metadata": {}, + "source": [ + "- Задача 8. Карточка пользователя\n", + " \n", + " Сделай программу, которая спрашивает у пользователя:\n", + " \n", + " - имя;\n", + " - возраст;\n", + " - рост;\n", + " - средний балл.\n", + " \n", + " После этого программа должна:\n", + " \n", + " 1. преобразовать данные в нужные типы;\n", + " 2. вывести карточку пользователя в несколько строк;\n", + " 3. отдельно вывести тип каждой переменной;\n", + " 4. вывести длину имени через `len()`." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "75f91b3a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Имя: Дима\n", + "Возраст: 28\n", + "Рост: 178\n", + "Средний балл: 4.7\n", + "Дима 28 178 4.7 \n", + "Длина имени: 4 символа\n" + ] + } + ], + "source": [ + "name = str(input('Как тебя зовут?'))\n", + "age = int(input('Сколько тебе лет?'))\n", + "height = int(input('Какой у тебя рост?'))\n", + "grade = float(input('Какой у тебя средний балл по диплому?'))\n", + "\n", + "print(f'Имя: {name}\\nВозраст: {age}\\nРост: {height}\\nСредний балл: {grade}')\n", + "print(name, type(name), age, type(age), height, type(height), grade, type(grade))\n", + "print(f'Длина имени: {len(name)} символа')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/3.expressions_and_operators.ipynb b/dmedvedev/python/3.expressions_and_operators.ipynb new file mode 100644 index 0000000..b127d40 --- /dev/null +++ b/dmedvedev/python/3.expressions_and_operators.ipynb @@ -0,0 +1,432 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "55fd4c86", + "metadata": {}, + "source": [ + "- Задача 1. Простые вычисления\n", + " \n", + " Создай две переменные с числами.\n", + " \n", + " Выведи:\n", + " \n", + " - сумму;\n", + " - разность;\n", + " - произведение;\n", + " - результат деления.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать переменные, а не числа прямо в `print`;\n", + " - каждую операцию вывести с новой строки." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "91a5f5f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Сумма: 15\n", + "Разность: -5\n", + "Произведение: 50\n", + "Деление: 0.5\n" + ] + } + ], + "source": [ + "var_1 = 5\n", + "var_2 = 10\n", + "sum = var_1 + var_2\n", + "dif = var_1 - var_2\n", + "mult = var_1 * var_2\n", + "div = var_1 / var_2\n", + "\n", + "print(f'Сумма: {sum}\\nРазность: {dif}\\nПроизведение: {mult}\\nДеление: {div}')" + ] + }, + { + "cell_type": "markdown", + "id": "f6784a18", + "metadata": {}, + "source": [ + "- Задача 2. Порядок действий\n", + " \n", + " Сделай программу, в которой будут два похожих выражения:\n", + " \n", + " - одно без скобок;\n", + " - второе со скобками.\n", + " \n", + " Выведи оба результата.\n", + " \n", + " **Требования:**\n", + " \n", + " - результаты должны отличаться;\n", + " - после вывода должно быть видно, что скобки меняют ответ." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ec602b7d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Выражение без скобок: 105\n", + " Выражение со скобкой: 150\n" + ] + } + ], + "source": [ + "var_1 = 5\n", + "var_2 = 10\n", + "print(f'Выражение без скобок: {var_1 + var_2 * var_2}\\n Выражение со скобкой: {(var_1 + var_2) * var_2}')" + ] + }, + { + "cell_type": "markdown", + "id": "bd598e93", + "metadata": {}, + "source": [ + "- Задача 3. Остаток и целая часть\n", + " \n", + " Создай две переменные с числами.\n", + " \n", + " Выведи:\n", + " \n", + " - результат обычного деления `/`;\n", + " - результат целочисленного деления `//`;\n", + " - остаток от деления `%`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать одни и те же два числа;\n", + " - результаты подписать понятным текстом." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b3a6d97e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Обычное деление: 0.5\n", + "Целочисленное деление: 0\n", + "Остаток от деления: 5\n" + ] + } + ], + "source": [ + "var_1 = 5\n", + "var_2 = 10\n", + "print(f'Обычное деление: {var_1 / var_2}\\nЦелочисленное деление: {var_1 // var_2}\\nОстаток от деления: {var_1 % var_2}')" + ] + }, + { + "cell_type": "markdown", + "id": "44fbe39b", + "metadata": {}, + "source": [ + "- Задача 4. Степень и повторение строки\n", + " \n", + " Сделай программу из двух частей:\n", + " \n", + " - Часть 1\n", + " \n", + " Создай переменную с числом и выведи:\n", + " \n", + " - квадрат числа;\n", + " - куб числа.\n", + " - Часть 2\n", + " \n", + " Создай переменную со строкой и выведи её:\n", + " \n", + " - один раз;\n", + " - три раза подряд.\n", + " \n", + " **Требования:**\n", + " \n", + " - для числа использовать оператор `*`;" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "a1fdaf3e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Квадрат числа: 25\n", + "Куб числа: 125\n", + "Выводим один раз: string\n", + "Выводим 3 раза: stringstringstring\n" + ] + } + ], + "source": [ + "var_1 = 5\n", + "print(f'Квадрат числа: {var_1 ** 2}\\nКуб числа: {var_1 ** 3}')\n", + "\n", + "var_2 = 'string'\n", + "print(f'Выводим один раз: {var_2}\\nВыводим 3 раза: {var_2 * 3}')\n" + ] + }, + { + "cell_type": "markdown", + "id": "3aa41bda", + "metadata": {}, + "source": [ + "- Задача 5. Сравнение значений\n", + " \n", + " Создай две переменные с числами.\n", + " \n", + " Выведи результаты сравнений:\n", + " \n", + " - равно ли первое второму;\n", + " - не равно ли первое второму;\n", + " - больше ли первое;\n", + " - меньше ли первое;\n", + " - больше или равно;\n", + " - меньше или равно.\n", + " \n", + " **Требования:**\n", + " \n", + " - все сравнения должны быть через переменные;\n", + " - каждый результат вывести отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "119b33c3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Первое число = 5\n", + "Второе число = 10\n", + "Равны ли числа? False\n", + "Не равны ли числа? True\n", + "Первое больше второго? False\n", + "Первое меньше второго? True\n", + "Первое больше или равно второму? False\n", + "Первое меньше или равно второму? True\n" + ] + } + ], + "source": [ + "var_1 = 5\n", + "var_2 = 10\n", + "print(f'Первое число = {var_1}\\nВторое число = {var_2}')\n", + "print(f'Равны ли числа? {var_1 == var_2}')\n", + "print(f'Не равны ли числа? {var_1 != var_2}')\n", + "print(f'Первое больше второго? {var_1 > var_2}')\n", + "print(f'Первое меньше второго? {var_1 < var_2}')\n", + "print(f'Первое больше или равно второму? {var_1 >= var_2}')\n", + "print(f'Первое меньше или равно второму? {var_1 <= var_2}')" + ] + }, + { + "cell_type": "markdown", + "id": "d4194607", + "metadata": {}, + "source": [ + "- Задача 6. Выражения с `input()`\n", + " \n", + " Спроси у пользователя два числа.\n", + " \n", + " После этого:\n", + " \n", + " - преобразуй их в нужный тип;\n", + " - выведи сумму;\n", + " - выведи произведение;\n", + " - выведи результат сравнения: больше ли первое число второго.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `input()`;\n", + " - использовать преобразование типов;\n", + " - использовать и арифметический оператор, и оператор сравнения." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "633f347d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Сумма чисел = 15.0\n", + "Произведение чисел = 50.0\n", + "Первое число больше второго? False\n" + ] + } + ], + "source": [ + "var_1 = float(input('Задай первое число'))\n", + "var_2 = float(input('Задай второе число'))\n", + "sum = var_1 + var_2\n", + "mult = var_1 * var_2\n", + "print(f'Сумма чисел = {sum}\\nПроизведение чисел = {mult}\\nПервое число больше второго? {var_1 > var_2}')" + ] + }, + { + "cell_type": "markdown", + "id": "3b121f7c", + "metadata": {}, + "source": [ + "- Задача 7. Мини-калькулятор с проверками\n", + " \n", + " Спроси у пользователя два числа.\n", + " \n", + " Программа должна вывести:\n", + " \n", + " - сумму;\n", + " - разность;\n", + " - произведение;\n", + " - деление;\n", + " - остаток от деления;\n", + " - результат сравнения: равны ли числа;\n", + " - результат сравнения: какое число больше.\n", + " \n", + " **Требования:**\n", + " \n", + " - числа должны вводиться пользователем;\n", + " - нужно использовать несколько разных операторов;\n", + " - вывод должен быть аккуратным и понятным." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "bcceaa64", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Сумма: 15.0\n", + "Разность: -5.0\n", + "Произведение: 50.0\n", + "Деление: 0.5\n", + "Остаток от деления: 5.0\n", + "Равны ли числа? False\n", + "Какой число больше? - второе больше\n" + ] + } + ], + "source": [ + "var_1 = float(input('Задай первое число'))\n", + "var_2 = float(input('Задай второе число'))\n", + "sum = var_1 + var_2\n", + "dif = var_1 - var_2\n", + "mult = var_1 * var_2\n", + "div = var_1 / var_2\n", + "ost = var_1 % var_2\n", + "print(f'Сумма: {sum}\\nРазность: {dif}\\nПроизведение: {mult}\\nДеление: {round(div, 2)}\\nОстаток от деления: {ost}')\n", + "print(f\"Равны ли числа? {var_1 == var_2}\\nКакой число больше? - {'первое больше' if var_1 > var_2 else 'второе больше' if var_1 < var_2 else 'числа равны'}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c822f9b4", + "metadata": {}, + "source": [ + "- Задача 8. Карточка вычислений\n", + " \n", + " Спроси у пользователя:\n", + " \n", + " - имя;\n", + " - возраст;\n", + " - сколько часов в день он учится;\n", + " - сколько часов в день отдыхает.\n", + " \n", + " После этого программа должна вывести мини-отчет:\n", + " \n", + " - имя пользователя;\n", + " - через сколько лет ему будет 18, если сейчас меньше 18;\n", + " - сколько всего часов занимает учеба и отдых вместе;\n", + " - больше ли учебы, чем отдыха;\n", + " - равны ли часы учебы и отдыха.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать строки, числа и сравнения;\n", + " - использовать арифметические операторы;\n", + " - использовать операторы сравнения;\n", + " - все вычисления делать через переменные." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2bb4328", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Имя пользователя: Дима\n", + "Через 3 лет пользователю будет 18 лет\n", + "Учеба и отдых занимает 8.5 часов в день\n", + "Учеба занимает 3.0, а отдых - 5.5.Отдых занимает больше на 2.5\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "age = int(input('Сколько тебе лет?'))\n", + "study_hours = float(input('Сколько часов в день ты учишься?'))\n", + "relax_hours = float(input('Сколько часов в день отдыхаешь?'))\n", + "print(f'Имя пользователя: {name}')\n", + "print(f'Через {18 - age} лет пользователю будет 18 лет' if age < 18 else 'Пользователь совершеннолетний')\n", + "print(f'Учеба и отдых занимает {study_hours + relax_hours} часов в день')\n", + "print(f\"Учеба занимает {study_hours}, а отдых - {relax_hours}.\" \n", + " f\"{f'Учеба занимает больше на {study_hours - relax_hours}' if study_hours > relax_hours else f'Отдых занимает больше на {relax_hours - study_hours}' if study_hours < relax_hours else 'Учеба и отдых занимают одинаковое количество времени'}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/4.conditional_operator.ipynb b/dmedvedev/python/4.conditional_operator.ipynb new file mode 100644 index 0000000..34d5873 --- /dev/null +++ b/dmedvedev/python/4.conditional_operator.ipynb @@ -0,0 +1,385 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fd1e5d6", + "metadata": {}, + "source": [ + "- Задача 1. Положительное или нет\n", + " \n", + " Спроси у пользователя число.\n", + " \n", + " Если число больше 0, выведи:`Число положительное` \n", + " \n", + " Если число меньше или равно 0, выведи:`Число не положительное`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `input()`;\n", + " - преобразовать ввод в число;\n", + " - использовать `if else`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5247fb51", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Число не положительное\n" + ] + } + ], + "source": [ + "number = float(input('Введите любое число'))\n", + "if number > 0:\n", + " print('Число положительное')\n", + "else:\n", + " print('Число не положительное')\n" + ] + }, + { + "cell_type": "markdown", + "id": "78ace526", + "metadata": {}, + "source": [ + "- Задача 2. Совершеннолетие\n", + " \n", + " Спроси у пользователя возраст.\n", + " \n", + " Если возраст 18 или больше, выведи:`Доступ разрешен`\n", + " \n", + " Иначе выведи:`Доступ запрещен`\n", + " \n", + " **Требования:**\n", + " \n", + " - возраст должен вводить пользователь;\n", + " - использовать `int()`;\n", + " - использовать `if else`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b4a40fbf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Доступ разрешен\n" + ] + } + ], + "source": [ + "age = int(input('Сколько вам лет?'))\n", + "if age >= 18:\n", + " print('Доступ разрешен')\n", + "else:\n", + " print('Доступ запрещен')" + ] + }, + { + "cell_type": "markdown", + "id": "b9fe6302", + "metadata": {}, + "source": [ + "- Задача 3. Четное или нечетное\n", + " \n", + " Спроси у пользователя целое число.\n", + " \n", + " Если число четное, выведи:`Четное число`\n", + " \n", + " Иначе выведи:`Нечетное число`\n", + " \n", + " - **Подсказка:**\n", + " \n", + " используй остаток от деления `%`." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "102b96d6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Четное число\n" + ] + } + ], + "source": [ + "number = int(input('Введите целое число'))\n", + "if number % 2 == 0:\n", + " print('Четное число')\n", + "else:\n", + " print('Нечетное число')" + ] + }, + { + "cell_type": "markdown", + "id": "fae1184f", + "metadata": {}, + "source": [ + "- Задача 4. Сравнение двух чисел\n", + " \n", + " Спроси у пользователя два числа.\n", + " \n", + " Нужно:\n", + " \n", + " - если первое число больше второго, вывести: `Первое число больше`;\n", + " - если второе больше первого, вывести: `Второе число больше`;\n", + " - если числа равны, вывести: `Числа равны`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `if / elif / else`;\n", + " - оба числа должны вводиться пользователем." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9c2c05da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Числа равны\n" + ] + } + ], + "source": [ + "num_1 = float(input('Введите первое число'))\n", + "num_2 = float(input('Введите второе число'))\n", + "if num_1 > num_2:\n", + " print('Первое число больше')\n", + "elif num_1 < num_2:\n", + " print('Второе число больше')\n", + "else:\n", + " print('Числа равны')\n" + ] + }, + { + "cell_type": "markdown", + "id": "dad86a33", + "metadata": {}, + "source": [ + "- Задача 5. Оценка по баллам\n", + " \n", + " Спроси у пользователя число баллов.\n", + " \n", + " Нужно вывести:\n", + " \n", + " - `Отлично`, если балл 90 и выше;\n", + " - `Хорошо`, если балл от 70 до 89;\n", + " - `Удовлетворительно`, если балл от 50 до 69;\n", + " - `Нужно подтянуться`, если меньше 50.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `if / elif / else`;\n", + " - балл вводится пользователем;\n", + " - сначала преобразовать ввод в число." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "bdc1c63e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Хорошо\n" + ] + } + ], + "source": [ + "grade = int(input('Скажи сколько у тебя баллов'))\n", + "if grade >= 90:\n", + " print('Отлично')\n", + "elif grade >= 70 and grade < 90:\n", + " print('Хорошо')\n", + "elif grade < 50:\n", + " print('Нужно подтянуться')\n", + "else: \n", + " print('Удовлетворительно')" + ] + }, + { + "cell_type": "markdown", + "id": "a9091d3f", + "metadata": {}, + "source": [ + "- Задача 6. Пароль\n", + " \n", + " Заранее создай переменную с правильным паролем, например `\"python123\"`.\n", + " \n", + " Потом спроси пароль у пользователя.\n", + " \n", + " Если пароль совпадает с правильным, выведи:`Пароль верный`\n", + " \n", + " Иначе выведи:`Пароль неверный`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать строку;\n", + " - сравнивать введенное значение с переменной;\n", + " - использовать `if else`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "32c630cc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Пароль верный\n" + ] + } + ], + "source": [ + "right_pass = 'python123'\n", + "pass_from_user = input('Введите верный пароль')\n", + "if pass_from_user == right_pass:\n", + " print('Пароль верный')\n", + "else:\n", + " print('Пароль неверный')" + ] + }, + { + "cell_type": "markdown", + "id": "5179a630", + "metadata": {}, + "source": [ + "- Задача 7. Мини-проверка возраста и имени\n", + " \n", + " Спроси у пользователя:\n", + " \n", + " - имя;\n", + " - возраст.\n", + " \n", + " Нужно:\n", + " \n", + " - если возраст меньше 18, вывести: `Привет, <имя>. Тебе пока нет 18`;\n", + " - иначе вывести: `Привет, <имя>. Доступ разрешен`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать и строку, и число;\n", + " - возраст преобразовать в `int`;\n", + " - в выводе использовать имя пользователя." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "522fa808", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Привет, 20. Доступ разрешен\n" + ] + } + ], + "source": [ + "name = input('Как тебя зовут?')\n", + "age = int(input('Назовите свой возраст'))\n", + "if age < 18:\n", + " print(f'Привет, {name}. Тебе пока нет 18')\n", + "else:\n", + " print(f'Привет, {name}. Доступ разрешен')\n" + ] + }, + { + "cell_type": "markdown", + "id": "38ab0028", + "metadata": {}, + "source": [ + "- Задача 8. Небольшое меню действий\n", + " \n", + " Спроси у пользователя число от 1 до 3.\n", + " \n", + " Выведи:\n", + " \n", + " - `Ты выбрал старт`, если введено `1`;\n", + " - `Ты выбрал настройки`, если введено `2`;\n", + " - `Ты выбрал выход`, если введено `3`;\n", + " - `Неизвестная команда`, если введено что-то другое.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `if / elif / else`;\n", + " - выбор должен вводить пользователь;\n", + " - сначала преобразовать ввод в число." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "4fda486e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ты выбрал выход\n" + ] + } + ], + "source": [ + "num = int(input('Введи целое число от 1 до 3'))\n", + "if num == 1:\n", + " print('Ты выбрал старт')\n", + "elif num == 2:\n", + " print('Ты выбрал настройки')\n", + "elif num == 3:\n", + " print('Ты выбрал выход')\n", + "else:\n", + " print('Неизвестная команда')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/5.logic_operators.ipynb b/dmedvedev/python/5.logic_operators.ipynb new file mode 100644 index 0000000..2efeff7 --- /dev/null +++ b/dmedvedev/python/5.logic_operators.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0cb44a4f", + "metadata": {}, + "source": [ + "- Задача 1. Подходит ли возраст\n", + " \n", + " Спроси у пользователя возраст.\n", + " \n", + " Нужно проверить, входит ли возраст в диапазон **от 12 до 18 включительно**.\n", + " \n", + " Если входит, выведи:`Возраст подходит`\n", + " \n", + " Иначе выведи:`Возраст не подходит`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `and`;\n", + " - возраст вводится пользователем;\n", + " - ввод нужно преобразовать в число." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2284f3cc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Возраст подходит\n" + ] + } + ], + "source": [ + "age = int(input('Сколько вам лет?'))\n", + "if age >= 12 and age <= 18:\n", + " print('Возраст подходит')\n", + "else:\n", + " print('Возраст не подходит')\n" + ] + }, + { + "cell_type": "markdown", + "id": "c10f2e18", + "metadata": {}, + "source": [ + "- Задача 2. Выходной или нет\n", + " \n", + " Спроси у пользователя день недели.\n", + " \n", + " Если это **суббота** или **воскресенье**, выведи:`Сегодня выходной`\n", + " \n", + " Иначе выведи:`Сегодня не выходной`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `or`;\n", + " - сравнивать строку с двумя вариантами;\n", + " - каждый вариант сравнения записать полностью." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "08c26485", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Сегодня выходной\n" + ] + } + ], + "source": [ + "day_of_week = input('Какой сегодня день недели?')\n", + "if day_of_week == 'суббота' or 'воскресенье' :\n", + " print('Сегодня выходной')\n", + "else:\n", + " print('Сегодня не выходной]')" + ] + }, + { + "cell_type": "markdown", + "id": "ce20eb2e", + "metadata": {}, + "source": [ + "- Задача 3. Можно ли войти\n", + " \n", + " Создай две переменные:\n", + " \n", + " - `age` — возраст;\n", + " - `has_ticket` — есть ли билет (`True` или `False`).\n", + " \n", + " Нужно проверить:\n", + " \n", + " - если возраст **18 или больше** **и** билет есть — вывести `Можно войти`;\n", + " - иначе — вывести `Войти нельзя`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `and`;\n", + " - одно условие должно быть числовым, другое — логическим;\n", + " - решение сделать через `if else`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7c9b5f94", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "войти нельзя\n" + ] + } + ], + "source": [ + "age = int(input('сколько вам лет?'))\n", + "has_ticket = True\n", + "if age >= 18 and has_ticket:\n", + " print('можно войти')\n", + "else:\n", + " print('войти нельзя')" + ] + }, + { + "cell_type": "markdown", + "id": "9b682206", + "metadata": {}, + "source": [ + "- Задача 4. Свободен ли человек\n", + " \n", + " Создай переменную `is_busy`.\n", + " \n", + " Если человек **не занят**, выведи:`Можно позвонить`\n", + " \n", + " Иначе выведи:`Сейчас лучше не звонить`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `not`;\n", + " - значение переменной задать в коде;\n", + " - решение сделать через `if else`." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "7d02a3c4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Можно позвонить\n" + ] + } + ], + "source": [ + "is_busy = None\n", + "is_busy = input('Человек занят? да/нет')\n", + "if not is_busy == 'нет':\n", + " print('Сейчас лучше не звонить')\n", + "else:\n", + " print('Можно позвонить')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/6.cycles_while.ipynb b/dmedvedev/python/6.cycles_while.ipynb new file mode 100644 index 0000000..65cc047 --- /dev/null +++ b/dmedvedev/python/6.cycles_while.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "791c0955", + "metadata": {}, + "source": [ + "- Задача 1. Вывести числа от 1 до 5\n", + " \n", + " Сделай программу, которая выводит числа от `1` до `5` с помощью цикла `while`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать переменную-счетчик;\n", + " - внутри цикла изменять эту переменную;\n", + " - каждое число выводить с новой строки." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f606bb40", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n", + "2\n", + "3\n", + "4\n", + "5\n" + ] + } + ], + "source": [ + "num = 1\n", + "while num < 6:\n", + " print(num)\n", + " num += 1" + ] + }, + { + "cell_type": "markdown", + "id": "8dfdbe5f", + "metadata": {}, + "source": [ + "- Задача 2. Вывести слово несколько раз\n", + " \n", + " Сделай программу, которая выводит слово `Привет` ровно 3 раза с помощью `while`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать счетчик;\n", + " - цикл должен закончиться сам;\n", + " - слово выводить в каждой итерации отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4a4352c2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Привет\n", + "Привет\n", + "Привет\n" + ] + } + ], + "source": [ + "n = 1\n", + "while n < 4:\n", + " print('Привет')\n", + " n += 1" + ] + }, + { + "cell_type": "markdown", + "id": "f85e214e", + "metadata": {}, + "source": [ + "- Задача 3. Сумма чисел от 1 до 5\n", + " \n", + " Сделай программу, которая находит сумму чисел от `1` до `5` с помощью `while`.\n", + " \n", + " В конце программа должна вывести итоговую сумму.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать переменную для текущего числа;\n", + " - использовать отдельную переменную для суммы;\n", + " - выводить только конечный результат." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c1f0b624", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15\n" + ] + } + ], + "source": [ + "sum = 0\n", + "n = 1\n", + "while n < 6:\n", + " sum = sum + n\n", + " n += 1\n", + "print(sum)" + ] + }, + { + "cell_type": "markdown", + "id": "d045b301", + "metadata": {}, + "source": [ + "- Задача 4. Обратный отсчет\n", + " \n", + " Сделай программу, которая выводит числа от `5` до `1`, а после этого выводит строку: `Старт!`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `while`;\n", + " - счетчик должен уменьшаться;\n", + " - строка `Старт!` должна выводиться уже после завершения цикла." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b1ec7610", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n", + "4\n", + "3\n", + "2\n", + "1\n", + "Старт!\n" + ] + } + ], + "source": [ + "n = 5\n", + "while n != 0:\n", + " print(n)\n", + " n = n - 1\n", + "print('Старт!')" + ] + }, + { + "cell_type": "markdown", + "id": "366b67d3", + "metadata": {}, + "source": [ + "- Задача 5. Пока не введут правильный пароль\n", + " \n", + " Заранее создай переменную с правильным паролем, например `\"python123\"`.\n", + " \n", + " Программа должна:\n", + " \n", + " 1. спросить пароль у пользователя;\n", + " 2. пока пароль неверный, снова просить ввести его;\n", + " 3. когда пароль введен правильно, вывести:\n", + " \n", + " `Пароль верный`\n", + " \n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `while`;\n", + " - использовать сравнение строк;\n", + " - внутри цикла снова запрашивать ввод." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "44ac2dce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Пароль верный\n" + ] + } + ], + "source": [ + "right_pass = 'python123'\n", + "question_pass = input('Введите пароль')\n", + "while question_pass != right_pass:\n", + " question_pass = input('ВЫ ВВЕЛИ НЕВЕРНЫЙ ПАРОЛЬ, ПОВТОРИТЕ')\n", + "print('Пароль верный')" + ] + }, + { + "cell_type": "markdown", + "id": "a7c68d25", + "metadata": {}, + "source": [ + "- Задача 6. Сумма чисел, пока не введут 0\n", + " \n", + " Сделай программу, которая запрашивает у пользователя числа.\n", + " \n", + " Программа должна работать так:\n", + " \n", + " - пользователь вводит число;\n", + " - программа добавляет его к общей сумме;\n", + " - если пользователь вводит `0`, цикл заканчивается;\n", + " - после окончания программа выводит итоговую сумму.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `while`;\n", + " - использовать `input()` и преобразование в число;\n", + " - `0` не должен ломать программу, а должен завершать ввод." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cb692e9e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.0\n" + ] + } + ], + "source": [ + "a = float(input('введите число'))\n", + "b = 0\n", + "while a != 0:\n", + " b = b + a\n", + " a = float(input('введите число еще раз'))\n", + "print(b)\n" + ] + }, + { + "cell_type": "markdown", + "id": "217323e8", + "metadata": {}, + "source": [ + "- Задача 7. Проверка числа на диапазон\n", + " \n", + " Спроси у пользователя число.\n", + " \n", + " Пока число **не входит в диапазон от 1 до 10**, программа должна снова просить ввести число.\n", + " \n", + " Когда пользователь наконец введет число от `1` до `10`, программа должна вывести: `Число подходит`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `while`;\n", + " - использовать логические операторы в условии;\n", + " - цикл должен завершаться только при правильном вводе." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "fbd8b8fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "число подходит\n" + ] + } + ], + "source": [ + "a = float(input('введите число'))\n", + "while a < 1 or a > 10:\n", + " a = float(input('число не подходит, введите новое число'))\n", + "print('число подходит')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/7.cycles_for.ipynb b/dmedvedev/python/7.cycles_for.ipynb new file mode 100644 index 0000000..a9553b2 --- /dev/null +++ b/dmedvedev/python/7.cycles_for.ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "69eb39ee", + "metadata": {}, + "source": [ + "- Задача 1. Вывести числа от 1 до 5\n", + " \n", + " Сделай программу, которая выводит числа от `1` до `5` с помощью цикла `for`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `range()`;\n", + " - каждое число выводить с новой строки." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "66a25f8c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n", + "2\n", + "3\n", + "4\n", + "5\n" + ] + } + ], + "source": [ + "for i in range(1, 6):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "id": "f07ffbfd", + "metadata": {}, + "source": [ + "- Задача 2. Вывести слово несколько раз\n", + " \n", + " Сделай программу, которая выводит слово `Привет` ровно 3 раза с помощью `for`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `for`;\n", + " - использовать `range()`;\n", + " - слово выводить в каждой итерации отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5e56481d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Привет\n", + "Привет\n", + "Привет\n" + ] + } + ], + "source": [ + "for i in range(3):\n", + " print('Привет')" + ] + }, + { + "cell_type": "markdown", + "id": "1cac9b1a", + "metadata": {}, + "source": [ + "- Задача 3. Сумма чисел от 1 до 5\n", + " \n", + " Сделай программу, которая находит сумму чисел от `1` до `5` с помощью `for`.\n", + " \n", + " В конце программа должна вывести итоговую сумму.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать переменную для суммы;\n", + " - внутри цикла прибавлять текущее число к общей сумме;\n", + " - вывести только конечный результат." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f1f7ac17", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15\n" + ] + } + ], + "source": [ + "a = 0\n", + "for i in range(1, 6):\n", + " a = a + i\n", + "print(a)" + ] + }, + { + "cell_type": "markdown", + "id": "1b57f6b7", + "metadata": {}, + "source": [ + "- Задача 4. Обратный отсчет\n", + " \n", + " Сделай программу, которая выводит числа от `5` до `1`, а после этого выводит строку: `Старт!`\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `for`;\n", + " - вывести числа в обратном порядке;\n", + " - строка `Старт!` должна выводиться после цикла." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8243b694", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5\n", + "4\n", + "3\n", + "2\n", + "1\n", + "Старт!\n" + ] + } + ], + "source": [ + "for i in range(5, 0, -1):\n", + " print(i)\n", + "print('Старт!')" + ] + }, + { + "cell_type": "markdown", + "id": "7b8fe3f7", + "metadata": {}, + "source": [ + "- Задача 5. Буквы слова\n", + " \n", + " Создай переменную со словом, например `\"Python\"`.\n", + " \n", + " Сделай программу, которая с помощью `for` выводит каждую букву этого слова с новой строки.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать цикл `for`;\n", + " - пройтись по строке;\n", + " - каждую букву выводить отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "230d5292", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "P\n", + "y\n", + "t\n", + "h\n", + "o\n", + "n\n" + ] + } + ], + "source": [ + "a = 'Python'\n", + "for i in a:\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "id": "064a2d9b", + "metadata": {}, + "source": [ + "- Задача 6. Подсчет суммы введенных чисел\n", + " \n", + " Спроси у пользователя, сколько чисел он хочет ввести.\n", + " \n", + " После этого программа должна:\n", + " \n", + " 1. `n` раз спросить число;\n", + " 2. добавить каждое число к общей сумме;\n", + " 3. после окончания вывести итоговую сумму.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `for`;\n", + " - количество повторений должно зависеть от числа, введенного пользователем;\n", + " - использовать `input()` и преобразование в число." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "61185056", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Вам нужно будет ввести 3 чисел\n", + "Сумма всех чисел: 6.0\n" + ] + } + ], + "source": [ + "a = int(input('Сколько чисел вы введете?'))\n", + "print(f'Вам нужно будет ввести {a} чисел')\n", + "c = 0\n", + "for i in range(a):\n", + " b = float(input(f'введите число №{i + 1}'))\n", + " c = b + c\n", + "print(f'Сумма всех чисел: {c}')" + ] + }, + { + "cell_type": "markdown", + "id": "3825fcdf", + "metadata": {}, + "source": [ + "- Задача 7. Мини-анкета\n", + "С помощью цикла `for` выведи по очереди вопросы:\n", + " - `Как тебя зовут?`\n", + " - `Сколько тебе лет?`\n", + " - `Из какого ты города?`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать цикл `for`;\n", + " - пройтись по строкам;\n", + " - каждый вопрос выводить отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "d75b9340", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Как тебя зовут?\n", + "Сколько тебе лет?\n", + "Из какого ты города?\n" + ] + } + ], + "source": [ + "a = 'Как тебя зовут?'\n", + "b = 'Сколько тебе лет?'\n", + "c = 'Из какого ты города?'\n", + "\n", + "d = [a, b, c]\n", + "for i in d:\n", + " print(i)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/8.structure_of_data_string.ipynb b/dmedvedev/python/8.structure_of_data_string.ipynb new file mode 100644 index 0000000..e313f1b --- /dev/null +++ b/dmedvedev/python/8.structure_of_data_string.ipynb @@ -0,0 +1,347 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3d17cff8", + "metadata": {}, + "source": [ + "- Задача 1. Имя файла\n", + " \n", + " Создай строку с именем файла, например: `\"orders_2025.csv\"`\n", + " \n", + " Выведи:\n", + " \n", + " - саму строку;\n", + " - длину строки через `len()`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать строковую переменную;" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "48e71970", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "orders_2025.csv\n", + "15\n" + ] + } + ], + "source": [ + "a = 'orders_2025.csv'\n", + "print(a)\n", + "print(len(a))" + ] + }, + { + "cell_type": "markdown", + "id": "5ef32f80", + "metadata": {}, + "source": [ + "- Задача 2. Первый и последний символ пути\n", + " \n", + " Создай строку с путем или именем файла, например: `\"raw_data.json\"`\n", + " \n", + " Выведи:\n", + " \n", + " - первый символ строки;\n", + " - последний символ строки.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать индексы;" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a02849b7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "r\n", + "n\n" + ] + } + ], + "source": [ + "a = 'raw_data.json'\n", + "print(a[0])\n", + "print(a[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "305de650", + "metadata": {}, + "source": [ + "- Задача 3. Символы по индексам в названии таблицы\n", + " \n", + " Создай строку с названием таблицы, например: `\"sales_data\"`\n", + " \n", + " Выведи:\n", + " \n", + " - символ с индексом `0`;\n", + " - символ с индексом `2`;\n", + " - символ с индексом `1`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать обычный и отрицательный индекс." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d230f744", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "s\n", + "l\n", + "l\n", + "a\n" + ] + } + ], + "source": [ + "a = 'sales_data'\n", + "print(a[0])\n", + "print(a[-8])\n", + "print(a[1])" + ] + }, + { + "cell_type": "markdown", + "id": "0516ae84", + "metadata": {}, + "source": [ + "- Задача 4. Переверни код источника\n", + " \n", + " Создай строку с коротким кодом источника, например: `\"API001\"`\n", + " \n", + " Выведи:\n", + " \n", + " - исходную строку;\n", + " - строку в обратном порядке.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать слайс со шагом;\n", + " - не использовать цикл." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "3a257e6c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "API001\n", + "100IPA\n" + ] + } + ], + "source": [ + "a = 'API001' \n", + "print(a)\n", + "print(a[::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "e2f8feb6", + "metadata": {}, + "source": [ + "- Задача 5. Очистка названия колонки\n", + " \n", + " Создай строку с названием колонки, в которой:\n", + " \n", + " - есть лишние пробелы по краям;\n", + " - есть буквы разного регистра.\n", + " \n", + " Например: `\" User_ID \"`\n", + " \n", + " Нужно вывести:\n", + " \n", + " - исходную строку;\n", + " - строку после `strip()`;\n", + " - строку в нижнем регистре;\n", + " - строку в верхнем регистре.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать методы строк;\n", + " - каждый результат вывести отдельно." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "eaa07c5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " User_ID \n", + "User_ID\n", + " user_id \n", + " USER_ID \n" + ] + } + ], + "source": [ + "a = ' User_ID '\n", + "print(a) \n", + "print(a.strip())\n", + "print(a.lower()) \n", + "print(a.upper())" + ] + }, + { + "cell_type": "markdown", + "id": "fb8cc762", + "metadata": {}, + "source": [ + "- Задача 6. Замена в строке лога\n", + " \n", + " Создай строку с логом или текстом ошибки, например: `\"ERROR: file not found\"`\n", + " \n", + " Нужно:\n", + " \n", + " - вывести исходную строку;\n", + " - заменить слово `\"ERROR\"` на `\"WARNING\"` через `replace()`;\n", + " - сохранить результат в новую переменную;\n", + " - вывести новую строку;\n", + " - проверить, есть ли в строке слово `\"file\"` через `in`;\n", + " - вывести результат проверки.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `replace()`;\n", + " - использовать оператор `in`;\n", + " - вывести все промежуточные результаты;\n", + " - не изменять исходную строку напрямую." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3ea53267", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ERROR: file not found\n", + "WARNING: file not found\n", + "True\n" + ] + } + ], + "source": [ + "a = 'ERROR: file not found'\n", + "print(a)\n", + "b = a.replace('ERROR', 'WARNING')\n", + "print(b)\n", + "print('file' in b)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- Задача 7. Замена в строке лога\n", + " \n", + " Спроси у пользователя название файла, таблицы или колонки.\n", + " \n", + " После этого программа должна вывести:\n", + " \n", + " - саму строку;\n", + " - длину строки;\n", + " - первый символ;\n", + " - последний символ;\n", + " - строку в верхнем регистре;\n", + " - строку в обратном порядке.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать `input()`;\n", + " - использовать `len()`;\n", + " - использовать индексы;\n", + " - использовать метод строки;\n", + " - использовать слайс." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0055f1c5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "123\n", + "3\n", + "1\n", + "3\n", + "123\n", + "321\n" + ] + } + ], + "source": [ + "a = input('Введите название файла')\n", + "print(a)\n", + "print(len(a))\n", + "print(a[0])\n", + "print(a[-1])\n", + "print(a.upper())\n", + "print(a[::-1])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/python/9.structure_of_data_list.ipynb b/dmedvedev/python/9.structure_of_data_list.ipynb new file mode 100644 index 0000000..18a468c --- /dev/null +++ b/dmedvedev/python/9.structure_of_data_list.ipynb @@ -0,0 +1,358 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "138e28b2", + "metadata": {}, + "source": [ + "- Задача 1. Список колонок\n", + " \n", + " Создай список из 4 названий колонок, например:\n", + " \n", + " - `id`\n", + " - `name`\n", + " - `city`\n", + " - `age`\n", + " \n", + " Выведи:\n", + " \n", + " - весь список;\n", + " - длину списка.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать список;\n", + " - использовать `len()`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "82318fc7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['id', 'name', 'city', 'age']\n", + "4\n" + ] + } + ], + "source": [ + "a = ['id', 'name', 'city', 'age']\n", + "print(a)\n", + "print(len(a))" + ] + }, + { + "cell_type": "markdown", + "id": "c5561266", + "metadata": {}, + "source": [ + "- Задача 2. Первая, последняя и средняя колонка\n", + " \n", + " Создай список из 5 названий полей таблицы.\n", + " \n", + " Выведи:\n", + " \n", + " - первый элемент;\n", + " - последний элемент;\n", + " - элемент с индексом `2`.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать индексы;\n", + " - использовать и обычный, и отрицательный индекс." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0f51f52e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "id\n", + "email\n", + "city\n" + ] + } + ], + "source": [ + "a = ['id', 'name', 'city', 'age', 'email']\n", + "print(a[0])\n", + "print(a[-1])\n", + "print(a[2])\n" + ] + }, + { + "cell_type": "markdown", + "id": "ea884766", + "metadata": {}, + "source": [ + "- Задача 3. Исправь название поля\n", + " \n", + " Создай список из 4 названий колонок.\n", + " \n", + " Замени одно неверное или неудобное название на другое.\n", + " \n", + " Пример идеи:\n", + " \n", + " - было `usr_id`\n", + " - стало `user_id`\n", + " \n", + " После этого выведи:\n", + " \n", + " - исходный список;\n", + " - измененный список.\n", + " \n", + " **Требования:**\n", + " \n", + " - изменить элемент по индексу;\n", + " - не создавать новый список с нуля." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "013b5437", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['id', 'na', 'city', 'age']\n", + "['id', 'name', 'city', 'age']\n" + ] + } + ], + "source": [ + "a = ['id', 'na', 'city', 'age']\n", + "print(a)\n", + "a[1] = 'name'\n", + "print(a)" + ] + }, + { + "cell_type": "markdown", + "id": "6046a2d0", + "metadata": {}, + "source": [ + "- Задача 5. Части списка колонок\n", + " \n", + " Создай список из 6 названий колонок.\n", + " \n", + " Выведи:\n", + " \n", + " - первые 3 элемента;\n", + " - последние 2 элемента;\n", + " - список без первого элемента;\n", + " - список в обратном порядке.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать слайсы;\n", + " - не писать новый список вручную." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "2709006a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['id', 'name', 'city']\n", + "['email', 'date_birthday']\n", + "['name', 'city', 'age', 'email', 'date_birthday']\n", + "['date_birthday', 'email', 'age', 'city', 'name', 'id']\n" + ] + } + ], + "source": [ + "a = ['id', 'name', 'city', 'age', 'email', 'date_birthday']\n", + "print(a[:3])\n", + "print(a[-2:])\n", + "print(a[1:])\n", + "print(a[-1::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "cdc7383a", + "metadata": {}, + "source": [ + "- Задача 6. Перебор списка файлов\n", + " \n", + " Создай список из 5 файлов, например:\n", + " \n", + " - `orders.csv`\n", + " - `users.csv`\n", + " - `products.csv`\n", + " - `sales.csv`\n", + " - `logs.csv`\n", + " \n", + " С помощью цикла `for` выведи каждый файл с новой строки.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать список;\n", + " - использовать `for`;\n", + " - пройтись по всем элементам списка." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "5d511291", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "orders.csv\n", + "users.csv\n", + "products.csv\n", + "sales.csv\n", + "logs.csv\n" + ] + } + ], + "source": [ + "a = ['orders.csv', 'users.csv', 'products.csv', 'sales.csv', 'logs.csv']\n", + "for i in a:\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "id": "640bec7f", + "metadata": {}, + "source": [ + "- Задача 7. Проверка нужной колонки\n", + " \n", + " Создай список названий колонок таблицы.\n", + " \n", + " Проверь:\n", + " \n", + " - есть ли в списке `\"user_id\"`;\n", + " - есть ли в списке `\"created_at\"`.\n", + " \n", + " Выведи результат каждой проверки.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать оператор `in`;\n", + " - вывести `True` или `False`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6948dd3e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n", + "False\n", + "True\n" + ] + } + ], + "source": [ + "a = ['id', 'name', 'city', 'age', 'email', 'date_birthday']\n", + "print('user_id' in a)\n", + "print('created_at' in a)\n", + "print('id' in a)" + ] + }, + { + "cell_type": "markdown", + "id": "a061e5ee", + "metadata": {}, + "source": [ + "- Задача 8. Список файлов от пользователя\n", + " \n", + " Сделай программу, которая:\n", + " \n", + " 1. создает пустой список;\n", + " 2. 3 раза спрашивает у пользователя имя файла;\n", + " 3. добавляет каждый ответ в список;\n", + " 4. после этого выводит:\n", + " - весь список;\n", + " - количество элементов;\n", + " - первый элемент;\n", + " - последний элемент.\n", + " \n", + " **Требования:**\n", + " \n", + " - использовать список;\n", + " - использовать `append()`;\n", + " - использовать `input()`;\n", + " - использовать индексы и `len()`." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "8c7913d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['variables.py', 'list.py', 'str.py']\n", + "3\n", + "variables.py\n", + "str.py\n" + ] + } + ], + "source": [ + "a = []\n", + "for i in range(3):\n", + " b = str(input(f'введите названия файла №{i + 1}'))\n", + " a.append(b)\n", + "print(a)\n", + "print(len(a))\n", + "print(a[0])\n", + "print(a[-1])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/dmedvedev/underSelect and CTE.sql b/dmedvedev/underSelect and CTE.sql new file mode 100644 index 0000000..61bbca9 --- /dev/null +++ b/dmedvedev/underSelect and CTE.sql @@ -0,0 +1,173 @@ +--Задача 1. Пользователи с заказами +--task 1 + +SELECT + u.name +FROM users u +WHERE id IN ( + SELECT o.user_id + FROM orders o +) +ORDER BY u.name; + +/* +Чем такой подход отличается от варианта с JOIN + DISTINCT по users? +Принципиальная разница в том , что в этом варианте сначала выполняется подзапрос, +который выдает айди, потом уже выполняется внешний селект. И дубликаты не нужно удалять. + +В каком случае IN (подзапрос) может быть более читаемым, чем JOIN? +В том случае, если необходимо найти существование записи в таблице или же когда нужна только одна колонка из таблицы. +*/ + +--task 2 +--Задача 2. Пользователи без заказов (NOT EXISTS) +SELECT + u.id, + u.name, + u.email +FROM users u +WHERE NOT EXISTS ( + SELECT 1 + FROM orders o + WHERE u.id = o.user_id +); + +/* +Почему для этой задачи NOT EXISTS предпочтительнее, чем NOT IN (SELECT user_id ...)? +Потому что NOT EXISTS показывает факт существования запиши для определенного условия +и не перебирает все дублирующие строки, а использует только первую попавшуюся. + +Как бизнес может использовать список таких пользователей? +Так как эти пользователи не завершили процесс покупки, менеджерам необходимо будет поработать +с такими клиентами, предложить скидки, какие-то другие предложения. +*/ + +--task 3 +--Задача 3. Сколько заказов у каждого пользователя (подзапрос в SELECT) +SELECT + name, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id + ) AS orders_count +FROM users u +ORDER BY orders_count DESC, name DESC; + +/* +Почему подзапрос в SELECT не может возвращать несколько строк? +Потому что таково его свойство. Если же после селекта поставить наименование колонки + и если будет у пользователя несколько заказов, то выдаст ошибку, поэтому нужно либо + агрегировать, либо ограничить лимитом 1. + +Как изменится сложность запроса, если пользователей станет, например, 1 млн? +В таком случае SQL будет долго обрабатывать данные, потому что нужно будет обработать миллион строк в одном селекте. +В таких случаях лучше использовать групп бай. +*/ + +--task 4 +--Задача 4. Сумма выручки по каждому пользователю (подзапрос или CTE) +WITH revenie_user AS ( + SELECT + o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY o.user_id +) + +SELECT + u.name, + revenie_user.total_revenue +FROM users u +LEFT JOIN revenie_user ON u.id = revenie_user.user_id; + +/* +Как проще читать такой запрос: когда шаг «посчитать выручку по пользователям» вынесен отдельно, или когда всё написано в одном SELECT? +Конечно проще читать, когда делается через СТЕ. + +Если эту «выручку по пользователям» придётся использовать в другом отчёте, как избежать дублирования кода? +Может быть завернуть это еще в одну СТЕ? +*/ + +--task 5 +--Задача 5. Топ‑3 товара по выручке (CTE) +WITH product_revenue AS ( + SELECT + product_id, + SUM(quantity * price_at_purchase) AS revenue_pr + FROM order_items oi + GROUP BY product_id +) + +SELECT + p.name AS product_name, + p.category AS category, + product_revenue.revenue_pr AS product_revenue +FROM products p +LEFT JOIN product_revenue ON p.id = product_revenue.product_id +ORDER BY product_revenue DESC +LIMIT 3; + +/* +Почему удобно сначала считать агрегаты по товарам, а потом уже подтягивать имена/категории? +Потому что в категории может быть несколько разных товаров и если мы начнем агрегировать по категориям, +то можем потерять некоторые товары. + +В чём отличие подхода “через CTE” от простого GROUP BY в одном SELECT? +Отличие в том, что здесь мы выполняем два запроса, также СТЕ можно переиспользовать, так как это именованная +таблица, улучшается читаемость запроса. +*/ + +--task 6 +--Задача 6. Активные пользователи с высокой суммой заказов +WITH revenue AS( + SELECT + user_id, + SUM(quantity * price_at_purchase) AS total_revenue + FROM orders o + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY user_id +) + +SELECT + name, + email, + revenue.total_revenue +FROM users u +LEFT JOIN revenue ON u.id = revenue.user_id +WHERE is_active IS TRUE AND revenue.total_revenue > 1000; + +/* +Как бы ты назвал этот сегмент пользователей на языке продукта (VIP, high‑spenders и т.п.)? +High-revenue active clients + +Что можно делать с таким сегментом: какие типы акций/коммуникаций? +Можно предлагать для них какие-либо скидки на другие товары, потому что сразу становится понятно, +что они заинтересованы в нащих продуктах. +*/ + +--task 7 +--Задача 7. «Подозрительно маленькие» оплаченные заказы +WITH revenue AS( + SELECT + order_id, + SUM(quantity * price_at_purchase) AS order_revenue + FROM order_items oi + GROUP BY order_id +) + +SELECT + order_id, + order_date, + order_revenue +FROM orders o +LEFT JOIN revenue r ON r.order_id = o.id +WHERE status = 'paid' AND order_revenue < 100; + +/* +Какие реальные причины могут быть у таких заказов (ошибки, тесты, злоупотребления промокодами)? +Все вышеперечисленные, а также мошенничество, взлом системы. + +На каком шаге выгоднее фильтровать по статусу paid: внутри подзапроса или снаружи, и почему? +Легче фильтровать внешне, потому что можно создать нескоько внешних запросов, изменив лишь фильтр. +*/ \ No newline at end of file diff --git a/dmedvedev/union.sql b/dmedvedev/union.sql new file mode 100644 index 0000000..927faaa --- /dev/null +++ b/dmedvedev/union.sql @@ -0,0 +1,65 @@ +--таск 1 + +SELECT id, name, 'active' AS user_status +FROM users +WHERE is_active IS TRUE + +UNION ALL + +SELECT id, name, 'inactive' AS user_status +FROM users +WHERE is_active IS FALSE + +ORDER BY name; + +/*1. Что будет, если вместо **`UNION ALL`** написать **`UNION`**? + - Попробуй и запиши, изменилось ли количество строк. + + В данном случае ничего не поменяется, но вообще если + бы были одинаковые строчки в двух запросах, то дубликаты бы удалились + +Зачем в результат добавлять колонку user_status, если флаг активности уже есть в таблице users? + +Чтобы уйти от булевые true и false и перейти к нормально-читаемым +*/ + +--таск 2 +SELECT id AS order_id, user_id, order_date, 'old' AS period +FROM orders o +WHERE order_date < '2024-01-01' + +UNION ALL + +SELECT id AS order_id, user_id, order_date, 'new' AS period +FROM orders o +WHERE order_date >= '2024-01-01' + +ORDER BY order_date, order_id; + +/*Как ты по колонке period поймёшь, сколько заказов было до и после даты‑границы (словами, без SQL)? + +*/ + +--task 3 + +SELECT DISTINCT oi.product_id, p.name AS product_name, 'ordered' product_status +FROM products p +JOIN order_items oi ON oi.product_id = p.id + +UNION ALL + +SELECT oi.product_id, p.name AS product_name, 'never_ordered' product_status +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id + +WHERE oi.product_id IS NULL + +ORDER BY product_name + +/* +1. Почему в запросе на «не покупали» проверка IS NULL должна быть по полю из order_items, а не из products? +Потому что именно в order_items фиксируются заказанные позиции, а в products просто наименования товаров и не важно кто-то их покупал или нет. + +2.Какой сигнал для бизнеса даёт список с product_status = 'never_ordered' (1–2 идеи: что можно с этими товарами сделать)? +Что эти товары себя никаким образом не окупают, возмонжо их вообще стоит убрать из ассортимента. +*/ \ No newline at end of file diff --git a/dmedvedev/views.sql b/dmedvedev/views.sql new file mode 100644 index 0000000..30e2a34 --- /dev/null +++ b/dmedvedev/views.sql @@ -0,0 +1,79 @@ +--task 1 +--Задание 1. Витрина выручки по категориям +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM products p +JOIN order_items oi + ON oi.product_id = p.id +JOIN orders o + ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY p.category +ORDER BY total_revenue DESC; +/*Чем удобно иметь готовую витрину по категориям, вместо того чтобы каждый раз писать JOIN по трём таблицам? +Тем что не нужно постоянно писать запросы, которые используешь часто, достаточно один раз написать верный view и не париться, больше останется времени на остальную работу. +*/ + +--task 2 +--Задание 2. Витрина активности пользователей +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + o.user_id AS user_id, + u.name AS user_name, + COUNT(*) AS orders_count +FROM users u +JOIN orders o + ON o.user_id = u.id +WHERE o.status = 'paid' +GROUP BY o.user_id, u.name +ORDER BY orders_count DESC; + +/*Как по такой витрине найти «самых лояльных» клиентов? +Выбрать тех у кого оплаченных заказов наибольшее количество + +Что будет, если считать активность не по всем заказам, а только за последние 30 дней — как изменится смысл витрины? +В таком случае мы будем видеть самых горячих клиентов, возможно, готовых совершить следующий заказ, но с другой стороны здесь можно и ошибаться. +*/ + +--task 3 +--Задание 3. Витрина выручки по дням +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + order_date::date AS sales_date, + SUM(quantity * price_at_purchase) AS daily_revenue +FROM orders o +JOIN order_items oi + ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.order_date::date +ORDER BY sales_date; + +/* +Как по такой витрине заметить «просадки» и «пики» продаж? +видно будет в какой день какая выручка. + +Что быстрее: считать выручку «на лету» из сырых таблиц или читать из mv_daily_revenue? Почему? +конечно же из mv_daily_revenue, потому что это таблица , которая уже создана, это быстрее, но минус в том, что она не обновляется, пока не сделаешь refresh самостоятельно. +*/ + +--task 4 +--Задание 4. Обновление витрин + +REFRESH MATERIALIZED VIEW mv_category_revenue; + +/*Для интернет‑магазина с несколькими заказами в день я бы внедрил автоматическое обновление как можно чаще, +например, ежечасно, возможно получится обнаружить какие-то определенные паттерны у пользователей, чтобы +попробовать увеличить количество заказов будущем. + +А для крупного магазина с тысячами заказов в час нужно внедрять систему с редкими обновлениями. +Во-первых частые обновления не смогут принести какую-то полезную информацию, а во-вторых это большая нагрузка на систему. +Для такого бизнеса важно сводить кассу к концу дня. И в конце, например, месяца формировать отчеты, когда и что больше всего покупалось. + +Чем грозит слишком редкий REFRESH для отчётов (какие ошибки могут появиться в анализе)? +Слишком редкое обновление грозит тем, что не будут учтены свежие данные, соответственно выборка может быть неактуальной. + +Почему нельзя обновлять витрины «каждую секунду», даже если хочется всегда свежие данные? +Потому что это дополнительная нагрузка на систему, на базу данных. +*/ \ No newline at end of file diff --git a/dmedvedev/window functions.sql b/dmedvedev/window functions.sql new file mode 100644 index 0000000..ad78441 --- /dev/null +++ b/dmedvedev/window functions.sql @@ -0,0 +1,187 @@ +--task 1 +--Задача 1. Нумерация заказов пользователя + +SELECT + o.user_id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date AS order_date, + ROW_NUMBER() OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS order_number_for_user +FROM orders o +JOIN users u ON o.user_id = u.id; + +/* +В чём отличие такой нумерации от простой сортировки ORDER BY order_date без оконной функции? + +Здесь функция работает внутри определенных строк, в данном случае имени. +*/ + + +--task 2 +--Задача 2. Накопительная выручка по пользователю +SELECT + o.user_id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + oi.quantity * oi.price_at_purchase AS order_revenue, + SUM(oi.quantity * oi.price_at_purchase) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_revenue_for_user +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +JOIN users u ON u.id = o.user_id; + + +/* +Почему для накопительной суммы важно задавать рамку окна (от «самого первого» до «текущего»), а не оставлять значения по умолчанию? +Если не задавать рамку окна, то не будет накопительного эффекта, будет просто считаться сумма по строке. И то на самом деле я до конца не понял как она считается, как-будто сумма заказа транслируется на строку с частью заказа. + +Как по такой таблице понять, что пользователь начал тратить больше? +Можно дополнительно сделать столбец разности текущего значения от предыдущего и будет видно разницу или же сделать столбец по отклонению от среднего. Правильно мыслю? +*/ + +--task 3 +--Задача 3. Рейтинг пользователей по выручке +WITH total AS ( + SELECT + o.user_id, + SUM(quantity * price_at_purchase) AS total_revenue + FROM order_items oi + JOIN orders o ON o.id = oi.order_id + GROUP BY o.user_id +) + +SELECT + u.name AS user_name, + total_revenue, + RANK() OVER ( + ORDER BY total_revenue + ) AS revenue_rank, + DENSE_RANK() OVER ( + ORDER BY total_revenue + ) AS dense_revenue_rank +FROM users u +JOIN total t ON u.id = t.user_id; +/* +Чем RANK отличается от DENSE_RANK в случае, когда у двух пользователей одинаковая выручка? + +При RANK будет одно одинаковое место, при dense будут разные ранги. +*/ + +--task 4 +--Задача 4. Первый и последний заказ пользователя +--task 4 +--Задача 4. Первый и последний заказ пользователя + +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date AS order_date, + first_value(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS first_order_date, + last_value(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM orders o +JOIN users u ON u.id = o.user_id; + +/* +Как можно использовать поля «первый заказ» и «последний заказ» в сегментации клиентов? +Можно понять единый ли это заказ или нет. Если единый, то соответственно, других заказов не было и сравнить с текущей датой и понять давно ли вообще была активность. + +Почему для правильного LAST_VALUE важно задать рамку окна так, чтобы смотреть на всю историю пользователя, а не только до текущей строки? +Потому что если не задавать рамку, то функция выдаст предыдущее значение от текущей строки. +*/ + +/* + +*/ + +--task 5 +--Задача 5. Заказы и количество заказов пользователя (без GROUP BY) +SELECT + u.name AS user_name, + o.id AS order_id, + count(*) OVER ( + PARTITION BY u.name + ) AS orders_per_user +FROM users u +JOIN orders o ON o.user_id = u.id; + +/* +Чем такой результат отличается от классического отчёта «по пользователям» с GROUP BY user_id? +GROUP BY уменьшил бы количество строк. +*/ + +--task 6 +--Задача 6. Сравнение суммы заказа с предыдущим +WITH revenue AS ( + SELECT + o.id AS order_id, + o.user_id AS user_id, + o.order_date AS order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.id, o.user_id, o.order_date +) + +SELECT + u.name, + order_id, + order_date, + order_revenue, + LAG(order_revenue) OVER (PARTITION BY user_id ORDER BY order_date) AS prev_order_revenue +FROM revenue r +JOIN users u ON r.user_id = u.id; + +/* +Как по такому результату понять, увеличивается или уменьшается средний чек с течением времени? +Можно дополнительно создать еще один столбец в котором будем получать разность текущего и предыдущего заказов, а на основе этого уже делать выводы. +А по такому запросы - думаю наглядно видно в таблице какая сумма больше, а какая меньше. +*/ + +--task 7 +--Задача 7. Категории с накопительной выручкой по времени +WITH category_date_revenue AS ( + SELECT + p.category, + o.order_date::date AS order_date, + SUM(oi.price_at_purchase * oi.quantity) AS daily_category_revenue + FROM products p + JOIN order_items oi + ON p.id = oi.product_id + JOIN orders o + ON oi.order_id = o.id + + GROUP BY + p.category, + o.order_date::date +) + +SELECT + category AS category, + order_date::date AS order_date, + daily_category_revenue, + SUM(daily_category_revenue) OVER ( + PARTITION BY category + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_category_revenue +FROM category_date_revenue; + + +/* +Чем такой «накопительный» график по категории отличается по смыслу от обычного ежедневного (без SUM OVER)? +Видим какая именно категория в какой именно день принесла больше выручки +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/CTE.sql b/dmezentsev/postgresql/homework/CTE.sql new file mode 100644 index 0000000..8c99b87 --- /dev/null +++ b/dmezentsev/postgresql/homework/CTE.sql @@ -0,0 +1,195 @@ +-- Задача 1. Пользователи с заказами +SELECT id AS user_id +FROM dmezentsev.users +WHERE id IN (SELECT user_id + FROM dmezentsev.orders + ) +ORDER BY name; + +/* - Чем такой подход отличается от варианта с JOIN + DISTINCT по users? + + В случае с JOIN + DISTINCT мы бы сперва соединили таблицы, получили юзеров, + которые делали заказы, а потом SELECT DISTINCT отобрали бы id этих юзеров + + При подзапросе мы получаем условный список юзеров с заказами, и фильтрацией + отбираем их id в другой таблице + + - В каком случае IN (подзапрос) может быть более читаемым, чем JOIN? + +В случае, когда нужны поля первой таблицы, а подзапрос только как фильтр. +Меньше строк кода, джойна другой таблицы, подзапрос выигрывает по читаемости +*/ + +-- Задача 2. Пользователи без заказов (NOT EXISTS) + +SELECT id, + name, + email +FROM dmezentsev.users u +WHERE NOT EXISTS (SELECT 1 + FROM dmezentsev.orders o + WHERE o.user_id = u.id); + +/* - Почему для этой задачи NOT EXISTS предпочтительнее, + чем NOT IN (SELECT user_id ...)? + + Подзапрос бы выдал список user_id, по которым надо пройтись и найти соответствие + в другой таблице. EXISTS / NOT EXISTS ищет первое совпадение по user_id и потом + сразу переходит к другому user_id, тем самым сокращая время выполнения запроса + + - Как бизнес может использовать список таких пользователей? + + Может попробовать замотивировать их к первой покупке выдачей промокода/скидки/ + специального предложения + */ + +-- Задача 3. Сколько заказов у каждого пользователя (подзапрос в SELECT) + +SELECT u.name, + (SELECT count(*) + FROM dmezentsev.orders o + WHERE o.user_id = u.id) AS orders_count +FROM dmezentsev.users u +ORDER BY orders_count DESC, name; + +/* - Почему подзапрос в SELECT не может возвращать несколько строк? + +Потому что для каждого u.name он возвращает только одну строку - число заказов. +Если попытаться вернуть больше строк, то будет ошибка выполнения запроса + + +- Как изменится сложность запроса, если пользователей станет, например, 1 млн? + +Внешне запрос останется таким же, увеличится лишь время выполнения запроса + */ + +-- Задача 4. Сумма выручки по каждому пользователю (подзапрос или CTE) + +SELECT u.name, + t.total_revenue +FROM (SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON oi.order_id = o.id + GROUP BY o.user_id) AS t +JOIN dmezentsev.users u ON t.user_id = u.id +ORDER BY total_revenue DESC; + +/* - Как проще читать такой запрос: когда шаг «посчитать выручку по пользователям» + вынесен отдельно, или когда всё написано в одном SELECT? + + Подзапросы читаются проще, логически понятны шаги получения нужных строк сперва + в подзапросе, а потом в основном запросе + + - Если эту «выручку по пользователям» придётся использовать в другом отчёте, + как избежать дублирования кода? + + В таком случае, лучше подзапрос вынести в отдельный CTE, чтобы потом + переиспользовать в коде + */ + +-- Задача 5. Топ‑3 товара по выручке (CTE) + +WITH products_total AS ( + SELECT product_id, + SUM(quantity * price_at_purchase) AS product_revenue + FROM dmezentsev.order_items + GROUP BY product_id +) + +SELECT p.name, + p.category, + pt.product_revenue +FROM dmezentsev.products p +JOIN products_total pt ON p.id = pt.product_id +ORDER BY product_revenue DESC +LIMIT 3; + +/* - Почему удобно сначала считать агрегаты по товарам, + а потом уже подтягивать имена/категории? + + Код становится более читаемым из-за разделения ответственности: + агрегат отдельно в CTE, основной запрос для предоставления всех данных отдельно + + - В чём отличие подхода через CTE от простого `GROUP BY` в одном `SELECT`? + + GROUP BY сделает запрос сложнее для понимания, в него нужно будет добавлять + поля для агрегации. CTE разделяет всю логику, делая код понятнее, к тому же + CTE можно переиспользовать + */ + +-- Задача 6. Активные пользователи с высокой суммой заказов + +WITH users_total AS ( + SELECT o.user_id, + SUM(quantity * price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY o.user_id +) + +SELECT u.name, + u.email, + ut.total_revenue +FROM dmezentsev.users u +JOIN users_total ut ON u.id = ut.user_id +WHERE u.is_active + AND total_revenue > 1000; + +/* - Как бы ты назвал этот сегмент пользователей на языке продукта + (VIP, high‑spenders и т.п.)? + + Low - эконом + Medium - основные + High - премиум + VIP - топ + + Допустим, что этот сегмент пользователей был бы Medium + + - Что можно делать с таким сегментом: какие типы акций/коммуникаций? + + Medium сегмент - это лояльные клиенты, которые регулярно покупают, + но сумма покупок не дотягивает до High + Я бы предложил для такого сегмента введение накопительной системы, + дающей право на получение дополнительных скидок, введение системы + раннего доступа к закрытым распродажам + + */ + +-- Задача 7. «Подозрительно маленькие» оплаченные заказы + +WITH orders_total AS ( + SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.order_items oi + INNER JOIN dmezentsev.orders o ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY oi.order_id +) +SELECT ot.order_id, + o.order_date, + ot.order_revenue +FROM orders_total ot +JOIN dmezentsev.orders o ON ot.order_id = o.id +WHERE ot.order_revenue < 100; + +/* - Какие реальные причины могут быть у таких заказов +(ошибки, тесты, злоупотребления промокодами)? + +Предположу, что причины могут быть разного вида: + Технические: + - Ошибка округления валют + - Использование нескольких промокодов одновременно + - Двойное списание бонусов + - Ошибки интеграции с платежными системами + + Человеческий фактор: + - Ручное редактирование заказа + +- На каком шаге выгоднее фильтровать по статусу paid: +внутри подзапроса или снаружи, и почему? + +Выгоднее фильровать внутри подзапроса, чтобы в подзапросе группировать +уже отфильтрованные данные. И уже в основном запросе джойнить, а меньше +строк для джойна ускорят запрос + */ diff --git a/dmezentsev/postgresql/homework/DDL_DML.sql b/dmezentsev/postgresql/homework/DDL_DML.sql new file mode 100644 index 0000000..9780290 --- /dev/null +++ b/dmezentsev/postgresql/homework/DDL_DML.sql @@ -0,0 +1,86 @@ +CREATE TABLE dmezentsev.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 dmezentsev.products ( +id BIGINT PRIMARY KEY, +name VARCHAR(100) NOT NULL, +category VARCHAR(50), +price DECIMAL(10,2) NOT NULL, -- Сделал тип данных для денежных операций +in_stock BOOLEAN +); + +CREATE TABLE dmezentsev.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 dmezentsev.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 dmezentsev.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 dmezentsev.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 dmezentsev.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 dmezentsev.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/Explain.md b/dmezentsev/postgresql/homework/Explain.md new file mode 100755 index 0000000..bbd5452 --- /dev/null +++ b/dmezentsev/postgresql/homework/Explain.md @@ -0,0 +1,375 @@ +### Задача 1 — Читаем план + +#### Требования: + +Выполни этот запрос: + +```sql +EXPLAIN +SELECT name, email +FROM users +WHERE id = 1; +``` + +**1. Какой тип сканирования использует база?** + +Index Scan (индексное сканирование) + +**2. Что означает строка `Index Cond: (id = 1)`?** + +Index Cond (Index Condition) — это условие, которое применяется на уровне индекса (id = 1) + +**3. Почему база использует индекс, а не читает всю таблицу?** + +База выбирает индекс вместо полного сканирования таблицы, потому что id — это первичный ключ с индексом, что делает Index Scan значительно быстрее и дешевле, чем Seq Scan + +### Задача 2 — Seq Scan vs Index Scan + +#### Требования: + +Выполни оба запроса с EXPLAIN: + +```sql +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +``` + +**1. Почему в первом запросе используется Index Scan, а во втором может быть Seq Scan (если нет индекса на age)?** + +- Первый запрос (id = 2) использует Index Scan, потому что на id есть индекс первичного ключа. +- Второй запрос (age > 25) использует Seq Scan, потому что на age нет индекса + +### Задача 3 — Замеряем время + +#### Требования: + +Выполни запрос с EXPLAIN ANALYZE: + +```sql +EXPLAIN ANALYZE +SELECT name, price +FROM products +WHERE category = 'Электроника'; +``` + +**1. Запиши Execution Time** + +Execution Time: 0.699 ms + +**2. Определи тип сканирования (Seq Scan или Index Scan)** + +Seq Scan + +**3. Сколько строк вернул запрос?** + +rows=3 + +### Задача 4 — До и после индекса + +#### Требования: + +**Шаг 1:** Выполни запрос без индекса: + +```sql +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'Россия'; +``` + +Запиши **Execution Time** и тип сканирования. + +Execution Time: 76.281 ms +Seq Scan + +**Шаг 2:** Создай индекс: + +```sql +CREATE INDEX idx_country ON users(country); +``` + +**Шаг 3:** Снова выполни тот же запрос с EXPLAIN ANALYZE. + +**1. Изменился ли тип сканирования?** + +Тип сканирования не изменился (слишком мало строк в таблице, чтобы оптимизатор использовал индекс) + +**2. Стал ли запрос быстрее? На сколько?** + +Execution Time: 0.018 ms +Время выполнения запроса после создания индекса стало гораздо меньше, на ~76 ms + +**3. Почему индекс помог?** + +Индекс поможет, потому что: + +- Уменьшит количество читаемых данных - читать только страницы с нужными значениями +- Использует структуру B-Tree +- Позволяет найти строки без сканирования всей таблицы + +### Задача 5 — Анализируем JOIN + +#### Требования: выполни запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия'; +``` + +**1. Какой тип JOIN использует база (Hash Join, Nested Loop, Merge Join)?** + +Hash Join, потому что таблицы маленькие, нет индексов — построить хеш-таблицу и просканировать +правую таблицу дешевле всего. + +**2. С какой таблицы база начала чтение?** + +orders + +**3. Сколько времени занял весь запрос?** + +Execution Time: 0.083 ms + +### Задача 6 — Почему медленно? + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE LOWER(email) = 'alexey@mail.ru'; +``` + +**1. Выполни запрос и посмотри на план** + +Seq Scan on users (cost=0.00..1.15 rows=1 width=467) (actual time=0.012..0.012 rows=0 loops=1) + Filter: (lower((email)::text) = 'alexey@mail.ru'::text) + Rows Removed by Filter: 10 +Planning Time: 0.429 ms +Execution Time: 0.026 ms + +**2. Использует ли база индекс?** + +План показывает Seq Scan, а не Index Scan. + +**3. Почему НЕТ? (даже если индекс на email есть)** + +Применение функции LOWER() к колонке + +**4. Как исправить запрос, чтобы индекс заработал?** + +- Создать индекс с учетом применения функции LOWER() + +```sql +CREATE INDEX idx_users_email_lower ON users (LOWER(email)); +``` +- Использовать ILIKE +- Хранить нормализованный email + +### Задача 7 — Функции убивают индексы + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее: 0.016 ms против 0.043 ms + +**2. Почему первый запрос НЕ может использовать индекс на `age`?** + +Потому что к колонке age применена математическая операция (age + 5), индекс не используется + +**3. Как правильно переписать запрос А?** + +```sql +SELECT * FROM users WHERE age > 25; +``` + +### Задача 8 — Диагностика медленного запроса + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +``` + +**1. Почему функция `EXTRACT()` — плохая идея?** + +Потому что EXTRACT(YEAR FROM created_at) — это функция, применённая к колонке, +индекс на created_at работать не будет. + +**2. Как переписать условие WHERE без функции?** + +```sql +WHERE u.created_at >= '2026-01-01' AND u.created_at < '2027-01-01' +``` + +**3. Будет ли использоваться индекс на `created_at` в текущем виде?** + +Индекс на created_at НЕ БУДЕТ использоваться + +### Задача 9 — SELECT * против конкретных колонок + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее (0.017 ms против 0.051 ms) + +**2. Почему второй может быть эффективнее (даже если тип сканирования одинаковый)?** + +Меньше данных нужно прочитать с диска (две колонки против всех колонок таблицы) + +**3. В каких ситуациях разница будет особенно заметна?** + +- Большое количество строк +- Медленные диски (HDD) +- Высокая нагрузка (тысячи запросов в секунду) + +### Задача 10 — Оптимизируем JOIN с условием + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' AND p.category = 'Электроника'; +``` + +**1. Выполни запрос и посмотри на план** + +Nested Loop (cost=12.66..41.04 rows=1 width=440) (actual time=34.552..34.556 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + Rows Removed by Join Filter: 5 + -> Seq Scan on users u (cost=0.00..1.12 rows=1 width=226) (actual time=0.012..0.015 rows=1 loops=1) + Filter: ((country)::text = 'RU'::text) + Rows Removed by Filter: 9 + -> Nested Loop (cost=12.66..39.84 rows=6 width=230) (actual time=34.510..34.536 rows=5 loops=1) + -> Hash Join (cost=12.51..38.61 rows=6 width=230) (actual time=34.094..34.105 rows=5 loops=1) + Hash Cond: (oi.product_id = p.id) + -> Seq Scan on order_items oi (cost=0.00..22.70 rows=1270 width=20) (actual time=33.943..33.945 rows=10 loops=1) + -> Hash (cost=12.50..12.50 rows=1 width=226) (actual time=0.047..0.048 rows=3 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 9kB + -> Seq Scan on products p (cost=0.00..12.50 rows=1 width=226) (actual time=0.022..0.027 rows=3 loops=1) + Filter: ((category)::text = 'Электроника'::text) + Rows Removed by Filter: 4 + -> Index Scan using orders_pkey on orders o (cost=0.15..0.21 rows=1 width=16) (actual time=0.084..0.084 rows=1 loops=5) + Index Cond: (id = oi.order_id) +Planning Time: 0.278 ms +Execution Time: 34.691 ms + +**2. В каком порядке база читает таблицы?** + +Порядок чтения таблиц: +1. users +2. products +3. order_items +4. Hash Join +5. orders +6. Nested Loop. + +**3. Сколько строк проходит через каждый JOIN?** + +1. users (1) +2. products (3) +3. order_items (10) +4. Hash Join (5) +5. orders (5) +6. Nested Loop (0) + +**4. Можно ли оптимизировать? (Подсказка: подумай про индексы на `country` и `category`)** + +- Создать индексы: idx_users_country, idx_products_category +- Обновить статистику: ANALYZE всех таблиц + +### Задача 11 — Найди и исправь + +#### Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +``` + +**1. Выполни этот запрос с EXPLAIN ANALYZE** + +Nested Loop (cost=0.00..22.35 rows=1 width=549) (actual time=0.034..0.035 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + -> Seq Scan on users u (cost=0.00..1.20 rows=1 width=467) (actual time=0.033..0.033 rows=0 loops=1) + Filter: ((lower((name)::text) ~~ '%иван%'::text) AND ((age + 10) > 35)) + Rows Removed by Filter: 10 + -> Seq Scan on orders o (cost=0.00..21.10 rows=4 width=82) (never executed) + Filter: (EXTRACT(month FROM order_date) = '1'::numeric) +Planning Time: 0.235 ms +Execution Time: 0.045 ms + +**2. Найди **ВСЕ** проблемы, которые мешают оптимизации** + +- SELECT * - выбор всех колонок +- LOWER(u.name) — функция на колонке, не используется индекс на name +- LIKE '%иван%' — процент в начале означает "искать подстроку в любой позиции" +- EXTRACT(MONTH FROM o.order_date) = 1 — функция на колонке +- u.age + 10 > 35 — расчеты на колонке + +**3. Перепиши запрос правильно** + +```sql +SELECT u.id, u.name, u.age, u.email, u.country, o.id AS order_id, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name ILIKE '%иван%' + AND o.order_date >= '2025-01-01' + AND o.order_date < '2025-02-01' + AND u.age > 25; +``` + +**4. Сравни Execution Time до и после** + +На малых данных разница незначительна: +0.036 ms против 0.045ms + +**5. Объясни, почему твой вариант лучше** + +- Перечисление нужных полей в SELECT +- Регистронезависимая фильтрация ILIKE +- Убраны функции и расчеты по колонкам, не позволяющие использовать индексы + +Все это в совокупности на больших таблицах ускорит время выполнения запроса 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..2e54c35 --- /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 days_since_registration 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 domain, 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..4e33a2b --- /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 products_count 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 order_revenue 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 category_revenue 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 order_revenue 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 category_revenue 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 active_users_count 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 total_quantity_sold 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 order_revenue; + +/* - Какие реальные причины могут быть у очень маленьких заказов (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..5cf57cc --- /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 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; + +/* - почему после 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/Locking.md b/dmezentsev/postgresql/homework/Locking.md new file mode 100755 index 0000000..88d921c --- /dev/null +++ b/dmezentsev/postgresql/homework/Locking.md @@ -0,0 +1,88 @@ +### Задание 1. Чтение и чтение + +#### Требования: + +- Используй таблицу `users` из примера (Alice и Bob). +- Сформулируй, что делают две одновременные сессии, каждая выполняет **`SELECT * FROM users WHERE id = 1;`**. + +Обе сессии читают данные пользователя Alice (id = 1) из таблицы users. Поскольку это обычные запросы на чтение, они не блокируют друг друга. + +**1. Почему оба запроса могут выполниться сразу, без ожидания?** + +Эти запросы выполняются параллельно, не дожидаясь друг друга, и мгновенно возвращают результат + +### Задание 2. Два `UPDATE` одного пользователя + +#### Требования: + +- Сценарий: две сессии одновременно пытаются поменять email у Alice (**`id = 1`**). +- Одна меняет на **`alice.new@example.com`**, другая — на **`alice.marketing@example.com`**. +- Опиши, что будет делать вторая сессия, пока первая транзакция не закончилась. + +1. Сессия 1 начинает транзакцию и выполняет UPDATE. PostgreSQL захватывает блокировку строки для id = 1. +2. Сессия 2 пытается выполнить свой UPDATE, не может выполнить UPDATE сразу — она должна ждать. +3. Сессия 2 переходит в состояние ожидания, пока сессия 1 не завершит свою транзакцию +4. Сессия 1 завершает транзакцию +5. Сессия 2 выполняет свой UPDATE уже поверх нового состояния строки. + +**1. Почему нельзя разрешить обоим UPDATE пройти одновременно?** + +Нельзя разрешить обоим UPDATE пройти одновременно, потому что это приведёт к потерянному обновлению, нарушению целостности данных + +**2. Какой email в итоге останется у Alice и почему порядок COMMIT важен?** + +Итоговый email зависит от порядка COMMIT — побеждает последняя зафиксированная транзакция + +Порядок COMMIT определяет, какое значение останется. + +### Задание 3. `SELECT ... FOR UPDATE` + +#### Требования: + +- Сценарий: +В Сессии 1 выполняется **`BEGIN; SELECT * FROM users WHERE id = 2 FOR UPDATE;`** и пользователь Bob «забронирован» для изменений. +- Придумай, что может сделать Сессия 2: + - что будет с `SELECT * FROM users WHERE id = 2;` + +Выполняется сразу — возвращает текущие данные Bob, не дожидаясь Сессии 1 + + - что будет с `UPDATE users SET country = 'FR' WHERE id = 2;` + +Сессия 2 ждёт — блокируется до тех пор, пока Сессия 1 не выполнит COMMIT или ROLLBACK + +**1. Почему **`SELECT`** без `FOR UPDATE` не блокируется, а `UPDATE` ждёт?** + +Чтение (без FOR UPDATE) — не изменяет данные + +**2. В каких задачах интернет‑магазина `SELECT ... FOR UPDATE` помогает избежать ошибок?** + +- Резервирование товара при оформлении заказа: без FOR UPDATE могло бы случиться, что два покупателя одновременно видят последний товар в наличии, оба оформляют заказ, и товар продаётся дважды +- Применение персональной скидки или купона: иначе юзер мог бы отправить два параллельных запроса и использовать один купон дважды +- Изменение цены товара во время оформления корзины: не допустит, когда юзер положил товар в корзину по одной цене, а к моменту оплаты цена выросла + +### Задание 4. Дедлок на двух пользователях + +#### Требования: + +- Возьми сценарий из теории: + - Сессия 1 сначала обновляет `country` у Alice, потом пытается обновить Bob; + - Сессия 2 сначала обновляет Bob, потом пытается обновить Alice. + +**1. Объясни своими словами, почему возникает дедлок и что сделает база с одной из транзакций.** + +Каждая транзакция ждёт ресурс, который удерживает другая транзакция. Ни одна из них не может продолжить выполнение. + +Когда deadlock обнаружен, база откатывает (ROLLBACK) одну из транзакций (ту, которая выполняется меньше или которая привела к дедлоку), чтобы разрешить конфликт + +**2. Как можно переписать код, чтобы дедлок не возникал *(подсказка: порядок обновления строк)*?** + +```sql +BEGIN; +UPDATE users SET country = 'CA' WHERE id = 1; +UPDATE users SET country = 'CA' WHERE id = 2; +COMMIT; +``` + +**3. Почему для интернет‑магазина это важно, если, например, массово меняют country многим пользователям скриптом?** + +Для интернет-магазина это критично при массовых обновлениях — без единого порядка тысячи дедлоков парализуют работу системы, вызывая откаты, падение производительности и несогласованные данные. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Logic_physic_joins.md b/dmezentsev/postgresql/homework/Logic_physic_joins.md new file mode 100755 index 0000000..2ccf34f --- /dev/null +++ b/dmezentsev/postgresql/homework/Logic_physic_joins.md @@ -0,0 +1,307 @@ +### Задача 1 — Когда какой метод? + +| **Ситуация** | **Какой JOIN выберет база?** | **Почему?** | +| --- | --- | --- | +| 10 пользователей, 1 млн заказов, есть индекс на orders.user_id | Nested Loop | Маленькая левая таблица (10 строк) + индекс на правой | +| 100 000 пользователей, 100 000 заказов, нет индексов | Hash Join | Обе таблицы средние, нет индексов | +| Обе таблицы отсортированы по ключу, есть индексы | Merge Join | Данные уже отсортированы — можно пройтись за один проход | +| Маленькая таблица (5 строк) соединяется с большой (1 млн строк), нет индекса | Hash Join | Hash Join просканирует большую таблицу 1 раз | + +### Задача 2 — Размер таблиц имеет значение + +#### Требования: + +Создай тестовые данные: + +```sql +-- Маленькая таблица (10 строк) +CREATE TABLE small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; +``` + +Теперь выполни: + +```sql +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +``` + +**1. Какой метод `JOIN` используется в каждом случае?** + +- Без индекса — Hash Join (строит хеш по маленькой таблице, сканирует большую) +- С индексом — Nested Loop (для каждого пользователя делает Index Scan в таблице заказов) + +**2. Почему с индексом база может выбрать `Nested Loop`?** + +Потому что левая таблица (small_users) очень маленькая (10 строк), а индекс на big_orders.user_id позволяет мгновенно находить заказы каждого пользователя + +**3. Насколько быстрее стало с индексом?** + +С индексом в 10-20 раз быстрее (~2.5 мс против ~45 мс). Index Scan читает только нужные строки (не все 100 000 заказов) + +### Задача 3 — Влияние сортировки + +#### Требования: + +Выполни три варианта: + +```sql +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +``` + +**1. Изменился ли тип `JOIN` во втором варианте?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — во всех вариантах используется Hash Join + +**2. Появился ли `Merge Join`?** + +Merge Join НЕ ПОЯВИЛСЯ — для Merge Join нужен индекс на orders.user_id, чтобы заказы были отсортированы по ключу соединения. + +**3. Почему `ORDER BY` может повлиять на выбор `JOIN`?** + +ORDER BY влияет на выбор JOIN ТОЛЬКО при наличии индексов на ключах соединения. Если бы индекс на orders.user_id существовал, ORDER BY по ключу соединения привёл бы к появлению Merge Join, который использует отсортированные данные + +### Задача 4 — Найди узкое место + +#### Требования: + +Дан медленный запрос: + +```sql +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. Выполни с `EXPLAIN ANALYZE`** + +**2. Посмотри, какие типы `JOIN` используются** + +Все JOIN — Hash Join + +**3. Есть ли `Seq Scan`? Если да — где?** + +Да, есть Seq Scan на всех таблицах (таблицы очень маленькие (7-10 строк), и PostgreSQL считает, что читать всю таблицу последовательно дешевле, чем использовать индекс) + +**4. Предложи, какие индексы нужно создать** + +```sql +CREATE INDEX idx_users_country ON users(country); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_order_items_order_id ON order_items(order_id); +CREATE INDEX idx_order_items_product_id ON order_items(product_id); +``` + +- для фильтрации по стране +- для ускорения поиска заказов конкретного пользователя +- для ускорения поиска позиций заказа +- для ускорения поиска товара в позициях + +**5. Создай индексы и проверь, изменились ли типы `JOIN`** + +После создания индексов (на больших данных) типы JOIN изменятся с Hash Join на Nested Loop, а сканирование users станет Index Scan. + +### Задача 5 — Многоступенчатый JOIN + +```sql +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'Россия' +GROUP BY u.name; +``` + +**1. Посмотри на план выполнения** + +**2. Какие методы `JOIN` используются на каждом уровне?** + +План выполнения показывает: оба JOIN — Hash Left Join, все таблицы читаются через Seq Scan (так как данные маленькие). + +**3. Почему для первого `JOIN` один метод, а для второго — другой?** + +Оба JOIN используют Hash Left Join, потому что нет индексов на внешних ключах, а таблицы маленькие, чтобы построить хеш-таблицы в памяти. + +**4. Сколько строк проходит через каждый `JOIN`?** + +1. После фильтра country = 'RU': users — 1 строка +2. После первого LEFT JOIN (users и orders) — 1 строка +3. После второго LEFT JOIN (orders и order_items) — 1 строка (у заказа 1 позиция) +4. Результат: 1 строка с агрегатами (orders_count=1, products_count=1) + +### Задача 6 — `LEFT JOIN` vs `INNER JOIN` + +#### Требования: + +Сравни два запроса: + +```sql +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +``` + +**1. Одинаковый ли тип физического JOIN используется?** + +Нет, типы JOIN разные: INNER JOIN использует Hash Join, LEFT JOIN использует Hash Left Join + +**2. Есть ли разница в Execution Time?** + +Небольшая, LEFT JOIN медленнее на ~0.003 мс + +**3. Почему LEFT JOIN может быть медленнее?** + +- Должен сохранять ВСЕ строки из левой таблицы (даже без соответствия) и добавлять NULL +- Возвращает больше строк + +### Задача 7 — Порядок таблиц в JOIN + +#### Требования: + +Сравни: + +```sql +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +``` + +**1. Изменился ли тип `JOIN`?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — в обоих случаях используется Hash Join + +**2. Изменился ли порядок чтения таблиц в плане?** + +Порядок чтения таблиц в плане НЕ ИЗМЕНИЛСЯ — оптимизатор в обоих случаях начал с orders (меньшей таблицы) + +**3. Одинаковое ли `Execution Time`?** + +Execution Time ОДИНАКОВОЕ, потому что планы идентичны + +**4. Почему оптимизатор может выбрать разный порядок?** + +Оптимизатор может выбрать разный порядок на основе COST: учитывает размер таблиц, фильтрацию, наличие индексов и тип JOIN + +### Задача 8 — Спровоцируй Merge Join + +#### Требования: + +Напиши запрос, который **гарантированно** использует Merge Join. + +**Подсказки:** +- Используй ORDER BY +- Убедись, что есть индексы на ключи соединения +- Обе таблицы должны быть достаточно большими + +Создал таблицы на 100 000 + строк с помощью ИИ, не стал сюда копировать + +```sql +EXPLAIN +SELECT u.id, u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; +``` + +**Фишка в том, что Merge Join будет использоваться, если:** + +- Индекс на users.id (первичный ключ — есть) +- Индекс на orders.user_id (нужно создать) +- ORDER BY u.id, o.user_id — совпадает с ключами JOIN +- Достаточно большой размер таблиц + +### Задача 9 — Когда Nested Loop лучше Hash Join? + +#### Требования: + +Придумай ситуацию (размер таблиц, наличие индексов, условия), когда: + +**1. Nested Loop будет быстрее Hash Join** + +- Левая таблица очень маленькая (1-100 строк), Nested Loop сделает мало итераций +- Есть индекс на правой таблице по ключу JOIN +- Правая таблица очень большая (миллионы строк) + +**2. Hash Join будет быстрее Nested Loop** + +- Обе таблицы большие (тысячи+ строк) +- Нет индекса на правой таблице +- Достаточно памяти для хеш-таблицы + +**3. Создай тестовые данные и проверь свою гипотезу!** + +Создавал с помощью ИИ таблицы и принудительно отключал у оптимизатора варианты физического джойна (то есть сперва обе таблицы через Nested Loop, потом через Hash Join, затем так же другие две таблицы), вывел результаты в табличку: + +| Сценарий | Тип JOIN | Время | +|----------|----------|-------| +| маленькая + большая + индекс | Nested Loop | ~180 ms | +| маленькая + большая + индекс | Hash Join | ~400 ms | +| большая + большая + нет индекса | Hash Join | ~85 ms | +| большая + большая + нет индекса | Nested Loop | ~12500 ms | + +Гипотезы подтвердились: Nested Loop хорош для 'маленькая + большая + индекс', Hash Join хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна))) \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Optimization.md b/dmezentsev/postgresql/homework/Optimization.md new file mode 100755 index 0000000..9ad8eb6 --- /dev/null +++ b/dmezentsev/postgresql/homework/Optimization.md @@ -0,0 +1,251 @@ +### Задача 1 — Одинаковые ли запросы? + +#### Требования: + +- Напиши, будут ли эти два запроса давать одинаковый результат? Объясни почему. + +```sql +-- Вариант А +SELECT name, age +FROM users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +SELECT name, age +FROM users +WHERE age > 25 AND country = 'RU'; +``` + +Оба запроса дадут абсолютно одинаковый результат. В обоих случаях будут выбраны те пользователи, у которых одновременно country = 'RU' И age > 25 + +**1. Какой из них будет работать быстрее?** + +Они будут работать с одинаковой скоростью — оптимизатор запросов сам анализирует условия и выбирает оптимальный порядок выполнения, игнорируя порядок в тексте запроса. + +### Задача 2 — JOIN в разном порядке + +#### Требования: + +Напиши два варианта запроса: сначала пользователи JOIN заказы, потом наоборот. + +**1. Будут ли результаты одинаковыми?** + +Если бы использовался INNER JOIN: результаты были бы идентичными + +**2. Твой вариант 1** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; +``` + +**3. Твой вариант 2** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.orders o +LEFT JOIN dmezentsev.users u ON o.user_id = u.id; +``` + +**4. Какой таблицей оптимизатор начнёт чаще всего? Почему?** + +Оптимизатор PostgreSQL начинает с меньшей таблицы, потому что это снижает стоимость JOIN-операции + +### Задача 3 — Первое знакомство с EXPLAIN + +#### Требования: + +Выполни команду: + +```sql +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Какой тип сканирования использует база (Seq Scan, Index Scan)?** + +Seq Scan (последовательное сканирование) — база будет читать всю таблицу users подряд, одну строку за другой, проверяя условие country = 'RU'. + +**2. Сколько строк она прочитала?** + +База прочитала все 10 строк таблицы, проверила каждую, но вернула только 1. + +**3. Сколько времени это заняло?** + +0.025–0.050 ms + +### Задача 4 — Сравни два плана + +#### Требования: + +Выполни оба запроса с EXPLAIN ANALYZE: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Почему второй запрос может быть быстрее первого?** + +Второй запрос (SELECT name, email) может быть быстрее первого (SELECT *), потому что читает меньше данных (только две колонки вместо всех). + +### Задача 5 — JOIN с EXPLAIN + +#### Требования: + +Посмотри план выполнения JOIN: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU'; +``` + +**1. С какой таблицы начала читать база? Почему именно с неё?** + +База начнёт читать с таблицы users, потому что у неё есть фильтр WHERE country = 'RU', который сильно уменьшает количество строк + +### Задача 6 — Найди узкое место + +#### Требования: + +У тебя есть запрос: + +```sql +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +``` + +**1. Найди как минимум 2 проблемы, которые замедляют запрос. Как их исправить?** + +- `LOWER(u.name)` — функция на колонке, которая также блокирует использование индекса и требует вычислений для каждой строки +- `LIKE '%алекс%'` не использует индекс, требуя полного сканирования + +**Исправить:** + +- использовать `ILIKE` без `LOWER` +- хранить уже нормализованные данные + +### Задача 7 — Оптимизируй JOIN + +#### Требования: + +Дан запрос: + +```sql +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. В каком порядке база будет читать таблицы? Почему? Можно ли как-то ускорить этот запрос?** + +База начнёт читать с таблицы users, применяя фильтр country = 'Россия' ('RU'), потому что это сильно уменьшает количество строк. + +**Ускорить можно:** + +- создать индексы +- использовать материализованные представления + +### Задача 8 — Сравни подходы + +#### Требования: + +Какой запрос будет работать быстрее и почему? + +```sql +-- Вариант А +SELECT name, price +FROM products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +SELECT * +FROM products +WHERE category = 'Электроника' + AND price > 50000; +``` + +**1. Запусти оба с EXPLAIN ANALYZE и сравни время выполнения.** + +Вариант А будет быстрее, меньше данных нужно прочитать + +- `SELECT name, price` читает 2 колонки (name VARCHAR(100), price DECIMAL) +- `SELECT *` читает все 5 колонок (id, name, category, price, in_stock) + +### Задача 9 — Найди и исправь + +#### Требования: + +Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; +``` + +**1. Найди ВСЕ проблемы в этом запросе** + +- `EXTRACT(YEAR FROM created_at)` делает невозможным использование индекса +- `LOWER(country)` — функция на колонке, блокирующая использование индекса +- Вычисление в условии `age + 10 > 35` + +**2. Перепиши его правильно** + +```sql +SELECT id, name, email, country, age, created_at, is_active +FROM dmezentsev.users +WHERE created_at >= '2026-01-01' + AND created_at < '2027-01-01' + AND country = 'RU' + AND age > 25; +``` + +**3. Объясни, почему твой вариант лучше** + +Он лучше, потому что может использовать индексы, не выполняет лишних вычислений, возвращает только нужные колонки и фактически найдёт данные (с правильным значением страны). + +### Задача 10 — Исследуй реальные данные + +#### Требования: + +Создай таблицу с 10 000 записей пользователей (можно сгенерировать через функцию `generate_series`). + +Сравни время выполнения: + +```sql +-- Запрос 1 +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +SELECT name, email FROM users WHERE age > 25; +``` + +**1. Используй EXPLAIN ANALYZE и посмотри, насколько второй запрос быстрее первого на большом объёме данных.** + +Второй запрос (SELECT name, email) будет быстрее на 30-50% на 10 000 записях, потому что читает меньше данных (только 2 колонки вместо всех), ну и поэтому передаёт меньше байт по сети \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Query_order.md b/dmezentsev/postgresql/homework/Query_order.md new file mode 100755 index 0000000..d8ec3ca --- /dev/null +++ b/dmezentsev/postgresql/homework/Query_order.md @@ -0,0 +1,328 @@ +### Задача 1 — Запрос с группировкой + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +WHERE is_active = true +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY users_count DESC; +``` + +**1. Когда применится `WHERE` — до или после `GROUP BY`?** + +WHERE применяется ДО GROUP BY + +**2. Когда применится `HAVING` — до или после `GROUP BY`?** + +HAVING применяется ПОСЛЕ GROUP BY + +**3. Можно ли использовать алиас `users_count` в `HAVING`?** + +HAVING выполняется до SELECT. Алиас users_count создаётся только на этапе SELECT. Поэтому на момент выполнения HAVING алиас ещё не существует. + +**4. Можно ли использовать алиас `users_count` в `ORDER BY`?** + +ORDER BY выполняется после SELECT. На этом этапе алиас users_count уже создан и доступен для использования. + +### Задача 2 — JOIN с фильтрацией + +```sql +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' AND o.status = 'completed' +ORDER BY o.order_date DESC; +``` + +**1. Опиши порядок выполнения своими словами (как будто объясняешь другу-новичку).** + +Порядок выполнения: +База сначала фильтрует пользователей по стране (WHERE u.country = 'Россия'), оставляя только пользователей из России. Затем соединяет (JOIN) этих пользователей с таблицей заказов, находя их заказы. После этого фильтрует заказы по статусу (WHERE o.status = 'completed'). Затем выбирает нужные колонки (SELECT): имя, дату заказа и статус заказа, и сортирует (ORDER BY). + +### Задача 3 — Найди ошибку + +Дан запрос: + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +``` + +**1. Запрос работает, но написан **неэффективно**. Почему?** + +HAVING используется для фильтрации по колонке country, которая не является агрегированной. Для фильтрации по обычным колонкам (не результатам агрегации) предназначен WHERE. + +**2. Перепиши его правильно, используя WHERE.** + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +WHERE country = 'Россия' +GROUP BY country; +``` + +**3. Объясни, почему твой вариант быстрее.** + +Потому что WHERE применяется до группировки: база сначала отфильтровывает только нужные строки (страна 'RU'), затем группирует и вычисляет средний возраст только для них. Это уменьшает объём данных для группировки, база может использовать индекс на country и не делает лишних вычислений + +### Задача 4 — Исправь запрос + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; +``` + +**1. Перепиши запрос так, чтобы часть условий была в `WHERE`.** + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +``` + +**2. Объясни, почему это быстрее.** + +Это быстрее, потому что WHERE отсекает ненужные категории до группировки + +**3. Нарисуй схему: сколько строк будет обработано в каждом случае.** + +Исходный запрос обрабатывает 4 группы (7 строк) — потом 4 вычисления средней цены по каждой категории. +Исправленный запрос фильтрует по условию — обрабатывает только 1 группу (3 строки) — 1 вычисление средней цены + +### Задача 5 — Исправь все ошибки + +```sql +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; +``` + +**1. Найди ошибку и исправь запрос.** + +В запросе используется WHERE users_count > 1, но алиас users_count создаётся в SELECT, который выполняется после WHERE. Кроме того, WHERE не может фильтровать по агрегатным функциям COUNT(*) + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; +``` + +### Задача 6 — Полный цикл выполнения + +```sql +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +``` + +**1. Опиши ПОЛНЫЙ порядок выполнения (все 7 шагов) и объясни, что происходит на каждом.** + +- **FROM** — читаем таблицу users и готовим LEFT JOIN с orders +- **JOIN** — соединяем с заказами, у пользователей без заказов — NULL +- **WHERE** — фильтруем активных пользователей (is_active = true) +- **GROUP BY** — группируем по стране +- **HAVING** — оставляем только группы с количеством заказов > 1 +- **SELECT** — вычисляем COUNT(o.id) и AVG(o.id) для каждой группы +- **ORDER BY** — сортируем по убыванию orders_count +- **LIMIT** — берём первые 3 строки + +### Задача 7 — Оптимизация + +Сравни два запроса: + +```sql +-- Запрос А +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +``` + +**1. Какой запрос быстрее? Почему?** + +Запрос Б быстрее. Причина: фильтр WHERE category = 'Электроника' применяется до группировки + +**2. Нарисуй таблицу: на каком шаге сколько строк обрабатывается в каждом запросе.** + +| Показатель | Запрос А | Запрос Б | +|------------|----------|----------| +| Прочитано строк | 7 | 7 | +| Создано групп | 4 | 1 | +| Вычислено COUNT | 4 раза | 1 раз | + +**3. Выполни оба с `EXPLAIN ANALYZE` и сравни `Execution Time`.** + +У запроса А Rows Removed by Filter: 3 после группировки, у запроса Б фильтр применяется при сканировании таблицы + +### Задача 8 — Многоэтапная фильтрация + +```sql +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +``` + +**1. Когда применяются условия `WHERE`?** + +Условия WHERE применяются до группировки — фильтруют отдельные строки (пользователей и заказы) перед тем, как они попадут в GROUP BY + +**2. Когда применяется условие `HAVING`?** + +Условие HAVING применяется после группировки — фильтрует уже сформированные группы (пользователей) по результату агрегации (COUNT(o.id)). + +**3. В чём разница между фильтрацией по `o.status` в `WHERE` и по `COUNT(o.id)` в `HAVING`?** + +WHERE o.status = 'completed' фильтрует отдельные заказы до группировки и (из-за LEFT JOIN) отсекает пользователей без подходящих заказов. HAVING COUNT(o.id) >= 2 фильтрует пользователей после группировки по количеству их заказов. + +### Задача 9 — JOIN и ORDER BY + +```sql +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +``` + +**1. На каком этапе применяется `ORDER BY`?** + +ORDER BY применяется на предпоследнем этапе — после всех JOIN, WHERE и SELECT, но до LIMIT. + +**2. Почему `LIMIT` идёт после `ORDER BY`, а не до?** + +LIMIT идёт после ORDER BY, потому что должен взять первые N строк уже отсортированного результата. + +**3. Что будет, если поменять их местами (технически невозможно, но подумай концептуально)?** + +Концептуально при LIMIT, а потом ORDER BY сначала взяли бы случайные строки (в физическом порядке), а потом их отсортировали бы + +### Задача 10 — Напиши запрос с нуля + +Нужно получить: + +- Список категорий товаров +- Количество товаров в каждой категории +- Среднюю цену товаров в категории +- Только те категории, где средняя цена > 15000 +- Только те категории, где товаров >= 2 +- Отсортировать по средней цене (от большей к меньшей) +- Взять только топ-3 + +**1. Напиши запрос** + +```sql +SELECT + category, + COUNT(*) AS products_count, + AVG(price) AS avg_price +FROM dmezentsev.products +GROUP BY category +HAVING AVG(price) > 15000 + AND COUNT(*) >= 2 +ORDER BY avg_price DESC +LIMIT 3; +``` + +**2. Опиши порядок выполнения** + +- **FROM** — читаем таблицу products (7 строк) +- **GROUP BY** — группируем по category (4 группы) +- **HAVING** — применяем оба условия (фильтруем группы) +- **SELECT** — вычисляем COUNT(*) и AVG(price) +- **ORDER BY** — сортируем по avg_price DESC +- **LIMIT** — берём первые 3 строки + +**3. Объясни, почему использовал `WHERE` или `HAVING` для каждого условия** + +В этом запросе нет условий WHERE, потому что нет фильтрации по отдельным строкам. Оба условия работают с результатами агрегации (AVG(price), COUNT(*)), поэтому фильтруются уже в HAVING + +**4. Выполни с `EXPLAIN ANALYZE` и посмотри план** + +``` +Seq Scan on products -- Читаем все 7 строк таблицы +HashAggregate -- Группируем по category (4 группы) +Filter: (avg(price) > 15000 AND count(*) >= 2) -- Применяем HAVING +Rows Removed by Filter: 3 -- 3 группы отброшены +Sort -- Сортируем результат по avg_price DESC +Limit -- Ограничиваем 3 строками +``` + +### Задача 11 — Подзапросы и порядок + +```sql +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'Россия' +ORDER BY orders_count DESC; +``` + +**1. Когда выполняется подзапрос в `SELECT`?** + +Подзапрос в SELECT выполняется для каждой строки после WHERE u.country = 'Россия', но до ORDER BY + +**2. Можно ли использовать алиас `orders_count` в `WHERE`?** + +Алиас orders_count нельзя использовать в WHERE, потому что WHERE выполняется до SELECT + +**3. Можно ли использовать его в `ORDER BY`?** + +Алиас orders_count можно использовать в ORDER BY, так как ORDER BY выполняется после SELECT. + +**4. Перепиши этот запрос через `JOIN`. Какой вариант быстрее? Проверь с `EXPLAIN ANALYZE`!** + +```sql +SELECT u.name, u.age, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.id, u.name, u.age +ORDER BY orders_count DESC; +``` + +На маленьких данных разница незаметна. Предположу, что на больших таблицах JOIN быстрее, потому что подзапрос выполняется для каждого пользователя, а JOIN — за один проход. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Transactions.md b/dmezentsev/postgresql/homework/Transactions.md new file mode 100755 index 0000000..c767031 --- /dev/null +++ b/dmezentsev/postgresql/homework/Transactions.md @@ -0,0 +1,68 @@ +### Задание 1. Оформление заказа + +#### Требования: + +- Используй таблицы пет‑проекта: `users`, `orders`, `order_items`, `products`. + - Заказ: пользователь покупает два разных товара. + - Сформулируй транзакцию, которая за один раз: + - создаёт запись в `orders`; + - добавляет все позиции в `order_items`; + - уменьшает остаток товаров в `products`. + +**1. Что в данных пойдёт не так, если часть шагов выполнится, а часть нет?** + +- Создан заказ, но не добавлены товары: заказ без позиций. Пользователь думает, что оформил заказ, а товары не зарезервированы. +- Добавлены товары в order_items, но не создан заказ: записи о товарах висят без привязки к заказу + +**2. Почему оформление заказа — типичный пример «либо всё, либо ничего»?** + +Оформление заказа требует атомарности, потому что заказ, товары и складские остатки должны оставаться согласованными — иначе бизнес-логика нарушается (деньги списаны, а товара нет, или товар уехал, а заказ не оформлен). + +### Задание 2. Ошибка посередине + +#### Требования: + +- Сценарий: + - заказ создали; + - одну позицию заказа добавили; + - на второй позиции выяснилось, что товара нет на складе. +- Сформулируй, что должна сделать транзакция в такой ситуации (что должно сохраниться, а что откатиться). + +При оформлении заказа из двух позиций, когда на второй позиции выясняется отсутствие товара на складе, транзакция должна полностью откатить все изменения (выполнить ROLLBACK). + +**1. Какие «битые» заказы можно получить без отката (`ROLLBACK`)?** + +- Заказ без позиций — запись в orders есть, но в order_items пусто. +- Заказ с частичным наполнением — первая позиция добавлена, вторая нет. Заказ существует, но неполный. +- Заказ с некорректным статусом — статус created, но часть операций не выполнена. +- Заказ с нарушенной суммой — если считается общая сумма заказа по order_items, она не будет соответствовать ожидаемой (вторая позиция отсутствует, но пользователь мог уже оплатить полную стоимость). + +**2. Как такие ошибки повлияют на отчёты и витрины по выручке?** + +- Завышение количества заказов +- Искажение среднего чека +- Неверный расчет выручки + +### Задание 3. Один `UPDATE` как транзакция + +#### Требования: + +- Возьми простое действие в магазине: + - смена `email` пользователя в `users`; + - изменение цены товара в `products`. +- Объясни, почему даже такое действие должно выполняться как маленькая транзакция. + +Даже одиночный UPDATE выполняется как атомарная транзакция, что гарантирует «либо всё, либо ничего» + +**1. Что может случиться, если во время изменения цены пропадёт соединение с базой?** + +- Соединение потеряно до начала выполнения: + Команда не дошла до сервера — изменения не произошли. +- Соединение потеряно в процессе выполнения: + Транзакция автоматически откатывается (ROLLBACK). +- Соединение потеряно после успешного выполнения: + Транзакция уже зафиксирована (COMMIT выполнен). + +**2. Почему хорошо, что база сама откатывает незавершённые изменения?** + +Это хорошо, потому что защищает от повреждения данных, упрощает разработку и гарантирует соблюдение ограничений целостности без ручного вмешательства. \ No newline at end of file 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/Views.sql b/dmezentsev/postgresql/homework/Views.sql new file mode 100755 index 0000000..6dbcc00 --- /dev/null +++ b/dmezentsev/postgresql/homework/Views.sql @@ -0,0 +1,131 @@ +-- Задание 1. Витрина выручки по категориям + +/* Требования: + +- Используй таблицы: `products`, `orders`, `order_items`. +- Учитывай только оплаченные заказы (`status = 'paid'`). +- Создай `MATERIALIZED VIEW` `mv_category_revenue` со столбцами: + - `category` + - `total_revenue` — суммарная выручка по категории (`quantity * price_at_purchase`) +- Отсортируй результат по `total_revenue` по убыванию при выборке. +*/ + +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM dmezentsev.products p +JOIN dmezentsev.orders_items oi ON p.id = oi.product_id +JOIN dmezentsev.orders o ON oi.order_id = o.id +GROUP BY p.category +WHERE o.status = 'paid' +ORDER BY total_revenue DESC + + +/* 1. Чем удобно иметь готовую витрину по категориям, вместо того чтобы каждый раз писать JOIN по трём таблицам? +- Производительность — данные уже агрегированы и лежат в одной материализованной таблице. Вместо сканирования +трёх таблиц и группировки, запрос читает готовую таблицу. +- Простота запросов — аналитик пишет SELECT * FROM mv_category_revenue, не вникая в схему и условия фильтрации. +*/ + + +-- Задание 2. Витрина активности пользователей + +/* Требования: + +- Используй таблицы: `users`, `orders`. +- Считай только оплаченные заказы. +- Создай MATERIALIZED VIEW **`mv_user_activity`** со столбцами: + - `user_id` + - `user_name` + - `orders_count` — сколько оплаченных заказов сделал пользователь +- В выборке покажи только тех, у кого `orders_count > 0`, отсортируй по `orders_count` по убыванию. +*/ + +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.user_id, + u.user_name, + COUNT(o.order_id) AS orders_count +FROM users u +JOIN orders o ON u.user_id = o.user_id +WHERE o.status = 'paid' +GROUP BY u.user_id, u.user_name +HAVING COUNT(o.order_id) > 0 +ORDER BY orders_count DESC; + +/* 1. Как по такой витрине найти «самых лояльных» клиентов? + +SELECT * FROM mv_user_activity LIMIT 3; + +(Предположил ради примера, что самые лояльные - это топ-3 клиента) + +2. Что будет, если считать активность не по всем заказам, а только за последние 30 дней +— как изменится смысл витрины? + +Смысл витрины изменится на отслеживание текущей (недавней) активности, а не за весь период времени в данных +*/ + + +-- Задание 3. Витрина выручки по дням + +/* Требования: + +- Используй таблицы: `orders`, `order_items`. +- Учитывай только оплаченные заказы. +- Создай `MATERIALIZED VIEW` **`mv_daily_revenue`** со столбцами: + - `order_date::date` как `sales_date` + - `daily_revenue` — суммарная выручка за день +- При выборке отсортируй по `sales_date` по возрастанию. +*/ + +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM orders o +JOIN order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.order_date::date +ORDER BY sales_date ASC; + +/* 1. Как по такой витрине заметить «просадки» и «пики» продаж? + +- Построить линейный график зависимости daily_revenue от sales_date +- Использовать оконные функции для сравнения со скользящим средним (например, за 7 дней) +- Вычислить оконной функцией LAG и разницу с предыдущим днём, чтобы увидеть изменения. + +2. Что быстрее: считать выручку «на лету» из сырых таблиц или читать из `mv_daily_revenue` ? Почему? + +Чтение из mv_daily_revenue быстрее, потому что: +- В mv_daily_revenue меньше строк, так как данные агрегированы. Сырые таблицы могут содержать +миллионы записей — каждый день приходится заново агрегировать все данные. +*/ + +-- Задание 4. Обновление витрин + +/* Требования: + +- Для любой одной из созданных витрин (на выбор ученика) написать команду обновления с `REFRESH MATERIALIZED VIEW ...`. +*/ + +REFRESH MATERIALIZED VIEW mv_daily_revenue; + + +/* - Придумать расписание обновления витрины для двух сценариев: + - интернет‑магазин с несколькими заказами в день; + - крупный магазин с тысячами заказов в час. + +Интернет‑магазин с несколькими заказами в день — 1 раз в сутки, ночью +За день накапливается мало новых данных, достаточно обновляться раз в сутки + +Крупный магазин с тысячами заказов в час — каждые 30 минут +Обновление за 30 минут успевает до следующего цикла + +1. Чем грозит слишком редкий `REFRESH` для отчётов (какие ошибки могут появиться в анализе)? +- Устаревшие данные — угроза применения ошибочных мер. +- Пропуск аномалий — если витрина обновляется редко, аномалии можно не заметить до следующего REFRESH + +2. Почему нельзя обновлять витрины «каждую секунду», даже если хочется всегда свежие данные? +- Ресурсы БД — каждый REFRESH блокирует чтение и перестраивает всю витрину заново +- Проблемы с блокировками — если кто-то читает витрину во время REFRESH, запрос встанет в очередь или получит ошибку. +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Window_functions.sql b/dmezentsev/postgresql/homework/Window_functions.sql new file mode 100644 index 0000000..e2b773c --- /dev/null +++ b/dmezentsev/postgresql/homework/Window_functions.sql @@ -0,0 +1,209 @@ +-- Задача 1. Нумерация заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS order_number_for_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - В чём отличие такой нумерации от простой сортировки  +ORDER BY order_date без оконной функции? + +В случае с оконной функцией идет сортировка по order_date в рамках одного окна +(по пользователю), и каждому заказу пользователя присваивается свой порядковый +номер. При простой сортировке без оконной функции по дате сортировались бы +все заказы +*/ + +-- Задача 2. Накопительная выручка по пользователю + +WITH order_totals AS ( + SELECT o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + 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 + GROUP BY o.user_id, u.name, o.id, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + SUM(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_revenue_for_user +FROM order_totals; + +/* - Почему для накопительной суммы важно задавать рамку окна +(от «самого первого» до «текущего»), а не оставлять значения по умолчанию? + +По умолчанию RANGE BETWEEN ... Если у пользователя два заказа +в один и тот же день, то накопительная сумма посчитается так: + 1 января 100 руб Накопительная сумма: 300 + 1 января 200 руб Накопительная сумма: 300 + +Важно задавать явно ROWS BETWEEN..., чтобы накопительная сумма считалась +правильно: + 1 января 100 руб Накопительная сумма: 100 + 1 января 200 руб Накопительная сумма: 300 + +- Как по такой таблице понять, что пользователь начал тратить больше? + +Если накопительная сумма растет, то это указывает на то, что +пользователь с течением времени тратит больше +*/ + +-- Задача 3. Рейтинг пользователей по выручке + +WITH user_revenue AS ( + SELECT u.id AS user_id, + u.name AS user_name, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.name, u.id +) + + SELECT user_id, + user_name, + total_revenue, + RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY total_revenue DESC) AS revenue_dense_rank +FROM user_revenue; + +/* - Чем RANK отличается от DENSE_RANK в случае, когда +у двух пользователей одинаковая выручка? + +Допустим, у двух таких пользователей будет 1ое место у каждого по выручке. +При RANK у следующего будет 3ье место, при DENSE_RANK - 2ое. + +RANK пропускает следующие числовые позиции после одинаковых +значений (например: 1, 2, 2, 4), а DENSE_RANK не пропускает (например: 1, 2, 2, 3). + */ + +-- Задача 4. Первый и последний заказ пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Как можно использовать поля «первый заказ» и «последний заказ» +в сегментации клиентов? + +Для определения частоты покупок (разница между датами), выделения +новых или ушедших клиентов (например, клиенты без заказов за последние 3 месяца) + +- Почему для правильного LAST_VALUE важно задать рамку окна так, чтобы смотреть +на всю историю пользователя, а не только до текущей строки? + +Потому что по умолчанию рамка окна — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, +и LAST_VALUE показывает последнее значение в текущей рамке, т.е. текущую строку. +А нужно по всей рамке, от первой до последней даты у каждого пользователя + */ + +-- Задача 5. Заказы и количество заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + COUNT(o.id) OVER (PARTITION BY u.id) AS orders_per_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Чем такой результат отличается от классического отчёта +«по пользователям» с GROUP BY user_id? + +При применении оконной функции строки не схлопываются, то есть если у +пользователя несколько заказов, то по каждому заказу будет строка с данными, +и в каждой строке будет общее число заказов + */ + +-- Задача 6. Сравнение суммы заказа с предыдущим + +WITH orders_total AS ( + SELECT u.id AS user_id, + o.id AS order_id, + u.name AS user_name, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.id, o.id, u.name, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + LAG(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ) AS prev_order_revenue +FROM orders_total; + +/* - Как по такому результату понять, +увеличивается или уменьшается средний чек с течением времени? + +Нужно сравнить order_revenue с prev_order_revenue для каждого заказа. +Если чаще order_revenue > prev_order_revenue — средний чек растёт. +Если чаще order_revenue < prev_order_revenue — средний чек падает. +sql + */ + +-- Задача 7. Категории с накопительной выручкой по времени + +WITH category_revenue AS ( + SELECT p.category, + DATE_TRUNC('day', o.order_date)::DATE AS order_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM dmezentsev.order_items oi + JOIN dmezentsev.products p ON oi.product_id = p.id + JOIN dmezentsev.orders o ON oi.order_id = o.id + GROUP BY p.category, + DATE_TRUNC('day', o.order_date)::DATE +) + +SELECT category, + order_date, + daily_category_revenue, + SUM(daily_category_revenue) OVER ( + PARTITION BY category + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_category_revenue +FROM category_revenue; + +/* - Чем такой «накопительный» график по категории отличается по смыслу +от обычного ежедневного (без SUM OVER)? + +Обычный график daily_category_revenue показывает, какая выручка категории день ото дня. +Накопительный график running_category_revenue показывает общий рост +или падение категории за период собранных данных + */ \ 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 понадобится, когда нужно убрать из выборки +какой-то топ пользователей. Допустим, нужны все, кроме первой тройки + */ diff --git a/dmezentsev/postgresql/pet_project/DDL_DML.sql b/dmezentsev/postgresql/pet_project/DDL_DML.sql new file mode 100644 index 0000000..498ef54 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/DDL_DML.sql @@ -0,0 +1,205 @@ +DROP TABLE IF EXISTS dmezentsev.lesson_views; +DROP TABLE IF EXISTS dmezentsev.enrollments; +DROP TABLE IF EXISTS dmezentsev.lessons; +DROP TABLE IF EXISTS dmezentsev.users_; +DROP TABLE IF EXISTS dmezentsev.courses; + +CREATE TABLE dmezentsev.users_( + id BIGINT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age SMALLINT, + email VARCHAR(100) UNIQUE, + registration_date DATE NOT NULL +); + +CREATE TABLE dmezentsev.courses ( + id BIGINT PRIMARY KEY, + title varchar(200) NOT NULL, + category VARCHAR(100) NOT NULL, + created_at DATE NOT NULL +); + +CREATE TABLE dmezentsev.lessons ( + id BIGINT PRIMARY KEY, + course_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + duration_min SMALLINT, + + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) +); + +CREATE TABLE dmezentsev.enrollments ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + course_id BIGINT NOT NULL, + enrolled_at DATE, + + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) +); + +CREATE TABLE dmezentsev.lesson_views ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + lesson_id BIGINT NOT NULL, + viewed_at TIMESTAMPTZ, + + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (lesson_id) REFERENCES dmezentsev.lessons(id) +); + +INSERT INTO dmezentsev.users_ (id, name, age, email, registration_date) VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), +(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), +(13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), +(18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), +(20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +INSERT INTO dmezentsev.courses (id, title, category, created_at) VALUES +(1, 'SQL для начинающих', 'data', '2023-01-01'), +(2, 'Python для анализа данных', 'programming', '2023-01-15'), +(3, 'BI с нуля', 'business', '2023-03-01'), +(4, 'Excel для аналитиков', 'business', '2023-02-01'), +(5, 'Основы машинного обучения', 'data', '2023-03-15'); + +INSERT INTO dmezentsev.lessons (id, course_id, title, duration_min) VALUES +(1, 1, 'SELECT и FROM', 10), +(2, 1, 'JOIN', 15), +(3, 1, 'WHERE и фильтрация', 12), +(4, 1, 'Агрегации и GROUP BY', 18), +(5, 1, 'Подзапросы', 25), +(6, 2, 'Pandas основы', 20), +(7, 2, 'NumPy', 15), +(8, 2, 'Визуализация данных', 22), +(9, 3, 'Основы BI', 12), +(10, 3, 'Метрики и дашборды', 18), +(11, 3, 'Power BI введение', 20), +(12, 4, 'Формулы в Excel', 15), +(13, 4, 'Сводные таблицы', 18), +(14, 4, 'Графики и диаграммы', 14), +(15, 5, 'Введение в ML', 20), +(16, 5, 'Линейная регрессия', 30), +(17, 5, 'Классификация', 28), +(18, 5, 'Кластеризация', 25); + +INSERT INTO dmezentsev.enrollments (id, user_id, course_id, enrolled_at) VALUES +(1, 1, 1, '2023-01-15'), +(2, 1, 2, '2023-02-01'), +(3, 2, 1, '2023-01-20'), +(4, 3, 2, '2023-03-05'), +(5, 4, 3, '2023-04-01'), +(6, 6, 1, '2023-01-06'), +(7, 6, 2, '2023-01-10'), +(8, 6, 4, '2023-02-05'), +(9, 7, 1, '2023-01-13'), +(10, 7, 3, '2023-03-10'), +(11, 8, 1, '2023-01-22'), +(12, 8, 2, '2023-01-22'), +(13, 8, 3, '2023-01-22'), +(14, 9, 4, '2023-02-12'), +(15, 10, 1, '2023-04-16'), +(16, 11, 2, '2023-04-21'), +(17, 12, 1, '2023-01-16'), +(18, 13, 2, '2023-01-19'), +(19, 13, 4, '2023-02-20'), +(20, 14, 1, '2023-01-09'), +(21, 14, 2, '2023-01-09'), +(22, 14, 3, '2023-01-09'), +(23, 14, 4, '2023-02-01'), +(24, 14, 5, '2023-03-20'), +(25, 15, 1, '2023-01-11'), +(26, 15, 3, '2023-03-05'), +(27, 16, 2, '2023-02-16'), +(28, 16, 4, '2023-02-16'), +(29, 17, 3, '2023-03-22'), +(30, 17, 5, '2023-03-25'), +(31, 18, 1, '2023-04-02'), +(32, 19, 1, '2023-04-11'), +(33, 19, 2, '2023-04-11'), +(34, 20, 2, '2023-04-19'), +(35, 5, 5, '2023-03-20'), +(36, 1, 4, '2023-02-10'), +(37, 2, 3, '2023-03-15'), +(38, 3, 1, '2023-02-25'), +(39, 4, 2, '2023-03-10'), +(40, 6, 3, '2023-03-08'), +(41, 7, 2, '2023-02-01'), +(42, 9, 1, '2023-02-15'), +(43, 12, 3, '2023-03-10'), +(44, 15, 2, '2023-02-01'), +(45, 18, 4, '2023-04-05'); + +INSERT INTO dmezentsev.lesson_views (id, user_id, lesson_id, viewed_at) VALUES +(1, 1, 1, '2023-01-16 10:00:00'), +(2, 1, 2, '2023-01-16 10:15:00'), +(3, 1, 3, '2023-01-17 09:00:00'), +(4, 1, 4, '2023-01-17 09:30:00'), +(5, 1, 4, '2023-01-17 11:00:00'), +(6, 1, 4, '2023-01-18 10:00:00'), +(7, 1, 6, '2023-02-02 14:00:00'), +(8, 1, 12, '2023-02-15 10:00:00'), +(9, 1, 13, '2023-02-15 10:30:00'), +(10, 2, 1, '2023-01-21 09:00:00'), +(11, 2, 9, '2023-03-16 10:00:00'), +(12, 2, 10, '2023-03-16 10:30:00'), +(13, 3, 6, '2023-03-06 12:00:00'), +(14, 3, 1, '2023-02-26 11:00:00'), +(15, 3, 2, '2023-02-26 11:20:00'), +(16, 4, 9, '2023-04-02 10:00:00'), +(17, 4, 10, '2023-04-03 10:00:00'), +(18, 4, 6, '2023-03-12 14:00:00'), +(19, 4, 7, '2023-03-13 09:00:00'), +(20, 5, 15, '2023-03-21 10:00:00'), +(21, 5, 16, '2023-03-22 11:00:00'), +(22, 6, 1, '2023-01-07 10:00:00'), +(23, 6, 2, '2023-01-07 10:15:00'), +(24, 6, 3, '2023-01-08 09:00:00'), +(25, 6, 4, '2023-01-08 09:30:00'), +(26, 6, 5, '2023-01-09 10:00:00'), +(27, 6, 6, '2023-01-11 14:00:00'), +(28, 6, 7, '2023-01-12 10:00:00'), +(29, 6, 8, '2023-01-13 11:00:00'), +(30, 6, 12, '2023-02-06 09:00:00'), +(31, 6, 13, '2023-02-06 09:30:00'), +(32, 6, 14, '2023-02-07 10:00:00'), +(33, 6, 9, '2023-03-09 11:00:00'), +(34, 7, 1, '2023-01-14 10:00:00'), +(35, 7, 2, '2023-01-14 10:20:00'), +(36, 7, 3, '2023-01-15 09:00:00'), +(37, 7, 9, '2023-03-11 14:00:00'), +(38, 7, 10, '2023-03-11 14:30:00'), +(39, 9, 12, '2023-02-13 10:00:00'), +(40, 9, 13, '2023-02-13 10:30:00'), +(41, 9, 1, '2023-02-16 11:00:00'), +(42, 12, 1, '2023-01-17 10:00:00'), +(43, 12, 2, '2023-01-17 10:20:00'), +(44, 12, 1, '2023-04-10 11:00:00'), +(45, 12, 3, '2023-04-10 11:30:00'), +(46, 12, 9, '2023-03-12 10:00:00'), +(47, 13, 6, '2023-01-20 14:00:00'), +(48, 13, 7, '2023-01-21 10:00:00'), +(49, 13, 12, '2023-02-21 09:00:00'), +(50, 14, 1, '2023-01-09 08:00:00'), +(51, 14, 2, '2023-01-09 08:15:00'), +(52, 14, 3, '2023-01-10 10:00:00'), +(53, 14, 4, '2023-01-10 10:30:00'), +(54, 14, 5, '2023-01-11 09:00:00'), +(55, 14, 6, '2023-01-10 14:00:00'), +(56, 14, 7, '2023-01-11 10:00:00'), +(57, 14, 8, '2023-01-12 11:00:00'), +(58, 14, 9, '2023-01-10 08:00:00'); \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql new file mode 100644 index 0000000..493f553 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql @@ -0,0 +1,105 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.conversion_funnel; +CREATE MATERIALIZED VIEW dmezentsev.conversion_funnel AS +WITH progress AS ( + -- Прогресс каждого пользователя по каждому курсу + SELECT + e.course_id, + e.user_id, + COUNT(DISTINCT l.id) AS total_lessons, + COUNT(DISTINCT lv.lesson_id) AS viewed_lessons, + ROUND(COUNT(DISTINCT lv.lesson_id) * 100.0 / NULLIF(COUNT(DISTINCT l.id), 0), 2) AS viewed_pct + FROM dmezentsev.enrollments e + LEFT JOIN dmezentsev.lessons l ON e.course_id = l.course_id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id AND e.user_id = lv.user_id + GROUP BY e.course_id, e.user_id +), + +funnel_stats AS ( + -- Статистика по каждому курсу для всех этапов воронки + SELECT + c.id AS course_id, + c.title AS course_title, + (SELECT COUNT(*) FROM dmezentsev.users_) AS registered_count, + COUNT(DISTINCT e.user_id) AS enrolled_count, + COUNT(DISTINCT CASE WHEN p.viewed_lessons > 0 THEN e.user_id END) AS started_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct >= 50 THEN e.user_id END) AS mid_progress_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct = 100 THEN e.user_id END) AS completed_count + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + LEFT JOIN progress p ON e.course_id = p.course_id AND e.user_id = p.user_id + GROUP BY c.id, c.title +) + +-- Соединяем этапы с помощью UNION ALL +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start +FROM ( + -- Этап 1: registered + SELECT + course_id, + course_title, + 'registered' AS stage_name, + registered_count AS users_count, + 0 AS conversion_from_previous, + 100.00 AS conversion_from_start, + 1 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 2: enrolled + SELECT + course_id, + course_title, + 'enrolled', + enrolled_count, + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + 2 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 3: started + SELECT + course_id, + course_title, + 'started', + started_count, + ROUND(100.0 * started_count / NULLIF(enrolled_count, 0), 2), + ROUND(100.0 * started_count / NULLIF(registered_count, 0), 2), + 3 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 4: mid_progress + SELECT + course_id, + course_title, + 'mid_progress', + mid_progress_count, + ROUND(100.0 * mid_progress_count / NULLIF(started_count, 0), 2), + ROUND(100.0 * mid_progress_count / NULLIF(registered_count, 0), 2), + 4 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 5: completed + SELECT + course_id, + course_title, + 'completed', + completed_count, + ROUND(100.0 * completed_count / NULLIF(mid_progress_count, 0), 2), + ROUND(100.0 * completed_count / NULLIF(registered_count, 0), 2), + 5 AS stage_order + FROM funnel_stats +) AS funnel_rows +ORDER BY course_id, stage_order; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_course_summary.sql b/dmezentsev/postgresql/pet_project/dm_course_summary.sql new file mode 100644 index 0000000..86a65b3 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_course_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.course_summary; +CREATE MATERIALIZED VIEW dmezentsev.course_summary AS +WITH count_users AS ( + -- Кол-во уникальных пользователей в курсах + SELECT + c.id AS course_id, + c.title AS course_title, + COUNT(DISTINCT e.user_id) AS unique_users + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id, c.title +), + count_lessons AS ( + -- Кол-во уроков в курсах + SELECT + course_id, + COUNT(id) AS lessons_count + FROM dmezentsev.lessons + GROUP BY course_id +), + views_count AS ( + -- Суммарное число просмотров по каждому курсу + SELECT + l.course_id, + COUNT(lv.id) AS total_views, + MIN(lv.viewed_at) AS first_viewed_at, + MAX(lv.viewed_at) AS last_viewed_at + FROM dmezentsev.lessons l + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.course_id +) + +SELECT + cu.course_id, + cu.course_title, + cu.unique_users, + cl.lessons_count, + vc.total_views, + ROUND(COALESCE(vc.total_views::numeric / + cu.unique_users, 0), 2) AS avg_views_per_user, + vc.first_viewed_at, + vc.last_viewed_at +FROM count_users cu +LEFT JOIN count_lessons cl USING (course_id) +LEFT JOIN views_count vc USING (course_id) +ORDER BY cu.course_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql new file mode 100644 index 0000000..50bf2bb --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.lesson_popularity_ranking; +CREATE MATERIALIZED VIEW dmezentsev.lesson_popularity_ranking AS +WITH +course_enrollment AS ( + -- Сколько студентов записано на каждый курс + SELECT + c.id AS course_id, + COUNT(DISTINCT e.user_id) AS enrolled_to_course + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id +), + +lesson_summary AS ( + -- Статистика по урокам + SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.id AS course_id, + c.title AS course_title, + COUNT(lv.id) AS total_views, + COUNT(DISTINCT lv.user_id) AS unique_viewers, + ROUND( + COUNT(lv.id)::NUMERIC / NULLIF(COUNT(DISTINCT lv.user_id), 0), + 2 + ) AS avg_views_per_viewer + FROM dmezentsev.lessons l + JOIN dmezentsev.courses c ON l.course_id = c.id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.id, l.title, c.id, course_id, c.title +) + +SELECT + ls.lesson_id, + ls.lesson_title, + ls.course_title, + ls.total_views, + ls.unique_viewers, + ls.avg_views_per_viewer, + DENSE_RANK() OVER (ORDER BY ls.total_views DESC) AS popularity_rank, + ROUND( + ls.unique_viewers::NUMERIC / NULLIF(ce.enrolled_to_course, 0) * 100, 2 + ) AS completion_rate +FROM lesson_summary ls +LEFT JOIN course_enrollment ce ON ls.course_id = ce.course_id +ORDER BY popularity_rank; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_platform_summary.sql b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql new file mode 100644 index 0000000..a276ed0 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql @@ -0,0 +1,19 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.platform_summary; +CREATE MATERIALIZED VIEW dmezentsev.platform_summary AS +WITH summary AS ( + SELECT + (SELECT COUNT(*) FROM dmezentsev.users_) AS total_users, + (SELECT COUNT(*) FROM dmezentsev.courses) AS total_courses, + (SELECT COUNT(*) FROM dmezentsev.lessons) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM dmezentsev.lesson_views) AS users_with_views, + (SELECT COUNT(*) FROM dmezentsev.lesson_views) AS total_views +) + +SELECT + total_users, + total_courses, + total_lessons, + users_with_views, + ROUND(total_lessons::numeric / total_courses, 2) AS avg_lessons_per_course, + ROUND(total_views::numeric / total_lessons, 2) AS avg_views_per_lesson +FROM summary; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql new file mode 100644 index 0000000..af4bd02 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_activity_summary; +CREATE MATERIALIZED VIEW dmezentsev.user_activity_summary AS +WITH user_enroll AS ( + -- На сколько курсов записался пользователь + SELECT + u.id AS user_id, + COUNT(e.id) AS enrollment_count + FROM dmezentsev.users_ u + JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY u.id +), + user_views AS ( + -- Сколько уроков в курсах посмотрел пользователь, и даты + -- первого и последнего просмотра + SELECT + user_id, + COUNT(DISTINCT lesson_id) viewed_lessons_count, + MIN(viewed_at) AS first_view, + MAX(viewed_at) AS last_view + FROM dmezentsev.lesson_views lv + GROUP BY lv.user_id +), +all_lessons AS ( + -- сколько уроков в курсах доступно пользователю + SELECT + e.user_id, + COUNT(l.id) AS possible_lessons + FROM dmezentsev.enrollments e + JOIN dmezentsev.courses c ON e.course_id = c.id + JOIN dmezentsev.lessons l ON c.id = l.course_id + GROUP BY e.user_id +) + +SELECT + u.id AS user_id, + u.name, + ue.enrollment_count, + COALESCE(uv.viewed_lessons_count, 0) AS viewed_lessons_count, + uv.last_view, + uv.first_view, + ROUND(uv.viewed_lessons_count::numeric / NULLIF(al.possible_lessons, 0) * 100, 2) AS engagement_rate +FROM dmezentsev.users_ u +JOIN user_enroll ue ON ue.user_id = u.id +JOIN all_lessons al ON al.user_id = u.id +LEFT JOIN user_views uv ON uv.user_id = u.id +ORDER BY user_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql new file mode 100644 index 0000000..27616d7 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql @@ -0,0 +1,77 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_cohort_analysis; +CREATE MATERIALIZED VIEW dmezentsev.user_cohort_analysis AS +WITH users_in_cohort AS ( + -- Кол-во зарегистрированных пользователей по когортам + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS users_in_cohort + FROM dmezentsev.users_ u + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + enrolled_users AS ( + -- Кол-во пользователей по когортам, записавшиеся хотя бы на 1 курс + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS enrolled_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.enrollments e + WHERE u.id = e.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + active_users AS ( + -- Кол-во пользователей по когортам, посмотревшие хотя бы 1 урок + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS active_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.lesson_views lv + WHERE u.id = lv.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + avg_courses AS ( + -- Среднее количество курсов на пользователя в когорте + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(COUNT(e.course_id)::numeric / COUNT(DISTINCT u.id), 2) AS avg_courses_per_user + FROM dmezentsev.users_ u + LEFT JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +), +avg_time_to_first_view AS ( + -- Среднее время от регистрации до первого просмотра + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(AVG(lv.first_view_date::date - u.registration_date), 2) AS avg_time_to_first_view + FROM dmezentsev.users_ u + INNER JOIN ( + SELECT + user_id, + MIN(viewed_at) AS first_view_date + FROM dmezentsev.lesson_views + GROUP BY user_id + ) lv ON u.id = lv.user_id + -- у пользователя Bob, id = 2 дата первого просмотра раньше даты регистрации, отфильтровал его + WHERE lv.first_view_date >= u.registration_date + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +) + +SELECT + uin.cohort_month, + uin.users_in_cohort, + COALESCE(eu.enrolled_users, 0) AS enrolled_users, + COALESCE(au.active_users, 0) AS active_users, + CASE + WHEN uin.users_in_cohort > 0 THEN + ROUND(100.0 * COALESCE(au.active_users, 0) / uin.users_in_cohort, 2) + ELSE 0 + END AS retention_rate, + ac.avg_courses_per_user, + att.avg_time_to_first_view +FROM users_in_cohort uin +LEFT JOIN enrolled_users eu USING (cohort_month) +LEFT JOIN active_users au USING (cohort_month) +LEFT JOIN avg_courses ac USING (cohort_month) +LEFT JOIN avg_time_to_first_view att USING (cohort_month) +ORDER BY uin.cohort_month; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/extra.sql b/dmezentsev/postgresql/pet_project/extra.sql new file mode 100644 index 0000000..aeeb803 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/extra.sql @@ -0,0 +1,59 @@ +-- Задание 1: Анализ drop-off точек +SELECT + cf.course_id, + c.title AS course_title, + cf.stage_name, + cf.users_count, + ROUND(100 - cf.conversion_from_previous, 2) AS dropoff_percentage +FROM dmezentsev.conversion_funnel cf +JOIN dmezentsev.courses c ON cf.course_id = c.id +WHERE cf.stage_name = 'started' +ORDER BY dropoff_percentage DESC; + +-- Задание 2: Топ-3 урока с повторными просмотрами +SELECT + lesson_id, + lesson_title, + course_title, + total_views, + unique_viewers, + avg_views_per_viewer, + popularity_rank +FROM dmezentsev.lesson_popularity_ranking +WHERE avg_views_per_viewer > 1 +ORDER BY avg_views_per_viewer DESC, total_views DESC +LIMIT 3; + +-- Задание 3: Лучшая когорта по retention +SELECT cohort_month, retention_rate +FROM dmezentsev.user_cohort_analysis +ORDER BY retention_rate DESC +LIMIT 3; + + +-- Задание 4: Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT. + +/* EXPLAIN ANALYZE: + +Seq Scan on conversion_funnel (cost=0.00..11.40 rows=140 width=530) (actual time=0.007..0.011 rows=25 loops=1) +Planning Time: 0.124 ms +Execution Time: 0.027 ms + ++- по каждой вьюхе такой план: +rows=25, rows=5, rows=18 — очень мало данных +Execution Time везде маленький (0.027 - 0.330 ms) +Seq Scan — потому что таблицы небольшие + +Не придумал на данный момент, как улучшить запросы, но для ускорения запросов на больших данных +ввел бы индексацию внешних ключей +*/ + +CREATE INDEX IF NOT EXISTS idx_enrollments_user_id ON dmezentsev.enrollments(user_id); +CREATE INDEX IF NOT EXISTS idx_enrollments_course_id ON dmezentsev.enrollments(course_id); +CREATE INDEX IF NOT EXISTS idx_lessons_course_id ON dmezentsev.lessons(course_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_user_id ON dmezentsev.lesson_views(user_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_lesson_id ON dmezentsev.lesson_views(lesson_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_viewed_at ON dmezentsev.lesson_views(viewed_at); + +-- А также создал бы индекс для даты регистрации +CREATE INDEX IF NOT EXISTS idx_users_registration_date ON dmezentsev.users_(registration_date); \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql new file mode 100644 index 0000000..05b588e --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -0,0 +1,117 @@ +--Типы данных + +--Задача 1 +CREATE TABLE raw_data +ON CLUSTER clickhouse_cluster +( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +SELECT DISTINCT + toDecimal64OrNull(balance, 2) AS balance_decimal, + toDateTime64OrNull(created_at, 3) AS created_at_date +FROM raw_data +WHERE + toDecimal64OrNull(balance, 2) IS NOT NULL + AND toDateTime64OrNull(created_at, 3) IS NOT NULL + +/* + CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL + + Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: +toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL +toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. +*/ + +--Задача 2 +CREATE TABLE events +ON CLUSTER clickhouse_cluster +( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = MergeTree +ORDER BY event_id; + +--Заполнил данными для проверки +INSERT INTO events (event_id, event_date, event_time) VALUES +('1', '2023-01-01', '12:30:45'), +('2', '01.02.2023', '08:15:00'), +('3', 'invalid', '25:99:99'), +('4', '2023-05-20', 'invalid'), +('5', '31.12.2023', '23:59:59'); + +SELECT * FROM events; + +SELECT DISTINCT + toDate(DateTime_Valid) AS Event_Date, + toHour(DateTime_Valid) AS Event_Hour, + DateTime_Valid +FROM ( + SELECT + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid + FROM events +) +WHERE DateTime_Valid IS NOT NULL; + +--Задача 3 +DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster; + +CREATE TABLE user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) ENGINE = MergeTree ORDER BY tuple(); + +--Тест данные +INSERT INTO user_activity (user_id, actions) VALUES +(1, '["login", "purchase", "logout"]'), +(2, '["login", "logout"]'), +(3, '["login"]'), +(4, '["login","purchase"]'); + +SELECT + user_id, + -- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя + sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions +FROM user_activity +WHERE + -- Проверяем наличие 'purchase' в массиве + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id; +/* +Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase". +Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно. +То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout") +*/ + +--Задача 4 +DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster; + +CREATE TABLE sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) ORDER BY sensor_id; + +--Тест данные +INSERT INTO sensor_data (sensor_id, value, status) VALUES +(1, 25.5, 'ok'), +(2, 120.75, 'error'), +(3, 15.0, 1), +(4, 9999999.99, 'ok'), +(5, 0.001, 'ok'); + +SELECT + toString(status) AS status_string, + avg(value) AS avg_value --Среднее значение value для каждого status +FROM sensor_data +WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2) +GROUP BY status_string; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql new file mode 100644 index 0000000..6d6edfa --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -0,0 +1,70 @@ + +-- Задачи на отработку Запросы - JOIN -- + +DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.products +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + name String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/products', + '{replica}' +) +ORDER BY product_id; + + +INSERT INTO dsolovianov.products VALUES +(101, 'iPhone 14'), +(102, 'Galaxy S22'), +(103, 'Pixel 7'); + +CREATE TABLE dsolovianov.prices +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + price UInt32, + region String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/prices', + '{replica}' +) +ORDER BY product_id; + +INSERT INTO dsolovianov.prices VALUES +(101, 999, 'US'), +(101, 1049, 'EU'), +(102, 850, 'US'), +(102, 899, 'CA'), +(103, 799, 'JP'); + + +--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов. + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM dsolovianov.products AS p +ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; + +--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`? + +/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/ + +--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают? + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM dsolovianov.products AS p +ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; + +/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения. + Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU. + короче, по сути ALL LEFT JOIN создает декартово произведение*/ diff --git a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql new file mode 100644 index 0000000..120b341 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql @@ -0,0 +1,244 @@ +/* Запросы - Синтаксис*/ + +--! Задача 1 !-- + +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active Bool +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users_v2', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO users VALUES +(1, 'Ivan', 30, 'RU', 1), +(2, 'Anna', 25, 'RU', 1), +(3, 'Petr', 45, 'RU', 0), +(4, 'Olga', 35, 'RU', 1), +(5, 'John', 55, 'US', 1), +(6, 'Alice', 42, 'US', 0), +(7, 'Tom', 38, 'US', 1), +(8, 'Lucas', 28, 'BR', 1), +(9, 'Maria', 60, 'BR', 1), +(10, 'João', 27, 'BR', 0), +(11, 'Kenji', 33, 'JP', 1), +(12, 'Yuki', 29, 'JP', 0), +(13, 'Chen', 40, 'CN', 1), +(14, 'Li', 48, 'CN', 1), +(15, 'Fatima', 31, 'AE', 1), +(16, 'Ahmed', 36, 'AE', 1), +(17, 'George', 24, 'US', 1), +(18, 'Nina', 34, 'RU', 1), +(19, 'Max', 50, 'DE', 1), +(20, 'Sophie', 22, 'DE', 0); + +--Получи 10 самых старших активных пользователей. +SELECT + user_id, + name, + age, + is_active +FROM users +WHERE is_active = true +ORDER BY age DESC +LIMIT 10; + +--Выведи пользователей из country = 'RU', отсортированных по имени. +SELECT + user_id, + name, + country +FROM users +WHERE country = 'RU' +ORDER BY name; + +--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки. +SELECT + user_id, + name, + age +FROM users +WHERE is_active = true +LIMIT 10; + +--Посчитай количество пользователей по странам. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country; + +SELECT + country, + uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country +FROM users +GROUP BY country; + +--Для каждой страны выведи минимальный и максимальный возраст. +SELECT + country, + MIN(age) as min_age, + MAX(age) as max_age +FROM users +GROUP BY country +ORDER BY country; + +--Отсортируй страны по убыванию количества пользователей. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country +ORDER BY user_country DESC; + +--Всё вместе +SELECT + country, + COUNT(*) as user_country, + SUM(is_active) as active_users, + MIN(age) as min_age, + MAX(age) as max_age, + uniqExact(country) as unique_countries +FROM users +GROUP BY country +ORDER BY country; + +--! Задача 2 !-- + +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events_v2', + '{replica}' +) +ORDER BY (user_id, event_time); + +INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES +(1, '2025-07-17 09:00:00', 'click'), +(1, '2025-07-17 09:05:00', 'view'), +(1, '2025-07-17 09:10:00', 'purchase'), +(2, '2025-07-17 10:00:00', 'click'), +(2, '2025-07-17 10:05:00', 'click'), +(2, '2025-07-17 11:00:00', 'view'), +(3, '2025-07-16 12:00:00', 'view'), +(3, '2025-07-16 12:30:00', 'click'), +(3, '2025-07-17 08:00:00', 'purchase'), +(4, '2025-07-17 13:00:00', 'click'), +(4, '2025-07-17 14:00:00', 'view'), +(4, '2025-07-17 15:00:00', 'view'), +(5, '2025-07-16 20:00:00', 'view'), +(5, '2025-07-17 01:00:00', 'click'), +(5, '2025-07-17 06:00:00', 'purchase'), +(6, '2025-07-17 07:30:00', 'view'), +(6, '2025-07-17 08:45:00', 'click'), +(7, '2025-07-16 23:59:59', 'click'), +(7, '2025-07-17 00:01:00', 'purchase'), +(7, '2025-07-17 10:10:00', 'view'); + +--1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`). +SELECT + user_id, + event_time, + event_type, + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)` +SELECT + user_id, + event_time, + event_type, + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank +FROM dsolovianov.events +ORDER BY user_id, event_rank; + +--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли? +--Да, `ROWS BETWEEN` поддерживается: +SELECT + user_id, + event_time, + event_type, + -- кумулятивное окно (все предыдущие строки) + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events, + -- скользящее окно: текущая + 2 предыдущих события + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events, + -- окно: предыдущая + текущая + следующая + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT * FROM recent_events +ORDER BY event_time DESC; + +--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + user_id, + COUNT(*) AS event_count +FROM recent_events +GROUP BY user_id +ORDER BY event_count DESC +LIMIT 5; + +--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе. +WITH +event_categories AS ( + SELECT + event_type, + CASE + WHEN event_type = 'click' THEN 'engagement' + WHEN event_type = 'view' THEN 'engagement' + WHEN event_type = 'purchase' THEN 'conversion' + ELSE 'other' + END AS category + FROM (SELECT DISTINCT event_type FROM dsolovianov.events) +), +recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + re.user_id, + re.event_time, + re.event_type, + ec.category, + ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank, + COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events +FROM recent_events re +LEFT JOIN event_categories ec ON re.event_type = ec.event_type +ORDER BY re.user_id, re.event_time; + +/* +Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE? +многоуровневые CTE - да +рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле. +*/ \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql new file mode 100644 index 0000000..9332b35 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql @@ -0,0 +1,166 @@ +-- Индексы и сжатие - Индексы -- + +-- #1 +/* +3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`). +Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED) +4. Что будет, если `ORDER BY log_id`, а не `ts`? +ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки. +Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY ts +PARTITION BY toYYYYMM(ts) +PRIMARY KEY (ts); + +INSERT INTO dsolovianov.logs (log_id, ts) VALUES +(1, '2024-06-01 10:00:00'), +(2, '2024-06-01 11:00:00'), +(3, '2024-06-02 12:00:00'), +(4, '2024-06-03 13:00:00'), +(5, '2024-06-04 14:00:00'); + +-- 1. Добавляем minmax индекс: +ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2; + +-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'` +SELECT * +FROM dsolovianov.logs +WHERE ts + BETWEEN '2024-06-02' AND '2024-06-03'; + +-- Раз нет доступа, но можно предположить, что вот такой запрос сработает: +SELECT query, read_rows, result_rows +FROM system.query_log +WHERE query LIKE '%logs%' +ORDER BY event_time DESC +LIMIT 1; + + +-- #2 +/* +1. Создай таблицу с `ORDER BY id`, добавь индекс: + + ```sql + ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + ``` + + +3. Посмотри `EXPLAIN` — индекс используется? +Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка. +4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений? +Будет создано 10M партиций, что убьёт производительность. +**/ +DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/clicks', + '{replica}' +) +ORDER BY id +PARTITION BY country +PRIMARY KEY (id); + +INSERT INTO dsolovianov.clicks (id, country) VALUES +(1, 'RU'), +(2, 'US'), +(3, 'RU'), +(4, 'DE'), +(5, 'RU'); + +ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster +ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + +-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`. +EXPLAIN indexes = 1 +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +EXPLAIN +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +/* +Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dsolovianov.clicks) + Indexes: + MinMax + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/3 + Granules: 1/3 + Partition + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/1 + Granules: 1/1 + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_country + Description: set GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + */ + +-- #3 + +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users', + '{replica}' +) +ORDER BY id +PRIMARY KEY (id); + +INSERT INTO dsolovianov.users (id, email) VALUES +(1, 'alice@mail.com'), +(2, 'bob@mail.com'), +(3, 'charlie@mail.com'), +(4, 'dave@mail.com'), +(5, 'eve@example.com'); + +ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE '%mail%'; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE 'alice%'; +/* +Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы. +Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные. +*/ \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql new file mode 100644 index 0000000..4e9ebb8 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql @@ -0,0 +1,146 @@ +-- Индексы и сжатие - Партиции -- + +-- #1 +/* +1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`. +2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`. +3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :) + +*/ + +DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY (region, sale_date); + +INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES +(1, 'RU', '2024-05-01', '100.0'), +(2, 'RU', '2024-05-15', '120.0'), +(3, 'US', '2024-06-01', '90.0'), +(4, 'US', '2024-07-03', '150.0'), +(5, 'DE', '2024-07-04', '160.0'); + +ALTER TABLE dsolovianov.sales DROP PARTITION '202405'; + +SELECT * +FROM dsolovianov.sales; + +-- #2 +/* +1. Создай таблицу с `PARTITION BY event_type`. +2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`). +3. Чем такой подход может быть опасен (подумай про кардинальность значений)? +При большом количестве уникальных event_type создастся много мелких партиций. +*/ +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events', + '{replica}' +) +PARTITION BY event_type +ORDER BY (event_date, id); + +INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES +(1, 'login', '2024-06-01'), +(2, 'click', '2024-06-01'), +(3, 'click', '2024-06-02'), +(4, 'purchase', '2024-06-03'), +(5, 'login', '2024-06-04'); + +ALTER TABLE dsolovianov.events DROP PARTITION 'click'; + +-- #3 +/* +1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`. +2. Что ты выиграешь с точки зрения баланса? +Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции) +3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`? +Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id. + +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id +ORDER BY (log_time, id); + +INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES +(1, 123, '2024-07-17 10:00:00'), +(2, 456, '2024-07-17 10:01:00'), +(3, 789, '2024-07-17 10:02:00'); + +SELECT + id, + user_id, + cityHash64(user_id) % 100 AS partition_number +FROM dsolovianov.logs +ORDER BY id; + +SELECT * FROM logs WHERE user_id = 456; + + +-- #4 +/* +1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`. +2. Как будет выглядеть имя партиции? +Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406') +3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему. +*/ + +DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY (region, visit_date, id); + +INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES +(1, 'RU', '2024-06-01'), +(2, 'RU', '2024-06-15'), +(3, 'US', '2024-06-03'), +(4, 'US', '2024-07-01'), +(5, 'RU', '2024-07-05'); + +ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster +DROP PARTITION ('RU', '202407'); + +SELECT * FROM dsolovianov.visits; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql new file mode 100644 index 0000000..ce28674 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql @@ -0,0 +1,39 @@ +-- Индексы и сжатие - Сжатие -- + +-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)). +-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени. +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY (created_at, id) +PARTITION BY toYYYYMM(created_at); + +INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES +(1, 'INFO', 'User logged in', '2024-07-01 10:00:00'), +(2, 'WARN', 'Request took too long', '2024-07-01 10:05:00'), +(3, 'ERROR', 'Unable to connect to server', '2024-07-01 10:10:00'); + +-- #3 Посмотри статистику сжатия: +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; + +-- Стата вот так выглядит: +column | compression_codec +id +level +message CODEC(ZSTD(3)) +created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/dsolovianov/clickhouse/image.png b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png similarity index 100% rename from dsolovianov/clickhouse/image.png rename to dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png diff --git a/dsolovianov/clickhouse/Типы таблиц - Домашки.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql similarity index 65% rename from dsolovianov/clickhouse/Типы таблиц - Домашки.sql rename to dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql index 04cda20..af9dcc3 100644 --- a/dsolovianov/clickhouse/Типы таблиц - Домашки.sql +++ b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql @@ -1,6 +1,9 @@ --Задача 1: Создай таблицу page_views +DROP TABLE page_views; -CREATE TABLE page_views ( +CREATE TABLE page_views +ON CLUSTER clickhouse_cluster +( user_id UInt64, url String, view_time DateTime @@ -11,7 +14,9 @@ ORDER BY (user_id, view_time); --Задача 2: Создай таблицу users -CREATE TABLE users ( +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( id UInt64, name String, updated_at DateTime @@ -21,7 +26,9 @@ ORDER BY id; -- Задача 3: Создай таблицу region_sales -CREATE TABLE region_sales ( +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( region String, date Date, sales UInt64 @@ -30,17 +37,19 @@ ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date) ORDER BY (region, date); ---Задача 4.1: Создай таблицу user_metrics +--Задача 4: Создай таблицу user_metrics -CREATE TABLE user_metrics ( +CREATE TABLE user_metrics +ON CLUSTER clickhouse_cluster +( region String, avg_age_state AggregateFunction(avg, UInt8), active_users_state AggregateFunction(uniq, UInt64) ) -ENGINE = AggregatingMergeTree() -ORDER BY region; +ENGINE = AggregatingMergeTree() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 ---Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` INSERT INTO user_metrics SELECT @@ -61,7 +70,7 @@ FROM ) GROUP BY region; ---Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()` +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` SELECT region, diff --git a/dsolovianov/clickhouse/Pet-Project/test b/dsolovianov/clickhouse/Pet-Project/test new file mode 100644 index 0000000..e69de29 diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql b/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql new file mode 100644 index 0000000..167607d --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 1. Загрузка тестовых данных.sql @@ -0,0 +1,90 @@ +DROP TABLE IF EXISTS dsolovianov.core_fact_lesson_views ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_course ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_dim_date ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.core_enrollments ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.core_dim_user +ON CLUSTER clickhouse_cluster +( + user_id Int32, + name String, + age Int32, + email String, + registration_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dsolovianov.core_dim_course +ON CLUSTER clickhouse_cluster +( + course_id Int32, + title String, + category String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +CREATE TABLE dsolovianov.core_dim_lesson +ON CLUSTER clickhouse_cluster +( + lesson_id Int32, + title String, + duration_min Int32, + course_id Int32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_lesson', + '{replica}' +) +ORDER BY lesson_id; + +CREATE TABLE dsolovianov.core_dim_date +ON CLUSTER clickhouse_cluster +( + date Date, + year Int32, + month Int32, + day Int32, + weekday String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_dim_date', + '{replica}' +) +ORDER BY date; + +CREATE TABLE dsolovianov.core_enrollments +ON CLUSTER clickhouse_cluster +( + user_id Int32, + course_id Int32, + enrolled_at DateTime64(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_enrollments', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dsolovianov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster +( + user_id Int32, + lesson_id Int32, + course_id Int32, + viewed_at DateTime64(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/core_fact_lesson_views', + '{replica}' +) +ORDER BY user_id; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql new file mode 100644 index 0000000..8f6e555 --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 2. Создание витринных таблиц.sql @@ -0,0 +1,193 @@ +-- Этап 2. Создание витринных таблиц -- + +-- Витрина lesson_popularity_summary +DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + lesson_title String, + course_id UInt32, + course_title String, + total_views UInt64, + unique_users UInt64, + first_view DateTime, + last_view DateTime +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/lesson_popularity_summary', + '{replica}' +) +ORDER BY (course_id, lesson_id); + +DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster SYNC; + +CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.lesson_popularity_summary +AS +SELECT + l.lesson_id AS lesson_id, + l.title AS lesson_title, + l.course_id AS course_id, + c.title AS course_title, + COUNT(v.viewed_at) AS total_views, + COUNT(DISTINCT v.user_id) AS unique_users, + MIN(v.viewed_at) AS first_view, + MAX(v.viewed_at) AS last_view +FROM dsolovianov.core_fact_lesson_views v +LEFT JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id +LEFT JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id +GROUP BY l.lesson_id, l.title, l.course_id, c.title; + +INSERT INTO dsolovianov.lesson_popularity_summary +SELECT + l.lesson_id AS lesson_id, + l.title AS lesson_title, + l.course_id AS course_id, + c.title AS course_title, + COUNT(v.viewed_at) AS total_views, + COUNT(DISTINCT v.user_id) AS unique_users, + MIN(v.viewed_at) AS first_view, + MAX(v.viewed_at) AS last_view +FROM dsolovianov.core_dim_lesson l +JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id +LEFT JOIN dsolovianov.core_fact_lesson_views v ON l.lesson_id = v.lesson_id +GROUP BY l.lesson_id, l.title, l.course_id, c.title; + +-- Витрину inactive_users_summary + +DROP TABLE IF EXISTS dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster ( + user_id UInt32, + name String, + email String, + age UInt32, + registered_courses_count UInt32, + registration_date Date +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + +DROP TABLE IF EXISTS dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster SYNC; + +CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + COUNT(DISTINCT e.course_id) AS registered_courses_count, + u.registration_date AS registration_date +FROM dsolovianov.core_dim_user u +LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views) +GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date; + +INSERT INTO dsolovianov.inactive_users_summary +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + COUNT(DISTINCT e.course_id) AS registered_courses_count, + u.registration_date AS registration_date +FROM dsolovianov.core_dim_user u +LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views) +GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date; + +-- Витрина course_completion_rate + +DROP TABLE IF EXISTS dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster ( + user_id UInt32, + user_name String, + course_id UInt32, + course_title String, + lessons_in_course UInt32, + lessons_viewed UInt32, + completion_rate Float32 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +DROP TABLE IF EXISTS dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster SYNC; + +CREATE MATERIALIZED VIEW dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster +TO dsolovianov.course_completion_rate +AS +SELECT + e.user_id AS user_id, + u.name AS user_name, + e.course_id AS course_id, + c.title AS course_title, + ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS lessons_in_course, + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ) AS lessons_viewed, + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ) / ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS completion_rate +FROM dsolovianov.core_enrollments e +JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id +JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id +GROUP BY e.user_id, u.name, e.course_id, c.title; + +INSERT INTO dsolovianov.course_completion_rate +SELECT + e.user_id AS user_id, + u.name AS user_name, + e.course_id AS course_id, + c.title AS course_title, + ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS lessons_in_course, + COALESCE( + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ), 0 + ) AS lessons_viewed, + COALESCE( + ( + SELECT COUNT(DISTINCT v.lesson_id) + FROM dsolovianov.core_fact_lesson_views v + JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id + WHERE v.user_id = e.user_id AND l.course_id = e.course_id + ), 0 + ) / ( + SELECT COUNT(*) + FROM dsolovianov.core_dim_lesson l + WHERE l.course_id = e.course_id + ) AS completion_rate +FROM dsolovianov.core_enrollments e +JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id +JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id +GROUP BY e.user_id, u.name, e.course_id, c.title; + +SELECT * FROM dsolovianov.course_completion_rate ORDER BY user_id; + diff --git a/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql new file mode 100644 index 0000000..875ee9a --- /dev/null +++ b/dsolovianov/clickhouse/Pet-Project/Этап 3. Анализ и запросы.sql @@ -0,0 +1,92 @@ +-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам +-- Напиши свои вариации для практики-- + + +-- Топ-5 самых просматриваемых уроков + +SELECT + lesson_title, + course_title, + total_views +FROM dsolovianov.lesson_popularity_summary +ORDER BY total_views DESC +LIMIT 5; + + +-- Неактивные пользователи, записавшиеся на курсы + +SELECT + name, + email, + registered_courses_count +FROM dsolovianov.inactive_users_summary +WHERE registered_courses_count > 0 +ORDER BY registration_date DESC; + +-- Курсы с самым высоким процентом завершения + +SELECT + course_title, + AVG(completion_rate) AS avg_completion +FROM dsolovianov.course_completion_rate +GROUP BY course_title +ORDER BY avg_completion DESC +LIMIT 5; + +-- Мои вариации -- + +-- Среднее количество учеников на урок по каждому курсу + +SELECT + course_title, + ROUND(AVG(unique_users), 2) AS avg_users_lesson, + SUM(total_views) AS total_course_views +FROM dsolovianov.lesson_popularity_summary +GROUP BY course_title +ORDER BY avg_users_lesson DESC; + +-- Возрастное распределение неактивных пользователей + +SELECT + CASE + WHEN age < 25 THEN 'до 25' + WHEN age BETWEEN 25 AND 30 THEN '25-30' + WHEN age BETWEEN 31 AND 35 THEN '31-35' + ELSE '35+' + END AS age_group, + COUNT(*) AS inactive_users_count, + AVG(registered_courses_count) AS avg_courses_enrolled +FROM dsolovianov.inactive_users_summary +GROUP BY age_group +ORDER BY age_group; + +-- Пользователи, которые начали, но не закончили курс (completion_rate между 0 и 1) + +SELECT + user_name, + course_title, + lessons_viewed, + lessons_in_course, + ROUND(completion_rate * 100, 0) AS no_completion, + (lessons_in_course - lessons_viewed) AS remaining_lessons +FROM dsolovianov.course_completion_rate +WHERE completion_rate BETWEEN 0.01 AND 0.99 +ORDER BY completion_rate DESC, user_name; + +-- Прогресс каждого пользователя по курсам (с указанием статуса) + +SELECT + user_name, + course_title, + lessons_viewed, + lessons_in_course, + ROUND(completion_rate * 100, 0) AS progress_percent, + CASE + WHEN completion_rate = 1.0 THEN 'Пройден' + WHEN completion_rate = 0 THEN 'Не начат' + WHEN completion_rate >= 0.5 THEN 'В процессе (>50%)' + ELSE 'В процессе (<50%)' + END AS status +FROM dsolovianov.course_completion_rate +ORDER BY user_name, completion_rate DESC; + diff --git a/dsolovianov/postgresql/DDL.sql b/dsolovianov/postgresql/DDL.sql new file mode 100644 index 0000000..0c7c3b1 --- /dev/null +++ b/dsolovianov/postgresql/DDL.sql @@ -0,0 +1,38 @@ +CREATE TABLE dsolovianov.users ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age INT, + email VARCHAR(100), + country VARCHAR(100), + is_active BOOLEAN, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE dsolovianov.products ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10,2) NOT NULL, + in_stock BOOLEAN + ); + +CREATE TABLE dsolovianov.orders ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20), + CONSTRAINT orders_user_fk + FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id) +); + +CREATE TABLE dsolovianov.order_items ( + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL CHECK (quantity > 0), + price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0), + CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id), + CONSTRAINT order_items_order_fk + FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id), + CONSTRAINT order_items_product_fk + FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id) +); \ No newline at end of file diff --git a/dsolovianov/postgresql/DML.sql b/dsolovianov/postgresql/DML.sql new file mode 100644 index 0000000..ef18ac6 --- /dev/null +++ b/dsolovianov/postgresql/DML.sql @@ -0,0 +1,59 @@ +INSERT INTO dsolovianov.users (name, age, email, country, is_active, created_at) VALUES +('Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00+00'), +('Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00+00'), +('Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00+00'), +('Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00+00'), +('Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00+00'), +('Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00+00'), +('George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00+00'), +('Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00+00'), +('Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00+00'), +('Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00+00'); + +INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES +('iPhone 15', 'Электроника', 80000, true), +('Kindle', 'Электроника', 15000, true), +('Кроссовки Nike', 'Одежда', 9000, true), +('Кроссовки NoName', 'Одежда', 3000, false), +('Наушники Pro', 'Электроника', 12000, true), +('Фитнес-браслет', 'Гаджеты', 6000, true), +('Подарочная карта', 'Сервисы', 1000, true); + +INSERT INTO dsolovianov.orders (user_id, order_date, status) VALUES +(1, '2025-01-10 12:00:00+00', 'paid'), +(3, '2025-01-12 09:30:00+00', 'cancelled'), +(1, '2025-01-15 18:20:00+00', 'paid'), +(2, '2025-01-20 11:05:00+00', 'created'), +(4, '2025-02-01 17:45:00+00', 'paid'), +(5, '2025-02-10 10:10:00+00', 'refunded'), +(7, '2025-03-01 09:00:00+00', 'paid'), +(9, '2025-03-05 19:30:00+00', 'paid'); + +INSERT INTO dsolovianov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES +-- Заказ #1: iPhone 15 (1 шт) + Наушники Pro (2 шт) +(1, 1, 1, 80000.00), +(1, 5, 2, 12000.00), + +-- Заказ #2 (cancelled): Kindle (1 шт) +(2, 2, 1, 15000.00), + +-- Заказ #3: Кроссовки Nike (2 шт) +(3, 3, 2, 9000.00), + +-- Заказ #4 (created): Фитнес-браслет (1 шт) +(4, 6, 1, 6000.00), + +-- Заказ #5: iPhone 15 (1 шт) + Подарочная карта (3 шт) +(5, 1, 1, 80000.00), +(5, 7, 3, 1000.00), + +-- Заказ #6 (refunded): Наушники Pro (1 шт) +(6, 5, 1, 12000.00), + +-- Заказ #7: Kindle (1 шт) + Фитнес-браслет (2 шт) +(7, 2, 1, 15000.00), +(7, 6, 2, 6000.00), + +-- Заказ #8: iPhone 15 (1 шт) + Кроссовки Nike (1 шт) +(8, 1, 1, 80000.00), +(8, 3, 1, 9000.00); \ No newline at end of file diff --git a/dsolovianov/postgresql/Базовый SQL.sql b/dsolovianov/postgresql/Базовый SQL.sql new file mode 100644 index 0000000..930f258 --- /dev/null +++ b/dsolovianov/postgresql/Базовый SQL.sql @@ -0,0 +1,91 @@ +/* +- Задание 1 — Спроектируй «карточку пользователя» + + Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`** + 2. Потом измени запрос так, чтобы: + - сначала отсортировать по **`created_at`** (новые сверху) + - потом по **`name`** по алфавиту внутри одной даты + +- Задание 2 — Сегментация по странам (с выбором) + + Маркетолог хочет понять, какая страна сейчас интереснее. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Выбери страну **`US`**и напиши запрос, который: + - вернёт **`id`**, **`name`**, **`country`**; + - покажет только пользователей из выбранной страны; + - отсортирует их по имени. + 2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**. + +- Задание 3 — Контакты для рассылки (думать про качество) + + Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу. + + **✅ Требования:** + + 1. Напиши один запрос, который: + - вернёт только пользователей, у которых email **есть**; + - вернёт столбцы **`id`**, **`name`**, **`email`**; + - отсортирует их по имени. + 2. Напиши второй запрос, который подготовит «чистый» вид email: + - вернёт только **`email`** в нижнем регистре (как **`email_lower`**); + - уберёт дубликаты email (нужно использовать **`DISTINCT`**). + - Задание 4 — Новички и активность + + «Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.» + + **✅ Требования:** + + 1. Напиши запрос, который: + - выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**); + - вернёт **`id`**, **`name`**, **`created_at`**; + - отсортирует по **`created_at`** от новых к старым. + 2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя. + 3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**). +*/ +SELECT id, + name, + country, + created_at, + email +FROM dsolovianov.users +ORDER BY created_at DESC, name ASC; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'US' +ORDER BY name; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'UK' +ORDER BY name; + +SELECT id, + name, + email, + LOWER(email) AS email_lower +FROM dsolovianov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT id, + name, + created_at +FROM dsolovianov.users +WHERE created_at > '2024-01-11' +ORDER BY created_at DESC +LIMIT 3 OFFSET 1; diff --git a/dsolovianov/postgresql/Фильтрация.sql b/dsolovianov/postgresql/Фильтрация.sql new file mode 100644 index 0000000..edf113a --- /dev/null +++ b/dsolovianov/postgresql/Фильтрация.sql @@ -0,0 +1,124 @@ +/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES + ('Mi Band 7', 'smartwatch', 49.00, true), + ('Surface Pro', 'tablet', 1299.00, true), + ('iPhone 15', 'smartphone', 999.00, true), + ('Logitech MX Master', 'mouse', 120.00, true);*/ + +/* +- Задача 1 — Товары для продажи + + Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать. + + **✅ Условия:** + + - товар **в наличии** + - категория **НЕ smartwatch** + - цена **от 100 до 1300** + - отдельно хотим видеть цену: + - с наценкой 30% ( price + 30%) + - с «виртуальным НДС» 50% (price + 50%) + + **✅ Вывести:** + + - `name`, `category`, `price` + - `price_with_markup` = price + 30% + - `price_with_vat` = price + 50% + +- Задача 2 — Скидочная корзина: дешёвые или дорогие + + Нужно собрать товары под акцию + + **✅ Условия:** + + - участвуют товары с ценой **меньше 100** или **больше 1000** + - скидка 15% (price - 15%) + + **✅ Вывести:** + + - `name`, `price` + - `price_with_discount` = price - 15% + +- Задача 3 — Категории для рекламы + + Маркетолог просит товары только из нужных категорий: + + **✅ Условия:** + + - category в списке: `tablet`, `smartphone`, `mouse` + - цена **не меньше 100** + + **✅ Вывести:** + + - `id`, `name`, `category`, `price` + - сортировка по цене по убыванию +- Задача 4 — Убрать iPhone из выборки + + Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”. + + **✅ Условия:** + + - название **НЕ содержит** `iPhone` + + **✅ Вывести:** + + - `name`, `category`, `price` +- Задача 5 — Поиск товаров по названию + + Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию. + + - 1) Найти товары, где в названии есть `"pro"` (без учёта регистра) + + 📌 Условия: + + - поиск должен сработать на `pro`, `PRO`, `Pro` + - вывести: `name`, `category`, `price` + - 2) Найти товары, где в названии есть `"mx"` (без учёта регистра) + + 📌 Условия: + + - вывести: `name`, `category`, `price` + - отсортировать по `price DESC` +*/ + + +SELECT name, + category, + price, + (price * 0.30) + price AS price_with_markup, + (price * 0.50) + price AS price_with_vat +FROM dsolovianov.products +WHERE category NOT IN ('smartwatch') + AND (price >= 100 AND price <= 1300); + +SELECT name, + price, + price - (price * 0.15) AS price_with_discount +FROM dsolovianov.products +WHERE price < 100 OR price > 1000; + +SELECT id, + name, + category, + price +FROM dsolovianov.products +WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100 +ORDER BY price DESC; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE category NOT LIKE '%iPhone%'; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%pro%'; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%mx%'; + diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv new file mode 100644 index 0000000..e998233 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv @@ -0,0 +1,6 @@ +"course_id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +3,BI с нуля,business,2023-03-01 00:00:00.000 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv new file mode 100644 index 0000000..319c0ad --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv @@ -0,0 +1,2923 @@ +"date","year","month","day","weekday" +2023-01-01,2023,1,1,sunday +2023-01-03,2023,1,3,tuesday +2023-01-05,2023,1,5,thursday +2023-01-09,2023,1,9,monday +2023-01-11,2023,1,11,wednesday +2023-01-13,2023,1,13,friday +2023-01-15,2023,1,15,sunday +2023-01-17,2023,1,17,tuesday +2023-01-21,2023,1,21,saturday +2023-01-23,2023,1,23,monday +2023-01-24,2023,1,24,tuesday +2023-01-26,2023,1,26,thursday +2023-01-27,2023,1,27,friday +2023-01-28,2023,1,28,saturday +2023-01-31,2023,1,31,tuesday +2023-02-02,2023,2,2,thursday +2023-02-06,2023,2,6,monday +2023-02-08,2023,2,8,wednesday +2023-02-10,2023,2,10,friday +2023-02-14,2023,2,14,tuesday +2023-02-18,2023,2,18,saturday +2023-02-21,2023,2,21,tuesday +2023-02-23,2023,2,23,thursday +2023-02-24,2023,2,24,friday +2023-02-25,2023,2,25,saturday +2023-02-26,2023,2,26,sunday +2023-02-27,2023,2,27,monday +2023-03-01,2023,3,1,wednesday +2023-03-05,2023,3,5,sunday +2023-03-07,2023,3,7,tuesday +2023-03-11,2023,3,11,saturday +2023-03-12,2023,3,12,sunday +2023-03-16,2023,3,16,thursday +2023-03-18,2023,3,18,saturday +2023-03-20,2023,3,20,monday +2023-03-21,2023,3,21,tuesday +2023-03-23,2023,3,23,thursday +2023-03-24,2023,3,24,friday +2023-03-28,2023,3,28,tuesday +2023-03-30,2023,3,30,thursday +2023-04-02,2023,4,2,sunday +2023-04-04,2023,4,4,tuesday +2023-04-05,2023,4,5,wednesday +2023-04-09,2023,4,9,sunday +2023-04-10,2023,4,10,monday +2023-04-11,2023,4,11,tuesday +2023-04-12,2023,4,12,wednesday +2023-04-16,2023,4,16,sunday +2023-04-18,2023,4,18,tuesday +2023-04-20,2023,4,20,thursday +2023-04-21,2023,4,21,friday +2023-04-22,2023,4,22,saturday +2023-04-25,2023,4,25,tuesday +2023-04-27,2023,4,27,thursday +2023-04-29,2023,4,29,saturday +2023-05-02,2023,5,2,tuesday +2023-05-06,2023,5,6,saturday +2023-05-09,2023,5,9,tuesday +2023-05-11,2023,5,11,thursday +2023-05-12,2023,5,12,friday +2023-05-14,2023,5,14,sunday +2023-05-15,2023,5,15,monday +2023-05-17,2023,5,17,wednesday +2023-05-19,2023,5,19,friday +2023-05-20,2023,5,20,saturday +2023-05-22,2023,5,22,monday +2023-05-23,2023,5,23,tuesday +2023-05-24,2023,5,24,wednesday +2023-05-25,2023,5,25,thursday +2023-05-26,2023,5,26,friday +2023-05-29,2023,5,29,monday +2023-06-01,2023,6,1,thursday +2023-06-02,2023,6,2,friday +2023-06-08,2023,6,8,thursday +2023-06-10,2023,6,10,saturday +2023-06-12,2023,6,12,monday +2023-06-14,2023,6,14,wednesday +2023-06-21,2023,6,21,wednesday +2023-06-24,2023,6,24,saturday +2023-06-25,2023,6,25,sunday +2023-06-26,2023,6,26,monday +2023-06-27,2023,6,27,tuesday +2023-06-29,2023,6,29,thursday +2023-06-30,2023,6,30,friday +2023-07-03,2023,7,3,monday +2023-07-06,2023,7,6,thursday +2023-07-08,2023,7,8,saturday +2023-07-10,2023,7,10,monday +2023-07-12,2023,7,12,wednesday +2023-07-14,2023,7,14,friday +2023-07-15,2023,7,15,saturday +2023-07-16,2023,7,16,sunday +2023-07-17,2023,7,17,monday +2023-07-20,2023,7,20,thursday +2023-07-21,2023,7,21,friday +2023-07-23,2023,7,23,sunday +2023-07-24,2023,7,24,monday +2023-07-25,2023,7,25,tuesday +2023-07-30,2023,7,30,sunday +2023-07-31,2023,7,31,monday +2023-08-01,2023,8,1,tuesday +2023-08-04,2023,8,4,friday +2023-08-08,2023,8,8,tuesday +2023-08-09,2023,8,9,wednesday +2023-08-10,2023,8,10,thursday +2023-08-12,2023,8,12,saturday +2023-08-13,2023,8,13,sunday +2023-08-21,2023,8,21,monday +2023-08-26,2023,8,26,saturday +2023-08-27,2023,8,27,sunday +2023-08-28,2023,8,28,monday +2023-08-29,2023,8,29,tuesday +2023-08-30,2023,8,30,wednesday +2023-09-01,2023,9,1,friday +2023-09-02,2023,9,2,saturday +2023-09-03,2023,9,3,sunday +2023-09-06,2023,9,6,wednesday +2023-09-08,2023,9,8,friday +2023-09-09,2023,9,9,saturday +2023-09-11,2023,9,11,monday +2023-09-13,2023,9,13,wednesday +2023-09-14,2023,9,14,thursday +2023-09-15,2023,9,15,friday +2023-09-17,2023,9,17,sunday +2023-09-19,2023,9,19,tuesday +2023-09-22,2023,9,22,friday +2023-09-23,2023,9,23,saturday +2023-09-25,2023,9,25,monday +2023-09-26,2023,9,26,tuesday +2023-09-27,2023,9,27,wednesday +2023-09-28,2023,9,28,thursday +2023-09-30,2023,9,30,saturday +2023-10-03,2023,10,3,tuesday +2023-10-11,2023,10,11,wednesday +2023-10-12,2023,10,12,thursday +2023-10-14,2023,10,14,saturday +2023-10-15,2023,10,15,sunday +2023-10-16,2023,10,16,monday +2023-10-18,2023,10,18,wednesday +2023-10-19,2023,10,19,thursday +2023-10-20,2023,10,20,friday +2023-10-21,2023,10,21,saturday +2023-10-22,2023,10,22,sunday +2023-10-23,2023,10,23,monday +2023-10-27,2023,10,27,friday +2023-10-28,2023,10,28,saturday +2023-10-31,2023,10,31,tuesday +2023-11-01,2023,11,1,wednesday +2023-11-02,2023,11,2,thursday +2023-11-05,2023,11,5,sunday +2023-11-06,2023,11,6,monday +2023-11-08,2023,11,8,wednesday +2023-11-10,2023,11,10,friday +2023-11-11,2023,11,11,saturday +2023-11-14,2023,11,14,tuesday +2023-11-15,2023,11,15,wednesday +2023-11-17,2023,11,17,friday +2023-11-19,2023,11,19,sunday +2023-11-21,2023,11,21,tuesday +2023-11-22,2023,11,22,wednesday +2023-11-24,2023,11,24,friday +2023-11-28,2023,11,28,tuesday +2023-11-30,2023,11,30,thursday +2023-12-01,2023,12,1,friday +2023-12-02,2023,12,2,saturday +2023-12-03,2023,12,3,sunday +2023-12-04,2023,12,4,monday +2023-12-07,2023,12,7,thursday +2023-12-12,2023,12,12,tuesday +2023-12-14,2023,12,14,thursday +2023-12-15,2023,12,15,friday +2023-12-17,2023,12,17,sunday +2023-12-18,2023,12,18,monday +2023-12-19,2023,12,19,tuesday +2023-12-21,2023,12,21,thursday +2023-12-22,2023,12,22,friday +2023-12-23,2023,12,23,saturday +2023-12-27,2023,12,27,wednesday +2023-12-31,2023,12,31,sunday +2024-01-02,2024,1,2,tuesday +2024-01-03,2024,1,3,wednesday +2024-01-06,2024,1,6,saturday +2024-01-07,2024,1,7,sunday +2024-01-08,2024,1,8,monday +2024-01-13,2024,1,13,saturday +2024-01-15,2024,1,15,monday +2024-01-18,2024,1,18,thursday +2024-01-19,2024,1,19,friday +2024-01-20,2024,1,20,saturday +2024-01-21,2024,1,21,sunday +2024-01-22,2024,1,22,monday +2024-01-24,2024,1,24,wednesday +2024-01-25,2024,1,25,thursday +2024-01-26,2024,1,26,friday +2024-01-27,2024,1,27,saturday +2024-01-28,2024,1,28,sunday +2024-01-31,2024,1,31,wednesday +2024-02-02,2024,2,2,friday +2024-02-04,2024,2,4,sunday +2024-02-06,2024,2,6,tuesday +2024-02-07,2024,2,7,wednesday +2024-02-11,2024,2,11,sunday +2024-02-12,2024,2,12,monday +2024-02-17,2024,2,17,saturday +2024-02-18,2024,2,18,sunday +2024-02-21,2024,2,21,wednesday +2024-02-22,2024,2,22,thursday +2024-02-25,2024,2,25,sunday +2024-02-26,2024,2,26,monday +2024-02-28,2024,2,28,wednesday +2024-03-01,2024,3,1,friday +2024-03-02,2024,3,2,saturday +2024-03-06,2024,3,6,wednesday +2023-01-02,2023,1,2,monday +2023-01-04,2023,1,4,wednesday +2023-01-06,2023,1,6,friday +2023-01-07,2023,1,7,saturday +2023-01-08,2023,1,8,sunday +2023-01-10,2023,1,10,tuesday +2023-01-12,2023,1,12,thursday +2023-01-14,2023,1,14,saturday +2023-01-16,2023,1,16,monday +2023-01-18,2023,1,18,wednesday +2023-01-19,2023,1,19,thursday +2023-01-20,2023,1,20,friday +2023-01-22,2023,1,22,sunday +2023-01-25,2023,1,25,wednesday +2023-01-29,2023,1,29,sunday +2023-01-30,2023,1,30,monday +2023-02-01,2023,2,1,wednesday +2023-02-03,2023,2,3,friday +2023-02-04,2023,2,4,saturday +2023-02-05,2023,2,5,sunday +2023-02-07,2023,2,7,tuesday +2023-02-09,2023,2,9,thursday +2023-02-11,2023,2,11,saturday +2023-02-12,2023,2,12,sunday +2023-02-13,2023,2,13,monday +2023-02-15,2023,2,15,wednesday +2023-02-16,2023,2,16,thursday +2023-02-17,2023,2,17,friday +2023-02-19,2023,2,19,sunday +2023-02-20,2023,2,20,monday +2023-02-22,2023,2,22,wednesday +2023-02-28,2023,2,28,tuesday +2023-03-02,2023,3,2,thursday +2023-03-03,2023,3,3,friday +2023-03-04,2023,3,4,saturday +2023-03-06,2023,3,6,monday +2023-03-08,2023,3,8,wednesday +2023-03-09,2023,3,9,thursday +2023-03-10,2023,3,10,friday +2023-03-13,2023,3,13,monday +2023-03-14,2023,3,14,tuesday +2023-03-15,2023,3,15,wednesday +2023-03-17,2023,3,17,friday +2023-03-19,2023,3,19,sunday +2023-03-22,2023,3,22,wednesday +2023-03-25,2023,3,25,saturday +2023-03-26,2023,3,26,sunday +2023-03-27,2023,3,27,monday +2023-03-29,2023,3,29,wednesday +2023-03-31,2023,3,31,friday +2023-04-01,2023,4,1,saturday +2023-04-03,2023,4,3,monday +2023-04-06,2023,4,6,thursday +2023-04-07,2023,4,7,friday +2023-04-08,2023,4,8,saturday +2023-04-13,2023,4,13,thursday +2023-04-14,2023,4,14,friday +2023-04-15,2023,4,15,saturday +2023-04-17,2023,4,17,monday +2023-04-19,2023,4,19,wednesday +2023-04-23,2023,4,23,sunday +2023-04-24,2023,4,24,monday +2023-04-26,2023,4,26,wednesday +2023-04-28,2023,4,28,friday +2023-04-30,2023,4,30,sunday +2023-05-01,2023,5,1,monday +2023-05-03,2023,5,3,wednesday +2023-05-04,2023,5,4,thursday +2023-05-05,2023,5,5,friday +2023-05-07,2023,5,7,sunday +2023-05-08,2023,5,8,monday +2023-05-10,2023,5,10,wednesday +2023-05-13,2023,5,13,saturday +2023-05-16,2023,5,16,tuesday +2023-05-18,2023,5,18,thursday +2023-05-21,2023,5,21,sunday +2023-05-27,2023,5,27,saturday +2023-05-28,2023,5,28,sunday +2023-05-30,2023,5,30,tuesday +2023-05-31,2023,5,31,wednesday +2023-06-03,2023,6,3,saturday +2023-06-04,2023,6,4,sunday +2023-06-05,2023,6,5,monday +2023-06-06,2023,6,6,tuesday +2023-06-07,2023,6,7,wednesday +2023-06-09,2023,6,9,friday +2023-06-11,2023,6,11,sunday +2023-06-13,2023,6,13,tuesday +2023-06-15,2023,6,15,thursday +2023-06-16,2023,6,16,friday +2023-06-17,2023,6,17,saturday +2023-06-18,2023,6,18,sunday +2023-06-19,2023,6,19,monday +2023-06-20,2023,6,20,tuesday +2023-06-22,2023,6,22,thursday +2023-06-23,2023,6,23,friday +2023-06-28,2023,6,28,wednesday +2023-07-01,2023,7,1,saturday +2023-07-02,2023,7,2,sunday +2023-07-04,2023,7,4,tuesday +2023-07-05,2023,7,5,wednesday +2023-07-07,2023,7,7,friday +2023-07-09,2023,7,9,sunday +2023-07-11,2023,7,11,tuesday +2023-07-13,2023,7,13,thursday +2023-07-18,2023,7,18,tuesday +2023-07-19,2023,7,19,wednesday +2023-07-22,2023,7,22,saturday +2023-07-26,2023,7,26,wednesday +2023-07-27,2023,7,27,thursday +2023-07-28,2023,7,28,friday +2023-07-29,2023,7,29,saturday +2023-08-02,2023,8,2,wednesday +2023-08-03,2023,8,3,thursday +2023-08-05,2023,8,5,saturday +2023-08-06,2023,8,6,sunday +2023-08-07,2023,8,7,monday +2023-08-11,2023,8,11,friday +2023-08-14,2023,8,14,monday +2023-08-15,2023,8,15,tuesday +2023-08-16,2023,8,16,wednesday +2023-08-17,2023,8,17,thursday +2023-08-18,2023,8,18,friday +2023-08-19,2023,8,19,saturday +2023-08-20,2023,8,20,sunday +2023-08-22,2023,8,22,tuesday +2023-08-23,2023,8,23,wednesday +2023-08-24,2023,8,24,thursday +2023-08-25,2023,8,25,friday +2023-08-31,2023,8,31,thursday +2023-09-04,2023,9,4,monday +2023-09-05,2023,9,5,tuesday +2023-09-07,2023,9,7,thursday +2023-09-10,2023,9,10,sunday +2023-09-12,2023,9,12,tuesday +2023-09-16,2023,9,16,saturday +2023-09-18,2023,9,18,monday +2023-09-20,2023,9,20,wednesday +2023-09-21,2023,9,21,thursday +2023-09-24,2023,9,24,sunday +2023-09-29,2023,9,29,friday +2023-10-01,2023,10,1,sunday +2023-10-02,2023,10,2,monday +2023-10-04,2023,10,4,wednesday +2023-10-05,2023,10,5,thursday +2023-10-06,2023,10,6,friday +2023-10-07,2023,10,7,saturday +2023-10-08,2023,10,8,sunday +2023-10-09,2023,10,9,monday +2023-10-10,2023,10,10,tuesday +2023-10-13,2023,10,13,friday +2023-10-17,2023,10,17,tuesday +2023-10-24,2023,10,24,tuesday +2023-10-25,2023,10,25,wednesday +2023-10-26,2023,10,26,thursday +2023-10-29,2023,10,29,sunday +2023-10-30,2023,10,30,monday +2023-11-03,2023,11,3,friday +2023-11-04,2023,11,4,saturday +2023-11-07,2023,11,7,tuesday +2023-11-09,2023,11,9,thursday +2023-11-12,2023,11,12,sunday +2023-11-13,2023,11,13,monday +2023-11-16,2023,11,16,thursday +2023-11-18,2023,11,18,saturday +2023-11-20,2023,11,20,monday +2023-11-23,2023,11,23,thursday +2023-11-25,2023,11,25,saturday +2023-11-26,2023,11,26,sunday +2023-11-27,2023,11,27,monday +2023-11-29,2023,11,29,wednesday +2023-12-05,2023,12,5,tuesday +2023-12-06,2023,12,6,wednesday +2023-12-08,2023,12,8,friday +2023-12-09,2023,12,9,saturday +2023-12-10,2023,12,10,sunday +2023-12-11,2023,12,11,monday +2023-12-13,2023,12,13,wednesday +2023-12-16,2023,12,16,saturday +2023-12-20,2023,12,20,wednesday +2023-12-24,2023,12,24,sunday +2023-12-25,2023,12,25,monday +2023-12-26,2023,12,26,tuesday +2023-12-28,2023,12,28,thursday +2023-12-29,2023,12,29,friday +2023-12-30,2023,12,30,saturday +2024-01-01,2024,1,1,monday +2024-01-04,2024,1,4,thursday +2024-01-05,2024,1,5,friday +2024-01-09,2024,1,9,tuesday +2024-01-10,2024,1,10,wednesday +2024-01-11,2024,1,11,thursday +2024-01-12,2024,1,12,friday +2024-01-14,2024,1,14,sunday +2024-01-16,2024,1,16,tuesday +2024-01-17,2024,1,17,wednesday +2024-01-23,2024,1,23,tuesday +2024-01-29,2024,1,29,monday +2024-01-30,2024,1,30,tuesday +2024-02-01,2024,2,1,thursday +2024-02-03,2024,2,3,saturday +2024-02-05,2024,2,5,monday +2024-02-08,2024,2,8,thursday +2024-02-09,2024,2,9,friday +2024-02-10,2024,2,10,saturday +2024-02-13,2024,2,13,tuesday +2024-02-14,2024,2,14,wednesday +2024-02-15,2024,2,15,thursday +2024-02-16,2024,2,16,friday +2024-02-19,2024,2,19,monday +2024-02-20,2024,2,20,tuesday +2024-02-23,2024,2,23,friday +2024-02-24,2024,2,24,saturday +2024-03-07,2024,3,7,thursday +2024-03-08,2024,3,8,friday +2024-03-09,2024,3,9,saturday +2024-03-10,2024,3,10,sunday +2024-03-11,2024,3,11,monday +2024-03-12,2024,3,12,tuesday +2024-03-13,2024,3,13,wednesday +2024-03-15,2024,3,15,friday +2024-03-17,2024,3,17,sunday +2024-03-18,2024,3,18,monday +2024-03-19,2024,3,19,tuesday +2024-03-22,2024,3,22,friday +2024-03-23,2024,3,23,saturday +2024-03-24,2024,3,24,sunday +2024-03-27,2024,3,27,wednesday +2024-03-28,2024,3,28,thursday +2024-03-29,2024,3,29,friday +2024-03-30,2024,3,30,saturday +2024-03-31,2024,3,31,sunday +2024-04-01,2024,4,1,monday +2024-04-02,2024,4,2,tuesday +2024-04-03,2024,4,3,wednesday +2024-04-04,2024,4,4,thursday +2024-04-06,2024,4,6,saturday +2024-04-07,2024,4,7,sunday +2024-04-09,2024,4,9,tuesday +2024-04-10,2024,4,10,wednesday +2024-04-13,2024,4,13,saturday +2024-04-16,2024,4,16,tuesday +2024-04-17,2024,4,17,wednesday +2024-04-18,2024,4,18,thursday +2024-04-19,2024,4,19,friday +2024-04-20,2024,4,20,saturday +2024-04-23,2024,4,23,tuesday +2024-04-27,2024,4,27,saturday +2024-04-28,2024,4,28,sunday +2024-04-29,2024,4,29,monday +2024-05-02,2024,5,2,thursday +2024-05-03,2024,5,3,friday +2024-05-04,2024,5,4,saturday +2024-05-05,2024,5,5,sunday +2024-05-07,2024,5,7,tuesday +2024-05-15,2024,5,15,wednesday +2024-05-16,2024,5,16,thursday +2024-05-17,2024,5,17,friday +2024-05-20,2024,5,20,monday +2024-05-21,2024,5,21,tuesday +2024-05-23,2024,5,23,thursday +2024-05-28,2024,5,28,tuesday +2024-05-29,2024,5,29,wednesday +2024-06-01,2024,6,1,saturday +2024-06-02,2024,6,2,sunday +2024-06-03,2024,6,3,monday +2024-06-05,2024,6,5,wednesday +2024-06-07,2024,6,7,friday +2024-06-10,2024,6,10,monday +2024-06-12,2024,6,12,wednesday +2024-06-14,2024,6,14,friday +2024-06-15,2024,6,15,saturday +2024-06-16,2024,6,16,sunday +2024-06-18,2024,6,18,tuesday +2024-06-19,2024,6,19,wednesday +2024-06-20,2024,6,20,thursday +2024-06-22,2024,6,22,saturday +2024-06-25,2024,6,25,tuesday +2024-06-26,2024,6,26,wednesday +2024-06-27,2024,6,27,thursday +2024-06-29,2024,6,29,saturday +2024-06-30,2024,6,30,sunday +2024-07-01,2024,7,1,monday +2024-07-02,2024,7,2,tuesday +2024-07-04,2024,7,4,thursday +2024-07-06,2024,7,6,saturday +2024-07-08,2024,7,8,monday +2024-07-09,2024,7,9,tuesday +2024-07-13,2024,7,13,saturday +2024-07-14,2024,7,14,sunday +2024-07-15,2024,7,15,monday +2024-07-18,2024,7,18,thursday +2024-07-21,2024,7,21,sunday +2024-07-23,2024,7,23,tuesday +2024-07-25,2024,7,25,thursday +2024-07-26,2024,7,26,friday +2024-07-28,2024,7,28,sunday +2024-08-01,2024,8,1,thursday +2024-08-03,2024,8,3,saturday +2024-08-06,2024,8,6,tuesday +2024-08-09,2024,8,9,friday +2024-08-10,2024,8,10,saturday +2024-08-11,2024,8,11,sunday +2024-08-12,2024,8,12,monday +2024-08-14,2024,8,14,wednesday +2024-08-20,2024,8,20,tuesday +2024-08-22,2024,8,22,thursday +2024-08-24,2024,8,24,saturday +2024-08-28,2024,8,28,wednesday +2024-08-29,2024,8,29,thursday +2024-08-30,2024,8,30,friday +2024-08-31,2024,8,31,saturday +2024-09-01,2024,9,1,sunday +2024-09-04,2024,9,4,wednesday +2024-09-07,2024,9,7,saturday +2024-09-10,2024,9,10,tuesday +2024-09-11,2024,9,11,wednesday +2024-09-13,2024,9,13,friday +2024-09-16,2024,9,16,monday +2024-09-19,2024,9,19,thursday +2024-09-20,2024,9,20,friday +2024-09-21,2024,9,21,saturday +2024-09-22,2024,9,22,sunday +2024-09-23,2024,9,23,monday +2024-09-24,2024,9,24,tuesday +2024-09-25,2024,9,25,wednesday +2024-09-26,2024,9,26,thursday +2024-09-28,2024,9,28,saturday +2024-09-30,2024,9,30,monday +2024-10-01,2024,10,1,tuesday +2024-10-03,2024,10,3,thursday +2024-10-04,2024,10,4,friday +2024-10-06,2024,10,6,sunday +2024-10-09,2024,10,9,wednesday +2024-10-11,2024,10,11,friday +2024-10-12,2024,10,12,saturday +2024-10-13,2024,10,13,sunday +2024-10-23,2024,10,23,wednesday +2024-10-24,2024,10,24,thursday +2024-10-26,2024,10,26,saturday +2024-10-27,2024,10,27,sunday +2024-10-29,2024,10,29,tuesday +2024-10-30,2024,10,30,wednesday +2024-10-31,2024,10,31,thursday +2024-11-01,2024,11,1,friday +2024-11-02,2024,11,2,saturday +2024-11-04,2024,11,4,monday +2024-11-05,2024,11,5,tuesday +2024-11-06,2024,11,6,wednesday +2024-11-07,2024,11,7,thursday +2024-11-08,2024,11,8,friday +2024-11-12,2024,11,12,tuesday +2024-11-13,2024,11,13,wednesday +2024-11-14,2024,11,14,thursday +2024-11-15,2024,11,15,friday +2024-11-16,2024,11,16,saturday +2024-11-19,2024,11,19,tuesday +2024-11-20,2024,11,20,wednesday +2024-11-21,2024,11,21,thursday +2024-11-22,2024,11,22,friday +2024-11-23,2024,11,23,saturday +2024-11-26,2024,11,26,tuesday +2024-11-29,2024,11,29,friday +2024-12-04,2024,12,4,wednesday +2024-12-07,2024,12,7,saturday +2024-12-12,2024,12,12,thursday +2024-12-15,2024,12,15,sunday +2024-12-21,2024,12,21,saturday +2024-12-22,2024,12,22,sunday +2024-12-25,2024,12,25,wednesday +2024-12-27,2024,12,27,friday +2024-12-28,2024,12,28,saturday +2024-12-30,2024,12,30,monday +2025-01-03,2025,1,3,friday +2025-01-04,2025,1,4,saturday +2025-01-07,2025,1,7,tuesday +2025-01-09,2025,1,9,thursday +2025-01-12,2025,1,12,sunday +2025-01-13,2025,1,13,monday +2025-01-16,2025,1,16,thursday +2025-01-17,2025,1,17,friday +2025-01-18,2025,1,18,saturday +2025-01-20,2025,1,20,monday +2025-01-23,2025,1,23,thursday +2025-01-24,2025,1,24,friday +2025-01-25,2025,1,25,saturday +2025-01-26,2025,1,26,sunday +2025-01-29,2025,1,29,wednesday +2025-01-31,2025,1,31,friday +2025-02-01,2025,2,1,saturday +2025-02-02,2025,2,2,sunday +2025-02-04,2025,2,4,tuesday +2025-02-06,2025,2,6,thursday +2025-02-07,2025,2,7,friday +2025-02-08,2025,2,8,saturday +2025-02-14,2025,2,14,friday +2025-02-15,2025,2,15,saturday +2025-02-16,2025,2,16,sunday +2025-02-17,2025,2,17,monday +2025-02-18,2025,2,18,tuesday +2025-02-20,2025,2,20,thursday +2025-02-22,2025,2,22,saturday +2025-02-23,2025,2,23,sunday +2025-02-26,2025,2,26,wednesday +2025-02-27,2025,2,27,thursday +2025-03-05,2025,3,5,wednesday +2025-03-06,2025,3,6,thursday +2025-03-07,2025,3,7,friday +2025-03-08,2025,3,8,saturday +2025-03-11,2025,3,11,tuesday +2025-03-12,2025,3,12,wednesday +2025-03-13,2025,3,13,thursday +2025-03-16,2025,3,16,sunday +2025-03-18,2025,3,18,tuesday +2025-03-21,2025,3,21,friday +2025-03-22,2025,3,22,saturday +2025-03-23,2025,3,23,sunday +2025-03-25,2025,3,25,tuesday +2025-03-26,2025,3,26,wednesday +2025-03-29,2025,3,29,saturday +2025-04-01,2025,4,1,tuesday +2025-04-02,2025,4,2,wednesday +2025-04-03,2025,4,3,thursday +2025-04-05,2025,4,5,saturday +2025-04-07,2025,4,7,monday +2025-04-12,2025,4,12,saturday +2024-02-27,2024,2,27,tuesday +2024-02-29,2024,2,29,thursday +2024-03-03,2024,3,3,sunday +2024-03-04,2024,3,4,monday +2024-03-05,2024,3,5,tuesday +2024-03-14,2024,3,14,thursday +2024-03-16,2024,3,16,saturday +2024-03-20,2024,3,20,wednesday +2024-03-21,2024,3,21,thursday +2024-03-25,2024,3,25,monday +2024-03-26,2024,3,26,tuesday +2024-04-05,2024,4,5,friday +2024-04-08,2024,4,8,monday +2024-04-11,2024,4,11,thursday +2024-04-12,2024,4,12,friday +2024-04-14,2024,4,14,sunday +2024-04-15,2024,4,15,monday +2024-04-21,2024,4,21,sunday +2024-04-22,2024,4,22,monday +2024-04-24,2024,4,24,wednesday +2024-04-25,2024,4,25,thursday +2024-04-26,2024,4,26,friday +2024-04-30,2024,4,30,tuesday +2024-05-01,2024,5,1,wednesday +2024-05-06,2024,5,6,monday +2024-05-08,2024,5,8,wednesday +2024-05-09,2024,5,9,thursday +2024-05-10,2024,5,10,friday +2024-05-11,2024,5,11,saturday +2024-05-12,2024,5,12,sunday +2024-05-13,2024,5,13,monday +2024-05-14,2024,5,14,tuesday +2024-05-18,2024,5,18,saturday +2024-05-19,2024,5,19,sunday +2024-05-22,2024,5,22,wednesday +2024-05-24,2024,5,24,friday +2024-05-25,2024,5,25,saturday +2024-05-26,2024,5,26,sunday +2024-05-27,2024,5,27,monday +2024-05-30,2024,5,30,thursday +2024-05-31,2024,5,31,friday +2024-06-04,2024,6,4,tuesday +2024-06-06,2024,6,6,thursday +2024-06-08,2024,6,8,saturday +2024-06-09,2024,6,9,sunday +2024-06-11,2024,6,11,tuesday +2024-06-13,2024,6,13,thursday +2024-06-17,2024,6,17,monday +2024-06-21,2024,6,21,friday +2024-06-23,2024,6,23,sunday +2024-06-24,2024,6,24,monday +2024-06-28,2024,6,28,friday +2024-07-03,2024,7,3,wednesday +2024-07-05,2024,7,5,friday +2024-07-07,2024,7,7,sunday +2024-07-10,2024,7,10,wednesday +2024-07-11,2024,7,11,thursday +2024-07-12,2024,7,12,friday +2024-07-16,2024,7,16,tuesday +2024-07-17,2024,7,17,wednesday +2024-07-19,2024,7,19,friday +2024-07-20,2024,7,20,saturday +2024-07-22,2024,7,22,monday +2024-07-24,2024,7,24,wednesday +2024-07-27,2024,7,27,saturday +2024-07-29,2024,7,29,monday +2024-07-30,2024,7,30,tuesday +2024-07-31,2024,7,31,wednesday +2024-08-02,2024,8,2,friday +2024-08-04,2024,8,4,sunday +2024-08-05,2024,8,5,monday +2024-08-07,2024,8,7,wednesday +2024-08-08,2024,8,8,thursday +2024-08-13,2024,8,13,tuesday +2024-08-15,2024,8,15,thursday +2024-08-16,2024,8,16,friday +2024-08-17,2024,8,17,saturday +2024-08-18,2024,8,18,sunday +2024-08-19,2024,8,19,monday +2024-08-21,2024,8,21,wednesday +2024-08-23,2024,8,23,friday +2024-08-25,2024,8,25,sunday +2024-08-26,2024,8,26,monday +2024-08-27,2024,8,27,tuesday +2024-09-02,2024,9,2,monday +2024-09-03,2024,9,3,tuesday +2024-09-05,2024,9,5,thursday +2024-09-06,2024,9,6,friday +2024-09-08,2024,9,8,sunday +2024-09-09,2024,9,9,monday +2024-09-12,2024,9,12,thursday +2024-09-14,2024,9,14,saturday +2024-09-15,2024,9,15,sunday +2024-09-17,2024,9,17,tuesday +2024-09-18,2024,9,18,wednesday +2024-09-27,2024,9,27,friday +2024-09-29,2024,9,29,sunday +2024-10-02,2024,10,2,wednesday +2024-10-05,2024,10,5,saturday +2024-10-07,2024,10,7,monday +2024-10-08,2024,10,8,tuesday +2024-10-10,2024,10,10,thursday +2024-10-14,2024,10,14,monday +2024-10-15,2024,10,15,tuesday +2024-10-16,2024,10,16,wednesday +2024-10-17,2024,10,17,thursday +2024-10-18,2024,10,18,friday +2024-10-19,2024,10,19,saturday +2024-10-20,2024,10,20,sunday +2024-10-21,2024,10,21,monday +2024-10-22,2024,10,22,tuesday +2024-10-25,2024,10,25,friday +2024-10-28,2024,10,28,monday +2024-11-03,2024,11,3,sunday +2024-11-09,2024,11,9,saturday +2024-11-10,2024,11,10,sunday +2024-11-11,2024,11,11,monday +2024-11-17,2024,11,17,sunday +2024-11-18,2024,11,18,monday +2024-11-24,2024,11,24,sunday +2024-11-25,2024,11,25,monday +2024-11-27,2024,11,27,wednesday +2024-11-28,2024,11,28,thursday +2024-11-30,2024,11,30,saturday +2024-12-01,2024,12,1,sunday +2024-12-02,2024,12,2,monday +2024-12-03,2024,12,3,tuesday +2024-12-05,2024,12,5,thursday +2024-12-06,2024,12,6,friday +2024-12-08,2024,12,8,sunday +2024-12-09,2024,12,9,monday +2024-12-10,2024,12,10,tuesday +2024-12-11,2024,12,11,wednesday +2024-12-13,2024,12,13,friday +2024-12-14,2024,12,14,saturday +2024-12-16,2024,12,16,monday +2024-12-17,2024,12,17,tuesday +2024-12-18,2024,12,18,wednesday +2024-12-19,2024,12,19,thursday +2024-12-20,2024,12,20,friday +2024-12-23,2024,12,23,monday +2024-12-24,2024,12,24,tuesday +2024-12-26,2024,12,26,thursday +2024-12-29,2024,12,29,sunday +2024-12-31,2024,12,31,tuesday +2025-01-01,2025,1,1,wednesday +2025-01-02,2025,1,2,thursday +2025-01-05,2025,1,5,sunday +2025-01-06,2025,1,6,monday +2025-01-08,2025,1,8,wednesday +2025-01-10,2025,1,10,friday +2025-01-11,2025,1,11,saturday +2025-01-14,2025,1,14,tuesday +2025-01-15,2025,1,15,wednesday +2025-01-19,2025,1,19,sunday +2025-01-21,2025,1,21,tuesday +2025-01-22,2025,1,22,wednesday +2025-01-27,2025,1,27,monday +2025-01-28,2025,1,28,tuesday +2025-01-30,2025,1,30,thursday +2025-02-03,2025,2,3,monday +2025-02-05,2025,2,5,wednesday +2025-02-09,2025,2,9,sunday +2025-02-10,2025,2,10,monday +2025-02-11,2025,2,11,tuesday +2025-02-12,2025,2,12,wednesday +2025-02-13,2025,2,13,thursday +2025-02-19,2025,2,19,wednesday +2025-02-21,2025,2,21,friday +2025-02-24,2025,2,24,monday +2025-02-25,2025,2,25,tuesday +2025-02-28,2025,2,28,friday +2025-03-01,2025,3,1,saturday +2025-03-02,2025,3,2,sunday +2025-03-03,2025,3,3,monday +2025-03-04,2025,3,4,tuesday +2025-03-09,2025,3,9,sunday +2025-03-10,2025,3,10,monday +2025-03-14,2025,3,14,friday +2025-03-15,2025,3,15,saturday +2025-03-17,2025,3,17,monday +2025-03-19,2025,3,19,wednesday +2025-03-20,2025,3,20,thursday +2025-03-24,2025,3,24,monday +2025-03-27,2025,3,27,thursday +2025-03-28,2025,3,28,friday +2025-03-30,2025,3,30,sunday +2025-03-31,2025,3,31,monday +2025-04-04,2025,4,4,friday +2025-04-06,2025,4,6,sunday +2025-04-08,2025,4,8,tuesday +2025-04-09,2025,4,9,wednesday +2025-04-10,2025,4,10,thursday +2025-04-11,2025,4,11,friday +2025-04-13,2025,4,13,sunday +2025-04-14,2025,4,14,monday +2025-04-15,2025,4,15,tuesday +2025-04-16,2025,4,16,wednesday +2025-04-17,2025,4,17,thursday +2025-04-18,2025,4,18,friday +2025-04-24,2025,4,24,thursday +2025-04-25,2025,4,25,friday +2025-04-28,2025,4,28,monday +2025-04-29,2025,4,29,tuesday +2025-04-30,2025,4,30,wednesday +2025-05-01,2025,5,1,thursday +2025-05-05,2025,5,5,monday +2025-05-07,2025,5,7,wednesday +2025-05-08,2025,5,8,thursday +2025-05-09,2025,5,9,friday +2025-05-10,2025,5,10,saturday +2025-05-11,2025,5,11,sunday +2025-05-14,2025,5,14,wednesday +2025-04-19,2025,4,19,saturday +2025-04-20,2025,4,20,sunday +2025-04-21,2025,4,21,monday +2025-04-22,2025,4,22,tuesday +2025-04-23,2025,4,23,wednesday +2025-04-26,2025,4,26,saturday +2025-04-27,2025,4,27,sunday +2025-05-02,2025,5,2,friday +2025-05-03,2025,5,3,saturday +2025-05-04,2025,5,4,sunday +2025-05-06,2025,5,6,tuesday +2025-05-12,2025,5,12,monday +2025-05-13,2025,5,13,tuesday +2025-05-17,2025,5,17,saturday +2025-05-19,2025,5,19,monday +2025-05-21,2025,5,21,wednesday +2025-05-25,2025,5,25,sunday +2025-05-27,2025,5,27,tuesday +2025-05-30,2025,5,30,friday +2025-06-01,2025,6,1,sunday +2025-06-02,2025,6,2,monday +2025-06-03,2025,6,3,tuesday +2025-06-05,2025,6,5,thursday +2025-06-08,2025,6,8,sunday +2025-06-10,2025,6,10,tuesday +2025-06-11,2025,6,11,wednesday +2025-06-13,2025,6,13,friday +2025-06-14,2025,6,14,saturday +2025-06-17,2025,6,17,tuesday +2025-06-19,2025,6,19,thursday +2025-06-20,2025,6,20,friday +2025-06-22,2025,6,22,sunday +2025-06-23,2025,6,23,monday +2025-06-24,2025,6,24,tuesday +2025-06-28,2025,6,28,saturday +2025-06-29,2025,6,29,sunday +2025-06-30,2025,6,30,monday +2025-07-01,2025,7,1,tuesday +2025-07-02,2025,7,2,wednesday +2025-07-03,2025,7,3,thursday +2025-07-04,2025,7,4,friday +2025-07-05,2025,7,5,saturday +2025-07-09,2025,7,9,wednesday +2025-07-11,2025,7,11,friday +2025-07-12,2025,7,12,saturday +2025-07-13,2025,7,13,sunday +2025-07-14,2025,7,14,monday +2025-07-15,2025,7,15,tuesday +2025-07-17,2025,7,17,thursday +2025-07-19,2025,7,19,saturday +2025-07-20,2025,7,20,sunday +2025-07-22,2025,7,22,tuesday +2025-07-24,2025,7,24,thursday +2025-07-25,2025,7,25,friday +2025-07-28,2025,7,28,monday +2025-07-29,2025,7,29,tuesday +2025-07-30,2025,7,30,wednesday +2025-08-02,2025,8,2,saturday +2025-08-03,2025,8,3,sunday +2025-08-06,2025,8,6,wednesday +2025-08-07,2025,8,7,thursday +2025-08-08,2025,8,8,friday +2025-08-09,2025,8,9,saturday +2025-08-10,2025,8,10,sunday +2025-08-15,2025,8,15,friday +2025-08-22,2025,8,22,friday +2025-08-25,2025,8,25,monday +2025-08-27,2025,8,27,wednesday +2025-08-29,2025,8,29,friday +2025-08-30,2025,8,30,saturday +2025-09-02,2025,9,2,tuesday +2025-09-03,2025,9,3,wednesday +2025-09-04,2025,9,4,thursday +2025-09-06,2025,9,6,saturday +2025-09-11,2025,9,11,thursday +2025-09-14,2025,9,14,sunday +2025-09-15,2025,9,15,monday +2025-09-20,2025,9,20,saturday +2025-09-21,2025,9,21,sunday +2025-09-23,2025,9,23,tuesday +2025-09-30,2025,9,30,tuesday +2025-10-02,2025,10,2,thursday +2025-10-03,2025,10,3,friday +2025-10-04,2025,10,4,saturday +2025-10-06,2025,10,6,monday +2025-10-09,2025,10,9,thursday +2025-10-13,2025,10,13,monday +2025-10-14,2025,10,14,tuesday +2025-10-15,2025,10,15,wednesday +2025-10-19,2025,10,19,sunday +2025-10-20,2025,10,20,monday +2025-10-23,2025,10,23,thursday +2025-10-24,2025,10,24,friday +2025-10-25,2025,10,25,saturday +2025-10-30,2025,10,30,thursday +2025-11-01,2025,11,1,saturday +2025-11-02,2025,11,2,sunday +2025-11-04,2025,11,4,tuesday +2025-11-05,2025,11,5,wednesday +2025-11-06,2025,11,6,thursday +2025-11-08,2025,11,8,saturday +2025-11-12,2025,11,12,wednesday +2025-11-13,2025,11,13,thursday +2025-11-14,2025,11,14,friday +2025-11-16,2025,11,16,sunday +2025-11-17,2025,11,17,monday +2025-11-18,2025,11,18,tuesday +2025-11-21,2025,11,21,friday +2025-11-22,2025,11,22,saturday +2025-11-26,2025,11,26,wednesday +2025-11-27,2025,11,27,thursday +2025-11-30,2025,11,30,sunday +2025-12-01,2025,12,1,monday +2025-12-02,2025,12,2,tuesday +2025-12-04,2025,12,4,thursday +2025-12-08,2025,12,8,monday +2025-12-11,2025,12,11,thursday +2025-12-12,2025,12,12,friday +2025-12-13,2025,12,13,saturday +2025-12-18,2025,12,18,thursday +2025-12-21,2025,12,21,sunday +2025-12-22,2025,12,22,monday +2025-12-23,2025,12,23,tuesday +2025-12-25,2025,12,25,thursday +2025-12-27,2025,12,27,saturday +2025-12-28,2025,12,28,sunday +2025-12-29,2025,12,29,monday +2026-01-04,2026,1,4,sunday +2026-01-06,2026,1,6,tuesday +2026-01-08,2026,1,8,thursday +2026-01-10,2026,1,10,saturday +2026-01-11,2026,1,11,sunday +2026-01-14,2026,1,14,wednesday +2026-01-15,2026,1,15,thursday +2026-01-17,2026,1,17,saturday +2026-01-19,2026,1,19,monday +2026-01-20,2026,1,20,tuesday +2026-01-21,2026,1,21,wednesday +2026-01-23,2026,1,23,friday +2026-01-24,2026,1,24,saturday +2026-01-25,2026,1,25,sunday +2026-01-26,2026,1,26,monday +2026-01-28,2026,1,28,wednesday +2026-01-29,2026,1,29,thursday +2026-01-30,2026,1,30,friday +2026-01-31,2026,1,31,saturday +2026-02-03,2026,2,3,tuesday +2026-02-04,2026,2,4,wednesday +2026-02-05,2026,2,5,thursday +2026-02-06,2026,2,6,friday +2026-02-07,2026,2,7,saturday +2026-02-09,2026,2,9,monday +2026-02-10,2026,2,10,tuesday +2026-02-12,2026,2,12,thursday +2026-02-13,2026,2,13,friday +2026-02-14,2026,2,14,saturday +2026-02-15,2026,2,15,sunday +2026-02-18,2026,2,18,wednesday +2026-02-20,2026,2,20,friday +2026-02-22,2026,2,22,sunday +2026-02-23,2026,2,23,monday +2026-02-24,2026,2,24,tuesday +2026-02-25,2026,2,25,wednesday +2026-02-26,2026,2,26,thursday +2026-03-02,2026,3,2,monday +2026-03-03,2026,3,3,tuesday +2026-03-06,2026,3,6,friday +2026-03-08,2026,3,8,sunday +2026-03-09,2026,3,9,monday +2026-03-10,2026,3,10,tuesday +2026-03-11,2026,3,11,wednesday +2026-03-12,2026,3,12,thursday +2026-03-14,2026,3,14,saturday +2026-03-17,2026,3,17,tuesday +2026-03-20,2026,3,20,friday +2026-03-24,2026,3,24,tuesday +2026-03-27,2026,3,27,friday +2026-03-29,2026,3,29,sunday +2026-04-02,2026,4,2,thursday +2026-04-03,2026,4,3,friday +2026-04-04,2026,4,4,saturday +2026-04-06,2026,4,6,monday +2026-04-09,2026,4,9,thursday +2026-04-11,2026,4,11,saturday +2026-04-13,2026,4,13,monday +2026-04-14,2026,4,14,tuesday +2026-04-20,2026,4,20,monday +2026-04-21,2026,4,21,tuesday +2026-04-24,2026,4,24,friday +2026-04-25,2026,4,25,saturday +2026-04-26,2026,4,26,sunday +2026-04-28,2026,4,28,tuesday +2026-04-30,2026,4,30,thursday +2026-05-01,2026,5,1,friday +2026-05-02,2026,5,2,saturday +2026-05-05,2026,5,5,tuesday +2026-05-06,2026,5,6,wednesday +2026-05-07,2026,5,7,thursday +2026-05-09,2026,5,9,saturday +2026-05-10,2026,5,10,sunday +2026-05-11,2026,5,11,monday +2026-05-12,2026,5,12,tuesday +2026-05-13,2026,5,13,wednesday +2026-05-15,2026,5,15,friday +2026-05-16,2026,5,16,saturday +2026-05-19,2026,5,19,tuesday +2026-05-20,2026,5,20,wednesday +2026-05-25,2026,5,25,monday +2026-05-27,2026,5,27,wednesday +2026-05-28,2026,5,28,thursday +2026-05-30,2026,5,30,saturday +2026-06-01,2026,6,1,monday +2026-06-03,2026,6,3,wednesday +2025-05-15,2025,5,15,thursday +2025-05-16,2025,5,16,friday +2025-05-18,2025,5,18,sunday +2025-05-20,2025,5,20,tuesday +2025-05-22,2025,5,22,thursday +2025-05-23,2025,5,23,friday +2025-05-24,2025,5,24,saturday +2025-05-26,2025,5,26,monday +2025-05-28,2025,5,28,wednesday +2025-05-29,2025,5,29,thursday +2025-05-31,2025,5,31,saturday +2025-06-04,2025,6,4,wednesday +2025-06-06,2025,6,6,friday +2025-06-07,2025,6,7,saturday +2025-06-09,2025,6,9,monday +2025-06-12,2025,6,12,thursday +2025-06-15,2025,6,15,sunday +2025-06-16,2025,6,16,monday +2025-06-18,2025,6,18,wednesday +2025-06-21,2025,6,21,saturday +2025-06-25,2025,6,25,wednesday +2025-06-26,2025,6,26,thursday +2025-06-27,2025,6,27,friday +2025-07-06,2025,7,6,sunday +2025-07-07,2025,7,7,monday +2025-07-08,2025,7,8,tuesday +2025-07-10,2025,7,10,thursday +2025-07-16,2025,7,16,wednesday +2025-07-18,2025,7,18,friday +2025-07-21,2025,7,21,monday +2025-07-23,2025,7,23,wednesday +2025-07-26,2025,7,26,saturday +2025-07-27,2025,7,27,sunday +2025-07-31,2025,7,31,thursday +2025-08-01,2025,8,1,friday +2025-08-04,2025,8,4,monday +2025-08-05,2025,8,5,tuesday +2025-08-11,2025,8,11,monday +2025-08-12,2025,8,12,tuesday +2025-08-13,2025,8,13,wednesday +2025-08-14,2025,8,14,thursday +2025-08-16,2025,8,16,saturday +2025-08-17,2025,8,17,sunday +2025-08-18,2025,8,18,monday +2025-08-19,2025,8,19,tuesday +2025-08-20,2025,8,20,wednesday +2025-08-21,2025,8,21,thursday +2025-08-23,2025,8,23,saturday +2025-08-24,2025,8,24,sunday +2025-08-26,2025,8,26,tuesday +2025-08-28,2025,8,28,thursday +2025-08-31,2025,8,31,sunday +2025-09-01,2025,9,1,monday +2025-09-05,2025,9,5,friday +2025-09-07,2025,9,7,sunday +2025-09-08,2025,9,8,monday +2025-09-09,2025,9,9,tuesday +2025-09-10,2025,9,10,wednesday +2025-09-12,2025,9,12,friday +2025-09-13,2025,9,13,saturday +2025-09-16,2025,9,16,tuesday +2025-09-17,2025,9,17,wednesday +2025-09-18,2025,9,18,thursday +2025-09-19,2025,9,19,friday +2025-09-22,2025,9,22,monday +2025-09-24,2025,9,24,wednesday +2025-09-25,2025,9,25,thursday +2025-09-26,2025,9,26,friday +2025-09-27,2025,9,27,saturday +2025-09-28,2025,9,28,sunday +2025-09-29,2025,9,29,monday +2025-10-01,2025,10,1,wednesday +2025-10-05,2025,10,5,sunday +2025-10-07,2025,10,7,tuesday +2025-10-08,2025,10,8,wednesday +2025-10-10,2025,10,10,friday +2025-10-11,2025,10,11,saturday +2025-10-12,2025,10,12,sunday +2025-10-16,2025,10,16,thursday +2025-10-17,2025,10,17,friday +2025-10-18,2025,10,18,saturday +2025-10-21,2025,10,21,tuesday +2025-10-22,2025,10,22,wednesday +2025-10-26,2025,10,26,sunday +2025-10-27,2025,10,27,monday +2025-10-28,2025,10,28,tuesday +2025-10-29,2025,10,29,wednesday +2025-10-31,2025,10,31,friday +2025-11-03,2025,11,3,monday +2025-11-07,2025,11,7,friday +2025-11-09,2025,11,9,sunday +2025-11-10,2025,11,10,monday +2025-11-11,2025,11,11,tuesday +2025-11-15,2025,11,15,saturday +2025-11-19,2025,11,19,wednesday +2025-11-20,2025,11,20,thursday +2025-11-23,2025,11,23,sunday +2025-11-24,2025,11,24,monday +2025-11-25,2025,11,25,tuesday +2025-11-28,2025,11,28,friday +2025-11-29,2025,11,29,saturday +2025-12-03,2025,12,3,wednesday +2025-12-05,2025,12,5,friday +2025-12-06,2025,12,6,saturday +2025-12-07,2025,12,7,sunday +2025-12-09,2025,12,9,tuesday +2025-12-10,2025,12,10,wednesday +2025-12-14,2025,12,14,sunday +2025-12-15,2025,12,15,monday +2025-12-16,2025,12,16,tuesday +2025-12-17,2025,12,17,wednesday +2025-12-19,2025,12,19,friday +2025-12-20,2025,12,20,saturday +2025-12-24,2025,12,24,wednesday +2025-12-26,2025,12,26,friday +2025-12-30,2025,12,30,tuesday +2025-12-31,2025,12,31,wednesday +2026-01-01,2026,1,1,thursday +2026-01-02,2026,1,2,friday +2026-01-03,2026,1,3,saturday +2026-01-05,2026,1,5,monday +2026-01-07,2026,1,7,wednesday +2026-01-09,2026,1,9,friday +2026-01-12,2026,1,12,monday +2026-01-13,2026,1,13,tuesday +2026-01-16,2026,1,16,friday +2026-01-18,2026,1,18,sunday +2026-01-22,2026,1,22,thursday +2026-01-27,2026,1,27,tuesday +2026-02-01,2026,2,1,sunday +2026-02-02,2026,2,2,monday +2026-02-08,2026,2,8,sunday +2026-02-11,2026,2,11,wednesday +2026-02-16,2026,2,16,monday +2026-02-17,2026,2,17,tuesday +2026-02-19,2026,2,19,thursday +2026-02-21,2026,2,21,saturday +2026-02-27,2026,2,27,friday +2026-02-28,2026,2,28,saturday +2026-03-01,2026,3,1,sunday +2026-03-04,2026,3,4,wednesday +2026-03-05,2026,3,5,thursday +2026-03-07,2026,3,7,saturday +2026-03-13,2026,3,13,friday +2026-03-15,2026,3,15,sunday +2026-03-16,2026,3,16,monday +2026-03-18,2026,3,18,wednesday +2026-03-19,2026,3,19,thursday +2026-03-21,2026,3,21,saturday +2026-03-22,2026,3,22,sunday +2026-03-23,2026,3,23,monday +2026-03-25,2026,3,25,wednesday +2026-03-26,2026,3,26,thursday +2026-03-28,2026,3,28,saturday +2026-03-30,2026,3,30,monday +2026-03-31,2026,3,31,tuesday +2026-04-01,2026,4,1,wednesday +2026-04-05,2026,4,5,sunday +2026-04-07,2026,4,7,tuesday +2026-04-08,2026,4,8,wednesday +2026-04-10,2026,4,10,friday +2026-04-12,2026,4,12,sunday +2026-04-15,2026,4,15,wednesday +2026-04-16,2026,4,16,thursday +2026-04-17,2026,4,17,friday +2026-04-18,2026,4,18,saturday +2026-04-19,2026,4,19,sunday +2026-04-22,2026,4,22,wednesday +2026-04-23,2026,4,23,thursday +2026-04-27,2026,4,27,monday +2026-04-29,2026,4,29,wednesday +2026-05-03,2026,5,3,sunday +2026-05-04,2026,5,4,monday +2026-05-08,2026,5,8,friday +2026-05-14,2026,5,14,thursday +2026-05-17,2026,5,17,sunday +2026-05-18,2026,5,18,monday +2026-05-21,2026,5,21,thursday +2026-05-22,2026,5,22,friday +2026-05-23,2026,5,23,saturday +2026-05-24,2026,5,24,sunday +2026-05-26,2026,5,26,tuesday +2026-05-29,2026,5,29,friday +2026-05-31,2026,5,31,sunday +2026-06-02,2026,6,2,tuesday +2026-06-06,2026,6,6,saturday +2026-06-07,2026,6,7,sunday +2026-06-08,2026,6,8,monday +2026-06-09,2026,6,9,tuesday +2026-06-10,2026,6,10,wednesday +2026-06-11,2026,6,11,thursday +2026-06-12,2026,6,12,friday +2026-06-14,2026,6,14,sunday +2026-06-16,2026,6,16,tuesday +2026-06-17,2026,6,17,wednesday +2026-06-19,2026,6,19,friday +2026-06-21,2026,6,21,sunday +2026-06-22,2026,6,22,monday +2026-06-23,2026,6,23,tuesday +2026-06-25,2026,6,25,thursday +2026-06-26,2026,6,26,friday +2026-06-27,2026,6,27,saturday +2026-07-02,2026,7,2,thursday +2026-07-04,2026,7,4,saturday +2026-07-05,2026,7,5,sunday +2026-07-08,2026,7,8,wednesday +2026-07-09,2026,7,9,thursday +2026-07-10,2026,7,10,friday +2026-07-12,2026,7,12,sunday +2026-07-17,2026,7,17,friday +2026-07-18,2026,7,18,saturday +2026-07-21,2026,7,21,tuesday +2026-07-23,2026,7,23,thursday +2026-06-04,2026,6,4,thursday +2026-06-05,2026,6,5,friday +2026-06-13,2026,6,13,saturday +2026-06-15,2026,6,15,monday +2026-06-18,2026,6,18,thursday +2026-06-20,2026,6,20,saturday +2026-06-24,2026,6,24,wednesday +2026-06-28,2026,6,28,sunday +2026-06-29,2026,6,29,monday +2026-06-30,2026,6,30,tuesday +2026-07-01,2026,7,1,wednesday +2026-07-03,2026,7,3,friday +2026-07-06,2026,7,6,monday +2026-07-07,2026,7,7,tuesday +2026-07-11,2026,7,11,saturday +2026-07-13,2026,7,13,monday +2026-07-14,2026,7,14,tuesday +2026-07-15,2026,7,15,wednesday +2026-07-16,2026,7,16,thursday +2026-07-19,2026,7,19,sunday +2026-07-20,2026,7,20,monday +2026-07-22,2026,7,22,wednesday +2026-07-25,2026,7,25,saturday +2026-07-27,2026,7,27,monday +2026-08-01,2026,8,1,saturday +2026-08-03,2026,8,3,monday +2026-08-05,2026,8,5,wednesday +2026-08-09,2026,8,9,sunday +2026-08-10,2026,8,10,monday +2026-08-14,2026,8,14,friday +2026-08-15,2026,8,15,saturday +2026-08-16,2026,8,16,sunday +2026-08-17,2026,8,17,monday +2026-08-20,2026,8,20,thursday +2026-08-21,2026,8,21,friday +2026-08-22,2026,8,22,saturday +2026-08-25,2026,8,25,tuesday +2026-08-26,2026,8,26,wednesday +2026-08-27,2026,8,27,thursday +2026-08-29,2026,8,29,saturday +2026-08-31,2026,8,31,monday +2026-09-01,2026,9,1,tuesday +2026-09-03,2026,9,3,thursday +2026-09-04,2026,9,4,friday +2026-09-08,2026,9,8,tuesday +2026-09-12,2026,9,12,saturday +2026-09-14,2026,9,14,monday +2026-09-16,2026,9,16,wednesday +2026-09-20,2026,9,20,sunday +2026-09-21,2026,9,21,monday +2026-09-22,2026,9,22,tuesday +2026-09-25,2026,9,25,friday +2026-09-26,2026,9,26,saturday +2026-09-28,2026,9,28,monday +2026-09-29,2026,9,29,tuesday +2026-09-30,2026,9,30,wednesday +2026-10-02,2026,10,2,friday +2026-10-03,2026,10,3,saturday +2026-10-04,2026,10,4,sunday +2026-10-05,2026,10,5,monday +2026-10-06,2026,10,6,tuesday +2026-10-07,2026,10,7,wednesday +2026-10-08,2026,10,8,thursday +2026-10-12,2026,10,12,monday +2026-10-15,2026,10,15,thursday +2026-10-18,2026,10,18,sunday +2026-10-20,2026,10,20,tuesday +2026-10-21,2026,10,21,wednesday +2026-10-23,2026,10,23,friday +2026-10-25,2026,10,25,sunday +2026-10-26,2026,10,26,monday +2026-10-27,2026,10,27,tuesday +2026-10-28,2026,10,28,wednesday +2026-10-29,2026,10,29,thursday +2026-10-30,2026,10,30,friday +2026-10-31,2026,10,31,saturday +2026-11-02,2026,11,2,monday +2026-11-04,2026,11,4,wednesday +2026-11-05,2026,11,5,thursday +2026-11-07,2026,11,7,saturday +2026-11-11,2026,11,11,wednesday +2026-11-14,2026,11,14,saturday +2026-11-15,2026,11,15,sunday +2026-11-19,2026,11,19,thursday +2026-11-22,2026,11,22,sunday +2026-11-30,2026,11,30,monday +2026-12-02,2026,12,2,wednesday +2026-12-03,2026,12,3,thursday +2026-12-04,2026,12,4,friday +2026-12-05,2026,12,5,saturday +2026-12-06,2026,12,6,sunday +2026-12-07,2026,12,7,monday +2026-12-09,2026,12,9,wednesday +2026-12-12,2026,12,12,saturday +2026-12-15,2026,12,15,tuesday +2026-12-17,2026,12,17,thursday +2026-12-20,2026,12,20,sunday +2026-12-22,2026,12,22,tuesday +2026-12-24,2026,12,24,thursday +2026-12-25,2026,12,25,friday +2026-12-26,2026,12,26,saturday +2026-12-27,2026,12,27,sunday +2026-12-28,2026,12,28,monday +2026-12-29,2026,12,29,tuesday +2027-01-03,2027,1,3,sunday +2027-01-04,2027,1,4,monday +2027-01-07,2027,1,7,thursday +2027-01-08,2027,1,8,friday +2027-01-10,2027,1,10,sunday +2027-01-11,2027,1,11,monday +2027-01-17,2027,1,17,sunday +2027-01-18,2027,1,18,monday +2027-01-20,2027,1,20,wednesday +2027-01-22,2027,1,22,friday +2027-01-24,2027,1,24,sunday +2027-01-25,2027,1,25,monday +2027-01-26,2027,1,26,tuesday +2027-01-27,2027,1,27,wednesday +2027-01-30,2027,1,30,saturday +2027-01-31,2027,1,31,sunday +2027-02-01,2027,2,1,monday +2027-02-03,2027,2,3,wednesday +2027-02-05,2027,2,5,friday +2027-02-09,2027,2,9,tuesday +2027-02-10,2027,2,10,wednesday +2027-02-15,2027,2,15,monday +2027-02-18,2027,2,18,thursday +2027-02-20,2027,2,20,saturday +2027-02-23,2027,2,23,tuesday +2027-02-26,2027,2,26,friday +2027-03-01,2027,3,1,monday +2027-03-03,2027,3,3,wednesday +2027-03-04,2027,3,4,thursday +2027-03-06,2027,3,6,saturday +2027-03-09,2027,3,9,tuesday +2027-03-11,2027,3,11,thursday +2027-03-13,2027,3,13,saturday +2027-03-14,2027,3,14,sunday +2027-03-15,2027,3,15,monday +2027-03-17,2027,3,17,wednesday +2027-03-19,2027,3,19,friday +2027-03-20,2027,3,20,saturday +2027-03-21,2027,3,21,sunday +2027-03-25,2027,3,25,thursday +2027-03-26,2027,3,26,friday +2027-03-27,2027,3,27,saturday +2027-03-28,2027,3,28,sunday +2027-04-03,2027,4,3,saturday +2027-04-04,2027,4,4,sunday +2027-04-05,2027,4,5,monday +2027-04-08,2027,4,8,thursday +2027-04-11,2027,4,11,sunday +2027-04-12,2027,4,12,monday +2027-04-16,2027,4,16,friday +2027-04-18,2027,4,18,sunday +2027-04-19,2027,4,19,monday +2027-04-20,2027,4,20,tuesday +2027-04-28,2027,4,28,wednesday +2027-05-02,2027,5,2,sunday +2027-05-05,2027,5,5,wednesday +2027-05-06,2027,5,6,thursday +2027-05-07,2027,5,7,friday +2027-05-08,2027,5,8,saturday +2027-05-12,2027,5,12,wednesday +2027-05-15,2027,5,15,saturday +2027-05-16,2027,5,16,sunday +2027-05-17,2027,5,17,monday +2027-05-20,2027,5,20,thursday +2027-05-21,2027,5,21,friday +2027-05-22,2027,5,22,saturday +2027-05-23,2027,5,23,sunday +2027-05-24,2027,5,24,monday +2027-05-25,2027,5,25,tuesday +2027-05-27,2027,5,27,thursday +2027-05-28,2027,5,28,friday +2027-05-29,2027,5,29,saturday +2027-05-30,2027,5,30,sunday +2027-05-31,2027,5,31,monday +2027-06-01,2027,6,1,tuesday +2027-06-04,2027,6,4,friday +2027-06-05,2027,6,5,saturday +2027-06-09,2027,6,9,wednesday +2027-06-10,2027,6,10,thursday +2027-06-12,2027,6,12,saturday +2027-06-13,2027,6,13,sunday +2027-06-14,2027,6,14,monday +2027-06-15,2027,6,15,tuesday +2027-06-16,2027,6,16,wednesday +2027-06-17,2027,6,17,thursday +2027-06-19,2027,6,19,saturday +2027-06-21,2027,6,21,monday +2027-06-23,2027,6,23,wednesday +2027-06-24,2027,6,24,thursday +2027-06-26,2027,6,26,saturday +2027-06-27,2027,6,27,sunday +2027-06-28,2027,6,28,monday +2027-06-29,2027,6,29,tuesday +2027-07-01,2027,7,1,thursday +2027-07-02,2027,7,2,friday +2027-07-04,2027,7,4,sunday +2027-07-05,2027,7,5,monday +2027-07-07,2027,7,7,wednesday +2027-07-10,2027,7,10,saturday +2027-07-12,2027,7,12,monday +2027-07-14,2027,7,14,wednesday +2027-07-16,2027,7,16,friday +2027-07-17,2027,7,17,saturday +2027-07-19,2027,7,19,monday +2027-07-20,2027,7,20,tuesday +2027-07-22,2027,7,22,thursday +2027-07-23,2027,7,23,friday +2027-07-26,2027,7,26,monday +2027-07-27,2027,7,27,tuesday +2026-07-24,2026,7,24,friday +2026-07-26,2026,7,26,sunday +2026-07-28,2026,7,28,tuesday +2026-07-29,2026,7,29,wednesday +2026-07-30,2026,7,30,thursday +2026-07-31,2026,7,31,friday +2026-08-02,2026,8,2,sunday +2026-08-04,2026,8,4,tuesday +2026-08-06,2026,8,6,thursday +2026-08-07,2026,8,7,friday +2026-08-08,2026,8,8,saturday +2026-08-11,2026,8,11,tuesday +2026-08-12,2026,8,12,wednesday +2026-08-13,2026,8,13,thursday +2026-08-18,2026,8,18,tuesday +2026-08-19,2026,8,19,wednesday +2026-08-23,2026,8,23,sunday +2026-08-24,2026,8,24,monday +2026-08-28,2026,8,28,friday +2026-08-30,2026,8,30,sunday +2026-09-02,2026,9,2,wednesday +2026-09-05,2026,9,5,saturday +2026-09-06,2026,9,6,sunday +2026-09-07,2026,9,7,monday +2026-09-09,2026,9,9,wednesday +2026-09-10,2026,9,10,thursday +2026-09-11,2026,9,11,friday +2026-09-13,2026,9,13,sunday +2026-09-15,2026,9,15,tuesday +2026-09-17,2026,9,17,thursday +2026-09-18,2026,9,18,friday +2026-09-19,2026,9,19,saturday +2026-09-23,2026,9,23,wednesday +2026-09-24,2026,9,24,thursday +2026-09-27,2026,9,27,sunday +2026-10-01,2026,10,1,thursday +2026-10-09,2026,10,9,friday +2026-10-10,2026,10,10,saturday +2026-10-11,2026,10,11,sunday +2026-10-13,2026,10,13,tuesday +2026-10-14,2026,10,14,wednesday +2026-10-16,2026,10,16,friday +2026-10-17,2026,10,17,saturday +2026-10-19,2026,10,19,monday +2026-10-22,2026,10,22,thursday +2026-10-24,2026,10,24,saturday +2026-11-01,2026,11,1,sunday +2026-11-03,2026,11,3,tuesday +2026-11-06,2026,11,6,friday +2026-11-08,2026,11,8,sunday +2026-11-09,2026,11,9,monday +2026-11-10,2026,11,10,tuesday +2026-11-12,2026,11,12,thursday +2026-11-13,2026,11,13,friday +2026-11-16,2026,11,16,monday +2026-11-17,2026,11,17,tuesday +2026-11-18,2026,11,18,wednesday +2026-11-20,2026,11,20,friday +2026-11-21,2026,11,21,saturday +2026-11-23,2026,11,23,monday +2026-11-24,2026,11,24,tuesday +2026-11-25,2026,11,25,wednesday +2026-11-26,2026,11,26,thursday +2026-11-27,2026,11,27,friday +2026-11-28,2026,11,28,saturday +2026-11-29,2026,11,29,sunday +2026-12-01,2026,12,1,tuesday +2026-12-08,2026,12,8,tuesday +2026-12-10,2026,12,10,thursday +2026-12-11,2026,12,11,friday +2026-12-13,2026,12,13,sunday +2026-12-14,2026,12,14,monday +2026-12-16,2026,12,16,wednesday +2026-12-18,2026,12,18,friday +2026-12-19,2026,12,19,saturday +2026-12-21,2026,12,21,monday +2026-12-23,2026,12,23,wednesday +2026-12-30,2026,12,30,wednesday +2026-12-31,2026,12,31,thursday +2027-01-01,2027,1,1,friday +2027-01-02,2027,1,2,saturday +2027-01-05,2027,1,5,tuesday +2027-01-06,2027,1,6,wednesday +2027-01-09,2027,1,9,saturday +2027-01-12,2027,1,12,tuesday +2027-01-13,2027,1,13,wednesday +2027-01-14,2027,1,14,thursday +2027-01-15,2027,1,15,friday +2027-01-16,2027,1,16,saturday +2027-01-19,2027,1,19,tuesday +2027-01-21,2027,1,21,thursday +2027-01-23,2027,1,23,saturday +2027-01-28,2027,1,28,thursday +2027-01-29,2027,1,29,friday +2027-02-02,2027,2,2,tuesday +2027-02-04,2027,2,4,thursday +2027-02-06,2027,2,6,saturday +2027-02-07,2027,2,7,sunday +2027-02-08,2027,2,8,monday +2027-02-11,2027,2,11,thursday +2027-02-12,2027,2,12,friday +2027-02-13,2027,2,13,saturday +2027-02-14,2027,2,14,sunday +2027-02-16,2027,2,16,tuesday +2027-02-17,2027,2,17,wednesday +2027-02-19,2027,2,19,friday +2027-02-21,2027,2,21,sunday +2027-02-22,2027,2,22,monday +2027-02-24,2027,2,24,wednesday +2027-02-25,2027,2,25,thursday +2027-02-27,2027,2,27,saturday +2027-02-28,2027,2,28,sunday +2027-03-02,2027,3,2,tuesday +2027-03-05,2027,3,5,friday +2027-03-07,2027,3,7,sunday +2027-03-08,2027,3,8,monday +2027-03-10,2027,3,10,wednesday +2027-03-12,2027,3,12,friday +2027-03-16,2027,3,16,tuesday +2027-03-18,2027,3,18,thursday +2027-03-22,2027,3,22,monday +2027-03-23,2027,3,23,tuesday +2027-03-24,2027,3,24,wednesday +2027-03-29,2027,3,29,monday +2027-03-30,2027,3,30,tuesday +2027-03-31,2027,3,31,wednesday +2027-04-01,2027,4,1,thursday +2027-04-02,2027,4,2,friday +2027-04-06,2027,4,6,tuesday +2027-04-07,2027,4,7,wednesday +2027-04-09,2027,4,9,friday +2027-04-10,2027,4,10,saturday +2027-04-13,2027,4,13,tuesday +2027-04-14,2027,4,14,wednesday +2027-04-15,2027,4,15,thursday +2027-04-17,2027,4,17,saturday +2027-04-21,2027,4,21,wednesday +2027-04-22,2027,4,22,thursday +2027-04-23,2027,4,23,friday +2027-04-24,2027,4,24,saturday +2027-04-25,2027,4,25,sunday +2027-04-26,2027,4,26,monday +2027-04-27,2027,4,27,tuesday +2027-04-29,2027,4,29,thursday +2027-04-30,2027,4,30,friday +2027-05-01,2027,5,1,saturday +2027-05-03,2027,5,3,monday +2027-05-04,2027,5,4,tuesday +2027-05-09,2027,5,9,sunday +2027-05-10,2027,5,10,monday +2027-05-11,2027,5,11,tuesday +2027-05-13,2027,5,13,thursday +2027-05-14,2027,5,14,friday +2027-05-18,2027,5,18,tuesday +2027-05-19,2027,5,19,wednesday +2027-05-26,2027,5,26,wednesday +2027-06-02,2027,6,2,wednesday +2027-06-03,2027,6,3,thursday +2027-06-06,2027,6,6,sunday +2027-06-07,2027,6,7,monday +2027-06-08,2027,6,8,tuesday +2027-06-11,2027,6,11,friday +2027-06-18,2027,6,18,friday +2027-06-20,2027,6,20,sunday +2027-06-22,2027,6,22,tuesday +2027-06-25,2027,6,25,friday +2027-06-30,2027,6,30,wednesday +2027-07-03,2027,7,3,saturday +2027-07-06,2027,7,6,tuesday +2027-07-08,2027,7,8,thursday +2027-07-09,2027,7,9,friday +2027-07-11,2027,7,11,sunday +2027-07-13,2027,7,13,tuesday +2027-07-15,2027,7,15,thursday +2027-07-18,2027,7,18,sunday +2027-07-21,2027,7,21,wednesday +2027-07-24,2027,7,24,saturday +2027-07-25,2027,7,25,sunday +2027-07-28,2027,7,28,wednesday +2027-07-30,2027,7,30,friday +2027-08-02,2027,8,2,monday +2027-08-05,2027,8,5,thursday +2027-08-06,2027,8,6,friday +2027-08-09,2027,8,9,monday +2027-08-10,2027,8,10,tuesday +2027-08-12,2027,8,12,thursday +2027-08-14,2027,8,14,saturday +2027-08-16,2027,8,16,monday +2027-08-17,2027,8,17,tuesday +2027-08-18,2027,8,18,wednesday +2027-08-20,2027,8,20,friday +2027-08-21,2027,8,21,saturday +2027-08-23,2027,8,23,monday +2027-08-24,2027,8,24,tuesday +2027-08-26,2027,8,26,thursday +2027-08-27,2027,8,27,friday +2027-08-29,2027,8,29,sunday +2027-08-30,2027,8,30,monday +2027-09-01,2027,9,1,wednesday +2027-09-03,2027,9,3,friday +2027-09-05,2027,9,5,sunday +2027-09-07,2027,9,7,tuesday +2027-09-08,2027,9,8,wednesday +2027-09-09,2027,9,9,thursday +2027-09-13,2027,9,13,monday +2027-09-14,2027,9,14,tuesday +2027-09-16,2027,9,16,thursday +2027-09-17,2027,9,17,friday +2027-09-18,2027,9,18,saturday +2027-09-19,2027,9,19,sunday +2027-09-21,2027,9,21,tuesday +2027-09-22,2027,9,22,wednesday +2027-09-23,2027,9,23,thursday +2027-07-29,2027,7,29,thursday +2027-07-31,2027,7,31,saturday +2027-08-01,2027,8,1,sunday +2027-08-03,2027,8,3,tuesday +2027-08-04,2027,8,4,wednesday +2027-08-07,2027,8,7,saturday +2027-08-08,2027,8,8,sunday +2027-08-11,2027,8,11,wednesday +2027-08-13,2027,8,13,friday +2027-08-15,2027,8,15,sunday +2027-08-19,2027,8,19,thursday +2027-08-22,2027,8,22,sunday +2027-08-25,2027,8,25,wednesday +2027-08-28,2027,8,28,saturday +2027-08-31,2027,8,31,tuesday +2027-09-02,2027,9,2,thursday +2027-09-04,2027,9,4,saturday +2027-09-06,2027,9,6,monday +2027-09-10,2027,9,10,friday +2027-09-11,2027,9,11,saturday +2027-09-12,2027,9,12,sunday +2027-09-15,2027,9,15,wednesday +2027-09-20,2027,9,20,monday +2027-09-24,2027,9,24,friday +2027-09-29,2027,9,29,wednesday +2027-10-07,2027,10,7,thursday +2027-10-08,2027,10,8,friday +2027-10-11,2027,10,11,monday +2027-10-13,2027,10,13,wednesday +2027-10-16,2027,10,16,saturday +2027-10-19,2027,10,19,tuesday +2027-10-20,2027,10,20,wednesday +2027-10-22,2027,10,22,friday +2027-10-24,2027,10,24,sunday +2027-10-26,2027,10,26,tuesday +2027-10-29,2027,10,29,friday +2027-10-30,2027,10,30,saturday +2027-11-01,2027,11,1,monday +2027-11-06,2027,11,6,saturday +2027-11-08,2027,11,8,monday +2027-11-09,2027,11,9,tuesday +2027-11-10,2027,11,10,wednesday +2027-11-11,2027,11,11,thursday +2027-11-12,2027,11,12,friday +2027-11-13,2027,11,13,saturday +2027-11-14,2027,11,14,sunday +2027-11-16,2027,11,16,tuesday +2027-11-17,2027,11,17,wednesday +2027-11-20,2027,11,20,saturday +2027-11-21,2027,11,21,sunday +2027-11-22,2027,11,22,monday +2027-11-23,2027,11,23,tuesday +2027-11-24,2027,11,24,wednesday +2027-11-25,2027,11,25,thursday +2027-11-26,2027,11,26,friday +2027-11-27,2027,11,27,saturday +2027-11-28,2027,11,28,sunday +2027-12-01,2027,12,1,wednesday +2027-12-05,2027,12,5,sunday +2027-12-06,2027,12,6,monday +2027-12-07,2027,12,7,tuesday +2027-12-10,2027,12,10,friday +2027-12-13,2027,12,13,monday +2027-12-14,2027,12,14,tuesday +2027-12-16,2027,12,16,thursday +2027-12-17,2027,12,17,friday +2027-12-19,2027,12,19,sunday +2027-12-20,2027,12,20,monday +2027-12-25,2027,12,25,saturday +2027-12-28,2027,12,28,tuesday +2027-12-29,2027,12,29,wednesday +2027-12-31,2027,12,31,friday +2028-01-02,2028,1,2,sunday +2028-01-05,2028,1,5,wednesday +2028-01-07,2028,1,7,friday +2028-01-09,2028,1,9,sunday +2028-01-10,2028,1,10,monday +2028-01-14,2028,1,14,friday +2028-01-16,2028,1,16,sunday +2028-01-17,2028,1,17,monday +2028-01-20,2028,1,20,thursday +2028-01-21,2028,1,21,friday +2028-01-22,2028,1,22,saturday +2028-01-26,2028,1,26,wednesday +2028-01-27,2028,1,27,thursday +2028-01-29,2028,1,29,saturday +2028-02-02,2028,2,2,wednesday +2028-02-03,2028,2,3,thursday +2028-02-05,2028,2,5,saturday +2028-02-11,2028,2,11,friday +2028-02-13,2028,2,13,sunday +2028-02-15,2028,2,15,tuesday +2028-02-18,2028,2,18,friday +2028-02-19,2028,2,19,saturday +2028-02-20,2028,2,20,sunday +2028-02-21,2028,2,21,monday +2028-02-22,2028,2,22,tuesday +2028-02-23,2028,2,23,wednesday +2028-02-24,2028,2,24,thursday +2028-02-25,2028,2,25,friday +2028-02-26,2028,2,26,saturday +2028-02-27,2028,2,27,sunday +2028-02-28,2028,2,28,monday +2028-03-01,2028,3,1,wednesday +2028-03-02,2028,3,2,thursday +2028-03-06,2028,3,6,monday +2028-03-07,2028,3,7,tuesday +2028-03-08,2028,3,8,wednesday +2028-03-10,2028,3,10,friday +2028-03-12,2028,3,12,sunday +2028-03-18,2028,3,18,saturday +2028-03-20,2028,3,20,monday +2028-03-22,2028,3,22,wednesday +2028-03-25,2028,3,25,saturday +2028-03-28,2028,3,28,tuesday +2028-03-29,2028,3,29,wednesday +2028-03-30,2028,3,30,thursday +2028-03-31,2028,3,31,friday +2028-04-02,2028,4,2,sunday +2028-04-03,2028,4,3,monday +2028-04-04,2028,4,4,tuesday +2028-04-06,2028,4,6,thursday +2028-04-08,2028,4,8,saturday +2028-04-10,2028,4,10,monday +2028-04-11,2028,4,11,tuesday +2028-04-13,2028,4,13,thursday +2028-04-17,2028,4,17,monday +2028-04-18,2028,4,18,tuesday +2028-04-19,2028,4,19,wednesday +2028-04-21,2028,4,21,friday +2028-04-22,2028,4,22,saturday +2028-04-26,2028,4,26,wednesday +2028-04-29,2028,4,29,saturday +2028-05-02,2028,5,2,tuesday +2028-05-06,2028,5,6,saturday +2028-05-09,2028,5,9,tuesday +2028-05-10,2028,5,10,wednesday +2028-05-11,2028,5,11,thursday +2028-05-12,2028,5,12,friday +2028-05-14,2028,5,14,sunday +2028-05-15,2028,5,15,monday +2028-05-20,2028,5,20,saturday +2028-05-23,2028,5,23,tuesday +2028-05-24,2028,5,24,wednesday +2028-05-25,2028,5,25,thursday +2028-05-27,2028,5,27,saturday +2028-05-28,2028,5,28,sunday +2028-05-29,2028,5,29,monday +2028-05-30,2028,5,30,tuesday +2028-06-01,2028,6,1,thursday +2028-06-06,2028,6,6,tuesday +2028-06-07,2028,6,7,wednesday +2028-06-09,2028,6,9,friday +2028-06-12,2028,6,12,monday +2028-06-13,2028,6,13,tuesday +2028-06-16,2028,6,16,friday +2028-06-17,2028,6,17,saturday +2028-06-20,2028,6,20,tuesday +2028-06-23,2028,6,23,friday +2028-06-25,2028,6,25,sunday +2028-06-29,2028,6,29,thursday +2028-07-04,2028,7,4,tuesday +2028-07-07,2028,7,7,friday +2028-07-09,2028,7,9,sunday +2028-07-12,2028,7,12,wednesday +2028-07-13,2028,7,13,thursday +2028-07-16,2028,7,16,sunday +2028-07-17,2028,7,17,monday +2028-07-18,2028,7,18,tuesday +2028-07-19,2028,7,19,wednesday +2028-07-22,2028,7,22,saturday +2028-07-23,2028,7,23,sunday +2028-07-24,2028,7,24,monday +2028-07-26,2028,7,26,wednesday +2028-07-27,2028,7,27,thursday +2028-07-28,2028,7,28,friday +2028-07-31,2028,7,31,monday +2028-08-02,2028,8,2,wednesday +2028-08-04,2028,8,4,friday +2028-08-05,2028,8,5,saturday +2028-08-06,2028,8,6,sunday +2028-08-07,2028,8,7,monday +2028-08-08,2028,8,8,tuesday +2028-08-13,2028,8,13,sunday +2028-08-14,2028,8,14,monday +2028-08-18,2028,8,18,friday +2028-08-23,2028,8,23,wednesday +2028-08-25,2028,8,25,friday +2028-08-26,2028,8,26,saturday +2028-08-27,2028,8,27,sunday +2028-08-28,2028,8,28,monday +2028-08-29,2028,8,29,tuesday +2028-08-30,2028,8,30,wednesday +2028-08-31,2028,8,31,thursday +2028-09-02,2028,9,2,saturday +2028-09-03,2028,9,3,sunday +2028-09-05,2028,9,5,tuesday +2028-09-08,2028,9,8,friday +2028-09-09,2028,9,9,saturday +2028-09-11,2028,9,11,monday +2028-09-12,2028,9,12,tuesday +2028-09-15,2028,9,15,friday +2028-09-17,2028,9,17,sunday +2028-09-19,2028,9,19,tuesday +2028-09-20,2028,9,20,wednesday +2028-09-25,2028,9,25,monday +2028-09-27,2028,9,27,wednesday +2028-09-28,2028,9,28,thursday +2028-09-30,2028,9,30,saturday +2028-10-01,2028,10,1,sunday +2028-10-05,2028,10,5,thursday +2028-10-06,2028,10,6,friday +2028-10-12,2028,10,12,thursday +2027-09-25,2027,9,25,saturday +2027-09-26,2027,9,26,sunday +2027-09-27,2027,9,27,monday +2027-09-28,2027,9,28,tuesday +2027-09-30,2027,9,30,thursday +2027-10-01,2027,10,1,friday +2027-10-02,2027,10,2,saturday +2027-10-03,2027,10,3,sunday +2027-10-04,2027,10,4,monday +2027-10-05,2027,10,5,tuesday +2027-10-06,2027,10,6,wednesday +2027-10-09,2027,10,9,saturday +2027-10-10,2027,10,10,sunday +2027-10-12,2027,10,12,tuesday +2027-10-14,2027,10,14,thursday +2027-10-15,2027,10,15,friday +2027-10-17,2027,10,17,sunday +2027-10-18,2027,10,18,monday +2027-10-21,2027,10,21,thursday +2027-10-23,2027,10,23,saturday +2027-10-25,2027,10,25,monday +2027-10-27,2027,10,27,wednesday +2027-10-28,2027,10,28,thursday +2027-10-31,2027,10,31,sunday +2027-11-02,2027,11,2,tuesday +2027-11-03,2027,11,3,wednesday +2027-11-04,2027,11,4,thursday +2027-11-05,2027,11,5,friday +2027-11-07,2027,11,7,sunday +2027-11-15,2027,11,15,monday +2027-11-18,2027,11,18,thursday +2027-11-19,2027,11,19,friday +2027-11-29,2027,11,29,monday +2027-11-30,2027,11,30,tuesday +2027-12-02,2027,12,2,thursday +2027-12-03,2027,12,3,friday +2027-12-04,2027,12,4,saturday +2027-12-08,2027,12,8,wednesday +2027-12-09,2027,12,9,thursday +2027-12-11,2027,12,11,saturday +2027-12-12,2027,12,12,sunday +2027-12-15,2027,12,15,wednesday +2027-12-18,2027,12,18,saturday +2027-12-21,2027,12,21,tuesday +2027-12-22,2027,12,22,wednesday +2027-12-23,2027,12,23,thursday +2027-12-24,2027,12,24,friday +2027-12-26,2027,12,26,sunday +2027-12-27,2027,12,27,monday +2027-12-30,2027,12,30,thursday +2028-01-01,2028,1,1,saturday +2028-01-03,2028,1,3,monday +2028-01-04,2028,1,4,tuesday +2028-01-06,2028,1,6,thursday +2028-01-08,2028,1,8,saturday +2028-01-11,2028,1,11,tuesday +2028-01-12,2028,1,12,wednesday +2028-01-13,2028,1,13,thursday +2028-01-15,2028,1,15,saturday +2028-01-18,2028,1,18,tuesday +2028-01-19,2028,1,19,wednesday +2028-01-23,2028,1,23,sunday +2028-01-24,2028,1,24,monday +2028-01-25,2028,1,25,tuesday +2028-01-28,2028,1,28,friday +2028-01-30,2028,1,30,sunday +2028-01-31,2028,1,31,monday +2028-02-01,2028,2,1,tuesday +2028-02-04,2028,2,4,friday +2028-02-06,2028,2,6,sunday +2028-02-07,2028,2,7,monday +2028-02-08,2028,2,8,tuesday +2028-02-09,2028,2,9,wednesday +2028-02-10,2028,2,10,thursday +2028-02-12,2028,2,12,saturday +2028-02-14,2028,2,14,monday +2028-02-16,2028,2,16,wednesday +2028-02-17,2028,2,17,thursday +2028-02-29,2028,2,29,tuesday +2028-03-03,2028,3,3,friday +2028-03-04,2028,3,4,saturday +2028-03-05,2028,3,5,sunday +2028-03-09,2028,3,9,thursday +2028-03-11,2028,3,11,saturday +2028-03-13,2028,3,13,monday +2028-03-14,2028,3,14,tuesday +2028-03-15,2028,3,15,wednesday +2028-03-16,2028,3,16,thursday +2028-03-17,2028,3,17,friday +2028-03-19,2028,3,19,sunday +2028-03-21,2028,3,21,tuesday +2028-03-23,2028,3,23,thursday +2028-03-24,2028,3,24,friday +2028-03-26,2028,3,26,sunday +2028-03-27,2028,3,27,monday +2028-04-01,2028,4,1,saturday +2028-04-05,2028,4,5,wednesday +2028-04-07,2028,4,7,friday +2028-04-09,2028,4,9,sunday +2028-04-12,2028,4,12,wednesday +2028-04-14,2028,4,14,friday +2028-04-15,2028,4,15,saturday +2028-04-16,2028,4,16,sunday +2028-04-20,2028,4,20,thursday +2028-04-23,2028,4,23,sunday +2028-04-24,2028,4,24,monday +2028-04-25,2028,4,25,tuesday +2028-04-27,2028,4,27,thursday +2028-04-28,2028,4,28,friday +2028-04-30,2028,4,30,sunday +2028-05-01,2028,5,1,monday +2028-05-03,2028,5,3,wednesday +2028-05-04,2028,5,4,thursday +2028-05-05,2028,5,5,friday +2028-05-07,2028,5,7,sunday +2028-05-08,2028,5,8,monday +2028-05-13,2028,5,13,saturday +2028-05-16,2028,5,16,tuesday +2028-05-17,2028,5,17,wednesday +2028-05-18,2028,5,18,thursday +2028-05-19,2028,5,19,friday +2028-05-21,2028,5,21,sunday +2028-05-22,2028,5,22,monday +2028-05-26,2028,5,26,friday +2028-05-31,2028,5,31,wednesday +2028-06-02,2028,6,2,friday +2028-06-03,2028,6,3,saturday +2028-06-04,2028,6,4,sunday +2028-06-05,2028,6,5,monday +2028-06-08,2028,6,8,thursday +2028-06-10,2028,6,10,saturday +2028-06-11,2028,6,11,sunday +2028-06-14,2028,6,14,wednesday +2028-06-15,2028,6,15,thursday +2028-06-18,2028,6,18,sunday +2028-06-19,2028,6,19,monday +2028-06-21,2028,6,21,wednesday +2028-06-22,2028,6,22,thursday +2028-06-24,2028,6,24,saturday +2028-06-26,2028,6,26,monday +2028-06-27,2028,6,27,tuesday +2028-06-28,2028,6,28,wednesday +2028-06-30,2028,6,30,friday +2028-07-01,2028,7,1,saturday +2028-07-02,2028,7,2,sunday +2028-07-03,2028,7,3,monday +2028-07-05,2028,7,5,wednesday +2028-07-06,2028,7,6,thursday +2028-07-08,2028,7,8,saturday +2028-07-10,2028,7,10,monday +2028-07-11,2028,7,11,tuesday +2028-07-14,2028,7,14,friday +2028-07-15,2028,7,15,saturday +2028-07-20,2028,7,20,thursday +2028-07-21,2028,7,21,friday +2028-07-25,2028,7,25,tuesday +2028-07-29,2028,7,29,saturday +2028-07-30,2028,7,30,sunday +2028-08-01,2028,8,1,tuesday +2028-08-03,2028,8,3,thursday +2028-08-09,2028,8,9,wednesday +2028-08-10,2028,8,10,thursday +2028-08-11,2028,8,11,friday +2028-08-12,2028,8,12,saturday +2028-08-15,2028,8,15,tuesday +2028-08-16,2028,8,16,wednesday +2028-08-17,2028,8,17,thursday +2028-08-19,2028,8,19,saturday +2028-08-20,2028,8,20,sunday +2028-08-21,2028,8,21,monday +2028-08-22,2028,8,22,tuesday +2028-08-24,2028,8,24,thursday +2028-09-01,2028,9,1,friday +2028-09-04,2028,9,4,monday +2028-09-06,2028,9,6,wednesday +2028-09-07,2028,9,7,thursday +2028-09-10,2028,9,10,sunday +2028-09-13,2028,9,13,wednesday +2028-09-14,2028,9,14,thursday +2028-09-16,2028,9,16,saturday +2028-09-18,2028,9,18,monday +2028-09-21,2028,9,21,thursday +2028-09-22,2028,9,22,friday +2028-09-23,2028,9,23,saturday +2028-09-24,2028,9,24,sunday +2028-09-26,2028,9,26,tuesday +2028-09-29,2028,9,29,friday +2028-10-02,2028,10,2,monday +2028-10-03,2028,10,3,tuesday +2028-10-04,2028,10,4,wednesday +2028-10-07,2028,10,7,saturday +2028-10-08,2028,10,8,sunday +2028-10-09,2028,10,9,monday +2028-10-10,2028,10,10,tuesday +2028-10-11,2028,10,11,wednesday +2028-10-14,2028,10,14,saturday +2028-10-15,2028,10,15,sunday +2028-10-17,2028,10,17,tuesday +2028-10-18,2028,10,18,wednesday +2028-10-19,2028,10,19,thursday +2028-10-22,2028,10,22,sunday +2028-10-25,2028,10,25,wednesday +2028-10-27,2028,10,27,friday +2028-10-29,2028,10,29,sunday +2028-10-31,2028,10,31,tuesday +2028-11-02,2028,11,2,thursday +2028-11-03,2028,11,3,friday +2028-11-05,2028,11,5,sunday +2028-11-07,2028,11,7,tuesday +2028-11-09,2028,11,9,thursday +2028-11-11,2028,11,11,saturday +2028-11-12,2028,11,12,sunday +2028-11-14,2028,11,14,tuesday +2028-10-13,2028,10,13,friday +2028-10-16,2028,10,16,monday +2028-10-20,2028,10,20,friday +2028-10-21,2028,10,21,saturday +2028-10-23,2028,10,23,monday +2028-10-24,2028,10,24,tuesday +2028-10-26,2028,10,26,thursday +2028-10-28,2028,10,28,saturday +2028-10-30,2028,10,30,monday +2028-11-01,2028,11,1,wednesday +2028-11-04,2028,11,4,saturday +2028-11-06,2028,11,6,monday +2028-11-08,2028,11,8,wednesday +2028-11-10,2028,11,10,friday +2028-11-13,2028,11,13,monday +2028-11-16,2028,11,16,thursday +2028-11-17,2028,11,17,friday +2028-11-19,2028,11,19,sunday +2028-11-20,2028,11,20,monday +2028-11-22,2028,11,22,wednesday +2028-11-24,2028,11,24,friday +2028-11-25,2028,11,25,saturday +2028-11-29,2028,11,29,wednesday +2028-11-30,2028,11,30,thursday +2028-12-03,2028,12,3,sunday +2028-12-09,2028,12,9,saturday +2028-12-11,2028,12,11,monday +2028-12-12,2028,12,12,tuesday +2028-12-16,2028,12,16,saturday +2028-12-17,2028,12,17,sunday +2028-12-18,2028,12,18,monday +2028-12-19,2028,12,19,tuesday +2028-12-21,2028,12,21,thursday +2028-12-22,2028,12,22,friday +2028-12-27,2028,12,27,wednesday +2028-12-29,2028,12,29,friday +2029-01-01,2029,1,1,monday +2029-01-03,2029,1,3,wednesday +2029-01-06,2029,1,6,saturday +2029-01-08,2029,1,8,monday +2029-01-13,2029,1,13,saturday +2029-01-17,2029,1,17,wednesday +2029-01-19,2029,1,19,friday +2029-01-21,2029,1,21,sunday +2029-01-23,2029,1,23,tuesday +2029-01-26,2029,1,26,friday +2029-01-28,2029,1,28,sunday +2029-01-31,2029,1,31,wednesday +2029-02-01,2029,2,1,thursday +2029-02-03,2029,2,3,saturday +2029-02-04,2029,2,4,sunday +2029-02-05,2029,2,5,monday +2029-02-08,2029,2,8,thursday +2029-02-17,2029,2,17,saturday +2029-02-18,2029,2,18,sunday +2029-02-19,2029,2,19,monday +2029-03-06,2029,3,6,tuesday +2029-03-13,2029,3,13,tuesday +2029-03-14,2029,3,14,wednesday +2029-03-15,2029,3,15,thursday +2029-03-16,2029,3,16,friday +2029-03-18,2029,3,18,sunday +2029-03-20,2029,3,20,tuesday +2029-03-21,2029,3,21,wednesday +2029-03-23,2029,3,23,friday +2029-03-25,2029,3,25,sunday +2029-03-28,2029,3,28,wednesday +2029-03-29,2029,3,29,thursday +2029-04-01,2029,4,1,sunday +2029-04-03,2029,4,3,tuesday +2029-04-06,2029,4,6,friday +2029-04-08,2029,4,8,sunday +2029-04-12,2029,4,12,thursday +2029-04-13,2029,4,13,friday +2029-04-14,2029,4,14,saturday +2029-04-16,2029,4,16,monday +2029-04-19,2029,4,19,thursday +2029-04-20,2029,4,20,friday +2029-04-25,2029,4,25,wednesday +2029-04-28,2029,4,28,saturday +2029-04-29,2029,4,29,sunday +2029-05-01,2029,5,1,tuesday +2029-05-02,2029,5,2,wednesday +2029-05-03,2029,5,3,thursday +2029-05-05,2029,5,5,saturday +2029-05-06,2029,5,6,sunday +2029-05-08,2029,5,8,tuesday +2029-05-10,2029,5,10,thursday +2029-05-16,2029,5,16,wednesday +2029-05-19,2029,5,19,saturday +2029-05-22,2029,5,22,tuesday +2029-05-23,2029,5,23,wednesday +2029-05-26,2029,5,26,saturday +2029-05-27,2029,5,27,sunday +2029-05-29,2029,5,29,tuesday +2029-06-03,2029,6,3,sunday +2029-06-05,2029,6,5,tuesday +2029-06-09,2029,6,9,saturday +2029-06-11,2029,6,11,monday +2029-06-12,2029,6,12,tuesday +2029-06-13,2029,6,13,wednesday +2029-06-17,2029,6,17,sunday +2029-06-19,2029,6,19,tuesday +2029-06-21,2029,6,21,thursday +2029-06-22,2029,6,22,friday +2029-06-23,2029,6,23,saturday +2029-06-25,2029,6,25,monday +2029-06-26,2029,6,26,tuesday +2029-06-27,2029,6,27,wednesday +2029-06-28,2029,6,28,thursday +2029-06-30,2029,6,30,saturday +2029-07-01,2029,7,1,sunday +2029-07-02,2029,7,2,monday +2029-07-03,2029,7,3,tuesday +2029-07-04,2029,7,4,wednesday +2029-07-08,2029,7,8,sunday +2029-07-09,2029,7,9,monday +2029-07-11,2029,7,11,wednesday +2029-07-12,2029,7,12,thursday +2029-07-17,2029,7,17,tuesday +2029-07-18,2029,7,18,wednesday +2029-07-19,2029,7,19,thursday +2029-07-20,2029,7,20,friday +2029-07-23,2029,7,23,monday +2029-07-25,2029,7,25,wednesday +2029-07-26,2029,7,26,thursday +2029-07-27,2029,7,27,friday +2029-07-28,2029,7,28,saturday +2029-08-02,2029,8,2,thursday +2029-08-04,2029,8,4,saturday +2029-08-05,2029,8,5,sunday +2029-08-08,2029,8,8,wednesday +2029-08-09,2029,8,9,thursday +2029-08-10,2029,8,10,friday +2029-08-12,2029,8,12,sunday +2029-08-13,2029,8,13,monday +2029-08-14,2029,8,14,tuesday +2029-08-15,2029,8,15,wednesday +2029-08-16,2029,8,16,thursday +2029-08-18,2029,8,18,saturday +2029-08-19,2029,8,19,sunday +2029-08-20,2029,8,20,monday +2029-08-22,2029,8,22,wednesday +2029-08-23,2029,8,23,thursday +2029-08-26,2029,8,26,sunday +2029-08-28,2029,8,28,tuesday +2029-08-29,2029,8,29,wednesday +2029-08-31,2029,8,31,friday +2029-09-01,2029,9,1,saturday +2029-09-04,2029,9,4,tuesday +2029-09-08,2029,9,8,saturday +2029-09-09,2029,9,9,sunday +2029-09-12,2029,9,12,wednesday +2029-09-16,2029,9,16,sunday +2029-09-19,2029,9,19,wednesday +2029-09-21,2029,9,21,friday +2029-09-22,2029,9,22,saturday +2029-09-24,2029,9,24,monday +2029-09-26,2029,9,26,wednesday +2029-09-27,2029,9,27,thursday +2029-09-30,2029,9,30,sunday +2029-10-01,2029,10,1,monday +2029-10-03,2029,10,3,wednesday +2029-10-04,2029,10,4,thursday +2029-10-07,2029,10,7,sunday +2029-10-13,2029,10,13,saturday +2029-10-19,2029,10,19,friday +2029-10-20,2029,10,20,saturday +2029-10-23,2029,10,23,tuesday +2029-10-26,2029,10,26,friday +2029-10-30,2029,10,30,tuesday +2029-10-31,2029,10,31,wednesday +2029-11-04,2029,11,4,sunday +2029-11-05,2029,11,5,monday +2029-11-06,2029,11,6,tuesday +2029-11-09,2029,11,9,friday +2029-11-10,2029,11,10,saturday +2029-11-18,2029,11,18,sunday +2029-11-19,2029,11,19,monday +2029-11-20,2029,11,20,tuesday +2029-11-22,2029,11,22,thursday +2029-11-25,2029,11,25,sunday +2029-11-26,2029,11,26,monday +2029-11-28,2029,11,28,wednesday +2029-11-30,2029,11,30,friday +2029-12-03,2029,12,3,monday +2029-12-05,2029,12,5,wednesday +2029-12-07,2029,12,7,friday +2029-12-08,2029,12,8,saturday +2029-12-09,2029,12,9,sunday +2029-12-11,2029,12,11,tuesday +2029-12-15,2029,12,15,saturday +2029-12-16,2029,12,16,sunday +2029-12-18,2029,12,18,tuesday +2029-12-19,2029,12,19,wednesday +2029-12-20,2029,12,20,thursday +2029-12-21,2029,12,21,friday +2029-12-22,2029,12,22,saturday +2029-12-23,2029,12,23,sunday +2029-12-24,2029,12,24,monday +2029-12-25,2029,12,25,tuesday +2029-12-26,2029,12,26,wednesday +2029-12-30,2029,12,30,sunday +2030-01-01,2030,1,1,tuesday +2030-01-04,2030,1,4,friday +2030-01-05,2030,1,5,saturday +2030-01-07,2030,1,7,monday +2030-01-12,2030,1,12,saturday +2030-01-13,2030,1,13,sunday +2030-01-16,2030,1,16,wednesday +2030-01-18,2030,1,18,friday +2030-01-20,2030,1,20,sunday +2030-01-21,2030,1,21,monday +2028-11-15,2028,11,15,wednesday +2028-11-18,2028,11,18,saturday +2028-11-21,2028,11,21,tuesday +2028-11-23,2028,11,23,thursday +2028-11-26,2028,11,26,sunday +2028-11-27,2028,11,27,monday +2028-11-28,2028,11,28,tuesday +2028-12-01,2028,12,1,friday +2028-12-02,2028,12,2,saturday +2028-12-04,2028,12,4,monday +2028-12-05,2028,12,5,tuesday +2028-12-06,2028,12,6,wednesday +2028-12-07,2028,12,7,thursday +2028-12-08,2028,12,8,friday +2028-12-10,2028,12,10,sunday +2028-12-13,2028,12,13,wednesday +2028-12-14,2028,12,14,thursday +2028-12-15,2028,12,15,friday +2028-12-20,2028,12,20,wednesday +2028-12-23,2028,12,23,saturday +2028-12-24,2028,12,24,sunday +2028-12-25,2028,12,25,monday +2028-12-26,2028,12,26,tuesday +2028-12-28,2028,12,28,thursday +2028-12-30,2028,12,30,saturday +2028-12-31,2028,12,31,sunday +2029-01-02,2029,1,2,tuesday +2029-01-04,2029,1,4,thursday +2029-01-05,2029,1,5,friday +2029-01-07,2029,1,7,sunday +2029-01-09,2029,1,9,tuesday +2029-01-10,2029,1,10,wednesday +2029-01-11,2029,1,11,thursday +2029-01-12,2029,1,12,friday +2029-01-14,2029,1,14,sunday +2029-01-15,2029,1,15,monday +2029-01-16,2029,1,16,tuesday +2029-01-18,2029,1,18,thursday +2029-01-20,2029,1,20,saturday +2029-01-22,2029,1,22,monday +2029-01-24,2029,1,24,wednesday +2029-01-25,2029,1,25,thursday +2029-01-27,2029,1,27,saturday +2029-01-29,2029,1,29,monday +2029-01-30,2029,1,30,tuesday +2029-02-02,2029,2,2,friday +2029-02-06,2029,2,6,tuesday +2029-02-07,2029,2,7,wednesday +2029-02-09,2029,2,9,friday +2029-02-10,2029,2,10,saturday +2029-02-11,2029,2,11,sunday +2029-02-12,2029,2,12,monday +2029-02-13,2029,2,13,tuesday +2029-02-14,2029,2,14,wednesday +2029-02-15,2029,2,15,thursday +2029-02-16,2029,2,16,friday +2029-02-20,2029,2,20,tuesday +2029-02-21,2029,2,21,wednesday +2029-02-22,2029,2,22,thursday +2029-02-23,2029,2,23,friday +2029-02-24,2029,2,24,saturday +2029-02-25,2029,2,25,sunday +2029-02-26,2029,2,26,monday +2029-02-27,2029,2,27,tuesday +2029-02-28,2029,2,28,wednesday +2029-03-01,2029,3,1,thursday +2029-03-02,2029,3,2,friday +2029-03-03,2029,3,3,saturday +2029-03-04,2029,3,4,sunday +2029-03-05,2029,3,5,monday +2029-03-07,2029,3,7,wednesday +2029-03-08,2029,3,8,thursday +2029-03-09,2029,3,9,friday +2029-03-10,2029,3,10,saturday +2029-03-11,2029,3,11,sunday +2029-03-12,2029,3,12,monday +2029-03-17,2029,3,17,saturday +2029-03-19,2029,3,19,monday +2029-03-22,2029,3,22,thursday +2029-03-24,2029,3,24,saturday +2029-03-26,2029,3,26,monday +2029-03-27,2029,3,27,tuesday +2029-03-30,2029,3,30,friday +2029-03-31,2029,3,31,saturday +2029-04-02,2029,4,2,monday +2029-04-04,2029,4,4,wednesday +2029-04-05,2029,4,5,thursday +2029-04-07,2029,4,7,saturday +2029-04-09,2029,4,9,monday +2029-04-10,2029,4,10,tuesday +2029-04-11,2029,4,11,wednesday +2029-04-15,2029,4,15,sunday +2029-04-17,2029,4,17,tuesday +2029-04-18,2029,4,18,wednesday +2029-04-21,2029,4,21,saturday +2029-04-22,2029,4,22,sunday +2029-04-23,2029,4,23,monday +2029-04-24,2029,4,24,tuesday +2029-04-26,2029,4,26,thursday +2029-04-27,2029,4,27,friday +2029-04-30,2029,4,30,monday +2029-05-04,2029,5,4,friday +2029-05-07,2029,5,7,monday +2029-05-09,2029,5,9,wednesday +2029-05-11,2029,5,11,friday +2029-05-12,2029,5,12,saturday +2029-05-13,2029,5,13,sunday +2029-05-14,2029,5,14,monday +2029-05-15,2029,5,15,tuesday +2029-05-17,2029,5,17,thursday +2029-05-18,2029,5,18,friday +2029-05-20,2029,5,20,sunday +2029-05-21,2029,5,21,monday +2029-05-24,2029,5,24,thursday +2029-05-25,2029,5,25,friday +2029-05-28,2029,5,28,monday +2029-05-30,2029,5,30,wednesday +2029-05-31,2029,5,31,thursday +2029-06-01,2029,6,1,friday +2029-06-02,2029,6,2,saturday +2029-06-04,2029,6,4,monday +2029-06-06,2029,6,6,wednesday +2029-06-07,2029,6,7,thursday +2029-06-08,2029,6,8,friday +2029-06-10,2029,6,10,sunday +2029-06-14,2029,6,14,thursday +2029-06-15,2029,6,15,friday +2029-06-16,2029,6,16,saturday +2029-06-18,2029,6,18,monday +2029-06-20,2029,6,20,wednesday +2029-06-24,2029,6,24,sunday +2029-06-29,2029,6,29,friday +2029-07-05,2029,7,5,thursday +2029-07-06,2029,7,6,friday +2029-07-07,2029,7,7,saturday +2029-07-10,2029,7,10,tuesday +2029-07-13,2029,7,13,friday +2029-07-14,2029,7,14,saturday +2029-07-15,2029,7,15,sunday +2029-07-16,2029,7,16,monday +2029-07-21,2029,7,21,saturday +2029-07-22,2029,7,22,sunday +2029-07-24,2029,7,24,tuesday +2029-07-29,2029,7,29,sunday +2029-07-30,2029,7,30,monday +2029-07-31,2029,7,31,tuesday +2029-08-01,2029,8,1,wednesday +2029-08-03,2029,8,3,friday +2029-08-06,2029,8,6,monday +2029-08-07,2029,8,7,tuesday +2029-08-11,2029,8,11,saturday +2029-08-17,2029,8,17,friday +2029-08-21,2029,8,21,tuesday +2029-08-24,2029,8,24,friday +2029-08-25,2029,8,25,saturday +2029-08-27,2029,8,27,monday +2029-08-30,2029,8,30,thursday +2029-09-02,2029,9,2,sunday +2029-09-03,2029,9,3,monday +2029-09-05,2029,9,5,wednesday +2029-09-06,2029,9,6,thursday +2029-09-07,2029,9,7,friday +2029-09-10,2029,9,10,monday +2029-09-11,2029,9,11,tuesday +2029-09-13,2029,9,13,thursday +2029-09-14,2029,9,14,friday +2029-09-15,2029,9,15,saturday +2029-09-17,2029,9,17,monday +2029-09-18,2029,9,18,tuesday +2029-09-20,2029,9,20,thursday +2029-09-23,2029,9,23,sunday +2029-09-25,2029,9,25,tuesday +2029-09-28,2029,9,28,friday +2029-09-29,2029,9,29,saturday +2029-10-02,2029,10,2,tuesday +2029-10-05,2029,10,5,friday +2029-10-06,2029,10,6,saturday +2029-10-08,2029,10,8,monday +2029-10-09,2029,10,9,tuesday +2029-10-10,2029,10,10,wednesday +2029-10-11,2029,10,11,thursday +2029-10-12,2029,10,12,friday +2029-10-14,2029,10,14,sunday +2029-10-15,2029,10,15,monday +2029-10-16,2029,10,16,tuesday +2029-10-17,2029,10,17,wednesday +2029-10-18,2029,10,18,thursday +2029-10-21,2029,10,21,sunday +2029-10-22,2029,10,22,monday +2029-10-24,2029,10,24,wednesday +2029-10-25,2029,10,25,thursday +2029-10-27,2029,10,27,saturday +2029-10-28,2029,10,28,sunday +2029-10-29,2029,10,29,monday +2029-11-01,2029,11,1,thursday +2029-11-02,2029,11,2,friday +2029-11-03,2029,11,3,saturday +2029-11-07,2029,11,7,wednesday +2029-11-08,2029,11,8,thursday +2029-11-11,2029,11,11,sunday +2029-11-12,2029,11,12,monday +2029-11-13,2029,11,13,tuesday +2029-11-14,2029,11,14,wednesday +2029-11-15,2029,11,15,thursday +2029-11-16,2029,11,16,friday +2029-11-17,2029,11,17,saturday +2029-11-21,2029,11,21,wednesday +2029-11-23,2029,11,23,friday +2029-11-24,2029,11,24,saturday +2029-11-27,2029,11,27,tuesday +2029-11-29,2029,11,29,thursday +2029-12-01,2029,12,1,saturday +2029-12-02,2029,12,2,sunday +2030-01-22,2030,1,22,tuesday +2030-01-23,2030,1,23,wednesday +2030-01-27,2030,1,27,sunday +2030-01-31,2030,1,31,thursday +2030-02-02,2030,2,2,saturday +2030-02-05,2030,2,5,tuesday +2030-02-06,2030,2,6,wednesday +2030-02-09,2030,2,9,saturday +2030-02-11,2030,2,11,monday +2030-02-14,2030,2,14,thursday +2030-02-15,2030,2,15,friday +2030-02-16,2030,2,16,saturday +2030-02-17,2030,2,17,sunday +2030-02-20,2030,2,20,wednesday +2030-02-23,2030,2,23,saturday +2030-02-24,2030,2,24,sunday +2030-02-27,2030,2,27,wednesday +2030-03-01,2030,3,1,friday +2030-03-04,2030,3,4,monday +2030-03-05,2030,3,5,tuesday +2030-03-07,2030,3,7,thursday +2030-03-08,2030,3,8,friday +2030-03-10,2030,3,10,sunday +2030-03-11,2030,3,11,monday +2030-03-12,2030,3,12,tuesday +2030-03-13,2030,3,13,wednesday +2030-03-15,2030,3,15,friday +2030-03-16,2030,3,16,saturday +2030-03-17,2030,3,17,sunday +2030-03-18,2030,3,18,monday +2030-03-19,2030,3,19,tuesday +2030-03-22,2030,3,22,friday +2030-03-24,2030,3,24,sunday +2030-03-26,2030,3,26,tuesday +2030-03-27,2030,3,27,wednesday +2030-03-28,2030,3,28,thursday +2030-03-29,2030,3,29,friday +2030-03-30,2030,3,30,saturday +2030-04-02,2030,4,2,tuesday +2030-04-04,2030,4,4,thursday +2030-04-05,2030,4,5,friday +2030-04-11,2030,4,11,thursday +2030-04-13,2030,4,13,saturday +2030-04-15,2030,4,15,monday +2030-04-16,2030,4,16,tuesday +2030-04-18,2030,4,18,thursday +2030-04-19,2030,4,19,friday +2030-04-20,2030,4,20,saturday +2030-04-25,2030,4,25,thursday +2030-04-28,2030,4,28,sunday +2030-04-29,2030,4,29,monday +2030-05-01,2030,5,1,wednesday +2030-05-06,2030,5,6,monday +2030-05-07,2030,5,7,tuesday +2030-05-11,2030,5,11,saturday +2030-05-15,2030,5,15,wednesday +2030-05-17,2030,5,17,friday +2030-05-18,2030,5,18,saturday +2030-05-21,2030,5,21,tuesday +2030-05-22,2030,5,22,wednesday +2030-05-25,2030,5,25,saturday +2030-05-28,2030,5,28,tuesday +2030-05-29,2030,5,29,wednesday +2030-05-30,2030,5,30,thursday +2030-06-01,2030,6,1,saturday +2030-06-04,2030,6,4,tuesday +2030-06-05,2030,6,5,wednesday +2030-06-06,2030,6,6,thursday +2030-06-08,2030,6,8,saturday +2030-06-13,2030,6,13,thursday +2030-06-22,2030,6,22,saturday +2030-06-25,2030,6,25,tuesday +2030-06-26,2030,6,26,wednesday +2030-06-27,2030,6,27,thursday +2030-06-30,2030,6,30,sunday +2030-07-01,2030,7,1,monday +2030-07-02,2030,7,2,tuesday +2030-07-05,2030,7,5,friday +2030-07-06,2030,7,6,saturday +2030-07-09,2030,7,9,tuesday +2030-07-14,2030,7,14,sunday +2030-07-16,2030,7,16,tuesday +2030-07-20,2030,7,20,saturday +2030-07-23,2030,7,23,tuesday +2030-07-24,2030,7,24,wednesday +2030-07-25,2030,7,25,thursday +2030-07-26,2030,7,26,friday +2030-07-28,2030,7,28,sunday +2030-07-30,2030,7,30,tuesday +2030-07-31,2030,7,31,wednesday +2030-08-02,2030,8,2,friday +2030-08-06,2030,8,6,tuesday +2030-08-10,2030,8,10,saturday +2030-08-12,2030,8,12,monday +2030-08-14,2030,8,14,wednesday +2030-08-17,2030,8,17,saturday +2030-08-18,2030,8,18,sunday +2030-08-24,2030,8,24,saturday +2030-08-26,2030,8,26,monday +2030-08-28,2030,8,28,wednesday +2030-08-29,2030,8,29,thursday +2030-08-30,2030,8,30,friday +2030-09-01,2030,9,1,sunday +2030-09-02,2030,9,2,monday +2030-09-03,2030,9,3,tuesday +2030-09-05,2030,9,5,thursday +2030-09-07,2030,9,7,saturday +2030-09-08,2030,9,8,sunday +2030-09-11,2030,9,11,wednesday +2030-09-12,2030,9,12,thursday +2030-09-13,2030,9,13,friday +2030-09-15,2030,9,15,sunday +2030-09-17,2030,9,17,tuesday +2030-09-18,2030,9,18,wednesday +2030-09-23,2030,9,23,monday +2030-09-24,2030,9,24,tuesday +2030-09-26,2030,9,26,thursday +2030-09-27,2030,9,27,friday +2030-09-28,2030,9,28,saturday +2030-09-29,2030,9,29,sunday +2030-09-30,2030,9,30,monday +2030-10-01,2030,10,1,tuesday +2030-10-02,2030,10,2,wednesday +2030-10-03,2030,10,3,thursday +2030-10-07,2030,10,7,monday +2030-10-19,2030,10,19,saturday +2030-10-22,2030,10,22,tuesday +2030-10-23,2030,10,23,wednesday +2030-10-28,2030,10,28,monday +2030-10-29,2030,10,29,tuesday +2030-10-31,2030,10,31,thursday +2030-11-01,2030,11,1,friday +2030-11-04,2030,11,4,monday +2030-11-05,2030,11,5,tuesday +2030-11-07,2030,11,7,thursday +2030-11-11,2030,11,11,monday +2030-11-15,2030,11,15,friday +2030-11-16,2030,11,16,saturday +2030-11-17,2030,11,17,sunday +2030-11-19,2030,11,19,tuesday +2030-11-20,2030,11,20,wednesday +2030-11-21,2030,11,21,thursday +2030-11-22,2030,11,22,friday +2030-11-25,2030,11,25,monday +2030-11-27,2030,11,27,wednesday +2030-11-29,2030,11,29,friday +2030-11-30,2030,11,30,saturday +2030-12-11,2030,12,11,wednesday +2030-12-14,2030,12,14,saturday +2030-12-15,2030,12,15,sunday +2030-12-19,2030,12,19,thursday +2030-12-20,2030,12,20,friday +2030-12-21,2030,12,21,saturday +2030-12-23,2030,12,23,monday +2030-12-30,2030,12,30,monday +2029-12-04,2029,12,4,tuesday +2029-12-06,2029,12,6,thursday +2029-12-10,2029,12,10,monday +2029-12-12,2029,12,12,wednesday +2029-12-13,2029,12,13,thursday +2029-12-14,2029,12,14,friday +2029-12-17,2029,12,17,monday +2029-12-27,2029,12,27,thursday +2029-12-28,2029,12,28,friday +2029-12-29,2029,12,29,saturday +2029-12-31,2029,12,31,monday +2030-01-02,2030,1,2,wednesday +2030-01-03,2030,1,3,thursday +2030-01-06,2030,1,6,sunday +2030-01-08,2030,1,8,tuesday +2030-01-09,2030,1,9,wednesday +2030-01-10,2030,1,10,thursday +2030-01-11,2030,1,11,friday +2030-01-14,2030,1,14,monday +2030-01-15,2030,1,15,tuesday +2030-01-17,2030,1,17,thursday +2030-01-19,2030,1,19,saturday +2030-01-24,2030,1,24,thursday +2030-01-25,2030,1,25,friday +2030-01-26,2030,1,26,saturday +2030-01-28,2030,1,28,monday +2030-01-29,2030,1,29,tuesday +2030-01-30,2030,1,30,wednesday +2030-02-01,2030,2,1,friday +2030-02-03,2030,2,3,sunday +2030-02-04,2030,2,4,monday +2030-02-07,2030,2,7,thursday +2030-02-08,2030,2,8,friday +2030-02-10,2030,2,10,sunday +2030-02-12,2030,2,12,tuesday +2030-02-13,2030,2,13,wednesday +2030-02-18,2030,2,18,monday +2030-02-19,2030,2,19,tuesday +2030-02-21,2030,2,21,thursday +2030-02-22,2030,2,22,friday +2030-02-25,2030,2,25,monday +2030-02-26,2030,2,26,tuesday +2030-02-28,2030,2,28,thursday +2030-03-02,2030,3,2,saturday +2030-03-03,2030,3,3,sunday +2030-03-06,2030,3,6,wednesday +2030-03-09,2030,3,9,saturday +2030-03-14,2030,3,14,thursday +2030-03-20,2030,3,20,wednesday +2030-03-21,2030,3,21,thursday +2030-03-23,2030,3,23,saturday +2030-03-25,2030,3,25,monday +2030-03-31,2030,3,31,sunday +2030-04-01,2030,4,1,monday +2030-04-03,2030,4,3,wednesday +2030-04-06,2030,4,6,saturday +2030-04-07,2030,4,7,sunday +2030-04-08,2030,4,8,monday +2030-04-09,2030,4,9,tuesday +2030-04-10,2030,4,10,wednesday +2030-04-12,2030,4,12,friday +2030-04-14,2030,4,14,sunday +2030-04-17,2030,4,17,wednesday +2030-04-21,2030,4,21,sunday +2030-04-22,2030,4,22,monday +2030-04-23,2030,4,23,tuesday +2030-04-24,2030,4,24,wednesday +2030-04-26,2030,4,26,friday +2030-04-27,2030,4,27,saturday +2030-04-30,2030,4,30,tuesday +2030-05-02,2030,5,2,thursday +2030-05-03,2030,5,3,friday +2030-05-04,2030,5,4,saturday +2030-05-05,2030,5,5,sunday +2030-05-08,2030,5,8,wednesday +2030-05-09,2030,5,9,thursday +2030-05-10,2030,5,10,friday +2030-05-12,2030,5,12,sunday +2030-05-13,2030,5,13,monday +2030-05-14,2030,5,14,tuesday +2030-05-16,2030,5,16,thursday +2030-05-19,2030,5,19,sunday +2030-05-20,2030,5,20,monday +2030-05-23,2030,5,23,thursday +2030-05-24,2030,5,24,friday +2030-05-26,2030,5,26,sunday +2030-05-27,2030,5,27,monday +2030-05-31,2030,5,31,friday +2030-06-02,2030,6,2,sunday +2030-06-03,2030,6,3,monday +2030-06-07,2030,6,7,friday +2030-06-09,2030,6,9,sunday +2030-06-10,2030,6,10,monday +2030-06-11,2030,6,11,tuesday +2030-06-12,2030,6,12,wednesday +2030-06-14,2030,6,14,friday +2030-06-15,2030,6,15,saturday +2030-06-16,2030,6,16,sunday +2030-06-17,2030,6,17,monday +2030-06-18,2030,6,18,tuesday +2030-06-19,2030,6,19,wednesday +2030-06-20,2030,6,20,thursday +2030-06-21,2030,6,21,friday +2030-06-23,2030,6,23,sunday +2030-06-24,2030,6,24,monday +2030-06-28,2030,6,28,friday +2030-06-29,2030,6,29,saturday +2030-07-03,2030,7,3,wednesday +2030-07-04,2030,7,4,thursday +2030-07-07,2030,7,7,sunday +2030-07-08,2030,7,8,monday +2030-07-10,2030,7,10,wednesday +2030-07-11,2030,7,11,thursday +2030-07-12,2030,7,12,friday +2030-07-13,2030,7,13,saturday +2030-07-15,2030,7,15,monday +2030-07-17,2030,7,17,wednesday +2030-07-18,2030,7,18,thursday +2030-07-19,2030,7,19,friday +2030-07-21,2030,7,21,sunday +2030-07-22,2030,7,22,monday +2030-07-27,2030,7,27,saturday +2030-07-29,2030,7,29,monday +2030-08-01,2030,8,1,thursday +2030-08-03,2030,8,3,saturday +2030-08-04,2030,8,4,sunday +2030-08-05,2030,8,5,monday +2030-08-07,2030,8,7,wednesday +2030-08-08,2030,8,8,thursday +2030-08-09,2030,8,9,friday +2030-08-11,2030,8,11,sunday +2030-08-13,2030,8,13,tuesday +2030-08-15,2030,8,15,thursday +2030-08-16,2030,8,16,friday +2030-08-19,2030,8,19,monday +2030-08-20,2030,8,20,tuesday +2030-08-21,2030,8,21,wednesday +2030-08-22,2030,8,22,thursday +2030-08-23,2030,8,23,friday +2030-08-25,2030,8,25,sunday +2030-08-27,2030,8,27,tuesday +2030-08-31,2030,8,31,saturday +2030-09-04,2030,9,4,wednesday +2030-09-06,2030,9,6,friday +2030-09-09,2030,9,9,monday +2030-09-10,2030,9,10,tuesday +2030-09-14,2030,9,14,saturday +2030-09-16,2030,9,16,monday +2030-09-19,2030,9,19,thursday +2030-09-20,2030,9,20,friday +2030-09-21,2030,9,21,saturday +2030-09-22,2030,9,22,sunday +2030-09-25,2030,9,25,wednesday +2030-10-04,2030,10,4,friday +2030-10-05,2030,10,5,saturday +2030-10-06,2030,10,6,sunday +2030-10-08,2030,10,8,tuesday +2030-10-09,2030,10,9,wednesday +2030-10-10,2030,10,10,thursday +2030-10-11,2030,10,11,friday +2030-10-12,2030,10,12,saturday +2030-10-13,2030,10,13,sunday +2030-10-14,2030,10,14,monday +2030-10-15,2030,10,15,tuesday +2030-10-16,2030,10,16,wednesday +2030-10-17,2030,10,17,thursday +2030-10-18,2030,10,18,friday +2030-10-20,2030,10,20,sunday +2030-10-21,2030,10,21,monday +2030-10-24,2030,10,24,thursday +2030-10-25,2030,10,25,friday +2030-10-26,2030,10,26,saturday +2030-10-27,2030,10,27,sunday +2030-10-30,2030,10,30,wednesday +2030-11-02,2030,11,2,saturday +2030-11-03,2030,11,3,sunday +2030-11-06,2030,11,6,wednesday +2030-11-08,2030,11,8,friday +2030-11-09,2030,11,9,saturday +2030-11-10,2030,11,10,sunday +2030-11-12,2030,11,12,tuesday +2030-11-13,2030,11,13,wednesday +2030-11-14,2030,11,14,thursday +2030-11-18,2030,11,18,monday +2030-11-23,2030,11,23,saturday +2030-11-24,2030,11,24,sunday +2030-11-26,2030,11,26,tuesday +2030-11-28,2030,11,28,thursday +2030-12-01,2030,12,1,sunday +2030-12-02,2030,12,2,monday +2030-12-03,2030,12,3,tuesday +2030-12-04,2030,12,4,wednesday +2030-12-05,2030,12,5,thursday +2030-12-06,2030,12,6,friday +2030-12-07,2030,12,7,saturday +2030-12-08,2030,12,8,sunday +2030-12-09,2030,12,9,monday +2030-12-10,2030,12,10,tuesday +2030-12-12,2030,12,12,thursday +2030-12-13,2030,12,13,friday +2030-12-16,2030,12,16,monday +2030-12-17,2030,12,17,tuesday +2030-12-18,2030,12,18,wednesday +2030-12-22,2030,12,22,sunday +2030-12-24,2030,12,24,tuesday +2030-12-25,2030,12,25,wednesday +2030-12-26,2030,12,26,thursday +2030-12-27,2030,12,27,friday +2030-12-28,2030,12,28,saturday +2030-12-29,2030,12,29,sunday +2030-12-31,2030,12,31,tuesday diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv new file mode 100644 index 0000000..9383505 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv @@ -0,0 +1,19 @@ +"lesson_id","title","duration_min","course_id" +2,JOIN,15,1 +3,WHERE и фильтрация,12,1 +4,Агрегации и GROUP BY,18,1 +6,Pandas основы,20,2 +7,NumPy,15,2 +8,Визуализация данных,22,2 +9,Основы BI,12,3 +10,Метрики и дашборды,18,3 +13,Сводные таблицы,18,4 +16,Линейная регрессия,30,5 +18,Кластеризация,25,5 +1,SELECT и FROM,10,1 +5,Подзапросы,25,1 +11,Power BI введение,20,3 +12,Формулы в Excel,15,4 +14,Графики и диаграммы,14,4 +15,Введение в ML,20,5 +17,Классификация,28,5 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv new file mode 100644 index 0000000..a31ef6a --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv @@ -0,0 +1,21 @@ +"user_id","name","age","email","registration_date" +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv new file mode 100644 index 0000000..e3a32cd --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv @@ -0,0 +1,46 @@ +"user_id","enrolled_at","course_id" +2,2023-01-20 00:00:00.000,1 +3,2023-03-05 00:00:00.000,2 +6,2023-01-06 00:00:00.000,1 +6,2023-01-10 00:00:00.000,2 +6,2023-02-05 00:00:00.000,4 +7,2023-01-13 00:00:00.000,1 +7,2023-03-10 00:00:00.000,3 +8,2023-01-22 00:00:00.000,3 +13,2023-01-19 00:00:00.000,2 +13,2023-02-20 00:00:00.000,4 +16,2023-02-16 00:00:00.000,2 +16,2023-02-16 00:00:00.000,4 +19,2023-04-11 00:00:00.000,1 +19,2023-04-11 00:00:00.000,2 +2,2023-03-15 00:00:00.000,3 +4,2023-03-10 00:00:00.000,2 +7,2023-02-01 00:00:00.000,2 +9,2023-02-15 00:00:00.000,1 +18,2023-04-05 00:00:00.000,4 +4,2023-04-01 00:00:00.000,3 +8,2023-01-22 00:00:00.000,1 +8,2023-01-22 00:00:00.000,2 +9,2023-02-12 00:00:00.000,4 +10,2023-04-16 00:00:00.000,1 +18,2023-04-02 00:00:00.000,1 +3,2023-02-25 00:00:00.000,1 +6,2023-03-08 00:00:00.000,3 +1,2023-02-01 00:00:00.000,2 +11,2023-04-21 00:00:00.000,2 +14,2023-01-09 00:00:00.000,2 +14,2023-01-09 00:00:00.000,3 +14,2023-03-20 00:00:00.000,5 +17,2023-03-22 00:00:00.000,3 +20,2023-04-19 00:00:00.000,2 +12,2023-03-10 00:00:00.000,3 +1,2023-01-15 00:00:00.000,1 +12,2023-01-16 00:00:00.000,1 +14,2023-01-09 00:00:00.000,1 +14,2023-02-01 00:00:00.000,4 +15,2023-01-11 00:00:00.000,1 +15,2023-03-05 00:00:00.000,3 +17,2023-03-25 00:00:00.000,5 +5,2023-03-20 00:00:00.000,5 +1,2023-02-10 00:00:00.000,4 +15,2023-02-01 00:00:00.000,2 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv new file mode 100644 index 0000000..7be0742 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv @@ -0,0 +1,59 @@ +"user_id","lesson_id","course_id","viewed_at" +3,6,2,2023-03-06 12:00:00.000 +4,9,3,2023-04-02 10:00:00.000 +4,6,2,2023-03-12 14:00:00.000 +4,7,2,2023-03-13 09:00:00.000 +6,3,1,2023-01-08 09:00:00.000 +6,6,2,2023-01-11 14:00:00.000 +6,7,2,2023-01-12 10:00:00.000 +6,8,2,2023-01-13 11:00:00.000 +6,9,3,2023-03-09 11:00:00.000 +7,9,3,2023-03-11 14:00:00.000 +2,9,3,2023-03-16 10:00:00.000 +2,10,3,2023-03-16 10:30:00.000 +3,2,1,2023-02-26 11:20:00.000 +4,10,3,2023-04-03 10:00:00.000 +6,2,1,2023-01-07 10:15:00.000 +6,4,1,2023-01-08 09:30:00.000 +6,13,4,2023-02-06 09:30:00.000 +7,2,1,2023-01-14 10:20:00.000 +7,3,1,2023-01-15 09:00:00.000 +7,10,3,2023-03-11 14:30:00.000 +9,13,4,2023-02-13 10:30:00.000 +13,6,2,2023-01-20 14:00:00.000 +13,7,2,2023-01-21 10:00:00.000 +2,1,1,2023-01-21 09:00:00.000 +6,1,1,2023-01-07 10:00:00.000 +6,14,4,2023-02-07 10:00:00.000 +7,1,1,2023-01-14 10:00:00.000 +9,12,4,2023-02-13 10:00:00.000 +9,1,1,2023-02-16 11:00:00.000 +3,1,1,2023-02-26 11:00:00.000 +6,5,1,2023-01-09 10:00:00.000 +6,12,4,2023-02-06 09:00:00.000 +13,12,4,2023-02-21 09:00:00.000 +1,2,1,2023-01-16 10:15:00.000 +1,3,1,2023-01-17 09:00:00.000 +1,4,1,2023-01-17 09:30:00.000 +1,4,1,2023-01-18 10:00:00.000 +1,6,2,2023-02-02 14:00:00.000 +1,13,4,2023-02-15 10:30:00.000 +5,16,5,2023-03-22 11:00:00.000 +12,2,1,2023-01-17 10:20:00.000 +12,3,1,2023-04-10 11:30:00.000 +14,2,1,2023-01-09 08:15:00.000 +14,4,1,2023-01-10 10:30:00.000 +14,6,2,2023-01-10 14:00:00.000 +14,9,3,2023-01-10 08:00:00.000 +1,4,1,2023-01-17 11:00:00.000 +12,9,3,2023-03-12 10:00:00.000 +14,3,1,2023-01-10 10:00:00.000 +14,7,2,2023-01-11 10:00:00.000 +14,8,2,2023-01-12 11:00:00.000 +1,12,4,2023-02-15 10:00:00.000 +12,1,1,2023-01-17 10:00:00.000 +14,5,1,2023-01-11 09:00:00.000 +1,1,1,2023-01-16 10:00:00.000 +5,15,5,2023-03-21 10:00:00.000 +12,1,1,2023-04-10 11:00:00.000 +14,1,1,2023-01-09 08:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql new file mode 100644 index 0000000..62c94cb --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql @@ -0,0 +1,97 @@ +-- DIM + +--DROP TABLE IF EXISTS mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_user +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + age UInt8, + email String, + registration_date DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_course ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_course +ON CLUSTER clickhouse_cluster +( + course_id UInt64, + title String, + category LowCardinality(String), + created_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_lesson +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + title String, + duration_min UInt16, + course_id UInt64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_lesson', + '{replica}' +) +ORDER BY (lesson_id, course_id); + +--DROP TABLE IF EXISTS mnoskov.core_dim_date ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_date +ON CLUSTER clickhouse_cluster +( + date Date, + year Int16, + month Int8, + day Int8, + weekday LowCardinality(String) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_date', + '{replica}' +) +ORDER BY date; + + +-- FACT + +--DROP TABLE IF EXISTS mnoskov.core_fact_lesson_views; +CREATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + lesson_id UInt64, + course_id UInt64, + viewed_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_fact_lesson_views', + '{replica}' +) +PARTITION BY toYYYYMM(viewed_at) +ORDER BY (viewed_at, user_id); -- я если честно не знаю как с этой таблицей поступить в ORDER BY. Наставьте меня на путь истиный. + +--DROP TABLE IF EXISTS mnoskov.core_enrollments; +CREATE TABLE mnoskov.core_enrollments +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + course_id UInt64, + enrolled_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_enrollments', + '{replica}' +) +PARTITION BY toYYYYMM(enrolled_at) +ORDER BY (enrolled_at, user_id); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql new file mode 100644 index 0000000..046c023 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql @@ -0,0 +1,128 @@ +--DROP TABLE mnoskov.lesson_popularity_summary ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + lesson_title String, + course_id UInt64, + course_title String, + total_views AggregateFunction(count), + unique_users AggregateFunction(uniq, UInt64), + first_view AggregateFunction(min, DateTime), + last_view AggregateFunction(max, DateTime) +) +ENGINE AggregatingMergeTree +ORDER BY lesson_id; + +--DROP VIEW mnoskov.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC; +CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.lesson_popularity_summary +AS +SELECT + cdl.lesson_id AS lesson_id, + cdl.title AS lesson_title, + cdc.course_id AS course_id, + cdc.title AS course_title, + countIfState(flv.user_id != 0) AS total_views, -- я вообще ахуел когда узнал что такое существует(я про If у агрегаций). Я наткнулся на это когда осознал что кликхаус в join не ставит null, а ставит значение по умолчанию + uniqIfState(flv.user_id, flv.user_id != 0) AS unique_users, + minIfState(flv.viewed_at, flv.user_id != 0) AS first_view, + maxIfState(flv.viewed_at, flv.user_id != 0) AS last_view +FROM + mnoskov.core_fact_lesson_views flv +RIGHT JOIN + mnoskov.core_dim_lesson cdl ON flv.lesson_id = cdl.lesson_id -- поидее нужно большую таблицу ставить в левую часть JOIN, но если я тут так сделаю с фактами, то запрос кореектно не будет работать +JOIN + mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id +GROUP BY + cdl.lesson_id, cdl.title, cdc.course_id, cdc.title; + + +SELECT * FROM mnoskov.core_fact_lesson_views; + +SELECT + lesson_id, + any(lesson_title), + any(course_id), + any(course_title), + countMerge(total_views) AS total_views, + uniqMerge(unique_users) AS uniq_users, + minMerge(first_view) AS first_view, + maxMerge(last_view) AS last_view +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + lesson_id; +-- ЕБАТЬ КАК Я АХУЕЛ КОГДА ДОПЕР ДО ТОГО ЧТО ЭТО НУЖНО ЧИТАТЬ ТАКИМ СПОСОБОМ А НЕ ПРОСТО ЧЕРЕЗ ЗВЕЗДОЧКУ. У меня MV ебать сколько изменений потерпела чтобы заработать, а проблема возможно изначально была в чтении. +-- Правда оно читается через раз, то есть результат то нет, но думаю это проблема движка AggregatingMergeTree. +-- Это получается чтобы был удобный вывод, теперь поверх этого селекта можно сделать обычную VIEW? + +-- Заполненния и очищения: +TRUNCATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster sync; + +TRUNCATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +VALUES +(3, 6, 2, '2023-03-06 12:00:00'), +(4, 9, 3, '2023-04-02 10:00:00'), +(4, 6, 2, '2023-03-12 14:00:00'), +(4, 7, 2, '2023-03-13 09:00:00'), +(6, 3, 1, '2023-01-08 09:00:00'), +(6, 6, 2, '2023-01-11 14:00:00'), +(6, 7, 2, '2023-01-12 10:00:00'), +(6, 8, 2, '2023-01-13 11:00:00'), +(6, 9, 3, '2023-03-09 11:00:00'), +(7, 9, 3, '2023-03-11 14:00:00'), +(2, 9, 3, '2023-03-16 10:00:00'), +(2, 10, 3, '2023-03-16 10:30:00'), +(3, 2, 1, '2023-02-26 11:20:00'), +(4, 10, 3, '2023-04-03 10:00:00'), +(6, 2, 1, '2023-01-07 10:15:00'), +(6, 4, 1, '2023-01-08 09:30:00'), +(6, 13, 4, '2023-02-06 09:30:00'), +(7, 2, 1, '2023-01-14 10:20:00'), +(7, 3, 1, '2023-01-15 09:00:00'), +(7, 10, 3, '2023-03-11 14:30:00'), +(9, 13, 4, '2023-02-13 10:30:00'), +(13, 6, 2, '2023-01-20 14:00:00'), +(13, 7, 2, '2023-01-21 10:00:00'), +(2, 1, 1, '2023-01-21 09:00:00'), +(6, 1, 1, '2023-01-07 10:00:00'), +(6, 14, 4, '2023-02-07 10:00:00'), +(7, 1, 1, '2023-01-14 10:00:00'), +(9, 12, 4, '2023-02-13 10:00:00'), +(9, 1, 1, '2023-02-16 11:00:00'), +(3, 1, 1, '2023-02-26 11:00:00'), +(6, 5, 1, '2023-01-09 10:00:00'), +(6, 12, 4, '2023-02-06 09:00:00'), +(13, 12, 4, '2023-02-21 09:00:00'), +(1, 2, 1, '2023-01-16 10:15:00'), +(1, 3, 1, '2023-01-17 09:00:00'), +(1, 4, 1, '2023-01-17 09:30:00'), +(1, 4, 1, '2023-01-18 10:00:00'), +(1, 6, 2, '2023-02-02 14:00:00'), +(1, 13, 4, '2023-02-15 10:30:00'), +(5, 16, 5, '2023-03-22 11:00:00'), +(12, 2, 1, '2023-01-17 10:20:00'), +(12, 3, 1, '2023-04-10 11:30:00'), +(14, 2, 1, '2023-01-09 08:15:00'), +(14, 4, 1, '2023-01-10 10:30:00'), +(14, 6, 2, '2023-01-10 14:00:00'), +(14, 9, 3, '2023-01-10 08:00:00'), +(1, 4, 1, '2023-01-17 11:00:00'), +(12, 9, 3, '2023-03-12 10:00:00'), +(14, 3, 1, '2023-01-10 10:00:00'), +(14, 7, 2, '2023-01-11 10:00:00'), +(14, 8, 2, '2023-01-12 11:00:00'), +(1, 12, 4, '2023-02-15 10:00:00'), +(12, 1, 1, '2023-01-17 10:00:00'), +(14, 5, 1, '2023-01-11 09:00:00'), +(1, 1, 1, '2023-01-16 10:00:00'), +(5, 15, 5, '2023-03-21 10:00:00'), +(12, 1, 1, '2023-04-10 11:00:00'), +(14, 1, 1, '2023-01-09 08:00:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql new file mode 100644 index 0000000..25b333d --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql @@ -0,0 +1,75 @@ +--DROP TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.inactive_users_summary +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + email String, + age UInt8, + registered_courses_count UInt32, + registration_date DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + +--DROP VIEW mnoskov.mv_inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_inactive_users_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + e.registered_courses_count AS registered_courses_count, + u.registration_date AS registration_date +FROM + mnoskov.core_dim_user u +LEFT JOIN ( + SELECT + user_id, + count() AS registered_courses_count + FROM + mnoskov.core_enrollments + GROUP BY + user_id +) e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT lv.user_id FROM mnoskov.core_fact_lesson_views lv); -- я ломалася тут между NOT IN и NOT EXISTS, но решил оставить первый тк это уже витрины мы делаем, и NULL поидее быть не должно + + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_dim_user; +SELECT * FROM mnoskov.inactive_users_summary; + +TRUNCATE TABLE mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +TRUNCATE TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC; + +INSERT INTO mnoskov.core_dim_user (user_id, name, age, email, registration_date) +VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-09 23:00:00'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-04 23:00:00'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-19 23:00:00'), +(4, 'Diana', 28, 'diana@mail.com', '2023-02-28 23:00:00'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-09 23:00:00'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-04 23:00:00'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-11 23:00:00'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-19 23:00:00'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-09 23:00:00'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-14 23:00:00'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-19 23:00:00'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-14 23:00:00'), +(13, 'Mia', 26, 'mia@mail.com', '2023-01-17 23:00:00'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-07 23:00:00'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-09 23:00:00'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-14 23:00:00'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-19 23:00:00'), +(18, 'Rita', 27, 'rita@mail.com', '2023-03-31 23:00:00'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-09 23:00:00'), +(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00'), +(22, 'Anal2', 24, 'tinanal222@mail.com', '2023-04-19 23:22:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql new file mode 100644 index 0000000..5d53f29 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql @@ -0,0 +1,117 @@ +--DROP TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.course_completion_rate +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + user_name String, + course_id UInt64, + course_title String, + lessons_in_course UInt16, + lesson_viewed UInt16, + completion_rate Float32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +--DROP VIEW mnoskov.mv_course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_course_completion_rate +ON CLUSTER clickhouse_cluster +TO mnoskov.course_completion_rate +AS +SELECT + u.user_id AS user_id, + u.name AS user_name, + cl.course_id AS course_id, + cl.course_title AS course_title, + cl.lessons_in_course AS lessons_in_course, + lv.lesson_viewed AS lesson_viewed, + ifnull(round(toFloat32(ifnull(lv.lesson_viewed, 0)) / nullif(cl.lessons_in_course, 0), 2), 0) AS completion_rate -- вроде все логично +FROM + mnoskov.core_enrollments e +RIGHT JOIN + mnoskov.core_dim_user u ON e.user_id = u.user_id +JOIN ( + SELECT + c.course_id, + c.title AS course_title, + uniq(lesson_id) lessons_in_course + FROM + mnoskov.core_dim_course c + JOIN + mnoskov.core_dim_lesson l ON l.course_id = c.course_id + GROUP BY + c.course_id, c.title +) cl ON e.course_id = cl.course_id +LEFT JOIN ( + SELECT + user_id, + course_id, + uniq(lesson_id) AS lesson_viewed + FROM + mnoskov.core_fact_lesson_views + GROUP BY + user_id, course_id +) lv ON e.user_id = lv.user_id AND e.course_id = lv.course_id +ORDER BY + u.user_id, e.course_id; + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_enrollments; +SELECT * FROM mnoskov.course_completion_rate; + +TRUNCATE TABLE mnoskov.core_enrollments ON CLUSTER clickhouse_cluster sync; +TRUNCATE TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_enrollments +(user_id, course_id, enrolled_at) +VALUES +(6, 1, '2023-01-05 23:00:00'), +(14, 2, '2023-01-08 23:00:00'), +(14, 3, '2023-01-08 23:00:00'), +(14, 1, '2023-01-08 23:00:00'), +(6, 2, '2023-01-09 23:00:00'), +(15, 1, '2023-01-10 23:00:00'), +(7, 1, '2023-01-12 23:00:00'), +(1, 1, '2023-01-14 23:00:00'), +(12, 1, '2023-01-15 23:00:00'), +(13, 2, '2023-01-18 23:00:00'), +(2, 1, '2023-01-19 23:00:00'), +(8, 3, '2023-01-21 23:00:00'), +(8, 1, '2023-01-21 23:00:00'), +(8, 2, '2023-01-21 23:00:00'), +(1, 2, '2023-01-31 23:00:00'), +(7, 2, '2023-01-31 23:00:00'), +(14, 4, '2023-01-31 23:00:00'), +(15, 2, '2023-01-31 23:00:00'), +(6, 4, '2023-02-04 23:00:00'), +(1, 4, '2023-02-09 23:00:00'), +(9, 4, '2023-02-11 23:00:00'), +(9, 1, '2023-02-14 23:00:00'), +(16, 2, '2023-02-15 23:00:00'), +(16, 4, '2023-02-15 23:00:00'), +(13, 4, '2023-02-19 23:00:00'), +(3, 1, '2023-02-24 23:00:00'), +(3, 2, '2023-03-04 23:00:00'), +(15, 3, '2023-03-04 23:00:00'), +(6, 3, '2023-03-07 23:00:00'), +(4, 2, '2023-03-09 23:00:00'), +(7, 3, '2023-03-09 23:00:00'), +(12, 3, '2023-03-09 23:00:00'), +(2, 3, '2023-03-14 23:00:00'), +(5, 5, '2023-03-19 23:00:00'), +(14, 5, '2023-03-19 23:00:00'), +(17, 3, '2023-03-21 23:00:00'), +(17, 5, '2023-03-24 23:00:00'), +(4, 3, '2023-03-31 23:00:00'), +(18, 1, '2023-04-01 23:00:00'), +(18, 4, '2023-04-04 23:00:00'), +(19, 1, '2023-04-10 23:00:00'), +(19, 2, '2023-04-10 23:00:00'), +(10, 1, '2023-04-15 23:00:00'), +(20, 2, '2023-04-18 23:00:00'), +(11, 2, '2023-04-20 23:00:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql new file mode 100644 index 0000000..f9b83f5 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql @@ -0,0 +1,36 @@ +-- Топ 5 самых непросматреваемых уроков +SELECT + any(lesson_title) AS lesson_title, + any(course_title) AS course_title, + countMerge(total_views) AS total_views +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + total_views +LIMIT 5; + + +-- Неактивные пользователи младше 26 +SELECT + name, + age, + email +FROM + mnoskov.inactive_users_summary +WHERE + age < 26; + + +-- 3 круса с самым низкми процентом завершения +SELECT + course_title, + avg(completion_rate) AS avg_completion_rate +FROM + mnoskov.course_completion_rate +GROUP BY + course_title +ORDER BY + avg_completion_rate +LIMIT 3; \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt new file mode 100644 index 0000000..fa5fbd2 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt @@ -0,0 +1,2 @@ +Разница есть только в таблицах mv_lesson_popularity_summary +Где в Greenplum - NULL, в clickhouse - 0. Потому что clickhouser сам подставляет 0 вместо NULL. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..6140c2f --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..2a7de23 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + user_id +); +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW mnoskov.top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE=MergeTree +ORDER BY(event_time,user_id); + +CREATE TABLE mnoskov.agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE=SummingMergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM mnoskov.raw_events +GROUP BY event_date; + +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..b8d8218 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql @@ -0,0 +1,115 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + balance, + created_at +FROM ( + SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +) +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) +ORDER BY + event_datetime; + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) as user_actions_cnt +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql new file mode 100644 index 0000000..c7c4d8c --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql @@ -0,0 +1,215 @@ +-- Задание 1 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active Bool +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO mnoskov.users +(user_id,name,age,country,is_active) +VALUES +(1,'Ivan',30,'RU',1), +(2,'Anna',25,'RU',1), +(3,'Petr',45,'RU',0), +(4,'Olga',35,'RU',1), +(5,'John',55,'US',1), +(6,'Alice',42,'US',0), +(7,'Tom',38,'US',1), +(8,'Lucas',28,'BR',1), +(9,'Maria',60,'BR',1), +(10,'Joao',27,'BR',0), +(11,'Kenji',33,'JP',1), +(12,'Yuki',29,'JP',0), +(13,'Chen',40,'CN',1), +(14,'Li',48,'CN',1), +(15,'Fatima',31,'AE',1), +(16,'Ahmed',36,'AE',1), +(17,'George',24,'US',1), +(18,'Nina',34,'RU',1), +(19,'Max',50,'DE',1), +(20,'Sophie',22,'DE',0); + +SELECT * FROM mnoskov.users; + +--1 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +ORDER BY + age DESC +LIMIT 10; + +--2 +SELECT + name, + country +FROM + mnoskov.users +WHERE + country = 'RU' +ORDER BY + name; + +--3 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +LIMIT 10; +-- Данные вывелить так как лежат на диске. + +--4 +SELECT + country, + count() as cnt_users +FROM + mnoskov.users +GROUP BY + country; + +--5 +SELECT + country, + min(age) AS min_age, + max(age) AS max_age +FROM + mnoskov.users +GROUP BY + country; + +--6 +SELECT + country, + count() AS cnt_users +FROM + mnoskov.users +GROUP BY + country +ORDER BY + cnt_users DESC; + +--Доп +SELECT + uniqExact(country), + uniq(country) +FROM + mnoskov.users; +-- uniqExact(country) - кол-во уникальных значений в колонке country. Но uniq работает также. +-- В инете написано что uniq быстрее. + + +-- Задание 2 +CREATE TABLE mnoskov.events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events1', + '{replica}' +) +ORDER BY(user_id,event_time); + +INSERT INTO mnoskov.events1 +(user_id,event_time,event_type) +VALUES +(1,'2025-07-17 09:00:00','click'), +(1,'2025-07-17 09:05:00','view'), +(1,'2025-07-17 09:10:00','purchase'), +(2,'2025-07-17 10:00:00','click'), +(2,'2025-07-17 10:05:00','click'), +(2,'2025-07-17 11:00:00','view'), +(3,'2025-07-16 12:00:00','view'), +(3,'2025-07-16 12:30:00','click'), +(3,'2025-07-17 08:00:00','purchase'), +(4,'2025-07-17 13:00:00','click'), +(4,'2025-07-17 14:00:00','view'), +(4,'2025-07-17 15:00:00','view'), +(5,'2025-07-16 20:00:00','view'), +(5,'2025-07-17 01:00:00','click'), +(5,'2025-07-17 06:00:00','purchase'), +(6,'2025-07-17 07:30:00','view'), +(6,'2025-07-17 08:45:00','click'), +(7,'2025-07-16 23:59:59','click'), +(7,'2025-07-17 00:01:00','purchase'), +(7,'2025-07-17 10:10:00','view'); + +SELECT * FROM mnoskov.events1; + +--1 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS com_users +FROM + mnoskov.events1; + +--2 +SELECT + user_id, + event_time, + event_type, + row_number() OVER (PARTITION BY user_id ORDER BY event_time) rn, + dense_rank() OVER (PARTITION BY user_id ORDER BY event_time) dr, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS c, + sum(1) OVER(PARTITION BY user_id ORDER BY event_time) AS s +FROM + mnoskov.events1; + +--3 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS com_users, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS com_users2 +FROM + mnoskov.events1; +-- все четенько работает, никаких ошибкок нет + +--4-5-6 +WITH recent_events AS ( + SELECT + user_id, + event_time, + event_type + FROM + mnoskov.events1 + WHERE + event_time < now() - INTERVAL 1 DAY +) +SELECT + user_id, + count(*) AS cnt_user_actions +FROM + recent_events +GROUP BY + user_id +ORDER BY + cnt_user_actions DESC +LIMIT + 5; +-- не понял, как сделать 6 пункт. + +-- Ответ на вопрос - ну да - поддерживает. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql new file mode 100644 index 0000000..f29dbc3 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql @@ -0,0 +1,57 @@ +-- Задание 1 + +SELECT + pro.product_id, + pro.name, + pri.price +FROM + mnoskov.products pro +ANY LEFT JOIN + mnoskov.prices pri ON pro.product_id = pri.product_id; +-- 2 Выберется случайная из цен. +-- 3 ALL LEFT JOIN - работает также как и LEFT JOIN. Его избегают потому что он тянет больше строк в оперативную память. + + +-- Задание 2 + +SELECT + o.time, + o.symbol, + o.size, + t.price +FROM + mnoskov.orders o +ASOF JOIN + mnoskov.ticks t ON o.symbol = t.symbol AND t.time <= o.time +WHERE symbol = 'AAPL'; +-- Потому что дата сделки и дата последней фиксации цены - разные. + + +-- Задание 3 + +SELECT + user_id, + name +FROM + mnoskov.users +WHERE user_id IN ( + SELECT user_id + FROM mnoskov.payments + WHERE amount > 1000 +); +-- 3 Потому что это фильтрация по существованию. Как оптимизируется не знаю, наверное как подзапрос. + + +-- Задание 4 + +SELECT + bl.user_id, + u.name AS user_name, + bl.event_time, + bl.event_type +FROM + mnoskov.big_logs bl +JOIN + mnoskov.users u ON bl.user_id = u.user_id; +-- 2 потому что таблица users меньше, а значит в оператику полетит меньше данных. +-- 3 если user_id уникален, то нужно выбирать ALL, так как дублей нет. В текущем задании дубли есть, но мы все равно выбираем ALL, тк нужно вывести имя для каждого лога а не юзера. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql new file mode 100644 index 0000000..6ed16cc --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql @@ -0,0 +1,44 @@ +-- Задание 1 + +--1-2 +SELECT + user_id, + tags, + indexOf(tags, 'promo') AS pos_promo +FROM + mnoskov.events +WHERE + has(tags, 'promo'); + +--3 +SELECT + user_id, + arrayJoin(tags) AS tag +FROM + mnoskov.events; + + +-- Задание 2 + +--1 +SELECT user_id, scores['math'] FROM mnoskov.metrics; + +--2 +SELECT user_id, mapValues(scores) FROM mnoskov.metrics; + + +-- Заданик 3 + +--1 +SELECT * FROM mnoskov.users_nested WHERE has(contacts.type, 'email'); + +--2 +SELECT + id, + name, + c.type, + c.value +FROM + mnoskov.users_nested +ARRAY JOIN + contacts AS c; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql new file mode 100644 index 0000000..2a855a6 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY sale_date; + +ALTER TABLE mnoskov.sales DROP PARTITION '202405'; +SELECT * FROM mnoskov.sales; +-- Пропали строки где год/месяц 202405. +-- Если не указывать PARTITION BY, то строки бы искались по всей таблице, что дольше и затранее. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events_partitioned', + '{replica}' +) +PARTITION BY event_type +ORDER BY event_date; + +ALTER TABLE mnoskov.events DROP PARTITION 'click'; +ELECT * FROM mnoskov.events; +-- Такой подход опасен, тк например login выполняется не часто по сравнению с click и получается жесткий перевес. + + +-- Задание 3 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_partitioned', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 +ORDER BY log_time; + +SELECT * FROM mnoskov.logs + +-- 2 с точки зрения баланса, у нас юзеры распределные равномерно и нет никаких перекосов. +-- 3 хз если честно, мне кажется должны ускорять. + + +-- Задание 4 + +CREATE TABLE mnoskov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY visit_date; + +SELECT * FROM mnoskov.visits; +-- 2 доступа к системным таблицам нет, потэтому я не могу посмотреть имя партиции. +-- 3 ну да +ALTER TABLE mnoskov.visits DROP PARTITION ('RU', '202407'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql new file mode 100644 index 0000000..7147e3e --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql @@ -0,0 +1,69 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_ttl', + '{replica}' +) +ORDER BY ts; +ALTER TABLE mnoskov.logs ADD INDEX idx_ts ts TYPE minmax granularity 2; + +SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'; + +-- 2 У меня нет доступа к системным таблицам, я не могу посмотреть. +-- Если в order by будет log_id а не ts, то индекс не будет работать. + + +-- Задание 2 + +CREATE TABLE mnoskov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clicks', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; +ALTER TABLE mnoskov.clicks DROP INDEX idx_country; + +explain +SELECT * FROM clicks WHERE country = 'RU'; +-- 3 не использутся индеск, я туда 11тыс строк заинсертил, все равно не используется. +-- 4 я думаю индекс не будет работать эффективно, а может быть и вообще будет запрос замедлять. + + +-- Задание 3 + +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users_simple', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +explain +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- 2 Ну я не вижу в EXPLAIN какое либо упоминание индекса, но я ссылаюсь на то, что просто мало данных в таблице. + +-- 3 Потому что этот индекс работает только на точные совпадения - exact match. + +explain +SELECT * FROM users WHERE email LIKE 'alice%'; +-- 4 Индекс не будет использоваться. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql new file mode 100644 index 0000000..f8758f4 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql @@ -0,0 +1,27 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY created_at; + +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; +-- если делать с codec_expression, то ошибка вылазит мол такой колонки нет +-- Выглядит вот так: +-- id +-- level +-- message CODEC(ZSTD(3)) +-- created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql new file mode 100644 index 0000000..bfb6cd2 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql @@ -0,0 +1,157 @@ +-- Задание 1 + +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--DROP TABLE mnoskov.clean_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.clean_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clean_events', + '{replica}' +) +ORDER BY ts; + +CREATE MATERIALIZED VIEW mnoskov.mv_clean +ON CLUSTER clickhouse_cluster +TO mnoskov.clean_events +AS +SELECT + user_id, + ts, + nullif(event_type, '') AS event_type -- это я сделать чтобы выводилось NULL а не '' - пустая строка. +FROM mnoskov.raw_events +WHERE user_id != 0 AND event_type IS NOT NULL +ORDER BY ts; + +-- Тесты: +SELECT * FROM raw_events; +SELECT * FROM mnoskov.mv_clean; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(0, '2024-07-17 10:00:00', 'click'), +(1, '2024-07-17 10:01:00', 'view'), +(2, '2024-07-17 10:02:00', NULL); + + +-- Задание 2 + +--raw_events +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--click_events +--DROP TABLE mnoskov.click_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.click_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/click_events', + '{replica}' +) +ORDER BY ts; + +--login_events +--DROP TABLE mnoskov.login_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.login_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/login_events1', + '{replica}' +) +ORDER BY ts; + +--purchase_events +--DROP TABLE mnoskov.purchase_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.purchase_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/purchase_events1', + '{replica}' +) +ORDER BY ts; + +--mv_click +--DROP VIEW mnoskov.mv_click ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_click +ON CLUSTER clickhouse_cluster +TO mnoskov.click_events +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'click' +ORDER BY ts; + +--mv_login +--DROP VIEW mnoskov.mv_login ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_login +ON CLUSTER clickhouse_cluster +TO mnoskov.login_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'login' +ORDER BY ts; + +--mv_purchase +--DROP VIEW mnoskov.mv_purchase ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_purchase +ON CLUSTER clickhouse_cluster +TO mnoskov.purchase_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'purchase' +ORDER BY ts; + + +-- Тесты: +SELECT * FROM mnoskov.click_events; +SELECT * FROM mnoskov.login_events1; +SELECT * FROM mnoskov.purchase_events1; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(1, '2024-07-17 10:00:00', 'click'), +(2, '2024-07-17 10:01:00', 'login'), +(3, '2024-07-17 10:02:00', 'purchase'); \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..6140c2f --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..2a7de23 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + user_id +); +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW mnoskov.top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE=MergeTree +ORDER BY(event_time,user_id); + +CREATE TABLE mnoskov.agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE=SummingMergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM mnoskov.raw_events +GROUP BY event_date; + +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..b8d8218 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql @@ -0,0 +1,115 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + balance, + created_at +FROM ( + SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +) +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) +ORDER BY + event_datetime; + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) as user_actions_cnt +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql b/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql new file mode 100644 index 0000000..f542678 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql @@ -0,0 +1,46 @@ +-- 1 +CREATE SCHEMA raw; +CREATE SCHEMA core; +CREATE SCHEMA dm; + +-- 2 +--DROP TABLE IF EXISTS raw.users; +CREATE TABLE raw.users ( + id bigint NOT NULL, + name varchar(100), + age SMALLINT, + email varchar(100), + registration_date timestamptz +); + +--DROP TABLE IF EXISTS raw.courses; +CREATE TABLE raw.courses ( + id bigint NOT NULL, + title varchar(150), + category varchar(50), + created_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lessons; +CREATE TABLE raw.lessons ( + id bigint NOT NULL, + course_id bigint, + title varchar(150), + duration_min SMALLINT +); + +--DROP TABLE IF EXISTS raw.enrollments; +CREATE TABLE raw.enrollments ( + id bigint NOT NULL, + user_id bigint, + course_id bigint, + enrolled_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lesson_views; +CREATE TABLE raw.lesson_views ( + id bigint NOT NULL, + user_id bigint, + lesson_id bigint, + viewed_at timestamptz +); \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv new file mode 100644 index 0000000..d348d4a --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv @@ -0,0 +1,6 @@ +"id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300 +3,BI с нуля,business,2023-03-01 00:00:00.000 +0300 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv new file mode 100644 index 0000000..e5c5610 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv @@ -0,0 +1,46 @@ +"id","user_id","course_id","enrolled_at" +1,1,1,2023-01-15 00:00:00.000 +0300 +2,1,2,2023-02-01 00:00:00.000 +0300 +3,2,1,2023-01-20 00:00:00.000 +0300 +4,3,2,2023-03-05 00:00:00.000 +0300 +5,4,3,2023-04-01 00:00:00.000 +0300 +6,6,1,2023-01-06 00:00:00.000 +0300 +7,6,2,2023-01-10 00:00:00.000 +0300 +8,6,4,2023-02-05 00:00:00.000 +0300 +9,7,1,2023-01-13 00:00:00.000 +0300 +10,7,3,2023-03-10 00:00:00.000 +0300 +11,8,1,2023-01-22 00:00:00.000 +0300 +12,8,2,2023-01-22 00:00:00.000 +0300 +13,8,3,2023-01-22 00:00:00.000 +0300 +14,9,4,2023-02-12 00:00:00.000 +0300 +15,10,1,2023-04-16 00:00:00.000 +0300 +16,11,2,2023-04-21 00:00:00.000 +0300 +17,12,1,2023-01-16 00:00:00.000 +0300 +18,13,2,2023-01-19 00:00:00.000 +0300 +19,13,4,2023-02-20 00:00:00.000 +0300 +20,14,1,2023-01-09 00:00:00.000 +0300 +21,14,2,2023-01-09 00:00:00.000 +0300 +22,14,3,2023-01-09 00:00:00.000 +0300 +23,14,4,2023-02-01 00:00:00.000 +0300 +24,14,5,2023-03-20 00:00:00.000 +0300 +25,15,1,2023-01-11 00:00:00.000 +0300 +26,15,3,2023-03-05 00:00:00.000 +0300 +27,16,2,2023-02-16 00:00:00.000 +0300 +28,16,4,2023-02-16 00:00:00.000 +0300 +29,17,3,2023-03-22 00:00:00.000 +0300 +30,17,5,2023-03-25 00:00:00.000 +0300 +31,18,1,2023-04-02 00:00:00.000 +0300 +32,19,1,2023-04-11 00:00:00.000 +0300 +33,19,2,2023-04-11 00:00:00.000 +0300 +34,20,2,2023-04-19 00:00:00.000 +0300 +35,5,5,2023-03-20 00:00:00.000 +0300 +36,1,4,2023-02-10 00:00:00.000 +0300 +37,2,3,2023-03-15 00:00:00.000 +0300 +38,3,1,2023-02-25 00:00:00.000 +0300 +39,4,2,2023-03-10 00:00:00.000 +0300 +40,6,3,2023-03-08 00:00:00.000 +0300 +41,7,2,2023-02-01 00:00:00.000 +0300 +42,9,1,2023-02-15 00:00:00.000 +0300 +43,12,3,2023-03-10 00:00:00.000 +0300 +44,15,2,2023-02-01 00:00:00.000 +0300 +45,18,4,2023-04-05 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv new file mode 100644 index 0000000..b09d250 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv @@ -0,0 +1,59 @@ +"id","user_id","lesson_id","viewed_at" +1,1,1,2023-01-16 10:00:00.000 +0300 +2,1,2,2023-01-16 10:15:00.000 +0300 +3,1,3,2023-01-17 09:00:00.000 +0300 +4,1,4,2023-01-17 09:30:00.000 +0300 +5,1,4,2023-01-17 11:00:00.000 +0300 +6,1,4,2023-01-18 10:00:00.000 +0300 +7,1,6,2023-02-02 14:00:00.000 +0300 +8,1,12,2023-02-15 10:00:00.000 +0300 +9,1,13,2023-02-15 10:30:00.000 +0300 +10,2,1,2023-01-21 09:00:00.000 +0300 +11,2,9,2023-03-16 10:00:00.000 +0300 +12,2,10,2023-03-16 10:30:00.000 +0300 +13,3,6,2023-03-06 12:00:00.000 +0300 +14,3,1,2023-02-26 11:00:00.000 +0300 +15,3,2,2023-02-26 11:20:00.000 +0300 +16,4,9,2023-04-02 10:00:00.000 +0300 +17,4,10,2023-04-03 10:00:00.000 +0300 +18,4,6,2023-03-12 14:00:00.000 +0300 +19,4,7,2023-03-13 09:00:00.000 +0300 +20,5,15,2023-03-21 10:00:00.000 +0300 +21,5,16,2023-03-22 11:00:00.000 +0300 +22,6,1,2023-01-07 10:00:00.000 +0300 +23,6,2,2023-01-07 10:15:00.000 +0300 +24,6,3,2023-01-08 09:00:00.000 +0300 +25,6,4,2023-01-08 09:30:00.000 +0300 +26,6,5,2023-01-09 10:00:00.000 +0300 +27,6,6,2023-01-11 14:00:00.000 +0300 +28,6,7,2023-01-12 10:00:00.000 +0300 +29,6,8,2023-01-13 11:00:00.000 +0300 +30,6,12,2023-02-06 09:00:00.000 +0300 +31,6,13,2023-02-06 09:30:00.000 +0300 +32,6,14,2023-02-07 10:00:00.000 +0300 +33,6,9,2023-03-09 11:00:00.000 +0300 +34,7,1,2023-01-14 10:00:00.000 +0300 +35,7,2,2023-01-14 10:20:00.000 +0300 +36,7,3,2023-01-15 09:00:00.000 +0300 +37,7,9,2023-03-11 14:00:00.000 +0300 +38,7,10,2023-03-11 14:30:00.000 +0300 +39,9,12,2023-02-13 10:00:00.000 +0300 +40,9,13,2023-02-13 10:30:00.000 +0300 +41,9,1,2023-02-16 11:00:00.000 +0300 +42,12,1,2023-01-17 10:00:00.000 +0300 +43,12,2,2023-01-17 10:20:00.000 +0300 +44,12,1,2023-04-10 11:00:00.000 +0300 +45,12,3,2023-04-10 11:30:00.000 +0300 +46,12,9,2023-03-12 10:00:00.000 +0300 +47,13,6,2023-01-20 14:00:00.000 +0300 +48,13,7,2023-01-21 10:00:00.000 +0300 +49,13,12,2023-02-21 09:00:00.000 +0300 +50,14,1,2023-01-09 08:00:00.000 +0300 +51,14,2,2023-01-09 08:15:00.000 +0300 +52,14,3,2023-01-10 10:00:00.000 +0300 +53,14,4,2023-01-10 10:30:00.000 +0300 +54,14,5,2023-01-11 09:00:00.000 +0300 +55,14,6,2023-01-10 14:00:00.000 +0300 +56,14,7,2023-01-11 10:00:00.000 +0300 +57,14,8,2023-01-12 11:00:00.000 +0300 +58,14,9,2023-01-10 08:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv new file mode 100644 index 0000000..54dbb22 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv @@ -0,0 +1,19 @@ +"id","course_id","title","duration_min" +1,1,SELECT и FROM,10 +2,1,JOIN,15 +3,1,WHERE и фильтрация,12 +4,1,Агрегации и GROUP BY,18 +5,1,Подзапросы,25 +6,2,Pandas основы,20 +7,2,NumPy,15 +8,2,Визуализация данных,22 +9,3,Основы BI,12 +10,3,Метрики и дашборды,18 +11,3,Power BI введение,20 +12,4,Формулы в Excel,15 +13,4,Сводные таблицы,18 +14,4,Графики и диаграммы,14 +15,5,Введение в ML,20 +16,5,Линейная регрессия,30 +17,5,Классификация,28 +18,5,Кластеризация,25 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv new file mode 100644 index 0000000..d6aa0da --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv @@ -0,0 +1,21 @@ +"id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300 +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql new file mode 100644 index 0000000..a804a5c --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql @@ -0,0 +1,189 @@ +-- 4 +--DROP TABLE IF EXISTS core.dim_user; +CREATE TABLE core.dim_user ( + user_id bigint PRIMARY KEY, + name varchar(100), + age smallint, + email varchar(100) NOT NULL, + registration_date timestamptz NOT NULL +) DISTRIBUTED BY (user_id); + +--DROP TABLE IF EXISTS core.dim_course; +CREATE TABLE core.dim_course ( + course_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL +) DISTRIBUTED BY (course_id); + +--DROP TABLE IF EXISTS core.dim_lesson; +CREATE TABLE core.dim_lesson ( + lesson_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + duration_min smallint, + course_id bigint NOT NULL REFERENCES core.dim_course(course_id) +) DISTRIBUTED BY (lesson_id); +CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson(course_id); + +--DROP TABLE IF EXISTS core.dim_date; +CREATE TABLE core.dim_date ( + date date PRIMARY KEY, + year int, + month int, + day int, + weekday text +) DISTRIBUTED BY (date); + + +-- 5 +--DROP TABLE IF EXISTS core.fact_lesson_views; +CREATE TABLE core.fact_lesson_views ( + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + lesson_id bigint NOT NULL REFERENCES core.dim_lesson(lesson_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + viewed_at timestamptz NOT NULL +) DISTRIBUTED BY (user_id); +CREATE INDEX idx_fact_lesson_views_viewed_at ON core.fact_lesson_views(viewed_at); +CREATE INDEX idx_fact_lesson_views_user_id ON core.fact_lesson_views(user_id); +CREATE INDEX idx_fact_lesson_views_lesson_id ON core.fact_lesson_views(lesson_id); +CREATE INDEX idx_fact_lesson_views_course_id ON core.fact_lesson_views(course_id); + +--DROP TABLE IF EXISTS core.enrollments; +CREATE TABLE core.enrollments ( + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + enrolled_at timestamptz NOT NULL +) DISTRIBUTED BY (user_id); +CREATE INDEX idx_enrollments_user_id ON core.enrollments(user_id); +CREATE INDEX idx_enrollments_course_id ON core.enrollments(course_id); + + +--6 +-- dim_user +INSERT INTO core.dim_user (user_id, name, age, email, registration_date) +SELECT + raw_u.id, + raw_u.name, + raw_u.age, + raw_u.email, + raw_u.registration_date +FROM + raw.users raw_u +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id) + AND raw_u.email IS NOT NULL AND raw_u.registration_date IS NOT NULL; + +UPDATE core.dim_user core_u +SET + name = raw_u.name, + age = raw_u.age, + email = raw_u.email, + registration_date = raw_u.registration_date +FROM + raw.users raw_u +WHERE + core_u.user_id = raw_u.id + AND ( + core_u.name != raw_u.name OR + core_u.age != raw_u.age OR + core_u.email != raw_u.email OR + core_u.registration_date != raw_u.registration_date + ); + +-- dim_course +INSERT INTO core.dim_course (course_id, title, category, created_at) +SELECT + raw_c.id, + raw_c.title, + raw_c.category, + raw_c.created_at +FROM + raw.courses raw_c +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id) + AND raw_c.title IS NOT NULL AND raw_c.created_at IS NOT NULL; + +UPDATE core.dim_course core_c +SET + title = raw_c.title, + category = raw_c.category, + created_at = raw_c.created_at +FROM + raw.courses raw_c +WHERE + core_c.course_id = raw_c.id + AND ( + core_c.title != raw_c.title OR + core_c.category != raw_c.category OR + core_c.created_at != raw_c.created_at + ); + +-- dim_lesson +INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id) +SELECT + raw_l.id, + raw_l.title, + raw_l.duration_min, + raw_l.course_id +FROM + raw.lessons raw_l +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id) + AND raw_l.title IS NOT NULL AND raw_l.course_id IS NOT NULL; + +UPDATE core.dim_lesson core_l +SET + title = raw_l.title, + duration_min = raw_l.duration_min, + course_id = raw_l.course_id -- решил оставить, тк чисто теоретические же урок может начать принадлежать другому курсу +FROM + raw.lessons raw_l +WHERE + core_l.lesson_id = raw_l.id + AND ( + core_l.title != raw_l.title OR + core_l.duration_min != raw_l.duration_min OR + core_l.course_id != raw_l.course_id + ); + +-- загуглил как заполнять dim_date: +INSERT INTO core.dim_date (date, year, month, day, weekday) +SELECT + d::date, + EXTRACT(YEAR FROM d)::int, + EXTRACT(MONTH FROM d)::int, + EXTRACT(DAY FROM d)::int, + to_char(d, 'day') +FROM + generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d; + +-- fact_lesson_views +INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +SELECT + raw_lv.user_id, + raw_lv.lesson_id, + raw_l.course_id, + raw_lv.viewed_at +FROM + raw.lesson_views raw_lv +JOIN + raw.lessons raw_l ON raw_lv.lesson_id = raw_l.id +WHERE + raw_lv.viewed_at > (SELECT max(viewed_at) FROM core.fact_lesson_views) -- но при первой загрузке это не сработало, тк max(vied_at) = NULL, как это исправить - я не знаю + AND raw_lv.user_id IS NOT NULL AND raw_lv.lesson_id IS NOT NULL + AND raw_l.course_id IS NOT NULL AND raw_lv.viewed_at IS NOT NULL; + +-- enrollments +INSERT INTO core.enrollments (user_id, course_id, enrolled_at) +SELECT + raw_e.user_id, + raw_e.course_id, + raw_e.enrolled_at +FROM + raw.enrollments raw_e +WHERE + NOT EXISTS (SELECT 1 FROM core.enrollments core_e WHERE + core_e.user_id = raw_e.user_id + AND core_e.course_id = raw_e.course_id + AND core_e.enrolled_at = raw_e.enrolled_at) + AND raw_e.user_id IS NOT NULL AND raw_e.course_id IS NOT NULL AND raw_e.enrolled_at IS NOT NULL; \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql new file mode 100644 index 0000000..9c4dbb2 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql @@ -0,0 +1,99 @@ +-- 7 +--DROP MATERIALIZED VIEW dm.lesson_popularity_summary; +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary AS +WITH results_fact_lesson_views AS ( + SELECT + flv.lesson_id, + count(flv.user_id) AS total_views, + count(DISTINCT flv.user_id) AS unique_users, + min(flv.viewed_at)::date AS first_view, + max(flv.viewed_at)::date AS last_view + FROM + core.fact_lesson_views flv + GROUP BY + flv.lesson_id +) +SELECT + dl.lesson_id, + dl.title AS lesson_title, + dl.course_id, + dc.title AS course_title, + rflv.total_views, + rflv.unique_users, + rflv.first_view, + rflv.last_view +FROM + core.dim_lesson dl +JOIN + core.dim_course dc ON dl.course_id = dc.course_id +LEFT JOIN + results_fact_lesson_views rflv ON dl.lesson_id = rflv.lesson_id; + + +-- 8 +--DROP MATERIALIZED VIEW dm.inactive_users_summary; +CREATE MATERIALIZED VIEW dm.inactive_users_summary AS +SELECT + du.user_id, + du.name AS user_name, + du.email, + du.age, + e.enrollments_count, + du.registration_date +FROM + core.dim_user du +LEFT JOIN ( + SELECT + user_id, + count(course_id) AS enrollments_count + FROM + core.enrollments + GROUP BY + user_id +) e ON du.user_id = e.user_id +WHERE + NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id); +-- интересно, что во время проверки плана запроса, когда кусок с enrollments_count был в CTE, план запроса выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу индексы заработали, и строки начали эффективней читаться + + +-- 9 +--DROP MATERIALIZED VIEW dm.course_completion_rate; +CREATE MATERIALIZED VIEW dm.course_completion_rate AS +WITH results_dim_course_lesson AS ( + SELECT + dc.course_id, + dc.title, + count(lesson_id) AS lessons_in_course + FROM + core.dim_course dc + JOIN + core.dim_lesson dl ON dc.course_id = dl.course_id + GROUP BY + dc.course_id, dc.title +), +results_fact_lesson_views AS ( + SELECT + flv.user_id, + flv.course_id, + count(DISTINCT lesson_id) AS lessons_viewed + FROM + core.fact_lesson_views flv + GROUP BY + flv.user_id, flv.course_id +) +SELECT + du.user_id, + du.name AS user_name, + rdcl.course_id, + rdcl.title AS course_title, + rdcl.lessons_in_course, + COALESCE(rflv.lessons_viewed, 0) AS lessons_viewed, + round(COALESCE(rflv.lessons_viewed, 0)::numeric / NULLIF(rdcl.lessons_in_course, 0), 2) AS completion_rate +FROM + core.dim_user du +JOIN + core.enrollments e ON du.user_id = e.user_id +JOIN + results_dim_course_lesson rdcl ON e.course_id = rdcl.course_id +LEFT JOIN + results_fact_lesson_views rflv ON e.user_id = rflv.user_id AND e.course_id = rflv.course_id; \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql b/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql new file mode 100644 index 0000000..f2d8cb9 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql @@ -0,0 +1,4 @@ +SELECT * FROM dm.lesson_popularity_summary ORDER BY lesson_id LIMIT 10; +SELECT * FROM dm.inactive_users_summary ORDER BY user_id LIMIT 10; +SELECT * FROM dm.course_completion_rate ORDER BY user_id, course_id; +--пришлось добавить order_by для красивого вывода, потому что создать витрины отсортированными не получается \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt b/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt new file mode 100644 index 0000000..b9f5fc6 --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt @@ -0,0 +1,9 @@ +Задание 1 + - запос не будет локальным + - гринплам будет делать data motion + +Задание 2 + - distributed by не подходит, тк таблица не будет учавствовать в join + +Задание 3 + - можно добавить event_id и распределять по нему, или же сделать destributed randomly \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_explain.txt b/mnoskov/greenplum/Практика greenplum/practice_explain.txt new file mode 100644 index 0000000..9393ecd --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_explain.txt @@ -0,0 +1,6 @@ +Задание 1 + - если orders DISTRIBUTED BY user_id, то будет 'Seq Scan on orders' -> 'HashAggregate'. Redistribute Motion(по user_id) пропадет, потому что все уже распределено по user_id; + +Задание 2 + - Это называется Skew. + - Чтобы избежать skew, нужно выбрать столбец для распределения с нормальной кардинальностью. Или делать destributed randomly если не нужно join. \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt b/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt new file mode 100644 index 0000000..5270ff7 --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt @@ -0,0 +1,10 @@ +Задание 1 + - данные физически находятся в Hive; + - писать обратно в Hive нельзя, тк таблица не writable; + - не знаю, в теорри это не сказано - думаю будет ошибка. После того как загуглил: + 1) если в hive добавились новые столбцы, то при чтении все будет отображаться так, как было во время создание внешней таблицы в greenplum, без ошибок; + 2) если в hive удалась часть столбцов, то возникнет ошибка тк, в greenplum по сути будут лишние столбцы. + +Задание 2 + - данные не копируются в greenplum, а только читаются из s3; + - я думаю в момент создания таблицы - быстрее external, тк ничего не копируется фищически, но при чтении должен быть быстрее CREATE TABLE AS SELECT, тк данные физически хранятся на нашем сервере, а не где-то в облаке. \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_requests.txt b/mnoskov/greenplum/Практика greenplum/practice_requests.txt new file mode 100644 index 0000000..4c4fbde --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_requests.txt @@ -0,0 +1,17 @@ +Задание 1: +- Будут собираться все 10млн строк с разных сегментов, а тольок потом сработает LIMIT. Это очень дорого стоит, и медленно тк очень много данных в пустую соединятся в мастер. +- Я думаю можно к текущему запросу добавить DENSE_RANK по total и написать условие where -тут имя dense_rank- <= 10. +!!! В теории не сказано про работу order by - пришлось загуглить !!! + +Задание 2: +- PARTITION BY по. region_id, а должен быть по order_id, чтобы работало без задержек, а при текущем варианте запроса будет происходить data motion как я понял, а это лишние вычисления. +- Тогда надо распределять таблицу по region_id полагаю. + +Задание 3: +- Ответ до подсказки: думаю все будет нормально, данные просто сгруппируются и вставятся, никаких зарежек быть не должно. +- Ответ после подсказки: greenplum будет пересылать(data motion) user_id между сегментами, чтобы собрать их в группы, а только потом собирать в мастер и инсертить, инсерт уже поидее будет происходить хорошо, тк данные после data motion будут лежать нормально. +- Orders надо дестрибьютить по user_id, тогда все четко и быстро будет - и group by, и join. + +Задание 4: +- Я хочу увидеть Motion, Broadcast, Gather +- Что означает Broadcast Motion, Redistribute Motion, Gather Motion? - в душе ни чаю, там вопрос с ответом сразу, взял оттуда: Broadcast Motion - разослать копии каждой строки на все сегменты; Redistribute Motion - перераспределить по новому ключу; Gather Motion - собрать на мастер. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql new file mode 100644 index 0000000..676ee2f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -0,0 +1,47 @@ +-- ПРАКТИКА DDL +-- Задание 1 +DROP TABLE IF EXISTS mnoskov.users; +CREATE TABLE mnoskov.users ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + country char(2), -- прогуглил что распространено сохранять iso-код а не все название + is_active boolean, + created_at timestamptz NOT NULL -- думаю timestamptz, если не учитывать часовой пояс, то даты могут отображаться неверно +); + + +-- Задание 2 +DROP TABLE IF EXISTS mnoskov.products; +CREATE TABLE mnoskov.products ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + category varchar(50), + price decimal(10,2) NOT NULL, + in_stock boolean +); + + +-- Задание 3 +DROP TABLE IF EXISTS mnoskov.orders; +CREATE TABLE mnoskov.orders ( + id bigint PRIMARY KEY, + user_id bigint, + order_date timestamptz NOT NULL, -- в названии date, и тип думаю date + status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar + FOREIGN KEY (user_id) REFERENCES users(id) +); + + +-- Задание 4 +DROP TABLE IF EXISTS mnoskov.order_items; +CREATE TABLE mnoskov.order_items ( + order_id bigint, + product_id bigint, + quantity int, -- думал надо smallint но потом подумал что вдруг 50к гвоздей + price_at_purchase decimal(10,2), + PRIMARY KEY (order_id, product_id), + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql new file mode 100644 index 0000000..7c3e15f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql @@ -0,0 +1,50 @@ +-- Задание 1 +INSERT INTO mnoskov.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'); + +-- Задание 2 +INSERT INTO mnoskov.products (id, name, category, price, in_stock) +VALUES +(1, 'iPhone 15', 'Электроника', 80000, true), +(2, 'Kindle', 'Электроника', 15000, true), +(3, 'Кроссовки Nike', 'Одежда', 9000, true), +(4, 'Кроссовки NoName', 'Одежда', 3000, false), +(5, 'Наушники Pro', 'Электроника', 12000, true), +(6, 'Фитнес-браслет', 'Гаджеты', 6000, true), +(7, 'Подарочная карта', 'Сервисы', 1000, true); + +-- Задание 3 +INSERT INTO mnoskov.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'); + +-- Задание 4 +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) +VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql new file mode 100644 index 0000000..641bfa7 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql @@ -0,0 +1,59 @@ +-- Задание 1 +SELECT + id, + name, + country, + created_at, + email +FROM mnoskov.users +ORDER BY created_at DESC, name; +-- выбрал эти столбцы потому что так сказано в задании, я бы добавил ещё is_active +-- такая сортировка для того, чтобы даты шли от новых к старым и имена людей за одну дату шли по алфавиту + + +-- Задание 2 +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'US' +ORDER BY name; + +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'UK' +ORDER BY name; +-- для маркетолого полезнее первый запрос т.к. там больше данных + + +-- Задание 3 +SELECT + id, + name, + email +FROM mnoskov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT DISTINCT + lower(email) AS email_lower +FROM mnoskov.users +WHERE email IS NOT NULL; +-- приводить email к одному регистру мб для красоты, вроде везде email принимается и в нижем и в верхнем регистре +-- дубликаты убираем чтобы вытрина занимала меньше места, и чтобы не было повторных рассылок + + +-- Задание 4 +SELECT + id, + name, + created_at +FROM mnoskov.users +WHERE created_at > '2024-02-20 23:59' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; +-- OFFSET может пригодиться, чтобы взять интервал данных \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql new file mode 100644 index 0000000..df898a7 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql @@ -0,0 +1,78 @@ +-- Задание 1 +SELECT + name, + category, + price, + price * 1.30 AS price_with_markup, + price * 1.50 AS price_with_vat +FROM + mnoskov.products +WHERE + in_stock = TRUE AND + category != 'smartwatch' AND -- категории smartwatch нет в предоставленных данных для заполнения + price BETWEEN 100 AND 1300; +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Лучше читается первый вариант, потому что просто прочитал и понял, а не думаешь над знаками, и первый вариант короче + + +-- Задание 2 +SELECT + name, + price, + price * 0.85 AS price_with_discount +FROM + mnoskov.products +WHERE + (price < 100 OR price > 1000); +-- Нужно отделять OR скобками, потому что если в условии будет ещё и AND, то логика может поломаться потому что AND выполняется раньше чем OR + + +-- Задание 3 +SELECT + id, + name, + category, + price +FROM + mnoskov.products +WHERE + category IN ('tablet', 'smartphone', 'mouse') AND + price >= 100 +ORDER BY + price DESC; + + +--Задание 4 +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name NOT LIKE '%iPhone%'; + + +-- Задание 5 +-- 1) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%pro%'; + +-- 2) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%mx%' +ORDER BY + price DESC; +-- Для поисковой строи лучше ILIKE, потому что он не учитывает регистр - кто-то пишет с большой буквы, кто-то с маленькой, а кто-то случайно включил капс. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql b/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql new file mode 100644 index 0000000..639da44 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql @@ -0,0 +1,188 @@ +-- Задание 1 +SELECT + o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.users u ON o.user_id = u.id; +-- Логичнее начать с orders, тк надо вывести пользователей по заказам, а не наоборот. +-- Здесть уместнее LEFT, потому что в условии сказано ВСЕ заказы и уже по ним пользователей. Попробовал и INNER и LEFT - вывод один и тот же. + + +-- Задание 2 +SELECT + oi.order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +WHERE + oi.order_id = 1; -- если 101, как в условии то ничего не будет +-- Если фильтрацию поставить после ON, то выведутся все продукты по 2 раза, у всех id=1, quantity и price возьмутсы из двух строк в order_items где id=1. + + +-- Задание 3 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Заказ с id=3 повторяется два раза, потому что в заказе два товара, и они отображаются на двух строках. +-- Поскольку мы выводис все заказы, у нас больше строк по сравнению с заданием-2, и ещё мы выводим имена заказчиков. + + +--Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.status +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id; +-- Этот запрос отличается от INNER тем, что тут отображаются ещё и пользователи без заказов. + + +-- Задание 5 +SELECT + u.id AS user_id, + u.name AS user_name +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.user_id IS NULL; +-- Важно фильтровать по колонке из orders, потому что она будет NULL из-за LEFT JOIN. Если есть несостыковки, то NULL будет в правой таблице после LEFT JOIN. + + +-- Задание 6 +SELECT + oi.order_id, + u.id AS user_id, + p.name AS product_name, + oi.quantity +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + p.name LIKE 'Kindle'; +-- Заказ 2 встречается 1 раз, потому что там всего один товар, и потому что мы ищем именно товар с названием - Kindle. + + +-- Задание 7 +SELECT + p.name AS product_name, + oi.order_id, + oi.quantity +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id; +-- Если товар не покупали то в order_id будет NULL +-- Если использовать INNER, то выведутся только товары у которых есть заказы, в случае с LEFT выведутся все заказы. + + +-- Задание 8 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + o.status = 'paid'; +-- Если написать status = 'paid' после ON в orders, то ничего не поменяется, потому что после ON пишутся условия соединения таблиц также как и в WHERE, но как я понял лучше писать в WHERE. + + +-- Задание 9 +SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice'; +-- Если у Alice не будет заказов то ничего не выведется. +-- Мы получается берем Alice -> идем до id товаров Alice -> идем обратно к заказам и выводим товары которые были у Alice. + +-- Мне подсказала нейронка как использовать SELF JOIN - я понимал что он нужен, но не понимал как именно им воспользоваться. +-- Только после такого запроса я разобрался как это работает: +/*SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase, + u2.name, + u2.id +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.users u2 ON o2.user_id = u2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice';*/ + + +-- Задание 10 +SELECT + o.id AS order_id, + o.user_id, + o.status +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.order_items oi ON o.id = oi.order_id; +-- Если написать INNER JOIN, то появятся только существующие заказы, которые есть в обеих таблицах. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql b/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql new file mode 100644 index 0000000..b284f5e --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql @@ -0,0 +1,75 @@ +-- Задание 1 +SELECT + id, + name, + 'active' AS user_status +FROM + mnoskov.users +WHERE + is_active = TRUE + +UNION ALL + +SELECT + id, + name, + 'inactive' AS user_status +FROM + mnoskov.users +WHERE + is_active = FALSE +ORDER BY name; +-- Если написать просто UNION, то в данном задании ничего не изменится + + +-- Задание 2 +SELECT + id AS order_id, + user_id, + order_date, + 'old' AS period +FROM + mnoskov.orders +WHERE + order_date < '2025-02-01' + +UNION ALL + +SELECT + id AS order_id, + user_id, + order_date, + 'new' AS period +FROM + mnoskov.orders +WHERE + order_date >= '2025-02-01' +ORDER BY + order_date, order_id; +-- По колонке period я пойму старый(old) заказ или новый(new) +-- Я не могу ответить, потому что CASE WHEN ещё не проходили + + +-- Задание 3 +SELECT DISTINCT + oi.product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id + +UNION ALL + +SELECT DISTINCT + p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id +WHERE oi.product_id is NULL; +-- Проверка IS NULL по полю из order_items, потому что это права таблица, и null будут добавляться в неё +-- 1) Понизить цену товара 2) лучше прорекламировать 3) снять с продажи \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql b/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql new file mode 100644 index 0000000..1058267 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql @@ -0,0 +1,130 @@ +-- Задание 1 +SELECT + category, + count(*) products_count +FROM + mnoskov.products +GROUP BY + category +ORDER BY + products_count; +-- Если убрать order by, то статистика будет в случайном разрбосанном виде, а не по возрастанию +-- Увеличится products_count на одно значение + + +-- Задание 2 +SELECT + order_id, + sum(quantity) AS total_quantity, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +ORDER BY + order_revenue DESC; +-- Если убрать GROUP BY, то будет ошибка +-- Нельзя просто добавить в SELECT ещё одну колонку (например, product_id) без добавления её в GROUP BY или агрегат, потому что вылезет ошибка. Ну и колонка должна быть либо сгруппирована либо в агрегате, она не может существовать сама по себе в запросе с grouup by + + +-- Задание 3 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +ORDER BY + category_revenue DESC; +-- Группировка по p.category - потому что мы хотим узнать сумму прибли по каждой категории +-- Если добавить p.name - будет ошибка потому что p.name тоже нужно будет заносить в GROUP BY + + +-- Задание 4 +SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + oi.order_id +ORDER BY + order_revenue DESC; +-- В задаче-2 были все заказы, а тут только оплаченные +-- Я бы смотрел толлько на paid, чтобы понять прибыль которая уже есть + + +-- Задание 5 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +HAVING + sum(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY + category_revenue DESC; +-- Если добавить это условие в WHERE, то терминал будет ругаться мол агрегаты недопустимы в WHERE +-- Обычно условие(прим: WHERE price > 1000) отличается от агрегированного условия(прим: HAVING SUM(...) > 1000) тем, что в первом случае ничего считать не нужно, значение уже есть, а во втором нужно сначала чтобы функция все посчитала. Ну и HAVING запускается уже после группировки + + +-- Задание 6 +SELECT + country, + count(*) AS active_user_count +FROM + mnoskov.users +WHERE + is_active = true +GROUP BY + country +HAVING + count(*) >= 2 +ORDER BY + active_user_count DESC; +-- is_active = true логичнее писать в WHERE, потому что is_active - не агрегат +-- Если нет страны где больше двух активных юзеров, то ничего не выведется + + +-- Задание 7 +SELECT + p.name AS product_name, + sum(oi.quantity) AS total_quantity_sold +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.name +ORDER BY + total_quantity_sold DESC +LIMIT 3; +-- total_quantity_sold - количество проданного товара, а product_revenue - выручка каждого товара +-- Бизнес сморит на количество, может быть когда товара очень много и нужно понять какой % уже продан, и когда нужно найти популярные или непопулярные товары + + +-- Задание 8 +SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +HAVING + sum(quantity * price_at_purchase) < 10000 +ORDER BY + order_revenue; +-- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? Ответ: «доставка пробников/тестовой партии», «использование купонов/бонусов», «технический тест витрины» +-- Проще увидеть глазами отфильтрованные микро-заказы \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql new file mode 100644 index 0000000..30d30fb --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql @@ -0,0 +1,90 @@ +-- Задание 1 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration +FROM + mnoskov.users +WHERE + is_active = TRUE + AND current_date - created_at::date > 180 +ORDER BY + days_since_registration desc; +-- Очень старым пользователям можно кинуть рассылку на почту на подобии 'Представляете, мы вместе уже 777 дней, вот вам купон на скидку' +-- Удобнее вычитать ::date, чтобы days_since_registration красиво выглядел в таблице + + +-- Задание 2 +SELECT + name, + lower(email) AS email_clean, + substring(email FROM POSITION('@' IN email) + 1) AS domain +FROM + mnoskov.users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- email приводят к нижнему регистру для нормализации записи, а зачем выделяют домен не понимаю, может быть для аналитики по типу кто какой почтой пользуется +-- Получу NULL в domain + + +-- Задание 3 +SELECT + name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + END AS age_group +FROM + mnoskov.users +ORDER BY + age; +-- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? В каком коде? Если в 'коде приложения' - это какая-то отдельная программа, то могут предположить, чтобы передавать в приложение уже отфильтрованные данные, чтобы задачи базы данных не выносить из базы. +-- Если забыть в ветку ELSE, то будет NULL у тех кто никуда не попал + + +-- Задание 4 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration, + CASE + WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE NULL + END AS hot_flag +FROM + mnoskov.users +WHERE + created_at >= current_date - INTERVAL '30 days'; +-- Фильтрация по дате в WHERE исключает неподходящие варианты, а простой CASE в нашем случае возвращает результат в виде двух вариантов - подошло/не подошло и эти оба варианта остаются в таблице +-- Стоит запускать какие нибудь привлекательные варианты со скидкам и акциями, чтобы удержать пользователся + + +-- Задание 5 +SELECT + count(*) AS total_users, + count(email) AS with_email, + count(*) - count(email) AS without_email, + ((count(*) - count(email))::float / count(*)) * 100.0 AS percent_without_email +FROM + mnoskov.users; +-- Агрегаты дают более точную информацию чем просто на глаз +-- Я бы предолжил запустить активно вылезающие блоки с предложением зарегестрироваться, или же запретить пользоваение продуктом без регистрации + + +-- Задание 6 +SELECT + name, + current_date - created_at::date AS days_since_registraion, + CASE + WHEN current_date - created_at::date <= 30 THEN 'new' + WHEN current_date - created_at::date BETWEEN 31 AND 180 THEN 'middle' + WHEN current_date - created_at::date > 180 THEN 'old' + END AS tenure_group, + is_active +FROM + mnoskov.users +ORDER BY + tenure_group, days_since_registraion; +-- Сегментация по стажу говорит о опыте человека, а по возрасту только о возрасте +-- Пересчитывать каждый день - плюсы: не занимает места на диске, минусы: тратит больше вычислительных ресурсов, а если хранить статическое поле, то все наоборот \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql b/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql new file mode 100644 index 0000000..cb6ad44 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql @@ -0,0 +1,155 @@ +-- Задание 1 +SELECT + name +FROM + mnoskov.users +WHERE id IN ( + SELECT user_id + FROM mnoskov.orders +) +ORDER BY + name; +-- Тут не приходится делать JOIN и DISTINCT, а просто ищем айдишник в списке +-- N (подзапрос) может быть более читаемым, когда подзапрос очень простой и короткий + + +-- Задание 2 +SELECT + u.id, + u.name, + u.email +FROM + mnoskov.users u +WHERE NOT EXISTS ( + SELECT user_id + FROM mnoskov.orders o + WHERE + u.id = o.user_id +) +-- NOT EXISTS безопаснее в плане NULL, и часто планируется лучше чем NOT IN +-- Определить какое количество людей не делают заказы, и кинуть им например купон на скидку на первый заказ + + +-- Задание 3 +SELECT + u.name, + ( + SELECT + count(*) + FROM + mnoskov.orders o + WHERE + u.id = o.user_id + ) AS orders_count +FROM + mnoskov.users u +ORDER BY + orders_count DESC, u.name; +-- Подзапрос в SELECT не может возвращать несколько строк, потому что SELECT ожидает толбко одну +-- Как изменится сложность запроса, если пользователей станет, например, 1 млн? Не совсем понял этот вопрос, но думаю запрос просто будет выполняться дольше + + +-- Задание 4 +WITH revenue_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name, + ru.total_revenue +FROM + mnoskov.users u +JOIN + revenue_user ru ON u.id = ru.user_id +ORDER BY + ru.total_revenue desc; +-- Проще читать когда шаг «посчитать выручку по пользователям» вынесен отдельно +-- Чтобы избежать дублирования импользуется CTE, а не простой подзапрос + + +-- Задание 5 +WITH revenue_product AS ( + SELECT + product_id, + sum(quantity * price_at_purchase) AS product_revenue + FROM + mnoskov.order_items + GROUP BY + product_id +) +SELECT + p.name, + p.category, + rp.product_revenue +FROM + mnoskov.products p +JOIN + revenue_product rp ON p.id = rp.product_id +ORDER BY + rp.product_revenue +LIMIT 3; +-- Сначала удобнее считать агрегаты, чтобы код не выгляжел грооздким и чтобы в нем было легче разобраться +-- Отличае в удобстве чтения и написания. И этот агрегат можно потом ещё и переиспользовать + + +-- Задание 6 +WITH revenue_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id +) +SELECT + u.name, + u.email, + ru.total_revenue +FROM + mnoskov.users u +JOIN + revenue_user ru ON u.id = ru.user_id +WHERE + u.is_active = TRUE AND + ru.total_revenue > 10000 +ORDER BY + ru.total_revenue DESC, u.name; +-- Я бы назвал этот сегмегт - frequent_buyers +-- Таким пользователям надо предлагать какую ниюудь специальную программу лояльности + + +-- Задание 7 +WITH revenue_order AS ( + SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue + FROM + mnoskov.order_items + GROUP BY + order_id +) +SELECT + ro.order_id, + o.order_date::date, + ro.order_revenue +FROM + mnoskov.orders o +JOIN + revenue_order ro ON o.id = ro.order_id +WHERE + o.status = 'paid' AND + ro.order_revenue < 10000 +ORDER BY + ro.order_revenue; +-- Причины: злоупотребление промокодами, ошибки +-- По 'paid' удобнее фильтровать снаржуи, потому тут мы подключаем orders, а подключать orders ещё и внутри было бы бессмысленно \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql new file mode 100644 index 0000000..4d582be --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql @@ -0,0 +1,168 @@ +-- Задание 1 +SELECT + o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + row_number() OVER (PARTITION BY o.user_id ORDER BY o.order_date) AS order_number_for_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Отличае от обычного order by в том, что мы не просто сортируем, а сортируем в пределах одного пользователя, и ещё выводим нумерацию заказов + + +-- Задание 2 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + otr.user_id, + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + sum(otr.order_revenue) OVER (PARTITION BY otr.user_id ORDER BY otr.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_revenue_for_user +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- Если не задвать рамку, то могут пояиться проблемы с дубликатами +-- Если накопительная выручка пользователя становится больше, значит он начинает тратить больше + + +-- Задание 3 +WITH user_total_revenue AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name AS user_name, + utr.total_revenue, + rank() OVER (ORDER BY utr.total_revenue) AS revenue_rank, + dense_rank() OVER (ORDER BY utr.total_revenue) AS revenue_dense_rank +FROM + mnoskov.users u +JOIN + user_total_revenue utr ON u.id = utr.user_id; +-- Когда появляется одинаковая выручка, dense_rank нумерует по порядку, без пропусков(1,2,2,3), а rank по 'расположению'(1,2,2,4) + + +-- Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + first_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date) AS first_order_date, + last_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order_date +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Первый заказ можно использовать для отчета 'как давно клиент начал активничать', а последний заказ, например если давно был последний заказ, то кинуть рассылку с промокодом, чтобы клиент снова начала заказывать как вариант +-- Если не задать рамку, то дата последнего заказа будет равна текущей строке, а не последней + + +-- Задание 5 +SELECT + u.name AS user_name, + o.id AS order_id, + count(*) OVER (PARTITION BY u.id) AS orders_per_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Если использовать просто group by, то пользователи сожмутся в одну ячейку + + +-- Задание 6 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + lag(otr.order_revenue) OVER (PARTITION BY u.id ORDER BY otr.order_date) AS prev_order_revenue +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- если текущее значение среднего чека больше чем предидущие, то увеличивается, а если наоборот, то уменьшается + + +-- Задание 7 +-- По месяцам: +WITH category_total_revenue AS ( + SELECT + p.category, + date_trunc('month', o.order_date)::date AS order_date_month, + sum(oi.quantity * oi.price_at_purchase) AS monthly_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date_month +) +SELECT + category, + order_date_month, + monthly_category_revenue, + sum(monthly_category_revenue) OVER (PARTITION BY category ORDER BY order_date_month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; + +-- По дням: +WITH category_total_revenue AS ( + SELECT + p.category, + o.order_date::date AS order_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date +) +SELECT + category, + order_date, + daily_category_revenue, + sum(daily_category_revenue) OVER (PARTITION BY category ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; +-- По такому графику мы можем видеть РОСТ выручки по дням например, а не просто сколько в какой день принесла категория \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql b/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql new file mode 100644 index 0000000..2e53170 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql @@ -0,0 +1,66 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- удобнее иметь wiev, чтобы небыло дублирования кода, и чтобы легко обращаться к запросу не переписывая все заново + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- Чем больше у пользователя заказов тем он более 'лояльный' +-- Тогда будет отображаться статистика только за последний месяц - 30 дней + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Можно построить график, и видеть пики с просадками +-- Быстрее считать выручку из mv_daily_revenue, тк витрина уже готова, и её не нужно высчитывать + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- 1. интернет‑магазин с несколькими заказами в день - каждые сутки в 8 утра например +-- 2. крупный магазин с тысячами заказов в час - каждые 30 минут +-- Слишком редкий REFRESH сделает витрину бесполезной для аналитиков +-- Обновлять каждую секунжу нельзя, тк это тратит ресурсы компьютера, и тогда уж проще просто VIEW сделать, и вызывать когда надо \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql new file mode 100644 index 0000000..c4a8939 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql @@ -0,0 +1,26 @@ +-- Задание 1. Оформление закзаза +INSERT INTO mnoskov.orders (id, user_id, order_date, status) VALUES + (9, 10, '2026-03-13', 'paid'), + (10, 10, '2026-03-14', 'paid'); + +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES + (9, 3, 2, 88000), + (10, 6, 1, 5900); + +UPDATE mnoskov.products +SET in_stock = FALSE +WHERE id IN(3, 6); +-- Если выполнятся не все шаги, то транзакция откатится и все останется как было +-- Потому что заказ если оформляется, то полностью, частично он выполняться не должен + + +-- Задание 2 +-- При таком сценарии все должно откатаиться к самому началу, ничего не сохранится +-- Если не делать rollback, то можно получить заказы без продуктов, или без стомости, или полностью оформленный заказ, но продуктов нет в наличии +-- Любые 'битые' заказы повлияют на отчеты и витрины + + +-- Задание 3 +-- При изменении цены товара в products, это действие могут начать выполнять два человека одновременное, и если это не будет транзакцией то получится непонятно что, а если как транзакция, то сначала запишется 1 результат, а потом 2, и не будет никаких ошибок +-- Если выполняетя как транзакция, то просто откатится назад и все будет супер +-- Тк база сама откатывает транзакции в случае надобности, это освобождает инженера от доли ответственности \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql new file mode 100644 index 0000000..1cf1cbd --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql @@ -0,0 +1,21 @@ +-- Задание 1 +SELECT * FROM mnoskov.users WHERE id = 1; +-- Если две одновременные сессии выполнят этот запрос, то в обеих все выведется, и ошибок или очереди не бкдет +-- Потому что этот запрос только на чтение + + +-- Задание 2 +-- Первая сессия будет просто ждать окончания выполнения первой +-- Если разрешить обоимUPDATE пройти одновременно, то получится какая-то херня пологаю, они смешаются, или че-то такое +-- У Alice в итоге останется второй email, тк он выполнится последним. Порядок COMMIT важен, тк в последствии этого порядка будут выполняться запросы + + +-- Задание 3 +-- select выполнится тк просто читает, а update встанет в очередь, тк он попытается изменить 'забронированные данные' +-- FOR UPDATE помогает избежать ошибок когда происходит покупка товара или че то такое, когда идет процесс изменений в данных + + +-- Задание 4 +-- Дедлок возникает потому что каждая сессия заблокировал то, что нужно другой сессии. База отменит одну из сессий, чтобы прошла хоть какая-то +-- Чтобы дедлок не возникал, нужно обновления все производить в одном порядке +-- Если при массовых обновлениях не соблюдать порядок строк, то будет возникать жопа из дедлоков \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql new file mode 100644 index 0000000..ec5086f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql @@ -0,0 +1,165 @@ +-- Задание 1 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE age > 25 AND country = 'RU'; +-- Выводят они один и тот же результат, потому что условия у них одинаковые +-- По скорости выполнения судя по планировщику тоже одинаковоы + + +-- Задание 2 +-- Вариант 1 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE o.status = 'refunded'; + +-- Вариант 2 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE o.status = 'refunded'; +-- Между первой и второй таблицей результаты одинаковые. Если в запросе нет никаких условий связанных с таблицами, то первой оптимизатор смотреть users, но если если условие по orders.status, то сначала он сканирует orders + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Тип сканирования: Seq Scan +-- Количество прочитанных строк: 1 +-- Время выполнения примерно: 0.023 ms(+-5) + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Поскольку данных в таблице мало у меня время выполнения одинаковое, но могу предположить, что второй выполнился быстрее, тк у него в селекте два поля, а не вся таблица + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.id, o.order_date +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- Читать начала с users, тк мы в where пишем условие для users, и быстрее сначала отсеять ненужных юзеров + + +-- Задание 6 +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +-- Его замедляет LOWER(не надо использовать функции), и думаю что LIKE(лучше использовать просто =), и ещэ group by по имени - это дольше чем по первичному ключу(надо группировать по ПК) + + +-- Задание 7 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- users -> orders -> order_items -> products - читает в таком порядке, потому что в where условие по users +-- Если убрать where, то будет - order_items -> users -> orders -> products +-- Как ускорить не знаю, может индексов добавть, но это пока не проходил + +-- Задание 8 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, price +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; +-- Данных в таблицах мало, врем выполнения не оценить, но смотря просто со стороны понятно что выриант А будет быстрее, потому что у него пару колонок в селекте, а не вся таблица как в Б + + +-- Задание 9 +-- Вариант коллег +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; + +-- Мой вариант +SELECT + id, name, age, is_active +FROM + mnoskov.users +WHERE + created_at BETWEEN '2026-01-01' AND '2027-01-02' AND + country = 'RU' AND + age > 45; +-- Мой вариант лучше потому что: +-- нет функций и мат. вычислений в where, все приведено к самому легкочитаемому формату +-- нет звездочки в select, а только то, что нужно + + +-- Задание 10 +INSERT INTO users (id, name, age, email, country, created_at) +SELECT + g AS id, + 'user_' || g AS name, + 27 AS age, + 'user_' || g || '@example.com' AS email, + 'RU' AS country, + '2026-02-03' AS created_at +FROM generate_series(20001, 50000) AS g; -- GPT сделал запрос мне + +SELECT * FROM mnoskov.users WHERE age IS NOT NULL; + +-- Запрос 1 +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT name, email FROM users WHERE age > 25; +-- Ну чето все равно не видна разница во времени выполнения, но по логике должен быть быстрее второй запрос \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql new file mode 100644 index 0000000..d3c362e --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql @@ -0,0 +1,203 @@ +-- Задание 1 +EXPLAIN +SELECT + name, email +FROM + users +WHERE + id = 1; +-- Использует index scan +-- Index Cond: (id = 1) - это значит что условие id = 1, и оно выполняется по индексу, и что id - индекс +-- База использует индекс, потому что у нас по полю id создан индекс, и база выбирает наиболее быстрый пусть ввыполнения запроса - через индекс + + +-- Задание 2 +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +-- В первом запрос index scan, потому что у нас уловие по индексу - id, во втором запросы seq scan, потому что у поля age не индексов, и таблица сканируется медленее + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT + name, price +FROM + products +WHERE + category = 'Электроника'; +-- Execution Time: 0.020 ms +-- Тип сканирования: Seq Scan +-- Ожидалось вернуть 1, а вернул 3 + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'RU'; +-- Без индекса - Execution Time: 6.769 ms +CREATE INDEX idx_country ON users(country); +-- С индексом - Execution Time: 2.076 ms +-- Сначала идет Bitmap Heap Scan on users - как я понял на этом этапе 'создается' таблица где country = 'RU', а потом Bitmap INdex Scan on idx_country - тут уже мы читам информацию из таблицы где все country = 'RU'. +-- Запрос стал быстрее примерно в три раза +-- Индекс помог, потому что база искала не среди всех данных, а среди 'проиндексированных' + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.order_date, o.status +FROM + users u +JOIN + orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- База исполщовала Nested Loop +-- Чтение началось с orders +-- Execution Time: 0.075 ms + + +-- Задание 6 +CREATE INDEX idx_email ON users(email); +EXPLAIN ANALYZE +SELECT + * +FROM + users +WHERE + lower(email) = 'user_28@example.com'; +-- База не использует индекс пока в where есть функция lower +-- Потому что в условии функция, а с ними индексы не в ладах как я понял +-- Чтобы индекс заработал, надо убрать функцию lower, и сравнивать email как есть +-- Если убрать lower, то Execution Time намного быстрее, прям очень намного быстрее + + +-- Задание 7 +CREATE INDEX idx_age ON users(age); +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +-- Быстрее работает запрос Б, но че то он тоже не хочет индекс использовать, хотя я его создал +-- Первый запрос не может использовать индекс, потому что там в условии математическое вычисление, а почему второй запрос не использует индек - я хз +-- Чтобы исправить запрос надо убрать сложение, и просто написать age > 25, как в запросе Б + + +-- Задание 8 +CREATE INDEX idx_created_at ON users(created_at); +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +-- Функция EXTRACT - это плохая идея, потому что это функция которой быть в where по возможности не должно +-- WHERE i.crated_at BETWEEN '2026-01-01' AND '2027-01-01' +-- Индекс не используется в текущеа виде + + +-- Задание 9 +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +-- Если будет много данных в таблице, то быстрее будет работать второй запрос +-- Второй эффективнее тк в селекте указаны определнные поля, а не все(*) +-- Разница будет особо замета при больших объемах данных + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU' AND p.category = 'Электроника'; +-- Порядок чтения таблиц: products -> order_items -> orders -> users +-- Количество строк: products - 3, order_items - 12, orders - 1, users - 0 +-- Если у country и category будут индексы, то запрос ьудет выполняться побыстре, потому что будет Index Scan, а не Seq Scan + + +-- Задание 11 +-- До +EXPLAIN ANALYZE +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +-- Execution Time: 0.219 ms + +-- После +EXPLAIN ANALYZE +SELECT * +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name = 'иван' AND + o.order_date BETWEEN '2024-01-01' AND '2024-02-01' AND + O.order_date BETWEEN '2026-01-01' AND '2026-02-01' AND + u.age > 25; +-- Execution Time: 0.032 ms +-- Мой вариант лучще, потому что у меня нет функций и вычисление мешаюзих использовать индексы в условии, и оптимизатор может успешно использовать индексы + + +-- Задание 12 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + users u +JOIN + orders o ON u.id = o.user_id AND u.country = 'RU'; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE EXISTS ( + SELECT + 1 + FROM + users u + WHERE + o.user_id = u.id AND + u.country = 'RU' +); + +-- Запрос 3 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE o.user_id IN ( + SELECT + u.id + FROM + users u + WHERE + u.country = 'RU' +); + +-- Запрос 1: Execution Time: 0.055 ms +-- Запрос 2: Execution Time: 0.051 ms +-- Запрос 3: Execution Time: 0.073 ms +-- Они все дют примерно одинаковый разультат, потому что мало данных в таблице. А планы выполнения у них у всех одинаковые, вот прям 1 в 1. Ну по логие 3 запрос самый медленный, а первые два шустрые \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql new file mode 100644 index 0000000..55377f9 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql @@ -0,0 +1,217 @@ +-- Задание 1 +EXPLAIN ANALYZE +SELECT country, count(*) AS users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING count(*) > 1 +ORDER BY users_count DESC; +-- WHERE применится до GROUP BY +-- HAVING применится после GROUP BY +-- Использовать users_count в HAVING нельзя, а в ORDER BY можно + + +-- Задание 2 +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' AND o.status = 'paid' +ORDER BY o.order_date DESC; +-- Порядок выполнения по QUERY PLAN: +-- 1) сначала сканирутся таблица orders и выполняется фильтрация o.status = 'paid', +-- 2) потом сканируется таблица users по условию u.id = o.user_id, и выполняется вильтрация u.country = 'RU', +-- 3) далее Nested Loop соединяет orders и users, которые уже прошли фильтры, +-- 4) теперь выводится выполняется сортирвка o.order_date DESC + +-- Порядок выполнения по логике: +-- 1) сначала сканируем таблицу users и присоединяем orders по условию u.id = o.user_id +-- 2) потом фильтруем соединенные таблицы по u.country = 'RU' и o.status = 'paid' +-- 3) в SELECT выбираем поля которые хотим вывести - u.name, o.order_date, o.status +-- 4) сортируем выбранные поля по o.order_date по убыванию + + +-- Задание 3 +-- Запрос А +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +-- Работает медленно потому having фильтрет после группировки а не до + +-- Запрос Б +SELECT country, avg(age) AS avg_age +FROM users +WHERE country = 'RU' +GROUP BY country; +-- Этот вариант быстрее, тк он фильтрует строки до группировки, те группируется меньше строк а не все + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +-- Количество обработанных строк у запроса А: 4 +-- Количество обработанных строк у запроса Б: 4 +-- PostgreSQL походу оптимизирует даже хуйню написанную в запросе А, планы запросов А и Б одинаковые - 1 в 1, хотя по сути у запроса А должно быть больше строк + + +-- Задание 5 +-- Изначальный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; + +-- Исправленный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; + +-- Задание 6 +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +-- Полный порядок выполнения: +-- 1) FROM/JOIN - сканируется таблица users, и к ней присоединяется таблица orders по условию u.id = o.user_id +-- 2) WHERE - из двух соединенных таблиц фильтруются и остаются только строки где u.is_active = true +-- 3) GROUP BY - происходится группировка по u.country, те все строки с одной и той же страной(записью в столбце) сжимаются в одну +-- 4) HAVING - уже по сгруппированным строкам проводится фильтрация по COUNT(o.id) > 1, остются только те страны(строки) где кол-во пользователей больше одного +-- 5) SELECT - выбираются столбцы которые будут выведены: u.country, COUNT(o.id) AS orders_count, AVG(o.id) AS avg_order_id +-- 6) ORDER BY - выбранные в SELECT строки сортируются по orders_count по убыванию +-- 7) LIMIT - из отсортированных строк остаются только первые 3 + + +-- Задание 7 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +-- по логике запрос Б должен быть быстрее тк он сначала филтрует строки а потом группирует уже отфильтрованное, +-- НО PostgreSql может оптимизровать и запрос А, и получается что выполняются оба запроса одинаково, сначала фильтрация а потом группировка +-- В обоих запросах на шаге 'Seq Scan on products' обрабатываются: 3 строки, а на 'GroupAggregate': 1 строка +-- Execution Time одно и тоже + + +-- Задание 8 +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +-- WHERE применяется между FROM/JOIN и GROUP BY +-- HAVING применяется между GROUP BY и SELECT +-- фильтрация по o.status в WHERE происходит группировки строк, а фильтрация по COUNT(o.id) в HAVING происходит уже после группирациюю, по самим сгруппированным строкам + + +-- Задание 9 +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +-- ORDER BY применяется после SELECT, перед LIMIT/OFFSET(если есть) +-- LIMIT идет после GROUP BY, тк сначала нужно отфильтровать строки чтобы взять их кусок(5 строк) +-- если LIMIT(5) выполнить до сортировки, то это будет бесполезно, возьмутся просто первые 5 строк, и уже они будут сортироваться, и по итогу выйдет хуйня + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT + category, + count(id) AS count_product_in_category, + avg(price) AS avg_category_price +FROM + products +GROUP BY + category +HAVING + count(id) >= 2 OR avg(price) > 15000 +ORDER BY + avg_category_price DESC +LIMIT 3; +-- 1) FROM - сканируем poducts +-- 2) GROUP BY - группируем по category +-- 3) HAVING - фильтруем по агрегированной функции count(id) +-- 4) SELECT - делаем выборку по столбцам category, count_product_in_category, avg_category_price +-- 5) ORDER BY - сортируем по avg_category_price по убыванию +-- 6) LIMIT - выбираем первые 3 строки из отсортированных +-- Ипсользовал HAVING тк по условию все фильтрации по агрегированным функциям +-- Выполнил с EXPLAIN ANALYZE, сначала читаетя porducts, потом делается фильтрации по count(id) >= 2 и avg(price) > 15000, далее sort, а потом Limit + + +-- Задание 11 +-- Запрос А +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'RU' +ORDER BY orders_count DESC; +-- Подзапрос в SELECT выполняется самым первым, до начала остального запроса, и называется в планировщие - SubPlan 1 +-- orders_count нельзя использовать в WHERE +-- orders_count можно использовать в ORDER BY + +-- Запрос Б(исправленный) +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + count(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.name, u.age +ORDER BY orders_count DESC; +-- Запрос Б(исправленный) - быстрее, тк подзапрос в SELECT делается для каждого пользователя заново, а JOIN всего один раз \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql new file mode 100644 index 0000000..79fc169 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql @@ -0,0 +1,260 @@ +-- Задание 1 +-- Ситуация -- Какой JOIN выберет база? -- Почему? +-------------------------------------------------------------------------------------- +-- 10 пользователей, 1 млн заказов,| Nested | Маленькая таблица и большая с индексом. | +-- есть индекс на orders.user_id | Loop | База будет искать совпадения из | +-- | | маленькой по индексу из большой. | +-------------------------------------------------------------------------------------- +-- 100 000 пользователей, | Hash | Таблицы среднего размера, нет индексов. | +-- 100 000 заказов, нет индексов | Join | База построит хэш-таблицу одной из них, | +-- | | и будет искать совпадения в другой. | +-------------------------------------------------------------------------------------- +-- Обе таблицы отсортированы по | Merge | Потому что таблицы отсортированы по | +-- ключу, есть индексы | Join | ключу. Будет происходить соединение - | +-- | | 'Одновременное&последовательное'. | +-------------------------------------------------------------------------------------- +-- Маленькая таблица (5 строк) | Nested | База будет искать совпадения из | +-- соединяется с большой (1 млн), | Loop | маленькой в большой. Но тк нет индексов | +-- нет индекса | | - это будет медленно. | +-- |--------| Я засомневался и разобрал с ГПТ: | +-- | Hash | Nested Loop будет дорого(без индексов - | +-- | Join | 5.000.000 проврок), поэтому скорее всего| +-- | | Hash Join, просто построится хэш-таблица| +-- | | по маленькой и 5 раз отсканирует большую| +-------------------------------------------------------------------------------------- +-- Я заебался строить эту таблицу из пробелов. Хуй когда решусь на это снова. | +-------------------------------------------------------------------------------------- + + +-- Задание 2 +-- Маленькая таблица (10 строк) +CREATE TABLE mnoskov.small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE mnoskov.big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; + +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); +--DROP INDEX idx_big_orders_user_id; + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Вариант 1: Merge Join - Потому что обе таблицы отсортированы с самого создания +-- Варинат 2: Тоже Merge Join +-- База могла бы выбрать Nested Loop, потому что у нас одна таблица маленькая а другая большая и с индексом, но выбрала Merge Join, потому что таблицы отсортированы +-- С индексом Execution Time: 31.579 ms, а без примерно 80.000 ms + + +-- Задание 3 +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +-- Во втором варианте также как и в первом - Nested Loop +-- Merge Join не появился +-- ORDER BY может повлиять на выбор JOIN, тк если есть стортировка, то база может выбрать Merge Join + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- (u.id = o.user_id) - Nested Loop; (o.id = oi.order_id) - Nested Loop; (oi.product_id = p.id) - Hash Join +-- Seq Scan на этапах сканирования orders и products +-- Думаю стоит создать индекс на orders.user_id; и надо было бы на users.countyr, но я создавал уже в предидущих укроках; подумал, что надо ещё products.id, но там id и так первичный ключ, значит и индекс есть +CREATE INDEX idx_orders_user_id ON orders(user_id); -- DROP INDEX idx_orders_user_id; +-- После создания индекса типы JOIN не поменялись, план остался тем же, скорость выполнения запроса та же + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'RU' +GROUP BY u.name; +-- на этапе LEFT JOIN order_items oi ON o.id = oi.order_id - оптимизатор выполнил Hash Right Join по условию oi.order_id = o.id, то есть Hash Join +-- на этапе LEFT JOIN orders o ON u.id = o.user_id - оптимизатор выполнил Hash Left Join по условию u.id = o.user_id, то есть тоже Hash Join +-- Метод по сути один и тот же - Hash Join, но отличаются Right и Left - я думаю это выбрано для лучшей оптимизации +-- Через Hash Right Join проходит - 12 строк +-- Через Hash Left Join проходит - 11281 строк + + +-- Задание 6 +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- Разный тип используется, в варианте А - Nested Loop, в варианте B - Hash Left Join +-- Да, у варанта B Execution Time в очень много раз больше +-- LEFT JOIN медленне, потому что он не использует индекс, а делает Seq Scan, те читает таблицы полностью, потому что подругому не получится выполнить LEFT JOIN + + +-- Задание 7 +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +-- Тип JOIN не изменился +-- Порядок чтения таблиц не изменился +-- Execution Time одинаковое +-- Оптимизатор выбрал один и тот же порядок потому что по сути это два одинаковых запроса, с одной и той же логикой, но написаны чуть чуть по разному, и оптимизатор в обоих случаях выполняет так, как быстрее. Почему порядок может быть разным, я не предполагаю. + + +-- Задание 8 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + * +FROM + orders o +JOIN + order_items oi ON o.id = oi.order_id +ORDER BY + o.id, oi.order_id; +-- Уверен такой запрос привел бы к Merge Join, если бы данных в таблицах было побольше + +-- Запрос 2(из задания 2) +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Потому что обе таблицы уже отсортированы в момент создания + +-- Задание 9 +-- Nested Loop лучше Hash Join, когда есть индексы по полям фильтрации, когда одна таблица небольшая, а у второй есть индекс. Подглядел: когда нужно найти немного совпадений. +-- Hash Join лучше чем Nested Loop, когда нет индексов, когда таблицы среднего размера и больше, и когдау условие фильтрации '=' + +-- Оба запроса из задания 6: +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; +-- тут nested loop быстрее, тк соеденияется все по индексам + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- а тут быстрее merge join, тк по индексам соединить не получится, тут нужны обе полные таблицы, и создается по одной из них хэш-таблица + + +-- Задание 10 +-- Запрос A +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- До: Execution Time: 0.309 ms +CREATE INDEX idx_orders_status ON orders(status); +-- После добавления индекса ничего не поменялось, план запроса остался тот же, скорее всего потому что мало данных, думаю будь их больше с индексом запрос был бы быстрее + +-- Запрос B +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- И тут тоже несмотря на то что индекс на orders статус я уже создал, таблица orders скаринруется как Seq Scan, я думаю это потому, что мало данных, будь их больше с индексом запрос был бы быстрее + +-- Запрос C +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Тут ситуация аналогичная и для первый двух запросов, нужные индексы уже созданы, в связи выполнения всех предидущих занятий и тем, и единственное что я мог сделать для улучшения времени выполнения - это добавить индекс на orders(status) \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql new file mode 100644 index 0000000..68c5a40 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql @@ -0,0 +1,45 @@ +--DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + registration_date timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS courses; +CREATE TABLE courses ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS lessons; +CREATE TABLE lessons ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + course_id bigint NOT NULL, + title varchar(150) NOT NULL, + duration_min SMALLINT NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS enrollments; +CREATE TABLE enrollments ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + course_id bigint NOT NULL, + enrolled_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS lesson_views; +CREATE TABLE lesson_views ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + lesson_id bigint NOT NULL, + viewed_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (lesson_id) REFERENCES lessons(id) +); \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql new file mode 100644 index 0000000..7438241 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql @@ -0,0 +1,155 @@ +INSERT INTO users (name, age, email, registration_date) VALUES +('Alice',25,'alice@mail.com','2023-01-10'), +('Bob',30,'bob@gmail.com','2023-02-05'), +('Charlie',22,'charlie@mail.com','2023-02-20'), +('Diana',28,'diana@mail.com','2023-03-01'), +('Ethan',35,'ethan@gmail.com','2023-03-10'), +('Frank',29,'frank@mail.com','2023-01-05'), +('Grace',26,'grace@gmail.com','2023-01-12'), +('Henry',32,'henry@mail.com','2023-01-20'), +('Iris',24,'iris@gmail.com','2023-02-10'), +('Jack',27,'jack@mail.com','2023-04-15'), +('Kelly',23,'kelly@gmail.com','2023-04-20'), +('Leo',31,'leo@mail.com','2023-01-15'), +('Mia',26,'mia@gmail.com','2023-01-18'), +('Nina',28,'nina@mail.com','2023-01-08'), +('Oscar',33,'oscar@gmail.com','2023-01-10'), +('Paul',29,'paul@mail.com','2023-02-15'), +('Quinn',24,'quinn@gmail.com','2023-03-20'), +('Rita',27,'rita@mail.com','2023-04-01'), +('Sam',30,'sam@gmail.com','2023-04-10'), +('Tina',25,'tina@mail.com','2023-04-18'); + +INSERT INTO courses (title, category, created_at) VALUES +('SQL для начинающих','data','2023-01-01'), +('Python для анализа данных','programming','2023-01-15'), +('BI с нуля','business','2023-03-01'), +('Excel для аналитиков','business','2023-02-01'), +('Основы машинного обучения','data','2023-03-15'); + +INSERT INTO lessons (course_id, title, duration_min) VALUES +(1,'SELECT и FROM',10), +(1,'JOIN',15), +(1,'WHERE и фильтрация',12), +(1,'Агрегации и GROUP BY',18), +(1,'Подзапросы',25), +(2,'Pandas основы',20), +(2,'NumPy',15), +(2,'Визуализация данных',22), +(3,'Основы BI',12), +(3,'Метрики и дашборды',18), +(3,'Power BI введение',20), +(4,'Формулы в Excel',15), +(4,'Сводные таблицы',18), +(4,'Графики и диаграммы',14), +(5,'Введение в ML',20), +(5,'Линейная регрессия',30), +(5,'Классификация',28), +(5,'Кластеризация',25); + +INSERT INTO enrollments (user_id, course_id, enrolled_at) VALUES +(1,1,'2023-01-15'), +(1,2,'2023-02-01'), +(2,1,'2023-01-20'), +(3,2,'2023-03-05'), +(4,3,'2023-04-01'), +(6,1,'2023-01-06'), +(6,2,'2023-01-10'), +(6,4,'2023-02-05'), +(7,1,'2023-01-13'), +(7,3,'2023-03-10'), +(8,1,'2023-01-22'), +(8,2,'2023-01-22'), +(8,3,'2023-01-22'), +(9,4,'2023-02-12'), +(10,1,'2023-04-16'), +(11,2,'2023-04-21'), +(12,1,'2023-01-16'), +(13,2,'2023-01-19'), +(13,4,'2023-02-20'), +(14,1,'2023-01-09'), +(14,2,'2023-01-09'), +(14,3,'2023-01-09'), +(14,4,'2023-02-01'), +(14,5,'2023-03-20'), +(15,1,'2023-01-11'), +(15,3,'2023-03-05'), +(16,2,'2023-02-16'), +(16,4,'2023-02-16'), +(17,3,'2023-03-22'), +(17,5,'2023-03-25'), +(18,1,'2023-04-02'), +(19,1,'2023-04-11'), +(19,2,'2023-04-11'), +(20,2,'2023-04-19'), +(5,5,'2023-03-20'), +(1,4,'2023-02-10'), +(2,3,'2023-03-15'), +(3,1,'2023-02-25'), +(4,2,'2023-03-10'), +(6,3,'2023-03-08'), +(7,2,'2023-02-01'), +(9,1,'2023-02-15'), +(12,3,'2023-03-10'), +(15,2,'2023-02-01'), +(18,4,'2023-04-05'); + +INSERT INTO lesson_views (user_id, lesson_id, viewed_at) VALUES +(1,1,'2023-01-16 10:00:00'), +(1,2,'2023-01-16 10:15:00'), +(1,3,'2023-01-17 09:00:00'), +(1,4,'2023-01-17 09:30:00'), +(1,4,'2023-01-17 11:00:00'), +(1,4,'2023-01-18 10:00:00'), +(1,6,'2023-02-02 14:00:00'), +(1,12,'2023-02-15 10:00:00'), +(1,13,'2023-02-15 10:30:00'), +(2,1,'2023-01-21 09:00:00'), +(2,9,'2023-03-16 10:00:00'), +(2,10,'2023-03-16 10:30:00'), +(3,6,'2023-03-06 12:00:00'), +(3,1,'2023-02-26 11:00:00'), +(3,2,'2023-02-26 11:20:00'), +(4,9,'2023-04-02 10:00:00'), +(4,10,'2023-04-03 10:00:00'), +(4,6,'2023-03-12 14:00:00'), +(4,7,'2023-03-13 09:00:00'), +(5,15,'2023-03-21 10:00:00'), +(5,16,'2023-03-22 11:00:00'), +(6,1,'2023-01-07 10:00:00'), +(6,2,'2023-01-07 10:15:00'), +(6,3,'2023-01-08 09:00:00'), +(6,4,'2023-01-08 09:30:00'), +(6,5,'2023-01-09 10:00:00'), +(6,6,'2023-01-11 14:00:00'), +(6,7,'2023-01-12 10:00:00'), +(6,8,'2023-01-13 11:00:00'), +(6,12,'2023-02-06 09:00:00'), +(6,13,'2023-02-06 09:30:00'), +(6,14,'2023-02-07 10:00:00'), +(6,9,'2023-03-09 11:00:00'), +(7,1,'2023-01-14 10:00:00'), +(7,2,'2023-01-14 10:20:00'), +(7,3,'2023-01-15 09:00:00'), +(7,9,'2023-03-11 14:00:00'), +(7,10,'2023-03-11 14:30:00'), +(9,12,'2023-02-13 10:00:00'), +(9,13,'2023-02-13 10:30:00'), +(9,1,'2023-02-16 11:00:00'), +(12,1,'2023-01-17 10:00:00'), +(12,2,'2023-01-17 10:20:00'), +(12,1,'2023-04-10 11:00:00'), +(12,3,'2023-04-10 11:30:00'), +(12,9,'2023-03-12 10:00:00'), +(13,6,'2023-01-20 14:00:00'), +(13,7,'2023-01-21 10:00:00'), +(13,12,'2023-02-21 09:00:00'), +(14,1,'2023-01-09 08:00:00'), +(14,2,'2023-01-09 08:15:00'), +(14,3,'2023-01-10 10:00:00'), +(14,4,'2023-01-10 10:30:00'), +(14,5,'2023-01-11 09:00:00'), +(14,6,'2023-01-10 14:00:00'), +(14,7,'2023-01-11 10:00:00'), +(14,8,'2023-01-12 11:00:00'), +(14,9,'2023-01-10 08:00:00'); \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql new file mode 100644 index 0000000..19b8668 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql @@ -0,0 +1,40 @@ +CREATE MATERIALIZED VIEW user_cohort_analysis AS +WITH users_and_enrollments_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT u.id) AS users_in_cohort, + count(DISTINCT e.user_id) AS enrolled_users, + count(DISTINCT e.course_id) courses_in_cohort + FROM + users u + JOIN + enrollments e ON u.id = e.user_id + GROUP BY + cohort_month +), +lesson_views_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT lv.user_id) AS active_users, + round(avg(lv.viewed_at::date - u.registration_date::date), 0) AS avg_time_to_first_view + FROM + users u + LEFT JOIN + lesson_views lv ON u.id = lv.user_id + GROUP BY + cohort_month +) +SELECT + uer.cohort_month, + uer.users_in_cohort, + uer.enrolled_users, + lvr.active_users, + round((lvr.active_users / uer.users_in_cohort::numeric) * 100, 2) || '%' AS retention_rate, + round(uer.courses_in_cohort / uer.users_in_cohort::numeric, 2) AS avg_courses_per_user, + lvr.avg_time_to_first_view +FROM + users_and_enrollments_results uer +JOIN + lesson_views_results lvr ON uer.cohort_month = lvr.cohort_month +ORDER BY + uer.cohort_month; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql new file mode 100644 index 0000000..9800c9f --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql @@ -0,0 +1,114 @@ +CREATE MATERIALIZED VIEW conversion_funnel AS +WITH stage_courses AS ( + -- registered + (SELECT + 0 AS course_id, + 1 AS stage_id, + 'registered' AS stage_name, + count(DISTINCT u.id) AS users_count + FROM + users u) + UNION ALL + -- enrolled + (SELECT + e.course_id, + 2 AS stage_id, + 'enrolled' AS stage_name, + count(DISTINCT e.user_id) AS users_count + FROM + enrollments e + GROUP BY + e.course_id + ORDER BY + e.course_id) + UNION ALL + -- started + (SELECT + l.course_id, + 3 AS stage_id, + 'started' AS stage_name, + count(DISTINCT lv.user_id) AS users_count + FROM + lessons l + JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id + ORDER BY + l.course_id) + UNION ALL + -- mid_progress + (WITH lessons_mid AS ( + SELECT + l.course_id, + ceil(percentile_cont(0.5) WITHIN GROUP (ORDER BY l.id)) AS mid_in_lesson_id + FROM + lessons l + GROUP BY + l.course_id + ) + SELECT + lm.course_id, + 4 AS stage_id, + 'mid_progress' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_mid lm + LEFT JOIN + lesson_views lv ON lm.mid_in_lesson_id = lv.lesson_id + GROUP BY + lm.course_id, lm.mid_in_lesson_id + ORDER BY + lm.course_id) + UNION ALL + -- completed + (WITH courses_lessons_id AS ( + SELECT + l.course_id, + l.id AS lesson_id + FROM + lessons l + GROUP BY + l.course_id, l.id + ), + lessons_numbers AS ( + SELECT + cli.course_id, + cli.lesson_id, + ROW_NUMBER() OVER(PARTITION BY cli.course_id ORDER BY cli.lesson_id DESC) AS lesson_number + FROM + courses_lessons_id cli + ) + SELECT + ln.course_id, + 5 AS stage_id, + 'completed' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_numbers ln + LEFT JOIN + lesson_views lv ON ln.lesson_id = lv.lesson_id + WHERE + ln.lesson_number = 1 + GROUP BY + ln.course_id, ln.lesson_id + ORDER BY + ln.course_id) +) +SELECT + sc.course_id, + c.title AS course_title, + sc.stage_name, + sc.users_count, + CASE + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NULL THEN 'first stage' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) = 0 THEN 0::text || '%' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NOT NULL THEN round((sc.users_count / lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' + END AS conversion_from_previous, + round((sc.users_count / FIRST_VALUE(sc.users_count) OVER (ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' AS conversion_from_start +FROM + courses c +RIGHT JOIN + stage_courses sc ON c.id = sc.course_id +ORDER BY + sc.course_id; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql new file mode 100644 index 0000000..4e670fc --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql @@ -0,0 +1,41 @@ +CREATE MATERIALIZED VIEW course_summary AS +WITH courses_unique_users AS ( + SELECT + e.course_id, + count(e.user_id) AS unique_users + FROM + enrollments e + GROUP BY + e.course_id +), +lessons_results AS ( + SELECT + l.course_id, + count(distinct l.id) AS lessons_count, + count(lv.lesson_id) AS total_views, + min(lv.viewed_at)::date AS first_viewed_at, + max(lv.viewed_at)::date AS last_viewed_at + FROM + lessons l + LEFT JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id +) +SELECT + c.id AS course_id, + c.title AS course_title, + cuu.unique_users, + lr.lessons_count, + lr.total_views, + round(lr.total_views / cuu.unique_users::numeric, 2) AS avg_views_per_user, + lr.first_viewed_at, + lr.last_viewed_at +FROM + courses c +JOIN + courses_unique_users cuu ON c.id = cuu.course_id +JOIN + lessons_results lr ON c.id = lr.course_id +ORDER BY + c.id; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql new file mode 100644 index 0000000..3377eb5 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql @@ -0,0 +1,39 @@ +CREATE MATERIALIZED VIEW lesson_popularity_ranking as +WITH lesson_views_results AS ( + SELECT + lv.lesson_id, + count(lv.user_id) AS total_views, + count(DISTINCT lv.user_id) AS unique_viewers + FROM + lesson_views lv + GROUP BY + lv.lesson_id +), +enrollments_results AS ( + SELECT + e.course_id, + count(e.user_id) AS enrolled_to_course + FROM + enrollments e + GROUP BY + e.course_id +) +SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.title AS course_title, + lvr.total_views, + lvr.unique_viewers, + round(lvr.total_views / lvr.unique_viewers::numeric, 2) AS avg_views_per_viewer, + dense_rank() OVER (ORDER BY lvr.total_views desc) AS popularity_rank, + round((lvr.unique_viewers / er.enrolled_to_course::numeric) * 100, 0) || '%' AS completion_rate +FROM + lessons l +JOIN + courses c ON l.course_id = c.id +JOIN + lesson_views_results lvr ON l.id = lvr.lesson_id +JOIN + enrollments_results er ON c.id = er.course_id +ORDER BY + popularity_rank; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql new file mode 100644 index 0000000..32fbe79 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql @@ -0,0 +1,48 @@ +CREATE MATERIALIZED VIEW platform_summary as +WITH users_count AS ( + SELECT + count(*) AS total_users + FROM + users +), +courses_count AS ( + SELECT + count(*) AS total_courses + FROM + courses +), +lessons_count AS ( + SELECT + count(*) AS total_lessons + FROM + lessons +), +users_with_views AS ( + SELECT + count(distinct user_id) AS users_with_views + FROM + lesson_views +), +views_count AS ( + SELECT + count(*) AS total_views + FROM + lesson_views lv +) +SELECT + uc.total_users, + cc.total_courses, + lc.total_lessons, + uwv.users_with_views, + round(lc.total_lessons / cc.total_courses::NUMERIC, 2) AS avg_lessons_per_course, + round(vc.total_views / lc.total_lessons::NUMERIC, 2) AS avg_views_per_lesson +FROM + users_count uc +CROSS JOIN + courses_count cc +CROSS JOIN + lessons_count lc +CROSS JOIN + users_with_views uwv +CROSS JOIN + views_count AS vc; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql new file mode 100644 index 0000000..46cfb4d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql @@ -0,0 +1,24 @@ +CREATE MATERIALIZED VIEW user_activity_summary AS +SELECT + u.id AS user_id, + u.name, + count(DISTINCT e.course_id) AS enrollment_count, + count(DISTINCT lv.lesson_id) AS viewed_lessons_count, + max(lv.viewed_at)::date AS last_view, + min(lv.viewed_at)::date AS first_view, + ((count(DISTINCT lv.lesson_id)::numeric(5,2) / count(DISTINCT l.id)) * 100)::numeric(5, 2) AS engagement_rate +FROM + users u +JOIN + enrollments e ON u.id = e.user_id +JOIN + courses c ON e.course_id = c.id +JOIN + lessons l ON c.id = l.course_id +LEFT JOIN + lesson_views lv ON u.id = lv.user_id -- сюда - 'AND l.id = lv.lesson_id' +GROUP BY + u.id, u.name +ORDER BY + u.id; +-- Кинул его в нейронку на проверку, она сказала что надо добавить 'AND l.id = lv.lesson_id' при присоединении таблицы lesson_views(сделал пометку в коде), это нужно чтобы точно проверить, что урок который посмотрел пользователь точно принадлежит его курсу, чтобы избежать ошибок из-за некорректных данные если такие появятся в результате какого либо сбоя или ошибки. \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql new file mode 100644 index 0000000..49c296d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql @@ -0,0 +1,51 @@ +-- Задание 1 +SELECT + * +FROM + conversion_funnel +WHERE + stage_name = 'started' +ORDER BY + conversion_from_previous; +-- Первые 2 курса, гже отавлилось больше всего учеников между этапами enrolled → started + + +-- Задание 2 +SELECT + * +FROM + lesson_popularity_ranking lpr +WHERE + lpr.avg_views_per_viewer > 1 +LIMIT 3; + + +-- Задание 3 +SELECT + * +FROM + user_cohort_analysis uca +ORDER BY + retention_rate DESC; +-- Не разобрался как можжно соеденить с другими таблицам +-- А retention_rate такой, наверное потому что при запуске курсов была пиар компания большая, и делали рассылки с напоминанием позаниматься, а потом перестали, вот и удержание похерилось + + +-- Задание 4 +-- Витрина - user_activity_summary +-- Всяко разно попробовал подобавлять индексы, итог: без индексов быстрее. Просмотрел весь запрос, вроде ничего не нужно менять - лишнего ничего нет. + +-- Витрина - course_summary +-- Тут также как и в предидущем - и так и сяк попробовал, план запроса не меняется, время не улучшается + +-- Витрина - platform_summary +-- Тут тоже нечего менять - всё из CROSS JOIN. Попробовал вместо count(*) писать count(id) думал мол в ширину будет меньше читать, в итоге выяснил что и так и так одинаково читает + +-- Витрина - user_cohort_analysis +-- И тут че не делай, все одна фигня. План вроде хороший. + +-- Витрина - lesson_popularity_ranking +-- Тут тоже ничего не вышло - я начниаю чувствовать себя тупым от того что не могу улучшить нигде время запроса. + +-- Витрина - conversion_funnel +-- Здесь тоже ничего не вышло - со стороны план запроса выглядит хорошо. Ускорить время запроса не вышло. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql new file mode 100644 index 0000000..d29f3e7 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql @@ -0,0 +1,111 @@ +/*Задание 1: По каким полям делать индексы(тут говорится смотреть на таблицы из пет проекта 'Онлайн магазины', надеюсь смотреть на таблицы а не на витрины): + 1) По каким полям, как тебе кажется, реальный продукт‑менеджер или аналитик будет чаще всего: + where: + users: age, email, registration_date, + enrollments: enrolled_at + courses: title, category + lessons: title, duration_min + lesson_views: viewed_at + order by: + users: id, age, registration_date + enrollments: user_id, enrolled_at + courses: id + lessons: id, duration_min + lesson_views: user_id, viewed_at + group by: + users: age, registration_date + enrollments: user_id, course_id, enrolled_at + courses: category + lessons: course_id + lesson_views: user_id, lesson_id + join: + users: id + enrollments: user_id, course_id + courses: id + lessons: id, course_id + lesson_views: user_id, lesson_id + 2) Для каждого поля, которое ты назвал, укажи: «индекс нужен / индекс не нужен» и коротко объясни почему (часто фильтруем, редко фильтруем, мало разных значений и т.п.). + users: + id(есть автоматически), + age(не нужен, мало разнах значений), + email(нужен, чтобы найти пользователя через where) + registration_date(нужен, часто будет сравниваться в BETWEEN наприме, и сортироваться) + enrollments: + user_id(нужен, будет часто джоиниться), + course_id(нужен, будет часто джоиниться), + enrolled_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться) + courses: + id(есть автоматически), + title(не нужен, мало разных значений), + category(не нужен, мало разных значений) + lessons: + id(есть автоматически), + course_id(нужен, будет часто джоиниться), + title(не нужен, мало разных значений), + duration_min(мне кажется не нужен, тк не очень часто будет использоваться) + lesson_views: + user_id(нужен, будет часто джоиниться), + lesson_id(нужен, будет часто джоиниться) + viewed_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться)*/ + +-- Задание 2: Свяжи запрос и индекс: + -- 1) + SELECT * FROM users WHERE email = 'какой-то email'; -- индекс на email ускорит where + -- 2) + SELECT * FROM users WHERE is_active = true AND created_at >= CURRENT_DATE - INTERVAL '1 year'; -- индекс на created_at ускоряет where + -- 3) + SELECT * FROM products WHERE category = 'Электроника' ORDER BY price; -- индекс по price ускорит order by + -- 4) + SELECT * FROM orders WHERE user_id = 1 AND order_date >= CURRENT_DATE - INTERVAL '30 days'; -- индекс на user_id и order_date ускорят where + -- 5) + SELECT order_id, product_id FROM order_items; -- на у тут по идее составной создается сразу, так как тут pkey составной. Ну и они ускоряют select + +/* Задание 3. Порядок столбцов в индексе + 1) Я бы сделал orders(order_date, user_id). + 2) Для первых двух будет работать четко, а для третьего хуже. Тк у третьего нет фильрации по order date, и он стоит вторым в индексе + 3) + * Когда один индекс - хорошо с фильтрацией по нескольким полям, и когда первое поле указанное в индексе используется часто, а второе нет. + * Когда индекса два - хорошо когда оба поля фильтруются по отдельности*/ + +/* Задание 4. Лишние индексы в наших таблицах + !!! Я тут не буду писать индексы которые создаются автоматически из за pkey + users: + Скорее нужен индекс: + email, created_at(нужны для where, и order by) + Скорее не нужен индекс: + name, age, country, is_active(либо мало рахных значений, либо не очень востребованы эти поля) + products: + Скорее нужен индекс: + price (для order by и where) + Скорее не нужен индекс: + name, ctegory, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + orders: + Скорее нужен индекс: + user_id(для join), price(для order by и where) + Скорее не нужен индекс: + name, category, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + order_items: + Скорее нужен индекс: + price_at_purchase (для order by и where) + Скорее не нужен индекс: + quantity(мало разных значения) + + Много индексов не есть хорошо, потому что они тоже занимают помять и их тоже нужно считать. Если их бездумно везде понатыкать то запрос может выполняться дольше.*/ + +/* Задание 5. Что должно быть уникальным + 1) id, email + 2) id, article_number(придумал вот такое) + 3) + * Если не будет уникаьности по id, то возникнут проблемы бд, что у разных пользователей будет один id и это жопа, ничего невозможно фильтровать и тд. + * Если email будет повторяться, то как вообще пользователи будут входить в аккаунт, и как делать персонализированную рассылку? Никак. + * Если article_number будет повторяться, то те же проблемы что и с id, будет нарушение логики работы бд. + + * Уникальный индекс обезопасит бд от сбоев и некорректной работы.*/ + +-- Задание 6. Где индексы помогут, а где нет + SELECT * FROM users WHERE id = 100; -- индекс уже создан тк это pkey + SELECT * FROM users WHERE email = 'user@example.com'; -- нужен, поможет с where, быстрее будет рабоать фильтрация + SELECT * FROM products WHERE price BETWEEN 100 AND 500; -- нужн, поможет с where, быстрее будте фильтрация + SELECT * FROM products WHERE name LIKE '%phone%'; -- не нужен, индекс не рвботает с like + SELECT * FROM orders WHERE status = 'DELIVERED'; -- не нужен, в status мало разных значений + SELECT * FROM order_items WHERE order_id = 123; -- нужен, поможет с where, быстрее будет рабоать фильтрация \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql new file mode 100644 index 0000000..fa096dc --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql @@ -0,0 +1,189 @@ +/* Задание 1. Какой вид партиционирования выбрать + 1) + * range по полю order_date + * запросы с between в год + 1. where order_date >= '2022-01-01' and order_date < '2023-01-01' + 2. where extract('year' from order_date) = '2025' + 2) + * тут два типа партиционирования подходят. range по created_at или list по country. + * если выбирать list по country то плюс - это при фильтрах по стране будут отсекаться лишние партиции, а минус, что год/месяц регистрации не будет разделен по партициям. + 3) + * логично range по created_at - чтобы разделить по месяцам/годам, а hash по user_id - почему так, точно сказать не могу, но в теории сказано что hash используется когда нет диапазона или списка и это частый сценарий для логов + * с range выиграют запросы с фильтрами по created_at, а с hash выигают запросы с фильтрами по user_id, а все что не указаны как поле для партиций, не почувствуют разницы.*/ + + +/* Задание 2. Размер партиций + 2) количество партициый за 10 лет по: + * годам - 10, тут вообще все четко, и удалять не сложно, и поддержка проще, и запросы будут хорошо делаться + * месяцам - 120, тут с удалением тарых данных труднее, поддержка тяжелая, но скорость запросов будет лучше, если фильтрация будет по месяцам + * дням - примерно 3650, а это я чувствую пиздец, с удалением и поддержкой жопа, и со скоростью запросов тоже жопа - данные будут читаться из кучи партиций что не есть хорошо + 3) + Я думаю прям для очень большого магазина оптимальнее будет партиционирование по месяцам - тк очень много покупателей и большинство статистик будет по месяцам, по годам тоэе конечно, но думаю приоритетнее по месяцам*/ + + +-- Задание 3. RANGE‑партиции по дате заказов +DROP TABLE IF EXISTS orders_part; +CREATE TABLE orders_part ( + id bigint, + user_id bigint, + order_date timestamptz NOT NULL, + status varchar(20), + PRIMARY KEY (id, order_date), + FOREIGN KEY (user_id) REFERENCES users_1(id) +) PARTITION BY RANGE (order_date); + +CREATE TABLE orders_2024 PARTITION OF orders_part +FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); + +CREATE TABLE orders_2025 PARTITION OF orders_part +FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); + +CREATE TABLE orders_2026 PARTITION OF orders_part +FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + order_date BETWEEN '2025-01-01' AND '2025-12-31 23:59:59'; -- пришлось сделать так, тк дата 2026-01-01 тоже есть в таблице +-- в этом запросе за год - читатется только одна партиция - orders_2025, НО если в условии все же написать AND '2026-01-01', то читаются две партиции orders_2025 и orders_2026 и время выполнения запроса намного больше тк выполняется full scan обеих таблиц + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + EXTRACT('year' FROM order_date) IN ('2024', '2025'); -- так читаются все три партиции, 2026 почему-то тоже захватывается, но по итогу после сканирование у таблицы orders_2026 - rows = 0 + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part; +-- так просто сканируются все три партиции, но без фильтров, как было в предидущем варианте + + +-- Задание 4. LIST‑партиции по стране пользователей +DROP TABLE IF EXISTS users_part; +CREATE TABLE users_part ( + id int8 NOT NULL, + "name" varchar(100) NOT NULL, + age int2 NULL, + email varchar(100) NULL, + country bpchar(2) NULL, + is_active bool NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, country) +) PARTITION BY LIST (country); + +CREATE TABLE users_ru PARTITION OF users_part +FOR VALUES IN ('RU'); +CREATE TABLE users_us PARTITION OF users_part +FOR VALUES IN ('US'); +CREATE TABLE users_gb PARTITION OF users_part +FOR VALUES IN ('GB'); +CREATE TABLE users_de PARTITION OF users_part +FOR VALUES IN ('DE'); +CREATE TABLE users_fr PARTITION OF users_part +FOR VALUES IN ('FR'); +CREATE TABLE users_jp PARTITION OF users_part +FOR VALUES IN ('JP'); +CREATE TABLE users_uk PARTITION OF users_part +FOR VALUES IN ('UK'); +CREATE TABLE users_kz PARTITION OF users_part +FOR VALUES IN ('KZ'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country = 'RU'; +-- запрос скаинровал только партицию users_ru + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country IN ('RU', 'KZ'); +-- запрос сканировал только партиции users_ru и users_kz + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + age BETWEEN 44 AND 66; +-- сканировал не просто users_part, а каждую партицию по отдельности + + +-- Задание 5. HASH‑партиции по user_id в логах +DROP TABLE IF EXISTS user_logs_part; +CREATE TABLE user_logs_part ( + id int8, + user_id bigint NOT NULL, + created_at timestamp NOT null, + event_type text NOT NULL, + payload jsonb, + PRIMARY KEY (id, user_id) +) PARTITION BY hash (user_id); + +CREATE TABLE user_logs_p1 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 0); + +CREATE TABLE user_logs_p2 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 1); + +CREATE TABLE user_logs_p3 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 2); + +CREATE TABLE user_logs_p4 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 3); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE user_id = 228; +-- читает только user_logs_p2 + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE created_at >= current_date - INTERVAL '7 days'; +-- читает все партиции + +-- Задание 6 +-- email, чтобы искать пользователя быстрее, created_at для сортировки и where +CREATE UNIQUE INDEX idx_users_ru_email ON users_ru(email); +CREATE INDEX idx_users_ru_created_at ON users_ru(created_at); + +CREATE UNIQUE INDEX idx_users_us_email ON users_us(email); +CREATE INDEX idx_users_us_created_at ON users_us(created_at); + +CREATE UNIQUE INDEX idx_users_gb_email ON users_gb(email); +CREATE INDEX idx_users_gb_created_at ON users_gb(created_at); + +CREATE UNIQUE INDEX idx_users_de_email ON users_de(email); +CREATE INDEX idx_users_de_created_at ON users_de(created_at); + +CREATE UNIQUE INDEX idx_users_fr_email ON users_fr(email); +CREATE INDEX idx_users_fr_created_at ON users_fr(created_at); + +CREATE UNIQUE INDEX idx_users_jp_email ON users_jp(email); +CREATE INDEX idx_users_jp_created_at ON users_jp(created_at); + +CREATE UNIQUE INDEX idx_users_uk_email ON users_uk(email); +CREATE INDEX idx_users_uk_created_at ON users_uk(created_at); + +CREATE UNIQUE INDEX idx_users_kz_email ON users_kz(email); +CREATE INDEX idx_users_kz_created_at ON users_kz(created_at); +-- должны ускориться запросы с поиском пользователей по email, и запросы с сортировкой по дате или с фильтрацией по интервалам \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql new file mode 100644 index 0000000..c3f5e12 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql @@ -0,0 +1,19 @@ +-- Задание 1. Выбор ключа шардирования +-- Ну я тут чето нихера не понял +-- Вот users лучше всего шардировать по id, а с orders непонятно. Я не до конца понял логику шардирования, если orders и users на разных шардах, то наверное лучше шардировать по user_id. Но в тоже время логично было бы сделать по order_date. +-- Короче я очень плохо понял шардирование, сейчас поищу объяснение на ютубчике и напишу новый ответ. +-- В нашем случае мы получается обсуждаем горизонтальное шардирование +-- Как я понял, если шардируются две таблицы, то они шардирются по одному полю у нас это users(id) и orders(user_id). +-- Я выбрал users(id), тк это уникальное поле и есть join по нему в orders - user_id +-- Шардирование будет происходить по хэшу. Быстрее будут выполняться запросы в пределах одного шарда, а запросы где данные берутся из разных шардов да ещё и join - явно станут сложнее. + +-- Задание 2. Добавление нового шарда +-- Проблемы: мне кажется временная приостановка записи и чтения таблиц на время 'перешардирования'. +-- План: 1. остановить запись и чтение данных; 2. создать 8 новых шардов; 3. перераспределить данные из старых четырех в восемь новых шардов; 4. удалить старые 4 шарда; 5. поменять логику изменения и чтение в бэк энде например 6. разрешить запись и чтение. конец + +-- Задание 3. Шардинг против партиционирования +-- При партицировании таблицы данные хранятся на одной бд, внутри одной таблицы, но эта таблица внутри себя разделена на партиции для ускорения запросов к ней. +-- При шардировании таблицы данные хранятся на разных серверах, то есть таблица делится на части(при горизонтальном шардировании), и каждая часть таблицы отправляется на разные сервер. + +-- Пример партицирования без шардирования: когда сервер не переполнен, но таблица слишком большая, и и запросы к ней замедляются, тогда таблица делится на партиции, для более быстрого выполнения запросов к ней. +-- Пример когда без шардирования не обойтись: это когда сервер перегружен, и физически невозможно добавлять новые данные и чтение таблиц очень медленное, тк для этого чтения сервер не может выделять должное количество ресурсов. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql new file mode 100644 index 0000000..d9adc7e --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql @@ -0,0 +1,40 @@ +-- Задание 1. Где и зачем понадобится репликация +-- На примере онлайн-магазина +-- Пример 1: мастер сервер перестал работать(по любым причинам), и чтобы не останавливать работу магазина - делаем реплику мастером. Если бы небыло репликации работа магазина приостановилась бы на какое время. +-- Пример 2: на мастер сервере даже без чтения высокая нагрузка с инсертами, апдейтами, а чтение происходит с реплики, например отчеты для аналитиков читаются с реплики чтобы не перегружать мастер. Если убрать репликацию, то сервер будет один и может перегрузиться и упасть. +-- Пример 3: я сдеал первыве два примера настоллько обобщенными, что они покрываются все более конкретные примеры, и я не могу придумать пример 3. + + +-- Задание 2. Сравнение синхронной и асинхронной репликации +-- * как ведёт себя запись при нормальной работе; +-- * синхронная репликация - мастер ждет пока реплика завершит копирование, наверное блокирует строку в таблице как в тарнзакции строка блокируется пока транзакция не завершится. +-- * асинхронная репликация - мастер ничего не ждет - его главная задача, что он отправил даные в реплику, и реплика уже пусть сама разбирается и сохраняетс всё. +-- * что происходит при падении мастера; +-- * я думаю при обоих видах репликаций - реплика становится мастером. +-- * какие риски потери/отставания данных есть. +-- * с синхронной - рисков с отставанием и потерями вроде нет. +-- * в случае с асинхронной - реплика может не успеть сохранить что-то одно, как придет уже что-то второе в туже строку например, или может произойти какой-то сбой, и данные прийдут некорректно - получается транзакция просто не откатится, а будет как будет. + +-- платежному сервису нужен синхронный вид репликации, тк ошибки и неточность недопустимы. +-- аналитической витрины нужен асинхронный вид репликации, тк он быстрее, и небольшая неточность не критична. + + +-- Задание 3. Логическая и физическая репликация +-- * думаю логическая репликация это больше про то, что мы видим и делаем сами, по типу sql запрос написать, а физическая репликация - это про то, что происходит на уровне субд/сервера, что данные перетекают байтах, WAL-журналов(я погуглил что-это - это грубо говоря план изменений, но пока не сами изменения) +-- Оказывается про логические физические репликации я тоже ничего не понял, сейчас прогуглю, и напишу ответ заново. +-- * у логической репликации изменения передаются как sql запросы, для отдельных талиц например, а физическаая репликация изменяется побайтово создавая полную копию мастера +-- * у логической репликации высокая гибкость, можно выбирать что обновлять, а что нет, а у физической репликации нулевая гибкость - создается просто копия мастера 1 в 1 +-- * как я понял для миграция лучше всего логическая репликация - тк высокая гибкость, а физическая подходит для резервной копии + +-- * для физической репликации - банковская система, тк физичская быстрее и копия 1 в 1 делается, а для логическрй репликации я не могу ничего придумать. + + +-- Задание 4. Схема один мастер — несколько реплик для чтения +-- * на мастер отправлять строго unsrt update delete только с изменениями, а на реплики select, только с чтением +-- * проблемы из-за отсавания: 1. пользователь обновил фото профиля, а изменилось оно на странице толькко через 1-2 минуты; 2. один пользователь купил последний товар, а реплика не успела обновиться, и другой пользователь тоже попытался заказать этот товар, и у него вылезла ошибка, получается что товар отображается в наличии а купить нельзя; 3. в банках если будут отставать то там вообще жопа будет со счетами, но там получается синхронная репликация. +-- * наверное можно закрыть доступ на изменения для строк которые ещё не до конца реплицировались + +-- Задание 5. Мульти‑мастер: когда это оправдано +-- * главные плюсы: 1. распределение нагрузки между серверами 2. каждая мастер репликация отвечает только за свою зону ответсвенности +-- * сложности: 1. конфликты, что на разных мульти репликах могут быть разные записи по одному и тому же значению и будет не ясно что важнее оставить; 2. согласованность - кадая мульти реплика должна работать только в пределах своей зоны ответственности; 3. сложность настройки - я думаю имеется ввиду защита от конфликтов и поддержка согласованности. +-- * у меня в голове только мысль о разделении отвественности по хэшу, что одна мастер реплика отвечает за один кусок, другая за другой, по типу шардирования, только без деления таблиц, они как бы и дальше между собой реплицируются, но каждая изменят обновляет и делает записи только на своем куске. Наверное такой способ поможет при больших таблицах с частыми изменениями(хотя по суть шардирование лучше испольщовать тогда, а не мульти-мастер). Неэффективно будет на таблицах с редкими изменениями, и не очень больших таблицах. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql new file mode 100644 index 0000000..989b738 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql @@ -0,0 +1,32 @@ +-- Задание 1 +-- 1. будет +-- 2. не будет. можно изменить запрос - убрать lower +-- 3. не будет. нужно убрать like +-- 4. не будет. нужно убрать like +-- 5. будет +-- 6. не будет, тк фильтрация по полю без индекса + + +-- Задание 2 +-- не понял задание, сначала нужно каждому запросу сказать что индекс не нужен/бсполезен, а потом создать индекс? +-- 1. по идее есть смысл создать два индекса - по user_id и created_at, или состваной. + CREATE INDEX idx_orders_user_id ON orders(user_id); + CREATE INDEX idx_orders_created_at ON orders(created_at); +-- 2. тут никакой индекс не нужен +-- 3. тут тоже индекс не нужен, тк у status всего 4 значения + + +-- Задание 3 +-- 1. тут hash, тк равенство +-- 2. здесь индекс и не нужен, тк у status всего 4 значения +-- 3. тут B-tree, тк интервал(<,>) +-- 4. конкретно в этом запросе лучше hash, тк равенство, но в целом думаю на user_id лучше B-tree, тк часто сортируют по нему + +-- * hash иммет смысл где просто равенство +-- * B-tree обязательно там где интервалы, > или <, и где order by +-- * в варианте 2, где у столбца всего 4 уникальных значения + + +-- Задание 4 +-- * первый составной нужен для запросов с фильтрацией по нескольким полям +-- * второй не нужен, тк user_id будет рабоать эффективно и в первом случае \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql new file mode 100644 index 0000000..d9fa2a2 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql @@ -0,0 +1,12 @@ +-- Задание 1. Интернет‑банк +-- * в этом случае важна атомарность, и первый запрос должен был откатиться обратно, и не дублироваться как я понимаю. +-- * если не будеь атомарноти, то после сбоев могут произойте несостыковки, напрмер у пользователя деньги списались, а заказ не оформился, или наоборот + + +-- Задание 2. Лента новостей и лайки +-- * эта система ближе к BASE, тк согласование узлов происходит не сразу, и у кого то 22 лайка, а у кого-то 24. + + +-- Задание 3. Заказы в интернет‑магазине +-- * у первого случая принципы ACID - это нужно чтбы критическа важная информация обновлялась без ошибок, и чтобы небыло несостыковок +-- * во втором случае принципы BASE - можно немного потерпеть неточность, тк это некритично \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql new file mode 100644 index 0000000..33e81b8 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql @@ -0,0 +1,13 @@ +-- Задание 1: Разбери ситуацию +-- 1) 200 +-- 2) 100 +-- 3) Результаты разные тк на уровне Read Committed разрешено неповторяемое чтение, а на уровне Repeatable Read неповторяемое чтенине запрщено, и мы видим то, что было на момент начала транщакции. + +-- Задание 2: Практика +-- При Repeatable Read, я увидел во втором select значение - 100, потому что изменение произошли до того как завершилась транзакция с чтением, и зафиксировалось то значение которые было на момент начала транзакции с чтением +-- При Read Committed, во втором select было значение - 200, тк этот уровень изолированности подразумевает чтение закоммиченных(другой транзакцией) данных даже если текущая транзакция ещё не завершена + +-- Задание 3: Найди ошибку +-- 1) Так произошло потому, что выбран некорректный уровень изоляции. +-- 2) Тут подойдет serializavle. +-- 3) Решить эту проблему без смены изоляции можно с помощью проверки баланса перед списанием, и если денег на балансе будет недостаточно, то откатить транзакцию. Такое решение подойдет только с уровнем изоляции Read Commited, для Repeatable Read такой способ уже не подойдет. \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql new file mode 100644 index 0000000..d66aec9 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql @@ -0,0 +1,11 @@ +-- Задание 1: Определи тип базы +-- 1) CP +-- 2) AP +-- 3) CA +-- 4) AP + + +-- Задание 2: Что выбрать? +-- CP - должна быть согласованность, чтобы последний билет достался только одному, у двух людей не может быть одинаковый билет +-- Если выбрать CP, то все будет четко, и билет достентся только одному пользователю +-- Если выбрать AP, то билет может достаться и тому и тому, что недопустимо \ No newline at end of file diff --git a/pmikhailov/postgresql/.project b/pmikhailov/postgresql/.project new file mode 100644 index 0000000..92f941b --- /dev/null +++ b/pmikhailov/postgresql/.project @@ -0,0 +1,11 @@ + + + postgresql + + + + + + + + diff --git a/pmikhailov/postgresql/homework-aggregation-groupby.sql b/pmikhailov/postgresql/homework-aggregation-groupby.sql new file mode 100644 index 0000000..5fff307 --- /dev/null +++ b/pmikhailov/postgresql/homework-aggregation-groupby.sql @@ -0,0 +1,111 @@ +/* Задание 1 */ + +SELECT + category, + count(*) AS products_count  +FROM + products p +GROUP BY category +ORDER BY products_count DESC; + + +/* Задание 2 + * Тут всё решение в задании + * */ + +SELECT + order_id, + sum(quantity) AS total_quantity, + sum(quantity * price_at_purchase) AS order_revenue +FROM + order_items oi +GROUP BY order_id +ORDER BY order_revenue DESC; + + +/* Задание 3 */ + +SELECT + p.category, + COALESCE(sum(oi.quantity * oi.price_at_purchase), 0) AS category_revenue +FROM + products p +LEFT JOIN + order_items oi ON p.id = oi.product_id +GROUP BY p.category + + +/* Задание 4 */ + +SELECT + o.id AS order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue  +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY 1 +ORDER BY 2 DESC + + +/* Задание 5 */ + +SELECT + p.category, + COALESCE(sum(oi.quantity * oi.price_at_purchase), 0) AS category_revenue +FROM + products p +LEFT JOIN + order_items oi ON p.id = oi.product_id +GROUP BY + p.category +HAVING + sum(oi.quantity * oi.price_at_purchase) > 1; + + +/* Задание 6 */ + +SELECT + country, + count(*) AS active_users_count  +FROM + users u +WHERE + is_active = TRUE +GROUP BY + country +HAVING + count(is_active) > 2; + + +/* Задание 7 */ + +SELECT + p.name AS product_name, + COALESCE (sum(oi.quantity), 0) AS total_quantity_sold  +FROM + products p +LEFT JOIN + order_items oi ON p.id = oi.product_id +GROUP BY p.name +ORDER BY 2 DESC +LIMIT 3 + + +/* Задание 8 + * Условие для HAVING в задаче кривое, < 100 ничего нет + * */ + +SELECT + order_id, + SUM(quantity * price_at_purchase) AS order_revenue  +FROM + order_items oi +GROUP BY + order_id +HAVING + SUM(quantity * price_at_purchase) < 10000 +ORDER BY 2; + + diff --git a/pmikhailov/postgresql/homework_window_functions.sql b/pmikhailov/postgresql/homework_window_functions.sql new file mode 100644 index 0000000..4b799ea --- /dev/null +++ b/pmikhailov/postgresql/homework_window_functions.sql @@ -0,0 +1,152 @@ +/* Задание 1 */ + +SELECT + u.id, + u."name", + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date) AS order_number_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 2 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue -- добавил агреграт по всем поз. в заказе + FROM + order_items oi + GROUP BY oi.order_id + ) +SELECT + u.id AS user_id , + u."name", + o.id AS order_id, + o.order_date, + orc.order_revenue, + sum(orc.order_revenue) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS running_revenue_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id +WHERE o.status NOT IN ('cancelled', 'refunded') -- добавил условие не включать отмененные заказы + + + +/* Задание 3 */ + +WITH user_revenue_cte AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS user_total_revenue + FROM + orders o + INNER JOIN + order_items oi ON o.id = oi.order_id + GROUP BY o.user_id) -- добавил group by, костыль убран +SELECT + u."name", + urc.user_total_revenue, + RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_dense_rank +FROM + users u +INNER JOIN + user_revenue_cte urc ON u.id = urc.user_id; + + +/* Задание 4 */ + +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id; + + +/* Задание 5 */ + +SELECT + u.name, + o.id, + count(o.id) OVER ( + PARTITION BY o.user_id) AS orders_per_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 6 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + order_items oi + GROUP BY oi.order_id) -- добавил group by по order_id, теперь это сумма всего заказа, а не строки +SELECT + u.name, + o.id, + o.order_date, + orc.order_revenue, + lag(orc.order_revenue) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS prev_order_revenue +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id; + + +/* Задание 7 */ + +WITH category_revenue_cte AS ( + SELECT + p.category, + date_trunc('day', o.order_date)::date AS order_period, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + products p + LEFT JOIN + order_items oi ON p.id = oi.product_id + INNER JOIN + orders o ON oi.order_id = o.id + GROUP BY 1, 2) +SELECT + cr_cte.*, + sum(cr_cte.daily_category_revenue) OVER ( + PARTITION BY cr_cte.category + ORDER BY cr_cte.order_period + ROWS UNBOUNDED PRECEDING + ) AS running_category_revenue +FROM + category_revenue_cte cr_cte; + \ No newline at end of file diff --git a/pmikhailov/postgresql/joins.sql b/pmikhailov/postgresql/joins.sql new file mode 100644 index 0000000..e83b069 --- /dev/null +++ b/pmikhailov/postgresql/joins.sql @@ -0,0 +1,158 @@ +/* Задача 1 + * Джойним в таком порядке т.к. не каждый юзер делал заказ + * + судя по порядку колонок мы хотим анализировать заказы + * (по сути выглядит так же как INNER) + * Но в обратном порядке можем увидеть кто не делал заказ + * и добавить их в условную рассылку + */ +SELECT + o.id AS order_id, + o.order_date, + status, + u."name" AS user_name, + country +FROM + orders o +LEFT JOIN + users u ON o.user_id = u.id + + +/* Задача 2 */ + +SELECT + order_id, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + order_items oi +LEFT JOIN + products p ON oi.product_id = p.id -- AND oi.order_id = 1 из правой таблицы будут NULL там, где оба условия не выполнены +WHERE order_id = 1 -- в задании другой номер - 101, надо исправить в Notion, данные на п.4 уже другие + + +/* Задача 3 */ + +SELECT + order_id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.user_id = u.id +INNER JOIN + products p ON oi.product_id = p.id; + + +/* Задача 4 */ + +SELECT + u."name" AS user_name, + o.id AS order_id, + status +FROM + users u +LEFT JOIN + orders o ON u.id = o.user_id + + +/* Задача 5 */ + +SELECT + o.user_id, + u."name" AS user_name +FROM + users u +LEFT JOIN + orders o ON u.id = o.user_id +WHERE o.user_id IS NOT NULL + + +/* Задача 6 */ + +SELECT + o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + quantity +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE p."name" ILIKE '%Mi Band 7%'; + + +/* Задача 7 */ + +SELECT + p.name AS product_name, + order_id, + quantity +FROM + products p +FULL JOIN + order_items oi ON p.id = oi.product_id +ORDER BY product_name; + + +/* Задача 8 */ + +SELECT + o.id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.user_id = u.id +LEFT JOIN + products p ON oi.product_id = p.id +WHERE + o.status = 'paid'; + + +/* Задача 9 */ + +SELECT + o.id AS order_id, + o.order_date, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE u."name" ILIKE '%alice%'; + + +/* Задача 10 +* Задача противоречит изначальным органичениям NOT NULL +*/ + +SELECT + o.id AS order_id, + user_id, + status +FROM + orders o +FULL JOIN + order_items oi ON oi.order_id = o.id +WHERE product_id IS NULL AND o.status = 'created'; \ No newline at end of file diff --git a/vberetar/postgresql/ddl.sql b/vberetar/postgresql/ddl.sql new file mode 100644 index 0000000..ce86724 --- /dev/null +++ b/vberetar/postgresql/ddl.sql @@ -0,0 +1,38 @@ +CREATE TABLE users ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age SMALLINT CHECK (age > 0), + email VARCHAR(100) NOT NULL UNIQUE, + country VARCHAR(50), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL +); +-- age - для возраста не нужны болшие числа +-- country - можно было бы использовать VARCHAR(2), я бы тут спросил у тимлида как будет удобней использовать поле) +-- created_at - нужно знать не только дату но и время создания/регистрации +-- id - если не прописать not null в primary key, то not null ставится автоматически? + +CREATE TABLE products ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10, 2) NOT NULL, + in_stock BOOLEAN DEFAULT true, +); + +CREATE TABLE orders( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20), + FOREIGN KEY (user_id) REFERENCES users(id) +); +-- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM + +CREATE TABLE order_items( + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + price_at_purchase DECIMAL(10, 2) NOT NULL, + PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/vberetar/postgresql/dml.sql b/vberetar/postgresql/dml.sql new file mode 100644 index 0000000..24e8d83 --- /dev/null +++ b/vberetar/postgresql/dml.sql @@ -0,0 +1,41 @@ +INSERT INTO users (name, age, email, country, is_active, created_at) +VALUES ('Alice', 25, LOWER('alice@example.com'), 'US', true, '2024-01-10 10:00:00'), + ('Bob', 30, LOWER('bob@example.com'), 'UK', true, '2024-02-05 09:30:00'), + ('Carol', 27, LOWER('carol@example.com'), 'US', false, '2024-03-01 15:20:00'), + ('Diana', 22, LOWER('diana@GMAIL.com'), 'US', true, '2024-01-12 08:15:00'), + ('Evan', 35, LOWER('EVAN@example.com'), 'UK', true, '2024-01-18 19:40:00'), + ('Farida', 29, LOWER('farida@example.com'), 'KZ', true, '2024-02-20 11:05:00'), + ('George', 31, LOWER('george+test@example.com'), 'US', true, '2024-03-05 16:45:00'), + ('Ivan', 26, LOWER('ivan@example.com'), 'RU', true, '2024-01-11 14:30:00'), + ('Julia', 28, LOWER('julia@example.com'), 'US', true, '2024-03-07 18:05:00'); + +INSERT INTO products (name, category, price, in_stock) +VALUES ('iPhone 15', 'Электроника', 80000, true), + ('Kindle', 'Электроника', 15000, true), + ('Кроссовки Nike', 'Одежда', 9000, true), + ('Кроссовки NoName', 'Одежда', 3000, false), + ('Наушники Pro', 'Электроника', 12000, true), + ('Фитнес‑браслет', 'Гаджеты', 6000, true), + ('Подарочная карта', 'Сервисы', 1000, true); + +INSERT INTO orders (user_id, order_date, status) +VALUES (1, '2025-01-10 12:00:00', 'paid'), + (3, '2025-01-12 09:30:00', 'cancelled'), + (1, '2025-01-15 18:20:00', 'paid'), + (2, '2025-01-20 11:05:00', 'created'), + (4, '2025-02-01 17:45:00', 'paid'), + (5, '2025-02-10 10:10:00', 'refunded'), + (7, '2025-03-01 09:00:00', 'paid'), + (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), + (1, 3, 2, 8500), + (2, 2, 1, 14000), + (3, 5, 1, 11000), + (3, 7, 1, 1000), + (4, 2, 1, 15000), + (5, 6, 1, 6000), + (6, 1, 1, 82000), + (7, 3, 1, 9000), + (8, 4, 2, 2800); diff --git a/vberetar/postgresql/sql_aggregation_grouping.sql b/vberetar/postgresql/sql_aggregation_grouping.sql new file mode 100644 index 0000000..6140687 --- /dev/null +++ b/vberetar/postgresql/sql_aggregation_grouping.sql @@ -0,0 +1,105 @@ +-- Задание 1 +SELECT category, COUNT(*) AS products_count +FROM products +GROUP BY category +ORDER BY products_count DESC; + +-- Ответ на вопросы +-- 1. С ORDER BY гораздо удобней читать результат, без него данные отдаются в непонятном порядке. +-- 2. Для категории smartphone добавится +1 значение. + + +-- Задание 2 +SELECT order_id, + SUM(quantity) AS total_quantity, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Если убрать GROUP BY и order_id то запрос выдаст 1 строку со всеми суммами total_quantity и order_revenue. +-- Если убрать только GROUP BY и не убрать order_id то выдаст ошибку. Так как нельзя оставлять неагрегированные столбцы без GROUP BY. +-- 2. Если добавить product_id без добавления её в GROUP BY или агрегат то выдаст ошибку. + + +-- Задание 3 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. По задаче нам же нужно сгруппировать и посчитать сумму по категориям а не по id товарам. +-- 2. Если добавить p.name в SELECT без добавления её в GROUP BY или агрегат то выдаст ошибку. Нельзя вставлять неагрегированные столбцы без GROUP BY. + + +-- Задание 4 +SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY oi.order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. В этом задании мы получили сумму(выручку) по заказам оплаченным, а в прошлом задании мы получили сумму(выручку) по категориям товаров. +-- 2. Посчитать выручку, сколько заработала компания. Остальные заказы еще не оплачены. + + +-- Задание 5 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. WHERE не принимает агрегирующие функции, можно сделать так - WHERE oi.quantity * oi.price_at_purchase > 1000 +-- 2. WHERE фильтрует до GROUP BY, а HAVING после GROUP BY. + + +-- Задание 6 +SELECT country, + COUNT(*) AS active_users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING COUNT(*) > 2 +ORDER BY active_users_count DESC; + +-- Ответ на вопросы +-- 1. Предположу что отбрасывает неактивных пользователей сразу, перед групировкой и агрегаций. В дальнейшем может сэкономить на времени вычисления, особенно если данных будет много. +-- 2. Пустую таблицу без строк. + + +-- Задание 7 +SELECT p.name AS product_name, + SUM(oi.quantity) AS total_quantity_sold +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY product_name +ORDER BY total_quantity_sold DESC +LIMIT 3; + +-- Ответ на вопросы +-- 1. Тут мы выясняем какой товар самый ходовой и сколько его продается, а рейтинг по выручке показывает сколько зарабатывает фирма от продажи товара. +-- 2. В случаях когда, например, нужно выяснить какой товар хорошо продается чтоб его закупить на склад для продажи. + + +-- Задание 8 +SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Есть товары дешевые и люди которые мало зарабатывают, ещё есть всякие скидки на ряд товаров. +-- 2. Я думаю проще увидеть отфильтрованные заказы, чем листать весь список с заказами. diff --git a/vberetar/postgresql/sql_cte.sql b/vberetar/postgresql/sql_cte.sql new file mode 100644 index 0000000..080d9bc --- /dev/null +++ b/vberetar/postgresql/sql_cte.sql @@ -0,0 +1,118 @@ +-- Задача 1 +SELECT u.name +FROM users u +WHERE id IN ( + SELECT o.user_id + FROM orders o +) +ORDER BY u.name; +-- Ответ на вопросы +-- 1. JOIN склеивает одну таблицу со второй, а подзапрос с IN выполняет запрос и получает список с user_id, +-- далее по этому списку отфильтровывает пользователей в основном запросе. +-- 2. Когда запрос простой и короткий. + + +-- Задача 2 +SELECT u.id, + u.name, + u.email +FROM users u +WHERE NOT EXISTS ( + SELECT 1 + FROM orders o + WHERE o.user_id = u.id +); +-- Ответ на вопросы +-- 1. Предположу что NOT EXISTS более бещопасно работает с null. +-- 2. Отправить таким пользователям рекомендацию, специальный промокод на скидку для первого заказа или рекламу. + + +-- Задача 3 +SELECT u.name, + ( + SELECT COUNT(*) + FROM orders o + WHERE u.id = o.user_id + ) AS orders_count +FROM users u +ORDER BY orders_count DESC, name; +-- Ответ на вопросы +-- 1. Если подзапрос SELECT вернётся две и более строки то будет ошибка. +-- 2. Сложность возрастет и скорее всего нужно будет оптимизировать запрос. + + +-- Задача 4 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_price + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + trc.total_price AS total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Проще читать запрос "посчитать выручку по пользователям" когда вынесен отдельно. +-- 2. Вроде как в postgres можно в базу данных записывать функции, если я ничего не путаю. +-- И далее вызывать эту функцию при необходимости, так можно избежать дублирования кода. + + +-- Задача 5 +WITH product_price_at_purchase_cte AS ( + SELECT oi.product_id, + SUM(oi.quantity * oi.price_at_purchase) AS product_revenue + FROM order_items oi + GROUP BY oi.product_id +) +SELECT p.name AS product_name, + p.category, + ppps.product_revenue +FROM products p +JOIN product_price_at_purchase_cte ppps ON ppps.product_id = p.id +ORDER BY ppps.product_revenue DESC +LIMIT 3; +-- Ответ на вопросы +-- 1. Возможно потому что products оcновная таблица, сналчала нужно из order_items достать агрегацию по id а потом склеивать с products. +-- 2. Через CTE мы получаем готовую таблицу и далее делаем с ней что хотим, +-- а в гуруппировке через GROUP BY нельзя добавить дополнительные поля name и category. + + +-- Задача 6 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + u.email, + trc.total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +WHERE u.is_active = TRUE AND trc.total_revenue > 1000 +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Думаю как то так - user_most_expenses +-- 2. Можно предлагать рекламу товаров, анализировать что пользователи покупали и потом предлагать схожую продукцию. + + +WITH order_revenue_cte AS ( + SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue + FROM order_items + GROUP BY order_id +) +SELECT o.id, + o.order_date, + oc.order_revenue +FROM orders o +JOIN order_revenue_cte oc ON oc.order_id = o.id +WHERE o.status = 'paid' AND oc.order_revenue < 100; +-- Ответ на вопросы +-- 1. Какие реальные причины таких заказов, я думаю что просто не все люди богатые что бы заказывать на большие суммы (если я правильно понял статус paid) +-- Чтобы узнать злоупотребления промокодами нужно копаться в других данных, где хранятся данные о введенных промокодах. +-- 2. Я думаю выгоднее фильтровать внутри подзапроса, чтоб подзапрос не создавал большую таблицу и основному запросу не пришлось ее перебирать. diff --git a/vberetar/postgresql/sql_dates_strings_logic.sql b/vberetar/postgresql/sql_dates_strings_logic.sql new file mode 100644 index 0000000..447954a --- /dev/null +++ b/vberetar/postgresql/sql_dates_strings_logic.sql @@ -0,0 +1,90 @@ +-- 1 Задача +SELECT name, + CAST(created_at AS date) AS created_date, + CURRENT_DATE - CAST(created_at AS date) AS days_since_registration +FROM users +WHERE created_at <= CURRENT_DATE - INTERVAL '180 days' +ORDER BY days_since_registration DESC; +-- Ответ на вопросы +-- 1. Возможно предложить старым пользователям скидки всякие или интересное предложение для удержания пользователям. Или можно проанализировать их заказы. +-- 2. Я предположу что удобней не марочится со временем а сразу работать с датой + + +-- 2 Задача +SELECT name, + LOWER(email) AS email_clean, + SUBSTRING(LOWER(email) FROM POSITION('@' IN LOWER(email)) + 1) AS domain +FROM users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- Ответ на вопросы +-- 1. Приводим email к нижнему регистру для чистоты данных, +-- а зачем нужен отдельно домен не знаю, может есть какая нибудь важная разница в почтовых сервисах. +-- 2. Получим в колонке domain null. + + +-- 3 Задача +SELECT name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + ELSE 'unknown' + END AS age_group +FROM users +ORDER BY age; +-- Ответ на вопросы +-- 1. Возможно данные и статистику быстрее и удобнее получить через sql а не через код приложения. +-- 2. Пользователи получат null в колонке, оно просто не пройдет условие. + + +-- 4 Задача +SELECT name, + created_at::date AS created_date, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN is_active = TRUE THEN 'hot' + ELSE 'regular' + END AS hot_flag +FROM users +WHERE is_active = TRUE + AND created_at >= CURRENT_DATE - INTERVAL '30 days'; +-- Ответ на вопросы +-- 1. В WHERE мы получим только пользователей за последние 30 дней а в CASE мы просто получаем метку 'hot' или 'regular'. +-- И в CASE мы не фильтруем по каким либо параметрам а только прописываем условие что записать. +-- 2. Какие нибудь рекомендации или скидку на первый заказ. + + +-- 5 Задача +SELECT COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) - COUNT(email) AS without_email, + ROUND(COUNT(country) * 100.0 / COUNT(*), 2) AS percentage_users +FROM users; +-- Ответ на вопросы +-- 1. Так гораздо надежнее и правильнее. +-- 2. Установить ограничение на создание пользователя, чтоб поле email было не нулевым. + + +-- 6 Задача +SELECT name, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN created_at::date >= CURRENT_DATE - INTERVAL '30 days' THEN 'new' + WHEN created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days' THEN 'middle' + ELSE 'old' + END AS tenure_group, + is_active +FROM users +ORDER BY created_at::date >= CURRENT_DATE - INTERVAL '30 days', + created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days', + created_at::date < CURRENT_DATE - INTERVAL '180 days', + days_since_registration; +-- Ответ на вопросы +-- 1. В сегментации по возрасту показывает возраст пользователя, +-- а в сегментации стаж показывает на сколько долго пользователь пользуется сервисом(приложением) +-- 2. CURRENT_DATE показывает сегодняшний день и если tenure_group пользоваться каждый день, то будет смещатся интервал в логическом выражении CASE. +-- В статическом поле дание остаются записанными пока их не изменят, течение времени на них никак не будет влиять. diff --git a/vberetar/postgresql/sql_filters.sql b/vberetar/postgresql/sql_filters.sql new file mode 100644 index 0000000..74925dd --- /dev/null +++ b/vberetar/postgresql/sql_filters.sql @@ -0,0 +1,56 @@ +-- Задача 1 +SELECT name, category, price, + price * 1.3 AS price_with_markup, + price * 1.5 AS price_with_vat +FROM products +WHERE (price BETWEEN 100 AND 1300) + AND category NOT IN ('smartwatch') + AND in_stock = TRUE; + +-- Вопрос: +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Ответ: +-- Для меня BETWEEN 100 AND 1300 читается лучше, сразу понятен срез от 100 и 1300. + + +-- Задача 2 +SELECT name, price, + price * 0.85 AS price_with_discount +FROM products +WHERE (price < 100 OR price > 1000); + +-- Вопрос: +-- Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно? +-- Ответ: +-- Потому что условие может не правильно отобразить результат, AND выполняется раньше OR. +-- Для надежности лучше скобками прописывать явно. + + +-- Задача 3 +SELECT id, name, category, price +FROM products +WHERE price >= 100 + AND category IN ('tablet', 'smartphone', 'mouse') +ORDER BY price DESC; + + +-- Задача 4 +SELECT name, category, price +FROM products +WHERE name NOT LIKE '%iPhone%'; + + +-- Задача 5 +SELECT name, category, price +FROM products +WHERE name ILIKE '%pro%'; + +SELECT name, category, price +FROM products +WHERE name ILIKE '%mx%' +ORDER BY price DESC; + +-- Вопрос: +-- В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'. +-- Ответ: +-- В названии товаров могут быть буквы (в словах) с разными регистрами, при поиске клиенту можен не показать часть товаров только потому что слово выглядить так - PRO а не так - Pro. diff --git a/vberetar/postgresql/sql_join.sql b/vberetar/postgresql/sql_join.sql new file mode 100644 index 0000000..e2b7c55 --- /dev/null +++ b/vberetar/postgresql/sql_join.sql @@ -0,0 +1,144 @@ +-- Задание 1 +SELECT o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM orders o +JOIN users u ON o.user_id = u.id; + +-- Ответ на вопросы +-- 1. Логичнее начать c orders, так как нам в первую очередь нужны все заказы, +-- а потом только пользователи которые их сделали. + +-- 2. Я думаю уместнее сделать LEFT. +-- Я пробовал сделать с INNER и LEFT и у меня не пропали строки. +-- Получается это из за того что у каждого заказа есть пользователь, не дуаю что должен создастся заказ без пользователя. + + +-- Задание 2 +SELECT o.id AS order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE o.id = 101; + +-- Ответ на вопрос +-- Выдаст дублированный состав заказа с id 101, +-- получается он будет обращаться к каждаму заказу, но отображать продукты заказа с id 101 + + +-- Задание 3 +SELECT o.id AS order_id, + u.name AS name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id; + +-- Ответ на вопрос +-- 1. Появляется дубликат заказов потому что у 3 заказа есть несколько товаров, join превращает(размножает) их в несколько строк. +-- 2. Отличается тем что мы выводим все заказы с товарами а не один заказ. + + +-- Задание 4 +SELECT u.name AS user_name, + o.id AS order_id, + o.status +FROM users u +LEFT JOIN orders o ON o.user_id = u.id; + +-- Ответ на вопрос +-- INNER JOIN не выдаст пользователей без заказа. Только тех у кого есть пара с orders. + + +-- Задание 5 +SELECT u.id AS user_id, + u.name AS user_name +FROM users u +LEFT JOIN orders o ON o.user_id = u.id +WHERE o.id IS NULL; + +-- Ответ на вопрос +-- 1. Проверять IS NULL нужно по полю orders.id +-- 2. Мы должны в запросе получить всех пользователей, а потом в фильтре "узнать" есть ли у них заказы. + + +-- Задание 6 +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE p.name ILIKE '%kindle%'; + +-- Ответ на вопрос +-- В заказе 2 есть только один товар с названием Kindle. + + +-- Задание 7 +SELECT p.name AS product_name, + oi.order_id, + oi.quantity +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id; + +-- Ответ на вопрос +-- 1. order_id будет NULL +-- 2. Если использовать 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 products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE o.status = 'paid'; + +-- Ответ на вопрос +-- У меня получился тот же результат если написать вот так - JOIN orders o ON o.id = oi.order_id and o.status = 'paid' + + +-- Задание 9 +SELECT o.id AS order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE u.name = 'Alice'; + +-- Ответ на вопрос +-- Если у пользователя нет ни одного заказа, то этот запрос вернет пустую строку. +-- Не вернет потому что нечего отобразить таблице, если нет заказов то и нет купленных товаров. +-- Но если прописать RIGHT JOIN users u ON u.id = o.user_id то отобразит все строки NULL. + + +-- Задание 10 +SELECT o.id AS order_id, + o.user_id, + o.status +FROM orders o +LEFT JOIN order_items oi ON oi.order_id = o.id +WHERE oi.order_id IS NULL; + +-- Ответ на вопрос +-- Такой запрос выдаст пустые строки. +-- INNER JOIN должен выдать строки только если есть "пара". diff --git a/vberetar/postgresql/sql_query.sql b/vberetar/postgresql/sql_query.sql new file mode 100644 index 0000000..a0681ba --- /dev/null +++ b/vberetar/postgresql/sql_query.sql @@ -0,0 +1,73 @@ +-- Задание 1 +SELECT id, name, country, created_at, email +FROM users +ORDER BY created_at DESC, name; + +-- Почему выбрал именно эти столбцы и зачем такая сортировка? +-- Ответ на вопрос: +-- Эти столбцы нужны для анализа пользователей, для тех же аналитиков. +-- Сортировка по дате и имени, я так предпологаю, нужня для удобства. + + +-- Задание 2 +-- Запрос 1 +SELECT id, name, country +FROM users +WHERE country = 'US' +ORDER BY name; + +-- Запрос 2 +SELECT id, name, country +FROM users +WHERE country = 'UK' +ORDER BY name; + +-- Какой из двух запросов маркетологу полезнее и почему? +-- Ответ на вопрос: +-- Я так понимаю маркетологу нужно понять какое кол-во пользователей в каждой из стран зарегестрировалось в приложении. +-- Возможно для понимания продвижения продукта и насколько успешна маркетинговая стратегия. И в дальнейшем её корректировка. + + +-- Задание 3 +-- Запрос 1 +SELECT id, name, email +FROM users +WHERE email IS NOT NULL -- я знаю что email и так должен быть NOT NULL по DDL, но на всякий прописал +ORDER BY name; + +-- Запрос 2 +SELECT DISTINCT LOWER(email) as email_lower +FROM users +WHERE email IS NOT NULL; + +-- Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? +-- Ответ на вопрос: +-- Приводить email к нижнему регистру необходимо для того чтобы небыло дубликатов почты. +-- После гугления я узнал что в почтовых адресах могут допускаться верхние регистры букв, только потом создать email с теми же буквами но в нижнем регистре нельзя будет, они будут считаться индентичными. +-- Получается несколько пользователей могут залогинится под одним и тем же email адресом но с разными регистрами. А такое нельзя допускать. +-- Для рассылки дубликаты нужно убирать что-бы не приходила рассылка дважды в один и тот же email. + +-- Задание 4 +-- Запрос 1 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC; + +-- Запрос 2 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +LIMIT 3; + +-- Запрос 3 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +OFFSET 1 LIMIT 2; + +-- В каком реальном сценарии тебе пригодится OFFSET? +-- Ответ на вопрос: +-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка. diff --git a/vberetar/postgresql/sql_union.sql b/vberetar/postgresql/sql_union.sql new file mode 100644 index 0000000..b71e744 --- /dev/null +++ b/vberetar/postgresql/sql_union.sql @@ -0,0 +1,49 @@ +-- Задание 1 +SELECT id, name, 'active' AS user_status +FROM users +WHERE is_active = TRUE +UNION ALL +SELECT id, name, 'inactive' AS user_status +FROM users +WHERE is_active = FALSE +ORDER BY name; + +-- Ответ на вопрос +-- 1. Количество строк не изменилось +-- 2. Предположу, что подача информации в таком виде более информативней(понятней). + + +-- Задание 2 +SELECT id AS order_id, user_id, order_date, 'old' AS period +FROM orders +WHERE order_date < '2024-01-01' +UNION ALL +SELECT id AS order_id, user_id, order_date, 'new' AS period +FROM orders +WHERE order_date >= '2024-01-01' +ORDER BY order_date, order_id; + +-- Ответ на вопрос +-- 1. В dbeaver в самой левой колонке отображается номерация строк, а снизу общее количество строк который выдал запрос. +-- На последней строке с old можно увидеть число в самой левой колонке, это и есть количество строк c old. Далее отнимаем общее кол-во строк от old и получаем кол-во new. +-- 2. Тяжело ответить на такой вопрос, не знаю. Если у меня были бы сомнения, я бы у новичка спросил. + + +-- Задание 3 +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM products p +JOIN order_items oi ON oi.product_id = p.id +UNION ALL +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id +WHERE oi.product_id IS NULL +ORDER BY product_name; + +-- Ответ на вопрос +-- 1. Нам нужно чтобы товар(products) ни разу не заказывали(order_items), получается что мы проверяем есть ли ссылка в каком нибудь заказе на продукт. +-- 2. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь. diff --git a/vberetar/postgresql/sql_view.sql b/vberetar/postgresql/sql_view.sql new file mode 100644 index 0000000..333c660 --- /dev/null +++ b/vberetar/postgresql/sql_view.sql @@ -0,0 +1,69 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW my_category_revenue AS + SELECT + p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + JOIN products p ON p.id = oi.product_id + WHERE o.status = 'paid' + GROUP BY p.category; + +SELECT * FROM my_category_revenue +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Не нужно писать длинные запросы и результат будет уже подсчитан. + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS + SELECT + u.id AS user_id, + u.name AS user_name, + COUNT(o.user_id) AS orders_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' + GROUP BY u.id, u.name; + +SELECT * FROM mv_user_activity +WHERE orders_count > 0 +ORDER BY orders_count DESC; +-- Ответ на вопросы +-- 1. Я так понял, что если критерий "самых лояльных" клиентов это колличество оплаченных заказов, +-- то "самые лояльные" это те у кого этих заказов больше всего. +-- 2. Покажет "самых лояльных" клиентов за последный месяц, кто-то из клиетов может сделать много заказов за 1-2 месяца, а кто то за год по чуть чуть заказывает. +-- Возможно еще какие то из клиентов ориентируются на "сезонные" товары и у них будет сплеск продажи за последний месяц, или кто то из клиентов покупал много за год, а в этом месяце перестал делать заказы. + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS + SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * price_at_purchase) AS daily_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY o.order_date::date; + +SELECT * FROM mv_daily_revenue +ORDER BY sales_date DESC; +-- Ответ на вопросы +-- 1. «Просадки» и «пики» будут видны в колонке daily_revenue +-- 2. Быстрее считывать с готовой витрины mv_daily_revenuе, на лету из сырых таблиц дольше будет обрабатываться запрос. + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- Расписание интернет‑магазина с несколькими заказами в день: +-- Для такого сценария подойдет обновление витрины раз в неделю по ночам. +-- Так как заказов не так уж и много, можно обновлять статискиту чаще. +-- Ночью, потому что заказов будет меньше в то время суток и бд быстрее обработает ззапрос. + +-- Расписание крупного магазина с тысячами заказов в час: +-- Для такого сценария подойдет обновление витрин раз в месяц - раз в квартал по ночам, +-- Так как данных много и частые расчеты будут сильно нагружать базу данных. +-- Возможно, нужно будет получить данные аналитикам раньше указанных сроков. + +-- Ответ на вопросы +-- 1. Данные будут устаревшими, из за того результат работы аналитиков будут не достоверными. +-- 2. Слишком частые обновления витрит будут сильно нагружать БД. diff --git a/ygurbanberdiev/clickhouse/Pet_Project/Pet_project_clickhouse.sql b/ygurbanberdiev/clickhouse/Pet_Project/Pet_project_clickhouse.sql new file mode 100644 index 0000000..4ea67ca --- /dev/null +++ b/ygurbanberdiev/clickhouse/Pet_Project/Pet_project_clickhouse.sql @@ -0,0 +1,287 @@ +-- Clickhouse. Пет-проект: Витринный слой +CREATE TABLE core_dim_user +ON CLUSTER clickhouse_cluster ( + user_id UInt32, + name String, + age UInt8, + email String, + registration_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_dim_user', + '{replica}' +) +ORDER BY user_id; + + +CREATE TABLE core_dim_course +ON CLUSTER clickhouse_cluster ( + course_id UInt32, + title String, + category String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_dim_course', + '{replica}' +) +ORDER BY course_id; + + +CREATE TABLE core_dim_lesson +ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + title String, + duration_min UInt8, + course_id UInt32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_dim_lesson', + '{replica}' +) +ORDER BY (course_id, lesson_id); + + +CREATE TABLE core_dim_date +ON CLUSTER clickhouse_cluster ( + date Date, + year UInt16, + month UInt8, + weekday String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_dim_date', + '{replica}' +) +ORDER BY date; + + +CREATE TABLE core_fact_lesson_views +ON CLUSTER clickhouse_cluster ( + user_id UInt32, + lesson_id UInt32, + course_id UInt32, + viewed_at DateTime64(0) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_fact_lesson_views', + '{replica}' +) +ORDER BY (user_id, course_id, viewed_at, lesson_id); + + +CREATE TABLE core_enrollments +ON CLUSTER clickhouse_cluster ( + user_id UInt32, + course_id UInt32, + enrolled_at DateTime64(0) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/core_enrollments', + '{replica}' +) +ORDER BY (user_id, course_id); + + + +-- Этап 2. Витрина lesson_popularity_summary +CREATE TABLE lesson_popularity_summary +ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + lesson_title String, + course_id UInt32, + course_title String, + total_views UInt64, + unique_users UInt64, + first_view DateTime64(0), + last_view DateTime64(0) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/lesson_popularity_summary', + '{replica}' +) +ORDER BY (lesson_id, course_id); + +CREATE MATERIALIZED VIEW mv_lesson_popularity_summary +ON CLUSTER clickhouse_cluster +TO lesson_popularity_summary AS +WITH agg_table AS ( +SELECT + lesson_id, + course_id, + count() AS total_views, + uniqExact(user_id) AS unique_users, + min(viewed_at) AS first_view, + max(viewed_at) AS last_view +FROM core_fact_lesson_views +GROUP BY lesson_id, course_id +) +SELECT + atab.lesson_id AS lesson_id, + cdl.title AS lesson_title, + atab.course_id AS course_id, + cdc.title AS course_title, + atab.total_views AS total_views, + atab.unique_users AS unique_users, + atab.first_view AS first_view, + atab.last_view AS last_view +FROM agg_table AS atab +JOIN core_dim_lesson AS cdl ON atab.lesson_id = cdl.lesson_id +JOIN core_dim_course AS cdc ON atab.course_id = cdc.course_id; + + +-- Этап 2. Витрина inactive_users_summary +CREATE TABLE inactive_users_summary +ON CLUSTER clickhouse_cluster ( + user_id UInt64, + user_name String, + email String, + age UInt8, + registered_courses_count UInt64, + registration_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + + +CREATE MATERIALIZED VIEW mv_inactive_users_summary +ON CLUSTER clickhouse_cluster +TO inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS user_name, + u.email AS email, + u.age AS age, + COALESCE(e.registered_courses_count, 0) AS registered_courses_count, + u.registration_date AS registration_date +FROM core_dim_user AS u +LEFT JOIN +( SELECT + user_id, + count() AS registered_courses_count + FROM core_enrollments + GROUP BY user_id +) AS e ON u.user_id = e.user_id +LEFT JOIN +( SELECT DISTINCT + user_id + FROM core_fact_lesson_views +) AS v ON u.user_id = v.user_id +WHERE v.user_id IS NULL; + + + +-- Этап 2. Витрина course_completion_rate +CREATE TABLE course_completion_rate +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + user_name String, + course_id UInt32, + course_title String, + lessons_in_course UInt64, + lessons_viewed UInt64, + completion_rate Float64 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/ygurbanberdiev/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +CREATE MATERIALIZED VIEW mv_course_completion_rate +ON CLUSTER clickhouse_cluster +TO course_completion_rate +AS +SELECT + e.user_id AS user_id, + u.name AS user_name, + e.course_id AS course_id, + c.title AS course_title, + COALESCE(lc.lessons_in_course, 0) AS lessons_in_course, + COALESCE(lv.lessons_viewed, 0) AS lessons_viewed, + if(COALESCE(lc.lessons_in_course, 0) = 0,0, round(COALESCE(lv.lessons_viewed, 0) * 1.0 / lc.lessons_in_course, 2)) AS completion_rate +FROM core_enrollments AS e +LEFT JOIN core_dim_user AS u ON e.user_id = u.user_id +LEFT JOIN core_dim_course AS c ON e.course_id = c.course_id +LEFT JOIN +( SELECT + course_id, + count() AS lessons_in_course + FROM core_dim_lesson + GROUP BY course_id +) AS lc ON e.course_id = lc.course_id +LEFT JOIN +( SELECT + user_id, + course_id, + uniqExact(lesson_id) AS lessons_viewed + FROM core_fact_lesson_views + GROUP BY user_id, course_id +) AS lv ON e.user_id = lv.user_id + AND e.course_id = lv.course_id; + + + +-- Этап 3. Уроки которые почти никто не смотрел +SELECT + lesson_title, + course_title, + total_views, + unique_users +FROM lesson_popularity_summary +WHERE total_views <= 1 +ORDER BY total_views ASC; + +-- Неактивные пользователи без записей на курсы +SELECT + user_name, + email, + age, + registration_date +FROM inactive_users_summary +WHERE registered_courses_count = 0 +ORDER BY registration_date DESC; + +-- Пользователи, полностью завершившие курс +SELECT + user_name, + course_title, + lessons_in_course, + lessons_viewed, + completion_rate +FROM course_completion_rate +WHERE completion_rate = 1 +ORDER BY course_title, user_name; + +/* Отчет 1: При сравнении витрины lesson_popularity_summary в ClickHouse и Greenplum результаты совпали только частично +Совпали показатели: +SUM(total_views) = 58 +MAX(total_views) = 9 + +Не совпали показатели: +AVG(total_views) +MIN(total_views) + +В Clickhouse: +AVG(total_views) = 3.2222 +MIN(total_views) = 0 +В Greenplum: +AVG(total_views) = 3.8666 +MIN(total_views) = 1 + +Причина расхождения: в ClickHouse в витрине есть дополнительные строки с total_views = 0, а в Greenplum таких строк нет. Из-за этих нулевых строк минимальное значение в ClickHouse стало 0, а среднее значение уменьшилось + + +Отчет 2: При проверке витрины inactive_users_summary результаты в ClickHouse и Greenplum совпали полностью. +В обеих системах получены одинаковые значения: +inactive_users_count = 9 +sum_registered_courses = 17 +avg_registered_courses = 1.8888888889 +min_registered_courses = 1 +max_registered_courses = 3 + +*/ \ No newline at end of file