Como faço para dividir uma string para acessar o item x?

493

Usando o SQL Server, como faço para dividir uma sequência para acessar o item x?

Pegue uma string "Olá John Smith". Como posso dividir a string por espaço e acessar o item no índice 1, que deve retornar "John"?

GateKiller
fonte
3
Veja stackoverflow.com/questions/314824/… também
Jarrod Dixon
5
built-in a partir das sql server 2016 msdn.microsoft.com/en-us/library/mt684588.aspx
Tim Abell
4
As respostas mais altas aqui são - pelo menos para mim - bastante antiquadas e desatualizadas. Localização processual, loops, recursões, CLR, funções, muitas linhas de código ... Pode ser interessante ler as respostas "ativas" para encontrar abordagens mais atualizadas .
Shnugo 12/07/16
Eu adicionei uma nova resposta com abordagem mais atualizada up-: stackoverflow.com/a/49669994/632604
Gorgi Rankovski
Tente Obter o enésimo elemento de uma lista -> portosql.wordpress.com/2019/05/27/enesimo-elemento-lista
José Diz

Respostas:

191

Você pode encontrar a solução na Função Definida pelo Usuário do SQL para Analisar uma Cadeia de caracteres Delimitada útil (no The Code Project ).

Você pode usar esta lógica simples:

Declare @products varchar(200) = '1|20|3|343|44|6|8765'
Declare @individual varchar(20) = null

WHILE LEN(@products) > 0
BEGIN
    IF PATINDEX('%|%', @products) > 0
    BEGIN
        SET @individual = SUBSTRING(@products,
                                    0,
                                    PATINDEX('%|%', @products))
        SELECT @individual

        SET @products = SUBSTRING(@products,
                                  LEN(@individual + '|') + 1,
                                  LEN(@products))
    END
    ELSE
    BEGIN
        SET @individual = @products
        SET @products = NULL
        SELECT @individual
    END
END
Jonesinator
fonte
1
porque SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( RTRIM( LTRIM( @p_SourceText)))e não SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( @p_SourceText)?
Beth
12
@GateKiller Esta solução não suporta Unicode e usa código numérico codificado (18,3), o que não a torna uma função "reutilizável" viável.
Filip De Vos
4
Isso funciona, mas aloca muita memória e desperdiça CPU.
Jjxtra # 26/15
2
A partir do SQL Server 2016, agora existe uma função interna STRING_SPLITque dividirá uma sequência e retornará um resultado da tabela de uma coluna que você pode usar em uma SELECTinstrução ou em outro local.
precisa
Pena que os caras para quem trabalho não estejam em 2016. Mas vou lembrar disso caso eles tirem a vantagem de seus sapatos. Ótima solução nesse ínterim. Eu o implementei como uma função e adicionei delimitador como argumento.
Brandon Griffin
355

Não acredito que o SQL Server tenha uma função de divisão interna; portanto, além de uma UDF, a única outra resposta que eu sei é seqüestrar a função PARSENAME:

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 2) 

PARSENAME pega uma string e a divide no caractere de ponto. Ele usa um número como segundo argumento e esse número especifica qual segmento da cadeia de caracteres retornar (trabalhando de trás para frente).

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 3)  --return Hello

O problema óbvio é quando a string já contém um ponto. Ainda acho que usar uma UDF é a melhor maneira ... outras sugestões?

Nathan Bedford
fonte
102
Obrigado Saul ... Devo ressaltar que esta solução é realmente uma solução ruim para o desenvolvimento real. PARSENAME espera apenas quatro partes; portanto, o uso de uma seqüência de caracteres com mais de quatro partes faz com que retorne NULL. As soluções UDF são obviamente melhores.
Nathan Bedford
33
Este é um ótimo truque e também me faz chorar que algo assim seja necessário para algo tão simples em idiomas reais.
Factor Mystic
36
Para fazer com que os índices funcionem da maneira "correta", ou seja, a partir de 1, sequestrei seu seqüestro com REVERSE: REVERSE (PARSENAME (REPLACE (REVERSE ('Hello John Smith'), '', '.') (1)) - Retorna Hello
NothingsImpossible
3
O primeiro formulário normal do @FactorMystic requer que você não coloque vários valores em um único campo. É literalmente a primeira regra de um RDBMS. Uma SPLIT()função não é fornecida porque incentiva um design de banco de dados ruim, e o banco de dados nunca será otimizado para usar os dados armazenados nesse formato. O RDBMS não é obrigado a ajudar os desenvolvedores a fazer coisas estúpidas que foram projetadas para não manipular. A resposta correta será sempre "Normalize seu banco de dados como dissemos a você 40 anos atrás". Nem o SQL nem o RDBMS são responsáveis ​​pelo design inadequado.
Bacon Bits
8
@BaconBits, embora eu concorde em teoria, na prática, ferramentas como essa são úteis para normalizar um design ruim produzido por alguém que veio antes de você.
Tim Abell
110

Primeiro, crie uma função (usando CTE, a expressão de tabela comum elimina a necessidade de uma tabela temporária)

 create function dbo.SplitString 
    (
        @str nvarchar(4000), 
        @separator char(1)
    )
    returns table
    AS
    return (
        with tokens(p, a, b) AS (
            select 
                1, 
                1, 
                charindex(@separator, @str)
            union all
            select
                p + 1, 
                b + 1, 
                charindex(@separator, @str, b + 1)
            from tokens
            where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
      )
    GO

Em seguida, use-o como qualquer tabela (ou modifique-o para caber no seu proc armazenado existente) como este.

select s 
from dbo.SplitString('Hello John Smith', ' ')
where zeroBasedOccurance=1

Atualizar

A versão anterior falharia na string de entrada com mais de 4000 caracteres. Esta versão cuida da limitação:

create function dbo.SplitString 
(
    @str nvarchar(max), 
    @separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
    select 
        cast(1 as bigint), 
        cast(1 as bigint), 
        charindex(@separator, @str)
    union all
    select
        p + 1, 
        b + 1, 
        charindex(@separator, @str, b + 1)
    from tokens
    where b > 0
)
select
    p-1 ItemIndex,
    substring(
        @str, 
        a, 
        case when b > 0 then b-a ELSE LEN(@str) end) 
    AS s
from tokens
);

GO

O uso permanece o mesmo.

vzczc
fonte
14
É elegante, mas só funciona para 100 elementos devido ao limite de profundidade da recursão.
Pking
4
@Pking, não, o padrão é 100(para evitar loop infinito). Use a dica MAXRECURSION para definir o número de níveis de recursão ( 0para 32767, 0é "sem limite" - pode esmagar o servidor). Entre, resposta muito melhor do que PARSENAME, porque é universal :-). +1
Michał Powaga 14/03/2013
Adicionando maxrecursiona esta solução, lembre-se desta pergunta e de suas respostas Como configurar a maxrecursionopção para um CTE dentro de uma função com valor de tabela .
Michał Powaga 15/03
Especificamente, faça referência à resposta de Crisfole - seu método diminui um pouco, mas é mais simples do que a maioria das outras opções.
AHiggins
ponto secundário, mas o uso não permanece o mesmo porque você alterou o nome da coluna, portanto snão está mais definido
Tim Abell
62

