Разделите строку на разделитель строк и разделитель столбцов таблицы в SQL server
как разбить строку, содержащую матрицу, на таблицу в SQL Server? Строки и столбцы и строки разделители.
Предположим у меня есть строка:
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 (...)
этот подход, хотя и очень быстрый, имеет небольшой недостаток, потому что он требует создания таблицы с правильным количеством столбцов. Я представил этот метод в ответ на мой собственный вопрос ниже просто чтобы вопрос был коротким.
другой идеей было бы записать строку в CSV-файл на сервере, а затем bulk insert
от него. Хотя я не знаю, как это сделать и что бы выполнение первой и второй идеи.
причина, по которой я задал этот вопрос, заключается в том, что я хочу импортировать данные из Excel в SQL Server. Поскольку я экспериментировал с различными подходами ADO, этот метод отправки строки матрицы-это победа оползня, особенно когда длина строки увеличивается. Я задал вопрос младшему брату-близнецу:включите диапазон Excel в строку VBA где вы найдете предложения, как подготовить такой строку из диапазона Excel.
награда я решил наградить матовая. Я весил очень Шон Лэнг ответ. Спасибо, Шон. Мне понравился ответ Мэтта за его простоту и краткость. Различные подходы, кроме Мэтта и Шона, могут использоваться параллельно, поэтому пока я не принимаю никакого ответа (обновление: наконец, через несколько месяцев я принял ответ Мэтта). Я хочу поблагодарить Ахмед Саед для его идеи с ценностями, потому что это хорошая эволюция ответа, с которого я начал. Конечно, он не сравнится с Мэттом или Шоном. Я поддержал каждый ответ. Я буду признателен за любые отзывы от вас об использовании этих методов. Спасибо за задание.
12 ответов
один из самых простых способов-преобразовать строку в 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)
- строка формата как XML
<rows><row><col></col><col></col></row><row><col></col><col></col></row></rows>
в основном вам нужно добавить начальные и конечные теги, а затем заменить разделитель столбцов тегами столбцов и разделителем строк тегами столбцов и строк - .узлы-это метод для типа данных xml ,который " полезен, когда вы хотите измельчить экземпляр типа данных xml в реляционные данные" https://msdn.microsoft.com/en-us/library/ms188282.aspx
-
as t(n)
сообщает вам, как вы получите доступ к строке и столбцу XML. t-псевдоним таблицы, а n-псевдоним узла (вроде строки). так что Ти.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
хорошо, эта головоломка заинтриговала меня, поэтому я решил посмотреть, смогу ли я сделать это без каких-либо циклов. Для этого есть несколько предпосылок. Во-первых, мы предположим, что у вас есть какая-то таблица подсчета. В случае, если у вас нет этого, вот код для моего. Я держу это в каждой системе, которую использую.
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
вторая часть этой головоломки нуждается в наборе на основе string splitter. Мое предпочтение для этого-Uber fast Jeff Moden splitter. Одно предостережение заключается в том, что это будет только работа с varchar значения до 8000. Этого достаточно для большинства строк с разделителями, с которыми я работаю. Здесь вы можете найти разделитель Джеффа Модена (DelimitedSplit8K).
http://www.sqlservercentral.com/articles/Tally + таблица/72993/
последним, но не менее важным является то, что метод, который я использую здесь, является динамической кросс-вкладкой. Этому я научился у Джеффа Модена. У него есть отличная статья на эту тему здесь.
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
-- EDIT--
вот функция 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
;
ниже код должен работать в SQL Server. Он использует общее табличное выражение и динамический 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/.
**--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)
Я отправляю ответ на мой вопрос, чтобы расширить вопрос, чтобы показать, что я использую в то время, когда я задаю вопрос.
идея в том, чтобы изменить исходную строку:
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'
генерирует следующую строку@:
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
. Тем не менее, я хочу поделиться этим решением, потому что оно работает, оно быстрое, простое и, возможно, послужит вдохновением для других ответов.
эта проблема может быть решена без необходимости временных таблиц, представлений, циклов или xml. Сначала вы можете создать функцию разветвителя строк на основе таблицы подсчета, как показано ниже:
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;
затем используйте функцию splitter для первого разделения строк на основе разделителей строк. Затем снова примените функцию splitter к каждой строке с помощью оператора OUTER APPLY. Наконец, поверните результат. Поскольку количество столбцов неизвестно, запрос должен быть выполнен как динамический SQL в примере ниже:
DECLARE @source VARCHAR(max) = 'A1,B1,C1,D1,E1,F1,G1;A2,B2,C2,D2,E2,F2,G2;A3,B3,C3,D3,E3,F3,G3;A4,B4,C4,D4,E4,F4,G4;A5,B5,C5,D5,E5,F5,G5;A6,B6,C6,D6,E6,F6,G6;A7,B7,C7,D7,E7,F7,G7;A8,B8,C8,D8,E8,F8,G8;A9,B9,C9,D9,E9,F9,G9;A10,B10,C10,D10,E10,F10,G10;A11,B11,C11,D11,E11,F11,G11;A12,B12,C12,D12,E12,F12,G12;A13,B13,C13,D13,E13,F13,G13;A14,B14,C14,D14,E14,F14,G14;A15,B15,C15,D15,E15,F15,G15;A16,B16,C16,D16,E16,F16,G16;A17,B17,C17,D17,E17,F17,G17;A18,B18,C18,D18,E18,F18,G18;A19,B19,C19,D19,E19,F19,G19;A20,B20,C20,D20,E20,F20,G20'
-- First determine the columns names. Since the string can be potential very long we don’t want to parse the entire string to determine how many columns
-- we have, instead get sub string of main string up to first row delimiter.
DECLARE @firstRow VARCHAR(max) = LEFT(@source, CHARINDEX(';', @source) - 1);
DECLARE @columnNames NVARCHAR(MAX) = '';
-- Use string splitter function on sub string to determine column names.
SELECT @columnNames = STUFF((
SELECT ',' + QUOTENAME(CAST(ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS VARCHAR(10)))
FROM [dbo].[SplitString](@firstRow, ',') Items
FOR XML PATH('')), 1, 1, '');
-- Next build dynamic query that will generate our matrix table.
-- CTE first split string by row delimiters then it applies the string split function again on each row.
DECLARE @pivotQuery NVARCHAR(MAX) ='
;WITH CTE_SplitData AS
(
SELECT R.Nr AS [Row]
,C.[Columns]
,ROW_NUMBER() OVER (PARTITION BY R.Nr ORDER BY R.Item) AS ColumnNr
FROM [dbo].[SplitString](@source, '';'') R
OUTER APPLY (
SELECT Item AS [Columns]
FROM [dbo].[SplitString](R.Item, '','')
) C
)
-- Pivoted reuslt
SELECT * FROM
(
SELECT *
FROM CTE_SplitData
)as T
PIVOT
(
max(T.[Columns])
for T.[ColumnNr] in (' + @columnNames + ')
) as P'
EXEC sp_executesql @pivotQuery,
N'@source VARCHAR(MAX)',
@source = @source; -- Pass the source string to be split as a parameter to the dynamic query.
некоторые XML с поворотным и динамическим SQL.
заменить
,
и;
теги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
в этом решении я буду использовать string manipulation как можно больше. Процедура построит динамический оператор SQL путем преобразования входной строки в форму, подходящую для ключевого слова VALUES, построит заголовки столбцов, подсчитав количество столбцов и сгенерирует необходимые заголовки. Затем просто выполните построенную инструкцию 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:
чтобы преодолеть проблему ограничения значений 1000 строк, как указано PrzemyslawRemin, вот небольшая модификация для преобразования входной строки в одно поле xml строки, а затем перекрестного применения его с отдельными элементами.
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', ';', ','
вот еще один подход.
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',' ')
просто для иллюстрации, первый разбор вернется
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
EDIT: или только для fun
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
вот способ сделать это с помощью dynamic 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
мое решение использует string_split и прочее.. Во-первых, пример того, как это работает
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
следующее Не совсем то, что OP попросил, но мне было удобно экспортировать электронную таблицу как CSV (на самом деле Tab-SV) с заголовками столбцов и преобразовать ее в таблицу SQL с правильными именами столбцов.
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