【问题标题】:SQL: join tables with valid from / valid to fieldsSQL:使用有效的 from / valid to 字段连接表
【发布时间】:2021-11-09 10:59:06
【问题描述】:

我在我们的 Postgresql 数据库中遇到了一个非常普遍的问题。许多表格包含仅在一段时间内有效的条目,例如可能随时间演变的合同的详细信息。

为了处理它,提供了valid fromvalid to两个字段来表示行内容的有效期。每次合同更改时,都会在表格中添加一行,其中包含实际信息和相应的有效日期。

在对具有重叠有效性字段的表执行连接时,会出现主要问题。更准确地说,给定第一个表:

fg     valid_from    valid_to    attr_table1
key1   2020-01-01   2020-01-18        A
key1   2020-01-19   null              B
key2   2020-01-01   2020-01-30        A
key2   2020-01-30   null              B

还有第二张桌子

fg     valid_from    valid_to    attr_table2
key1   2020-01-01   2020-01-10       1.0
key1   2020-01-10   null             3.0
key2   2020-01-01   2020-01-30      10.0
key2   2020-01-30   null            11.0

我想建立一个联结表,其有效性字段嵌入两个表的有效期,如:

fg     valid_from   valid_to    attr_table1  attr_table2
key1   2020-01-01   2020-01-10        A         1.0
key1   2020-01-10   2020-01-18        A         3.0
key1   2020-01-18   null              B         3.0
key2   2020-01-01   2020-01-30        A         10.0
key2   2020-01-30   null              B         11.0

到目前为止,我最有说服力的尝试是切换到 Postgresql 特定类型 daterange,并使用 && 运算符(“有共同点”)。我将valid fromvalid to 字段连接到validity 字段中,下一个查询似乎可以完成这项工作:

select t1.fg,
       (case when upper(t1.validity) is null
             then case when (upper(t2.validity) is null) 
                       then case when lower(t1.validity) > lower(t2.validity) 
                                 then daterange(lower(t1.validity), null)
                                 else daterange(lower(t2.validity), null)
                                 end
                       else case when lower(t1.validity) > lower(t2.validity) 
                                 then daterange(lower(t1.validity), upper(t2.validity)) 
                                 else daterange(lower(t2.validity), upper(t2.validity)) 
                                 end
                       end
             when upper(t2.validity) is null
             then case when (upper(t1.validity) is null) 
                       then case when lower(t1.validity) > lower(t2.validity) 
                                 then daterange(lower(t1.validity), null)
                                 else daterange(lower(t2.validity), null)
                                 end
                       else case when lower(t1.validity) > lower(t2.validity) 
                                 then daterange(lower(t1.validity), upper(t1.validity)) 
                                 else daterange(lower(t2.validity), upper(t1.validity)) 
                                 end
                       end
             when lower(t1.validity) <= lower(t2.validity)
             then case when upper(t1.validity) >= upper(t2.validity) 
                       then daterange(lower(t2.validity), upper(t2.validity))
                       else daterange(lower(t2.validity), upper(t1.validity))
                       end
             else case when upper(t1.validity) >= upper(t2.validity) 
                       then daterange(lower(t1.validity), upper(t2.validity))
                       else daterange(lower(t1.validity), upper(t1.validity))
                       end
             end
            ) as validity,
       t1.attr_table1, 
       t2.attr_table2
  from table1 as t1 
       join table2 as t2
         on t1.fg = t2.fg
        and t1.validity && t2.validity
order by fg, validity

但是,当第一个表的起点与第二个表中的任何条目都不匹配时,此查询将失败。例如,在第一个和第二个表中增加一行,例如

在表1中:

key1 2019-12-25 2020-01-01 A

在表2中:

key1 2019-12-27 2020-01-01 -1

输出表的第一行结果是

key1 2019-12-27 2020-01-01 A -1

而不是

key1   2019-12-25   2019-12-27        A    null
key1   2019-12-27   2020-01-01        A    -1 

有人知道更好的方法吗?

编辑:创建table1和table2的代码,依赖于日期范围:

create table table1
( 
  fg text, 
  validity daterange, 
  attr_table1 text
);
insert into table1 
values
('key1', daterange('2020-01-01', '2020-01-18'),  'A'),
('key1', daterange('2020-01-18', null        ),  'B'),
('key2', daterange('2020-01-01', '2020-01-30'),  'A'),
('key2', daterange('2020-01-30', null        ),  'B')