A maioria das soluções aqui são usadas enquanto loops ou CTEs recursivas. Uma abordagem baseada em conjunto será superior, prometo, se você puder usar um delimitador que não seja um espaço:

CREATE FUNCTION [dbo].[SplitString]
    (
        @List NVARCHAR(MAX),
        @Delim VARCHAR(255)
    )
    RETURNS TABLE
    AS
        RETURN ( SELECT [Value], idx = RANK() OVER (ORDER BY n) FROM 
          ( 
            SELECT n = Number, 
              [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
              CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
            FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
              FROM sys.all_objects) AS x
              WHERE Number <= LEN(@List)
              AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim)) = @Delim
          ) AS y
        );

Uso da amostra:

SELECT Value FROM dbo.SplitString('foo,bar,blat,foo,splunge',',')
  WHERE idx = 3;

Resultados:

----
blat

Você também pode adicionar o idxargumento desejado à função, mas deixarei isso como um exercício para o leitor.

Você não pode fazer isso apenas com a função nativaSTRING_SPLIT adicionada no SQL Server 2016, porque não há garantia de que a saída será renderizada na ordem da lista original. Em outras palavras, se você passar 3,6,1o resultado, provavelmente estará nessa ordem, mas poderia estar 1,3,6. Pedi a ajuda da comunidade para melhorar a função incorporada aqui:

Com bastante feedback qualitativo , eles podem considerar fazer algumas dessas melhorias:

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:

No SQL Server 2016 ou superior, no entanto, você deve observar STRING_SPLIT()e STRING_AGG():

Aaron Bertrand
fonte
1
Melhor resposta, IMHO. Em algumas das outras respostas, há a questão do limite de recursão do SQL de 100, mas não neste caso. Implementação muito rápida e muito simples. Onde está o botão +2?
T-MOTY
5
Eu tentei essa função na íntegra com o uso: select * from DBO.SplitString('Hello John smith', ' ');e a saída produzida foi: Valor Olá ello llo lo o John ohn hn n smith mito om th h
wwmbes
2
@AaronBertrand O problema original publicado pelo GateKiller envolve um delimitador de espaço.
Wwmbes 26/10/16
1
@ user1255933 Endereçado.
Aaron Bertrand
1
@ Michael Sim, isso é verdade. Você também não teria uma tabela para selecionar se não tivesse a permissão ALTER SCHEMA e não conseguiria selecioná-la se não tiver a permissão SELECT. Você sempre pode pedir a alguém para criar a função para você . Ou crie-o em algum lugar que você possa criá-lo (mesmo que temporariamente, digamos em tempdb). E no ano de 2016 ou mais, você deve usar STRING_SPLIT () e não uma função que precisa criar para si mesmo.
Aaron Bertrand
38

Você pode aproveitar uma tabela Number para fazer a análise de string.

Crie uma tabela de números físicos:

    create table dbo.Numbers (N int primary key);
    insert into dbo.Numbers
        select top 1000 row_number() over(order by number) from master..spt_values
    go

Criar tabela de teste com 1000000 linhas

    create table #yak (i int identity(1,1) primary key, array varchar(50))

    insert into #yak(array)
        select 'a,b,c' from dbo.Numbers n cross join dbo.Numbers nn
    go

Crie a função

    create function [dbo].[ufn_ParseArray]
        (   @Input      nvarchar(4000), 
            @Delimiter  char(1) = ',',
            @BaseIdent  int
        )
    returns table as
    return  
        (   select  row_number() over (order by n asc) + (@BaseIdent - 1) [i],
                    substring(@Input, n, charindex(@Delimiter, @Input + @Delimiter, n) - n) s
            from    dbo.Numbers
            where   n <= convert(int, len(@Input)) and
                    substring(@Delimiter + @Input, n, 1) = @Delimiter
        )
    go

Uso (gera linhas de 3mil em 40s no meu laptop)

    select * 
    from #yak 
    cross apply dbo.ufn_ParseArray(array, ',', 1)

Limpar

    drop table dbo.Numbers;
    drop function  [dbo].[ufn_ParseArray]

O desempenho aqui não é incrível, mas chamar uma função em uma tabela de um milhão de linhas não é a melhor idéia. Se estiver executando uma string dividida em muitas linhas, eu evitaria a função.

