ฟังก์ชัน SQL ตารางที่มีค่าเพื่อแยกคอลัมน์บนเครื่องหมายจุลภาค


10

ฉันเขียน Table Valued Function ใน Microsoft SQL Server 2008 เพื่อใช้คอลัมน์คั่นด้วยเครื่องหมายจุลภาคในฐานข้อมูลเพื่อแยกแถวที่แยกออกสำหรับแต่ละค่า

เช่น: "หนึ่งสองสามสี่" จะส่งคืนตารางใหม่โดยมีเพียงหนึ่งคอลัมน์ที่มีค่าต่อไปนี้:

one
two
three
four

รหัสนี้มีข้อผิดพลาดเกิดขึ้นกับคุณหรือเปล่า? เมื่อฉันทดสอบด้วย

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

มันวิ่งไปตลอดกาลและจะไม่ส่งคืนสิ่งใดเลย นี่เป็นสิ่งที่ทำให้ท้อใจโดยเฉพาะอย่างยิ่งเนื่องจากไม่มีฟังก์ชั่นแยกในเซิร์ฟเวอร์ MSSQL (ทำไมเพราะอะไร?!) และฟังก์ชั่นที่คล้ายกันทั้งหมดที่ฉันพบบนเว็บเป็นถังขยะแน่นอนหรือไม่เกี่ยวข้องกับสิ่งที่ฉันพยายามทำ .

นี่คือฟังก์ชั่น:

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

คำตอบ:


1

ทำงานซ้ำอีกเล็กน้อย ...

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

ฉันจะไม่ทำสิ่งนี้ด้วยการวนซ้ำ; มีทางเลือกที่ดีกว่ามาก โดยไกลที่ดีที่สุดเมื่อคุณมีการแยกเป็น CLR และวิธีการที่อดัมช่างเป็นที่เร็วที่สุดที่ผมเคยทดสอบ

วิธีที่ดีที่สุดถัดไป IMHO หากคุณไม่สามารถใช้ CLR ได้คือตารางตัวเลข:

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

... ซึ่งอนุญาตให้ใช้ฟังก์ชันนี้:

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

ฉันเชื่อว่าสิ่งเหล่านี้จะทำงานได้ดีกว่าฟังก์ชั่นที่คุณมีเมื่อคุณทำให้มันทำงานโดยเฉพาะอย่างยิ่งเนื่องจากมันเป็นแบบอินไลน์แทนที่จะเป็นแบบหลายคำสั่ง ฉันไม่ได้ตรวจสอบว่าทำไมคุณถึงไม่ทำงานเพราะฉันคิดว่ามันไม่คุ้มค่าที่จะให้ฟังก์ชั่นนั้นใช้งานได้

แต่ทุกคนพูดว่า ...

เมื่อคุณใช้ SQL Server 2008 มีเหตุผลที่คุณต้องแยกกันตั้งแต่แรกไหม? ฉันอยากจะใช้ TVP สำหรับสิ่งนี้:

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

ตอนนี้คุณสามารถยอมรับสิ่งนี้เป็นพารามิเตอร์ของโพรซีเดอร์ที่เก็บไว้และใช้เนื้อหาเช่นเดียวกับที่คุณใช้ 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

และคุณสามารถส่ง TVP โดยตรงจาก C # และอื่น ๆ ในรูปแบบ DataTable วิธีนี้จะมีประสิทธิภาพเหนือกว่าโซลูชันใด ๆ ข้างต้นโดยเฉพาะอย่างยิ่งถ้าคุณกำลังสร้างสตริงที่คั่นด้วยเครื่องหมายจุลภาคในแอปของคุณโดยเฉพาะเพื่อให้ขั้นตอนการจัดเก็บของคุณสามารถเรียก TVP แยกออกอีกครั้ง สำหรับข้อมูลเพิ่มเติมเกี่ยวกับจำนวนมาก TVPs ดูบทความดีดี Erland Sommarskog ของ

ไม่นานมานี้ฉันได้เขียนชุดข้อความโดยแยกสตริง:

และถ้าคุณใช้ SQL Server 2016 หรือใหม่กว่า (หรือฐานข้อมูล Azure SQL) มีฟังก์ชั่นใหม่STRING_SPLITซึ่งฉันบล็อกเกี่ยวกับที่นี่:


6

SQL Server 2016 แนะนำฟังก์ชันSTRING_SPLIT () มันมีสองพารามิเตอร์ - สตริงที่จะสับและตัวแยก ผลลัพธ์คือหนึ่งแถวต่อค่าที่ส่งคืน

สำหรับตัวอย่างที่กำหนด

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

จะกลับมา

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

1

ฉันได้ใช้และรักตัวแยกสายของ Jeff Moden เพียงเกี่ยวกับมันออกมา

นับ OH! ฟังก์ชั่น“ CSV Splitter” SQL 8K ที่ได้รับการปรับปรุง

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

ป้อนคำอธิบายรูปภาพที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.