Ciąg podziału T-SQL


139

Mam kolumnę SQL Server 2008 R2 zawierającą ciąg, który muszę podzielić przecinkiem. Widziałem wiele odpowiedzi na StackOverflow, ale żadna z nich nie działa w R2. Upewniłem się, że mam uprawnienia do wybierania dowolnych przykładów funkcji podziału. Każda pomoc mile widziana.


7
To jedna z milionów odpowiedzi, które lubię stackoverflow.com/a/1846561/227755
nurettin

2
Co masz na myśli mówiąc „żaden z nich nie działa”? Czy mógłbyś to sprecyzować?
Aaron Bertrand

Andy wskazał mi właściwy kierunek, ponieważ wykonywałem tę funkcję nieprawidłowo. Dlatego żadna z pozostałych odpowiedzi nie zadziałała. Moja wina.
Lee Grindon

2
możliwy duplikat Split string w SQL
Luv

W mdq.RegexSplitdodatku „Master Data Services” znajduje się funkcja, która może pomóc. Z pewnością warte zbadania .
jpaugh

Odpowiedzi:


233

Używałem wcześniej tego SQL, który może Ci pomóc: -

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE CHARINDEX(',', @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END

i używać: -

SELECT * FROM dbo.splitstring('91,12,65,78,56,789')

1
Fajnie, właśnie tego szukałem, bardzo dziękuję
Lee Grindon

2
Wielkie dzięki Andy. Dokonałem niewielkiego rozszerzenia skryptu, aby umożliwić funkcji zwracanie elementu o określonym indeksie w podzielonym ciągu. Jest to przydatne tylko w sytuacjach, gdy analizowana jest struktura kolumny, w której się znajdujesz. gist.github.com/klimaye/8147193
CF_Maintainer,

1
Zamieściłem kilka ulepszeń (z kopii przypadków testowych) na mojej stronie github tutaj .
Opublikuję

8
Chociaż jest to świetna odpowiedź, jest przestarzała ... Podejścia proceduralne (zwłaszcza pętle) są czymś, czego należy unikać ... Warto przyjrzeć się nowszym odpowiedziom ...
Shnugo

2
Całkowicie zgadzam się z @Shnugo. Rozdzielacze pętli działają, ale strasznie wolno. Coś takiego sqlservercentral.com/articles/Tally+Table/72993 jest znacznie lepsze. Kilka innych doskonałych opcji opartych na zestawach można znaleźć tutaj. sqlperformance.com/2012/07/t-sql-queries/split-strings
Sean Lange

61

Zamiast rekurencyjnych CTE i pętli while, czy ktoś rozważał podejście bardziej oparte na zbiorach? Zauważ, że ta funkcja została napisana dla pytania, które było oparte na SQL Server 2008 i przecinku jako separatorze . W programie SQL Server 2016 i nowszych (oraz na poziomie zgodności 130 i nowszych) STRING_SPLIT()jest lepszą opcją .

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT [Value] FROM 
  ( 
    SELECT [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
      CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
    FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
      FROM sys.all_columns) AS x WHERE Number <= LEN(@List)
      AND SUBSTRING(@Delim + @List, [Number], DATALENGTH(@Delim)/2) = @Delim
    ) AS y
  );
GO

Jeśli chcesz uniknąć ograniczenia długości ciągu wynoszącej <= liczba wierszy w sys.all_columns(9 980 cali modelw programie SQL Server 2017; znacznie więcej we własnych bazach danych użytkowników), możesz użyć innych metod wyprowadzania liczb, takich jak budowanie własnej tabeli liczb . Możesz również użyć rekurencyjnego CTE w przypadkach, gdy nie możesz używać tabel systemowych lub tworzyć własnych:

CREATE FUNCTION dbo.SplitString
(
  @List     nvarchar(max),
  @Delim    nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
   RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 
       FROM n WHERE n <= LEN(@List))
       SELECT [Value] = SUBSTRING(@List, n, 
       CHARINDEX(@Delim, @List + @Delim, n) - n)
       FROM n WHERE n <= LEN(@List)
      AND SUBSTRING(@Delim + @List, n, DATALENGTH(@Delim)/2) = @Delim
   );
GO

Ale będziesz musiał dołączyć OPTION (MAXRECURSION 0)(lub MAXRECURSION <longest possible string length if < 32768>) do zewnętrznego zapytania, aby uniknąć błędów z rekurencją dla ciągów> 100 znaków. Jeśli to również nie jest dobra alternatywa, zobacz tę odpowiedź, jak wskazano w komentarzach.

