Biến một chuỗi Dấu phẩy được phân tách thành các hàng riêng lẻ


234

Tôi có một Bảng SQL như thế này:

| SomeID         | OtherID     | Data
+----------------+-------------+-------------------
| abcdef-.....   | cdef123-... | 18,20,22
| abcdef-.....   | 4554a24-... | 17,19
| 987654-.....   | 12324a2-... | 13,19,20

Có một truy vấn mà tôi có thể thực hiện một truy vấn như thế SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......'trả về các hàng riêng lẻ, như thế này:

| OtherID     | SplitData
+-------------+-------------------
| cdef123-... | 18
| cdef123-... | 20
| cdef123-... | 22
| 4554a24-... | 17
| 4554a24-... | 19

Về cơ bản phân chia dữ liệu của tôi tại dấu phẩy thành các hàng riêng lẻ?

Tôi biết rằng việc lưu trữ một comma-separatedchuỗi vào cơ sở dữ liệu quan hệ nghe có vẻ ngu ngốc, nhưng trường hợp sử dụng thông thường trong ứng dụng tiêu dùng làm cho điều đó thực sự hữu ích.

Tôi không muốn phân chia ứng dụng vì tôi cần phân trang, vì vậy tôi muốn khám phá các tùy chọn trước khi cấu trúc lại toàn bộ ứng dụng.

Đó là SQL Server 2008(không phải R2).


Câu trả lời:


265

Bạn có thể sử dụng các hàm đệ quy tuyệt vời từ SQL Server:


Bảng mẫu:

CREATE TABLE Testdata
(
    SomeID INT,
    OtherID INT,
    String VARCHAR(MAX)
)

INSERT Testdata SELECT 1,  9, '18,20,22'
INSERT Testdata SELECT 2,  8, '17,19'
INSERT Testdata SELECT 3,  7, '13,19,20'
INSERT Testdata SELECT 4,  6, ''
INSERT Testdata SELECT 9, 11, '1,2,3,4'

Truy vấn

;WITH tmp(SomeID, OtherID, DataItem, String) AS
(
    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM Testdata
    UNION all

    SELECT
        SomeID,
        OtherID,
        LEFT(String, CHARINDEX(',', String + ',') - 1),
        STUFF(String, 1, CHARINDEX(',', String + ','), '')
    FROM tmp
    WHERE
        String > ''
)

SELECT
    SomeID,
    OtherID,
    DataItem
FROM tmp
ORDER BY SomeID
-- OPTION (maxrecursion 0)
-- normally recursion is limited to 100. If you know you have very long
-- strings, uncomment the option

Đầu ra

 SomeID | OtherID | DataItem 
--------+---------+----------
 1      | 9       | 18       
 1      | 9       | 20       
 1      | 9       | 22       
 2      | 8       | 17       
 2      | 8       | 19       
 3      | 7       | 13       
 3      | 7       | 19       
 3      | 7       | 20       
 4      | 6       |          
 9      | 11      | 1        
 9      | 11      | 2        
 9      | 11      | 3        
 9      | 11      | 4        

1
Mã không hoạt động nếu thay đổi kiểu dữ liệu của cột Datatừ varchar(max)sang varchar(4000), ví dụ create table Testdata(SomeID int, OtherID int, Data varchar(4000))?
ca9163d9

4
@NickW điều này có thể là do các phần trước và sau UNION ALL trả về các loại khác nhau từ hàm LEFT. Cá nhân tôi không thấy lý do tại sao bạn sẽ không nhảy lên MAX khi bạn đạt tới 4000 ...
RichardTheKiwi

Đối với bộ giá trị LỚN, giá trị này có thể vượt quá giới hạn đệ quy cho CTE.
DSz

3
@dsz Đó là khi bạn sử dụngOPTION (maxrecursion 0)
RichardTheKiwi

