Dividir função equivalente em T-SQL?


128

Eu estou olhando para dividir '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ...' (delimitado por vírgula) em uma tabela ou variável de tabela .

Alguém tem uma função que retorna cada um em uma linha?



1
Erland Sommarskog manteve a resposta autorizada a esta pergunta nos últimos 12 anos: http://www.sommarskog.se/arrays-in-sql.html Não vale a pena reproduzir todas as opções aqui no StackOverflow, basta visitar sua página e você aprenderá tudo o que sempre quis saber.
Portman

2
Recentemente, realizei um estudo menor comparando as abordagens mais comuns para esse problema, que pode valer a pena ler: sqlperformance.com/2012/07/t-sql-queries/split-strings e sqlperformance.com/2012/08/t- sql-queries /…
Aaron Bertrand

1
possível duplicata Separa strings em SQL
Luv

Parece que você tem algumas boas respostas aqui; por que não marcar um deles como resposta ou descrever seu problema com mais detalhes se ele ainda não foi respondido.
RyanfaeScotland

Respostas:


51

Aqui está uma solução um tanto antiquada:

/*
    Splits string into parts delimitered with specified character.
*/
CREATE FUNCTION [dbo].[SDF_SplitString]
(
    @sString nvarchar(2048),
    @cDelimiter nchar(1)
)
RETURNS @tParts TABLE ( part nvarchar(2048) )
AS
BEGIN
    if @sString is null return
    declare @iStart int,
            @iPos int
    if substring( @sString, 1, 1 ) = @cDelimiter 
    begin
        set @iStart = 2
        insert into @tParts
        values( null )
    end
    else 
        set @iStart = 1
    while 1=1
    begin
        set @iPos = charindex( @cDelimiter, @sString, @iStart )
        if @iPos = 0
            set @iPos = len( @sString )+1
        if @iPos - @iStart > 0          
            insert into @tParts
            values  ( substring( @sString, @iStart, @iPos-@iStart ))
        else
            insert into @tParts
            values( null )
        set @iStart = @iPos+1
        if @iStart > len( @sString ) 
            break
    end
    RETURN

END

No SQL Server 2008, você pode obter o mesmo com o código .NET. Talvez funcione mais rápido, mas definitivamente essa abordagem é mais fácil de gerenciar.


Obrigado, eu também gostaria de saber. Há algum erro aqui? Eu escrevi esse código há talvez 6 anos e estava funcionando bem desde quando.
XOR

Concordo. Essa é uma solução muito boa quando você não deseja (ou simplesmente não pode) se envolver na criação de parâmetros de tipo de tabela, o que seria o caso na minha instância. Os DBAs bloquearam esse recurso e não o permitirão. Obrigado XOR!
dscarr

DECLARAR VarString NVARCHAR (2048) = 'Mike / John / Miko / Matt'; DECLARAR CaracString NVARCHAR (1) = '/'; SELECT * FROM dbo.FnSplitString (VarString, CaracString)
fernando yevenes

55

Tente isto

DECLARE @xml xml, @str varchar(100), @delimiter varchar(10)
SET @str = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15'
SET @delimiter = ','
SET @xml = cast(('<X>'+replace(@str, @delimiter, '</X><X>')+'</X>') as xml)
SELECT C.value('.', 'varchar(10)') as value FROM @xml.nodes('X') as X(C)

OU

DECLARE @str varchar(100), @delimiter varchar(10)
SET @str = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15'
SET @delimiter = ','
;WITH cte AS
(
    SELECT 0 a, 1 b
    UNION ALL
    SELECT b, CHARINDEX(@delimiter, @str, b) + LEN(@delimiter)
    FROM CTE
    WHERE b > a
)
SELECT SUBSTRING(@str, a,
CASE WHEN b > LEN(@delimiter) 
    THEN b - a - LEN(@delimiter) 
    ELSE LEN(@str) - a + 1 END) value      
FROM cte WHERE a > 0

Muitas outras maneiras de fazer o mesmo estão aqui Como dividir uma string delimitada por vírgula?


9
Nota para quem procura um divisor de seqüência geral: A primeira solução fornecida aqui não é um divisor de seqüência geral - é seguro apenas se você tiver certeza de que a entrada nunca conterá <, >ou &(por exemplo, entrada é uma sequência de números inteiros). Qualquer um dos três caracteres acima fará com que você obtenha um erro de análise em vez do resultado esperado.
miroxlav

1
Evento com os problemas mencionados por miroxlav (que deve ser resolvido com alguma reflexão), essa é definitivamente uma das soluções mais criativas que encontrei (a primeira)! Muito agradável!
major-mann

A linha SELECT b, CHARINDEX(@delimiter, @str, b) + LEN(@delimiter)deve realmente ser SELECT b, CHARINDEX(@delimiter, @str, b+1) + LEN(@delimiter). O b + 1 faz uma grande diferença. Testado aqui com espaço como delimitador, não funcionou sem essa correção.
precisa