create table table2
( 
  fg text, 
  validity daterange,  
  attr_table2 text
);
insert into table2 
values
('key1', daterange('2020-01-01', '2020-01-10'),   1.0),
('key1', daterange('2020-01-10', null        ),   3.0),
('key2', daterange('2020-01-01', '2020-01-30'),  10.0),
('key2', daterange('2020-01-30', null        ),  11.0)

【问题讨论】:

  • 您能否发布创建表和插入或创建 sqlfiddle 以便我们可以处理您的数据?
  • 你可以使用range_merge来简化你的case switch部分
  • @NuLo,你说得对,我使用 daterange 函数添加了创建表和插入。
  • 每个键在每个表的第一行中是否具有相同的 valid_from 日期?比如key1在两个表中都是从2020-01-01开始的,但是table2中的第一行有没有可能是2020-01-02呢?
  • @MatBailie,是的,不同的键可以有不同的开始日期,同一个键在两个表中可以有不同的开始日期。这是我提出的方法的一个真正问题。 && 运算符无法按我的意愿合并第一行,如果开始日期不同..

标签: sql postgresql join date-range


【解决方案1】:

[更新]

  • 根据fg 制作所有现有时间跨度的日历表
  • LEFT 将 table1 和 table2 连接到此表
  • [为了便于比较,我将valid_to NULL 更改为infinity]

create table table1
(
  fg text,
  validity daterange,
  attr_table1 text
);

insert into table1
values
('key1', daterange('2019-12-25', '2020-01-01'),  'A'), -- NEW
('key1', daterange('2020-01-01', '2020-01-18'),  'A'),
('key1', daterange('2020-01-19', 'infinity'        ),  'B'),
('key2', daterange('2020-01-01', '2020-01-30'),  'A'),
('key2', daterange('2020-01-30', 'infinity'        ),  'B');

create table table2
(
  fg text,
  validity daterange,
  attr_table2 text
);
insert into table2
values
('key1', daterange('2019-12-27', '2020-01-01'),  -1  ), -- NEW
('key1', daterange('2020-01-01', '2020-01-10'),   1.0),
('key1', daterange('2020-01-10', 'infinity'        ),   3.0),
('key2', daterange('2020-01-01', '2020-01-30'),  10.0),
('key2', daterange('2020-01-30', 'infinity'        ),  11.0);


        -- Make a 'CALENDAR' table with all points in time (per fg)
        -- ---------------------------------------------------------
WITH pits AS (
        select distinct fg, lower(validity) as pit FROM table1
UNION
        select distinct fg, upper(validity) as pit FROM table1
UNION
        select distinct fg, lower(validity) as pit FROM table2
UNION
        select distinct fg, upper(validity) as pit FROM table2
        )
        -- combine all adjacent PITs to ranges
        -- ---------------------------------------
, pairs AS (
        SELECT fg, pit AS opit
        , lead(pit) OVER (PARTITION BY fg ORDER BY pit) AS npit
        from pits
        )
        -- Make dateranges from them
        -- --------------------------
, tablex AS (
        SELECT fg
        , daterange(opit,npit) AS validity
        FROM pairs
        WHERE npit IS NOT NULL
        -- ORDER BY 1,2;
        )
        -- Left join both table1 and table2 to all_rages
        -- ----------------------------------------------
SELECT tx.fg
        , tx.validity
        , t1.validity * t2.validity AS overlapped
        , t1.attr_table1
        , t2.attr_table2
FROM tablex tx
LEFT JOIN table1 t1 ON t1.fg = tx.fg AND t1.validity && tx.validity
LEFT JOIN table2 t2 ON t2.fg = tx.fg AND t2.validity && tx.validity
ORDER BY 1,2
        ;

结果:


DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
INSERT 0 5
CREATE TABLE
INSERT 0 5
  fg  |        validity         |       overlapped        | attr_table1 | attr_table2 
------+-------------------------+-------------------------+-------------+-------------
 key1 | [2019-12-25,2019-12-27) |                         | A           | 
 key1 | [2019-12-27,2020-01-01) | [2019-12-27,2020-01-01) | A           | -1
 key1 | [2020-01-01,2020-01-10) | [2020-01-01,2020-01-10) | A           | 1.0
 key1 | [2020-01-10,2020-01-18) | [2020-01-10,2020-01-18) | A           | 3.0
 key1 | [2020-01-18,2020-01-19) |                         |             | 3.0
 key1 | [2020-01-19,infinity)   | [2020-01-19,infinity)   | B           | 3.0
 key2 | [2020-01-01,2020-01-30) | [2020-01-01,2020-01-30) | A           | 10.0
 key2 | [2020-01-30,infinity)   | [2020-01-30,infinity)   | B           | 11.0