(Również separator będzie musiał być NCHAR(<=1228). Wciąż szukam przyczyny).

Więcej o funkcjach rozdzielania, dlaczego (i udowodnij, że), podczas gdy pętle i rekurencyjne CTE nie skalują się i lepsze alternatywy, jeśli dzielisz ciągi pochodzące z warstwy aplikacji:


1
W tej procedurze występuje mały błąd w przypadku, gdy na końcu ciągu wystąpiłaby wartość null - na przykład w '1,2,, 4,' - ponieważ końcowa wartość nie jest analizowana. Aby naprawić ten błąd, wyrażenie „WHERE Number <= LEN (@List)” należy zastąpić wyrażeniem „WHERE Number <= LEN (@List) + 1”.
SylvainL

@SylvainL Myślę, że to zależy od tego, jakiego zachowania chcesz. Z mojego doświadczenia wynika, że ​​większość ludzi chce ignorować wszelkie końcowe przecinki, ponieważ tak naprawdę nie reprezentują one rzeczywistego elementu (ile kopii pustego ciągu potrzebujesz)? W każdym razie, prawdziwym sposobem na zrobienie tego - jeśli podążasz za drugim łączem - jest ominięcie bałaganu z dzieleniem dużych brzydkich ciągów w wolnym T-SQL.
Aaron Bertrand

1
Jak powiedziałeś, większość ludzi chce zignorować wszelkie końcowe przecinki, ale niestety nie wszystkie. Przypuszczam, że bardziej kompletnym rozwiązaniem byłoby dodanie parametru określającego, co robić w tym przypadku, ale mój komentarz to tylko mała uwaga, aby nikt nie zapomniał o tej możliwości, ponieważ w wielu przypadkach może być całkiem realna.
SylvainL

Mam dziwne zachowanie z tą funkcją. Jeśli używam bezpośrednio łańcucha jako parametru - działa. Jeśli mam varchar, nie ma. Możesz łatwo odtworzyć: zadeklaruj invarchar jako varchar set invarchar = 'ta; aa; qq' SELECT Wartość z [dbo]. [SplitString] (invarchar, ';') SELECT Wartość z [dbo]. [SplitString] ('ta; aa; qq ','; ')
Patrick Desjardins

Podoba mi się to podejście, ale jeśli liczba obiektów zwróconych przez sys.all_objectsjest mniejsza niż liczba znaków w ciągu wejściowym, spowoduje to obcięcie ciągu i zniknięcie wartości. Ponieważ sys.all_objectsjest używany tylko jako sztuczka do generowania wierszy, istnieją lepsze sposoby, aby to zrobić, np. Ta odpowiedź .
knuckles

56

Wreszcie czekanie dobiegło końca w SQL Server 2016 wprowadzili funkcję Split string:STRING_SPLIT

select * From STRING_SPLIT ('a,b', ',') cs 

Wszystkie inne metody dzielenia ciągów, takie jak XML, tabela Tally, pętla while itp., Zostały zdmuchnięte przez tę STRING_SPLITfunkcję.

Oto doskonały artykuł z porównaniem wydajności: Niespodzianki dotyczące wydajności i założenia: STRING_SPLIT


5
oczywiście odpowiada na pytanie, jak podzielić ciąg znaków dla tych ze zaktualizowanymi serwerami, ale ci z nas, którzy nadal utknęli na 2008 / 2008R2, będą musieli wybrać jedną z innych odpowiedzi tutaj.
mpag

2
Musisz przyjrzeć się poziomowi zgodności w swojej bazie danych. Jeśli jest niższy niż 130, nie będzie można użyć funkcji STRING_SPLIT.
Luis Teijon

Właściwie, jeśli kompatybilność jest inna niż 130 i używasz 2016 (lub Azure SQL), możesz ustawić kompatybilność do 130 za pomocą: ALTER DATABASE DatabaseName SET COMPATIBILITY_LEVEL = 130
Michieal

23

Najłatwiej to zrobić, używając XMLformatu.

1. Konwersja łańcucha do wierszy bez tabeli

PYTANIE

DECLARE @String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE @Delimiter CHAR = ','    

SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT CAST ('<M>' + REPLACE(@String, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

WYNIK

 x---------x
 | Value   |
 x---------x
 | String1 |
 | String2 |
 | String3 |
 x---------x

2. Konwersja do wierszy z tabeli, które mają identyfikator dla każdego wiersza CSV

TABELA ŹRÓDEŁ

 x-----x--------------------------x
 | Id  |           Value          |
 x-----x--------------------------x
 |  1  |  String1,String2,String3 |
 |  2  |  String4,String5,String6 |     
 x-----x--------------------------x

PYTANIE

-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE @Delimiter CHAR = ','

SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value' 
FROM  
(     
     SELECT ID,CAST ('<M>' + REPLACE(VALUE, @Delimiter, '</M><M>') + '</M>' AS XML) AS Data            
     FROM TABLENAME
) AS A 
CROSS APPLY Data.nodes ('/M') AS Split(a)

WYNIK

 x-----x----------x
 | Id  |  Value   |
 x-----x----------x
 |  1  |  String1 |
 |  1  |  String2 |  
 |  1  |  String3 |
 |  2  |  String4 |  
 |  2  |  String5 |
 |  2  |  String6 |     
 x-----x----------x

To podejście się zepsuje, jeśli @Stringzawiera zabronione znaki ... Właśnie opublikowałem odpowiedź, aby rozwiązać ten problem.
Shnugo

9

Potrzebowałem szybkiego sposobu na pozbycie +4się kodu pocztowego .

UPDATE #Emails 
  SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1)) 
  WHERE ZIPCode LIKE '%-%'

Bez proc ... bez UDF ... tylko jedno małe polecenie wbudowane, które robi to, co musi. Nie wyszukane, nie eleganckie.

Zmień separator w razie potrzeby itp. I będzie działać na wszystko.


4
Nie o to chodzi w tym pytaniu. OP ma wartość taką jak „234,542,23” i chcą podzielić go na trzy wiersze… Pierwszy wiersz: 234, drugi wiersz: 542, trzeci wiersz: 23. Jest to trudne zadanie w SQL.
codeulike

7

jeśli wymienisz

WHILE CHARINDEX(',', @stringToSplit) > 0

z

WHILE LEN(@stringToSplit) > 0

możesz wyeliminować ostatnią wstawkę po pętli while!

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE LEN(@stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(',', @stringToSplit)


if @pos = 0
        SELECT @pos = LEN(@stringToSplit)


  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 RETURN
END

Spowodowałoby to obcięcie ostatniego znaku ostatniego elementu. tzn. „AL, AL” stałoby się „AL” | „A”, czyli „ABC, ABC, ABC” zmieniłoby się w „ABC” | „ABC” | „AB”
Microsoft Developer

dołączenie +1do SELECT @pos = LEN(@stringToSplit)wydaje się rozwiązać ten problem. Jednak SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)zwróci, Invalid length parameter passed to the LEFT or SUBSTRING functionchyba że dodasz również +1do trzeciego parametru SUBSTRING. lub możesz zamienić to przypisanie naSET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) --MAX len of nvarchar is 4000
mpag

1
Zamieściłem kilka ulepszeń (z kopii przypadków testowych) na mojej stronie github tutaj .
Opublikuję

Ja również zwróciłem uwagę na kwestię wskazaną przez Terry'ego powyżej. Ale logika podana przez @AviG jest tak fajna, że ​​nie zawodzi w połowie długiej listy tokenów. Spróbuj to wywołanie testowe, aby zweryfikować (To wywołanie powinno zwrócić tokeny 969) wybierz * z dbo.splitstring ('token1, token2 ,,,,,,,, token969') Następnie wypróbowałem kod podany przez mpag, aby sprawdzić wyniki dla tego samego zadzwoń powyżej i stwierdziłem, że może zwrócić tylko 365 tokenów. W końcu poprawiłem kod przez AviG powyżej i zamieściłem funkcję wolną od błędów poniżej jako nową odpowiedź, ponieważ komentarz tutaj pozwala tylko na ograniczony tekst. Sprawdź odpowiedź pod moim nazwiskiem, aby spróbować.
Gemunu R Wickremasinghe

3

Wszystkie funkcje do dzielenia ciągów, które używają pewnego rodzaju pętli (iteracji), mają złą wydajność. Należy je zastąpić rozwiązaniem opartym na zestawie.

Ten kod działa doskonale.

CREATE FUNCTION dbo.SplitStrings
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO

To podejście się zepsuje, jeśli @Listzawiera zabronione znaki ... Właśnie opublikowałem odpowiedź, aby rozwiązać ten problem.
Shnugo,