14
Các hàm LEFT có thể cần một CAST để hoạt động .... ví dụ: LEFT (CAST (Data AS VARCHAR (MAX)) ....
smoore4 15/07/16

141

Cuối cùng, sự chờ đợi đã kết thúc với SQL Server 2016 . Họ đã giới thiệu hàm Split chuỗi , STRING_SPLIT:

select OtherID, cs.Value --SplitData
from yourtable
cross apply STRING_SPLIT (Data, ',') cs

Tất cả các phương thức khác để phân tách chuỗi như XML, bảng Tally, vòng lặp while, v.v. đã bị STRING_SPLIThàm này thổi bay .

Dưới đây là một bài viết tuyệt vời với so sánh hiệu suất: Hiệu suất bất ngờ và Giả định: STRINGinksLIT .

Đối với các phiên bản cũ hơn, sử dụng bảng kiểm đếm ở đây là một hàm chuỗi tách (cách tiếp cận tốt nhất có thể)

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
;

Được giới thiệu từ Tally OH! Một chức năng Splitter Splitter SQL 8K được cải tiến


9
câu trả lời rất quan trọng
Syed Md. Kamruzzaman

Tôi sẽ sử dụng STRINGinksLIT nếu chỉ có máy chủ trên SQL Server 2016! BTW theo trang bạn đã liên kết đến, tên trường mà nó xuất ra value, không phải SplitData.
Stewart

89

Kiểm tra điều này

 SELECT A.OtherID,  
     Split.a.value('.', 'VARCHAR(100)') AS Data  
 FROM  
 (
     SELECT OtherID,  
         CAST ('<M>' + REPLACE(Data, ',', '</M><M>') + '</M>' AS XML) AS Data  
     FROM  Table1
 ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

8
Khi sử dụng phương pháp này, bạn phải đảm bảo rằng không có giá trị nào của bạn chứa thứ gì đó là XML bất hợp pháp
user1151923

Điều đó thật tuyệt. Tôi có thể hỏi bạn, tôi sẽ viết lại như thế nào nếu tôi muốn cột mới chỉ hiển thị ký tự đầu tiên từ chuỗi phân tách của tôi?
Kiểm soát

Điều này làm việc hoàn hảo, cảm ơn bạn! Tôi đã phải cập nhật giới hạn VARCHAR nhưng nó đã hoạt động hoàn hảo sau đó.
chazbot7

Tôi phải nói với bạn rằng phương thức đó là "lovl" (cảm nhận được tình yêu?) Được gọi là "Phương pháp bộ chia XML" và gần như chậm như một Vòng lặp While hoặc CTE đệ quy. Tôi thực sự khuyên bạn nên tránh nó mọi lúc. Sử dụng DeliatedSplit8K thay thế. Nó thổi bay mọi thứ trừ chức năng Split_String () năm 2016 hoặc CLR được viết tốt.
Jeff Moden

20
select t.OtherID,x.Kod
    from testData t
    cross apply (select Code from dbo.Split(t.Data,',') ) x

3
Có chính xác những gì tôi đã theo sau và dễ đọc hơn nhiều ví dụ khác không (với điều kiện là đã có một hàm trong DB để phân tách chuỗi phân tách). Như một người trước đây không quen thuộc CROSS APPLY, điều đó khá hữu ích!
tobriand

Tôi không thể hiểu phần này (chọn Mã từ dbo.Split (t.Data, ','))? dbo.Split là một bảng tồn tại và này là Cột trong bảng Chia? tôi không thể tìm thấy danh sách các bảng hoặc giá trị ở bất kỳ đâu trong Trang này?
Jayendran

1
Mã làm việc của tôi là:select t.OtherID, x.* from testData t cross apply (select item as Data from dbo.Split(t.Data,',') ) x
Akbar Kautsar

12

Kể từ tháng 2 năm 2016 - xem ví dụ về bảng TALLY - rất có khả năng vượt trội hơn TVF của tôi bên dưới, kể từ tháng 2 năm 2014. Giữ bài đăng gốc bên dưới cho hậu thế:


Quá nhiều mã lặp lại theo ý thích của tôi trong các ví dụ trên. Và tôi không thích hiệu suất của CTE và XML. Ngoài ra, một điều rõ ràng Idđể người tiêu dùng đặt hàng cụ thể có thể chỉ định một ORDER BYđiều khoản.

CREATE FUNCTION dbo.Split
(
    @Line nvarchar(MAX),
    @SplitOn nvarchar(5) = ','
)
RETURNS @RtnValue table
(
    Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
    Data nvarchar(100) NOT NULL
)
AS
BEGIN
    IF @Line IS NULL RETURN

    DECLARE @split_on_len INT = LEN(@SplitOn)
    DECLARE @start_at INT = 1
    DECLARE @end_at INT
    DECLARE @data_len INT

    WHILE 1=1
    BEGIN
        SET @end_at = CHARINDEX(@SplitOn,@Line,@start_at)
        SET @data_len = CASE @end_at WHEN 0 THEN LEN(@Line) ELSE @end_at-@start_at END
        INSERT INTO @RtnValue (data) VALUES( SUBSTRING(@Line,@start_at,@data_len) );
        IF @end_at = 0 BREAK;
        SET @start_at = @end_at + @split_on_len
    END

    RETURN
END

6

Rất vui khi thấy rằng nó đã được giải quyết trong phiên bản 2016, nhưng đối với tất cả những điều không có trong đó, đây là hai phiên bản tổng quát và đơn giản hóa của các phương pháp ở trên.

Phương thức XML ngắn hơn, nhưng tất nhiên yêu cầu chuỗi phải cho phép xml-trick (không có ký tự 'xấu').

Phương pháp XML:

create function dbo.splitString(@input Varchar(max), @Splitter VarChar(99)) returns table as
Return
    SELECT Split.a.value('.', 'VARCHAR(max)') AS Data FROM
    ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data 
    ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a); 

Phương pháp đệ quy:

create function dbo.splitString(@input Varchar(max), @Splitter Varchar(99)) returns table as
Return
  with tmp (DataItem, ix) as
   ( select @input  , CHARINDEX('',@Input)  --Recu. start, ignored val to get the types right
     union all
     select Substring(@input, ix+1,ix2-ix-1), ix2
     from (Select *, CHARINDEX(@Splitter,@Input+@Splitter,ix+1) ix2 from tmp) x where ix2<>0
   ) select DataItem from tmp where ix<>0

Chức năng hoạt động

Create table TEST_X (A int, CSV Varchar(100));
Insert into test_x select 1, 'A,B';
Insert into test_x select 2, 'C,D';

Select A,data from TEST_X x cross apply dbo.splitString(x.CSV,',') Y;

Drop table TEST_X

PHƯƠNG PHÁP XML 2: Thân thiện với Unicode ( Phép lịch sự bổ sung của Max Hodges) create function dbo.splitString(@input nVarchar(max), @Splitter nVarchar(99)) returns table as Return SELECT Split.a.value('.', 'NVARCHAR(max)') AS Data FROM ( SELECT CAST ('<M>' + REPLACE(@input, @Splitter, '</M><M>') + '</M>' AS XML) AS Data ) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);


