533 Stimmen

Wie teile ich einen abgegrenzten String auf, um auf einzelne Elemente zugreifen zu können?

Mit SQL Server, wie kann ich einen String aufteilen, um auf Element x zugreifen zu können?

Nehmen wir einen String "Hallo John Smith". Wie kann ich den String nach Leerzeichen aufteilen und auf das Element an Index 1 zugreifen, das "John" zurückgeben sollte?

3 Stimmen

5 Stimmen

Die höchsten Antworten hier sind - zumindest für mich - ziemlich altmodisch und eher veraltet. Prozedurale Logik, Schleifen, Rekursionen, CLR, Funktionen, viele Codezeilen... Es könnte interessant sein, die "aktiven" Antworten zu lesen, um aktuellere Ansätze zu finden.

0 Stimmen

Ich habe eine neue Antwort mit einem aktuelleren Ansatz hinzugefügt: stackoverflow.com/a/49669994/632604

371voto

Nathan Bedford Punkte 8604

Ich glaube nicht, dass SQL Server eine eingebaute Split-Funktion hat, also außer einer UDF ist die einzige andere Antwort, die ich kenne, die PARSENAME-Funktion zu übernehmen:

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 2) 

PARSENAME nimmt eine Zeichenkette und teilt sie am Punktzeichen auf. Es nimmt eine Zahl als zweites Argument, und diese Zahl gibt an, welches Segment der Zeichenkette zurückgegeben werden soll (von hinten nach vorne arbeiten).

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 3)  --gibt Hello zurück

Das offensichtliche Problem besteht darin, dass die Zeichenkette bereits einen Punkt enthält. Ich denke immer noch, dass die Verwendung einer UDF der beste Weg ist...irgendwelche anderen Vorschläge?

104 Stimmen

Vielen Dank, Saul...Ich sollte darauf hinweisen, dass diese Lösung für die tatsächliche Entwicklung wirklich keine gute Lösung ist. PARSENAME erwartet nur vier Teile, daher führt die Verwendung einer Zeichenfolge mit mehr als vier Teilen dazu, dass NULL zurückgegeben wird. Die UDF-Lösungen sind offensichtlich besser.

33 Stimmen

Dies ist ein großartiger Hack und lässt mich auch weinen, dass so etwas für etwas so verdammt simples in echten Sprachen notwendig ist.

37 Stimmen

Um die Indizes auf die "richtige" Weise funktionieren zu lassen, das heißt, bei 1 zu beginnen, habe ich Ihren Hijack mit REVERSE gekapert: REVERSE(PARSENAME(REPLACE(REVERSE('Hello John Smith'), ' ', '.'), 1)) -- Gibt Hello zurück

194voto

Jonesinator Punkte 4138

Sie finden möglicherweise die Lösung in _SQL User Defined Function to Parse a Delimited String_ hilfreich (von The Code Project).

Sie können diese einfache Logik verwenden:

Declare @products varchar(200) = '1|20|3|343|44|6|8765'
Declare @individual varchar(20) = null

WHILE LEN(@products) > 0
BEGIN
    IF PATINDEX('%|%', @products) > 0
    BEGIN
        SET @individual = SUBSTRING(@products,
                                    0,
                                    PATINDEX('%|%', @products))
        SELECT @individual

        SET @products = SUBSTRING(@products,
                                  LEN(@individual + '|') + 1,
                                  LEN(@products))
    END
    ELSE
    BEGIN
        SET @individual = @products
        SET @products = NULL
        SELECT @individual
    END
END

1 Stimmen

Warum SET @p_SourceText = RTRIM(LTRIM(@p_SourceText)) SET @w_Length = DATALENGTH(RTRIM(LTRIM(@p_SourceText))) und nicht SET @p_SourceText = RTRIM(LTRIM(@p_SourceText)) SET @w_Length = DATALENGTH(@p_SourceText)?

12 Stimmen

@GateKiller Diese Lösung unterstützt kein Unicode und verwendet fest codierte numerische (18,3) Werte, was sie nicht zu einer "wiederverwendbaren" Funktion macht.

4 Stimmen

Das funktioniert, belegt jedoch viel Speicher und verschwendet CPU-Ressourcen.

112voto

vzczc Punkte 8712

Zuerst erstellen Sie eine Funktion (mit CTE, Common Table Expression entfällt die Notwendigkeit einer temp-Tabelle)

 create function dbo.SplitString 
    (
        @str nvarchar(4000), 
        @separator char(1)
    )
    returns table
    AS
    return (
        with tokens(p, a, b) AS (
            select 
                1, 
                1, 
                charindex(@separator, @str)
            union all
            select
                p + 1, 
                b + 1, 
                charindex(@separator, @str, b + 1)
            from tokens
            where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
      )
    GO

Dann verwenden Sie es wie eine normale Tabelle (oder passen Sie es an, um in Ihrer vorhandenen gespeicherten Prozedur zu passen) wie folgt.

select s 
from dbo.SplitString('Hello John Smith', ' ')
where zeroBasedOccurance=1

Aktualisierung

Die vorherige Version würde bei einer Eingabezeichenfolge länger als 4000 Zeichen fehlschlagen. Diese Version überwindet diese Einschränkung:

create function dbo.SplitString 
(
    @str nvarchar(max), 
    @separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
    select 
        cast(1 as bigint), 
        cast(1 as bigint), 
        charindex(@separator, @str)
    union all
    select
        p + 1, 
        b + 1, 
        charindex(@separator, @str, b + 1)
    from tokens
    where b > 0
)
select
    p-1 ItemIndex,
    substring(
        @str, 
        a, 
        case when b > 0 then b-a ELSE LEN(@str) end) 
    AS s
from tokens
);

