T Función de valor de tabla SQL para dividir una columna en comas

10

Escribí una Función de valor de tabla en Microsoft SQL Server 2008 para tomar una columna delimitada por comas en una base de datos para escupir filas separadas para cada valor.

Ej: "uno, dos, tres, cuatro" devolvería una nueva tabla con solo una columna que contiene los siguientes valores:

one
two
three
four

¿Este código parece propenso a errores para ustedes? Cuando lo pruebo con

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

simplemente funciona para siempre y nunca devuelve nada. Esto se está volviendo realmente desalentador, especialmente porque no hay funciones divididas integradas en el servidor MSSQL (¡¿POR QUÉ POR QUÉ ?!) y todas las funciones similares que he encontrado en la web son basura absoluta o simplemente irrelevantes para lo que estoy tratando de hacer .

Aquí está la función:

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
Hornos
fuente

Respuestas:

1

Reelaborado ligeramente ...

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
fuente
20

No haría esto con un bucle; Hay alternativas mucho mejores. Con mucho, el mejor, cuando tiene que dividirse, es CLR, y el enfoque de Adam Machanic es el más rápido que he probado .

El siguiente mejor enfoque en mi humilde opinión, si no puede implementar CLR, es una tabla 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 función:

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

Creo que todo esto funcionará mejor que la función que tiene, cuando lo haga funcionar, especialmente porque están en línea en lugar de múltiples declaraciones. No he investigado por qué el suyo no funciona, porque no creo que valga la pena hacer que esa función funcione.

Pero todo eso dijo ...

Dado que está utilizando SQL Server 2008, ¿hay alguna razón por la que deba dividirse en primer lugar? Prefiero usar un TVP para esto:

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

Ahora puede aceptar esto como un parámetro para sus procedimientos almacenados, y usar los contenidos tal como usaría un 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

Y puede pasar un TVP directamente desde C #, etc. como DataTable. Esto seguramente superará a cualquiera de las soluciones anteriores, especialmente si está creando una cadena separada por comas en su aplicación específicamente para que su procedimiento almacenado pueda llamar a un TVP para dividirlo nuevamente. Para obtener más información sobre los TVP, consulte el excelente artículo de Erland Sommarskog .

Más recientemente, he escrito una serie sobre la división de cadenas:

Y si está utilizando SQL Server 2016 o posterior (o Azure SQL Database), hay una nueva STRING_SPLITfunción , sobre la que escribí en un blog aquí:

Aaron Bertrand
fuente
6

SQL Server 2016 introdujo la función STRING_SPLIT () . Tiene dos parámetros: la cadena que se va a cortar y el separador. El resultado es una fila por valor devuelto.

Para el ejemplo dado

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

volverá

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

He estado usando y amando el divisor de cuerda de Jeff Moden casi desde que salió.

Tally OH! Una función mejorada de SQL 8K "CSV Splitter"

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
fuente
-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', ' ')

ingrese la descripción de la imagen aquí

Mudassir
fuente