SQL — вывод таблицы ежедневного статуса пользователей из таблицы активности пользователей

У меня есть таблица, в которой указаны действия пользователей по их членству (активировать или деактивировать) и когда эти действия произошли. Таблица активности выглядит следующим образом:

ID пользователя активность дата 123 активировать 01.06.2024 123 деактивировать 15.06.2024 123 активировать 20.06.2024 123 деактивировать 30.06.2024 456 активировать 25.06.2024 123 деактивировать 08.07.2024 123 активировать 10.07.2024

Существует еще одна таблица под названием dim_date, которая в основном содержит одну строку для каждой даты до сегодняшнего дня, например:

дата 01.06.2024 02.06.2024 03.06.2024 ... 21.07.2024

Каков оптимальный способ написания SQL для возврата таблицы ежедневного статуса, в которой есть одна строка для каждого пользователя в день, показывающая, является ли его статус членства активным или неактивным? Выходная таблица должна выглядеть так:

ID пользователя дата членский_статус 123 01.06.2024 активный 123 02.06.2024 активный 123 ... активный 123 14.06.2024 активный 123 15.06.2024 неактивный 123 16.06.2024 неактивный 123 ... неактивный 123 19.06.2024 неактивный 123 20.06.2024 активный 123 21.06.2024 активный 123 ... активный 123 29.06.2024 активный 123 30.06.2024 неактивный 123 01.07.2024 неактивный 123 ... неактивный 123 21.07.2024 неактивный

На данный момент у меня есть:

with cte as ( 
select   
a.user_id   
,a.activity   
,a.date as activity_date   
,dd.date   
,row_number() over (partition by a.user_id, dd.date order by a.date desc) as rn 
from activity a 
left join dim_date dd on a.date <= dd.date 
) 
select    
user_id   
,date   
,case when a.activity = "activate" then "active" else "inactive" end as membership_status 
from cte 
where rn = 1

Интересно, может ли кто-нибудь предложить что-нибудь попроще? Возможно без cte/подзапроса?

Будет идеально, если вы сможете разместить таблицы DDL и образцы данных в онлайн-скрипте, например sqize.online или другом.

Slava Rozhnev 22.07.2024 07:49

Что не так с вашим запросом? Это кажется достаточно простым.

Eric 22.07.2024 16:29

@Эрик, извини, я хотел попросить более оптимизированный запрос, а не просто более простой.

Mia 24.07.2024 05:48
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
3
51
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Возможно, не совсем проще, но я надеюсь, что это будет понятно и легко читать с помощью скалярного подзапроса для membership_status.

select dd.date, user_id,
 (
   select case a.activity when 'activate' then 'active' when 'deactivate' then 'inactive' end
   from activity a
   where user_id = u.user_id and a.date <= dd.date
   order by a.date desc limit 1
 )  membership_status
from dim_date dd
cross join (select distinct user_id from activity) u
order by user_id, dd.date;

Демо-версия DB Fiddle

Ответ принят как подходящий

«Проще» — это субъективно. Основная проблема вашего исходного запроса не в сложности, он не очень сложен. Но у него будет большой промежуточный набор результатов с большой избыточностью, чтобы row_number() мог рассчитывать только на наличие условия a.date <= dd.date.

Явное определение эффективного диапазона (начало/конец) с помощью lead() и присоединение к дате, попадающей в этот диапазон, вероятно, будет более эффективным.

Рабочий пример: https://dbfiddle.uk/8WcyWwOa

Примечание. От coalesce() до «9999-12-31» — это немного синтаксического сахара, просто чтобы заставить дату измерения оказаться ниже производного конца последнего статуса пользователя.

CREATE TABLE activities (
    user_id INTEGER,
    activity VARCHAR(20),
    date DATE
);

INSERT INTO activities (user_id, activity, date) VALUES
(123, 'activate', '2024-06-01'),
(123, 'deactivate', '2024-06-15'),
(123, 'activate', '2024-06-20'),
(123, 'deactivate', '2024-06-30'),
(456, 'activate', '2024-06-25'),
(123, 'deactivate', '2024-07-08'),
(123, 'activate', '2024-07-10');

CREATE TABLE dim_date (
    date_value DATE
);

INSERT INTO dim_date (date_value)
SELECT generate_series('2024-06-01'::DATE, '2024-07-22'::DATE, '1 day'::INTERVAL)::DATE;

with assign_end_date_to_status as 
  (
   select a.*,
          coalesce(
            lead(a.date) over ( partition by a.user_id order by a.date) 
              - interval '1 DAY', '9999-12-31'::DATE)  as status_end
     from activities a
  )
select ads.user_id,
       dd.date_value,
       case when ads.activity = 'activate' then 'active'
            else 'inactive'
        end as membership_status
  from assign_end_date_to_status ads
 inner
  join dim_date dd
    on dd.date_value between ads.date and ads.status_end;

Другие вопросы по теме