TSQL - 父子(1 到零/多个)分组/聚合

Posted

技术标签:

【中文标题】TSQL - 父子(1 到零/多个)分组/聚合【英文标题】:TSQL - Parent Child (1 to zero/many) Grouping/Aggregation 【发布时间】:2020-01-24 19:48:19 【问题描述】:

代码(示例数据暂存):

DECLARE @Emp TABLE
    (
        [EId] INT IDENTITY(1, 1)
      , [FN]  NVARCHAR(50)
      , [LN]  NVARCHAR(50)
    ) ;
DECLARE @EmpPhCont TABLE
    (
        [EId]       INT
      , [PhType]    VARCHAR(10)
      , [PhNum]     VARCHAR(16)
      , [PhExt]     VARCHAR(10)
      , [IsMain]    BIT
      , [CreatedOn] DATETIME
    ) ;

INSERT INTO @Emp
VALUES
    ( N'Emp1', N'Emp1' )
  , ( N'Emp2', N'Emp2' )
  , ( N'Emp3', N'Emp3' )
  , ( N'Emp4', N'Emp4' )
  , ( N'Emp5', N'Emp5' )
  , ( N'Emp6', N'Emp5' ) ;

INSERT INTO @EmpPhCont
VALUES
    ( 1, 'Home', '111111111', NULL, 0, '2020-01-01 00:00:01' )
  , ( 1, 'Mobile', '222222222', NULL, 1, '2020-01-01 00:00:02' )
  , ( 1, 'Work', '333333333', NULL, 0, '2020-01-01 00:00:03' )

  , ( 2, 'Work', '444444444', '567', 1, '2020-01-01 00:00:04' )
  , ( 2, 'Mobile', '555555555', NULL, 0, '2020-01-01 00:00:05' )
  , ( 2, 'Mobile', '454545454', NULL, 0, '2020-01-01 00:00:06' )

  , ( 3, 'Home', '777777777', NULL, 0, '2020-01-01 00:00:07' )
  , ( 3, 'Mobile', '888888888', NULL, 1, '2020-01-01 00:00:08' )
  , ( 3, 'Mobile', '12121212', NULL, 0, '2020-01-01 00:00:09' )

  , ( 4, 'Work', '101010101', '111', 1, '2020-01-01 00:00:10' )
  , ( 4, 'Work', '101010102', '232', 0, '2020-01-01 00:00:11' )

  , ( 5, 'Work', '545454545', '456', 0, '2020-01-01 00:00:10' )
  , ( 5, 'Work', '456456456', NULL, 1, '2020-01-01 00:00:11' )  ;

说明:

@Emp 是示例员工表(唯一员工记录)。

EId = 员工 ID FN = 名字 LN = 姓氏

@EmpPhCont 是示例 Employee Phone Contact 表(@Emp 表中的每个 Emp 在此处可以有零个、一个或多个电话号码 - 根据 Emp/Type 是唯一的)。

PhType = 电话类型(家庭、手机、工作等) PhNum = 电话号码 PhExt = 电话分机(主要适用于“工作”PhType) IsMain = 是否为主要联系人号码。每个拥有电话号码的员工都将有 1 条记录标记为 IsMain。 CreatedOn = 创建记录的日期

目标:

使用以下列为每位员工输出 1 条记录

EID |主页号码 |手机号码 |工作编号 | WorkNumExt | MainPhType

规则:

返回来自@Emp 的所有记录的所有EId,无论它们是否有@EmpPhCont 记录。

对于每个拥有@EmpPhCont 记录的emp,返回对应PhType 的最新创建的PhNum 和PhExt,除非同一Emp/PhType 的旧记录被标记为IsMain = 1(对于任何 emp,对于任何 PhType,如果 IsMain = 1,则始终返回 PhNum 和 PhExt 值)。

预期输出:

