【问题标题】:Split semicolon-delimited column into multiple columns将分号分隔的列拆分为多列
【发布时间】:2015-10-13 20:49:51
【问题描述】:

我正在尝试将 SQL 中的一列拆分为多个列。

我的数据如下所示:

Column1 | Column2 | Column3  
ABC     | 123     | User7;User9  
nbm     | qre     | User1;User2;User3  
POI     | kjh     | User1;User4;User5;User9  

我需要将 Column3 拆分为 4 个新列 - 每列包含第一个“用户”。此列中的每个值都由分号分隔。我遇到的问题之一是 Column3 可以列出任意数量的用户(全部用分号分隔),所以我不知道需要添加多少“新”列。

最终输出需要如下所示:

Column1 | Column2 | Column3 | NewColumn1 | NewColumn2 | ETC.

【问题讨论】:

  • 您需要同一个表中的用户有什么原因吗?为什么不将它们分开到另一个表中,然后创建一个链接表来加入它们呢?这样您就不必担心会有多少用户......
  • 您是真的想修改包含数据的表结构,还是只是为了显示目的?
  • 您遇到问题的原因是您的数据未正确规范化。您首先必须将该分隔列表拆分为可用的内容。然后,您将需要一个动态交叉表或数据透视表来生成您需要的动态列。
  • 分隔字符串是一个很大的不。你应该修复你的数据结构。

标签: sql sql-server sql-server-2008


【解决方案1】:

除了这是一个糟糕的设计之外,这里还有一个解决方案:

只需将其粘贴到一个空的查询窗口中并执行。适应您的需求...

declare @tbl TABLE(Column1 VARCHAR(15),Column2 VARCHAR(15),Column3 VARCHAR(150));
INSERT INTO @tbl VALUES
 ('ABC','123','User7;User9')  
,('nbm','qre','User1;User2;User3')  
,('POI','kjh','User1;User4;User5;User9');

WITH Splitted AS
(
    SELECT Column1,Column2,CAST('<x>'+REPLACE(Column3,';','</x><x>')+'</x>' AS XML) AS Col3Splitted
    FROM @tbl
) 
SELECT Column1,Column2,Col3Splitted
                      ,Col3Splitted.value('x[1]','varchar(max)') AS Column4
                      ,Col3Splitted.value('x[2]','varchar(max)') AS Column5
                      ,Col3Splitted.value('x[3]','varchar(max)') AS Column6
                      ,Col3Splitted.value('x[4]','varchar(max)') AS Column7
                      /*Add as many as you need*/
FROM Splitted 

在与@SeanLang 讨论之后,我添加了这种动态方法。它将计算 Column3 中分号的最高数量并动态构建上面的语句。

CREATE TABLE #tbl (Column1 VARCHAR(15),Column2 VARCHAR(15),Column3 VARCHAR(150));
INSERT INTO #tbl VALUES
 ('ABC','123','User7;User9')  
,('nbm','qre','User1;User2;User3')  
,('POI','kjh','User1;User4;User5;User9');

DECLARE @sql VARCHAR(MAX)=
'WITH Splitted AS
(
    SELECT Column1,Column2,CAST(''<x>''+REPLACE(Column3,'';'',''</x><x>'')+''</x>'' AS XML) AS Col3Splitted
    FROM #tbl
) 
SELECT Column1,Column2';
DECLARE @counter INT = 0;
WHILE @counter<=(SELECT MAX(LEN(Column3) - LEN(REPLACE(Column3, ';', ''))) from #tbl)
BEGIN
    SET @counter=@counter+1;
    SET @sql=@sql+',Col3Splitted.value(''x[' +  CAST(@counter AS VARCHAR(10)) + ']'',''varchar(max)'') AS Column' + CAST(@counter+3 AS VARCHAR(10));
END
SET @sql=@sql+ ' FROM Splitted;';

EXEC (@sql);  

DROP TABLE #tbl;

【讨论】:

  • 这可行,但不支持未知数量的列要求。为此,它需要是动态的。
  • @SeanLange,是和否...您可以添加预期的最大列数,如果为空,它们将返回 NULL。设计已经够糟糕了。这将 - 至少 - 以定义结构返回;-)
  • 同意该设计比 1000 年前的鸡蛋更臭,但 OP 确实表示他们不知道将返回多少列。在这方面,您的方法不是动态的。
  • @SeanLange,在我看来,动态方法会使情况变得更糟。有了明确的列数,您至少可以坚持一项安全的事情......
  • 很多时候我们无法改变结构,不得不让它与呈现给我们的糟糕的东西一起工作。有时这需要基于数据的动态列数。
【解决方案2】:

这是一种 100% 动态的方法。它将仅根据它找到的数据生成任意数量的列。围绕 SO 的流行方法是动态支点。我更喜欢动态交叉表,因为我发现语法不那么迟钝,而且从性能的角度来看它也有一点好处。 :)

这是一篇很好地解释了这种方法的文章。 http://www.sqlservercentral.com/articles/Crosstab/65048/

另外,我正在使用最初由 Jeff Moden 编写的 DelimitedSplit8K 函数,并随着时间的推移通过社区进行了改进。您可以阅读它并在此处找到它的代码。 http://www.sqlservercentral.com/articles/Tally+Table/72993/

if OBJECT_ID('tempdb..#Something') is not null
    drop table #Something;

create table #something
(
    Column1 varchar(5)
    , Column2 varchar(5)
    , Column3 varchar(50)
);

insert #something
select 'ABC', '123', 'User7;User9' union all
select 'nbm', 'qre', 'User1;User2;User3' union all
select 'POI', 'kjh', 'User1;User4;User5;User9';

declare @StaticPortion nvarchar(2000) = 'with orderedResults as
(
    select s.Column1
        , s.Column2
        , x.Item
        , x.ItemNumber
    from #something s
    cross apply dbo.DelimitedSplit8K(Column3, '';'') x
)

select Column1
    , Column2
';

declare @DynamicPortion nvarchar(max) = '';

    WITH
    E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
    E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
    E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
    cteTally(N) AS 
    (
        SELECT  ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
    )

select @DynamicPortion = @DynamicPortion + ', MAX(Case when ItemNumber = ' + CAST(N as varchar(6)) + ' then Item end) as Subject' + CAST(N as varchar(6)) + CHAR(10)
from cteTally t
where t.N <=
(
    select top 1 COUNT(*)
    from #something s
    cross apply dbo.DelimitedSplit8K(Column3, ';') x
    group by Column1
    Order by COUNT(*) desc
);

declare @FinalStaticPortion nvarchar(2000) = ' from orderedResults group by Column1, Column2 order by Column1, Column2';

declare @SqlToExecute nvarchar(max) = @StaticPortion + @DynamicPortion + @FinalStaticPortion;

select @SqlToExecute;
--uncomment the following line when you are sure this is what you want to execute.
--exec sp_executesql @SqlToExecute;

【讨论】:

    【解决方案3】:

    对于 MySQl,可以使用下面的代码。这只是一个示例代码

    @num_Column3 := 1 + LENGTH(@Column3) - LENGTH(REPLACE(@Column3, '|', '')) AS num_Column3
    ,IF(@num_Column3 > 0, SUBSTRING_INDEX(SUBSTRING_INDEX(@Column3)), '|', 1), '|', -1), '') AS RC_user
    

    【讨论】:

      猜你喜欢
      • 2018-11-02
      • 2019-11-10
      • 1970-01-01
      • 1970-01-01
      • 2018-03-17
      • 2022-12-10
      • 2018-11-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多