Głosuję za twoją odpowiedzią, ponieważ Twoja działa z odstępem jako separatorem, a ten z najwyższą liczbą głosów nie
KMC,

3

Często używane podejście z elementami XML nie działa w przypadku zabronionych znaków. Jest to podejście do używania tej metody z dowolnym rodzajem znaku, nawet ze średnikiem jako separatorem.

Sztuczka polega na tym, że najpierw należy użyć, SELECT SomeString AS [*] FOR XML PATH('')aby wszystkie zabronione postacie prawidłowo uciekły. To jest powód, dla którego zastępuję ogranicznik magiczną wartością, aby uniknąć problemów z ;ogranicznikiem.

DECLARE @Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO @Dummy VALUES
 (1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');

DECLARE @Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!

WITH Casted AS
(
    SELECT *
          ,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,@Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
    FROM @Dummy
)
SELECT Casted.ID
      ,x.value(N'.',N'nvarchar(max)') AS Part 
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)

Wynik

ID  Part
1   A&B
1   C
1   D
1   E, F
2   "C" & 'D'
2   <C>
2   D
2   E, F

2

Ostatnio musiałem coś takiego napisać. Oto rozwiązanie, które wymyśliłem. Jest uogólniony dla dowolnego ciągu separatora i myślę, że działałby nieco lepiej:

CREATE FUNCTION [dbo].[SplitString] 
    ( @string nvarchar(4000)
    , @delim nvarchar(100) )
RETURNS
    @result TABLE 
        ( [Value] nvarchar(4000) NOT NULL
        , [Index] int NOT NULL )
AS
BEGIN
    DECLARE @str nvarchar(4000)
          , @pos int 
          , @prv int = 1

    SELECT @pos = CHARINDEX(@delim, @string)
    WHILE @pos > 0
    BEGIN
        SELECT @str = SUBSTRING(@string, @prv, @pos - @prv)
        INSERT INTO @result SELECT @str, @prv

        SELECT @prv = @pos + LEN(@delim)
             , @pos = CHARINDEX(@delim, @string, @pos + 1)
    END

    INSERT INTO @result SELECT SUBSTRING(@string, @prv, 4000), @prv
    RETURN
END

1

Rozwiązanie wykorzystujące CTE, gdyby ktoś tego potrzebował (oprócz mnie, który oczywiście to zrobił, dlatego to napisałem).

declare @StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare @SplitChar varchar(10) = ',';

with StringToSplit as (
  select 
      ltrim( rtrim( substring( @StringToSplit, 1, charindex( @SplitChar, @StringToSplit ) - 1 ) ) ) Head
    , substring( @StringToSplit, charindex( @SplitChar, @StringToSplit ) + 1, len( @StringToSplit ) ) Tail

  union all

  select
      ltrim( rtrim( substring( Tail, 1, charindex( @SplitChar, Tail ) - 1 ) ) ) Head
    , substring( Tail, charindex( @SplitChar, Tail ) + 1, len( Tail ) ) Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) > 0

  union all

  select
      ltrim( rtrim( Tail ) ) Head
    , '' Tail
  from StringToSplit
  where charindex( @SplitChar, Tail ) = 0
    and len( Tail ) > 0
)
select Head from StringToSplit

1

Jest to węższy sposób. Kiedy to robię, zwykle mam rozdzieloną przecinkami listę unikalnych identyfikatorów (INT lub BIGINT), które chcę rzutować jako tabelę, aby użyć jej jako sprzężenia wewnętrznego do innej tabeli, która ma klucz podstawowy INT lub BIGINT. Chcę, aby funkcja z wartościami przechowywanymi w tabeli została zwrócona, aby uzyskać jak najbardziej wydajne sprzężenie.

Przykładowe użycie to:

 DECLARE @IDs VARCHAR(1000);
 SET @IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
 SELECT me.Value
 FROM dbo.MyEnum me
 INNER JOIN dbo.GetIntIdsTableFromDelimitedString(@IDs) ids ON me.PrimaryKey = ids.ID

Ukradłem ten pomysł z http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html , zmieniając go tak, aby był wartościowany w tabeli i rzutowany jako INT.