Nathan Skerl
fonte
2
A melhor solução IMO, os outros têm algum tipo de limitação .. isso é rápido e pode analisar longas seqüências de caracteres com muitos elementos.
Pking
Por que você pede n descendente? Se havia três itens e começamos a numerar em 1, o primeiro item será o número 3 e o último será o número 1. Não daria resultados mais intuitivos se eles descfossem removidos?
machado - feito com SOverflow 28/10
1
Concordado, seria mais intuitivo na direção asc. Eu estava seguindo a convenção parsename () que usa desc #
Nathan Skerl
3
alguma explicação de como funciona este seria ótimo
Tim Abell
Em um teste em 100 milhões de linhas de até 3 campos para analisar, o ufn_ParseArray não foi concluído após 25 minutos, enquanto o REVERSE(PARSENAME(REPLACE(REVERSE('Hello John Smith'), ' ', '.'), 1)) @NothingsImpossible foi concluído em 1,5 minutos . @hello_earth Como sua solução se compara em seqüências mais longas com mais de 4 campos?
Wwmbes 28/10/16
32

Esta questão não é sobre uma abordagem de divisão de cadeias , mas sobre como obter o enésimo elemento .

Todas as respostas aqui estão fazendo algum tipo de divisão string usando recursão, CTEs, múltipla CHARINDEX, REVERSEe PATINDEX, funções inventando, chamada de métodos CLR, tabelas de números, CROSS APPLYé ... A maioria das respostas cobrir muitas linhas de código.

Mas - se você realmente deseja nada além de uma abordagem para obter o enésimo elemento - isso pode ser feito como uma única linha , sem UDF, nem mesmo como uma sub-seleção ... E como um benefício extra: digite safe

Obtenha a parte 2 delimitada por um espaço:

DECLARE @input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'<x>' + REPLACE(@input,N' ',N'</x><x>') + N'</x>' AS XML).value('/x[2]','nvarchar(max)')

Obviamente, você pode usar variáveis para delimitador e posição (use sql:columnpara recuperar a posição diretamente do valor de uma consulta):

DECLARE @dlmt NVARCHAR(10)=N' ';
DECLARE @pos INT = 2;
SELECT CAST(N'<x>' + REPLACE(@input,@dlmt,N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)')

Se a sua sequência incluir caracteres proibidos (especialmente um entre eles &><), você ainda poderá fazê-lo dessa maneira. Basta usar FOR XML PATHsua string primeiro para substituir todos os caracteres proibidos pela seqüência de escape apropriada.

É um caso muito especial se - além disso - seu delimitador for o ponto e vírgula . Nesse caso, substituo o delimitador primeiro por '# DLMT #' e substituo-o pelas tags XML finalmente:

SET @input=N'Some <, > and &;Other äöü@€;One more';
SET @dlmt=N';';
SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(@input,@dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)');

ATUALIZAÇÃO para SQL-Server 2016 ou superior

Lamentavelmente, os desenvolvedores esqueceram de retornar o índice da peça STRING_SPLIT. Mas, usando o SQL-Server 2016+, há JSON_VALUEe OPENJSON.

Com JSON_VALUEpodemos passar na posição como o array do índice.

Para OPENJSONa documentação afirma claramente:

Quando OPENJSON analisa uma matriz JSON, a função retorna os índices dos elementos no texto JSON como chaves.

A string como 1,2,3necessidades nada mais do que colchetes: [1,2,3].
Uma série de palavras como this is an exampleprecisa ser ["this","is","an","example"].
Essas são operações de cadeia muito fáceis. Apenas tente:

DECLARE @str VARCHAR(100)='Hello John Smith';
DECLARE @position INT = 2;

--We can build the json-path '$[1]' using CONCAT
SELECT JSON_VALUE('["' + REPLACE(@str,' ','","') + '"]',CONCAT('$[',@position-1,']'));

--Veja isto para um separador de cadeia de posição seguro ( baseado em zero ):

SELECT  JsonArray.[key] AS [Position]
       ,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(@str,' ','","') + '"]') JsonArray

Em este post eu testei várias abordagens e encontrado, que OPENJSONé muito rápido. Até muito mais rápido que o famoso método "delimitedSplit8k ()" ...

ATUALIZAÇÃO 2 - Obtenha os valores seguros para o tipo

Podemos usar uma matriz dentro de uma matriz simplesmente usando o dobro [[]]. Isso permite uma WITHcláusula digitada :

DECLARE  @SomeDelimitedString VARCHAR(100)='part1|1|20190920';

DECLARE @JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(@SomeDelimitedString,'|','","'),'"]]');

SELECT @SomeDelimitedString          AS TheOriginal
      ,@JsonArray                    AS TransformedToJSON
      ,ValuesFromTheArray.*
FROM OPENJSON(@JsonArray)
WITH(TheFirstFragment  VARCHAR(100) '$[0]'
    ,TheSecondFragment INT          '$[1]'
    ,TheThirdFragment  DATE         '$[2]') ValuesFromTheArray
Shnugo
fonte
Re: se sua string pode incluir caracteres proibidos ... você pode simplesmente envolver as substrings dessa forma <x><![CDATA[x<&>x]]></x>.
Salman A
@SalmanA, sim, as CDATAseções também podem lidar com isso ... Mas depois do elenco elas desaparecem (alteradas para escapadas text()implicitamente). Eu não gosto de mágica sob o capô , então prefiro a (SELECT 'Text with <&>' AS [*] FOR XML PATH(''))abordagem. Isso parece mais limpo para mim e acontece de qualquer maneira ... (Um pouco mais sobre CDATA e XML ).
Shnugo 28/01/19
22

Aqui está uma UDF que fará isso. Ele retornará uma tabela com os valores delimitados, ainda não experimentou todos os cenários, mas seu exemplo funciona bem.


