¿Cómo divido una cadena para poder acceder al elemento x?

493

Con SQL Server, ¿cómo divido una cadena para poder acceder al elemento x?

Tome una cadena "Hola John Smith". ¿Cómo puedo dividir la cadena por espacio y acceder al elemento en el índice 1 que debería devolver "John"?

GateKiller
fuente
3
Ver stackoverflow.com/questions/314824/… también
Jarrod Dixon
55
incorporado a partir del servidor sql 2016 msdn.microsoft.com/en-us/library/mt684588.aspx
Tim Abell
44
Las respuestas más altas aquí son, al menos para mí, bastante anticuadas y bastante anticuadas. Locig de procedimiento, bucles, recursiones, CLR, funciones, muchas líneas de código ... Puede ser interesante leer las respuestas "activas" para encontrar enfoques más actualizados .
Shnugo
He agregado una nueva respuesta con un enfoque más actualizado: stackoverflow.com/a/49669994/632604
Gorgi Rankovski
Intenta obtener el enésimo elemento de una lista -> portosql.wordpress.com/2019/05/27/enesimo-elemento-lista
José Diz

Respuestas:

191

Puede encontrar útil la solución en la función definida por el usuario de SQL para analizar una cadena delimitada (desde The Code Project ).

Puedes usar esta lógica simple:

Declare @products varchar(200) = '1|20|3|343|44|6|8765'
Declare @individual varchar(20) = null

WHILE LEN(@products) > 0
BEGIN
    IF PATINDEX('%|%', @products) > 0
    BEGIN
        SET @individual = SUBSTRING(@products,
                                    0,
                                    PATINDEX('%|%', @products))
        SELECT @individual

        SET @products = SUBSTRING(@products,
                                  LEN(@individual + '|') + 1,
                                  LEN(@products))
    END
    ELSE
    BEGIN
        SET @individual = @products
        SET @products = NULL
        SELECT @individual
    END
END
Jonesinator
fuente
1
¿Por qué SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( RTRIM( LTRIM( @p_SourceText)))no SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( @p_SourceText)?
Beth
12
@GateKiller Esta solución no es compatible con Unicode y utiliza números numéricos codificados (18,3) que no la convierten en una función viable "reutilizable".
Filip De Vos
44
Esto funciona pero asigna mucha memoria y desperdicia CPU.
jjxtra
2
A partir de SQL Server 2016, ahora hay una función integrada STRING_SPLITque dividirá una cadena y devolverá un resultado de tabla de una columna que puede usar en una SELECTdeclaración o en otro lugar.
qJake
Lástima que los chicos para los que trabajo no estén en 2016. Pero, lo tendré en cuenta en caso de que alguna vez se salgan con la suya. Gran solución en el ínterin. Lo implementé como una función y agregué delimitador como argumento.
Brandon Griffin
355

No creo que SQL Server tenga una función de división integrada, por lo que, aparte de un UDF, la única otra respuesta que conozco es secuestrar la función PARSENAME:

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 2) 

PARSENAME toma una cadena y la divide en el carácter de punto. Toma un número como su segundo argumento, y ese número especifica qué segmento de la cadena devolver (trabajando de atrás hacia adelante).

SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 3)  --return Hello

El problema obvio es cuando la cadena ya contiene un punto. Sigo pensando que usar un UDF es la mejor manera ... ¿alguna otra sugerencia?

Nathan Bedford
fuente
102
Gracias Saul ... Debo señalar que esta solución es realmente una mala solución para el desarrollo real. PARSENAME solo espera cuatro partes, por lo que usar una cadena con más de cuatro partes hace que devuelva NULL. Las soluciones UDF son obviamente mejores.
Nathan Bedford el
33
Este es un gran truco, y también me hace llorar porque algo como esto es necesario para algo tan simple en idiomas reales.
Factor Mystic
36
Para que los índices funcionen de la manera "correcta", es decir, comenzando en 1, he secuestrado su secuestro con REVERSE: REVERSE (PARSENAME (REPLACE (REVERSE ('Hello John Smith'), '', '.') , 1)) - Devoluciones Hello
NothingsImpossible
3
@FactorMystic First Normal Form requiere que no coloque varios valores en un solo campo. Es literalmente la primera regla de un RDBMS. SPLIT()No se proporciona una función porque fomenta el diseño deficiente de la base de datos, y la base de datos nunca se optimizará para usar datos almacenados en este formato. El RDBMS no está obligado a ayudar a los desarrolladores a hacer cosas estúpidas para las que ha sido diseñado para no manejar. La respuesta correcta siempre será "Normalice su base de datos como le dijimos hace 40 años". Ni SQL ni RDBMS tienen la culpa de un diseño deficiente.
Bacon Bits
8
@BaconBits, aunque estoy de acuerdo en teoría, en la práctica, herramientas como esta son útiles para normalizar un diseño deficiente producido por alguien que vino antes que usted.
Tim Abell
110

Primero, cree una función (usando CTE, la expresión de tabla común elimina la necesidad de una tabla temporal)

 create function dbo.SplitString 
    (
        @str nvarchar(4000), 
        @separator char(1)
    )
    returns table
    AS
    return (
        with tokens(p, a, b) AS (
            select 
                1, 
                1, 
                charindex(@separator, @str)
            union all
            select
                p + 1, 
                b + 1, 
                charindex(@separator, @str, b + 1)
            from tokens
            where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
      )
    GO

Luego, úselo como cualquier tabla (o modifíquelo para que se ajuste a su proceso almacenado existente) como este.

select s 
from dbo.SplitString('Hello John Smith', ' ')
where zeroBasedOccurance=1

Actualizar

La versión anterior fallaría para una cadena de entrada de más de 4000 caracteres. Esta versión se encarga de la limitación:

create function dbo.SplitString 
(
    @str nvarchar(max), 
    @separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
    select 
        cast(1 as bigint), 
        cast(1 as bigint), 
        charindex(@separator, @str)
    union all
    select
        p + 1, 
        b + 1, 
        charindex(@separator, @str, b + 1)
    from tokens
    where b > 0
)
select
    p-1 ItemIndex,
    substring(
        @str, 
        a, 
        case when b > 0 then b-a ELSE LEN(@str) end) 
    AS s
from tokens
);

