【问题标题】:left outer join on table with conditional select of record Oracle SQL/PL/SQL具有条件选择记录 Oracle SQL/PL/SQL 的表上的左外连接
【发布时间】:2014-12-12 20:54:22
【问题描述】:

我有一个场景,我需要从我的起始表 account_tbl 加入到 stu_ssn_tbl。我加入的列是stu_nostu_seq_no

这是 stu_ssn_tbl 的结构

CREATE TABLE stu_ssn_tbl 
(
curr_ssn_ind    character(1),
no  character(9),
pseudo_ssn_ind  character(1),
src character(8),
stu_dob date,
stu_no  integer ,
stu_seq_no  integer,
sys_cr_dt   date,
do_not_use character(1)
);

这是我的示例数据:

Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','100      ','R','234600  ',to_timestamp('29-DEC-50','DD-MON-RR HH.MI.SSXFF AM'),1,7,to_timestamp('14-AUG-12','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','101','R','234600  ',to_timestamp('29-DEC-50','DD-MON-RR HH.MI.SSXFF AM'),1,7,to_timestamp('14-AUG-12','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','102     ','R','713001  ',to_timestamp('11-JAN-00','DD-MON-RR HH.MI.SSXFF AM'),1,6,to_timestamp('14-APR-03','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','203      ','R','368700  ',to_timestamp('05-DEC-53','DD-MON-RR HH.MI.SSXFF AM'),2,3,to_timestamp('01-JUL-98','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','104 ','R','368700  ',to_timestamp('05-DEC-53','DD-MON-RR HH.MI.SSXFF AM'),2,3,to_timestamp('01-JUL-98','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','105     ','P','341100  ',to_timestamp('20-JUL-43','DD-MON-RR HH.MI.SSXFF AM'),46,3,to_timestamp('11-FEB-13','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','106','P','341100  ',to_timestamp('20-JUL-43','DD-MON-RR HH.MI.SSXFF AM'),46,3,to_timestamp('11-FEB-13','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','107      ','R','184300  ',to_timestamp('27-JAN-55','DD-MON-RR HH.MI.SSXFF AM'),3,2,to_timestamp('22-MAY-01','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','108','R','184300  ',to_timestamp('27-JAN-55','DD-MON-RR HH.MI.SSXFF AM'),3,2,to_timestamp('22-MAY-01','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','109      ','R','368700  ',to_timestamp('27-NOV-61','DD-MON-RR HH.MI.SSXFF AM'),1,3,to_timestamp('01-JUL-98','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','601 ','R','368700  ',to_timestamp('27-NOV-61','DD-MON-RR HH.MI.SSXFF AM'),1,3,to_timestamp('01-JUL-98','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','900      ','R','293900  ',to_timestamp('20-JUN-71','DD-MON-RR HH.MI.SSXFF AM'),1,5,to_timestamp('23-APR-02','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('Y','2449','R','293900  ',to_timestamp('20-JUN-71','DD-MON-RR HH.MI.SSXFF AM'),1,5,to_timestamp('09-APR-03','DD-MON-RR HH.MI.SSXFF AM'),null);
Insert into stu_ssn_tbl (CURR_SSN_IND,NO,PSEUDO_SSN_IND,SRC,STU_DOB,STU_NO,STU_SEQ_NO,SYS_CR_DT,DO_NOT_USE) values ('N','95','P','293900  ',to_timestamp('20-JUN-71','DD-MON-RR HH.MI.SSXFF AM'),1,5,to_timestamp('23-APR-02','DD-MON-RR HH.MI.SSXFF AM'),null);

为简单起见,我们称这是account 表的结构。

CREATE TABLE account_tbl
(
id integer,
stu_no integer,
stu_seq_no integer
);

所以我要在 stu_no 和 stu_seq_no 上从 account 转到 stu_ssn_tbl

SELECT *
FROM ACCOUNT_TBL ACCT
LEFT OUTER JOIN STU_SSN_TBL SSN
ON ACCT.STU_NO = SSN.STU_NO
AND ACCT.STU_SEQ_NO = SSN.STU_SEQ_NO;

问题在于,这些表中的任何一个都没有强制唯一性(这些表是从没人知道的旧数据库中转储的数据)。我需要一种方法来根据以下规则从 stu_ssn_tbl 中选择正确的记录。

  • SYS_CR_DT 最接近 ROLLUPDATE 的日期(将提供的日期,无需重复)。为了测试用例,我们可以说'2012-12-12'
  • 如果存在两个或多个相同的 SYS_CR_DT 日期,请选择 CURR_SSN_IND = 'Y' 的记录。
  • 如果无法根据 CURR_SSN_IND = 'Y' 选择记录(如果它们都是 'Y' 或 'N'),请选择 PSEUDO_SSN_IND = 'R' 的记录
  • 如果仍然无法选择匹配,请随机选择记录。

