Fonction Split équivalente en T-SQL?


128

Je cherche à diviser '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ...' (délimité par des virgules) en une table ou une variable de table .

Quelqu'un a-t-il une fonction qui renvoie chacun dans une rangée?



1
Erland Sommarskog a maintenu la réponse officielle à cette question au cours des 12 dernières années: http://www.sommarskog.se/arrays-in-sql.html Cela ne vaut pas la peine de reproduire toutes les options ici sur StackOverflow, visitez simplement sa page et vous apprendrez tout ce que vous avez toujours voulu savoir.
Portman

2
J'ai récemment effectué une étude mineure comparant les approches les plus courantes à ce problème, qui vaut peut-être la peine d'être lue: sqlperformance.com/2012/07/t-sql-queries/split-strings et sqlperformance.com/2012/08/t- sql-queries /…
Aaron Bertrand

1
duplication possible de la chaîne Split en SQL
Luv

On dirait que vous avez quelques bonnes réponses ici; pourquoi ne pas marquer l'une d'elles comme réponse ou décrire votre problème plus en détail s'il ne répond toujours pas.
RyanfaeScotland

Réponses:


51

Voici une solution quelque peu démodée:

/*
    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

Dans SQL Server 2008, vous pouvez réaliser la même chose avec du code .NET. Peut-être que cela fonctionnerait plus rapidement, mais cette approche est certainement plus facile à gérer.


Merci, j'aimerais aussi savoir. Y a-t-il une erreur ici? J'ai écrit ce code il y a peut-être 6 ans et il fonctionnait bien depuis quand.
XOR

Je suis d'accord. C'est une très bonne solution lorsque vous ne voulez pas (ou ne pouvez tout simplement pas) vous impliquer dans la création de paramètres de type table, ce qui serait le cas dans mon cas. Les DBA ont verrouillé cette fonctionnalité et ne l'autoriseront pas. Merci XOR!
dscarr

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

55

Essaye ça

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

De nombreuses autres façons de faire la même chose sont ici Comment diviser une chaîne délimitée par des virgules?


9
Remarque pour tous ceux qui recherchent un séparateur de chaînes général: La première solution donnée ici n'est pas un séparateur de chaînes général - il n'est sûr que si vous êtes sûr que l'entrée ne contiendra jamais <, >ou &(par exemple, l'entrée est une séquence d'entiers). N'importe lequel des trois caractères ci-dessus entraînera une erreur d'analyse au lieu du résultat attendu.
miroxlav

1
Événement avec les problèmes mentionnés par miroxlav (qui devrait être résolu avec un peu de réflexion), c'est certainement l'une des solutions les plus créatives que j'ai trouvées (la première)! Très agréable!
major-mann

La ligne SELECT b, CHARINDEX(@delimiter, @str, b) + LEN(@delimiter)devrait en fait être SELECT b, CHARINDEX(@delimiter, @str, b+1) + LEN(@delimiter). Le b + 1 fait une grande différence. Testé ici avec un espace comme délimiteur, n'a pas fonctionné sans ce correctif.
JwJosefy

@miroxlav De plus, d'après mon expérience, utiliser XML pour diviser une chaîne est un détour extrêmement coûteux.
underscore_d

Excellentes solutions! Il convient de noter que les utilisateurs peuvent souhaiter ajouter une MAXRECURSIONoption pour diviser plus de 100 pièces, remplacer LENpar quelque chose de stackoverflow.com/q/2025585 pour gérer les espaces et exclure les NULLlignes pour les NULLentrées.
Kevinoid

27

Vous avez marqué ce SQL Server 2008, mais les futurs visiteurs de cette question (utilisant SQL Server 2016+) voudront probablement en savoir plus STRING_SPLIT.

Avec cette nouvelle fonction intégrée, vous pouvez maintenant simplement utiliser

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

Certaines restrictions de cette fonction et certains résultats prometteurs des tests de performances sont dans ce billet de blog par Aaron Bertrand .


13

Cela ressemble plus à .NET, pour ceux d'entre vous qui connaissent cette fonction:

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

voici la fonction split que tu as demandé

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

exécutez la fonction comme ceci

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)

Source de cette réponse: http://sqlhint.com/sqlserver/how-to/best-split-function-tsql-delimited


Bien que cela puisse théoriquement répondre à la question, il serait préférable d'inclure ici les parties essentielles de la réponse et de fournir le lien pour référence.
Xavi López

1
@Xavi: ok, j'ai inclus les parties essentielles de la réponse. Merci pour votre indice.
Mihai Bejenariu

3

Je suis tenté de me glisser dans ma solution préférée. La table résultante sera composée de 2 colonnes: PosIdx pour la position de l'entier trouvé; et Valeur en entier.


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

Il fonctionne en utilisant le CTE récursif comme liste de positions, de 1 à 100 par défaut. Si vous devez travailler avec une chaîne supérieure à 100, appelez simplement cette fonction en utilisant 'option (maxrecursion 4000)' comme suit:


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 pour mentionner l'option maxrecursion. Évidemment, la récursivité lourde doit être utilisée avec précaution dans un environnement de production, mais elle est idéale pour utiliser les CTE pour effectuer des tâches lourdes d'importation ou de conversion de données.
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

usage

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

3

Ce CTE simple donnera ce qu'il faut:

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 Vous voudrez peut-être regarder cette réponse, elle est plus efficace que la réponse acceptée et elle est plus simple.
Michał Turczyn

2

Ceci est une autre version qui n'a vraiment aucune restriction (par exemple: caractères spéciaux lors de l'utilisation de l'approche xml, nombre d'enregistrements dans l'approche CTE) et elle s'exécute beaucoup plus rapidement sur la base d'un test sur plus de 10 millions d'enregistrements avec une longueur moyenne de chaîne source de 4000. J'espère que cela pourrait aider.

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

En utilisant la table de pointage, voici une fonction de chaîne fractionnée (meilleure approche possible) par 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
;

Référé de Tally OH! Une fonction améliorée de «séparateur CSV» SQL 8K


0

Ce blog est venu avec une assez bonne solution utilisant XML dans T-SQL.

C'est la fonction que j'ai créée sur la base de ce blog (changer le nom de la fonction et le type de résultat en fonction des besoins):

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

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.