Cadena dividida T-SQL


139

Tengo una columna de SQL Server 2008 R2 que contiene una cadena que necesito dividir por una coma. He visto muchas respuestas en StackOverflow pero ninguna de ellas funciona en R2. Me aseguré de tener permisos de selección en cualquier ejemplo de función dividida. Cualquier ayuda muy apreciada.


77
Esta es una de las millones de respuestas que me gustan stackoverflow.com/a/1846561/227755
nurettin

2
¿Qué quieres decir con "ninguno de ellos funciona"? ¿Puedes ser mas específico?
Aaron Bertrand

Andy me señaló en la dirección correcta ya que estaba ejecutando la función incorrectamente. Es por eso que ninguna de las otras respuestas de la pila funcionó. Mi culpa.
Lee Grindon

2
posible duplicado de cadena dividida en SQL
Luv

Hay una mdq.RegexSplitfunción en el complemento "Master Data Services", que puede ayudar. Ciertamente vale la pena investigar .
jpaugh 01 de

Respuestas:


233

He usado este SQL antes, que puede funcionar para usted:

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

y para usarlo: -

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

1
Agradable, esto es exactamente lo que estaba buscando muchas gracias
Lee Grindon

2
Muchas gracias Andy. Hice una pequeña mejora en su script para permitir que la función devuelva un elemento en un índice específico en la cadena dividida. Es útil solo en situaciones en las que se analiza la estructura de la columna uno. gist.github.com/klimaye/8147193
CF_Maintainer

1
Publiqué algunas mejoras (con casos de prueba de respaldo) en mi página de github aquí . Lo publicaré como respuesta en este hilo de desbordamiento de pila cuando tenga suficiente representante para exceder la "protección" de la
publicación

8
Aunque esta es una gran respuesta, está desactualizada ... Los enfoques de procedimiento (especialmente los bucles) son algo que debe evitarse ... Vale la pena buscar nuevas respuestas ...
Shnugo

2
Estoy totalmente de acuerdo con @Shnugo. Los divisores en bucle funcionan pero son terriblemente lentos. Algo como esto sqlservercentral.com/articles/Tally+Table/72993 es mucho mejor. Algunas otras excelentes opciones basadas en conjuntos se pueden encontrar aquí. sqlperformance.com/2012/07/t-sql-queries/split-strings
Sean Lange

61

En lugar de CTE recursivos y bucles while, ¿alguien ha considerado un enfoque más basado en conjuntos? Tenga en cuenta que esta función se escribió para la pregunta, que se basó en SQL Server 2008 y coma como delimitador . En SQL Server 2016 y superior (y en el nivel de compatibilidad 130 y superior), STRING_SPLIT()es una mejor opción .

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

Si desea evitar que la longitud de la cadena sea <= el número de filas en sys.all_columns(9,980 pulgadas modelen SQL Server 2017; mucho más alto en sus propias bases de datos de usuarios), puede usar otros enfoques para derivar los números, como construyendo tu propia tabla de números . También podría usar un CTE recursivo en los casos en que no pueda usar tablas del sistema o crear las suyas propias:

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

Pero deberá agregar OPTION (MAXRECURSION 0)(o MAXRECURSION <longest possible string length if < 32768>) a la consulta externa para evitar errores con recursividad para cadenas> 100 caracteres. Si esa tampoco es una buena alternativa, vea esta respuesta como se señala en los comentarios.

(Además, el delimitador tendrá que ser NCHAR(<=1228). Todavía estoy investigando por qué).

Más información sobre las funciones divididas, por qué (y prueba de ello) mientras que los bucles y los CTE recursivos no se escalan, y mejores alternativas, si las cadenas de división provienen de la capa de aplicación:


1
Hay un pequeño error en este procedimiento para el caso en que habría un valor nulo al final de la cadena, como en '1,2, 4', ya que el valor final no se analiza. Para corregir este error, la expresión "WHERE Number <= LEN (@List)" debe reemplazarse por "WHERE Number <= LEN (@List) + 1".
SylvainL

@SylvainL Supongo que eso depende del comportamiento que quieras. En mi experiencia, la mayoría de la gente quiere ignorar las comas finales ya que realmente no representan un elemento real (¿cuántas copias de una cadena en blanco necesita)? De todos modos, la forma real de hacer esto, si sigues el segundo enlace, es jugar con dividir cadenas grandes y feas en T-SQL lento de todos modos.
Aaron Bertrand

1
Como ha dicho, la mayoría de la gente quiere ignorar las comas finales, pero no todas. Supongo que una solución más completa sería agregar un parámetro para especificar qué hacer en este caso, pero mi comentario es solo una pequeña nota para asegurarme de que nadie se olvide de esta posibilidad, ya que puede ser bastante real en muchos casos.
SylvainL

Tengo un comportamiento extraño con esa función. Si uso directamente una cadena como parámetro, funciona. Si tengo un varchar, no lo tiene. Puede reproducir fácilmente: declare invarchar como varchar set invarchar = 'ta; aa; qq' SELECT Value from [dbo]. [SplitString] (invarchar, ';') SELECT Value from [dbo]. [SplitString] ('ta; aa; qq ','; ')
Patrick Desjardins