我没有要求使用 PL/SQL 或常规 SQL 来完成此操作,只需要记住我正在处理 account_tbl 中的超过 3 亿条记录和接近 1 亿条记录stu_ssn_tbl。

对此我有两种想法。最好的方法是在 account_tbl 和 stu_ssn_tbl 之间的连接期间。但是,如果这是不可能的,我添加了 do_not_use 列,它可用于在功能上删除我不希望在预处理中的记录。

到目前为止,我有这个 - 这有助于我解决第 1 个问题,我决定通过 desc 订购 CURR_SSN_IND 和 PSEUDO_SSN_IND 的订单,因为它会给我在顶部观看的记录。我尝试在其上放置前 1 行,但这为整个表带回了 1 条记录。

SELECT 
SRC.*
FROM STU_SSN_TBL SRC
LEFT OUTER JOIN STU_SSN_TBL CHK
ON SRC.STU_NO = CHK.STU_NO AND SRC.STU_SEQ_NO = CHK.STU_SEQ_NO
AND SRC.SYS_CR_DT < CHK.SYS_CR_DT
AND SRC.SYS_CR_DT < TO_DATE('2012-12-12', 'yyyy-mm-dd')
WHERE CHK.SYS_CR_DT IS NULL
ORDER BY SRC.STU_NO ASC, SRC.STU_SEQ_NO ASC, SRC.CURR_SSN_IND DESC , SRC.PSEUDO_SSN_IND DESC

【问题讨论】:

  • 是否可以将 stu_ssn_tbl 转换为临时表并根据您提供的规则使用干净的数据填充新表 stu_ssn_tbl_final?
  • 我的第一个想法是使用 CTE 简化输入数据,但 3 亿行最好创建一个新表
  • 是的,我没有任何存储限制......但是......这是 do_not_use 列背后的想法,通过将列设置为“Y”并选择在哪里删除这些记录do_not_use != 'Y'
  • 现在无法提供有效的查询 - 但我可以在星期一尝试(可以访问 Oracle 数据库)。此类查询背后的想法是进行常规连接(包含所有重复数据)并添加有关哪些规则已完成以及哪些未完成的信息列,并且只选择最佳结果。
  • 到目前为止,我的想法是通过最佳匹配选择,并将这些记录的 do_not_use 列更新为 'N',将表连接到自身 do_not_use != 'N' 并进行下一个最佳匹配,等等,直到规则得到满足。然后在我从 account_tbl 实际加入时添加到加入子句 do_not_use = 'N'

标签: sql oracle11g


【解决方案1】:

这是一个经典的 问题,但在 300m 记录上。关于这个主题的canonical question 有许多选项,从逻辑上讲,所有这些选项都应该有效。但是,大多数需要执行额外的表或索引扫描或执行大量排序。因此,我强烈建议使用 FIRSTLAST 聚合函数(也就是在 Oracle 文档中最难搜索的内容)。您可能知道这些为KEEP 子句。

FIRST 和 LAST 是对 SQL 标准的特定于 Oracle 的扩展,它们对大型数据集的有用性来自于对数据集立即执行 SORT GROUP BY 的事实,这可以大大减少考虑的记录数量您查询的其余部分。我强烈推荐阅读Rob van Wijk's blog post 以获得更全面的解释。

您需要提供排序顺序是正确的,但您希望在每个组的上下文中进行排序;通过你的逻辑:

最接近 ROLLUPDATE 的 SYS_CR_DT 日期(将提供的日期,无需重复)。为了测试用例,我们可以说'2012-12-12'

假设最大日期包含在您希望通过SYS_CR_DATE desc 订购的 ORDER BY 子句中。

如果存在两个或多个相同的 SYS_CR_DT 日期,请选择 CURR_SSN_IND = 'Y' 的记录。

然后,通过一个确定特定值优先级的语句。我通常会使用 CASE 语句来实现这一点:case when CURR_SSN_IND = 'Y' then 0 else 1 end asc(升序是默认的排序顺序,但在做复杂的事情时,如果你明确表示会更清楚)。

如果无法根据 CURR_SSN_IND = 'Y' 选择记录(如果它们都是 'Y' 或 'N'),请选择 PSEUDO_SSN_IND = 'R' 的记录

这和上面的逻辑一样,用不同的列和值保存;给你:case when PSEUDO_SSN_IND = 'R' then 0 else 1 end asc

如果仍然无法选择匹配,则随机选择记录。