CREATE FUNCTION SplitString 
(
    -- Add the parameters for the function here
    @myString varchar(500),
    @deliminator varchar(10)
)
RETURNS 
@ReturnTable TABLE 
(
    -- Add the column definitions for the TABLE variable here
    [id] [int] IDENTITY(1,1) NOT NULL,
    [part] [varchar](50) NULL
)
AS
BEGIN
        Declare @iSpaces int
        Declare @part varchar(50)

        --initialize spaces
        Select @iSpaces = charindex(@deliminator,@myString,0)
        While @iSpaces > 0

        Begin
            Select @part = substring(@myString,0,charindex(@deliminator,@myString,0))

            Insert Into @ReturnTable(part)
            Select @part

    Select @myString = substring(@mystring,charindex(@deliminator,@myString,0)+ len(@deliminator),len(@myString) - charindex(' ',@myString,0))


            Select @iSpaces = charindex(@deliminator,@myString,0)
        end

        If len(@myString) > 0
            Insert Into @ReturnTable
            Select @myString

    RETURN 
END
GO

Você poderia chamar assim:


Select * From SplitString('Hello John Smith',' ')

Edit: Solução atualizada para manipular delimters com len> 1 como em:


select * From SplitString('Hello**John**Smith','**')
brendan
fonte
Não funcionou para selecionar * de dbo.ethos_SplitString_fn ('cara, mechas, estava aqui', ',') parte da identificação ----------- ------------ -------------------------------------- 1 cara 2 pavio
Guy
2
cuidado com len (), uma vez que não vai voltar número correto se seu argumento tem espaços à direita, por exemplo, len ( '-'). = 2.
Rory
Não funciona em: select * from dbo.SplitString ('foo, foo test ,,,, foo', ',')
cbp
1
Correção para o CBP .. Select @myString = substring (@ mystring, @ iSpaces + len (@deliminator), len (@myString) - charindex (@ deliminator, @ myString, 0))
Alxwest
16

Aqui eu posto uma maneira simples de solução

CREATE FUNCTION [dbo].[split](
          @delimited NVARCHAR(MAX),
          @delimiter NVARCHAR(100)
        ) RETURNS @t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX))
        AS
        BEGIN
          DECLARE @xml XML
          SET @xml = N'<t>' + REPLACE(@delimited,@delimiter,'</t><t>') + '</t>'

          INSERT INTO @t(val)
          SELECT  r.value('.','varchar(MAX)') as item
          FROM  @xml.nodes('/t') as records(r)
          RETURN
        END


Execute a função como esta

  select * from dbo.split('Hello John Smith',' ')
Sivaganesh Tamilvendhan
fonte
Gostei desta solução. Expandiu para retornar um valor escalar com base na coluna especificada nos resultados.
22713 Alan Alan
Eu fui queimado com um '&' na string a ser dividida usando isso #
KeithL 13/11/18
10

Na minha opinião, vocês estão tornando as coisas muito complicadas. Basta criar um CLR UDF e pronto.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.Generic;

public partial class UserDefinedFunctions {
  [SqlFunction]
  public static SqlString SearchString(string Search) {
    List<string> SearchWords = new List<string>();
    foreach (string s in Search.Split(new char[] { ' ' })) {
      if (!s.ToLower().Equals("or") && !s.ToLower().Equals("and")) {
        SearchWords.Add(s);
      }
    }

    return new SqlString(string.Join(" OR ", SearchWords.ToArray()));
  }
};
Damon Drake
fonte
20
Acho que isso é muito complicado, porque preciso ter o Visual Studio, habilitar o CLR no servidor, criar e compilar o projeto e, finalmente, adicionar os assemblies ao banco de dados para usá-lo. Mas ainda é uma resposta interessante.
Guillermo Gutiérrez
3
@ guillegr123, não precisa ser complicado. Você pode apenas baixar e instalar (de graça!), SQL #, que é uma biblioteca de funções e procs do SQLCLR. Você pode obtê-lo no SQLsharp.com . Sim, sou o autor, mas o String_Split está incluído na versão gratuita.
Solomon Rutzky
10

Que tal usar stringe values()declaração?

DECLARE @str varchar(max)
SET @str = 'Hello John Smith'

DECLARE @separator varchar(max)
SET @separator = ' '

DECLARE @Splited TABLE(id int IDENTITY(1,1), item varchar(max))