EId HomeNum     MobileNum   WorkNum     WorkNumExt  MainPhType
1   111111111   222222222   333333333   NULL        Mobile
2   NULL        454545454   444444444   567         Work
3   777777777   888888888   NULL        NULL        Mobile
4   NULL        NULL        101010102   111         Work
5   NULL        NULL        456456456   NULL        Work
6   NULL        NULL        NULL        NULL        NULL

我的失败尝试:

SELECT      [EM].[EId]
          , MAX ( IIF([PH].[PhType] = 'Home', [PH].[PhNum], NULL)) AS [HomePhNum]
          , MAX ( IIF([PH].[PhType] = 'Mobile', [PH].[PhNum], NULL)) AS [MobilePhNum]
          , MAX ( IIF([PH].[PhType] = 'Work', [PH].[PhNum], NULL)) AS [WorkPhNum]
FROM        @Emp AS [EM]
LEFT JOIN   @EmpPhCont AS [PH]
ON          [EM].[EId] = [PH].[EId]
GROUP BY    [EM].[EId] ;

【问题讨论】:

EId 3 为什么只能得到一个手机号码?为什么你得到你所做的那个的逻辑是什么?我认为您需要先查看 ROW_NUMBER,以便您可以获得每个 EId 和 PhoneType 的正确行...提示(PARTITION BY) @SeanLange Question 已经说明了这个规则:"返回 newest 为相应的 PhType 创建的 PhNum 和 PhExt,UNLESS相同 Emp/PhType 的旧记录被标记为 IsMain = 1" @Andreas 哎呀错过了。 :P 【参考方案1】:

CTE 中使用ROW_NUMBER() 窗口函数从@EmpPhCont 中获取您想要返回的行并将此CTE 加入到@Emp

with cte as (
  select *,
    row_number() over (partition by [EId], [PhType] order by [IsMain] desc, [CreatedOn] desc) rn
  from @EmpPhCont
)
select e.[EId],
  max(case when c.[PhType] = 'Home' then c.[PhNum] end) HomeNum,
  max(case when c.[PhType] = 'Mobile' then c.[PhNum] end) MobileNum,
  max(case when c.[PhType] = 'Work' then c.[PhNum] end) WorkNum,
  max(case when c.[PhType] = 'Work' then c.[PhExt] end) WorkNumExt,
  max(case when c.[IsMain] = 1 then c.[PhType] end) MainPhType 
from @Emp e left join cte c
on c.[EId] = e.[EId] and c.rn = 1
group by e.[EId]

请参阅demo。 结果:

> EId | HomeNum   | MobileNum | WorkNum   | WorkNumExt | MainPhType
> --: | :-------- | :-------- | :-------- | :--------- | :---------
>   1 | 111111111 | 222222222 | 333333333 | null       | Mobile    
>   2 | null      | 454545454 | 444444444 | 567        | Work      
>   3 | 777777777 | 888888888 | null      | null       | Mobile    
>   4 | null      | null      | 101010101 | 111        | Work      
>   5 | null      | null      | 456456456 | null       | Work      
>   6 | null      | null      | null      | null       | null 

【讨论】:

需要将c.[PhExt] 限制为c.[PhType] = 'Work'。 --- 不过,+1 以获得简洁的解决方案。 我认为大部分可用于“工作”PhType 会涵盖它,但我会编辑。 “大部分”“仅”? 即将发布一个几乎相同的解决方案。条件聚合再次获胜。 感谢@forpas 提供这个简洁的解决方案。 R_Num 窗口函数的有趣方法。干杯!【参考方案2】:

我会使用APPLY

SELECT EId, HomeNum, MobileNum, WorkNum, WorkNumExt
     , COALESCE(HomeMain, MobileMain, WorkMain) AS MainPhType
  FROM Emp e
 OUTER APPLY (
          SELECT TOP 1 c.[PhNum] AS HomeNum
               , CASE WHEN c.[IsMain] = 1 THEN 'Home' END AS HomeMain
            FROM EmpPhCont c
           WHERE c.[EId] = e.[EId]
             AND c.[PhType] = 'Home'
           ORDER BY c.[IsMain] DESC, c.[CreatedOn] DESC
       ) home
 OUTER APPLY (
          SELECT TOP 1 c.[PhNum] AS MobileNum
               , CASE WHEN c.[IsMain] = 1 THEN 'Mobile' END AS MobileMain
            FROM EmpPhCont c
           WHERE c.[EId] = e.[EId]
             AND c.[PhType] = 'Mobile'
           ORDER BY c.[IsMain] DESC, c.[CreatedOn] DESC
       ) mobile
 OUTER APPLY (
          SELECT TOP 1 c.[PhNum] AS WorkNum
               , c.[PhExt] AS WorkNumExt
               , CASE WHEN c.[IsMain] = 1 THEN 'Work' END AS WorkMain
            FROM EmpPhCont c
           WHERE c.[EId] = e.[EId]
             AND c.[PhType] = 'Work'
           ORDER BY c.[IsMain] DESC, c.[CreatedOn] DESC
       ) work

请参阅SQL Fiddle 进行演示。

输出

EId | HomeNum   | MobileNum | WorkNum   | WorkNumExt | MainPhType
1   | 111111111 | 222222222 | 333333333 | (null)     | Mobile
2   | (null)    | 454545454 | 444444444 | 567        | Work
3   | 777777777 | 888888888 | (null)    | (null)     | Mobile
4   | (null)    | (null)    | 101010101 | 111        | Work
5   | (null)    | (null)    | 456456456 | (null)     | Work
6   | (null)    | (null)    | (null)    | (null)     | (null)

注意:如果EmpPhCont 表在[EId], [PhType] 上有索引,则此解决方案仅适用于大型数据集,否则会太慢。

【讨论】:

即使有索引,这也会比条件聚合慢,因为它必须多次访问同一个表。 @SeanLange True,对于员工表的完整转储,就像这里的情况一样,但如果员工被过滤,这个解决方案可能会更快。这是一个不错的选择,采用更有针对性的方法。 同意。我当然没有说这不是一个好的选择。找到多种方法来解决同一个问题总是好的。 非常感谢@Andreas 提供的解决方案。干杯!【参考方案3】:

row_number(),外应用和聚合:

select *
from @Emp as e
outer apply
(
    select 
          MAX ( case when d.[PhType] = 'Home' then d.[PhNum] end) AS [HomePhNum]
        , MAX ( case when d.[PhType] = 'Mobile' then d.[PhNum] end) AS [MobilePhNum]
        , MAX ( case when d.[PhType] = 'Work' then d.[PhNum] end) AS [WorkPhNum]
        , MAX ( case when d.[PhType] = 'Work' then d.[PhExt] end) AS [WorkNumExt]
        , MAX ( case when IsMain = 1 then d.[PhType] end) AS MainPhType --work is max if both mob&work as set as main..
    from
    (
    select *, row_number() over(partition by PhType order by IsMain DESC, CreatedOn DESC) as rownum
    from @EmpPhCont as p
    where p.EId = e.EId
    ) as d
    where d.rownum = 1  
) as ph;

【讨论】:

感谢@lptr 提供了解决方案的无 CTE 变体。干杯!

以上是关于TSQL - 父子(1 到零/多个)分组/聚合的主要内容,如果未能解决你的问题,请参考以下文章

用于分组的 tsql 聚合字符串

TSQL - 将 Sales_Rep 行分组到列并将 DESCRIP 列显示为行 无聚合

在使用条件聚合进行分组时选择多个第 n 个值 - 熊猫

如何分组并将操作聚合到多个列?

pandas使用groupby函数按照多个分组变量进行分组聚合统计使用agg函数计算分组的多个统计指标(grouping by multiple columns in dataframe)

Python通过多个键单向分组和聚合字典列表