GO

Die Verwendung bleibt gleich.

14 Stimmen

Es ist elegant, funktioniert jedoch nur für 100 Elemente aufgrund der Begrenzung der Rekursionstiefe.

4 Stimmen

@Pking, nein, der Standardwert ist 100 (um eine Endlosschleife zu verhindern). Verwenden Sie den MAXRECURSION-Hinweis, um die Anzahl der Rekursionsebenen (0 bis 32767, 0 bedeutet "keine Begrenzung" - kann den Server zum Absturz bringen) zu definieren. Übrigens, viel bessere Antwort als PARSENAME, weil es universell ist :-). +1

0 Stimmen

Das Hinzufügen von maxrecursion zu dieser Lösung beachten Sie diese Frage und ihre Antworten Wie richte ich die maxrecursion-Option für eine CTE innerhalb einer tabelarischen Funktionsfunktion ein.

74voto

Aaron Bertrand Punkte 259330

Die meisten Lösungen hier verwenden while-Schleifen oder rekursive CTEs. Ein set-basierter Ansatz wird überlegen sein, versprochen, wenn Sie ein Trennzeichen verwenden können, das nicht ein Leerzeichen ist:

CREATE FUNCTION [dbo].[SplitString]
    (
        @List NVARCHAR(MAX),
        @Delim VARCHAR(255)
    )
    RETURNS TABLE
    AS
        RETURN ( SELECT [Value], idx = RANK() OVER (ORDER BY n) FROM 
          ( 
            SELECT n = Number, 
              [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
              CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
            FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
              FROM sys.all_objects) AS x
              WHERE Number <= LEN(@List)
              AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim)) = @Delim
          ) AS y
        );

Beispielhafte Verwendung:

SELECT Value FROM dbo.SplitString('foo,bar,blat,foo,splunge',',')
  WHERE idx = 3;

Ergebnisse:

----
blat

Sie könnten auch das idx, das Sie möchten, als Argument an die Funktion übergeben, aber das überlasse ich dem Leser als Übung.

Dies ist nicht möglich nur mit der nativen STRING_SPLIT-Funktion, die in SQL Server 2016 hinzugefügt wurde, da nicht garantiert ist, dass die Ausgabe in der Reihenfolge der ursprünglichen Liste angezeigt wird. Mit anderen Worten, wenn Sie 3,6,1 übergeben, wird das Ergebnis wahrscheinlich in dieser Reihenfolge sein, aber es könnte auch 1,3,6 sein. Ich habe um Hilfe aus der Community gebeten, um die eingebaute Funktion hier zu verbessern:

Mit genügend qualitativem Feedback werden sie vielleicht tatsächlich einige dieser Verbesserungen in Betracht ziehen:

Mehr über Split-Funktionen, warum (und Beweis dafür), dass while-Schleifen und rekursive CTEs nicht skalieren, und bessere Alternativen, wenn Strings aus der Anwendungsebene kommen:

Auf SQL Server 2016 oder höher sollten Sie jedoch STRING_SPLIT() und STRING_AGG() verwenden:

1 Stimmen

Beste Antwort, meiner bescheidenen Meinung nach. In einigen anderen Antworten gibt es das Problem des SQL-Rekursionslimits von 100, aber nicht in diesem Fall. Sehr schnelle und sehr einfache Umsetzung. Wo ist der +2-Button?

0 Stimmen

Danke an Aaron; Aber kann mir jemand erklären, warum, wenn ich varchar (nicht varchar(max)) als Argument übergebe, diese Funktion eine leere Liste zurückgibt? wie declare @list varchar = 'etwas'; select from dbo.SplitString(@list, ';');

0 Stimmen

@Mikhail Weil varchar ohne Länge entweder varchar(30) oder varchar(1) je nach Kontext sein kann. Versuchen Sie nicht, dieses Problem zu verstehen - verwenden Sie diese Syntax einfach nicht. Niemals.

41voto

Shnugo Punkte 64209

Diese Frage dreht sich nicht um einen String-Split-Ansatz, sondern darum, wie man das n-te Element erhält.

Die meisten Antworten hier verwenden irgendeine Form von String-Splitting mit Rekursion, CTEs, mehrere CHARINDEX, REVERSE und PATINDEX, erfinden Funktionen, rufen CLR-Methoden auf, verwenden Nummertabellen, CROSS APPLY... Die meisten Antworten bestehen aus vielen Codezeilen.

