【问题标题】:How can I shift column values forward if the previous value is in a list of excluded values?如果前一个值在排除值列表中,如何将列值向前移动?
【发布时间】:2014-06-04 14:22:25
【问题描述】:

我有一些 TSQL 代码可以从组织良好的关系表中生成非规范化的平面文件。代码完成得很快,而且数据不是压倒性的,所以任何建议都有可能有所帮助。我不必太担心性能,因为此过程仅打算每月运行 1 次。在这方面我有一些回旋的余地。

例如,源数据的布局如下:一个人(表 1)可以有许多事件(表 2)。每个事件可以有许多与之相关的代码(表 3)。每个代码都有一个有序的序列。因此,在将其展平后,提取文件中的一行可能如下所示:

Name                IncidentId  Code1   Code2   Code3   Code4
Sue Ellen Crandell  1991        abc1    def1    xyz0    888

这些非规范化的有序代码列可能超过 50。问题是,有一个新要求,即如果有序代码列之一具有排除列表中的值,则以下有序代码列值应向前移动一位。这意味着如果def1 在排除列表中,则该行应如下所示:

Name                IncidentId  Code1   Code2   Code3   Code4
Sue Ellen Crandell  1991        abc1    xyz0    888     <empty string>

在获取其他关系数据并将结果导出到文件之前,我使用动态 T-SQL 将这些有序代码值反规范化到临时表中。由于不想弄乱动态 T-SQL,以及在该部分过程中能够使用条件来移动列的可能限制,我认为最容易放置排除列表评估的位置是排序后的代码值进入临时表。

如果我有一个看起来像上面第一个数据行的临时表,我该怎么做

  1. 检查每一列的值。
  2. 如果值包含在排除列表中,则删除它
  3. 遇到排除列表中的值时有条件地向前移动值,如上面的行示例所示?

排除列表只是一些静态值,我可以将它们转储到临时表中或与IN 运算符一起使用。我猜可能需要 CTE,但递归逻辑对我来说并不清楚。

【问题讨论】:

  • 粗略...对其进行规范化、过滤、再次反规范化,使用PIVOTUNPIVOT

标签: sql sql-server tsql sql-server-2005


【解决方案1】:

首先创建一个取消透视表的 CTE,以便每个代码位于单独的行上:

with cte(Name, IncidentId, CodeName, Code)
as(
    select Name, IncidentId, CodeName, Code
    from Incident i 
    unpivot(Code for CodeName in (Code1, Code2, Code3, Code4)) unpvt
)

现在您在 CTE 上对其自身进行外部联接,过滤掉排除的代码。这为每个 Name-Incident-Code 元组提供了一行,但在排除代码的行中有空值(您需要空行来维护正确的代码计数)。

Select *, t1.Name, t1.IncidentId, isnull(t2.Code, '') Code, 
ROW_NUMBER() over(partition by t1.Name, t1.IncidentId order by isnull(t2.CodeName, 'zzz')) CodeNumber
    from cte t1
    left outer join cte t2 on t1.Name = t2.Name and 
           t1.IncidentId = t2.IncidentId and 
           t1.Code = t2.Code and 
           not exists(select 1 from Exclude e where e.Code = t2.Code)

此处的 ROW_NUMBER() 将创建新的 CodeNumber。 order byisnull(t2.CodeNumber, 'zzz')) 将空行推到末尾,以便首先对具有有效代码的行进行编号(因为“zzz”大于“Code-whatever-”)。

现在您只需将先前的查询转回,以便代码再次成为列:

select Name, IncidentId, [1] Code1, [2] Code2, [3] as Code3, [4] as Code4
from
(
    Select t1.Name, t1.IncidentId, isnull(t2.Code, '') Code, ROW_NUMBER() over(partition by t1.Name, t1.IncidentId order by isnull(t2.CodeName, 'zzz')) CodeNumber
    from cte t1
    left outer join cte t2 on t1.Name = t2.Name and t1.IncidentId = t2.IncidentId and t1.Code = t2.Code and not exists(select 1 from Exclude e where e.Code = t2.Code)
) x
pivot(max(Code) for CodeNumber in ([1], [2], [3], [4])
) as pvt