1
Điều này có vẻ rõ ràng, nhưng làm thế nào để bạn sử dụng hai chức năng này? Đặc biệt, bạn có thể chỉ cho bạn cách sử dụng nó trong trường hợp sử dụng của OP không?
jpaugh

1
Dưới đây là một ví dụ nhanh: Tạo bảng TEST_X (A int, CSV Varchar (100)); Chèn vào test_x chọn 1, 'A, B'; Chèn vào test_x chọn 2, 'C, D'; Chọn A, dữ liệu từ TEST_X x áp dụng chéo dbo.splitString (x.CSV, ',') Y; Bàn thả TEST_X
Eske Rahn

Đây chính xác là những gì tôi cần! Cảm ơn bạn.
Nitin Badole

5

Vui lòng tham khảo bên dưới TSQL. Chức năng STRINGinksLIT chỉ khả dụng dưới mức tương thích 130 trở lên.

TSQL:

DECLARE @stringValue NVARCHAR(400) = 'red,blue,green,yellow,black'  
DECLARE @separator CHAR = ','

SELECT [value]  As Colour
FROM STRING_SPLIT(@stringValue, @separator); 

KẾT QUẢ:

Màu sắc

đỏ xanh xanh vàng đen


5

Rất muộn nhưng hãy thử điều này:

SELECT ColumnID, Column1, value  --Do not change 'value' name. Leave it as it is.
FROM tbl_Sample  
CROSS APPLY STRING_SPLIT(Tags, ','); --'Tags' is the name of column containing comma separated values

Vì vậy, chúng tôi đã có điều này: tbl_Sample:

ColumnID|   Column1 |   Tags
--------|-----------|-------------
1       |   ABC     |   10,11,12    
2       |   PQR     |   20,21,22

Sau khi chạy truy vấn này:

ColumnID|   Column1 |   value
--------|-----------|-----------
1       |   ABC     |   10
1       |   ABC     |   11
1       |   ABC     |   12
2       |   PQR     |   20
2       |   PQR     |   21
2       |   PQR     |   22

Cảm ơn!


STRING_SPLITtuy tiện lợi nhưng nó yêu cầu SQL Server 2016. docs.microsoft.com/en-us/sql/t-sql/fifts/ trộm
Craig Silver

