Fonction de valeur de table SQL T pour fractionner une colonne par des virgules


10

J'ai écrit une fonction de valeur de table dans Microsoft SQL Server 2008 pour prendre une colonne séparée par des virgules dans une base de données pour cracher des lignes distinctes pour chaque valeur.

Ex: "un, deux, trois, quatre" retournerait une nouvelle table avec une seule colonne contenant les valeurs suivantes:

one
two
three
four

Ce code vous semble-t-il sujet aux erreurs? Quand je le teste avec

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

il fonctionne pour toujours et ne renvoie jamais rien. Cela devient vraiment décourageant, d'autant plus qu'il n'y a pas de fonctions séparées intégrées sur le serveur MSSQL (POURQUOI POURQUOI POURQUOI?!) Et toutes les fonctions similaires que j'ai trouvées sur le Web sont des ordures absolues ou tout simplement sans rapport avec ce que j'essaie de faire .

Voici la fonction:

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

Réponses:


1

Retravaillé légèrement ...

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

20

Je ne ferais pas ça avec une boucle; il existe de bien meilleures alternatives. De loin le meilleur, quand vous devez vous séparer, est le CLR, et l'approche d'Adam Machanic est la plus rapide que j'ai testée .

La meilleure approche suivante IMHO, si vous ne pouvez pas implémenter CLR, est un tableau de nombres:

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

... qui permet cette fonction:

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

Je crois que tous ces éléments fonctionneront mieux que la fonction que vous avez, lorsque vous le faites fonctionner, d'autant plus qu'ils sont en ligne au lieu de plusieurs instructions. Je n'ai pas cherché à savoir pourquoi la vôtre ne fonctionne pas, car je ne pense pas que cela vaille la peine de faire fonctionner cette fonction.

Mais tout cela a dit ...

Puisque vous utilisez SQL Server 2008, y a-t-il une raison pour laquelle vous devez vous séparer en premier lieu? Je préfère utiliser un TVP pour cela:

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

Vous pouvez maintenant accepter cela en tant que paramètre de vos procédures stockées et utiliser le contenu comme vous utiliseriez 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

Et vous pouvez passer un TVP directement à partir de C # etc. en tant que DataTable. Cela surpassera presque certainement l'une des solutions ci-dessus, surtout si vous créez une chaîne séparée par des virgules dans votre application spécifiquement afin que votre procédure stockée puisse appeler un TVP pour la diviser à nouveau. Pour beaucoup plus d'informations sur les TVP, voir le grand article d'Erland Sommarskog .

Plus récemment, j'ai écrit une série sur le fractionnement de chaînes:

Et si vous utilisez SQL Server 2016 ou plus récent (ou Azure SQL Database), il y a une nouvelle STRING_SPLITfonction , sur laquelle j'ai blogué ici:


6

SQL Server 2016 a introduit la fonction STRING_SPLIT () . Il a deux paramètres - la chaîne à couper et le séparateur. La sortie est une ligne par valeur renvoyée.

Pour l'exemple donné

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

reviendra

value
------------------
one
two
three
four

1

J'utilise et j'adore le séparateur de cordes de Jeff Moden depuis sa sortie.

Tally OH! Une fonction SQL 8K «CSV Splitter» améliorée

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
;

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

entrez la description de l'image ici

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.