(8 rows)

【讨论】:

  • 我认为这并不能回答问题的第二部分,它只是对原始查询的简化
【解决方案2】:

赶到下一次会议,稍后会写一个解释,但现在......

  • 主要依赖于行之间没有任何间隙
  • 还依赖于 attrib1 或 attrib2 的值从不为空(空值被以前的非空值替换)

给予...

with
  combined AS
(
  select fg, lower(validity) AS valid_from, attr_table1, NULL as attr_table2 from table1
  union all
  select fg, lower(validity) AS valid_from, NULL AS attr_table1, attr_table2 from table2
),
  aggregated AS
(
  select
    fg,
    valid_from,
    max(attr_table1)  as attr_table1,
    max(attr_table2)  as attr_table2,
    count(max(attr_table1)) over (partition by fg order by valid_from) attrib1_grp,
    count(max(attr_table2)) over (partition by fg order by valid_from) attrib2_grp
  from
    combined
  group by
    fg,
    valid_from
)
SELECT
  fg,
  valid_from,
  lead(valid_from) over (partition by fg order by valid_from)  as valid_to,
  max(attr_table1) over (partition by fg, attrib1_grp)         as attr_table1,
  max(attr_table2) over (partition by fg, attrib2_grp)         as attr_table2
from
  aggregated
order by
  fg,
  valid_from

演示:https://dbfiddle.uk/?rdbms=postgres_13&fiddle=7d97c9623e5f9efb4d729775ff61e7b5


编辑:

上述代码的前提是,如果 either 表中某个键的属性发生变化,则结果集也需要在该日期发生变化。

这意味着我们可以合并两个表,只保留valid_from,并使用LEAD()计算valid_to(有时会从同一个表中提取valid_from,有时会从另一个表中提取)。

这会在属性列中留下一堆空值。如果attrib1 发生变化,attrib2 将成为联合集中的NULL,反之亦然。

填充这些空值所需的是回顾新的时间序列以找到该属性的最新NOT NULL 值。由于LAST_VALUE() 没有SKIP NULLS 选项,所以我推出了自己的...

  • 有一个累积计数器,用于记录属性被NOT NULL 的次数,并将其用作组标识符
  • 根据定义,组中的第一个属性是NOT NULL,所有行都是NULL
  • 因此,将MAX(attribute) 用于组可以让我获得所有NULL 行的先前NOT NULL

这个演示可以更容易地查看正在发生的计算...


编辑:

认为这现在适用于间隙(属性隐式设置为NULL)和属性显式的行设置为NULL...

  • 假设没有两行(同一个键,同一个表)在同一个日期开始
    • 如果发生这种情况,该属性将选择该日期的 MAX() 值
  • 假设在前一行(同一个键,同一个表)结束之前没有任何行可以开始
    • 如果发生这种情况,则会返回乱码

(虽然我建议进行更严格的测试...)

with
  combined(
    fg, valid_from, attr_table1, attr_table2, atrib1_set, atrib2_set
  ) AS
(
  select fg, lower(validity), attr_table1, NULL       , 1, NULL::int from table1
  union all
  select fg, upper(validity), NULL,        NULL       , 1, NULL::int from table1
  union all
  select fg, lower(validity), NULL       , attr_table2, NULL, 1 from table2
  union all
  select fg, upper(validity), NULL       , NULL       , NULL, 1 from table2
),
  aggregated AS
(
  select
    fg,
    valid_from,
    max(attr_table1)  as attr_table1,
    max(attr_table2)  as attr_table2,
    count(max(atrib1_set)) over (partition by fg order by valid_from) attrib1_grp,
    count(max(atrib2_set)) over (partition by fg order by valid_from) attrib2_grp
  from
    combined
  where
    valid_from is not null
  group by
    fg,
    valid_from
)
SELECT
  fg,
  valid_from,
  lead(valid_from) over (partition by fg order by valid_from)  as valid_to,
  max(attr_table1) over (partition by fg, attrib1_grp)         as attr_table1,
  max(attr_table2) over (partition by fg, attrib2_grp)         as attr_table2
from
  aggregated
order by
  fg,
  valid_from

演示:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-02
    • 2018-01-02
    • 1970-01-01
    • 1970-01-01
    • 2013-09-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多