这是您可能遇到问题的地方。 Oracle 会为您选择一条随机记录,无需明确说明随机性。但是,因为FIRSTLAST 将随机排序每列,您需要明确提供一个消除随机性的ORDER BY。我会使用表中列的[ROWIDs]5。这是not guaranteed to be consistent over time,但在交易期间将保持不变。


查看您的最后一个查询,我认为您尚未完全考虑到这一点,因为您没有从两个表中进行选择,而且我对您的日期逻辑有点困惑。我也有点不确定 LEFT OUTER JOIN。我在这里假设您想要一个在STU_NOSTU_SEQ_NO 上唯一的表,其中包含ACCOUNT_TBL 中存在的所有内容,并且它们是否存在于STU_SSN_TBL 中并不重要。

所以,把逻辑放在一起,这就是你最终的结果:

select a.stu_no
     , a.stu_seq_no
     , max(s.curr_ssn_id) keep (
         dense_rank first order by sys_cr_date desc
                                 , case when curr_ssn_ind = 'Y' then 0 else 1 end asc                     
                                 , case when pseudo_ssn_ind = 'R' then 0 else 1 end asc
                                 , s.rowid asc
                                 , a.rowid asc
                                   ) as curr_ssn_id
     , max(s.no) keep (
         dense_rank first order by sys_cr_date desc
                                 , case when curr_ssn_ind = 'Y' then 0 else 1 end asc                     
                                 , case when pseudo_ssn_ind = 'R' then 0 else 1 end asc
                                 , s.rowid asc
                                 , a.rowid asc
                                   ) as no
     , -- ...
     , -- keep repeating for every column
     , -- ...
     , max(s.sys_cr_dt) keep (
         dense_rank first order by sys_cr_date desc
                                 , case when curr_ssn_ind = 'Y' then 0 else 1 end asc                     
                                 , case when pseudo_ssn_ind = 'R' then 0 else 1 end asc
                                 , s.rowid asc
                                 , a.rowid asc
                                   ) as sys_cr_date
     , -- keep repeating for every column
  from account_tbl a
  left outer join stu_ssn_tbl s
    on a.stu_no = s.stu_no
   and a.stu_seq_no = s.stu_seq_no
 where s.sys_cr_date < date '2012-12-12'
 group by a.stu_no
        , a.stu_seq_no

如您所见,这非常……丑陋。但是,这应该是最有效的执行方式。

您的下一个问题是 300m 行。在任何半体面的盒子/磁盘上,我都不认为会有任何问题,即使是单线程的。但是,如果您有企业版,则值得使用parallel hint,如果您没有,DBMS_PARALLEL_EXECUTE 会更复杂,但可以用于分块ACCOUNT_TBL,以便您可以并行化。确保您通过 SQL 执行此操作,这样您就没有多个块中的 STU_NO/STU_SEQ_NO 组合。

您提到您有很多磁盘空间,这很好,但是如果您可以避开磁盘,那么您的位置会更好。您必须在这里对表进行访问,并使用一个哈希连接,Oracle 会将所有这些写入您的临时表空间,然后执行一些排序操作。您希望避免执行这些连接和排序的次数超出您的需要。


您的下一个问题应该是您是否需要重现此内容。如果你有,你有两个选择:

  1. 如果您需要保留所有可用数据,请使用DO_NOT_USE 标志的想法。我希望这是一个矛盾,你不需要这样做。
  2. 创建一组新的表,其中包含您需要的信息并将其余信息归档 - 将大量完全未使用的数据保持“热”以供使用是没有意义的。

如果您要经常执行此类操作,请考虑调整 [buffer cache][9] 的大小,以便能够将整个批次(以及您的正常操作)放入 RAM。

【讨论】:

  • 这让我大开眼界,因为我还没有听说过密集排名或保持功能。我投了赞成票,一旦有机会对其进行测试,我将接受作为答案。推荐的博客也正是我所需要的,因为它处理有效期。我唯一担心的是这是我的第一个连接,最后我将连接另外 20 个左右的表以获得一个大型非规范化表。
  • `s.rowid asc , acct.rowid asc` 的命令是做什么的?另外,分别选择每个最大列对性能有何影响?有没有办法一次选择它们,如果没有,将它们全部组合成一个字符串然后将它们分开是否有意义?
  • 性能下降会随着您选择的行数而降低。如果您只选择几个,那么通常会更快。几个的数量可能很容易达到 20 个左右,因此您必须确定自己的数据集。 rowid 的命令我想我已经解释过了;这是为了确保在重复的行中,无论可能是哪一行,您仍然选择相同的记录作为决胜局。
猜你喜欢
  • 1970-01-01
  • 2012-02-26
  • 1970-01-01
  • 2016-10-02
  • 2010-11-10
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2017-12-30
相关资源
最近更新 更多