fix mistakes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -43,3 +43,394 @@ FROM user_cohort_analysis;
|
||||
-- Запусти `EXPLAIN ANALYZE` на всех витринах. Найди узкие места: Seq Scan, высокие cost, JOIN без индексов, DISTINCT, подзапросы в SELECT.
|
||||
-- Перепиши запросы чтобы достигнуть минимального времени выполнения.
|
||||
-- Напиши что сделал, почему, и насколько быстрее стал запрос
|
||||
|
||||
*** 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;
|
||||
|
||||
Reference in New Issue
Block a user