Merge pull request 'add pet-project' (#187) from dmezentsev_postgresql_DEIN-2902 into main
Reviewed-on: https://gitea.de-infra.servehttp.com/de-infrastructure/DWH/pulls/187
This commit is contained in:
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user