Cadeia de divisão T-SQL


139

Eu tenho uma coluna do SQL Server 2008 R2 contendo uma sequência que eu preciso dividir por vírgula. Eu já vi muitas respostas no StackOverflow, mas nenhuma delas funciona no R2. Verifiquei se tenho permissões de seleção em qualquer exemplo de função de divisão. Qualquer ajuda muito apreciada.


7
Este é um dos milhões de respostas que eu como stackoverflow.com/a/1846561/227755
nurettin

2
Como assim "nenhum deles funciona"? Você pode ser mais específico?
Aaron Bertrand

Andy me apontou na direção certa enquanto eu executava a função incorretamente. É por isso que nenhuma das outras respostas da pilha funcionou. Minha culpa.
Lee Grindon

2
possível duplicata Separa strings em SQL
Luv

Há uma mdq.RegexSplitfunção no complemento "Master Data Services", que pode ajudar. Certamente vale a pena investigar .
jpaugh

Respostas:


233

Eu usei esse SQL antes, o que pode funcionar para você: -

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE CHARINDEX(',', @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END

e para usá-lo: -

SELECT * FROM dbo.splitstring('91,12,65,78,56,789')

1
Nice one, este é exatamente o que eu estava procurando muito obrigado
Lee Grindon

2
Muito obrigado Andy. Fiz um pequeno aprimoramento no seu script para permitir que a função retorne um item em um índice específico na cadeia de caracteres de divisão. É útil apenas em situações em que a estrutura da coluna está analisando. gist.github.com/klimaye/8147193
CF_Maintainer

1
Publiquei algumas melhorias (com casos de teste de apoio) na minha página do github aqui . Vou publicá-la como uma resposta neste Stack Overflow fio quando tenho rep suficiente para ultrapassar a "proteção" pós
MPAG

8
Embora esta seja uma grande resposta, ele está desatualizado ... abordagens processuais (especialmente loops) são algo para evitar ... vale a pena olhar para as respostas mais recentes ...
Shnugo

2
Eu concordo totalmente com @Shnugo. Os divisores de loop funcionam mas terrivelmente lentos. Algo como este sqlservercentral.com/articles/Tally+Table/72993 é muito melhor. Algumas outras excelentes opções baseadas em conjuntos podem ser encontradas aqui. sqlperformance.com/2012/07/t-sql-queries/split-strings #
Sean Lange

61

Em vez de CTEs recursivas e enquanto loops, alguém considerou uma abordagem mais baseada em conjuntos? Observe que essa função foi escrita para a pergunta, baseada no SQL Server 2008 e vírgula como delimitador . No SQL Server 2016 e acima (e no nível de compatibilidade 130 e acima), STRING_SPLIT()é uma opção melhor .

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT [Value] FROM 
  ( 
    SELECT [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
      CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
    FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
      FROM sys.all_columns) AS x WHERE Number <= LEN(@List)
      AND SUBSTRING(@Delim + @List, [Number], DATALENGTH(@Delim)/2) = @Delim
    ) AS y
  );
GO

Se você quiser evitar que a limitação do comprimento da string seja <= o número de linhas sys.all_columns((9.980 modelno SQL Server 2017; muito maior nos seus próprios bancos de dados de usuários)), você pode usar outras abordagens para derivar os números, como construindo sua própria tabela de números . Você também pode usar uma CTE recursiva nos casos em que não pode usar tabelas do sistema ou criar suas próprias:

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
   RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 
       FROM n WHERE n <= LEN(@List))
       SELECT [Value] = SUBSTRING(@List, n, 
       CHARINDEX(@Delim, @List + @Delim, n) - n)
       FROM n WHERE n <= LEN(@List)
      AND SUBSTRING(@Delim + @List, n, DATALENGTH(@Delim)/2) = @Delim
   );
GO

Mas você precisará anexar OPTION (MAXRECURSION 0)(ou MAXRECURSION <longest possible string length if < 32768>) à consulta externa para evitar erros de recursão para cadeias> 100 caracteres. Se essa também não é uma boa alternativa, veja esta resposta conforme indicado nos comentários.