GO

El uso sigue siendo el mismo.

vzczc
fuente
14
Es elegante pero solo funciona para 100 elementos debido al límite de profundidad de recursión.
Pking
44
@Pking, no, el valor predeterminado es 100(para evitar el bucle infinito). Use la sugerencia MAXRECURSION para definir el número de niveles de recursión ( 0hasta 32767, 0es "sin límite", puede aplastar el servidor). Por cierto, mucho mejor respuesta que PARSENAME, porque es universal :-). +1
Michał Powaga
Además maxrecursionde esta solución, tenga en cuenta esta pregunta y sus respuestas. Cómo configurar la maxrecursionopción para un CTE dentro de una función con valores de tabla .
Michał Powaga
Específicamente, haga referencia a la respuesta de Crisfole : su método lo ralentiza un poco, pero es más simple que la mayoría de las otras opciones.
AHiggins
punto menor, pero el uso no sigue siendo el mismo porque cambió el nombre de la columna, por slo que ya no está definido
Tim Abell
62

La mayoría de las soluciones aquí usan bucles while o CTE recursivos. Un enfoque basado en conjuntos será superior, lo prometo, si puede usar un delimitador que no sea un espacio:

CREATE FUNCTION [dbo].[SplitString]
    (
        @List NVARCHAR(MAX),
        @Delim VARCHAR(255)
    )
    RETURNS TABLE
    AS
        RETURN ( SELECT [Value], idx = RANK() OVER (ORDER BY n) FROM 
          ( 
            SELECT n = Number, 
              [Value] = LTRIM(RTRIM(SUBSTRING(@List, [Number],
              CHARINDEX(@Delim, @List + @Delim, [Number]) - [Number])))
            FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
              FROM sys.all_objects) AS x
              WHERE Number <= LEN(@List)
              AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim)) = @Delim
          ) AS y
        );

Uso de la muestra:

SELECT Value FROM dbo.SplitString('foo,bar,blat,foo,splunge',',')
  WHERE idx = 3;

Resultados:

----
blat

También puedes agregar el idx que desee como argumento a la función, pero lo dejaré como ejercicio para el lector.

No puede hacer esto solo con la función nativaSTRING_SPLIT agregada en SQL Server 2016, porque no hay garantía de que la salida se procesará en el orden de la lista original. En otras palabras, si pasa 3,6,1el resultado, es probable que esté en ese orden, pero podría serlo 1,3,6. He pedido la ayuda de la comunidad para mejorar la función incorporada aquí:

Con suficientes comentarios cualitativos , en realidad pueden considerar hacer algunas de estas mejoras:

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:

Sin embargo, en SQL Server 2016 o superior, debe mirar STRING_SPLIT()y STRING_AGG():

Aaron Bertrand
fuente
1
La mejor respuesta, en mi humilde opinión. En algunas de las otras respuestas existe el problema del límite de recursión SQL de 100, pero no en este caso. Implementación muy rápida y muy simple. ¿Dónde está el botón +2?
T-moty
55
Probé esta función textualmente con el uso: select * from DBO.SplitString('Hello John smith', ' ');y la salida producida fue: Valor Hola llo llo o John ohn hn n smith mith ith th h
wwmbes
2
@AaronBertrand El problema original publicado por GateKiller involucra un delimitador de espacio.
wwmbes
1
@ usuario1255933 Abordado.
Aaron Bertrand
1
@ Michael Sí, eso es cierto. Tampoco tendría una tabla para seleccionar si no tuviera el permiso ALTER SCHEMA, y no podría seleccionarla si no tiene el permiso SELECT. Siempre puede pedirle a alguien que cree la función por usted . O créelo en algún lugar donde pueda crearlo (incluso temporalmente, por ejemplo en tempdb). Y en 2016+ debería usar STRING_SPLIT () y no una función que deba crear usted mismo de todos modos.
Aaron Bertrand
38

Puede aprovechar una tabla de números para analizar las cadenas.

Crea una tabla de números físicos:

    create table dbo.Numbers (N int primary key);
    insert into dbo.Numbers
        select top 1000 row_number() over(order by number) from master..spt_values
    go

Crear tabla de prueba con 1000000 filas

    create table #yak (i int identity(1,1) primary key, array varchar(50))

    insert into #yak(array)
        select 'a,b,c' from dbo.Numbers n cross join dbo.Numbers nn
    go

Crea la función

    create function [dbo].[ufn_ParseArray]
        (   @Input      nvarchar(4000), 
            @Delimiter  char(1) = ',',
            @BaseIdent  int
        )
    returns table as
    return  
        (   select  row_number() over (order by n asc) + (@BaseIdent - 1) [i],
                    substring(@Input, n, charindex(@Delimiter, @Input + @Delimiter, n) - n) s
            from    dbo.Numbers
            where   n <= convert(int, len(@Input)) and
                    substring(@Delimiter + @Input, n, 1) = @Delimiter
        )
    go

Uso (produce 3mil filas en 40s en mi computadora portátil)

    select * 
    from #yak 
    cross apply dbo.ufn_ParseArray(array, ',', 1)

limpiar

    drop table dbo.Numbers;
    drop function  [dbo].[ufn_ParseArray]

El rendimiento aquí no es sorprendente, pero llamar a una función en una tabla de un millón de filas no es la mejor idea. Si realiza una cadena dividida en muchas filas, evitaría la función.