@miroxlav Além disso, na minha experiência, o uso de XML para dividir uma string é um desvio extremamente caro.
underscore_d

Ótimas soluções! Vale notar que os usuários podem querer adicionar uma MAXRECURSIONopção para dividir mais de 100 partes, substituir LENpor algo de stackoverflow.com/q/2025585 para manipular espaços e excluir NULLlinhas para NULLentradas.
`` Kevinoid

27

Você marcou este SQL Server 2008, mas é provável que os futuros visitantes desta pergunta (usando o SQL Server 2016 ou superior) desejem conhecer STRING_SPLIT.

Com esta nova função integrada, agora você pode apenas usar

SELECT TRY_CAST(value AS INT)
FROM   STRING_SPLIT ('1,2,3,4,5,6,7,8,9,10,11,12,13,14,15', ',') 

Algumas restrições dessa função e alguns resultados promissores de teste de desempenho estão neste post de Aaron Bertrand .


13

Isso é mais parecido com o .NET, para aqueles que estão familiarizados com essa função:

CREATE FUNCTION dbo.[String.Split]
(
    @Text VARCHAR(MAX),
    @Delimiter VARCHAR(100),
    @Index INT
)
RETURNS VARCHAR(MAX)
AS BEGIN
    DECLARE @A TABLE (ID INT IDENTITY, V VARCHAR(MAX));
    DECLARE @R VARCHAR(MAX);
    WITH CTE AS
    (
    SELECT 0 A, 1 B
    UNION ALL
    SELECT B, CONVERT(INT,CHARINDEX(@Delimiter, @Text, B) + LEN(@Delimiter))
    FROM CTE
    WHERE B > A
    )
    INSERT @A(V)
    SELECT SUBSTRING(@Text,A,CASE WHEN B > LEN(@Delimiter) THEN B-A-LEN(@Delimiter) ELSE LEN(@Text) - A + 1 END) VALUE      
    FROM CTE WHERE A >0

    SELECT      @R
    =           V
    FROM        @A
    WHERE       ID = @Index + 1
    RETURN      @R
END

SELECT dbo.[String.Split]('121,2,3,0',',',1) -- gives '2'

9

aqui está a função de divisão que você pediu

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 assim

select * from dbo.split('1,2,3,4,5,6,7,8,9,10,11,12,13,14,15',',')

5
DECLARE
    @InputString NVARCHAR(MAX) = 'token1,token2,token3,token4,token5'
    , @delimiter varchar(10) = ','

DECLARE @xml AS XML = CAST(('<X>'+REPLACE(@InputString,@delimiter ,'</X><X>')+'</X>') AS XML)
SELECT C.value('.', 'varchar(10)') AS value
FROM @xml.nodes('X') as X(C)

Origem desta resposta: http://sqlhint.com/sqlserver/how-to/best-split-function-tsql-delimited


Embora isso possa teoricamente responder à pergunta, seria preferível incluir aqui as partes essenciais da resposta e fornecer o link para referência.
Xavi López

1
@ Xavi: ok, incluí as partes essenciais da resposta. Obrigado pela sua dica.
precisa

3

Estou tentado a apertar minha solução favorita. A tabela resultante será composta por 2 colunas: PosIdx para a posição do número inteiro encontrado; e Valor em número inteiro.


create function FnSplitToTableInt
(
    @param nvarchar(4000)
)
returns table as
return
    with Numbers(Number) as 
    (
        select 1 
        union all 
        select Number + 1 from Numbers where Number < 4000
    ),
    Found as
    (
        select 
            Number as PosIdx,
            convert(int, ltrim(rtrim(convert(nvarchar(4000), 
                substring(@param, Number, 
                charindex(N',' collate Latin1_General_BIN, 
                @param + N',', Number) - Number))))) as Value
        from   
            Numbers 
        where  
            Number <= len(@param)
        and substring(N',' + @param, Number, 1) = N',' collate Latin1_General_BIN
    )
    select 
        PosIdx, 
        case when isnumeric(Value) = 1 
            then convert(int, Value) 
            else convert(int, null) end as Value 
    from 
        Found

Ele funciona usando CTE recursiva como a lista de posições, de 1 a 100 por padrão. Se você precisar trabalhar com uma string maior que 100, chame esta função usando 'option (maxrecursion 4000)' como o seguinte:


select * from FnSplitToTableInt
(
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' + 
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' +
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' +
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' +
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0'
) 
option (maxrecursion 4000)

2
+1 por mencionar a opção maxrecursion. Obviamente, a recursão pesada deve ser usada com cuidado em um ambiente de produção, mas é ótimo usar CTEs para executar tarefas pesadas de importação ou conversão de dados.
Tim Medora

3
CREATE FUNCTION Split
(
  @delimited nvarchar(max),
  @delimiter nvarchar(100)
) RETURNS @t TABLE
(
-- Id column can be commented out, not required for sql splitting string
  id int identity(1,1), -- I use this column for numbering splitted parts
  val nvarchar(max)
)
AS
BEGIN
  declare @xml xml
  set @xml = N'<root><r>' + replace(@delimited,@delimiter,'</r><r>') + '</r></root>'

  insert into @t(val)
  select
    r.value('.','varchar(max)') as item
  from @xml.nodes('//root/r') as records(r)

  RETURN