(Além disso, o delimitador precisará ser NCHAR(<=1228). Ainda pesquisando o porquê.)

Mais sobre funções de divisão, por que (e prova disso) enquanto loops e CTEs recursivos não são dimensionados, e melhores alternativas, se as seqüências de caracteres provenientes da camada de aplicação são divididas:


1
Há um pequeno erro neste procedimento para o caso em que haveria um valor nulo no final da string - como em '1,2,, 4,' - pois o valor final não é analisado. Para corrigir esse erro, a expressão "WHERE Number <= LEN (@List)" deve ser substituída por "WHERE Number <= LEN (@List) + 1".
SylvainL

@SylvainL Acho que depende do comportamento que você deseja. Na minha experiência, a maioria das pessoas deseja ignorar as vírgulas finais, pois elas realmente não representam um elemento real (quantas cópias de uma sequência em branco você precisa)? De qualquer forma, a maneira real de fazer isso - se você seguir o segundo link - é brincar com a divisão de grandes seqüências feias em T-SQL lento de qualquer maneira.
Aaron Bertrand

1
Como você disse, a maioria das pessoas deseja ignorar as vírgulas finais, mas infelizmente não todas. Suponho que uma solução mais completa seria adicionar um parâmetro para especificar o que fazer nesse caso, mas meu comentário é apenas uma pequena nota para garantir que ninguém esqueça essa possibilidade, pois pode ser bastante real em muitos casos.
precisa saber é o seguinte

Eu tenho um comportamento estranho com essa função. Se eu usar diretamente uma string como parâmetro - funciona. Se eu tenho um varchar, ele não possui. Você pode reproduzir facilmente: declarar invarchar como varchar set invarchar = 'ta; aa; qq' SELECT Valor de [dbo]. [SplitString] (invarchar, ';') SELECT Valor de [dbo]. [SplitString] ('ta; aa; qq ','; ')
Patrick Desjardins

Eu gosto dessa abordagem, mas se o número de objetos retornados por sys.all_objectsfor menor que o número de caracteres na string de entrada, ele truncará a string e os valores desaparecerão. Como sys.all_objectsestá sendo usado apenas como um hack para gerar linhas, existem maneiras melhores de fazer isso, por exemplo, esta resposta .
juntas

56

Finalmente, a espera terminou no SQL Server 2016 e eles introduziram a função String de divisão:STRING_SPLIT

select * From STRING_SPLIT ('a,b', ',') 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


5
obviamente responde à pergunta de como dividir a string para aqueles com servidores atualizados, mas ainda estamos presos em 2008 / 2008R2, e temos uma das outras respostas aqui.
mpag

2
Você precisa dar uma olhada no nível de compatibilidade no seu banco de dados. Se for menor que 130, você não poderá usar a função STRING_SPLIT.
Luis Teijon

Na verdade, se a compatibilidade não for 130 e você estiver executando o 2016 (ou SQL do Azure), poderá configurá-la até 130 usando: ALTER DATABASE DatabaseName SET COMPATIBILITY_LEVEL = 130
Michieal

23

A maneira mais fácil de fazer isso é usando o XMLformato.

1. Convertendo string em linhas sem tabela

INQUERIR

DECLARE @String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE @Delimiter CHAR = ','    

SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT CAST ('<M>' + REPLACE(@String, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

RESULTADO

 x---------x
 | Value   |
 x---------x
 | String1 |
 | String2 |
 | String3 |
 x---------x

2. Convertendo para linhas de uma tabela que possui um ID para cada linha CSV

TABELA DE FONTES

 x-----x--------------------------x
 | Id  |           Value          |
 x-----x--------------------------x
 |  1  |  String1,String2,String3 |
 |  2  |  String4,String5,String6 |     
 x-----x--------------------------x

INQUERIR

-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE @Delimiter CHAR = ','

SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT ID,CAST ('<M>' + REPLACE(VALUE, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
     FROM TABLENAME
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

RESULTADO

 x-----x----------x
 | Id  |  Value   |
 x-----x----------x
 |  1  |  String1 |
 |  1  |  String2 |  
 |  1  |  String3 |
 |  2  |  String4 |  
 |  2  |  String5 |
 |  2  |  String6 |     
 x-----x----------x

Essa abordagem será interrompida se @Stringcontiver caracteres proibidos ... Acabei de postar uma resposta para superar esse problema.
Shnugo 02/02

9

Eu precisava de uma maneira rápida de se livrar da +4a partir de um código postal .

UPDATE #Emails 
  SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1)) 
  WHERE ZIPCode LIKE '%-%'

No proc ... no UDF ... apenas um pequeno comando embutido que faz o que deve. Não é chique, não é elegante.

Mude o delimitador conforme necessário, etc, e ele funcionará para qualquer coisa.


4
Não é disso que se trata. O OP tem um valor como '234.542,23' e eles querem dividi-lo em três linhas ... 1ª linha: 234, 2ª linha: 542, 3ª linha: 23. É uma coisa complicada de se fazer no SQL.
codeulike

7

se você substituir

WHILE CHARINDEX(',', @stringToSplit) > 0

com

WHILE LEN(@stringToSplit) > 0

você pode eliminar a última inserção após o loop while!

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE LEN(@stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)


if @pos = 0
        SELECT @pos = LEN(@stringToSplit)


  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 RETURN
END

Isso resultaria no último caractere do último elemento sendo truncado. ou seja, "AL, AL" se tornaria "AL" | "A", ou seja, "ABC, ABC, ABC" se tornaria "ABC" | "Abc" "AB"
Microsoft Developer

anexar +1a SELECT @pos = LEN(@stringToSplit)aparece para resolver esse problema. No entanto, o valor SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)retornará, a Invalid length parameter passed to the LEFT or SUBSTRING functionmenos que você adicione +1ao terceiro parâmetro de SUBSTRING também. ou você pode substituir essa tarefa porSET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) --MAX len of nvarchar is 4000
mpag

1
Publiquei algumas melhorias (com casos de teste de apoio) na minha página do github aqui . Vou publicá-la como uma resposta neste Stack Overflow fio quando tenho rep suficiente para ultrapassar a "proteção" pós
MPAG

Também observei a questão apontada por Terry acima. Mas a lógica dada pelo @AviG é tão legal que não falha no meio de uma longa lista de tokens. Tente esta chamada de teste para verificar (esta chamada deve retornar 969 tokens) selecione * from dbo.splitstring ('token1, token2 ,,,,,,,, token969') Então tentei o código fornecido pelo mpag para verificar os resultados da mesma forma ligue acima e constate que ele pode retornar apenas 365 tokens. Finalmente, eu corrigi o código da AviG acima e postei a função livre de erros como uma nova resposta abaixo, já que o comentário aqui permite apenas texto limitado. Verifique a resposta em meu nome para tentar.
Gemunu R Wickremasinghe

3

Todas as funções para divisão de string que usam algum tipo de loop (iterações) apresentam desempenho ruim. Eles devem ser substituídos por uma solução baseada em conjunto.

Este código executa excelente.

CREATE FUNCTION dbo.SplitStrings
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO

Essa abordagem será interrompida se @Listcontiver caracteres proibidos ... Acabei de postar uma resposta para superar esse problema.
Shnugo 02/02

Estou upvoting sua resposta, porque o seu trabalho com espaço como delimitador e mais votado um não
KMC

3

A abordagem frequentemente usada com elementos XML é interrompida no caso de caracteres proibidos. Essa é uma abordagem para usar esse método com qualquer tipo de caractere, mesmo com o ponto e vírgula como delimitador.

O truque é, primeiro usar SELECT SomeString AS [*] FOR XML PATH('')para obter todos os caracteres proibidos adequadamente escapados. Essa é a razão pela qual substituo o delimitador por um valor mágico para evitar problemas ;como delimitador.

