【问题标题】:LAG within CASE giving false negative offset案例中的滞后给出假阴性偏移
【发布时间】:2017-03-30 17:06:40
【问题描述】:

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(已解决): 对于每一行,对于每封电子邮件,返回一个附加列,其中包含使用此电子邮件创建第一条记录的用户的名称。

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

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

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

现在,与上面类似,有 TASK 2:

任务 2: 对于每一行,对于每封电子邮件,返回创建第一个副本的用户的名称。 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

解释:对于每个分区中的第一条记录,从后面的记录(即从包含第一个重复的记录)返回[createdby]。对于同一分区中的所有其他记录,从 (rn-2) 记录之前返回 [createdby](即,对于 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 中),以便它总是从每个分区中 rn = 2 的记录中返回值,但是 - 这是重要的一点 - 不进行自联接操作(我知道我可以在单独的子查询中准备 rn = 2 的行,但这意味着对整个表进行额外扫描,并且还会运行不必要的自联接)。

【问题讨论】:

  • 编辑您的问题并包含您希望为样本数据获取的结果
  • 这听起来可能很愚蠢,但如果在q1 中你使用ROW_NUMBER()...+2 作为rn 会怎样?在您的 case 表达式中,您可以使用 CASE q1.rn WHEN 3 THEN ...... ELSE LAG(q1.createdby, q1.rn)

标签: sql tsql sql-server-2012 window-functions


【解决方案1】:

我认为您可以简单地使用 max 窗口函数,因为您尝试从 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 的结果。

【讨论】:

  • 当您过于关注特定语言功能(在本例中为:LAG / LEAD)以致忘记了简单的事情时,就会发生这种情况。这是最明显的答案,我现在很惭愧。谢谢。
【解决方案2】:

您可以使用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() 自然会被用来解决这个问题。

【讨论】:

    【解决方案3】:

    /耸耸肩

    ; 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
    ;
    

    【讨论】:

    • 这正是我试图避免的(自加入),但感谢您提供全面的 SQL 格式化/命名课程。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-10-13
    • 2014-10-07
    • 2012-10-27
    • 2018-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多