T SQL-Tabellenwertfunktion zum Teilen einer Spalte in Kommas


10

Ich habe in Microsoft SQL Server 2008 eine Tabellenwertfunktion geschrieben, um eine durch Kommas getrennte Spalte in einer Datenbank zu verwenden und für jeden Wert separate Zeilen auszuspucken.

Beispiel: "eins, zwei, drei, vier" würde eine neue Tabelle mit nur einer Spalte zurückgeben, die die folgenden Werte enthält:

one
two
three
four

Sieht dieser Code für euch fehleranfällig aus? Wenn ich es mit teste

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

es läuft nur für immer und gibt nie etwas zurück. Dies wird wirklich entmutigend, zumal es auf dem MSSQL-Server keine integrierten Split-Funktionen gibt (WARUM WARUM WARUM?!) Und alle ähnlichen Funktionen, die ich im Web gefunden habe, absoluter Papierkorb sind oder für das, was ich versuche, einfach irrelevant sind .

Hier ist die Funktion:

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

Antworten:


1

Es leicht überarbeitet ...

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

Ich würde das nicht mit einer Schleife machen; Es gibt viel bessere Alternativen. Bei weitem das Beste, wenn Sie sich trennen müssen , ist CLR, und Adam Machanics Ansatz ist der schnellste, den ich getestet habe .

Der nächstbeste Ansatz IMHO, wenn Sie CLR nicht implementieren können, ist eine Zahlentabelle:

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

... was diese Funktion ermöglicht:

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

Ich glaube, all dies wird eine bessere Leistung erbringen als die Funktion, die Sie haben, wenn Sie es zum Laufen bringen, zumal sie inline statt mit mehreren Anweisungen sind. Ich habe nicht untersucht, warum Ihre nicht funktioniert, weil ich nicht denke, dass es sich lohnt, diese Funktion zum Laufen zu bringen.

Aber das alles sagte ...

Gibt es einen Grund, warum Sie sich zuerst trennen müssen, da Sie SQL Server 2008 verwenden? Ich würde dafür lieber einen TVP verwenden:

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

Jetzt können Sie dies als Parameter für Ihre gespeicherten Prozeduren akzeptieren und den Inhalt wie einen TVF verwenden:

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

Und Sie können einen TVP direkt von C # usw. als DataTable übergeben. Dies übertrifft mit ziemlicher Sicherheit alle oben genannten Lösungen, insbesondere wenn Sie in Ihrer App eine durch Kommas getrennte Zeichenfolge erstellen, damit Ihre gespeicherte Prozedur einen TVP aufrufen kann, um sie erneut aufzuteilen. Weitere Informationen zu TVPs finden Sie in Erland Sommarskogs großartigem Artikel .

In jüngerer Zeit habe ich eine Reihe über das Teilen von Strings geschrieben:

Und wenn Sie SQL Server 2016 oder neuer (oder Azure SQL Database) verwenden, gibt es eine neue STRING_SPLITFunktion , über die ich hier gebloggt habe:


6

SQL Server 2016 hat die Funktion STRING_SPLIT () eingeführt . Es hat zwei Parameter - den zu zerhackenden String und das Trennzeichen. Die Ausgabe ist eine Zeile pro zurückgegebenem Wert.

Für das gegebene Beispiel

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

wird zurückkehren

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

1

Ich benutze und liebe Jeff Modens Saitensplitter, seit er herauskommt.

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

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

Geben Sie hier die Bildbeschreibung ein

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.