From f443cd48550c9701b348d45048b14f78251f5914 Mon Sep 17 00:00:00 2001 From: Dmitry Medvedev Date: Wed, 22 Jul 2026 21:15:19 +0500 Subject: [PATCH] fix mistakes --- .../11.4 Pet project Cohort_analysis.sql | 2 +- .../11.7 Pet Project Additional tasks.sql | 393 +++++++++++++++++- 2 files changed, 393 insertions(+), 2 deletions(-) diff --git a/dmedvedev/11.4 Pet project Cohort_analysis.sql b/dmedvedev/11.4 Pet project Cohort_analysis.sql index d25e5fa..42a5d55 100644 --- a/dmedvedev/11.4 Pet project Cohort_analysis.sql +++ b/dmedvedev/11.4 Pet project Cohort_analysis.sql @@ -7,7 +7,7 @@ SELECT COUNT(DISTINCT pe.user_id) AS enrolled_users, COUNT(DISTINCT plv.user_id) AS active_users, --ROUND(COUNT(DISTINCT plv.user_id) * 100.0 / COUNT(DISTINCT pu.id), 2) || '%' AS retention_rate, - ROUND(COUNT(pe.id) * 100.0 / COUNT(DISTINCT pu.id), 2) || '%' AS retention_rate, + ROUND(COUNT(DISTINCT pe.user_id) * 100.0 / COUNT(DISTINCT pu.id), 2) || '%' AS retention_rate, ROUND(AVG((SELECT COUNT(pe.id) FROM pet_enrollments pe)) / COUNT(DISTINCT pu.id), 2) AS avg_courses_per_user, (SELECT diff --git a/dmedvedev/11.7 Pet Project Additional tasks.sql b/dmedvedev/11.7 Pet Project Additional tasks.sql index 5233dc7..bb0a73a 100644 --- a/dmedvedev/11.7 Pet Project Additional tasks.sql +++ b/dmedvedev/11.7 Pet Project Additional tasks.sql @@ -42,4 +42,395 @@ FROM user_cohort_analysis; -- 4. Задание 4: Анализ плана выполнения -- Запусти `EXPLAIN ANALYZE` на всех витринах. Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT. -- Перепиши запросы чтобы достигнуть минимального времени выполнения. --- Напиши что сделал, почему, и насколько быстрее стал запрос \ No newline at end of file +-- Напиши что сделал, почему, и насколько быстрее стал запрос + +*** ACTIVITY USERS *** +Стоимость я писал прогнозную, в конце указал реальные до/после. В Activity users у меня проблема с чтением таблиц, как я вижу оптимизатор показывает, что в какой-то момент он читает 3625 строк, хочу если подумать, таблица та не большие по факту, а когда будет рабочий проект, то там строк будет намного больше и из-за таких же ошибок как у меня сейчас, строк может стать в сотни раз больше. Причем итоговая стоимость составляет 380. +Сам я не смог придумать что можно было бы оптимизировать, обратился к ИИ, он предлагает оптимизировать таблицу, которую оптимизатор читает аж 3 раза (я с этим согласен полностью, вижу что в 3х СТЕ обращаюсь к одной и той же таблице). Также еще говорит, что у меня бесполезный GROUP BY в конце. Теперь я попробую это оптимизировать самостоятельно без использования ИИ. +После сокращения 3х СТЕ до одной получилась стоимость 364, лучше, но все равно как-будто много, однако, максимальное количество строк уже не 3625, а 210. +Также я сам увидел бесполезную сортировку внутри СТЕ, убрал ее, стоимость стала 356. +Потом я понял, что зря вообще использовал оконные функции DENSE_RANK, FIRST_VALUE, LAST_VALUE, можно обойтись агрегатными функциями, я сделал замену и получил стоимость 342. +Я убрал подзапрос из конца и сделал его проще в СТЕ, теперь нет подзапроса. +По итогу стоимость теперь 325. А фактическая 0.183, а изначально было 2.174 (снижение более чем в 10 раз). + +ОПТИМИЗИРОВАННЫЙ КОД +EXPLAIN ANALYZE +--Активность пользователей +CREATE MATERIALIZED VIEW user_activity_summary AS + +--CTE кол-во курсов на которые записан пользователь +WITH enrollment_count AS ( + SELECT + pe.user_id, + COUNT(*) AS enrollment_count + FROM pet_enrollments pe + GROUP BY pe.user_id +), + +--CTE просмотренные уникальные уроки пользователем (кол-во), last view and first view +viewed_lessons_count_max_min AS ( + SELECT + plv.user_id, + COUNT(DISTINCT plv.lesson_id) AS viewed_lessons_count, + MIN(plv.viewed_at) AS first_view, + MAX(plv.viewed_at) AS last_view + + FROM pet_lesson_views plv + GROUP BY plv.user_id +), + + +--CTE кол-во доступных уроков пользователю +lesson_user_all AS ( + SELECT + pe.user_id, + COUNT(pl.id) AS lesson_user_all_open + FROM pet_enrollments pe + LEFT JOIN pet_lessons pl ON pl.course_id = pe.course_id + GROUP BY pe.user_id + +) +--main запрос +SELECT + pu.id AS user_id, + pu.name AS name, + ec.enrollment_count AS enrollment_count, + vlcmm.viewed_lessons_count, + vlcmm.first_view, + vlcmm.last_view, + ROUND(vlcmm.viewed_lessons_count * 100.0 / lua.lesson_user_all_open, 2) || '%' AS engagement_rate + +FROM pet_users pu +LEFT JOIN enrollment_count ec ON pu.id = ec.user_id +LEFT JOIN viewed_lessons_count_max_min vlcmm ON pu.id = vlcmm.user_id +LEFT JOIN lesson_user_all lua ON lua.user_id = pu.id +ORDER BY pu.id; + +*** EFFECTIVE COURSES *** +actual time моего кода составляет 0.992, количество строк при сортировке максимум 1022. +Для начала убрал ненужные сортировки в СТЕ. +Для поиска первых и последних времен просмотра уроков в курсе убрал из использования оконные функции, заменил на агрегаты MIN, MAX и объединил эти 2 СТЕ в одну плюс к ним также внес еще одну СТЕ внутрб, которая ссылалась на pet_lesson_views. +Почему-то у меня в главном селекте была группировка, убрал ее. +По итогу фактическое время стало 0.120, а максимальное количество строк 510. + +--Эффективность курсов +--ОПТИМИЗИРОВАННЫЙ КОД +CREATE MATERIALIZED VIEW course_summary AS + +--кол-во уникальных пользователей в рамках одного курса (ready) +WITH unique_users AS ( + SELECT + pe.course_id, + COUNT(DISTINCT pe.user_id) AS unique_users + FROM pet_enrollments pe + GROUP BY pe.course_id +), + +--общее кол-во уроков по курсу +lesson_count AS ( + SELECT + pl.course_id, + COUNT(*) AS lesson_count + FROM pet_lessons pl + GROUP BY pl.course_id + +), + +--дата и время первого и последнего просмотра любого урока курса, суммарное число просмотров уроков по курсу +first_last_viewed_at AS ( + SELECT + pl.course_id, + MIN(plv.viewed_at) AS first_viewed_at, + MAX(plv.viewed_at) AS last_viewed_at, + COUNT(plv.id) AS total_views + FROM pet_lessons pl + LEFT JOIN pet_lesson_views plv ON pl.id = plv.lesson_id + GROUP BY pl.course_id +) + +SELECT + pc.id AS course_id, + pc.title AS course_title, + COALESCE(uu.unique_users, 0), + COALESCE(lc.lesson_count, 0), + COALESCE(flva.total_views, 0), + ROUND(COALESCE(flva.total_views, 0) * 1.0 / NULLIF(uu.unique_users, 0), 2) AS avg_views_per_user, + flva.first_viewed_at, + flva.last_viewed_at + +FROM pet_courses pc +LEFT JOIN unique_users uu ON pc.id = uu.course_id +LEFT JOIN lesson_count lc ON pc.id = lc.course_id +LEFT JOIN first_last_viewed_at flva ON pc.id = flva.course_id + +ORDER BY course_id; + +*** PLATFORM SUMMARY *** +У меня и так маленький базовый запрос, поэтому я попытался его оптимизировать как знаю, но думаю, что это не ведет к чему-то более эффективному. +actual time примерно одинаковое . +execution time тоже скачет везде , но как-будто одинаковое. +По итогу я убрал СТЕ и все поместил внутрь одного селекта. У меня как и было 8 Seq Scan, так и осталось. + +EXPLAIN ANALYZE +SELECT + (SELECT COUNT(*) total_users FROM pet_users pu) AS total_users, + (SELECT COUNT(*) total_courses FROM pet_courses pc) AS total_courses, + (SELECT COUNT(*) total_lessons FROM pet_lessons pl) AS total_lessons, + (SELECT COUNT(DISTINCT user_id) FROM pet_lesson_views plv) AS users_with_views, + ROUND(((SELECT COUNT(*) total_lessons FROM pet_lessons pl) * 1.0 / (SELECT COUNT(*) total_courses FROM pet_courses pc)), 2) AS avg_lessons_per_course, + ROUND(((SELECT COUNT(*) FROM pet_lesson_views plv) * 1.0 / (SELECT COUNT(*) total_lessons FROM pet_lessons pl)), 2) AS avg_views_per_lesson; + +НО по итогу я зашел в ИИ и он сказал, что для решения такой задачи можно использовать CROSS JOIN для которого кстати как я теперь понимаю не нужно соединение по индексу через ON. +КОНЕЧНЫЙ ВАИАНТ СЛЕДУЮЩИЙ, В НЕМ УЖЕ 5 РАЗ POSTGRE ЧИТАЮТ ТАБЛИЦЫ, А НЕ 8. Execution time упало на 30%. + +EXPLAIN ANALYZE +SELECT + tu.total_users, + tc.total_courses, + tl.total_lessons, + fplv.users_with_views, + ROUND((tl.total_lessons * 1.0 / tc.total_courses), 2) AS avg_lessons_per_course, + ROUND((fplv.total_views * 1.0 / tl.total_lessons), 2) AS avg_views_per_lesson +FROM + (SELECT COUNT(*) AS total_users FROM pet_users pu) tu +CROSS JOIN + (SELECT COUNT(*) AS total_courses FROM pet_courses pc) tc +CROSS JOIN + (SELECT COUNT(*) AS total_lessons FROM pet_lessons pl) tl +CROSS JOIN + (SELECT + COUNT(DISTINCT user_id) AS users_with_views, + COUNT(*) AS total_views + FROM pet_lesson_views plv + ) fplv; + +*** COHORT ANALYSIS *** +В данном случае исходный код показывал Execution time 0.45, а после преобразований 0.33. Вижу проблему множественного обращения к одним и тем же таблицам. +Теперь код реализован через СТЕ, сначала считаются метрика по зарегист пользователям, потом по записавшимся, потом по перввым просмотрам (эти же пользователи берутся как активные в дальнейших расчетах). +Далее делаем таблицу под каждого пользователя в другой СТЕ. +И в конечном запросе собираем все в когорты. + +ИТОГОВЫЙ КОД: +--CREATE MATERIALIZED VIEW user_cohort_analysis AS +EXPLAIN ANALYZE + +WITH +user_registraion AS ( + SELECT + pu.id AS user_id, + TO_CHAR(DATE_TRUNC('month', pu.registration_date),'YYYY-MM') AS MONTH + FROM pet_users pu +), + +user_enrollments_summary AS ( + SELECT + pe.user_id AS user_id, + COUNT(DISTINCT pe.course_id) AS unique_courses_count + FROM pet_enrollments pe + GROUP BY user_id +), + +first_viewed AS ( + SELECT + plv.user_id AS user_id, + MIN(viewed_at) AS first_viewed_date_for_each_user + FROM pet_lesson_views plv + GROUP BY user_id +), + +user_metrics AS ( + SELECT + pu.id AS user_id, + DATE_TRUNC('month', pu.registration_date) AS MONTH, + CASE WHEN ur.user_id IS NOT NULL THEN 1 ELSE 0 END AS registration_user, + CASE WHEN ues.user_id IS NOT NULL THEN 1 ELSE 0 END AS enrolled_user, + CASE WHEN fv.user_id IS NOT NULL THEN 1 ELSE 0 END AS active_user, + COALESCE(ues.unique_courses_count, 0) AS courses_count, + fv.first_viewed_date_for_each_user::date - registration_date AS differens_between_reg_and_fv + FROM pet_users pu + LEFT JOIN user_registraion ur ON pu.id = ur.user_id + LEFT JOIN user_enrollments_summary ues ON pu.id = ues.user_id + LEFT JOIN first_viewed fv ON pu.id = fv.user_id +) + +SELECT + TO_CHAR(user_metrics.MONTH, 'YYYY-MM') AS cohort_month, + COUNT(DISTINCT user_metrics.user_id) AS users_in_cohort, + SUM(user_metrics.enrolled_user) AS enrolled_users, + SUM(user_metrics.active_user) AS active_users, + ROUND(SUM(user_metrics.registration_user * 100.0) / SUM(user_metrics.enrolled_user), 2) || '%' AS retention_rate, + ROUND(SUM(user_metrics.courses_count) / SUM(user_metrics.enrolled_user), 2) AS avg_courses_per_user, + AVG(user_metrics.differens_between_reg_and_fv) AS avg_time_to_first_view +FROM user_metrics +GROUP BY cohort_month +ORDER BY cohort_month; + +*** LESSON POPULARITY RANKING*** + +Вижу раздутость кода. Колчиество строк достигает 1850. В одном запросе множество подзапросов, для начала лучше все закинуть в разные СТЕ. +По исходному коду Execution time от 0.4 до 1.8, по оптимизированному 0.27-0.4. +Я обращался к одной таблице 3 раза, а также переписал запрос и в конце не нужна группировка. +Но проблему 1850 строк не смог решить. Вообще возможно ли ее решить? + +WITH +lesson_views_summary AS ( + SELECT + lesson_id, + COUNT(*) AS total_views, + COUNT(DISTINCT user_id) AS unique_v, + COUNT(*) * 1.0 / COUNT(DISTINCT user_id) AS views_one_user_per_one_lesson + FROM pet_lesson_views + GROUP BY lesson_id +), + +table_enrolled_users AS ( + SELECT + course_id, + COUNT(DISTINCT user_id) AS enrolled_to_course + FROM pet_enrollments pe + GROUP BY course_id +) + + +SELECT + pl.id AS lesson_id, + pl.title AS lesson_title, + pc.id AS course_id, + pc.title AS course_title, + COALESCE(lvs.total_views, 0) AS total_views, + COALESCE(lvs.unique_v, 0) AS unique_viewers, + COALESCE(ROUND(lvs.views_one_user_per_one_lesson, 2), 0) AS avg_views_per_viewer, --также это можно было посчитать total_views/unique_viewers + DENSE_RANK() OVER ( + PARTITION BY pc.id + ORDER BY lvs.total_views DESC NULLS LAST) AS popularity_rank, + + ROUND(COALESCE(lvs.unique_v, 0) * 100.0 / NULLIF(teu.enrolled_to_course, 0), 2) || '%' AS completion_rate + + FROM pet_lessons pl + LEFT JOIN lesson_views_summary lvs ON lvs.lesson_id = pl.id + LEFT JOIN pet_courses pc ON pc.id = pl.course_id + LEFT JOIN table_enrolled_users teu ON teu.course_id = pc.id + + ORDER BY popularity_rank; + + +*** CONVENSION FUNNEL *** +В моем запросе выполняется порядка 7 SubPlan, execution time 60 ms. +Я в своем новом запросе создал СТЕ, которая один раз считает количество пользователей на платформе. +Далее 1 раз считаем кол-во уроков в каждом курсе, собираем все метрики на каждого юсера на курсе. И строим воронку. +У оптимизированного запроса execution time составляет 0.8 ms. + +--CREATE MATERIALIZED VIEW conversion_funnel AS + +--Считаем общее число пользователей на платформе +WITH total_platform_users AS ( + SELECT COUNT(*) AS total_users FROM pet_users +), + +--Считаем количество уроков в каждом курсе +lessons_per_course AS ( + SELECT + course_id, + COUNT(*) AS total_lessons + FROM pet_lessons + GROUP BY course_id +), + +-- Собираем ВСЕ метрики каждого студента на курсе за один проход +user_course_progress AS ( + SELECT + pe.course_id, + pe.user_id, + COUNT(DISTINCT plv.lesson_id) * 1.0 / NULLIF(lpc.total_lessons, 0) AS progress + FROM pet_enrollments pe + LEFT JOIN pet_lessons pl ON pl.course_id = pe.course_id + -- Строго проверяем просмотры только для этого записанного пользователя + LEFT JOIN pet_lesson_views plv ON plv.lesson_id = pl.id AND plv.user_id = pe.user_id + LEFT JOIN lessons_per_course lpc ON lpc.course_id = pe.course_id + GROUP BY pe.course_id, pe.user_id, lpc.total_lessons +), + +-- Получаем метрики студентов по курсу +course_funnel_aggregates AS ( + SELECT + course_id, + COUNT(DISTINCT user_id) AS enrolled_count, + -- Начал смотреть = посмотрел хотя бы 1 урок (прогресс > 0) + COUNT(DISTINCT CASE WHEN progress > 0 THEN user_id END) AS started_count, + -- Прошел половину = прогресс >= 50% + COUNT(DISTINCT CASE WHEN progress >= 0.5 THEN user_id END) AS mid_count, + -- Завершил = прогресс >= 100% + COUNT(DISTINCT CASE WHEN progress >= 1.0 THEN user_id END) AS completed_count + FROM user_course_progress + GROUP BY course_id +), + +-- Строим воронку +main_table AS ( +-- Зарегистрированы + SELECT + pc.id AS course_id, pc.title AS course_title, 'registered' AS stage_name, 0 AS stage_order, + tpu.total_users AS users_count, + '100.00%' AS conversion_from_previous, + '100.00%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + + UNION ALL + +--Записались на курс + SELECT + pc.id AS course_id, pc.title AS course_title, 'enrolled' AS stage_name, 1 AS stage_order, + COALESCE(cfa.enrolled_count, 0) AS users_count, + ROUND(COALESCE(cfa.enrolled_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.enrolled_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Начали смотреть + SELECT + pc.id AS course_id, pc.title AS course_title, 'started' AS stage_name, 2 AS stage_order, + COALESCE(cfa.started_count, 0) AS users_count, + ROUND(COALESCE(cfa.started_count, 0) * 100.0 / NULLIF(cfa.enrolled_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.started_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Прошли половину курса + SELECT + pc.id AS course_id, pc.title AS course_title, 'mid_progress' AS stage_name, 3 AS stage_order, + COALESCE(cfa.mid_count, 0) AS users_count, + ROUND(COALESCE(cfa.mid_count, 0) * 100.0 / NULLIF(cfa.started_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.mid_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id + + UNION ALL + +--Полностью завершили курс + SELECT + pc.id AS course_id, pc.title AS course_title, 'completed' AS stage_name, 4 AS stage_order, + COALESCE(cfa.completed_count, 0) AS users_count, + ROUND(COALESCE(cfa.completed_count, 0) * 100.0 / NULLIF(cfa.mid_count, 0), 2) || '%' AS conversion_from_previous, + ROUND(COALESCE(cfa.completed_count, 0) * 100.0 / NULLIF(tpu.total_users, 0), 2) || '%' AS conversion_from_start + FROM pet_courses pc + CROSS JOIN total_platform_users tpu + LEFT JOIN course_funnel_aggregates cfa ON cfa.course_id = pc.id +) + +SELECT + course_id, + course_title, + stage_name, + users_count, + conversion_from_previous, + conversion_from_start +FROM main_table +ORDER BY course_id, stage_order;