Nathan Skerl
fuente
2
La mejor solución de la OMI, las otras tienen algún tipo de limitación ... esto es rápido y puede analizar cadenas largas con muchos elementos.
Pking
¿Por qué pides n descendente? Si hay tres elementos, y comenzamos a numerar en 1, entonces el primer elemento será el número 3, y el último será el número 1. ¿No daría resultados más intuitivos si descse eliminaran?
hacha - hecho con SOverflow
1
De acuerdo, sería más intuitivo en la dirección asc. Estaba siguiendo la convención parsename () que usa desc
Nathan Skerl el
3
alguna explicación sobre cómo funciona esto sería genial
Tim Abell
En una prueba en 100 millones de filas de hasta 3 campos para analizar, ufn_ParseArray no terminó después de 25 minutos, mientras que REVERSE(PARSENAME(REPLACE(REVERSE('Hello John Smith'), ' ', '.'), 1)) desde @NothingsImpossible se completó en 1,5 minutos. @hello_earth ¿Cómo se compararía su solución en cadenas más largas con más de 4 campos?
wwmbes
32

Esta pregunta es no se trata de un enfoque de división de cadenas , sino de cómo obtener el enésimo elemento .

Todas las respuestas aquí están haciendo algún tipo de división de cadenas usando recursividad, CTEs, múltipleCHARINDEX , REVERSEy PATINDEX, inventando funciones, llamada a los métodos de CLR, tablas de números, CROSS APPLYes ... La mayoría de las respuestas cubre muchas líneas de código.

Pero, si realmente no quieres nada más que un enfoque para obtener el enésimo elemento , esto se puede hacer como una sola línea real , sin UDF, ni siquiera una sub-selección ... Y como un beneficio adicional: escriba seguro

Obtenga la parte 2 delimitada por un espacio:

DECLARE @input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'<x>' + REPLACE(@input,N' ',N'</x><x>') + N'</x>' AS XML).value('/x[2]','nvarchar(max)')

Por supuesto , puede usar variables para delimitador y posición (usesql:column para recuperar la posición directamente del valor de una consulta):

DECLARE @dlmt NVARCHAR(10)=N' ';
DECLARE @pos INT = 2;
SELECT CAST(N'<x>' + REPLACE(@input,@dlmt,N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)')

Si su cadena puede incluir caracteres prohibidos (especialmente uno entre &><), aún puede hacerlo de esta manera. Solo use FOR XML PATHen su cadena primero para reemplazar todos los caracteres prohibidos con la secuencia de escape adecuada implícitamente.

Es un caso muy especial si, además, su delimitador es el punto y coma . En este caso, reemplazo el delimitador primero en '# DLMT #' y finalmente lo reemplazo por las etiquetas XML:

SET @input=N'Some <, > and &;Other äöü@€;One more';
SET @dlmt=N';';
SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(@input,@dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("@pos")][1]','nvarchar(max)');

ACTUALIZACIÓN para SQL-Server 2016+

Lamentablemente, los desarrolladores olvidaron devolver el índice de la pieza STRING_SPLIT. Pero, usando SQL-Server 2016+, hayJSON_VALUE y OPENJSON.

Con JSON_VALUE podemos pasar en la posición como el índice 'matriz.

Para OPENJSONla documentación indica claramente:

Cuando OPENJSON analiza una matriz JSON, la función devuelve los índices de los elementos en el texto JSON como claves.

Una cadena como 1,2,3necesita nada más que entre paréntesis: [1,2,3].
Una cadena de palabras como this is an exampledebe ser ["this","is","an","example"].
Estas son operaciones de cadena muy fáciles. Solo pruébalo:

DECLARE @str VARCHAR(100)='Hello John Smith';
DECLARE @position INT = 2;

--We can build the json-path '$[1]' using CONCAT
SELECT JSON_VALUE('["' + REPLACE(@str,' ','","') + '"]',CONCAT('$[',@position-1,']'));

- Vea esto para un separador de cadenas seguro para la posición ( basado en cero ):

SELECT  JsonArray.[key] AS [Position]
       ,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(@str,' ','","') + '"]') JsonArray

En esta publicación probé varios enfoques y descubrí que OPENJSONes realmente rápido. Incluso mucho más rápido que el famoso método "delimitedSplit8k ()" ...

ACTUALIZACIÓN 2: obtenga los valores de tipo seguro

Podemos usar una matriz dentro de una matriz simplemente usando doubled [[]]. Esto permite una WITHcláusula escrita :

DECLARE  @SomeDelimitedString VARCHAR(100)='part1|1|20190920';

DECLARE @JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(@SomeDelimitedString,'|','","'),'"]]');

SELECT @SomeDelimitedString          AS TheOriginal
      ,@JsonArray                    AS TransformedToJSON
      ,ValuesFromTheArray.*
FROM OPENJSON(@JsonArray)
WITH(TheFirstFragment  VARCHAR(100) '$[0]'
    ,TheSecondFragment INT          '$[1]'
    ,TheThirdFragment  DATE         '$[2]') ValuesFromTheArray
Shnugo
fuente
Re: si su cadena puede incluir caracteres prohibidos ... simplemente podría envolver las subcadenas de esta manera <x><![CDATA[x<&>x]]></x>.
Salman A
@SalmanA, sí, las CDATAsecciones también pueden ocuparse de esto ... Pero después del elenco se han ido (cambiado para escapar text()implícitamente). No me gusta la magia debajo del capó , así que preferiría el (SELECT 'Text with <&>' AS [*] FOR XML PATH(''))enfoque. Esto me parece más limpio y sucede de todos modos ... (Un poco más sobre CDATA y XML ).
Shnugo
22

Aquí hay un UDF que lo hará. Devolverá una tabla de los valores delimitados, no he probado todos los escenarios, pero su ejemplo funciona bien.


