diff --git a/aharlamcev/PET-project_task3 b/aharlamcev/PET-project_task3 new file mode 100644 index 0000000..0de3482 --- /dev/null +++ b/aharlamcev/PET-project_task3 @@ -0,0 +1,26 @@ +CREATE MATERIALIZED VIEW platform_summary AS +with users_counts as ( + select + COUNT(distinct u.id) as total_users, + COUNT(distinct lv.user_id) as users_with_views + from users u + left join lesson_views lv on lv.user_id = u.id + ), +courses_lessons_count as ( + select COUNT(distinct c.id) as total_courses, + COUNT(distinct l.id) as total_lessons + from courses c + left join lessons l on l.course_id = c.id + ), +views_count as ( + select COUNT(distinct lv.lesson_id) as lessons_with_views, + COUNT(distinct lv.id) as total_views + from lesson_views lv + ) +select uc.total_users, clc.total_courses, clc.total_lessons, uc.users_with_views, +ROUND(1.00 * clc.total_lessons / nullif(clc.total_courses, 0), 2) as avg_lessons_per_course, +ROUND(1.00 * vc.total_views / nullif(clc.total_lessons, 0), 2) as avg_views_per_lesson +from users_counts uc +cross join courses_lessons_count clc +cross join views_count vc +; \ No newline at end of file diff --git a/aharlamcev/PET-project_task4 b/aharlamcev/PET-project_task4 new file mode 100644 index 0000000..d25c088 --- /dev/null +++ b/aharlamcev/PET-project_task4 @@ -0,0 +1,28 @@ +CREATE MATERIALIZED VIEW users_cohort_analysis AS +with needed_users as ( + select distinct u.id as user_id, + u.registration_date as reg_date, e.course_id, lv.id as view_id, + lv.lesson_id as viewed_lesson_id, + FIRST_VALUE(lv.viewed_at) over (partition by lv.user_id order by lv.viewed_at) as first_view_date + from users u + left join enrollments e on u.id = e.user_id + left join lesson_views lv on e.user_id = lv.user_id + order by user_id + ), +CTE as ( + select + TO_CHAR(reg_date, 'YYYY-MM') as cohort_month, + COUNT(distinct user_id) as users_in_cohort, + COUNT(distinct case when user_id in (select user_id from enrollments) then user_id end) as enrolled_users, + COUNT(distinct case when user_id in (select user_id from lesson_views) then user_id end) as active_users, + COUNT(course_id) as courses_count, + extract('days' from AVG(first_view_date - reg_date)) as avg_time_to_first_view + from needed_users + group by cohort_month + ) +select cohort_month, users_in_cohort, enrolled_users, active_users, +ROUND(100.0 * active_users / users_in_cohort, 2) as retention_rate, +courses_count::numeric / users_in_cohort as avg_courses_per_user, +avg_time_to_first_view +from CTE +; \ No newline at end of file diff --git a/aharlamcev/postgresql/Pet-projectsql.sql b/aharlamcev/postgresql/Pet-projectsql.sql new file mode 100644 index 0000000..7ad986c --- /dev/null +++ b/aharlamcev/postgresql/Pet-projectsql.sql @@ -0,0 +1,466 @@ +create table users ( +id INT primary key, +name VARCHAR(50) NOT NULL, +age INT, +email VARCHAR(50) NOT NULL, +registration_date date +); + +INSERT INTO users (id, name, age, email, registration_date) +VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), +(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), +(13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), +(18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), +(20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +create table courses ( +id INT primary key, +title VARCHAR(100), +category VARCHAR(50), +created_at date +); + +INSERT INTO courses (id, title, category, created_at) +VALUES +(1, 'SQL для начинающих', 'data', '2023-01-01'), +(2, 'Python для анализа данных', 'programming', '2023-01-15'), +(3, 'BI с нуля', 'business', '2023-03-01'), +(4, 'Excel для аналитиков', 'business', '2023-02-01'), +(5, 'Основы машинного обучения', 'data', '2023-03-15'); + + +create table lessons ( +id INT primary key, +course_id INT, +title VARCHAR(100), +duration_min INT, +foreign key (course_id) references courses(id) +); + +INSERT INTO lessons (id, course_id, title, duration_min) +VALUES +(1, 1, 'SELECT и FROM', 10), +(2, 1, 'JOIN', 15), +(3, 1, 'WHERE и фильтрация', 12), +(4, 1, 'Агрегации и GROUP BY', 18), +(5, 1, 'Подзапросы', 25), +(6, 2, 'Pandas основы', 20), +(7, 2, 'NumPy', 15), +(8, 2, 'Визуализация данных', 22), +(9, 3, 'Основы BI', 12), +(10, 3, 'Метрики и дашборды', 18), +(11, 3, 'Power BI введение', 20), +(12, 4, 'Формулы в Excel', 15), +(13, 4, 'Сводные таблицы', 18), +(14, 4, 'Графики и диаграммы', 14), +(15, 5, 'Введение в ML', 20), +(16, 5, 'Линейная регрессия', 30), +(17, 5, 'Классификация', 28), +(18, 5, 'Кластеризация', 25); + +create table enrollments ( +id INT primary key, +user_id INT, +course_id INT, +enrolled_at date, +foreign key (user_id) references users(id), +foreign key (course_id) references courses(id) +); + +INSERT INTO enrollments (id, user_id, course_id, enrolled_at) +VALUES +(1, 1, 1, '2023-01-15'), +(2, 1, 2, '2023-02-01'), +(3, 2, 1, '2023-01-20'), +(4, 3, 2, '2023-03-05'), +(5, 4, 3, '2023-04-01'), +(6, 6, 1, '2023-01-06'), +(7, 6, 2, '2023-01-10'), +(8, 6, 4, '2023-02-05'), +(9, 7, 1, '2023-01-13'), +(10, 7, 3, '2023-03-10'), +(11, 8, 1, '2023-01-22'), +(12, 8, 2, '2023-01-22'), +(13, 8, 3, '2023-01-22'), +(14, 9, 4, '2023-02-12'), +(15, 10, 1, '2023-04-16'), +(16, 11, 2, '2023-04-21'), +(17, 12, 1, '2023-01-16'), +(18, 13, 2, '2023-01-19'), +(19, 13, 4, '2023-02-20'), +(20, 14, 1, '2023-01-09'), +(21, 14, 2, '2023-01-09'), +(22, 14, 3, '2023-01-09'), +(23, 14, 4, '2023-02-01'), +(24, 14, 5, '2023-03-20'), +(25, 15, 1, '2023-01-11'), +(26, 15, 3, '2023-03-05'), +(27, 16, 2, '2023-02-16'), +(28, 16, 4, '2023-02-16'), +(29, 17, 3, '2023-03-22'), +(30, 17, 5, '2023-03-25'), +(31, 18, 1, '2023-04-02'), +(32, 19, 1, '2023-04-11'), +(33, 19, 2, '2023-04-11'), +(34, 20, 2, '2023-04-19'), +(35, 5, 5, '2023-03-20'), +(36, 1, 4, '2023-02-10'), +(37, 2, 3, '2023-03-15'), +(38, 3, 1, '2023-02-25'), +(39, 4, 2, '2023-03-10'), +(40, 6, 3, '2023-03-08'), +(41, 7, 2, '2023-02-01'), +(42, 9, 1, '2023-02-15'), +(43, 12, 3, '2023-03-10'), +(44, 15, 2, '2023-02-01'), +(45, 18, 4, '2023-04-05'); + +create table lesson_views ( +id INT primary key, +user_id INT, +lesson_id INT, +viewed_at timestamp, +foreign key (user_id) references users(id), +foreign key (lesson_id) references lessons(id) +); + +INSERT INTO lesson_views (id, user_id, lesson_id, viewed_at) +VALUES +(1, 1, 1, '2023-01-16 10:00:00'), +(2, 1, 2, '2023-01-16 10:15:00'), +(3, 1, 3, '2023-01-17 09:00:00'), +(4, 1, 4, '2023-01-17 09:30:00'), +(5, 1, 4, '2023-01-17 11:00:00'), +(6, 1, 4, '2023-01-18 10:00:00'), +(7, 1, 6, '2023-02-02 14:00:00'), +(8, 1, 12, '2023-02-15 10:00:00'), +(9, 1, 13, '2023-02-15 10:30:00'), +(10, 2, 1, '2023-01-21 09:00:00'), +(11, 2, 9, '2023-03-16 10:00:00'), +(12, 2, 10, '2023-03-16 10:30:00'), +(13, 3, 6, '2023-03-06 12:00:00'), +(14, 3, 1, '2023-02-26 11:00:00'), +(15, 3, 2, '2023-02-26 11:20:00'), +(16, 4, 9, '2023-04-02 10:00:00'), +(17, 4, 10, '2023-04-03 10:00:00'), +(18, 4, 6, '2023-03-12 14:00:00'), +(19, 4, 7, '2023-03-13 09:00:00'), +(20, 5, 15, '2023-03-21 10:00:00'), +(21, 5, 16, '2023-03-22 11:00:00'), +(22, 6, 1, '2023-01-07 10:00:00'), +(23, 6, 2, '2023-01-07 10:15:00'), +(24, 6, 3, '2023-01-08 09:00:00'), +(25, 6, 4, '2023-01-08 09:30:00'), +(26, 6, 5, '2023-01-09 10:00:00'), +(27, 6, 6, '2023-01-11 14:00:00'), +(28, 6, 7, '2023-01-12 10:00:00'), +(29, 6, 8, '2023-01-13 11:00:00'), +(30, 6, 12, '2023-02-06 09:00:00'), +(31, 6, 13, '2023-02-06 09:30:00'), +(32, 6, 14, '2023-02-07 10:00:00'), +(33, 6, 9, '2023-03-09 11:00:00'), +(34, 7, 1, '2023-01-14 10:00:00'), +(35, 7, 2, '2023-01-14 10:20:00'), +(36, 7, 3, '2023-01-15 09:00:00'), +(37, 7, 9, '2023-03-11 14:00:00'), +(38, 7, 10, '2023-03-11 14:30:00'), +(39, 9, 12, '2023-02-13 10:00:00'), +(40, 9, 13, '2023-02-13 10:30:00'), +(41, 9, 1, '2023-02-16 11:00:00'), +(42, 12, 1, '2023-01-17 10:00:00'), +(43, 12, 2, '2023-01-17 10:20:00'), +(44, 12, 1, '2023-04-10 11:00:00'), +(45, 12, 3, '2023-04-10 11:30:00'), +(46, 12, 9, '2023-03-12 10:00:00'), +(47, 13, 6, '2023-01-20 14:00:00'), +(48, 13, 7, '2023-01-21 10:00:00'), +(49, 13, 12, '2023-02-21 09:00:00'), +(50, 14, 1, '2023-01-09 08:00:00'), +(51, 14, 2, '2023-01-09 08:15:00'), +(52, 14, 3, '2023-01-10 10:00:00'), +(53, 14, 4, '2023-01-10 10:30:00'), +(54, 14, 5, '2023-01-11 09:00:00'), +(55, 14, 6, '2023-01-10 14:00:00'), +(56, 14, 7, '2023-01-11 10:00:00'), +(57, 14, 8, '2023-01-12 11:00:00'), +(58, 14, 9, '2023-01-10 08:00:00'); + + +--------------------------------------------------------- +-- 1 задание Активные пользователи + +create materialized view user_activity_summary AS +with enroll_count as ( + select u.id as user_id, u.name as user_name, + COUNT(e.course_id) as enrollment_count + from users u + join enrollments e on u.id = e.user_id + group by u.id, u.name + ), +viewed_count as ( + select ec.user_id, ec.user_name, enrollment_count, + COUNT(distinct lv.lesson_id) as viewed_lessons_count, + MIN(lv.viewed_at) as first_view, + MAX(lv.viewed_at) as last_view + from enroll_count ec + left join lesson_views lv on ec.user_id = lv.user_id + group by ec.user_id, ec.user_name, enrollment_count + ), +possible_lessons as ( + select u.id as user_id, COUNT(l.id) as poss_lessons + from users u + left join enrollments e on u.id = e.user_id + left join lessons l on e.course_id = l.course_id + group by u.id + ) +select vc.user_id, user_name, enrollment_count, viewed_lessons_count, first_view, last_view, +ROUND(100.0 * viewed_lessons_count / pl.poss_lessons, 2) as engagement_rate +from viewed_count vc +left join possible_lessons pl on vc.user_id = pl.user_id +; + +------------------------------------------------------------------------------------------------------------------ +-- 2 задание Эффективность курсов +create materialized view course_summary AS + with lessons_per_course as ( + select c.id as course_id, c.title as course_title, + COUNT(distinct e.user_id) as unique_users, + COUNT(distinct l.id) as lessons_count + from courses c + left join enrollments e on c.id = e.course_id + join lessons l on e.course_id = l.course_id + group by c.id, c.title + ), + course_views as ( + select l.course_id, + COUNT(lv.id) as total_views, + MIN(lv.viewed_at) as first_viewed_at, + MAX(lv.viewed_at) as last_viewed_at + from lesson_views lv + join lessons l on lv.lesson_id = l.id + group by l.course_id + ) + select lpc.course_id, lpc.course_title, lpc.unique_users, lpc.lessons_count, + ROUND(1.00 * cv.total_views / lpc.unique_users, 2) as avg_views_per_user, + cv.first_viewed_at, + cv.last_viewed_at + from lessons_per_course lpc + join course_views cv on lpc.course_id = cv.course_id + order by course_id +; + +-------------------------------------------------------------------------------------------------- +-- 3 задание Общая отчетность по взаимодействию +create materialized view platform_summary as +with total_counts as ( + select COUNT(id) as total_users, + (select COUNT(id) from courses) as total_courses, + (select COUNT(id) from lessons) as total_lessons, + (select COUNT(id) from lesson_views) as total_views, + (select COUNT(distinct user_id) from lesson_views) as users_with_views + from users + ) +SELECT total_users, total_courses, total_lessons, users_with_views, +ROUND(1.00 * total_lessons / total_courses, 2) as avg_lessons_per_course, +ROUND(1.00 * total_views / total_lessons, 2) as avg_views_per_lesson +from total_counts +; + +----------------------------------------------------------------------------------------------------- +-- 4 Задание Когортный анализ пользователей (Для каждого месяца регистрации) +create materialized view user_cohort_analysis AS +with needed_users as ( + select distinct u.id as user_id, u.registration_date as reg_date, e.course_id, lv.id as view_id, + lv.lesson_id as viewed_lesson_id, + FIRST_VALUE(lv.viewed_at) over (partition by lv.user_id order by lv.viewed_at) as first_view_date, + e.user_id as enrolled_users, + lv.user_id as active_users + from users u + left join enrollments e on u.id = e.user_id + left join lesson_views lv on e.user_id = lv.user_id + order by user_id + ), +CTE as ( + select + TO_CHAR(reg_date, 'YYYY-MM') as cohort_month, + COUNT(distinct user_id) as users_in_cohort, + COUNT(distinct enrolled_users) as enrolled_users, + COUNT(distinct active_users) as active_users, + COUNT(course_id) as courses_count, + extract('days' from AVG(first_view_date - reg_date)) as avg_time_to_first_view + from needed_users + group by cohort_month + ) +select cohort_month, users_in_cohort, enrolled_users, active_users, +ROUND(100.0 * active_users / users_in_cohort, 2) as retention_rate, +courses_count / users_in_cohort as avg_courses_per_user, +avg_time_to_first_view +from CTE +; + +------------------------------------------------------------------------------------------------------------------ +-- 5 Задание Популярность курсов +create materialized view lesson_popularity_ranking as +with lessons_in_courses as ( + select l.id as lesson_id, l.title as lesson_title, c.title as course_title, + COUNT(e.user_id) as enrolled_users + from lessons l + join courses c on l.course_id = c.id + join enrollments e on e.course_id = c.id + group by l.id, l.title, c.title + order by lesson_id + ), +views_cte as ( + select lic.lesson_id, + COUNT(lv.id) as total_views, + COUNT(distinct lv.user_id) as unique_viewers, + 1.00 * COUNT(lv.id) / nullif(COUNT(distinct lv.user_id), 0) as avg_views_per_user, + COUNT(lic.enrolled_users) as enrolled_to_course + from lessons_in_courses lic + left join lesson_views lv on lic.lesson_id = lv.lesson_id + group by lic.lesson_id + ) +select lic.lesson_id, lic.lesson_title, lic.course_title, +vc.total_views, +vc.unique_viewers, +coalesce(vc.avg_views_per_user, 0) as avg_views_per_user, +dense_rank() OVER (ORDER BY vc.total_views desc) as popularity_rank, +ROUND(100.0 * unique_viewers / enrolled_to_course, 2) as completion_rate +from lessons_in_courses lic +join views_cte vc on lic.lesson_id = vc.lesson_id +; +----------------------------------------------------------------------------------------------- +-- 6 Задания Воронка Конверсии +create materialized view conversion_funnel AS +with course_lessons as ( + select c.id as course_id, c.title as course_title, + COUNT(l.id) as total_lessons + from courses c + join lessons l on l.course_id = c.id + group by c.id, c.title + ), +registered_users as ( + select COUNT(distinct id) as total_users + from users + ), +enrolled_users as ( + select e.course_id, + COUNT(distinct e.user_id) as enrolled_count + from enrollments e + group by e.course_id + ), +started_users as ( + select l.course_id, + COUNT(distinct lv.user_id) as started_count + from lessons l + join lesson_views lv on l.id = lv.lesson_id + group by l.course_id + ), +user_progress as ( + select l.course_id, lv.user_id, + COUNT(distinct lv.lesson_id) as viewed_lessons + from lesson_views lv + join lessons l on lv.lesson_id = l.id + group by l.course_id, lv.user_id + ), +mid_progress_users as ( + select up.course_id, + COUNT(distinct up.user_id) as mid_progress_count + from user_progress up + join course_lessons cl on up.course_id = cl.course_id + where up.viewed_lessons >= cl.total_lessons * 0.5 + group by up.course_id + ), +completed_users as ( + select up.course_id, + COUNT(distinct up.user_id) as completed_count + from user_progress up + join course_lessons cl on up.course_id = cl.course_id + where up.viewed_lessons >= cl.total_lessons + group by up.course_id + ), +funnel_stages as ( + select cl.course_id, cl.course_title, + 'registered' as stage_name, + ru.total_users as users_count, + 1 as stage_order + from course_lessons cl + cross join registered_users ru + union all + select cl.course_id, cl.course_title, + 'enrolled' as stage_name, + eu.enrolled_count as users_count, + 2 as stage_order + from course_lessons cl + left join enrolled_users eu on cl.course_id = eu.course_id + union all + select cl.course_id, cl.course_title, + 'started' as stage_name, + su.started_count as users_count, + 3 as stage_order + from course_lessons cl + left join started_users su on cl.course_id = su.course_id + union all + select cl.course_id, cl.course_title, + 'mid_progress' as stage_name, + coalesce(mpu.mid_progress_count, 0) as users_count, + 4 as stage_order + from course_lessons cl + left join mid_progress_users mpu on cl.course_id = mpu.course_id + union all + select cl.course_id, cl.course_title, + 'completed' as stage_name, + coalesce(cu.completed_count, 0) as users_count, + 5 as stage_order + from course_lessons cl + left join completed_users cu on cl.course_id = cu.course_id + ) + select course_id, course_title, stage_name, users_count, + ROUND(100.0 * users_count / NULLIF(LAG(users_count) over (partition by course_id order by stage_order), 0), 2) + as conversion_from_previous, + ROUND(100.0 * users_count / NULLIF(FIRST_VALUE(users_count) over (partition by course_id order by stage_order), 0), 2) + as conversion_from_start + from funnel_stages +; +-- Признаюсь 6-е задание сделал с помощью нейросети, потому что сам вообще никак к нему подобраться не мог. + +----------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №1 Анализ drop-off точек +select course_id, stage_name, users_count, +100 - conversion_from_previous as lost_precentage +from conversion_funnel +where stage_name = 'started' +order by lost_precentage desc +; +-------------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №2 Топ-3 урока с повторными просмотрами +select lesson_id +from lesson_popularity_ranking +where avg_views_per_user > 1 +limit 3 +; +--------------------------------------------------------------------------------------------------------------------- +-- Доп. Задание №3 Лучшая когорта по retention +select cohort_month, retention_rate +from user_cohort_analysis +order by retention_rate desc +limit 2 +; +-- В нашем случае два месяца с одинаковыми лучшими показателями retention это 2023-01 и 2023-02. Это является следствием того, что у нас +-- наибольшее количество зарегистрированных пользователей именно в эти месяцы, а именно 2023-01 = 40%, а 2023-02 = 20%, соотвественно 60% пользователей были зарегистрированы в эти месяцы. \ No newline at end of file diff --git a/aharlamcev/postgresql/SQL2.0FULL.sql b/aharlamcev/postgresql/SQL2.0FULL.sql new file mode 100644 index 0000000..c44e8e7 --- /dev/null +++ b/aharlamcev/postgresql/SQL2.0FULL.sql @@ -0,0 +1,525 @@ +----------------------------------------------------------- +--DDL +create table users ( +id INT primary key not null, +name VARCHAR(100), +age INT, +email VARCHAR(100), +country VARCHAR(50), +is_active BOOLEAN, +created_at DATE +); + +create table products ( +id INT primary key, +name VARCHAR(100) not null, +category VARCHAR(50), +price DECIMAL(10, 2) not null, +in_stock BOOLEAN +); + +create table orders ( +id INT primary key, +user_id INT, +order_date DATE not null, +status VARCHAR(20) +FOREIGN KEY (user_id) REFERENCES users(id) +); + +create table order_items ( +order_id INT, +product_id INT, +quantity INT not null, +price_at_purchase DECIMAL(10, 2) not null, +primary key (order_id, product_id) +FOREIGN KEY () +); + +alter table users +alter column created_at type timestamptz; + +-------------------------------------------------------------------------------------- +-- DML +INSERT INTO users (id, name, age, email, country, is_active, created_at) +VALUES +(1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), +(2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), +(3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), +(4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), +(5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), +(6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), +(7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), +(8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), +(9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), +(10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); + + +INSERT INTO products (id, name, category, price, in_stock) +VALUES +(1, 'iPhone 15', 'Электроника', 80000, true), +(2, 'Kindle', 'Электроника', 15000, true), +(3, 'Кроссовки Nike', 'Одежда', 9000, true), +(4, 'Кроссовки NoName', 'Одежда', 3000, false), +(5, 'Наушники Pro', 'Электроника', 12000, true), +(6, 'Фитнес‑браслет', 'Гаджеты', 6000, true), +(7, 'Подарочная карта', 'Сервисы', 1000, true); + + +INSERT INTO orders (id, user_id, order_date, status) +VALUES +(1, 1, '2025-01-10 12:00:00', 'paid'), +(2, 3, '2025-01-12 09:30:00', 'cancelled'), +(3, 1, '2025-01-15 18:20:00', 'paid'), +(4, 2, '2025-01-20 11:05:00', 'created'), +(5, 4, '2025-02-01 17:45:00', 'paid'), +(6, 5, '2025-02-10 10:10:00', 'refunded'), +(7, 7, '2025-03-01 09:00:00', 'paid'), +(8, 9, '2025-03-05 19:30:00', 'paid'); + + +INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); + + +-------------------------------------------------------------------------------------------- +--BASE SQL +select id, name, country, created_at, email from users +order by created_at desc, name; + +select id, name, country from users +where country = 'US' -- 'UK' +order by name; + +select id, name, email from users +where email is not null +order by name; + +select distinct LOWER(email) as email_lower from users +where email is not null; + +select id, name, created_at from users +where created_at > '2024-01-11' +order by created_at desc +limit 2 offset 1; +------------------------------------------------------------------------------------------------------ +-- FILTRATION +select name, category, price, +price * 1.3 as price_with_markup, +price * 1.5 as price_with_vat from products +where in_stock is true +and category not in ('Гаджеты') +and price >= 100 and price <=1300; + +select name, price, +price * 0.85 as price_with_discount +from products +where price < 100 or price > 1000; + +select id, name, category, price from products +where category in ('Электроника') and price >= 15000; + +select name, category, price from products +where name not like '%iPhone%'; + +select * from products +where name ilike '%pro%'; + +--------------------------------------------------------------------------------------------------- +-- JOIN +-- 1 задание +select o.id as order_id, order_date, status, u.name as user_name, country from orders o +join users u on o.user_id = u.id; + +-- 2 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.id = 1; + +-- 3 задание +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id; + +-- 4 задание +select u.name as user_name, o.id as order_id, o.status from users u +left join orders o on u.id = o.user_id; + +-- 5 задание +select u.id as user_id, u.name as user_name from users u +left join orders o on u.id = o.user_id +where o.user_id is null; + +-- 6 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where p.name = 'Фитнес‑браслет'; + +-- 7 задание + +select o.id as order_id, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.status = 'paid'; + +-- 8 задание + +select o.id, o.order_date, p.name as product_name, oi.quantity, oi.price_at_purchase from orders o +join order_items oi on o.id = oi.order_id +join products p on oi.product_id = p.id +where o.user_id in (select id from users where name = 'Alice'); + +-- 9 задание + +select o.id as order_id, o.user_id, o.status from orders o +left join order_items oi on o.id = oi.order_id +where oi.product_id is null; -- таких у нас заказов нет, во всех заказах есть товары. + +-------------------------------------------------------------------------------------------- +-- UNION +-- 1 задание +select id, name, is_active from users +where is_active is false +union +select id, name, is_active from users +where is_active is true +order by name; + +-- 2 задание +select id as order_id, user_id, order_date, +case when o.order_date < '2025-02-01' then 'old' else 'new' end as period +from orders o; +-- 3е задание не имеет смысла, т.к. все заказы покупались, там нечего соединять + +---------------------------------------------------------------------------------------------- +-- Agregation +-- 1 задание +select category, COUNT(*) as products_count from products +group by category +order by products_count desc; + +-- 2 задание +select order_id, SUM(quantity) as total_quantity, SUM(quantity * price_at_purchase) as order_revenue from order_items +group by order_id +order by order_revenue desc; + +-- 3 задание +select p.category, SUM(oi.quantity * oi.price_at_purchase) as category_revenue from products p +join order_items oi on oi.product_id = p.id +group by p.category +order by category_revenue desc; + +-- 4 задание +select order_id, SUM(quantity * price_at_purchase) as order_revenue from order_items oi +join orders o on oi.order_id = o.id +where o.status = 'paid' +group by oi.order_id +order by order_revenue desc; + +-- 5 задание +select p.category, SUM(oi.quantity * oi.price_at_purchase) as category_revenue from products p +join order_items oi on oi.product_id = p.id +group by p.category +having SUM(oi.quantity * oi.price_at_purchase) > 1000 +order by category_revenue desc; + +-- 6 задание +select country, COUNT(*) as active_users_count +from users +where is_active is true +group by country +having COUNT(*) >= 2 +order by active_users_count desc; + +-- 7 задание +select p.name, SUM(oi.quantity) as total_quantity_sold from products p +join order_items oi on oi.product_id = p.id +group by p.name +order by total_quantity_sold desc +limit 3; + +-- 8 задание + +select order_id, SUM(quantity * price_at_purchase) as order_revenue +from order_items +group by order_id +having SUM(quantity * price_at_purchase) < 10000 -- сделал меньше 10000 потому что заказов дешевле 100 нет. +order by order_revenue; + +-------------------------------------------------------------------------------------------------------------- +--Functions:date strings logic +-- 1 задание +select name, +created_at::date as created_date, +CURRENT_DATE - created_at::date as days_since_registration +from users +where CURRENT_DATE - created_at::date > 750 +order by days_since_registration desc; + +-- 2 задание +select name, LOWER(email) as email_clean, +substring(LOWER(email) from position('@' in email) + 1) as domain +from users +where email is not null +order by domain, name; + +-- 3 задание +select name, age, +case when age < 20 then 'young' + when age between 20 and 30 then 'adult' + else 'senior' + end as age_group +from users +order by age; + +-- 4 задание +with CTE as + (select name, created_at::date, + current_date - created_at::date as days_since_registration, + case when is_active = true then 'hot' + when current_date - created_at::date < 30 then 'hot' + else 'regular' + end as hot_flag + from users) +select * from CTE +where hot_flag = 'hot'; + +-- 5 задание +with CTE as + (select count(id) as total_users, + COUNT(email) as with_email, + (select count(*) from users where email is null) as without_email + from users) +select total_users, with_email, without_email, + ROUND(100.0 * without_email / total_users, 2) as percentage_no_email + from CTE; + + -- 6 задание + select name, + CURRENT_DATE - created_at::date as days_since_registration, + case + when CURRENT_DATE - created_at::date <= 720 then 'new' + when CURRENT_DATE - created_at::date BETWEEN 721 and 750 then 'middle' + else 'old' + end as tenure_group + from users + order by days_since_registration; + + ---------------------------------------------------------------------------------------------------- + --Subquery/CTE + -- 1 задание +select name +from users +where id in (select distinct user_id from orders) +order by name; + +-- 2 задание +select id, name, email +from users u +where not exists ( +select user_id +from orders o +where o.user_id = u.id) + +-- 3 задание +select name, +(select COUNT(*) orders_count from orders o +where o.user_id = u.id) +from users u +order by orders_count desc, name; + +-- 4 задание +with CTE as ( + select user_id, SUM(quantity * price_at_purchase) as total_revenue + from orders o + join order_items oi on o.id = oi.order_id + group by user_id + ) +select u.name, c.total_revenue from users u +join CTE c on u.id = c.user_id +order by c.total_revenue desc; + +-- 5 задание +with CTE as ( + select product_id, SUM(quantity * price_at_purchase) as product_revenue + from order_items + group by product_id + ) +select p.name as product_name, p.category, c.product_revenue from CTE c +join products p on c.product_id = p.id +order by c.product_revenue desc +limit 3; + +-- 6 задание +with CTE as ( + select o.user_id, SUM(quantity * price_at_purchase) as total_revenue from orders o + join order_items oi on o.id = oi.order_id + group by o.user_id + ) +select u.name, u.email, c.total_revenue from users u +join CTE c on u.id = c.user_id +where u.is_active is true +and c.total_revenue > 10000; + +-- 7 задание + +with CTE as ( + select order_id, SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + where o.status = 'paid' + group by order_id + ) +select * from CTE +where order_revenue < 10000; + +--------------------------------------------------------------------------------------------- +--WINDOW FUNCTIONS +-- 1 задание +select u.id as user_id, u.name, o.id as order_id, o.order_date, +row_number() over (partition by o.user_id order by o.order_date) as order_number_for_user +from users u +join orders o on u.id = o.user_id; + +-- 2 задание +with CTE as ( + select u.id as user_id, u.name as user_name, order_id, o.order_date, SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name, order_id, o.order_date + order by user_id + ) +select user_id, user_name, order_id, order_date, order_revenue, +SUM(order_revenue) over (partition by user_id order by order_date) as running_revenue_for_user +from CTE + +-- 3 задание +with CTE as ( + select u.id as user_id, u.name as user_name, SUM(quantity * price_at_purchase) as total_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name + ) +select user_name, total_revenue, +RANK() over (order by total_revenue desc) as revenue_rank, +dense_rank() over (order by total_revenue desc) as dense_revenue_rank +from CTE; + + +-- 4 задание +select u.name, o.id as order_id, order_date, +first_value(o.order_date) over (partition by user_id order by order_date) as first_order_date, +last_value(o.order_date) over (partition by user_id order by order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as last_order_date +from orders o +join users u on u.id = o.user_id; + + +-- 5 задание +select u.name as user_name, o.id as order_id, +COUNT(*) over (partition by u.id) as orders_per_user +from users u +join orders o on u.id = o.user_id; + +-- 6 задание +with CTE as ( + select u.id as user_id, u.name as user_name, order_id, o.order_date as order_date, + SUM(quantity * price_at_purchase) as order_revenue + from order_items oi + join orders o on oi.order_id = o.id + join users u on o.user_id = u.id + group by u.id, u.name, order_id, o.order_date + order by user_id + ) +select user_name, order_id, order_date, order_revenue, +LAG(order_revenue) over (partition by user_id order by order_date) as prev_order_revenue +from CTE; + +-- 7 задание +with CTEa as ( + select product_id, + o.order_date::date as order_date, + SUM(quantity * price_at_purchase) as daily_category_revenue + from order_items oi + join orders o on oi.order_id = o.id + group by product_id, order_date + order by oi.product_id + ), + CTEb as ( + select p.category, order_date, daily_category_revenue + from CTEa a + join products p on a.product_id = p.id + ) +select category, order_date, daily_category_revenue, +SUM(daily_category_revenue) over (partition by category order by order_date) as running_category_revenue +from CTEb; + +----------------------------------------------------------------------------------------------------------- +-- VIEW and MATERIALIZED VIEW +-- 1 задание +create MATERIALIZED view mv_category_revenue +as + select p.category, SUM(quantity * price_at_purchase) as total_revenue + from products p + join order_items oi on oi.product_id = p.id + join orders o on oi.order_id = o.id and o.status = 'paid' + group by p.category + order by total_revenue desc +; + +-- 2 задание +create materialized view +as + select u.id, u.name, COUNT(o.id) as order_count + from users u + join orders o on u.id = o.user_id and o.status = 'paid' + group by u.id, u.name + order by order_count desc +; + +-- 3 задание +create materialized view mv_daily_revenue +as + select order_date::date as sales_date, SUM(quantity * price_at_purchase) as daily_revenue from orders o + join order_items oi on o.id = oi.order_id + where o.status = 'paid' + group by sales_date + order by sales_date +; + +-- 4 задание +refresh materialized view mv_daily_revenue; + +-- В 1 случае витрину можно обновлять 1 раз в день в 10:00 или 2 раза в день в 10:00 и в 22:00 +-- Во 2 случае витрину нужно обновлять несколько раз в день, думаю каждые часа 2-3 то есть в 8 в 10 в 12 и тд. + + + + + + + + + + + + + + + + + diff --git a/dmezentsev/postgresql/homework/CTE.sql b/dmezentsev/postgresql/homework/CTE.sql new file mode 100644 index 0000000..8c99b87 --- /dev/null +++ b/dmezentsev/postgresql/homework/CTE.sql @@ -0,0 +1,195 @@ +-- Задача 1. Пользователи с заказами +SELECT id AS user_id +FROM dmezentsev.users +WHERE id IN (SELECT user_id + FROM dmezentsev.orders + ) +ORDER BY name; + +/* - Чем такой подход отличается от варианта с JOIN + DISTINCT по users? + + В случае с JOIN + DISTINCT мы бы сперва соединили таблицы, получили юзеров, + которые делали заказы, а потом SELECT DISTINCT отобрали бы id этих юзеров + + При подзапросе мы получаем условный список юзеров с заказами, и фильтрацией + отбираем их id в другой таблице + + - В каком случае IN (подзапрос) может быть более читаемым, чем JOIN? + +В случае, когда нужны поля первой таблицы, а подзапрос только как фильтр. +Меньше строк кода, джойна другой таблицы, подзапрос выигрывает по читаемости +*/ + +-- Задача 2. Пользователи без заказов (NOT EXISTS) + +SELECT id, + name, + email +FROM dmezentsev.users u +WHERE NOT EXISTS (SELECT 1 + FROM dmezentsev.orders o + WHERE o.user_id = u.id); + +/* - Почему для этой задачи NOT EXISTS предпочтительнее, + чем NOT IN (SELECT user_id ...)? + + Подзапрос бы выдал список user_id, по которым надо пройтись и найти соответствие + в другой таблице. EXISTS / NOT EXISTS ищет первое совпадение по user_id и потом + сразу переходит к другому user_id, тем самым сокращая время выполнения запроса + + - Как бизнес может использовать список таких пользователей? + + Может попробовать замотивировать их к первой покупке выдачей промокода/скидки/ + специального предложения + */ + +-- Задача 3. Сколько заказов у каждого пользователя (подзапрос в SELECT) + +SELECT u.name, + (SELECT count(*) + FROM dmezentsev.orders o + WHERE o.user_id = u.id) AS orders_count +FROM dmezentsev.users u +ORDER BY orders_count DESC, name; + +/* - Почему подзапрос в SELECT не может возвращать несколько строк? + +Потому что для каждого u.name он возвращает только одну строку - число заказов. +Если попытаться вернуть больше строк, то будет ошибка выполнения запроса + + +- Как изменится сложность запроса, если пользователей станет, например, 1 млн? + +Внешне запрос останется таким же, увеличится лишь время выполнения запроса + */ + +-- Задача 4. Сумма выручки по каждому пользователю (подзапрос или CTE) + +SELECT u.name, + t.total_revenue +FROM (SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON oi.order_id = o.id + GROUP BY o.user_id) AS t +JOIN dmezentsev.users u ON t.user_id = u.id +ORDER BY total_revenue DESC; + +/* - Как проще читать такой запрос: когда шаг «посчитать выручку по пользователям» + вынесен отдельно, или когда всё написано в одном SELECT? + + Подзапросы читаются проще, логически понятны шаги получения нужных строк сперва + в подзапросе, а потом в основном запросе + + - Если эту «выручку по пользователям» придётся использовать в другом отчёте, + как избежать дублирования кода? + + В таком случае, лучше подзапрос вынести в отдельный CTE, чтобы потом + переиспользовать в коде + */ + +-- Задача 5. Топ‑3 товара по выручке (CTE) + +WITH products_total AS ( + SELECT product_id, + SUM(quantity * price_at_purchase) AS product_revenue + FROM dmezentsev.order_items + GROUP BY product_id +) + +SELECT p.name, + p.category, + pt.product_revenue +FROM dmezentsev.products p +JOIN products_total pt ON p.id = pt.product_id +ORDER BY product_revenue DESC +LIMIT 3; + +/* - Почему удобно сначала считать агрегаты по товарам, + а потом уже подтягивать имена/категории? + + Код становится более читаемым из-за разделения ответственности: + агрегат отдельно в CTE, основной запрос для предоставления всех данных отдельно + + - В чём отличие подхода через CTE от простого `GROUP BY` в одном `SELECT`? + + GROUP BY сделает запрос сложнее для понимания, в него нужно будет добавлять + поля для агрегации. CTE разделяет всю логику, делая код понятнее, к тому же + CTE можно переиспользовать + */ + +-- Задача 6. Активные пользователи с высокой суммой заказов + +WITH users_total AS ( + SELECT o.user_id, + SUM(quantity * price_at_purchase) AS total_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY o.user_id +) + +SELECT u.name, + u.email, + ut.total_revenue +FROM dmezentsev.users u +JOIN users_total ut ON u.id = ut.user_id +WHERE u.is_active + AND total_revenue > 1000; + +/* - Как бы ты назвал этот сегмент пользователей на языке продукта + (VIP, high‑spenders и т.п.)? + + Low - эконом + Medium - основные + High - премиум + VIP - топ + + Допустим, что этот сегмент пользователей был бы Medium + + - Что можно делать с таким сегментом: какие типы акций/коммуникаций? + + Medium сегмент - это лояльные клиенты, которые регулярно покупают, + но сумма покупок не дотягивает до High + Я бы предложил для такого сегмента введение накопительной системы, + дающей право на получение дополнительных скидок, введение системы + раннего доступа к закрытым распродажам + + */ + +-- Задача 7. «Подозрительно маленькие» оплаченные заказы + +WITH orders_total AS ( + SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.order_items oi + INNER JOIN dmezentsev.orders o ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY oi.order_id +) +SELECT ot.order_id, + o.order_date, + ot.order_revenue +FROM orders_total ot +JOIN dmezentsev.orders o ON ot.order_id = o.id +WHERE ot.order_revenue < 100; + +/* - Какие реальные причины могут быть у таких заказов +(ошибки, тесты, злоупотребления промокодами)? + +Предположу, что причины могут быть разного вида: + Технические: + - Ошибка округления валют + - Использование нескольких промокодов одновременно + - Двойное списание бонусов + - Ошибки интеграции с платежными системами + + Человеческий фактор: + - Ручное редактирование заказа + +- На каком шаге выгоднее фильтровать по статусу paid: +внутри подзапроса или снаружи, и почему? + +Выгоднее фильровать внутри подзапроса, чтобы в подзапросе группировать +уже отфильтрованные данные. И уже в основном запросе джойнить, а меньше +строк для джойна ускорят запрос + */ diff --git a/dmezentsev/postgresql/homework/DDL_DML.sql b/dmezentsev/postgresql/homework/DDL_DML.sql new file mode 100644 index 0000000..9780290 --- /dev/null +++ b/dmezentsev/postgresql/homework/DDL_DML.sql @@ -0,0 +1,86 @@ +CREATE TABLE dmezentsev.users ( +id BIGINT PRIMARY KEY, /* сделал первичные ключи в каждой таблице BIGINT, кол-во +записей может быть большим */ +name VARCHAR(100) NOT NULL, +age SMALLINT, -- Тип данных INT посчитал избыточным +email VARCHAR(100), +country VARCHAR(2), -- Идеально для ISO - кода страны +is_active BOOLEAN, +created_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE dmezentsev.products ( +id BIGINT PRIMARY KEY, +name VARCHAR(100) NOT NULL, +category VARCHAR(50), +price DECIMAL(10,2) NOT NULL, -- Сделал тип данных для денежных операций +in_stock BOOLEAN +); + +CREATE TABLE dmezentsev.orders ( +id BIGINT PRIMARY KEY, +user_id BIGINT NOT NULL, +order_date TIMESTAMPTZ NOT NULL, +status VARCHAR(20), -- Читаемый статус нагляднее, не нужен справочник по кодам + +FOREIGN KEY (user_id) REFERENCES users(id) +); + +CREATE TABLE dmezentsev.order_items ( +order_id BIGINT, +product_id BIGINT, +quantity INT NOT NULL, /* Предположил, что типа данных SMALL INT возможно не +хватит для обозначения количества */ +price_at_purchase DECIMAL(10,2) NOT NULL, + +PRIMARY KEY(order_id, product_id), +FOREIGN KEY (order_id) REFERENCES orders(id), +FOREIGN KEY (product_id) REFERENCES products(id) +); + +INSERT INTO dmezentsev.users (id, name, age, email, country, is_active, created_at) +VALUES + (1, 'Alice', 25, 'alice@example.com', 'US', TRUE, '2024-01-10 10:00:00'), + (2, 'Bob', 30, 'bob@example.com', 'UK', TRUE, '2024-02-05 09:30:00'), + (3, 'Carol', 27, 'carol@example.com', 'US', FALSE, '2024-03-01 15:20:00'), + (4, 'Diana', 22, 'diana@GMAIL.com', 'US', TRUE, '2024-01-12 08:15:00'), + (5, 'Evan', 35, 'EVAN@example.com', 'UK', TRUE, '2024-01-18 19:40:00'), + (6, 'Farida', 29, 'farida@example.com', 'KZ', TRUE, '2024-02-20 11:05:00'), + (7, 'George', 31, 'george+test@example.com', 'US', TRUE, '2024-03-05 16:45:00'), + (8, 'Helen', 24, NULL, 'UK', FALSE, '2024-03-06 08:20:00'), + (9, 'Ivan', 26, 'ivan@example.com', 'RU', TRUE, '2024-01-11 14:30:00'), + (10, 'Julia', 28, 'julia@example.com', 'US', TRUE, '2024-03-07 18:05:00'); + +INSERT INTO dmezentsev.products (id, name, category, price, in_stock) +VALUES + (1, 'iPhone 15', 'Электроника', 80000.00, TRUE), + (2, 'Kindle', 'Электроника', 15000.00, TRUE), + (3, 'Кроссовки Nike', 'Одежда', 9000.00, TRUE), + (4, 'Кроссовки NoName', 'Одежда', 3000.00, FALSE), + (5, 'Наушники Pro', 'Электроника', 12000.00, TRUE), + (6, 'Фитнес-браслет', 'Гаджеты', 6000.00, TRUE), + (7, 'Подарочная карта', 'Сервисы', 1000.00, TRUE); + +INSERT INTO dmezentsev.orders (id, user_id, order_date, status) +VALUES + (1, 1, '2025-01-10 12:00:00', 'paid'), + (2, 3, '2025-01-12 09:30:00', 'cancelled'), + (3, 1, '2025-01-15 18:20:00', 'paid'), + (4, 2, '2025-01-20 11:05:00', 'created'), + (5, 4, '2025-02-01 17:45:00', 'paid'), + (6, 5, '2025-02-10 10:10:00', 'refunded'), + (7, 7, '2025-03-01 09:00:00', 'paid'), + (8, 9, '2025-03-05 19:30:00', 'paid'); + +INSERT INTO dmezentsev.order_items (order_id, product_id, quantity, price_at_purchase) +VALUES + (1, 1, 1, 78000.00), + (1, 3, 2, 8500.00), + (2, 2, 1, 14000.00), + (3, 5, 1, 11000.00), + (3, 7, 1, 1000.00), + (4, 2, 1, 15000.00), + (5, 6, 1, 6000.00), + (6, 1, 1, 82000.00), + (7, 3, 1, 9000.00), + (8, 4, 2, 2800.00); \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Explain.md b/dmezentsev/postgresql/homework/Explain.md new file mode 100755 index 0000000..bbd5452 --- /dev/null +++ b/dmezentsev/postgresql/homework/Explain.md @@ -0,0 +1,375 @@ +### Задача 1 — Читаем план + +#### Требования: + +Выполни этот запрос: + +```sql +EXPLAIN +SELECT name, email +FROM users +WHERE id = 1; +``` + +**1. Какой тип сканирования использует база?** + +Index Scan (индексное сканирование) + +**2. Что означает строка `Index Cond: (id = 1)`?** + +Index Cond (Index Condition) — это условие, которое применяется на уровне индекса (id = 1) + +**3. Почему база использует индекс, а не читает всю таблицу?** + +База выбирает индекс вместо полного сканирования таблицы, потому что id — это первичный ключ с индексом, что делает Index Scan значительно быстрее и дешевле, чем Seq Scan + +### Задача 2 — Seq Scan vs Index Scan + +#### Требования: + +Выполни оба запроса с EXPLAIN: + +```sql +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +``` + +**1. Почему в первом запросе используется Index Scan, а во втором может быть Seq Scan (если нет индекса на age)?** + +- Первый запрос (id = 2) использует Index Scan, потому что на id есть индекс первичного ключа. +- Второй запрос (age > 25) использует Seq Scan, потому что на age нет индекса + +### Задача 3 — Замеряем время + +#### Требования: + +Выполни запрос с EXPLAIN ANALYZE: + +```sql +EXPLAIN ANALYZE +SELECT name, price +FROM products +WHERE category = 'Электроника'; +``` + +**1. Запиши Execution Time** + +Execution Time: 0.699 ms + +**2. Определи тип сканирования (Seq Scan или Index Scan)** + +Seq Scan + +**3. Сколько строк вернул запрос?** + +rows=3 + +### Задача 4 — До и после индекса + +#### Требования: + +**Шаг 1:** Выполни запрос без индекса: + +```sql +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'Россия'; +``` + +Запиши **Execution Time** и тип сканирования. + +Execution Time: 76.281 ms +Seq Scan + +**Шаг 2:** Создай индекс: + +```sql +CREATE INDEX idx_country ON users(country); +``` + +**Шаг 3:** Снова выполни тот же запрос с EXPLAIN ANALYZE. + +**1. Изменился ли тип сканирования?** + +Тип сканирования не изменился (слишком мало строк в таблице, чтобы оптимизатор использовал индекс) + +**2. Стал ли запрос быстрее? На сколько?** + +Execution Time: 0.018 ms +Время выполнения запроса после создания индекса стало гораздо меньше, на ~76 ms + +**3. Почему индекс помог?** + +Индекс поможет, потому что: + +- Уменьшит количество читаемых данных - читать только страницы с нужными значениями +- Использует структуру B-Tree +- Позволяет найти строки без сканирования всей таблицы + +### Задача 5 — Анализируем JOIN + +#### Требования: выполни запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия'; +``` + +**1. Какой тип JOIN использует база (Hash Join, Nested Loop, Merge Join)?** + +Hash Join, потому что таблицы маленькие, нет индексов — построить хеш-таблицу и просканировать +правую таблицу дешевле всего. + +**2. С какой таблицы база начала чтение?** + +orders + +**3. Сколько времени занял весь запрос?** + +Execution Time: 0.083 ms + +### Задача 6 — Почему медленно? + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE LOWER(email) = 'alexey@mail.ru'; +``` + +**1. Выполни запрос и посмотри на план** + +Seq Scan on users (cost=0.00..1.15 rows=1 width=467) (actual time=0.012..0.012 rows=0 loops=1) + Filter: (lower((email)::text) = 'alexey@mail.ru'::text) + Rows Removed by Filter: 10 +Planning Time: 0.429 ms +Execution Time: 0.026 ms + +**2. Использует ли база индекс?** + +План показывает Seq Scan, а не Index Scan. + +**3. Почему НЕТ? (даже если индекс на email есть)** + +Применение функции LOWER() к колонке + +**4. Как исправить запрос, чтобы индекс заработал?** + +- Создать индекс с учетом применения функции LOWER() + +```sql +CREATE INDEX idx_users_email_lower ON users (LOWER(email)); +``` +- Использовать ILIKE +- Хранить нормализованный email + +### Задача 7 — Функции убивают индексы + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее: 0.016 ms против 0.043 ms + +**2. Почему первый запрос НЕ может использовать индекс на `age`?** + +Потому что к колонке age применена математическая операция (age + 5), индекс не используется + +**3. Как правильно переписать запрос А?** + +```sql +SELECT * FROM users WHERE age > 25; +``` + +### Задача 8 — Диагностика медленного запроса + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +``` + +**1. Почему функция `EXTRACT()` — плохая идея?** + +Потому что EXTRACT(YEAR FROM created_at) — это функция, применённая к колонке, +индекс на created_at работать не будет. + +**2. Как переписать условие WHERE без функции?** + +```sql +WHERE u.created_at >= '2026-01-01' AND u.created_at < '2027-01-01' +``` + +**3. Будет ли использоваться индекс на `created_at` в текущем виде?** + +Индекс на created_at НЕ БУДЕТ использоваться + +### Задача 9 — SELECT * против конкретных колонок + +#### Сравни два запроса: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +``` + +**1. Какой запрос работает быстрее?** + +Запрос Б работает быстрее (0.017 ms против 0.051 ms) + +**2. Почему второй может быть эффективнее (даже если тип сканирования одинаковый)?** + +Меньше данных нужно прочитать с диска (две колонки против всех колонок таблицы) + +**3. В каких ситуациях разница будет особенно заметна?** + +- Большое количество строк +- Медленные диски (HDD) +- Высокая нагрузка (тысячи запросов в секунду) + +### Задача 10 — Оптимизируем JOIN с условием + +#### Дан запрос: + +```sql +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' AND p.category = 'Электроника'; +``` + +**1. Выполни запрос и посмотри на план** + +Nested Loop (cost=12.66..41.04 rows=1 width=440) (actual time=34.552..34.556 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + Rows Removed by Join Filter: 5 + -> Seq Scan on users u (cost=0.00..1.12 rows=1 width=226) (actual time=0.012..0.015 rows=1 loops=1) + Filter: ((country)::text = 'RU'::text) + Rows Removed by Filter: 9 + -> Nested Loop (cost=12.66..39.84 rows=6 width=230) (actual time=34.510..34.536 rows=5 loops=1) + -> Hash Join (cost=12.51..38.61 rows=6 width=230) (actual time=34.094..34.105 rows=5 loops=1) + Hash Cond: (oi.product_id = p.id) + -> Seq Scan on order_items oi (cost=0.00..22.70 rows=1270 width=20) (actual time=33.943..33.945 rows=10 loops=1) + -> Hash (cost=12.50..12.50 rows=1 width=226) (actual time=0.047..0.048 rows=3 loops=1) + Buckets: 1024 Batches: 1 Memory Usage: 9kB + -> Seq Scan on products p (cost=0.00..12.50 rows=1 width=226) (actual time=0.022..0.027 rows=3 loops=1) + Filter: ((category)::text = 'Электроника'::text) + Rows Removed by Filter: 4 + -> Index Scan using orders_pkey on orders o (cost=0.15..0.21 rows=1 width=16) (actual time=0.084..0.084 rows=1 loops=5) + Index Cond: (id = oi.order_id) +Planning Time: 0.278 ms +Execution Time: 34.691 ms + +**2. В каком порядке база читает таблицы?** + +Порядок чтения таблиц: +1. users +2. products +3. order_items +4. Hash Join +5. orders +6. Nested Loop. + +**3. Сколько строк проходит через каждый JOIN?** + +1. users (1) +2. products (3) +3. order_items (10) +4. Hash Join (5) +5. orders (5) +6. Nested Loop (0) + +**4. Можно ли оптимизировать? (Подсказка: подумай про индексы на `country` и `category`)** + +- Создать индексы: idx_users_country, idx_products_category +- Обновить статистику: ANALYZE всех таблиц + +### Задача 11 — Найди и исправь + +#### Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +``` + +**1. Выполни этот запрос с EXPLAIN ANALYZE** + +Nested Loop (cost=0.00..22.35 rows=1 width=549) (actual time=0.034..0.035 rows=0 loops=1) + Join Filter: (u.id = o.user_id) + -> Seq Scan on users u (cost=0.00..1.20 rows=1 width=467) (actual time=0.033..0.033 rows=0 loops=1) + Filter: ((lower((name)::text) ~~ '%иван%'::text) AND ((age + 10) > 35)) + Rows Removed by Filter: 10 + -> Seq Scan on orders o (cost=0.00..21.10 rows=4 width=82) (never executed) + Filter: (EXTRACT(month FROM order_date) = '1'::numeric) +Planning Time: 0.235 ms +Execution Time: 0.045 ms + +**2. Найди **ВСЕ** проблемы, которые мешают оптимизации** + +- SELECT * - выбор всех колонок +- LOWER(u.name) — функция на колонке, не используется индекс на name +- LIKE '%иван%' — процент в начале означает "искать подстроку в любой позиции" +- EXTRACT(MONTH FROM o.order_date) = 1 — функция на колонке +- u.age + 10 > 35 — расчеты на колонке + +**3. Перепиши запрос правильно** + +```sql +SELECT u.id, u.name, u.age, u.email, u.country, o.id AS order_id, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name ILIKE '%иван%' + AND o.order_date >= '2025-01-01' + AND o.order_date < '2025-02-01' + AND u.age > 25; +``` + +**4. Сравни Execution Time до и после** + +На малых данных разница незначительна: +0.036 ms против 0.045ms + +**5. Объясни, почему твой вариант лучше** + +- Перечисление нужных полей в SELECT +- Регистронезависимая фильтрация ILIKE +- Убраны функции и расчеты по колонкам, не позволяющие использовать индексы + +Все это в совокупности на больших таблицах ускорит время выполнения запроса diff --git a/dmezentsev/postgresql/homework/Filtering.sql b/dmezentsev/postgresql/homework/Filtering.sql new file mode 100644 index 0000000..89e366c --- /dev/null +++ b/dmezentsev/postgresql/homework/Filtering.sql @@ -0,0 +1,69 @@ +-- Задача 1 — Товары для продажи + +SELECT name, + category, + price, + (price * 1.3) price_with_markup, + (price * 1.5) price_with_vat +FROM dmezentsev.products p +WHERE in_stock = TRUE +AND category != '%smartwatch%' +AND price BETWEEN 100 AND 1300; + +/* - Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? + +По мне, так BETWEEN короче и лаконичнее. Разве что с датами надо быть осторожнее :) +*/ + +-- Задача 2 — Скидочная корзина: дешёвые или дорогие + +SELECT name, + price, + (price * 0.85) price_with_discount +FROM dmezentsev.products p +WHERE (price < 100) OR (price > 1000); + +/* - Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно? + +Если в условии есть и OR и AND — скобки обязательны. Можно ошибиться с логикой выполнения, +так как приоритет AND выше OR. Лучше перестраховаться +и ставить скобки везде */ + +-- Задача 3 — Категории для рекламы + +SELECT id, + name, + category, + price +FROM dmezentsev.products p +WHERE category IN ('tablet', 'smartphone', 'mouse') +AND price >= 100 +ORDER BY price DESC; + +-- Задача 4 — Убрать iPhone из выборки + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name NOT LIKE '%iPhone%'; + +-- Задача 5 — Поиск товаров по названию + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name ILIKE '%pro%'; + +SELECT name, + category, + price +FROM dmezentsev.products p +WHERE name ILIKE '%mx%'; + +/* - В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'. + +Потому что = 'Pro' найдёт только точное совпадение с учётом регистра, + а ILIKE '%pro%' найдёт любые вхождения "pro" в любом регистре +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Functions.sql b/dmezentsev/postgresql/homework/Functions.sql new file mode 100644 index 0000000..2e54c35 --- /dev/null +++ b/dmezentsev/postgresql/homework/Functions.sql @@ -0,0 +1,144 @@ +-- Задача 1. Давность регистрации пользователей + +SELECT name, + CAST(created_at AS DATE) AS created_date, + CURRENT_DATE - created_at::DATE AS days_since_registration, + is_active +FROM dmezentsev.users +WHERE CURRENT_DATE - created_at::DATE > 180 +ORDER BY days_since_registration DESC; + +/* - Как бы ты использовал такое поле в реальном продукте +(что можно делать с очень «старыми» пользователями)? + +В реальности можно давать какие-то бонусы самым лояльным клиентам. Ну или +наоборот, удалить их, если зарегистрированы давно, а активности никакой + +- Почему удобнее сразу вычитать created_at::date, а не created_at как timestamp? + +Если вычитать ::date, то получается число, удобно сравнивать, применять при +фильтрации, в арифметических выражениях. Если timestamp, то интервал + */ + +-- Задача 2. Нормализация email и доменов + +SELECT name, + LOWER(email) AS email_clean, + SUBSTRING(LOWER(email) FROM POSITION('@' IN email) + 1) AS domain +FROM dmezentsev.users +WHERE email IS NOT NULL +ORDER BY domain, name; + +/* - Зачем в реальных системах приводят email к нижнему регистру и выделяют домен отдельно? + +email - адреса приводят к нижнему регистру, потому что в БД один и тот же +почтовый ящик в разном регистре будет считаться двумя почтовыми ящиками, что +приведет к дублям. Так же если пользователь введет свой почтовый ящик не в таком +регистре, в котором ящик сохранен в БД, то он не сможет залогиниться +Домен выделяют отдельно для анализа, маркетинга. Так же ради безопасности, +чтобы блочить подозрительные домены + */ + +-- Задача 3. Сегменты по возрасту + +SELECT name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + ELSE 'senior' + END AS age_group +FROM dmezentsev.users +ORDER BY age; + +/* - Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? + +Вот тут точно не знаю, но мне кажется, что сегментация в SQL вернет результат. +А если писать это в коде приложения, то выгрузка всех строк выльется в большой +объем данных, которые нужно будет еще и обработать + +- Что произойдёт, если в CASE забыть ветку ELSE  +(что получат пользователи, не попавшие ни в одно условие)? + +Если забыть ELSE, то пользователи, которые не попадут ни под одно из условий, +получат NULL + */ + +-- Задача 4. «Горячие» пользователи по дате и активности + +SELECT + name, + created_at::DATE AS created_at_date, + CURRENT_DATE - created_at::DATE AS days_since_registration, + CASE + WHEN is_active AND (CURRENT_DATE - created_at::DATE <= 30) THEN 'hot' + ELSE 'regular' + END AS hot_flag +FROM dmezentsev.users +WHERE is_active AND (CURRENT_DATE - created_at::DATE <= 30); + +/* - Чем отличается фильтрация по дате в WHERE от простого CASE с меткой 'hot', +но без фильтрации? + +С фильтрацией в WHERE получим горячих с датой регистрации не больше 30 дней. +Если без WHERE, то получим всех пользователей (и горячих, и обычных) + +- Какие типы рассылок/кампаний логично запускать только на «горячих»? + +Предположу, что горячим можно рассылать специальные предложения, чтобы +дополнительно завлечь. Так же можно сделать рассылку по онбордингу, чтобы +легче вкатиться в фичи сервиса + */ + +-- Задача 5. Качество данных по email + +SELECT COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) FILTER (WHERE email IS NULL) AS without_email, + COUNT(*) FILTER (WHERE email IS NULL)::NUMERIC + / COUNT(*) * 100 AS without_email_percent +FROM dmezentsev.users; + +/* - Почему для проверки «долей» и «заполненности» полей +лучше использовать агрегаты в SQL, а не просто «на глаз» смотреть на таблицу? + +Агрегаты гораздо легче воспринимать на глаз, чем смотреть всю таблицу. +По сути, это уже готовый ответ на поставленный вопрос. Другое дело +пытаться понять по всем данным, что вообще происходит. +Особенно, если там миллионы строк. + +- Какие действия по данным ты бы предложил продукту, +если доля пользователей без email слишком большая? + +Если бы доля юзеров, не заполняюших email, оказалась слишком большой, то я +бы советовал сделать ввод email обязательным, ну или использовать email как +логин. Как вариант, предложить какой-то бонус типа промокода + */ + +-- Задача 6. «Стаж» пользователя и его группа + +SELECT name, + CASE + WHEN CURRENT_DATE - created_AT::DATE <= 30 THEN 'new' + WHEN CURRENT_DATE - created_AT::DATE BETWEEN 31 AND 180 THEN 'middle' + ELSE 'old' + END AS tenure_group, + CURRENT_DATE - created_AT::DATE AS days_since_registration, + is_active +FROM dmezentsev.users +ORDER BY tenure_group, + days_since_registration; + +/* Чем сегментация по «стажу» отличается по смыслу от сегментации по возрасту? + +Стаж - это время с момента регистрации. Думаю, такая сегментация необходима для того, чтобы +по этим сегментам проводить какую-то свою аналитику, отличную для каждого из них. +Например: стаж - аналитика лояльности, возраст - аналитика платежеспособности + +- Если пересчитывать tenure_group прямо в SQL каждый день, +какие плюсы и минусы по сравнению с хранением статического поля segment? + +Плюс перерасчета стажа каждый день - это актуальность данных, которые потом +будут использоваться аналитиками + */ + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/GROUP_BY.sql b/dmezentsev/postgresql/homework/GROUP_BY.sql new file mode 100644 index 0000000..4e33a2b --- /dev/null +++ b/dmezentsev/postgresql/homework/GROUP_BY.sql @@ -0,0 +1,160 @@ +-- Задача 1. Сколько товаров в каждой категории + +SELECT category, + count(id) AS products_count +FROM dmezentsev.products +GROUP BY category +ORDER BY products_count DESC; + +/* - Что будет, если убрать ORDER BY — как изменится чтение результата +для продукта/аналитика? + +Если убрать сортировку, то строки в результирующей таблице будут выведены +произвольно, а точнее это будет зависеть от плана выполнения запроса + +- Если добавить в таблицу новый товар с категорией smartphone, +как изменится строка для этой категории? + +Если добавить новый товар с категорией, которая есть в таблице, то в поле +products_count будет +1 значение по этой категории +*/ + +-- Задача 2. Выручка по каждому заказу + +SELECT order_id, + SUM(quantity) AS total_quantity, + SUM(quantity * price_at_purchase) AS order_revenue +FROM dmezentsev.order_items +GROUP BY order_id +ORDER BY order_revenue DESC; + +/* - Что будет, если убрать GROUP BY и оставить SUM(quantity * price_at_purchase)? + +Если убрать GROUP BY, то будет ошибка. Поле, запрашиваемое в SELECT и +не участвующее в агрегирующей функции, должно быть в GROUP BY + */ + +-- Задача 3. Выручка по категориям товаров + +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY p.category +ORDER BY category_revenue DESC; + +/* - Почему группировка идёт по p.category, а не по p.id? + +Группировка по p.category, а не по p.id, потому что нам нужно узнать +общую выручку по категориям, а не по товарам в категориях + +- Если добавить в SELECT ещё p.name, что произойдёт с результатом и почему? + +Если добавить в SELECT поле p.name, то запрос выдаст ошибку, что это +поле должно быть в GROUP BY, так как не является агрегирующей функцией + */ + +-- Задача 4. Только оплаченные заказы и их выручка + +SELECT o.id AS order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM dmezentsev.orders o +JOIN order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.id +ORDER BY order_revenue DESC; + +/* - Чем результат этой задачи будет отличаться от задачи 2 (где статус не учитывался)? + +В этом запросе в результирующую таблицу попадут только заказы со статусом 'paid' + +- В какой реальной ситуации ты бы смотрел только на paid заказы, а не на все? + +В реальной ситуации такая фильтрация была бы одной из основополагающих +и отображающих метрик успешности бизнеса + */ + +-- Задача 5. Категории с выручкой больше 1000 + +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY p.category +HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY category_revenue DESC; + +/* - Почему нельзя написать WHERE category_revenue > 1000? + +Условие в WHERE после GROUP BY не сработает, так как оно фильтрует данные до +группировки + +- Чем WHERE price > 1000 принципиально отличается от HAVING SUM(...) > 1000? + +WHERE price > 1000 отличается от HAVING SUM(...) > 1000 тем, что в WHERE +будет отфильтрованы товары с ценой больше 1000, а HAVING отфильтрует сгруппированные +категории, у которых общая выручка больше 1000 + */ + +-- Задача 6. Страны с достаточным числом активных пользователей + +SELECT country, + COUNT(id) AS active_users_count +FROM dmezentsev.users +WHERE is_active = TRUE +GROUP BY country +HAVING COUNT(id) >= 2 +ORDER BY active_users_count DESC; + +/* - Почему фильтр по активности (is_active = true) логичнее писать в WHERE, а не в HAVING? + +Логичнее условие писать в WHERE, потому что производительнее сперва будет +отфильтровать нужные данные, а по ним уже потом проводить группировку + +- Что покажет запрос, если в данных пока нет ни одной страны с active_users_count >= 2? + +Если под условие в HAVING не попадет ни один агрегат, то запрос не выведет ни +одной строки + */ + +-- Задача 7. Топ‑товары по количеству продаж + +SELECT name AS product_name, + SUM(quantity) AS total_quantity_sold +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id +GROUP BY name +ORDER BY total_quantity_sold DESC +LIMIT 3; + +/* - Чем рейтинг по total_quantity_sold отличается от +рейтинга по выручке (SUM(quantity * price_at_purchase))? + +- В каких случаях бизнесу важнее смотреть на количество, а не на деньги? + +В поле total_quantity_sold выводим товары, которые продавались чаще всего. +SUM(quantity * price_at_purchase) вывел бы выручку наиболее часто продаваемых товаров +Предположу, что кол-во важнее выручки в случае, когда важно завоевать место в нише. +Как только увеличится база лояльных клиентов, обычно потом начинают поднимать цену + */ + +-- Задача 8. Заказы с «маленькой» выручкой + +SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue +FROM dmezentsev.order_items +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY order_revenue; + +/* - Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? + +Маленький по выручке заказ - это покупка чего-то дешевого (либо повседневного). Или же +использование скидок/купонов. + +- Что проще увидеть глазами: просто все заказы с суммой или +именно отфильтрованные «микро‑заказы»? + +В этом полезность фильтрации, чтобы не размывать взгляд на абсолютно все +заказы, а оставить конкретные (< 100, например). И уже проанализировать их + */ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/JOIN.sql b/dmezentsev/postgresql/homework/JOIN.sql new file mode 100644 index 0000000..5cf57cc --- /dev/null +++ b/dmezentsev/postgresql/homework/JOIN.sql @@ -0,0 +1,207 @@ +-- Задание 1: Заказы + кто заказал + +SELECT u.name AS user_name, + u.country, + o.id AS order_id, + o.order_date, + o.status +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON o.user_id = u.id; + +SELECT u.name AS user_name, + u.country, + o.id AS order_id, + o.order_date, + o.status +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON o.user_id = u.id; + +/* - С какой таблицы логичнее начать: orders или users? Почему? + +Логичнее начать с таблицы users. А дальше вариантами джойна выбирать, что +нужно: если оставить только тех пользователей, у которых был хотя бы один заказ, +то использовать INNER JOIN. Если вывести так же пользователей, у которых заказа +не было, то LEFT JOIN + +- Какой тип JOIN здесь уместнее: INNER или LEFT? Напиши запрос в обоих вариантах и посмотри, +где пропадут строки. + +При INNER JOIN пропадут три строки с пользователями, у которых не было ни одного заказа +*/ + +-- Задание 2: Чек одного заказа + +SELECT oi.order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.order_items oi +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE oi.order_id = 101; + +/* - Что изменится, если эту фильтрацию order_id = 101 перенести в JOIN (после ON)? + +При использовании INNER JOIN оба варианта (WHERE order_id = 101 или +перенести в JOIN (после ON)) дадут одинаковый результат. + +Но: +LEFT JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE oi.order_id = 101 +Сначала делается джойн, потом фильтруются данные по условию. Вернутся строки, +удовлетворяющие условию фильтрации + +LEFT JOIN dmezentsev.products p ON oi.product_id = p.id AND oi.order_id = 101 +Тут возможно будет больше строк, так как вернутся строки из order_items c NULL +в таблице products +*/ + +-- Задание 3: Полный чек по всем заказам + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id; + +/* - почему после JOIN заказ 3 появляется в нескольких строках? + +Заказ 3 появился в двух строках, потому что при джойне кол-во строк может +увеличиваться. Это связано с тем, что в этом заказе было два наименования, +отсюда и увеличение строк + +- Чем результат отличается от задания 2 (по количеству строк и информации)? + +В этом запросе строк и информации больше, потому что информация запрошена +по всему магазину, а не по конкретному заказу 101 + */ + +-- Задание 4: Все пользователи + их заказы (включая тех, у кого заказов нет) + +SELECT u.name AS user_name, + o.id AS order_id, + o.status +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Чем этот запрос отличается от такого же, но с INNER JOIN? + +Если бы тут был INNER JOIN, то пользователи без заказов не попали бы в +результирующую таблицу + */ + +-- Задание 5: Пользователи без заказов + +SELECT u.id AS user_id, + u.name AS user_name +FROM dmezentsev.users u +LEFT JOIN dmezentsev.orders o ON u.id = o.user_id +WHERE o.id IS NULL; + +/* - Почему тут важно фильтровать именно по колонке из **`orders`**, а не из **`users`**? + +Фильтрую по полю orders.id, потому что это первичный ключ таблицы, +который подразумевает собой факт покупки. То есть если после джойна +это поле IS NULL, то у пользователя не было ни одного заказа + */ + +-- Задание 6: Заказы, где есть Kindle + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE p.name LIKE '%Kindle%'; + +-- Задание 7: Товары и в каких заказах они покупались + +SELECT p.name AS product_name, + oi.order_id, + oi.quantity +FROM dmezentsev.products p +LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id; + +/* - Что будет в order_id, если товар ни разу не покупали? + +Если бы товар не был ни разу куплен, то в поле order_id был NULL +из-за использования LEFT JOIN + +- Чем это отличается от варианта, если использовать INNER JOIN? + +При использовании INNER JOIN товар, который ни разу не заказывали, +не попал бы в итоговую таблицу + */ + +-- Задание 8: Только оплаченные заказы + их состав + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE o.status = 'paid'; + +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id AND o.status = 'paid' +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id; + +/* - Что будет, если условие status = 'paid' написать +в JOIN с orders (там же, где ON)? Изменится ли результат сейчас и почему? + +Результат будет один и тот же, потому что используется INNER JOIN. +Строки по условию o.status = 'paid' отсекутся в любом случае, что +при WHERE, что на шаге JOIN + */ + +-- Задание 9: Заказы пользователя Alice + +SELECT o.id AS order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM dmezentsev.orders o +JOIN dmezentsev.users u ON o.user_id = u.id +JOIN dmezentsev.order_items oi ON o.id = oi.order_id +JOIN dmezentsev.products p ON oi.product_id = p.id +WHERE u.name = 'Alice'; + +/* - Если у Alice нет ни одного заказа, сколько строк вернёт твой запрос и почему? + +Если бы у Alice не было заказов, то запрос не вернул ничего, так как +не было бы ни одной строки, удовлетворяющей условию фильтрации + */ + +-- Задание 10: Заказы без товаров + +SELECT + o.id AS order_id, + o.user_id, + o.status +FROM dmezentsev.orders o +LEFT JOIN dmezentsev.order_items oi ON o.id = oi.order_id +WHERE oi.order_id IS NULL; + +/* - Что будет, если вместо LEFT JOIN написать INNER JOIN — изменится ли результат и как? + +Если изменить LEFT JOIN на INNER JOIN, то результирующих строк вообще не будет. +INNER JOIN оставит только те заказы, у которых есть хотя бы одна запись в order_items. +То есть при INNER JOIN колонка oi.order_id никогда не будет NULL + */ diff --git a/dmezentsev/postgresql/homework/Locking.md b/dmezentsev/postgresql/homework/Locking.md new file mode 100755 index 0000000..88d921c --- /dev/null +++ b/dmezentsev/postgresql/homework/Locking.md @@ -0,0 +1,88 @@ +### Задание 1. Чтение и чтение + +#### Требования: + +- Используй таблицу `users` из примера (Alice и Bob). +- Сформулируй, что делают две одновременные сессии, каждая выполняет **`SELECT * FROM users WHERE id = 1;`**. + +Обе сессии читают данные пользователя Alice (id = 1) из таблицы users. Поскольку это обычные запросы на чтение, они не блокируют друг друга. + +**1. Почему оба запроса могут выполниться сразу, без ожидания?** + +Эти запросы выполняются параллельно, не дожидаясь друг друга, и мгновенно возвращают результат + +### Задание 2. Два `UPDATE` одного пользователя + +#### Требования: + +- Сценарий: две сессии одновременно пытаются поменять email у Alice (**`id = 1`**). +- Одна меняет на **`alice.new@example.com`**, другая — на **`alice.marketing@example.com`**. +- Опиши, что будет делать вторая сессия, пока первая транзакция не закончилась. + +1. Сессия 1 начинает транзакцию и выполняет UPDATE. PostgreSQL захватывает блокировку строки для id = 1. +2. Сессия 2 пытается выполнить свой UPDATE, не может выполнить UPDATE сразу — она должна ждать. +3. Сессия 2 переходит в состояние ожидания, пока сессия 1 не завершит свою транзакцию +4. Сессия 1 завершает транзакцию +5. Сессия 2 выполняет свой UPDATE уже поверх нового состояния строки. + +**1. Почему нельзя разрешить обоим UPDATE пройти одновременно?** + +Нельзя разрешить обоим UPDATE пройти одновременно, потому что это приведёт к потерянному обновлению, нарушению целостности данных + +**2. Какой email в итоге останется у Alice и почему порядок COMMIT важен?** + +Итоговый email зависит от порядка COMMIT — побеждает последняя зафиксированная транзакция + +Порядок COMMIT определяет, какое значение останется. + +### Задание 3. `SELECT ... FOR UPDATE` + +#### Требования: + +- Сценарий: +В Сессии 1 выполняется **`BEGIN; SELECT * FROM users WHERE id = 2 FOR UPDATE;`** и пользователь Bob «забронирован» для изменений. +- Придумай, что может сделать Сессия 2: + - что будет с `SELECT * FROM users WHERE id = 2;` + +Выполняется сразу — возвращает текущие данные Bob, не дожидаясь Сессии 1 + + - что будет с `UPDATE users SET country = 'FR' WHERE id = 2;` + +Сессия 2 ждёт — блокируется до тех пор, пока Сессия 1 не выполнит COMMIT или ROLLBACK + +**1. Почему **`SELECT`** без `FOR UPDATE` не блокируется, а `UPDATE` ждёт?** + +Чтение (без FOR UPDATE) — не изменяет данные + +**2. В каких задачах интернет‑магазина `SELECT ... FOR UPDATE` помогает избежать ошибок?** + +- Резервирование товара при оформлении заказа: без FOR UPDATE могло бы случиться, что два покупателя одновременно видят последний товар в наличии, оба оформляют заказ, и товар продаётся дважды +- Применение персональной скидки или купона: иначе юзер мог бы отправить два параллельных запроса и использовать один купон дважды +- Изменение цены товара во время оформления корзины: не допустит, когда юзер положил товар в корзину по одной цене, а к моменту оплаты цена выросла + +### Задание 4. Дедлок на двух пользователях + +#### Требования: + +- Возьми сценарий из теории: + - Сессия 1 сначала обновляет `country` у Alice, потом пытается обновить Bob; + - Сессия 2 сначала обновляет Bob, потом пытается обновить Alice. + +**1. Объясни своими словами, почему возникает дедлок и что сделает база с одной из транзакций.** + +Каждая транзакция ждёт ресурс, который удерживает другая транзакция. Ни одна из них не может продолжить выполнение. + +Когда deadlock обнаружен, база откатывает (ROLLBACK) одну из транзакций (ту, которая выполняется меньше или которая привела к дедлоку), чтобы разрешить конфликт + +**2. Как можно переписать код, чтобы дедлок не возникал *(подсказка: порядок обновления строк)*?** + +```sql +BEGIN; +UPDATE users SET country = 'CA' WHERE id = 1; +UPDATE users SET country = 'CA' WHERE id = 2; +COMMIT; +``` + +**3. Почему для интернет‑магазина это важно, если, например, массово меняют country многим пользователям скриптом?** + +Для интернет-магазина это критично при массовых обновлениях — без единого порядка тысячи дедлоков парализуют работу системы, вызывая откаты, падение производительности и несогласованные данные. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Logic_physic_joins.md b/dmezentsev/postgresql/homework/Logic_physic_joins.md new file mode 100755 index 0000000..2ccf34f --- /dev/null +++ b/dmezentsev/postgresql/homework/Logic_physic_joins.md @@ -0,0 +1,307 @@ +### Задача 1 — Когда какой метод? + +| **Ситуация** | **Какой JOIN выберет база?** | **Почему?** | +| --- | --- | --- | +| 10 пользователей, 1 млн заказов, есть индекс на orders.user_id | Nested Loop | Маленькая левая таблица (10 строк) + индекс на правой | +| 100 000 пользователей, 100 000 заказов, нет индексов | Hash Join | Обе таблицы средние, нет индексов | +| Обе таблицы отсортированы по ключу, есть индексы | Merge Join | Данные уже отсортированы — можно пройтись за один проход | +| Маленькая таблица (5 строк) соединяется с большой (1 млн строк), нет индекса | Hash Join | Hash Join просканирует большую таблицу 1 раз | + +### Задача 2 — Размер таблиц имеет значение + +#### Требования: + +Создай тестовые данные: + +```sql +-- Маленькая таблица (10 строк) +CREATE TABLE small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; +``` + +Теперь выполни: + +```sql +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +``` + +**1. Какой метод `JOIN` используется в каждом случае?** + +- Без индекса — Hash Join (строит хеш по маленькой таблице, сканирует большую) +- С индексом — Nested Loop (для каждого пользователя делает Index Scan в таблице заказов) + +**2. Почему с индексом база может выбрать `Nested Loop`?** + +Потому что левая таблица (small_users) очень маленькая (10 строк), а индекс на big_orders.user_id позволяет мгновенно находить заказы каждого пользователя + +**3. Насколько быстрее стало с индексом?** + +С индексом в 10-20 раз быстрее (~2.5 мс против ~45 мс). Index Scan читает только нужные строки (не все 100 000 заказов) + +### Задача 3 — Влияние сортировки + +#### Требования: + +Выполни три варианта: + +```sql +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +``` + +**1. Изменился ли тип `JOIN` во втором варианте?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — во всех вариантах используется Hash Join + +**2. Появился ли `Merge Join`?** + +Merge Join НЕ ПОЯВИЛСЯ — для Merge Join нужен индекс на orders.user_id, чтобы заказы были отсортированы по ключу соединения. + +**3. Почему `ORDER BY` может повлиять на выбор `JOIN`?** + +ORDER BY влияет на выбор JOIN ТОЛЬКО при наличии индексов на ключах соединения. Если бы индекс на orders.user_id существовал, ORDER BY по ключу соединения привёл бы к появлению Merge Join, который использует отсортированные данные + +### Задача 4 — Найди узкое место + +#### Требования: + +Дан медленный запрос: + +```sql +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. Выполни с `EXPLAIN ANALYZE`** + +**2. Посмотри, какие типы `JOIN` используются** + +Все JOIN — Hash Join + +**3. Есть ли `Seq Scan`? Если да — где?** + +Да, есть Seq Scan на всех таблицах (таблицы очень маленькие (7-10 строк), и PostgreSQL считает, что читать всю таблицу последовательно дешевле, чем использовать индекс) + +**4. Предложи, какие индексы нужно создать** + +```sql +CREATE INDEX idx_users_country ON users(country); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_order_items_order_id ON order_items(order_id); +CREATE INDEX idx_order_items_product_id ON order_items(product_id); +``` + +- для фильтрации по стране +- для ускорения поиска заказов конкретного пользователя +- для ускорения поиска позиций заказа +- для ускорения поиска товара в позициях + +**5. Создай индексы и проверь, изменились ли типы `JOIN`** + +После создания индексов (на больших данных) типы JOIN изменятся с Hash Join на Nested Loop, а сканирование users станет Index Scan. + +### Задача 5 — Многоступенчатый JOIN + +```sql +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'Россия' +GROUP BY u.name; +``` + +**1. Посмотри на план выполнения** + +**2. Какие методы `JOIN` используются на каждом уровне?** + +План выполнения показывает: оба JOIN — Hash Left Join, все таблицы читаются через Seq Scan (так как данные маленькие). + +**3. Почему для первого `JOIN` один метод, а для второго — другой?** + +Оба JOIN используют Hash Left Join, потому что нет индексов на внешних ключах, а таблицы маленькие, чтобы построить хеш-таблицы в памяти. + +**4. Сколько строк проходит через каждый `JOIN`?** + +1. После фильтра country = 'RU': users — 1 строка +2. После первого LEFT JOIN (users и orders) — 1 строка +3. После второго LEFT JOIN (orders и order_items) — 1 строка (у заказа 1 позиция) +4. Результат: 1 строка с агрегатами (orders_count=1, products_count=1) + +### Задача 6 — `LEFT JOIN` vs `INNER JOIN` + +#### Требования: + +Сравни два запроса: + +```sql +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +``` + +**1. Одинаковый ли тип физического JOIN используется?** + +Нет, типы JOIN разные: INNER JOIN использует Hash Join, LEFT JOIN использует Hash Left Join + +**2. Есть ли разница в Execution Time?** + +Небольшая, LEFT JOIN медленнее на ~0.003 мс + +**3. Почему LEFT JOIN может быть медленнее?** + +- Должен сохранять ВСЕ строки из левой таблицы (даже без соответствия) и добавлять NULL +- Возвращает больше строк + +### Задача 7 — Порядок таблиц в JOIN + +#### Требования: + +Сравни: + +```sql +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +``` + +**1. Изменился ли тип `JOIN`?** + +Тип JOIN НЕ ИЗМЕНИЛСЯ — в обоих случаях используется Hash Join + +**2. Изменился ли порядок чтения таблиц в плане?** + +Порядок чтения таблиц в плане НЕ ИЗМЕНИЛСЯ — оптимизатор в обоих случаях начал с orders (меньшей таблицы) + +**3. Одинаковое ли `Execution Time`?** + +Execution Time ОДИНАКОВОЕ, потому что планы идентичны + +**4. Почему оптимизатор может выбрать разный порядок?** + +Оптимизатор может выбрать разный порядок на основе COST: учитывает размер таблиц, фильтрацию, наличие индексов и тип JOIN + +### Задача 8 — Спровоцируй Merge Join + +#### Требования: + +Напиши запрос, который **гарантированно** использует Merge Join. + +**Подсказки:** +- Используй ORDER BY +- Убедись, что есть индексы на ключи соединения +- Обе таблицы должны быть достаточно большими + +Создал таблицы на 100 000 + строк с помощью ИИ, не стал сюда копировать + +```sql +EXPLAIN +SELECT u.id, u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; +``` + +**Фишка в том, что Merge Join будет использоваться, если:** + +- Индекс на users.id (первичный ключ — есть) +- Индекс на orders.user_id (нужно создать) +- ORDER BY u.id, o.user_id — совпадает с ключами JOIN +- Достаточно большой размер таблиц + +### Задача 9 — Когда Nested Loop лучше Hash Join? + +#### Требования: + +Придумай ситуацию (размер таблиц, наличие индексов, условия), когда: + +**1. Nested Loop будет быстрее Hash Join** + +- Левая таблица очень маленькая (1-100 строк), Nested Loop сделает мало итераций +- Есть индекс на правой таблице по ключу JOIN +- Правая таблица очень большая (миллионы строк) + +**2. Hash Join будет быстрее Nested Loop** + +- Обе таблицы большие (тысячи+ строк) +- Нет индекса на правой таблице +- Достаточно памяти для хеш-таблицы + +**3. Создай тестовые данные и проверь свою гипотезу!** + +Создавал с помощью ИИ таблицы и принудительно отключал у оптимизатора варианты физического джойна (то есть сперва обе таблицы через Nested Loop, потом через Hash Join, затем так же другие две таблицы), вывел результаты в табличку: + +| Сценарий | Тип JOIN | Время | +|----------|----------|-------| +| маленькая + большая + индекс | Nested Loop | ~180 ms | +| маленькая + большая + индекс | Hash Join | ~400 ms | +| большая + большая + нет индекса | Hash Join | ~85 ms | +| большая + большая + нет индекса | Nested Loop | ~12500 ms | + +Гипотезы подтвердились: Nested Loop хорош для 'маленькая + большая + индекс', Hash Join хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна))) \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Optimization.md b/dmezentsev/postgresql/homework/Optimization.md new file mode 100755 index 0000000..9ad8eb6 --- /dev/null +++ b/dmezentsev/postgresql/homework/Optimization.md @@ -0,0 +1,251 @@ +### Задача 1 — Одинаковые ли запросы? + +#### Требования: + +- Напиши, будут ли эти два запроса давать одинаковый результат? Объясни почему. + +```sql +-- Вариант А +SELECT name, age +FROM users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +SELECT name, age +FROM users +WHERE age > 25 AND country = 'RU'; +``` + +Оба запроса дадут абсолютно одинаковый результат. В обоих случаях будут выбраны те пользователи, у которых одновременно country = 'RU' И age > 25 + +**1. Какой из них будет работать быстрее?** + +Они будут работать с одинаковой скоростью — оптимизатор запросов сам анализирует условия и выбирает оптимальный порядок выполнения, игнорируя порядок в тексте запроса. + +### Задача 2 — JOIN в разном порядке + +#### Требования: + +Напиши два варианта запроса: сначала пользователи JOIN заказы, потом наоборот. + +**1. Будут ли результаты одинаковыми?** + +Если бы использовался INNER JOIN: результаты были бы идентичными + +**2. Твой вариант 1** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; +``` + +**3. Твой вариант 2** + +```sql +SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status +FROM dmezentsev.orders o +LEFT JOIN dmezentsev.users u ON o.user_id = u.id; +``` + +**4. Какой таблицей оптимизатор начнёт чаще всего? Почему?** + +Оптимизатор PostgreSQL начинает с меньшей таблицы, потому что это снижает стоимость JOIN-операции + +### Задача 3 — Первое знакомство с EXPLAIN + +#### Требования: + +Выполни команду: + +```sql +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Какой тип сканирования использует база (Seq Scan, Index Scan)?** + +Seq Scan (последовательное сканирование) — база будет читать всю таблицу users подряд, одну строку за другой, проверяя условие country = 'RU'. + +**2. Сколько строк она прочитала?** + +База прочитала все 10 строк таблицы, проверила каждую, но вернула только 1. + +**3. Сколько времени это заняло?** + +0.025–0.050 ms + +### Задача 4 — Сравни два плана + +#### Требования: + +Выполни оба запроса с EXPLAIN ANALYZE: + +```sql +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM users +WHERE country = 'RU'; +``` + +**1. Почему второй запрос может быть быстрее первого?** + +Второй запрос (SELECT name, email) может быть быстрее первого (SELECT *), потому что читает меньше данных (только две колонки вместо всех). + +### Задача 5 — JOIN с EXPLAIN + +#### Требования: + +Посмотри план выполнения JOIN: + +```sql +EXPLAIN ANALYZE +SELECT u.name, o.id, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU'; +``` + +**1. С какой таблицы начала читать база? Почему именно с неё?** + +База начнёт читать с таблицы users, потому что у неё есть фильтр WHERE country = 'RU', который сильно уменьшает количество строк + +### Задача 6 — Найди узкое место + +#### Требования: + +У тебя есть запрос: + +```sql +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +``` + +**1. Найди как минимум 2 проблемы, которые замедляют запрос. Как их исправить?** + +- `LOWER(u.name)` — функция на колонке, которая также блокирует использование индекса и требует вычислений для каждой строки +- `LIKE '%алекс%'` не использует индекс, требуя полного сканирования + +**Исправить:** + +- использовать `ILIKE` без `LOWER` +- хранить уже нормализованные данные + +### Задача 7 — Оптимизируй JOIN + +#### Требования: + +Дан запрос: + +```sql +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия'; +``` + +**1. В каком порядке база будет читать таблицы? Почему? Можно ли как-то ускорить этот запрос?** + +База начнёт читать с таблицы users, применяя фильтр country = 'Россия' ('RU'), потому что это сильно уменьшает количество строк. + +**Ускорить можно:** + +- создать индексы +- использовать материализованные представления + +### Задача 8 — Сравни подходы + +#### Требования: + +Какой запрос будет работать быстрее и почему? + +```sql +-- Вариант А +SELECT name, price +FROM products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +SELECT * +FROM products +WHERE category = 'Электроника' + AND price > 50000; +``` + +**1. Запусти оба с EXPLAIN ANALYZE и сравни время выполнения.** + +Вариант А будет быстрее, меньше данных нужно прочитать + +- `SELECT name, price` читает 2 колонки (name VARCHAR(100), price DECIMAL) +- `SELECT *` читает все 5 колонок (id, name, category, price, in_stock) + +### Задача 9 — Найди и исправь + +#### Требования: + +Представь, что тебе дали код коллеги: + +```sql +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; +``` + +**1. Найди ВСЕ проблемы в этом запросе** + +- `EXTRACT(YEAR FROM created_at)` делает невозможным использование индекса +- `LOWER(country)` — функция на колонке, блокирующая использование индекса +- Вычисление в условии `age + 10 > 35` + +**2. Перепиши его правильно** + +```sql +SELECT id, name, email, country, age, created_at, is_active +FROM dmezentsev.users +WHERE created_at >= '2026-01-01' + AND created_at < '2027-01-01' + AND country = 'RU' + AND age > 25; +``` + +**3. Объясни, почему твой вариант лучше** + +Он лучше, потому что может использовать индексы, не выполняет лишних вычислений, возвращает только нужные колонки и фактически найдёт данные (с правильным значением страны). + +### Задача 10 — Исследуй реальные данные + +#### Требования: + +Создай таблицу с 10 000 записей пользователей (можно сгенерировать через функцию `generate_series`). + +Сравни время выполнения: + +```sql +-- Запрос 1 +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +SELECT name, email FROM users WHERE age > 25; +``` + +**1. Используй EXPLAIN ANALYZE и посмотри, насколько второй запрос быстрее первого на большом объёме данных.** + +Второй запрос (SELECT name, email) будет быстрее на 30-50% на 10 000 записях, потому что читает меньше данных (только 2 колонки вместо всех), ну и поэтому передаёт меньше байт по сети \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Query_order.md b/dmezentsev/postgresql/homework/Query_order.md new file mode 100755 index 0000000..d8ec3ca --- /dev/null +++ b/dmezentsev/postgresql/homework/Query_order.md @@ -0,0 +1,328 @@ +### Задача 1 — Запрос с группировкой + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +WHERE is_active = true +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY users_count DESC; +``` + +**1. Когда применится `WHERE` — до или после `GROUP BY`?** + +WHERE применяется ДО GROUP BY + +**2. Когда применится `HAVING` — до или после `GROUP BY`?** + +HAVING применяется ПОСЛЕ GROUP BY + +**3. Можно ли использовать алиас `users_count` в `HAVING`?** + +HAVING выполняется до SELECT. Алиас users_count создаётся только на этапе SELECT. Поэтому на момент выполнения HAVING алиас ещё не существует. + +**4. Можно ли использовать алиас `users_count` в `ORDER BY`?** + +ORDER BY выполняется после SELECT. На этом этапе алиас users_count уже создан и доступен для использования. + +### Задача 2 — JOIN с фильтрацией + +```sql +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' AND o.status = 'completed' +ORDER BY o.order_date DESC; +``` + +**1. Опиши порядок выполнения своими словами (как будто объясняешь другу-новичку).** + +Порядок выполнения: +База сначала фильтрует пользователей по стране (WHERE u.country = 'Россия'), оставляя только пользователей из России. Затем соединяет (JOIN) этих пользователей с таблицей заказов, находя их заказы. После этого фильтрует заказы по статусу (WHERE o.status = 'completed'). Затем выбирает нужные колонки (SELECT): имя, дату заказа и статус заказа, и сортирует (ORDER BY). + +### Задача 3 — Найди ошибку + +Дан запрос: + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +``` + +**1. Запрос работает, но написан **неэффективно**. Почему?** + +HAVING используется для фильтрации по колонке country, которая не является агрегированной. Для фильтрации по обычным колонкам (не результатам агрегации) предназначен WHERE. + +**2. Перепиши его правильно, используя WHERE.** + +```sql +SELECT country, AVG(age) AS avg_age +FROM users +WHERE country = 'Россия' +GROUP BY country; +``` + +**3. Объясни, почему твой вариант быстрее.** + +Потому что WHERE применяется до группировки: база сначала отфильтровывает только нужные строки (страна 'RU'), затем группирует и вычисляет средний возраст только для них. Это уменьшает объём данных для группировки, база может использовать индекс на country и не делает лишних вычислений + +### Задача 4 — Исправь запрос + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; +``` + +**1. Перепиши запрос так, чтобы часть условий была в `WHERE`.** + +```sql +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +``` + +**2. Объясни, почему это быстрее.** + +Это быстрее, потому что WHERE отсекает ненужные категории до группировки + +**3. Нарисуй схему: сколько строк будет обработано в каждом случае.** + +Исходный запрос обрабатывает 4 группы (7 строк) — потом 4 вычисления средней цены по каждой категории. +Исправленный запрос фильтрует по условию — обрабатывает только 1 группу (3 строки) — 1 вычисление средней цены + +### Задача 5 — Исправь все ошибки + +```sql +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; +``` + +**1. Найди ошибку и исправь запрос.** + +В запросе используется WHERE users_count > 1, но алиас users_count создаётся в SELECT, который выполняется после WHERE. Кроме того, WHERE не может фильтровать по агрегатным функциям COUNT(*) + +```sql +SELECT country, COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; +``` + +### Задача 6 — Полный цикл выполнения + +```sql +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +``` + +**1. Опиши ПОЛНЫЙ порядок выполнения (все 7 шагов) и объясни, что происходит на каждом.** + +- **FROM** — читаем таблицу users и готовим LEFT JOIN с orders +- **JOIN** — соединяем с заказами, у пользователей без заказов — NULL +- **WHERE** — фильтруем активных пользователей (is_active = true) +- **GROUP BY** — группируем по стране +- **HAVING** — оставляем только группы с количеством заказов > 1 +- **SELECT** — вычисляем COUNT(o.id) и AVG(o.id) для каждой группы +- **ORDER BY** — сортируем по убыванию orders_count +- **LIMIT** — берём первые 3 строки + +### Задача 7 — Оптимизация + +Сравни два запроса: + +```sql +-- Запрос А +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +``` + +**1. Какой запрос быстрее? Почему?** + +Запрос Б быстрее. Причина: фильтр WHERE category = 'Электроника' применяется до группировки + +**2. Нарисуй таблицу: на каком шаге сколько строк обрабатывается в каждом запросе.** + +| Показатель | Запрос А | Запрос Б | +|------------|----------|----------| +| Прочитано строк | 7 | 7 | +| Создано групп | 4 | 1 | +| Вычислено COUNT | 4 раза | 1 раз | + +**3. Выполни оба с `EXPLAIN ANALYZE` и сравни `Execution Time`.** + +У запроса А Rows Removed by Filter: 3 после группировки, у запроса Б фильтр применяется при сканировании таблицы + +### Задача 8 — Многоэтапная фильтрация + +```sql +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +``` + +**1. Когда применяются условия `WHERE`?** + +Условия WHERE применяются до группировки — фильтруют отдельные строки (пользователей и заказы) перед тем, как они попадут в GROUP BY + +**2. Когда применяется условие `HAVING`?** + +Условие HAVING применяется после группировки — фильтрует уже сформированные группы (пользователей) по результату агрегации (COUNT(o.id)). + +**3. В чём разница между фильтрацией по `o.status` в `WHERE` и по `COUNT(o.id)` в `HAVING`?** + +WHERE o.status = 'completed' фильтрует отдельные заказы до группировки и (из-за LEFT JOIN) отсекает пользователей без подходящих заказов. HAVING COUNT(o.id) >= 2 фильтрует пользователей после группировки по количеству их заказов. + +### Задача 9 — JOIN и ORDER BY + +```sql +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +``` + +**1. На каком этапе применяется `ORDER BY`?** + +ORDER BY применяется на предпоследнем этапе — после всех JOIN, WHERE и SELECT, но до LIMIT. + +**2. Почему `LIMIT` идёт после `ORDER BY`, а не до?** + +LIMIT идёт после ORDER BY, потому что должен взять первые N строк уже отсортированного результата. + +**3. Что будет, если поменять их местами (технически невозможно, но подумай концептуально)?** + +Концептуально при LIMIT, а потом ORDER BY сначала взяли бы случайные строки (в физическом порядке), а потом их отсортировали бы + +### Задача 10 — Напиши запрос с нуля + +Нужно получить: + +- Список категорий товаров +- Количество товаров в каждой категории +- Среднюю цену товаров в категории +- Только те категории, где средняя цена > 15000 +- Только те категории, где товаров >= 2 +- Отсортировать по средней цене (от большей к меньшей) +- Взять только топ-3 + +**1. Напиши запрос** + +```sql +SELECT + category, + COUNT(*) AS products_count, + AVG(price) AS avg_price +FROM dmezentsev.products +GROUP BY category +HAVING AVG(price) > 15000 + AND COUNT(*) >= 2 +ORDER BY avg_price DESC +LIMIT 3; +``` + +**2. Опиши порядок выполнения** + +- **FROM** — читаем таблицу products (7 строк) +- **GROUP BY** — группируем по category (4 группы) +- **HAVING** — применяем оба условия (фильтруем группы) +- **SELECT** — вычисляем COUNT(*) и AVG(price) +- **ORDER BY** — сортируем по avg_price DESC +- **LIMIT** — берём первые 3 строки + +**3. Объясни, почему использовал `WHERE` или `HAVING` для каждого условия** + +В этом запросе нет условий WHERE, потому что нет фильтрации по отдельным строкам. Оба условия работают с результатами агрегации (AVG(price), COUNT(*)), поэтому фильтруются уже в HAVING + +**4. Выполни с `EXPLAIN ANALYZE` и посмотри план** + +``` +Seq Scan on products -- Читаем все 7 строк таблицы +HashAggregate -- Группируем по category (4 группы) +Filter: (avg(price) > 15000 AND count(*) >= 2) -- Применяем HAVING +Rows Removed by Filter: 3 -- 3 группы отброшены +Sort -- Сортируем результат по avg_price DESC +Limit -- Ограничиваем 3 строками +``` + +### Задача 11 — Подзапросы и порядок + +```sql +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'Россия' +ORDER BY orders_count DESC; +``` + +**1. Когда выполняется подзапрос в `SELECT`?** + +Подзапрос в SELECT выполняется для каждой строки после WHERE u.country = 'Россия', но до ORDER BY + +**2. Можно ли использовать алиас `orders_count` в `WHERE`?** + +Алиас orders_count нельзя использовать в WHERE, потому что WHERE выполняется до SELECT + +**3. Можно ли использовать его в `ORDER BY`?** + +Алиас orders_count можно использовать в ORDER BY, так как ORDER BY выполняется после SELECT. + +**4. Перепиши этот запрос через `JOIN`. Какой вариант быстрее? Проверь с `EXPLAIN ANALYZE`!** + +```sql +SELECT u.name, u.age, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.id, u.name, u.age +ORDER BY orders_count DESC; +``` + +На маленьких данных разница незаметна. Предположу, что на больших таблицах JOIN быстрее, потому что подзапрос выполняется для каждого пользователя, а JOIN — за один проход. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Transactions.md b/dmezentsev/postgresql/homework/Transactions.md new file mode 100755 index 0000000..c767031 --- /dev/null +++ b/dmezentsev/postgresql/homework/Transactions.md @@ -0,0 +1,68 @@ +### Задание 1. Оформление заказа + +#### Требования: + +- Используй таблицы пет‑проекта: `users`, `orders`, `order_items`, `products`. + - Заказ: пользователь покупает два разных товара. + - Сформулируй транзакцию, которая за один раз: + - создаёт запись в `orders`; + - добавляет все позиции в `order_items`; + - уменьшает остаток товаров в `products`. + +**1. Что в данных пойдёт не так, если часть шагов выполнится, а часть нет?** + +- Создан заказ, но не добавлены товары: заказ без позиций. Пользователь думает, что оформил заказ, а товары не зарезервированы. +- Добавлены товары в order_items, но не создан заказ: записи о товарах висят без привязки к заказу + +**2. Почему оформление заказа — типичный пример «либо всё, либо ничего»?** + +Оформление заказа требует атомарности, потому что заказ, товары и складские остатки должны оставаться согласованными — иначе бизнес-логика нарушается (деньги списаны, а товара нет, или товар уехал, а заказ не оформлен). + +### Задание 2. Ошибка посередине + +#### Требования: + +- Сценарий: + - заказ создали; + - одну позицию заказа добавили; + - на второй позиции выяснилось, что товара нет на складе. +- Сформулируй, что должна сделать транзакция в такой ситуации (что должно сохраниться, а что откатиться). + +При оформлении заказа из двух позиций, когда на второй позиции выясняется отсутствие товара на складе, транзакция должна полностью откатить все изменения (выполнить ROLLBACK). + +**1. Какие «битые» заказы можно получить без отката (`ROLLBACK`)?** + +- Заказ без позиций — запись в orders есть, но в order_items пусто. +- Заказ с частичным наполнением — первая позиция добавлена, вторая нет. Заказ существует, но неполный. +- Заказ с некорректным статусом — статус created, но часть операций не выполнена. +- Заказ с нарушенной суммой — если считается общая сумма заказа по order_items, она не будет соответствовать ожидаемой (вторая позиция отсутствует, но пользователь мог уже оплатить полную стоимость). + +**2. Как такие ошибки повлияют на отчёты и витрины по выручке?** + +- Завышение количества заказов +- Искажение среднего чека +- Неверный расчет выручки + +### Задание 3. Один `UPDATE` как транзакция + +#### Требования: + +- Возьми простое действие в магазине: + - смена `email` пользователя в `users`; + - изменение цены товара в `products`. +- Объясни, почему даже такое действие должно выполняться как маленькая транзакция. + +Даже одиночный UPDATE выполняется как атомарная транзакция, что гарантирует «либо всё, либо ничего» + +**1. Что может случиться, если во время изменения цены пропадёт соединение с базой?** + +- Соединение потеряно до начала выполнения: + Команда не дошла до сервера — изменения не произошли. +- Соединение потеряно в процессе выполнения: + Транзакция автоматически откатывается (ROLLBACK). +- Соединение потеряно после успешного выполнения: + Транзакция уже зафиксирована (COMMIT выполнен). + +**2. Почему хорошо, что база сама откатывает незавершённые изменения?** + +Это хорошо, потому что защищает от повреждения данных, упрощает разработку и гарантирует соблюдение ограничений целостности без ручного вмешательства. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/UNION.sql b/dmezentsev/postgresql/homework/UNION.sql new file mode 100644 index 0000000..9e2a3c8 --- /dev/null +++ b/dmezentsev/postgresql/homework/UNION.sql @@ -0,0 +1,107 @@ +-- Задание 1. Активные и неактивные пользователи в одном списке +SELECT id, + name, + CASE + WHEN is_active THEN 'active' + END AS user_status +FROM dmezentsev.users +WHERE is_active = TRUE + +UNION ALL + +SELECT id, + name, + CASE + WHEN NOT is_active THEN 'inactive' + END AS user_status +FROM dmezentsev.users +WHERE is_active = FALSE + +ORDER BY name; + +/* - Что будет, если вместо UNION ALL написать UNION? + +Кол-во строк при UNION и UNION ALL не изменится, потому что все строки +в таблице users уникальны + +- Зачем в результат добавлять колонку user_status, если флаг активности +уже есть в таблице users? + +Предположу, что добавляем колонку user_status только лишь для удобства +понимания результирующей таблицы всем тем, кто далек от TRUE или FALSE. + */ + +-- Задание 2. Заказы до и после даты в одной ленте + +SELECT + id AS order_id, + user_id, + order_date, + 'old' AS period +FROM dmezentsev.orders +WHERE order_date < '2024-01-01' + +UNION ALL + +SELECT + id AS order_id, + user_id, + order_date, + 'new' AS period +FROM dmezentsev.orders +WHERE order_date >= '2024-01-01' + +ORDER BY order_date, order_id; + +/* - Как ты по колонке period поймёшь, сколько заказов было + до и после даты‑границы (словами, без SQL)? + +Нужно посчитать количество строк, где в колонке period стоит +значение 'old' — это и будет число заказов до 2024-01-01. +А количество строк со значением 'new' — это число заказов начиная с 2024-01-01 и позже. + +- Какой подход тебе кажется понятнее для новичка в команде: +два SELECT + UNION ALL или один SELECT с CASE WHEN? Почему? + +Вот совсем для новичка, пожалуй, вариант с UNION ALL выглядит понятнее. +Два простых запроса, которые потом объединены в общую таблицу, к тому же +их можно выполнить отдельно + */ + +-- Задание 3. Товары, которые покупали, и которые не покупали + +SELECT DISTINCT + p.id AS product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM dmezentsev.products p +JOIN dmezentsev.order_items oi ON p.id = oi.product_id + +UNION ALL + +SELECT + p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM dmezentsev.products p +LEFT JOIN dmezentsev.order_items oi ON p.id = oi.product_id +WHERE oi.product_id IS NULL + +ORDER BY product_name; + +/* - Почему в запросе на «не покупали» проверка IS NULL должна быть +по полю из order_items, а не из products? + +LEFT JOIN гарантирует, что все строки из products останутся в результате. +Если для товара нет записей в order_items, то все колонки из order_items будут NULL. + +- Какой сигнал для бизнеса даёт список с product_status = 'never_ordered'  +(1–2 идеи: что можно с этими товарами сделать)? + +Список never_ordered даст понимание бизнесу, что либо товар не востребован +(очень дорогой, непродающее описание), либо о товаре никто не знает (проблемы +с рекламой и продвижением). То есть решив эти кейсы, ситуацию можно изменить +в лучшую сторону. +*/ + + \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Views.sql b/dmezentsev/postgresql/homework/Views.sql new file mode 100755 index 0000000..6dbcc00 --- /dev/null +++ b/dmezentsev/postgresql/homework/Views.sql @@ -0,0 +1,131 @@ +-- Задание 1. Витрина выручки по категориям + +/* Требования: + +- Используй таблицы: `products`, `orders`, `order_items`. +- Учитывай только оплаченные заказы (`status = 'paid'`). +- Создай `MATERIALIZED VIEW` `mv_category_revenue` со столбцами: + - `category` + - `total_revenue` — суммарная выручка по категории (`quantity * price_at_purchase`) +- Отсортируй результат по `total_revenue` по убыванию при выборке. +*/ + +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM dmezentsev.products p +JOIN dmezentsev.orders_items oi ON p.id = oi.product_id +JOIN dmezentsev.orders o ON oi.order_id = o.id +GROUP BY p.category +WHERE o.status = 'paid' +ORDER BY total_revenue DESC + + +/* 1. Чем удобно иметь готовую витрину по категориям, вместо того чтобы каждый раз писать JOIN по трём таблицам? +- Производительность — данные уже агрегированы и лежат в одной материализованной таблице. Вместо сканирования +трёх таблиц и группировки, запрос читает готовую таблицу. +- Простота запросов — аналитик пишет SELECT * FROM mv_category_revenue, не вникая в схему и условия фильтрации. +*/ + + +-- Задание 2. Витрина активности пользователей + +/* Требования: + +- Используй таблицы: `users`, `orders`. +- Считай только оплаченные заказы. +- Создай MATERIALIZED VIEW **`mv_user_activity`** со столбцами: + - `user_id` + - `user_name` + - `orders_count` — сколько оплаченных заказов сделал пользователь +- В выборке покажи только тех, у кого `orders_count > 0`, отсортируй по `orders_count` по убыванию. +*/ + +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.user_id, + u.user_name, + COUNT(o.order_id) AS orders_count +FROM users u +JOIN orders o ON u.user_id = o.user_id +WHERE o.status = 'paid' +GROUP BY u.user_id, u.user_name +HAVING COUNT(o.order_id) > 0 +ORDER BY orders_count DESC; + +/* 1. Как по такой витрине найти «самых лояльных» клиентов? + +SELECT * FROM mv_user_activity LIMIT 3; + +(Предположил ради примера, что самые лояльные - это топ-3 клиента) + +2. Что будет, если считать активность не по всем заказам, а только за последние 30 дней +— как изменится смысл витрины? + +Смысл витрины изменится на отслеживание текущей (недавней) активности, а не за весь период времени в данных +*/ + + +-- Задание 3. Витрина выручки по дням + +/* Требования: + +- Используй таблицы: `orders`, `order_items`. +- Учитывай только оплаченные заказы. +- Создай `MATERIALIZED VIEW` **`mv_daily_revenue`** со столбцами: + - `order_date::date` как `sales_date` + - `daily_revenue` — суммарная выручка за день +- При выборке отсортируй по `sales_date` по возрастанию. +*/ + +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM orders o +JOIN order_items oi ON o.id = oi.order_id +WHERE o.status = 'paid' +GROUP BY o.order_date::date +ORDER BY sales_date ASC; + +/* 1. Как по такой витрине заметить «просадки» и «пики» продаж? + +- Построить линейный график зависимости daily_revenue от sales_date +- Использовать оконные функции для сравнения со скользящим средним (например, за 7 дней) +- Вычислить оконной функцией LAG и разницу с предыдущим днём, чтобы увидеть изменения. + +2. Что быстрее: считать выручку «на лету» из сырых таблиц или читать из `mv_daily_revenue` ? Почему? + +Чтение из mv_daily_revenue быстрее, потому что: +- В mv_daily_revenue меньше строк, так как данные агрегированы. Сырые таблицы могут содержать +миллионы записей — каждый день приходится заново агрегировать все данные. +*/ + +-- Задание 4. Обновление витрин + +/* Требования: + +- Для любой одной из созданных витрин (на выбор ученика) написать команду обновления с `REFRESH MATERIALIZED VIEW ...`. +*/ + +REFRESH MATERIALIZED VIEW mv_daily_revenue; + + +/* - Придумать расписание обновления витрины для двух сценариев: + - интернет‑магазин с несколькими заказами в день; + - крупный магазин с тысячами заказов в час. + +Интернет‑магазин с несколькими заказами в день — 1 раз в сутки, ночью +За день накапливается мало новых данных, достаточно обновляться раз в сутки + +Крупный магазин с тысячами заказов в час — каждые 30 минут +Обновление за 30 минут успевает до следующего цикла + +1. Чем грозит слишком редкий `REFRESH` для отчётов (какие ошибки могут появиться в анализе)? +- Устаревшие данные — угроза применения ошибочных мер. +- Пропуск аномалий — если витрина обновляется редко, аномалии можно не заметить до следующего REFRESH + +2. Почему нельзя обновлять витрины «каждую секунду», даже если хочется всегда свежие данные? +- Ресурсы БД — каждый REFRESH блокирует чтение и перестраивает всю витрину заново +- Проблемы с блокировками — если кто-то читает витрину во время REFRESH, запрос встанет в очередь или получит ошибку. +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Window_functions.sql b/dmezentsev/postgresql/homework/Window_functions.sql new file mode 100644 index 0000000..e2b773c --- /dev/null +++ b/dmezentsev/postgresql/homework/Window_functions.sql @@ -0,0 +1,209 @@ +-- Задача 1. Нумерация заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS order_number_for_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - В чём отличие такой нумерации от простой сортировки  +ORDER BY order_date без оконной функции? + +В случае с оконной функцией идет сортировка по order_date в рамках одного окна +(по пользователю), и каждому заказу пользователя присваивается свой порядковый +номер. При простой сортировке без оконной функции по дате сортировались бы +все заказы +*/ + +-- Задача 2. Накопительная выручка по пользователю + +WITH order_totals AS ( + SELECT o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.orders o + JOIN dmezentsev.users u ON o.user_id = u.id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY o.user_id, u.name, o.id, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + SUM(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_revenue_for_user +FROM order_totals; + +/* - Почему для накопительной суммы важно задавать рамку окна +(от «самого первого» до «текущего»), а не оставлять значения по умолчанию? + +По умолчанию RANGE BETWEEN ... Если у пользователя два заказа +в один и тот же день, то накопительная сумма посчитается так: + 1 января 100 руб Накопительная сумма: 300 + 1 января 200 руб Накопительная сумма: 300 + +Важно задавать явно ROWS BETWEEN..., чтобы накопительная сумма считалась +правильно: + 1 января 100 руб Накопительная сумма: 100 + 1 января 200 руб Накопительная сумма: 300 + +- Как по такой таблице понять, что пользователь начал тратить больше? + +Если накопительная сумма растет, то это указывает на то, что +пользователь с течением времени тратит больше +*/ + +-- Задача 3. Рейтинг пользователей по выручке + +WITH user_revenue AS ( + SELECT u.id AS user_id, + u.name AS user_name, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.name, u.id +) + + SELECT user_id, + user_name, + total_revenue, + RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY total_revenue DESC) AS revenue_dense_rank +FROM user_revenue; + +/* - Чем RANK отличается от DENSE_RANK в случае, когда +у двух пользователей одинаковая выручка? + +Допустим, у двух таких пользователей будет 1ое место у каждого по выручке. +При RANK у следующего будет 3ье место, при DENSE_RANK - 2ое. + +RANK пропускает следующие числовые позиции после одинаковых +значений (например: 1, 2, 2, 4), а DENSE_RANK не пропускает (например: 1, 2, 2, 3). + */ + +-- Задача 4. Первый и последний заказ пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Как можно использовать поля «первый заказ» и «последний заказ» +в сегментации клиентов? + +Для определения частоты покупок (разница между датами), выделения +новых или ушедших клиентов (например, клиенты без заказов за последние 3 месяца) + +- Почему для правильного LAST_VALUE важно задать рамку окна так, чтобы смотреть +на всю историю пользователя, а не только до текущей строки? + +Потому что по умолчанию рамка окна — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, +и LAST_VALUE показывает последнее значение в текущей рамке, т.е. текущую строку. +А нужно по всей рамке, от первой до последней даты у каждого пользователя + */ + +-- Задача 5. Заказы и количество заказов пользователя + +SELECT u.id AS user_id, + u.name AS user_name, + o.id AS order_id, + COUNT(o.id) OVER (PARTITION BY u.id) AS orders_per_user +FROM dmezentsev.users u +JOIN dmezentsev.orders o ON u.id = o.user_id; + +/* - Чем такой результат отличается от классического отчёта +«по пользователям» с GROUP BY user_id? + +При применении оконной функции строки не схлопываются, то есть если у +пользователя несколько заказов, то по каждому заказу будет строка с данными, +и в каждой строке будет общее число заказов + */ + +-- Задача 6. Сравнение суммы заказа с предыдущим + +WITH orders_total AS ( + SELECT u.id AS user_id, + o.id AS order_id, + u.name AS user_name, + o.order_date, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM dmezentsev.users u + JOIN dmezentsev.orders o ON u.id = o.user_id + JOIN dmezentsev.order_items oi ON o.id = oi.order_id + GROUP BY u.id, o.id, u.name, o.order_date +) + +SELECT user_id, + user_name, + order_id, + order_date, + order_revenue, + LAG(order_revenue) OVER ( + PARTITION BY user_id + ORDER BY order_date + ) AS prev_order_revenue +FROM orders_total; + +/* - Как по такому результату понять, +увеличивается или уменьшается средний чек с течением времени? + +Нужно сравнить order_revenue с prev_order_revenue для каждого заказа. +Если чаще order_revenue > prev_order_revenue — средний чек растёт. +Если чаще order_revenue < prev_order_revenue — средний чек падает. +sql + */ + +-- Задача 7. Категории с накопительной выручкой по времени + +WITH category_revenue AS ( + SELECT p.category, + DATE_TRUNC('day', o.order_date)::DATE AS order_date, + SUM(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM dmezentsev.order_items oi + JOIN dmezentsev.products p ON oi.product_id = p.id + JOIN dmezentsev.orders o ON oi.order_id = o.id + GROUP BY p.category, + DATE_TRUNC('day', o.order_date)::DATE +) + +SELECT category, + order_date, + daily_category_revenue, + SUM(daily_category_revenue) OVER ( + PARTITION BY category + ORDER BY order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS running_category_revenue +FROM category_revenue; + +/* - Чем такой «накопительный» график по категории отличается по смыслу +от обычного ежедневного (без SUM OVER)? + +Обычный график daily_category_revenue показывает, какая выручка категории день ото дня. +Накопительный график running_category_revenue показывает общий рост +или падение категории за период собранных данных + */ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/base.sql b/dmezentsev/postgresql/homework/base.sql new file mode 100644 index 0000000..b5833e4 --- /dev/null +++ b/dmezentsev/postgresql/homework/base.sql @@ -0,0 +1,73 @@ +-- Задание 1 — Спроектируй «карточку пользователя» + +SELECT id, + name, + country, + created_at, + email +FROM dmezentsev.users u +ORDER BY created_at DESC, name; + +/* - Почему выбрал именно эти столбцы и зачем такая сортировка? + + Сортировка нужна для того, чтобы вывести пользователей с датой регистрации по убыванию. + Если даты бы совпадали, то была дополнительная сортировка по имени внутри одной даты + */ + +-- Задание 2 — Сегментация по странам (с выбором) + +SELECT id, + name, + country +FROM dmezentsev.users u +WHERE country = 'US' +ORDER BY name; + +SELECT id, + name, + country +FROM dmezentsev.users u +WHERE country = 'UK' +ORDER BY name; + +/* - Какой из двух запросов маркетологу полезнее и почему? + + Предположу, что маркетологу выгоднее рассматривать пользователей из US, + потому что их больше (5 US > 3 UK) + */ + +-- Задание 3 — Контакты для рассылки + +SELECT id, + name, + email +FROM dmezentsev.users u +WHERE email IS NOT NULL +ORDER BY name; + +SELECT DISTINCT(LOWER(email)) email +FROM dmezentsev.users u +WHERE email IS NOT NULL; + +/* - Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? + +Нужно приводить email к одному виду, потому что хранение одного и того же email +в разных регистрах приведет к дублированию данных. +По факту же один пользователь - один email +*/ + +-- Задание 4 — Новички и активность + +SELECT id, + name, + created_at +FROM dmezentsev.users u +WHERE created_at < '2024-03-11' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; + +/* - В каком реальном сценарии тебе пригодится OFFSET? + +Предположу, OFFSET понадобится, когда нужно убрать из выборки +какой-то топ пользователей. Допустим, нужны все, кроме первой тройки + */ diff --git a/dmezentsev/postgresql/pet_project/DDL_DML.sql b/dmezentsev/postgresql/pet_project/DDL_DML.sql new file mode 100644 index 0000000..498ef54 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/DDL_DML.sql @@ -0,0 +1,205 @@ +DROP TABLE IF EXISTS dmezentsev.lesson_views; +DROP TABLE IF EXISTS dmezentsev.enrollments; +DROP TABLE IF EXISTS dmezentsev.lessons; +DROP TABLE IF EXISTS dmezentsev.users_; +DROP TABLE IF EXISTS dmezentsev.courses; + +CREATE TABLE dmezentsev.users_( + id BIGINT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age SMALLINT, + email VARCHAR(100) UNIQUE, + registration_date DATE NOT NULL +); + +CREATE TABLE dmezentsev.courses ( + id BIGINT PRIMARY KEY, + title varchar(200) NOT NULL, + category VARCHAR(100) NOT NULL, + created_at DATE NOT NULL +); + +CREATE TABLE dmezentsev.lessons ( + id BIGINT PRIMARY KEY, + course_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + duration_min SMALLINT, + + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) +); + +CREATE TABLE dmezentsev.enrollments ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + course_id BIGINT NOT NULL, + enrolled_at DATE, + + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (course_id) REFERENCES dmezentsev.courses(id) +); + +CREATE TABLE dmezentsev.lesson_views ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + lesson_id BIGINT NOT NULL, + viewed_at TIMESTAMPTZ, + + FOREIGN KEY (user_id) REFERENCES dmezentsev.users_(id), + FOREIGN KEY (lesson_id) REFERENCES dmezentsev.lessons(id) +); + +INSERT INTO dmezentsev.users_ (id, name, age, email, registration_date) VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'), +(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-05'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-12'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-20'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-10'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-15'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-20'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-15'), +(13, 'Mia', 26, 'mia@gmail.com', '2023-01-18'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-08'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-10'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-15'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-20'), +(18, 'Rita', 27, 'rita@mail.com', '2023-04-01'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-10'), +(20, 'Tina', 25, 'tina@mail.com', '2023-04-18'); + +INSERT INTO dmezentsev.courses (id, title, category, created_at) VALUES +(1, 'SQL для начинающих', 'data', '2023-01-01'), +(2, 'Python для анализа данных', 'programming', '2023-01-15'), +(3, 'BI с нуля', 'business', '2023-03-01'), +(4, 'Excel для аналитиков', 'business', '2023-02-01'), +(5, 'Основы машинного обучения', 'data', '2023-03-15'); + +INSERT INTO dmezentsev.lessons (id, course_id, title, duration_min) VALUES +(1, 1, 'SELECT и FROM', 10), +(2, 1, 'JOIN', 15), +(3, 1, 'WHERE и фильтрация', 12), +(4, 1, 'Агрегации и GROUP BY', 18), +(5, 1, 'Подзапросы', 25), +(6, 2, 'Pandas основы', 20), +(7, 2, 'NumPy', 15), +(8, 2, 'Визуализация данных', 22), +(9, 3, 'Основы BI', 12), +(10, 3, 'Метрики и дашборды', 18), +(11, 3, 'Power BI введение', 20), +(12, 4, 'Формулы в Excel', 15), +(13, 4, 'Сводные таблицы', 18), +(14, 4, 'Графики и диаграммы', 14), +(15, 5, 'Введение в ML', 20), +(16, 5, 'Линейная регрессия', 30), +(17, 5, 'Классификация', 28), +(18, 5, 'Кластеризация', 25); + +INSERT INTO dmezentsev.enrollments (id, user_id, course_id, enrolled_at) VALUES +(1, 1, 1, '2023-01-15'), +(2, 1, 2, '2023-02-01'), +(3, 2, 1, '2023-01-20'), +(4, 3, 2, '2023-03-05'), +(5, 4, 3, '2023-04-01'), +(6, 6, 1, '2023-01-06'), +(7, 6, 2, '2023-01-10'), +(8, 6, 4, '2023-02-05'), +(9, 7, 1, '2023-01-13'), +(10, 7, 3, '2023-03-10'), +(11, 8, 1, '2023-01-22'), +(12, 8, 2, '2023-01-22'), +(13, 8, 3, '2023-01-22'), +(14, 9, 4, '2023-02-12'), +(15, 10, 1, '2023-04-16'), +(16, 11, 2, '2023-04-21'), +(17, 12, 1, '2023-01-16'), +(18, 13, 2, '2023-01-19'), +(19, 13, 4, '2023-02-20'), +(20, 14, 1, '2023-01-09'), +(21, 14, 2, '2023-01-09'), +(22, 14, 3, '2023-01-09'), +(23, 14, 4, '2023-02-01'), +(24, 14, 5, '2023-03-20'), +(25, 15, 1, '2023-01-11'), +(26, 15, 3, '2023-03-05'), +(27, 16, 2, '2023-02-16'), +(28, 16, 4, '2023-02-16'), +(29, 17, 3, '2023-03-22'), +(30, 17, 5, '2023-03-25'), +(31, 18, 1, '2023-04-02'), +(32, 19, 1, '2023-04-11'), +(33, 19, 2, '2023-04-11'), +(34, 20, 2, '2023-04-19'), +(35, 5, 5, '2023-03-20'), +(36, 1, 4, '2023-02-10'), +(37, 2, 3, '2023-03-15'), +(38, 3, 1, '2023-02-25'), +(39, 4, 2, '2023-03-10'), +(40, 6, 3, '2023-03-08'), +(41, 7, 2, '2023-02-01'), +(42, 9, 1, '2023-02-15'), +(43, 12, 3, '2023-03-10'), +(44, 15, 2, '2023-02-01'), +(45, 18, 4, '2023-04-05'); + +INSERT INTO dmezentsev.lesson_views (id, user_id, lesson_id, viewed_at) VALUES +(1, 1, 1, '2023-01-16 10:00:00'), +(2, 1, 2, '2023-01-16 10:15:00'), +(3, 1, 3, '2023-01-17 09:00:00'), +(4, 1, 4, '2023-01-17 09:30:00'), +(5, 1, 4, '2023-01-17 11:00:00'), +(6, 1, 4, '2023-01-18 10:00:00'), +(7, 1, 6, '2023-02-02 14:00:00'), +(8, 1, 12, '2023-02-15 10:00:00'), +(9, 1, 13, '2023-02-15 10:30:00'), +(10, 2, 1, '2023-01-21 09:00:00'), +(11, 2, 9, '2023-03-16 10:00:00'), +(12, 2, 10, '2023-03-16 10:30:00'), +(13, 3, 6, '2023-03-06 12:00:00'), +(14, 3, 1, '2023-02-26 11:00:00'), +(15, 3, 2, '2023-02-26 11:20:00'), +(16, 4, 9, '2023-04-02 10:00:00'), +(17, 4, 10, '2023-04-03 10:00:00'), +(18, 4, 6, '2023-03-12 14:00:00'), +(19, 4, 7, '2023-03-13 09:00:00'), +(20, 5, 15, '2023-03-21 10:00:00'), +(21, 5, 16, '2023-03-22 11:00:00'), +(22, 6, 1, '2023-01-07 10:00:00'), +(23, 6, 2, '2023-01-07 10:15:00'), +(24, 6, 3, '2023-01-08 09:00:00'), +(25, 6, 4, '2023-01-08 09:30:00'), +(26, 6, 5, '2023-01-09 10:00:00'), +(27, 6, 6, '2023-01-11 14:00:00'), +(28, 6, 7, '2023-01-12 10:00:00'), +(29, 6, 8, '2023-01-13 11:00:00'), +(30, 6, 12, '2023-02-06 09:00:00'), +(31, 6, 13, '2023-02-06 09:30:00'), +(32, 6, 14, '2023-02-07 10:00:00'), +(33, 6, 9, '2023-03-09 11:00:00'), +(34, 7, 1, '2023-01-14 10:00:00'), +(35, 7, 2, '2023-01-14 10:20:00'), +(36, 7, 3, '2023-01-15 09:00:00'), +(37, 7, 9, '2023-03-11 14:00:00'), +(38, 7, 10, '2023-03-11 14:30:00'), +(39, 9, 12, '2023-02-13 10:00:00'), +(40, 9, 13, '2023-02-13 10:30:00'), +(41, 9, 1, '2023-02-16 11:00:00'), +(42, 12, 1, '2023-01-17 10:00:00'), +(43, 12, 2, '2023-01-17 10:20:00'), +(44, 12, 1, '2023-04-10 11:00:00'), +(45, 12, 3, '2023-04-10 11:30:00'), +(46, 12, 9, '2023-03-12 10:00:00'), +(47, 13, 6, '2023-01-20 14:00:00'), +(48, 13, 7, '2023-01-21 10:00:00'), +(49, 13, 12, '2023-02-21 09:00:00'), +(50, 14, 1, '2023-01-09 08:00:00'), +(51, 14, 2, '2023-01-09 08:15:00'), +(52, 14, 3, '2023-01-10 10:00:00'), +(53, 14, 4, '2023-01-10 10:30:00'), +(54, 14, 5, '2023-01-11 09:00:00'), +(55, 14, 6, '2023-01-10 14:00:00'), +(56, 14, 7, '2023-01-11 10:00:00'), +(57, 14, 8, '2023-01-12 11:00:00'), +(58, 14, 9, '2023-01-10 08:00:00'); \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql new file mode 100644 index 0000000..493f553 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_conversion_funnel.sql @@ -0,0 +1,105 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.conversion_funnel; +CREATE MATERIALIZED VIEW dmezentsev.conversion_funnel AS +WITH progress AS ( + -- Прогресс каждого пользователя по каждому курсу + SELECT + e.course_id, + e.user_id, + COUNT(DISTINCT l.id) AS total_lessons, + COUNT(DISTINCT lv.lesson_id) AS viewed_lessons, + ROUND(COUNT(DISTINCT lv.lesson_id) * 100.0 / NULLIF(COUNT(DISTINCT l.id), 0), 2) AS viewed_pct + FROM dmezentsev.enrollments e + LEFT JOIN dmezentsev.lessons l ON e.course_id = l.course_id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id AND e.user_id = lv.user_id + GROUP BY e.course_id, e.user_id +), + +funnel_stats AS ( + -- Статистика по каждому курсу для всех этапов воронки + SELECT + c.id AS course_id, + c.title AS course_title, + (SELECT COUNT(*) FROM dmezentsev.users_) AS registered_count, + COUNT(DISTINCT e.user_id) AS enrolled_count, + COUNT(DISTINCT CASE WHEN p.viewed_lessons > 0 THEN e.user_id END) AS started_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct >= 50 THEN e.user_id END) AS mid_progress_count, + COUNT(DISTINCT CASE WHEN p.viewed_pct = 100 THEN e.user_id END) AS completed_count + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + LEFT JOIN progress p ON e.course_id = p.course_id AND e.user_id = p.user_id + GROUP BY c.id, c.title +) + +-- Соединяем этапы с помощью UNION ALL +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start +FROM ( + -- Этап 1: registered + SELECT + course_id, + course_title, + 'registered' AS stage_name, + registered_count AS users_count, + 0 AS conversion_from_previous, + 100.00 AS conversion_from_start, + 1 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 2: enrolled + SELECT + course_id, + course_title, + 'enrolled', + enrolled_count, + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + ROUND(100.0 * enrolled_count / NULLIF(registered_count, 0), 2), + 2 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 3: started + SELECT + course_id, + course_title, + 'started', + started_count, + ROUND(100.0 * started_count / NULLIF(enrolled_count, 0), 2), + ROUND(100.0 * started_count / NULLIF(registered_count, 0), 2), + 3 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 4: mid_progress + SELECT + course_id, + course_title, + 'mid_progress', + mid_progress_count, + ROUND(100.0 * mid_progress_count / NULLIF(started_count, 0), 2), + ROUND(100.0 * mid_progress_count / NULLIF(registered_count, 0), 2), + 4 AS stage_order + FROM funnel_stats + + UNION ALL + + -- Этап 5: completed + SELECT + course_id, + course_title, + 'completed', + completed_count, + ROUND(100.0 * completed_count / NULLIF(mid_progress_count, 0), 2), + ROUND(100.0 * completed_count / NULLIF(registered_count, 0), 2), + 5 AS stage_order + FROM funnel_stats +) AS funnel_rows +ORDER BY course_id, stage_order; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_course_summary.sql b/dmezentsev/postgresql/pet_project/dm_course_summary.sql new file mode 100644 index 0000000..86a65b3 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_course_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.course_summary; +CREATE MATERIALIZED VIEW dmezentsev.course_summary AS +WITH count_users AS ( + -- Кол-во уникальных пользователей в курсах + SELECT + c.id AS course_id, + c.title AS course_title, + COUNT(DISTINCT e.user_id) AS unique_users + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id, c.title +), + count_lessons AS ( + -- Кол-во уроков в курсах + SELECT + course_id, + COUNT(id) AS lessons_count + FROM dmezentsev.lessons + GROUP BY course_id +), + views_count AS ( + -- Суммарное число просмотров по каждому курсу + SELECT + l.course_id, + COUNT(lv.id) AS total_views, + MIN(lv.viewed_at) AS first_viewed_at, + MAX(lv.viewed_at) AS last_viewed_at + FROM dmezentsev.lessons l + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.course_id +) + +SELECT + cu.course_id, + cu.course_title, + cu.unique_users, + cl.lessons_count, + vc.total_views, + ROUND(COALESCE(vc.total_views::numeric / + cu.unique_users, 0), 2) AS avg_views_per_user, + vc.first_viewed_at, + vc.last_viewed_at +FROM count_users cu +LEFT JOIN count_lessons cl USING (course_id) +LEFT JOIN views_count vc USING (course_id) +ORDER BY cu.course_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql new file mode 100644 index 0000000..50bf2bb --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_lesson_popularity_ranking.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.lesson_popularity_ranking; +CREATE MATERIALIZED VIEW dmezentsev.lesson_popularity_ranking AS +WITH +course_enrollment AS ( + -- Сколько студентов записано на каждый курс + SELECT + c.id AS course_id, + COUNT(DISTINCT e.user_id) AS enrolled_to_course + FROM dmezentsev.courses c + LEFT JOIN dmezentsev.enrollments e ON c.id = e.course_id + GROUP BY c.id +), + +lesson_summary AS ( + -- Статистика по урокам + SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.id AS course_id, + c.title AS course_title, + COUNT(lv.id) AS total_views, + COUNT(DISTINCT lv.user_id) AS unique_viewers, + ROUND( + COUNT(lv.id)::NUMERIC / NULLIF(COUNT(DISTINCT lv.user_id), 0), + 2 + ) AS avg_views_per_viewer + FROM dmezentsev.lessons l + JOIN dmezentsev.courses c ON l.course_id = c.id + LEFT JOIN dmezentsev.lesson_views lv ON l.id = lv.lesson_id + GROUP BY l.id, l.title, c.id, course_id, c.title +) + +SELECT + ls.lesson_id, + ls.lesson_title, + ls.course_title, + ls.total_views, + ls.unique_viewers, + ls.avg_views_per_viewer, + DENSE_RANK() OVER (ORDER BY ls.total_views DESC) AS popularity_rank, + ROUND( + ls.unique_viewers::NUMERIC / NULLIF(ce.enrolled_to_course, 0) * 100, 2 + ) AS completion_rate +FROM lesson_summary ls +LEFT JOIN course_enrollment ce ON ls.course_id = ce.course_id +ORDER BY popularity_rank; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_platform_summary.sql b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql new file mode 100644 index 0000000..a276ed0 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_platform_summary.sql @@ -0,0 +1,19 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.platform_summary; +CREATE MATERIALIZED VIEW dmezentsev.platform_summary AS +WITH summary AS ( + SELECT + (SELECT COUNT(*) FROM dmezentsev.users_) AS total_users, + (SELECT COUNT(*) FROM dmezentsev.courses) AS total_courses, + (SELECT COUNT(*) FROM dmezentsev.lessons) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM dmezentsev.lesson_views) AS users_with_views, + (SELECT COUNT(*) FROM dmezentsev.lesson_views) AS total_views +) + +SELECT + total_users, + total_courses, + total_lessons, + users_with_views, + ROUND(total_lessons::numeric / total_courses, 2) AS avg_lessons_per_course, + ROUND(total_views::numeric / total_lessons, 2) AS avg_views_per_lesson +FROM summary; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql new file mode 100644 index 0000000..af4bd02 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_activity_summary.sql @@ -0,0 +1,46 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_activity_summary; +CREATE MATERIALIZED VIEW dmezentsev.user_activity_summary AS +WITH user_enroll AS ( + -- На сколько курсов записался пользователь + SELECT + u.id AS user_id, + COUNT(e.id) AS enrollment_count + FROM dmezentsev.users_ u + JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY u.id +), + user_views AS ( + -- Сколько уроков в курсах посмотрел пользователь, и даты + -- первого и последнего просмотра + SELECT + user_id, + COUNT(DISTINCT lesson_id) viewed_lessons_count, + MIN(viewed_at) AS first_view, + MAX(viewed_at) AS last_view + FROM dmezentsev.lesson_views lv + GROUP BY lv.user_id +), +all_lessons AS ( + -- сколько уроков в курсах доступно пользователю + SELECT + e.user_id, + COUNT(l.id) AS possible_lessons + FROM dmezentsev.enrollments e + JOIN dmezentsev.courses c ON e.course_id = c.id + JOIN dmezentsev.lessons l ON c.id = l.course_id + GROUP BY e.user_id +) + +SELECT + u.id AS user_id, + u.name, + ue.enrollment_count, + COALESCE(uv.viewed_lessons_count, 0) AS viewed_lessons_count, + uv.last_view, + uv.first_view, + ROUND(uv.viewed_lessons_count::numeric / NULLIF(al.possible_lessons, 0) * 100, 2) AS engagement_rate +FROM dmezentsev.users_ u +JOIN user_enroll ue ON ue.user_id = u.id +JOIN all_lessons al ON al.user_id = u.id +LEFT JOIN user_views uv ON uv.user_id = u.id +ORDER BY user_id; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql new file mode 100644 index 0000000..27616d7 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql @@ -0,0 +1,77 @@ +DROP MATERIALIZED VIEW IF EXISTS dmezentsev.user_cohort_analysis; +CREATE MATERIALIZED VIEW dmezentsev.user_cohort_analysis AS +WITH users_in_cohort AS ( + -- Кол-во зарегистрированных пользователей по когортам + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS users_in_cohort + FROM dmezentsev.users_ u + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + enrolled_users AS ( + -- Кол-во пользователей по когортам, записавшиеся хотя бы на 1 курс + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS enrolled_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.enrollments e + WHERE u.id = e.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + active_users AS ( + -- Кол-во пользователей по когортам, посмотревшие хотя бы 1 урок + SELECT + TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') AS cohort_month, + COUNT(*) AS active_users + FROM dmezentsev.users_ u + WHERE EXISTS (SELECT 1 + FROM dmezentsev.lesson_views lv + WHERE u.id = lv.user_id) + GROUP BY TO_CHAR(DATE_TRUNC('month', registration_date), 'YYYY-MM') +), + avg_courses AS ( + -- Среднее количество курсов на пользователя в когорте + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(COUNT(e.course_id)::numeric / COUNT(DISTINCT u.id), 2) AS avg_courses_per_user + FROM dmezentsev.users_ u + LEFT JOIN dmezentsev.enrollments e ON u.id = e.user_id + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +), +avg_time_to_first_view AS ( + -- Среднее время от регистрации до первого просмотра + SELECT + TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') AS cohort_month, + ROUND(AVG(lv.first_view_date::date - u.registration_date), 2) AS avg_time_to_first_view + FROM dmezentsev.users_ u + INNER JOIN ( + SELECT + user_id, + MIN(viewed_at) AS first_view_date + FROM dmezentsev.lesson_views + GROUP BY user_id + ) lv ON u.id = lv.user_id + -- у пользователя Bob, id = 2 дата первого просмотра раньше даты регистрации, отфильтровал его + WHERE lv.first_view_date >= u.registration_date + GROUP BY TO_CHAR(DATE_TRUNC('month', u.registration_date), 'YYYY-MM') +) + +SELECT + uin.cohort_month, + uin.users_in_cohort, + COALESCE(eu.enrolled_users, 0) AS enrolled_users, + COALESCE(au.active_users, 0) AS active_users, + CASE + WHEN uin.users_in_cohort > 0 THEN + ROUND(100.0 * COALESCE(au.active_users, 0) / uin.users_in_cohort, 2) + ELSE 0 + END AS retention_rate, + ac.avg_courses_per_user, + att.avg_time_to_first_view +FROM users_in_cohort uin +LEFT JOIN enrolled_users eu USING (cohort_month) +LEFT JOIN active_users au USING (cohort_month) +LEFT JOIN avg_courses ac USING (cohort_month) +LEFT JOIN avg_time_to_first_view att USING (cohort_month) +ORDER BY uin.cohort_month; \ No newline at end of file diff --git a/dmezentsev/postgresql/pet_project/extra.sql b/dmezentsev/postgresql/pet_project/extra.sql new file mode 100644 index 0000000..aeeb803 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/extra.sql @@ -0,0 +1,59 @@ +-- Задание 1: Анализ drop-off точек +SELECT + cf.course_id, + c.title AS course_title, + cf.stage_name, + cf.users_count, + ROUND(100 - cf.conversion_from_previous, 2) AS dropoff_percentage +FROM dmezentsev.conversion_funnel cf +JOIN dmezentsev.courses c ON cf.course_id = c.id +WHERE cf.stage_name = 'started' +ORDER BY dropoff_percentage DESC; + +-- Задание 2: Топ-3 урока с повторными просмотрами +SELECT + lesson_id, + lesson_title, + course_title, + total_views, + unique_viewers, + avg_views_per_viewer, + popularity_rank +FROM dmezentsev.lesson_popularity_ranking +WHERE avg_views_per_viewer > 1 +ORDER BY avg_views_per_viewer DESC, total_views DESC +LIMIT 3; + +-- Задание 3: Лучшая когорта по retention +SELECT cohort_month, retention_rate +FROM dmezentsev.user_cohort_analysis +ORDER BY retention_rate DESC +LIMIT 3; + + +-- Задание 4: Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT. + +/* EXPLAIN ANALYZE: + +Seq Scan on conversion_funnel (cost=0.00..11.40 rows=140 width=530) (actual time=0.007..0.011 rows=25 loops=1) +Planning Time: 0.124 ms +Execution Time: 0.027 ms + ++- по каждой вьюхе такой план: +rows=25, rows=5, rows=18 — очень мало данных +Execution Time везде маленький (0.027 - 0.330 ms) +Seq Scan — потому что таблицы небольшие + +Не придумал на данный момент, как улучшить запросы, но для ускорения запросов на больших данных +ввел бы индексацию внешних ключей +*/ + +CREATE INDEX IF NOT EXISTS idx_enrollments_user_id ON dmezentsev.enrollments(user_id); +CREATE INDEX IF NOT EXISTS idx_enrollments_course_id ON dmezentsev.enrollments(course_id); +CREATE INDEX IF NOT EXISTS idx_lessons_course_id ON dmezentsev.lessons(course_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_user_id ON dmezentsev.lesson_views(user_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_lesson_id ON dmezentsev.lesson_views(lesson_id); +CREATE INDEX IF NOT EXISTS idx_lesson_views_viewed_at ON dmezentsev.lesson_views(viewed_at); + +-- А также создал бы индекс для даты регистрации +CREATE INDEX IF NOT EXISTS idx_users_registration_date ON dmezentsev.users_(registration_date); \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql new file mode 100644 index 0000000..05b588e --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Homework - Типы данных.sql @@ -0,0 +1,117 @@ +--Типы данных + +--Задача 1 +CREATE TABLE raw_data +ON CLUSTER clickhouse_cluster +( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +SELECT DISTINCT + toDecimal64OrNull(balance, 2) AS balance_decimal, + toDateTime64OrNull(created_at, 3) AS created_at_date +FROM raw_data +WHERE + toDecimal64OrNull(balance, 2) IS NOT NULL + AND toDateTime64OrNull(created_at, 3) IS NOT NULL + +/* + CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL + + Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть: +toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL +toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль. +*/ + +--Задача 2 +CREATE TABLE events +ON CLUSTER clickhouse_cluster +( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = MergeTree +ORDER BY event_id; + +--Заполнил данными для проверки +INSERT INTO events (event_id, event_date, event_time) VALUES +('1', '2023-01-01', '12:30:45'), +('2', '01.02.2023', '08:15:00'), +('3', 'invalid', '25:99:99'), +('4', '2023-05-20', 'invalid'), +('5', '31.12.2023', '23:59:59'); + +SELECT * FROM events; + +SELECT DISTINCT + toDate(DateTime_Valid) AS Event_Date, + toHour(DateTime_Valid) AS Event_Hour, + DateTime_Valid +FROM ( + SELECT + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid + FROM events +) +WHERE DateTime_Valid IS NOT NULL; + +--Задача 3 +DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster; + +CREATE TABLE user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) ENGINE = MergeTree ORDER BY tuple(); + +--Тест данные +INSERT INTO user_activity (user_id, actions) VALUES +(1, '["login", "purchase", "logout"]'), +(2, '["login", "logout"]'), +(3, '["login"]'), +(4, '["login","purchase"]'); + +SELECT + user_id, + -- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя + sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions +FROM user_activity +WHERE + -- Проверяем наличие 'purchase' в массиве + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id; +/* +Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase". +Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно. +То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout") +*/ + +--Задача 4 +DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster; + +CREATE TABLE sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) ORDER BY sensor_id; + +--Тест данные +INSERT INTO sensor_data (sensor_id, value, status) VALUES +(1, 25.5, 'ok'), +(2, 120.75, 'error'), +(3, 15.0, 1), +(4, 9999999.99, 'ok'), +(5, 0.001, 'ok'); + +SELECT + toString(status) AS status_string, + avg(value) AS avg_value --Среднее значение value для каждого status +FROM sensor_data +WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2) +GROUP BY status_string; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql new file mode 100644 index 0000000..6d6edfa --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - JOIN.sql @@ -0,0 +1,70 @@ + +-- Задачи на отработку Запросы - JOIN -- + +DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC; +DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.products +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + name String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/products', + '{replica}' +) +ORDER BY product_id; + + +INSERT INTO dsolovianov.products VALUES +(101, 'iPhone 14'), +(102, 'Galaxy S22'), +(103, 'Pixel 7'); + +CREATE TABLE dsolovianov.prices +ON CLUSTER clickhouse_cluster +( + product_id UInt32, + price UInt32, + region String +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/prices', + '{replica}' +) +ORDER BY product_id; + +INSERT INTO dsolovianov.prices VALUES +(101, 999, 'US'), +(101, 1049, 'EU'), +(102, 850, 'US'), +(102, 899, 'CA'), +(103, 799, 'JP'); + + +--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов. + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM dsolovianov.products AS p +ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; + +--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`? + +/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/ + +--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают? + +SELECT + p.product_id, + p.name, + pr.price, + pr.region +FROM dsolovianov.products AS p +ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id; + +/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения. + Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU. + короче, по сути ALL LEFT JOIN создает декартово произведение*/ diff --git a/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql new file mode 100644 index 0000000..120b341 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Запросы - Синтаксис.sql @@ -0,0 +1,244 @@ +/* Запросы - Синтаксис*/ + +--! Задача 1 !-- + +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active Bool +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users_v2', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO users VALUES +(1, 'Ivan', 30, 'RU', 1), +(2, 'Anna', 25, 'RU', 1), +(3, 'Petr', 45, 'RU', 0), +(4, 'Olga', 35, 'RU', 1), +(5, 'John', 55, 'US', 1), +(6, 'Alice', 42, 'US', 0), +(7, 'Tom', 38, 'US', 1), +(8, 'Lucas', 28, 'BR', 1), +(9, 'Maria', 60, 'BR', 1), +(10, 'João', 27, 'BR', 0), +(11, 'Kenji', 33, 'JP', 1), +(12, 'Yuki', 29, 'JP', 0), +(13, 'Chen', 40, 'CN', 1), +(14, 'Li', 48, 'CN', 1), +(15, 'Fatima', 31, 'AE', 1), +(16, 'Ahmed', 36, 'AE', 1), +(17, 'George', 24, 'US', 1), +(18, 'Nina', 34, 'RU', 1), +(19, 'Max', 50, 'DE', 1), +(20, 'Sophie', 22, 'DE', 0); + +--Получи 10 самых старших активных пользователей. +SELECT + user_id, + name, + age, + is_active +FROM users +WHERE is_active = true +ORDER BY age DESC +LIMIT 10; + +--Выведи пользователей из country = 'RU', отсортированных по имени. +SELECT + user_id, + name, + country +FROM users +WHERE country = 'RU' +ORDER BY name; + +--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки. +SELECT + user_id, + name, + age +FROM users +WHERE is_active = true +LIMIT 10; + +--Посчитай количество пользователей по странам. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country; + +SELECT + country, + uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country +FROM users +GROUP BY country; + +--Для каждой страны выведи минимальный и максимальный возраст. +SELECT + country, + MIN(age) as min_age, + MAX(age) as max_age +FROM users +GROUP BY country +ORDER BY country; + +--Отсортируй страны по убыванию количества пользователей. +SELECT + country, + COUNT(*) as user_country +FROM users +GROUP BY country +ORDER BY user_country DESC; + +--Всё вместе +SELECT + country, + COUNT(*) as user_country, + SUM(is_active) as active_users, + MIN(age) as min_age, + MAX(age) as max_age, + uniqExact(country) as unique_countries +FROM users +GROUP BY country +ORDER BY country; + +--! Задача 2 !-- + +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events_v2', + '{replica}' +) +ORDER BY (user_id, event_time); + +INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES +(1, '2025-07-17 09:00:00', 'click'), +(1, '2025-07-17 09:05:00', 'view'), +(1, '2025-07-17 09:10:00', 'purchase'), +(2, '2025-07-17 10:00:00', 'click'), +(2, '2025-07-17 10:05:00', 'click'), +(2, '2025-07-17 11:00:00', 'view'), +(3, '2025-07-16 12:00:00', 'view'), +(3, '2025-07-16 12:30:00', 'click'), +(3, '2025-07-17 08:00:00', 'purchase'), +(4, '2025-07-17 13:00:00', 'click'), +(4, '2025-07-17 14:00:00', 'view'), +(4, '2025-07-17 15:00:00', 'view'), +(5, '2025-07-16 20:00:00', 'view'), +(5, '2025-07-17 01:00:00', 'click'), +(5, '2025-07-17 06:00:00', 'purchase'), +(6, '2025-07-17 07:30:00', 'view'), +(6, '2025-07-17 08:45:00', 'click'), +(7, '2025-07-16 23:59:59', 'click'), +(7, '2025-07-17 00:01:00', 'purchase'), +(7, '2025-07-17 10:10:00', 'view'); + +--1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`). +SELECT + user_id, + event_time, + event_type, + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)` +SELECT + user_id, + event_time, + event_type, + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank +FROM dsolovianov.events +ORDER BY user_id, event_rank; + +--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли? +--Да, `ROWS BETWEEN` поддерживается: +SELECT + user_id, + event_time, + event_type, + -- кумулятивное окно (все предыдущие строки) + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events, + -- скользящее окно: текущая + 2 предыдущих события + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events, + -- окно: предыдущая + текущая + следующая + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next +FROM dsolovianov.events +ORDER BY user_id, event_time; + +--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT * FROM recent_events +ORDER BY event_time DESC; + +--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий. +WITH recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + user_id, + COUNT(*) AS event_count +FROM recent_events +GROUP BY user_id +ORDER BY event_count DESC +LIMIT 5; + +--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе. +WITH +event_categories AS ( + SELECT + event_type, + CASE + WHEN event_type = 'click' THEN 'engagement' + WHEN event_type = 'view' THEN 'engagement' + WHEN event_type = 'purchase' THEN 'conversion' + ELSE 'other' + END AS category + FROM (SELECT DISTINCT event_type FROM dsolovianov.events) +), +recent_events AS ( + SELECT * + FROM dsolovianov.events + WHERE event_time >= '2025-07-16 15:00:00' +) +SELECT + re.user_id, + re.event_time, + re.event_type, + ec.category, + ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank, + COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events +FROM recent_events re +LEFT JOIN event_categories ec ON re.event_type = ec.event_type +ORDER BY re.user_id, re.event_time; + +/* +Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE? +многоуровневые CTE - да +рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле. +*/ \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql new file mode 100644 index 0000000..9332b35 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Индексы.sql @@ -0,0 +1,166 @@ +-- Индексы и сжатие - Индексы -- + +-- #1 +/* +3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`). +Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED) +4. Что будет, если `ORDER BY log_id`, а не `ts`? +ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки. +Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY ts +PARTITION BY toYYYYMM(ts) +PRIMARY KEY (ts); + +INSERT INTO dsolovianov.logs (log_id, ts) VALUES +(1, '2024-06-01 10:00:00'), +(2, '2024-06-01 11:00:00'), +(3, '2024-06-02 12:00:00'), +(4, '2024-06-03 13:00:00'), +(5, '2024-06-04 14:00:00'); + +-- 1. Добавляем minmax индекс: +ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2; + +-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'` +SELECT * +FROM dsolovianov.logs +WHERE ts + BETWEEN '2024-06-02' AND '2024-06-03'; + +-- Раз нет доступа, но можно предположить, что вот такой запрос сработает: +SELECT query, read_rows, result_rows +FROM system.query_log +WHERE query LIKE '%logs%' +ORDER BY event_time DESC +LIMIT 1; + + +-- #2 +/* +1. Создай таблицу с `ORDER BY id`, добавь индекс: + + ```sql + ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + ``` + + +3. Посмотри `EXPLAIN` — индекс используется? +Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка. +4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений? +Будет создано 10M партиций, что убьёт производительность. +**/ +DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/clicks', + '{replica}' +) +ORDER BY id +PARTITION BY country +PRIMARY KEY (id); + +INSERT INTO dsolovianov.clicks (id, country) VALUES +(1, 'RU'), +(2, 'US'), +(3, 'RU'), +(4, 'DE'), +(5, 'RU'); + +ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster +ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + +-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`. +EXPLAIN indexes = 1 +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +EXPLAIN +SELECT * FROM dsolovianov.clicks WHERE country = 'RU'; + +/* +Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dsolovianov.clicks) + Indexes: + MinMax + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/3 + Granules: 1/3 + Partition + Keys: + country + Condition: (country in ['RU', 'RU']) + Parts: 1/1 + Granules: 1/1 + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_country + Description: set GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + */ + +-- #3 + +DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/users', + '{replica}' +) +ORDER BY id +PRIMARY KEY (id); + +INSERT INTO dsolovianov.users (id, email) VALUES +(1, 'alice@mail.com'), +(2, 'bob@mail.com'), +(3, 'charlie@mail.com'), +(4, 'dave@mail.com'), +(5, 'eve@example.com'); + +ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE '%mail%'; + +EXPLAIN indexes = 1 +SELECT * FROM users WHERE email LIKE 'alice%'; +/* +Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы. +Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные. +*/ \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql new file mode 100644 index 0000000..4e9ebb8 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Партиции.sql @@ -0,0 +1,146 @@ +-- Индексы и сжатие - Партиции -- + +-- #1 +/* +1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`. +2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`. +3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :) + +*/ + +DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY (region, sale_date); + +INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES +(1, 'RU', '2024-05-01', '100.0'), +(2, 'RU', '2024-05-15', '120.0'), +(3, 'US', '2024-06-01', '90.0'), +(4, 'US', '2024-07-03', '150.0'), +(5, 'DE', '2024-07-04', '160.0'); + +ALTER TABLE dsolovianov.sales DROP PARTITION '202405'; + +SELECT * +FROM dsolovianov.sales; + +-- #2 +/* +1. Создай таблицу с `PARTITION BY event_type`. +2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`). +3. Чем такой подход может быть опасен (подумай про кардинальность значений)? +При большом количестве уникальных event_type создастся много мелких партиций. +*/ +DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/events', + '{replica}' +) +PARTITION BY event_type +ORDER BY (event_date, id); + +INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES +(1, 'login', '2024-06-01'), +(2, 'click', '2024-06-01'), +(3, 'click', '2024-06-02'), +(4, 'purchase', '2024-06-03'), +(5, 'login', '2024-06-04'); + +ALTER TABLE dsolovianov.events DROP PARTITION 'click'; + +-- #3 +/* +1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`. +2. Что ты выиграешь с точки зрения баланса? +Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции) +3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`? +Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id. + +*/ + +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id +ORDER BY (log_time, id); + +INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES +(1, 123, '2024-07-17 10:00:00'), +(2, 456, '2024-07-17 10:01:00'), +(3, 789, '2024-07-17 10:02:00'); + +SELECT + id, + user_id, + cityHash64(user_id) % 100 AS partition_number +FROM dsolovianov.logs +ORDER BY id; + +SELECT * FROM logs WHERE user_id = 456; + + +-- #4 +/* +1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`. +2. Как будет выглядеть имя партиции? +Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406') +3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему. +*/ + +DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY (region, visit_date, id); + +INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES +(1, 'RU', '2024-06-01'), +(2, 'RU', '2024-06-15'), +(3, 'US', '2024-06-03'), +(4, 'US', '2024-07-01'), +(5, 'RU', '2024-07-05'); + +ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster +DROP PARTITION ('RU', '202407'); + +SELECT * FROM dsolovianov.visits; \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql new file mode 100644 index 0000000..ce28674 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Индексы и сжатие - Сжатие.sql @@ -0,0 +1,39 @@ +-- Индексы и сжатие - Сжатие -- + +-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)). +-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени. +DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC; + +CREATE TABLE dsolovianov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dsolovianov/logs', + '{replica}' +) +ORDER BY (created_at, id) +PARTITION BY toYYYYMM(created_at); + +INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES +(1, 'INFO', 'User logged in', '2024-07-01 10:00:00'), +(2, 'WARN', 'Request took too long', '2024-07-01 10:05:00'), +(3, 'ERROR', 'Unable to connect to server', '2024-07-01 10:10:00'); + +-- #3 Посмотри статистику сжатия: +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; + +-- Стата вот так выглядит: +column | compression_codec +id +level +message CODEC(ZSTD(3)) +created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png new file mode 100644 index 0000000..7638600 Binary files /dev/null and b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.png differ diff --git a/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql new file mode 100644 index 0000000..af9dcc3 --- /dev/null +++ b/dsolovianov/clickhouse/Homework/Типы таблиц - Homework.sql @@ -0,0 +1,81 @@ +--Задача 1: Создай таблицу page_views +DROP TABLE page_views; + +CREATE TABLE 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: Создай таблицу users + +CREATE TABLE users +ON CLUSTER clickhouse_cluster +( +id UInt64, +name String, +updated_at DateTime + ) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +-- Задача 3: Создай таблицу region_sales + +CREATE TABLE region_sales +ON CLUSTER clickhouse_cluster +( +region String, +date Date, +sales UInt64 + ) +ENGINE = SummingMergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +--Задача 4: Создай таблицу 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() --Подзадача 4.2 +ORDER BY region; --Подзадача 4.3 + +--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()` + +INSERT INTO user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(toUInt64(user_id)) AS active_users_state +FROM +( + SELECT 'Samara' AS region, 25 AS age, 1 AS user_id + UNION ALL + SELECT 'Moscow', 30, 2 + UNION ALL + SELECT 'Moscow', 22, 3 + UNION ALL + SELECT 'Samara', 22, 4 + UNION ALL + SELECT 'Sochi', 34, 5 +) +GROUP BY region; + +--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()` + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY 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/greenplum/Script-1.sql b/dsolovianov/greenplum/Script-1.sql new file mode 100644 index 0000000..bb78b2a --- /dev/null +++ b/dsolovianov/greenplum/Script-1.sql @@ -0,0 +1,105 @@ +--Витрина dm.lesson_popularity_summary +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary as +select + l.lesson_id, + l.title as lesson_title, + c.course_id, + c.title as course_title, + COUNT(f.user_id) as total_views, + COUNT(DISTINCT f.user_id) as unique_users, + MIN(f.viewed_at) as first_view, + MAX(f.viewed_at) as last_view +FROM core.fact_lesson_views f +JOIN core.dim_lesson l + ON f.lesson_id = l.lesson_id +JOIN core.dim_course c + ON l.course_id = c.course_id +GROUP BY + l.lesson_id, + l.title, + c.course_id, + c.title +order by lesson_id; + +select * from dm.lesson_popularity_summary; + +--Удалил лишний индекс +DROP INDEX core.idx_fact_views_course_id; + +-- Все индексы для CORE-таблиц +SELECT + tablename, + indexname, + indexdef +FROM pg_indexes +WHERE schemaname = 'core' + AND tablename IN ( + 'dim_course', + 'dim_date', + 'dim_lesson', + 'dim_user', + 'enrollments', + 'fact_lesson_views' + ) +ORDER by indexname, tablename; + + +--Витрина 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, + COUNT(e.course_id) as enrollments_count, + du.registration_date +from core.dim_user du +left join core.enrollments e + on du.user_id = e.user_id +left join core.fact_lesson_views flv + on du.user_id = flv.user_id +where flv.user_id is NULL +group by + du.user_id, + du.name, + du.email, + du.age, + du.registration_date +order by du.user_id; + +select * from dm.inactive_users_summary; + +--Витрина dm.course_completion_rate +CREATE MATERIALIZED VIEW dm.course_completion_rate as +select + du.user_id, + du.name as user_name, + dc.course_id, + dc.title as course_title, + COUNT(DISTINCT dl.lesson_id) AS lessons_in_course, + COUNT(DISTINCT flv.lesson_id) AS lessons_viewed, + ROUND(COUNT(DISTINCT flv.lesson_id)::numeric / NULLIF(COUNT(DISTINCT dl.lesson_id), 0), 2) AS completion_rate +from core.enrollments e +left join core.dim_user du + on du.user_id = e.user_id +left join core.dim_course dc + on dc.course_id = e.course_id +left join core.dim_lesson dl + on dl.course_id = e.course_id +left join core.fact_lesson_views flv + on flv.user_id = e.user_id + and flv.lesson_id = dl.lesson_id +group by + du.user_id, + du.name, + dc.course_id, + dc.title +order by du.user_id; + +select * from dm.course_completion_rate order by user_id; + +--Этап 5. Проверка +SELECT * FROM dm.lesson_popularity_summary LIMIT 10; +SELECT * FROM dm.inactive_users_summary LIMIT 10; +SELECT * FROM dm.course_completion_rate LIMIT 10; +--Все витрины отображаются корректно. Скрины: "название витрины".png \ No newline at end of file diff --git a/dsolovianov/greenplum/dm.course_completion_rate.png b/dsolovianov/greenplum/dm.course_completion_rate.png new file mode 100644 index 0000000..142dbef Binary files /dev/null and b/dsolovianov/greenplum/dm.course_completion_rate.png differ diff --git a/dsolovianov/greenplum/dm.inactive_users_summary.png b/dsolovianov/greenplum/dm.inactive_users_summary.png new file mode 100644 index 0000000..a0e1aee Binary files /dev/null and b/dsolovianov/greenplum/dm.inactive_users_summary.png differ diff --git a/dsolovianov/greenplum/dm.lesson_popularity_summary.png b/dsolovianov/greenplum/dm.lesson_popularity_summary.png new file mode 100644 index 0000000..f43c677 Binary files /dev/null and b/dsolovianov/greenplum/dm.lesson_popularity_summary.png differ diff --git a/dsolovianov/greenplum/Добавлены индексы.sql b/dsolovianov/greenplum/Добавлены индексы.sql new file mode 100644 index 0000000..7608706 --- /dev/null +++ b/dsolovianov/greenplum/Добавлены индексы.sql @@ -0,0 +1,508 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 9.4.26 +-- Dumped by pg_dump version 17.5 + +-- Started on 2026-01-17 22:42:00 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 10 (class 2615 OID 25610) +-- Name: core; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA core; + + +ALTER SCHEMA core OWNER TO dsolovianov; + +-- +-- TOC entry 11 (class 2615 OID 25611) +-- Name: dm; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA dm; + + +ALTER SCHEMA dm OWNER TO dsolovianov; + +-- +-- TOC entry 9 (class 2615 OID 25609) +-- Name: raw; Type: SCHEMA; Schema: -; Owner: dsolovianov +-- + +CREATE SCHEMA raw; + + +ALTER SCHEMA raw OWNER TO dsolovianov; + +SET default_tablespace = ''; + +-- +-- TOC entry 273 (class 1259 OID 25635) +-- Name: dim_course; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_course ( + course_id integer NOT NULL, + title text, + category text +); + + +ALTER TABLE core.dim_course OWNER TO dsolovianov; + +-- +-- TOC entry 275 (class 1259 OID 25656) +-- Name: dim_date; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_date ( + date date NOT NULL, + year integer, + month integer, + day integer, + weekday text +); + + +ALTER TABLE core.dim_date OWNER TO dsolovianov; + +-- +-- TOC entry 274 (class 1259 OID 25643) +-- Name: dim_lesson; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_lesson ( + lesson_id integer NOT NULL, + title text, + duration_min integer, + course_id integer +); + + +ALTER TABLE core.dim_lesson OWNER TO dsolovianov; + +-- +-- TOC entry 272 (class 1259 OID 25627) +-- Name: dim_user; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.dim_user ( + user_id integer NOT NULL, + name text, + age integer, + email text, + registration_date date +); + + +ALTER TABLE core.dim_user OWNER TO dsolovianov; + +-- +-- TOC entry 276 (class 1259 OID 25872) +-- Name: enrollments; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.enrollments ( + user_id integer, + course_id integer, + enrolled_at timestamp without time zone +); + + +ALTER TABLE core.enrollments OWNER TO dsolovianov; + +-- +-- TOC entry 277 (class 1259 OID 25885) +-- Name: fact_lesson_views; Type: TABLE; Schema: core; Owner: dsolovianov +-- + +CREATE TABLE core.fact_lesson_views ( + user_id integer, + lesson_id integer, + course_id integer, + viewed_at timestamp without time zone +); + + +ALTER TABLE core.fact_lesson_views OWNER TO dsolovianov; + +-- +-- TOC entry 268 (class 1259 OID 25615) +-- Name: courses; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.courses ( + id integer, + title character varying(50), + category character varying(50), + created_at character varying(50) +); + + +ALTER TABLE raw.courses OWNER TO dsolovianov; + +-- +-- TOC entry 270 (class 1259 OID 25621) +-- Name: enrollments; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.enrollments ( + id integer, + user_id integer, + course_id integer, + enrolled_at character varying(50) +); + + +ALTER TABLE raw.enrollments OWNER TO dsolovianov; + +-- +-- TOC entry 271 (class 1259 OID 25624) +-- Name: lesson_views; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.lesson_views ( + id integer, + user_id integer, + lesson_id integer, + viewed_at character varying(50) +); + + +ALTER TABLE raw.lesson_views OWNER TO dsolovianov; + +-- +-- TOC entry 269 (class 1259 OID 25618) +-- Name: lessons; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.lessons ( + id integer, + course_id integer, + title character varying(50), + duration_min integer +); + + +ALTER TABLE raw.lessons OWNER TO dsolovianov; + +-- +-- TOC entry 267 (class 1259 OID 25612) +-- Name: users; Type: TABLE; Schema: raw; Owner: dsolovianov +-- + +CREATE TABLE raw.users ( + id integer, + name character varying(50), + age integer, + email character varying(50), + registration_date character varying(50) +); + + +ALTER TABLE raw.users OWNER TO dsolovianov; + +-- +-- TOC entry 2506 (class 0 OID 25635) +-- Dependencies: 273 +-- Data for Name: dim_course; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_course (course_id, title, category) FROM stdin; +2 Python для анализа данных programming +1 SQL для начинающих data +3 BI с нуля business +\. + + +-- +-- TOC entry 2508 (class 0 OID 25656) +-- Dependencies: 275 +-- Data for Name: dim_date; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_date (date, year, month, day, weekday) FROM stdin; +2023-01-15 2023 1 15 Sunday +2023-01-16 2023 1 16 Monday +2023-01-21 2023 1 21 Saturday +2023-01-20 2023 1 20 Friday +2023-03-05 2023 3 5 Sunday +2023-02-01 2023 2 1 Wednesday +2023-04-02 2023 4 2 Sunday +2023-03-06 2023 3 6 Monday +2023-04-01 2023 4 1 Saturday +2023-04-03 2023 4 3 Monday +\. + + +-- +-- TOC entry 2507 (class 0 OID 25643) +-- Dependencies: 274 +-- Data for Name: dim_lesson; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_lesson (lesson_id, title, duration_min, course_id) FROM stdin; +4 Основы BI 12 3 +5 Метрики и дашборды 18 3 +2 JOIN 15 1 +1 SELECT и FROM 10 1 +3 Pandas 20 2 +\. + + +-- +-- TOC entry 2505 (class 0 OID 25627) +-- Dependencies: 272 +-- Data for Name: dim_user; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_user (user_id, name, age, email, registration_date) FROM stdin; +3 Charlie 22 charlie@mail.com 2023-02-20 +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2509 (class 0 OID 25872) +-- Dependencies: 276 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.enrollments (user_id, course_id, enrolled_at) FROM stdin; +4 3 2023-04-01 00:00:00 +1 2 2023-02-01 00:00:00 +2 1 2023-01-20 00:00:00 +1 1 2023-01-15 00:00:00 +3 2 2023-03-05 00:00:00 +\. + + +-- +-- TOC entry 2510 (class 0 OID 25885) +-- Dependencies: 277 +-- Data for Name: fact_lesson_views; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) FROM stdin; +2 1 1 2023-01-21 09:00:00 +1 2 1 2023-01-16 10:15:00 +3 3 2 2023-03-06 12:00:00 +1 1 1 2023-01-16 10:00:00 +4 4 3 2023-04-02 10:00:00 +4 5 3 2023-04-03 10:00:00 +\. + + +-- +-- TOC entry 2501 (class 0 OID 25615) +-- Dependencies: 268 +-- Data for Name: courses; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.courses (id, title, category, created_at) FROM stdin; +3 BI с нуля business 2023-03-01 +1 SQL для начинающих data 2023-01-01 +2 Python для анализа данных programming 2023-01-15 +\. + + +-- +-- TOC entry 2503 (class 0 OID 25621) +-- Dependencies: 270 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.enrollments (id, user_id, course_id, enrolled_at) FROM stdin; +1 1 1 2023-01-15 +2 1 2 2023-02-01 +3 2 1 2023-01-20 +4 3 2 2023-03-05 +5 4 3 2023-04-01 +\. + + +-- +-- TOC entry 2504 (class 0 OID 25624) +-- Dependencies: 271 +-- Data for Name: lesson_views; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lesson_views (id, user_id, lesson_id, viewed_at) FROM stdin; +2 1 2 2023-01-16 10:15:00.000 +1 1 1 2023-01-16 10:00:00.000 +3 2 1 2023-01-21 09:00:00.000 +5 4 4 2023-04-02 10:00:00.000 +4 3 3 2023-03-06 12:00:00.000 +6 4 5 2023-04-03 10:00:00.000 +\. + + +-- +-- TOC entry 2502 (class 0 OID 25618) +-- Dependencies: 269 +-- Data for Name: lessons; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lessons (id, course_id, title, duration_min) FROM stdin; +1 1 SELECT и FROM 10 +2 1 JOIN 15 +3 2 Pandas 20 +4 3 Основы BI 12 +5 3 Метрики и дашборды 18 +\. + + +-- +-- TOC entry 2500 (class 0 OID 25612) +-- Dependencies: 267 +-- Data for Name: users; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.users (id, name, age, email, registration_date) FROM stdin; +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +3 Charlie 22 charlie@mail.com 2023-02-20 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2314 (class 2606 OID 25642) +-- Name: dim_course dim_course_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_course + ADD CONSTRAINT dim_course_pkey PRIMARY KEY (course_id); + + +-- +-- TOC entry 2319 (class 2606 OID 25663) +-- Name: dim_date dim_date_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_date + ADD CONSTRAINT dim_date_pkey PRIMARY KEY (date); + + +-- +-- TOC entry 2316 (class 2606 OID 25650) +-- Name: dim_lesson dim_lesson_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_pkey PRIMARY KEY (lesson_id); + + +-- +-- TOC entry 2312 (class 2606 OID 25634) +-- Name: dim_user dim_user_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_user + ADD CONSTRAINT dim_user_pkey PRIMARY KEY (user_id); + + +-- +-- TOC entry 2317 (class 1259 OID 25906) +-- Name: idx_dim_lesson_course_id; Type: INDEX; Schema: core; Owner: dsolovianov +-- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +/* +1) Создание INDEX на таблицы в CORE-слое +*/ + +CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson USING btree (course_id); + +CREATE INDEX idx_enrollments_course_id ON core.enrollments USING btree (course_id); + +CREATE INDEX idx_fact_views_course_id ON core.fact_lesson_views USING btree (course_id); + +CREATE INDEX idx_fact_views_course_viewed_at ON core.fact_lesson_views USING btree (course_id, viewed_at); + +CREATE INDEX idx_fact_views_lesson_id ON core.fact_lesson_views USING btree (lesson_id); + +CREATE INDEX idx_fact_views_user_id ON core.fact_lesson_views USING btree (user_id); + +CREATE INDEX idx_fact_views_user_viewed_at ON core.fact_lesson_views USING btree (user_id, viewed_at); +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2327 (class 2606 OID 25880) +-- Name: enrollments enrollments_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2328 (class 2606 OID 25875) +-- Name: enrollments enrollments_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- +-- TOC entry 2329 (class 2606 OID 25898) +-- Name: fact_lesson_views fact_lesson_views_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2330 (class 2606 OID 25893) +-- Name: fact_lesson_views fact_lesson_views_lesson_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_lesson_id_fkey FOREIGN KEY (lesson_id) REFERENCES core.dim_lesson(lesson_id); + + +-- +-- TOC entry 2331 (class 2606 OID 25888) +-- Name: fact_lesson_views fact_lesson_views_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- Completed on 2026-01-17 22:42:02 + +-- +-- PostgreSQL database dump complete +-- + diff --git a/dsolovianov/greenplum/Этапы 1-3.sql b/dsolovianov/greenplum/Этапы 1-3.sql new file mode 100644 index 0000000..045de55 --- /dev/null +++ b/dsolovianov/greenplum/Этапы 1-3.sql @@ -0,0 +1,417 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- ЭТАП 1 +-- 1.1 - СОЗДАНИЕ СХЕМ + +CREATE SCHEMA core; + + +ALTER SCHEMA core OWNER TO dsolovianov; + + +CREATE SCHEMA dm; + + +ALTER SCHEMA dm OWNER TO dsolovianov; + + +CREATE SCHEMA raw; + + +ALTER SCHEMA raw OWNER TO dsolovianov; + +SET default_tablespace = ''; + + + +-- 1.2 - Создание raw-таблиц + +CREATE TABLE raw.courses ( + id integer, + title character varying(50), + category character varying(50), + created_at character varying(50) +); + + +ALTER TABLE raw.courses OWNER TO dsolovianov; + + +CREATE TABLE raw.enrollments ( + id integer, + user_id integer, + course_id integer, + enrolled_at character varying(50) +); + + +ALTER TABLE raw.enrollments OWNER TO dsolovianov; + + +CREATE TABLE raw.lesson_views ( + id integer, + user_id integer, + lesson_id integer, + viewed_at character varying(50) +); + + +ALTER TABLE raw.lesson_views OWNER TO dsolovianov; + + +CREATE TABLE raw.lessons ( + id integer, + course_id integer, + title character varying(50), + duration_min integer +); + + +ALTER TABLE raw.lessons OWNER TO dsolovianov; + + +CREATE TABLE raw.users ( + id integer, + name character varying(50), + age integer, + email character varying(50), + registration_date character varying(50) +); + + +ALTER TABLE raw.users OWNER TO dsolovianov; + + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Этап 3. Слой Core + +CREATE TABLE core.dim_course ( + course_id integer NOT NULL, + title text, + category text +); + + +ALTER TABLE core.dim_course OWNER TO dsolovianov; + + +CREATE TABLE core.dim_date ( + date date NOT NULL, + year integer, + month integer, + day integer, + weekday text +); + + +ALTER TABLE core.dim_date OWNER TO dsolovianov; + + +CREATE TABLE core.dim_lesson ( + lesson_id integer NOT NULL, + title text, + duration_min integer, + course_id integer +); + + +ALTER TABLE core.dim_lesson OWNER TO dsolovianov; + + +CREATE TABLE core.dim_user ( + user_id integer NOT NULL, + name text, + age integer, + email text, + registration_date date +); + + +ALTER TABLE core.dim_user OWNER TO dsolovianov; + + +CREATE TABLE core.enrollments ( + user_id integer, + course_id integer, + enrolled_at timestamp without time zone +); + + +ALTER TABLE core.enrollments OWNER TO dsolovianov; + + +CREATE TABLE core.fact_lesson_views ( + user_id integer, + lesson_id integer, + course_id integer, + viewed_at timestamp without time zone +); + + +ALTER TABLE core.fact_lesson_views OWNER TO dsolovianov; + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +COPY core.dim_course (course_id, title, category) FROM stdin; +2 Python для анализа данных programming +1 SQL для начинающих data +3 BI с нуля business +\. + + +COPY core.dim_date (date, year, month, day, weekday) FROM stdin; +2023-01-15 2023 1 15 Sunday +2023-01-16 2023 1 16 Monday +2023-01-21 2023 1 21 Saturday +2023-01-20 2023 1 20 Friday +2023-03-05 2023 3 5 Sunday +2023-02-01 2023 2 1 Wednesday +2023-04-02 2023 4 2 Sunday +2023-03-06 2023 3 6 Monday +2023-04-01 2023 4 1 Saturday +2023-04-03 2023 4 3 Monday +\. + + +-- +-- TOC entry 2500 (class 0 OID 25643) +-- Dependencies: 274 +-- Data for Name: dim_lesson; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_lesson (lesson_id, title, duration_min, course_id) FROM stdin; +4 Основы BI 12 3 +5 Метрики и дашборды 18 3 +2 JOIN 15 1 +1 SELECT и FROM 10 1 +3 Pandas 20 2 +\. + + +-- +-- TOC entry 2498 (class 0 OID 25627) +-- Dependencies: 272 +-- Data for Name: dim_user; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.dim_user (user_id, name, age, email, registration_date) FROM stdin; +3 Charlie 22 charlie@mail.com 2023-02-20 +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2502 (class 0 OID 25872) +-- Dependencies: 276 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.enrollments (user_id, course_id, enrolled_at) FROM stdin; +4 3 2023-04-01 00:00:00 +1 2 2023-02-01 00:00:00 +2 1 2023-01-20 00:00:00 +1 1 2023-01-15 00:00:00 +3 2 2023-03-05 00:00:00 +\. + + +-- +-- TOC entry 2503 (class 0 OID 25885) +-- Dependencies: 277 +-- Data for Name: fact_lesson_views; Type: TABLE DATA; Schema: core; Owner: dsolovianov +-- + +COPY core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) FROM stdin; +2 1 1 2023-01-21 09:00:00 +1 2 1 2023-01-16 10:15:00 +3 3 2 2023-03-06 12:00:00 +1 1 1 2023-01-16 10:00:00 +4 4 3 2023-04-02 10:00:00 +4 5 3 2023-04-03 10:00:00 +\. + + +-- +-- TOC entry 2494 (class 0 OID 25615) +-- Dependencies: 268 +-- Data for Name: courses; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.courses (id, title, category, created_at) FROM stdin; +3 BI с нуля business 2023-03-01 +1 SQL для начинающих data 2023-01-01 +2 Python для анализа данных programming 2023-01-15 +\. + + +-- +-- TOC entry 2496 (class 0 OID 25621) +-- Dependencies: 270 +-- Data for Name: enrollments; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.enrollments (id, user_id, course_id, enrolled_at) FROM stdin; +1 1 1 2023-01-15 +2 1 2 2023-02-01 +3 2 1 2023-01-20 +4 3 2 2023-03-05 +5 4 3 2023-04-01 +\. + + +-- +-- TOC entry 2497 (class 0 OID 25624) +-- Dependencies: 271 +-- Data for Name: lesson_views; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lesson_views (id, user_id, lesson_id, viewed_at) FROM stdin; +2 1 2 2023-01-16 10:15:00.000 +1 1 1 2023-01-16 10:00:00.000 +3 2 1 2023-01-21 09:00:00.000 +5 4 4 2023-04-02 10:00:00.000 +4 3 3 2023-03-06 12:00:00.000 +6 4 5 2023-04-03 10:00:00.000 +\. + + +-- +-- TOC entry 2495 (class 0 OID 25618) +-- Dependencies: 269 +-- Data for Name: lessons; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.lessons (id, course_id, title, duration_min) FROM stdin; +1 1 SELECT и FROM 10 +2 1 JOIN 15 +3 2 Pandas 20 +4 3 Основы BI 12 +5 3 Метрики и дашборды 18 +\. + + +-- +-- TOC entry 2493 (class 0 OID 25612) +-- Dependencies: 267 +-- Data for Name: users; Type: TABLE DATA; Schema: raw; Owner: dsolovianov +-- + +COPY raw.users (id, name, age, email, registration_date) FROM stdin; +1 Alice 25 alice@mail.com 2023-01-10 +2 Bob 30 bob@gmail.com 2023-02-05 +3 Charlie 22 charlie@mail.com 2023-02-20 +5 Ethan 35 ethan@gmail.com 2023-03-10 +4 Diana 28 diana@mail.com 2023-03-01 +\. + + +-- +-- TOC entry 2314 (class 2606 OID 25642) +-- Name: dim_course dim_course_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_course + ADD CONSTRAINT dim_course_pkey PRIMARY KEY (course_id); + + +-- +-- TOC entry 2318 (class 2606 OID 25663) +-- Name: dim_date dim_date_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_date + ADD CONSTRAINT dim_date_pkey PRIMARY KEY (date); + + +-- +-- TOC entry 2316 (class 2606 OID 25650) +-- Name: dim_lesson dim_lesson_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_pkey PRIMARY KEY (lesson_id); + + +-- +-- TOC entry 2312 (class 2606 OID 25634) +-- Name: dim_user dim_user_pkey; Type: CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_user + ADD CONSTRAINT dim_user_pkey PRIMARY KEY (user_id); + + +-- +-- TOC entry 2319 (class 2606 OID 25651) +-- Name: dim_lesson dim_lesson_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.dim_lesson + ADD CONSTRAINT dim_lesson_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2320 (class 2606 OID 25880) +-- Name: enrollments enrollments_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2321 (class 2606 OID 25875) +-- Name: enrollments enrollments_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.enrollments + ADD CONSTRAINT enrollments_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- +-- TOC entry 2322 (class 2606 OID 25898) +-- Name: fact_lesson_views fact_lesson_views_course_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_course_id_fkey FOREIGN KEY (course_id) REFERENCES core.dim_course(course_id); + + +-- +-- TOC entry 2323 (class 2606 OID 25893) +-- Name: fact_lesson_views fact_lesson_views_lesson_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_lesson_id_fkey FOREIGN KEY (lesson_id) REFERENCES core.dim_lesson(lesson_id); + + +-- +-- TOC entry 2324 (class 2606 OID 25888) +-- Name: fact_lesson_views fact_lesson_views_user_id_fkey; Type: FK CONSTRAINT; Schema: core; Owner: dsolovianov +-- + +ALTER TABLE ONLY core.fact_lesson_views + ADD CONSTRAINT fact_lesson_views_user_id_fkey FOREIGN KEY (user_id) REFERENCES core.dim_user(user_id); + + +-- Completed on 2026-01-14 21:29:24 + +-- +-- PostgreSQL database dump complete +-- + 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/courses_202601031845.csv b/dsolovianov/postgresql/courses_202601031845.csv new file mode 100644 index 0000000..f604a2e --- /dev/null +++ b/dsolovianov/postgresql/courses_202601031845.csv @@ -0,0 +1,4 @@ +"id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 +2,Python для анализа данных,programming,2023-01-15 +3,BI с нуля,business,2023-03-01 diff --git a/dsolovianov/postgresql/enrollments_202601031845.csv b/dsolovianov/postgresql/enrollments_202601031845.csv new file mode 100644 index 0000000..7b27fa5 --- /dev/null +++ b/dsolovianov/postgresql/enrollments_202601031845.csv @@ -0,0 +1,6 @@ +"id","user_id","course_id","enrolled_at" +1,1,1,2023-01-15 +2,1,2,2023-02-01 +3,2,1,2023-01-20 +4,3,2,2023-03-05 +5,4,3,2023-04-01 diff --git a/dsolovianov/postgresql/lesson_views_202601031846.csv b/dsolovianov/postgresql/lesson_views_202601031846.csv new file mode 100644 index 0000000..23e3561 --- /dev/null +++ b/dsolovianov/postgresql/lesson_views_202601031846.csv @@ -0,0 +1,7 @@ +"id","user_id","lesson_id","viewed_at" +1,1,1,2023-01-16 10:00:00.000 +2,1,2,2023-01-16 10:15:00.000 +3,2,1,2023-01-21 09:00:00.000 +4,3,3,2023-03-06 12:00:00.000 +5,4,4,2023-04-02 10:00:00.000 +6,4,5,2023-04-03 10:00:00.000 diff --git a/dsolovianov/postgresql/lessons_202601031846.csv b/dsolovianov/postgresql/lessons_202601031846.csv new file mode 100644 index 0000000..24505cf --- /dev/null +++ b/dsolovianov/postgresql/lessons_202601031846.csv @@ -0,0 +1,6 @@ +"id","course_id","title","duration_min" +1,1,SELECT и FROM,10 +2,1,JOIN,15 +3,2,Pandas,20 +4,3,Основы BI,12 +5,3,Метрики и дашборды,18 diff --git a/dsolovianov/postgresql/users_202601031846.csv b/dsolovianov/postgresql/users_202601031846.csv new file mode 100644 index 0000000..54c69df --- /dev/null +++ b/dsolovianov/postgresql/users_202601031846.csv @@ -0,0 +1,6 @@ +"id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 +2,Bob,30,bob@gmail.com,2023-02-05 +3,Charlie,22,charlie@mail.com,2023-02-20 +4,Diana,28,diana@mail.com,2023-03-01 +5,Ethan,35,ethan@gmail.com,2023-03-10 diff --git a/dsolovianov/postgresql/Базовый SQL.sql b/dsolovianov/postgresql/Базовый SQL.sql new file mode 100644 index 0000000..930f258 --- /dev/null +++ b/dsolovianov/postgresql/Базовый SQL.sql @@ -0,0 +1,91 @@ +/* +- Задание 1 — Спроектируй «карточку пользователя» + + Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`** + 2. Потом измени запрос так, чтобы: + - сначала отсортировать по **`created_at`** (новые сверху) + - потом по **`name`** по алфавиту внутри одной даты + +- Задание 2 — Сегментация по странам (с выбором) + + Маркетолог хочет понять, какая страна сейчас интереснее. + + Работаем с таблицей **`users`** + + **✅ Требования:** + + 1. Выбери страну **`US`**и напиши запрос, который: + - вернёт **`id`**, **`name`**, **`country`**; + - покажет только пользователей из выбранной страны; + - отсортирует их по имени. + 2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**. + +- Задание 3 — Контакты для рассылки (думать про качество) + + Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу. + + **✅ Требования:** + + 1. Напиши один запрос, который: + - вернёт только пользователей, у которых email **есть**; + - вернёт столбцы **`id`**, **`name`**, **`email`**; + - отсортирует их по имени. + 2. Напиши второй запрос, который подготовит «чистый» вид email: + - вернёт только **`email`** в нижнем регистре (как **`email_lower`**); + - уберёт дубликаты email (нужно использовать **`DISTINCT`**). + - Задание 4 — Новички и активность + + «Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.» + + **✅ Требования:** + + 1. Напиши запрос, который: + - выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**); + - вернёт **`id`**, **`name`**, **`created_at`**; + - отсортирует по **`created_at`** от новых к старым. + 2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя. + 3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**). +*/ +SELECT id, + name, + country, + created_at, + email +FROM dsolovianov.users +ORDER BY created_at DESC, name ASC; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'US' +ORDER BY name; + +SELECT id, + name, + country +FROM dsolovianov.users +WHERE country LIKE 'UK' +ORDER BY name; + +SELECT id, + name, + email, + LOWER(email) AS email_lower +FROM dsolovianov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT id, + name, + created_at +FROM dsolovianov.users +WHERE created_at > '2024-01-11' +ORDER BY created_at DESC +LIMIT 3 OFFSET 1; diff --git a/dsolovianov/postgresql/Фильтрация.sql b/dsolovianov/postgresql/Фильтрация.sql new file mode 100644 index 0000000..edf113a --- /dev/null +++ b/dsolovianov/postgresql/Фильтрация.sql @@ -0,0 +1,124 @@ +/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES + ('Mi Band 7', 'smartwatch', 49.00, true), + ('Surface Pro', 'tablet', 1299.00, true), + ('iPhone 15', 'smartphone', 999.00, true), + ('Logitech MX Master', 'mouse', 120.00, true);*/ + +/* +- Задача 1 — Товары для продажи + + Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать. + + **✅ Условия:** + + - товар **в наличии** + - категория **НЕ smartwatch** + - цена **от 100 до 1300** + - отдельно хотим видеть цену: + - с наценкой 30% ( price + 30%) + - с «виртуальным НДС» 50% (price + 50%) + + **✅ Вывести:** + + - `name`, `category`, `price` + - `price_with_markup` = price + 30% + - `price_with_vat` = price + 50% + +- Задача 2 — Скидочная корзина: дешёвые или дорогие + + Нужно собрать товары под акцию + + **✅ Условия:** + + - участвуют товары с ценой **меньше 100** или **больше 1000** + - скидка 15% (price - 15%) + + **✅ Вывести:** + + - `name`, `price` + - `price_with_discount` = price - 15% + +- Задача 3 — Категории для рекламы + + Маркетолог просит товары только из нужных категорий: + + **✅ Условия:** + + - category в списке: `tablet`, `smartphone`, `mouse` + - цена **не меньше 100** + + **✅ Вывести:** + + - `id`, `name`, `category`, `price` + - сортировка по цене по убыванию +- Задача 4 — Убрать iPhone из выборки + + Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”. + + **✅ Условия:** + + - название **НЕ содержит** `iPhone` + + **✅ Вывести:** + + - `name`, `category`, `price` +- Задача 5 — Поиск товаров по названию + + Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию. + + - 1) Найти товары, где в названии есть `"pro"` (без учёта регистра) + + 📌 Условия: + + - поиск должен сработать на `pro`, `PRO`, `Pro` + - вывести: `name`, `category`, `price` + - 2) Найти товары, где в названии есть `"mx"` (без учёта регистра) + + 📌 Условия: + + - вывести: `name`, `category`, `price` + - отсортировать по `price DESC` +*/ + + +SELECT name, + category, + price, + (price * 0.30) + price AS price_with_markup, + (price * 0.50) + price AS price_with_vat +FROM dsolovianov.products +WHERE category NOT IN ('smartwatch') + AND (price >= 100 AND price <= 1300); + +SELECT name, + price, + price - (price * 0.15) AS price_with_discount +FROM dsolovianov.products +WHERE price < 100 OR price > 1000; + +SELECT id, + name, + category, + price +FROM dsolovianov.products +WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100 +ORDER BY price DESC; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE category NOT LIKE '%iPhone%'; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%pro%'; + +SELECT name, + category, + price +FROM dsolovianov.products +WHERE name ILIKE '%mx%'; + diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv new file mode 100644 index 0000000..e998233 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_course_202605201139.csv @@ -0,0 +1,6 @@ +"course_id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +3,BI с нуля,business,2023-03-01 00:00:00.000 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv new file mode 100644 index 0000000..319c0ad --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_date_202605191312.csv @@ -0,0 +1,2923 @@ +"date","year","month","day","weekday" +2023-01-01,2023,1,1,sunday +2023-01-03,2023,1,3,tuesday +2023-01-05,2023,1,5,thursday +2023-01-09,2023,1,9,monday +2023-01-11,2023,1,11,wednesday +2023-01-13,2023,1,13,friday +2023-01-15,2023,1,15,sunday +2023-01-17,2023,1,17,tuesday +2023-01-21,2023,1,21,saturday +2023-01-23,2023,1,23,monday +2023-01-24,2023,1,24,tuesday +2023-01-26,2023,1,26,thursday +2023-01-27,2023,1,27,friday +2023-01-28,2023,1,28,saturday +2023-01-31,2023,1,31,tuesday +2023-02-02,2023,2,2,thursday +2023-02-06,2023,2,6,monday +2023-02-08,2023,2,8,wednesday +2023-02-10,2023,2,10,friday +2023-02-14,2023,2,14,tuesday +2023-02-18,2023,2,18,saturday +2023-02-21,2023,2,21,tuesday +2023-02-23,2023,2,23,thursday +2023-02-24,2023,2,24,friday +2023-02-25,2023,2,25,saturday +2023-02-26,2023,2,26,sunday +2023-02-27,2023,2,27,monday +2023-03-01,2023,3,1,wednesday +2023-03-05,2023,3,5,sunday +2023-03-07,2023,3,7,tuesday +2023-03-11,2023,3,11,saturday +2023-03-12,2023,3,12,sunday +2023-03-16,2023,3,16,thursday +2023-03-18,2023,3,18,saturday +2023-03-20,2023,3,20,monday +2023-03-21,2023,3,21,tuesday +2023-03-23,2023,3,23,thursday +2023-03-24,2023,3,24,friday +2023-03-28,2023,3,28,tuesday +2023-03-30,2023,3,30,thursday +2023-04-02,2023,4,2,sunday +2023-04-04,2023,4,4,tuesday +2023-04-05,2023,4,5,wednesday +2023-04-09,2023,4,9,sunday +2023-04-10,2023,4,10,monday +2023-04-11,2023,4,11,tuesday +2023-04-12,2023,4,12,wednesday +2023-04-16,2023,4,16,sunday +2023-04-18,2023,4,18,tuesday +2023-04-20,2023,4,20,thursday +2023-04-21,2023,4,21,friday +2023-04-22,2023,4,22,saturday +2023-04-25,2023,4,25,tuesday +2023-04-27,2023,4,27,thursday +2023-04-29,2023,4,29,saturday +2023-05-02,2023,5,2,tuesday +2023-05-06,2023,5,6,saturday +2023-05-09,2023,5,9,tuesday +2023-05-11,2023,5,11,thursday +2023-05-12,2023,5,12,friday +2023-05-14,2023,5,14,sunday +2023-05-15,2023,5,15,monday +2023-05-17,2023,5,17,wednesday +2023-05-19,2023,5,19,friday +2023-05-20,2023,5,20,saturday +2023-05-22,2023,5,22,monday +2023-05-23,2023,5,23,tuesday +2023-05-24,2023,5,24,wednesday +2023-05-25,2023,5,25,thursday +2023-05-26,2023,5,26,friday +2023-05-29,2023,5,29,monday +2023-06-01,2023,6,1,thursday +2023-06-02,2023,6,2,friday +2023-06-08,2023,6,8,thursday +2023-06-10,2023,6,10,saturday +2023-06-12,2023,6,12,monday +2023-06-14,2023,6,14,wednesday +2023-06-21,2023,6,21,wednesday +2023-06-24,2023,6,24,saturday +2023-06-25,2023,6,25,sunday +2023-06-26,2023,6,26,monday +2023-06-27,2023,6,27,tuesday +2023-06-29,2023,6,29,thursday +2023-06-30,2023,6,30,friday +2023-07-03,2023,7,3,monday +2023-07-06,2023,7,6,thursday +2023-07-08,2023,7,8,saturday +2023-07-10,2023,7,10,monday +2023-07-12,2023,7,12,wednesday +2023-07-14,2023,7,14,friday +2023-07-15,2023,7,15,saturday +2023-07-16,2023,7,16,sunday +2023-07-17,2023,7,17,monday +2023-07-20,2023,7,20,thursday +2023-07-21,2023,7,21,friday +2023-07-23,2023,7,23,sunday +2023-07-24,2023,7,24,monday +2023-07-25,2023,7,25,tuesday +2023-07-30,2023,7,30,sunday +2023-07-31,2023,7,31,monday +2023-08-01,2023,8,1,tuesday +2023-08-04,2023,8,4,friday +2023-08-08,2023,8,8,tuesday +2023-08-09,2023,8,9,wednesday +2023-08-10,2023,8,10,thursday +2023-08-12,2023,8,12,saturday +2023-08-13,2023,8,13,sunday +2023-08-21,2023,8,21,monday +2023-08-26,2023,8,26,saturday +2023-08-27,2023,8,27,sunday +2023-08-28,2023,8,28,monday +2023-08-29,2023,8,29,tuesday +2023-08-30,2023,8,30,wednesday +2023-09-01,2023,9,1,friday +2023-09-02,2023,9,2,saturday +2023-09-03,2023,9,3,sunday +2023-09-06,2023,9,6,wednesday +2023-09-08,2023,9,8,friday +2023-09-09,2023,9,9,saturday +2023-09-11,2023,9,11,monday +2023-09-13,2023,9,13,wednesday +2023-09-14,2023,9,14,thursday +2023-09-15,2023,9,15,friday +2023-09-17,2023,9,17,sunday +2023-09-19,2023,9,19,tuesday +2023-09-22,2023,9,22,friday +2023-09-23,2023,9,23,saturday +2023-09-25,2023,9,25,monday +2023-09-26,2023,9,26,tuesday +2023-09-27,2023,9,27,wednesday +2023-09-28,2023,9,28,thursday +2023-09-30,2023,9,30,saturday +2023-10-03,2023,10,3,tuesday +2023-10-11,2023,10,11,wednesday +2023-10-12,2023,10,12,thursday +2023-10-14,2023,10,14,saturday +2023-10-15,2023,10,15,sunday +2023-10-16,2023,10,16,monday +2023-10-18,2023,10,18,wednesday +2023-10-19,2023,10,19,thursday +2023-10-20,2023,10,20,friday +2023-10-21,2023,10,21,saturday +2023-10-22,2023,10,22,sunday +2023-10-23,2023,10,23,monday +2023-10-27,2023,10,27,friday +2023-10-28,2023,10,28,saturday +2023-10-31,2023,10,31,tuesday +2023-11-01,2023,11,1,wednesday +2023-11-02,2023,11,2,thursday +2023-11-05,2023,11,5,sunday +2023-11-06,2023,11,6,monday +2023-11-08,2023,11,8,wednesday +2023-11-10,2023,11,10,friday +2023-11-11,2023,11,11,saturday +2023-11-14,2023,11,14,tuesday +2023-11-15,2023,11,15,wednesday +2023-11-17,2023,11,17,friday +2023-11-19,2023,11,19,sunday +2023-11-21,2023,11,21,tuesday +2023-11-22,2023,11,22,wednesday +2023-11-24,2023,11,24,friday +2023-11-28,2023,11,28,tuesday +2023-11-30,2023,11,30,thursday +2023-12-01,2023,12,1,friday +2023-12-02,2023,12,2,saturday +2023-12-03,2023,12,3,sunday +2023-12-04,2023,12,4,monday +2023-12-07,2023,12,7,thursday +2023-12-12,2023,12,12,tuesday +2023-12-14,2023,12,14,thursday +2023-12-15,2023,12,15,friday +2023-12-17,2023,12,17,sunday +2023-12-18,2023,12,18,monday +2023-12-19,2023,12,19,tuesday +2023-12-21,2023,12,21,thursday +2023-12-22,2023,12,22,friday +2023-12-23,2023,12,23,saturday +2023-12-27,2023,12,27,wednesday +2023-12-31,2023,12,31,sunday +2024-01-02,2024,1,2,tuesday +2024-01-03,2024,1,3,wednesday +2024-01-06,2024,1,6,saturday +2024-01-07,2024,1,7,sunday +2024-01-08,2024,1,8,monday +2024-01-13,2024,1,13,saturday +2024-01-15,2024,1,15,monday +2024-01-18,2024,1,18,thursday +2024-01-19,2024,1,19,friday +2024-01-20,2024,1,20,saturday +2024-01-21,2024,1,21,sunday +2024-01-22,2024,1,22,monday +2024-01-24,2024,1,24,wednesday +2024-01-25,2024,1,25,thursday +2024-01-26,2024,1,26,friday +2024-01-27,2024,1,27,saturday +2024-01-28,2024,1,28,sunday +2024-01-31,2024,1,31,wednesday +2024-02-02,2024,2,2,friday +2024-02-04,2024,2,4,sunday +2024-02-06,2024,2,6,tuesday +2024-02-07,2024,2,7,wednesday +2024-02-11,2024,2,11,sunday +2024-02-12,2024,2,12,monday +2024-02-17,2024,2,17,saturday +2024-02-18,2024,2,18,sunday +2024-02-21,2024,2,21,wednesday +2024-02-22,2024,2,22,thursday +2024-02-25,2024,2,25,sunday +2024-02-26,2024,2,26,monday +2024-02-28,2024,2,28,wednesday +2024-03-01,2024,3,1,friday +2024-03-02,2024,3,2,saturday +2024-03-06,2024,3,6,wednesday +2023-01-02,2023,1,2,monday +2023-01-04,2023,1,4,wednesday +2023-01-06,2023,1,6,friday +2023-01-07,2023,1,7,saturday +2023-01-08,2023,1,8,sunday +2023-01-10,2023,1,10,tuesday +2023-01-12,2023,1,12,thursday +2023-01-14,2023,1,14,saturday +2023-01-16,2023,1,16,monday +2023-01-18,2023,1,18,wednesday +2023-01-19,2023,1,19,thursday +2023-01-20,2023,1,20,friday +2023-01-22,2023,1,22,sunday +2023-01-25,2023,1,25,wednesday +2023-01-29,2023,1,29,sunday +2023-01-30,2023,1,30,monday +2023-02-01,2023,2,1,wednesday +2023-02-03,2023,2,3,friday +2023-02-04,2023,2,4,saturday +2023-02-05,2023,2,5,sunday +2023-02-07,2023,2,7,tuesday +2023-02-09,2023,2,9,thursday +2023-02-11,2023,2,11,saturday +2023-02-12,2023,2,12,sunday +2023-02-13,2023,2,13,monday +2023-02-15,2023,2,15,wednesday +2023-02-16,2023,2,16,thursday +2023-02-17,2023,2,17,friday +2023-02-19,2023,2,19,sunday +2023-02-20,2023,2,20,monday +2023-02-22,2023,2,22,wednesday +2023-02-28,2023,2,28,tuesday +2023-03-02,2023,3,2,thursday +2023-03-03,2023,3,3,friday +2023-03-04,2023,3,4,saturday +2023-03-06,2023,3,6,monday +2023-03-08,2023,3,8,wednesday +2023-03-09,2023,3,9,thursday +2023-03-10,2023,3,10,friday +2023-03-13,2023,3,13,monday +2023-03-14,2023,3,14,tuesday +2023-03-15,2023,3,15,wednesday +2023-03-17,2023,3,17,friday +2023-03-19,2023,3,19,sunday +2023-03-22,2023,3,22,wednesday +2023-03-25,2023,3,25,saturday +2023-03-26,2023,3,26,sunday +2023-03-27,2023,3,27,monday +2023-03-29,2023,3,29,wednesday +2023-03-31,2023,3,31,friday +2023-04-01,2023,4,1,saturday +2023-04-03,2023,4,3,monday +2023-04-06,2023,4,6,thursday +2023-04-07,2023,4,7,friday +2023-04-08,2023,4,8,saturday +2023-04-13,2023,4,13,thursday +2023-04-14,2023,4,14,friday +2023-04-15,2023,4,15,saturday +2023-04-17,2023,4,17,monday +2023-04-19,2023,4,19,wednesday +2023-04-23,2023,4,23,sunday +2023-04-24,2023,4,24,monday +2023-04-26,2023,4,26,wednesday +2023-04-28,2023,4,28,friday +2023-04-30,2023,4,30,sunday +2023-05-01,2023,5,1,monday +2023-05-03,2023,5,3,wednesday +2023-05-04,2023,5,4,thursday +2023-05-05,2023,5,5,friday +2023-05-07,2023,5,7,sunday +2023-05-08,2023,5,8,monday +2023-05-10,2023,5,10,wednesday +2023-05-13,2023,5,13,saturday +2023-05-16,2023,5,16,tuesday +2023-05-18,2023,5,18,thursday +2023-05-21,2023,5,21,sunday +2023-05-27,2023,5,27,saturday +2023-05-28,2023,5,28,sunday +2023-05-30,2023,5,30,tuesday +2023-05-31,2023,5,31,wednesday +2023-06-03,2023,6,3,saturday +2023-06-04,2023,6,4,sunday +2023-06-05,2023,6,5,monday +2023-06-06,2023,6,6,tuesday +2023-06-07,2023,6,7,wednesday +2023-06-09,2023,6,9,friday +2023-06-11,2023,6,11,sunday +2023-06-13,2023,6,13,tuesday +2023-06-15,2023,6,15,thursday +2023-06-16,2023,6,16,friday +2023-06-17,2023,6,17,saturday +2023-06-18,2023,6,18,sunday +2023-06-19,2023,6,19,monday +2023-06-20,2023,6,20,tuesday +2023-06-22,2023,6,22,thursday +2023-06-23,2023,6,23,friday +2023-06-28,2023,6,28,wednesday +2023-07-01,2023,7,1,saturday +2023-07-02,2023,7,2,sunday +2023-07-04,2023,7,4,tuesday +2023-07-05,2023,7,5,wednesday +2023-07-07,2023,7,7,friday +2023-07-09,2023,7,9,sunday +2023-07-11,2023,7,11,tuesday +2023-07-13,2023,7,13,thursday +2023-07-18,2023,7,18,tuesday +2023-07-19,2023,7,19,wednesday +2023-07-22,2023,7,22,saturday +2023-07-26,2023,7,26,wednesday +2023-07-27,2023,7,27,thursday +2023-07-28,2023,7,28,friday +2023-07-29,2023,7,29,saturday +2023-08-02,2023,8,2,wednesday +2023-08-03,2023,8,3,thursday +2023-08-05,2023,8,5,saturday +2023-08-06,2023,8,6,sunday +2023-08-07,2023,8,7,monday +2023-08-11,2023,8,11,friday +2023-08-14,2023,8,14,monday +2023-08-15,2023,8,15,tuesday +2023-08-16,2023,8,16,wednesday +2023-08-17,2023,8,17,thursday +2023-08-18,2023,8,18,friday +2023-08-19,2023,8,19,saturday +2023-08-20,2023,8,20,sunday +2023-08-22,2023,8,22,tuesday +2023-08-23,2023,8,23,wednesday +2023-08-24,2023,8,24,thursday +2023-08-25,2023,8,25,friday +2023-08-31,2023,8,31,thursday +2023-09-04,2023,9,4,monday +2023-09-05,2023,9,5,tuesday +2023-09-07,2023,9,7,thursday +2023-09-10,2023,9,10,sunday +2023-09-12,2023,9,12,tuesday +2023-09-16,2023,9,16,saturday +2023-09-18,2023,9,18,monday +2023-09-20,2023,9,20,wednesday +2023-09-21,2023,9,21,thursday +2023-09-24,2023,9,24,sunday +2023-09-29,2023,9,29,friday +2023-10-01,2023,10,1,sunday +2023-10-02,2023,10,2,monday +2023-10-04,2023,10,4,wednesday +2023-10-05,2023,10,5,thursday +2023-10-06,2023,10,6,friday +2023-10-07,2023,10,7,saturday +2023-10-08,2023,10,8,sunday +2023-10-09,2023,10,9,monday +2023-10-10,2023,10,10,tuesday +2023-10-13,2023,10,13,friday +2023-10-17,2023,10,17,tuesday +2023-10-24,2023,10,24,tuesday +2023-10-25,2023,10,25,wednesday +2023-10-26,2023,10,26,thursday +2023-10-29,2023,10,29,sunday +2023-10-30,2023,10,30,monday +2023-11-03,2023,11,3,friday +2023-11-04,2023,11,4,saturday +2023-11-07,2023,11,7,tuesday +2023-11-09,2023,11,9,thursday +2023-11-12,2023,11,12,sunday +2023-11-13,2023,11,13,monday +2023-11-16,2023,11,16,thursday +2023-11-18,2023,11,18,saturday +2023-11-20,2023,11,20,monday +2023-11-23,2023,11,23,thursday +2023-11-25,2023,11,25,saturday +2023-11-26,2023,11,26,sunday +2023-11-27,2023,11,27,monday +2023-11-29,2023,11,29,wednesday +2023-12-05,2023,12,5,tuesday +2023-12-06,2023,12,6,wednesday +2023-12-08,2023,12,8,friday +2023-12-09,2023,12,9,saturday +2023-12-10,2023,12,10,sunday +2023-12-11,2023,12,11,monday +2023-12-13,2023,12,13,wednesday +2023-12-16,2023,12,16,saturday +2023-12-20,2023,12,20,wednesday +2023-12-24,2023,12,24,sunday +2023-12-25,2023,12,25,monday +2023-12-26,2023,12,26,tuesday +2023-12-28,2023,12,28,thursday +2023-12-29,2023,12,29,friday +2023-12-30,2023,12,30,saturday +2024-01-01,2024,1,1,monday +2024-01-04,2024,1,4,thursday +2024-01-05,2024,1,5,friday +2024-01-09,2024,1,9,tuesday +2024-01-10,2024,1,10,wednesday +2024-01-11,2024,1,11,thursday +2024-01-12,2024,1,12,friday +2024-01-14,2024,1,14,sunday +2024-01-16,2024,1,16,tuesday +2024-01-17,2024,1,17,wednesday +2024-01-23,2024,1,23,tuesday +2024-01-29,2024,1,29,monday +2024-01-30,2024,1,30,tuesday +2024-02-01,2024,2,1,thursday +2024-02-03,2024,2,3,saturday +2024-02-05,2024,2,5,monday +2024-02-08,2024,2,8,thursday +2024-02-09,2024,2,9,friday +2024-02-10,2024,2,10,saturday +2024-02-13,2024,2,13,tuesday +2024-02-14,2024,2,14,wednesday +2024-02-15,2024,2,15,thursday +2024-02-16,2024,2,16,friday +2024-02-19,2024,2,19,monday +2024-02-20,2024,2,20,tuesday +2024-02-23,2024,2,23,friday +2024-02-24,2024,2,24,saturday +2024-03-07,2024,3,7,thursday +2024-03-08,2024,3,8,friday +2024-03-09,2024,3,9,saturday +2024-03-10,2024,3,10,sunday +2024-03-11,2024,3,11,monday +2024-03-12,2024,3,12,tuesday +2024-03-13,2024,3,13,wednesday +2024-03-15,2024,3,15,friday +2024-03-17,2024,3,17,sunday +2024-03-18,2024,3,18,monday +2024-03-19,2024,3,19,tuesday +2024-03-22,2024,3,22,friday +2024-03-23,2024,3,23,saturday +2024-03-24,2024,3,24,sunday +2024-03-27,2024,3,27,wednesday +2024-03-28,2024,3,28,thursday +2024-03-29,2024,3,29,friday +2024-03-30,2024,3,30,saturday +2024-03-31,2024,3,31,sunday +2024-04-01,2024,4,1,monday +2024-04-02,2024,4,2,tuesday +2024-04-03,2024,4,3,wednesday +2024-04-04,2024,4,4,thursday +2024-04-06,2024,4,6,saturday +2024-04-07,2024,4,7,sunday +2024-04-09,2024,4,9,tuesday +2024-04-10,2024,4,10,wednesday +2024-04-13,2024,4,13,saturday +2024-04-16,2024,4,16,tuesday +2024-04-17,2024,4,17,wednesday +2024-04-18,2024,4,18,thursday +2024-04-19,2024,4,19,friday +2024-04-20,2024,4,20,saturday +2024-04-23,2024,4,23,tuesday +2024-04-27,2024,4,27,saturday +2024-04-28,2024,4,28,sunday +2024-04-29,2024,4,29,monday +2024-05-02,2024,5,2,thursday +2024-05-03,2024,5,3,friday +2024-05-04,2024,5,4,saturday +2024-05-05,2024,5,5,sunday +2024-05-07,2024,5,7,tuesday +2024-05-15,2024,5,15,wednesday +2024-05-16,2024,5,16,thursday +2024-05-17,2024,5,17,friday +2024-05-20,2024,5,20,monday +2024-05-21,2024,5,21,tuesday +2024-05-23,2024,5,23,thursday +2024-05-28,2024,5,28,tuesday +2024-05-29,2024,5,29,wednesday +2024-06-01,2024,6,1,saturday +2024-06-02,2024,6,2,sunday +2024-06-03,2024,6,3,monday +2024-06-05,2024,6,5,wednesday +2024-06-07,2024,6,7,friday +2024-06-10,2024,6,10,monday +2024-06-12,2024,6,12,wednesday +2024-06-14,2024,6,14,friday +2024-06-15,2024,6,15,saturday +2024-06-16,2024,6,16,sunday +2024-06-18,2024,6,18,tuesday +2024-06-19,2024,6,19,wednesday +2024-06-20,2024,6,20,thursday +2024-06-22,2024,6,22,saturday +2024-06-25,2024,6,25,tuesday +2024-06-26,2024,6,26,wednesday +2024-06-27,2024,6,27,thursday +2024-06-29,2024,6,29,saturday +2024-06-30,2024,6,30,sunday +2024-07-01,2024,7,1,monday +2024-07-02,2024,7,2,tuesday +2024-07-04,2024,7,4,thursday +2024-07-06,2024,7,6,saturday +2024-07-08,2024,7,8,monday +2024-07-09,2024,7,9,tuesday +2024-07-13,2024,7,13,saturday +2024-07-14,2024,7,14,sunday +2024-07-15,2024,7,15,monday +2024-07-18,2024,7,18,thursday +2024-07-21,2024,7,21,sunday +2024-07-23,2024,7,23,tuesday +2024-07-25,2024,7,25,thursday +2024-07-26,2024,7,26,friday +2024-07-28,2024,7,28,sunday +2024-08-01,2024,8,1,thursday +2024-08-03,2024,8,3,saturday +2024-08-06,2024,8,6,tuesday +2024-08-09,2024,8,9,friday +2024-08-10,2024,8,10,saturday +2024-08-11,2024,8,11,sunday +2024-08-12,2024,8,12,monday +2024-08-14,2024,8,14,wednesday +2024-08-20,2024,8,20,tuesday +2024-08-22,2024,8,22,thursday +2024-08-24,2024,8,24,saturday +2024-08-28,2024,8,28,wednesday +2024-08-29,2024,8,29,thursday +2024-08-30,2024,8,30,friday +2024-08-31,2024,8,31,saturday +2024-09-01,2024,9,1,sunday +2024-09-04,2024,9,4,wednesday +2024-09-07,2024,9,7,saturday +2024-09-10,2024,9,10,tuesday +2024-09-11,2024,9,11,wednesday +2024-09-13,2024,9,13,friday +2024-09-16,2024,9,16,monday +2024-09-19,2024,9,19,thursday +2024-09-20,2024,9,20,friday +2024-09-21,2024,9,21,saturday +2024-09-22,2024,9,22,sunday +2024-09-23,2024,9,23,monday +2024-09-24,2024,9,24,tuesday +2024-09-25,2024,9,25,wednesday +2024-09-26,2024,9,26,thursday +2024-09-28,2024,9,28,saturday +2024-09-30,2024,9,30,monday +2024-10-01,2024,10,1,tuesday +2024-10-03,2024,10,3,thursday +2024-10-04,2024,10,4,friday +2024-10-06,2024,10,6,sunday +2024-10-09,2024,10,9,wednesday +2024-10-11,2024,10,11,friday +2024-10-12,2024,10,12,saturday +2024-10-13,2024,10,13,sunday +2024-10-23,2024,10,23,wednesday +2024-10-24,2024,10,24,thursday +2024-10-26,2024,10,26,saturday +2024-10-27,2024,10,27,sunday +2024-10-29,2024,10,29,tuesday +2024-10-30,2024,10,30,wednesday +2024-10-31,2024,10,31,thursday +2024-11-01,2024,11,1,friday +2024-11-02,2024,11,2,saturday +2024-11-04,2024,11,4,monday +2024-11-05,2024,11,5,tuesday +2024-11-06,2024,11,6,wednesday +2024-11-07,2024,11,7,thursday +2024-11-08,2024,11,8,friday +2024-11-12,2024,11,12,tuesday +2024-11-13,2024,11,13,wednesday +2024-11-14,2024,11,14,thursday +2024-11-15,2024,11,15,friday +2024-11-16,2024,11,16,saturday +2024-11-19,2024,11,19,tuesday +2024-11-20,2024,11,20,wednesday +2024-11-21,2024,11,21,thursday +2024-11-22,2024,11,22,friday +2024-11-23,2024,11,23,saturday +2024-11-26,2024,11,26,tuesday +2024-11-29,2024,11,29,friday +2024-12-04,2024,12,4,wednesday +2024-12-07,2024,12,7,saturday +2024-12-12,2024,12,12,thursday +2024-12-15,2024,12,15,sunday +2024-12-21,2024,12,21,saturday +2024-12-22,2024,12,22,sunday +2024-12-25,2024,12,25,wednesday +2024-12-27,2024,12,27,friday +2024-12-28,2024,12,28,saturday +2024-12-30,2024,12,30,monday +2025-01-03,2025,1,3,friday +2025-01-04,2025,1,4,saturday +2025-01-07,2025,1,7,tuesday +2025-01-09,2025,1,9,thursday +2025-01-12,2025,1,12,sunday +2025-01-13,2025,1,13,monday +2025-01-16,2025,1,16,thursday +2025-01-17,2025,1,17,friday +2025-01-18,2025,1,18,saturday +2025-01-20,2025,1,20,monday +2025-01-23,2025,1,23,thursday +2025-01-24,2025,1,24,friday +2025-01-25,2025,1,25,saturday +2025-01-26,2025,1,26,sunday +2025-01-29,2025,1,29,wednesday +2025-01-31,2025,1,31,friday +2025-02-01,2025,2,1,saturday +2025-02-02,2025,2,2,sunday +2025-02-04,2025,2,4,tuesday +2025-02-06,2025,2,6,thursday +2025-02-07,2025,2,7,friday +2025-02-08,2025,2,8,saturday +2025-02-14,2025,2,14,friday +2025-02-15,2025,2,15,saturday +2025-02-16,2025,2,16,sunday +2025-02-17,2025,2,17,monday +2025-02-18,2025,2,18,tuesday +2025-02-20,2025,2,20,thursday +2025-02-22,2025,2,22,saturday +2025-02-23,2025,2,23,sunday +2025-02-26,2025,2,26,wednesday +2025-02-27,2025,2,27,thursday +2025-03-05,2025,3,5,wednesday +2025-03-06,2025,3,6,thursday +2025-03-07,2025,3,7,friday +2025-03-08,2025,3,8,saturday +2025-03-11,2025,3,11,tuesday +2025-03-12,2025,3,12,wednesday +2025-03-13,2025,3,13,thursday +2025-03-16,2025,3,16,sunday +2025-03-18,2025,3,18,tuesday +2025-03-21,2025,3,21,friday +2025-03-22,2025,3,22,saturday +2025-03-23,2025,3,23,sunday +2025-03-25,2025,3,25,tuesday +2025-03-26,2025,3,26,wednesday +2025-03-29,2025,3,29,saturday +2025-04-01,2025,4,1,tuesday +2025-04-02,2025,4,2,wednesday +2025-04-03,2025,4,3,thursday +2025-04-05,2025,4,5,saturday +2025-04-07,2025,4,7,monday +2025-04-12,2025,4,12,saturday +2024-02-27,2024,2,27,tuesday +2024-02-29,2024,2,29,thursday +2024-03-03,2024,3,3,sunday +2024-03-04,2024,3,4,monday +2024-03-05,2024,3,5,tuesday +2024-03-14,2024,3,14,thursday +2024-03-16,2024,3,16,saturday +2024-03-20,2024,3,20,wednesday +2024-03-21,2024,3,21,thursday +2024-03-25,2024,3,25,monday +2024-03-26,2024,3,26,tuesday +2024-04-05,2024,4,5,friday +2024-04-08,2024,4,8,monday +2024-04-11,2024,4,11,thursday +2024-04-12,2024,4,12,friday +2024-04-14,2024,4,14,sunday +2024-04-15,2024,4,15,monday +2024-04-21,2024,4,21,sunday +2024-04-22,2024,4,22,monday +2024-04-24,2024,4,24,wednesday +2024-04-25,2024,4,25,thursday +2024-04-26,2024,4,26,friday +2024-04-30,2024,4,30,tuesday +2024-05-01,2024,5,1,wednesday +2024-05-06,2024,5,6,monday +2024-05-08,2024,5,8,wednesday +2024-05-09,2024,5,9,thursday +2024-05-10,2024,5,10,friday +2024-05-11,2024,5,11,saturday +2024-05-12,2024,5,12,sunday +2024-05-13,2024,5,13,monday +2024-05-14,2024,5,14,tuesday +2024-05-18,2024,5,18,saturday +2024-05-19,2024,5,19,sunday +2024-05-22,2024,5,22,wednesday +2024-05-24,2024,5,24,friday +2024-05-25,2024,5,25,saturday +2024-05-26,2024,5,26,sunday +2024-05-27,2024,5,27,monday +2024-05-30,2024,5,30,thursday +2024-05-31,2024,5,31,friday +2024-06-04,2024,6,4,tuesday +2024-06-06,2024,6,6,thursday +2024-06-08,2024,6,8,saturday +2024-06-09,2024,6,9,sunday +2024-06-11,2024,6,11,tuesday +2024-06-13,2024,6,13,thursday +2024-06-17,2024,6,17,monday +2024-06-21,2024,6,21,friday +2024-06-23,2024,6,23,sunday +2024-06-24,2024,6,24,monday +2024-06-28,2024,6,28,friday +2024-07-03,2024,7,3,wednesday +2024-07-05,2024,7,5,friday +2024-07-07,2024,7,7,sunday +2024-07-10,2024,7,10,wednesday +2024-07-11,2024,7,11,thursday +2024-07-12,2024,7,12,friday +2024-07-16,2024,7,16,tuesday +2024-07-17,2024,7,17,wednesday +2024-07-19,2024,7,19,friday +2024-07-20,2024,7,20,saturday +2024-07-22,2024,7,22,monday +2024-07-24,2024,7,24,wednesday +2024-07-27,2024,7,27,saturday +2024-07-29,2024,7,29,monday +2024-07-30,2024,7,30,tuesday +2024-07-31,2024,7,31,wednesday +2024-08-02,2024,8,2,friday +2024-08-04,2024,8,4,sunday +2024-08-05,2024,8,5,monday +2024-08-07,2024,8,7,wednesday +2024-08-08,2024,8,8,thursday +2024-08-13,2024,8,13,tuesday +2024-08-15,2024,8,15,thursday +2024-08-16,2024,8,16,friday +2024-08-17,2024,8,17,saturday +2024-08-18,2024,8,18,sunday +2024-08-19,2024,8,19,monday +2024-08-21,2024,8,21,wednesday +2024-08-23,2024,8,23,friday +2024-08-25,2024,8,25,sunday +2024-08-26,2024,8,26,monday +2024-08-27,2024,8,27,tuesday +2024-09-02,2024,9,2,monday +2024-09-03,2024,9,3,tuesday +2024-09-05,2024,9,5,thursday +2024-09-06,2024,9,6,friday +2024-09-08,2024,9,8,sunday +2024-09-09,2024,9,9,monday +2024-09-12,2024,9,12,thursday +2024-09-14,2024,9,14,saturday +2024-09-15,2024,9,15,sunday +2024-09-17,2024,9,17,tuesday +2024-09-18,2024,9,18,wednesday +2024-09-27,2024,9,27,friday +2024-09-29,2024,9,29,sunday +2024-10-02,2024,10,2,wednesday +2024-10-05,2024,10,5,saturday +2024-10-07,2024,10,7,monday +2024-10-08,2024,10,8,tuesday +2024-10-10,2024,10,10,thursday +2024-10-14,2024,10,14,monday +2024-10-15,2024,10,15,tuesday +2024-10-16,2024,10,16,wednesday +2024-10-17,2024,10,17,thursday +2024-10-18,2024,10,18,friday +2024-10-19,2024,10,19,saturday +2024-10-20,2024,10,20,sunday +2024-10-21,2024,10,21,monday +2024-10-22,2024,10,22,tuesday +2024-10-25,2024,10,25,friday +2024-10-28,2024,10,28,monday +2024-11-03,2024,11,3,sunday +2024-11-09,2024,11,9,saturday +2024-11-10,2024,11,10,sunday +2024-11-11,2024,11,11,monday +2024-11-17,2024,11,17,sunday +2024-11-18,2024,11,18,monday +2024-11-24,2024,11,24,sunday +2024-11-25,2024,11,25,monday +2024-11-27,2024,11,27,wednesday +2024-11-28,2024,11,28,thursday +2024-11-30,2024,11,30,saturday +2024-12-01,2024,12,1,sunday +2024-12-02,2024,12,2,monday +2024-12-03,2024,12,3,tuesday +2024-12-05,2024,12,5,thursday +2024-12-06,2024,12,6,friday +2024-12-08,2024,12,8,sunday +2024-12-09,2024,12,9,monday +2024-12-10,2024,12,10,tuesday +2024-12-11,2024,12,11,wednesday +2024-12-13,2024,12,13,friday +2024-12-14,2024,12,14,saturday +2024-12-16,2024,12,16,monday +2024-12-17,2024,12,17,tuesday +2024-12-18,2024,12,18,wednesday +2024-12-19,2024,12,19,thursday +2024-12-20,2024,12,20,friday +2024-12-23,2024,12,23,monday +2024-12-24,2024,12,24,tuesday +2024-12-26,2024,12,26,thursday +2024-12-29,2024,12,29,sunday +2024-12-31,2024,12,31,tuesday +2025-01-01,2025,1,1,wednesday +2025-01-02,2025,1,2,thursday +2025-01-05,2025,1,5,sunday +2025-01-06,2025,1,6,monday +2025-01-08,2025,1,8,wednesday +2025-01-10,2025,1,10,friday +2025-01-11,2025,1,11,saturday +2025-01-14,2025,1,14,tuesday +2025-01-15,2025,1,15,wednesday +2025-01-19,2025,1,19,sunday +2025-01-21,2025,1,21,tuesday +2025-01-22,2025,1,22,wednesday +2025-01-27,2025,1,27,monday +2025-01-28,2025,1,28,tuesday +2025-01-30,2025,1,30,thursday +2025-02-03,2025,2,3,monday +2025-02-05,2025,2,5,wednesday +2025-02-09,2025,2,9,sunday +2025-02-10,2025,2,10,monday +2025-02-11,2025,2,11,tuesday +2025-02-12,2025,2,12,wednesday +2025-02-13,2025,2,13,thursday +2025-02-19,2025,2,19,wednesday +2025-02-21,2025,2,21,friday +2025-02-24,2025,2,24,monday +2025-02-25,2025,2,25,tuesday +2025-02-28,2025,2,28,friday +2025-03-01,2025,3,1,saturday +2025-03-02,2025,3,2,sunday +2025-03-03,2025,3,3,monday +2025-03-04,2025,3,4,tuesday +2025-03-09,2025,3,9,sunday +2025-03-10,2025,3,10,monday +2025-03-14,2025,3,14,friday +2025-03-15,2025,3,15,saturday +2025-03-17,2025,3,17,monday +2025-03-19,2025,3,19,wednesday +2025-03-20,2025,3,20,thursday +2025-03-24,2025,3,24,monday +2025-03-27,2025,3,27,thursday +2025-03-28,2025,3,28,friday +2025-03-30,2025,3,30,sunday +2025-03-31,2025,3,31,monday +2025-04-04,2025,4,4,friday +2025-04-06,2025,4,6,sunday +2025-04-08,2025,4,8,tuesday +2025-04-09,2025,4,9,wednesday +2025-04-10,2025,4,10,thursday +2025-04-11,2025,4,11,friday +2025-04-13,2025,4,13,sunday +2025-04-14,2025,4,14,monday +2025-04-15,2025,4,15,tuesday +2025-04-16,2025,4,16,wednesday +2025-04-17,2025,4,17,thursday +2025-04-18,2025,4,18,friday +2025-04-24,2025,4,24,thursday +2025-04-25,2025,4,25,friday +2025-04-28,2025,4,28,monday +2025-04-29,2025,4,29,tuesday +2025-04-30,2025,4,30,wednesday +2025-05-01,2025,5,1,thursday +2025-05-05,2025,5,5,monday +2025-05-07,2025,5,7,wednesday +2025-05-08,2025,5,8,thursday +2025-05-09,2025,5,9,friday +2025-05-10,2025,5,10,saturday +2025-05-11,2025,5,11,sunday +2025-05-14,2025,5,14,wednesday +2025-04-19,2025,4,19,saturday +2025-04-20,2025,4,20,sunday +2025-04-21,2025,4,21,monday +2025-04-22,2025,4,22,tuesday +2025-04-23,2025,4,23,wednesday +2025-04-26,2025,4,26,saturday +2025-04-27,2025,4,27,sunday +2025-05-02,2025,5,2,friday +2025-05-03,2025,5,3,saturday +2025-05-04,2025,5,4,sunday +2025-05-06,2025,5,6,tuesday +2025-05-12,2025,5,12,monday +2025-05-13,2025,5,13,tuesday +2025-05-17,2025,5,17,saturday +2025-05-19,2025,5,19,monday +2025-05-21,2025,5,21,wednesday +2025-05-25,2025,5,25,sunday +2025-05-27,2025,5,27,tuesday +2025-05-30,2025,5,30,friday +2025-06-01,2025,6,1,sunday +2025-06-02,2025,6,2,monday +2025-06-03,2025,6,3,tuesday +2025-06-05,2025,6,5,thursday +2025-06-08,2025,6,8,sunday +2025-06-10,2025,6,10,tuesday +2025-06-11,2025,6,11,wednesday +2025-06-13,2025,6,13,friday +2025-06-14,2025,6,14,saturday +2025-06-17,2025,6,17,tuesday +2025-06-19,2025,6,19,thursday +2025-06-20,2025,6,20,friday +2025-06-22,2025,6,22,sunday +2025-06-23,2025,6,23,monday +2025-06-24,2025,6,24,tuesday +2025-06-28,2025,6,28,saturday +2025-06-29,2025,6,29,sunday +2025-06-30,2025,6,30,monday +2025-07-01,2025,7,1,tuesday +2025-07-02,2025,7,2,wednesday +2025-07-03,2025,7,3,thursday +2025-07-04,2025,7,4,friday +2025-07-05,2025,7,5,saturday +2025-07-09,2025,7,9,wednesday +2025-07-11,2025,7,11,friday +2025-07-12,2025,7,12,saturday +2025-07-13,2025,7,13,sunday +2025-07-14,2025,7,14,monday +2025-07-15,2025,7,15,tuesday +2025-07-17,2025,7,17,thursday +2025-07-19,2025,7,19,saturday +2025-07-20,2025,7,20,sunday +2025-07-22,2025,7,22,tuesday +2025-07-24,2025,7,24,thursday +2025-07-25,2025,7,25,friday +2025-07-28,2025,7,28,monday +2025-07-29,2025,7,29,tuesday +2025-07-30,2025,7,30,wednesday +2025-08-02,2025,8,2,saturday +2025-08-03,2025,8,3,sunday +2025-08-06,2025,8,6,wednesday +2025-08-07,2025,8,7,thursday +2025-08-08,2025,8,8,friday +2025-08-09,2025,8,9,saturday +2025-08-10,2025,8,10,sunday +2025-08-15,2025,8,15,friday +2025-08-22,2025,8,22,friday +2025-08-25,2025,8,25,monday +2025-08-27,2025,8,27,wednesday +2025-08-29,2025,8,29,friday +2025-08-30,2025,8,30,saturday +2025-09-02,2025,9,2,tuesday +2025-09-03,2025,9,3,wednesday +2025-09-04,2025,9,4,thursday +2025-09-06,2025,9,6,saturday +2025-09-11,2025,9,11,thursday +2025-09-14,2025,9,14,sunday +2025-09-15,2025,9,15,monday +2025-09-20,2025,9,20,saturday +2025-09-21,2025,9,21,sunday +2025-09-23,2025,9,23,tuesday +2025-09-30,2025,9,30,tuesday +2025-10-02,2025,10,2,thursday +2025-10-03,2025,10,3,friday +2025-10-04,2025,10,4,saturday +2025-10-06,2025,10,6,monday +2025-10-09,2025,10,9,thursday +2025-10-13,2025,10,13,monday +2025-10-14,2025,10,14,tuesday +2025-10-15,2025,10,15,wednesday +2025-10-19,2025,10,19,sunday +2025-10-20,2025,10,20,monday +2025-10-23,2025,10,23,thursday +2025-10-24,2025,10,24,friday +2025-10-25,2025,10,25,saturday +2025-10-30,2025,10,30,thursday +2025-11-01,2025,11,1,saturday +2025-11-02,2025,11,2,sunday +2025-11-04,2025,11,4,tuesday +2025-11-05,2025,11,5,wednesday +2025-11-06,2025,11,6,thursday +2025-11-08,2025,11,8,saturday +2025-11-12,2025,11,12,wednesday +2025-11-13,2025,11,13,thursday +2025-11-14,2025,11,14,friday +2025-11-16,2025,11,16,sunday +2025-11-17,2025,11,17,monday +2025-11-18,2025,11,18,tuesday +2025-11-21,2025,11,21,friday +2025-11-22,2025,11,22,saturday +2025-11-26,2025,11,26,wednesday +2025-11-27,2025,11,27,thursday +2025-11-30,2025,11,30,sunday +2025-12-01,2025,12,1,monday +2025-12-02,2025,12,2,tuesday +2025-12-04,2025,12,4,thursday +2025-12-08,2025,12,8,monday +2025-12-11,2025,12,11,thursday +2025-12-12,2025,12,12,friday +2025-12-13,2025,12,13,saturday +2025-12-18,2025,12,18,thursday +2025-12-21,2025,12,21,sunday +2025-12-22,2025,12,22,monday +2025-12-23,2025,12,23,tuesday +2025-12-25,2025,12,25,thursday +2025-12-27,2025,12,27,saturday +2025-12-28,2025,12,28,sunday +2025-12-29,2025,12,29,monday +2026-01-04,2026,1,4,sunday +2026-01-06,2026,1,6,tuesday +2026-01-08,2026,1,8,thursday +2026-01-10,2026,1,10,saturday +2026-01-11,2026,1,11,sunday +2026-01-14,2026,1,14,wednesday +2026-01-15,2026,1,15,thursday +2026-01-17,2026,1,17,saturday +2026-01-19,2026,1,19,monday +2026-01-20,2026,1,20,tuesday +2026-01-21,2026,1,21,wednesday +2026-01-23,2026,1,23,friday +2026-01-24,2026,1,24,saturday +2026-01-25,2026,1,25,sunday +2026-01-26,2026,1,26,monday +2026-01-28,2026,1,28,wednesday +2026-01-29,2026,1,29,thursday +2026-01-30,2026,1,30,friday +2026-01-31,2026,1,31,saturday +2026-02-03,2026,2,3,tuesday +2026-02-04,2026,2,4,wednesday +2026-02-05,2026,2,5,thursday +2026-02-06,2026,2,6,friday +2026-02-07,2026,2,7,saturday +2026-02-09,2026,2,9,monday +2026-02-10,2026,2,10,tuesday +2026-02-12,2026,2,12,thursday +2026-02-13,2026,2,13,friday +2026-02-14,2026,2,14,saturday +2026-02-15,2026,2,15,sunday +2026-02-18,2026,2,18,wednesday +2026-02-20,2026,2,20,friday +2026-02-22,2026,2,22,sunday +2026-02-23,2026,2,23,monday +2026-02-24,2026,2,24,tuesday +2026-02-25,2026,2,25,wednesday +2026-02-26,2026,2,26,thursday +2026-03-02,2026,3,2,monday +2026-03-03,2026,3,3,tuesday +2026-03-06,2026,3,6,friday +2026-03-08,2026,3,8,sunday +2026-03-09,2026,3,9,monday +2026-03-10,2026,3,10,tuesday +2026-03-11,2026,3,11,wednesday +2026-03-12,2026,3,12,thursday +2026-03-14,2026,3,14,saturday +2026-03-17,2026,3,17,tuesday +2026-03-20,2026,3,20,friday +2026-03-24,2026,3,24,tuesday +2026-03-27,2026,3,27,friday +2026-03-29,2026,3,29,sunday +2026-04-02,2026,4,2,thursday +2026-04-03,2026,4,3,friday +2026-04-04,2026,4,4,saturday +2026-04-06,2026,4,6,monday +2026-04-09,2026,4,9,thursday +2026-04-11,2026,4,11,saturday +2026-04-13,2026,4,13,monday +2026-04-14,2026,4,14,tuesday +2026-04-20,2026,4,20,monday +2026-04-21,2026,4,21,tuesday +2026-04-24,2026,4,24,friday +2026-04-25,2026,4,25,saturday +2026-04-26,2026,4,26,sunday +2026-04-28,2026,4,28,tuesday +2026-04-30,2026,4,30,thursday +2026-05-01,2026,5,1,friday +2026-05-02,2026,5,2,saturday +2026-05-05,2026,5,5,tuesday +2026-05-06,2026,5,6,wednesday +2026-05-07,2026,5,7,thursday +2026-05-09,2026,5,9,saturday +2026-05-10,2026,5,10,sunday +2026-05-11,2026,5,11,monday +2026-05-12,2026,5,12,tuesday +2026-05-13,2026,5,13,wednesday +2026-05-15,2026,5,15,friday +2026-05-16,2026,5,16,saturday +2026-05-19,2026,5,19,tuesday +2026-05-20,2026,5,20,wednesday +2026-05-25,2026,5,25,monday +2026-05-27,2026,5,27,wednesday +2026-05-28,2026,5,28,thursday +2026-05-30,2026,5,30,saturday +2026-06-01,2026,6,1,monday +2026-06-03,2026,6,3,wednesday +2025-05-15,2025,5,15,thursday +2025-05-16,2025,5,16,friday +2025-05-18,2025,5,18,sunday +2025-05-20,2025,5,20,tuesday +2025-05-22,2025,5,22,thursday +2025-05-23,2025,5,23,friday +2025-05-24,2025,5,24,saturday +2025-05-26,2025,5,26,monday +2025-05-28,2025,5,28,wednesday +2025-05-29,2025,5,29,thursday +2025-05-31,2025,5,31,saturday +2025-06-04,2025,6,4,wednesday +2025-06-06,2025,6,6,friday +2025-06-07,2025,6,7,saturday +2025-06-09,2025,6,9,monday +2025-06-12,2025,6,12,thursday +2025-06-15,2025,6,15,sunday +2025-06-16,2025,6,16,monday +2025-06-18,2025,6,18,wednesday +2025-06-21,2025,6,21,saturday +2025-06-25,2025,6,25,wednesday +2025-06-26,2025,6,26,thursday +2025-06-27,2025,6,27,friday +2025-07-06,2025,7,6,sunday +2025-07-07,2025,7,7,monday +2025-07-08,2025,7,8,tuesday +2025-07-10,2025,7,10,thursday +2025-07-16,2025,7,16,wednesday +2025-07-18,2025,7,18,friday +2025-07-21,2025,7,21,monday +2025-07-23,2025,7,23,wednesday +2025-07-26,2025,7,26,saturday +2025-07-27,2025,7,27,sunday +2025-07-31,2025,7,31,thursday +2025-08-01,2025,8,1,friday +2025-08-04,2025,8,4,monday +2025-08-05,2025,8,5,tuesday +2025-08-11,2025,8,11,monday +2025-08-12,2025,8,12,tuesday +2025-08-13,2025,8,13,wednesday +2025-08-14,2025,8,14,thursday +2025-08-16,2025,8,16,saturday +2025-08-17,2025,8,17,sunday +2025-08-18,2025,8,18,monday +2025-08-19,2025,8,19,tuesday +2025-08-20,2025,8,20,wednesday +2025-08-21,2025,8,21,thursday +2025-08-23,2025,8,23,saturday +2025-08-24,2025,8,24,sunday +2025-08-26,2025,8,26,tuesday +2025-08-28,2025,8,28,thursday +2025-08-31,2025,8,31,sunday +2025-09-01,2025,9,1,monday +2025-09-05,2025,9,5,friday +2025-09-07,2025,9,7,sunday +2025-09-08,2025,9,8,monday +2025-09-09,2025,9,9,tuesday +2025-09-10,2025,9,10,wednesday +2025-09-12,2025,9,12,friday +2025-09-13,2025,9,13,saturday +2025-09-16,2025,9,16,tuesday +2025-09-17,2025,9,17,wednesday +2025-09-18,2025,9,18,thursday +2025-09-19,2025,9,19,friday +2025-09-22,2025,9,22,monday +2025-09-24,2025,9,24,wednesday +2025-09-25,2025,9,25,thursday +2025-09-26,2025,9,26,friday +2025-09-27,2025,9,27,saturday +2025-09-28,2025,9,28,sunday +2025-09-29,2025,9,29,monday +2025-10-01,2025,10,1,wednesday +2025-10-05,2025,10,5,sunday +2025-10-07,2025,10,7,tuesday +2025-10-08,2025,10,8,wednesday +2025-10-10,2025,10,10,friday +2025-10-11,2025,10,11,saturday +2025-10-12,2025,10,12,sunday +2025-10-16,2025,10,16,thursday +2025-10-17,2025,10,17,friday +2025-10-18,2025,10,18,saturday +2025-10-21,2025,10,21,tuesday +2025-10-22,2025,10,22,wednesday +2025-10-26,2025,10,26,sunday +2025-10-27,2025,10,27,monday +2025-10-28,2025,10,28,tuesday +2025-10-29,2025,10,29,wednesday +2025-10-31,2025,10,31,friday +2025-11-03,2025,11,3,monday +2025-11-07,2025,11,7,friday +2025-11-09,2025,11,9,sunday +2025-11-10,2025,11,10,monday +2025-11-11,2025,11,11,tuesday +2025-11-15,2025,11,15,saturday +2025-11-19,2025,11,19,wednesday +2025-11-20,2025,11,20,thursday +2025-11-23,2025,11,23,sunday +2025-11-24,2025,11,24,monday +2025-11-25,2025,11,25,tuesday +2025-11-28,2025,11,28,friday +2025-11-29,2025,11,29,saturday +2025-12-03,2025,12,3,wednesday +2025-12-05,2025,12,5,friday +2025-12-06,2025,12,6,saturday +2025-12-07,2025,12,7,sunday +2025-12-09,2025,12,9,tuesday +2025-12-10,2025,12,10,wednesday +2025-12-14,2025,12,14,sunday +2025-12-15,2025,12,15,monday +2025-12-16,2025,12,16,tuesday +2025-12-17,2025,12,17,wednesday +2025-12-19,2025,12,19,friday +2025-12-20,2025,12,20,saturday +2025-12-24,2025,12,24,wednesday +2025-12-26,2025,12,26,friday +2025-12-30,2025,12,30,tuesday +2025-12-31,2025,12,31,wednesday +2026-01-01,2026,1,1,thursday +2026-01-02,2026,1,2,friday +2026-01-03,2026,1,3,saturday +2026-01-05,2026,1,5,monday +2026-01-07,2026,1,7,wednesday +2026-01-09,2026,1,9,friday +2026-01-12,2026,1,12,monday +2026-01-13,2026,1,13,tuesday +2026-01-16,2026,1,16,friday +2026-01-18,2026,1,18,sunday +2026-01-22,2026,1,22,thursday +2026-01-27,2026,1,27,tuesday +2026-02-01,2026,2,1,sunday +2026-02-02,2026,2,2,monday +2026-02-08,2026,2,8,sunday +2026-02-11,2026,2,11,wednesday +2026-02-16,2026,2,16,monday +2026-02-17,2026,2,17,tuesday +2026-02-19,2026,2,19,thursday +2026-02-21,2026,2,21,saturday +2026-02-27,2026,2,27,friday +2026-02-28,2026,2,28,saturday +2026-03-01,2026,3,1,sunday +2026-03-04,2026,3,4,wednesday +2026-03-05,2026,3,5,thursday +2026-03-07,2026,3,7,saturday +2026-03-13,2026,3,13,friday +2026-03-15,2026,3,15,sunday +2026-03-16,2026,3,16,monday +2026-03-18,2026,3,18,wednesday +2026-03-19,2026,3,19,thursday +2026-03-21,2026,3,21,saturday +2026-03-22,2026,3,22,sunday +2026-03-23,2026,3,23,monday +2026-03-25,2026,3,25,wednesday +2026-03-26,2026,3,26,thursday +2026-03-28,2026,3,28,saturday +2026-03-30,2026,3,30,monday +2026-03-31,2026,3,31,tuesday +2026-04-01,2026,4,1,wednesday +2026-04-05,2026,4,5,sunday +2026-04-07,2026,4,7,tuesday +2026-04-08,2026,4,8,wednesday +2026-04-10,2026,4,10,friday +2026-04-12,2026,4,12,sunday +2026-04-15,2026,4,15,wednesday +2026-04-16,2026,4,16,thursday +2026-04-17,2026,4,17,friday +2026-04-18,2026,4,18,saturday +2026-04-19,2026,4,19,sunday +2026-04-22,2026,4,22,wednesday +2026-04-23,2026,4,23,thursday +2026-04-27,2026,4,27,monday +2026-04-29,2026,4,29,wednesday +2026-05-03,2026,5,3,sunday +2026-05-04,2026,5,4,monday +2026-05-08,2026,5,8,friday +2026-05-14,2026,5,14,thursday +2026-05-17,2026,5,17,sunday +2026-05-18,2026,5,18,monday +2026-05-21,2026,5,21,thursday +2026-05-22,2026,5,22,friday +2026-05-23,2026,5,23,saturday +2026-05-24,2026,5,24,sunday +2026-05-26,2026,5,26,tuesday +2026-05-29,2026,5,29,friday +2026-05-31,2026,5,31,sunday +2026-06-02,2026,6,2,tuesday +2026-06-06,2026,6,6,saturday +2026-06-07,2026,6,7,sunday +2026-06-08,2026,6,8,monday +2026-06-09,2026,6,9,tuesday +2026-06-10,2026,6,10,wednesday +2026-06-11,2026,6,11,thursday +2026-06-12,2026,6,12,friday +2026-06-14,2026,6,14,sunday +2026-06-16,2026,6,16,tuesday +2026-06-17,2026,6,17,wednesday +2026-06-19,2026,6,19,friday +2026-06-21,2026,6,21,sunday +2026-06-22,2026,6,22,monday +2026-06-23,2026,6,23,tuesday +2026-06-25,2026,6,25,thursday +2026-06-26,2026,6,26,friday +2026-06-27,2026,6,27,saturday +2026-07-02,2026,7,2,thursday +2026-07-04,2026,7,4,saturday +2026-07-05,2026,7,5,sunday +2026-07-08,2026,7,8,wednesday +2026-07-09,2026,7,9,thursday +2026-07-10,2026,7,10,friday +2026-07-12,2026,7,12,sunday +2026-07-17,2026,7,17,friday +2026-07-18,2026,7,18,saturday +2026-07-21,2026,7,21,tuesday +2026-07-23,2026,7,23,thursday +2026-06-04,2026,6,4,thursday +2026-06-05,2026,6,5,friday +2026-06-13,2026,6,13,saturday +2026-06-15,2026,6,15,monday +2026-06-18,2026,6,18,thursday +2026-06-20,2026,6,20,saturday +2026-06-24,2026,6,24,wednesday +2026-06-28,2026,6,28,sunday +2026-06-29,2026,6,29,monday +2026-06-30,2026,6,30,tuesday +2026-07-01,2026,7,1,wednesday +2026-07-03,2026,7,3,friday +2026-07-06,2026,7,6,monday +2026-07-07,2026,7,7,tuesday +2026-07-11,2026,7,11,saturday +2026-07-13,2026,7,13,monday +2026-07-14,2026,7,14,tuesday +2026-07-15,2026,7,15,wednesday +2026-07-16,2026,7,16,thursday +2026-07-19,2026,7,19,sunday +2026-07-20,2026,7,20,monday +2026-07-22,2026,7,22,wednesday +2026-07-25,2026,7,25,saturday +2026-07-27,2026,7,27,monday +2026-08-01,2026,8,1,saturday +2026-08-03,2026,8,3,monday +2026-08-05,2026,8,5,wednesday +2026-08-09,2026,8,9,sunday +2026-08-10,2026,8,10,monday +2026-08-14,2026,8,14,friday +2026-08-15,2026,8,15,saturday +2026-08-16,2026,8,16,sunday +2026-08-17,2026,8,17,monday +2026-08-20,2026,8,20,thursday +2026-08-21,2026,8,21,friday +2026-08-22,2026,8,22,saturday +2026-08-25,2026,8,25,tuesday +2026-08-26,2026,8,26,wednesday +2026-08-27,2026,8,27,thursday +2026-08-29,2026,8,29,saturday +2026-08-31,2026,8,31,monday +2026-09-01,2026,9,1,tuesday +2026-09-03,2026,9,3,thursday +2026-09-04,2026,9,4,friday +2026-09-08,2026,9,8,tuesday +2026-09-12,2026,9,12,saturday +2026-09-14,2026,9,14,monday +2026-09-16,2026,9,16,wednesday +2026-09-20,2026,9,20,sunday +2026-09-21,2026,9,21,monday +2026-09-22,2026,9,22,tuesday +2026-09-25,2026,9,25,friday +2026-09-26,2026,9,26,saturday +2026-09-28,2026,9,28,monday +2026-09-29,2026,9,29,tuesday +2026-09-30,2026,9,30,wednesday +2026-10-02,2026,10,2,friday +2026-10-03,2026,10,3,saturday +2026-10-04,2026,10,4,sunday +2026-10-05,2026,10,5,monday +2026-10-06,2026,10,6,tuesday +2026-10-07,2026,10,7,wednesday +2026-10-08,2026,10,8,thursday +2026-10-12,2026,10,12,monday +2026-10-15,2026,10,15,thursday +2026-10-18,2026,10,18,sunday +2026-10-20,2026,10,20,tuesday +2026-10-21,2026,10,21,wednesday +2026-10-23,2026,10,23,friday +2026-10-25,2026,10,25,sunday +2026-10-26,2026,10,26,monday +2026-10-27,2026,10,27,tuesday +2026-10-28,2026,10,28,wednesday +2026-10-29,2026,10,29,thursday +2026-10-30,2026,10,30,friday +2026-10-31,2026,10,31,saturday +2026-11-02,2026,11,2,monday +2026-11-04,2026,11,4,wednesday +2026-11-05,2026,11,5,thursday +2026-11-07,2026,11,7,saturday +2026-11-11,2026,11,11,wednesday +2026-11-14,2026,11,14,saturday +2026-11-15,2026,11,15,sunday +2026-11-19,2026,11,19,thursday +2026-11-22,2026,11,22,sunday +2026-11-30,2026,11,30,monday +2026-12-02,2026,12,2,wednesday +2026-12-03,2026,12,3,thursday +2026-12-04,2026,12,4,friday +2026-12-05,2026,12,5,saturday +2026-12-06,2026,12,6,sunday +2026-12-07,2026,12,7,monday +2026-12-09,2026,12,9,wednesday +2026-12-12,2026,12,12,saturday +2026-12-15,2026,12,15,tuesday +2026-12-17,2026,12,17,thursday +2026-12-20,2026,12,20,sunday +2026-12-22,2026,12,22,tuesday +2026-12-24,2026,12,24,thursday +2026-12-25,2026,12,25,friday +2026-12-26,2026,12,26,saturday +2026-12-27,2026,12,27,sunday +2026-12-28,2026,12,28,monday +2026-12-29,2026,12,29,tuesday +2027-01-03,2027,1,3,sunday +2027-01-04,2027,1,4,monday +2027-01-07,2027,1,7,thursday +2027-01-08,2027,1,8,friday +2027-01-10,2027,1,10,sunday +2027-01-11,2027,1,11,monday +2027-01-17,2027,1,17,sunday +2027-01-18,2027,1,18,monday +2027-01-20,2027,1,20,wednesday +2027-01-22,2027,1,22,friday +2027-01-24,2027,1,24,sunday +2027-01-25,2027,1,25,monday +2027-01-26,2027,1,26,tuesday +2027-01-27,2027,1,27,wednesday +2027-01-30,2027,1,30,saturday +2027-01-31,2027,1,31,sunday +2027-02-01,2027,2,1,monday +2027-02-03,2027,2,3,wednesday +2027-02-05,2027,2,5,friday +2027-02-09,2027,2,9,tuesday +2027-02-10,2027,2,10,wednesday +2027-02-15,2027,2,15,monday +2027-02-18,2027,2,18,thursday +2027-02-20,2027,2,20,saturday +2027-02-23,2027,2,23,tuesday +2027-02-26,2027,2,26,friday +2027-03-01,2027,3,1,monday +2027-03-03,2027,3,3,wednesday +2027-03-04,2027,3,4,thursday +2027-03-06,2027,3,6,saturday +2027-03-09,2027,3,9,tuesday +2027-03-11,2027,3,11,thursday +2027-03-13,2027,3,13,saturday +2027-03-14,2027,3,14,sunday +2027-03-15,2027,3,15,monday +2027-03-17,2027,3,17,wednesday +2027-03-19,2027,3,19,friday +2027-03-20,2027,3,20,saturday +2027-03-21,2027,3,21,sunday +2027-03-25,2027,3,25,thursday +2027-03-26,2027,3,26,friday +2027-03-27,2027,3,27,saturday +2027-03-28,2027,3,28,sunday +2027-04-03,2027,4,3,saturday +2027-04-04,2027,4,4,sunday +2027-04-05,2027,4,5,monday +2027-04-08,2027,4,8,thursday +2027-04-11,2027,4,11,sunday +2027-04-12,2027,4,12,monday +2027-04-16,2027,4,16,friday +2027-04-18,2027,4,18,sunday +2027-04-19,2027,4,19,monday +2027-04-20,2027,4,20,tuesday +2027-04-28,2027,4,28,wednesday +2027-05-02,2027,5,2,sunday +2027-05-05,2027,5,5,wednesday +2027-05-06,2027,5,6,thursday +2027-05-07,2027,5,7,friday +2027-05-08,2027,5,8,saturday +2027-05-12,2027,5,12,wednesday +2027-05-15,2027,5,15,saturday +2027-05-16,2027,5,16,sunday +2027-05-17,2027,5,17,monday +2027-05-20,2027,5,20,thursday +2027-05-21,2027,5,21,friday +2027-05-22,2027,5,22,saturday +2027-05-23,2027,5,23,sunday +2027-05-24,2027,5,24,monday +2027-05-25,2027,5,25,tuesday +2027-05-27,2027,5,27,thursday +2027-05-28,2027,5,28,friday +2027-05-29,2027,5,29,saturday +2027-05-30,2027,5,30,sunday +2027-05-31,2027,5,31,monday +2027-06-01,2027,6,1,tuesday +2027-06-04,2027,6,4,friday +2027-06-05,2027,6,5,saturday +2027-06-09,2027,6,9,wednesday +2027-06-10,2027,6,10,thursday +2027-06-12,2027,6,12,saturday +2027-06-13,2027,6,13,sunday +2027-06-14,2027,6,14,monday +2027-06-15,2027,6,15,tuesday +2027-06-16,2027,6,16,wednesday +2027-06-17,2027,6,17,thursday +2027-06-19,2027,6,19,saturday +2027-06-21,2027,6,21,monday +2027-06-23,2027,6,23,wednesday +2027-06-24,2027,6,24,thursday +2027-06-26,2027,6,26,saturday +2027-06-27,2027,6,27,sunday +2027-06-28,2027,6,28,monday +2027-06-29,2027,6,29,tuesday +2027-07-01,2027,7,1,thursday +2027-07-02,2027,7,2,friday +2027-07-04,2027,7,4,sunday +2027-07-05,2027,7,5,monday +2027-07-07,2027,7,7,wednesday +2027-07-10,2027,7,10,saturday +2027-07-12,2027,7,12,monday +2027-07-14,2027,7,14,wednesday +2027-07-16,2027,7,16,friday +2027-07-17,2027,7,17,saturday +2027-07-19,2027,7,19,monday +2027-07-20,2027,7,20,tuesday +2027-07-22,2027,7,22,thursday +2027-07-23,2027,7,23,friday +2027-07-26,2027,7,26,monday +2027-07-27,2027,7,27,tuesday +2026-07-24,2026,7,24,friday +2026-07-26,2026,7,26,sunday +2026-07-28,2026,7,28,tuesday +2026-07-29,2026,7,29,wednesday +2026-07-30,2026,7,30,thursday +2026-07-31,2026,7,31,friday +2026-08-02,2026,8,2,sunday +2026-08-04,2026,8,4,tuesday +2026-08-06,2026,8,6,thursday +2026-08-07,2026,8,7,friday +2026-08-08,2026,8,8,saturday +2026-08-11,2026,8,11,tuesday +2026-08-12,2026,8,12,wednesday +2026-08-13,2026,8,13,thursday +2026-08-18,2026,8,18,tuesday +2026-08-19,2026,8,19,wednesday +2026-08-23,2026,8,23,sunday +2026-08-24,2026,8,24,monday +2026-08-28,2026,8,28,friday +2026-08-30,2026,8,30,sunday +2026-09-02,2026,9,2,wednesday +2026-09-05,2026,9,5,saturday +2026-09-06,2026,9,6,sunday +2026-09-07,2026,9,7,monday +2026-09-09,2026,9,9,wednesday +2026-09-10,2026,9,10,thursday +2026-09-11,2026,9,11,friday +2026-09-13,2026,9,13,sunday +2026-09-15,2026,9,15,tuesday +2026-09-17,2026,9,17,thursday +2026-09-18,2026,9,18,friday +2026-09-19,2026,9,19,saturday +2026-09-23,2026,9,23,wednesday +2026-09-24,2026,9,24,thursday +2026-09-27,2026,9,27,sunday +2026-10-01,2026,10,1,thursday +2026-10-09,2026,10,9,friday +2026-10-10,2026,10,10,saturday +2026-10-11,2026,10,11,sunday +2026-10-13,2026,10,13,tuesday +2026-10-14,2026,10,14,wednesday +2026-10-16,2026,10,16,friday +2026-10-17,2026,10,17,saturday +2026-10-19,2026,10,19,monday +2026-10-22,2026,10,22,thursday +2026-10-24,2026,10,24,saturday +2026-11-01,2026,11,1,sunday +2026-11-03,2026,11,3,tuesday +2026-11-06,2026,11,6,friday +2026-11-08,2026,11,8,sunday +2026-11-09,2026,11,9,monday +2026-11-10,2026,11,10,tuesday +2026-11-12,2026,11,12,thursday +2026-11-13,2026,11,13,friday +2026-11-16,2026,11,16,monday +2026-11-17,2026,11,17,tuesday +2026-11-18,2026,11,18,wednesday +2026-11-20,2026,11,20,friday +2026-11-21,2026,11,21,saturday +2026-11-23,2026,11,23,monday +2026-11-24,2026,11,24,tuesday +2026-11-25,2026,11,25,wednesday +2026-11-26,2026,11,26,thursday +2026-11-27,2026,11,27,friday +2026-11-28,2026,11,28,saturday +2026-11-29,2026,11,29,sunday +2026-12-01,2026,12,1,tuesday +2026-12-08,2026,12,8,tuesday +2026-12-10,2026,12,10,thursday +2026-12-11,2026,12,11,friday +2026-12-13,2026,12,13,sunday +2026-12-14,2026,12,14,monday +2026-12-16,2026,12,16,wednesday +2026-12-18,2026,12,18,friday +2026-12-19,2026,12,19,saturday +2026-12-21,2026,12,21,monday +2026-12-23,2026,12,23,wednesday +2026-12-30,2026,12,30,wednesday +2026-12-31,2026,12,31,thursday +2027-01-01,2027,1,1,friday +2027-01-02,2027,1,2,saturday +2027-01-05,2027,1,5,tuesday +2027-01-06,2027,1,6,wednesday +2027-01-09,2027,1,9,saturday +2027-01-12,2027,1,12,tuesday +2027-01-13,2027,1,13,wednesday +2027-01-14,2027,1,14,thursday +2027-01-15,2027,1,15,friday +2027-01-16,2027,1,16,saturday +2027-01-19,2027,1,19,tuesday +2027-01-21,2027,1,21,thursday +2027-01-23,2027,1,23,saturday +2027-01-28,2027,1,28,thursday +2027-01-29,2027,1,29,friday +2027-02-02,2027,2,2,tuesday +2027-02-04,2027,2,4,thursday +2027-02-06,2027,2,6,saturday +2027-02-07,2027,2,7,sunday +2027-02-08,2027,2,8,monday +2027-02-11,2027,2,11,thursday +2027-02-12,2027,2,12,friday +2027-02-13,2027,2,13,saturday +2027-02-14,2027,2,14,sunday +2027-02-16,2027,2,16,tuesday +2027-02-17,2027,2,17,wednesday +2027-02-19,2027,2,19,friday +2027-02-21,2027,2,21,sunday +2027-02-22,2027,2,22,monday +2027-02-24,2027,2,24,wednesday +2027-02-25,2027,2,25,thursday +2027-02-27,2027,2,27,saturday +2027-02-28,2027,2,28,sunday +2027-03-02,2027,3,2,tuesday +2027-03-05,2027,3,5,friday +2027-03-07,2027,3,7,sunday +2027-03-08,2027,3,8,monday +2027-03-10,2027,3,10,wednesday +2027-03-12,2027,3,12,friday +2027-03-16,2027,3,16,tuesday +2027-03-18,2027,3,18,thursday +2027-03-22,2027,3,22,monday +2027-03-23,2027,3,23,tuesday +2027-03-24,2027,3,24,wednesday +2027-03-29,2027,3,29,monday +2027-03-30,2027,3,30,tuesday +2027-03-31,2027,3,31,wednesday +2027-04-01,2027,4,1,thursday +2027-04-02,2027,4,2,friday +2027-04-06,2027,4,6,tuesday +2027-04-07,2027,4,7,wednesday +2027-04-09,2027,4,9,friday +2027-04-10,2027,4,10,saturday +2027-04-13,2027,4,13,tuesday +2027-04-14,2027,4,14,wednesday +2027-04-15,2027,4,15,thursday +2027-04-17,2027,4,17,saturday +2027-04-21,2027,4,21,wednesday +2027-04-22,2027,4,22,thursday +2027-04-23,2027,4,23,friday +2027-04-24,2027,4,24,saturday +2027-04-25,2027,4,25,sunday +2027-04-26,2027,4,26,monday +2027-04-27,2027,4,27,tuesday +2027-04-29,2027,4,29,thursday +2027-04-30,2027,4,30,friday +2027-05-01,2027,5,1,saturday +2027-05-03,2027,5,3,monday +2027-05-04,2027,5,4,tuesday +2027-05-09,2027,5,9,sunday +2027-05-10,2027,5,10,monday +2027-05-11,2027,5,11,tuesday +2027-05-13,2027,5,13,thursday +2027-05-14,2027,5,14,friday +2027-05-18,2027,5,18,tuesday +2027-05-19,2027,5,19,wednesday +2027-05-26,2027,5,26,wednesday +2027-06-02,2027,6,2,wednesday +2027-06-03,2027,6,3,thursday +2027-06-06,2027,6,6,sunday +2027-06-07,2027,6,7,monday +2027-06-08,2027,6,8,tuesday +2027-06-11,2027,6,11,friday +2027-06-18,2027,6,18,friday +2027-06-20,2027,6,20,sunday +2027-06-22,2027,6,22,tuesday +2027-06-25,2027,6,25,friday +2027-06-30,2027,6,30,wednesday +2027-07-03,2027,7,3,saturday +2027-07-06,2027,7,6,tuesday +2027-07-08,2027,7,8,thursday +2027-07-09,2027,7,9,friday +2027-07-11,2027,7,11,sunday +2027-07-13,2027,7,13,tuesday +2027-07-15,2027,7,15,thursday +2027-07-18,2027,7,18,sunday +2027-07-21,2027,7,21,wednesday +2027-07-24,2027,7,24,saturday +2027-07-25,2027,7,25,sunday +2027-07-28,2027,7,28,wednesday +2027-07-30,2027,7,30,friday +2027-08-02,2027,8,2,monday +2027-08-05,2027,8,5,thursday +2027-08-06,2027,8,6,friday +2027-08-09,2027,8,9,monday +2027-08-10,2027,8,10,tuesday +2027-08-12,2027,8,12,thursday +2027-08-14,2027,8,14,saturday +2027-08-16,2027,8,16,monday +2027-08-17,2027,8,17,tuesday +2027-08-18,2027,8,18,wednesday +2027-08-20,2027,8,20,friday +2027-08-21,2027,8,21,saturday +2027-08-23,2027,8,23,monday +2027-08-24,2027,8,24,tuesday +2027-08-26,2027,8,26,thursday +2027-08-27,2027,8,27,friday +2027-08-29,2027,8,29,sunday +2027-08-30,2027,8,30,monday +2027-09-01,2027,9,1,wednesday +2027-09-03,2027,9,3,friday +2027-09-05,2027,9,5,sunday +2027-09-07,2027,9,7,tuesday +2027-09-08,2027,9,8,wednesday +2027-09-09,2027,9,9,thursday +2027-09-13,2027,9,13,monday +2027-09-14,2027,9,14,tuesday +2027-09-16,2027,9,16,thursday +2027-09-17,2027,9,17,friday +2027-09-18,2027,9,18,saturday +2027-09-19,2027,9,19,sunday +2027-09-21,2027,9,21,tuesday +2027-09-22,2027,9,22,wednesday +2027-09-23,2027,9,23,thursday +2027-07-29,2027,7,29,thursday +2027-07-31,2027,7,31,saturday +2027-08-01,2027,8,1,sunday +2027-08-03,2027,8,3,tuesday +2027-08-04,2027,8,4,wednesday +2027-08-07,2027,8,7,saturday +2027-08-08,2027,8,8,sunday +2027-08-11,2027,8,11,wednesday +2027-08-13,2027,8,13,friday +2027-08-15,2027,8,15,sunday +2027-08-19,2027,8,19,thursday +2027-08-22,2027,8,22,sunday +2027-08-25,2027,8,25,wednesday +2027-08-28,2027,8,28,saturday +2027-08-31,2027,8,31,tuesday +2027-09-02,2027,9,2,thursday +2027-09-04,2027,9,4,saturday +2027-09-06,2027,9,6,monday +2027-09-10,2027,9,10,friday +2027-09-11,2027,9,11,saturday +2027-09-12,2027,9,12,sunday +2027-09-15,2027,9,15,wednesday +2027-09-20,2027,9,20,monday +2027-09-24,2027,9,24,friday +2027-09-29,2027,9,29,wednesday +2027-10-07,2027,10,7,thursday +2027-10-08,2027,10,8,friday +2027-10-11,2027,10,11,monday +2027-10-13,2027,10,13,wednesday +2027-10-16,2027,10,16,saturday +2027-10-19,2027,10,19,tuesday +2027-10-20,2027,10,20,wednesday +2027-10-22,2027,10,22,friday +2027-10-24,2027,10,24,sunday +2027-10-26,2027,10,26,tuesday +2027-10-29,2027,10,29,friday +2027-10-30,2027,10,30,saturday +2027-11-01,2027,11,1,monday +2027-11-06,2027,11,6,saturday +2027-11-08,2027,11,8,monday +2027-11-09,2027,11,9,tuesday +2027-11-10,2027,11,10,wednesday +2027-11-11,2027,11,11,thursday +2027-11-12,2027,11,12,friday +2027-11-13,2027,11,13,saturday +2027-11-14,2027,11,14,sunday +2027-11-16,2027,11,16,tuesday +2027-11-17,2027,11,17,wednesday +2027-11-20,2027,11,20,saturday +2027-11-21,2027,11,21,sunday +2027-11-22,2027,11,22,monday +2027-11-23,2027,11,23,tuesday +2027-11-24,2027,11,24,wednesday +2027-11-25,2027,11,25,thursday +2027-11-26,2027,11,26,friday +2027-11-27,2027,11,27,saturday +2027-11-28,2027,11,28,sunday +2027-12-01,2027,12,1,wednesday +2027-12-05,2027,12,5,sunday +2027-12-06,2027,12,6,monday +2027-12-07,2027,12,7,tuesday +2027-12-10,2027,12,10,friday +2027-12-13,2027,12,13,monday +2027-12-14,2027,12,14,tuesday +2027-12-16,2027,12,16,thursday +2027-12-17,2027,12,17,friday +2027-12-19,2027,12,19,sunday +2027-12-20,2027,12,20,monday +2027-12-25,2027,12,25,saturday +2027-12-28,2027,12,28,tuesday +2027-12-29,2027,12,29,wednesday +2027-12-31,2027,12,31,friday +2028-01-02,2028,1,2,sunday +2028-01-05,2028,1,5,wednesday +2028-01-07,2028,1,7,friday +2028-01-09,2028,1,9,sunday +2028-01-10,2028,1,10,monday +2028-01-14,2028,1,14,friday +2028-01-16,2028,1,16,sunday +2028-01-17,2028,1,17,monday +2028-01-20,2028,1,20,thursday +2028-01-21,2028,1,21,friday +2028-01-22,2028,1,22,saturday +2028-01-26,2028,1,26,wednesday +2028-01-27,2028,1,27,thursday +2028-01-29,2028,1,29,saturday +2028-02-02,2028,2,2,wednesday +2028-02-03,2028,2,3,thursday +2028-02-05,2028,2,5,saturday +2028-02-11,2028,2,11,friday +2028-02-13,2028,2,13,sunday +2028-02-15,2028,2,15,tuesday +2028-02-18,2028,2,18,friday +2028-02-19,2028,2,19,saturday +2028-02-20,2028,2,20,sunday +2028-02-21,2028,2,21,monday +2028-02-22,2028,2,22,tuesday +2028-02-23,2028,2,23,wednesday +2028-02-24,2028,2,24,thursday +2028-02-25,2028,2,25,friday +2028-02-26,2028,2,26,saturday +2028-02-27,2028,2,27,sunday +2028-02-28,2028,2,28,monday +2028-03-01,2028,3,1,wednesday +2028-03-02,2028,3,2,thursday +2028-03-06,2028,3,6,monday +2028-03-07,2028,3,7,tuesday +2028-03-08,2028,3,8,wednesday +2028-03-10,2028,3,10,friday +2028-03-12,2028,3,12,sunday +2028-03-18,2028,3,18,saturday +2028-03-20,2028,3,20,monday +2028-03-22,2028,3,22,wednesday +2028-03-25,2028,3,25,saturday +2028-03-28,2028,3,28,tuesday +2028-03-29,2028,3,29,wednesday +2028-03-30,2028,3,30,thursday +2028-03-31,2028,3,31,friday +2028-04-02,2028,4,2,sunday +2028-04-03,2028,4,3,monday +2028-04-04,2028,4,4,tuesday +2028-04-06,2028,4,6,thursday +2028-04-08,2028,4,8,saturday +2028-04-10,2028,4,10,monday +2028-04-11,2028,4,11,tuesday +2028-04-13,2028,4,13,thursday +2028-04-17,2028,4,17,monday +2028-04-18,2028,4,18,tuesday +2028-04-19,2028,4,19,wednesday +2028-04-21,2028,4,21,friday +2028-04-22,2028,4,22,saturday +2028-04-26,2028,4,26,wednesday +2028-04-29,2028,4,29,saturday +2028-05-02,2028,5,2,tuesday +2028-05-06,2028,5,6,saturday +2028-05-09,2028,5,9,tuesday +2028-05-10,2028,5,10,wednesday +2028-05-11,2028,5,11,thursday +2028-05-12,2028,5,12,friday +2028-05-14,2028,5,14,sunday +2028-05-15,2028,5,15,monday +2028-05-20,2028,5,20,saturday +2028-05-23,2028,5,23,tuesday +2028-05-24,2028,5,24,wednesday +2028-05-25,2028,5,25,thursday +2028-05-27,2028,5,27,saturday +2028-05-28,2028,5,28,sunday +2028-05-29,2028,5,29,monday +2028-05-30,2028,5,30,tuesday +2028-06-01,2028,6,1,thursday +2028-06-06,2028,6,6,tuesday +2028-06-07,2028,6,7,wednesday +2028-06-09,2028,6,9,friday +2028-06-12,2028,6,12,monday +2028-06-13,2028,6,13,tuesday +2028-06-16,2028,6,16,friday +2028-06-17,2028,6,17,saturday +2028-06-20,2028,6,20,tuesday +2028-06-23,2028,6,23,friday +2028-06-25,2028,6,25,sunday +2028-06-29,2028,6,29,thursday +2028-07-04,2028,7,4,tuesday +2028-07-07,2028,7,7,friday +2028-07-09,2028,7,9,sunday +2028-07-12,2028,7,12,wednesday +2028-07-13,2028,7,13,thursday +2028-07-16,2028,7,16,sunday +2028-07-17,2028,7,17,monday +2028-07-18,2028,7,18,tuesday +2028-07-19,2028,7,19,wednesday +2028-07-22,2028,7,22,saturday +2028-07-23,2028,7,23,sunday +2028-07-24,2028,7,24,monday +2028-07-26,2028,7,26,wednesday +2028-07-27,2028,7,27,thursday +2028-07-28,2028,7,28,friday +2028-07-31,2028,7,31,monday +2028-08-02,2028,8,2,wednesday +2028-08-04,2028,8,4,friday +2028-08-05,2028,8,5,saturday +2028-08-06,2028,8,6,sunday +2028-08-07,2028,8,7,monday +2028-08-08,2028,8,8,tuesday +2028-08-13,2028,8,13,sunday +2028-08-14,2028,8,14,monday +2028-08-18,2028,8,18,friday +2028-08-23,2028,8,23,wednesday +2028-08-25,2028,8,25,friday +2028-08-26,2028,8,26,saturday +2028-08-27,2028,8,27,sunday +2028-08-28,2028,8,28,monday +2028-08-29,2028,8,29,tuesday +2028-08-30,2028,8,30,wednesday +2028-08-31,2028,8,31,thursday +2028-09-02,2028,9,2,saturday +2028-09-03,2028,9,3,sunday +2028-09-05,2028,9,5,tuesday +2028-09-08,2028,9,8,friday +2028-09-09,2028,9,9,saturday +2028-09-11,2028,9,11,monday +2028-09-12,2028,9,12,tuesday +2028-09-15,2028,9,15,friday +2028-09-17,2028,9,17,sunday +2028-09-19,2028,9,19,tuesday +2028-09-20,2028,9,20,wednesday +2028-09-25,2028,9,25,monday +2028-09-27,2028,9,27,wednesday +2028-09-28,2028,9,28,thursday +2028-09-30,2028,9,30,saturday +2028-10-01,2028,10,1,sunday +2028-10-05,2028,10,5,thursday +2028-10-06,2028,10,6,friday +2028-10-12,2028,10,12,thursday +2027-09-25,2027,9,25,saturday +2027-09-26,2027,9,26,sunday +2027-09-27,2027,9,27,monday +2027-09-28,2027,9,28,tuesday +2027-09-30,2027,9,30,thursday +2027-10-01,2027,10,1,friday +2027-10-02,2027,10,2,saturday +2027-10-03,2027,10,3,sunday +2027-10-04,2027,10,4,monday +2027-10-05,2027,10,5,tuesday +2027-10-06,2027,10,6,wednesday +2027-10-09,2027,10,9,saturday +2027-10-10,2027,10,10,sunday +2027-10-12,2027,10,12,tuesday +2027-10-14,2027,10,14,thursday +2027-10-15,2027,10,15,friday +2027-10-17,2027,10,17,sunday +2027-10-18,2027,10,18,monday +2027-10-21,2027,10,21,thursday +2027-10-23,2027,10,23,saturday +2027-10-25,2027,10,25,monday +2027-10-27,2027,10,27,wednesday +2027-10-28,2027,10,28,thursday +2027-10-31,2027,10,31,sunday +2027-11-02,2027,11,2,tuesday +2027-11-03,2027,11,3,wednesday +2027-11-04,2027,11,4,thursday +2027-11-05,2027,11,5,friday +2027-11-07,2027,11,7,sunday +2027-11-15,2027,11,15,monday +2027-11-18,2027,11,18,thursday +2027-11-19,2027,11,19,friday +2027-11-29,2027,11,29,monday +2027-11-30,2027,11,30,tuesday +2027-12-02,2027,12,2,thursday +2027-12-03,2027,12,3,friday +2027-12-04,2027,12,4,saturday +2027-12-08,2027,12,8,wednesday +2027-12-09,2027,12,9,thursday +2027-12-11,2027,12,11,saturday +2027-12-12,2027,12,12,sunday +2027-12-15,2027,12,15,wednesday +2027-12-18,2027,12,18,saturday +2027-12-21,2027,12,21,tuesday +2027-12-22,2027,12,22,wednesday +2027-12-23,2027,12,23,thursday +2027-12-24,2027,12,24,friday +2027-12-26,2027,12,26,sunday +2027-12-27,2027,12,27,monday +2027-12-30,2027,12,30,thursday +2028-01-01,2028,1,1,saturday +2028-01-03,2028,1,3,monday +2028-01-04,2028,1,4,tuesday +2028-01-06,2028,1,6,thursday +2028-01-08,2028,1,8,saturday +2028-01-11,2028,1,11,tuesday +2028-01-12,2028,1,12,wednesday +2028-01-13,2028,1,13,thursday +2028-01-15,2028,1,15,saturday +2028-01-18,2028,1,18,tuesday +2028-01-19,2028,1,19,wednesday +2028-01-23,2028,1,23,sunday +2028-01-24,2028,1,24,monday +2028-01-25,2028,1,25,tuesday +2028-01-28,2028,1,28,friday +2028-01-30,2028,1,30,sunday +2028-01-31,2028,1,31,monday +2028-02-01,2028,2,1,tuesday +2028-02-04,2028,2,4,friday +2028-02-06,2028,2,6,sunday +2028-02-07,2028,2,7,monday +2028-02-08,2028,2,8,tuesday +2028-02-09,2028,2,9,wednesday +2028-02-10,2028,2,10,thursday +2028-02-12,2028,2,12,saturday +2028-02-14,2028,2,14,monday +2028-02-16,2028,2,16,wednesday +2028-02-17,2028,2,17,thursday +2028-02-29,2028,2,29,tuesday +2028-03-03,2028,3,3,friday +2028-03-04,2028,3,4,saturday +2028-03-05,2028,3,5,sunday +2028-03-09,2028,3,9,thursday +2028-03-11,2028,3,11,saturday +2028-03-13,2028,3,13,monday +2028-03-14,2028,3,14,tuesday +2028-03-15,2028,3,15,wednesday +2028-03-16,2028,3,16,thursday +2028-03-17,2028,3,17,friday +2028-03-19,2028,3,19,sunday +2028-03-21,2028,3,21,tuesday +2028-03-23,2028,3,23,thursday +2028-03-24,2028,3,24,friday +2028-03-26,2028,3,26,sunday +2028-03-27,2028,3,27,monday +2028-04-01,2028,4,1,saturday +2028-04-05,2028,4,5,wednesday +2028-04-07,2028,4,7,friday +2028-04-09,2028,4,9,sunday +2028-04-12,2028,4,12,wednesday +2028-04-14,2028,4,14,friday +2028-04-15,2028,4,15,saturday +2028-04-16,2028,4,16,sunday +2028-04-20,2028,4,20,thursday +2028-04-23,2028,4,23,sunday +2028-04-24,2028,4,24,monday +2028-04-25,2028,4,25,tuesday +2028-04-27,2028,4,27,thursday +2028-04-28,2028,4,28,friday +2028-04-30,2028,4,30,sunday +2028-05-01,2028,5,1,monday +2028-05-03,2028,5,3,wednesday +2028-05-04,2028,5,4,thursday +2028-05-05,2028,5,5,friday +2028-05-07,2028,5,7,sunday +2028-05-08,2028,5,8,monday +2028-05-13,2028,5,13,saturday +2028-05-16,2028,5,16,tuesday +2028-05-17,2028,5,17,wednesday +2028-05-18,2028,5,18,thursday +2028-05-19,2028,5,19,friday +2028-05-21,2028,5,21,sunday +2028-05-22,2028,5,22,monday +2028-05-26,2028,5,26,friday +2028-05-31,2028,5,31,wednesday +2028-06-02,2028,6,2,friday +2028-06-03,2028,6,3,saturday +2028-06-04,2028,6,4,sunday +2028-06-05,2028,6,5,monday +2028-06-08,2028,6,8,thursday +2028-06-10,2028,6,10,saturday +2028-06-11,2028,6,11,sunday +2028-06-14,2028,6,14,wednesday +2028-06-15,2028,6,15,thursday +2028-06-18,2028,6,18,sunday +2028-06-19,2028,6,19,monday +2028-06-21,2028,6,21,wednesday +2028-06-22,2028,6,22,thursday +2028-06-24,2028,6,24,saturday +2028-06-26,2028,6,26,monday +2028-06-27,2028,6,27,tuesday +2028-06-28,2028,6,28,wednesday +2028-06-30,2028,6,30,friday +2028-07-01,2028,7,1,saturday +2028-07-02,2028,7,2,sunday +2028-07-03,2028,7,3,monday +2028-07-05,2028,7,5,wednesday +2028-07-06,2028,7,6,thursday +2028-07-08,2028,7,8,saturday +2028-07-10,2028,7,10,monday +2028-07-11,2028,7,11,tuesday +2028-07-14,2028,7,14,friday +2028-07-15,2028,7,15,saturday +2028-07-20,2028,7,20,thursday +2028-07-21,2028,7,21,friday +2028-07-25,2028,7,25,tuesday +2028-07-29,2028,7,29,saturday +2028-07-30,2028,7,30,sunday +2028-08-01,2028,8,1,tuesday +2028-08-03,2028,8,3,thursday +2028-08-09,2028,8,9,wednesday +2028-08-10,2028,8,10,thursday +2028-08-11,2028,8,11,friday +2028-08-12,2028,8,12,saturday +2028-08-15,2028,8,15,tuesday +2028-08-16,2028,8,16,wednesday +2028-08-17,2028,8,17,thursday +2028-08-19,2028,8,19,saturday +2028-08-20,2028,8,20,sunday +2028-08-21,2028,8,21,monday +2028-08-22,2028,8,22,tuesday +2028-08-24,2028,8,24,thursday +2028-09-01,2028,9,1,friday +2028-09-04,2028,9,4,monday +2028-09-06,2028,9,6,wednesday +2028-09-07,2028,9,7,thursday +2028-09-10,2028,9,10,sunday +2028-09-13,2028,9,13,wednesday +2028-09-14,2028,9,14,thursday +2028-09-16,2028,9,16,saturday +2028-09-18,2028,9,18,monday +2028-09-21,2028,9,21,thursday +2028-09-22,2028,9,22,friday +2028-09-23,2028,9,23,saturday +2028-09-24,2028,9,24,sunday +2028-09-26,2028,9,26,tuesday +2028-09-29,2028,9,29,friday +2028-10-02,2028,10,2,monday +2028-10-03,2028,10,3,tuesday +2028-10-04,2028,10,4,wednesday +2028-10-07,2028,10,7,saturday +2028-10-08,2028,10,8,sunday +2028-10-09,2028,10,9,monday +2028-10-10,2028,10,10,tuesday +2028-10-11,2028,10,11,wednesday +2028-10-14,2028,10,14,saturday +2028-10-15,2028,10,15,sunday +2028-10-17,2028,10,17,tuesday +2028-10-18,2028,10,18,wednesday +2028-10-19,2028,10,19,thursday +2028-10-22,2028,10,22,sunday +2028-10-25,2028,10,25,wednesday +2028-10-27,2028,10,27,friday +2028-10-29,2028,10,29,sunday +2028-10-31,2028,10,31,tuesday +2028-11-02,2028,11,2,thursday +2028-11-03,2028,11,3,friday +2028-11-05,2028,11,5,sunday +2028-11-07,2028,11,7,tuesday +2028-11-09,2028,11,9,thursday +2028-11-11,2028,11,11,saturday +2028-11-12,2028,11,12,sunday +2028-11-14,2028,11,14,tuesday +2028-10-13,2028,10,13,friday +2028-10-16,2028,10,16,monday +2028-10-20,2028,10,20,friday +2028-10-21,2028,10,21,saturday +2028-10-23,2028,10,23,monday +2028-10-24,2028,10,24,tuesday +2028-10-26,2028,10,26,thursday +2028-10-28,2028,10,28,saturday +2028-10-30,2028,10,30,monday +2028-11-01,2028,11,1,wednesday +2028-11-04,2028,11,4,saturday +2028-11-06,2028,11,6,monday +2028-11-08,2028,11,8,wednesday +2028-11-10,2028,11,10,friday +2028-11-13,2028,11,13,monday +2028-11-16,2028,11,16,thursday +2028-11-17,2028,11,17,friday +2028-11-19,2028,11,19,sunday +2028-11-20,2028,11,20,monday +2028-11-22,2028,11,22,wednesday +2028-11-24,2028,11,24,friday +2028-11-25,2028,11,25,saturday +2028-11-29,2028,11,29,wednesday +2028-11-30,2028,11,30,thursday +2028-12-03,2028,12,3,sunday +2028-12-09,2028,12,9,saturday +2028-12-11,2028,12,11,monday +2028-12-12,2028,12,12,tuesday +2028-12-16,2028,12,16,saturday +2028-12-17,2028,12,17,sunday +2028-12-18,2028,12,18,monday +2028-12-19,2028,12,19,tuesday +2028-12-21,2028,12,21,thursday +2028-12-22,2028,12,22,friday +2028-12-27,2028,12,27,wednesday +2028-12-29,2028,12,29,friday +2029-01-01,2029,1,1,monday +2029-01-03,2029,1,3,wednesday +2029-01-06,2029,1,6,saturday +2029-01-08,2029,1,8,monday +2029-01-13,2029,1,13,saturday +2029-01-17,2029,1,17,wednesday +2029-01-19,2029,1,19,friday +2029-01-21,2029,1,21,sunday +2029-01-23,2029,1,23,tuesday +2029-01-26,2029,1,26,friday +2029-01-28,2029,1,28,sunday +2029-01-31,2029,1,31,wednesday +2029-02-01,2029,2,1,thursday +2029-02-03,2029,2,3,saturday +2029-02-04,2029,2,4,sunday +2029-02-05,2029,2,5,monday +2029-02-08,2029,2,8,thursday +2029-02-17,2029,2,17,saturday +2029-02-18,2029,2,18,sunday +2029-02-19,2029,2,19,monday +2029-03-06,2029,3,6,tuesday +2029-03-13,2029,3,13,tuesday +2029-03-14,2029,3,14,wednesday +2029-03-15,2029,3,15,thursday +2029-03-16,2029,3,16,friday +2029-03-18,2029,3,18,sunday +2029-03-20,2029,3,20,tuesday +2029-03-21,2029,3,21,wednesday +2029-03-23,2029,3,23,friday +2029-03-25,2029,3,25,sunday +2029-03-28,2029,3,28,wednesday +2029-03-29,2029,3,29,thursday +2029-04-01,2029,4,1,sunday +2029-04-03,2029,4,3,tuesday +2029-04-06,2029,4,6,friday +2029-04-08,2029,4,8,sunday +2029-04-12,2029,4,12,thursday +2029-04-13,2029,4,13,friday +2029-04-14,2029,4,14,saturday +2029-04-16,2029,4,16,monday +2029-04-19,2029,4,19,thursday +2029-04-20,2029,4,20,friday +2029-04-25,2029,4,25,wednesday +2029-04-28,2029,4,28,saturday +2029-04-29,2029,4,29,sunday +2029-05-01,2029,5,1,tuesday +2029-05-02,2029,5,2,wednesday +2029-05-03,2029,5,3,thursday +2029-05-05,2029,5,5,saturday +2029-05-06,2029,5,6,sunday +2029-05-08,2029,5,8,tuesday +2029-05-10,2029,5,10,thursday +2029-05-16,2029,5,16,wednesday +2029-05-19,2029,5,19,saturday +2029-05-22,2029,5,22,tuesday +2029-05-23,2029,5,23,wednesday +2029-05-26,2029,5,26,saturday +2029-05-27,2029,5,27,sunday +2029-05-29,2029,5,29,tuesday +2029-06-03,2029,6,3,sunday +2029-06-05,2029,6,5,tuesday +2029-06-09,2029,6,9,saturday +2029-06-11,2029,6,11,monday +2029-06-12,2029,6,12,tuesday +2029-06-13,2029,6,13,wednesday +2029-06-17,2029,6,17,sunday +2029-06-19,2029,6,19,tuesday +2029-06-21,2029,6,21,thursday +2029-06-22,2029,6,22,friday +2029-06-23,2029,6,23,saturday +2029-06-25,2029,6,25,monday +2029-06-26,2029,6,26,tuesday +2029-06-27,2029,6,27,wednesday +2029-06-28,2029,6,28,thursday +2029-06-30,2029,6,30,saturday +2029-07-01,2029,7,1,sunday +2029-07-02,2029,7,2,monday +2029-07-03,2029,7,3,tuesday +2029-07-04,2029,7,4,wednesday +2029-07-08,2029,7,8,sunday +2029-07-09,2029,7,9,monday +2029-07-11,2029,7,11,wednesday +2029-07-12,2029,7,12,thursday +2029-07-17,2029,7,17,tuesday +2029-07-18,2029,7,18,wednesday +2029-07-19,2029,7,19,thursday +2029-07-20,2029,7,20,friday +2029-07-23,2029,7,23,monday +2029-07-25,2029,7,25,wednesday +2029-07-26,2029,7,26,thursday +2029-07-27,2029,7,27,friday +2029-07-28,2029,7,28,saturday +2029-08-02,2029,8,2,thursday +2029-08-04,2029,8,4,saturday +2029-08-05,2029,8,5,sunday +2029-08-08,2029,8,8,wednesday +2029-08-09,2029,8,9,thursday +2029-08-10,2029,8,10,friday +2029-08-12,2029,8,12,sunday +2029-08-13,2029,8,13,monday +2029-08-14,2029,8,14,tuesday +2029-08-15,2029,8,15,wednesday +2029-08-16,2029,8,16,thursday +2029-08-18,2029,8,18,saturday +2029-08-19,2029,8,19,sunday +2029-08-20,2029,8,20,monday +2029-08-22,2029,8,22,wednesday +2029-08-23,2029,8,23,thursday +2029-08-26,2029,8,26,sunday +2029-08-28,2029,8,28,tuesday +2029-08-29,2029,8,29,wednesday +2029-08-31,2029,8,31,friday +2029-09-01,2029,9,1,saturday +2029-09-04,2029,9,4,tuesday +2029-09-08,2029,9,8,saturday +2029-09-09,2029,9,9,sunday +2029-09-12,2029,9,12,wednesday +2029-09-16,2029,9,16,sunday +2029-09-19,2029,9,19,wednesday +2029-09-21,2029,9,21,friday +2029-09-22,2029,9,22,saturday +2029-09-24,2029,9,24,monday +2029-09-26,2029,9,26,wednesday +2029-09-27,2029,9,27,thursday +2029-09-30,2029,9,30,sunday +2029-10-01,2029,10,1,monday +2029-10-03,2029,10,3,wednesday +2029-10-04,2029,10,4,thursday +2029-10-07,2029,10,7,sunday +2029-10-13,2029,10,13,saturday +2029-10-19,2029,10,19,friday +2029-10-20,2029,10,20,saturday +2029-10-23,2029,10,23,tuesday +2029-10-26,2029,10,26,friday +2029-10-30,2029,10,30,tuesday +2029-10-31,2029,10,31,wednesday +2029-11-04,2029,11,4,sunday +2029-11-05,2029,11,5,monday +2029-11-06,2029,11,6,tuesday +2029-11-09,2029,11,9,friday +2029-11-10,2029,11,10,saturday +2029-11-18,2029,11,18,sunday +2029-11-19,2029,11,19,monday +2029-11-20,2029,11,20,tuesday +2029-11-22,2029,11,22,thursday +2029-11-25,2029,11,25,sunday +2029-11-26,2029,11,26,monday +2029-11-28,2029,11,28,wednesday +2029-11-30,2029,11,30,friday +2029-12-03,2029,12,3,monday +2029-12-05,2029,12,5,wednesday +2029-12-07,2029,12,7,friday +2029-12-08,2029,12,8,saturday +2029-12-09,2029,12,9,sunday +2029-12-11,2029,12,11,tuesday +2029-12-15,2029,12,15,saturday +2029-12-16,2029,12,16,sunday +2029-12-18,2029,12,18,tuesday +2029-12-19,2029,12,19,wednesday +2029-12-20,2029,12,20,thursday +2029-12-21,2029,12,21,friday +2029-12-22,2029,12,22,saturday +2029-12-23,2029,12,23,sunday +2029-12-24,2029,12,24,monday +2029-12-25,2029,12,25,tuesday +2029-12-26,2029,12,26,wednesday +2029-12-30,2029,12,30,sunday +2030-01-01,2030,1,1,tuesday +2030-01-04,2030,1,4,friday +2030-01-05,2030,1,5,saturday +2030-01-07,2030,1,7,monday +2030-01-12,2030,1,12,saturday +2030-01-13,2030,1,13,sunday +2030-01-16,2030,1,16,wednesday +2030-01-18,2030,1,18,friday +2030-01-20,2030,1,20,sunday +2030-01-21,2030,1,21,monday +2028-11-15,2028,11,15,wednesday +2028-11-18,2028,11,18,saturday +2028-11-21,2028,11,21,tuesday +2028-11-23,2028,11,23,thursday +2028-11-26,2028,11,26,sunday +2028-11-27,2028,11,27,monday +2028-11-28,2028,11,28,tuesday +2028-12-01,2028,12,1,friday +2028-12-02,2028,12,2,saturday +2028-12-04,2028,12,4,monday +2028-12-05,2028,12,5,tuesday +2028-12-06,2028,12,6,wednesday +2028-12-07,2028,12,7,thursday +2028-12-08,2028,12,8,friday +2028-12-10,2028,12,10,sunday +2028-12-13,2028,12,13,wednesday +2028-12-14,2028,12,14,thursday +2028-12-15,2028,12,15,friday +2028-12-20,2028,12,20,wednesday +2028-12-23,2028,12,23,saturday +2028-12-24,2028,12,24,sunday +2028-12-25,2028,12,25,monday +2028-12-26,2028,12,26,tuesday +2028-12-28,2028,12,28,thursday +2028-12-30,2028,12,30,saturday +2028-12-31,2028,12,31,sunday +2029-01-02,2029,1,2,tuesday +2029-01-04,2029,1,4,thursday +2029-01-05,2029,1,5,friday +2029-01-07,2029,1,7,sunday +2029-01-09,2029,1,9,tuesday +2029-01-10,2029,1,10,wednesday +2029-01-11,2029,1,11,thursday +2029-01-12,2029,1,12,friday +2029-01-14,2029,1,14,sunday +2029-01-15,2029,1,15,monday +2029-01-16,2029,1,16,tuesday +2029-01-18,2029,1,18,thursday +2029-01-20,2029,1,20,saturday +2029-01-22,2029,1,22,monday +2029-01-24,2029,1,24,wednesday +2029-01-25,2029,1,25,thursday +2029-01-27,2029,1,27,saturday +2029-01-29,2029,1,29,monday +2029-01-30,2029,1,30,tuesday +2029-02-02,2029,2,2,friday +2029-02-06,2029,2,6,tuesday +2029-02-07,2029,2,7,wednesday +2029-02-09,2029,2,9,friday +2029-02-10,2029,2,10,saturday +2029-02-11,2029,2,11,sunday +2029-02-12,2029,2,12,monday +2029-02-13,2029,2,13,tuesday +2029-02-14,2029,2,14,wednesday +2029-02-15,2029,2,15,thursday +2029-02-16,2029,2,16,friday +2029-02-20,2029,2,20,tuesday +2029-02-21,2029,2,21,wednesday +2029-02-22,2029,2,22,thursday +2029-02-23,2029,2,23,friday +2029-02-24,2029,2,24,saturday +2029-02-25,2029,2,25,sunday +2029-02-26,2029,2,26,monday +2029-02-27,2029,2,27,tuesday +2029-02-28,2029,2,28,wednesday +2029-03-01,2029,3,1,thursday +2029-03-02,2029,3,2,friday +2029-03-03,2029,3,3,saturday +2029-03-04,2029,3,4,sunday +2029-03-05,2029,3,5,monday +2029-03-07,2029,3,7,wednesday +2029-03-08,2029,3,8,thursday +2029-03-09,2029,3,9,friday +2029-03-10,2029,3,10,saturday +2029-03-11,2029,3,11,sunday +2029-03-12,2029,3,12,monday +2029-03-17,2029,3,17,saturday +2029-03-19,2029,3,19,monday +2029-03-22,2029,3,22,thursday +2029-03-24,2029,3,24,saturday +2029-03-26,2029,3,26,monday +2029-03-27,2029,3,27,tuesday +2029-03-30,2029,3,30,friday +2029-03-31,2029,3,31,saturday +2029-04-02,2029,4,2,monday +2029-04-04,2029,4,4,wednesday +2029-04-05,2029,4,5,thursday +2029-04-07,2029,4,7,saturday +2029-04-09,2029,4,9,monday +2029-04-10,2029,4,10,tuesday +2029-04-11,2029,4,11,wednesday +2029-04-15,2029,4,15,sunday +2029-04-17,2029,4,17,tuesday +2029-04-18,2029,4,18,wednesday +2029-04-21,2029,4,21,saturday +2029-04-22,2029,4,22,sunday +2029-04-23,2029,4,23,monday +2029-04-24,2029,4,24,tuesday +2029-04-26,2029,4,26,thursday +2029-04-27,2029,4,27,friday +2029-04-30,2029,4,30,monday +2029-05-04,2029,5,4,friday +2029-05-07,2029,5,7,monday +2029-05-09,2029,5,9,wednesday +2029-05-11,2029,5,11,friday +2029-05-12,2029,5,12,saturday +2029-05-13,2029,5,13,sunday +2029-05-14,2029,5,14,monday +2029-05-15,2029,5,15,tuesday +2029-05-17,2029,5,17,thursday +2029-05-18,2029,5,18,friday +2029-05-20,2029,5,20,sunday +2029-05-21,2029,5,21,monday +2029-05-24,2029,5,24,thursday +2029-05-25,2029,5,25,friday +2029-05-28,2029,5,28,monday +2029-05-30,2029,5,30,wednesday +2029-05-31,2029,5,31,thursday +2029-06-01,2029,6,1,friday +2029-06-02,2029,6,2,saturday +2029-06-04,2029,6,4,monday +2029-06-06,2029,6,6,wednesday +2029-06-07,2029,6,7,thursday +2029-06-08,2029,6,8,friday +2029-06-10,2029,6,10,sunday +2029-06-14,2029,6,14,thursday +2029-06-15,2029,6,15,friday +2029-06-16,2029,6,16,saturday +2029-06-18,2029,6,18,monday +2029-06-20,2029,6,20,wednesday +2029-06-24,2029,6,24,sunday +2029-06-29,2029,6,29,friday +2029-07-05,2029,7,5,thursday +2029-07-06,2029,7,6,friday +2029-07-07,2029,7,7,saturday +2029-07-10,2029,7,10,tuesday +2029-07-13,2029,7,13,friday +2029-07-14,2029,7,14,saturday +2029-07-15,2029,7,15,sunday +2029-07-16,2029,7,16,monday +2029-07-21,2029,7,21,saturday +2029-07-22,2029,7,22,sunday +2029-07-24,2029,7,24,tuesday +2029-07-29,2029,7,29,sunday +2029-07-30,2029,7,30,monday +2029-07-31,2029,7,31,tuesday +2029-08-01,2029,8,1,wednesday +2029-08-03,2029,8,3,friday +2029-08-06,2029,8,6,monday +2029-08-07,2029,8,7,tuesday +2029-08-11,2029,8,11,saturday +2029-08-17,2029,8,17,friday +2029-08-21,2029,8,21,tuesday +2029-08-24,2029,8,24,friday +2029-08-25,2029,8,25,saturday +2029-08-27,2029,8,27,monday +2029-08-30,2029,8,30,thursday +2029-09-02,2029,9,2,sunday +2029-09-03,2029,9,3,monday +2029-09-05,2029,9,5,wednesday +2029-09-06,2029,9,6,thursday +2029-09-07,2029,9,7,friday +2029-09-10,2029,9,10,monday +2029-09-11,2029,9,11,tuesday +2029-09-13,2029,9,13,thursday +2029-09-14,2029,9,14,friday +2029-09-15,2029,9,15,saturday +2029-09-17,2029,9,17,monday +2029-09-18,2029,9,18,tuesday +2029-09-20,2029,9,20,thursday +2029-09-23,2029,9,23,sunday +2029-09-25,2029,9,25,tuesday +2029-09-28,2029,9,28,friday +2029-09-29,2029,9,29,saturday +2029-10-02,2029,10,2,tuesday +2029-10-05,2029,10,5,friday +2029-10-06,2029,10,6,saturday +2029-10-08,2029,10,8,monday +2029-10-09,2029,10,9,tuesday +2029-10-10,2029,10,10,wednesday +2029-10-11,2029,10,11,thursday +2029-10-12,2029,10,12,friday +2029-10-14,2029,10,14,sunday +2029-10-15,2029,10,15,monday +2029-10-16,2029,10,16,tuesday +2029-10-17,2029,10,17,wednesday +2029-10-18,2029,10,18,thursday +2029-10-21,2029,10,21,sunday +2029-10-22,2029,10,22,monday +2029-10-24,2029,10,24,wednesday +2029-10-25,2029,10,25,thursday +2029-10-27,2029,10,27,saturday +2029-10-28,2029,10,28,sunday +2029-10-29,2029,10,29,monday +2029-11-01,2029,11,1,thursday +2029-11-02,2029,11,2,friday +2029-11-03,2029,11,3,saturday +2029-11-07,2029,11,7,wednesday +2029-11-08,2029,11,8,thursday +2029-11-11,2029,11,11,sunday +2029-11-12,2029,11,12,monday +2029-11-13,2029,11,13,tuesday +2029-11-14,2029,11,14,wednesday +2029-11-15,2029,11,15,thursday +2029-11-16,2029,11,16,friday +2029-11-17,2029,11,17,saturday +2029-11-21,2029,11,21,wednesday +2029-11-23,2029,11,23,friday +2029-11-24,2029,11,24,saturday +2029-11-27,2029,11,27,tuesday +2029-11-29,2029,11,29,thursday +2029-12-01,2029,12,1,saturday +2029-12-02,2029,12,2,sunday +2030-01-22,2030,1,22,tuesday +2030-01-23,2030,1,23,wednesday +2030-01-27,2030,1,27,sunday +2030-01-31,2030,1,31,thursday +2030-02-02,2030,2,2,saturday +2030-02-05,2030,2,5,tuesday +2030-02-06,2030,2,6,wednesday +2030-02-09,2030,2,9,saturday +2030-02-11,2030,2,11,monday +2030-02-14,2030,2,14,thursday +2030-02-15,2030,2,15,friday +2030-02-16,2030,2,16,saturday +2030-02-17,2030,2,17,sunday +2030-02-20,2030,2,20,wednesday +2030-02-23,2030,2,23,saturday +2030-02-24,2030,2,24,sunday +2030-02-27,2030,2,27,wednesday +2030-03-01,2030,3,1,friday +2030-03-04,2030,3,4,monday +2030-03-05,2030,3,5,tuesday +2030-03-07,2030,3,7,thursday +2030-03-08,2030,3,8,friday +2030-03-10,2030,3,10,sunday +2030-03-11,2030,3,11,monday +2030-03-12,2030,3,12,tuesday +2030-03-13,2030,3,13,wednesday +2030-03-15,2030,3,15,friday +2030-03-16,2030,3,16,saturday +2030-03-17,2030,3,17,sunday +2030-03-18,2030,3,18,monday +2030-03-19,2030,3,19,tuesday +2030-03-22,2030,3,22,friday +2030-03-24,2030,3,24,sunday +2030-03-26,2030,3,26,tuesday +2030-03-27,2030,3,27,wednesday +2030-03-28,2030,3,28,thursday +2030-03-29,2030,3,29,friday +2030-03-30,2030,3,30,saturday +2030-04-02,2030,4,2,tuesday +2030-04-04,2030,4,4,thursday +2030-04-05,2030,4,5,friday +2030-04-11,2030,4,11,thursday +2030-04-13,2030,4,13,saturday +2030-04-15,2030,4,15,monday +2030-04-16,2030,4,16,tuesday +2030-04-18,2030,4,18,thursday +2030-04-19,2030,4,19,friday +2030-04-20,2030,4,20,saturday +2030-04-25,2030,4,25,thursday +2030-04-28,2030,4,28,sunday +2030-04-29,2030,4,29,monday +2030-05-01,2030,5,1,wednesday +2030-05-06,2030,5,6,monday +2030-05-07,2030,5,7,tuesday +2030-05-11,2030,5,11,saturday +2030-05-15,2030,5,15,wednesday +2030-05-17,2030,5,17,friday +2030-05-18,2030,5,18,saturday +2030-05-21,2030,5,21,tuesday +2030-05-22,2030,5,22,wednesday +2030-05-25,2030,5,25,saturday +2030-05-28,2030,5,28,tuesday +2030-05-29,2030,5,29,wednesday +2030-05-30,2030,5,30,thursday +2030-06-01,2030,6,1,saturday +2030-06-04,2030,6,4,tuesday +2030-06-05,2030,6,5,wednesday +2030-06-06,2030,6,6,thursday +2030-06-08,2030,6,8,saturday +2030-06-13,2030,6,13,thursday +2030-06-22,2030,6,22,saturday +2030-06-25,2030,6,25,tuesday +2030-06-26,2030,6,26,wednesday +2030-06-27,2030,6,27,thursday +2030-06-30,2030,6,30,sunday +2030-07-01,2030,7,1,monday +2030-07-02,2030,7,2,tuesday +2030-07-05,2030,7,5,friday +2030-07-06,2030,7,6,saturday +2030-07-09,2030,7,9,tuesday +2030-07-14,2030,7,14,sunday +2030-07-16,2030,7,16,tuesday +2030-07-20,2030,7,20,saturday +2030-07-23,2030,7,23,tuesday +2030-07-24,2030,7,24,wednesday +2030-07-25,2030,7,25,thursday +2030-07-26,2030,7,26,friday +2030-07-28,2030,7,28,sunday +2030-07-30,2030,7,30,tuesday +2030-07-31,2030,7,31,wednesday +2030-08-02,2030,8,2,friday +2030-08-06,2030,8,6,tuesday +2030-08-10,2030,8,10,saturday +2030-08-12,2030,8,12,monday +2030-08-14,2030,8,14,wednesday +2030-08-17,2030,8,17,saturday +2030-08-18,2030,8,18,sunday +2030-08-24,2030,8,24,saturday +2030-08-26,2030,8,26,monday +2030-08-28,2030,8,28,wednesday +2030-08-29,2030,8,29,thursday +2030-08-30,2030,8,30,friday +2030-09-01,2030,9,1,sunday +2030-09-02,2030,9,2,monday +2030-09-03,2030,9,3,tuesday +2030-09-05,2030,9,5,thursday +2030-09-07,2030,9,7,saturday +2030-09-08,2030,9,8,sunday +2030-09-11,2030,9,11,wednesday +2030-09-12,2030,9,12,thursday +2030-09-13,2030,9,13,friday +2030-09-15,2030,9,15,sunday +2030-09-17,2030,9,17,tuesday +2030-09-18,2030,9,18,wednesday +2030-09-23,2030,9,23,monday +2030-09-24,2030,9,24,tuesday +2030-09-26,2030,9,26,thursday +2030-09-27,2030,9,27,friday +2030-09-28,2030,9,28,saturday +2030-09-29,2030,9,29,sunday +2030-09-30,2030,9,30,monday +2030-10-01,2030,10,1,tuesday +2030-10-02,2030,10,2,wednesday +2030-10-03,2030,10,3,thursday +2030-10-07,2030,10,7,monday +2030-10-19,2030,10,19,saturday +2030-10-22,2030,10,22,tuesday +2030-10-23,2030,10,23,wednesday +2030-10-28,2030,10,28,monday +2030-10-29,2030,10,29,tuesday +2030-10-31,2030,10,31,thursday +2030-11-01,2030,11,1,friday +2030-11-04,2030,11,4,monday +2030-11-05,2030,11,5,tuesday +2030-11-07,2030,11,7,thursday +2030-11-11,2030,11,11,monday +2030-11-15,2030,11,15,friday +2030-11-16,2030,11,16,saturday +2030-11-17,2030,11,17,sunday +2030-11-19,2030,11,19,tuesday +2030-11-20,2030,11,20,wednesday +2030-11-21,2030,11,21,thursday +2030-11-22,2030,11,22,friday +2030-11-25,2030,11,25,monday +2030-11-27,2030,11,27,wednesday +2030-11-29,2030,11,29,friday +2030-11-30,2030,11,30,saturday +2030-12-11,2030,12,11,wednesday +2030-12-14,2030,12,14,saturday +2030-12-15,2030,12,15,sunday +2030-12-19,2030,12,19,thursday +2030-12-20,2030,12,20,friday +2030-12-21,2030,12,21,saturday +2030-12-23,2030,12,23,monday +2030-12-30,2030,12,30,monday +2029-12-04,2029,12,4,tuesday +2029-12-06,2029,12,6,thursday +2029-12-10,2029,12,10,monday +2029-12-12,2029,12,12,wednesday +2029-12-13,2029,12,13,thursday +2029-12-14,2029,12,14,friday +2029-12-17,2029,12,17,monday +2029-12-27,2029,12,27,thursday +2029-12-28,2029,12,28,friday +2029-12-29,2029,12,29,saturday +2029-12-31,2029,12,31,monday +2030-01-02,2030,1,2,wednesday +2030-01-03,2030,1,3,thursday +2030-01-06,2030,1,6,sunday +2030-01-08,2030,1,8,tuesday +2030-01-09,2030,1,9,wednesday +2030-01-10,2030,1,10,thursday +2030-01-11,2030,1,11,friday +2030-01-14,2030,1,14,monday +2030-01-15,2030,1,15,tuesday +2030-01-17,2030,1,17,thursday +2030-01-19,2030,1,19,saturday +2030-01-24,2030,1,24,thursday +2030-01-25,2030,1,25,friday +2030-01-26,2030,1,26,saturday +2030-01-28,2030,1,28,monday +2030-01-29,2030,1,29,tuesday +2030-01-30,2030,1,30,wednesday +2030-02-01,2030,2,1,friday +2030-02-03,2030,2,3,sunday +2030-02-04,2030,2,4,monday +2030-02-07,2030,2,7,thursday +2030-02-08,2030,2,8,friday +2030-02-10,2030,2,10,sunday +2030-02-12,2030,2,12,tuesday +2030-02-13,2030,2,13,wednesday +2030-02-18,2030,2,18,monday +2030-02-19,2030,2,19,tuesday +2030-02-21,2030,2,21,thursday +2030-02-22,2030,2,22,friday +2030-02-25,2030,2,25,monday +2030-02-26,2030,2,26,tuesday +2030-02-28,2030,2,28,thursday +2030-03-02,2030,3,2,saturday +2030-03-03,2030,3,3,sunday +2030-03-06,2030,3,6,wednesday +2030-03-09,2030,3,9,saturday +2030-03-14,2030,3,14,thursday +2030-03-20,2030,3,20,wednesday +2030-03-21,2030,3,21,thursday +2030-03-23,2030,3,23,saturday +2030-03-25,2030,3,25,monday +2030-03-31,2030,3,31,sunday +2030-04-01,2030,4,1,monday +2030-04-03,2030,4,3,wednesday +2030-04-06,2030,4,6,saturday +2030-04-07,2030,4,7,sunday +2030-04-08,2030,4,8,monday +2030-04-09,2030,4,9,tuesday +2030-04-10,2030,4,10,wednesday +2030-04-12,2030,4,12,friday +2030-04-14,2030,4,14,sunday +2030-04-17,2030,4,17,wednesday +2030-04-21,2030,4,21,sunday +2030-04-22,2030,4,22,monday +2030-04-23,2030,4,23,tuesday +2030-04-24,2030,4,24,wednesday +2030-04-26,2030,4,26,friday +2030-04-27,2030,4,27,saturday +2030-04-30,2030,4,30,tuesday +2030-05-02,2030,5,2,thursday +2030-05-03,2030,5,3,friday +2030-05-04,2030,5,4,saturday +2030-05-05,2030,5,5,sunday +2030-05-08,2030,5,8,wednesday +2030-05-09,2030,5,9,thursday +2030-05-10,2030,5,10,friday +2030-05-12,2030,5,12,sunday +2030-05-13,2030,5,13,monday +2030-05-14,2030,5,14,tuesday +2030-05-16,2030,5,16,thursday +2030-05-19,2030,5,19,sunday +2030-05-20,2030,5,20,monday +2030-05-23,2030,5,23,thursday +2030-05-24,2030,5,24,friday +2030-05-26,2030,5,26,sunday +2030-05-27,2030,5,27,monday +2030-05-31,2030,5,31,friday +2030-06-02,2030,6,2,sunday +2030-06-03,2030,6,3,monday +2030-06-07,2030,6,7,friday +2030-06-09,2030,6,9,sunday +2030-06-10,2030,6,10,monday +2030-06-11,2030,6,11,tuesday +2030-06-12,2030,6,12,wednesday +2030-06-14,2030,6,14,friday +2030-06-15,2030,6,15,saturday +2030-06-16,2030,6,16,sunday +2030-06-17,2030,6,17,monday +2030-06-18,2030,6,18,tuesday +2030-06-19,2030,6,19,wednesday +2030-06-20,2030,6,20,thursday +2030-06-21,2030,6,21,friday +2030-06-23,2030,6,23,sunday +2030-06-24,2030,6,24,monday +2030-06-28,2030,6,28,friday +2030-06-29,2030,6,29,saturday +2030-07-03,2030,7,3,wednesday +2030-07-04,2030,7,4,thursday +2030-07-07,2030,7,7,sunday +2030-07-08,2030,7,8,monday +2030-07-10,2030,7,10,wednesday +2030-07-11,2030,7,11,thursday +2030-07-12,2030,7,12,friday +2030-07-13,2030,7,13,saturday +2030-07-15,2030,7,15,monday +2030-07-17,2030,7,17,wednesday +2030-07-18,2030,7,18,thursday +2030-07-19,2030,7,19,friday +2030-07-21,2030,7,21,sunday +2030-07-22,2030,7,22,monday +2030-07-27,2030,7,27,saturday +2030-07-29,2030,7,29,monday +2030-08-01,2030,8,1,thursday +2030-08-03,2030,8,3,saturday +2030-08-04,2030,8,4,sunday +2030-08-05,2030,8,5,monday +2030-08-07,2030,8,7,wednesday +2030-08-08,2030,8,8,thursday +2030-08-09,2030,8,9,friday +2030-08-11,2030,8,11,sunday +2030-08-13,2030,8,13,tuesday +2030-08-15,2030,8,15,thursday +2030-08-16,2030,8,16,friday +2030-08-19,2030,8,19,monday +2030-08-20,2030,8,20,tuesday +2030-08-21,2030,8,21,wednesday +2030-08-22,2030,8,22,thursday +2030-08-23,2030,8,23,friday +2030-08-25,2030,8,25,sunday +2030-08-27,2030,8,27,tuesday +2030-08-31,2030,8,31,saturday +2030-09-04,2030,9,4,wednesday +2030-09-06,2030,9,6,friday +2030-09-09,2030,9,9,monday +2030-09-10,2030,9,10,tuesday +2030-09-14,2030,9,14,saturday +2030-09-16,2030,9,16,monday +2030-09-19,2030,9,19,thursday +2030-09-20,2030,9,20,friday +2030-09-21,2030,9,21,saturday +2030-09-22,2030,9,22,sunday +2030-09-25,2030,9,25,wednesday +2030-10-04,2030,10,4,friday +2030-10-05,2030,10,5,saturday +2030-10-06,2030,10,6,sunday +2030-10-08,2030,10,8,tuesday +2030-10-09,2030,10,9,wednesday +2030-10-10,2030,10,10,thursday +2030-10-11,2030,10,11,friday +2030-10-12,2030,10,12,saturday +2030-10-13,2030,10,13,sunday +2030-10-14,2030,10,14,monday +2030-10-15,2030,10,15,tuesday +2030-10-16,2030,10,16,wednesday +2030-10-17,2030,10,17,thursday +2030-10-18,2030,10,18,friday +2030-10-20,2030,10,20,sunday +2030-10-21,2030,10,21,monday +2030-10-24,2030,10,24,thursday +2030-10-25,2030,10,25,friday +2030-10-26,2030,10,26,saturday +2030-10-27,2030,10,27,sunday +2030-10-30,2030,10,30,wednesday +2030-11-02,2030,11,2,saturday +2030-11-03,2030,11,3,sunday +2030-11-06,2030,11,6,wednesday +2030-11-08,2030,11,8,friday +2030-11-09,2030,11,9,saturday +2030-11-10,2030,11,10,sunday +2030-11-12,2030,11,12,tuesday +2030-11-13,2030,11,13,wednesday +2030-11-14,2030,11,14,thursday +2030-11-18,2030,11,18,monday +2030-11-23,2030,11,23,saturday +2030-11-24,2030,11,24,sunday +2030-11-26,2030,11,26,tuesday +2030-11-28,2030,11,28,thursday +2030-12-01,2030,12,1,sunday +2030-12-02,2030,12,2,monday +2030-12-03,2030,12,3,tuesday +2030-12-04,2030,12,4,wednesday +2030-12-05,2030,12,5,thursday +2030-12-06,2030,12,6,friday +2030-12-07,2030,12,7,saturday +2030-12-08,2030,12,8,sunday +2030-12-09,2030,12,9,monday +2030-12-10,2030,12,10,tuesday +2030-12-12,2030,12,12,thursday +2030-12-13,2030,12,13,friday +2030-12-16,2030,12,16,monday +2030-12-17,2030,12,17,tuesday +2030-12-18,2030,12,18,wednesday +2030-12-22,2030,12,22,sunday +2030-12-24,2030,12,24,tuesday +2030-12-25,2030,12,25,wednesday +2030-12-26,2030,12,26,thursday +2030-12-27,2030,12,27,friday +2030-12-28,2030,12,28,saturday +2030-12-29,2030,12,29,sunday +2030-12-31,2030,12,31,tuesday diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv new file mode 100644 index 0000000..9383505 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_lesson_202605191312.csv @@ -0,0 +1,19 @@ +"lesson_id","title","duration_min","course_id" +2,JOIN,15,1 +3,WHERE и фильтрация,12,1 +4,Агрегации и GROUP BY,18,1 +6,Pandas основы,20,2 +7,NumPy,15,2 +8,Визуализация данных,22,2 +9,Основы BI,12,3 +10,Метрики и дашборды,18,3 +13,Сводные таблицы,18,4 +16,Линейная регрессия,30,5 +18,Кластеризация,25,5 +1,SELECT и FROM,10,1 +5,Подзапросы,25,1 +11,Power BI введение,20,3 +12,Формулы в Excel,15,4 +14,Графики и диаграммы,14,4 +15,Введение в ML,20,5 +17,Классификация,28,5 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv new file mode 100644 index 0000000..a31ef6a --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/dim_user_202605201142.csv @@ -0,0 +1,21 @@ +"user_id","name","age","email","registration_date" +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv new file mode 100644 index 0000000..e3a32cd --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/enrollments_202605201134.csv @@ -0,0 +1,46 @@ +"user_id","enrolled_at","course_id" +2,2023-01-20 00:00:00.000,1 +3,2023-03-05 00:00:00.000,2 +6,2023-01-06 00:00:00.000,1 +6,2023-01-10 00:00:00.000,2 +6,2023-02-05 00:00:00.000,4 +7,2023-01-13 00:00:00.000,1 +7,2023-03-10 00:00:00.000,3 +8,2023-01-22 00:00:00.000,3 +13,2023-01-19 00:00:00.000,2 +13,2023-02-20 00:00:00.000,4 +16,2023-02-16 00:00:00.000,2 +16,2023-02-16 00:00:00.000,4 +19,2023-04-11 00:00:00.000,1 +19,2023-04-11 00:00:00.000,2 +2,2023-03-15 00:00:00.000,3 +4,2023-03-10 00:00:00.000,2 +7,2023-02-01 00:00:00.000,2 +9,2023-02-15 00:00:00.000,1 +18,2023-04-05 00:00:00.000,4 +4,2023-04-01 00:00:00.000,3 +8,2023-01-22 00:00:00.000,1 +8,2023-01-22 00:00:00.000,2 +9,2023-02-12 00:00:00.000,4 +10,2023-04-16 00:00:00.000,1 +18,2023-04-02 00:00:00.000,1 +3,2023-02-25 00:00:00.000,1 +6,2023-03-08 00:00:00.000,3 +1,2023-02-01 00:00:00.000,2 +11,2023-04-21 00:00:00.000,2 +14,2023-01-09 00:00:00.000,2 +14,2023-01-09 00:00:00.000,3 +14,2023-03-20 00:00:00.000,5 +17,2023-03-22 00:00:00.000,3 +20,2023-04-19 00:00:00.000,2 +12,2023-03-10 00:00:00.000,3 +1,2023-01-15 00:00:00.000,1 +12,2023-01-16 00:00:00.000,1 +14,2023-01-09 00:00:00.000,1 +14,2023-02-01 00:00:00.000,4 +15,2023-01-11 00:00:00.000,1 +15,2023-03-05 00:00:00.000,3 +17,2023-03-25 00:00:00.000,5 +5,2023-03-20 00:00:00.000,5 +1,2023-02-10 00:00:00.000,4 +15,2023-02-01 00:00:00.000,2 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv new file mode 100644 index 0000000..7be0742 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/csv files/fact_lesson_views_202605191356.csv @@ -0,0 +1,59 @@ +"user_id","lesson_id","course_id","viewed_at" +3,6,2,2023-03-06 12:00:00.000 +4,9,3,2023-04-02 10:00:00.000 +4,6,2,2023-03-12 14:00:00.000 +4,7,2,2023-03-13 09:00:00.000 +6,3,1,2023-01-08 09:00:00.000 +6,6,2,2023-01-11 14:00:00.000 +6,7,2,2023-01-12 10:00:00.000 +6,8,2,2023-01-13 11:00:00.000 +6,9,3,2023-03-09 11:00:00.000 +7,9,3,2023-03-11 14:00:00.000 +2,9,3,2023-03-16 10:00:00.000 +2,10,3,2023-03-16 10:30:00.000 +3,2,1,2023-02-26 11:20:00.000 +4,10,3,2023-04-03 10:00:00.000 +6,2,1,2023-01-07 10:15:00.000 +6,4,1,2023-01-08 09:30:00.000 +6,13,4,2023-02-06 09:30:00.000 +7,2,1,2023-01-14 10:20:00.000 +7,3,1,2023-01-15 09:00:00.000 +7,10,3,2023-03-11 14:30:00.000 +9,13,4,2023-02-13 10:30:00.000 +13,6,2,2023-01-20 14:00:00.000 +13,7,2,2023-01-21 10:00:00.000 +2,1,1,2023-01-21 09:00:00.000 +6,1,1,2023-01-07 10:00:00.000 +6,14,4,2023-02-07 10:00:00.000 +7,1,1,2023-01-14 10:00:00.000 +9,12,4,2023-02-13 10:00:00.000 +9,1,1,2023-02-16 11:00:00.000 +3,1,1,2023-02-26 11:00:00.000 +6,5,1,2023-01-09 10:00:00.000 +6,12,4,2023-02-06 09:00:00.000 +13,12,4,2023-02-21 09:00:00.000 +1,2,1,2023-01-16 10:15:00.000 +1,3,1,2023-01-17 09:00:00.000 +1,4,1,2023-01-17 09:30:00.000 +1,4,1,2023-01-18 10:00:00.000 +1,6,2,2023-02-02 14:00:00.000 +1,13,4,2023-02-15 10:30:00.000 +5,16,5,2023-03-22 11:00:00.000 +12,2,1,2023-01-17 10:20:00.000 +12,3,1,2023-04-10 11:30:00.000 +14,2,1,2023-01-09 08:15:00.000 +14,4,1,2023-01-10 10:30:00.000 +14,6,2,2023-01-10 14:00:00.000 +14,9,3,2023-01-10 08:00:00.000 +1,4,1,2023-01-17 11:00:00.000 +12,9,3,2023-03-12 10:00:00.000 +14,3,1,2023-01-10 10:00:00.000 +14,7,2,2023-01-11 10:00:00.000 +14,8,2,2023-01-12 11:00:00.000 +1,12,4,2023-02-15 10:00:00.000 +12,1,1,2023-01-17 10:00:00.000 +14,5,1,2023-01-11 09:00:00.000 +1,1,1,2023-01-16 10:00:00.000 +5,15,5,2023-03-21 10:00:00.000 +12,1,1,2023-04-10 11:00:00.000 +14,1,1,2023-01-09 08:00:00.000 diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql new file mode 100644 index 0000000..62c94cb --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 1. Загрузка тестовых данных/task_1_upload_csv_files.sql @@ -0,0 +1,97 @@ +-- DIM + +--DROP TABLE IF EXISTS mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_user +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + age UInt8, + email String, + registration_date DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_course ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_course +ON CLUSTER clickhouse_cluster +( + course_id UInt64, + title String, + category LowCardinality(String), + created_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +--DROP TABLE IF EXISTS mnoskov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_lesson +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + title String, + duration_min UInt16, + course_id UInt64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_lesson', + '{replica}' +) +ORDER BY (lesson_id, course_id); + +--DROP TABLE IF EXISTS mnoskov.core_dim_date ON CLUSTER clickhouse_cluster SYNC; +CREATE TABLE mnoskov.core_dim_date +ON CLUSTER clickhouse_cluster +( + date Date, + year Int16, + month Int8, + day Int8, + weekday LowCardinality(String) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_dim_date', + '{replica}' +) +ORDER BY date; + + +-- FACT + +--DROP TABLE IF EXISTS mnoskov.core_fact_lesson_views; +CREATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + lesson_id UInt64, + course_id UInt64, + viewed_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_fact_lesson_views', + '{replica}' +) +PARTITION BY toYYYYMM(viewed_at) +ORDER BY (viewed_at, user_id); -- я если честно не знаю как с этой таблицей поступить в ORDER BY. Наставьте меня на путь истиный. + +--DROP TABLE IF EXISTS mnoskov.core_enrollments; +CREATE TABLE mnoskov.core_enrollments +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + course_id UInt64, + enrolled_at DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/core_enrollments', + '{replica}' +) +PARTITION BY toYYYYMM(enrolled_at) +ORDER BY (enrolled_at, user_id); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql new file mode 100644 index 0000000..046c023 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_2_lesson_popularity_summary.sql @@ -0,0 +1,128 @@ +--DROP TABLE mnoskov.lesson_popularity_summary ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster +( + lesson_id UInt64, + lesson_title String, + course_id UInt64, + course_title String, + total_views AggregateFunction(count), + unique_users AggregateFunction(uniq, UInt64), + first_view AggregateFunction(min, DateTime), + last_view AggregateFunction(max, DateTime) +) +ENGINE AggregatingMergeTree +ORDER BY lesson_id; + +--DROP VIEW mnoskov.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC; +CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.lesson_popularity_summary +AS +SELECT + cdl.lesson_id AS lesson_id, + cdl.title AS lesson_title, + cdc.course_id AS course_id, + cdc.title AS course_title, + countIfState(flv.user_id != 0) AS total_views, -- я вообще ахуел когда узнал что такое существует(я про If у агрегаций). Я наткнулся на это когда осознал что кликхаус в join не ставит null, а ставит значение по умолчанию + uniqIfState(flv.user_id, flv.user_id != 0) AS unique_users, + minIfState(flv.viewed_at, flv.user_id != 0) AS first_view, + maxIfState(flv.viewed_at, flv.user_id != 0) AS last_view +FROM + mnoskov.core_fact_lesson_views flv +RIGHT JOIN + mnoskov.core_dim_lesson cdl ON flv.lesson_id = cdl.lesson_id -- поидее нужно большую таблицу ставить в левую часть JOIN, но если я тут так сделаю с фактами, то запрос кореектно не будет работать +JOIN + mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id +GROUP BY + cdl.lesson_id, cdl.title, cdc.course_id, cdc.title; + + +SELECT * FROM mnoskov.core_fact_lesson_views; + +SELECT + lesson_id, + any(lesson_title), + any(course_id), + any(course_title), + countMerge(total_views) AS total_views, + uniqMerge(unique_users) AS uniq_users, + minMerge(first_view) AS first_view, + maxMerge(last_view) AS last_view +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + lesson_id; +-- ЕБАТЬ КАК Я АХУЕЛ КОГДА ДОПЕР ДО ТОГО ЧТО ЭТО НУЖНО ЧИТАТЬ ТАКИМ СПОСОБОМ А НЕ ПРОСТО ЧЕРЕЗ ЗВЕЗДОЧКУ. У меня MV ебать сколько изменений потерпела чтобы заработать, а проблема возможно изначально была в чтении. +-- Правда оно читается через раз, то есть результат то нет, но думаю это проблема движка AggregatingMergeTree. +-- Это получается чтобы был удобный вывод, теперь поверх этого селекта можно сделать обычную VIEW? + +-- Заполненния и очищения: +TRUNCATE TABLE mnoskov.core_fact_lesson_views +ON CLUSTER clickhouse_cluster sync; + +TRUNCATE TABLE mnoskov.lesson_popularity_summary +ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +VALUES +(3, 6, 2, '2023-03-06 12:00:00'), +(4, 9, 3, '2023-04-02 10:00:00'), +(4, 6, 2, '2023-03-12 14:00:00'), +(4, 7, 2, '2023-03-13 09:00:00'), +(6, 3, 1, '2023-01-08 09:00:00'), +(6, 6, 2, '2023-01-11 14:00:00'), +(6, 7, 2, '2023-01-12 10:00:00'), +(6, 8, 2, '2023-01-13 11:00:00'), +(6, 9, 3, '2023-03-09 11:00:00'), +(7, 9, 3, '2023-03-11 14:00:00'), +(2, 9, 3, '2023-03-16 10:00:00'), +(2, 10, 3, '2023-03-16 10:30:00'), +(3, 2, 1, '2023-02-26 11:20:00'), +(4, 10, 3, '2023-04-03 10:00:00'), +(6, 2, 1, '2023-01-07 10:15:00'), +(6, 4, 1, '2023-01-08 09:30:00'), +(6, 13, 4, '2023-02-06 09:30:00'), +(7, 2, 1, '2023-01-14 10:20:00'), +(7, 3, 1, '2023-01-15 09:00:00'), +(7, 10, 3, '2023-03-11 14:30:00'), +(9, 13, 4, '2023-02-13 10:30:00'), +(13, 6, 2, '2023-01-20 14:00:00'), +(13, 7, 2, '2023-01-21 10:00:00'), +(2, 1, 1, '2023-01-21 09:00:00'), +(6, 1, 1, '2023-01-07 10:00:00'), +(6, 14, 4, '2023-02-07 10:00:00'), +(7, 1, 1, '2023-01-14 10:00:00'), +(9, 12, 4, '2023-02-13 10:00:00'), +(9, 1, 1, '2023-02-16 11:00:00'), +(3, 1, 1, '2023-02-26 11:00:00'), +(6, 5, 1, '2023-01-09 10:00:00'), +(6, 12, 4, '2023-02-06 09:00:00'), +(13, 12, 4, '2023-02-21 09:00:00'), +(1, 2, 1, '2023-01-16 10:15:00'), +(1, 3, 1, '2023-01-17 09:00:00'), +(1, 4, 1, '2023-01-17 09:30:00'), +(1, 4, 1, '2023-01-18 10:00:00'), +(1, 6, 2, '2023-02-02 14:00:00'), +(1, 13, 4, '2023-02-15 10:30:00'), +(5, 16, 5, '2023-03-22 11:00:00'), +(12, 2, 1, '2023-01-17 10:20:00'), +(12, 3, 1, '2023-04-10 11:30:00'), +(14, 2, 1, '2023-01-09 08:15:00'), +(14, 4, 1, '2023-01-10 10:30:00'), +(14, 6, 2, '2023-01-10 14:00:00'), +(14, 9, 3, '2023-01-10 08:00:00'), +(1, 4, 1, '2023-01-17 11:00:00'), +(12, 9, 3, '2023-03-12 10:00:00'), +(14, 3, 1, '2023-01-10 10:00:00'), +(14, 7, 2, '2023-01-11 10:00:00'), +(14, 8, 2, '2023-01-12 11:00:00'), +(1, 12, 4, '2023-02-15 10:00:00'), +(12, 1, 1, '2023-01-17 10:00:00'), +(14, 5, 1, '2023-01-11 09:00:00'), +(1, 1, 1, '2023-01-16 10:00:00'), +(5, 15, 5, '2023-03-21 10:00:00'), +(12, 1, 1, '2023-04-10 11:00:00'), +(14, 1, 1, '2023-01-09 08:00:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql new file mode 100644 index 0000000..25b333d --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_3_inactive_users_summary.sql @@ -0,0 +1,75 @@ +--DROP TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.inactive_users_summary +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + name String, + email String, + age UInt8, + registered_courses_count UInt32, + registration_date DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/inactive_users_summary', + '{replica}' +) +ORDER BY user_id; + +--DROP VIEW mnoskov.mv_inactive_users_summary ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_inactive_users_summary +ON CLUSTER clickhouse_cluster +TO mnoskov.inactive_users_summary +AS +SELECT + u.user_id AS user_id, + u.name AS name, + u.email AS email, + u.age AS age, + e.registered_courses_count AS registered_courses_count, + u.registration_date AS registration_date +FROM + mnoskov.core_dim_user u +LEFT JOIN ( + SELECT + user_id, + count() AS registered_courses_count + FROM + mnoskov.core_enrollments + GROUP BY + user_id +) e ON u.user_id = e.user_id +WHERE u.user_id NOT IN (SELECT lv.user_id FROM mnoskov.core_fact_lesson_views lv); -- я ломалася тут между NOT IN и NOT EXISTS, но решил оставить первый тк это уже витрины мы делаем, и NULL поидее быть не должно + + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_dim_user; +SELECT * FROM mnoskov.inactive_users_summary; + +TRUNCATE TABLE mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC; +TRUNCATE TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC; + +INSERT INTO mnoskov.core_dim_user (user_id, name, age, email, registration_date) +VALUES +(1, 'Alice', 25, 'alice@mail.com', '2023-01-09 23:00:00'), +(2, 'Bob', 30, 'bob@gmail.com', '2023-02-04 23:00:00'), +(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-19 23:00:00'), +(4, 'Diana', 28, 'diana@mail.com', '2023-02-28 23:00:00'), +(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-09 23:00:00'), +(6, 'Frank', 29, 'frank@mail.com', '2023-01-04 23:00:00'), +(7, 'Grace', 26, 'grace@gmail.com', '2023-01-11 23:00:00'), +(8, 'Henry', 32, 'henry@mail.com', '2023-01-19 23:00:00'), +(9, 'Iris', 24, 'iris@gmail.com', '2023-02-09 23:00:00'), +(10, 'Jack', 27, 'jack@mail.com', '2023-04-14 23:00:00'), +(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-19 23:00:00'), +(12, 'Leo', 31, 'leo@mail.com', '2023-01-14 23:00:00'), +(13, 'Mia', 26, 'mia@mail.com', '2023-01-17 23:00:00'), +(14, 'Nina', 28, 'nina@mail.com', '2023-01-07 23:00:00'), +(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-09 23:00:00'), +(16, 'Paul', 29, 'paul@mail.com', '2023-02-14 23:00:00'), +(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-19 23:00:00'), +(18, 'Rita', 27, 'rita@mail.com', '2023-03-31 23:00:00'), +(19, 'Sam', 30, 'sam@gmail.com', '2023-04-09 23:00:00'), +(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00'), +(22, 'Anal2', 24, 'tinanal222@mail.com', '2023-04-19 23:22:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql new file mode 100644 index 0000000..5d53f29 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 2. Создание витринных таблиц/task_4_course_completion_rate.sql @@ -0,0 +1,117 @@ +--DROP TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.course_completion_rate +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + user_name String, + course_id UInt64, + course_title String, + lessons_in_course UInt16, + lesson_viewed UInt16, + completion_rate Float32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id); + +--DROP VIEW mnoskov.mv_course_completion_rate ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_course_completion_rate +ON CLUSTER clickhouse_cluster +TO mnoskov.course_completion_rate +AS +SELECT + u.user_id AS user_id, + u.name AS user_name, + cl.course_id AS course_id, + cl.course_title AS course_title, + cl.lessons_in_course AS lessons_in_course, + lv.lesson_viewed AS lesson_viewed, + ifnull(round(toFloat32(ifnull(lv.lesson_viewed, 0)) / nullif(cl.lessons_in_course, 0), 2), 0) AS completion_rate -- вроде все логично +FROM + mnoskov.core_enrollments e +RIGHT JOIN + mnoskov.core_dim_user u ON e.user_id = u.user_id +JOIN ( + SELECT + c.course_id, + c.title AS course_title, + uniq(lesson_id) lessons_in_course + FROM + mnoskov.core_dim_course c + JOIN + mnoskov.core_dim_lesson l ON l.course_id = c.course_id + GROUP BY + c.course_id, c.title +) cl ON e.course_id = cl.course_id +LEFT JOIN ( + SELECT + user_id, + course_id, + uniq(lesson_id) AS lesson_viewed + FROM + mnoskov.core_fact_lesson_views + GROUP BY + user_id, course_id +) lv ON e.user_id = lv.user_id AND e.course_id = lv.course_id +ORDER BY + u.user_id, e.course_id; + +-- Чтение очищение заполнение +SHOW TABLES FROM mnoskov; + +SELECT * FROM mnoskov.core_enrollments; +SELECT * FROM mnoskov.course_completion_rate; + +TRUNCATE TABLE mnoskov.core_enrollments ON CLUSTER clickhouse_cluster sync; +TRUNCATE TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync; + +INSERT INTO mnoskov.core_enrollments +(user_id, course_id, enrolled_at) +VALUES +(6, 1, '2023-01-05 23:00:00'), +(14, 2, '2023-01-08 23:00:00'), +(14, 3, '2023-01-08 23:00:00'), +(14, 1, '2023-01-08 23:00:00'), +(6, 2, '2023-01-09 23:00:00'), +(15, 1, '2023-01-10 23:00:00'), +(7, 1, '2023-01-12 23:00:00'), +(1, 1, '2023-01-14 23:00:00'), +(12, 1, '2023-01-15 23:00:00'), +(13, 2, '2023-01-18 23:00:00'), +(2, 1, '2023-01-19 23:00:00'), +(8, 3, '2023-01-21 23:00:00'), +(8, 1, '2023-01-21 23:00:00'), +(8, 2, '2023-01-21 23:00:00'), +(1, 2, '2023-01-31 23:00:00'), +(7, 2, '2023-01-31 23:00:00'), +(14, 4, '2023-01-31 23:00:00'), +(15, 2, '2023-01-31 23:00:00'), +(6, 4, '2023-02-04 23:00:00'), +(1, 4, '2023-02-09 23:00:00'), +(9, 4, '2023-02-11 23:00:00'), +(9, 1, '2023-02-14 23:00:00'), +(16, 2, '2023-02-15 23:00:00'), +(16, 4, '2023-02-15 23:00:00'), +(13, 4, '2023-02-19 23:00:00'), +(3, 1, '2023-02-24 23:00:00'), +(3, 2, '2023-03-04 23:00:00'), +(15, 3, '2023-03-04 23:00:00'), +(6, 3, '2023-03-07 23:00:00'), +(4, 2, '2023-03-09 23:00:00'), +(7, 3, '2023-03-09 23:00:00'), +(12, 3, '2023-03-09 23:00:00'), +(2, 3, '2023-03-14 23:00:00'), +(5, 5, '2023-03-19 23:00:00'), +(14, 5, '2023-03-19 23:00:00'), +(17, 3, '2023-03-21 23:00:00'), +(17, 5, '2023-03-24 23:00:00'), +(4, 3, '2023-03-31 23:00:00'), +(18, 1, '2023-04-01 23:00:00'), +(18, 4, '2023-04-04 23:00:00'), +(19, 1, '2023-04-10 23:00:00'), +(19, 2, '2023-04-10 23:00:00'), +(10, 1, '2023-04-15 23:00:00'), +(20, 2, '2023-04-18 23:00:00'), +(11, 2, '2023-04-20 23:00:00'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql new file mode 100644 index 0000000..f9b83f5 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 3. Анализ и запросы/Task_5_SQL_queries_for_dm.sql @@ -0,0 +1,36 @@ +-- Топ 5 самых непросматреваемых уроков +SELECT + any(lesson_title) AS lesson_title, + any(course_title) AS course_title, + countMerge(total_views) AS total_views +FROM + mnoskov.lesson_popularity_summary +GROUP BY + lesson_id +ORDER BY + total_views +LIMIT 5; + + +-- Неактивные пользователи младше 26 +SELECT + name, + age, + email +FROM + mnoskov.inactive_users_summary +WHERE + age < 26; + + +-- 3 круса с самым низкми процентом завершения +SELECT + course_title, + avg(completion_rate) AS avg_completion_rate +FROM + mnoskov.course_completion_rate +GROUP BY + course_title +ORDER BY + avg_completion_rate +LIMIT 3; \ No newline at end of file diff --git a/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt new file mode 100644 index 0000000..fa5fbd2 --- /dev/null +++ b/mnoskov/clickhouse/Пет-проект Витринный слой/Этап 4. Тестирование и валидация/task_6_compare_data_with_Greenplum.txt @@ -0,0 +1,2 @@ +Разница есть только в таблицах mv_lesson_popularity_summary +Где в Greenplum - NULL, в clickhouse - 0. Потому что clickhouser сам подставляет 0 вместо NULL. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..6140c2f --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..2a7de23 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + user_id +); +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW mnoskov.top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE=MergeTree +ORDER BY(event_time,user_id); + +CREATE TABLE mnoskov.agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE=SummingMergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM mnoskov.raw_events +GROUP BY event_date; + +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..b8d8218 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql @@ -0,0 +1,115 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + balance, + created_at +FROM ( + SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +) +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) +ORDER BY + event_datetime; + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) as user_actions_cnt +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql new file mode 100644 index 0000000..c7c4d8c --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/1. Синтаксис/practice_syntax.sql @@ -0,0 +1,215 @@ +-- Задание 1 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + age UInt8, + country String, + is_active Bool +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users', + '{replica}' +) +ORDER BY user_id; + +INSERT INTO mnoskov.users +(user_id,name,age,country,is_active) +VALUES +(1,'Ivan',30,'RU',1), +(2,'Anna',25,'RU',1), +(3,'Petr',45,'RU',0), +(4,'Olga',35,'RU',1), +(5,'John',55,'US',1), +(6,'Alice',42,'US',0), +(7,'Tom',38,'US',1), +(8,'Lucas',28,'BR',1), +(9,'Maria',60,'BR',1), +(10,'Joao',27,'BR',0), +(11,'Kenji',33,'JP',1), +(12,'Yuki',29,'JP',0), +(13,'Chen',40,'CN',1), +(14,'Li',48,'CN',1), +(15,'Fatima',31,'AE',1), +(16,'Ahmed',36,'AE',1), +(17,'George',24,'US',1), +(18,'Nina',34,'RU',1), +(19,'Max',50,'DE',1), +(20,'Sophie',22,'DE',0); + +SELECT * FROM mnoskov.users; + +--1 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +ORDER BY + age DESC +LIMIT 10; + +--2 +SELECT + name, + country +FROM + mnoskov.users +WHERE + country = 'RU' +ORDER BY + name; + +--3 +SELECT + user_id, + age, + is_active +FROM + mnoskov.users +WHERE + is_active = TRUE +LIMIT 10; +-- Данные вывелить так как лежат на диске. + +--4 +SELECT + country, + count() as cnt_users +FROM + mnoskov.users +GROUP BY + country; + +--5 +SELECT + country, + min(age) AS min_age, + max(age) AS max_age +FROM + mnoskov.users +GROUP BY + country; + +--6 +SELECT + country, + count() AS cnt_users +FROM + mnoskov.users +GROUP BY + country +ORDER BY + cnt_users DESC; + +--Доп +SELECT + uniqExact(country), + uniq(country) +FROM + mnoskov.users; +-- uniqExact(country) - кол-во уникальных значений в колонке country. Но uniq работает также. +-- В инете написано что uniq быстрее. + + +-- Задание 2 +CREATE TABLE mnoskov.events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_time DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events1', + '{replica}' +) +ORDER BY(user_id,event_time); + +INSERT INTO mnoskov.events1 +(user_id,event_time,event_type) +VALUES +(1,'2025-07-17 09:00:00','click'), +(1,'2025-07-17 09:05:00','view'), +(1,'2025-07-17 09:10:00','purchase'), +(2,'2025-07-17 10:00:00','click'), +(2,'2025-07-17 10:05:00','click'), +(2,'2025-07-17 11:00:00','view'), +(3,'2025-07-16 12:00:00','view'), +(3,'2025-07-16 12:30:00','click'), +(3,'2025-07-17 08:00:00','purchase'), +(4,'2025-07-17 13:00:00','click'), +(4,'2025-07-17 14:00:00','view'), +(4,'2025-07-17 15:00:00','view'), +(5,'2025-07-16 20:00:00','view'), +(5,'2025-07-17 01:00:00','click'), +(5,'2025-07-17 06:00:00','purchase'), +(6,'2025-07-17 07:30:00','view'), +(6,'2025-07-17 08:45:00','click'), +(7,'2025-07-16 23:59:59','click'), +(7,'2025-07-17 00:01:00','purchase'), +(7,'2025-07-17 10:10:00','view'); + +SELECT * FROM mnoskov.events1; + +--1 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS com_users +FROM + mnoskov.events1; + +--2 +SELECT + user_id, + event_time, + event_type, + row_number() OVER (PARTITION BY user_id ORDER BY event_time) rn, + dense_rank() OVER (PARTITION BY user_id ORDER BY event_time) dr, + count() OVER(PARTITION BY user_id ORDER BY event_time) AS c, + sum(1) OVER(PARTITION BY user_id ORDER BY event_time) AS s +FROM + mnoskov.events1; + +--3 +SELECT + user_id, + event_time, + event_type, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS com_users, + count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS com_users2 +FROM + mnoskov.events1; +-- все четенько работает, никаких ошибкок нет + +--4-5-6 +WITH recent_events AS ( + SELECT + user_id, + event_time, + event_type + FROM + mnoskov.events1 + WHERE + event_time < now() - INTERVAL 1 DAY +) +SELECT + user_id, + count(*) AS cnt_user_actions +FROM + recent_events +GROUP BY + user_id +ORDER BY + cnt_user_actions DESC +LIMIT + 5; +-- не понял, как сделать 6 пункт. + +-- Ответ на вопрос - ну да - поддерживает. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql new file mode 100644 index 0000000..f29dbc3 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/2. JOIN/practice_join.sql @@ -0,0 +1,57 @@ +-- Задание 1 + +SELECT + pro.product_id, + pro.name, + pri.price +FROM + mnoskov.products pro +ANY LEFT JOIN + mnoskov.prices pri ON pro.product_id = pri.product_id; +-- 2 Выберется случайная из цен. +-- 3 ALL LEFT JOIN - работает также как и LEFT JOIN. Его избегают потому что он тянет больше строк в оперативную память. + + +-- Задание 2 + +SELECT + o.time, + o.symbol, + o.size, + t.price +FROM + mnoskov.orders o +ASOF JOIN + mnoskov.ticks t ON o.symbol = t.symbol AND t.time <= o.time +WHERE symbol = 'AAPL'; +-- Потому что дата сделки и дата последней фиксации цены - разные. + + +-- Задание 3 + +SELECT + user_id, + name +FROM + mnoskov.users +WHERE user_id IN ( + SELECT user_id + FROM mnoskov.payments + WHERE amount > 1000 +); +-- 3 Потому что это фильтрация по существованию. Как оптимизируется не знаю, наверное как подзапрос. + + +-- Задание 4 + +SELECT + bl.user_id, + u.name AS user_name, + bl.event_time, + bl.event_type +FROM + mnoskov.big_logs bl +JOIN + mnoskov.users u ON bl.user_id = u.user_id; +-- 2 потому что таблица users меньше, а значит в оператику полетит меньше данных. +-- 3 если user_id уникален, то нужно выбирать ALL, так как дублей нет. В текущем задании дубли есть, но мы все равно выбираем ALL, тк нужно вывести имя для каждого лога а не юзера. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql new file mode 100644 index 0000000..6ed16cc --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/2. Запросы/3. Массивы, Map и вложенные структуры/practice_arrays_maps_and_nested_structures.sql @@ -0,0 +1,44 @@ +-- Задание 1 + +--1-2 +SELECT + user_id, + tags, + indexOf(tags, 'promo') AS pos_promo +FROM + mnoskov.events +WHERE + has(tags, 'promo'); + +--3 +SELECT + user_id, + arrayJoin(tags) AS tag +FROM + mnoskov.events; + + +-- Задание 2 + +--1 +SELECT user_id, scores['math'] FROM mnoskov.metrics; + +--2 +SELECT user_id, mapValues(scores) FROM mnoskov.metrics; + + +-- Заданик 3 + +--1 +SELECT * FROM mnoskov.users_nested WHERE has(contacts.type, 'email'); + +--2 +SELECT + id, + name, + c.type, + c.value +FROM + mnoskov.users_nested +ARRAY JOIN + contacts AS c; \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql new file mode 100644 index 0000000..2a855a6 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/1. Партиции/practice_partitions.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.sales +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + sale_date Date, + amount Float64 +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sales', + '{replica}' +) +PARTITION BY toYYYYMM(sale_date) +ORDER BY sale_date; + +ALTER TABLE mnoskov.sales DROP PARTITION '202405'; +SELECT * FROM mnoskov.sales; +-- Пропали строки где год/месяц 202405. +-- Если не указывать PARTITION BY, то строки бы искались по всей таблице, что дольше и затранее. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + id UInt64, + event_type String, + event_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events_partitioned', + '{replica}' +) +PARTITION BY event_type +ORDER BY event_date; + +ALTER TABLE mnoskov.events DROP PARTITION 'click'; +ELECT * FROM mnoskov.events; +-- Такой подход опасен, тк например login выполняется не часто по сравнению с click и получается жесткий перевес. + + +-- Задание 3 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + user_id UInt64, + log_time DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_partitioned', + '{replica}' +) +PARTITION BY cityHash64(user_id) % 100 +ORDER BY log_time; + +SELECT * FROM mnoskov.logs + +-- 2 с точки зрения баланса, у нас юзеры распределные равномерно и нет никаких перекосов. +-- 3 хз если честно, мне кажется должны ускорять. + + +-- Задание 4 + +CREATE TABLE mnoskov.visits +ON CLUSTER clickhouse_cluster +( + id UInt64, + region String, + visit_date Date +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/visits', + '{replica}' +) +PARTITION BY (region, toYYYYMM(visit_date)) +ORDER BY visit_date; + +SELECT * FROM mnoskov.visits; +-- 2 доступа к системным таблицам нет, потэтому я не могу посмотреть имя партиции. +-- 3 ну да +ALTER TABLE mnoskov.visits DROP PARTITION ('RU', '202407'); \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql new file mode 100644 index 0000000..7147e3e --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/2. Индексы/practice_indexes.sql @@ -0,0 +1,69 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + log_id UInt64, + ts DateTime +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs_ttl', + '{replica}' +) +ORDER BY ts; +ALTER TABLE mnoskov.logs ADD INDEX idx_ts ts TYPE minmax granularity 2; + +SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'; + +-- 2 У меня нет доступа к системным таблицам, я не могу посмотреть. +-- Если в order by будет log_id а не ts, то индекс не будет работать. + + +-- Задание 2 + +CREATE TABLE mnoskov.clicks +ON CLUSTER clickhouse_cluster +( + id UInt64, + country String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clicks', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; +ALTER TABLE mnoskov.clicks DROP INDEX idx_country; + +explain +SELECT * FROM clicks WHERE country = 'RU'; +-- 3 не использутся индеск, я туда 11тыс строк заинсертил, все равно не используется. +-- 4 я думаю индекс не будет работать эффективно, а может быть и вообще будет запрос замедлять. + + +-- Задание 3 + +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + email String +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/users_simple', + '{replica}' +) +ORDER BY id; + +ALTER TABLE mnoskov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + +explain +SELECT * FROM users WHERE email = 'alice@mail.com'; +-- 2 Ну я не вижу в EXPLAIN какое либо упоминание индекса, но я ссылаюсь на то, что просто мало данных в таблице. + +-- 3 Потому что этот индекс работает только на точные совпадения - exact match. + +explain +SELECT * FROM users WHERE email LIKE 'alice%'; +-- 4 Индекс не будет использоваться. \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql new file mode 100644 index 0000000..f8758f4 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/3. Индексы и сжатие/3. Сжатие/practice_compression.sql @@ -0,0 +1,27 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster +( + id UInt64, + level String, + message String CODEC(ZSTD(3)), + created_at DateTime CODEC(Delta, ZSTD) +) +ENGINE=ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY created_at; + +SELECT + column, + compression_codec +FROM system.columns +WHERE table = 'logs'; +-- если делать с codec_expression, то ошибка вылазит мол такой колонки нет +-- Выглядит вот так: +-- id +-- level +-- message CODEC(ZSTD(3)) +-- created_at CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql new file mode 100644 index 0000000..bfb6cd2 --- /dev/null +++ b/mnoskov/clickhouse/Практика clickhouse/4. Streaming в Clickhouse/1. Materialized View (MV)/practice_materialized_view_MV.sql @@ -0,0 +1,157 @@ +-- Задание 1 + +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--DROP TABLE mnoskov.clean_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.clean_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/clean_events', + '{replica}' +) +ORDER BY ts; + +CREATE MATERIALIZED VIEW mnoskov.mv_clean +ON CLUSTER clickhouse_cluster +TO mnoskov.clean_events +AS +SELECT + user_id, + ts, + nullif(event_type, '') AS event_type -- это я сделать чтобы выводилось NULL а не '' - пустая строка. +FROM mnoskov.raw_events +WHERE user_id != 0 AND event_type IS NOT NULL +ORDER BY ts; + +-- Тесты: +SELECT * FROM raw_events; +SELECT * FROM mnoskov.mv_clean; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(0, '2024-07-17 10:00:00', 'click'), +(1, '2024-07-17 10:01:00', 'view'), +(2, '2024-07-17 10:02:00', NULL); + + +-- Задание 2 + +--raw_events +--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_events', + '{replica}' +) +ORDER BY ts; + +--click_events +--DROP TABLE mnoskov.click_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.click_events +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/click_events', + '{replica}' +) +ORDER BY ts; + +--login_events +--DROP TABLE mnoskov.login_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.login_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/login_events1', + '{replica}' +) +ORDER BY ts; + +--purchase_events +--DROP TABLE mnoskov.purchase_events ON CLUSTER clickhouse_cluster sync; +CREATE TABLE mnoskov.purchase_events1 +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + ts DateTime, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/purchase_events1', + '{replica}' +) +ORDER BY ts; + +--mv_click +--DROP VIEW mnoskov.mv_click ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_click +ON CLUSTER clickhouse_cluster +TO mnoskov.click_events +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'click' +ORDER BY ts; + +--mv_login +--DROP VIEW mnoskov.mv_login ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_login +ON CLUSTER clickhouse_cluster +TO mnoskov.login_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'login' +ORDER BY ts; + +--mv_purchase +--DROP VIEW mnoskov.mv_purchase ON CLUSTER clickhouse_cluster sync; +CREATE MATERIALIZED VIEW mnoskov.mv_purchase +ON CLUSTER clickhouse_cluster +TO mnoskov.purchase_events1 +AS +SELECT * FROM mnoskov.raw_events WHERE event_type = 'purchase' +ORDER BY ts; + + +-- Тесты: +SELECT * FROM mnoskov.click_events; +SELECT * FROM mnoskov.login_events1; +SELECT * FROM mnoskov.purchase_events1; + +TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync; +INSERT INTO mnoskov.raw_events +(user_id, ts, event_type) +VALUES +(1, '2024-07-17 10:00:00', 'click'), +(2, '2024-07-17 10:01:00', 'login'), +(3, '2024-07-17 10:02:00', 'purchase'); \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql new file mode 100644 index 0000000..6140c2f --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/1. Семейство MergeTree/practice_table_types.sql @@ -0,0 +1,120 @@ +-- Задание 1 + +CREATE TABLE mnoskov.page_views +ON CLUSTER clickhouse_cluster +( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + + +-- Задание 2 +CREATE TABLE mnoskov.users +ON CLUSTER clickhouse_cluster +( + id UInt64, + name String, + update_at DateTime +) +ENGINE = ReplacingMergeTree(update_at) +ORDER BY id; + + +-- Задание 3 +CREATE TABLE mnoskov.region_sales +ON CLUSTER clickhouse_cluster +( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO mnoskov.region_sales (region, date, sales) +VALUES + ('Moscow', '2026-05-01', 100), + ('Moscow', '2026-05-01', 150), + ('Moscow', '2026-05-01', 200), + ('Berlin', '2026-05-02', 300), + ('Berlin', '2026-05-02', 120), + ('Berlin', '2026-05-02', 80), + ('Paris', '2026-05-03', 500), + ('Paris', '2026-05-03', 250), + ('London', '2026-05-04', 400), + ('Tokyo', '2026-05-05', 700); + +SELECT * FROM mnoskov.region_sales; +-- Все четко работает. + + +-- Задание 4 +CREATE TABLE mnoskov.user_metrics +ON CLUSTER clickhouse_cluster +( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +CREATE TABLE mnoskov.user_metrics_source +ON CLUSTER clickhouse_cluster +( + region String, + age UInt8, + user_id UInt64 +) +ENGINE = MergeTree +ORDER BY region; + +INSERT INTO mnoskov.user_metrics_source +(region, age, user_id) +VALUES + ('Moscow', 25, 1001), + ('Moscow', 30, 1002), + ('Moscow', 22, 1003), + ('Moscow', 25, 1001), + ('Berlin', 40, 2001), + ('Berlin', 35, 2002), + ('Berlin', 29, 2003), + ('Berlin', 35, 2002), + ('Paris', 31, 3001), + ('Paris', 28, 3002), + ('Paris', 45, 3003), + ('Paris', 31, 3001), + ('London', 50, 4001), + ('London', 41, 4002), + ('London', 38, 4003), + ('London', 41, 4002), + ('Tokyo', 27, 5001), + ('Tokyo', 33, 5002), + ('Tokyo', 29, 5003), + ('Tokyo', 33, 5002); +SELECT * FROM mnoskov.user_metrics_source; + +INSERT INTO mnoskov.user_metrics +SELECT + region, + avgState(age) AS avg_age_state, + uniqState(user_id) AS active_users_state +FROM + mnoskov.user_metrics_source +GROUP BY + region; + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM + mnoskov.user_metrics +GROUP BY + region; +-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают. \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql new file mode 100644 index 0000000..2a7de23 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/1. Движки/2. View и Distribute движки/practice_view_and_distribute_engines.sql @@ -0,0 +1,86 @@ +-- Задание 1 + +CREATE TABLE mnoskov.logs +ON CLUSTER clickhouse_cluster ( + dt DateTime, + user_id UInt32, + event_type String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/logs', + '{replica}' +) +ORDER BY dt; + +CREATE TABLE mnoskov.dist_logs +ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'mnoskov', + 'logs', + user_id +); +-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке. + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY (ts, user_id); + +CREATE VIEW mnoskov.top_active_users AS +SELECT + user_id, + count(*) AS cnt_user +FROM + mnoskov.events +GROUP BY + user_id +ORDER BY + cnt_user DESC, user_id +LIMIT 100; +-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW. + + +-- Задание 3 + +CREATE TABLE mnoskov.raw_events +ON CLUSTER clickhouse_cluster +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE=MergeTree +ORDER BY(event_time,user_id); + +CREATE TABLE mnoskov.agr_clicks_per_day +ON CLUSTER clickhouse_cluster +( + event_date Date, + count UInt64 +) +ENGINE=SummingMergeTree +ORDER BY event_date; + +CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day +ON CLUSTER clickhouse_cluster +TO mnoskov.agr_clicks_per_day AS +SELECT + toDate(event_time) AS event_date, + count(*) AS count +FROM mnoskov.raw_events +GROUP BY event_date; + +SELECT * FROM mnoskov.raw_events; +SELECT * FROM mnoskov.agr_clicks_per_day; \ No newline at end of file diff --git a/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql new file mode 100644 index 0000000..b8d8218 --- /dev/null +++ b/mnoskov/clickhouse/практика clickhouse/1. Таблицы и типы данных/2. Типы данных/practice_data_types.sql @@ -0,0 +1,115 @@ +-- Задание 1 + +CREATE TABLE mnoskov.raw_data1 +ON CLUSTER clickhouse_cluster ( + user_id String, + balance String, + created_at String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/raw_data1', + '{replica}' +) +ORDER BY tuple(); + +SELECT + user_id, + balance, + created_at +FROM ( + SELECT + user_id, + toDecimal64OrNull(balance,2) AS balance, + toDateTime64OrNull(created_at, 3) AS created_at +FROM + mnoskov.raw_data1 +) +WHERE + balance IS NOT NULL AND created_at IS NOT NULL; + + +-- Задание 2 + +CREATE TABLE mnoskov.events +ON CLUSTER clickhouse_cluster ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/events', + '{replica}' +) +ORDER BY + event_id; + +SELECT * FROM mnoskov.events; + + +SELECT + event_id, + event_datetime +FROM ( + SELECT + event_id, + parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime + FROM + mnoskov.events +) +ORDER BY + event_datetime; + + +-- Задание 3 + +CREATE TABLE mnoskov.user_activity +ON CLUSTER clickhouse_cluster +( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/user_activity', + '{replica}' +) +ORDER BY + user_id; + +SELECT + user_id, + count(*) as user_actions_cnt +FROM + mnoskov.user_activity +WHERE + has(JSONExtract(actions, 'Array(String)'), 'purchase') +GROUP BY + user_id +ORDER BY + user_id; + + +-- Задание 4 + +CREATE TABLE mnoskov.sensor_data +ON CLUSTER clickhouse_cluster +( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/mnoskov/sensor_data', + '{replica}' +) +ORDER BY sensor_id; + +--1-2. +SELECT + toString(status) AS str_status, + avg(value) AS avg_value +FROM + mnoskov.sensor_data +GROUP BY + str_status; +--3 +-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0. \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql b/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql new file mode 100644 index 0000000..f542678 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_1_project_Initialization.sql @@ -0,0 +1,46 @@ +-- 1 +CREATE SCHEMA raw; +CREATE SCHEMA core; +CREATE SCHEMA dm; + +-- 2 +--DROP TABLE IF EXISTS raw.users; +CREATE TABLE raw.users ( + id bigint NOT NULL, + name varchar(100), + age SMALLINT, + email varchar(100), + registration_date timestamptz +); + +--DROP TABLE IF EXISTS raw.courses; +CREATE TABLE raw.courses ( + id bigint NOT NULL, + title varchar(150), + category varchar(50), + created_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lessons; +CREATE TABLE raw.lessons ( + id bigint NOT NULL, + course_id bigint, + title varchar(150), + duration_min SMALLINT +); + +--DROP TABLE IF EXISTS raw.enrollments; +CREATE TABLE raw.enrollments ( + id bigint NOT NULL, + user_id bigint, + course_id bigint, + enrolled_at timestamptz +); + +--DROP TABLE IF EXISTS raw.lesson_views; +CREATE TABLE raw.lesson_views ( + id bigint NOT NULL, + user_id bigint, + lesson_id bigint, + viewed_at timestamptz +); \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv new file mode 100644 index 0000000..d348d4a --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/courses_202604212117.csv @@ -0,0 +1,6 @@ +"id","title","category","created_at" +1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300 +2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300 +3,BI с нуля,business,2023-03-01 00:00:00.000 +0300 +4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300 +5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv new file mode 100644 index 0000000..e5c5610 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/enrollments_202604212130.csv @@ -0,0 +1,46 @@ +"id","user_id","course_id","enrolled_at" +1,1,1,2023-01-15 00:00:00.000 +0300 +2,1,2,2023-02-01 00:00:00.000 +0300 +3,2,1,2023-01-20 00:00:00.000 +0300 +4,3,2,2023-03-05 00:00:00.000 +0300 +5,4,3,2023-04-01 00:00:00.000 +0300 +6,6,1,2023-01-06 00:00:00.000 +0300 +7,6,2,2023-01-10 00:00:00.000 +0300 +8,6,4,2023-02-05 00:00:00.000 +0300 +9,7,1,2023-01-13 00:00:00.000 +0300 +10,7,3,2023-03-10 00:00:00.000 +0300 +11,8,1,2023-01-22 00:00:00.000 +0300 +12,8,2,2023-01-22 00:00:00.000 +0300 +13,8,3,2023-01-22 00:00:00.000 +0300 +14,9,4,2023-02-12 00:00:00.000 +0300 +15,10,1,2023-04-16 00:00:00.000 +0300 +16,11,2,2023-04-21 00:00:00.000 +0300 +17,12,1,2023-01-16 00:00:00.000 +0300 +18,13,2,2023-01-19 00:00:00.000 +0300 +19,13,4,2023-02-20 00:00:00.000 +0300 +20,14,1,2023-01-09 00:00:00.000 +0300 +21,14,2,2023-01-09 00:00:00.000 +0300 +22,14,3,2023-01-09 00:00:00.000 +0300 +23,14,4,2023-02-01 00:00:00.000 +0300 +24,14,5,2023-03-20 00:00:00.000 +0300 +25,15,1,2023-01-11 00:00:00.000 +0300 +26,15,3,2023-03-05 00:00:00.000 +0300 +27,16,2,2023-02-16 00:00:00.000 +0300 +28,16,4,2023-02-16 00:00:00.000 +0300 +29,17,3,2023-03-22 00:00:00.000 +0300 +30,17,5,2023-03-25 00:00:00.000 +0300 +31,18,1,2023-04-02 00:00:00.000 +0300 +32,19,1,2023-04-11 00:00:00.000 +0300 +33,19,2,2023-04-11 00:00:00.000 +0300 +34,20,2,2023-04-19 00:00:00.000 +0300 +35,5,5,2023-03-20 00:00:00.000 +0300 +36,1,4,2023-02-10 00:00:00.000 +0300 +37,2,3,2023-03-15 00:00:00.000 +0300 +38,3,1,2023-02-25 00:00:00.000 +0300 +39,4,2,2023-03-10 00:00:00.000 +0300 +40,6,3,2023-03-08 00:00:00.000 +0300 +41,7,2,2023-02-01 00:00:00.000 +0300 +42,9,1,2023-02-15 00:00:00.000 +0300 +43,12,3,2023-03-10 00:00:00.000 +0300 +44,15,2,2023-02-01 00:00:00.000 +0300 +45,18,4,2023-04-05 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv new file mode 100644 index 0000000..b09d250 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lesson_views_202604212133.csv @@ -0,0 +1,59 @@ +"id","user_id","lesson_id","viewed_at" +1,1,1,2023-01-16 10:00:00.000 +0300 +2,1,2,2023-01-16 10:15:00.000 +0300 +3,1,3,2023-01-17 09:00:00.000 +0300 +4,1,4,2023-01-17 09:30:00.000 +0300 +5,1,4,2023-01-17 11:00:00.000 +0300 +6,1,4,2023-01-18 10:00:00.000 +0300 +7,1,6,2023-02-02 14:00:00.000 +0300 +8,1,12,2023-02-15 10:00:00.000 +0300 +9,1,13,2023-02-15 10:30:00.000 +0300 +10,2,1,2023-01-21 09:00:00.000 +0300 +11,2,9,2023-03-16 10:00:00.000 +0300 +12,2,10,2023-03-16 10:30:00.000 +0300 +13,3,6,2023-03-06 12:00:00.000 +0300 +14,3,1,2023-02-26 11:00:00.000 +0300 +15,3,2,2023-02-26 11:20:00.000 +0300 +16,4,9,2023-04-02 10:00:00.000 +0300 +17,4,10,2023-04-03 10:00:00.000 +0300 +18,4,6,2023-03-12 14:00:00.000 +0300 +19,4,7,2023-03-13 09:00:00.000 +0300 +20,5,15,2023-03-21 10:00:00.000 +0300 +21,5,16,2023-03-22 11:00:00.000 +0300 +22,6,1,2023-01-07 10:00:00.000 +0300 +23,6,2,2023-01-07 10:15:00.000 +0300 +24,6,3,2023-01-08 09:00:00.000 +0300 +25,6,4,2023-01-08 09:30:00.000 +0300 +26,6,5,2023-01-09 10:00:00.000 +0300 +27,6,6,2023-01-11 14:00:00.000 +0300 +28,6,7,2023-01-12 10:00:00.000 +0300 +29,6,8,2023-01-13 11:00:00.000 +0300 +30,6,12,2023-02-06 09:00:00.000 +0300 +31,6,13,2023-02-06 09:30:00.000 +0300 +32,6,14,2023-02-07 10:00:00.000 +0300 +33,6,9,2023-03-09 11:00:00.000 +0300 +34,7,1,2023-01-14 10:00:00.000 +0300 +35,7,2,2023-01-14 10:20:00.000 +0300 +36,7,3,2023-01-15 09:00:00.000 +0300 +37,7,9,2023-03-11 14:00:00.000 +0300 +38,7,10,2023-03-11 14:30:00.000 +0300 +39,9,12,2023-02-13 10:00:00.000 +0300 +40,9,13,2023-02-13 10:30:00.000 +0300 +41,9,1,2023-02-16 11:00:00.000 +0300 +42,12,1,2023-01-17 10:00:00.000 +0300 +43,12,2,2023-01-17 10:20:00.000 +0300 +44,12,1,2023-04-10 11:00:00.000 +0300 +45,12,3,2023-04-10 11:30:00.000 +0300 +46,12,9,2023-03-12 10:00:00.000 +0300 +47,13,6,2023-01-20 14:00:00.000 +0300 +48,13,7,2023-01-21 10:00:00.000 +0300 +49,13,12,2023-02-21 09:00:00.000 +0300 +50,14,1,2023-01-09 08:00:00.000 +0300 +51,14,2,2023-01-09 08:15:00.000 +0300 +52,14,3,2023-01-10 10:00:00.000 +0300 +53,14,4,2023-01-10 10:30:00.000 +0300 +54,14,5,2023-01-11 09:00:00.000 +0300 +55,14,6,2023-01-10 14:00:00.000 +0300 +56,14,7,2023-01-11 10:00:00.000 +0300 +57,14,8,2023-01-12 11:00:00.000 +0300 +58,14,9,2023-01-10 08:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv new file mode 100644 index 0000000..54dbb22 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/lessons_202604212131.csv @@ -0,0 +1,19 @@ +"id","course_id","title","duration_min" +1,1,SELECT и FROM,10 +2,1,JOIN,15 +3,1,WHERE и фильтрация,12 +4,1,Агрегации и GROUP BY,18 +5,1,Подзапросы,25 +6,2,Pandas основы,20 +7,2,NumPy,15 +8,2,Визуализация данных,22 +9,3,Основы BI,12 +10,3,Метрики и дашборды,18 +11,3,Power BI введение,20 +12,4,Формулы в Excel,15 +13,4,Сводные таблицы,18 +14,4,Графики и диаграммы,14 +15,5,Введение в ML,20 +16,5,Линейная регрессия,30 +17,5,Классификация,28 +18,5,Кластеризация,25 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv new file mode 100644 index 0000000..d6aa0da --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_2_Loading Data/users_202604212132.csv @@ -0,0 +1,21 @@ +"id","name","age","email","registration_date" +1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300 +2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300 +3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300 +4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300 +5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300 +6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300 +7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300 +8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300 +9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300 +10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300 +11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300 +12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300 +13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300 +14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300 +15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300 +16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300 +17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300 +18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300 +19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300 +20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300 diff --git a/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql b/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql new file mode 100644 index 0000000..a804a5c --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_3_core_layer.sql @@ -0,0 +1,189 @@ +-- 4 +--DROP TABLE IF EXISTS core.dim_user; +CREATE TABLE core.dim_user ( + user_id bigint PRIMARY KEY, + name varchar(100), + age smallint, + email varchar(100) NOT NULL, + registration_date timestamptz NOT NULL +) DISTRIBUTED BY (user_id); + +--DROP TABLE IF EXISTS core.dim_course; +CREATE TABLE core.dim_course ( + course_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL +) DISTRIBUTED BY (course_id); + +--DROP TABLE IF EXISTS core.dim_lesson; +CREATE TABLE core.dim_lesson ( + lesson_id bigint PRIMARY KEY, + title varchar(150) NOT NULL, + duration_min smallint, + course_id bigint NOT NULL REFERENCES core.dim_course(course_id) +) DISTRIBUTED BY (lesson_id); +CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson(course_id); + +--DROP TABLE IF EXISTS core.dim_date; +CREATE TABLE core.dim_date ( + date date PRIMARY KEY, + year int, + month int, + day int, + weekday text +) DISTRIBUTED BY (date); + + +-- 5 +--DROP TABLE IF EXISTS core.fact_lesson_views; +CREATE TABLE core.fact_lesson_views ( + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + lesson_id bigint NOT NULL REFERENCES core.dim_lesson(lesson_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + viewed_at timestamptz NOT NULL +) DISTRIBUTED BY (user_id); +CREATE INDEX idx_fact_lesson_views_viewed_at ON core.fact_lesson_views(viewed_at); +CREATE INDEX idx_fact_lesson_views_user_id ON core.fact_lesson_views(user_id); +CREATE INDEX idx_fact_lesson_views_lesson_id ON core.fact_lesson_views(lesson_id); +CREATE INDEX idx_fact_lesson_views_course_id ON core.fact_lesson_views(course_id); + +--DROP TABLE IF EXISTS core.enrollments; +CREATE TABLE core.enrollments ( + user_id bigint NOT NULL REFERENCES core.dim_user(user_id), + course_id bigint NOT NULL REFERENCES core.dim_course(course_id), + enrolled_at timestamptz NOT NULL +) DISTRIBUTED BY (user_id); +CREATE INDEX idx_enrollments_user_id ON core.enrollments(user_id); +CREATE INDEX idx_enrollments_course_id ON core.enrollments(course_id); + + +--6 +-- dim_user +INSERT INTO core.dim_user (user_id, name, age, email, registration_date) +SELECT + raw_u.id, + raw_u.name, + raw_u.age, + raw_u.email, + raw_u.registration_date +FROM + raw.users raw_u +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id) + AND raw_u.email IS NOT NULL AND raw_u.registration_date IS NOT NULL; + +UPDATE core.dim_user core_u +SET + name = raw_u.name, + age = raw_u.age, + email = raw_u.email, + registration_date = raw_u.registration_date +FROM + raw.users raw_u +WHERE + core_u.user_id = raw_u.id + AND ( + core_u.name != raw_u.name OR + core_u.age != raw_u.age OR + core_u.email != raw_u.email OR + core_u.registration_date != raw_u.registration_date + ); + +-- dim_course +INSERT INTO core.dim_course (course_id, title, category, created_at) +SELECT + raw_c.id, + raw_c.title, + raw_c.category, + raw_c.created_at +FROM + raw.courses raw_c +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id) + AND raw_c.title IS NOT NULL AND raw_c.created_at IS NOT NULL; + +UPDATE core.dim_course core_c +SET + title = raw_c.title, + category = raw_c.category, + created_at = raw_c.created_at +FROM + raw.courses raw_c +WHERE + core_c.course_id = raw_c.id + AND ( + core_c.title != raw_c.title OR + core_c.category != raw_c.category OR + core_c.created_at != raw_c.created_at + ); + +-- dim_lesson +INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id) +SELECT + raw_l.id, + raw_l.title, + raw_l.duration_min, + raw_l.course_id +FROM + raw.lessons raw_l +WHERE + NOT EXISTS (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id) + AND raw_l.title IS NOT NULL AND raw_l.course_id IS NOT NULL; + +UPDATE core.dim_lesson core_l +SET + title = raw_l.title, + duration_min = raw_l.duration_min, + course_id = raw_l.course_id -- решил оставить, тк чисто теоретические же урок может начать принадлежать другому курсу +FROM + raw.lessons raw_l +WHERE + core_l.lesson_id = raw_l.id + AND ( + core_l.title != raw_l.title OR + core_l.duration_min != raw_l.duration_min OR + core_l.course_id != raw_l.course_id + ); + +-- загуглил как заполнять dim_date: +INSERT INTO core.dim_date (date, year, month, day, weekday) +SELECT + d::date, + EXTRACT(YEAR FROM d)::int, + EXTRACT(MONTH FROM d)::int, + EXTRACT(DAY FROM d)::int, + to_char(d, 'day') +FROM + generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d; + +-- fact_lesson_views +INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at) +SELECT + raw_lv.user_id, + raw_lv.lesson_id, + raw_l.course_id, + raw_lv.viewed_at +FROM + raw.lesson_views raw_lv +JOIN + raw.lessons raw_l ON raw_lv.lesson_id = raw_l.id +WHERE + raw_lv.viewed_at > (SELECT max(viewed_at) FROM core.fact_lesson_views) -- но при первой загрузке это не сработало, тк max(vied_at) = NULL, как это исправить - я не знаю + AND raw_lv.user_id IS NOT NULL AND raw_lv.lesson_id IS NOT NULL + AND raw_l.course_id IS NOT NULL AND raw_lv.viewed_at IS NOT NULL; + +-- enrollments +INSERT INTO core.enrollments (user_id, course_id, enrolled_at) +SELECT + raw_e.user_id, + raw_e.course_id, + raw_e.enrolled_at +FROM + raw.enrollments raw_e +WHERE + NOT EXISTS (SELECT 1 FROM core.enrollments core_e WHERE + core_e.user_id = raw_e.user_id + AND core_e.course_id = raw_e.course_id + AND core_e.enrolled_at = raw_e.enrolled_at) + AND raw_e.user_id IS NOT NULL AND raw_e.course_id IS NOT NULL AND raw_e.enrolled_at IS NOT NULL; \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql b/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql new file mode 100644 index 0000000..9c4dbb2 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_4_preparing_display_cases_at_greenplum.sql @@ -0,0 +1,99 @@ +-- 7 +--DROP MATERIALIZED VIEW dm.lesson_popularity_summary; +CREATE MATERIALIZED VIEW dm.lesson_popularity_summary AS +WITH results_fact_lesson_views AS ( + SELECT + flv.lesson_id, + count(flv.user_id) AS total_views, + count(DISTINCT flv.user_id) AS unique_users, + min(flv.viewed_at)::date AS first_view, + max(flv.viewed_at)::date AS last_view + FROM + core.fact_lesson_views flv + GROUP BY + flv.lesson_id +) +SELECT + dl.lesson_id, + dl.title AS lesson_title, + dl.course_id, + dc.title AS course_title, + rflv.total_views, + rflv.unique_users, + rflv.first_view, + rflv.last_view +FROM + core.dim_lesson dl +JOIN + core.dim_course dc ON dl.course_id = dc.course_id +LEFT JOIN + results_fact_lesson_views rflv ON dl.lesson_id = rflv.lesson_id; + + +-- 8 +--DROP MATERIALIZED VIEW dm.inactive_users_summary; +CREATE MATERIALIZED VIEW dm.inactive_users_summary AS +SELECT + du.user_id, + du.name AS user_name, + du.email, + du.age, + e.enrollments_count, + du.registration_date +FROM + core.dim_user du +LEFT JOIN ( + SELECT + user_id, + count(course_id) AS enrollments_count + FROM + core.enrollments + GROUP BY + user_id +) e ON du.user_id = e.user_id +WHERE + NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id); +-- интересно, что во время проверки плана запроса, когда кусок с enrollments_count был в CTE, план запроса выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу индексы заработали, и строки начали эффективней читаться + + +-- 9 +--DROP MATERIALIZED VIEW dm.course_completion_rate; +CREATE MATERIALIZED VIEW dm.course_completion_rate AS +WITH results_dim_course_lesson AS ( + SELECT + dc.course_id, + dc.title, + count(lesson_id) AS lessons_in_course + FROM + core.dim_course dc + JOIN + core.dim_lesson dl ON dc.course_id = dl.course_id + GROUP BY + dc.course_id, dc.title +), +results_fact_lesson_views AS ( + SELECT + flv.user_id, + flv.course_id, + count(DISTINCT lesson_id) AS lessons_viewed + FROM + core.fact_lesson_views flv + GROUP BY + flv.user_id, flv.course_id +) +SELECT + du.user_id, + du.name AS user_name, + rdcl.course_id, + rdcl.title AS course_title, + rdcl.lessons_in_course, + COALESCE(rflv.lessons_viewed, 0) AS lessons_viewed, + round(COALESCE(rflv.lessons_viewed, 0)::numeric / NULLIF(rdcl.lessons_in_course, 0), 2) AS completion_rate +FROM + core.dim_user du +JOIN + core.enrollments e ON du.user_id = e.user_id +JOIN + results_dim_course_lesson rdcl ON e.course_id = rdcl.course_id +LEFT JOIN + results_fact_lesson_views rflv ON e.user_id = rflv.user_id AND e.course_id = rflv.course_id; \ No newline at end of file diff --git a/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql b/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql new file mode 100644 index 0000000..f2d8cb9 --- /dev/null +++ b/mnoskov/greenplum/Пет проект DWH/Step_5_verification.sql @@ -0,0 +1,4 @@ +SELECT * FROM dm.lesson_popularity_summary ORDER BY lesson_id LIMIT 10; +SELECT * FROM dm.inactive_users_summary ORDER BY user_id LIMIT 10; +SELECT * FROM dm.course_completion_rate ORDER BY user_id, course_id; +--пришлось добавить order_by для красивого вывода, потому что создать витрины отсортированными не получается \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt b/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt new file mode 100644 index 0000000..b9f5fc6 --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_distributed_by.txt @@ -0,0 +1,9 @@ +Задание 1 + - запос не будет локальным + - гринплам будет делать data motion + +Задание 2 + - distributed by не подходит, тк таблица не будет учавствовать в join + +Задание 3 + - можно добавить event_id и распределять по нему, или же сделать destributed randomly \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_explain.txt b/mnoskov/greenplum/Практика greenplum/practice_explain.txt new file mode 100644 index 0000000..9393ecd --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_explain.txt @@ -0,0 +1,6 @@ +Задание 1 + - если orders DISTRIBUTED BY user_id, то будет 'Seq Scan on orders' -> 'HashAggregate'. Redistribute Motion(по user_id) пропадет, потому что все уже распределено по user_id; + +Задание 2 + - Это называется Skew. + - Чтобы избежать skew, нужно выбрать столбец для распределения с нормальной кардинальностью. Или делать destributed randomly если не нужно join. \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt b/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt new file mode 100644 index 0000000..5270ff7 --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_external_sources_and_integration.txt @@ -0,0 +1,10 @@ +Задание 1 + - данные физически находятся в Hive; + - писать обратно в Hive нельзя, тк таблица не writable; + - не знаю, в теорри это не сказано - думаю будет ошибка. После того как загуглил: + 1) если в hive добавились новые столбцы, то при чтении все будет отображаться так, как было во время создание внешней таблицы в greenplum, без ошибок; + 2) если в hive удалась часть столбцов, то возникнет ошибка тк, в greenplum по сути будут лишние столбцы. + +Задание 2 + - данные не копируются в greenplum, а только читаются из s3; + - я думаю в момент создания таблицы - быстрее external, тк ничего не копируется фищически, но при чтении должен быть быстрее CREATE TABLE AS SELECT, тк данные физически хранятся на нашем сервере, а не где-то в облаке. \ No newline at end of file diff --git a/mnoskov/greenplum/Практика greenplum/practice_requests.txt b/mnoskov/greenplum/Практика greenplum/practice_requests.txt new file mode 100644 index 0000000..4c4fbde --- /dev/null +++ b/mnoskov/greenplum/Практика greenplum/practice_requests.txt @@ -0,0 +1,17 @@ +Задание 1: +- Будут собираться все 10млн строк с разных сегментов, а тольок потом сработает LIMIT. Это очень дорого стоит, и медленно тк очень много данных в пустую соединятся в мастер. +- Я думаю можно к текущему запросу добавить DENSE_RANK по total и написать условие where -тут имя dense_rank- <= 10. +!!! В теории не сказано про работу order by - пришлось загуглить !!! + +Задание 2: +- PARTITION BY по. region_id, а должен быть по order_id, чтобы работало без задержек, а при текущем варианте запроса будет происходить data motion как я понял, а это лишние вычисления. +- Тогда надо распределять таблицу по region_id полагаю. + +Задание 3: +- Ответ до подсказки: думаю все будет нормально, данные просто сгруппируются и вставятся, никаких зарежек быть не должно. +- Ответ после подсказки: greenplum будет пересылать(data motion) user_id между сегментами, чтобы собрать их в группы, а только потом собирать в мастер и инсертить, инсерт уже поидее будет происходить хорошо, тк данные после data motion будут лежать нормально. +- Orders надо дестрибьютить по user_id, тогда все четко и быстро будет - и group by, и join. + +Задание 4: +- Я хочу увидеть Motion, Broadcast, Gather +- Что означает Broadcast Motion, Redistribute Motion, Gather Motion? - в душе ни чаю, там вопрос с ответом сразу, взял оттуда: Broadcast Motion - разослать копии каждой строки на все сегменты; Redistribute Motion - перераспределить по новому ключу; Gather Motion - собрать на мастер. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql new file mode 100644 index 0000000..676ee2f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DDL_create_first_tables.sql @@ -0,0 +1,47 @@ +-- ПРАКТИКА DDL +-- Задание 1 +DROP TABLE IF EXISTS mnoskov.users; +CREATE TABLE mnoskov.users ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + country char(2), -- прогуглил что распространено сохранять iso-код а не все название + is_active boolean, + created_at timestamptz NOT NULL -- думаю timestamptz, если не учитывать часовой пояс, то даты могут отображаться неверно +); + + +-- Задание 2 +DROP TABLE IF EXISTS mnoskov.products; +CREATE TABLE mnoskov.products ( + id bigint PRIMARY KEY, + name varchar(100) NOT NULL, + category varchar(50), + price decimal(10,2) NOT NULL, + in_stock boolean +); + + +-- Задание 3 +DROP TABLE IF EXISTS mnoskov.orders; +CREATE TABLE mnoskov.orders ( + id bigint PRIMARY KEY, + user_id bigint, + order_date timestamptz NOT NULL, -- в названии date, и тип думаю date + status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar + FOREIGN KEY (user_id) REFERENCES users(id) +); + + +-- Задание 4 +DROP TABLE IF EXISTS mnoskov.order_items; +CREATE TABLE mnoskov.order_items ( + order_id bigint, + product_id bigint, + quantity int, -- думал надо smallint но потом подумал что вдруг 50к гвоздей + price_at_purchase decimal(10,2), + PRIMARY KEY (order_id, product_id), + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql new file mode 100644 index 0000000..7c3e15f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/1. Создание_и_изменение_таблиц_и_данных/DML_first_inserts_in_tables.sql @@ -0,0 +1,50 @@ +-- Задание 1 +INSERT INTO mnoskov.users (id, name, age, email, country, is_active, created_at) +VALUES +(1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'), +(2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'), +(3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'), +(4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'), +(5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'), +(6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'), +(7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'), +(8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'), +(9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'), +(10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00'); + +-- Задание 2 +INSERT INTO mnoskov.products (id, name, category, price, in_stock) +VALUES +(1, 'iPhone 15', 'Электроника', 80000, true), +(2, 'Kindle', 'Электроника', 15000, true), +(3, 'Кроссовки Nike', 'Одежда', 9000, true), +(4, 'Кроссовки NoName', 'Одежда', 3000, false), +(5, 'Наушники Pro', 'Электроника', 12000, true), +(6, 'Фитнес-браслет', 'Гаджеты', 6000, true), +(7, 'Подарочная карта', 'Сервисы', 1000, true); + +-- Задание 3 +INSERT INTO mnoskov.orders (id, user_id, order_date, status) +VALUES +(1, 1, '2025-01-10 12:00:00', 'paid'), +(2, 3, '2025-01-12 09:30:00', 'cancelled'), +(3, 1, '2025-01-15 18:20:00', 'paid'), +(4, 2, '2025-01-20 11:05:00', 'created'), +(5, 4, '2025-02-01 17:45:00', 'paid'), +(6, 5, '2025-02-10 10:10:00', 'refunded'), +(7, 7, '2025-03-01 09:00:00', 'paid'), +(8, 9, '2025-03-05 19:30:00', 'paid'); + +-- Задание 4 +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) +VALUES +(1, 1, 1, 78000), +(1, 3, 2, 8500), +(2, 2, 1, 14000), +(3, 5, 1, 11000), +(3, 7, 1, 1000), +(4, 2, 1, 15000), +(5, 6, 1, 6000), +(6, 1, 1, 82000), +(7, 3, 1, 9000), +(8, 4, 2, 2800); \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql new file mode 100644 index 0000000..641bfa7 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/2. Базовый_SQL/DQL_basic_SQL_practice.sql @@ -0,0 +1,59 @@ +-- Задание 1 +SELECT + id, + name, + country, + created_at, + email +FROM mnoskov.users +ORDER BY created_at DESC, name; +-- выбрал эти столбцы потому что так сказано в задании, я бы добавил ещё is_active +-- такая сортировка для того, чтобы даты шли от новых к старым и имена людей за одну дату шли по алфавиту + + +-- Задание 2 +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'US' +ORDER BY name; + +SELECT + id, + name, + country +FROM mnoskov.users +WHERE country = 'UK' +ORDER BY name; +-- для маркетолого полезнее первый запрос т.к. там больше данных + + +-- Задание 3 +SELECT + id, + name, + email +FROM mnoskov.users +WHERE email IS NOT NULL +ORDER BY name; + +SELECT DISTINCT + lower(email) AS email_lower +FROM mnoskov.users +WHERE email IS NOT NULL; +-- приводить email к одному регистру мб для красоты, вроде везде email принимается и в нижем и в верхнем регистре +-- дубликаты убираем чтобы вытрина занимала меньше места, и чтобы не было повторных рассылок + + +-- Задание 4 +SELECT + id, + name, + created_at +FROM mnoskov.users +WHERE created_at > '2024-02-20 23:59' +ORDER BY created_at DESC +LIMIT 2 OFFSET 1; +-- OFFSET может пригодиться, чтобы взять интервал данных \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql b/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql new file mode 100644 index 0000000..df898a7 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/1. Основы/3. Фильтрация/DQL_filtration_practice.sql @@ -0,0 +1,78 @@ +-- Задание 1 +SELECT + name, + category, + price, + price * 1.30 AS price_with_markup, + price * 1.50 AS price_with_vat +FROM + mnoskov.products +WHERE + in_stock = TRUE AND + category != 'smartwatch' AND -- категории smartwatch нет в предоставленных данных для заполнения + price BETWEEN 100 AND 1300; +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Лучше читается первый вариант, потому что просто прочитал и понял, а не думаешь над знаками, и первый вариант короче + + +-- Задание 2 +SELECT + name, + price, + price * 0.85 AS price_with_discount +FROM + mnoskov.products +WHERE + (price < 100 OR price > 1000); +-- Нужно отделять OR скобками, потому что если в условии будет ещё и AND, то логика может поломаться потому что AND выполняется раньше чем OR + + +-- Задание 3 +SELECT + id, + name, + category, + price +FROM + mnoskov.products +WHERE + category IN ('tablet', 'smartphone', 'mouse') AND + price >= 100 +ORDER BY + price DESC; + + +--Задание 4 +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name NOT LIKE '%iPhone%'; + + +-- Задание 5 +-- 1) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%pro%'; + +-- 2) +SELECT + name, + category, + price +FROM + mnoskov.products +WHERE + name ILIKE '%mx%' +ORDER BY + price DESC; +-- Для поисковой строи лучше ILIKE, потому что он не учитывает регистр - кто-то пишет с большой буквы, кто-то с маленькой, а кто-то случайно включил капс. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql b/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql new file mode 100644 index 0000000..639da44 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/2. JOIN/DQL_join_practice.sql @@ -0,0 +1,188 @@ +-- Задание 1 +SELECT + o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.users u ON o.user_id = u.id; +-- Логичнее начать с orders, тк надо вывести пользователей по заказам, а не наоборот. +-- Здесть уместнее LEFT, потому что в условии сказано ВСЕ заказы и уже по ним пользователей. Попробовал и INNER и LEFT - вывод один и тот же. + + +-- Задание 2 +SELECT + oi.order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +WHERE + oi.order_id = 1; -- если 101, как в условии то ничего не будет +-- Если фильтрацию поставить после ON, то выведутся все продукты по 2 раза, у всех id=1, quantity и price возьмутсы из двух строк в order_items где id=1. + + +-- Задание 3 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Заказ с id=3 повторяется два раза, потому что в заказе два товара, и они отображаются на двух строках. +-- Поскольку мы выводис все заказы, у нас больше строк по сравнению с заданием-2, и ещё мы выводим имена заказчиков. + + +--Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.status +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id; +-- Этот запрос отличается от INNER тем, что тут отображаются ещё и пользователи без заказов. + + +-- Задание 5 +SELECT + u.id AS user_id, + u.name AS user_name +FROM + mnoskov.users u +LEFT JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.user_id IS NULL; +-- Важно фильтровать по колонке из orders, потому что она будет NULL из-за LEFT JOIN. Если есть несостыковки, то NULL будет в правой таблице после LEFT JOIN. + + +-- Задание 6 +SELECT + oi.order_id, + u.id AS user_id, + p.name AS product_name, + oi.quantity +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + p.name LIKE 'Kindle'; +-- Заказ 2 встречается 1 раз, потому что там всего один товар, и потому что мы ищем именно товар с названием - Kindle. + + +-- Задание 7 +SELECT + p.name AS product_name, + oi.order_id, + oi.quantity +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id; +-- Если товар не покупали то в order_id будет NULL +-- Если использовать INNER, то выведутся только товары у которых есть заказы, в случае с LEFT выведутся все заказы. + + +-- Задание 8 +SELECT + oi.order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +JOIN + mnoskov.orders o ON oi.order_id = o.id +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE + o.status = 'paid'; +-- Если написать status = 'paid' после ON в orders, то ничего не поменяется, потому что после ON пишутся условия соединения таблиц также как и в WHERE, но как я понял лучше писать в WHERE. + + +-- Задание 9 +SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice'; +-- Если у Alice не будет заказов то ничего не выведется. +-- Мы получается берем Alice -> идем до id товаров Alice -> идем обратно к заказам и выводим товары которые были у Alice. + +-- Мне подсказала нейронка как использовать SELF JOIN - я понимал что он нужен, но не понимал как именно им воспользоваться. +-- Только после такого запроса я разобрался как это работает: +/*SELECT + oi.order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase, + u2.name, + u2.id +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +JOIN + mnoskov.order_items oi2 ON oi.product_id = oi2.product_id +JOIN + mnoskov.orders o2 ON oi2.order_id = o2.id +JOIN + mnoskov.users u2 ON o2.user_id = u2.id +JOIN + mnoskov.products p ON oi2.product_id = p.id +WHERE + u.name = 'Alice';*/ + + +-- Задание 10 +SELECT + o.id AS order_id, + o.user_id, + o.status +FROM + mnoskov.orders o +LEFT JOIN + mnoskov.order_items oi ON o.id = oi.order_id; +-- Если написать INNER JOIN, то появятся только существующие заказы, которые есть в обеих таблицах. \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql b/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql new file mode 100644 index 0000000..b284f5e --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/3. UNION/DQL_union_practice.sql @@ -0,0 +1,75 @@ +-- Задание 1 +SELECT + id, + name, + 'active' AS user_status +FROM + mnoskov.users +WHERE + is_active = TRUE + +UNION ALL + +SELECT + id, + name, + 'inactive' AS user_status +FROM + mnoskov.users +WHERE + is_active = FALSE +ORDER BY name; +-- Если написать просто UNION, то в данном задании ничего не изменится + + +-- Задание 2 +SELECT + id AS order_id, + user_id, + order_date, + 'old' AS period +FROM + mnoskov.orders +WHERE + order_date < '2025-02-01' + +UNION ALL + +SELECT + id AS order_id, + user_id, + order_date, + 'new' AS period +FROM + mnoskov.orders +WHERE + order_date >= '2025-02-01' +ORDER BY + order_date, order_id; +-- По колонке period я пойму старый(old) заказ или новый(new) +-- Я не могу ответить, потому что CASE WHEN ещё не проходили + + +-- Задание 3 +SELECT DISTINCT + oi.product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id + +UNION ALL + +SELECT DISTINCT + p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM + mnoskov.products p +LEFT JOIN + mnoskov.order_items oi ON p.id = oi.product_id +WHERE oi.product_id is NULL; +-- Проверка IS NULL по полю из order_items, потому что это права таблица, и null будут добавляться в неё +-- 1) Понизить цену товара 2) лучше прорекламировать 3) снять с продажи \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql b/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql new file mode 100644 index 0000000..1058267 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/4. Агрегации и группировки/DQL_aggregations&groupings_practice.sql @@ -0,0 +1,130 @@ +-- Задание 1 +SELECT + category, + count(*) products_count +FROM + mnoskov.products +GROUP BY + category +ORDER BY + products_count; +-- Если убрать order by, то статистика будет в случайном разрбосанном виде, а не по возрастанию +-- Увеличится products_count на одно значение + + +-- Задание 2 +SELECT + order_id, + sum(quantity) AS total_quantity, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +ORDER BY + order_revenue DESC; +-- Если убрать GROUP BY, то будет ошибка +-- Нельзя просто добавить в SELECT ещё одну колонку (например, product_id) без добавления её в GROUP BY или агрегат, потому что вылезет ошибка. Ну и колонка должна быть либо сгруппирована либо в агрегате, она не может существовать сама по себе в запросе с grouup by + + +-- Задание 3 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +ORDER BY + category_revenue DESC; +-- Группировка по p.category - потому что мы хотим узнать сумму прибли по каждой категории +-- Если добавить p.name - будет ошибка потому что p.name тоже нужно будет заносить в GROUP BY + + +-- Задание 4 +SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + oi.order_id +ORDER BY + order_revenue DESC; +-- В задаче-2 были все заказы, а тут только оплаченные +-- Я бы смотрел толлько на paid, чтобы понять прибыль которая уже есть + + +-- Задание 5 +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.category +HAVING + sum(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY + category_revenue DESC; +-- Если добавить это условие в WHERE, то терминал будет ругаться мол агрегаты недопустимы в WHERE +-- Обычно условие(прим: WHERE price > 1000) отличается от агрегированного условия(прим: HAVING SUM(...) > 1000) тем, что в первом случае ничего считать не нужно, значение уже есть, а во втором нужно сначала чтобы функция все посчитала. Ну и HAVING запускается уже после группировки + + +-- Задание 6 +SELECT + country, + count(*) AS active_user_count +FROM + mnoskov.users +WHERE + is_active = true +GROUP BY + country +HAVING + count(*) >= 2 +ORDER BY + active_user_count DESC; +-- is_active = true логичнее писать в WHERE, потому что is_active - не агрегат +-- Если нет страны где больше двух активных юзеров, то ничего не выведется + + +-- Задание 7 +SELECT + p.name AS product_name, + sum(oi.quantity) AS total_quantity_sold +FROM + mnoskov.order_items oi +JOIN + mnoskov.products p ON oi.product_id = p.id +GROUP BY + p.name +ORDER BY + total_quantity_sold DESC +LIMIT 3; +-- total_quantity_sold - количество проданного товара, а product_revenue - выручка каждого товара +-- Бизнес сморит на количество, может быть когда товара очень много и нужно понять какой % уже продан, и когда нужно найти популярные или непопулярные товары + + +-- Задание 8 +SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue +FROM + mnoskov.order_items +GROUP BY + order_id +HAVING + sum(quantity * price_at_purchase) < 10000 +ORDER BY + order_revenue; +-- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? Ответ: «доставка пробников/тестовой партии», «использование купонов/бонусов», «технический тест витрины» +-- Проще увидеть глазами отфильтрованные микро-заказы \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql new file mode 100644 index 0000000..30d30fb --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/5. Функции даты, строки, логика/DQL_functions_practice.sql @@ -0,0 +1,90 @@ +-- Задание 1 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration +FROM + mnoskov.users +WHERE + is_active = TRUE + AND current_date - created_at::date > 180 +ORDER BY + days_since_registration desc; +-- Очень старым пользователям можно кинуть рассылку на почту на подобии 'Представляете, мы вместе уже 777 дней, вот вам купон на скидку' +-- Удобнее вычитать ::date, чтобы days_since_registration красиво выглядел в таблице + + +-- Задание 2 +SELECT + name, + lower(email) AS email_clean, + substring(email FROM POSITION('@' IN email) + 1) AS domain +FROM + mnoskov.users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- email приводят к нижнему регистру для нормализации записи, а зачем выделяют домен не понимаю, может быть для аналитики по типу кто какой почтой пользуется +-- Получу NULL в domain + + +-- Задание 3 +SELECT + name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + END AS age_group +FROM + mnoskov.users +ORDER BY + age; +-- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? В каком коде? Если в 'коде приложения' - это какая-то отдельная программа, то могут предположить, чтобы передавать в приложение уже отфильтрованные данные, чтобы задачи базы данных не выносить из базы. +-- Если забыть в ветку ELSE, то будет NULL у тех кто никуда не попал + + +-- Задание 4 +SELECT + name, + created_at::date, + current_date - created_at::date AS days_since_registration, + CASE + WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE NULL + END AS hot_flag +FROM + mnoskov.users +WHERE + created_at >= current_date - INTERVAL '30 days'; +-- Фильтрация по дате в WHERE исключает неподходящие варианты, а простой CASE в нашем случае возвращает результат в виде двух вариантов - подошло/не подошло и эти оба варианта остаются в таблице +-- Стоит запускать какие нибудь привлекательные варианты со скидкам и акциями, чтобы удержать пользователся + + +-- Задание 5 +SELECT + count(*) AS total_users, + count(email) AS with_email, + count(*) - count(email) AS without_email, + ((count(*) - count(email))::float / count(*)) * 100.0 AS percent_without_email +FROM + mnoskov.users; +-- Агрегаты дают более точную информацию чем просто на глаз +-- Я бы предолжил запустить активно вылезающие блоки с предложением зарегестрироваться, или же запретить пользоваение продуктом без регистрации + + +-- Задание 6 +SELECT + name, + current_date - created_at::date AS days_since_registraion, + CASE + WHEN current_date - created_at::date <= 30 THEN 'new' + WHEN current_date - created_at::date BETWEEN 31 AND 180 THEN 'middle' + WHEN current_date - created_at::date > 180 THEN 'old' + END AS tenure_group, + is_active +FROM + mnoskov.users +ORDER BY + tenure_group, days_since_registraion; +-- Сегментация по стажу говорит о опыте человека, а по возрасту только о возрасте +-- Пересчитывать каждый день - плюсы: не занимает места на диске, минусы: тратит больше вычислительных ресурсов, а если хранить статическое поле, то все наоборот \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql b/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql new file mode 100644 index 0000000..cb6ad44 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/6. Подзапросы и CTE/DQL_cte_practice.sql @@ -0,0 +1,155 @@ +-- Задание 1 +SELECT + name +FROM + mnoskov.users +WHERE id IN ( + SELECT user_id + FROM mnoskov.orders +) +ORDER BY + name; +-- Тут не приходится делать JOIN и DISTINCT, а просто ищем айдишник в списке +-- N (подзапрос) может быть более читаемым, когда подзапрос очень простой и короткий + + +-- Задание 2 +SELECT + u.id, + u.name, + u.email +FROM + mnoskov.users u +WHERE NOT EXISTS ( + SELECT user_id + FROM mnoskov.orders o + WHERE + u.id = o.user_id +) +-- NOT EXISTS безопаснее в плане NULL, и часто планируется лучше чем NOT IN +-- Определить какое количество людей не делают заказы, и кинуть им например купон на скидку на первый заказ + + +-- Задание 3 +SELECT + u.name, + ( + SELECT + count(*) + FROM + mnoskov.orders o + WHERE + u.id = o.user_id + ) AS orders_count +FROM + mnoskov.users u +ORDER BY + orders_count DESC, u.name; +-- Подзапрос в SELECT не может возвращать несколько строк, потому что SELECT ожидает толбко одну +-- Как изменится сложность запроса, если пользователей станет, например, 1 млн? Не совсем понял этот вопрос, но думаю запрос просто будет выполняться дольше + + +-- Задание 4 +WITH revenue_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name, + ru.total_revenue +FROM + mnoskov.users u +JOIN + revenue_user ru ON u.id = ru.user_id +ORDER BY + ru.total_revenue desc; +-- Проще читать когда шаг «посчитать выручку по пользователям» вынесен отдельно +-- Чтобы избежать дублирования импользуется CTE, а не простой подзапрос + + +-- Задание 5 +WITH revenue_product AS ( + SELECT + product_id, + sum(quantity * price_at_purchase) AS product_revenue + FROM + mnoskov.order_items + GROUP BY + product_id +) +SELECT + p.name, + p.category, + rp.product_revenue +FROM + mnoskov.products p +JOIN + revenue_product rp ON p.id = rp.product_id +ORDER BY + rp.product_revenue +LIMIT 3; +-- Сначала удобнее считать агрегаты, чтобы код не выгляжел грооздким и чтобы в нем было легче разобраться +-- Отличае в удобстве чтения и написания. И этот агрегат можно потом ещё и переиспользовать + + +-- Задание 6 +WITH revenue_user AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id +) +SELECT + u.name, + u.email, + ru.total_revenue +FROM + mnoskov.users u +JOIN + revenue_user ru ON u.id = ru.user_id +WHERE + u.is_active = TRUE AND + ru.total_revenue > 10000 +ORDER BY + ru.total_revenue DESC, u.name; +-- Я бы назвал этот сегмегт - frequent_buyers +-- Таким пользователям надо предлагать какую ниюудь специальную программу лояльности + + +-- Задание 7 +WITH revenue_order AS ( + SELECT + order_id, + sum(quantity * price_at_purchase) AS order_revenue + FROM + mnoskov.order_items + GROUP BY + order_id +) +SELECT + ro.order_id, + o.order_date::date, + ro.order_revenue +FROM + mnoskov.orders o +JOIN + revenue_order ro ON o.id = ro.order_id +WHERE + o.status = 'paid' AND + ro.order_revenue < 10000 +ORDER BY + ro.order_revenue; +-- Причины: злоупотребление промокодами, ошибки +-- По 'paid' удобнее фильтровать снаржуи, потому тут мы подключаем orders, а подключать orders ещё и внутри было бы бессмысленно \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql b/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql new file mode 100644 index 0000000..4d582be --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/7. Оконные функции/DQL_window_functions_practice.sql @@ -0,0 +1,168 @@ +-- Задание 1 +SELECT + o.user_id, + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + row_number() OVER (PARTITION BY o.user_id ORDER BY o.order_date) AS order_number_for_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Отличае от обычного order by в том, что мы не просто сортируем, а сортируем в пределах одного пользователя, и ещё выводим нумерацию заказов + + +-- Задание 2 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.order_items oi + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + otr.user_id, + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + sum(otr.order_revenue) OVER (PARTITION BY otr.user_id ORDER BY otr.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_revenue_for_user +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- Если не задвать рамку, то могут пояиться проблемы с дубликатами +-- Если накопительная выручка пользователя становится больше, значит он начинает тратить больше + + +-- Задание 3 +WITH user_total_revenue AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id +) +SELECT + u.name AS user_name, + utr.total_revenue, + rank() OVER (ORDER BY utr.total_revenue) AS revenue_rank, + dense_rank() OVER (ORDER BY utr.total_revenue) AS revenue_dense_rank +FROM + mnoskov.users u +JOIN + user_total_revenue utr ON u.id = utr.user_id; +-- Когда появляется одинаковая выручка, dense_rank нумерует по порядку, без пропусков(1,2,2,3), а rank по 'расположению'(1,2,2,4) + + +-- Задание 4 +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date::date, + first_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date) AS first_order_date, + last_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order_date +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Первый заказ можно использовать для отчета 'как давно клиент начал активничать', а последний заказ, например если давно был последний заказ, то кинуть рассылку с промокодом, чтобы клиент снова начала заказывать как вариант +-- Если не задать рамку, то дата последнего заказа будет равна текущей строке, а не последней + + +-- Задание 5 +SELECT + u.name AS user_name, + o.id AS order_id, + count(*) OVER (PARTITION BY u.id) AS orders_per_user +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id; +-- Если использовать просто group by, то пользователи сожмутся в одну ячейку + + +-- Задание 6 +WITH order_total_revenue AS ( + SELECT + o.user_id, + oi.order_id, + o.order_date::date, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + mnoskov.orders o + JOIN + mnoskov.order_items oi ON o.id = oi.order_id + GROUP BY + o.user_id, oi.order_id, o.order_date +) +SELECT + u.name AS user_name, + otr.order_id, + otr.order_date::date, + otr.order_revenue, + lag(otr.order_revenue) OVER (PARTITION BY u.id ORDER BY otr.order_date) AS prev_order_revenue +FROM + mnoskov.users u +JOIN + order_total_revenue otr ON u.id = otr.user_id; +-- если текущее значение среднего чека больше чем предидущие, то увеличивается, а если наоборот, то уменьшается + + +-- Задание 7 +-- По месяцам: +WITH category_total_revenue AS ( + SELECT + p.category, + date_trunc('month', o.order_date)::date AS order_date_month, + sum(oi.quantity * oi.price_at_purchase) AS monthly_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date_month +) +SELECT + category, + order_date_month, + monthly_category_revenue, + sum(monthly_category_revenue) OVER (PARTITION BY category ORDER BY order_date_month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; + +-- По дням: +WITH category_total_revenue AS ( + SELECT + p.category, + o.order_date::date AS order_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + mnoskov.products p + JOIN + mnoskov.order_items oi ON p.id = oi.product_id + JOIN + mnoskov.orders o ON oi.order_id = o.id + GROUP BY + p.category, order_date +) +SELECT + category, + order_date, + daily_category_revenue, + sum(daily_category_revenue) OVER (PARTITION BY category ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue +FROM + category_total_revenue; +-- По такому графику мы можем видеть РОСТ выручки по дням например, а не просто сколько в какой день принесла категория \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql b/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql new file mode 100644 index 0000000..2e53170 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/8. Представления и витрины данных/DQL_views_practice.sql @@ -0,0 +1,66 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- удобнее иметь wiev, чтобы небыло дублирования кода, и чтобы легко обращаться к запросу не переписывая все заново + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- Чем больше у пользователя заказов тем он более 'лояльный' +-- Тогда будет отображаться статистика только за последний месяц - 30 дней + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Можно построить график, и видеть пики с просадками +-- Быстрее считать выручку из mv_daily_revenue, тк витрина уже готова, и её не нужно высчитывать + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- 1. интернет‑магазин с несколькими заказами в день - каждые сутки в 8 утра например +-- 2. крупный магазин с тысячами заказов в час - каждые 30 минут +-- Слишком редкий REFRESH сделает витрину бесполезной для аналитиков +-- Обновлять каждую секунжу нельзя, тк это тратит ресурсы компьютера, и тогда уж проще просто VIEW сделать, и вызывать когда надо \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql new file mode 100644 index 0000000..c4a8939 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/1. Транзакции/DQL_transactions_practice.sql @@ -0,0 +1,26 @@ +-- Задание 1. Оформление закзаза +INSERT INTO mnoskov.orders (id, user_id, order_date, status) VALUES + (9, 10, '2026-03-13', 'paid'), + (10, 10, '2026-03-14', 'paid'); + +INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES + (9, 3, 2, 88000), + (10, 6, 1, 5900); + +UPDATE mnoskov.products +SET in_stock = FALSE +WHERE id IN(3, 6); +-- Если выполнятся не все шаги, то транзакция откатится и все останется как было +-- Потому что заказ если оформляется, то полностью, частично он выполняться не должен + + +-- Задание 2 +-- При таком сценарии все должно откатаиться к самому началу, ничего не сохранится +-- Если не делать rollback, то можно получить заказы без продуктов, или без стомости, или полностью оформленный заказ, но продуктов нет в наличии +-- Любые 'битые' заказы повлияют на отчеты и витрины + + +-- Задание 3 +-- При изменении цены товара в products, это действие могут начать выполнять два человека одновременное, и если это не будет транзакцией то получится непонятно что, а если как транзакция, то сначала запишется 1 результат, а потом 2, и не будет никаких ошибок +-- Если выполняетя как транзакция, то просто откатится назад и все будет супер +-- Тк база сама откатывает транзакции в случае надобности, это освобождает инженера от доли ответственности \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql new file mode 100644 index 0000000..1cf1cbd --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/2. Блокировки/DQL_blocking_prectice.sql @@ -0,0 +1,21 @@ +-- Задание 1 +SELECT * FROM mnoskov.users WHERE id = 1; +-- Если две одновременные сессии выполнят этот запрос, то в обеих все выведется, и ошибок или очереди не бкдет +-- Потому что этот запрос только на чтение + + +-- Задание 2 +-- Первая сессия будет просто ждать окончания выполнения первой +-- Если разрешить обоимUPDATE пройти одновременно, то получится какая-то херня пологаю, они смешаются, или че-то такое +-- У Alice в итоге останется второй email, тк он выполнится последним. Порядок COMMIT важен, тк в последствии этого порядка будут выполняться запросы + + +-- Задание 3 +-- select выполнится тк просто читает, а update встанет в очередь, тк он попытается изменить 'забронированные данные' +-- FOR UPDATE помогает избежать ошибок когда происходит покупка товара или че то такое, когда идет процесс изменений в данных + + +-- Задание 4 +-- Дедлок возникает потому что каждая сессия заблокировал то, что нужно другой сессии. База отменит одну из сессий, чтобы прошла хоть какая-то +-- Чтобы дедлок не возникал, нужно обновления все производить в одном порядке +-- Если при массовых обновлениях не соблюдать порядок строк, то будет возникать жопа из дедлоков \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql new file mode 100644 index 0000000..ec5086f --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/3. Оптимизатор запросов/DQL_optimizer_pracitce.sql @@ -0,0 +1,165 @@ +-- Задание 1 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE country = 'RU' AND age > 25; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT name, age +FROM mnoskov.users +WHERE age > 25 AND country = 'RU'; +-- Выводят они один и тот же результат, потому что условия у них одинаковые +-- По скорости выполнения судя по планировщику тоже одинаковоы + + +-- Задание 2 +-- Вариант 1 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE o.status = 'refunded'; + +-- Вариант 2 +EXPLAIN ANALYZE +SELECT + u.name, + u.email, + o.order_date::date, + o.status +FROM + mnoskov.orders o +JOIN + mnoskov.users u ON o.user_id = u.id +WHERE o.status = 'refunded'; +-- Между первой и второй таблицей результаты одинаковые. Если в запросе нет никаких условий связанных с таблицами, то первой оптимизатор смотреть users, но если если условие по orders.status, то сначала он сканирует orders + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Тип сканирования: Seq Scan +-- Количество прочитанных строк: 1 +-- Время выполнения примерно: 0.023 ms(+-5) + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.users +WHERE country = 'RU'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, email +FROM mnoskov.users +WHERE country = 'RU'; +-- Поскольку данных в таблице мало у меня время выполнения одинаковое, но могу предположить, что второй выполнился быстрее, тк у него в селекте два поля, а не вся таблица + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.id, o.order_date +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- Читать начала с users, тк мы в where пишем условие для users, и быстрее сначала отсеять ненужных юзеров + + +-- Задание 6 +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND LOWER(u.name) LIKE '%алекс%' +GROUP BY u.name; +-- Его замедляет LOWER(не надо использовать функции), и думаю что LIKE(лучше использовать просто =), и ещэ group by по имени - это дольше чем по первичному ключу(надо группировать по ПК) + + +-- Задание 7 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- users -> orders -> order_items -> products - читает в таком порядке, потому что в where условие по users +-- Если убрать where, то будет - order_items -> users -> orders -> products +-- Как ускорить не знаю, может индексов добавть, но это пока не проходил + +-- Задание 8 +-- Вариант А +EXPLAIN ANALYZE +SELECT name, price +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; + +-- Вариант Б +EXPLAIN ANALYZE +SELECT * +FROM mnoskov.products +WHERE category = 'Электроника' + AND price > 50000; +-- Данных в таблицах мало, врем выполнения не оценить, но смотря просто со стороны понятно что выриант А будет быстрее, потому что у него пару колонок в селекте, а не вся таблица как в Б + + +-- Задание 9 +-- Вариант коллег +SELECT * +FROM users +WHERE EXTRACT(YEAR FROM created_at) = 2026 + AND LOWER(country) = 'россия' + AND age + 10 > 35; + +-- Мой вариант +SELECT + id, name, age, is_active +FROM + mnoskov.users +WHERE + created_at BETWEEN '2026-01-01' AND '2027-01-02' AND + country = 'RU' AND + age > 45; +-- Мой вариант лучше потому что: +-- нет функций и мат. вычислений в where, все приведено к самому легкочитаемому формату +-- нет звездочки в select, а только то, что нужно + + +-- Задание 10 +INSERT INTO users (id, name, age, email, country, created_at) +SELECT + g AS id, + 'user_' || g AS name, + 27 AS age, + 'user_' || g || '@example.com' AS email, + 'RU' AS country, + '2026-02-03' AS created_at +FROM generate_series(20001, 50000) AS g; -- GPT сделал запрос мне + +SELECT * FROM mnoskov.users WHERE age IS NOT NULL; + +-- Запрос 1 +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT name, email FROM users WHERE age > 25; +-- Ну чето все равно не видна разница во времени выполнения, но по логике должен быть быстрее второй запрос \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql new file mode 100644 index 0000000..d3c362e --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/4. EXPLAIN и EXPLAIN ANALYZE/DQL_explain_analyze_practice.sql @@ -0,0 +1,203 @@ +-- Задание 1 +EXPLAIN +SELECT + name, email +FROM + users +WHERE + id = 1; +-- Использует index scan +-- Index Cond: (id = 1) - это значит что условие id = 1, и оно выполняется по индексу, и что id - индекс +-- База использует индекс, потому что у нас по полю id создан индекс, и база выбирает наиболее быстрый пусть ввыполнения запроса - через индекс + + +-- Задание 2 +-- Запрос А +EXPLAIN +SELECT * FROM users WHERE id = 2; + +-- Запрос Б +EXPLAIN +SELECT * FROM users WHERE age > 25; +-- В первом запрос index scan, потому что у нас уловие по индексу - id, во втором запросы seq scan, потому что у поля age не индексов, и таблица сканируется медленее + + +-- Задание 3 +EXPLAIN ANALYZE +SELECT + name, price +FROM + products +WHERE + category = 'Электроника'; +-- Execution Time: 0.020 ms +-- Тип сканирования: Seq Scan +-- Ожидалось вернуть 1, а вернул 3 + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT * FROM users WHERE country = 'RU'; +-- Без индекса - Execution Time: 6.769 ms +CREATE INDEX idx_country ON users(country); +-- С индексом - Execution Time: 2.076 ms +-- Сначала идет Bitmap Heap Scan on users - как я понял на этом этапе 'создается' таблица где country = 'RU', а потом Bitmap INdex Scan on idx_country - тут уже мы читам информацию из таблицы где все country = 'RU'. +-- Запрос стал быстрее примерно в три раза +-- Индекс помог, потому что база искала не среди всех данных, а среди 'проиндексированных' + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, o.order_date, o.status +FROM + users u +JOIN + orders o ON u.id = o.user_id +WHERE + u.country = 'RU'; +-- База исполщовала Nested Loop +-- Чтение началось с orders +-- Execution Time: 0.075 ms + + +-- Задание 6 +CREATE INDEX idx_email ON users(email); +EXPLAIN ANALYZE +SELECT + * +FROM + users +WHERE + lower(email) = 'user_28@example.com'; +-- База не использует индекс пока в where есть функция lower +-- Потому что в условии функция, а с ними индексы не в ладах как я понял +-- Чтобы индекс заработал, надо убрать функцию lower, и сравнивать email как есть +-- Если убрать lower, то Execution Time намного быстрее, прям очень намного быстрее + + +-- Задание 7 +CREATE INDEX idx_age ON users(age); +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM users WHERE age + 5 > 30; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT * FROM users WHERE age > 25; +-- Быстрее работает запрос Б, но че то он тоже не хочет индекс использовать, хотя я его создал +-- Первый запрос не может использовать индекс, потому что там в условии математическое вычисление, а почему второй запрос не использует индек - я хз +-- Чтобы исправить запрос надо убрать сложение, и просто написать age > 25, как в запросе Б + + +-- Задание 8 +CREATE INDEX idx_created_at ON users(created_at); +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE EXTRACT(YEAR FROM u.created_at) = 2026 +GROUP BY u.name; +-- Функция EXTRACT - это плохая идея, потому что это функция которой быть в where по возможности не должно +-- WHERE i.crated_at BETWEEN '2026-01-01' AND '2027-01-01' +-- Индекс не используется в текущеа виде + + +-- Задание 9 +-- Запрос А +EXPLAIN ANALYZE +SELECT * FROM products WHERE price > 50000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT name, price FROM products WHERE price > 50000; +-- Если будет много данных в таблице, то быстрее будет работать второй запрос +-- Второй эффективнее тк в селекте указаны определнные поля, а не все(*) +-- Разница будет особо замета при больших объемах данных + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT u.name, p.name AS product_name, oi.quantity +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU' AND p.category = 'Электроника'; +-- Порядок чтения таблиц: products -> order_items -> orders -> users +-- Количество строк: products - 3, order_items - 12, orders - 1, users - 0 +-- Если у country и category будут индексы, то запрос ьудет выполняться побыстре, потому что будет Index Scan, а не Seq Scan + + +-- Задание 11 +-- До +EXPLAIN ANALYZE +SELECT * +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE LOWER(u.name) LIKE '%иван%' + AND EXTRACT(MONTH FROM o.order_date) = 1 + AND u.age + 10 > 35; +-- Execution Time: 0.219 ms + +-- После +EXPLAIN ANALYZE +SELECT * +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.name = 'иван' AND + o.order_date BETWEEN '2024-01-01' AND '2024-02-01' AND + O.order_date BETWEEN '2026-01-01' AND '2026-02-01' AND + u.age > 25; +-- Execution Time: 0.032 ms +-- Мой вариант лучще, потому что у меня нет функций и вычисление мешаюзих использовать индексы в условии, и оптимизатор может успешно использовать индексы + + +-- Задание 12 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + users u +JOIN + orders o ON u.id = o.user_id AND u.country = 'RU'; + +-- Запрос 2 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE EXISTS ( + SELECT + 1 + FROM + users u + WHERE + o.user_id = u.id AND + u.country = 'RU' +); + +-- Запрос 3 +EXPLAIN ANALYZE +SELECT + o.user_id, + o.id AS order_id +FROM + orders o +WHERE o.user_id IN ( + SELECT + u.id + FROM + users u + WHERE + u.country = 'RU' +); + +-- Запрос 1: Execution Time: 0.055 ms +-- Запрос 2: Execution Time: 0.051 ms +-- Запрос 3: Execution Time: 0.073 ms +-- Они все дют примерно одинаковый разультат, потому что мало данных в таблице. А планы выполнения у них у всех одинаковые, вот прям 1 в 1. Ну по логие 3 запрос самый медленный, а первые два шустрые \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql new file mode 100644 index 0000000..55377f9 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/5. Порядок выполнения команд/DQL_command_execution_order_practice.sql @@ -0,0 +1,217 @@ +-- Задание 1 +EXPLAIN ANALYZE +SELECT country, count(*) AS users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING count(*) > 1 +ORDER BY users_count DESC; +-- WHERE применится до GROUP BY +-- HAVING применится после GROUP BY +-- Использовать users_count в HAVING нельзя, а в ORDER BY можно + + +-- Задание 2 +EXPLAIN ANALYZE +SELECT u.name, o.order_date, o.status +FROM users u +JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' AND o.status = 'paid' +ORDER BY o.order_date DESC; +-- Порядок выполнения по QUERY PLAN: +-- 1) сначала сканирутся таблица orders и выполняется фильтрация o.status = 'paid', +-- 2) потом сканируется таблица users по условию u.id = o.user_id, и выполняется вильтрация u.country = 'RU', +-- 3) далее Nested Loop соединяет orders и users, которые уже прошли фильтры, +-- 4) теперь выводится выполняется сортирвка o.order_date DESC + +-- Порядок выполнения по логике: +-- 1) сначала сканируем таблицу users и присоединяем orders по условию u.id = o.user_id +-- 2) потом фильтруем соединенные таблицы по u.country = 'RU' и o.status = 'paid' +-- 3) в SELECT выбираем поля которые хотим вывести - u.name, o.order_date, o.status +-- 4) сортируем выбранные поля по o.order_date по убыванию + + +-- Задание 3 +-- Запрос А +SELECT country, AVG(age) AS avg_age +FROM users +GROUP BY country +HAVING country = 'Россия'; +-- Работает медленно потому having фильтрет после группировки а не до + +-- Запрос Б +SELECT country, avg(age) AS avg_age +FROM users +WHERE country = 'RU' +GROUP BY country; +-- Этот вариант быстрее, тк он фильтрует строки до группировки, те группируется меньше строк а не все + + +-- Задание 4 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +GROUP BY category +HAVING category = 'Электроника' AND AVG(price) > 20000; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, AVG(price) AS avg_price +FROM products +WHERE category = 'Электроника' +GROUP BY category +HAVING AVG(price) > 20000; +-- Количество обработанных строк у запроса А: 4 +-- Количество обработанных строк у запроса Б: 4 +-- PostgreSQL походу оптимизирует даже хуйню написанную в запросе А, планы запросов А и Б одинаковые - 1 в 1, хотя по сути у запроса А должно быть больше строк + + +-- Задание 5 +-- Изначальный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +WHERE users_count > 1 +GROUP BY country +ORDER BY country; + +-- Исправленный запрос +SELECT + country, + COUNT(*) AS users_count +FROM users +GROUP BY country +HAVING COUNT(*) > 1 +ORDER BY country; + +-- Задание 6 +SELECT + u.country, + COUNT(o.id) AS orders_count, + AVG(o.id) AS avg_order_id +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.is_active = true +GROUP BY u.country +HAVING COUNT(o.id) > 1 +ORDER BY orders_count DESC +LIMIT 3; +-- Полный порядок выполнения: +-- 1) FROM/JOIN - сканируется таблица users, и к ней присоединяется таблица orders по условию u.id = o.user_id +-- 2) WHERE - из двух соединенных таблиц фильтруются и остаются только строки где u.is_active = true +-- 3) GROUP BY - происходится группировка по u.country, те все строки с одной и той же страной(записью в столбце) сжимаются в одну +-- 4) HAVING - уже по сгруппированным строкам проводится фильтрация по COUNT(o.id) > 1, остются только те страны(строки) где кол-во пользователей больше одного +-- 5) SELECT - выбираются столбцы которые будут выведены: u.country, COUNT(o.id) AS orders_count, AVG(o.id) AS avg_order_id +-- 6) ORDER BY - выбранные в SELECT строки сортируются по orders_count по убыванию +-- 7) LIMIT - из отсортированных строк остаются только первые 3 + + +-- Задание 7 +-- Запрос А +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +GROUP BY category +HAVING category = 'Электроника'; + +-- Запрос Б +EXPLAIN ANALYZE +SELECT category, COUNT(*) +FROM products +WHERE category = 'Электроника' +GROUP BY category; +-- по логике запрос Б должен быть быстрее тк он сначала филтрует строки а потом группирует уже отфильтрованное, +-- НО PostgreSql может оптимизровать и запрос А, и получается что выполняются оба запроса одинаково, сначала фильтрация а потом группировка +-- В обоих запросах на шаге 'Seq Scan on products' обрабатываются: 3 строки, а на 'GroupAggregate': 1 строка +-- Execution Time одно и тоже + + +-- Задание 8 +SELECT + u.name, + COUNT(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'Россия' + AND u.age > 25 + AND o.status = 'completed' +GROUP BY u.name +HAVING COUNT(o.id) >= 2 +ORDER BY orders_count DESC; +-- WHERE применяется между FROM/JOIN и GROUP BY +-- HAVING применяется между GROUP BY и SELECT +-- фильтрация по o.status в WHERE происходит группировки строк, а фильтрация по COUNT(o.id) в HAVING происходит уже после группирациюю, по самим сгруппированным строкам + + +-- Задание 9 +SELECT + u.name, + p.name AS product_name, + p.price +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'Россия' +ORDER BY p.price DESC +LIMIT 5; +-- ORDER BY применяется после SELECT, перед LIMIT/OFFSET(если есть) +-- LIMIT идет после GROUP BY, тк сначала нужно отфильтровать строки чтобы взять их кусок(5 строк) +-- если LIMIT(5) выполнить до сортировки, то это будет бесполезно, возьмутся просто первые 5 строк, и уже они будут сортироваться, и по итогу выйдет хуйня + + +-- Задание 10 +EXPLAIN ANALYZE +SELECT + category, + count(id) AS count_product_in_category, + avg(price) AS avg_category_price +FROM + products +GROUP BY + category +HAVING + count(id) >= 2 OR avg(price) > 15000 +ORDER BY + avg_category_price DESC +LIMIT 3; +-- 1) FROM - сканируем poducts +-- 2) GROUP BY - группируем по category +-- 3) HAVING - фильтруем по агрегированной функции count(id) +-- 4) SELECT - делаем выборку по столбцам category, count_product_in_category, avg_category_price +-- 5) ORDER BY - сортируем по avg_category_price по убыванию +-- 6) LIMIT - выбираем первые 3 строки из отсортированных +-- Ипсользовал HAVING тк по условию все фильтрации по агрегированным функциям +-- Выполнил с EXPLAIN ANALYZE, сначала читаетя porducts, потом делается фильтрации по count(id) >= 2 и avg(price) > 15000, далее sort, а потом Limit + + +-- Задание 11 +-- Запрос А +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + (SELECT COUNT(*) + FROM orders o + WHERE o.user_id = u.id) AS orders_count +FROM users u +WHERE u.country = 'RU' +ORDER BY orders_count DESC; +-- Подзапрос в SELECT выполняется самым первым, до начала остального запроса, и называется в планировщие - SubPlan 1 +-- orders_count нельзя использовать в WHERE +-- orders_count можно использовать в ORDER BY + +-- Запрос Б(исправленный) +EXPLAIN ANALYZE +SELECT + u.name, + u.age, + count(o.id) AS orders_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.country = 'RU' +GROUP BY u.name, u.age +ORDER BY orders_count DESC; +-- Запрос Б(исправленный) - быстрее, тк подзапрос в SELECT делается для каждого пользователя заново, а JOIN всего один раз \ No newline at end of file diff --git a/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql new file mode 100644 index 0000000..79fc169 --- /dev/null +++ b/mnoskov/postgresql/SQL с нуля/9. Оптимизация/6. JOIN (физические)/DQL_physical_JOIN_practice.sql @@ -0,0 +1,260 @@ +-- Задание 1 +-- Ситуация -- Какой JOIN выберет база? -- Почему? +-------------------------------------------------------------------------------------- +-- 10 пользователей, 1 млн заказов,| Nested | Маленькая таблица и большая с индексом. | +-- есть индекс на orders.user_id | Loop | База будет искать совпадения из | +-- | | маленькой по индексу из большой. | +-------------------------------------------------------------------------------------- +-- 100 000 пользователей, | Hash | Таблицы среднего размера, нет индексов. | +-- 100 000 заказов, нет индексов | Join | База построит хэш-таблицу одной из них, | +-- | | и будет искать совпадения в другой. | +-------------------------------------------------------------------------------------- +-- Обе таблицы отсортированы по | Merge | Потому что таблицы отсортированы по | +-- ключу, есть индексы | Join | ключу. Будет происходить соединение - | +-- | | 'Одновременное&последовательное'. | +-------------------------------------------------------------------------------------- +-- Маленькая таблица (5 строк) | Nested | База будет искать совпадения из | +-- соединяется с большой (1 млн), | Loop | маленькой в большой. Но тк нет индексов | +-- нет индекса | | - это будет медленно. | +-- |--------| Я засомневался и разобрал с ГПТ: | +-- | Hash | Nested Loop будет дорого(без индексов - | +-- | Join | 5.000.000 проврок), поэтому скорее всего| +-- | | Hash Join, просто построится хэш-таблица| +-- | | по маленькой и 5 раз отсканирует большую| +-------------------------------------------------------------------------------------- +-- Я заебался строить эту таблицу из пробелов. Хуй когда решусь на это снова. | +-------------------------------------------------------------------------------------- + + +-- Задание 2 +-- Маленькая таблица (10 строк) +CREATE TABLE mnoskov.small_users AS +SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name; + +-- Большая таблица (100 000 строк) +CREATE TABLE mnoskov.big_orders AS +SELECT + generate_series(1, 100000) AS id, + (random() * 9 + 1)::int AS user_id, + CURRENT_DATE - (random() * 365)::int AS order_date; + +-- Вариант 1: Без индекса +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; + +-- Вариант 2: С индексом +CREATE INDEX idx_big_orders_user_id ON big_orders(user_id); +--DROP INDEX idx_big_orders_user_id; + +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Вариант 1: Merge Join - Потому что обе таблицы отсортированы с самого создания +-- Варинат 2: Тоже Merge Join +-- База могла бы выбрать Nested Loop, потому что у нас одна таблица маленькая а другая большая и с индексом, но выбрала Merge Join, потому что таблицы отсортированы +-- С индексом Execution Time: 31.579 ms, а без примерно 80.000 ms + + +-- Задание 3 +-- Вариант 1: Без ORDER BY +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: С ORDER BY по ключу соединения +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY u.id, o.user_id; + +-- Вариант 3: С ORDER BY по другому полю +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +ORDER BY o.order_date; +-- Во втором варианте также как и в первом - Nested Loop +-- Merge Join не появился +-- ORDER BY может повлиять на выбор JOIN, тк если есть стортировка, то база может выбрать Merge Join + + +-- Задание 4 +EXPLAIN ANALYZE +SELECT + u.name, + p.name AS product_name, + oi.quantity, + o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id +JOIN order_items oi ON o.id = oi.order_id +JOIN products p ON oi.product_id = p.id +WHERE u.country = 'RU'; +-- (u.id = o.user_id) - Nested Loop; (o.id = oi.order_id) - Nested Loop; (oi.product_id = p.id) - Hash Join +-- Seq Scan на этапах сканирования orders и products +-- Думаю стоит создать индекс на orders.user_id; и надо было бы на users.countyr, но я создавал уже в предидущих укроках; подумал, что надо ещё products.id, но там id и так первичный ключ, значит и индекс есть +CREATE INDEX idx_orders_user_id ON orders(user_id); -- DROP INDEX idx_orders_user_id; +-- После создания индекса типы JOIN не поменялись, план остался тем же, скорость выполнения запроса та же + + +-- Задание 5 +EXPLAIN ANALYZE +SELECT + u.name, + COUNT(DISTINCT o.id) AS orders_count, + COUNT(DISTINCT oi.product_id) AS products_count +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +LEFT JOIN order_items oi ON o.id = oi.order_id +WHERE u.country = 'RU' +GROUP BY u.name; +-- на этапе LEFT JOIN order_items oi ON o.id = oi.order_id - оптимизатор выполнил Hash Right Join по условию oi.order_id = o.id, то есть Hash Join +-- на этапе LEFT JOIN orders o ON u.id = o.user_id - оптимизатор выполнил Hash Left Join по условию u.id = o.user_id, то есть тоже Hash Join +-- Метод по сути один и тот же - Hash Join, но отличаются Right и Left - я думаю это выбрано для лучшей оптимизации +-- Через Hash Right Join проходит - 12 строк +-- Через Hash Left Join проходит - 11281 строк + + +-- Задание 6 +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- Разный тип используется, в варианте А - Nested Loop, в варианте B - Hash Left Join +-- Да, у варанта B Execution Time в очень много раз больше +-- LEFT JOIN медленне, потому что он не использует индекс, а делает Seq Scan, те читает таблицы полностью, потому что подругому не получится выполнить LEFT JOIN + + +-- Задание 7 +-- Вариант 1: users JOIN orders +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +JOIN orders o ON u.id = o.user_id; + +-- Вариант 2: orders JOIN users +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM orders o +JOIN users u ON o.user_id = u.id; +-- Тип JOIN не изменился +-- Порядок чтения таблиц не изменился +-- Execution Time одинаковое +-- Оптимизатор выбрал один и тот же порядок потому что по сути это два одинаковых запроса, с одной и той же логикой, но написаны чуть чуть по разному, и оптимизатор в обоих случаях выполняет так, как быстрее. Почему порядок может быть разным, я не предполагаю. + + +-- Задание 8 +-- Запрос 1 +EXPLAIN ANALYZE +SELECT + * +FROM + orders o +JOIN + order_items oi ON o.id = oi.order_id +ORDER BY + o.id, oi.order_id; +-- Уверен такой запрос привел бы к Merge Join, если бы данных в таблицах было побольше + +-- Запрос 2(из задания 2) +EXPLAIN ANALYZE +SELECT u.name, COUNT(o.id) +FROM small_users u +JOIN big_orders o ON u.id = o.user_id +GROUP BY u.name; +-- Потому что обе таблицы уже отсортированы в момент создания + +-- Задание 9 +-- Nested Loop лучше Hash Join, когда есть индексы по полям фильтрации, когда одна таблица небольшая, а у второй есть индекс. Подглядел: когда нужно найти немного совпадений. +-- Hash Join лучше чем Nested Loop, когда нет индексов, когда таблицы среднего размера и больше, и когдау условие фильтрации '=' + +-- Оба запроса из задания 6: +-- Вариант A: INNER JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +INNER JOIN orders o ON u.id = o.user_id; +-- тут nested loop быстрее, тк соеденияется все по индексам + +-- Вариант B: LEFT JOIN +EXPLAIN ANALYZE +SELECT u.name, o.order_date +FROM users u +LEFT JOIN orders o ON u.id = o.user_id; +-- а тут быстрее merge join, тк по индексам соединить не получится, тут нужны обе полные таблицы, и создается по одной из них хэш-таблица + + +-- Задание 10 +-- Запрос A +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_category_revenue AS +SELECT + p.category, + sum(oi.quantity * oi.price_at_purchase) AS total_revenue +FROM + mnoskov.products p +JOIN + mnoskov.order_items oi ON p.id = oi.product_id +JOIN + mnoskov.orders o ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY + p.category +ORDER BY + total_revenue desc; +-- До: Execution Time: 0.309 ms +CREATE INDEX idx_orders_status ON orders(status); +-- После добавления индекса ничего не поменялось, план запроса остался тот же, скорее всего потому что мало данных, думаю будь их больше с индексом запрос был бы быстрее + +-- Запрос B +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_user_activity AS +SELECT + u.id AS user_id, + u.name AS user_name, + count(*) AS orders_count +FROM + mnoskov.users u +JOIN + mnoskov.orders o ON u.id = o.user_id +WHERE + o.status = 'paid' +GROUP BY + u.id, u.name +HAVING + count(*) > 0 +ORDER BY + orders_count desc; +-- И тут тоже несмотря на то что индекс на orders статус я уже создал, таблица orders скаринруется как Seq Scan, я думаю это потому, что мало данных, будь их больше с индексом запрос был бы быстрее + +-- Запрос C +EXPLAIN ANALYZE +--CREATE MATERIALIZED VIEW mv_daily_revenue AS +SELECT + o.order_date::date AS sales_date, + sum(oi.quantity * oi.price_at_purchase) AS daily_revenue +FROM + mnoskov.orders o +JOIN + mnoskov.order_items oi ON o.id = oi.order_id +WHERE + o.status = 'paid' +GROUP BY + o.order_date +ORDER BY + sales_date; +-- Тут ситуация аналогичная и для первый двух запросов, нужные индексы уже созданы, в связи выполнения всех предидущих занятий и тем, и единственное что я мог сделать для улучшения времени выполнения - это добавить индекс на orders(status) \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql new file mode 100644 index 0000000..68c5a40 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DDL_create_tables_for_pet_project.sql @@ -0,0 +1,45 @@ +--DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name varchar(100) NOT NULL, + age SMALLINT, + email varchar(100), + registration_date timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS courses; +CREATE TABLE courses ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + title varchar(150) NOT NULL, + category varchar(50), + created_at timestamptz NOT NULL +); + +--DROP TABLE IF EXISTS lessons; +CREATE TABLE lessons ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + course_id bigint NOT NULL, + title varchar(150) NOT NULL, + duration_min SMALLINT NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS enrollments; +CREATE TABLE enrollments ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + course_id bigint NOT NULL, + enrolled_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (course_id) REFERENCES courses(id) +); + +--DROP TABLE IF EXISTS lesson_views; +CREATE TABLE lesson_views ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + user_id bigint NOT NULL, + lesson_id bigint NOT NULL, + viewed_at timestamptz NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (lesson_id) REFERENCES lessons(id) +); \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql new file mode 100644 index 0000000..7438241 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/1. Создание таблиц и связей/DML_insert_in_tables_for_pet_project.sql @@ -0,0 +1,155 @@ +INSERT INTO users (name, age, email, registration_date) VALUES +('Alice',25,'alice@mail.com','2023-01-10'), +('Bob',30,'bob@gmail.com','2023-02-05'), +('Charlie',22,'charlie@mail.com','2023-02-20'), +('Diana',28,'diana@mail.com','2023-03-01'), +('Ethan',35,'ethan@gmail.com','2023-03-10'), +('Frank',29,'frank@mail.com','2023-01-05'), +('Grace',26,'grace@gmail.com','2023-01-12'), +('Henry',32,'henry@mail.com','2023-01-20'), +('Iris',24,'iris@gmail.com','2023-02-10'), +('Jack',27,'jack@mail.com','2023-04-15'), +('Kelly',23,'kelly@gmail.com','2023-04-20'), +('Leo',31,'leo@mail.com','2023-01-15'), +('Mia',26,'mia@gmail.com','2023-01-18'), +('Nina',28,'nina@mail.com','2023-01-08'), +('Oscar',33,'oscar@gmail.com','2023-01-10'), +('Paul',29,'paul@mail.com','2023-02-15'), +('Quinn',24,'quinn@gmail.com','2023-03-20'), +('Rita',27,'rita@mail.com','2023-04-01'), +('Sam',30,'sam@gmail.com','2023-04-10'), +('Tina',25,'tina@mail.com','2023-04-18'); + +INSERT INTO courses (title, category, created_at) VALUES +('SQL для начинающих','data','2023-01-01'), +('Python для анализа данных','programming','2023-01-15'), +('BI с нуля','business','2023-03-01'), +('Excel для аналитиков','business','2023-02-01'), +('Основы машинного обучения','data','2023-03-15'); + +INSERT INTO lessons (course_id, title, duration_min) VALUES +(1,'SELECT и FROM',10), +(1,'JOIN',15), +(1,'WHERE и фильтрация',12), +(1,'Агрегации и GROUP BY',18), +(1,'Подзапросы',25), +(2,'Pandas основы',20), +(2,'NumPy',15), +(2,'Визуализация данных',22), +(3,'Основы BI',12), +(3,'Метрики и дашборды',18), +(3,'Power BI введение',20), +(4,'Формулы в Excel',15), +(4,'Сводные таблицы',18), +(4,'Графики и диаграммы',14), +(5,'Введение в ML',20), +(5,'Линейная регрессия',30), +(5,'Классификация',28), +(5,'Кластеризация',25); + +INSERT INTO enrollments (user_id, course_id, enrolled_at) VALUES +(1,1,'2023-01-15'), +(1,2,'2023-02-01'), +(2,1,'2023-01-20'), +(3,2,'2023-03-05'), +(4,3,'2023-04-01'), +(6,1,'2023-01-06'), +(6,2,'2023-01-10'), +(6,4,'2023-02-05'), +(7,1,'2023-01-13'), +(7,3,'2023-03-10'), +(8,1,'2023-01-22'), +(8,2,'2023-01-22'), +(8,3,'2023-01-22'), +(9,4,'2023-02-12'), +(10,1,'2023-04-16'), +(11,2,'2023-04-21'), +(12,1,'2023-01-16'), +(13,2,'2023-01-19'), +(13,4,'2023-02-20'), +(14,1,'2023-01-09'), +(14,2,'2023-01-09'), +(14,3,'2023-01-09'), +(14,4,'2023-02-01'), +(14,5,'2023-03-20'), +(15,1,'2023-01-11'), +(15,3,'2023-03-05'), +(16,2,'2023-02-16'), +(16,4,'2023-02-16'), +(17,3,'2023-03-22'), +(17,5,'2023-03-25'), +(18,1,'2023-04-02'), +(19,1,'2023-04-11'), +(19,2,'2023-04-11'), +(20,2,'2023-04-19'), +(5,5,'2023-03-20'), +(1,4,'2023-02-10'), +(2,3,'2023-03-15'), +(3,1,'2023-02-25'), +(4,2,'2023-03-10'), +(6,3,'2023-03-08'), +(7,2,'2023-02-01'), +(9,1,'2023-02-15'), +(12,3,'2023-03-10'), +(15,2,'2023-02-01'), +(18,4,'2023-04-05'); + +INSERT INTO lesson_views (user_id, lesson_id, viewed_at) VALUES +(1,1,'2023-01-16 10:00:00'), +(1,2,'2023-01-16 10:15:00'), +(1,3,'2023-01-17 09:00:00'), +(1,4,'2023-01-17 09:30:00'), +(1,4,'2023-01-17 11:00:00'), +(1,4,'2023-01-18 10:00:00'), +(1,6,'2023-02-02 14:00:00'), +(1,12,'2023-02-15 10:00:00'), +(1,13,'2023-02-15 10:30:00'), +(2,1,'2023-01-21 09:00:00'), +(2,9,'2023-03-16 10:00:00'), +(2,10,'2023-03-16 10:30:00'), +(3,6,'2023-03-06 12:00:00'), +(3,1,'2023-02-26 11:00:00'), +(3,2,'2023-02-26 11:20:00'), +(4,9,'2023-04-02 10:00:00'), +(4,10,'2023-04-03 10:00:00'), +(4,6,'2023-03-12 14:00:00'), +(4,7,'2023-03-13 09:00:00'), +(5,15,'2023-03-21 10:00:00'), +(5,16,'2023-03-22 11:00:00'), +(6,1,'2023-01-07 10:00:00'), +(6,2,'2023-01-07 10:15:00'), +(6,3,'2023-01-08 09:00:00'), +(6,4,'2023-01-08 09:30:00'), +(6,5,'2023-01-09 10:00:00'), +(6,6,'2023-01-11 14:00:00'), +(6,7,'2023-01-12 10:00:00'), +(6,8,'2023-01-13 11:00:00'), +(6,12,'2023-02-06 09:00:00'), +(6,13,'2023-02-06 09:30:00'), +(6,14,'2023-02-07 10:00:00'), +(6,9,'2023-03-09 11:00:00'), +(7,1,'2023-01-14 10:00:00'), +(7,2,'2023-01-14 10:20:00'), +(7,3,'2023-01-15 09:00:00'), +(7,9,'2023-03-11 14:00:00'), +(7,10,'2023-03-11 14:30:00'), +(9,12,'2023-02-13 10:00:00'), +(9,13,'2023-02-13 10:30:00'), +(9,1,'2023-02-16 11:00:00'), +(12,1,'2023-01-17 10:00:00'), +(12,2,'2023-01-17 10:20:00'), +(12,1,'2023-04-10 11:00:00'), +(12,3,'2023-04-10 11:30:00'), +(12,9,'2023-03-12 10:00:00'), +(13,6,'2023-01-20 14:00:00'), +(13,7,'2023-01-21 10:00:00'), +(13,12,'2023-02-21 09:00:00'), +(14,1,'2023-01-09 08:00:00'), +(14,2,'2023-01-09 08:15:00'), +(14,3,'2023-01-10 10:00:00'), +(14,4,'2023-01-10 10:30:00'), +(14,5,'2023-01-11 09:00:00'), +(14,6,'2023-01-10 14:00:00'), +(14,7,'2023-01-11 10:00:00'), +(14,8,'2023-01-12 11:00:00'), +(14,9,'2023-01-10 08:00:00'); \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql new file mode 100644 index 0000000..19b8668 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_cohort_analysis_of_users.sql @@ -0,0 +1,40 @@ +CREATE MATERIALIZED VIEW user_cohort_analysis AS +WITH users_and_enrollments_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT u.id) AS users_in_cohort, + count(DISTINCT e.user_id) AS enrolled_users, + count(DISTINCT e.course_id) courses_in_cohort + FROM + users u + JOIN + enrollments e ON u.id = e.user_id + GROUP BY + cohort_month +), +lesson_views_results AS ( + SELECT + date_trunc('month', u.registration_date)::date AS cohort_month, + count(DISTINCT lv.user_id) AS active_users, + round(avg(lv.viewed_at::date - u.registration_date::date), 0) AS avg_time_to_first_view + FROM + users u + LEFT JOIN + lesson_views lv ON u.id = lv.user_id + GROUP BY + cohort_month +) +SELECT + uer.cohort_month, + uer.users_in_cohort, + uer.enrolled_users, + lvr.active_users, + round((lvr.active_users / uer.users_in_cohort::numeric) * 100, 2) || '%' AS retention_rate, + round(uer.courses_in_cohort / uer.users_in_cohort::numeric, 2) AS avg_courses_per_user, + lvr.avg_time_to_first_view +FROM + users_and_enrollments_results uer +JOIN + lesson_views_results lvr ON uer.cohort_month = lvr.cohort_month +ORDER BY + uer.cohort_month; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql new file mode 100644 index 0000000..9800c9f --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_conversion_funnel.sql @@ -0,0 +1,114 @@ +CREATE MATERIALIZED VIEW conversion_funnel AS +WITH stage_courses AS ( + -- registered + (SELECT + 0 AS course_id, + 1 AS stage_id, + 'registered' AS stage_name, + count(DISTINCT u.id) AS users_count + FROM + users u) + UNION ALL + -- enrolled + (SELECT + e.course_id, + 2 AS stage_id, + 'enrolled' AS stage_name, + count(DISTINCT e.user_id) AS users_count + FROM + enrollments e + GROUP BY + e.course_id + ORDER BY + e.course_id) + UNION ALL + -- started + (SELECT + l.course_id, + 3 AS stage_id, + 'started' AS stage_name, + count(DISTINCT lv.user_id) AS users_count + FROM + lessons l + JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id + ORDER BY + l.course_id) + UNION ALL + -- mid_progress + (WITH lessons_mid AS ( + SELECT + l.course_id, + ceil(percentile_cont(0.5) WITHIN GROUP (ORDER BY l.id)) AS mid_in_lesson_id + FROM + lessons l + GROUP BY + l.course_id + ) + SELECT + lm.course_id, + 4 AS stage_id, + 'mid_progress' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_mid lm + LEFT JOIN + lesson_views lv ON lm.mid_in_lesson_id = lv.lesson_id + GROUP BY + lm.course_id, lm.mid_in_lesson_id + ORDER BY + lm.course_id) + UNION ALL + -- completed + (WITH courses_lessons_id AS ( + SELECT + l.course_id, + l.id AS lesson_id + FROM + lessons l + GROUP BY + l.course_id, l.id + ), + lessons_numbers AS ( + SELECT + cli.course_id, + cli.lesson_id, + ROW_NUMBER() OVER(PARTITION BY cli.course_id ORDER BY cli.lesson_id DESC) AS lesson_number + FROM + courses_lessons_id cli + ) + SELECT + ln.course_id, + 5 AS stage_id, + 'completed' AS stage_name, + count(lv.user_id) AS users_count + FROM + lessons_numbers ln + LEFT JOIN + lesson_views lv ON ln.lesson_id = lv.lesson_id + WHERE + ln.lesson_number = 1 + GROUP BY + ln.course_id, ln.lesson_id + ORDER BY + ln.course_id) +) +SELECT + sc.course_id, + c.title AS course_title, + sc.stage_name, + sc.users_count, + CASE + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NULL THEN 'first stage' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) = 0 THEN 0::text || '%' + WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NOT NULL THEN round((sc.users_count / lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' + END AS conversion_from_previous, + round((sc.users_count / FIRST_VALUE(sc.users_count) OVER (ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' AS conversion_from_start +FROM + courses c +RIGHT JOIN + stage_courses sc ON c.id = sc.course_id +ORDER BY + sc.course_id; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql new file mode 100644 index 0000000..4e670fc --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_effectivity.sql @@ -0,0 +1,41 @@ +CREATE MATERIALIZED VIEW course_summary AS +WITH courses_unique_users AS ( + SELECT + e.course_id, + count(e.user_id) AS unique_users + FROM + enrollments e + GROUP BY + e.course_id +), +lessons_results AS ( + SELECT + l.course_id, + count(distinct l.id) AS lessons_count, + count(lv.lesson_id) AS total_views, + min(lv.viewed_at)::date AS first_viewed_at, + max(lv.viewed_at)::date AS last_viewed_at + FROM + lessons l + LEFT JOIN + lesson_views lv ON l.id = lv.lesson_id + GROUP BY + l.course_id +) +SELECT + c.id AS course_id, + c.title AS course_title, + cuu.unique_users, + lr.lessons_count, + lr.total_views, + round(lr.total_views / cuu.unique_users::numeric, 2) AS avg_views_per_user, + lr.first_viewed_at, + lr.last_viewed_at +FROM + courses c +JOIN + courses_unique_users cuu ON c.id = cuu.course_id +JOIN + lessons_results lr ON c.id = lr.course_id +ORDER BY + c.id; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql new file mode 100644 index 0000000..3377eb5 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_courses_popularity.sql @@ -0,0 +1,39 @@ +CREATE MATERIALIZED VIEW lesson_popularity_ranking as +WITH lesson_views_results AS ( + SELECT + lv.lesson_id, + count(lv.user_id) AS total_views, + count(DISTINCT lv.user_id) AS unique_viewers + FROM + lesson_views lv + GROUP BY + lv.lesson_id +), +enrollments_results AS ( + SELECT + e.course_id, + count(e.user_id) AS enrolled_to_course + FROM + enrollments e + GROUP BY + e.course_id +) +SELECT + l.id AS lesson_id, + l.title AS lesson_title, + c.title AS course_title, + lvr.total_views, + lvr.unique_viewers, + round(lvr.total_views / lvr.unique_viewers::numeric, 2) AS avg_views_per_viewer, + dense_rank() OVER (ORDER BY lvr.total_views desc) AS popularity_rank, + round((lvr.unique_viewers / er.enrolled_to_course::numeric) * 100, 0) || '%' AS completion_rate +FROM + lessons l +JOIN + courses c ON l.course_id = c.id +JOIN + lesson_views_results lvr ON l.id = lvr.lesson_id +JOIN + enrollments_results er ON c.id = er.course_id +ORDER BY + popularity_rank; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql new file mode 100644 index 0000000..32fbe79 --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_reporting_on_interactions.sql @@ -0,0 +1,48 @@ +CREATE MATERIALIZED VIEW platform_summary as +WITH users_count AS ( + SELECT + count(*) AS total_users + FROM + users +), +courses_count AS ( + SELECT + count(*) AS total_courses + FROM + courses +), +lessons_count AS ( + SELECT + count(*) AS total_lessons + FROM + lessons +), +users_with_views AS ( + SELECT + count(distinct user_id) AS users_with_views + FROM + lesson_views +), +views_count AS ( + SELECT + count(*) AS total_views + FROM + lesson_views lv +) +SELECT + uc.total_users, + cc.total_courses, + lc.total_lessons, + uwv.users_with_views, + round(lc.total_lessons / cc.total_courses::NUMERIC, 2) AS avg_lessons_per_course, + round(vc.total_views / lc.total_lessons::NUMERIC, 2) AS avg_views_per_lesson +FROM + users_count uc +CROSS JOIN + courses_count cc +CROSS JOIN + lessons_count lc +CROSS JOIN + users_with_views uwv +CROSS JOIN + views_count AS vc; \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql new file mode 100644 index 0000000..46cfb4d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/2. Создание отчетности (витрин)/DQL_materialized_view_user_activity_summary.sql @@ -0,0 +1,24 @@ +CREATE MATERIALIZED VIEW user_activity_summary AS +SELECT + u.id AS user_id, + u.name, + count(DISTINCT e.course_id) AS enrollment_count, + count(DISTINCT lv.lesson_id) AS viewed_lessons_count, + max(lv.viewed_at)::date AS last_view, + min(lv.viewed_at)::date AS first_view, + ((count(DISTINCT lv.lesson_id)::numeric(5,2) / count(DISTINCT l.id)) * 100)::numeric(5, 2) AS engagement_rate +FROM + users u +JOIN + enrollments e ON u.id = e.user_id +JOIN + courses c ON e.course_id = c.id +JOIN + lessons l ON c.id = l.course_id +LEFT JOIN + lesson_views lv ON u.id = lv.user_id -- сюда - 'AND l.id = lv.lesson_id' +GROUP BY + u.id, u.name +ORDER BY + u.id; +-- Кинул его в нейронку на проверку, она сказала что надо добавить 'AND l.id = lv.lesson_id' при присоединении таблицы lesson_views(сделал пометку в коде), это нужно чтобы точно проверить, что урок который посмотрел пользователь точно принадлежит его курсу, чтобы избежать ошибок из-за некорректных данные если такие появятся в результате какого либо сбоя или ошибки. \ No newline at end of file diff --git a/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql b/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql new file mode 100644 index 0000000..49c296d --- /dev/null +++ b/mnoskov/postgresql/Пет-проект Онлайн-курсы/3. Доп. задания/DQL_additional_tasks.sql @@ -0,0 +1,51 @@ +-- Задание 1 +SELECT + * +FROM + conversion_funnel +WHERE + stage_name = 'started' +ORDER BY + conversion_from_previous; +-- Первые 2 курса, гже отавлилось больше всего учеников между этапами enrolled → started + + +-- Задание 2 +SELECT + * +FROM + lesson_popularity_ranking lpr +WHERE + lpr.avg_views_per_viewer > 1 +LIMIT 3; + + +-- Задание 3 +SELECT + * +FROM + user_cohort_analysis uca +ORDER BY + retention_rate DESC; +-- Не разобрался как можжно соеденить с другими таблицам +-- А retention_rate такой, наверное потому что при запуске курсов была пиар компания большая, и делали рассылки с напоминанием позаниматься, а потом перестали, вот и удержание похерилось + + +-- Задание 4 +-- Витрина - user_activity_summary +-- Всяко разно попробовал подобавлять индексы, итог: без индексов быстрее. Просмотрел весь запрос, вроде ничего не нужно менять - лишнего ничего нет. + +-- Витрина - course_summary +-- Тут также как и в предидущем - и так и сяк попробовал, план запроса не меняется, время не улучшается + +-- Витрина - platform_summary +-- Тут тоже нечего менять - всё из CROSS JOIN. Попробовал вместо count(*) писать count(id) думал мол в ширину будет меньше читать, в итоге выяснил что и так и так одинаково читает + +-- Витрина - user_cohort_analysis +-- И тут че не делай, все одна фигня. План вроде хороший. + +-- Витрина - lesson_popularity_ranking +-- Тут тоже ничего не вышло - я начниаю чувствовать себя тупым от того что не могу улучшить нигде время запроса. + +-- Витрина - conversion_funnel +-- Здесь тоже ничего не вышло - со стороны план запроса выглядит хорошо. Ускорить время запроса не вышло. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql new file mode 100644 index 0000000..d29f3e7 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/1. Индексы/practice_indexes.sql @@ -0,0 +1,111 @@ +/*Задание 1: По каким полям делать индексы(тут говорится смотреть на таблицы из пет проекта 'Онлайн магазины', надеюсь смотреть на таблицы а не на витрины): + 1) По каким полям, как тебе кажется, реальный продукт‑менеджер или аналитик будет чаще всего: + where: + users: age, email, registration_date, + enrollments: enrolled_at + courses: title, category + lessons: title, duration_min + lesson_views: viewed_at + order by: + users: id, age, registration_date + enrollments: user_id, enrolled_at + courses: id + lessons: id, duration_min + lesson_views: user_id, viewed_at + group by: + users: age, registration_date + enrollments: user_id, course_id, enrolled_at + courses: category + lessons: course_id + lesson_views: user_id, lesson_id + join: + users: id + enrollments: user_id, course_id + courses: id + lessons: id, course_id + lesson_views: user_id, lesson_id + 2) Для каждого поля, которое ты назвал, укажи: «индекс нужен / индекс не нужен» и коротко объясни почему (часто фильтруем, редко фильтруем, мало разных значений и т.п.). + users: + id(есть автоматически), + age(не нужен, мало разнах значений), + email(нужен, чтобы найти пользователя через where) + registration_date(нужен, часто будет сравниваться в BETWEEN наприме, и сортироваться) + enrollments: + user_id(нужен, будет часто джоиниться), + course_id(нужен, будет часто джоиниться), + enrolled_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться) + courses: + id(есть автоматически), + title(не нужен, мало разных значений), + category(не нужен, мало разных значений) + lessons: + id(есть автоматически), + course_id(нужен, будет часто джоиниться), + title(не нужен, мало разных значений), + duration_min(мне кажется не нужен, тк не очень часто будет использоваться) + lesson_views: + user_id(нужен, будет часто джоиниться), + lesson_id(нужен, будет часто джоиниться) + viewed_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться)*/ + +-- Задание 2: Свяжи запрос и индекс: + -- 1) + SELECT * FROM users WHERE email = 'какой-то email'; -- индекс на email ускорит where + -- 2) + SELECT * FROM users WHERE is_active = true AND created_at >= CURRENT_DATE - INTERVAL '1 year'; -- индекс на created_at ускоряет where + -- 3) + SELECT * FROM products WHERE category = 'Электроника' ORDER BY price; -- индекс по price ускорит order by + -- 4) + SELECT * FROM orders WHERE user_id = 1 AND order_date >= CURRENT_DATE - INTERVAL '30 days'; -- индекс на user_id и order_date ускорят where + -- 5) + SELECT order_id, product_id FROM order_items; -- на у тут по идее составной создается сразу, так как тут pkey составной. Ну и они ускоряют select + +/* Задание 3. Порядок столбцов в индексе + 1) Я бы сделал orders(order_date, user_id). + 2) Для первых двух будет работать четко, а для третьего хуже. Тк у третьего нет фильрации по order date, и он стоит вторым в индексе + 3) + * Когда один индекс - хорошо с фильтрацией по нескольким полям, и когда первое поле указанное в индексе используется часто, а второе нет. + * Когда индекса два - хорошо когда оба поля фильтруются по отдельности*/ + +/* Задание 4. Лишние индексы в наших таблицах + !!! Я тут не буду писать индексы которые создаются автоматически из за pkey + users: + Скорее нужен индекс: + email, created_at(нужны для where, и order by) + Скорее не нужен индекс: + name, age, country, is_active(либо мало рахных значений, либо не очень востребованы эти поля) + products: + Скорее нужен индекс: + price (для order by и where) + Скорее не нужен индекс: + name, ctegory, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + orders: + Скорее нужен индекс: + user_id(для join), price(для order by и where) + Скорее не нужен индекс: + name, category, in_stock(либо мало разных значений, либо не очень востребованы эти поля) + order_items: + Скорее нужен индекс: + price_at_purchase (для order by и where) + Скорее не нужен индекс: + quantity(мало разных значения) + + Много индексов не есть хорошо, потому что они тоже занимают помять и их тоже нужно считать. Если их бездумно везде понатыкать то запрос может выполняться дольше.*/ + +/* Задание 5. Что должно быть уникальным + 1) id, email + 2) id, article_number(придумал вот такое) + 3) + * Если не будет уникаьности по id, то возникнут проблемы бд, что у разных пользователей будет один id и это жопа, ничего невозможно фильтровать и тд. + * Если email будет повторяться, то как вообще пользователи будут входить в аккаунт, и как делать персонализированную рассылку? Никак. + * Если article_number будет повторяться, то те же проблемы что и с id, будет нарушение логики работы бд. + + * Уникальный индекс обезопасит бд от сбоев и некорректной работы.*/ + +-- Задание 6. Где индексы помогут, а где нет + SELECT * FROM users WHERE id = 100; -- индекс уже создан тк это pkey + SELECT * FROM users WHERE email = 'user@example.com'; -- нужен, поможет с where, быстрее будет рабоать фильтрация + SELECT * FROM products WHERE price BETWEEN 100 AND 500; -- нужн, поможет с where, быстрее будте фильтрация + SELECT * FROM products WHERE name LIKE '%phone%'; -- не нужен, индекс не рвботает с like + SELECT * FROM orders WHERE status = 'DELIVERED'; -- не нужен, в status мало разных значений + SELECT * FROM order_items WHERE order_id = 123; -- нужен, поможет с where, быстрее будет рабоать фильтрация \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql new file mode 100644 index 0000000..fa096dc --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/2. Партиции/practice_partitions.sql @@ -0,0 +1,189 @@ +/* Задание 1. Какой вид партиционирования выбрать + 1) + * range по полю order_date + * запросы с between в год + 1. where order_date >= '2022-01-01' and order_date < '2023-01-01' + 2. where extract('year' from order_date) = '2025' + 2) + * тут два типа партиционирования подходят. range по created_at или list по country. + * если выбирать list по country то плюс - это при фильтрах по стране будут отсекаться лишние партиции, а минус, что год/месяц регистрации не будет разделен по партициям. + 3) + * логично range по created_at - чтобы разделить по месяцам/годам, а hash по user_id - почему так, точно сказать не могу, но в теории сказано что hash используется когда нет диапазона или списка и это частый сценарий для логов + * с range выиграют запросы с фильтрами по created_at, а с hash выигают запросы с фильтрами по user_id, а все что не указаны как поле для партиций, не почувствуют разницы.*/ + + +/* Задание 2. Размер партиций + 2) количество партициый за 10 лет по: + * годам - 10, тут вообще все четко, и удалять не сложно, и поддержка проще, и запросы будут хорошо делаться + * месяцам - 120, тут с удалением тарых данных труднее, поддержка тяжелая, но скорость запросов будет лучше, если фильтрация будет по месяцам + * дням - примерно 3650, а это я чувствую пиздец, с удалением и поддержкой жопа, и со скоростью запросов тоже жопа - данные будут читаться из кучи партиций что не есть хорошо + 3) + Я думаю прям для очень большого магазина оптимальнее будет партиционирование по месяцам - тк очень много покупателей и большинство статистик будет по месяцам, по годам тоэе конечно, но думаю приоритетнее по месяцам*/ + + +-- Задание 3. RANGE‑партиции по дате заказов +DROP TABLE IF EXISTS orders_part; +CREATE TABLE orders_part ( + id bigint, + user_id bigint, + order_date timestamptz NOT NULL, + status varchar(20), + PRIMARY KEY (id, order_date), + FOREIGN KEY (user_id) REFERENCES users_1(id) +) PARTITION BY RANGE (order_date); + +CREATE TABLE orders_2024 PARTITION OF orders_part +FOR VALUES FROM ('2024-01-01') TO ('2025-01-01'); + +CREATE TABLE orders_2025 PARTITION OF orders_part +FOR VALUES FROM ('2025-01-01') TO ('2026-01-01'); + +CREATE TABLE orders_2026 PARTITION OF orders_part +FOR VALUES FROM ('2026-01-01') TO ('2027-01-01'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + order_date BETWEEN '2025-01-01' AND '2025-12-31 23:59:59'; -- пришлось сделать так, тк дата 2026-01-01 тоже есть в таблице +-- в этом запросе за год - читатется только одна партиция - orders_2025, НО если в условии все же написать AND '2026-01-01', то читаются две партиции orders_2025 и orders_2026 и время выполнения запроса намного больше тк выполняется full scan обеих таблиц + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part +WHERE + EXTRACT('year' FROM order_date) IN ('2024', '2025'); -- так читаются все три партиции, 2026 почему-то тоже захватывается, но по итогу после сканирование у таблицы orders_2026 - rows = 0 + +EXPLAIN ANALYZE +SELECT + * +FROM + orders_part; +-- так просто сканируются все три партиции, но без фильтров, как было в предидущем варианте + + +-- Задание 4. LIST‑партиции по стране пользователей +DROP TABLE IF EXISTS users_part; +CREATE TABLE users_part ( + id int8 NOT NULL, + "name" varchar(100) NOT NULL, + age int2 NULL, + email varchar(100) NULL, + country bpchar(2) NULL, + is_active bool NULL, + created_at timestamptz NOT NULL, + PRIMARY KEY (id, country) +) PARTITION BY LIST (country); + +CREATE TABLE users_ru PARTITION OF users_part +FOR VALUES IN ('RU'); +CREATE TABLE users_us PARTITION OF users_part +FOR VALUES IN ('US'); +CREATE TABLE users_gb PARTITION OF users_part +FOR VALUES IN ('GB'); +CREATE TABLE users_de PARTITION OF users_part +FOR VALUES IN ('DE'); +CREATE TABLE users_fr PARTITION OF users_part +FOR VALUES IN ('FR'); +CREATE TABLE users_jp PARTITION OF users_part +FOR VALUES IN ('JP'); +CREATE TABLE users_uk PARTITION OF users_part +FOR VALUES IN ('UK'); +CREATE TABLE users_kz PARTITION OF users_part +FOR VALUES IN ('KZ'); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country = 'RU'; +-- запрос скаинровал только партицию users_ru + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + country IN ('RU', 'KZ'); +-- запрос сканировал только партиции users_ru и users_kz + +EXPLAIN ANALYZE +SELECT + * +FROM + users_part +WHERE + age BETWEEN 44 AND 66; +-- сканировал не просто users_part, а каждую партицию по отдельности + + +-- Задание 5. HASH‑партиции по user_id в логах +DROP TABLE IF EXISTS user_logs_part; +CREATE TABLE user_logs_part ( + id int8, + user_id bigint NOT NULL, + created_at timestamp NOT null, + event_type text NOT NULL, + payload jsonb, + PRIMARY KEY (id, user_id) +) PARTITION BY hash (user_id); + +CREATE TABLE user_logs_p1 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 0); + +CREATE TABLE user_logs_p2 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 1); + +CREATE TABLE user_logs_p3 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 2); + +CREATE TABLE user_logs_p4 PARTITION OF user_logs_part +FOR VALUES WITH (modulus 4, remainder 3); + +-- сделал insert который попросил сгенерировать нейронку + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE user_id = 228; +-- читает только user_logs_p2 + +EXPLAIN ANALYZE +SELECT * FROM user_logs_part WHERE created_at >= current_date - INTERVAL '7 days'; +-- читает все партиции + +-- Задание 6 +-- email, чтобы искать пользователя быстрее, created_at для сортировки и where +CREATE UNIQUE INDEX idx_users_ru_email ON users_ru(email); +CREATE INDEX idx_users_ru_created_at ON users_ru(created_at); + +CREATE UNIQUE INDEX idx_users_us_email ON users_us(email); +CREATE INDEX idx_users_us_created_at ON users_us(created_at); + +CREATE UNIQUE INDEX idx_users_gb_email ON users_gb(email); +CREATE INDEX idx_users_gb_created_at ON users_gb(created_at); + +CREATE UNIQUE INDEX idx_users_de_email ON users_de(email); +CREATE INDEX idx_users_de_created_at ON users_de(created_at); + +CREATE UNIQUE INDEX idx_users_fr_email ON users_fr(email); +CREATE INDEX idx_users_fr_created_at ON users_fr(created_at); + +CREATE UNIQUE INDEX idx_users_jp_email ON users_jp(email); +CREATE INDEX idx_users_jp_created_at ON users_jp(created_at); + +CREATE UNIQUE INDEX idx_users_uk_email ON users_uk(email); +CREATE INDEX idx_users_uk_created_at ON users_uk(created_at); + +CREATE UNIQUE INDEX idx_users_kz_email ON users_kz(email); +CREATE INDEX idx_users_kz_created_at ON users_kz(created_at); +-- должны ускориться запросы с поиском пользователей по email, и запросы с сортировкой по дате или с фильтрацией по интервалам \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql new file mode 100644 index 0000000..c3f5e12 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/3. Шарды/practice_sharding.sql @@ -0,0 +1,19 @@ +-- Задание 1. Выбор ключа шардирования +-- Ну я тут чето нихера не понял +-- Вот users лучше всего шардировать по id, а с orders непонятно. Я не до конца понял логику шардирования, если orders и users на разных шардах, то наверное лучше шардировать по user_id. Но в тоже время логично было бы сделать по order_date. +-- Короче я очень плохо понял шардирование, сейчас поищу объяснение на ютубчике и напишу новый ответ. +-- В нашем случае мы получается обсуждаем горизонтальное шардирование +-- Как я понял, если шардируются две таблицы, то они шардирются по одному полю у нас это users(id) и orders(user_id). +-- Я выбрал users(id), тк это уникальное поле и есть join по нему в orders - user_id +-- Шардирование будет происходить по хэшу. Быстрее будут выполняться запросы в пределах одного шарда, а запросы где данные берутся из разных шардов да ещё и join - явно станут сложнее. + +-- Задание 2. Добавление нового шарда +-- Проблемы: мне кажется временная приостановка записи и чтения таблиц на время 'перешардирования'. +-- План: 1. остановить запись и чтение данных; 2. создать 8 новых шардов; 3. перераспределить данные из старых четырех в восемь новых шардов; 4. удалить старые 4 шарда; 5. поменять логику изменения и чтение в бэк энде например 6. разрешить запись и чтение. конец + +-- Задание 3. Шардинг против партиционирования +-- При партицировании таблицы данные хранятся на одной бд, внутри одной таблицы, но эта таблица внутри себя разделена на партиции для ускорения запросов к ней. +-- При шардировании таблицы данные хранятся на разных серверах, то есть таблица делится на части(при горизонтальном шардировании), и каждая часть таблицы отправляется на разные сервер. + +-- Пример партицирования без шардирования: когда сервер не переполнен, но таблица слишком большая, и и запросы к ней замедляются, тогда таблица делится на партиции, для более быстрого выполнения запросов к ней. +-- Пример когда без шардирования не обойтись: это когда сервер перегружен, и физически невозможно добавлять новые данные и чтение таблиц очень медленное, тк для этого чтения сервер не может выделять должное количество ресурсов. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql new file mode 100644 index 0000000..d9adc7e --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/4. Репликация/practice_replication.sql @@ -0,0 +1,40 @@ +-- Задание 1. Где и зачем понадобится репликация +-- На примере онлайн-магазина +-- Пример 1: мастер сервер перестал работать(по любым причинам), и чтобы не останавливать работу магазина - делаем реплику мастером. Если бы небыло репликации работа магазина приостановилась бы на какое время. +-- Пример 2: на мастер сервере даже без чтения высокая нагрузка с инсертами, апдейтами, а чтение происходит с реплики, например отчеты для аналитиков читаются с реплики чтобы не перегружать мастер. Если убрать репликацию, то сервер будет один и может перегрузиться и упасть. +-- Пример 3: я сдеал первыве два примера настоллько обобщенными, что они покрываются все более конкретные примеры, и я не могу придумать пример 3. + + +-- Задание 2. Сравнение синхронной и асинхронной репликации +-- * как ведёт себя запись при нормальной работе; +-- * синхронная репликация - мастер ждет пока реплика завершит копирование, наверное блокирует строку в таблице как в тарнзакции строка блокируется пока транзакция не завершится. +-- * асинхронная репликация - мастер ничего не ждет - его главная задача, что он отправил даные в реплику, и реплика уже пусть сама разбирается и сохраняетс всё. +-- * что происходит при падении мастера; +-- * я думаю при обоих видах репликаций - реплика становится мастером. +-- * какие риски потери/отставания данных есть. +-- * с синхронной - рисков с отставанием и потерями вроде нет. +-- * в случае с асинхронной - реплика может не успеть сохранить что-то одно, как придет уже что-то второе в туже строку например, или может произойти какой-то сбой, и данные прийдут некорректно - получается транзакция просто не откатится, а будет как будет. + +-- платежному сервису нужен синхронный вид репликации, тк ошибки и неточность недопустимы. +-- аналитической витрины нужен асинхронный вид репликации, тк он быстрее, и небольшая неточность не критична. + + +-- Задание 3. Логическая и физическая репликация +-- * думаю логическая репликация это больше про то, что мы видим и делаем сами, по типу sql запрос написать, а физическая репликация - это про то, что происходит на уровне субд/сервера, что данные перетекают байтах, WAL-журналов(я погуглил что-это - это грубо говоря план изменений, но пока не сами изменения) +-- Оказывается про логические физические репликации я тоже ничего не понял, сейчас прогуглю, и напишу ответ заново. +-- * у логической репликации изменения передаются как sql запросы, для отдельных талиц например, а физическаая репликация изменяется побайтово создавая полную копию мастера +-- * у логической репликации высокая гибкость, можно выбирать что обновлять, а что нет, а у физической репликации нулевая гибкость - создается просто копия мастера 1 в 1 +-- * как я понял для миграция лучше всего логическая репликация - тк высокая гибкость, а физическая подходит для резервной копии + +-- * для физической репликации - банковская система, тк физичская быстрее и копия 1 в 1 делается, а для логическрй репликации я не могу ничего придумать. + + +-- Задание 4. Схема один мастер — несколько реплик для чтения +-- * на мастер отправлять строго unsrt update delete только с изменениями, а на реплики select, только с чтением +-- * проблемы из-за отсавания: 1. пользователь обновил фото профиля, а изменилось оно на странице толькко через 1-2 минуты; 2. один пользователь купил последний товар, а реплика не успела обновиться, и другой пользователь тоже попытался заказать этот товар, и у него вылезла ошибка, получается что товар отображается в наличии а купить нельзя; 3. в банках если будут отставать то там вообще жопа будет со счетами, но там получается синхронная репликация. +-- * наверное можно закрыть доступ на изменения для строк которые ещё не до конца реплицировались + +-- Задание 5. Мульти‑мастер: когда это оправдано +-- * главные плюсы: 1. распределение нагрузки между серверами 2. каждая мастер репликация отвечает только за свою зону ответсвенности +-- * сложности: 1. конфликты, что на разных мульти репликах могут быть разные записи по одному и тому же значению и будет не ясно что важнее оставить; 2. согласованность - кадая мульти реплика должна работать только в пределах своей зоны ответственности; 3. сложность настройки - я думаю имеется ввиду защита от конфликтов и поддержка согласованности. +-- * у меня в голове только мысль о разделении отвественности по хэшу, что одна мастер реплика отвечает за один кусок, другая за другой, по типу шардирования, только без деления таблиц, они как бы и дальше между собой реплицируются, но каждая изменят обновляет и делает записи только на своем куске. Наверное такой способ поможет при больших таблицах с частыми изменениями(хотя по суть шардирование лучше испольщовать тогда, а не мульти-мастер). Неэффективно будет на таблицах с редкими изменениями, и не очень больших таблицах. \ No newline at end of file diff --git a/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql new file mode 100644 index 0000000..989b738 --- /dev/null +++ b/mnoskov/Теория БД/1. Ускорение и масштабирование/5. Индексы Алгоритмы/practice_indexes_algorithms.sql @@ -0,0 +1,32 @@ +-- Задание 1 +-- 1. будет +-- 2. не будет. можно изменить запрос - убрать lower +-- 3. не будет. нужно убрать like +-- 4. не будет. нужно убрать like +-- 5. будет +-- 6. не будет, тк фильтрация по полю без индекса + + +-- Задание 2 +-- не понял задание, сначала нужно каждому запросу сказать что индекс не нужен/бсполезен, а потом создать индекс? +-- 1. по идее есть смысл создать два индекса - по user_id и created_at, или состваной. + CREATE INDEX idx_orders_user_id ON orders(user_id); + CREATE INDEX idx_orders_created_at ON orders(created_at); +-- 2. тут никакой индекс не нужен +-- 3. тут тоже индекс не нужен, тк у status всего 4 значения + + +-- Задание 3 +-- 1. тут hash, тк равенство +-- 2. здесь индекс и не нужен, тк у status всего 4 значения +-- 3. тут B-tree, тк интервал(<,>) +-- 4. конкретно в этом запросе лучше hash, тк равенство, но в целом думаю на user_id лучше B-tree, тк часто сортируют по нему + +-- * hash иммет смысл где просто равенство +-- * B-tree обязательно там где интервалы, > или <, и где order by +-- * в варианте 2, где у столбца всего 4 уникальных значения + + +-- Задание 4 +-- * первый составной нужен для запросов с фильтрацией по нескольким полям +-- * второй не нужен, тк user_id будет рабоать эффективно и в первом случае \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql new file mode 100644 index 0000000..d9fa2a2 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/1. ACID и BASE/practice_ASID&BASE.sql @@ -0,0 +1,12 @@ +-- Задание 1. Интернет‑банк +-- * в этом случае важна атомарность, и первый запрос должен был откатиться обратно, и не дублироваться как я понимаю. +-- * если не будеь атомарноти, то после сбоев могут произойте несостыковки, напрмер у пользователя деньги списались, а заказ не оформился, или наоборот + + +-- Задание 2. Лента новостей и лайки +-- * эта система ближе к BASE, тк согласование узлов происходит не сразу, и у кого то 22 лайка, а у кого-то 24. + + +-- Задание 3. Заказы в интернет‑магазине +-- * у первого случая принципы ACID - это нужно чтбы критическа важная информация обновлялась без ошибок, и чтобы небыло несостыковок +-- * во втором случае принципы BASE - можно немного потерпеть неточность, тк это некритично \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql new file mode 100644 index 0000000..33e81b8 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/2. Уровни изоляции/practice_isolation_levels.sql @@ -0,0 +1,13 @@ +-- Задание 1: Разбери ситуацию +-- 1) 200 +-- 2) 100 +-- 3) Результаты разные тк на уровне Read Committed разрешено неповторяемое чтение, а на уровне Repeatable Read неповторяемое чтенине запрщено, и мы видим то, что было на момент начала транщакции. + +-- Задание 2: Практика +-- При Repeatable Read, я увидел во втором select значение - 100, потому что изменение произошли до того как завершилась транзакция с чтением, и зафиксировалось то значение которые было на момент начала транзакции с чтением +-- При Read Committed, во втором select было значение - 200, тк этот уровень изолированности подразумевает чтение закоммиченных(другой транзакцией) данных даже если текущая транзакция ещё не завершена + +-- Задание 3: Найди ошибку +-- 1) Так произошло потому, что выбран некорректный уровень изоляции. +-- 2) Тут подойдет serializavle. +-- 3) Решить эту проблему без смены изоляции можно с помощью проверки баланса перед списанием, и если денег на балансе будет недостаточно, то откатить транзакцию. Такое решение подойдет только с уровнем изоляции Read Commited, для Repeatable Read такой способ уже не подойдет. \ No newline at end of file diff --git a/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql new file mode 100644 index 0000000..d66aec9 --- /dev/null +++ b/mnoskov/Теория БД/2. Гарантии работы БД/3. CAP-теорема/practice_CAP_theorem.sql @@ -0,0 +1,11 @@ +-- Задание 1: Определи тип базы +-- 1) CP +-- 2) AP +-- 3) CA +-- 4) AP + + +-- Задание 2: Что выбрать? +-- CP - должна быть согласованность, чтобы последний билет достался только одному, у двух людей не может быть одинаковый билет +-- Если выбрать CP, то все будет четко, и билет достентся только одному пользователю +-- Если выбрать AP, то билет может достаться и тому и тому, что недопустимо \ No newline at end of file diff --git a/pmikhailov/postgresql/homework_window_functions.sql b/pmikhailov/postgresql/homework_window_functions.sql new file mode 100644 index 0000000..4b799ea --- /dev/null +++ b/pmikhailov/postgresql/homework_window_functions.sql @@ -0,0 +1,152 @@ +/* Задание 1 */ + +SELECT + u.id, + u."name", + o.id AS order_id, + o.order_date, + ROW_NUMBER() OVER ( + PARTITION BY u.id + ORDER BY o.order_date) AS order_number_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 2 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue -- добавил агреграт по всем поз. в заказе + FROM + order_items oi + GROUP BY oi.order_id + ) +SELECT + u.id AS user_id , + u."name", + o.id AS order_id, + o.order_date, + orc.order_revenue, + sum(orc.order_revenue) OVER ( + PARTITION BY u.id + ORDER BY o.order_date + ) AS running_revenue_for_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id +WHERE o.status NOT IN ('cancelled', 'refunded') -- добавил условие не включать отмененные заказы + + + +/* Задание 3 */ + +WITH user_revenue_cte AS ( + SELECT + o.user_id, + sum(oi.quantity * oi.price_at_purchase) AS user_total_revenue + FROM + orders o + INNER JOIN + order_items oi ON o.id = oi.order_id + GROUP BY o.user_id) -- добавил group by, костыль убран +SELECT + u."name", + urc.user_total_revenue, + RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_rank, + DENSE_RANK() OVER (ORDER BY urc.user_total_revenue DESC) AS revenue_dense_rank +FROM + users u +INNER JOIN + user_revenue_cte urc ON u.id = urc.user_id; + + +/* Задание 4 */ + +SELECT + u.name AS user_name, + o.id AS order_id, + o.order_date, + FIRST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS first_order_date, + LAST_VALUE(o.order_date) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + ) AS last_order_date +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id; + + +/* Задание 5 */ + +SELECT + u.name, + o.id, + count(o.id) OVER ( + PARTITION BY o.user_id) AS orders_per_user +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id + + +/* Задание 6 */ + +WITH order_revenue_cte AS ( + SELECT + oi.order_id, + sum(oi.quantity * oi.price_at_purchase) AS order_revenue + FROM + order_items oi + GROUP BY oi.order_id) -- добавил group by по order_id, теперь это сумма всего заказа, а не строки +SELECT + u.name, + o.id, + o.order_date, + orc.order_revenue, + lag(orc.order_revenue) OVER ( + PARTITION BY o.user_id + ORDER BY o.order_date + ) AS prev_order_revenue +FROM + users u +INNER JOIN + orders o ON u.id = o.user_id +INNER JOIN + order_revenue_cte orc ON o.id = orc.order_id; + + +/* Задание 7 */ + +WITH category_revenue_cte AS ( + SELECT + p.category, + date_trunc('day', o.order_date)::date AS order_period, + sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue + FROM + products p + LEFT JOIN + order_items oi ON p.id = oi.product_id + INNER JOIN + orders o ON oi.order_id = o.id + GROUP BY 1, 2) +SELECT + cr_cte.*, + sum(cr_cte.daily_category_revenue) OVER ( + PARTITION BY cr_cte.category + ORDER BY cr_cte.order_period + ROWS UNBOUNDED PRECEDING + ) AS running_category_revenue +FROM + category_revenue_cte cr_cte; + \ No newline at end of file diff --git a/pmikhailov/postgresql/joins.sql b/pmikhailov/postgresql/joins.sql new file mode 100644 index 0000000..e83b069 --- /dev/null +++ b/pmikhailov/postgresql/joins.sql @@ -0,0 +1,158 @@ +/* Задача 1 + * Джойним в таком порядке т.к. не каждый юзер делал заказ + * + судя по порядку колонок мы хотим анализировать заказы + * (по сути выглядит так же как INNER) + * Но в обратном порядке можем увидеть кто не делал заказ + * и добавить их в условную рассылку + */ +SELECT + o.id AS order_id, + o.order_date, + status, + u."name" AS user_name, + country +FROM + orders o +LEFT JOIN + users u ON o.user_id = u.id + + +/* Задача 2 */ + +SELECT + order_id, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + order_items oi +LEFT JOIN + products p ON oi.product_id = p.id -- AND oi.order_id = 1 из правой таблицы будут NULL там, где оба условия не выполнены +WHERE order_id = 1 -- в задании другой номер - 101, надо исправить в Notion, данные на п.4 уже другие + + +/* Задача 3 */ + +SELECT + order_id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.user_id = u.id +INNER JOIN + products p ON oi.product_id = p.id; + + +/* Задача 4 */ + +SELECT + u."name" AS user_name, + o.id AS order_id, + status +FROM + users u +LEFT JOIN + orders o ON u.id = o.user_id + + +/* Задача 5 */ + +SELECT + o.user_id, + u."name" AS user_name +FROM + users u +LEFT JOIN + orders o ON u.id = o.user_id +WHERE o.user_id IS NOT NULL + + +/* Задача 6 */ + +SELECT + o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + quantity +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE p."name" ILIKE '%Mi Band 7%'; + + +/* Задача 7 */ + +SELECT + p.name AS product_name, + order_id, + quantity +FROM + products p +FULL JOIN + order_items oi ON p.id = oi.product_id +ORDER BY product_name; + + +/* Задача 8 */ + +SELECT + o.id, + u."name" AS user_name, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + users u ON o.user_id = u.id +LEFT JOIN + products p ON oi.product_id = p.id +WHERE + o.status = 'paid'; + + +/* Задача 9 */ + +SELECT + o.id AS order_id, + o.order_date, + p."name" AS product_name, + quantity, + price_at_purchase +FROM + orders o +LEFT JOIN + order_items oi ON o.id = oi.order_id +INNER JOIN + products p ON oi.product_id = p.id +INNER JOIN + users u ON o.user_id = u.id +WHERE u."name" ILIKE '%alice%'; + + +/* Задача 10 +* Задача противоречит изначальным органичениям NOT NULL +*/ + +SELECT + o.id AS order_id, + user_id, + status +FROM + orders o +FULL JOIN + order_items oi ON oi.order_id = o.id +WHERE product_id IS NULL AND o.status = 'created'; \ No newline at end of file diff --git a/test/postgresql/homework.sql b/test/postgresql/homework.sql new file mode 100644 index 0000000..3df145e --- /dev/null +++ b/test/postgresql/homework.sql @@ -0,0 +1 @@ +select * from users; \ No newline at end of file diff --git a/test/postgresql/homework2.sql b/test/postgresql/homework2.sql new file mode 100644 index 0000000..3df145e --- /dev/null +++ b/test/postgresql/homework2.sql @@ -0,0 +1 @@ +select * from users; \ No newline at end of file diff --git a/vberetar/postgresql/ddl.sql b/vberetar/postgresql/ddl.sql new file mode 100644 index 0000000..ce86724 --- /dev/null +++ b/vberetar/postgresql/ddl.sql @@ -0,0 +1,38 @@ +CREATE TABLE users ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + age SMALLINT CHECK (age > 0), + email VARCHAR(100) NOT NULL UNIQUE, + country VARCHAR(50), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ NOT NULL +); +-- age - для возраста не нужны болшие числа +-- country - можно было бы использовать VARCHAR(2), я бы тут спросил у тимлида как будет удобней использовать поле) +-- created_at - нужно знать не только дату но и время создания/регистрации +-- id - если не прописать not null в primary key, то not null ставится автоматически? + +CREATE TABLE products ( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category VARCHAR(50), + price DECIMAL(10, 2) NOT NULL, + in_stock BOOLEAN DEFAULT true, +); + +CREATE TABLE orders( + id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + user_id INT, + order_date TIMESTAMPTZ NOT NULL, + status VARCHAR(20), + FOREIGN KEY (user_id) REFERENCES users(id) +); +-- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM + +CREATE TABLE order_items( + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL, + price_at_purchase DECIMAL(10, 2) NOT NULL, + PRIMARY KEY (order_id, product_id) +); \ No newline at end of file diff --git a/vberetar/postgresql/dml.sql b/vberetar/postgresql/dml.sql new file mode 100644 index 0000000..24e8d83 --- /dev/null +++ b/vberetar/postgresql/dml.sql @@ -0,0 +1,41 @@ +INSERT INTO users (name, age, email, country, is_active, created_at) +VALUES ('Alice', 25, LOWER('alice@example.com'), 'US', true, '2024-01-10 10:00:00'), + ('Bob', 30, LOWER('bob@example.com'), 'UK', true, '2024-02-05 09:30:00'), + ('Carol', 27, LOWER('carol@example.com'), 'US', false, '2024-03-01 15:20:00'), + ('Diana', 22, LOWER('diana@GMAIL.com'), 'US', true, '2024-01-12 08:15:00'), + ('Evan', 35, LOWER('EVAN@example.com'), 'UK', true, '2024-01-18 19:40:00'), + ('Farida', 29, LOWER('farida@example.com'), 'KZ', true, '2024-02-20 11:05:00'), + ('George', 31, LOWER('george+test@example.com'), 'US', true, '2024-03-05 16:45:00'), + ('Ivan', 26, LOWER('ivan@example.com'), 'RU', true, '2024-01-11 14:30:00'), + ('Julia', 28, LOWER('julia@example.com'), 'US', true, '2024-03-07 18:05:00'); + +INSERT INTO products (name, category, price, in_stock) +VALUES ('iPhone 15', 'Электроника', 80000, true), + ('Kindle', 'Электроника', 15000, true), + ('Кроссовки Nike', 'Одежда', 9000, true), + ('Кроссовки NoName', 'Одежда', 3000, false), + ('Наушники Pro', 'Электроника', 12000, true), + ('Фитнес‑браслет', 'Гаджеты', 6000, true), + ('Подарочная карта', 'Сервисы', 1000, true); + +INSERT INTO orders (user_id, order_date, status) +VALUES (1, '2025-01-10 12:00:00', 'paid'), + (3, '2025-01-12 09:30:00', 'cancelled'), + (1, '2025-01-15 18:20:00', 'paid'), + (2, '2025-01-20 11:05:00', 'created'), + (4, '2025-02-01 17:45:00', 'paid'), + (5, '2025-02-10 10:10:00', 'refunded'), + (7, '2025-03-01 09:00:00', 'paid'), + (9, '2025-03-05 19:30:00', 'paid'); + +INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase) +VALUES (1, 1, 1, 78000), + (1, 3, 2, 8500), + (2, 2, 1, 14000), + (3, 5, 1, 11000), + (3, 7, 1, 1000), + (4, 2, 1, 15000), + (5, 6, 1, 6000), + (6, 1, 1, 82000), + (7, 3, 1, 9000), + (8, 4, 2, 2800); diff --git a/vberetar/postgresql/sql_aggregation_grouping.sql b/vberetar/postgresql/sql_aggregation_grouping.sql new file mode 100644 index 0000000..6140687 --- /dev/null +++ b/vberetar/postgresql/sql_aggregation_grouping.sql @@ -0,0 +1,105 @@ +-- Задание 1 +SELECT category, COUNT(*) AS products_count +FROM products +GROUP BY category +ORDER BY products_count DESC; + +-- Ответ на вопросы +-- 1. С ORDER BY гораздо удобней читать результат, без него данные отдаются в непонятном порядке. +-- 2. Для категории smartphone добавится +1 значение. + + +-- Задание 2 +SELECT order_id, + SUM(quantity) AS total_quantity, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Если убрать GROUP BY и order_id то запрос выдаст 1 строку со всеми суммами total_quantity и order_revenue. +-- Если убрать только GROUP BY и не убрать order_id то выдаст ошибку. Так как нельзя оставлять неагрегированные столбцы без GROUP BY. +-- 2. Если добавить product_id без добавления её в GROUP BY или агрегат то выдаст ошибку. + + +-- Задание 3 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. По задаче нам же нужно сгруппировать и посчитать сумму по категориям а не по id товарам. +-- 2. Если добавить p.name в SELECT без добавления её в GROUP BY или агрегат то выдаст ошибку. Нельзя вставлять неагрегированные столбцы без GROUP BY. + + +-- Задание 4 +SELECT oi.order_id, + SUM(oi.quantity * oi.price_at_purchase) AS order_revenue +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +WHERE o.status = 'paid' +GROUP BY oi.order_id +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. В этом задании мы получили сумму(выручку) по заказам оплаченным, а в прошлом задании мы получили сумму(выручку) по категориям товаров. +-- 2. Посчитать выручку, сколько заработала компания. Остальные заказы еще не оплачены. + + +-- Задание 5 +SELECT p.category, + SUM(oi.quantity * oi.price_at_purchase) AS category_revenue +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY p.category +HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000 +ORDER BY category_revenue; + +-- Ответ на вопросы +-- 1. WHERE не принимает агрегирующие функции, можно сделать так - WHERE oi.quantity * oi.price_at_purchase > 1000 +-- 2. WHERE фильтрует до GROUP BY, а HAVING после GROUP BY. + + +-- Задание 6 +SELECT country, + COUNT(*) AS active_users_count +FROM users +WHERE is_active = TRUE +GROUP BY country +HAVING COUNT(*) > 2 +ORDER BY active_users_count DESC; + +-- Ответ на вопросы +-- 1. Предположу что отбрасывает неактивных пользователей сразу, перед групировкой и агрегаций. В дальнейшем может сэкономить на времени вычисления, особенно если данных будет много. +-- 2. Пустую таблицу без строк. + + +-- Задание 7 +SELECT p.name AS product_name, + SUM(oi.quantity) AS total_quantity_sold +FROM order_items oi +JOIN products p ON p.id = oi.product_id +GROUP BY product_name +ORDER BY total_quantity_sold DESC +LIMIT 3; + +-- Ответ на вопросы +-- 1. Тут мы выясняем какой товар самый ходовой и сколько его продается, а рейтинг по выручке показывает сколько зарабатывает фирма от продажи товара. +-- 2. В случаях когда, например, нужно выяснить какой товар хорошо продается чтоб его закупить на склад для продажи. + + +-- Задание 8 +SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue +FROM order_items +GROUP BY order_id +HAVING SUM(quantity * price_at_purchase) < 100 +ORDER BY order_revenue DESC; + +-- Ответ на вопросы +-- 1. Есть товары дешевые и люди которые мало зарабатывают, ещё есть всякие скидки на ряд товаров. +-- 2. Я думаю проще увидеть отфильтрованные заказы, чем листать весь список с заказами. diff --git a/vberetar/postgresql/sql_cte.sql b/vberetar/postgresql/sql_cte.sql new file mode 100644 index 0000000..080d9bc --- /dev/null +++ b/vberetar/postgresql/sql_cte.sql @@ -0,0 +1,118 @@ +-- Задача 1 +SELECT u.name +FROM users u +WHERE id IN ( + SELECT o.user_id + FROM orders o +) +ORDER BY u.name; +-- Ответ на вопросы +-- 1. JOIN склеивает одну таблицу со второй, а подзапрос с IN выполняет запрос и получает список с user_id, +-- далее по этому списку отфильтровывает пользователей в основном запросе. +-- 2. Когда запрос простой и короткий. + + +-- Задача 2 +SELECT u.id, + u.name, + u.email +FROM users u +WHERE NOT EXISTS ( + SELECT 1 + FROM orders o + WHERE o.user_id = u.id +); +-- Ответ на вопросы +-- 1. Предположу что NOT EXISTS более бещопасно работает с null. +-- 2. Отправить таким пользователям рекомендацию, специальный промокод на скидку для первого заказа или рекламу. + + +-- Задача 3 +SELECT u.name, + ( + SELECT COUNT(*) + FROM orders o + WHERE u.id = o.user_id + ) AS orders_count +FROM users u +ORDER BY orders_count DESC, name; +-- Ответ на вопросы +-- 1. Если подзапрос SELECT вернётся две и более строки то будет ошибка. +-- 2. Сложность возрастет и скорее всего нужно будет оптимизировать запрос. + + +-- Задача 4 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_price + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + trc.total_price AS total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Проще читать запрос "посчитать выручку по пользователям" когда вынесен отдельно. +-- 2. Вроде как в postgres можно в базу данных записывать функции, если я ничего не путаю. +-- И далее вызывать эту функцию при необходимости, так можно избежать дублирования кода. + + +-- Задача 5 +WITH product_price_at_purchase_cte AS ( + SELECT oi.product_id, + SUM(oi.quantity * oi.price_at_purchase) AS product_revenue + FROM order_items oi + GROUP BY oi.product_id +) +SELECT p.name AS product_name, + p.category, + ppps.product_revenue +FROM products p +JOIN product_price_at_purchase_cte ppps ON ppps.product_id = p.id +ORDER BY ppps.product_revenue DESC +LIMIT 3; +-- Ответ на вопросы +-- 1. Возможно потому что products оcновная таблица, сналчала нужно из order_items достать агрегацию по id а потом склеивать с products. +-- 2. Через CTE мы получаем готовую таблицу и далее делаем с ней что хотим, +-- а в гуруппировке через GROUP BY нельзя добавить дополнительные поля name и category. + + +-- Задача 6 +WITH total_revenue_cte AS ( + SELECT o.user_id, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + GROUP BY o.user_id +) +SELECT u.name, + u.email, + trc.total_revenue +FROM users u +JOIN total_revenue_cte trc ON trc.user_id = u.id +WHERE u.is_active = TRUE AND trc.total_revenue > 1000 +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Думаю как то так - user_most_expenses +-- 2. Можно предлагать рекламу товаров, анализировать что пользователи покупали и потом предлагать схожую продукцию. + + +WITH order_revenue_cte AS ( + SELECT order_id, + SUM(quantity * price_at_purchase) AS order_revenue + FROM order_items + GROUP BY order_id +) +SELECT o.id, + o.order_date, + oc.order_revenue +FROM orders o +JOIN order_revenue_cte oc ON oc.order_id = o.id +WHERE o.status = 'paid' AND oc.order_revenue < 100; +-- Ответ на вопросы +-- 1. Какие реальные причины таких заказов, я думаю что просто не все люди богатые что бы заказывать на большие суммы (если я правильно понял статус paid) +-- Чтобы узнать злоупотребления промокодами нужно копаться в других данных, где хранятся данные о введенных промокодах. +-- 2. Я думаю выгоднее фильтровать внутри подзапроса, чтоб подзапрос не создавал большую таблицу и основному запросу не пришлось ее перебирать. diff --git a/vberetar/postgresql/sql_dates_strings_logic.sql b/vberetar/postgresql/sql_dates_strings_logic.sql new file mode 100644 index 0000000..447954a --- /dev/null +++ b/vberetar/postgresql/sql_dates_strings_logic.sql @@ -0,0 +1,90 @@ +-- 1 Задача +SELECT name, + CAST(created_at AS date) AS created_date, + CURRENT_DATE - CAST(created_at AS date) AS days_since_registration +FROM users +WHERE created_at <= CURRENT_DATE - INTERVAL '180 days' +ORDER BY days_since_registration DESC; +-- Ответ на вопросы +-- 1. Возможно предложить старым пользователям скидки всякие или интересное предложение для удержания пользователям. Или можно проанализировать их заказы. +-- 2. Я предположу что удобней не марочится со временем а сразу работать с датой + + +-- 2 Задача +SELECT name, + LOWER(email) AS email_clean, + SUBSTRING(LOWER(email) FROM POSITION('@' IN LOWER(email)) + 1) AS domain +FROM users +WHERE email IS NOT NULL +ORDER BY domain, name; +-- Ответ на вопросы +-- 1. Приводим email к нижнему регистру для чистоты данных, +-- а зачем нужен отдельно домен не знаю, может есть какая нибудь важная разница в почтовых сервисах. +-- 2. Получим в колонке domain null. + + +-- 3 Задача +SELECT name, + age, + CASE + WHEN age < 25 THEN 'young' + WHEN age BETWEEN 25 AND 35 THEN 'adult' + WHEN age > 35 THEN 'senior' + ELSE 'unknown' + END AS age_group +FROM users +ORDER BY age; +-- Ответ на вопросы +-- 1. Возможно данные и статистику быстрее и удобнее получить через sql а не через код приложения. +-- 2. Пользователи получат null в колонке, оно просто не пройдет условие. + + +-- 4 Задача +SELECT name, + created_at::date AS created_date, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN is_active = TRUE THEN 'hot' + ELSE 'regular' + END AS hot_flag +FROM users +WHERE is_active = TRUE + AND created_at >= CURRENT_DATE - INTERVAL '30 days'; +-- Ответ на вопросы +-- 1. В WHERE мы получим только пользователей за последние 30 дней а в CASE мы просто получаем метку 'hot' или 'regular'. +-- И в CASE мы не фильтруем по каким либо параметрам а только прописываем условие что записать. +-- 2. Какие нибудь рекомендации или скидку на первый заказ. + + +-- 5 Задача +SELECT COUNT(*) AS total_users, + COUNT(email) AS with_email, + COUNT(*) - COUNT(email) AS without_email, + ROUND(COUNT(country) * 100.0 / COUNT(*), 2) AS percentage_users +FROM users; +-- Ответ на вопросы +-- 1. Так гораздо надежнее и правильнее. +-- 2. Установить ограничение на создание пользователя, чтоб поле email было не нулевым. + + +-- 6 Задача +SELECT name, + CURRENT_DATE - created_at::date AS days_since_registration, + CASE + WHEN created_at::date >= CURRENT_DATE - INTERVAL '30 days' THEN 'new' + WHEN created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days' THEN 'middle' + ELSE 'old' + END AS tenure_group, + is_active +FROM users +ORDER BY created_at::date >= CURRENT_DATE - INTERVAL '30 days', + created_at::date < CURRENT_DATE - INTERVAL '30 days' + AND created_at::date > CURRENT_DATE - INTERVAL '180 days', + created_at::date < CURRENT_DATE - INTERVAL '180 days', + days_since_registration; +-- Ответ на вопросы +-- 1. В сегментации по возрасту показывает возраст пользователя, +-- а в сегментации стаж показывает на сколько долго пользователь пользуется сервисом(приложением) +-- 2. CURRENT_DATE показывает сегодняшний день и если tenure_group пользоваться каждый день, то будет смещатся интервал в логическом выражении CASE. +-- В статическом поле дание остаются записанными пока их не изменят, течение времени на них никак не будет влиять. diff --git a/vberetar/postgresql/sql_filters.sql b/vberetar/postgresql/sql_filters.sql new file mode 100644 index 0000000..74925dd --- /dev/null +++ b/vberetar/postgresql/sql_filters.sql @@ -0,0 +1,56 @@ +-- Задача 1 +SELECT name, category, price, + price * 1.3 AS price_with_markup, + price * 1.5 AS price_with_vat +FROM products +WHERE (price BETWEEN 100 AND 1300) + AND category NOT IN ('smartwatch') + AND in_stock = TRUE; + +-- Вопрос: +-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему? +-- Ответ: +-- Для меня BETWEEN 100 AND 1300 читается лучше, сразу понятен срез от 100 и 1300. + + +-- Задача 2 +SELECT name, price, + price * 0.85 AS price_with_discount +FROM products +WHERE (price < 100 OR price > 1000); + +-- Вопрос: +-- Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно? +-- Ответ: +-- Потому что условие может не правильно отобразить результат, AND выполняется раньше OR. +-- Для надежности лучше скобками прописывать явно. + + +-- Задача 3 +SELECT id, name, category, price +FROM products +WHERE price >= 100 + AND category IN ('tablet', 'smartphone', 'mouse') +ORDER BY price DESC; + + +-- Задача 4 +SELECT name, category, price +FROM products +WHERE name NOT LIKE '%iPhone%'; + + +-- Задача 5 +SELECT name, category, price +FROM products +WHERE name ILIKE '%pro%'; + +SELECT name, category, price +FROM products +WHERE name ILIKE '%mx%' +ORDER BY price DESC; + +-- Вопрос: +-- В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'. +-- Ответ: +-- В названии товаров могут быть буквы (в словах) с разными регистрами, при поиске клиенту можен не показать часть товаров только потому что слово выглядить так - PRO а не так - Pro. diff --git a/vberetar/postgresql/sql_join.sql b/vberetar/postgresql/sql_join.sql new file mode 100644 index 0000000..e2b7c55 --- /dev/null +++ b/vberetar/postgresql/sql_join.sql @@ -0,0 +1,144 @@ +-- Задание 1 +SELECT o.id AS order_id, + o.order_date, + o.status, + u.name AS user_name, + u.country +FROM orders o +JOIN users u ON o.user_id = u.id; + +-- Ответ на вопросы +-- 1. Логичнее начать c orders, так как нам в первую очередь нужны все заказы, +-- а потом только пользователи которые их сделали. + +-- 2. Я думаю уместнее сделать LEFT. +-- Я пробовал сделать с INNER и LEFT и у меня не пропали строки. +-- Получается это из за того что у каждого заказа есть пользователь, не дуаю что должен создастся заказ без пользователя. + + +-- Задание 2 +SELECT o.id AS order_id, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE o.id = 101; + +-- Ответ на вопрос +-- Выдаст дублированный состав заказа с id 101, +-- получается он будет обращаться к каждаму заказу, но отображать продукты заказа с id 101 + + +-- Задание 3 +SELECT o.id AS order_id, + u.name AS name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id; + +-- Ответ на вопрос +-- 1. Появляется дубликат заказов потому что у 3 заказа есть несколько товаров, join превращает(размножает) их в несколько строк. +-- 2. Отличается тем что мы выводим все заказы с товарами а не один заказ. + + +-- Задание 4 +SELECT u.name AS user_name, + o.id AS order_id, + o.status +FROM users u +LEFT JOIN orders o ON o.user_id = u.id; + +-- Ответ на вопрос +-- INNER JOIN не выдаст пользователей без заказа. Только тех у кого есть пара с orders. + + +-- Задание 5 +SELECT u.id AS user_id, + u.name AS user_name +FROM users u +LEFT JOIN orders o ON o.user_id = u.id +WHERE o.id IS NULL; + +-- Ответ на вопрос +-- 1. Проверять IS NULL нужно по полю orders.id +-- 2. Мы должны в запросе получить всех пользователей, а потом в фильтре "узнать" есть ли у них заказы. + + +-- Задание 6 +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity +FROM orders o +JOIN users u ON u.id = o.user_id +JOIN order_items oi ON oi.order_id = o.id +JOIN products p ON p.id = oi.product_id +WHERE p.name ILIKE '%kindle%'; + +-- Ответ на вопрос +-- В заказе 2 есть только один товар с названием Kindle. + + +-- Задание 7 +SELECT p.name AS product_name, + oi.order_id, + oi.quantity +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id; + +-- Ответ на вопрос +-- 1. order_id будет NULL +-- 2. Если использовать INNER JOIN то он не покажет товар который не заказывали. + + +-- Задание 8 +SELECT o.id AS order_id, + u.name AS user_name, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE o.status = 'paid'; + +-- Ответ на вопрос +-- У меня получился тот же результат если написать вот так - JOIN orders o ON o.id = oi.order_id and o.status = 'paid' + + +-- Задание 9 +SELECT o.id AS order_id, + o.order_date, + p.name AS product_name, + oi.quantity, + oi.price_at_purchase +FROM products p +JOIN order_items oi ON oi.product_id = p.id +JOIN orders o ON o.id = oi.order_id +JOIN users u ON u.id = o.user_id +WHERE u.name = 'Alice'; + +-- Ответ на вопрос +-- Если у пользователя нет ни одного заказа, то этот запрос вернет пустую строку. +-- Не вернет потому что нечего отобразить таблице, если нет заказов то и нет купленных товаров. +-- Но если прописать RIGHT JOIN users u ON u.id = o.user_id то отобразит все строки NULL. + + +-- Задание 10 +SELECT o.id AS order_id, + o.user_id, + o.status +FROM orders o +LEFT JOIN order_items oi ON oi.order_id = o.id +WHERE oi.order_id IS NULL; + +-- Ответ на вопрос +-- Такой запрос выдаст пустые строки. +-- INNER JOIN должен выдать строки только если есть "пара". diff --git a/vberetar/postgresql/sql_query.sql b/vberetar/postgresql/sql_query.sql new file mode 100644 index 0000000..a0681ba --- /dev/null +++ b/vberetar/postgresql/sql_query.sql @@ -0,0 +1,73 @@ +-- Задание 1 +SELECT id, name, country, created_at, email +FROM users +ORDER BY created_at DESC, name; + +-- Почему выбрал именно эти столбцы и зачем такая сортировка? +-- Ответ на вопрос: +-- Эти столбцы нужны для анализа пользователей, для тех же аналитиков. +-- Сортировка по дате и имени, я так предпологаю, нужня для удобства. + + +-- Задание 2 +-- Запрос 1 +SELECT id, name, country +FROM users +WHERE country = 'US' +ORDER BY name; + +-- Запрос 2 +SELECT id, name, country +FROM users +WHERE country = 'UK' +ORDER BY name; + +-- Какой из двух запросов маркетологу полезнее и почему? +-- Ответ на вопрос: +-- Я так понимаю маркетологу нужно понять какое кол-во пользователей в каждой из стран зарегестрировалось в приложении. +-- Возможно для понимания продвижения продукта и насколько успешна маркетинговая стратегия. И в дальнейшем её корректировка. + + +-- Задание 3 +-- Запрос 1 +SELECT id, name, email +FROM users +WHERE email IS NOT NULL -- я знаю что email и так должен быть NOT NULL по DDL, но на всякий прописал +ORDER BY name; + +-- Запрос 2 +SELECT DISTINCT LOWER(email) as email_lower +FROM users +WHERE email IS NOT NULL; + +-- Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы? +-- Ответ на вопрос: +-- Приводить email к нижнему регистру необходимо для того чтобы небыло дубликатов почты. +-- После гугления я узнал что в почтовых адресах могут допускаться верхние регистры букв, только потом создать email с теми же буквами но в нижнем регистре нельзя будет, они будут считаться индентичными. +-- Получается несколько пользователей могут залогинится под одним и тем же email адресом но с разными регистрами. А такое нельзя допускать. +-- Для рассылки дубликаты нужно убирать что-бы не приходила рассылка дважды в один и тот же email. + +-- Задание 4 +-- Запрос 1 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC; + +-- Запрос 2 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +LIMIT 3; + +-- Запрос 3 +SELECT id, name, created_at +FROM users +WHERE created_at > '2024-01-14' +ORDER BY created_at DESC +OFFSET 1 LIMIT 2; + +-- В каком реальном сценарии тебе пригодится OFFSET? +-- Ответ на вопрос: +-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка. diff --git a/vberetar/postgresql/sql_union.sql b/vberetar/postgresql/sql_union.sql new file mode 100644 index 0000000..b71e744 --- /dev/null +++ b/vberetar/postgresql/sql_union.sql @@ -0,0 +1,49 @@ +-- Задание 1 +SELECT id, name, 'active' AS user_status +FROM users +WHERE is_active = TRUE +UNION ALL +SELECT id, name, 'inactive' AS user_status +FROM users +WHERE is_active = FALSE +ORDER BY name; + +-- Ответ на вопрос +-- 1. Количество строк не изменилось +-- 2. Предположу, что подача информации в таком виде более информативней(понятней). + + +-- Задание 2 +SELECT id AS order_id, user_id, order_date, 'old' AS period +FROM orders +WHERE order_date < '2024-01-01' +UNION ALL +SELECT id AS order_id, user_id, order_date, 'new' AS period +FROM orders +WHERE order_date >= '2024-01-01' +ORDER BY order_date, order_id; + +-- Ответ на вопрос +-- 1. В dbeaver в самой левой колонке отображается номерация строк, а снизу общее количество строк который выдал запрос. +-- На последней строке с old можно увидеть число в самой левой колонке, это и есть количество строк c old. Далее отнимаем общее кол-во строк от old и получаем кол-во new. +-- 2. Тяжело ответить на такой вопрос, не знаю. Если у меня были бы сомнения, я бы у новичка спросил. + + +-- Задание 3 +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'ordered' AS product_status +FROM products p +JOIN order_items oi ON oi.product_id = p.id +UNION ALL +SELECT DISTINCT p.id AS product_id, + p.name AS product_name, + 'never_ordered' AS product_status +FROM products p +LEFT JOIN order_items oi ON oi.product_id = p.id +WHERE oi.product_id IS NULL +ORDER BY product_name; + +-- Ответ на вопрос +-- 1. Нам нужно чтобы товар(products) ни разу не заказывали(order_items), получается что мы проверяем есть ли ссылка в каком нибудь заказе на продукт. +-- 2. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь. diff --git a/vberetar/postgresql/sql_view.sql b/vberetar/postgresql/sql_view.sql new file mode 100644 index 0000000..333c660 --- /dev/null +++ b/vberetar/postgresql/sql_view.sql @@ -0,0 +1,69 @@ +-- Задание 1 +CREATE MATERIALIZED VIEW my_category_revenue AS + SELECT + p.category, + SUM(oi.quantity * oi.price_at_purchase) AS total_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + JOIN products p ON p.id = oi.product_id + WHERE o.status = 'paid' + GROUP BY p.category; + +SELECT * FROM my_category_revenue +ORDER BY total_revenue DESC; +-- Ответ на вопросы +-- 1. Не нужно писать длинные запросы и результат будет уже подсчитан. + + +-- Задание 2 +CREATE MATERIALIZED VIEW mv_user_activity AS + SELECT + u.id AS user_id, + u.name AS user_name, + COUNT(o.user_id) AS orders_count + FROM users u + LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' + GROUP BY u.id, u.name; + +SELECT * FROM mv_user_activity +WHERE orders_count > 0 +ORDER BY orders_count DESC; +-- Ответ на вопросы +-- 1. Я так понял, что если критерий "самых лояльных" клиентов это колличество оплаченных заказов, +-- то "самые лояльные" это те у кого этих заказов больше всего. +-- 2. Покажет "самых лояльных" клиентов за последный месяц, кто-то из клиетов может сделать много заказов за 1-2 месяца, а кто то за год по чуть чуть заказывает. +-- Возможно еще какие то из клиентов ориентируются на "сезонные" товары и у них будет сплеск продажи за последний месяц, или кто то из клиентов покупал много за год, а в этом месяце перестал делать заказы. + + +-- Задание 3 +CREATE MATERIALIZED VIEW mv_daily_revenue AS + SELECT + o.order_date::date AS sales_date, + SUM(oi.quantity * price_at_purchase) AS daily_revenue + FROM orders o + JOIN order_items oi ON oi.order_id = o.id + WHERE o.status = 'paid' + GROUP BY o.order_date::date; + +SELECT * FROM mv_daily_revenue +ORDER BY sales_date DESC; +-- Ответ на вопросы +-- 1. «Просадки» и «пики» будут видны в колонке daily_revenue +-- 2. Быстрее считывать с готовой витрины mv_daily_revenuе, на лету из сырых таблиц дольше будет обрабатываться запрос. + + +-- Задание 4 +REFRESH MATERIALIZED VIEW mv_daily_revenue; +-- Расписание интернет‑магазина с несколькими заказами в день: +-- Для такого сценария подойдет обновление витрины раз в неделю по ночам. +-- Так как заказов не так уж и много, можно обновлять статискиту чаще. +-- Ночью, потому что заказов будет меньше в то время суток и бд быстрее обработает ззапрос. + +-- Расписание крупного магазина с тысячами заказов в час: +-- Для такого сценария подойдет обновление витрин раз в месяц - раз в квартал по ночам, +-- Так как данных много и частые расчеты будут сильно нагружать базу данных. +-- Возможно, нужно будет получить данные аналитикам раньше указанных сроков. + +-- Ответ на вопросы +-- 1. Данные будут устаревшими, из за того результат работы аналитиков будут не достоверными. +-- 2. Слишком частые обновления витрит будут сильно нагружать БД.