根据最近日期生成类似于 vlookup 的联接
Generate a join similar to a vlookup based on closest date
我有以下两个表:
movie_sales
(每日提供)
- movie_id
- 日期
- 收入
movie_rank
(每隔几天或几周提供一次)
- movie_id
- 日期
- 排名
棘手的是,我每天都有销售数据,但每隔几天只有一次排名数据。这是示例数据的示例:
`movie_sales`
- titanic (ID), 2014-06-01 (date), 4.99 (revenue)
- titanic (ID), 2014-06-02 (date), 5.99 (revenue)
`movie_rank`
- titanic (ID), 2014-05-14 (date), 905 (rank)
- titanic (ID), 2014-07-01 (date), 927 (rank)
而且,因为2014-05-14
的movie_rate.date
离两个销售日期比较近,所以输出应该是:
id date revenue closest_rank
titanic 2014-06-01 4.99 905
titanic 2014-06-02 5.99 905
以下查询通过获取子select中的最小日期差异来获取结果:
SELECT
id,
date,
revenue,
(SELECT rank from movie_rank where id=s.id ORDER BY ABS(DATEDIFF(date, s.date)) ASC LIMIT 1)
FROM
movie_sales s
但我担心这会有 可怕的 性能,因为它实际上会在数百万行上执行数百万次 selects...。执行此操作的更好方法是什么,或者真的没有正确的方法来执行此操作,因为无法使用 DATEDIFF
正确完成索引?
不幸的是,你是对的。必须为每个电影销售搜索电影排名 table,并在所有匹配的电影行中选择最接近的。
使用 movie_rank(id)
上的索引,DBMS 可以快速找到电影行,但是 movie_rank(id, date)
上的索引会更好,因为可以从索引中读取日期并且只有一个最佳匹配将从 table.
读取
但是你也说每隔几天就有新的排名。如果保证在一定范围内找到排名,例如对于每个日期,前二十天至少有一个排名,后二十天至少有一个排名,您可以相应地限制搜索。 (不过,movie_rank(id, date)
上的索引对此至关重要。)
SELECT
id,
date,
revenue,
(
select r.rank
from movie_rank r
where r.id = s.id
and r.date between s.date - interval 20 days
and s.date + interval 20 days
order by abs(datediff(date, s.date)) asc
limit 1
)
FROM movie_sales s;
最终的解决方案不是每次都计算所有排名,而是将它们存储(在新列中,或者如果您不想更改现有 tables).
每次更新时,您都可以查找没有排名的销售数据,并只计算这些数据。
使用上述方法,您总是从销售数据之前的最后可用排名中获得排名(例如,如果您有 14 天前和 1 天后的数据,仍然会使用之前的数据)
如果您确实需要使用时间最近的排名,那么您还需要 运行 更新新到达的排名信息。我相信它在长期 运行.
中仍然会更有效率
SQL 很难做到这一点。在编程语言中,我会选择这个算法:
- 按日期对两个表进行排序并指向第一行。
- 向前移动排名指针,直到我们匹配销售日期或超出销售日期。 (如果我们还没有的话。)
- 将销售日期与我们指向的排名日期以及上一行的排名日期进行比较。拿近一点的。
- 将销售指针向前移动一排。
- 转到2。
有了这个算法,我们就已经处于我们想要的位置了。让我们看看,如果我们可以用 SQL 做同样的事情。在 SQL 中使用递归查询完成迭代。这些在 MySQL 版本 8.0 中可用。
我们从对行进行排序开始,即给它们编号。然后我们遍历两个数据集。
with recursive
sales as
(
select *, row_number() over (partition by movie_id order by date) as rn
from movie_sales
),
ranks as
(
select *, row_number() over (partition by movie_id order by date) as rn
from movie_rank
),
cte (movie_id, revenue, srn, rrn, sdate, rdate, rrank, closest_rank) as
(
select
movie_id, s.revenue, s.rn, r.rn, s.date, r.date, r.ranking,
case when s.date <= r.date then r.ranking end
from (select * from sales where rn = 1) s
join (select * from ranks where rn = 1) r using (movie_id)
union all
select
cte.movie_id,
cte.revenue,
coalesce(s.rn, cte.srn),
coalesce(r.rn, cte.rrn),
coalesce(s.date, cte.sdate),
coalesce(r.date, cte.rdate),
coalesce(r.ranking, cte.rrank),
case when coalesce(r.date, cte.rdate) >= coalesce(s.date, cte.sdate) then
case when abs(datediff(coalesce(r.date, cte.rdate), coalesce(s.date, cte.sdate))) <
abs(datediff(cte.rdate, coalesce(s.date, cte.sdate)))
then coalesce(r.ranking, cte.rrank)
else cte.rrank
end
end
from cte
left join sales s on s.movie_id = cte.movie_id and s.rn = cte.srn + 1 and cte.closest_rank is not null
left join ranks r on r.movie_id = cte.movie_id and r.rn = cte.rrn + 1 and cte.rdate < cte.sdate
where s.movie_id is not null or r.movie_id is not null
-- where cte.closest_rank is null
)
select
movie_id,
sdate,
revenue,
closest_rank
from cte
where closest_rank is not null;
(顺便说一句:我将该列命名为 ranking
,因为 rank
是 SQL 中的保留字。)
演示:https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=e994cb56798efabc8f7249fd8320e1cf
这可能仍然很慢。原因是:SQL 中没有指向一行的指针。如果我们想从第 1 行转到第 2 行,我们必须搜索该行,而在编程语言中,我们实际上只是将指针向前移动了一步。如果表有 ID,我们可以构建一个链 (next_row_id) 而不是使用行号。这可以加快这个过程。但是,我猜你已经注意到了:这不是为 SQL.
设计的算法
另一种方法...通过清理数据避免问题。
确保每天都有排名。当新日期到来时,找到之前的排名,然后填写中间日期的所有行。
(这将需要一些初始努力 'fix' 所有以前缺失的日期。之后,当新的排名列表出现时,这是一个很小的努力。)
“报告”将是一个简单的 JOIN
日期。您可能需要 2 列 INDEX(movie_id, date)
或类似的东西。
我有以下两个表:
movie_sales
(每日提供)
- movie_id
- 日期
- 收入
movie_rank
(每隔几天或几周提供一次)
- movie_id
- 日期
- 排名
棘手的是,我每天都有销售数据,但每隔几天只有一次排名数据。这是示例数据的示例:
`movie_sales`
- titanic (ID), 2014-06-01 (date), 4.99 (revenue)
- titanic (ID), 2014-06-02 (date), 5.99 (revenue)
`movie_rank`
- titanic (ID), 2014-05-14 (date), 905 (rank)
- titanic (ID), 2014-07-01 (date), 927 (rank)
而且,因为2014-05-14
的movie_rate.date
离两个销售日期比较近,所以输出应该是:
id date revenue closest_rank
titanic 2014-06-01 4.99 905
titanic 2014-06-02 5.99 905
以下查询通过获取子select中的最小日期差异来获取结果:
SELECT
id,
date,
revenue,
(SELECT rank from movie_rank where id=s.id ORDER BY ABS(DATEDIFF(date, s.date)) ASC LIMIT 1)
FROM
movie_sales s
但我担心这会有 可怕的 性能,因为它实际上会在数百万行上执行数百万次 selects...。执行此操作的更好方法是什么,或者真的没有正确的方法来执行此操作,因为无法使用 DATEDIFF
正确完成索引?
不幸的是,你是对的。必须为每个电影销售搜索电影排名 table,并在所有匹配的电影行中选择最接近的。
使用 movie_rank(id)
上的索引,DBMS 可以快速找到电影行,但是 movie_rank(id, date)
上的索引会更好,因为可以从索引中读取日期并且只有一个最佳匹配将从 table.
但是你也说每隔几天就有新的排名。如果保证在一定范围内找到排名,例如对于每个日期,前二十天至少有一个排名,后二十天至少有一个排名,您可以相应地限制搜索。 (不过,movie_rank(id, date)
上的索引对此至关重要。)
SELECT
id,
date,
revenue,
(
select r.rank
from movie_rank r
where r.id = s.id
and r.date between s.date - interval 20 days
and s.date + interval 20 days
order by abs(datediff(date, s.date)) asc
limit 1
)
FROM movie_sales s;
最终的解决方案不是每次都计算所有排名,而是将它们存储(在新列中,或者如果您不想更改现有 tables).
每次更新时,您都可以查找没有排名的销售数据,并只计算这些数据。
使用上述方法,您总是从销售数据之前的最后可用排名中获得排名(例如,如果您有 14 天前和 1 天后的数据,仍然会使用之前的数据)
如果您确实需要使用时间最近的排名,那么您还需要 运行 更新新到达的排名信息。我相信它在长期 运行.
中仍然会更有效率SQL 很难做到这一点。在编程语言中,我会选择这个算法:
- 按日期对两个表进行排序并指向第一行。
- 向前移动排名指针,直到我们匹配销售日期或超出销售日期。 (如果我们还没有的话。)
- 将销售日期与我们指向的排名日期以及上一行的排名日期进行比较。拿近一点的。
- 将销售指针向前移动一排。
- 转到2。
有了这个算法,我们就已经处于我们想要的位置了。让我们看看,如果我们可以用 SQL 做同样的事情。在 SQL 中使用递归查询完成迭代。这些在 MySQL 版本 8.0 中可用。
我们从对行进行排序开始,即给它们编号。然后我们遍历两个数据集。
with recursive
sales as
(
select *, row_number() over (partition by movie_id order by date) as rn
from movie_sales
),
ranks as
(
select *, row_number() over (partition by movie_id order by date) as rn
from movie_rank
),
cte (movie_id, revenue, srn, rrn, sdate, rdate, rrank, closest_rank) as
(
select
movie_id, s.revenue, s.rn, r.rn, s.date, r.date, r.ranking,
case when s.date <= r.date then r.ranking end
from (select * from sales where rn = 1) s
join (select * from ranks where rn = 1) r using (movie_id)
union all
select
cte.movie_id,
cte.revenue,
coalesce(s.rn, cte.srn),
coalesce(r.rn, cte.rrn),
coalesce(s.date, cte.sdate),
coalesce(r.date, cte.rdate),
coalesce(r.ranking, cte.rrank),
case when coalesce(r.date, cte.rdate) >= coalesce(s.date, cte.sdate) then
case when abs(datediff(coalesce(r.date, cte.rdate), coalesce(s.date, cte.sdate))) <
abs(datediff(cte.rdate, coalesce(s.date, cte.sdate)))
then coalesce(r.ranking, cte.rrank)
else cte.rrank
end
end
from cte
left join sales s on s.movie_id = cte.movie_id and s.rn = cte.srn + 1 and cte.closest_rank is not null
left join ranks r on r.movie_id = cte.movie_id and r.rn = cte.rrn + 1 and cte.rdate < cte.sdate
where s.movie_id is not null or r.movie_id is not null
-- where cte.closest_rank is null
)
select
movie_id,
sdate,
revenue,
closest_rank
from cte
where closest_rank is not null;
(顺便说一句:我将该列命名为 ranking
,因为 rank
是 SQL 中的保留字。)
演示:https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=e994cb56798efabc8f7249fd8320e1cf
这可能仍然很慢。原因是:SQL 中没有指向一行的指针。如果我们想从第 1 行转到第 2 行,我们必须搜索该行,而在编程语言中,我们实际上只是将指针向前移动了一步。如果表有 ID,我们可以构建一个链 (next_row_id) 而不是使用行号。这可以加快这个过程。但是,我猜你已经注意到了:这不是为 SQL.
设计的算法另一种方法...通过清理数据避免问题。
确保每天都有排名。当新日期到来时,找到之前的排名,然后填写中间日期的所有行。
(这将需要一些初始努力 'fix' 所有以前缺失的日期。之后,当新的排名列表出现时,这是一个很小的努力。)
“报告”将是一个简单的 JOIN
日期。您可能需要 2 列 INDEX(movie_id, date)
或类似的东西。