CREATE FUNCTION SplitString 
(
    -- Add the parameters for the function here
    @myString varchar(500),
    @deliminator varchar(10)
)
RETURNS 
@ReturnTable TABLE 
(
    -- Add the column definitions for the TABLE variable here
    [id] [int] IDENTITY(1,1) NOT NULL,
    [part] [varchar](50) NULL
)
AS
BEGIN
        Declare @iSpaces int
        Declare @part varchar(50)

        --initialize spaces
        Select @iSpaces = charindex(@deliminator,@myString,0)
        While @iSpaces > 0

        Begin
            Select @part = substring(@myString,0,charindex(@deliminator,@myString,0))

            Insert Into @ReturnTable(part)
            Select @part

    Select @myString = substring(@mystring,charindex(@deliminator,@myString,0)+ len(@deliminator),len(@myString) - charindex(' ',@myString,0))


            Select @iSpaces = charindex(@deliminator,@myString,0)
        end

        If len(@myString) > 0
            Insert Into @ReturnTable
            Select @myString

    RETURN 
END
GO

Lo llamarías así:


Select * From SplitString('Hello John Smith',' ')

Editar: Solución actualizada para manejar delimitadores con un len> 1 como en:


select * From SplitString('Hello**John**Smith','**')
Brendan
fuente
No funcionó para select * from dbo.ethos_SplitString_fn ('guy, wicks, was here', ',') parte de identificación ----------- ------------ -------------------------------------- 1 chico 2 mecha
Chico
2
tenga cuidado con len () ya que no devolverá el número correcto si su argumento tiene espacios finales, por ejemplo, len ('-') = 2.
Rory el
No funciona en: seleccione * de dbo.SplitString ('foo, foo test ,,,, foo', ',')
cbp
1
Arreglo para cbp .. Seleccione @myString = substring (@ mystring, @ iSpaces + len (@deliminator), len (@myString) - charindex (@ deliminator, @ myString, 0))
Alxwest
16

Aquí publico una forma simple de solución

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


Ejecute la función así

  select * from dbo.split('Hello John Smith',' ')
Sivaganesh Tamilvendhan
fuente
Me gustó esta solución. Se amplió para devolver un valor escalar basado en la columna especificada dentro de los resultados.
Alan
Me quemé con un '&' en la cadena para dividirlo usando esto
KeithL
10

En mi opinión, ustedes lo están haciendo demasiado complicado. Simplemente cree un CLR UDF y termine con él.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Collections.Generic;

public partial class UserDefinedFunctions {
  [SqlFunction]
  public static SqlString SearchString(string Search) {
    List<string> SearchWords = new List<string>();
    foreach (string s in Search.Split(new char[] { ' ' })) {
      if (!s.ToLower().Equals("or") && !s.ToLower().Equals("and")) {
        SearchWords.Add(s);
      }
    }

    return new SqlString(string.Join(" OR ", SearchWords.ToArray()));
  }
};
Damon Drake
fuente
20
Supongo que esto es demasiado complicado, porque necesito tener Visual Studio, luego habilitar CLR en el servidor, luego crear y compilar el proyecto, y finalmente agregar los ensamblados a la base de datos, para poder usarlo. Pero aún así es una respuesta interesante.
Guillermo Gutiérrez
3
@ guillegr123, no tiene que ser complicado. Puede descargar e instalar (¡gratis!), SQL #, que es una biblioteca de funciones y procedimientos SQLCLR. Puede obtenerlo de SQLsharp.com . Sí, soy el autor, pero String_Split está incluido en la versión gratuita.
Solomon Rutzky
10

¿Qué pasa con el uso stringy la values()declaración?

DECLARE @str varchar(max)
SET @str = 'Hello John Smith'

DECLARE @separator varchar(max)
SET @separator = ' '

DECLARE @Splited TABLE(id int IDENTITY(1,1), item varchar(max))

