【问题标题】:Generate a join similar to a vlookup based on closest date根据最近的日期生成类似于 vlookup 的连接
【发布时间】:2020-10-02 22:59:17
【问题描述】:

我有以下两张表:

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-14movie_rate.date 更接近两个销售日期,所以输出应该是:

id         date             revenue           closest_rank
titanic    2014-06-01       4.99               905
titanic    2014-06-02       5.99               905

以下查询通过获取子选择中的最小日期差异来获取结果:

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

但我担心这会产生糟糕的性能,因为它实际上会在数百万行上执行数百万次子选择。有什么更好的方法来做到这一点,或者真的没有合适的方法来做到这一点,因为索引不能用 DATEDIFF 正确完成?

【问题讨论】:

    标签: mysql sql query-performance


    【解决方案1】:

    不幸的是,你是对的。必须在电影排名表中搜索每个电影销售,并在所有匹配的电影行中选择最接近的行。

    使用movie_rank(id) 上的索引,DBMS 可以快速找到电影行,但movie_rank(id, date) 上的索引会更好,因为可以从索引中读取日期,并且只会从表中读取一个最佳匹配项.

    但你也说每隔几个日期就会有新的排名。如果保证在一定范围内找到排名,例如对于每个日期,前二十天至少有一个排名,后二十天至少有一个排名,您可以相应地限制搜索。 (不过,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;
    

    【讨论】:

    • 谢谢,我认为由于我每天只执行一次此查询(在数据加载作业中),因此此查询是最易于维护的,无需担心出现的许多其他因素采用不同的方法。
    【解决方案2】:

    这很难用 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 设计的算法。

    【讨论】:

    • 这很酷,我很乐意这样做——我应该用 mysql5.7 标记这个问题,但是是的,这是一个非常好的方法,谢谢!
    • 一个问题,Sort the two tables by date and point to the first rows.。你为什么要先这样做?为什么不先按片名分区,再按日期排序?
    • 好吧,我按电影分区并按日期排序。只是,根据定义,表是一组无序的数据。子查询结果又是一个表(只是它不存储到磁盘,而只存在于内存中)。因此,虽然我可以通过对行进行编号来记住顺序,但在完成第 1 行之后,我仍然需要搜索第 2 行,因为 #2 在物理上并不位于 #1 之后。这是尝试在 SQL 中实现此算法时的一大缺点。
    【解决方案3】:

    另一种方法...通过清理数据来避免问题。

    确保每天都有排名可用。当有新日期到来时,找到之前的排名,然后填写中间日期的所有行。

    (这将需要一些初步的努力来“修复”所有之前丢失的日期。之后,当新的排名列表出现时,这是一个很小的努力。)

    “报告”将是日期的简单JOIN。您可能需要 2 列 INDEX(movie_id, date) 或类似的东西。

    【讨论】:

    • 是的,这就是我目前正在做的事情。我更喜欢在 SQL 中完成这一切,因为一些数据以非常奇怪的方式更新(回填 ids...销售数据在与排名不同的时间出现等)
    【解决方案4】:

    最终的解决方案不是每次都计算所有排名,而是将它们存储(在新列中,如果您不想更改现有表,甚至在新表中)。

    每次更新时,您都可以查找没有排名的销售数据并仅计算这些数据。

    使用上述方法,您总是从销售数据之前的最后一个可用排名获得排名(例如,如果您有 14 天前和 1 天后的数据,仍然会使用之前的数据)

    如果您严格需要使用最接近时间的排名,那么您还需要对新到达的排名信息运行 UPDATE。我相信从长远来看它仍然会更有效率。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多