【问题标题】:oracle join one to many relationship, return 1st joining dateoracle加入一对多关系,返回第一个加入日期
【发布时间】:2016-10-21 09:30:27
【问题描述】:

我正在尝试加入两张桌子,其中一个人可以拥有一张以上的卡,其中一些可能会被取消。

例如:

**Customer Card**
Cust ID | Cust Acct | Card No | Join Date | Cancel Date
1       | 10001     | E100001 | 20150501  | 20160101
1       | 10001     | E100002 | 20151001  | 0
2       | 10002     | E100003 | 20150101  | 20160601
3       | 10003     | E100004 | 20150201  | 0
4       | 10003     | E100005 | 20160101  | 0 

**Customer Account**
Cust ID | Cust Acct 
1       | 10001     
2       | 10002     
3       | 10003 

基本上,我想显示第一个加入卡号的所有帐户,即使卡被取消。如果第一张卡被取消,则需要显示第二张卡的加入日期。

预期结果:

Cust ID | Cust Acct | Card No | Join Date | Cancel Date
1       | 10001     | E100002 | 20151001  | 0
2       | 10002     | E100003 | 20151001  | 20160601
3       | 10003     | E100004 | 20150201  | 0

感谢您的帮助!有什么想法吗?

【问题讨论】:

  • 如果客户的所有卡都取消了怎么办,在这种情况下应该显示什么?
  • 显示最早加入

标签: sql oracle join one-to-many


【解决方案1】:

一种方法使用row_number()

select cc.*, ca.CardNo, ca.JoinDate, ca.CancelDate
from customercard cc join
     (select ca.*, 
             row_number() over (partition by custid order by joindate asc) as seqnum
      from customeraccount ca
     ) ca
     on cc.custid = ca.custid and seqnum = 1;

【讨论】:

  • 抱歉,我编辑了我的问题。我错过了一个条件。如果第一张卡被取消,则需要显示第二张卡加入日期而不是第一张卡加入日期。
【解决方案2】:

这可以使用GROUP BYKEEP(DENSE_RANK FIRST) 一次性遍历数据(不需要子查询和外部查询)。

首先是一些家务。

  • 表名和列名中不能有空格(除非您使用双引号,在大多数情况下这是一种不必要且非常糟糕的做法)。
  • 您的日期列似乎是数字格式,这是一种非常糟糕的做法。如何防止像 20151490(第 14 个月的第 90 天)这样的输入存储在数据库中?所有日期都应该存储为日期。但是,以这种格式存储它们可以进行正确的顺序比较(尽管这只是偶然的,不应该依赖)。不过,由于这不是您问题的重点,所以我按原样使用数据。
  • 为什么需要加入?第一个表不应包含cust_id - 包含它违反了数据库设计的第二个正常形式。实际上,如果您确实在第一个表中有该列,我认为不需要第二个表或连接。 (如果cust_id 不在第一个表中,那么您确实需要加入,但我将把它放在一边,因为问题实际上是关于选择正确的行,而不是加入 - 尽管有标题)。
  • 在第一个表中,您有两个 cust_id,3 和 4,与同一个帐户相关联(也与第二个表相矛盾)。我认为这是一个错字,实际上 4 应该是 3 - 但这正好说明了为什么第二范式如此重要。第一个表中不应包含 cust_id

重新制定要求的关键是条件排序。如果给定帐户的所有卡都已取消,或者没有取消,则选择最早join_date 的卡。但是,如果一个帐户混合了两种卡,则选择最早未取消的卡。在 SQL 中,这可以通过复合排序来实现(通过两个表达式,其中 SECOND 是join_date)。第一个标准是“条件”部分。在下面的解决方案中,我使用表达式CASE when cancel_date = 0 then 0 end。也就是说,一张未被取消的卡片的标志为 0,而被取消的卡片的标志为NULL(如果CASE 表达式中没有ELSE 部分,则为默认值)。默认情况下,NULL 在排序中排在最后(默认情况下为 ascending)。因此,如果所有卡仍然有效,它们都将具有标志 0,并且该标志的排序无关紧要。如果全部取消,则该标志对所有都为 NULL,因此按此标志排序将无关紧要。但是如果有的是有效的,有的是取消的,那么有效的会在前,所以最早的日期只会从有效的卡片中选择。

注意then 0(标志值为0)无关紧要;我可以将其设为 1,甚至可以设为字符串 (then 'a'),并且“条件排序”的工作原理相同,并且出于相同的原因。我将不是NULL 的内容附加到有效卡上,将NULL 附加到取消的卡上;这才是最重要的。

这也是 Gordon 需要进行的更改才能使他的解决方案发挥作用。但是,在这种情况下,我更喜欢 KEEP(DENSE_RANK FIRST) 方法,尤其是在性能很重要的情况下(当您有大量客户、帐户和信用卡存档时可能会出现这种情况)。

with
     customer_card ( cust_id , cust_acct , card_no , join_date , cancel_date ) as (
       select 1, 10001, 'E100001', 20150501, 20160101 from dual union all
       select 1, 10001, 'E100002', 20151001,        0 from dual union all
       select 2, 10002, 'E100003', 20150101, 20160601 from dual union all
       select 3, 10003, 'E100004', 20150201,        0 from dual union all
       select 3, 10003, 'E100005', 20160101,        0 from dual
     )
-- end of test data; actual solution begins HERE
select cust_id, cust_acct,
       min(card_no) keep (dense_rank first
             order by case when cancel_date = 0 then 0 end, join_date) as card_no,
       min(join_date) keep (dense_rank first
             order by case when cancel_date = 0 then 0 end, join_date) as join_date,
       min(cancel_date) keep (dense_rank first
             order by case when cancel_date = 0 then 0 end, join_date) as cancel_date
from   customer_card
group by cust_id, cust_acct
order by cust_id, cust_acct   --  ORDER BY is optional
;

输出

  CUST_ID  CUST_ACCT CARD_NO JOIN_DATE CANCEL_DATE
--------- ---------- ------- --------- -----------
        1      10001 E100002  20151001           0
        2      10002 E100003  20150101    20160601
        3      10003 E100004  20150201           0

【讨论】:

    【解决方案3】:

    试试这个:

    编辑:我正在窃取 mathguy 的“customer_card”表创建。可以想象他的方式也有效,所以这里有另一个解决方案:

    with
         customer_card ( cust_id , cust_acct , card_no , join_date , cancel_date ) as (
           select 1, 10001, 'E100001', 20150501, 20160101 from dual union all
           select 1, 10001, 'E100002', 20151001,        0 from dual union all
           select 2, 10002, 'E100003', 20150101, 20160601 from dual union all
           select 3, 10003, 'E100004', 20150201,        0 from dual union all
           select 3, 10003, 'E100005', 20160101,        0 from dual
         )
    , allresults as(
    select
    cust_id,
    cust_acct,
    card_no,
    join_date,
    cancel_date,
    rank() over(partition by cust_acct order by decode(cancel_date, 0, 1, 2), join_date, rownum) DATE_RANK
    
    from customer_card
    )
    
    select
    *
    from allresults
    where DATE_RANK = 1
    

    【讨论】:

    • “窃取”表(更确切地说,测试数据子查询)创建绝对没问题,我尽可能自己做。 {:-)
    • 谢谢。不给你功劳就做不到:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-13
    • 1970-01-01
    相关资源
    最近更新 更多