SQL Fiddle

更新

上面的代码有几个问题。首先,当我使用 ROW_NUMBER() 创建 CodeNumber 时,我按 CodeName 排序。这在 9 个代码列之后会发生故障,因为它们不再正确排序(它们按字母顺序而不是数字顺序排序)。所以我需要在 CTE 中提取代码编号,以便稍后使用它进行排序:

with cte(Name, IncidentId, CodeName, CodeNumber, Code)
as(
    select Name, IncidentId, CodeName, convert(int, SUBSTRING(CodeName, 5, len(CodeName))), Code
    from Incident i 
    unpivot(Code for CodeName in (Code1, Code2, Code3, Code4, Code5, Code6, Code7, Code8, Code9, Code10)) unpvt
)

现在查询的其余部分如下所示:

select Name, IncidentId, [1] Code1, [2] Code2, [3] as Code3, [4] as Code4, [5] as Code5, [6] as Code6, [7] as Code7, [8] as Code8, [9] as Code9, [10] as Code10
from
(
    Select t1.Name, t1.IncidentId, isnull(t2.Code, '') Code, ROW_NUMBER() over(partition by t1.Name, t1.IncidentId order by isnull(t2.CodeNumber, 999)) NewCodeNumber
    from cte t1
    left outer join cte t2 on t1.Name = t2.Name and t1.IncidentId = t2.IncidentId and t1.Code = t2.Code and not exists(select 1 from Exclude e where e.Code = t2.Code)
) x
pivot(max(Code) for NewCodeNumber in ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10])
) as pvt

请注意,由于我现在在 CTE 中有一个名为 CodeNumber 的列,因此我将新生成的数字称为“NewCodeNumber”。另外,我通过t2.CodeNumber 订购,而不是t1.Code

更新SQL Fiddle

更新

关于您评论中的问题,您实质上是在询问是否取消旋转多个列,这不像取消旋转单个列那么简单。实现它的一种方法是分别取消透视代码和编码日期:

with cteCode(Name, IncidentId, CodeName, CodeNumber, Code)
as(
    select Name, IncidentId, CodeName, convert(int, SUBSTRING(CodeName, 5, len(CodeName))), Code
    from Incident i 
    unpivot(Code for CodeName in (Code1, Code2, Code3, Code4, Code5, Code6, Code7, Code8, Code9, Code10)) unpvt
), cteCodeDate(Name, IncidentId, CodeName, CodeNumber, CodeDate)
as(
    select Name, IncidentId, CodeName, convert(int, SUBSTRING(CodeName, 9, len(CodeName))), CodeDate
    from Incident i 
    unpivot(CodeDate for CodeName in (CodeDate1, CodeDate2, CodeDate3, CodeDate4, CodeDate5, CodeDate6, CodeDate7, CodeDate8, CodeDate9, CodeDate10)) unpvt
)

然后将它们重新组合在一起:

Select t1.Name, t1.IncidentId, isnull(t2.Code, '') Code, ROW_NUMBER() over(partition by t1.Name, t1.IncidentId order by isnull(t2.CodeNumber, 999)) NewCodeNumber, t3.CodeDate
    from cteCode t1
    join cteCodeDate t3 on t3.Name = t1.Name and t3.IncidentId = t1.IncidentId and t3.CodeNumber = t1.CodeNumber
    left outer join cteCode t2 on t1.Name = t2.Name and t1.IncidentId = t2.IncidentId and t1.Code = t2.Code and not exists(select 1 from Exclude e where e.Code = t2.Code)

旋转多列也不像单列那么容易,所以我走了一条不同的路线来获得最终结果:

select Name, IncidentId,
MAX(case when newCodeNumber = 1 then Code end) Code1,
MAX(case when newCodeNumber = 1 then CodeDate end) CodeDate1,
MAX(case when newCodeNumber = 2 then Code end) Code2,
MAX(case when newCodeNumber = 2 then CodeDate end) CodeDate2,
MAX(case when newCodeNumber = 3 then Code end) Code3,
MAX(case when newCodeNumber = 3 then CodeDate end) CodeDate3,
MAX(case when newCodeNumber = 4 then Code end) Code4,
MAX(case when newCodeNumber = 4 then CodeDate end) CodeDate4,
MAX(case when newCodeNumber = 5 then Code end) Code5,
MAX(case when newCodeNumber = 5 then CodeDate end) CodeDate5,
MAX(case when newCodeNumber = 6 then Code end) Code6,
MAX(case when newCodeNumber = 6 then CodeDate end) CodeDate6,
MAX(case when newCodeNumber = 7 then Code end) Code7,
MAX(case when newCodeNumber = 7 then CodeDate end) CodeDate7,
MAX(case when newCodeNumber = 8 then Code end) Code8,
MAX(case when newCodeNumber = 8 then CodeDate end) CodeDate8,
MAX(case when newCodeNumber = 9 then Code end) Code9,
MAX(case when newCodeNumber = 9 then CodeDate end) CodeDate9,
MAX(case when newCodeNumber = 10 then Code end) Code10,
MAX(case when newCodeNumber = 10 then CodeDate end) CodeDate10
from
(
    Select t1.Name, t1.IncidentId, isnull(t2.Code, '') Code, ROW_NUMBER() over(partition by t1.Name, t1.IncidentId order by isnull(t2.CodeNumber, 999)) NewCodeNumber, t3.CodeDate
    from cteCode t1
    join cteCodeDate t3 on t3.Name = t1.Name and t3.IncidentId = t1.IncidentId and t3.CodeNumber = t1.CodeNumber
    left outer join cteCode t2 on t1.Name = t2.Name and t1.IncidentId = t2.IncidentId and t1.Code = t2.Code and not exists(select 1 from Exclude e where e.Code = t2.Code)
) x
group by Name, IncidentId

SQL Fiddle

【讨论】:

  • 我在 SQL Fiddle 结果中发现了一个问题。原始列的顺序没有保留。删除排除值后,第一行中代码列的顺序应为:abc1、xyz0、888,其中第一列值将保持其位置,而排除值后面的列将向前移动一个位置。
  • 糟糕。 ROW_NUMBER() 中的 order by 应该是 order by isnull(t2.CodeName, 'zzz') 而不是 order by isnull(t2.Code, 'zzz')。我已经更新了答案和小提琴链接:sqlfiddle.com/#!3/a198d/4/0
  • 演示看起来不错。我打算在本周末之前尝试一下。
  • 看看这个。我可能需要 50 个占位符,但是当我测试到只有 10 个时,列值位置在结果中看起来不正确。无论出于何种原因,某些值会跳转到位置 7、8 等。sqlfiddle.com/#!3/5a852/1
  • 如果我对每个 CodeX 列(如 CodeDateX)都有相应的列,您有什么建议吗? x 的值在两者之间是相同的,我仍然只评估 CodeX 的值。但是,如果需要转变,我必须同时推动 CodeX 和 CodeDateX。
【解决方案2】:

评论太长了。

最好使用动态 SQL 来处理。将事情从一列移到另一列以处理排除项充其量是很麻烦的。它最终会成为以下方面的一些变体:

if code1 is not excluded then code1
   else if code2 is not excluded then code2
   else if code3 is not excluded then code3
   else code4 is not excluded then code4 as code1
if code1 is not excluded 
   if code2 is not excluded then code2
   else if code3 is not excluded then code3
   else if code4 is not excluded then code4
and so on, and so on and so on

相反,您可能有一个地方可以在动态 SQL 中添加类似这样的内容:

where not exists (select 1 from ExcludedCodes ec where ec.code <> the.code)

而且你会在支点之前消除它们。

【讨论】:

    猜你喜欢
    • 2021-12-21
    • 2017-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-05
    • 2023-03-19
    • 2020-03-25
    相关资源
    最近更新 更多