DECLARE @Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO @Dummy VALUES
 (1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');

DECLARE @Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!

WITH Casted AS
(
    SELECT *
          ,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,@Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
    FROM @Dummy
)
SELECT Casted.ID
      ,x.value(N'.',N'nvarchar(max)') AS Part 
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)

O resultado

ID  Part
1   A&B
1   C
1   D
1   E, F
2   "C" & 'D'
2   <C>
2   D
2   E, F

2

Eu tive que escrever algo assim recentemente. Aqui está a solução que eu criei. É generalizado para qualquer string delimitadora e acho que teria um desempenho um pouco melhor:

CREATE FUNCTION [dbo].[SplitString] 
    ( @string nvarchar(4000)
    , @delim nvarchar(100) )
RETURNS
    @result TABLE 
        ( [Value] nvarchar(4000) NOT NULL
        , [Index] int NOT NULL )
AS
BEGIN
    DECLARE @str nvarchar(4000)
          , @pos int 
          , @prv int = 1

    SELECT @pos = CHARINDEX(@delim, @string)
    WHILE @pos > 0
    BEGIN
        SELECT @str = SUBSTRING(@string, @prv, @pos - @prv)
        INSERT INTO @result SELECT @str, @prv

        SELECT @prv = @pos + LEN(@delim)
             , @pos = CHARINDEX(@delim, @string, @pos + 1)
    END

    INSERT INTO @result SELECT SUBSTRING(@string, @prv, 4000), @prv
    RETURN
END

1

Uma solução usando um CTE, se alguém precisar disso (além de mim, que obviamente fez, foi por isso que eu escrevi).

declare @StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare @SplitChar varchar(10) = ',';

with StringToSplit as (
  select 
      ltrim( rtrim( substring( @StringToSplit, 1, charindex( @SplitChar, @StringToSplit ) - 1 ) ) ) Head
    , substring( @StringToSplit, charindex( @SplitChar, @StringToSplit ) + 1, len( @StringToSplit ) ) Tail

  union all

  select
      ltrim( rtrim( substring( Tail, 1, charindex( @SplitChar, Tail ) - 1 ) ) ) Head
    , substring( Tail, charindex( @SplitChar, Tail ) + 1, len( Tail ) ) Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) > 0

  union all

  select
      ltrim( rtrim( Tail ) ) Head
    , '' Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) = 0
    and len( Tail ) > 0
)
select Head from StringToSplit

1

Isso é mais restrito. Quando faço isso, geralmente tenho uma lista delimitada por vírgula de IDs exclusivos (INT ou BIGINT), que quero converter como uma tabela para usar como uma junção interna a outra tabela que tenha uma chave primária de INT ou BIGINT. Desejo que uma função com valor de tabela in-line seja retornada para que eu tenha a junção mais eficiente possível.

O uso da amostra seria:

 DECLARE @IDs VARCHAR(1000);
 SET @IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
 SELECT me.Value
 FROM dbo.MyEnum me
 INNER JOIN dbo.GetIntIdsTableFromDelimitedString(@IDs) ids ON me.PrimaryKey = ids.ID

Eu roubei a ideia de http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html , alterando-a para ser valorizada em tabela e convertida como INT.