create function dbo.GetIntIDTableFromDelimitedString
    (
    @IDs VARCHAR(1000)  --this parameter must start and end with a comma, eg ',123,456,'
                        --all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
 RETURN

SELECT
    CAST(SUBSTRING(@IDs,Nums.number + 1,CHARINDEX(',',@IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID 
FROM   
     [master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P' 
AND    Nums.number BETWEEN 1 AND DATALENGTH(@IDs)
AND    SUBSTRING(@IDs,Nums.number,1) = ','
AND    CHARINDEX(',',@IDs,(Nums.number+1)) > Nums.number;

GO

1

Jest tutaj poprawna wersja, ale pomyślałem, że byłoby miło dodać trochę tolerancji na błędy w przypadku, gdy mają końcowy przecinek, a także zrobić to, abyś mógł go używać nie jako funkcji, ale jako części większego fragmentu kodu . Na wypadek, gdybyś używał go tylko raz i nie potrzebujesz funkcji. Dotyczy to również liczb całkowitych (do tego potrzebowałem), więc być może będziesz musiał zmienić typy danych.

DECLARE @StringToSeperate VARCHAR(10)
SET @StringToSeperate = '1,2,5'

--SELECT @StringToSeperate IDs INTO #Test

DROP TABLE #IDs
CREATE TABLE #IDs (ID int) 

DECLARE @CommaSeperatedValue NVARCHAR(255) = ''
DECLARE @Position INT = LEN(@StringToSeperate)

--Add Each Value
WHILE CHARINDEX(',', @StringToSeperate) > 0
BEGIN
    SELECT @Position  = CHARINDEX(',', @StringToSeperate)  
    SELECT @CommaSeperatedValue = SUBSTRING(@StringToSeperate, 1, @Position-1)

    INSERT INTO #IDs 
    SELECT @CommaSeperatedValue

    SELECT @StringToSeperate = SUBSTRING(@StringToSeperate, @Position+1, LEN(@StringToSeperate)-@Position)

END

--Add Last Value
IF (LEN(LTRIM(RTRIM(@StringToSeperate)))>0)
BEGIN
    INSERT INTO #IDs
    SELECT SUBSTRING(@StringToSeperate, 1, @Position)
END

SELECT * FROM #IDs

jeśli miałbyś zrobić SET @StringToSeperate = @StringToSeperate+','bezpośrednio przed WHILEpętlą, myślę, że możesz być w stanie wyeliminować blok „dodaj ostatnią wartość”. Zobacz także mój sol'n na github
mpag

Na jakiej odpowiedzi jest to oparte? Odpowiedzi jest wiele i jest to trochę zagmatwane. Dzięki.
jpaugh

1

Trochę zmodyfikowałem + funkcję Andy Robinsona. Teraz możesz wybrać tylko wymaganą część z tabeli zwrotów:

CREATE FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(MAX) )

RETURNS

 @returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN

 DECLARE @name NVARCHAR(255)

 DECLARE @pos INT

 DECLARE @orderNum INT

 SET @orderNum=0

 WHILE CHARINDEX('.', @stringToSplit) > 0

 BEGIN
    SELECT @orderNum=@orderNum+1;
  SELECT @pos  = CHARINDEX('.', @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @orderNum,@name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END
    SELECT @orderNum=@orderNum+1;
 INSERT INTO @returnList
 SELECT @orderNum, @stringToSplit

 RETURN
END


Usage:

SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5


1

Jeśli potrzebujesz szybkiego rozwiązania ad hoc dla typowych przypadków z minimalnym kodem, to zrobi to ten rekurencyjny dwuwierszowy CTE:

DECLARE @s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'

;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', @s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b

Użyj tego jako samodzielnej instrukcji lub po prostu dodaj powyższe CTE do dowolnego zapytania, a będziesz mógł połączyć wynikową tabelę bz innymi w celu użycia w dalszych wyrażeniach.

edytuj (przez Shnugo)

Jeśli dodasz licznik, wraz z Listą otrzymasz indeks pozycji:

DECLARE @s VARCHAR(200) = '1,2333,344,4'

;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', @s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(@s, i+1, IIF(j>0, j, LEN(@s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;

Wynik:

n   s
1   1
2   2333
3   344
4   4

Podoba mi się to podejście. Mam nadzieję, że nie masz nic przeciwko, że dodałem pewne ulepszenie bezpośrednio do Twojej odpowiedzi. Po prostu edytuj to w dowolny wygodny sposób ...
Shnugo,

1

Wybieram trasę xml, zawijając wartości w elementy (M, ale wszystko działa):

declare @v nvarchar(max) = '100,201,abcde'

select 
    a.value('.', 'varchar(max)')
from
    (select cast('<M>' + REPLACE(@v, ',', '</M><M>') + '</M>' AS XML) as col) as A
    CROSS APPLY A.col.nodes ('/M') AS Split(a)

0

tutaj jest wersja, którą można podzielić na wzór przy użyciu patindex, prostej adaptacji powyższego postu. Miałem przypadek, w którym musiałem podzielić ciąg zawierający wiele znaków separatora.


alter FUNCTION dbo.splitstring ( @stringToSplit VARCHAR(1000), @splitPattern varchar(10) )
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

 DECLARE @name NVARCHAR(255)
 DECLARE @pos INT

 WHILE PATINDEX(@splitPattern, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = PATINDEX(@splitPattern, @stringToSplit)  
  SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)

  INSERT INTO @returnList 
  SELECT @name

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');

wynik wygląda następująco

stringa stringb x y z


0

Osobiście używam tej funkcji:

ALTER FUNCTION [dbo].[CUST_SplitString]
(
    @String NVARCHAR(4000),
    @Delimiter NCHAR(1)
)
RETURNS TABLE 
AS
RETURN 
(
    WITH Split(stpos,endpos) 
    AS(
        SELECT 0 AS stpos, CHARINDEX(@Delimiter,@String) AS endpos
        UNION ALL
        SELECT endpos+1, CHARINDEX(@Delimiter,@String,endpos+1) 
        FROM Split
        WHERE endpos > 0
    )
    SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
        'Data' = SUBSTRING(@String,stpos,COALESCE(NULLIF(endpos,0),LEN(@String)+1)-stpos)
    FROM Split
)

0

Opracowałem podwójny rozdzielacz (zajmuje dwa rozdzielone znaki) zgodnie z żądaniem tutaj . Może mieć jakąś wartość w tym wątku, ponieważ jest najczęściej przywoływany w przypadku zapytań dotyczących dzielenia ciągów.

CREATE FUNCTION uft_DoubleSplitter 
(   
    -- Add the parameters for the function here
    @String VARCHAR(4000), 
    @Splitter1 CHAR,
    @Splitter2 CHAR
)
RETURNS @Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE @FResult TABLE(Id INT IDENTITY(1, 1),
                   SValue VARCHAR(4000))
DECLARE @SResult TABLE(Id INT IDENTITY(1, 1),
                   MId INT,
                   SValue VARCHAR(4000))
SET @String = @String+@Splitter1

WHILE CHARINDEX(@Splitter1, @String) > 0
    BEGIN
       DECLARE @WorkingString VARCHAR(4000) = NULL

       SET @WorkingString = SUBSTRING(@String, 1, CHARINDEX(@Splitter1, @String) - 1)
       --Print @workingString

       INSERT INTO @FResult
       SELECT CASE
            WHEN @WorkingString = '' THEN NULL
            ELSE @WorkingString
            END

       SET @String = SUBSTRING(@String, LEN(@WorkingString) + 2, LEN(@String))

    END
IF ISNULL(@Splitter2, '') != ''
    BEGIN
       DECLARE @OStartLoop INT
       DECLARE @OEndLoop INT

       SELECT @OStartLoop = MIN(Id),
            @OEndLoop = MAX(Id)
       FROM @FResult

       WHILE @OStartLoop <= @OEndLoop
          BEGIN
             DECLARE @iString VARCHAR(4000)
             DECLARE @iMId INT

             SELECT @iString = SValue+@Splitter2,
                   @iMId = Id
             FROM @FResult
             WHERE Id = @OStartLoop

             WHILE CHARINDEX(@Splitter2, @iString) > 0
                BEGIN
                    DECLARE @iWorkingString VARCHAR(4000) = NULL

                    SET @IWorkingString = SUBSTRING(@iString, 1, CHARINDEX(@Splitter2, @iString) - 1)

                    INSERT INTO @SResult
                    SELECT @iMId,
                         CASE
                         WHEN @iWorkingString = '' THEN NULL
                         ELSE @iWorkingString
                         END

                    SET @iString = SUBSTRING(@iString, LEN(@iWorkingString) + 2, LEN(@iString))

                END

             SET @OStartLoop = @OStartLoop + 1
          END
       INSERT INTO @Result
       SELECT MId AS PrimarySplitID,
            ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
            SValue
       FROM @SResult
    END
ELSE
    BEGIN
       INSERT INTO @Result
       SELECT Id AS PrimarySplitID,
            NULL AS SecondarySplitID,
            SValue
       FROM @FResult
    END
RETURN

Stosowanie:

--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)

--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')

Możliwe użycie (pobierz drugą wartość każdego podziału):

SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2

0

Oto przykład, którego możesz użyć jako funkcji lub możesz również umieścić tę samą logikę w procedurze. --WYBIERZ * z [dbo] .fn_SplitString;

CREATE FUNCTION [dbo].[fn_SplitString]
(@CSV VARCHAR(MAX), @Delimeter VARCHAR(100) = ',')
       RETURNS @retTable TABLE 
(

    [value] VARCHAR(MAX) NULL
)AS

BEGIN

DECLARE
       @vCSV VARCHAR (MAX) = @CSV,
       @vDelimeter VARCHAR (100) = @Delimeter;

IF @vDelimeter = ';'
BEGIN
    SET @vCSV = REPLACE(@vCSV, ';', '~!~#~');
    SET @vDelimeter = REPLACE(@vDelimeter, ';', '~!~#~');
END;

SET @vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(@vCSV, '&', '&amp;'), '<', '&lt;'), '>', '&gt;'), '''', '&apos;'), '"', '&quot;');

DECLARE @xml XML;

SET @xml = '<i>' + REPLACE(@vCSV, @vDelimeter, '</i><i>') + '</i>';

INSERT INTO @retTable
SELECT
       x.i.value('.', 'varchar(max)') AS COLUMNNAME
  FROM @xml.nodes('//i')AS x(i);

 RETURN;
END;

To podejście się zepsuje, jeśli @vCSVzawiera zabronione znaki ... Właśnie opublikowałem odpowiedź, aby rozwiązać ten problem.
Shnugo

0

Rekurencyjne rozwiązanie oparte na cte

declare @T table (iden int identity, col1 varchar(100));
insert into @T(col1) values
       ('ROOT/South America/Lima/Test/Test2')
     , ('ROOT/South America/Peru/Test/Test2')
     , ('ROOT//South America/Venuzuala ')
     , ('RtT/South America / ') 
     , ('ROOT/South Americas// '); 
declare @split char(1) = '/';
select @split as split;
with cte as 
(  select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = @split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + @split end  as col1, 0 as pos                             , 1 as cnt
   from @T t
   union all 
   select t.iden, t.col1                                                                                                                              , charindex(@split, t.col1, t.pos + 1), cnt + 1 
   from cte t 
   where charindex(@split, t.col1, t.pos + 1) > 0 
)
select t1.*, t2.pos, t2.cnt
     , ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1 
join cte t2 
  on t2.iden = t1.iden 
 and t2.cnt  = t1.cnt+1
 and t2.pos > t1.pos 
order by t1.iden, t1.cnt;

0

To jest oparte na odpowiedzi Andy'ego Robertsona, potrzebowałem separatora innego niż przecinek.

CREATE FUNCTION dbo.splitstring ( @stringToSplit nvarchar(MAX), @delim nvarchar(max))
RETURNS
 @returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN

 DECLARE @value NVARCHAR(max)
 DECLARE @pos INT

 WHILE CHARINDEX(@delim, @stringToSplit) > 0
 BEGIN
  SELECT @pos  = CHARINDEX(@delim, @stringToSplit)  
  SELECT @value = SUBSTRING(@stringToSplit, 1, @pos - 1)

  INSERT INTO @returnList 
  SELECT @value

  SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos + LEN(@delim), LEN(@stringToSplit) - @pos)
 END

 INSERT INTO @returnList
 SELECT @stringToSplit

 RETURN
END
GO

I aby z niego skorzystać:

SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');

(Testowane na SQL Server 2008 R2)

EDYCJA: poprawny kod testu


0

/ *

Odpowiedź na podzielony łańcuch T-SQL
Na podstawie odpowiedzi Andy'ego Robinsona i AviG
Rozszerzona funkcjonalność ref: Funkcja LEN bez końcowych spacji w SQL Server
Ten „plik” powinien być ważny zarówno jako plik przeceny, jak i plik SQL


*/

    CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
        @stringToSplit NVARCHAR(MAX)
    ) RETURNS @returnList TABLE ([Item] NVARCHAR (MAX))
    AS BEGIN
        DECLARE @name NVARCHAR(MAX)
        DECLARE @pos BIGINT
        SET @stringToSplit = @stringToSplit + ','             -- this should allow entries that end with a `,` to have a blank value in that "column"
        WHILE ((LEN(@stringToSplit+'_') > 1)) BEGIN           -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
            SET @pos = COALESCE(NULLIF(CHARINDEX(',', @stringToSplit),0),LEN(@stringToSplit+'_')) -- COALESCE grabs first non-null value
            SET @name = SUBSTRING(@stringToSplit, 1, @pos-1)  --MAX size of string of type nvarchar is 4000 
            SET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
            INSERT INTO @returnList SELECT @name --additional debugging parameters below can be added
            -- + ' pos:' + CAST(@pos as nvarchar) + ' remain:''' + @stringToSplit + '''(' + CAST(LEN(@stringToSplit+'_')-1 as nvarchar) + ')'
        END
        RETURN
    END
    GO

/*

Przypadki testowe: patrz URL określany powyżej jako „rozszerzona funkcjonalność”

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')

Item | L
---  | ---
a    | 1
     | 0
b    | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')

Item | L   
---  | ---
a    | 1
     | 0
     | 0

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')

Item | L   
---  | ---
a    | 1
     | 0
     | 1

SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')

Item | L   
---  | ---
a    | 1
     | 0
 c   | 3

* /


przywrócony do honoru „Ten 'plik' powinien być poprawny zarówno jako plik przeceny, jak i plik SQL”
mpag

-1
ALTER FUNCTION [dbo].func_split_string
(
    @input as varchar(max),
    @delimiter as varchar(10) = ";"

)
RETURNS @result TABLE
(
    id smallint identity(1,1),
    csv_value varchar(max) not null
)
AS
BEGIN
    DECLARE @pos AS INT;
    DECLARE @string AS VARCHAR(MAX) = '';

    WHILE LEN(@input) > 0
    BEGIN           
        SELECT @pos = CHARINDEX(@delimiter,@input);

        IF(@pos<=0)
            select @pos = len(@input)

        IF(@pos <> LEN(@input))
            SELECT @string = SUBSTRING(@input, 1, @pos-1);
        ELSE
            SELECT @string = SUBSTRING(@input, 1, @pos);

        INSERT INTO @result SELECT @string

        SELECT @input = SUBSTRING(@input, @pos+len(@delimiter), LEN(@input)-@pos)       
    END
    RETURN  
END

-1

Możesz użyć tej funkcji:

        CREATE FUNCTION SplitString
        (    
           @Input NVARCHAR(MAX),
           @Character CHAR(1)
          )
            RETURNS @Output TABLE (
            Item NVARCHAR(1000)
          )
        AS
        BEGIN

      DECLARE @StartIndex INT, @EndIndex INT
      SET @StartIndex = 1
      IF SUBSTRING(@Input, LEN(@Input) - 1, LEN(@Input)) <> @Character
      BEGIN
            SET @Input = @Input + @Character
      END

      WHILE CHARINDEX(@Character, @Input) > 0
      BEGIN
            SET @EndIndex = CHARINDEX(@Character, @Input)

            INSERT INTO @Output(Item)
            SELECT SUBSTRING(@Input, @StartIndex, @EndIndex - 1)

            SET @Input = SUBSTRING(@Input, @EndIndex + 1, LEN(@Input))
      END

      RETURN
END
GO

-1

Z całym szacunkiem dla @AviG jest to opracowana przez niego wolna od błędów wersja funkcji zwracania wszystkich tokenów w całości.

IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO

-- =============================================
-- Author:  AviG
-- Amendments:  Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from   [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from   [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString 
( @stringToSplit VARCHAR(MAX) ,
  @delimeter char = ','
)
RETURNS
 @returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN

    DECLARE @name NVARCHAR(255)
    DECLARE @pos INT

    WHILE LEN(@stringToSplit) > 0
    BEGIN
        SELECT @pos  = CHARINDEX(@delimeter, @stringToSplit)


        if @pos = 0
        BEGIN
            SELECT @pos = LEN(@stringToSplit)
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos)  
        END
        else 
        BEGIN
            SELECT @name = SUBSTRING(@stringToSplit, 1, @pos-1)
        END

        INSERT INTO @returnList 
        SELECT @name

        SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)
    END

 RETURN
END

-3

Najłatwiejszy sposób:

  1. Zainstaluj SQL Server 2016
  2. Użyj STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx

Działa nawet w wydaniu ekspresowym :).


Nie zapomnij ustawić „Poziom zgodności” na SQL Server 2016 (130) - w studiu zarządzania kliknij prawym przyciskiem myszy bazę danych, właściwości / opcje / poziom zgodności.
Tomino,

1
Oryginalny post mówi o SQL 2008 R2. Instalacja SQL 2016 może nie wchodzić w grę
Shawn Gavett
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.