giải pháp thanh lịch.
Sangram Nandkhile

3
DECLARE @id_list VARCHAR(MAX) = '1234,23,56,576,1231,567,122,87876,57553,1216'
DECLARE @table TABLE ( id VARCHAR(50) )
DECLARE @x INT = 0
DECLARE @firstcomma INT = 0
DECLARE @nextcomma INT = 0

SET @x = LEN(@id_list) - LEN(REPLACE(@id_list, ',', '')) + 1 -- number of ids in id_list

WHILE @x > 0
    BEGIN
        SET @nextcomma = CASE WHEN CHARINDEX(',', @id_list, @firstcomma + 1) = 0
                              THEN LEN(@id_list) + 1
                              ELSE CHARINDEX(',', @id_list, @firstcomma + 1)
                         END
        INSERT  INTO @table
        VALUES  ( SUBSTRING(@id_list, @firstcomma + 1, (@nextcomma - @firstcomma) - 1) )
        SET @firstcomma = CHARINDEX(',', @id_list, @firstcomma + 1)
        SET @x = @x - 1
    END

SELECT  *
FROM    @table

Đây là một trong số ít các phương thức hoạt động với hỗ trợ SQL giới hạn trong Kho dữ liệu SQL Azure.
Aaron Schultz

1
;WITH tmp(SomeID, OtherID, DataItem, Data) as (
    SELECT SomeID, OtherID, LEFT(Data, CHARINDEX(',',Data+',')-1),
        STUFF(Data, 1, CHARINDEX(',',Data+','), '')
FROM Testdata
WHERE Data > ''
)
SELECT SomeID, OtherID, Data
FROM tmp
ORDER BY SomeID

chỉ với một chút sửa đổi nhỏ cho truy vấn trên ...


6
Bạn có thể giải thích ngắn gọn về việc đây là một cải tiến so với phiên bản trong câu trả lời được chấp nhận không?
Leigh

Không có liên minh tất cả ... ít mã. Vì nó đang sử dụng liên minh thay vì liên minh, nên không phải là một sự khác biệt hiệu suất?
TamusJRoyce

1
Điều này đã không trả lại tất cả các hàng cần có. Tôi không chắc chắn những gì về dữ liệu yêu cầu tất cả liên minh, nhưng giải pháp của bạn trả về cùng số lượng hàng như bảng ban đầu.
Oedhel Setren 27/2/2015

1
(vấn đề ở đây là phần đệ quy là phần bị bỏ qua ...)
Eske Rahn

Không mang lại cho tôi sản lượng dự kiến ​​chỉ đưa ra bản ghi đầu tiên trong hàng riêng biệt
Ankit Misra

1

Khi sử dụng phương pháp này, bạn phải đảm bảo rằng không có giá trị nào của bạn chứa thứ gì đó là XML bất hợp pháp - user1151923

