Разделить строку на таблицу с разделителем строк и разделителем столбцов на SQL-сервере

Как разбить строку, содержащую матрицу, на таблицу в 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.

Bounty Я решил наградить Matt. Я высоко оценил ответ Шона Ланжа. Спасибо, Шон. Мне понравился Мэтт за его простоту и краткость. Различные подходы, помимо Мэтта и Шона, могут использоваться параллельно, поэтому пока я не принимаю никакого ответа (обновление: наконец, через несколько месяцев я принял ответ Мэтта). Я хочу поблагодарить Ahmed Saeed за его идею с VALUES, потому что это хорошая эволюция ответа, с которого я начал. Конечно, это не подходит для Мэтта или Шона. Я поддержал каждый ответ. Я буду признателен за любые отзывы от вас об использовании этих методов. Спасибо за квест.

Ответ 1

Один из самых простых способов - преобразовать строку в 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> В основном вам нужно добавить начальный и конечный теги, а затем заменить разделитель столбцов тегами столбцов и разделителем строк с тегами столбцов и строк
  • .nodes - это метод типа данных xml, который "полезен, когда вы хотите уничтожить экземпляр типа данных xml в реляционных данных" https://msdn.microsoft.com/en-us/library/ms188282.aspx
  • as t(n) сообщает вам, как вы получите доступ к строке и столбцу XML. t - псевдоним таблицы, а n - псевдоним node (вроде строки). поэтому 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

ОК, эта загадка заинтриговала меня, поэтому я решил посмотреть, смогу ли я сделать это без каких-либо циклов. Для этого есть несколько предварительных условий. Во-первых, мы предположим, что у вас есть какая-то таблица подсчета. В случае, если у вас нет этого, это код для моего. Я держу это в каждой системе, которую я использую.

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

Вторая часть этой головоломки нуждается в наборе разделителей строк. Мое предпочтение этому - быстрый сплетчик Джеффа Модена. Одно из предостережений заключается в том, что он будет работать только с значениями varchar до 8000. Это много для большинства строк с разделителями, с которыми я работаю. Здесь вы можете найти сплиттер Джеффа Модена (DelimitedSplit8K).

http://www.sqlservercentral.com/articles/Tally+Table/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
;

Ответ 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  '[email protected]+'
FROM    ( SELECT * FROM #RawData 
        ) AS b
PIVOT   (MAX(RowString) FOR CounterValue IN ('[email protected]+')) AS p
'

EXEC (@sqlQuery);

введите описание изображения здесь

Ниже приведен скриншот статистики для вышеуказанного запроса из http://statisticsparser.com/.

введите описание изображения здесь

Ответ 4

**--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)

Ответ 5

Я отправляю ответ на свой вопрос только для того, чтобы расширить вопрос, чтобы показать, что я использую в то время, когда я задаю вопрос.

Идея состоит в том, чтобы изменить исходную строку на:

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))[email protected] 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 '[email protected]+' values '+char(13)+char(10)+'(''' [email protected] +''');'
set @String = replace(@String,'''''','null') --optional, changes empty strings to nulls

set @String = CASE 
    WHEN @Delete = 1 THEN 'delete from '[email protected]+';'+char(13)+char(10)[email protected] 
    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. Тем не менее, я хочу поделиться этим решением, потому что оно работает, оно быстро, просто и, возможно, это послужит источником вдохновения для некоторых других ответов.

Ответ 6

Вот еще один подход.

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',' ')

Чтобы проиллюстрировать, первый Parse вернет

RowNr   String
1       A,B,C
2       D,E,F
3       X,Y,Z

Затем это снова анализируется с помощью CROSS APPLY, который возвращает следующее и сохраняется в таблице temp

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

Ответ 7

Эта проблема может быть решена без необходимости временных таблиц, представлений, циклов или 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;

Затем используйте функцию сплиттера, чтобы сначала разделить строки на основе разделителей строк. Затем снова примените функцию сплиттера в каждой строке с помощью инструкции 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.

Ответ 8

Некоторые 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

Ответ 9

в этом решении я буду использовать строковые манипуляции как можно больше. Процедура построит динамический оператор 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<[email protected] 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('[email protected]+')')    

-- exec dbo.Spliter 'A,B,C;D,E,F;X,Y,Z', ';', ','

Способ-2:

Чтобы преодолеть проблему ограничения строк 1000 строк, как указано PrzemyslawRemin, вот небольшая модификация для преобразования входной строки в одно xml-поле строки, а затем CROSS APPLY с ее отдельными элементами.

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<[email protected] 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 ' [email protected] 
    + ' From(Values(Cast('''[email protected]+''' 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', ';', ','

Ответ 10

Вот как это сделать, используя динамический 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

Query

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

Ответ 11

Мое решение использует 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 ' [email protected] + '
from cte2 
pivot (max(value) for rownum in (' + @cols + ')) p '

exec sp_executesql @sql

Если вы используете SQL Server < 2016, тогда мы можем написать собственную функцию разделения

Ответ 12

Не совсем то, что 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) = '&#x0d;'
    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, @[email protected]

GO

Пример преобразования (обратите внимание, что пробелы являются вкладками!):

    EXEC dbo.uspDumpMultilinesWithHeaderIntoTable 'Deleteme', 'Left Centre  Right
A   B   C
D   E   F
G   H   I'

в (через 'SELECT * FROM deleteeme')

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('? [email protected]', @PosCr, ', [email protected]', @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 = '&#x0d;'
    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 = '&#x0d;'
    SET @MultilinesSafe = @Multilines
    SET @MultilinesSafe = (SELECT @MultilinesSafe FOR XML PATH(''))  -- escape any XML confusion
END
ELSE IF @TypeRowDelim = 'NL' BEGIN
    SET @RowDelim = '&#x0d;'
    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, @[email protected]

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