SET @str = REPLACE(@str, @separator, '''),(''')
SET @str = 'SELECT * FROM (VALUES(''' + @str + ''')) AS V(A)' 

INSERT INTO @Splited
EXEC(@str)

SELECT * FROM @Splited

Conjunto de resultados alcanzado.

id  item
1   Hello
2   John
3   Smith
Frederic
fuente
1
Utilicé su respuesta pero no funcionó, pero modifiqué y esto funcionó con union all, estoy usando sql 2005
angel
9

Uso la respuesta de frederic pero esto no funcionó en SQL Server 2005

Lo modifiqué y estoy usando selectcon union ally funciona

DECLARE @str varchar(max)
SET @str = 'Hello John Smith how are you'

DECLARE @separator varchar(max)
SET @separator = ' '

DECLARE @Splited table(id int IDENTITY(1,1), item varchar(max))

SET @str = REPLACE(@str, @separator, ''' UNION ALL SELECT ''')
SET @str = ' SELECT  ''' + @str + '''  ' 

INSERT INTO @Splited
EXEC(@str)

SELECT * FROM @Splited

Y el conjunto de resultados es:

id  item
1   Hello
2   John
3   Smith
4   how
5   are
6   you
ángel
fuente
Esto es realmente genial que he visto en cosas SQL, funcionó para mi trabajo y lo aprecio, ¡gracias!
Abdurrahman I.
Me emocioné mucho cuando vi esto porque parecía muy limpio y fácil de entender, pero desafortunadamente no puedes poner esto dentro de un UDF debido a eso EXEC. EXECllama implícitamente a un procedimiento almacenado, y no puede usar procedimientos almacenados en UDF.
Kristen Hammack
¡Esto funciona perfectamente! Estaba buscando usar una función (SplitStrings_Moden) desde aquí: sqlperformance.com/2012/07/t-sql-queries/split-strings#comments que hace esto y me tomó un minuto y medio dividir los datos y volver las filas cuando solo se usan 4 números de cuenta. ¡Probé su versión con una combinación izquierda en la tabla con los datos de los números de cuenta y me tomó como 2 o 3 segundos! Gran diferencia y funciona a la perfección! ¡Daría estos 20 votos si es posible!
MattE
8

Este patrón funciona bien y puedes generalizar

Convert(xml,'<n>'+Replace(FIELD,'.','</n><n>')+'</n>').value('(/n[INDEX])','TYPE')
                          ^^^^^                                   ^^^^^     ^^^^

nota CAMPO , ÍNDICE y TIPO .

Deje alguna tabla con identificadores como

sys.message.1234.warning.A45
sys.message.1235.error.O98
....

Entonces, puedes escribir

SELECT Source         = q.value('(/n[1])', 'varchar(10)'),
       RecordType     = q.value('(/n[2])', 'varchar(20)'),
       RecordNumber   = q.value('(/n[3])', 'int'),
       Status         = q.value('(/n[4])', 'varchar(5)')
FROM   (
         SELECT   q = Convert(xml,'<n>'+Replace(fieldName,'.','</n><n>')+'</n>')
         FROM     some_TABLE
       ) Q

División y fundición de todas las partes.

josejuan
fuente
Esta es la única solución aquí que le permite emitir a tipos específicos, y es moderadamente eficiente (CLR aún es más eficiente, pero este enfoque maneja una tabla de filas de 8 gb, 10 tokens, 10 M en aproximadamente 9 minutos (servidor aws m3, 4k iops unidad aprovisionada)
Andrew Hill
7

Si su base de datos tiene un nivel de compatibilidad de 130 o superior, puede usar la función STRING_SPLIT junto con OFFSET FETCH cláusulas para obtener el elemento específico por índice.

Para obtener el elemento en el índice N (basado en cero), puede usar el siguiente código

SELECT value
FROM STRING_SPLIT('Hello John Smith',' ')
ORDER BY (SELECT NULL)
OFFSET N ROWS
FETCH NEXT 1 ROWS ONLY

Para verificar el nivel de compatibilidad de su base de datos , ejecute este código:

SELECT compatibility_level  
FROM sys.databases WHERE name = 'YourDBName';
Gorgi Rankovski
fuente
El truco está en las FILAS DE DESPLAZAMIENTO 1, que omitirá el primer elemento y devolverá el segundo elemento. Si sus índices están basados ​​en 0 y @X es la variable que contiene el índice de elementos que desea obtener, puede hacer OFFSET @X ROWS
Gorgi Rankovski
De acuerdo, no xmlusé esto antes ... Es bueno saberlo ... Todavía preferiría el enfoque basado en -split, ya que permite obtener el valor de tipo seguro y no necesita una subconsulta, pero esta es una bueno +1 de mi lado
Shnugo
3
El problema aquí es que STRING_SPLIT no garantiza el orden de los resultados devueltos. Entonces su artículo 1 puede o no ser mi artículo 1.
user1443098
@GorgiRankovski, Uso de STRING_SPLITdemandas para v2016 +. En este caso, es mucho mejor usar OPENJSONo JSON_VALUE. Es posible que desee comprobar mi respuesta
Shnugo 01 de
6

Estaba buscando la solución en la red y lo siguiente funciona para mí. Ref .

Y llamas a la función así:

SELECT * FROM dbo.split('ram shyam hari gopal',' ')

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE FUNCTION [dbo].[Split](@String VARCHAR(8000), @Delimiter CHAR(1))       
RETURNS @temptable TABLE (items VARCHAR(8000))       
AS       
BEGIN       
    DECLARE @idx INT       
    DECLARE @slice VARCHAR(8000)        
    SELECT @idx = 1       
    IF len(@String)<1 OR @String IS NULL  RETURN       
    WHILE @idx!= 0       
    BEGIN       
        SET @idx = charindex(@Delimiter,@String)       
        IF @idx!=0       
            SET @slice = LEFT(@String,@idx - 1)       
        ELSE       
            SET @slice = @String       
        IF(len(@slice)>0)  
            INSERT INTO @temptable(Items) VALUES(@slice)       
        SET @String = RIGHT(@String,len(@String) - @idx)       
        IF len(@String) = 0 break       
    END   
    RETURN       
END
kta
fuente
No puede acceder fácilmente al enésimo elemento con esta función.
Björn Lindqvist
6

Sin embargo, otro obtiene la enésima parte de la cadena por función delimitador:

create function GetStringPartByDelimeter (
    @value as nvarchar(max),
    @delimeter as nvarchar(max),
    @position as int
) returns NVARCHAR(MAX) 
AS BEGIN
    declare @startPos as int
    declare @endPos as int
    set @endPos = -1
    while (@position > 0 and @endPos != 0) begin
        set @startPos = @endPos + 1
        set @endPos = charindex(@delimeter, @value, @startPos)

        if(@position = 1) begin
            if(@endPos = 0)
                set @endPos = len(@value) + 1

            return substring(@value, @startPos, @endPos - @startPos)
        end

        set @position = @position - 1
    end

    return null
end

y el uso:

select dbo.GetStringPartByDelimeter ('a;b;c;d;e', ';', 3)

que devuelve:

c
Ramazan Binarbasi
fuente
Me gusta esta solución como una opción para devolver una sola subcadena en lugar de obtener una tabla analizada que luego debe seleccionar. Usar un resultado de tabla tiene sus usos, pero para lo que necesitaba funcionaba perfectamente.
James H
5

Prueba esto:

CREATE function [SplitWordList]
(
 @list varchar(8000)
)
returns @t table 
(
 Word varchar(50) not null,
 Position int identity(1,1) not null
)
as begin
  declare 
    @pos int,
    @lpos int,
    @item varchar(100),
    @ignore varchar(100),
    @dl int,
    @a1 int,
    @a2 int,
    @z1 int,
    @z2 int,
    @n1 int,
    @n2 int,
    @c varchar(1),
    @a smallint
  select 
    @a1 = ascii('a'),
    @a2 = ascii('A'),
    @z1 = ascii('z'),
    @z2 = ascii('Z'),
    @n1 = ascii('0'),
    @n2 = ascii('9')
  set @ignore = '''"'
  set @pos = 1
  set @dl = datalength(@list)
  set @lpos = 1
  set @item = ''
  while (@pos <= @dl) begin
    set @c = substring(@list, @pos, 1)
    if (@ignore not like '%' + @c + '%') begin
      set @a = ascii(@c)
      if ((@a >= @a1) and (@a <= @z1))  
        or ((@a >= @a2) and (@a <= @z2))
        or ((@a >= @n1) and (@a <= @n2))
      begin
        set @item = @item + @c
      end else if (@item > '') begin
        insert into @t values (@item)
        set @item = ''
      end
    end 
    set @pos = @pos + 1
  end
  if (@item > '') begin
    insert into @t values (@item)
  end
  return