SET @str = REPLACE(@str, @separator, '''),(''')
SET @str = 'SELECT * FROM (VALUES(''' + @str + ''')) AS V(A)' 

INSERT INTO @Splited
EXEC(@str)

SELECT * FROM @Splited

Conjunto de resultados alcançado.

id  item
1   Hello
2   John
3   Smith
Frederic
fonte
1
i usado a sua resposta, mas não funcionou, mas eu modificado e isso funcionou com a união de tudo, eu estou usando SQL 2005
angel
9

Eu uso a resposta de frederic, mas isso não funcionou no SQL Server 2005

Eu modifiquei e eu estou usando selectcom union alle funciona

DECLARE @str varchar(max)
SET @str = 'Hello John Smith how are you'

DECLARE @separator varchar(max)
SET @separator = ' '

DECLARE @Splited table(id int IDENTITY(1,1), item varchar(max))

SET @str = REPLACE(@str, @separator, ''' UNION ALL SELECT ''')
SET @str = ' SELECT  ''' + @str + '''  ' 

INSERT INTO @Splited
EXEC(@str)

SELECT * FROM @Splited

E o conjunto de resultados é:

id  item
1   Hello
2   John
3   Smith
4   how
5   are
6   you
anjo
fonte
Isso é realmente ótimo que eu já vi em coisas sql, funcionou para o meu trabalho e eu aprecio isso, obrigado!
Abdurrahman I. 24/03
Fiquei realmente empolgado quando vi isso porque parecia tão limpo e fácil de entender, mas infelizmente você não pode colocar isso dentro de uma UDF por causa da EXEC. EXECchama implicitamente um procedimento armazenado e você não pode usar procedimentos armazenados em UDFs.
Kristen Hammack
Isso funciona perfeitamente !! Eu estava olhando para usar uma função (SplitStrings_Moden) daqui: sqlperformance.com/2012/07/t-sql-queries/split-strings#comments que faz isso e estava demorando um minuto e meio para dividir os dados e retornar as linhas ao usar apenas 4 números de conta. Testei sua versão com uma junção esquerda na tabela com os dados dos números das contas e demorou 2 ou 3 segundos! Enorme diferença e funciona perfeitamente! Eu daria 20 votos se possível!
Matte
8

Esse padrão funciona bem e você pode generalizar

Convert(xml,'<n>'+Replace(FIELD,'.','</n><n>')+'</n>').value('(/n[INDEX])','TYPE')
                          ^^^^^                                   ^^^^^     ^^^^

note FIELD , INDEX e TYPE .

Deixe alguma tabela com identificadores como

sys.message.1234.warning.A45
sys.message.1235.error.O98
....

Então, você pode escrever

SELECT Source         = q.value('(/n[1])', 'varchar(10)'),
       RecordType     = q.value('(/n[2])', 'varchar(20)'),
       RecordNumber   = q.value('(/n[3])', 'int'),
       Status         = q.value('(/n[4])', 'varchar(5)')
FROM   (
         SELECT   q = Convert(xml,'<n>'+Replace(fieldName,'.','</n><n>')+'</n>')
         FROM     some_TABLE
       ) Q

dividir e fundir todas as peças.

josejuan
fonte
Esta é a única solução aqui que permite transmitir para tipos específicos e é moderadamente eficiente (o CLR ainda é o mais eficiente, mas essa abordagem lida com uma tabela de linhas de 8 GB, 10 token e 10 M em cerca de 9 minutos (servidor aws m3, 4k iops) unidade provisionada)
Andrew Hill
7

Se o seu banco de dados tiver um nível de compatibilidade 130 ou superior, você poderá usar a função STRING_SPLIT junto com as cláusulas OFFSET FETCH para obter o item específico por índice.

Para obter o item no índice N (baseado em zero), você pode usar o seguinte código

SELECT value
FROM STRING_SPLIT('Hello John Smith',' ')
ORDER BY (SELECT NULL)
OFFSET N ROWS
FETCH NEXT 1 ROWS ONLY

Para verificar o nível de compatibilidade do seu banco de dados , execute este código:

SELECT compatibility_level  
FROM sys.databases WHERE name = 'YourDBName';
Gorgi Rankovski
fonte
O truque está nas OFFSET 1 ROWS, que pulam o primeiro item e retornam o segundo item. Se seus índices são 0-based e @X é a variável que contém o índice item que deseja buscar, você pode certeza que OFFSET @x ROWS
Gorgi Rankovski
Ok, não usei isso antes ... É bom saber ... Eu ainda preferiria a xmlabordagem baseada em -split, pois ela permite buscar o valor com segurança de tipo e não precisa de uma subconsulta, mas é um bom. +1 do meu lado
Shnugo
3
o problema aqui é que STRING_SPLIT não garante a ordem dos resultados retornados. Portanto, o seu artigo 1 pode ou não pode ser meu item 1.
user1443098
@GorgiRankovski, Usando STRING_SPLITdemandas para v2016 +. Nesse caso, é muito melhor usar OPENJSONou JSON_VALUE. Você pode querer verificar minha resposta
Shnugo
6

Eu estava procurando a solução na net e o abaixo funciona para mim. Ref .

E você chama a função assim:

SELECT * FROM dbo.split('ram shyam hari gopal',' ')

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE FUNCTION [dbo].[Split](@String VARCHAR(8000), @Delimiter CHAR(1))       
RETURNS @temptable TABLE (items VARCHAR(8000))       
AS       
BEGIN       
    DECLARE @idx INT       
    DECLARE @slice VARCHAR(8000)        
    SELECT @idx = 1       
    IF len(@String)<1 OR @String IS NULL  RETURN       
    WHILE @idx!= 0       
    BEGIN       
        SET @idx = charindex(@Delimiter,@String)       
        IF @idx!=0       
            SET @slice = LEFT(@String,@idx - 1)       
        ELSE       
            SET @slice = @String       
        IF(len(@slice)>0)  
            INSERT INTO @temptable(Items) VALUES(@slice)       
        SET @String = RIGHT(@String,len(@String) - @idx)       
        IF len(@String) = 0 break       
    END   
    RETURN       
END
kta
fonte
Você não pode acessar facilmente o item enésimo usando esta função.
Björn Lindqvist
6

Ainda outra não obtém parte da string pela função delimeter:

create function GetStringPartByDelimeter (
    @value as nvarchar(max),
    @delimeter as nvarchar(max),
    @position as int
) returns NVARCHAR(MAX) 
AS BEGIN
    declare @startPos as int
    declare @endPos as int
    set @endPos = -1
    while (@position > 0 and @endPos != 0) begin
        set @startPos = @endPos + 1
        set @endPos = charindex(@delimeter, @value, @startPos)

        if(@position = 1) begin
            if(@endPos = 0)
                set @endPos = len(@value) + 1

            return substring(@value, @startPos, @endPos - @startPos)
        end

        set @position = @position - 1
    end

    return null
end

e o uso:

select dbo.GetStringPartByDelimeter ('a;b;c;d;e', ';', 3)

que retorna:

c
Ramazan Binarbasi
fonte
Eu gosto dessa solução como uma opção para retornar uma única substring em vez de obter uma tabela analisada que você precisa selecionar. Usar um resultado de tabela tem seus usos, mas para o que eu precisava, isso funcionou perfeitamente.
James H
5

Tente o seguinte:

CREATE function [SplitWordList]
(
 @list varchar(8000)
)
returns @t table 
(
 Word varchar(50) not null,
 Position int identity(1,1) not null
)
as begin
  declare 
    @pos int,
    @lpos int,
    @item varchar(100),
    @ignore varchar(100),
    @dl int,
    @a1 int,
    @a2 int,
    @z1 int,
    @z2 int,
    @n1 int,
    @n2 int,
    @c varchar(1),
    @a smallint
  select 
    @a1 = ascii('a'),
    @a2 = ascii('A'),
    @z1 = ascii('z'),
    @z2 = ascii('Z'),
    @n1 = ascii('0'),
    @n2 = ascii('9')
  set @ignore = '''"'
  set @pos = 1
  set @dl = datalength(@list)
  set @lpos = 1
  set @item = ''
  while (@pos <= @dl) begin
    set @c = substring(@list, @pos, 1)
    if (@ignore not like '%' + @c + '%') begin
      set @a = ascii(@c)
      if ((@a >= @a1) and (@a <= @z1))  
        or ((@a >= @a2) and (@a <= @z2))
        or ((@a >= @n1) and (@a <= @n2))
      begin
        set @item = @item + @c
      end else if (@item > '') begin
        insert into @t values (@item)
        set @item = ''
      end
    end 
    set @pos = @pos + 1
  end
  if (@item > '') begin
    insert into @t values (@item)
  end
  return
