Разделите строку на разделитель строк и разделитель столбцов таблицы в 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);

enter image description here

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

enter image description here


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

  1. заменить , и ; теги p и row, чтобы бросить его как XML,

  2. затем подсчитайте количество столбцов и поместите его в @i,

  3. С colsPiv CTE мы генерируем строку и помещаем ее в @col, строка типа ,[1],[2],..[n] он будет использоваться в распашных,

  4. чем мы генерируем динамический сводный запрос, и выполнить его. Мы также передаем 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) = '&#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, @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 = '&#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, @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