end

Pruébalo así:

select * from SplitWordList('Hello John Smith')
Seibar
fuente
¡Lo he pasado y es perfectamente como lo que quiero! ¡incluso puedo personalizarlo para ignorar los caracteres especiales que elijo!
Vikas
5

El siguiente ejemplo usa un CTE recursivo

Actualización 18.09.2013

CREATE FUNCTION dbo.SplitStrings_CTE(@List nvarchar(max), @Delimiter nvarchar(1))
RETURNS @returns TABLE (val nvarchar(max), [level] int, PRIMARY KEY CLUSTERED([level]))
AS
BEGIN
;WITH cte AS
 (
  SELECT SUBSTRING(@List, 0, CHARINDEX(@Delimiter,  @List + @Delimiter)) AS val,
         CAST(STUFF(@List + @Delimiter, 1, CHARINDEX(@Delimiter, @List + @Delimiter), '') AS nvarchar(max)) AS stval, 
         1 AS [level]
  UNION ALL
  SELECT SUBSTRING(stval, 0, CHARINDEX(@Delimiter, stval)),
         CAST(STUFF(stval, 1, CHARINDEX(@Delimiter, stval), '') AS nvarchar(max)),
         [level] + 1
  FROM cte
  WHERE stval != ''
  )
  INSERT @returns
  SELECT REPLACE(val, ' ','' ) AS val, [level]
  FROM cte
  WHERE val > ''
  RETURN
END

Demostración en SQLFiddle

Aleksandr Fedorenko
fuente
2


    Alter Function dbo.fn_Split
    (
    @Expression nvarchar(max),
    @Delimiter  nvarchar(20) = ',',
    @Qualifier  char(1) = Null
    )
    RETURNS @Results TABLE (id int IDENTITY(1,1), value nvarchar(max))
    AS
    BEGIN
       /* USAGE
            Select * From dbo.fn_Split('apple pear grape banana orange honeydew cantalope 3 2 1 4', ' ', Null)
            Select * From dbo.fn_Split('1,abc,"Doe, John",4', ',', '"')
            Select * From dbo.fn_Split('Hello 0,"&""&&&&', ',', '"')
       */

       -- Declare Variables
       DECLARE
          @X     xml,
          @Temp  nvarchar(max),
          @Temp2 nvarchar(max),
          @Start int,
          @End   int

       -- HTML Encode @Expression
       Select @Expression = (Select @Expression For XML Path(''))

       -- Find all occurences of @Delimiter within @Qualifier and replace with |||***|||
       While PATINDEX('%' + @Qualifier + '%', @Expression) > 0 AND Len(IsNull(@Qualifier, '')) > 0
       BEGIN
          Select
             -- Starting character position of @Qualifier
             @Start = PATINDEX('%' + @Qualifier + '%', @Expression),
             -- @Expression starting at the @Start position
             @Temp = SubString(@Expression, @Start + 1, LEN(@Expression)-@Start+1),
             -- Next position of @Qualifier within @Expression
             @End = PATINDEX('%' + @Qualifier + '%', @Temp) - 1,
             -- The part of Expression found between the @Qualifiers
             @Temp2 = Case When @End < 0 Then @Temp Else Left(@Temp, @End) End,
             -- New @Expression
             @Expression = REPLACE(@Expression,
                                   @Qualifier + @Temp2 + Case When @End < 0 Then '' Else @Qualifier End,
                                   Replace(@Temp2, @Delimiter, '|||***|||')
                           )
       END

       -- Replace all occurences of @Delimiter within @Expression with '</fn_Split><fn_Split>'
       -- And convert it to XML so we can select from it
       SET
          @X = Cast('<fn_Split>' +
                    Replace(@Expression, @Delimiter, '</fn_Split><fn_Split>') +
                    '</fn_Split>' as xml)

       -- Insert into our returnable table replacing '|||***|||' back to @Delimiter
       INSERT @Results
       SELECT
          "Value" = LTRIM(RTrim(Replace(C.value('.', 'nvarchar(max)'), '|||***|||', @Delimiter)))
       FROM
          @X.nodes('fn_Split') as X(C)

       -- Return our temp table
       RETURN
    END
Tirano saurio Rex
fuente
2

Puede dividir una cadena en SQL sin necesidad de una función:

DECLARE @bla varchar(MAX)
SET @bla = 'BED40DFC-F468-46DD-8017-00EF2FA3E4A4,64B59FC5-3F4D-4B0E-9A48-01F3D4F220B0,A611A108-97CA-42F3-A2E1-057165339719,E72D95EA-578F-45FC-88E5-075F66FD726C'

-- http://stackoverflow.com/questions/14712864/how-to-query-values-from-xml-nodes
SELECT 
    x.XmlCol.value('.', 'varchar(36)') AS val 
FROM 
(
    SELECT 
    CAST('<e>' + REPLACE(@bla, ',', '</e><e>') + '</e>' AS xml) AS RawXml
) AS b 
CROSS APPLY b.RawXml.nodes('e') x(XmlCol);

Si necesita admitir cadenas arbitrarias (con caracteres especiales xml)

DECLARE @bla NVARCHAR(MAX)
SET @bla = '<html>unsafe & safe Utf8CharsDon''tGetEncoded ÄöÜ - "Conex"<html>,Barnes & Noble,abc,def,ghi'