END
GO

uso

Select * from dbo.Split(N'1,2,3,4,6',',')

3

Este CTE simples fornecerá o que é necessário:

DECLARE @csv varchar(max) = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15';
--append comma to the list for CTE to work correctly
SET @csv = @csv + ',';
--remove double commas (empty entries)
SET @csv = replace(@csv, ',,', ',');
WITH CteCsv AS (
    SELECT CHARINDEX(',', @csv) idx, SUBSTRING(@csv, 1, CHARINDEX(',', @csv) - 1) [Value]
    UNION ALL
    SELECT CHARINDEX(',', @csv, idx + 1), SUBSTRING(@csv, idx + 1, CHARINDEX(',', @csv, idx + 1) - idx - 1) FROM CteCsv
    WHERE CHARINDEX(',', @csv, idx + 1) > 0
)

SELECT [Value] FROM CteCsv

@jinsungy Você pode olhar para esta resposta, é mais eficiente que a resposta aceita e é mais simples.
Michał Turczyn

2

Esta é outra versão que realmente não possui restrições (por exemplo: caracteres especiais ao usar a abordagem xml, número de registros na abordagem CTE) e é executada muito mais rapidamente com base em um teste em mais de 10 milhões de registros com comprimento médio de cadeia de origem de 4000. Espero que isso Poderia ajudar.

Create function [dbo].[udf_split] (
    @ListString nvarchar(max),
    @Delimiter  nvarchar(1000),
    @IncludeEmpty bit) 
Returns @ListTable TABLE (ID int, ListValue nvarchar(1000))
AS
BEGIN
    Declare @CurrentPosition int, @NextPosition int, @Item nvarchar(max), @ID int, @L int
    Select @ID = 1,
   @L = len(replace(@Delimiter,' ','^')),
            @ListString = @ListString + @Delimiter,
            @CurrentPosition = 1 
    Select @NextPosition = Charindex(@Delimiter, @ListString, @CurrentPosition)
   While @NextPosition > 0 Begin
   Set  @Item = LTRIM(RTRIM(SUBSTRING(@ListString, @CurrentPosition, @NextPosition-@CurrentPosition)))
   If      @IncludeEmpty=1 or LEN(@Item)>0 Begin 
     Insert Into @ListTable (ID, ListValue) Values (@ID, @Item)
     Set @ID = @ID+1
   End
   Set  @CurrentPosition = @NextPosition+@L
   Set  @NextPosition = Charindex(@Delimiter, @ListString, @CurrentPosition)
  End
    RETURN
END

1

Usando a tabela registro aqui é uma função string split (melhor abordagem possível) por Jeff Moden

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


0

Este blog veio com uma solução muito boa usando XML em T-SQL.

Esta é a função que criei com base nesse blog (altere o nome da função e o tipo de resultado convertido por necessidade):

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[SplitIntoBigints]
(@List varchar(MAX), @Splitter char)
RETURNS TABLE 
AS
RETURN 
(
    WITH SplittedXML AS(
        SELECT CAST('<v>' + REPLACE(@List, @Splitter, '</v><v>') + '</v>' AS XML) AS Splitted
    )
    SELECT x.v.value('.', 'bigint') AS Value
    FROM SplittedXML
    CROSS APPLY Splitted.nodes('//v') x(v)
)
GO

0
CREATE Function [dbo].[CsvToInt] ( @Array varchar(4000)) 
returns @IntTable table 
(IntValue int)
AS
begin
declare @separator char(1)
set @separator = ','
declare @separator_position int 
declare @array_value varchar(4000) 

set @array = @array + ','

while patindex('%,%' , @array) <> 0 
begin

select @separator_position = patindex('%,%' , @array)
select @array_value = left(@array, @separator_position - 1)

Insert @IntTable
Values (Cast(@array_value as int))
select @array = stuff(@array, 1, @separator_position, '')
end

0
/* *Object:  UserDefinedFunction [dbo].[Split]    Script Date: 10/04/2013 18:18:38* */
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[Split]
(@List varchar(8000),@SplitOn Nvarchar(5))
RETURNS @RtnValue table
(Id int identity(1,1),Value nvarchar(100))
AS
BEGIN
    Set @List = Replace(@List,'''','')
    While (Charindex(@SplitOn,@List)>0)
    Begin

    Insert Into @RtnValue (value)
    Select
    Value = ltrim(rtrim(Substring(@List,1,Charindex(@SplitOn,@List)-1)))

    Set @List = Substring(@List,Charindex(@SplitOn,@List)+len(@SplitOn),len(@List))
    End

    Insert Into @RtnValue (Value)
    Select Value = ltrim(rtrim(@List))

    Return
END
go

Select *
From [Clv].[Split] ('1,2,3,3,3,3,',',')
GO

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.