Превращение разделенной запятой строки в отдельные строки


234

У меня есть таблица SQL, как это:

| SomeID         | OtherID     | Data
+----------------+-------------+-------------------
| abcdef-.....   | cdef123-... | 18,20,22
| abcdef-.....   | 4554a24-... | 17,19
| 987654-.....   | 12324a2-... | 13,19,20

Есть ли запрос, где я могу выполнить запрос, SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'который возвращает отдельные строки, например:

| OtherID     | SplitData
+-------------+-------------------
| cdef123-... | 18
| cdef123-... | 20
| cdef123-... | 22
| 4554a24-... | 17
| 4554a24-... | 19

В основном разделить мои данные по запятой на отдельные строки?

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

Я не хочу делать разделение в приложении, так как мне нужно подкачки страниц, поэтому я хотел изучить варианты перед рефакторингом всего приложения.

Это SQL Server 2008(не-R2).


Смотрите также: periscopedata.com/blog/...
Rick James

Ответы:


265

Вы можете использовать замечательные рекурсивные функции из SQL Server:


Образец таблицы:

CREATE TABLE Testdata
(
    SomeID INT,
    OtherID INT,
    String VARCHAR(MAX)
)

INSERT Testdata SELECT 1,  9, '18,20,22'
INSERT Testdata SELECT 2,  8, '17,19'
INSERT Testdata SELECT 3,  7, '13,19,20'
INSERT Testdata SELECT 4,  6, ''
INSERT Testdata SELECT 9, 11, '1,2,3,4'

Запрос

;WITH tmp(SomeID, OtherID, DataItem, String) AS
(
    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM Testdata
    UNION all

    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM tmp
    WHERE
        String > ''
)

SELECT
    SomeID,
    OtherID,
    DataItem
FROM tmp
ORDER BY SomeID
-- OPTION (maxrecursion 0)
-- normally recursion is limited to 100. If you know you have very long
-- strings, uncomment the option

Вывод

 SomeID | OtherID | DataItem 
--------+---------+----------
 1      | 9       | 18       
 1      | 9       | 20       
 1      | 9       | 22       
 2      | 8       | 17       
 2      | 8       | 19       
 3      | 7       | 13       
 3      | 7       | 19       
 3      | 7       | 20       
 4      | 6       |          
 9      | 11      | 1        
 9      | 11      | 2        
 9      | 11      | 3        
 9      | 11      | 4        

1
Код не работает , если изменить тип данных столбца Dataот varchar(max)до varchar(4000), например create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW это может быть связано с тем, что детали до и после UNION ALL возвращают различные типы из функции LEFT. Лично я не понимаю, почему ты не
прыгнешь

Для БОЛЬШОГО набора значений это может превысить пределы рекурсии для CTE.
dsz

3
@dsz Вот когда вы используетеOPTION (maxrecursion 0)
RichardTheKiwi

14
Для работы функций LEFT может потребоваться CAST .... например, LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4

141

Наконец, ожидание закончено с SQL Server 2016 . Они представили функцию Split string STRING_SPLIT:

select OtherID, cs.Value --SplitData
from yourtable
cross apply STRING_SPLIT (Data, ',') cs

Все остальные методы разделения строки, такие как XML, таблица Tally, цикл while и т. Д., Были уничтожены этой STRING_SPLITфункцией.

Вот отличная статья со сравнением производительности: сюрпризы производительности и предположения: STRING_SPLIT .

Для более старых версий использование таблицы подсчета здесь представляет собой одну функцию разделения строк (наилучший возможный подход)