-- http://stackoverflow.com/questions/14712864/how-to-query-values-from-xml-nodes
SELECT 
    x.XmlCol.value('.', 'nvarchar(MAX)') AS val 
FROM 
(
    SELECT 
    CAST('<e>' + REPLACE((SELECT @bla FOR XML PATH('')), ',', '</e><e>') + '</e>' AS xml) AS RawXml
) AS b 
CROSS APPLY b.RawXml.nodes('e') x(XmlCol); 
Stefan Steiger
fuente
1

Sé que es una vieja pregunta, pero creo que alguien puede beneficiarse de mi solución.

select 
SUBSTRING(column_name,1,CHARINDEX(' ',column_name,1)-1)
,SUBSTRING(SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name))
    ,1
    ,CHARINDEX(' ',SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name)),1)-1)
,SUBSTRING(SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name))
    ,CHARINDEX(' ',SUBSTRING(column_name,CHARINDEX(' ',column_name,1)+1,LEN(column_name)),1)+1
    ,LEN(column_name))
from table_name

FIDDLE SQL

Ventajas:

  • Separa los 3 delimitadores de subcadenas por ''.
  • No se debe usar el ciclo while, ya que disminuye el rendimiento.
  • No es necesario pivotar, ya que todas las subcadenas resultantes se mostrarán en una fila

Limitaciones:

  • Uno debe saber el no total. de espacios (subcadena).

Nota : la solución puede dar subcadenas hasta N.

Para superar la limitación, podemos usar la siguiente referencia .

Pero, de nuevo, la solución anterior no se puede usar en una tabla (Actaully no pude usarla).

De nuevo, espero que esta solución pueda ayudar a alguien.

Actualización: en el caso de registros> 50000, no es aconsejable usarlo, LOOPSya que degradará el rendimiento

Luv
fuente
1

Solución pura basada en conjuntos usando TVFcon recursivo CTE. Puede JOINy APPLYesta función a cualquier conjunto de datos.

create function [dbo].[SplitStringToResultSet] (@value varchar(max), @separator char(1))
returns table
as return
with r as (
    select value, cast(null as varchar(max)) [x], -1 [no] from (select rtrim(cast(@value as varchar(max))) [value]) as j
    union all
    select right(value, len(value)-case charindex(@separator, value) when 0 then len(value) else charindex(@separator, value) end) [value]
    , left(r.[value], case charindex(@separator, r.value) when 0 then len(r.value) else abs(charindex(@separator, r.[value])-1) end ) [x]
    , [no] + 1 [no]
    from r where value > '')

select ltrim(x) [value], [no] [index] from r where x is not null;
go

Uso:

select *
from [dbo].[SplitStringToResultSet]('Hello John Smith', ' ')
where [index] = 1;

Resultado:

value   index
-------------
John    1
Andrey Morozov
fuente
1

Casi todas las otras respuestas están reemplazando la cadena que se divide, lo que desperdicia los ciclos de la CPU y realiza asignaciones de memoria innecesarias.

Cubro una forma mucho mejor de hacer una división de cadenas aquí: http://www.digitalruby.com/split-string-sql-server/

Aquí está el código:

SET NOCOUNT ON

-- You will want to change nvarchar(MAX) to nvarchar(50), varchar(50) or whatever matches exactly with the string column you will be searching against
DECLARE @SplitStringTable TABLE (Value nvarchar(MAX) NOT NULL)
DECLARE @StringToSplit nvarchar(MAX) = 'your|string|to|split|here'
DECLARE @SplitEndPos int
DECLARE @SplitValue nvarchar(MAX)
DECLARE @SplitDelim nvarchar(1) = '|'
DECLARE @SplitStartPos int = 1

SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)

WHILE @SplitEndPos > 0
BEGIN
    SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, (@SplitEndPos - @SplitStartPos))
    INSERT @SplitStringTable (Value) VALUES (@SplitValue)
    SET @SplitStartPos = @SplitEndPos + 1
    SET @SplitEndPos = CHARINDEX(@SplitDelim, @StringToSplit, @SplitStartPos)
END

SET @SplitValue = SUBSTRING(@StringToSplit, @SplitStartPos, 2147483647)
INSERT @SplitStringTable (Value) VALUES(@SplitValue)

SET NOCOUNT OFF

-- You can select or join with the values in @SplitStringTable at this point.
jjxtra
fuente
0

Solución CTE recursiva con dolor de servidor, pruébela

Configuración del esquema de MS SQL Server 2008 :

create table Course( Courses varchar(100) );
insert into Course values ('Hello John Smith');

Consulta 1 :

with cte as
   ( select 
        left( Courses, charindex( ' ' , Courses) ) as a_l,
        cast( substring( Courses, 
                         charindex( ' ' , Courses) + 1 , 
                         len(Courses ) ) + ' ' 
              as varchar(100) )  as a_r,
        Courses as a,
        0 as n
     from Course t
    union all
      select 
        left(a_r, charindex( ' ' , a_r) ) as a_l,
        substring( a_r, charindex( ' ' , a_r) + 1 , len(a_R ) ) as a_r,
        cte.a,
        cte.n + 1 as n
    from Course t inner join cte 
         on t.Courses = cte.a and len( a_r ) > 0

   )
select a_l, n from cte
--where N = 1

Resultados :

|    A_L | N |
|--------|---|
| Hello  | 0 |
|  John  | 1 |
| Smith  | 2 |
dani herrera
fuente
0

Si bien es similar a la respuesta basada en xml de josejuan, descubrí que procesar la ruta xml solo una vez, luego pivotar fue moderadamente más eficiente:

