CASE 内的 LAG 给出假阴性偏移

LAG within CASE giving false negative offset

TL;DR:向下滚动到任务 2。

我正在处理以下数据集:

email,createdby,createdon
a@b.c,jsmith,2016-10-10
a@b.c,nsmythe,2016-09-09
a@b.c,vstark,2016-11-11
b@x.y,ajohnson,2015-02-03
b@x.y,elear,2015-01-01
...

等等。保证每封邮件在数据集中至少有一份重复。

现在,有两个任务要解决;我解决了其中一个问题,但正在努力解决另一个问题。为了完整起见,我现在将介绍这两项任务。

任务 1(已解决): 对于每一行,对于每封电子邮件,return 一个附加列,其中包含使用该电子邮件创建第一条记录的用户的姓名。

上述样本数据集的预期结果:

email,createdby,createdon,original_createdby
a@b.c,jsmith,2016-10-10,nsmythe
a@b.c,nsmythe,2016-09-09,nsmythe
a@b.c,vstark,2016-11-11,nsmythe
b@x.y,ajohnson,2015-02-03,elear
b@x.y,elear,2015-01-01,elear

获取以上代码:

;WITH   q0 -- this is just a security measure in case there are unique emails in the data set
          AS ( SELECT   t.email
               FROM     t
               GROUP BY t.email
               HAVING   COUNT(*) > 1) ,
        q1
          AS ( SELECT   q0.email
                      , createdon
                      , createdby
                      , ROW_NUMBER() OVER ( PARTITION BY q0.email ORDER BY createdon ) rn
               FROM     t
               JOIN     q0
                        ON t.email = q0.email)
    SELECT  q1.email
          , q1.createdon
          , q1.createdby
          , LAG(q1.createdby, q1.rn - 1) OVER ( ORDER BY q1.email, q1.createdon ) original_createdby
    FROM    q1
    ORDER BY q1.email
          , q1.rn

简要说明:我按电子邮件对数据集进行分区,然后按创建日期对每个分区中的行进行编号,最后我 return [createdby] 来自第 (rn-1) 条记录的值。完全按照预期工作。

现在,与上面类似,还有任务 2:

任务 2: 对于每一行,对于每封电子邮件,return 创建第一个副本的用户的姓名。 IE。用户名,其中 rn=2.

预期结果:

email,createdby,createdon,first_dupl_createdby
a@b.c,jsmith,2016-10-10,jsmith
a@b.c,nsmythe,2016-09-09,jsmith
a@b.c,vstark,2016-11-11,jsmith
b@x.y,ajohnson,2015-02-03,ajohnson
b@x.y,elear,2015-01-01,ajohnson

我想保持性能,所以尝试使用 LEAD-LAG 函数:

    WITH    q0
          AS ( SELECT   t.email
               FROM     t
               GROUP BY t.email
               HAVING   COUNT(*) > 1) ,
        q1
          AS ( SELECT   q0.email
                      , createdon
                      , createdby
                      , ROW_NUMBER() OVER ( PARTITION BY q0.email ORDER BY createdon ) rn
               FROM     t
               JOIN     q0
                        ON t.email = q0.email)
    SELECT  q1.email
          , q1.createdon
          , q1.createdby
          , q1.rn
          , CASE q1.rn
              WHEN 1 THEN LEAD(q1.createdby, 1) OVER ( ORDER BY q1.email, q1.createdon )
              ELSE LAG(q1.createdby, q1.rn - 2) OVER ( ORDER BY q1.email, q1.createdon )
            END AS first_dupl_createdby
    FROM    q1
    ORDER BY q1.email
          , q1.rn

说明:对于每个分区中的第一条记录,return [createdby] 来自以下记录(即来自包含第一个重复项的记录)。对于同一分区 return [createdby] 来自 (rn-2) 条记录的所有其他记录(即对于 rn = 2,我们停留在同一记录上,对于 rn = 3,我们将返回 1 条记录, 对于 rn = 4 - 2 条记录等等)。

出现问题
ELSE LAG(q1.createdby, q1.rn - 2)

操作。显然,与任何逻辑相反,尽管存在前一行 (WHEN 1 THEN...),ELSE 块也被评估为 rn = 1,导致传递给 LAG 函数的负偏移值:

消息 8730,级别 16,状态 2,第 37 行 Lag 和 Lead 函数的偏移参数不能为负值。

当我注释掉 ELSE 行时,整个过程都正常,但显然我没有在 rn > 1 的 first_dupl_createdby 列中得到任何结果。

问题: 有没有什么方法可以重写上面的 CASE 语句(在任务 #2 中),以便它总是 returns 来自每个分区中 rn = 2 的记录的值但是 - 这很重要 - 没有做自连接操作(我知道我可以在单独的子查询中准备 rn = 2 的行,但这将意味着对整个 table 和 运行 不必要的自连接进行额外扫描)。

您可以使用 row_number() 和条件聚合获取每封电子邮件的信息:

select email,
       max(case when seqnum = 1 then createdby end) as createdby_first,
       max(case when seqnum = 2 then createdby end) as createdby_second
from (select t.*,
             row_number() over (partition by email order by createdon) as seqnum
      from t
     ) t
group by email;

你可以join将这些信息回溯到原始数据,得到你想要的信息。我不明白 lag() 自然会如何解决这个问题。

我认为您可以简单地使用 max window 函数,因为您试图从每个分区的 rownumber = 2 中获取值。

SELECT  q1.email
          , q1.createdon
          , q1.createdby
          , q1.rn
          , max(case when rn=2 then q1.createdby end) over(partition by q1.email) first_dup_created_by
FROM    q1
ORDER BY q1.email, q1.rn

对于第一种情况,您也可以使用类似的查询来获取 rownumber=1 的结果。

/耸肩

; WITH duplicate_email_addresses AS (
  SELECT email
  FROM   t
  GROUP
      BY email
  HAVING Count(*) > 1
)
, records_with_duplicate_email_addresses AS (
  SELECT email
       , createdon
       , createdby
       , Row_Number() OVER (PARTITION BY email ORDER BY createdon) AS sequencer
  FROM   t
  WHERE  EXISTS (
           SELECT *
           FROM   duplicate_email_addresses
           WHERE  email = t.email
         )
)
, second_duplicate_record AS ( -- Why do you need any more than this?
  SELECT email
       , createdon
       , createdby
  FROM   records_with_duplicate_email_addresses
  WHERE  sequencer = 2
)
SELECT records_with_duplicate_email_addresses.email
     , records_with_duplicate_email_addresses.createdon
     , records_with_duplicate_email_addresses.createdby
     , second_duplicate_record.createdby AS first_duplicate_createdby
FROM   records_with_duplicate_email_addresses
 INNER
  JOIN second_duplicate_record
    ON second_duplicate_record.email = records_with_duplicate_email_addresses.email
;