end

Teste assim:

select * from SplitWordList('Hello John Smith')
Seibar
fonte
Eu já passei por isso e é perfeitamente como o que eu quero! até eu também posso personalizá-lo para ignorar caracteres especiais que eu escolher!
Vikas
5

O exemplo a seguir usa um CTE recursivo

Atualização 18.09.2013

CREATE FUNCTION dbo.SplitStrings_CTE(@List nvarchar(max), @Delimiter nvarchar(1))
RETURNS @returns TABLE (val nvarchar(max), [level] int, PRIMARY KEY CLUSTERED([level]))
AS
BEGIN
;WITH cte AS
 (
  SELECT SUBSTRING(@List, 0, CHARINDEX(@Delimiter,  @List + @Delimiter)) AS val,
         CAST(STUFF(@List + @Delimiter, 1, CHARINDEX(@Delimiter, @List + @Delimiter), '') AS nvarchar(max)) AS stval, 
         1 AS [level]
  UNION ALL
  SELECT SUBSTRING(stval, 0, CHARINDEX(@Delimiter, stval)),
         CAST(STUFF(stval, 1, CHARINDEX(@Delimiter, stval), '') AS nvarchar(max)),
         [level] + 1
  FROM cte
  WHERE stval != ''
  )
  INSERT @returns
  SELECT REPLACE(val, ' ','' ) AS val, [level]
  FROM cte
  WHERE val > ''
  RETURN
END

Demonstração no SQLFiddle

Aleksandr Fedorenko
fonte
2


    Alter Function dbo.fn_Split
    (
    @Expression nvarchar(max),
    @Delimiter  nvarchar(20) = ',',
    @Qualifier  char(1) = Null
    )
    RETURNS @Results TABLE (id int IDENTITY(1,1), value nvarchar(max))
    AS
    BEGIN
       /* USAGE
            Select * From dbo.fn_Split('apple pear grape banana orange honeydew cantalope 3 2 1 4', ' ', Null)
            Select * From dbo.fn_Split('1,abc,"Doe, John",4', ',', '"')
            Select * From dbo.fn_Split('Hello 0,"&""&&&&', ',', '"')
       */

       -- Declare Variables
       DECLARE
          @X     xml,
          @Temp  nvarchar(max),
          @Temp2 nvarchar(max),
          @Start int,
          @End   int

       -- HTML Encode @Expression
       Select @Expression = (Select @Expression For XML Path(''))

       -- Find all occurences of @Delimiter within @Qualifier and replace with |||***|||
       While PATINDEX('%' + @Qualifier + '%', @Expression) > 0 AND Len(IsNull(@Qualifier, '')) > 0
       BEGIN
          Select
             -- Starting character position of @Qualifier
             @Start = PATINDEX('%' + @Qualifier + '%', @Expression),
             -- @Expression starting at the @Start position
             @Temp = SubString(@Expression, @Start + 1, LEN(@Expression)-@Start+1),
             -- Next position of @Qualifier within @Expression
             @End = PATINDEX('%' + @Qualifier + '%', @Temp) - 1,
             -- The part of Expression found between the @Qualifiers
             @Temp2 = Case When @End < 0 Then @Temp Else Left(@Temp, @End) End,
             -- New @Expression
             @Expression = REPLACE(@Expression,
                                   @Qualifier + @Temp2 + Case When @End < 0 Then '' Else @Qualifier End,
                                   Replace(@Temp2, @Delimiter, '|||***|||')
                           )
       END

       -- Replace all occurences of @Delimiter within @Expression with '</fn_Split><fn_Split>'
       -- And convert it to XML so we can select from it
       SET
          @X = Cast('<fn_Split>' +
                    Replace(@Expression, @Delimiter, '</fn_Split><fn_Split>') +
                    '</fn_Split>' as xml)

       -- Insert into our returnable table replacing '|||***|||' back to @Delimiter
       INSERT @Results
       SELECT
          "Value" = LTRIM(RTrim(Replace(C.value('.', 'nvarchar(max)'), '|||***|||', @Delimiter)))
       FROM
          @X.nodes('fn_Split') as X(C)

       -- Return our temp table
       RETURN
    END
T-Rex
fonte
2

Você pode dividir uma string no SQL sem precisar de uma função:

DECLARE @bla varchar(MAX)
SET @bla = 'BED40DFC-F468-46DD-8017-00EF2FA3E4A4,64B59FC5-3F4D-4B0E-9A48-01F3D4F220B0,A611A108-97CA-42F3-A2E1-057165339719,E72D95EA-578F-45FC-88E5-075F66FD726C'

-- http://stackoverflow.com/questions/14712864/how-to-query-values-from-xml-nodes
SELECT 
    x.XmlCol.value('.', 'varchar(36)') AS val 
