【问题标题】:Trying to merge rows into one row with certain conditions尝试在特定条件下将行合并为一行
【发布时间】:2012-05-01 12:12:07
【问题描述】:

给定 2 行或更多行选择合并,其中之一被标识为模板行。其他行应将其数据合并到模板具有的任何空值列中。

示例数据:

Id  Name     Address          City          State   Active  Email             Date
1   Acme1    NULL             NULL          NULL    NULL    blah@yada.com     3/1/2011
2   Acme1    1234 Abc Rd      Springfield   OR      0       blah@gmail.com    1/12/2012
3   Acme2    NULL             NULL          NULL    1       blah@yahoo.com    4/19/2012

假设用户选择了id为1的行作为模板行,ids为2和3的行将合并到第1行中,然后删除。行 Id 1 中的任何空值列都应填充(如果存在)最新的(请参阅日期列)非空值,并且行 Id 1 中已经存在的非空值将保持原样。对上述数据的查询结果应该是这样的:

Id  Name     Address          City          State   Active  Email             Date
1   Acme1    1234 Abc Road    Springfield   OR      1       blah@yada.com     3/1/2011

请注意,Active 值是 1,而不是 0,因为行 Id 3 具有最近的日期。

附注此外,如果不事先明确定义/知道所有列名是什么,有什么方法可以做到这一点?我正在使用的实际表有很多列,并且一直在添加新列。有没有办法查找表中的所有列名,然后使用该子查询或临时表来完成这项工作?

【问题讨论】:

  • 电子邮件是否也应该是 blah@yahoo.com,或者 Acme2 是否被认为超出了合并范围?
  • 如何识别数据组?我猜你想合并双数据。那么如何识别表中数千条或更多条记录中的一组数据呢?如果您知道,那么您可以编写一个存储过程来执行此操作。如有必要,我可以编写一个示例 SP 作为答案然后
  • 嗨,Russell Fox,不,电子邮件应该保留为 blah@yada.com,因为它已经存在于 Id 1 行中。应该只修改 Id 1 中的空值列。
  • 嗨 YvesR,用户通过数据网格选择数据组,用户通过复选框选择行。行 ID 通过参数传递到 SP:一个逗号分隔的 Id varchar,我使用迭代函数解析 Id,然后使用它们从各自的表中提取数据以返回您看到的数据更多。模板行的 Id 也作为参数传递给 SP,以便我知道哪一行是要合并其他行的基本行。
  • 附言。当用户在 UI 中选择行,然后单击“合并”按钮时,将显示一个包含所有选定行的弹出窗口,然后用户在其中单击其中一行上的单选按钮以指示他们希望合并的行其他人进入。

标签: sql sql-server tsql sql-server-2008-r2


【解决方案1】:

您可以通过首先按模板标志对行进行排序,然后按日期降序对行进行排序。模板行应该始终是最后一个。每一行都按该​​顺序分配一个编号。使用 max() 我们找到了被占用的单元格(按数字的降序排列)。然后我们从与这些最大值匹配的行中选择列。

; with rows as (
    select test.*,
  -- Template row must be last - how do you decide which one is template row?
  -- In this case template row is the one with id = 1
    row_number() over (order by case when id = 1 then 1 else 0 end,
                       date) rn
    from test
  -- Your list of rows to merge goes here
  -- where id in ( ... )
),
-- Finding first occupied row per column
positions as (
  select
    max (case when Name is not null then rn else 0 end) NamePosition,
    max (case when Address is not null then rn else 0 end) AddressPosition,
    max (case when City is not null then rn else 0 end) CityPosition,
    max (case when State is not null then rn else 0 end) StatePosition,
    max (case when Active is not null then rn else 0 end) ActivePosition,
    max (case when Email is not null then rn else 0 end) EmailPosition,
    max (case when Date is not null then rn else 0 end) DatePosition
  from rows
)
-- Finally join this columns in one row
select 
  (select Name from rows cross join Positions where rn = NamePosition) name,
  (select Address from rows cross join Positions where rn = AddressPosition) Address,
  (select City from rows cross join Positions where rn = CityPosition) City,
  (select State from rows cross join Positions where rn = StatePosition) State,
  (select Active from rows cross join Positions where rn = ActivePosition) Active,
  (select Email from rows cross join Positions where rn = EmailPosition) Email,
  (select Date from rows cross join Positions where rn = DatePosition) Date
from test
-- Any id will suffice, or even DISTINCT
where id = 1

You might check it at Sql Fiddle.

编辑:

最后一节中的交叉连接实际上可能是 rows.rn = xxxPosition 上的内部连接。它以这种方式工作,但更改为内部连接将是一个改进。

【讨论】:

    【解决方案2】:

    没那么复杂。

    一开始.. DECLARE @templateID INT = 1 ..所以你可以记住哪一行被视为模板..

    现在查找最新的NOT NULL 值(不包括模板行)。最简单的方法是对每一列使用TOP 1 子查询:

    SELECT
    (SELECT TOP 1 Name FROM DataTab WHERE Name IS NOT NULL AND NOT ID = @templateID ORDER BY Date DESC) AS LatestName,
    (SELECT TOP 1 Address FROM DataTab WHERE Address IS NOT NULL AND NOT ID = @templateID ORDER BY Date DESC) AS AddressName
    -- add more columns here
    

    将上面的内容包装到 CTE(通用表表达式)中,这样您就可以为 UDPATE.. 提供很好的输入。

    WITH Latest_CTE (CTE_LatestName, CTE_AddressName) -- add more columns here; I like CTE prefix to distinguish source columns from target columns..
    AS
    -- Define the CTE query.
    (
        SELECT
        (SELECT TOP 1 Name FROM DataTab WHERE Name IS NOT NULL AND NOT ID = @templateID ORDER BY Date DESC) AS LatestName,
        (SELECT TOP 1 Address FROM DataTab WHERE Address IS NOT NULL AND NOT ID = @templateID ORDER BY Date DESC) AS AddressName
        -- add more columns here
    )
    UPDATE
    <update statement here (below)>
    

    现在,使用ISNULL 对您的模板行进行智能UPDATE - 它将充当条件更新 - 仅在目标列为空时更新

    WITH
    <common expression statement here (above)>
    UPDATE DataTab
    SET 
    Name = ISNULL(Name, CTE_LatestName), -- if Name is null then set Name to CTE_LatestName else keep Name as Name
    Address = ISNULL(Address, CTE_LatestAddress)
    -- add more columns here..
    WHERE ID = @templateID
    

    最后一个任务是删除模板行以外的行..

    DELETE FROM DataTab WHERE NOT ID = @templateID
    

    清除?

    【讨论】:

    • 我认为这是有道理的,我会试一试,但是如果不事先明确定义/知道所有列名是什么,有没有办法做到这一点?我正在使用的实际表有很多列,并且一直在添加新列。有没有办法查找表中的所有列名,然后使用该子查询或临时表来完成这项工作?
    • 使用 sys.columns 目录视图从您的表中获取所有列名称(从该集合中排除 ID 列)。现在 1) 使用动态 SQL - 我认为这很头疼,性能下降。或者 2) 将列名从 sys.columns 输出到文本文件中,然后使用 AWK/GAWK 程序使用“模板”生成目标脚本。保存您的 GAWK 程序以供将来使用(重建/刷新您的 SQL 脚本)。
    • 在另一个 SQL 问题上查看我的 GAWK 解决方案以了解这个想法:stackoverflow.com/a/10122169/1280816 我确定您有很多列,但最终产品中的列数将是静态的。最好使用外部 SQL 脚本生成器,而不是在每次查询执行时降低性能并循环遍历列名。
    【解决方案3】:

    对于动态列,您需要使用动态 SQL 编写解决方案。

    您可以查询 sys.columns 和 sys.tables 以获取您需要的列列表,然后您希望为每个空列向后循环一次,找到该列的第一个非空行并为此更新您的输出行柱子。一旦你在循环中达到 0,你就有一个完整的行,然后你可以向用户显示。

    【讨论】:

    • 感谢您的回答;这听起来正是我需要做的,但我很难在没有看到一些代码的情况下进行具体的概念化。如果不是很麻烦,你能在这里写一些代码让我更好地理解吗?非常感谢。
    【解决方案4】:

    我应该注意发布日期。无论如何,这是一个使用动态 SQL 构建更新语句的解决方案。无论如何,它应该为您提供一些构建基础。

    其中有一些额外的代码来验证结果,但我试图以一种使非重要代码明显的方式进行评论。

    CREATE TABLE 
    dbo.Dummy 
        (
        [ID] int ,
        [Name] varchar(30),
        [Address] varchar(40) null,
        [City]  varchar(30) NULL,
        [State] varchar(2) NULL,
        [Active] tinyint NULL,
        [Email] varchar(30) NULL,
        [Date] date NULL
        );
    --
    INSERT dbo.Dummy
    VALUES
    (
        1, 'Acme1', NULL, NULL, NULL, NULL, 'blah@yada.com', '3/1/2011'
    )
    ,
    (
        2, 'Acme1', '1234 Abc Rd', 'Springfield', 'OR', 0, 'blah@gmail.com', '1/12/2012'
    )
    ,
    (
        3, 'Acme2', NULL, NULL, NULL, 1, 'blah@yahoo.com', '4/19/2012'
    );
    DECLARE 
        @TableName nvarchar(128) = 'Dummy',
        @TemplateID int = 1,
        @SetStmtList nvarchar(max) = '',
        @LoopCounter int = 0,
        @ColumnCount int = 0,
        @SQL nvarchar(max) = ''
        ;
    --
    --Create a table to hold the column names
    DECLARE     
        @ColumnList table 
            (
            ColumnID tinyint IDENTITY,
            ColumnName nvarchar(128)
            );
    --
    --Get the column names
    INSERT @ColumnList
    (
        ColumnName
    )
        SELECT
            c.name
        FROM
            sys.columns AS c
            JOIN
            sys.tables AS t
                ON
                    t.object_id = c.object_id
        WHERE
            t.name = @TableName;
    --
    --Create loop boundaries to build out the SQL statement
    SELECT
        @ColumnCount = MAX( l.ColumnID ),
        @LoopCounter = MIN (l.ColumnID )
    FROM
        @ColumnList AS l;
    --
    --Loop over the column names
    WHILE @LoopCounter <= @ColumnCount
    BEGIN
        --Dynamically construct SET statements for each column except ID (See the WHERE clause)
        SELECT 
            @SetStmtList = @SetStmtList + ',' + l.ColumnName + ' =COALESCE(' + l.ColumnName + ', (SELECT TOP 1 ' + l.ColumnName + ' FROM ' + @TableName + ' WHERE ' + l.ColumnName + ' IS NOT NULL AND ID <> ' + CAST(@TemplateID AS NVARCHAR(MAX )) + ' ORDER BY Date DESC)) '
        FROM 
            @ColumnList AS l
        WHERE 
            l.ColumnID = @LoopCounter
            AND
            l.ColumnName <> 'ID';
    --
        SELECT
            @LoopCounter = @LoopCounter + 1;
    --
    END;
    
    --TESTING - Validate the initial table values
    SELECT * FROM dbo.Dummy ;
    --
    --Get rid of the leading common in the SetStmtList
    SET @SetStmtList = SUBSTRING( @SetStmtList, 2, LEN( @SetStmtList ) - 1 );
    --Build out the rest of the UPDATE statement
    SET @SQL = 'UPDATE ' + @TableName  + ' SET ' + @SetStmtList + ' WHERE ID = ' + CAST(@TemplateID AS NVARCHAR(MAX ))
    --Then execute the update
    EXEC sys.sp_executesql
        @SQL;
    --
    --TESTING - Validate the updated table values
    SELECT * FROM dbo.Dummy ;
    --
    --Build out the DELETE statement
    SET @SQL = 'DELETE FROM ' + @TableName + ' WHERE ID <> ' + CAST(@TemplateID AS NVARCHAR(MAX ))
    --Execute the DELETE
    EXEC sys.sp_executesql
        @SQL;
    --
    --TESTING - Validate the final table values
    SELECT * FROM dbo.Dummy; 
    --
    DROP TABLE dbo.Dummy;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-11
      • 1970-01-01
      • 2021-11-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多