select ID,
    [3] as PathProvidingID,
    [4] as PathProvider,
    [5] as ComponentProvidingID,
    [6] as ComponentProviding,
    [7] as InputRecievingID,
    [8] as InputRecieving,
    [9] as RowsPassed,
    [10] as InputRecieving2
    from
    (
    select id,message,d.* from sysssislog cross apply       ( 
          SELECT Item = y.i.value('(./text())[1]', 'varchar(200)'),
              row_number() over(order by y.i) as rn
          FROM 
          ( 
             SELECT x = CONVERT(XML, '<i>' + REPLACE(Message, ':', '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY x.nodes('i') AS y(i)
       ) d
       WHERE event
       = 
       'OnPipelineRowsSent'
    ) as tokens 
    pivot 
    ( max(item) for [rn] in ([3],[4],[5],[6],[7],[8],[9],[10]) 
    ) as data

corrió a las 8:30

select id,
tokens.value('(/n[3])', 'varchar(100)')as PathProvidingID,
tokens.value('(/n[4])', 'varchar(100)') as PathProvider,
tokens.value('(/n[5])', 'varchar(100)') as ComponentProvidingID,
tokens.value('(/n[6])', 'varchar(100)') as ComponentProviding,
tokens.value('(/n[7])', 'varchar(100)') as InputRecievingID,
tokens.value('(/n[8])', 'varchar(100)') as InputRecieving,
tokens.value('(/n[9])', 'varchar(100)') as RowsPassed
 from
(
    select id, Convert(xml,'<n>'+Replace(message,'.','</n><n>')+'</n>') tokens
         from sysssislog 
       WHERE event
       = 
       'OnPipelineRowsSent'
    ) as data

corrió en 9:20

Andrew Hill
fuente
0
CREATE FUNCTION [dbo].[fnSplitString] 
( 
    @string NVARCHAR(MAX), 
    @delimiter CHAR(1) 
) 
RETURNS @output TABLE(splitdata NVARCHAR(MAX) 
) 
BEGIN 
    DECLARE @start INT, @end INT 
    SELECT @start = 1, @end = CHARINDEX(@delimiter, @string) 
    WHILE @start < LEN(@string) + 1 BEGIN 
        IF @end = 0  
            SET @end = LEN(@string) + 1

        INSERT INTO @output (splitdata)  
        VALUES(SUBSTRING(@string, @start, @end - @start)) 
        SET @start = @end + 1 
        SET @end = CHARINDEX(@delimiter, @string, @start)

    END 
    RETURN 
END

Y USARLO

select *from dbo.fnSplitString('Querying SQL Server','')
Savas Adar
fuente
0

si alguien quiere obtener solo una parte del texto separado puede usar esto

seleccione * de fromSplitStringSep ('Word1 wordr2 word3', '')

CREATE function [dbo].[SplitStringSep] 
(
    @str nvarchar(4000), 
    @separator char(1)
)
returns table
AS
return (
    with tokens(p, a, b) AS (
        select 
        1, 
        1, 
        charindex(@separator, @str)
        union all
        select
            p + 1, 
            b + 1, 
            charindex(@separator, @str, b + 1)
        from tokens
        where b > 0
        )
        select
            p-1 zeroBasedOccurance,
            substring(
                @str, 
                a, 
                case when b > 0 then b-a ELSE 4000 end) 
            AS s
        from tokens
  )
nazim hatipoglu
fuente
0

Desarrollé esto,

declare @x nvarchar(Max) = 'ali.veli.deli.';
declare @item nvarchar(Max);
declare @splitter char='.';

while CHARINDEX(@splitter,@x) != 0
begin
    set @item = LEFT(@x,CHARINDEX(@splitter,@x))
    set @x    = RIGHT(@x,len(@x)-len(@item) )
     select @item as item, @x as x;
end

la única atención que debes hacer es dot '.' ese final de la @x siempre debe estar allí.

Ali CAKIL
fuente
0

basándose en la solución @NothingsImpossible, o, más bien, comentando la respuesta más votada (justo debajo de la respuesta aceptada), encontré la siguiente pregunta rápida y sucia solución satisface mis propias necesidades: tiene el beneficio de estar únicamente dentro del dominio SQL.

dado una cadena "primero; segundo; tercero; cuarto; quinto", digamos, quiero obtener el tercer token esto funciona solo si sabemos cuántos tokens tendrá la cadena, en este caso son 5. así que mi forma de acción es cortar los dos últimos tokens (consulta interna) y luego cortar los dos primeros tokens ( consulta externa)

Sé que esto es feo y cubre las condiciones específicas en las que estaba, pero lo estoy publicando en caso de que alguien lo encuentre útil. salud

select 
    REVERSE(
        SUBSTRING(
            reverse_substring, 
            0, 
            CHARINDEX(';', reverse_substring)
        )
    ) 
from 
(
    select 
        msg,
        SUBSTRING(
            REVERSE(msg), 
            CHARINDEX(
                ';', 
                REVERSE(msg), 
                CHARINDEX(
                    ';',
                    REVERSE(msg)
                )+1
            )+1,
            1000
        ) reverse_substring
    from 
    (
        select 'first;second;third;fourth;fifth' msg
    ) a
) b
hola_ tierra
fuente
esto funciona solo si sabemos cuántos tokens tendrá la cadena - una limitación de ruptura ...
Shnugo
0
declare @strng varchar(max)='hello john smith'
select (
    substring(
        @strng,
        charindex(' ', @strng) + 1,
        (
          (charindex(' ', @strng, charindex(' ', @strng) + 1))
          - charindex(' ',@strng)
        )
    ))
Smart003
fuente
0

A partir de SQL Server 2016 , string_split

DECLARE @string varchar(100) = 'Richard, Mike, Mark'

SELECT value FROM string_split(@string, ',')
Victor Hugo Terceros
fuente
Esto está muy bien, pero no aborda la cuestión de obtener el enésimo resultado.
Johnie Karr
STRING_SPLITno garantiza devolver el mismo pedido. Pero OPENJSONsí (ver mi respuesta (sección de actualización) )
Shnugo