如何将包含矩阵的字符串拆分到SQL Server中的表中? String具有列和行分隔符。
假设我有一个字符串:
declare @str varchar(max)='A,B,C;D,E,F;X,Y,Z';
预期结果(分三栏):
+---+---+---+
| A | B | C |
+---+---+---+
| D | E | F |
+---+---+---+
| X | Y | Z |
+---+---+---+
我正在寻找没有定义列数和行数的通用解决方案。所以字符串:
declare @str varchar(max)='A,B;D,E';
将拆分为包含两列的表格:
+---+---+
| A | B |
+---+---+
| D | E |
+---+---+
我的努力。我的第一个想法是使用动态SQL将字符串转换为:
insert into dbo.temp values (...)
这种方法虽然非常快,但有一个小缺点,因为它需要先创建一个具有正确列数的表。我在the answer to my own question below中提出了这个方法,只是为了让问题简短。
另一个想法是将字符串写入服务器上的CSV文件,然后从bulk insert
写下来。虽然我不知道该怎么做以及第一和第二个想法会有什么表现。
我问这个问题的原因是因为我想将数据从Excel导入SQL Server。由于我已经尝试了不同的ADO方法,这种发送矩阵串的方法是一种压倒性的胜利,特别是当字符串的长度增加时。我问了一个年轻的孪生兄弟这个问题: Turn Excel range into VBA string 你会在哪里找到如何从Excel范围准备这样一个字符串的建议。
Bounty 我决定奖励 Matt 。我高度评价了 Sean Lange的答案。谢谢肖恩。我喜欢Matt的简单和简洁的答案。除了Matt和Sean之外的其他方法可以并行使用,所以暂时我不接受任何答案(更新:最后,几个月后,我接受了Matt的回答)。我要感谢 Ahmed Saeed 对VALUES的想法,因为这是我开始的答案的一个很好的演变。当然,这与马特或肖恩不匹配。我赞成了每一个答案。我将非常感谢您对使用这些方法的任何反馈。谢谢你的追求。
答案 0 :(得分:6)
好的这个谜题引起了我的兴趣所以我决定看看我是否可以在没有任何循环的情况下做到这一点。这有两个先决条件。首先,我们假设您有某种计数表。如果你没有,那么这是我的代码。我把它放在我使用的每个系统上。
create View [dbo].[cteTally] as
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 N from cteTally
这个难题的第二部分需要一套基于字符串的分割器。我对此的偏好是超级快速的Jeff Moden分离器。需要注意的是,它只适用于最多8,000的varchar值。这适用于我使用的大多数分隔字符串。你可以在这里找到Jeff Moden的分离器(DelimitedSplit8K)。
http://www.sqlservercentral.com/articles/Tally+Table/72993/
最后但并非最不重要的是,我在这里使用的技术是动态交叉表。这是我从Jeff Moden那里学到的东西。他在这里有一篇很棒的文章。
http://www.sqlservercentral.com/articles/Crosstab/65048/
把所有这些放在一起你可以得到类似的东西,它会非常快并且可以很好地扩展。
declare @str varchar(max)='A,B,C;D,E,F;X,Y,Z';
declare @StaticPortion nvarchar(2000) =
'declare @str varchar(max)=''' + @str + ''';with OrderedResults as
(
select s.ItemNumber
, s.Item as DelimitedValues
, x.ItemNumber as RowNum
, x.Item
from dbo.DelimitedSplit8K(@str, '';'') s
cross apply dbo.DelimitedSplit8K(s.Item, '','') x
)
select '
declare @DynamicPortion nvarchar(max) = '';
declare @FinalStaticPortion nvarchar(2000) = ' from OrderedResults group by ItemNumber';
select @DynamicPortion = @DynamicPortion +
', MAX(Case when RowNum = ' + CAST(N as varchar(6)) + ' then Item end) as Column' + CAST(N as varchar(6)) + CHAR(10)
from cteTally t
where t.N <= (select MAX(len(Item) - LEN(replace(Item, ',', ''))) + 1
from dbo.DelimitedSplit8K(@str, ';')
)
declare @SqlToExecute nvarchar(max) = @StaticPortion + stuff(@DynamicPortion, 1, 1, '') + @FinalStaticPortion
exec sp_executesql @SqlToExecute
- 编辑 -
如果链接无效,这是DelimitedSplit8K函数。
ALTER FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(@pString VARCHAR(8000), @pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
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 (--==== This provides the "zero base" and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT 0 UNION ALL
SELECT TOP (DATALENGTH(ISNULL(@pString,1))) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT t.N+1
FROM cteTally t
WHERE (SUBSTRING(@pString,t.N,1) = @pDelimiter OR t.N = 0)
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY s.N1),
Item = SUBSTRING(@pString,s.N1,ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000))
FROM cteStart s
;
答案 1 :(得分:5)
更简单的方法之一是根据替换分隔符将字符串转换为XML。
declare @str varchar(max)='A,B,C;D,E,F;X,Y,Z';
DECLARE @xmlstr XML
SET @xmlstr = CAST(('<rows><row><col>' + REPLACE(REPLACE(@str,';','</col></row><row><col>'),',','</col><col>') + '</col></row></rows>') AS XML)
SELECT
t.n.value('col[1]','CHAR(1)') as Col1
,t.n.value('col[2]','CHAR(1)') as Col2
,t.n.value('col[3]','CHAR(1)') as Col3
FROM
@xmlstr.nodes ('/rows/row') AS t(n)
<rows><row><col></col><col></col></row><row><col></col><col></col></row></rows>
基本上,您需要添加开始和结束标记,然后将列分隔符替换为列标记,将行分隔符替换为列和行标记as t(n)
告诉您如何最终访问XML行和列。 t是表别名,n是节点别名(类似于行)。所以t.n.value()得到一个特定的行COL[1]
表示获取第一个COL
标记,它是1,所以2是下一个,然后是3等。CHAR(1)
是一个数据类型定义,表示1个字符,它基于每列只有1个字符的示例数据。您可能已经注意到我在动态查询中创建了VARCHAR(MAX)
,因为如果数据类型未知,那么您将需要更多的灵活性。或动态
DECLARE @str varchar(max)='A,B,C,D,E;F,G,H,I,J;K,L,M,N,O';
DECLARE @NumOfColumns INT
SET @NumOfColumns = (LEN(@str) - LEN(REPLACE(@str,',',''))) / (LEN(@str) - LEN(REPLACE(@str,';','')) + 1) + 1
DECLARE @xmlstr XML
SET @xmlstr = CAST(('<rows><row><col>' + REPLACE(REPLACE(@str,';','</col></row><row><col>'),',','</col><col>') + '</col></row></rows>') AS XML)
DECLARE @ParameterDef NVARCHAR(MAX) = N'@XMLInputString xml'
DECLARE @SQL NVARCHAR(MAX) = 'SELECT '
DECLARE @i INT = 1
WHILE @i <= @NumOfColumns
BEGIN
SET @SQL = @SQL + IIF(@i > 1,',','') + 't.n.value(''col[' + CAST(@i AS VARCHAR(10)) + ']'',''NVARCHAR(MAX)'') as Col' + CAST(@i AS VARCHAR(10))
SET @i = @i + 1
END
SET @SQL = @SQL + ' FROM
@XMLInputString.nodes (''/rows/row'') AS t(n)'
EXECUTE sp_executesql @SQL,@ParameterDef,@XMLInputString = @xmlstr
答案 2 :(得分:3)
下面的代码应该可以在SQL Server中使用。它使用Common Table Expression和Dynamic SQL,只需很少的操作。只需将字符串值赋给@str
变量,即可一次执行完整代码。由于它使用CTE,因此很容易在每一步分析数据。
Declare @Str varchar(max)= 'A,B,C;D,E,F;X,Y,Z';
IF OBJECT_ID('tempdb..#RawData') IS NOT NULL
DROP TABLE #RawData;
;WITH T_String AS
(
SELECT RIGHT(@Str,LEN(@Str)-CHARINDEX(';',@Str,1)) AS RawString, LEFT(@Str,CHARINDEX(';',@Str,1)-1) AS RowString, 1 AS CounterValue, len(@Str) - len(replace(@Str, ';', '')) AS RowSize
--
UNION ALL
--
SELECT IIF(CHARINDEX(';',RawString,1)=0,NULL,RIGHT(RawString,LEN(RawString)-CHARINDEX(';',RawString,1))) AS RawString, IIF(CHARINDEX(';',RawString,1)=0,RawString,LEFT(RawString,CHARINDEX(';',RawString,1)-1)) AS RowString, CounterValue+1 AS CounterValue, RowSize AS RowSize
FROM T_String AS r
WHERE CounterValue <= RowSize
)
,T_Columns AS
(
SELECT RowString AS RowValue, RIGHT(a.RowString,LEN(a.RowString)-CHARINDEX(',',a.RowString,1)) AS RawString,
LEFT(a.RowString,CHARINDEX(',',a.RowString,1)-1) AS RowString, 1 AS CounterValue, len(a.RowString) - len(replace(a.RowString, ',', '')) AS RowSize
FROM T_String AS a
--WHERE a.CounterValue = 1
--
UNION ALL
--
SELECT RowValue, IIF(CHARINDEX(',',RawString,1)=0,NULL,RIGHT(RawString,LEN(RawString)-CHARINDEX(',',RawString,1))) AS RawString, IIF(CHARINDEX(',',RawString,1)=0,RawString,LEFT(RawString,CHARINDEX(',',RawString,1)-1)) AS RowString, CounterValue+1 AS CounterValue, RowSize AS RowSize
FROM T_Columns AS r
WHERE CounterValue <= RowSize
)
,T_Data_Prior2Pivot AS
(
SELECT c.RowValue, c.RowString, c.CounterValue
FROM T_Columns AS c
INNER JOIN
T_String AS r
ON r.RowString = c.RowValue
)
SELECT *
INTO #RawData
FROM T_Data_Prior2Pivot;
DECLARE @columnNames VARCHAR(MAX)
,@sqlQuery VARCHAR(MAX)
SELECT @columnNames = COALESCE(@columnNames+', ['+CAST(CounterValue AS VARCHAR)+']','['+CAST(CounterValue AS VARCHAR)+']') FROM (SELECT DISTINCT CounterValue FROM #RawData) T
PRINT @columnNames
SET @sqlQuery = '
SELECT '+@columnNames+'
FROM ( SELECT * FROM #RawData
) AS b
PIVOT (MAX(RowString) FOR CounterValue IN ('+@columnNames+')) AS p
'
EXEC (@sqlQuery);
以下是来自http://statisticsparser.com/的上述查询的统计信息截图。
答案 3 :(得分:2)
**--Using dynamic queries..**
declare @str varchar(max)='A,B,C;D,E,F;X,Y,Z';
declare @cc int
select @cc = len (substring (@str, 0, charindex(';', @str))) - len(replace(substring (@str, 0, charindex(';', @str)), ',', ''))
declare @ctq varchar(max) = 'create table t('
declare @i int = 0
while @i <= @cc
begin
select @ctq = @ctq + 'column' + char(65 + @i) + ' varchar(max), '
select @i = @i + 1
end
select @ctq = @ctq + ')'
select @str = '''' + replace(@str, ',', ''',''') + ''''
select @str = 'insert t select ' + @str
select @str = replace (@str, ';', ''' union all select ''')
exec(@ctq)
exec(@str)
答案 4 :(得分:2)
我发布问题的答案只是为了扩展问题,以显示我在问这个问题时使用的内容。
我们的想法是将原始字符串更改为:
insert into dbo.temp values (...)(...)
这是一个存储过程:
create PROC [dbo].[StringToMatrix]
(
@String nvarchar(max)
,@DelimiterCol nvarchar(50)=','
,@DelimiterRow nvarchar(50)=';'
,@InsertTable nvarchar(200) ='dbo.temp'
,@Delete int=1 --delete is ON
)
AS
BEGIN
set nocount on;
set @String = case when right(@String,len(@DelimiterRow))=@DelimiterRow then left(@string,len(@String)-len(@DelimiterRow)) else @String end --if present, removes the last row delimiter at the very end of string
set @String = replace(@String,@DelimiterCol,''',''')
set @String = replace(@String,@DelimiterRow,'''),'+char(13)+char(10)+'(''')
set @String = 'insert into '+@InsertTable+' values '+char(13)+char(10)+'(''' +@String +''');'
set @String = replace(@String,'''''','null') --optional, changes empty strings to nulls
set @String = CASE
WHEN @Delete = 1 THEN 'delete from '+@InsertTable+';'+char(13)+char(10)+@String
ELSE @String
END
--print @String
exec (@String)
END
使用代码执行proc:
exec [dbo].[StringToMatrix] 'A,B,C;D,E,F;X,Y,Z'
生成以下@String:
delete from [dbo].[temp];
insert into [dbo].[temp] values
('A','B','C'),
('D','E','F'),
('X','Y','Z');
在proc的最后一行是动态执行的。
解决方案需要先创建适当的dbo.table
,然后插入值。这是一个小缺点。因此,解决方案不具有动态性,如果它具有结构:select * into dbo.temp
。不过,我想分享这个解决方案,因为它有效,快速,简单,也许它会成为其他答案的灵感。
答案 5 :(得分:2)
这是另一种方法。
Declare @Str varchar(max)='A,B,C;D,E,F;X,Y,Z';
Select A.*,B.*
Into #TempSplit
From (Select RowNr=RetSeq,String=RetVal From [dbo].[udf-Str-Parse](@Str,';')) A
Cross Apply [dbo].[udf-Str-Parse](A.String,',') B
Declare @SQL varchar(max) = ''
Select @SQL = @SQL+Concat(',Col',RetSeq,'=max(IIF(RetSeq=',RetSeq,',RetVal,null))')
From (Select Distinct RetSeq from #TempSplit) A
Order By A.RetSeq
Set @SQL ='
If Object_ID(''[dbo].[Temp]'', ''U'') IS NOT NULL
Drop Table [dbo].[Temp];
Select ' + Stuff(@SQL,1,1,'') + ' Into [dbo].[Temp] From #TempSplit Group By RowNr Order By RowNr
'
Exec(@SQL)
Select * from Temp
返回
Col1 Col2 Col3
A B C
D E F
X Y Z
现在,这确实需要一个下面列出的解析器:
CREATE FUNCTION [dbo].[udf-Str-Parse] (@String varchar(max),@Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>'+ Replace(@String,@Delimiter,'</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
为了说明,First Parse将返回
RowNr String
1 A,B,C
2 D,E,F
3 X,Y,Z
然后通过CROSS APPLY
再次解析,返回以下内容并存储在临时表中
RowNr String RetSeq RetVal
1 A,B,C 1 A
1 A,B,C 2 B
1 A,B,C 3 C
2 D,E,F 1 D
2 D,E,F 2 E
2 D,E,F 3 F
3 X,Y,Z 1 X
3 X,Y,Z 2 Y
3 X,Y,Z 3 Z
编辑:或只是为了好玩
Declare @String varchar(max)='A,B,C;D,E,F;X,Y,Z';
Declare @SQL varchar(max) = '',@Col int = Len(Left(@String,CharIndex(';',@String)-1))-Len(replace(Left(@String,CharIndex(';',@String)-1),',',''))+1
Select @SQL = @SQL+SQL From (Select Top (@Col) SQL=Concat(',xRow.xNode.value(''col[',N,']'',''varchar(max)'') as Col',N) From (Select N From (Values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) N(N) ) N ) A
Select @SQL = Replace('Declare @XML XML = Cast((''<row><col>'' + Replace(Replace(''[getString]'','';'',''</col></row><row><col>''),'','',''</col><col>'') + ''</col></row>'') as XML);Select '+Stuff(@SQL,1,1,'')+' From @XML.nodes(''/row'') AS xRow(xNode) ','[getString]',@String)
Exec (@SQL)
返回
Col1 Col2 Col3
A B C
D E F
X Y Z
答案 6 :(得分:2)
无需临时表,视图,循环或xml即可解决此问题。首先,您可以根据计数表创建字符串拆分器功能,如下例所示:
public void containsUser(String user) {
DatabaseReference ref = getDatabaseRef("users");
ref.orderByKey().equalTo("user1").addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
System.out.println(dataSnapshot.getKey());
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s){}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {}
@Override
public void onCancelled(DatabaseError databaseError) {
System.out.println(databaseError.getMessage());
}
});
}
然后使用拆分器功能首先根据行分隔符拆分字符串。然后使用OUTER APPLY语句在每一行上再次应用拆分器功能。最后转动结果。 由于列数未知,因此查询必须作为动态SQL执行,如下例所示:
ALTER FUNCTION [dbo].[SplitString]
(
@delimitedString VARCHAR(MAX),
@delimiter VARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
WITH E1(N) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
E2(N) AS (SELECT 1 FROM E1 a, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b),
E42(N) AS (SELECT 1 FROM E4 a, E2 b),
cteTally(N) AS (SELECT 0 UNION ALL SELECT TOP (DATALENGTH(ISNULL(@delimitedString,1)))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E42),
cteStart(N1) AS (SELECT t.N+1 FROM cteTally t
WHERE (SUBSTRING(@delimitedString,t.N,1) = @delimiter OR t.N = 0))
SELECT ROW_NUMBER() OVER (ORDER BY s.N1) AS Nr
,Item = SUBSTRING(@delimitedString, s.N1, ISNULL(NULLIF(CHARINDEX(@delimiter,@delimitedString,s.N1),0)-s.N1,8000))
FROM cteStart s;
答案 7 :(得分:2)
一些带有透视和动态SQL的XML。
将,
和;
替换为代码p
和row
,将其转换为XML,
然后计算列数并将其放入@i
,
使用colsPiv
CTE生成一个字符串并将其放入@col
,字符串就像,[1],[2],..[n]
一样,它将用于旋转,
我们生成动态数据透视查询并执行它。我们还传递了2个参数XML和列数。
以下是查询:
--declare @str varchar(max)='A,B;D,E;X,Y',
declare @str varchar(max)='A,B,C;D,E,F;X,Y,Z',
@x xml,
@col nvarchar(max),
@sql nvarchar(max),
@params nvarchar(max) = '@x xml, @i int',
@i int
SELECT @x = CAST('<row>'+REPLACE(('<p>'+REPLACE(@str,',','</p><p>')+'</p>'),';','</p></row><row><p>')+'</row>' as xml),
@str = REPLACE(@str,';',',;')+',;',
@i = (LEN(@str)-LEN(REPLACE(@str,',','')))/(LEN(@str)-LEN(REPLACE(@str,';','')))
;WITH colsPiv AS (
SELECT 1 as col
UNION ALL
SELECT col+1
FROM colsPiv
WHERE col < @i
)
SELECT @col = (
SELECT ','+QUOTENAME(col)
FROM colsPiv
FOR XML PATH('')
)
SELECT @sql = N'
;WITH cte AS (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) RowNum,
t.c.value(''.'',''nvarchar(max)'') as [Values]
FROM @x.nodes(''/row/p'') as t(c)
)
SELECT '+STUFF(@col,1,1,'')+'
FROM (
SELECT RowNum - CASE WHEN RowNum%@i = 0 THEN @i ELSE RowNum%@i END Seq ,
CASE WHEN RowNum%@i = 0 THEN @i ELSE RowNum%@i END as [ColumnNum],
[Values]
FROM cte
) as t
PIVOT (
MAX([Values]) FOR [ColumnNum] IN ('+STUFF(@col,1,1,'')+')
) as pvt'
EXEC sp_executesql @sql, @params, @x = @x, @i = @i
A,B,C;D,E,F;X,Y,Z
的输出:
1 2 3
A B C
D E F
X Y Z
A,B;D,E;X,Y
:
1 2
A B
D E
X Y
答案 8 :(得分:2)
在这个解决方案中,我将尽可能地使用字符串操作。该过程将通过将输入字符串转换为适合VALUES关键字的形式来构造动态SQL语句,通过计算列数并生成所需的标题来构建列标题。然后,只需执行构造的SQL语句。
Create Proc dbo.Spliter
(
@str varchar(max), @RowSep char(1), @ColSep char(1)
)
as
declare @FirstRow varchar(max), @hdr varchar(max), @n int, @i int=0
-- Generate the Column names
set @FirstRow=iif(CHARINDEX(@RowSep, @str)=0, @str, Left(@str, CHARINDEX(@RowSep, @str)-1))
set @n=LEN(@FirstRow) - len(REPLACE(@FirstRow, @ColSep,''))
while @i<=@n begin
Set @hdr= coalesce(@hdr+', ', '') + 'Col' +convert(varchar, @i)
set @i+=1
end
--Convert the input string to a form suitable for Values keyword
--i.e. similar to Values(('A'),('B'),('C')),(('D'),('E'),('F')), ...etc
set @str =REPLACE(@str, @ColSep,'''),(''')
set @str = 'Values((''' + REPLACE(@str, @RowSep, ''')),((''') + '''))'
exec('SELECT * FROM (' + @str + ') as t('+@hdr+')')
-- exec dbo.Spliter 'A,B,C;D,E,F;X,Y,Z', ';', ','
方法-2:
为了克服PrzemyslawRemin指示的1000行值限制的问题,这里是一个小的修改,将输入字符串转换为一行xml字段,然后CROSS应用它的各个元素。
Create Proc dbo.Spliter2
(
@str varchar(max), @RowSep char(1), @ColSep char(1)
)
as
declare @FirstRow varchar(max), @hdr varchar(max), @ColCount int, @i int=0
, @ColTemplate varchar(max)= 'Col.value(''(./c)[$]'', ''VARCHAR(max)'') AS Col$'
-- Determin the number of columns
set @FirstRow=iif(CHARINDEX(@RowSep, @str)=0, @str, Left(@str, CHARINDEX(@RowSep, @str)-1))
set @ColCount = LEN(@FirstRow) - len(REPLACE(@FirstRow, @ColSep,''))
-- Construct Column Headers by replacing the $ with the column number
-- similar to: Col.value('(./c)[1]', 'VARCHAR(max)') AS Col1, Col.value('(./c)[2]', 'VARCHAR(max)') AS Col2
while @i<=@ColCount begin
Set @hdr= coalesce(@hdr+', ', '') + Replace(@ColTemplate, '$', convert(varchar, @i+1))
set @i+=1
end
-- Convert the input string to XML format
-- similar to '<r><c>A</c><c>B</c><c>c</c></r> <r><c>D</c><c>E</c><c>f</c> </r>
set @str='<c>'+replace(@str, ',', '</c>'+'<c>')+'</c>'
set @str='<r>'+replace(@str , ';', '</c></r><r><c>')+'</r>'
set @str='SELECT ' +@HDR
+ ' From(Values(Cast('''+@str+''' as xml))) as t1(x)
CROSS APPLY x.nodes(''/r'') as t2(Col)'
exec( @str)
-- exec dbo.Spliter2 'A,B,C;D,E,F;X,Y,Z', ';', ','
答案 9 :(得分:1)
以下是使用PIVOT
自定义函数通过动态Split
执行此操作的方法:
拆分功能
CREATE FUNCTION [dbo].[fn_Split](@text varchar(MAX), @delimiter varchar(20) = ' ')
RETURNS @Strings TABLE
(
position int IDENTITY PRIMARY KEY,
value varchar(MAX)
)
AS
BEGIN
DECLARE @index int
SET @index = -1
WHILE (LEN(@text) > 0)
BEGIN
SET @index = CHARINDEX(@delimiter , @text)
IF (@index = 0) AND (LEN(@text) > 0)
BEGIN
INSERT INTO @Strings VALUES (@text)
BREAK
END
IF (@index > 1)
BEGIN
INSERT INTO @Strings VALUES (LEFT(@text, @index - 1))
SET @text = RIGHT(@text, (LEN(@text) - @index))
END
ELSE
SET @text = RIGHT(@text, (LEN(@text) - @index))
END
RETURN
END
GO
<强>查询强>
Declare @Str Varchar (Max) = 'A,B,C;D,E,F;X,Y,Z';
Declare @Sql NVarchar (Max) = '',
@Cols NVarchar (Max) = '';
;With Rows As
(
Select Position, Value As Row
From dbo.fn_Split(@str, ';')
), Columns As
(
Select Rows.Position As RowNum,
Cols.Position As ColNum,
Cols.Value As ColValue
From Rows
Cross Apply dbo.fn_Split(Row, ',') Cols
)
Select *
Into #Columns
From Columns
Select @Cols = Stuff(( Select Distinct ',' + QuoteName(ColNum)
From #Columns
For Xml Path(''), Type).value('.', 'NVARCHAR(MAX)')
, 1, 1, '')
Select @SQL = 'SELECT ' + @Cols + ' FROM #Columns
Pivot
(
Max(ColValue)
For ColNum In (' + @Cols + ')
) P
Order By RowNum'
Execute (@SQL)
<强>结果
1 2 3
A B C
D E F
X Y Z
答案 10 :(得分:1)
我的解决方案是使用string_split和stuff ..首先是关于它如何工作的示例
DECLARE @str varchar(max) = 'A,B,C;D,E,F;X,Y,Z';
;WITH cte
AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS rn, *
FROM string_split(@str, ';')),
cte2
AS (SELECT rn, ROW_NUMBER() OVER (PARTITION BY rn ORDER BY (SELECT NULL)) rownum, val.value
FROM cte c
CROSS APPLY string_split(value, ',') val)
SELECT
[1], [2], [3]
FROM cte2
PIVOT (MAX(value) FOR rownum IN ([1], [2], [3])) p
使用动态sql,我们可以识别列的列表,它适用于任何输入
declare @str varchar(max)='A,B;D,E;X,Y';
declare @sql nvarchar(max)
declare @cols varchar(max) = ''
;with cte as (
select row_number() over(order by (select null)) rn from string_split( substring(@str,1,charindex(';', @str)-1),',')
) select @cols=concat(@cols,',',quotename(rn)) from cte
select @cols = stuff(@cols,1,1,'')
set @sql = N'
declare @str varchar(max)=''A,B;D,E;X,Y'';
with cte as
(
select row_number() over( order by (select null)) as rn, * from string_split(@str,'';'')
), cte2 as (
select rn, row_number() over(partition by rn order by (select null)) rownum, val.value from cte c cross apply string_split(value,'','') val
)
select ' +@cols + '
from cte2
pivot (max(value) for rownum in (' + @cols + ')) p '
exec sp_executesql @sql
如果您使用的是SQL Server&lt; 2016,那么我们可以编写自己的分割功能
答案 11 :(得分:0)
以下不是OP所要求的,但对我来说,将电子表格导出为带有列标题的CSV(实际上是Tab-SV),并将其转换为具有正确列名的SQL表,这对我来说非常方便。 / p>
IF OBJECT_ID('dbo.uspDumpMultilinesWithHeaderIntoTable', 'P') IS NOT NULL
DROP PROCEDURE dbo.uspDumpMultilinesWithHeaderIntoTable;
GO
CREATE PROCEDURE dbo.uspDumpMultilinesWithHeaderIntoTable @TableName VARCHAR(32), @Multilines VARCHAR(MAX)
AS
SET NOCOUNT ON
IF OBJECT_ID('tempdb..#RawData') IS NOT NULL DROP TABLE #RawData
IF OBJECT_ID('tempdb..#RawDataColumnnames') IS NOT NULL DROP TABLE #RawDataColumnnames
DECLARE @RowDelim VARCHAR(9) = '
'
DECLARE @ColDelim VARCHAR(9) = CHAR(9)
DECLARE @MultilinesSafe VARCHAR(MAX)
DECLARE @MultilinesXml XML--VARCHAR(MAX)
DECLARE @ColumnNamesAsString VARCHAR(4000)
DECLARE @SQL NVARCHAR(4000), @ParamDef NVARCHAR(4000)
SET @MultilinesSafe = REPLACE(@Multilines, CHAR(10), '') -- replace LF
SET @MultilinesSafe = (SELECT REPLACE(@MultilinesSafe, CHAR(10), '') FOR XML PATH('')) -- escape any XML confusion
SET @MultilinesSafe = '<rows><row first="1"><cols><col first="1">' + REPLACE(REPLACE(@MultilinesSafe, @RowDelim, '</col></cols></row><row first="0"><cols><col first="0">'), @ColDelim, '</col><col>') + '</col></cols></row></rows>'
SET @MultilinesXml = @MultilinesSafe
--PRINT CAST(@MultilinesXml AS VARCHAR(MAX))
-- extract Column names
SELECT
IDENTITY(INT, 1, 1) AS ID,
t.n.query('.').value('.', 'VARCHAR(4000)') AS ColName
INTO #RawDataColumnnames
FROM @MultilinesXml.nodes('/rows/row[@first="1"]/cols/col') AS t(n) -- just first row
ALTER TABLE #RawDataColumnnames ADD CONSTRAINT [PK_#RawDataColumnnames] PRIMARY KEY CLUSTERED(ID)
-- now tidy any strange characters in column name
UPDATE T SET ColName = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ColName, '.', '_'), ' ', '_'), '[', ''), ']', ''), '.', ''), '$', '') FROM #RawDataColumnnames T
-- create output table
SET @SQL = 'IF OBJECT_ID(''' + @TableName + ''') IS NOT NULL DROP TABLE ' + @TableName
--PRINT 'TableDelete SQL=' + @SQL
EXEC sp_executesql @SQL
SET @SQL = 'CREATE TABLE ' + @TableName + '('
SELECT @SQL = @SQL + CASE T.ID WHEN 1 THEN '' ELSE ', ' END
+ CHAR(13) + '['+ T.ColName + '] VARCHAR(4000) NULL'
FROM #RawDataColumnnames T
ORDER BY ID
SET @SQL = @SQL + ')'
--PRINT 'TableCreate SQL=' + @SQL
EXEC sp_executesql @SQL
-- insert data into output table
SET @SQL = 'INSERT INTO ' + @TableName + ' SELECT '
SELECT @SQL = @SQL + CONCAT(CHAR(13)
, CASE T.ID WHEN 1 THEN ' ' ELSE ',' END
, ' t.n.value(''col[', T.ID, ']'', ''VARCHAR(4000)'') AS TheCol', T.ID)
FROM #RawDataColumnnames T
ORDER BY ID
SET @SQL = @SQL + CONCAT(CHAR(13), 'FROM @TheXml.nodes(''/rows/row[@first="0"]/cols'') as t(n)')
--PRINT 'Insert SQL=' + @SQL
SET @ParamDef = N'@TheXml XML'
EXEC sp_ExecuteSql @SQL, @ParamDef, @TheXml=@MultilinesXml
GO
转换示例(注意空格是制表符!):
EXEC dbo.uspDumpMultilinesWithHeaderIntoTable 'Deleteme', 'Left Centre Right
A B C
D E F
G H I'
进入(通过'SELECT * FROM deleteme')
Left Centre Right
A B C
D E F
G H I
请注意,这是一个实用的代码,不是为了提高工作效率,而是为了完成工作。
IF OBJECT_ID('dbo.uspDumpMultilinesWithHeaderIntoTable', 'P') IS NOT NULL DROP PROCEDURE dbo.uspDumpMultilinesWithHeaderIntoTable;
GO
CREATE PROCEDURE dbo.uspDumpMultilinesWithHeaderIntoTable @TableName VARCHAR(127), @Multilines VARCHAR(MAX), @ColDelimDefault VARCHAR(9) = NULL, @Debug BIT = NULL
AS
SET NOCOUNT ON
IF OBJECT_ID('tempdb..#RawData') IS NOT NULL DROP TABLE #RawData
IF OBJECT_ID('tempdb..#RawDataColumnnames') IS NOT NULL DROP TABLE #RawDataColumnnames
DECLARE @Msg VARCHAR(4000)
DECLARE @PosCr INT, @PosNl INT, @TypeRowDelim VARCHAR(20)
-- work out type of row delimiter(s)
SET @PosCr = CHARINDEX(CHAR(13), @Multilines)
SET @PosNl = CHARINDEX(CHAR(10), @Multilines)
SET @TypeRowDelim = CASE
WHEN @PosCr = @PosNl + 1 THEN 'NL_CR'
WHEN @PosCr = @PosNl - 1 THEN 'CR_NL'
WHEN @PosCr = 0 AND @PosNl > 0 THEN 'NL'
WHEN @PosCr > 0 AND @PosNl = 0 THEN 'CR'
ELSE CONCAT('? CR@', @PosCr, ', NL@', @PosNl, ' is unexpected') END
-- CR(x0d) is a 'good' row delimiter - make the data fit
DECLARE @RowDelim VARCHAR(9)
DECLARE @MultilinesSafe VARCHAR(MAX)
IF @TypeRowDelim = 'CR_NL' OR @TypeRowDelim = 'NL_CR' BEGIN
SET @RowDelim = '
'
SET @MultilinesSafe = REPLACE(@Multilines, CHAR(10), '') -- strip LF
SET @MultilinesSafe = (SELECT @MultilinesSafe FOR XML PATH('')) -- escape any XML confusion
END
ELSE IF @TypeRowDelim = 'CR' BEGIN
SET @RowDelim = '
'
SET @MultilinesSafe = @Multilines
SET @MultilinesSafe = (SELECT @MultilinesSafe FOR XML PATH('')) -- escape any XML confusion
END
ELSE IF @TypeRowDelim = 'NL' BEGIN
SET @RowDelim = '
'
SET @MultilinesSafe = REPLACE(@Multilines, CHAR(10), CHAR(13)) -- change LF to CR
SET @MultilinesSafe = (SELECT @MultilinesSafe FOR XML PATH('')) -- escape any XML confusion
END
ELSE
RAISERROR(@TypeRowDelim , 10, 10)
DECLARE @ColDelim VARCHAR(9) = COALESCE(@ColDelimDefault, CHAR(9))
DECLARE @MultilinesXml XML
DECLARE @ColumnNamesAsString VARCHAR(4000)
DECLARE @SQL NVARCHAR(4000), @ParamDef NVARCHAR(4000)
IF @Debug = 1 BEGIN
SET @Msg = CONCAT('TN=<', @TableName, '>, TypeRowDelim=<', @TypeRowDelim, '>, RowDelim(XML)=<', @RowDelim, '>, ColDelim=<', @ColDelim, '>, LEN(@Multilines)=', LEN(@Multilines))
PRINT @Msg
END
SET @MultilinesSafe = '<rows><row first="1"><cols><col first="1">' + REPLACE(REPLACE(@MultilinesSafe, @RowDelim, '</col></cols></row><row first="0"><cols><col first="0">'), @ColDelim, '</col><col>') + '</col></cols></row></rows>'
SET @MultilinesXml = @MultilinesSafe
--IF @Debug = 1 PRINT CAST(@MultilinesXml AS VARCHAR(MAX))
-- extract Column names
SELECT
IDENTITY(INT, 1, 1) AS ID,
t.n.query('.').value('.', 'VARCHAR(4000)') AS ColName
INTO #RawDataColumnnames
FROM @MultilinesXml.nodes('/rows/row[@first="1"]/cols/col') AS t(n) -- just first row
ALTER TABLE #RawDataColumnnames ADD CONSTRAINT [PK_#RawDataColumnnames] PRIMARY KEY CLUSTERED(ID)
-- now tidy any strange characters in column name
UPDATE T SET ColName = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ColName, '.', '_'), ' ', '_'), '[', ''), ']', ''), '.', ''), '$', '') FROM #RawDataColumnnames T
-- now fix any empty column names
UPDATE T SET ColName = CONCAT('_Col_', ID, '_') FROM #RawDataColumnnames T WHERE ColName = ''
IF @Debug = 1 BEGIN
SET @Msg = CONCAT('#Cols(FromHdr)=', (SELECT COUNT(*) FROM #RawDataColumnnames) )
PRINT @Msg
END
-- create output table
SET @SQL = 'IF OBJECT_ID(''' + @TableName + ''') IS NOT NULL DROP TABLE ' + @TableName
--PRINT 'TableDelete SQL=' + @SQL
EXEC sp_executesql @SQL
SET @SQL = 'CREATE TABLE ' + @TableName + '('
SET @SQL = @SQL + '[_Row_PK_] INT IDENTITY(1,1) PRIMARY KEY,' -- PK
SELECT @SQL = @SQL + CASE T.ID WHEN 1 THEN '' ELSE ', ' END
+ CHAR(13) + '['+ T.ColName + '] VARCHAR(4000) NULL'
FROM #RawDataColumnnames T
ORDER BY ID
SET @SQL = @SQL + ')'
--PRINT 'TableCreate SQL=' + @SQL
EXEC sp_executesql @SQL
-- insert data into output table
SET @SQL = 'INSERT INTO ' + @TableName + ' SELECT '
SELECT @SQL = @SQL + CONCAT(CHAR(13)
, CASE T.ID WHEN 1 THEN ' ' ELSE ',' END
, ' t.n.value(''col[', T.ID, ']'', ''VARCHAR(4000)'') AS TheCol', T.ID)
FROM #RawDataColumnnames T
ORDER BY ID
SET @SQL = @SQL + CONCAT(CHAR(13), 'FROM @TheXml.nodes(''/rows/row[@first="0"]/cols'') as t(n)')
--PRINT 'Insert SQL=' + @SQL
SET @ParamDef = N'@TheXml XML'
EXEC sp_ExecuteSql @SQL, @ParamDef, @TheXml=@MultilinesXml
GO
使用
运行此功能 EXEC dbo.uspDumpMultilinesWithHeaderIntoTable 'Deleteme', 'Left Right
A B C
D E F
G H I'
结果
_Row_PK_ Left _Col_2_ Right
1 A B C
2 D E F
3 G H I