Postgres 加入动态子查询

Postgres Join on Dynamic Subquery

背景

我有2个数据表。

对于表 A 中的每一行,我想在表 B 中找到日期最近的行,并将这些值连接到表 A 中的行。

示例表:

表A:

p_id category l_date
1 catA 2005-01-05
1 catB 2005-06-10
2 catC 2000-01-10

表B:

p_id e_id e_date
1 22 2005-01-01
1 23 2005-01-06
1 24 2005-01-06
1 28 2005-01-10
2 29 2010-08-10

期望的结果:

p_id category l_date e_id e_date
1 catA 2005-01-05 23 2005-01-06
1 catA 2005-01-05 24 2005-01-06
1 catB 2005-06-10 28 2005-01-10
2 catC 2000-01-10 29 2010-08-10

尝试过

这个查询不起作用,但我认为这是我应该去的方向。

select a.p_id, a.category, a.l_date, c.e_id, c.e_date from tableA a 
left join lateral
(
  select top 1 p_id, e_id, e_date from tableB b
  where a.pid = b.pid 
  order by abs(datediff(days, a.l_date, b.e_date))
) c on True;

TableA 和 tableB 很大,分别为 17m 和 150m 行。

这听起来像是正确的方法吗?

使用 redshift 集群,运行 postgres 8.x

相关子查询方法或完全交叉连接方法都将执行将一个 table 中的每一行与另一个中的每一行进行比较的任务(以一种或另一种方式)。当 table 变大时比较(加入)所有这些行变得令人望而却步。在这些情况下,需要不同的方法。

暴力破解不会很快(即使它能完成)所以我们需要更有效地解决这个问题。我告诉客户如果我给他们一堆索引卡,他们将如何(手动)执行此查询。一个人珍惜他们的时间,所以他们不会通过所有可能的组合来解决这个问题,他们会想出一种更有效的方法来快速完成并回到他们的生活中。在您描述的情况下,您需要找到更有效的方法。我很乐意与您更多地讨论如何构建这些类型的查询。

获取您的数据(并针对一些更有趣的案例对其进行一些修饰)我创建了一个示例来说明如何执行此操作。 (是的,您可以交叉连接小 tables 并使用更简单的 SQL 来执行此操作,但这不会扩展。)

数据设置:

create table tableA (p_id int, category varchar(64), l_date date);
insert into tableA values
(1,'catA','2005-01-05'),
(1,'catB','2005-06-10'),
(2,'catC','2000-01-10');

create table tableB (p_id int, e_id int, e_date date);
insert into tableB values
(1,22,'2005-01-01'),
(1,23,'2005-01-06'),
(1,24,'2005-06-01'),
(1,28,'2005-06-15'),
(2,29,'2010-08-10');

查询如下:

with combined as 
(
  select
    *,
    coalesce(max(l_date) OVER (partition by p_id order by
      dt rows between unbounded preceding and 1 preceding), '1970-01-01'::date) cb,
    coalesce(min(l_date) OVER (partition by p_id order by
      dt desc rows between unbounded preceding and 1 preceding), '2100-01-01'::date) ca 
  from
    (
      select
        p_id,
        category,
        l_date,
        NULL as e_id,
        NULL as e_date,
        l_date dt 
      from
        tableA 
      union all
      select
        p_id,
        NULL as category,
        NULL as l_date,
        e_id,
        e_date,
        e_date dt 
      from
        tableB 
    ) c
)
,
closest as 
(
  select
    p_id,
    e_id,
    e_date,
    cb,
    ca,
    case
      when
        coalesce(e_date - cb, 0) > (ca - e_date) 
      then ca 
      else cb 
    end closest 
  from
    combined 
  where
    e_date is not NULL
)
select
  c.p_id,
  a.category,
  a.l_date,
  c.e_id,
  c.e_date 
from
  closest c 
  left join tableA a 
  on c.closest = a.l_date and c.p_id = a.p_id 
order by
  c.p_id,
  c.e_id ;

虽然这看起来很像,但并没有那么复杂。首先 CTE 找到最接近的 l_date 早于 e_date (cb) 和最接近的 l_date 晚于 e_date (ca)。它在 UNIONed 数据集上执行此操作以允许 windowing。第二个 CTE 只是确定哪个更接近,ca 或 cb,并将其生成为“最接近”。它还删除了 UNION 添加的所有 tableB 信息(不再需要)。最后,这个“最接近”的日期提供了构建最终结果所需信息的连接。

现在这个查询没有考虑许多可能发生的现实世界数据问题,所以以此为起点。我还根据测试数据对您的数据做了一些假设(比如 tableA 中没有 2 行具有相同的 l_date 和 P_id)。所以以此为起点。

关于性能的最后一句话 - 虽然 window 函数并不便宜,并且随着数据 table 大小的增加会做更多的工作,但它们的性能比交叉函数高出几个数量级加入大量 tables。您要做的事情很复杂,因此需要一些时间,但这是我发现执行这些通常会出现大量循环问题的复杂操作的最快方法。