Tôi luôn sử dụng phương thức XML. Hãy chắc chắn rằng bạn sử dụng VALID XML. Tôi có hai chức năng để chuyển đổi giữa XML và Văn bản hợp lệ. (Tôi có xu hướng loại bỏ tiền gửi xe ngựa vì tôi thường không cần chúng.

CREATE FUNCTION dbo.udf_ConvertTextToXML (@Text varchar(MAX)) 
    RETURNS varchar(MAX)
AS
    BEGIN
        SET @Text = REPLACE(@Text,CHAR(10),'')
        SET @Text = REPLACE(@Text,CHAR(13),'')
        SET @Text = REPLACE(@Text,'<','&lt;')
        SET @Text = REPLACE(@Text,'&','&amp;')
        SET @Text = REPLACE(@Text,'>','&gt;')
        SET @Text = REPLACE(@Text,'''','&apos;')
        SET @Text = REPLACE(@Text,'"','&quot;')
    RETURN @Text
END


CREATE FUNCTION dbo.udf_ConvertTextFromXML (@Text VARCHAR(MAX)) 
    RETURNS VARCHAR(max)
AS
    BEGIN
        SET @Text = REPLACE(@Text,'&lt;','<')
        SET @Text = REPLACE(@Text,'&amp;','&')
        SET @Text = REPLACE(@Text,'&gt;','>')
        SET @Text = REPLACE(@Text,'&apos;','''')
        SET @Text = REPLACE(@Text,'&quot;','"')
    RETURN @Text
END

1
Có một vấn đề nhỏ với mã bạn đã nhận được ở đó. Nó sẽ thay đổi '<' thành '& amp; lt;' thay vì '& lt;' thích nó nên Vì vậy, bạn cần mã hóa '&' trước tiên.
Stewart

Không cần chức năng như vậy ... Chỉ cần sử dụng các khả năng tiềm ẩn. Hãy thử điều này:SELECT (SELECT '<&> blah' + CHAR(13)+CHAR(10) + 'next line' FOR XML PATH(''))
Shnugo

1

Chức năng

CREATE FUNCTION dbo.SplitToRows (@column varchar(100), @separator varchar(10))
RETURNS @rtnTable TABLE
  (
  ID int identity(1,1),
  ColumnA varchar(max)
  )
 AS
BEGIN
    DECLARE @position int = 0
    DECLARE @endAt int = 0
    DECLARE @tempString varchar(100)

    set @column = ltrim(rtrim(@column))

    WHILE @position<=len(@column)
    BEGIN       
        set @endAt = CHARINDEX(@separator,@column,@position)
            if(@endAt=0)
            begin
            Insert into @rtnTable(ColumnA) Select substring(@column,@position,len(@column)-@position)
            break;
            end
        set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

        Insert into @rtnTable(ColumnA) select @tempString
        set @position=@endAt+1;
    END
    return
END

Ca sử dụng

select * from dbo.SplitToRows('T14; p226.0001; eee; 3554;', ';')

Hoặc chỉ là một lựa chọn với nhiều tập kết quả

DECLARE @column varchar(max)= '1234; 4748;abcde; 324432'
DECLARE @separator varchar(10) = ';'
DECLARE @position int = 0
DECLARE @endAt int = 0
DECLARE @tempString varchar(100)

set @column = ltrim(rtrim(@column))

WHILE @position<=len(@column)
BEGIN       
    set @endAt = CHARINDEX(@separator,@column,@position)
        if(@endAt=0)
        begin
        Select substring(@column,@position,len(@column)-@position)
        break;
        end
    set @tempString = substring(ltrim(rtrim(@column)),@position,@endAt-@position)

    select @tempString
    set @position=@endAt+1;
END

Sử dụng một vòng lặp while bên trong một hàm có giá trị của bảng nhiều tầng chỉ là cách tồi tệ nhất có thể để phân tách các chuỗi. Có rất nhiều tùy chọn dựa trên câu hỏi này.
Sean Lange

0

Dưới đây hoạt động trên máy chủ sql 2008

select *, ROW_NUMBER() OVER(order by items) as row# 
from 
( select 134 myColumn1, 34 myColumn2, 'd,c,k,e,f,g,h,a' comaSeperatedColumn) myTable
    cross apply 
SPLIT (rtrim(comaSeperatedColumn), ',') splitedTable -- gives 'items'  column 

Sẽ nhận được tất cả sản phẩm của Cartesian với các cột của bảng gốc cộng với "các mục" của bảng chia.


0

Bạn có thể sử dụng chức năng sau để trích xuất dữ liệu

CREATE FUNCTION [dbo].[SplitString]
(    
    @RowData NVARCHAR(MAX),
    @Delimeter NVARCHAR(MAX)
)
RETURNS @RtnValue TABLE 
(
    ID INT IDENTITY(1,1),
    Data NVARCHAR(MAX)
) 
AS
BEGIN 
    DECLARE @Iterator INT
    SET @Iterator = 1

    DECLARE @FoundIndex INT
    SET @FoundIndex = CHARINDEX(@Delimeter,@RowData)

    WHILE (@FoundIndex>0)
    BEGIN
        INSERT INTO @RtnValue (data)
        SELECT 
            Data = LTRIM(RTRIM(SUBSTRING(@RowData, 1, @FoundIndex - 1)))

        SET @RowData = SUBSTRING(@RowData,
                @FoundIndex + DATALENGTH(@Delimeter) / 2,
                LEN(@RowData))

        SET @Iterator = @Iterator + 1
        SET @FoundIndex = CHARINDEX(@Delimeter, @RowData)
    END

    INSERT INTO @RtnValue (Data)
    SELECT Data = LTRIM(RTRIM(@RowData))

    RETURN
END

Sử dụng một vòng lặp while bên trong một hàm có giá trị của bảng nhiều tầng chỉ là cách tồi tệ nhất có thể để phân tách các chuỗi. Có rất nhiều tùy chọn dựa trên câu hỏi này.
Sean Lange
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.