Transformando uma sequência separada por vírgulas em linhas individuais


234

Eu tenho uma tabela SQL como esta:

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

existe uma consulta em que eu possa executar uma consulta como SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'essa retorna linhas individuais, assim:

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

Dividir basicamente meus dados na vírgula em linhas individuais?

Estou ciente de que armazenar uma comma-separatedstring em um banco de dados relacional parece idiota, mas o caso de uso normal no aplicativo consumidor torna isso realmente útil.

Não quero fazer a divisão no aplicativo, pois preciso de paginação; portanto, queria explorar as opções antes de refatorar o aplicativo inteiro.

É SQL Server 2008(não R2).


Respostas:


265

Você pode usar as maravilhosas funções recursivas do SQL Server:


Tabela de exemplo:

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'

A pergunta

;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

Resultado

 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
O código não funciona se alterar o tipo de dados da coluna Datade varchar(max)para varchar(4000), por exemplo create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@ NickW, pode ser que as partes antes e depois de UNION ALL retornem tipos diferentes da função ESQUERDA. Pessoalmente, eu não vejo por que você não iria saltar para MAX uma vez que você chegar a 4000 ...
RichardTheKiwi

Para um conjunto de valores GRANDE, isso pode exceder os limites de recursão para CTEs.
dsz 14/01

3
@dsz Isso é quando você usaOPTION (maxrecursion 0)
RichardTheKiwi

14
As funções LEFT pode precisar de um CAST para o trabalho .... por exemplo LEFT (CAST (dados como VARCHAR (MAX)) ....
smoore4

141

Finalmente, a espera terminou com o SQL Server 2016 . Eles introduziram a função String dividida STRING_SPLIT:

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

Todos os outros métodos para dividir seqüências de caracteres como XML, tabela Tally, loop while, etc., foram surpreendidos por essa STRING_SPLITfunção.

Aqui está um excelente artigo com comparação de desempenho: Surpresas e premissas de desempenho: STRING_SPLIT .

Para versões mais antigas, o uso da tabela de contagem aqui é uma função de cadeia de divisão (melhor abordagem possível)

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
;

Referido de Tally OH! Uma função aprimorada de “CSV Splitter” do SQL 8K


9
resposta muito importante
Syed Md. Kamruzzaman 6/17/17

Eu usaria STRING_SPLIT se apenas o servidor estivesse no SQL Server 2016! BTW, de acordo com a página à qual você vinculou, o nome do campo que ele gera é value, não SplitData.
Stewart

89

Verifique isto

 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
Ao usar essa abordagem, você tem que ter certeza que nenhum dos seus valores contém algo que seria XML ilegal
user1151923

Isso é ótimo. Posso perguntar-lhe, como eu reescreveria isso se eu quisesse que a nova coluna mostrasse apenas o primeiro caractere da minha string dividida?
Control

Isso funcionou perfeitamente, obrigado! Eu tive que atualizar o limite VARCHAR, mas funcionou perfeitamente depois disso.
precisa saber é o seguinte

Eu tenho que lhe dizer que o método é "lovingl" (sente o amor?) Chamado "XML Splitter Method" e é quase tão lento quanto um While Loop ou CTE Recursive. Eu recomendo fortemente que você evite isso o tempo todo. Use DelimitedSplit8K. Ele abre as portas de tudo, exceto a função Split_String () em 2016 ou um CLR bem escrito.
Jeff Moden 11/06

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

3
Faz exatamente o que eu estava procurando e é mais fácil de ler do que muitos outros exemplos (desde que já exista uma função no banco de dados para a divisão delimitada de cadeias). Como alguém que não está familiarizado anteriormente CROSS APPLY, isso é meio útil!
precisa

Não consegui entender esta parte (selecione Código de dbo.Split (t.Data, ','))? dbo.Split é uma tabela em que isso existe e também Code é a coluna na tabela Split? não consegui encontrar a lista dessas tabelas ou valores em nenhum lugar desta página?
Jayendran

1
Meu código de trabalho são:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

A partir de fevereiro de 2016 - veja o exemplo da tabela TALLY - muito provavelmente superará meu TVF abaixo, a partir de fevereiro de 2014. Mantendo a postagem original abaixo para posteridade:


Muito código repetido para o meu gosto nos exemplos acima. E não gosto do desempenho de CTEs e XML. Além disso, um explícito Idpara que os consumidores específicos de pedidos possam especificar uma ORDER BYcláusula.

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

É bom ver que ele foi resolvido na versão de 2016, mas para todos aqueles que não estão nessa, aqui estão duas versões generalizadas e simplificadas dos métodos acima.

O método XML é mais curto, mas é claro que requer a string para permitir o xml-trick (sem caracteres 'ruins').

Método 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); 

