Перетворення рядка, розділеного комами, в окремі рядки


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/…
Рік Джеймс

Відповіді:


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 повертають різні типи з лівої функції. Особисто я не бачу, чому ви не підскочите до MAX, як тільки доберетеся до 4000 ...
RichardTheKiwi

Для набору значень BIG це може перевищити межі рекурсії для CTE.
dsz

3
@dsz Це коли ви використовуєтеOPTION (maxrecursion 0)
RichardTheKiwi

14
Функціям НАЛЕВО може знадобитися CAST для роботи .... наприклад, ЛІВО (CAST (Дані AS VARCHAR (MAX)) ....
smoore4

141

Нарешті, очікування закінчено із SQL Server 2016 . Вони ввели функцію розділення рядків STRING_SPLIT:

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

Усі інші методи розділення рядків, такі як XML, таблиця Tally, цикл тощо, були підірвані за допомогою цієї 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! Покращена функція SQL 8K “CSV Splitter”


9
дуже важлива відповідь
Syed Md. Kamruzzaman

Я б використовував STRING_SPLIT, якби тільки сервер був на SQL Server 2016! BTW відповідно до сторінки, на яку ви пов’язані, назва поля, яке вона виводить, - 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" і майже такий же повільний, як або цикл Хоча, або Рекурсивний 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 - стовпець у розділеній таблиці? я не міг знайти список цих таблиць або значень ніде на цій сторінці?
Jayendran

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

12

Станом на лютий 2016 року - див. Приклад таблиці ТАЛЬНО - дуже ймовірно, що перевершить мій показник 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 😀 ( Додано люб’язно Максу Ходжесу) 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 хрест застосуйте dbo.splitString (x.CSV, ',') Y; Таблиця скидання TEST_X
Eske

Це саме те, що мені було потрібно! Дякую.
Нітін Бадоле

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

Дякую!



елегантне рішення.
Sangram Nandkhile

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 у сховищі даних Azure SQL.
Аарон Шульц

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
Це не повернуло всіх рядків, які воно повинно мати. Я не впевнений, що стосовно даних вимагає об'єднання всіх, але ваше рішення повернуло ту саму кількість рядків, що і вихідна таблиця.
Едхель Сетрен

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

Не даючи мені очікуваного виходу, лише даючи перший запис в окремому ряду
Анкіт Місра

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(''))
Шнуго

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

Використання циклу "time" всередині багатоступеневої таблиці, яка оцінюється функцією, - це приблизно найгірший спосіб розділити рядки. В цьому питанні вже існує безліч наборів варіантів.
Шон Ланге

0

Нижче працює на сервері sql 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

Використання циклу "time" всередині багатоступеневої таблиці, яка оцінюється функцією, - це приблизно найгірший спосіб розділити рядки. В цьому питанні вже існує безліч наборів варіантів.
Шон Ланге
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.