Aber - wenn du wirklich nichts weiter als einen Ansatz benötigst, um das n-te Element zu erhalten - kann dies als echter Einzeiler gemacht werden, keine UDF, nicht einmal ein Sub-Select... Und als zusätzlicher Vorteil: typsicher

Hole Teil 2, der durch ein Leerzeichen begrenzt ist:

DECLARE @input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'' + REPLACE(@input,N' ',N'') + N'' AS XML).value('/x[2]','nvarchar(max)')

Natürlich kannst du Variablen verwenden für das Trennzeichen und die Position (verwende sql:column, um die Position direkt aus dem Wert einer Abfrage abzurufen):

DECLARE @dlmt NVARCHAR(10)=N' ';
DECLARE @pos INT = 2;
SELECT CAST(N'' + REPLACE(@input,@dlmt,N'') + N'' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)')

Wenn dein String möglicherweise verbotene Zeichen enthält (insbesondere eines unter &><), kannst du es trotzdem auf diese Weise tun. Verwende einfach zuerst FOR XML PATH auf deinem String, um alle verbotenen Zeichen implizit durch die passende Escape-Sequenz zu ersetzen.

Es ist ein sehr spezieller Fall, wenn - zusätzlich - dein Trennzeichen das Semikolon ist. In diesem Fall ersetze ich das Trennzeichen zuerst durch '#DLMT#' und ersetze dies schließlich durch die XML-Tags:

SET @input=N'Some <, > and &;Other äöü@€;One more';
SET @dlmt=N';';
SELECT CAST(N'' + REPLACE((SELECT REPLACE(@input,@dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'') + N'' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)');

UPDATE für SQL-Server 2016+

Bedauerlicherweise haben die Entwickler vergessen, den Index des Teils mit STRING_SPLIT zurückzugeben. Aber, unter Verwendung von SQL-Server 2016+ gibt es JSON_VALUE und OPENJSON.

Mit JSON_VALUE können wir die Position als Index-Array übergeben.

Für OPENJSON besagt die Dokumentation klar:

Wenn OPENJSON ein JSON-Array analysiert, gibt die Funktion die Indizes der Elemente im JSON-Text als Schlüssel zurück.

Ein String wie 1,2,3 braucht nichts weiter als Klammern: [1,2,3].
Ein String von Wörtern wie this is an example muss als ["this","is","an","example"] vorliegen.
Dies sind sehr einfache Zeichenkettenoperationen. Probiere es einfach aus:

DECLARE @str VARCHAR(100)='Hello John Smith';
DECLARE @position INT = 2;

--Wir können den JSON-Pfad '$[1]' mit CONCAT aufbauen
SELECT JSON_VALUE('["' + REPLACE(@str,' ','","') + '"]',CONCAT('$[',@position-1,']'));

--Siehe dies für einen positions sicheren String-Splitter (nullbasiert):

SELECT  JsonArray.[key] AS [Position]
       ,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(@str,' ','","') + '"]') JsonArray

In diesem Beitrag habe ich verschiedene Ansätze getestet und festgestellt, dass OPENJSON wirklich schnell ist. Sogar viel schneller als die berühmte "delimitedSplit8k()" Methode...

UPDATE 2 - Die Werte typsicher abrufen

Wir können ganz einfach ein Array innerhalb eines Arrays durch die Verwendung von verdoppelten [[]] erstellen. Dies ermöglicht eine getypte WITH-Klausel:

DECLARE  @SomeDelimitedString VARCHAR(100)='part1|1|20190920';

DECLARE @JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(@SomeDelimitedString,'|','","'),'"]]');

SELECT @SomeDelimitedString          AS TheOriginal
      ,@JsonArray                    AS TransformedToJSON
      ,ValuesFromTheArray.*
FROM OPENJSON(@JsonArray)
WITH(TheFirstFragment  VARCHAR(100) '$[0]'
    ,TheSecondFragment INT          '$[1]'
    ,TheThirdFragment  DATE         '$[2]') ValuesFromTheArray

0 Stimmen

Re: Wenn Ihre Zeichenfolge verbotene Zeichen enthalten könnte ... könnten Sie die Teilzeichenfolgen einfach so einwickeln x]]>.

0 Stimmen

@SalmanA, ja, CDATA-Abschnitte können damit auch umgehen... Aber nach dem Cast sind sie weg (zu escaped text() implizit geändert). Ich mag kein magisches Under-the-Hood-Ding, daher bevorzuge ich den (SELECT 'Text mit <&>' AS [*] FOR XML PATH('')) - Ansatz. Das sieht für mich sauberer aus und passiert sowieso... (Mehr über CDATA und XML).

1 Stimmen

Bevor Sie das ERSETZEN in der JSON-Version ausführen, sollten Sie STRING_ESCAPE(@SomeDelimitedString, 'JSON') aufrufen dbfiddle.uk/mWShPwRP

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X