Método recursivo:

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

Função em ação

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

MÉTODO XML 2: Compatível com Unicode 😀 (cortesia de Max Hodges) 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
Isso pode parecer óbvio, mas como você usa essas duas funções? Especialmente, você pode mostrar como usá-lo no caso de uso do OP?
jpaugh

1
Aqui está um exemplo rápido: Crie a tabela TEST_X (A int, CSV Varchar (100)); Insira no teste_x, selecione 1, 'A, B'; Insira em test_x selecione 2, 'C, D'; Selecione A, dados de TEST_X x cross apply dbo.splitString (x.CSV, ',') Y; Drop table TEST_X
Eske Rahn

Isso é exatamente o que eu precisava! Obrigado.
Nitin Badole

5

Consulte abaixo TSQL. A função STRING_SPLIT está disponível apenas no nível de compatibilidade 130 e acima.

TSQL:

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

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

RESULTADO:

Cor

vermelho azul verde amarelo preto


5

Muito tarde, mas tente isso:

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

Então estávamos tendo o seguinte: tbl_Sample:

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

Depois de executar esta consulta:

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

Obrigado!


STRING_SPLITé bacana, mas requer o SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/functions/…
Craig Silver

solução elegante.
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

Este é um dos poucos métodos que funciona com o suporte limitado ao SQL no Azure SQL Data Warehouse.
Aaron Schultz

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

com apenas pequenas modificações para a consulta acima ...


6
Você pode explicar brevemente como isso é uma melhoria em relação à versão na resposta aceita?
Leigh

Sem união tudo ... menos código. Uma vez que está usando união em vez de união, não deve haver uma diferença de desempenho?
precisa saber é o seguinte

1
Isso não retornou todas as linhas que deveria ter. Não sei ao certo o que dizer dos dados exige a união de todos, mas sua solução retornou o mesmo número de linhas que a tabela original.
Oedhel Setren

1
(o problema aqui é que a parte recursiva é o omitido ...)
Eske Rahn

Não me dar esperada saída somente dando primeiro registro na linha separado
Ankit Misra

1

Ao usar essa abordagem, você deve garantir que nenhum dos seus valores contenha algo que seria XML ilegal - user1151923

Eu sempre uso o método XML. Certifique-se de usar XML válido. Eu tenho duas funções para converter entre XML e texto válidos. (Costumo retirar os retornos de carro, pois geralmente não preciso deles.

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
Há um pequeno problema com o código que você tem lá. Vai mudar '<' para '& amp; lt;' em vez de '& lt;' como deveria. Então você precisa codificar '&' primeiro.
Stewart

Não há necessidade de tal função ... Basta usar as habilidades implícitas. Tente isto:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

Função

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

Caso de uso

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

Ou apenas uma seleção com vários resultados

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

Usar um loop while dentro de uma função com valor de tabela de várias instruções é praticamente a pior maneira possível de dividir seqüências de caracteres. Já existem muitas opções baseadas em conjunto nessa questão.
Sean Lange

0

Abaixo trabalha no 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 

Obterá todo o produto cartesiano com as colunas da tabela de origem mais "itens" da tabela dividida.


0

Você pode usar a seguinte função para extrair dados

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

Usar um loop while dentro de uma função com valor de tabela de várias instruções é praticamente a pior maneira possível de dividir seqüências de caracteres. Já existem muitas opções baseadas em conjunto nessa questão.
Sean Lange
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.