it-swarm-eu.dev

T SQL Tabulka Valued Funkce pro rozdělení sloupce na čárky

Napsal jsem funkci Table Valued Function na serveru Microsoft SQL Server 2008, abych v sloupci oddělil čárku v databázi a vyplivoval samostatné řádky pro každou hodnotu.

Příklad: "jeden, dva, tři, čtyři" by vrátil novou tabulku s pouze jedním sloupcem obsahujícím následující hodnoty:

one
two
three
four

Vypadá tento kód na vás náchylný k chybám? Když to vyzkouším

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

to prostě běží věčně a nikdy nic nevrací. Je to opravdu skličující zejména proto, že na serveru MSSQL nejsou vestavěné funkce rozdělení (PROČ PROČ PROČ ?!) a všechny podobné funkce, které jsem našel na webu, jsou absolutní odpadky nebo prostě irelevantní pro to, co se snažím dělat. .

Zde je funkce:

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,@[email protected])
                  SET @FoundIndex = CHARINDEX(@delimiter, @String)
                  INSERT @SplitValues (Asset_ID) VALUES (@ReturnValue)
            END

            RETURN
END
10
OvetS

Přepracoval jsem to trochu ...

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
1
Derek Kromm

Nedělal bych to se smyčkou; existují mnohem lepší alternativy. Zdaleka nejlepší, když se máte rozdělit, je CLR, a přístup Adam Machanic je nejrychlejší, který jsem testoval .

Dalším nejlepším přístupem IMHO, pokud nemůžete implementovat CLR, je tabulka čísel:

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

... což umožňuje tuto funkci:

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

Věřím, že všechny tyto funkce budou fungovat lépe než funkce, kterou máte, kdy to bude fungovat, zvláště proto, že jsou vloženy místo multi-prohlášení. Nezkoumal jsem, proč vaše nefunguje, protože si nemyslím, že stojí za to, aby tato funkce fungovala.

Ale to všechno řeklo ...

Protože používáte SQL Server 2008, existuje důvod, proč se musíte nejprve rozdělit? Raději bych na to použil TVP:

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

Nyní to můžete přijmout jako parametr vašich uložených procedur a používat obsah stejně jako byste použili 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

A můžete předat TVP přímo z C # atd. Jako DataTable. To téměř jistě překoná některá z výše uvedených řešení, zejména pokud v aplikaci vytváříte řetězec oddělený čárkami, aby vaše uložená procedura mohla zavolat TVP a znovu ji rozdělit. Pro více informací o TVP viz Erland Sommarskogův skvělý článek .

Nedávno jsem napsal řadu dělících řetězců:

A pokud používáte SQL Server 2016 nebo novější (nebo Azure SQL Database), existuje nový STRING_SPLIT function , o kterém jsem zde blogoval:

20
Aaron Bertrand

SQL Server 2016 představil funkci STRING_SPLIT () . Má dva parametry - řetězec, který má být nasekán, a oddělovač. Výstupem je jeden řádek na vrácenou hodnotu.

Pro daný příklad

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

vrátí se

value
------------------
one
two
three
four
6
Michael Green

Používám a miluji Jeff Modenův rozdělovač strun, právě od doby, kdy vyšel.

Tally OH! Vylepšená funkce SQL 8K „CSV Splitter“

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
;
1
Erik Darling