FROM 
(
    SELECT 
    CAST('<e>' + REPLACE(@bla, ',', '</e><e>') + '</e>' AS xml) AS RawXml
) AS b 
CROSS APPLY b.RawXml.nodes('e') x(XmlCol);

Se você precisar suportar seqüências de caracteres arbitrárias (com caracteres especiais xml)

DECLARE @bla NVARCHAR(MAX)
SET @bla = '<html>unsafe & safe Utf8CharsDon''tGetEncoded ÄöÜ - "Conex"<html>,Barnes & Noble,abc,def,ghi'

-- http://stackoverflow.com/questions/14712864/how-to-query-values-from-xml-nodes
SELECT 
    x.XmlCol.value('.', 'nvarchar(MAX)') AS val 
FROM 
(
    SELECT 
    CAST('<e>' + REPLACE((SELECT @bla FOR XML PATH('')), ',', '</e><e>') + '</e>' AS xml) AS RawXml
) AS b 
CROSS APPLY b.RawXml.nodes('e') x(XmlCol); 
Stefan Steiger
fonte
1

Eu sei que é uma pergunta antiga, mas acho que alguém pode se beneficiar da minha solução.

select 
SUBSTRING(column_name,1,CHARINDEX(' ',column_name,1)-1)
,SUBSTRING(SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name))
    ,1
    ,CHARINDEX(' ',SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name)),1)-1)
,SUBSTRING(SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name))
    ,CHARINDEX(' ',SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name)),1)+1
    ,LEN(column_name))
from table_name

SQL FIDDLE

Vantagens:

  • Ele separa todos os 3 delimitadores de sub-strings por ''.
  • Não se deve usar o loop while, pois isso diminui o desempenho.
  • Não há necessidade de dinamizar, pois todas as sub-seqüências resultantes serão exibidas em uma linha

Limitações:

  • É preciso conhecer o total não. de espaços (sub-string).

Nota : a solução pode fornecer sub-string até N.

Para superar a limitação, podemos usar a seguinte ref .

Mas, novamente, a solução acima não pode ser usada em uma tabela (não foi possível usá-la).

Mais uma vez, espero que esta solução possa ajudar alguém.

Atualização: No caso de Registros> 50000, não é aconselhável usar, LOOPSpois isso prejudicará o desempenho

Luv
fonte
1

Solução pura baseada em conjunto usando TVFcom recursiva CTE. Você pode JOINe APPLYesta função para qualquer conjunto de dados.

create function [dbo].[SplitStringToResultSet] (@value varchar(max), @separator char(1))
returns table
as return
with r as (
    select value, cast(null as varchar(max)) [x], -1 [no] from (select rtrim(cast(@value as varchar(max))) [value]) as j
    union all
    select right(value, len(value)-case charindex(@separator, value) when 0 then len(value) else charindex(@separator, value) end) [value]
    , left(r.[value], case charindex(@separator, r.value) when 0 then len(r.value) else abs(charindex(@separator, r.[value])-1) end ) [x]
    , [no] + 1 [no]
    from r where value > '')

select ltrim(x) [value], [no] [index] from r where x is not null;
go

Uso:

select *
from [dbo].[SplitStringToResultSet]('Hello John Smith', ' ')
where [index] = 1;

Resultado:

value   index
-------------
John    1
Andrey Morozov
fonte
1

Quase todas as outras respostas estão substituindo a sequência que está sendo dividida, que desperdiça ciclos da CPU e executa alocações de memória desnecessárias.

Cubro uma maneira muito melhor de fazer uma divisão de string aqui: http://www.digitalruby.com/split-string-sql-server/

Aqui está o código:

SET NOCOUNT ON

-- You will want to change nvarchar(MAX) to nvarchar(50), varchar(50) or whatever matches exactly with the string column you will be searching against
DECLARE @SplitStringTable TABLE (Value nvarchar(MAX) NOT NULL)
DECLARE @StringToSplit nvarchar(MAX) = 'your|string|to|split|here'
DECLARE @SplitEndPos int
DECLARE @SplitValue nvarchar(MAX)
DECLARE @SplitDelim nvarchar(1) = '|'
DECLARE @SplitStartPos int = 1

SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)

WHILE @SplitEndPos > 0
BEGIN
    SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, (@SplitEndPos - @SplitStartPos))
    INSERT @SplitStringTable (Value) VALUES (@SplitValue)
    SET @SplitStartPos = @SplitEndPos + 1
    SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)
END

SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, 2147483647)
INSERT @SplitStringTable (Value) VALUES(@SplitValue)

SET NOCOUNT OFF

-- You can select or join with the values in @SplitStringTable at this point.
jjxtra
fonte
0

Solução CTE recursiva com problemas no servidor, teste-a

Configuração do esquema do MS SQL Server 2008 :

create table Course( Courses varchar(100) );
insert into Course values ('Hello John Smith');

Consulta 1 :

with cte as
   ( select 
        left( Courses, charindex( ' ' , Courses) ) as a_l,
        cast( substring( Courses, 
                         charindex( ' ' , Courses) + 1 , 
                         len(Courses ) ) + ' ' 
              as varchar(100) )  as a_r,
        Courses as a,
        0 as n
     from Course t
    union all
      select 
        left(a_r, charindex( ' ' , a_r) ) as a_l,
        substring( a_r, charindex( ' ' , a_r) + 1 , len(a_R ) ) as a_r,
        cte.a,
        cte.n + 1 as n
    from Course t inner join cte 
         on t.Courses = cte.a and len( a_r ) > 0

   )
select a_l, n from cte
--where N = 1

Resultados :

|    A_L | N |
|--------|---|
| Hello  | 0 |
|  John  | 1 |
| Smith  | 2 |
dani herrera
fonte
0

embora semelhante à resposta baseada em xml de josejuan, eu descobri que o processamento do caminho xml apenas uma vez; em seguida, o giro foi moderadamente mais eficiente:

select ID,
    [3] as PathProvidingID,
    [4] as PathProvider,
    [5] as ComponentProvidingID,
    [6] as ComponentProviding,
    [7] as InputRecievingID,
    [8] as InputRecieving,
    [9] as RowsPassed,
    [10] as InputRecieving2
    from
    (
    select id,message,d.* from sysssislog cross apply       ( 
          SELECT Item = y.i.value('(./text())[1]', 'varchar(200)'),
              row_number() over(order by y.i) as rn
          FROM 
          ( 
             SELECT x = CONVERT(XML, '<i>' + REPLACE(Message, ':', '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY x.nodes('i') AS y(i)
       ) d
       WHERE event
       = 
       'OnPipelineRowsSent'
    ) as tokens 
    pivot 
    ( max(item) for [rn] in ([3],[4],[5],[6],[7],[8],[9],[10]) 
    ) as data

correu às 8:30

select id,
tokens.value('(/n[3])', 'varchar(100)')as PathProvidingID,
tokens.value('(/n[4])', 'varchar(100)') as PathProvider,
tokens.value('(/n[5])', 'varchar(100)') as ComponentProvidingID,
tokens.value('(/n[6])', 'varchar(100)') as ComponentProviding,
tokens.value('(/n[7])', 'varchar(100)') as InputRecievingID,
tokens.value('(/n[8])', 'varchar(100)') as InputRecieving,
tokens.value('(/n[9])', 'varchar(100)') as RowsPassed
 from
(
    select id, Convert(xml,'<n>'+Replace(message,'.','</n><n>')+'</n>') tokens
         from sysssislog 
       WHERE event
       = 
       'OnPipelineRowsSent'
    ) as data

correu às 9:20

Andrew Hill
fonte
0
CREATE FUNCTION [dbo].[fnSplitString] 
( 
    @string NVARCHAR(MAX), 
    @delimiter CHAR(1) 
) 
RETURNS @output TABLE(splitdata NVARCHAR(MAX) 
) 
BEGIN 
    DECLARE @start INT, @end INT 
    SELECT @start = 1, @end = CHARINDEX(@delimiter, @string) 
    WHILE @start < LEN(@string) + 1 BEGIN 
        IF @end = 0  
            SET @end = LEN(@string) + 1

        INSERT INTO @output (splitdata)  
        VALUES(SUBSTRING(@string, @start, @end - @start)) 
        SET @start = @end + 1 
        SET @end = CHARINDEX(@delimiter, @string, @start)

    END 
    RETURN 
END

E USE

select *from dbo.fnSplitString('Querying SQL Server','')
Savas Adar
fonte
0

se alguém quiser obter apenas uma parte do texto separado pode usar isso

select * fromSplitStringSep ('Word1 wordr2 word3', '')

CREATE function [dbo].[SplitStringSep] 
(
    @str nvarchar(4000), 
    @separator char(1)
)
returns table
AS
return (
    with tokens(p, a, b) AS (
        select 
        1, 
        1, 
        charindex(@separator, @str)
        union all
        select
            p + 1, 
            b + 1, 
            charindex(@separator, @str, b + 1)
        from tokens
        where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
  )
nazim hatipoglu
fonte
0

Eu desenvolvi isso,

declare @x nvarchar(Max) = 'ali.veli.deli.';
declare @item nvarchar(Max);
declare @splitter char='.';

while CHARINDEX(@splitter,@x) != 0
begin
    set @item = LEFT(@x,CHARINDEX(@splitter,@x))
    set @x    = RIGHT(@x,len(@x)-len(@item) )
     select @item as item, @x as x;
end

a única atenção que você deve ter é o ponto '.' esse fim do @x sempre deve estar lá.

Ali CAKIL
fonte
0

com base na solução @NothingsImpossible, ou melhor, comente a resposta mais votada (logo abaixo da resposta aceita), achei a seguinte solução rápida e suja que atende às minhas próprias necessidades - ela tem o benefício de estar exclusivamente no domínio SQL.

dada uma string "primeiro; segundo; terceiro; quarto; quinto", por exemplo, quero obter o terceiro token. isso funciona apenas se soubermos quantos tokens a string terá - nesse caso, são 5. Portanto, minha maneira de ação é cortar os dois últimos tokens (consulta interna) e depois os dois primeiros tokens ( consulta externa)

Eu sei que isso é feio e cobre as condições específicas em que eu estava, mas estou publicando apenas no caso de alguém achar útil. Felicidades

select 
    REVERSE(
        SUBSTRING(
            reverse_substring, 
            0, 
            CHARINDEX(';', reverse_substring)
        )
    ) 
from 
(
    select 
        msg,
        SUBSTRING(
            REVERSE(msg), 
            CHARINDEX(
                ';', 
                REVERSE(msg), 
                CHARINDEX(
                    ';',
                    REVERSE(msg)
                )+1
            )+1,
            1000
        ) reverse_substring
    from 
    (
        select 'first;second;third;fourth;fifth' msg
    ) a
) b
hello_earth
fonte
isso funciona somente se sabemos quantas fichas a corda vai ter - uma limitação quebra ...
Shnugo
0
declare @strng varchar(max)='hello john smith'
select (
    substring(
        @strng,
        charindex(' ', @strng) + 1,
        (
          (charindex(' ', @strng, charindex(' ', @strng) + 1))
          - charindex(' ',@strng)
        )
    ))
Smart003
fonte
0

A partir do SQL Server 2016 , string_split

DECLARE @string varchar(100) = 'Richard, Mike, Mark'

SELECT value FROM string_split(@string, ',')
Victor Hugo Terceros
fonte
Isso é bom, mas não aborda a questão de obter o enésimo resultado.
precisa saber é o seguinte
STRING_SPLITnão garante devolver o mesmo pedido. Mas OPENJSONnão (ver a minha resposta (seção de atualização) )
Shnugo