Me gusta este enfoque, pero si el número de objetos devueltos por sys.all_objectses menor que el número de caracteres en la cadena de entrada, truncará la cadena y los valores se perderán. Como sys.all_objectssolo se está utilizando como un truco para generar filas, existen mejores formas de hacerlo, por ejemplo, esta respuesta .
nudillos

56

Finalmente, la espera ha terminado en SQL Server 2016 , han introducido la función de cadena dividida:STRING_SPLIT

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

Todos los demás métodos para dividir cadenas como XML, tabla de conteo, bucle, etc., han quedado asombrados por esta STRING_SPLITfunción.

Aquí hay un excelente artículo con comparación de rendimiento: Sorpresas y supuestos de rendimiento: STRING_SPLIT


55
obviamente responde a la pregunta de cómo dividir cadenas para aquellos con servidores actualizados, pero aquellos de nosotros que todavía estamos atrapados en 2008 / 2008R2, tendremos que ir con una de las otras respuestas aquí.
mpag

2
Debe echar un vistazo al nivel de compatibilidad en su base de datos. Si es inferior a 130, no podrá utilizar la función STRING_SPLIT.
Luis Teijon

En realidad, si la compatibilidad no es 130 y está ejecutando 2016 (o Azure SQL), puede configurar la compatibilidad hasta 130 usando: ALTER DATABASE DatabaseName SET COMPATIBILITY_LEVEL = 130
Michieal

23

La forma más fácil de hacerlo es mediante el XMLformato.

1. Convertir cadenas a filas sin tabla

CONSULTA

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)

RESULTADO

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

2. Convertir a filas de una tabla que tiene una ID para cada fila CSV

TABLA DE FUENTES

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

CONSULTA

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

RESULTADO

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

Este enfoque se romperá si @Stringcontiene caracteres prohibidos ... Acabo de publicar una respuesta para superar este problema.
Shnugo

9

Necesitaba una manera rápida de deshacerse de la +4de un código postal .

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

Sin proceso ... sin UDF ... solo un pequeño comando en línea que hace lo que debe hacer. No es lujoso, no elegante.

Cambie el delimitador según sea necesario, etc., y funcionará para cualquier cosa.


44
De esto no se trata la pregunta. El OP tiene un valor como '234,542,23' y quieren dividirlo en tres filas ... 1ra fila: 234, 2da fila: 542, 3ra fila: 23. Es algo complicado de hacer en SQL.
codeulike

7

si reemplazas

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

con

WHILE LEN(@stringToSplit) > 0

¡puedes eliminar esa última inserción después del ciclo 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

Esto daría como resultado el último carácter del último elemento truncado. es decir, "AL, AL" se convertiría en "AL" | "A", es decir, "ABC, ABC, ABC" se convertiría en "ABC" | "ABC" | "AB"
Desarrollador de Microsoft

añadiendo +1que SELECT @pos = LEN(@stringToSplit)aparece para abordar esa cuestión. Sin embargo, SELECT @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, LEN(@stringToSplit)-@pos)volverá a Invalid length parameter passed to the LEFT or SUBSTRING functionmenos que agregue también +1al tercer parámetro de SUBSTRING. o puede reemplazar esa tarea conSET @stringToSplit = SUBSTRING(@stringToSplit, @pos+1, 4000) --MAX len of nvarchar is 4000
mpag

1
Publiqué algunas mejoras (con casos de prueba de respaldo) en mi página de github aquí . Lo publicaré como respuesta en este hilo de desbordamiento de pila cuando tenga suficiente representante para exceder la "protección" de la
publicación

Yo también he notado el problema señalado por Terry arriba. Pero la lógica dada por @AviG es tan genial que no falla en el medio para una larga lista de tokens. Intente esta llamada de prueba para verificar (Esta llamada debe devolver 969 tokens) seleccione * de dbo.splitstring ('token1, token2 ,,,,,,,, token969') Luego probé el código proporcionado por mpag para verificar los resultados. llame arriba y descubrió que solo puede devolver 365 tokens. Finalmente arreglé el código de AviG arriba y publiqué la función libre de errores como una nueva respuesta a continuación, ya que el comentario aquí solo permite texto limitado. Comprueba la respuesta debajo de mi nombre para probarlo.
Gemunu R Wickremasinghe

3

Todas las funciones para la división de cadenas que utilizan algún tipo de bucle (iteraciones) tienen un mal rendimiento. Deben reemplazarse con una solución basada en conjuntos.

Este código se ejecuta excelente.

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

Este enfoque se romperá si @Listcontiene caracteres prohibidos ... Acabo de publicar una respuesta para superar este problema.
Shnugo

Estoy votando su respuesta porque la suya funciona con el espacio como delimitador y el que más votó no lo hace
KMC

3

El enfoque de uso frecuente con elementos XML se rompe en caso de caracteres prohibidos. Este es un enfoque para usar este método con cualquier tipo de carácter, incluso con el punto y coma como delimitador.

