2009-03-30 34 views
112

Sto cercando di dividere '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 ...' (virgola delimitata) in un tavolo o variabile di tabella.Equivalente della funzione di divisione in T-SQL?

Qualcuno ha una funzione che restituisce uno di seguito?

+2

Recentemente ho eseguito uno studio minore confronto tra gli approcci più comuni questo problema, che può valere una lettura: http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings e http://www.sqlperformance.com/2012/08/t- sql-queries/splitting-string-follow-up –

+1

possibile duplicato o f [Split string in SQL] (http://stackoverflow.com/questions/2647/split-string-in-sql) – Luv

+1

Erland Sommarskog ha mantenuto la risposta autorevole a questa domanda negli ultimi 12 anni: [http://www.sommarskog.se/arrays-in-sql.html](http://www.sommarskog.se/arrays-in-sql.html) Non vale la pena di riprodurre tutte le opzioni qui su StackOverflow, basta visitare la sua pagina e imparerai tutto quello che avresti voluto sapere. – Portman

risposta

45

Qui è un po 'soluzione antiquata:

/* 
    Splits string into parts delimitered with specified character. 
*/ 
CREATE FUNCTION [dbo].[SDF_SplitString] 
(
    @sString nvarchar(2048), 
    @cDelimiter nchar(1) 
) 
RETURNS @tParts TABLE (part nvarchar(2048)) 
AS 
BEGIN 
    if @sString is null return 
    declare @iStart int, 
      @iPos int 
    if substring(@sString, 1, 1) = @cDelimiter 
    begin 
     set @iStart = 2 
     insert into @tParts 
     values(null) 
    end 
    else 
     set @iStart = 1 
    while 1=1 
    begin 
     set @iPos = charindex(@cDelimiter, @sString, @iStart) 
     if @iPos = 0 
      set @iPos = len(@sString)+1 
     if @iPos - @iStart > 0   
      insert into @tParts 
      values (substring(@sString, @iStart, @[email protected])) 
     else 
      insert into @tParts 
      values(null) 
     set @iStart = @iPos+1 
     if @iStart > len(@sString) 
      break 
    end 
    RETURN 

END 

In SQL Server 2008 è possibile ottenere lo stesso con il codice di .NET. Forse funzionerebbe più velocemente, ma sicuramente questo approccio è più facile da gestire.

+0

Grazie, vorrei anche sapere. C'è un errore qui? Ho scritto questo codice forse 6 anni fa e funzionava bene da quando. – XOR

+0

Sono d'accordo. Questa è un'ottima soluzione quando non si vuole (o semplicemente non si può) partecipare alla creazione dei parametri del tipo di tabella, come nel mio esempio. Gli amministratori di database hanno bloccato questa funzione e non lo consentono. Grazie XOR! – dscarr

2

sono tentato di spremere in mia soluzione preferita. La tabella risultante sarà composta da 2 colonne: PosIdx per la posizione del numero intero trovato; e valore in numero intero.


create function FnSplitToTableInt 
(
    @param nvarchar(4000) 
) 
returns table as 
return 
    with Numbers(Number) as 
    (
     select 1 
     union all 
     select Number + 1 from Numbers where Number < 4000 
    ), 
    Found as 
    (
     select 
      Number as PosIdx, 
      convert(int, ltrim(rtrim(convert(nvarchar(4000), 
       substring(@param, Number, 
       charindex(N',' collate Latin1_General_BIN, 
       @param + N',', Number) - Number))))) as Value 
     from 
      Numbers 
     where 
      Number <= len(@param) 
     and substring(N',' + @param, Number, 1) = N',' collate Latin1_General_BIN 
    ) 
    select 
     PosIdx, 
     case when isnumeric(Value) = 1 
      then convert(int, Value) 
      else convert(int, null) end as Value 
    from 
     Found 

Funziona utilizzando CTE ricorsiva come elenco di posizioni, da 1 a 100 per impostazione predefinita. Se avete bisogno di lavorare con la stringa di più di 100, è sufficiente chiamare questa funzione con 'opzione (MAXRECURSION 4000)' come la seguente:


select * from FnSplitToTableInt 
(
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' + 
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' + 
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' + 
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0, ' + 
    '9, 8, 7, 6, 5, 4, 3, 2, 1, 0' 
) 
option (maxrecursion 4000) 
+2

+1 per aver menzionato l'opzione maxrecursion. Ovviamente la ricorsione pesante dovrebbe essere utilizzata con attenzione in un ambiente di produzione, ma è ottimo per l'utilizzo di CTE per eseguire pesanti attività di importazione o conversione dei dati. –

43

Prova questo

DECLARE @xml xml, @str varchar(100), @delimiter varchar(10) 
SET @str = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15' 
SET @delimiter = ',' 
SET @xml = cast(('<X>'+replace(@str, @delimiter, '</X><X>')+'</X>') as xml) 
SELECT C.value('.', 'varchar(10)') as value FROM @xml.nodes('X') as X(C) 

O

DECLARE @str varchar(100), @delimiter varchar(10) 
SET @str = '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15' 
SET @delimiter = ',' 
;WITH cte AS 
(
    SELECT 0 a, 1 b 
    UNION ALL 
    SELECT b, CHARINDEX(@delimiter, @str, b) + LEN(@delimiter) 
    FROM CTE 
    WHERE b > a 
) 
SELECT SUBSTRING(@str, a, 
CASE WHEN b > LEN(@delimiter) 
    THEN b - a - LEN(@delimiter) 
    ELSE LEN(@str) - a + 1 END) value  
FROM cte WHERE a > 0 

Qui ci sono molti altri modi per fare lo stesso How to split comma delimited string?

+8

Nota per chiunque cerchi uno splitter di stringa generale: la prima soluzione fornita qui non è uno splitter di stringa generale: è sicuro solo se si è sicuri che l'input non contenga mai '<', '>' o '&' (ad esempio, input è una sequenza di numeri interi). Uno qualsiasi dei tre caratteri precedenti causerà un errore di analisi invece del risultato previsto. – miroxlav

+1

Evento con i problemi menzionati da miroxlav (che dovrebbe essere risolvibile con qualche pensiero), questa sicuramente una delle soluzioni più creative che ho trovato (la prima)! Molto bella! –

+0

La riga 'SELECT b, CHARINDEX (@delimiter, @str, b) + LEN (@delimiter)' dovrebbe essere effettivamente 'SELECT b, CHARINDEX (@delimiter, @str, b + 1) + LEN (@delimiter)' . Il ** b + 1 ** fa una grande differenza. Testato qui con lo spazio come delimitatore, non ha funzionato senza questa correzione. – JwJosefy

10

Questo è molto simile a .NET, per quelli di voi che hanno familiarità con questa funzione:

CREATE FUNCTION dbo.[String.Split] 
(
    @Text VARCHAR(MAX), 
    @Delimiter VARCHAR(100), 
    @Index INT 
) 
RETURNS VARCHAR(MAX) 
AS BEGIN 
    DECLARE @A TABLE (ID INT IDENTITY, V VARCHAR(MAX)); 
    DECLARE @R VARCHAR(MAX); 
    WITH CTE AS 
    (
    SELECT 0 A, 1 B 
    UNION ALL 
    SELECT B, CONVERT(INT,CHARINDEX(@Delimiter, @Text, B) + LEN(@Delimiter)) 
    FROM CTE 
    WHERE B > A 
    ) 
    INSERT @A(V) 
    SELECT SUBSTRING(@Text,A,CASE WHEN B > LEN(@Delimiter) THEN B-A-LEN(@Delimiter) ELSE LEN(@Text) - A + 1 END) VALUE  
    FROM CTE WHERE A >0 

    SELECT  @R 
    =   V 
    FROM  @A 
    WHERE  ID = @Index + 1 
    RETURN  @R 
END 

SELECT dbo.[String.Split]('121,2,3,0',',',1) -- gives '2' 
+0

Questo ha funzionato per me dopo aver aggiunto GO dopo END – clairestreb

0

This blog è venuto con una buona soluzione che utilizza XML in T-SQL.

Questa è la funzione che mi è venuta in base a quel blog (nome funzione di cambio e il risultato cast di tipo per ogni esigenza):

SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
CREATE FUNCTION [dbo].[SplitIntoBigints] 
(@List varchar(MAX), @Splitter char) 
RETURNS TABLE 
AS 
RETURN 
(
    WITH SplittedXML AS(
     SELECT CAST('<v>' + REPLACE(@List, @Splitter, '</v><v>') + '</v>' AS XML) AS Splitted 
    ) 
    SELECT x.v.value('.', 'bigint') AS Value 
    FROM SplittedXML 
    CROSS APPLY Splitted.nodes('//v') x(v) 
) 
GO 
7

qui è la funzione split che u chiesto

CREATE FUNCTION [dbo].[split](
      @delimited NVARCHAR(MAX), 
      @delimiter NVARCHAR(100) 
     ) RETURNS @t TABLE (id INT IDENTITY(1,1), val NVARCHAR(MAX)) 
     AS 
     BEGIN 
      DECLARE @xml XML 
      SET @xml = N'<t>' + REPLACE(@delimited,@delimiter,'</t><t>') + '</t>' 

      INSERT INTO @t(val) 
      SELECT r.value('.','varchar(MAX)') as item 
      FROM @xml.nodes('/t') as records(r) 
      RETURN 
     END 

eseguire la funzione in questo

select * from dbo.split('1,2,3,4,5,6,7,8,9,10,11,12,13,14,15',',') 
+1

Così semplice, ha funzionato come un fascino –

0
CREATE Function [dbo].[CsvToInt] (@Array varchar(4000)) 
returns @IntTable table 
(IntValue int) 
AS 
begin 
declare @separator char(1) 
set @separator = ',' 
declare @separator_position int 
declare @array_value varchar(4000) 

set @array = @array + ',' 

while patindex('%,%' , @array) <> 0 
begin 

select @separator_position = patindex('%,%' , @array) 
select @array_value = left(@array, @separator_position - 1) 

Insert @IntTable 
Values (Cast(@array_value as int)) 
select @array = stuff(@array, 1, @separator_position, '') 
end 
2

Questa è un'altra versione che in realtà non ha alcuna restrizione (es: caratteri speciali quando si utilizza l'approccio xml, numero di record nell'approccio CTE) e viene eseguita molto più velocemente in base a un test su record 10M + con lunghezza della stringa di origine media di 4000. Speranza questo potrebbe aiutare.

Create function [dbo].[udf_split] (
    @ListString nvarchar(max), 
    @Delimiter nvarchar(1000), 
    @IncludeEmpty bit) 
Returns @ListTable TABLE (ID int, ListValue nvarchar(1000)) 
AS 
BEGIN 
    Declare @CurrentPosition int, @NextPosition int, @Item nvarchar(max), @ID int, @L int 
    Select @ID = 1, 
    @L = len(replace(@Delimiter,' ','^')), 
      @ListString = @ListString + @Delimiter, 
      @CurrentPosition = 1 
    Select @NextPosition = Charindex(@Delimiter, @ListString, @CurrentPosition) 
    While @NextPosition > 0 Begin 
    Set @Item = LTRIM(RTRIM(SUBSTRING(@ListString, @CurrentPosition, @[email protected]))) 
    If  @IncludeEmpty=1 or LEN(@Item)>0 Begin 
    Insert Into @ListTable (ID, ListValue) Values (@ID, @Item) 
    Set @ID = @ID+1 
    End 
    Set @CurrentPosition = @[email protected] 
    Set @NextPosition = Charindex(@Delimiter, @ListString, @CurrentPosition) 
    End 
    RETURN 
END 
1
/* *Object: UserDefinedFunction [dbo].[Split] Script Date: 10/04/2013 18:18:38* */ 
SET ANSI_NULLS ON 
GO 
SET QUOTED_IDENTIFIER ON 
GO 
ALTER FUNCTION [dbo].[Split] 
(@List varchar(8000),@SplitOn Nvarchar(5)) 
RETURNS @RtnValue table 
(Id int identity(1,1),Value nvarchar(100)) 
AS 
BEGIN 
    Set @List = Replace(@List,'''','') 
    While (Charindex(@SplitOn,@List)>0) 
    Begin 

    Insert Into @RtnValue (value) 
    Select 
    Value = ltrim(rtrim(Substring(@List,1,Charindex(@SplitOn,@List)-1))) 

    Set @List = Substring(@List,Charindex(@SplitOn,@List)+len(@SplitOn),len(@List)) 
    End 

    Insert Into @RtnValue (Value) 
    Select Value = ltrim(rtrim(@List)) 

    Return 
END 
go 

Select * 
From [Clv].[Split] ('1,2,3,3,3,3,',',') 
GO 
4
DECLARE 
    @InputString NVARCHAR(MAX) = 'token1,token2,token3,token4,token5' 
    , @delimiter varchar(10) = ',' 

DECLARE @xml AS XML = CAST(('<X>'+REPLACE(@InputString,@delimiter ,'</X><X>')+'</X>') AS XML) 
SELECT C.value('.', 'varchar(10)') AS value 
FROM @xml.nodes('X') as X(C) 

Fonte di questa risposta: http://sqlhint.com/sqlserver/how-to/best-split-function-tsql-delimited

+0

Mentre questo può teoricamente rispondere alla domanda, [sarebbe preferibile] (http://meta.stackexchange.com/q/8259) per includere qui le parti essenziali della risposta e fornire il link per riferimento. –

+1

@Xavi: ok, ho incluso le parti essenziali della risposta. Grazie per il tuo suggerimento. –

2
CREATE FUNCTION Split 
(
    @delimited nvarchar(max), 
    @delimiter nvarchar(100) 
) RETURNS @t TABLE 
(
-- Id column can be commented out, not required for sql splitting string 
    id int identity(1,1), -- I use this column for numbering splitted parts 
    val nvarchar(max) 
) 
AS 
BEGIN 
    declare @xml xml 
    set @xml = N'<root><r>' + replace(@delimited,@delimiter,'</r><r>') + '</r></root>' 

    insert into @t(val) 
    select 
    r.value('.','varchar(max)') as item 
    from @xml.nodes('//root/r') as records(r) 

    RETURN 
END 
GO 

utilizzo

Select * from dbo.Split(N'1,2,3,4,6',',') 
17

Hai codificato questo SQL Server 2008, ma futuri visitatori a questa domanda (utilizzando SQL Server 2016+) sarà probabilmente desiderare di conoscere STRING_SPLIT.

Con questa nuova funzione built-in è possibile ora solo usare

SELECT TRY_CAST(value AS INT) 
FROM STRING_SPLIT ('1,2,3,4,5,6,7,8,9,10,11,12,13,14,15', ',') 

Alcune restrizioni di questa funzione e alcuni risultati promettenti del test delle prestazioni è in this blog post by Aaron Bertrand.

0

Utilizzando tavolo bottino qui è funzione di stringa uno split (miglior approccio possibile) di Jeff Moden

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 
; 

Riferito da Tally OH! An Improved SQL 8K “CSV Splitter” Function

Problemi correlati