根据最近日期生成类似于 vlookup 的联接

Generate a join similar to a vlookup based on closest date

我有以下两个表:

movie_sales(每日提供)

movie_rank(每隔几天或几周提供一次)

棘手的是,我每天都有销售数据,但每隔几天只有一次排名数据。这是示例数据的示例:

`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-14movie_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 很难做到这一点。在编程语言中,我会选择这个算法:

  1. 按日期对两个表进行排序并指向第一行。
  2. 向前移动排名指针,直到我们匹配销售日期或超出销售日期。 (如果我们还没有的话。)
  3. 将销售日期与我们指向的排名日期以及上一行的排名日期进行比较。拿近一点的。
  4. 将销售指针向前移动一排。
  5. 转到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) 或类似的东西。