在 Redshift 中查找下一个最旧的行

Find the next oldest row in Redshift

我在 Redshift 中有一个名为 user_activity 的 table,它有部门 user_id、activity_type、activity_id、activity_date。

我想查询自上次事件(任何类型)以来多少天的每日报告。使用 CROSS APPLY(SQL 服务器)或 LATERAL JOIN(Postgres 9+),我会做类似...

SELECT d.date, a.last_activity_date
FROM date_table d
CROSS JOIN (
            SELECT DISTINCT user_id FROM activity_table
        ) u
CROSS APPLY (
                SELECT TOP 1 activity_date as last_activity_date
                FROM activity_table
                WHERE user_id = u.user_id AND activity_date <= d.date
                ORDER BY activity_date DESC
            ) a

目前写的和下面差不多,但是有点慢,恐怕只会越来越慢。

with user_activity as (
    select distinct activity_date, user_id from activity_table
)
select
    d.date, u.user_id,
    max(u.activity_date) as last_activity_date
from date_table d
inner join user_activity u on u.activity_date <= d.date
where d.date between '2020-01-01' and current_date
group by 1, 2

有人可以根据我的需要或交叉应用/横向连接提出一个好的替代方案。

如您所见,交叉联接和不等式联接会随着数据的增长而减慢速度,并且通常不是您在 Redshift 中想要的方法。这是由于在应用于 Redshift 中典型的大数据 table 时,此类操作会增加数据量。

您想使用 window 函数来执行此类分析。但是您需要退后一步,重新考虑如何构建 SQL。一个 MAX(activity_date) window 函数,按 user_id 分区并按日期排序,并带有所有前面行的框架子句,将找到最近的 activity 到任何 activity.

现在这将只生成 user_ids 的行和数据 table 中存在的日期,看起来你想要每个 user_id 的每个日期 1 行,对吧?为此,您需要在 window 函数之前的每个 user_id 的每个日期具有 1 行的数据帧中进行 UNION。您将需要为其他列输入 NULL,以便数据宽度匹配。您还希望日期位于 activity_date 的单独列中。现在所有用户 ID 的所有日期都将在源中,window 函数将为您提供所需的结果。

您还问“这比连接有什么好处?”好吧,在连接中,您正在根据可能变得非常大的日期数复制所有数据记录。在这种方法中,您只有原始数据记录加上每个日期每个 user_id 的一行(这是输出的大小),并且随着每个 user_id 的记录数增加,这种方法不会。

————请求根据对他们方法的评论修改提问者的代码————

您的代码肯定是在正确的轨道上,因为您已经删除了原始代码中的大量不等式连接。我对此发表了 2 条评论。首先是我相信你需要 GROUP BY user_id, date 来防止每个 user_id 每个日期有多行,如果在一个日期有相同 user_id 的记录但不同activity_types。这是一个简单的疏忽。

第二个是声明我打算让您在结合实际数据和 user_id/date 框架时使用 UNION ALL,而不是 LEFT JOIN。您的方法工作正常,但我发现与大量数据联合通常比加入更快,但您确实需要确保列匹配。无论哪种方式,我们最终都会得到一个包含 3 列的数据段 - 2 个日期列,一个包含框架行的 NULL,以及 1 个 user_id。你的方法很好,性能差异可能很小,除非你有很大的 tables.

由于您要求重写,这里包含两个更改。 (注意:我的笔记本电脑在商店里,所以我目前还没有准备好访问 Redshift 并且这个 SQL 未经测试。如果意图不明确并且你需要我调试它会延迟几天后。我会保留你的设置方法和 SQL 结构。)

with date_table as (
    select '2000-01-01'::date as date
    union all
    select '2000-01-02'::date
    union all
    select '2000-01-03'::date
    union all
    select '2000-01-04'::date
    union all
    select '2000-01-05'::date
    union all
    select '2000-01-06'::date
),
users as (
    select 1 as user_id
    union all
    select 2
    union all
    select 3
),
user_activity as (
    select 1 as user_id, '2000-01-01'::date as activity_date
    union all
    select 1 as user_id, '2000-01-04'::date as activity_date
    union all
    select 3 as user_id, '2000-01-03'::date as activity_date
    union all
    select 1 as user_id, '2000-01-05'::date as activity_date
    union all
    select 1 as user_id, '2000-01-06'::date as activity_date
),
user_dates as (
    select d.date, u.user_id
    from date_table d
    cross join users u
),
user_date_activity as (
    select cal_date, user_id,
        lag(max(activity_date), 1) ignore nulls over (partition by user_id order by date) as last_activity_date
    from (
        Select user_id, date as cal_date, NULL as activity_date from user_dates
        Union all
        Select user_id, activity_date as cal_date, activity_date from user_activity 
    )
    Group by user_id, cal_date
)
select * from user_date_activity
order by user_id, cal_date```

这是我根据 Bill 的回答提出的查询。

with date_table as (
    select '2000-01-01'::date as date
    union all
    select '2000-01-02'::date
    union all
    select '2000-01-03'::date
    union all
    select '2000-01-04'::date
    union all
    select '2000-01-05'::date
    union all
    select '2000-01-06'::date
),
users as (
    select 1 as user_id
    union all
    select 2
    union all
    select 3
),
user_activity as (
    select 1 as user_id, '2000-01-01'::date as activity_date
    union all
    select 1 as user_id, '2000-01-04'::date as activity_date
    union all
    select 3 as user_id, '2000-01-03'::date as activity_date
    union all
    select 1 as user_id, '2000-01-05'::date as activity_date
    union all
    select 1 as user_id, '2000-01-06'::date as activity_date
),
user_dates as (
    select d.date, u.user_id
    from date_table d
    cross join users u
),
user_date_activity as (
    select ud.date, ud.user_id,
        lag(ua.activity_date, 1) ignore nulls over (partition by ud.user_id order by ud.date) as last_activity_date
    from user_dates ud
    left join user_activity ua on ud.date = ua.activity_date and ud.user_id = ua.user_id
)
select * from user_date_activity
order by user_id, date