CREATE FUNCTION [dbo].[DelimitedSplit8K]
        (@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 NVARCHAR(4000)
  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 "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) 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 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== 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 l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l
;

По рекомендации от Tally OH! Улучшенная функция CSV Splitter для SQL 8K


9
очень важный ответ
Syed Md. Kamruzzaman

Я бы использовал STRING_SPLIT, если бы только сервер был на SQL Server 2016! Кстати, в соответствии со страницей, на которую вы ссылаетесь, имя поля, которое он выводит, - valueнет SplitData.
Стюарт

89

Проверь это

 SELECT A.OtherID,  
     Split.a.value('.', 'VARCHAR(100)') AS Data  
 FROM  
 (
     SELECT OtherID,  
         CAST ('<M>' + REPLACE(Data, ',', '</M><M>') + '</M>' AS XML) AS Data  
     FROM  Table1
 ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

8
При использовании этого подхода вы должны убедиться, что ни одно из ваших значений не содержит чего-то недопустимого XML
user1151923

Это круто. Могу я спросить вас, как бы я переписал это, если бы я хотел, чтобы новый столбец показывал только первый символ из моей разделенной строки?
Контроль

Это сработало отлично, спасибо! Мне пришлось обновить лимит VARCHAR, но после этого он заработал.
chazbot7

Я должен сказать вам, что метод «lovingl» (чувствовать любовь?) Называется «Метод XML Splitter» и почти такой же медленный, как и цикл Loop, и рекурсивный CTE. Я настоятельно рекомендую вам всегда избегать этого. Вместо этого используйте DelimitedSplit8K. Он разрушает все, кроме функции Split_String () в 2016 году или хорошо написанного CLR.
Джефф Моден

20
select t.OtherID,x.Kod
    from testData t
    cross apply (select Code from dbo.Split(t.Data,',') ) x

3
Делает именно то, что мне было нужно, и его легче читать, чем во многих других примерах (при условии, что в БД уже есть функция для разделения строк с разделителями). Как кто-то, с кем ранее не знаком CROSS APPLY, это довольно полезно
Тобриан

Я не мог понять эту часть (выберите Код из dbo.Split (t.Data, ','))? dbo.Split - это таблица, где она существует, а также Code - это столбец в таблице Split? я не могу найти список этих таблиц или значений в любом месте на этой странице?
Джаендран

1
Мой рабочий код:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Акбар Каутсар

12

По состоянию на февраль 2016 года - см. Пример таблицы TALLY - очень вероятно, что он превзойдет мой TVF ниже, с февраля 2014 года. Сохранение исходного поста ниже для потомков:


Слишком много повторного кода на мой взгляд в приведенных выше примерах. И мне не нравится производительность CTE и XML. Кроме того, явный, Idтак что потребители, которые являются специфическими для заказа, могут указать ORDER BYпредложение.

CREATE FUNCTION dbo.Split
(
    @Line nvarchar(MAX),
    @SplitOn nvarchar(5) = ','
)
RETURNS @RtnValue table
(
    Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
    Data nvarchar(100) NOT NULL
)
AS
BEGIN
    IF @Line IS NULL RETURN

    DECLARE @split_on_len INT = LEN(@SplitOn)
    DECLARE @start_at INT = 1
    DECLARE @end_at INT
    DECLARE @data_len INT

    WHILE 1=1
    BEGIN
        SET @end_at = CHARINDEX(@SplitOn,@Line,@start_at)
        SET @data_len = CASE @end_at WHEN 0 THEN LEN(@Line) ELSE @end_at-@start_at END
        INSERT INTO @RtnValue (data) VALUES( SUBSTRING(@Line,@start_at,@data_len) );
        IF @end_at = 0 BREAK;
        SET @start_at = @end_at + @split_on_len
    END

    RETURN
END

6

Приятно видеть, что это было решено в версии 2016 года, но для всех тех, кто не об этом, вот две обобщенные и упрощенные версии описанных выше методов.

XML-метод короче, но, конечно, требуется, чтобы строка учитывала xml-трюк (без «плохих» символов).

XML-метод:

create function dbo.splitString(@input Varchar(max), @Splitter VarChar(99)) returns table as
Return
    SELECT Split.a.value('.', 'VARCHAR(max)') AS Data FROM
    ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data 
    ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

Рекурсивный метод:

create function dbo.splitString(@input Varchar(max), @Splitter Varchar(99)) returns table as
Return
  with tmp (DataItem, ix) as
   ( select @input  , CHARINDEX('',@Input)  --Recu. start, ignored val to get the types right
     union all
     select Substring(@input, ix+1,ix2-ix-1), ix2
     from (Select *, CHARINDEX(@Splitter,@Input+@Splitter,ix+1) ix2 from tmp) x where ix2<>0
   ) select DataItem from tmp where ix<>0

Функция в действии

Create table TEST_X (A int, CSV Varchar(100));
Insert into test_x select 1, 'A,B';
Insert into test_x select 2, 'C,D';

Select A,data from TEST_X x cross apply dbo.splitString(x.CSV,',') Y;

Drop table TEST_X

XML-МЕТОД 2: Unicode Friendly Ad (Дополнение любезно предоставлено Максом Ходжесом) create function dbo.splitString(@input nVarchar(max), @Splitter nVarchar(99)) returns table as Return SELECT Split.a.value('.', 'NVARCHAR(max)') AS Data FROM ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);


1
Это может показаться очевидным, но как вы используете эти две функции? В частности, вы можете показать, как использовать его в случае использования ОП?
jpaugh

1
Вот краткий пример: Создать таблицу TEST_X (A int, CSV Varchar (100)); Вставьте в test_x выберите 1, «A, B»; Вставьте в test_x выберите 2, «C, D»; Выберите A, данные из TEST_X x cross применяются dbo.splitString (x.CSV, ',') Y; Стол для выпадения TEST_X
Eske Rahn

Это именно то, что мне было нужно! Спасибо.
Нитин Бадоле

5

Пожалуйста, обратитесь ниже TSQL. Функция STRING_SPLIT доступна только на уровне совместимости 130 и выше.

TSQL:

DECLARE @stringValue NVARCHAR(400) = 'red,blue,green,yellow,black'  
DECLARE @separator CHAR = ','

SELECT [value]  As Colour
FROM STRING_SPLIT(@stringValue, @separator); 

РЕЗУЛЬТАТ:

Цвет

красный синий зелёный жёлтый чёрный


5

Очень поздно, но попробуйте это:

SELECT ColumnID, Column1, value  --Do not change 'value' name. Leave it as it is.
FROM tbl_Sample  
CROSS APPLY STRING_SPLIT(Tags, ','); --'Tags' is the name of column containing comma separated values

Итак, у нас было это: tbl_Sample:

ColumnID|   Column1 |   Tags
--------|-----------|-------------
1       |   ABC     |   10,11,12    
2       |   PQR     |   20,21,22

После выполнения этого запроса:

ColumnID|   Column1 |   value
--------|-----------|-----------
1       |   ABC     |   10
1       |   ABC     |   11
1       |   ABC     |   12
2       |   PQR     |   20
2       |   PQR     |   21
2       |   PQR     |   22

Спасибо!


STRING_SPLITизящен, но требует SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Крейг Сильвер

элегантное решение.
Санграм Нандхиле

3
DECLARE @id_list VARCHAR(MAX) = '1234,23,56,576,1231,567,122,87876,57553,1216'
DECLARE @table TABLE ( id VARCHAR(50) )
DECLARE @x INT = 0
DECLARE @firstcomma INT = 0
DECLARE @nextcomma INT = 0

SET @x = LEN(@id_list) - LEN(REPLACE(@id_list, ',', '')) + 1 -- number of ids in id_list

WHILE @x > 0
    BEGIN
        SET @nextcomma = CASE WHEN CHARINDEX(',', @id_list, @firstcomma + 1) = 0
                              THEN LEN(@id_list) + 1
                              ELSE CHARINDEX(',', @id_list, @firstcomma + 1)
                         END
        INSERT  INTO @table
        VALUES  ( SUBSTRING(@id_list, @firstcomma + 1, (@nextcomma - @firstcomma) - 1) )
        SET @firstcomma = CHARINDEX(',', @id_list, @firstcomma + 1)
        SET @x = @x - 1
    END

SELECT  *
FROM    @table

Это один из немногих методов, который работает с ограниченной поддержкой SQL в хранилище данных SQL Azure.
Аарон Шульц

1
;WITH tmp(SomeID, OtherID, DataItem, Data) as (
    SELECT SomeID, OtherID, LEFT(Data, CHARINDEX(',',Data+',')-1),
        STUFF(Data, 1, CHARINDEX(',',Data+','), '')
FROM Testdata
WHERE Data > ''
)
SELECT SomeID, OtherID, Data
FROM tmp
ORDER BY SomeID

только с небольшим изменением вышеупомянутого запроса ...


6
Не могли бы вы вкратце объяснить, как это улучшение по сравнению с версией в принятом ответе?
Ли

Нет союза все ... меньше кода. Так как он использует union all вместо union, не должно ли быть разница в производительности?
TamusJRoyce

1
Это не вернуло все строки, которые должны были быть. Я не уверен, что для данных требуется объединение всех, но ваше решение вернуло то же количество строк, что и исходная таблица.
Oedhel Setren

1
(проблема здесь в том, что рекурсивная часть пропущена ...)
Eske Rahn

Не дает мне ожидаемый результат, только давая первую запись в отдельном ряду
Ankit Misra

1

При использовании этого подхода вы должны убедиться, что ни одно из ваших значений не содержит чего-то недопустимого XML - user1151923

Я всегда использую метод XML. Убедитесь, что вы используете VALID XML. У меня есть две функции для преобразования между действительным XML и текстом. (Я, как правило, убираю возврат каретки, потому что я обычно не нуждаюсь в них

CREATE FUNCTION dbo.udf_ConvertTextToXML (@Text varchar(MAX)) 
    RETURNS varchar(MAX)
AS
    BEGIN
        SET @Text = REPLACE(@Text,CHAR(10),'')
        SET @Text = REPLACE(@Text,CHAR(13),'')
        SET @Text = REPLACE(@Text,'<','&lt;')
        SET @Text = REPLACE(@Text,'&','&amp;')
        SET @Text = REPLACE(@Text,'>','&gt;')
        SET @Text = REPLACE(@Text,'''','&apos;')
        SET @Text = REPLACE(@Text,'"','&quot;')
    RETURN @Text
END


CREATE FUNCTION dbo.udf_ConvertTextFromXML (@Text VARCHAR(MAX)) 
    RETURNS VARCHAR(max)
AS
    BEGIN
        SET @Text = REPLACE(@Text,'&lt;','<')
        SET @Text = REPLACE(@Text,'&amp;','&')
        SET @Text = REPLACE(@Text,'&gt;','>')
        SET @Text = REPLACE(@Text,'&apos;','''')
        SET @Text = REPLACE(@Text,'&quot;','"')
    RETURN @Text
END

1
Есть небольшая проблема с кодом, который у вас есть. Это изменит '<' на '& amp; lt;' вместо "& lt;" как и должно быть. Поэтому вам нужно сначала закодировать '&'.
Стюарт

Там нет необходимости для такой функции ... Просто используйте неявные способности. Попробуйте это:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

функция

CREATE FUNCTION dbo.SplitToRows (@column varchar(100), @separator varchar(10))
RETURNS @rtnTable TABLE
  (
  ID int identity(1,1),
  ColumnA varchar(max)
  )
 AS
BEGIN
    DECLARE @position int = 0
    DECLARE @endAt int = 0
    DECLARE @tempString varchar(100)

    set @column = ltrim(rtrim(@column))

    WHILE @position<=len(@column)
    BEGIN       
        set @endAt = CHARINDEX(@separator,@column,@position)
            if(@endAt=0)
            begin
            Insert into @rtnTable(ColumnA) Select substring(@column,@position,len(@column)-@position)
            break;
            end
        set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

        Insert into @rtnTable(ColumnA) select @tempString
        set @position=@endAt+1;
    END
    return
END

Случай использования

select * from dbo.SplitToRows('T14; p226.0001; eee; 3554;', ';')

Или просто выберите с множественным набором результатов

DECLARE @column varchar(max)= '1234; 4748;abcde; 324432'
DECLARE @separator varchar(10) = ';'
DECLARE @position int = 0
DECLARE @endAt int = 0
DECLARE @tempString varchar(100)

set @column = ltrim(rtrim(@column))

WHILE @position<=len(@column)
BEGIN       
    set @endAt = CHARINDEX(@separator,@column,@position)
        if(@endAt=0)
        begin
        Select substring(@column,@position,len(@column)-@position)
        break;
        end
    set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

    select @tempString
    set @position=@endAt+1;
END

Использование цикла while внутри многозначной табличной функции является наихудшим способом разделения строк. В этом вопросе уже так много опций, основанных на множествах.
Шон Ланге

0

Ниже работает на SQL Server 2008

select *, ROW_NUMBER() OVER(order by items) as row# 
from 
( select 134 myColumn1, 34 myColumn2, 'd,c,k,e,f,g,h,a' comaSeperatedColumn) myTable
    cross apply 
SPLIT (rtrim(comaSeperatedColumn), ',') splitedTable -- gives 'items'  column 

Получит все декартовы произведения со столбцами таблицы происхождения плюс «элементы» разделенной таблицы.


0

Вы можете использовать следующую функцию для извлечения данных

CREATE FUNCTION [dbo].[SplitString]
(    
    @RowData NVARCHAR(MAX),
    @Delimeter NVARCHAR(MAX)
)
RETURNS @RtnValue TABLE 
(
    ID INT IDENTITY(1,1),
    Data NVARCHAR(MAX)
) 
AS
BEGIN 
    DECLARE @Iterator INT
    SET @Iterator = 1

    DECLARE @FoundIndex INT
    SET @FoundIndex = CHARINDEX(@Delimeter,@RowData)

    WHILE (@FoundIndex>0)
    BEGIN
        INSERT INTO @RtnValue (data)
        SELECT 
            Data = LTRIM(RTRIM(SUBSTRING(@RowData, 1, @FoundIndex - 1)))

        SET @RowData = SUBSTRING(@RowData,
                @FoundIndex + DATALENGTH(@Delimeter) / 2,
                LEN(@RowData))

        SET @Iterator = @Iterator + 1
        SET @FoundIndex = CHARINDEX(@Delimeter, @RowData)
    END

    INSERT INTO @RtnValue (Data)
    SELECT Data = LTRIM(RTRIM(@RowData))

    RETURN
END

Использование цикла while внутри многозначной табличной функции является наихудшим способом разделения строк. В этом вопросе уже так много опций, основанных на множествах.
Шон Ланге
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.