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/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_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_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? +-- Ответ на вопрос: +-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка.