尝试在特定条件下将行合并为一行
Posted
技术标签:
【中文标题】尝试在特定条件下将行合并为一行【英文标题】: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 中选择行,然后单击“合并”按钮时,将显示一个包含所有选定行的弹出窗口,然后用户在其中单击其中一行上的单选按钮以指示他们希望合并的行其他人进入。 【参考方案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 解决方案以了解这个想法:***.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;
【讨论】:
以上是关于尝试在特定条件下将行合并为一行的主要内容,如果未能解决你的问题,请参考以下文章