Geteiltes Funktionsäquivalent in T-SQL?


128

Ich möchte '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ...' (durch Kommas getrennt) in eine Tabelle oder Tabellenvariable aufteilen .

Hat jemand eine Funktion, die jede nacheinander zurückgibt?



1
Erland Sommarskog hat die maßgebliche Antwort auf diese Frage in den letzten 12 Jahren beibehalten: http://www.sommarskog.se/arrays-in-sql.html Es lohnt sich nicht, alle Optionen hier auf StackOverflow zu reproduzieren. Besuchen Sie einfach seine Seite und Sie werden alles lernen, was Sie schon immer wissen wollten.
Portman

2
Ich habe kürzlich eine kleine Studie durchgeführt, in der die gängigsten Ansätze für dieses Problem verglichen wurden, die möglicherweise eine Lektüre wert sind: sqlperformance.com/2012/07/t-sql-queries/split-strings und sqlperformance.com/2012/08/t- SQL-Abfragen /…
Aaron Bertrand

1
Mögliches Duplikat der geteilten Zeichenfolge in SQL
Luv

Sieht so aus, als hätten Sie hier ein paar gute Antworten. Warum markieren Sie nicht eine davon als Antwort oder beschreiben Sie Ihr Problem detaillierter, wenn es immer noch nicht beantwortet wird?
RyanfaeScotland

Antworten:


51

Hier ist eine etwas altmodische Lösung:

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

In SQL Server 2008 können Sie dasselbe mit .NET-Code erreichen. Vielleicht würde es schneller funktionieren, aber definitiv ist dieser Ansatz einfacher zu verwalten.


Danke, ich würde es auch gerne wissen. Gibt es hier einen Fehler? Ich habe diesen Code vor vielleicht 6 Jahren geschrieben und seitdem funktioniert er einwandfrei.
XOR

Genau. Dies ist eine sehr gute Lösung, wenn Sie sich nicht mit der Erstellung von Tabellentypparametern befassen möchten (oder können), was in meinem Fall der Fall wäre. Die Datenbankadministratoren haben diese Funktion gesperrt und lassen sie nicht zu. Danke XOR!
dscarr

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

55

Versuche dies

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)

ODER

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

Viele weitere Möglichkeiten, dasselbe zu tun, finden Sie hier. Wie wird eine durch Kommas getrennte Zeichenfolge aufgeteilt?


9
Hinweis für alle, die einen allgemeinen String-Splitter suchen: Die erste hier angegebene Lösung ist kein allgemeiner String-Splitter. Sie ist nur dann sicher, wenn Sie sicher sind, dass die Eingabe niemals enthält <, >oder &(z. B. ist die Eingabe eine Folge von Ganzzahlen). Jedes der oben genannten drei Zeichen führt dazu, dass anstelle des erwarteten Ergebnisses ein Analysefehler angezeigt wird.
Miroxlav

1
Event mit den von miroxlav erwähnten Problemen (die mit einigem Nachdenken lösbar sein sollten), dies ist definitiv eine der kreativsten Lösungen, die ich gefunden habe (die erste)! Sehr schön!
Major-Mann

Die Linie SELECT b, CHARINDEX(@delimiter, @str, b) + LEN(@delimiter)sollte eigentlich sein SELECT b, CHARINDEX(@delimiter, @str, b+1) + LEN(@delimiter). Das b + 1 macht einen großen Unterschied. Hier mit Leerzeichen als Trennzeichen getestet, funktionierte ohne dieses Update nicht.
JwJosefy

@miroxlav Meiner Erfahrung nach ist die Verwendung von XML zum Teilen eines Strings ein extrem teurer Umweg.
underscore_d

Tolle Lösungen! Es ist erwähnenswert, dass Benutzer möglicherweise eine MAXRECURSIONOption hinzufügen möchten, um mehr als 100 Teile zu teilen, durch LENetwas von stackoverflow.com/q/2025585 zu ersetzen, um Leerzeichen zu verarbeiten, und NULLZeilen für NULLEingaben auszuschließen .
Kevinoid

27

Sie haben diesen SQL Server 2008 markiert, aber zukünftige Besucher dieser Frage (mit SQL Server 2016+) möchten wahrscheinlich etwas darüber wissen STRING_SPLIT.

Mit dieser neu eingebauten Funktion können Sie jetzt einfach verwenden

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

Einige Einschränkungen dieser Funktion und einige vielversprechende Ergebnisse von Leistungstests sind in diesem Blog-Beitrag von Aaron Bertrand enthalten .


13

Dies ähnelt am ehesten .NET für diejenigen unter Ihnen, die mit dieser Funktion vertraut sind:

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

Hier ist die Split-Funktion, die du gefragt hast

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

Führen Sie die Funktion folgendermaßen aus

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)

Quelle dieser Antwort: http://sqlhint.com/sqlserver/how-to/best-split-function-tsql-delimited


Während dies theoretisch die Frage beantworten kann, wäre es vorzuziehen , die wesentlichen Teile der Antwort hier aufzunehmen und den Link als Referenz bereitzustellen.
Xavi López

1
@ Xavi: ok, ich habe die wesentlichen Teile der Antwort aufgenommen. Danke für deinen Hinweis.
Mihai

3

Ich bin versucht, meine Lieblingslösung einzudrücken. Die resultierende Tabelle besteht aus 2 Spalten: PosIdx für die Position der gefundenen Ganzzahl; und Wert in Ganzzahl.


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

Es verwendet rekursiven CTE als Liste der Positionen, standardmäßig von 1 bis 100. Wenn Sie mit einer Zeichenfolge arbeiten müssen, die länger als 100 ist, rufen Sie diese Funktion einfach mit 'option (maxrecursion 4000)' wie folgt auf:


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 für die Erwähnung der Option maxrecursion. Natürlich sollte eine starke Rekursion in einer Produktionsumgebung mit Vorsicht angewendet werden, aber es ist großartig, CTEs zu verwenden, um umfangreiche Datenimport- oder -konvertierungsaufgaben auszuführen.
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

Verwendung

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

3

Dieser einfache CTE gibt an, was benötigt wird:

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 Vielleicht möchten Sie sich diese Antwort ansehen, sie ist effizienter als die akzeptierte Antwort und einfacher.
Michał Turczyn

2

Dies ist eine andere Version, die wirklich keine Einschränkungen hat (z. B. Sonderzeichen bei Verwendung des XML-Ansatzes, Anzahl der Datensätze im CTE-Ansatz) und die viel schneller ausgeführt wird, basierend auf einem Test mit mehr als 10 Millionen Datensätzen mit einer durchschnittlichen Länge der Quellzeichenfolge von 4000. Hoffe dies könnte helfen.

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

Die Verwendung der Tally-Tabelle ist eine Split-String-Funktion (bestmöglicher Ansatz) von 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
;

Empfohlen von Tally OH! Eine verbesserte SQL 8K "CSV Splitter" -Funktion


0

Dieser Blog lieferte eine ziemlich gute Lösung für die Verwendung von XML in T-SQL.

Dies ist die Funktion, die ich basierend auf diesem Blog entwickelt habe (Ändern Sie den Funktionsnamen und den Ergebnistyp je nach Bedarf):

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.