create function dbo.GetIntIDTableFromDelimitedString
    (
    @IDs VARCHAR(1000)  --this parameter must start and end with a comma, eg ',123,456,'
                        --all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
 RETURN

SELECT
    CAST(SUBSTRING(@IDs,Nums.number + 1,CHARINDEX(',',@IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID 
FROM   
     [master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P' 
AND    Nums.number BETWEEN 1 AND DATALENGTH(@IDs)
AND    SUBSTRING(@IDs,Nums.number,1) = ','
AND    CHARINDEX(',',@IDs,(Nums.number+1)) > Nums.number;

GO

1

Há uma versão correta aqui, mas achei que seria bom adicionar um pouco de tolerância a falhas caso eles tenham uma vírgula à direita, além de torná-la para que você possa usá-la não como uma função, mas como parte de um código maior . Apenas no caso de você usá-lo apenas uma vez e não precisar de uma função. Isso também é para números inteiros (que é o que eu precisava) para que você possa ter que alterar seus tipos de dados.

DECLARE @StringToSeperate VARCHAR(10)
SET @StringToSeperate = '1,2,5'

--SELECT @StringToSeperate IDs INTO #Test

DROP TABLE #IDs
CREATE TABLE #IDs (ID int) 

DECLARE @CommaSeperatedValue NVARCHAR(255) = ''
DECLARE @Position INT = LEN(@StringToSeperate)

--Add Each Value
WHILE CHARINDEX(',', @StringToSeperate) > 0
BEGIN
    SELECT @Position  = CHARINDEX(',', @StringToSeperate)  
    SELECT @CommaSeperatedValue = SUBSTRING(@StringToSeperate, 1, @Position-1)

    INSERT INTO #IDs 
    SELECT @CommaSeperatedValue

    SELECT @StringToSeperate = SUBSTRING(@StringToSeperate, @Position+1, LEN(@StringToSeperate)-@Position)

END

--Add Last Value
IF (LEN(LTRIM(RTRIM(@StringToSeperate)))>0)
BEGIN
    INSERT INTO #IDs
    SELECT SUBSTRING(@StringToSeperate, 1, @Position)
END

SELECT * FROM #IDs

se você fosse SET @StringToSeperate = @StringToSeperate+','imediatamente antes do WHILEloop, acho que você poderá eliminar o bloco "adicionar último valor". Veja também meu sol'n no github
mpag

Em que resposta se baseia? Há muitas respostas aqui, e é um pouco confuso. Obrigado.
jpaugh

1

Modifiquei um pouco a função de Andy Robinson. Agora você pode selecionar apenas a parte necessária da tabela de retorno:

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )

RETURNS

 @returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN

 DECLARE @name NVARCHAR(255)

 DECLARE @pos INT

 DECLARE @orderNum INT

 SET @orderNum=0

 WHILE CHARINDEX('.', @stringToSplit) > 0

 BEGIN
    SELECT @orderNum=@orderNum+1;
  SELECT @pos  = CHARINDEX('.', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @orderNum,@name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END
    SELECT @orderNum=@orderNum+1;
 INSERT INTO @returnList
 SELECT @orderNum, @stringToSplit

 RETURN
END


Usage:

SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5


1

Se você precisar de uma solução ad-hoc rápida para casos comuns com código mínimo, essa linha recursiva CTE de duas linhas fará isso:

DECLARE @s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'

;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', @s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b

Use isso como uma declaração autônoma ou apenas adicione os CTEs acima a qualquer uma das suas consultas e poderá ingressar na tabela resultante bcom outras pessoas para usar em outras expressões.

editar (por Shnugo)

Se você adicionar um contador, receberá um índice de posição junto com a Lista:

DECLARE @s VARCHAR(200) = '1,2333,344,4'

;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', @s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;

O resultado:

n   s
1   1
2   2333
3   344
4   4

Eu gosto dessa abordagem. Espero que você não se importe, porque eu adicionei algumas melhorias diretamente na sua resposta. Apenas sinta-se livre para editar esta em qualquer forma conveniente ...
Shnugo

1

Eu pego a rota xml envolvendo os valores em elementos (M, mas tudo funciona):

declare @v nvarchar(max) = '100,201,abcde'

select 
    a.value('.', 'varchar(max)')
from
    (select cast('<M>' + REPLACE(@v, ',', '</M><M>') + '</M>' AS XML) as col) as A
    CROSS APPLY A.col.nodes ('/M') AS Split(a)

0

Aqui está uma versão que pode ser dividida em um padrão usando patindex, uma simples adaptação da postagem acima. Eu tive um caso em que precisava dividir uma string que continha vários caracteres separadores.


alter FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(1000), @splitPattern varchar(10) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE PATINDEX(@splitPattern, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = PATINDEX(@splitPattern, @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');

resultado se parece com isso

stringa stringb x y z


0

Pessoalmente, uso esta função:

ALTER FUNCTION [dbo].[CUST_SplitString]
(
    @String NVARCHAR(4000),
    @Delimiter NCHAR(1)
)
RETURNS TABLE 
AS
RETURN 
(
    WITH Split(stpos,endpos) 
    AS(
        SELECT 0 AS stpos, CHARINDEX(@Delimiter,@String) AS endpos
        UNION ALL
        SELECT endpos+1, CHARINDEX(@Delimiter,@String,endpos+1) 
        FROM Split
        WHERE endpos > 0
    )
    SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
        'Data' = SUBSTRING(@String,stpos,COALESCE(NULLIF(endpos,0),LEN(@String)+1)-stpos)
    FROM Split
)

0

Eu desenvolvi um divisor duplo (ocupa dois caracteres divididos) conforme solicitado aqui . Pode ser de algum valor nesse segmento, já que é o mais referenciado para consultas relacionadas à divisão de cadeias.

CREATE FUNCTION uft_DoubleSplitter 
(   
    -- Add the parameters for the function here
    @String VARCHAR(4000), 
    @Splitter1 CHAR,
    @Splitter2 CHAR
)
RETURNS @Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE @FResult TABLE(Id INT IDENTITY(1, 1),
                   SValue VARCHAR(4000))
DECLARE @SResult TABLE(Id INT IDENTITY(1, 1),
                   MId INT,
                   SValue VARCHAR(4000))
SET @String = @String+@Splitter1

WHILE CHARINDEX(@Splitter1, @String) > 0
    BEGIN
       DECLARE @WorkingString VARCHAR(4000) = NULL

       SET @WorkingString = SUBSTRING(@String, 1, CHARINDEX(@Splitter1, @String) - 1)
       --Print @workingString

       INSERT INTO @FResult
       SELECT CASE
            WHEN @WorkingString = '' THEN NULL
            ELSE @WorkingString
            END

       SET @String = SUBSTRING(@String, LEN(@WorkingString) + 2, LEN(@String))

    END
IF ISNULL(@Splitter2, '') != ''
    BEGIN
       DECLARE @OStartLoop INT
       DECLARE @OEndLoop INT

       SELECT @OStartLoop = MIN(Id),
            @OEndLoop = MAX(Id)
       FROM @FResult

       WHILE @OStartLoop <= @OEndLoop
          BEGIN
             DECLARE @iString VARCHAR(4000)
             DECLARE @iMId INT

             SELECT @iString = SValue+@Splitter2,
                   @iMId = Id
             FROM @FResult
             WHERE Id = @OStartLoop

             WHILE CHARINDEX(@Splitter2, @iString) > 0
                BEGIN
                    DECLARE @iWorkingString VARCHAR(4000) = NULL

                    SET @IWorkingString = SUBSTRING(@iString, 1, CHARINDEX(@Splitter2, @iString) - 1)

                    INSERT INTO @SResult
                    SELECT @iMId,
                         CASE
                         WHEN @iWorkingString = '' THEN NULL
                         ELSE @iWorkingString
                         END

                    SET @iString = SUBSTRING(@iString, LEN(@iWorkingString) + 2, LEN(@iString))

                END

             SET @OStartLoop = @OStartLoop + 1
          END
       INSERT INTO @Result
       SELECT MId AS PrimarySplitID,
            ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
            SValue
       FROM @SResult
    END
ELSE
    BEGIN
       INSERT INTO @Result
       SELECT Id AS PrimarySplitID,
            NULL AS SecondarySplitID,
            SValue
       FROM @FResult
    END
RETURN

Uso:

--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)

--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')

Uso possível (obtenha o segundo valor de cada divisão):

SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2

0

Aqui está um exemplo que você pode usar como função ou também pode colocar a mesma lógica no procedimento. --SELECT * de [dbo] .fn_SplitString;

CREATE FUNCTION [dbo].[fn_SplitString]
(@CSV VARCHAR(MAX), @Delimeter VARCHAR(100) = ',')
       RETURNS @retTable TABLE 
(

    [value] VARCHAR(MAX) NULL
)AS

BEGIN

DECLARE
       @vCSV VARCHAR (MAX) = @CSV,
       @vDelimeter VARCHAR (100) = @Delimeter;

IF @vDelimeter = ';'
BEGIN
    SET @vCSV = REPLACE(@vCSV, ';', '~!~#~');
    SET @vDelimeter = REPLACE(@vDelimeter, ';', '~!~#~');
END;

SET @vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(@vCSV, '&', '&amp;'), '<', '&lt;'), '>', '&gt;'), '''', '&apos;'), '"', '&quot;');

DECLARE @xml XML;

SET @xml = '<i>' + REPLACE(@vCSV, @vDelimeter, '</i><i>') + '</i>';

INSERT INTO @retTable
SELECT
       x.i.value('.', 'varchar(max)') AS COLUMNNAME
  FROM @xml.nodes('//i')AS x(i);

 RETURN;
END;

Essa abordagem será interrompida se @vCSVcontiver caracteres proibidos ... Acabei de postar uma resposta para superar esse problema.
Shnugo 02/02

0

Uma solução recursiva baseada em cte

declare @T table (iden int identity, col1 varchar(100));
insert into @T(col1) values
       ('ROOT/South America/Lima/Test/Test2')
     , ('ROOT/South America/Peru/Test/Test2')
     , ('ROOT//South America/Venuzuala ')
     , ('RtT/South America / ') 
     , ('ROOT/South Americas// '); 
declare @split char(1) = '/';
select @split as split;
with cte as 
(  select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = @split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + @split end  as col1, 0 as pos                             , 1 as cnt
   from @T t
   union all 
   select t.iden, t.col1                                                                                                                              , charindex(@split, t.col1, t.pos + 1), cnt + 1 
   from cte t 
   where charindex(@split, t.col1, t.pos + 1) > 0 
)
select t1.*, t2.pos, t2.cnt
     , ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1 
join cte t2 
  on t2.iden = t1.iden 
 and t2.cnt  = t1.cnt+1
 and t2.pos > t1.pos 
order by t1.iden, t1.cnt;

0

Isso é baseado na resposta de Andy Robertson, eu precisava de um delimitador que não fosse vírgula.

CREATE FUNCTION dbo.splitstring ( @stringToSplit nvarchar(MAX), @delim nvarchar(max))
RETURNS
 @returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN

 DECLARE @value NVARCHAR(max)
 DECLARE @pos INT

 WHILE CHARINDEX(@delim, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(@delim, @stringToSplit)  
  SELECT @value = SUBSTRING(@stringToSplit, 1, @pos - 1)

  INSERT INTO @returnList 
  SELECT @value

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos + LEN(@delim), LEN(@stringToSplit) - @pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
GO

E para usá-lo:

SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');

(Testado no SQL Server 2008 R2)

EDIT: código de teste correto


0

/ *

Resposta à seqüência de caracteres dividida T-SQL
Com base nas respostas de Andy Robinson e AviG
Funcionalidade aprimorada ref: função LEN que não inclui espaços à direita no SQL Server
Esse 'arquivo' deve ser válido como um arquivo de remarcação e um arquivo SQL


*/

    CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
        @stringToSplit NVARCHAR(MAX)
    ) RETURNS @returnList TABLE ([Item] NVARCHAR (MAX))
    AS BEGIN
        DECLARE @name NVARCHAR(MAX)
        DECLARE @pos BIGINT
        SET @stringToSplit = @stringToSplit + ','             -- this should allow entries that end with a `,` to have a blank value in that "column"
        WHILE ((LEN(@stringToSplit+'_') > 1)) BEGIN           -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
            SET @pos = COALESCE(NULLIF(CHARINDEX(',', @stringToSplit),0),LEN(@stringToSplit+'_')) -- COALESCE grabs first non-null value
            SET @name = SUBSTRING(@stringToSplit, 1, @pos-1)  --MAX size of string of type nvarchar is 4000 
            SET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
            INSERT INTO @returnList SELECT @name --additional debugging parameters below can be added
            -- + ' pos:' + CAST(@pos as nvarchar) + ' remain:''' + @stringToSplit + '''(' + CAST(LEN(@stringToSplit+'_')-1 as nvarchar) + ')'
        END
        RETURN
    END
    GO

/*

Casos de teste: veja o URL mencionado como "funcionalidade aprimorada" acima

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')

Item | L
---  | ---
a    | 1
     | 0
b    | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')

Item | L   
---  | ---
a    | 1
     | 0
     | 0

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')

Item | L   
---  | ---
a    | 1
     | 0
     | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')

Item | L   
---  | ---
a    | 1
     | 0
 c   | 3

* /


volta rolou para honra "Este 'file' deve ser válido tanto como um arquivo de remarcação e um arquivo de SQL"
MPAG

-1
ALTER FUNCTION [dbo].func_split_string
(
    @input as varchar(max),
    @delimiter as varchar(10) = ";"

)
RETURNS @result TABLE
(
    id smallint identity(1,1),
    csv_value varchar(max) not null
)
AS
BEGIN
    DECLARE @pos AS INT;
    DECLARE @string AS VARCHAR(MAX) = '';

    WHILE LEN(@input) > 0
    BEGIN           
        SELECT @pos = CHARINDEX(@delimiter,@input);

        IF(@pos<=0)
            select @pos = len(@input)

        IF(@pos <> LEN(@input))
            SELECT @string = SUBSTRING(@input, 1, @pos-1);
        ELSE
            SELECT @string = SUBSTRING(@input, 1, @pos);

        INSERT INTO @result SELECT @string

        SELECT @input = SUBSTRING(@input, @pos+len(@delimiter), LEN(@input)-@pos)       
    END
    RETURN  
END

-1

Você pode usar esta função:

        CREATE FUNCTION SplitString
        (    
           @Input NVARCHAR(MAX),
           @Character CHAR(1)
          )
            RETURNS @Output TABLE (
            Item NVARCHAR(1000)
          )
        AS
        BEGIN

      DECLARE @StartIndex INT, @EndIndex INT
      SET @StartIndex = 1
      IF SUBSTRING(@Input, LEN(@Input) - 1, LEN(@Input)) <> @Character
      BEGIN
            SET @Input = @Input + @Character
      END

      WHILE CHARINDEX(@Character, @Input) > 0
      BEGIN
            SET @EndIndex = CHARINDEX(@Character, @Input)

            INSERT INTO @Output(Item)
            SELECT SUBSTRING(@Input, @StartIndex, @EndIndex - 1)

            SET @Input = SUBSTRING(@Input, @EndIndex + 1, LEN(@Input))
      END

      RETURN
END
GO

-1

Com todo o respeito ao @AviG, esta é a versão sem erros da função criada por ele para retornar todos os tokens na íntegra.

IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO

-- =============================================
-- Author:  AviG
-- Amendments:  Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from   [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from   [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString 
( @stringToSplit VARCHAR(MAX) ,
  @delimeter char = ','
)
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

    DECLARE @name NVARCHAR(255)
    DECLARE @pos INT

    WHILE LEN(@stringToSplit) > 0
    BEGIN
        SELECT @pos  = CHARINDEX(@delimeter, @stringToSplit)


        if @pos = 0
        BEGIN
            SELECT @pos = LEN(@stringToSplit)
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos)  
        END
        else 
        BEGIN
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)
        END

        INSERT INTO @returnList 
        SELECT @name

        SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
    END

 RETURN
END

-3

A maneira mais fácil:

  1. Instale o SQL Server 2016
  2. Use STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx

Funciona mesmo em edição expressa :).


Não se esqueça de definir "Nível de compatibilidade" como SQL Server 2016 (130) - no estúdio de gerenciamento, clique com o botão direito no banco de dados, propriedades / opções / nível de compatibilidade.
Tomino

1
A postagem original dizia para SQL 2008 R2. Instalar SQL 2016 podem não ser uma opção
Shawn Gavett
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.