El truco es, primero, usar SELECT SomeString AS [*] FOR XML PATH('')para que todos los personajes prohibidos escapen adecuadamente. Esa es la razón por la cual reemplazo el delimitador a un valor mágico para evitar problemas con el ;delimitador.

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)

El resultado

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

2

Tuve que escribir algo como esto recientemente. Aquí está la solución que se me ocurrió. Está generalizado para cualquier cadena de delimitador y creo que funcionaría un poco mejor:

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

Una solución usando un CTE, si alguien lo necesita (aparte de mí, quién obviamente lo hizo, por eso lo escribí).

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

Esto es más estrictamente adaptado. Cuando hago esto, generalmente tengo una lista delimitada por comas de identificadores únicos (INT o BIGINT), que quiero convertir como una tabla para usar como una unión interna a otra tabla que tiene una clave primaria de INT o BIGINT. Quiero que se devuelva una función con valores de tabla en línea para que tenga la combinación más eficiente posible.

El uso de la muestra sería:

 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

Robé la idea de http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html , cambiándola para que tenga un valor de tabla en línea y se convierta en 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

Hay una versión correcta aquí, pero pensé que sería bueno agregar un poco de tolerancia a fallas en caso de que tengan una coma final, así como hacerlo para que pueda usarlo no como una función sino como parte de un código más grande . En caso de que solo lo use una vez y no necesite una función. Esto también es para enteros (que es para lo que lo necesitaba), por lo que es posible que tenga que cambiar sus tipos de datos.

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

si lo SET @StringToSeperate = @StringToSeperate+','hiciera inmediatamente antes del WHILEciclo, creo que podría eliminar el bloque "agregar último valor". Vea también mi solución en github
mpag

¿En qué respuesta se basa esto? Aquí hay muchas respuestas, y es un poco confuso. Gracias.
jpaugh 01 de

1

Modifiqué un poco la función de Andy Robinson. Ahora puede seleccionar solo la parte requerida de la tabla de retorno:

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

Si necesita una solución rápida ad-hoc para casos comunes con un código mínimo, este CTE recursivo de dos líneas lo hará:

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

Use esto como una declaración independiente o simplemente agregue los CTE anteriores a cualquiera de sus consultas y podrá unirse a la tabla resultante bcon otros para usar en cualquier otra expresión.

editar (por Shnugo)

Si agrega un contador, obtendrá un índice de posición junto con la Lista:

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;

El resultado:

n   s
1   1
2   2333
3   344
4   4

Me gusta este enfoque. Espero que no te importe, que agregué algunas mejoras directamente en tu respuesta. Simplemente no dude en modificar este cualquier manera conveniente ...
Shnugo

1

Tomo la ruta xml envolviendo los valores en elementos (M pero todo funciona):

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

Aquí hay una versión que puede dividirse en un patrón usando patindex, una simple adaptación de la publicación anterior. Tuve un caso en el que necesitaba dividir una cadena que contenía múltiples caracteres separadores.


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','%[/,]%');

el resultado se ve así

stringa stringb x y z


0

Personalmente uso esta función:

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

He desarrollado un divisor doble (toma dos caracteres divididos) como se solicitó aquí . Podría ser de algún valor en este hilo, ya que es el más referenciado para consultas relacionadas con la división de cadenas.

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

Uso:

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

Uso posible (Obtenga el segundo valor de cada división):

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

Aquí hay un ejemplo que puede usar como función o también puede poner la misma lógica en el procedimiento. --SELECT * from [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;

Este enfoque se romperá si @vCSVcontiene caracteres prohibidos ... Acabo de publicar una respuesta para superar este problema.
Shnugo

0

Una solución recursiva basada en 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

Esto se basa en la respuesta de Andy Robertson, necesitaba un delimitador distinto de la coma.

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

Y para usarlo:

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

(Probado en SQL Server 2008 R2)

EDITAR: código de prueba correcto


0

/ *

Respuesta a la cadena dividida T-SQL
Basado en las respuestas de Andy Robinson y AviG
Funcionalidad mejorada ref: función LEN sin incluir espacios finales en SQL Server
Este 'archivo' debe ser válido como un archivo de descuento y un archivo 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

/*

Casos de prueba: ver URL referenciada como "funcionalidad mejorada" arriba

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

* /


revertido para honrar "Este 'archivo' debe ser válido como un archivo de rebajas y un archivo 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

Puedes usar esta función:

        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

Con el debido respeto a @AviG, esta es la versión libre de errores de la función que le ha devuelto para devolver todos los tokens en su totalidad.

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

La forma más fácil:

  1. Instalar SQL Server 2016
  2. Use STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx

Funciona incluso en edición express :).


No olvide establecer el "Nivel de compatibilidad" en SQL Server 2016 (130): en el estudio de administración, haga clic con el botón derecho en la base de datos, propiedades / opciones / nivel de compatibilidad.
Tomino

1
La publicación original decía para SQL 2008 R2. La instalación de SQL 2016 puede no ser una opción
Shawn Gavett
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.