T Função com valor de tabela SQL para dividir uma coluna em vírgulas

10

Eu escrevi uma Função com valor de tabela no Microsoft SQL Server 2008 para obter uma coluna delimitada por vírgula em um banco de dados para cuspir linhas separadas para cada valor.

Ex: "um, dois, três, quatro" retornaria uma nova tabela com apenas uma coluna contendo os seguintes valores:

one
two
three
four

Esse código parece propenso a erros para vocês? Quando eu testo com

SELECT * FROM utvf_Split('one,two,three,four',',') 

apenas funciona para sempre e nunca retorna nada. Isso está ficando realmente desanimador, especialmente porque não há funções divididas integradas no servidor MSSQL (POR QUE POR QUÊ ?!) e todas as funções semelhantes que encontrei na Web são lixo absoluto ou simplesmente irrelevantes para o que estou tentando fazer .

Aqui está a função:

USE *myDBname*
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER FUNCTION [dbo].[utvf_SPlit] (@String VARCHAR(MAX), @delimiter CHAR)

RETURNS @SplitValues TABLE
(
    Asset_ID VARCHAR(MAX) NOT NULL
)

AS
BEGIN
            DECLARE @FoundIndex INT
            DECLARE @ReturnValue VARCHAR(MAX)

            SET @FoundIndex = CHARINDEX(@delimiter, @String)

            WHILE (@FoundIndex <> 0)
            BEGIN
                  DECLARE @NextFoundIndex INT
                  SET @NextFoundIndex = CHARINDEX(@delimiter, @String, @FoundIndex+1)
                  SET @ReturnValue = SUBSTRING(@String, @FoundIndex,@NextFoundIndex-@FoundIndex)
                  SET @FoundIndex = CHARINDEX(@delimiter, @String)
                  INSERT @SplitValues (Asset_ID) VALUES (@ReturnValue)
            END

            RETURN
END
OvetS
fonte

Respostas:

1

Re-trabalhou um pouco ...

DECLARE @FoundIndex INT
DECLARE @ReturnValue VARCHAR(MAX)

SET @FoundIndex = CHARINDEX(@delimiter, @String)

WHILE (@FoundIndex <> 0)
BEGIN
      SET @ReturnValue = SUBSTRING(@String, 0, @FoundIndex)
      INSERT @SplitValues (Asset_ID) VALUES (@ReturnValue)
      SET @String = SUBSTRING(@String, @FoundIndex + 1, len(@String) - @FoundIndex)
      SET @FoundIndex = CHARINDEX(@delimiter, @String)
END

INSERT @SplitValues (Asset_ID) VALUES (@String)

RETURN
Derek Kromm
fonte
20

Eu não faria isso com um loop; existem alternativas muito melhores. De longe, o melhor, quando você precisa dividir, é o CLR, e a abordagem de Adam Machanic é a mais rápida que eu testei .

A próxima melhor abordagem do IMHO, se você não pode implementar o CLR, é uma tabela de números:

SET NOCOUNT ON;

DECLARE @UpperLimit INT = 1000000;

WITH n AS
(
    SELECT
        x = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
    FROM       sys.all_objects AS s1
    CROSS JOIN sys.all_objects AS s2
    CROSS JOIN sys.all_objects AS s3
)
SELECT Number = x
  INTO dbo.Numbers
  FROM n
  WHERE x BETWEEN 1 AND @UpperLimit
  OPTION (MAXDOP 1); -- protecting from Paul White's observation

GO
CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(Number) 
    --WITH (DATA_COMPRESSION = PAGE);
GO

... que permite esta função:

CREATE FUNCTION dbo.SplitStrings_Numbers
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN
   (
       SELECT Item = SUBSTRING(@List, Number, 
         CHARINDEX(@Delimiter, @List + @Delimiter, Number) - Number)
       FROM dbo.Numbers
       WHERE Number <= CONVERT(INT, LEN(@List))
         AND SUBSTRING(@Delimiter + @List, Number, 1) = @Delimiter
   );
GO

Acredito que tudo isso terá um desempenho melhor do que a função que você possui, quando você a trabalhar, especialmente porque elas estão embutidas em vez de múltiplas instruções. Não investiguei por que o seu não está funcionando, porque acho que não vale a pena fazer essa função funcionar.

Mas tudo isso disse ...

Como você está usando o SQL Server 2008, existe um motivo para dividir em primeiro lugar? Prefiro usar um TVP para isso:

CREATE TYPE dbo.strings AS TABLE
(
  string NVARCHAR(4000)
);

Agora você pode aceitar isso como um parâmetro para seus procedimentos armazenados e usar o conteúdo como usaria um TVF:

CREATE PROCEDURE dbo.foo
  @strings dbo.strings READONLY
AS
BEGIN
  SET NOCOUNT ON;

  SELECT Asset_ID = string FROM @strings;
  -- SELECT Asset_ID FROM dbo.utvf_split(@other_param, ',');
END

E você pode transmitir um TVP diretamente do C # etc. como uma DataTable. Isso quase certamente superará qualquer uma das soluções acima, especialmente se você estiver criando uma sequência separada por vírgula no seu aplicativo especificamente para que seu procedimento armazenado possa chamar um TVP para dividi-lo novamente. Para muito mais informações sobre TVPs, consulte o excelente artigo de Erland Sommarskog .

Mais recentemente, escrevi uma série sobre como dividir strings:

E se você estiver usando o SQL Server 2016 ou mais recente (ou o Banco de Dados SQL do Azure), há uma nova STRING_SPLITfunção , sobre a qual escrevi aqui:

Aaron Bertrand
fonte
6

O SQL Server 2016 introduziu a função STRING_SPLIT () . Ele tem dois parâmetros - a string a ser cortada e o separador. A saída é uma linha por valor retornado.

Para o exemplo dado

SELECT * FROM string_split('one,two,three,four', ',');

retornará

value
------------------
one
two
three
four
Michael Green
fonte
1

Eu tenho usado e amado o separador de cordas de Jeff Moden desde que ele foi lançado.

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

CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  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
;
Erik Darling
fonte
-2
CREATE FUNCTION [dbo].[fnSplit]
(

    @sInputList VARCHAR(8000),         -- List of delimited items

    @sDelimiter VARCHAR(8000) = ','    -- delimiter that separates items

)
RETURNS @List TABLE (colData VARCHAR(8000))

BEGIN

DECLARE @sItem VARCHAR(8000)

    WHILE CHARINDEX(@sDelimiter,@sInputList,0) <> 0

    BEGIN

        SELECT @sItem=RTRIM(LTRIM(SUBSTRING(@sInputList,1,CHARINDEX
(@sDelimiter,@sInputList,0)-1))),

        @sInputList=RTRIM(LTRIM(SUBSTRING(@sInputList,CHARINDEX(@sDelimiter,@sInputList,0)
+LEN(@sDelimiter),LEN(@sInputList))))

        IF LEN(@sItem) > 0
            INSERT INTO @List SELECT @sItem
        END

        IF LEN(@sInputList) > 0
            INSERT INTO @List SELECT @sInputList -- Put the last item in
        RETURN
    END

--TEST

--Example 1: select * from fnSplit('1,22,333,444,,5555,666', ',')

--Example 2: select * from fnSplit('1##22#333##444','##')  --note second colData has embedded #

--Example 3: select * from fnSplit('1 22 333 444  5555 666', ' ')

insira a descrição da imagem aqui

Mudassir
fonte