#sql #postgresql
Вопрос:
Источник данных
Идентификатор пользователя | Дата Посещения |
---|---|
1 | 2020-01-01 12:29:15 |
1 | 2020-01-02 12:30:11 |
1 | 2020-04-01 12:31:01 |
2 | 2020-05-01 12:31:14 |
Проблема
Мне нужен совет, я пытаюсь выполнить дополнительный запрос для этого результата, чтобы отметить пользователя как удерживающего, если он не посещал его в течение 3 месяцев. я использую этот запрос для получения данных для получения последнего посещения пользователя каждый месяц, включая null
select u.user_id, gs.yyyymm, s.last_visit_date
from (select distinct user_id from source s) u cross join
generate_series('2021-01-01'::timestamp, '2021-12-01'::timestamp, interval '1 month'
) gs(yyyymm) left join lateral
(select max(s.visit_date) as last_visit_date
from source s
where s.user_id = u.user_id and
s.visit_date >= gs.yyyymm and
s.visit_date < gs.yyyymm interval '1 month'
) s
on 1=1;
но я думаю, что это действительно влияет на производительность, если пользователь продолжает расти, у вас, ребята, есть какие-либо советы для достижения результата, как показано ниже?
ожидаемый результат
Месяц | Идентификатор пользователя | Тип |
---|---|---|
1 | 1 | Первый |
2 | 1 | удержание |
3 | 1 | удержание |
4 | 1 | АКТИВИРОВАТЬ ЗАНОВО |
…. | ||
12 | 1 | нулевой |
1 | 2 | нулевой |
… | ||
5 | 2 | Первый |
6 | 2 | удержание |
7 | 2 | удержание |
8 | 2 | удержание |
9 | 2 | нулевой |
… и так далее |
или это может быть так
Месяц | Первый | Удержание | Реактивировать |
---|---|---|---|
1 | 1 | 0 | 0 |
2 | 0 | 1 | 0 |
3 | 0 | 1 | 0 |
4 | 0 | 0 | 1 |
5 | 1 | 0 | 0 |
6 | 0 | 1 | 0 |
7 | 0 | 1 | 0 |
8 | 0 | 1 | 0 |
9 | 0 | 0 | 0 |
… и так далее |
Комментарии:
1. Из вашего описания вам нужны только флаги «ПЕРВЫЙ», «СОХРАНЕНИЕ» и «ПОВТОРНАЯ АКТИВАЦИЯ». Вам также нужен номер месяца?
2. Не должен ли год в дате посещения в вашем источнике данных быть 2021 в соответствии с вашим запросом и результатами?
3. пожалуйста, кратко объясните логику и предоставьте несколько допустимых выборочных данных и ожидаемых результатов на основе приведенных выборочных данных.
4. что происходит с идентификатором пользователя 1 в 5, 6, 7 и 8 месяцах?
5. Могу ли я предположить, что в таблице посещений есть только вставки. Не удалять и не обновлять?
Ответ №1:
У моего решения есть некоторые разумные требования, но я могу работать и без него (цена-некоторая производительность).
Я построил несколько вспомогательных таблиц, которые автоматически заполняются триггером. Требование состоит в том, что UPDATE
или DELETE
не допускается к посещению стола.
в таблице mst_user хранятся разные идентификаторы пользователей, и это first_visit. в таблице user_monthly_visit хранятся дата последнего и первого посещения, идентификатор пользователя и месяц.
таблицы
CREATE TABLE mst_user (
id BIGINT,
first_visit TIMESTAMP,
CONSTRAINT pk_mst_user PRIMARY KEY (id)
);
CREATE TABLE visit (
user_id BIGINT,
visit_date TIMESTAMP,
CONSTRAINT visit_user_fkey FOREIGN KEY (user_id) REFERENCES mst_user (id)
);
CREATE TABLE user_monthly_visit (
user_id BIGINT,
month DATE,
first_visit_this_month TIMESTAMP,
last_visit_this_month TIMESTAMP,
CONSTRAINT pk_user_monthly_visit PRIMARY KEY (user_id, month),
CONSTRAINT user_monthly_visit_user_fkey FOREIGN KEY (user_id) REFERENCES mst_user (id)
);
CREATE INDEX ix_user_monthly_visit_month ON user_monthly_visit(month);
спусковой крючок
CREATE OR REPLACE FUNCTION trf_visit() RETURNS trigger
VOLATILE
AS $xx$
DECLARE
l_user_id BIGINT;
l_row RECORD;
l_user_monthly_visit user_monthly_visit;
BEGIN
IF (tg_op = 'INSERT')
THEN
l_row := NEW;
INSERT INTO mst_user(id, first_visit) VALUES (l_row.user_id, l_row.visit_date)
ON CONFLICT(id) DO UPDATE SET first_visit = LEAST(mst_user.first_visit, l_row.visit_date);
INSERT INTO user_monthly_visit(user_id,month,first_visit_this_month,last_visit_this_month) VALUES (l_row.user_id,date_trunc('month',l_row.visit_date),l_row.visit_date,l_row.visit_date)
ON CONFLICT(user_id,month) DO UPDATE SET first_visit_this_month = LEAST(user_monthly_visit.first_visit_this_month,l_row.visit_date),
last_visit_this_month = GREATEST(user_monthly_visit.last_visit_this_month,l_row.visit_date);
ELSE
RAISE EXCEPTION 'UPDATE and DELETE arent allowed!';
END IF;
RETURN l_row;
END;
$xx$ LANGUAGE plpgsql;
CREATE TRIGGER trig_visit
BEFORE INSERT OR DELETE OR UPDATE ON visit
FOR EACH ROW
EXECUTE PROCEDURE trf_visit();
ТЕСТОВЫЕ ДАННЫЕ
INSERT INTO visit (user_id, visit_date)
VALUES (1, '20200101 122915');
INSERT INTO visit (user_id, visit_date)
VALUES (1, '20200102 123011');
INSERT INTO visit (user_id, visit_date)
VALUES (1, '20200401 123101');
INSERT INTO visit (user_id, visit_date)
VALUES (2, '20200501 123114');
запрос
SELECT mnt AS month, user_id,
CASE WHEN first_visit IS NULL OR first_visit> yyyymm INTERVAL '1 month' THEN NULL
WHEN first_visit_this_month = first_visit THEN 'FIRST'
WHEN first_visit_this_month IS NULL AND last_three_month INTERVAL '3 month' >= yyyymm THEN 'RETENTION'
WHEN first_visit_this_month IS NOT NULL THEN 'REACTIVATE'
ELSE NULL
END user_type
FROM
(SELECT date_part('month', gs.yyyymm)::INTEGER AS mnt, gs.yyyymm, u.id user_id, umv.first_visit_this_month, umv.last_visit_this_month, u.first_visit,
GREATEST(
LAG(last_visit_this_month) OVER w,
LAG(last_visit_this_month,2) OVER w,
LAG(last_visit_this_month,3) OVER w
) last_three_month
FROM
generate_series('2020-01-01'::TIMESTAMP, '2020-12-01'::TIMESTAMP, INTERVAL '1 month') gs(yyyymm)
CROSS JOIN mst_user u
LEFT JOIN user_monthly_visit umv on (umv.user_id=u.id AND umv.month = gs.yyyymm)
WINDOW w AS (PARTITION BY u.id ORDER BY gs.yyyymm)
) monthly_visit
ORDER BY user_id,mnt;
Результат
месяц | идентификатор пользователя | тип пользователя |
---|---|---|
1 | 1 | Первый |
2 | 1 | удержание |
3 | 1 | удержание |
4 | 1 | АКТИВИРОВАТЬ ЗАНОВО |
5 | 1 | удержание |
6 | 1 | удержание |
7 | 1 | удержание |
8 | 1 | (ноль) |
9 | 1 | (ноль) |
10 | 1 | (ноль) |
11 | 1 | (ноль) |
12 | 1 | (ноль) |
1 | 2 | (ноль) |
2 | 2 | (ноль) |
3 | 2 | (ноль) |
4 | 2 | (ноль) |
5 | 2 | Первый |
6 | 2 | удержание |
7 | 2 | удержание |
8 | 2 | удержание |
9 | 2 | (ноль) |
10 | 2 | (ноль) |
11 | 2 | (ноль) |
12 | 2 | (ноль) |
Ответ №2:
повторяю ваш вопрос, чтобы мне было понятнее: в течение любого данного месяца пользователь классифицируется как first
retention
или reactivate
на основе следующих критериев
- первый: месяц первого посещения
- срок хранения: в течение 3 месяцев с момента предыдущего посещения
- повторная активация: месяц посещения и отсутствие посещения в предыдущем месяце
Если я правильно понял, вы можете получить первый желаемый результат со следующим запросом
Схема (PostgreSQL v13)
create table visits (user_id int, visit_at timestamp);
insert into visits values
(1, '2020-01-01 12:29:15'),
(1, '2020-01-02 12:30:11'),
(1, '2020-04-01 12:31:01'),
(2, '2020-05-01 12:31:14');
Запрос
WITH trange AS (
SELECT
user_id
, DATE_TRUNC('month', min(visit_at)) visit_from
, DATE_TRUNC('month', max(visit_at)) interval '3 month' visit_to
FROM visits
GROUP BY 1
)
, monthly_visits AS (
SELECT DISTINCT
user_id
, DATE_TRUNC('month', visit_at) visit_month
FROM visits
)
SELECT
trange.user_id
, DATE(m) report_month
, CASE
WHEN visit_from = m THEN 'FIRST'
WHEN visit_month = m AND LAST_VALUE(visit_month) OVER w IS NULL THEN 'REACTIVATE'
WHEN m <= MAX(visit_month) OVER w INTERVAL '3 MONTH' THEN 'RETENTION'
ELSE NULL END user_type
FROM trange
LEFT JOIN LATERAL GENERATE_SERIES(visit_from, visit_to, '1 month') m(m)
ON true
LEFT JOIN monthly_visits
ON monthly_visits.user_id = trange.user_id
AND monthly_visits.visit_month = m.m
WINDOW w AS (
PARTITION BY trange.user_id
ORDER BY m.m
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
);
идентификатор пользователя | отчет_месяц | тип пользователя |
---|---|---|
1 | 2020-01-01T00:00:00.000 Z | Первый |
1 | 2020-02-01T00:00:00.000 Z | удержание |
1 | 2020-03-01T00:00:00.000 Z | удержание |
1 | 2020-04-01T00:00:00.000 Z | АКТИВИРОВАТЬ ЗАНОВО |
1 | 2020-05-01T00:00:00.000 Z | удержание |
1 | 2020-06-01T00:00:00.000 Z | удержание |
1 | 2020-07-01T00:00:00.000 Z | удержание |
2 | 2020-05-01T00:00:00.000 Z | Первый |
2 | 2020-06-01T00:00:00.000 Z | удержание |
2 | 2020-07-01T00:00:00.000 Z | удержание |
2 | 2020-08-01T00:00:00.000 Z | удержание |