diff --git a/dmezentsev/postgresql/pet_project/DDL_DML.sql b/dmezentsev/postgresql/pet_project/DDL_DML.sql new file mode 100644 index 0000000..00b36ad --- /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 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 users_(id), + FOREIGN KEY (course_id) REFERENCES 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 users_(id), + FOREIGN KEY (lesson_id) REFERENCES 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..699f678 --- /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 / al.possible_lessons * 100, 2) AS engagement_rate +FROM 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..f7d8008 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/dm_user_cohort_analysis.sql @@ -0,0 +1,77 @@ +DROP MATERIALIZED VIEW IF EXISTS user_cohort_analysis; +CREATE MATERIALIZED VIEW 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..d6f2fb5 --- /dev/null +++ b/dmezentsev/postgresql/pet_project/extra.sql @@ -0,0 +1,58 @@ +-- Задание 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; + +-- Задание 3: Лучшая когорта по retention +SELECT cohort_month, retention_rate +FROM 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