Как разделить строку, чтобы получить доступ к элементу x?


493

Используя SQL Server, как мне разбить строку, чтобы я мог получить доступ к элементу x?

Возьми строку «Привет, Джон Смит». Как я могу разбить строку по пробелам и получить доступ к элементу с индексом 1, который должен возвращать «Джон»?



5
встроенный в SQL Server 2016 msdn.microsoft.com/en-us/library/mt684588.aspx
Тим Абелл

4
Самые высокие ответы здесь - по крайней мере для меня - довольно старомодные и довольно устаревшие. Процедурный локатор, циклы, рекурсии, CLR, функции, много строк кода ... Было бы интересно прочитать «активные» ответы, чтобы найти более современные подходы.
Шнуго

Я добавил новый ответ с более современным подходом: stackoverflow.com/a/49669994/632604
Горги Ранковски

Попробуйте получить n-й элемент списка -> portosql.wordpress.com/2019/05/27/enesimo-elemento-lista
Хосе Диз,

Ответы:


191

Вы можете найти решение в пользовательской функции SQL для анализа строки с разделителями полезно (из проекта кода ).

Вы можете использовать эту простую логику:

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

1
почему SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( RTRIM( LTRIM( @p_SourceText)))и нет SET @p_SourceText = RTRIM( LTRIM( @p_SourceText)) SET @w_Length = DATALENGTH( @p_SourceText)?
Бет

12
@GateKiller Это решение не поддерживает Unicode, и оно использует жестко закодированное число (18,3), что не делает его жизнеспособной «повторно используемой» функцией.
Филип Де Вос

4
Это работает, но выделяет много памяти и тратит процессор.
JJXTRA

2
Начиная с SQL Server 2016, теперь есть встроенная функция, STRING_SPLITкоторая будет разбивать строку и возвращать результат таблицы с одним столбцом, который можно использовать в SELECTвыражении или в другом месте.
qJake

Жаль, что парни, на которых я работаю, не в 2016 году. Но я буду помнить об этом на случай, если они когда-нибудь получат преимущество. Отличное решение в промежутке. Я реализовал это как функцию и добавил разделитель в качестве аргумента.
Брэндон Гриффин

355

Я не верю, что в SQL Server есть встроенная функция разбиения, поэтому, кроме UDF, единственный другой ответ, который я знаю, - это захват функции PARSENAME:

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

PARSENAME берет строку и разбивает ее на символ точки. Он принимает число в качестве второго аргумента, и это число указывает, какой сегмент строки нужно вернуть (работает сзади).

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

Очевидная проблема - когда строка уже содержит точку. Я все еще думаю, что использование UDF - лучший способ ... какие-нибудь другие предложения?


102
Спасибо, Сол ... Я должен отметить, что это решение действительно плохое решение для реальной разработки. PARSENAME ожидает только четыре части, поэтому использование строки из более чем четырех частей приводит к тому, что она возвращает NULL. Решения UDF явно лучше.
Натан Бедфорд

33
Это отличный хак, а также заставляет меня плакать, что что-то подобное необходимо для чего-то такого простого в реальных языках.
Фактор Мистик

36
Чтобы заставить индексы работать «правильным» образом, то есть начиная с 1, я угнал ваш угон с помощью REVERSE: REVERSE (PARSENAME (REPLACE (REVERSE ('Hello John Smith'), '', '.')) , 1)) - Возвращает Hello
NothingsImpossible

3
@FactorMystic Первая нормальная форма требует, чтобы вы не помещали несколько значений в одно поле. Это буквально первое правило СУРБД. SPLIT()Функция не входит в комплект , потому что она поощряет плохой дизайн базы данных, и база данных не будет оптимизирована для использования данных , сохраненных в этом формате. РСУБД не обязан помогают разработчикам делать глупости , что он был разработан не для ручки. Правильный ответ всегда будет: «Нормализуйте свою базу данных, как мы говорили вам 40 лет назад». Ни SQL, ни RDBMS не виноваты в плохом дизайне.
Бекон Бит

8
@BaconBits, хотя я согласен в теории, на практике подобные инструменты полезны при нормализации плохого дизайна, созданного кем-то, кто был до вас.
Тим Абелл

110

Во-первых, создайте функцию (используя CTE, общее табличное выражение устраняет необходимость во временной таблице)

 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

Затем используйте его как любую таблицу (или измените ее так, чтобы она соответствовала существующему хранимому процессу), например так.

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

Обновить

Предыдущая версия не будет работать для входной строки длиннее 4000 символов. Эта версия заботится об ограничении:

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

Использование остается прежним.


14
Это элегантно, но работает только для 100 элементов из-за ограничения глубины рекурсии.
Pking

4
@Pking, нет, по умолчанию 100(для предотвращения бесконечного цикла). Используйте MAXRECURSION намек , чтобы определить число уровней рекурсии ( 0к 32767, 0«нет предела» - может раздавить сервер). Кстати, гораздо лучший ответ, чем PARSENAME, потому что он универсален :-). +1
Михал Повага

При добавлении maxrecursionк этому решению помните этот вопрос и ответы на него. Как настроить maxrecursionопцию для CTE внутри функции с табличным значением .
Михал Повага

В частности, обратитесь к ответу Crisfole - его метод несколько замедляет его, но проще, чем большинство других вариантов.
Хиггинс

второстепенный пункт, но использование не остается прежним, потому что вы изменили имя столбца, поэтому sбольше не определяется
Тим Абелл

62

Большинство решений здесь используют циклы while или рекурсивные CTE. Я обещаю, что подход, основанный на множествах, будет лучше, если вы можете использовать разделитель, отличный от пробела:

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

Пример использования:

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

Результаты:

----
blat

Вы также можете добавить idxжелаемое в качестве аргумента функции, но я оставлю это в качестве упражнения для читателя.

Это невозможно сделать только с помощью встроенной STRING_SPLITфункции, добавленной в SQL Server 2016, поскольку нет гарантии, что выходные данные будут отображаться в порядке исходного списка. Другими словами, если вы передадите 3,6,1результат, скорее всего, будет в таком порядке, но это может быть 1,3,6. Я попросил помощи сообщества в улучшении встроенной функции здесь:

Имея достаточно качественную обратную связь, они могут рассмотреть некоторые из следующих улучшений:

Более подробно о функциях разделения, почему (и доказательство этого), когда циклы и рекурсивные CTE не масштабируются, и более эффективные альтернативы, если разделение строк происходит из уровня приложения:

На SQL Server 2016 или выше, однако, вы должны смотреть STRING_SPLIT()иSTRING_AGG() :


1
Лучший ответ, ИМХО. В некоторых других ответах есть проблема предела рекурсии SQL 100, но не в этом случае. Очень быстрая и очень простая реализация. Где кнопка +2?
T-moty

5
Я попробовал эту функцию дословно с использованием: select * from DBO.SplitString('Hello John smith', ' ');и был получен результат: Значение Привет, привет, привет, Джон, Джон, Хан, Смит, Мит, й, ч
wwmbes

2
@AaronBertrand Первоначальная проблема, опубликованная GateKiller, касается разделителя пробелов.
wwmbes

1
@ user1255933 Адрес.
Аарон Бертран

1
@ Майкл Да, это правда. У вас также не будет таблицы для выбора, если у вас нет разрешения ALTER SCHEMA, и вы не сможете выбрать из нее, если у вас нет разрешения SELECT. Вы всегда можете попросить кого-нибудь создать функцию для вас. , Или создать его где-нибудь, где вы можете создать его (даже временно, скажем, в tempdb). А в 2016+ вы должны использовать STRING_SPLIT (), а не функцию, которую вы все равно должны создавать сами.
Аарон Бертран

38

Вы можете использовать таблицу чисел для анализа строк.

Создайте таблицу физических чисел:

    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

Создать тестовую таблицу с 1000000 строками

    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

Создать функцию

    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

Использование (выводит 3 миллиона строк в 40 с на моем ноутбуке)

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

уборка

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

Производительность здесь не удивительна, но вызов функции из таблицы с миллионами строк - не лучшая идея. При выполнении разбивки строки на несколько строк я бы избегал этой функции.


2
Лучшее решение IMO, остальные имеют какое-то ограничение ... это быстро и может анализировать длинные строки со многими элементами.
Pking

Почему вы заказываете n по убыванию? Если там, где три элемента, и мы начали нумерацию с 1, то первый элемент будет номером 3, а последний - номером 1. Разве это не даст более интуитивно понятных результатов, если они descбудут удалены?
топор - сделано с SOverflow

1
Согласитесь, было бы более интуитивно понятно в направлении asc. Я следовал соглашению parsename (), в котором используется desc
Nathan Skerl

3
Некоторое объяснение того, как это работает, было бы замечательно
Тим Абелл

В тесте на 100 миллионов строк до 3 полей для анализа ufn_ParseArray не завершился через 25 минут, в то время как REVERSE(PARSENAME(REPLACE(REVERSE('Hello John Smith'), ' ', '.'), 1)) из @NothingsImpossible завершился через 1,5 минуты. @hello_earth Как ваше решение будет сравниваться на более длинных строках с более чем 4 полями?
wwmbes

32

Этот вопрос не о подходе разделения строк , а о том, как получить n-й элемент .

Все ответы здесь делают какое - то строка расщепления с помощью рекурсии, CTEс, множественный CHARINDEX, REVERSEи PATINDEX, придумав функции, вызов методов CLR, количество таблиц,CROSS APPLY S ... Большинство ответов охватывают множество строк коды.

Но - если вы действительно хотите не более чем подход к получению n-го элемента - это может быть сделано в виде реальной строки , без UDF, даже без дополнительного выбора ... И в качестве дополнительного преимущества: type safe

Получить часть 2, разделенную пробелом:

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

Конечно, вы можете использовать переменные для разделителя и позиции (используйте sql:columnдля получения позиции непосредственно из значения запроса):

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

Если ваша строка может содержать запрещенные символы (особенно один из них &><), вы все равно можете сделать это следующим образом. Просто используйтеFOR XML PATH сначала строку, чтобы неявно заменить все запрещенные символы подходящей escape-последовательностью.

Это особый случай, если, кроме того, вашим разделителем является точка с запятой . В этом случае сначала я заменяю разделитель на «# DLMT #», и наконец заменяю его тегами 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)');

ОБНОВЛЕНИЕ для SQL-Server 2016+

К сожалению, разработчики забыли вернуть индекс детали с помощью STRING_SPLIT. Но, используя SQL-Server 2016+, есть JSON_VALUEи OPENJSON.

С помощью JSON_VALUEмы можем передать позицию в качестве индекса 'массив.

Для документации четко сказано:OPENJSON

Когда OPENJSON анализирует массив JSON, функция возвращает индексы элементов в тексте JSON в качестве ключей.

Строка , как 1,2,3нужно ничего более скобки: [1,2,3].
Строка слов вроде this is an exampleдолжна быть ["this","is","an","example"].
Это очень простые строковые операции. Просто попробуйте:

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

- Смотрите это для безопасного разделения строк (начиная с нуля ):

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

В этом посте я протестировал различные подходы и обнаружил, что OPENJSONэто действительно быстро. Даже намного быстрее, чем знаменитый метод delimitedSplit8k () ...

ОБНОВЛЕНИЕ 2 - Получить значения типа безопасны

Мы можем использовать массив внутри массива просто используя doubled [[]]. Это позволяет ввести WITH-clause:

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

Re: если ваша строка может содержать запрещенные символы ... вы можете просто обернуть подстроки, как это <x><![CDATA[x<&>x]]></x>.
Салман А

@SalmanA, да, CDATA-разделы тоже могут с этим справиться ... Но после броска они пропали (поменялись на text()неявные). Я не люблю магию под капотом , поэтому я бы предпочел (SELECT 'Text with <&>' AS [*] FOR XML PATH(''))- подход. Это выглядит чище для меня и происходит в любом случае ... (Еще немного о CDATA и XML ).
Шнуго

22

Вот UDF, который сделает это. Он вернет таблицу значений с разделителями, не пробовал все сценарии, но ваш пример работает нормально.


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

Вы бы назвали это так:


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

Изменить: Обновленное решение для обработки разделителей с len> 1, как в:


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

Не работает для select * из части идентификатора dbo.ethos_SplitString_fn ('guy, wicks, was here', ',') ----------- ------------ -------------------------------------- 1 парень 2 фитиля
парень

2
Остерегайтесь с len (), так как он не вернет правильное число, если его аргумент имеет завершающие пробелы. Например, len ('-') = 2.
Rory

Не работает с: select * from dbo.SplitString ('foo, foo test ,,,, foo', ',')
cbp

1
Исправление для cbp. Выберите @myString = substring (@ mystring, @ iSpaces + len (@deliminator), len (@myString) - charindex (@ deliminator, @ myString, 0))
Alxwest

16

Здесь я публикую простой способ решения

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


Выполните функцию следующим образом

  select * from dbo.split('Hello John Smith',' ')

Мне понравилось это решение. Расширено, чтобы вернуть скалярное значение на основе указанного столбца в результатах.
Алан

Я был сожжен '&' в строке, чтобы разделить, используя это
KeithL

10

По моему мнению, вы, ребята, делаете это слишком сложно. Просто создайте CLR UDF и покончите с этим.

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()));
  }
};

20
Я думаю, это слишком сложно, потому что мне нужно иметь Visual Studio, затем включить CLR на сервере, затем создать и скомпилировать проект, и, наконец, добавить сборки в базу данных, чтобы использовать его. Но все еще интересный ответ.
Гильермо Гутьеррес

3
@ guillegr123, это не должно быть сложным. Вы можете просто скачать и установить (бесплатно!) SQL #, который является библиотекой функций и процедур SQLCLR. Вы можете получить его с SQLsharp.com . Да, я автор, но String_Split включен в бесплатную версию.
Соломон Руцкий

10

Как насчет использования stringи values()заявления?

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

Результат достигнут.

id  item
1   Hello
2   John
3   Smith

1
Я использовал ваш ответ, но не работал, но я изменил, и это работало с Union All, я использую SQL 2005
ангел

9

Я использую ответ Фредерика, но это не сработало в SQL Server 2005

Я изменил его, и я использую selectс, union allи это работает

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

И набор результатов:

id  item
1   Hello
2   John
3   Smith
4   how
5   are
6   you

Это действительно здорово, я когда-либо видел в sql вещи, это работало на мою работу, и я ценю это, спасибо!
Абдуррахман И.

Я очень обрадовался, когда увидел это, потому что это выглядело так чисто и легко для понимания, но, к сожалению, вы не можете поместить это в UDF из-за EXEC. EXECнеявно вызывает хранимую процедуру, и вы не можете использовать хранимые процедуры в UDF.
Кристен Хаммак

Это работает отлично! я искал с помощью функции (SplitStrings_Moden) здесь: sqlperformance.com/2012/07/t-sql-queries/split-strings#comments, которая делает это, и это заняло полторы минуты, чтобы разделить данные и вернуть строки, когда используются только 4 номера счета. Я проверил вашу версию с левым соединением на столе с данными о номерах счетов, и это заняло 2 или 3 секунды! Огромная разница и работает без нареканий! Я бы отдал 20 голосов, если это возможно!
MattE

8

Этот шаблон работает нормально, и вы можете обобщить

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

обратите внимание ПОЛЕ , ИНДЕКС и ТИП .

Пусть некоторые таблицы с идентификаторами, как

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

Затем вы можете написать

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

раскол и отливка всех деталей.


Это единственное решение, которое позволяет приводить к конкретным типам и является умеренно эффективным (CLR по-прежнему наиболее эффективен, но этот подход обрабатывает таблицу строк размером 8 ГБ, 10 токенов и 10 МБ примерно за 9 минут (сервер AWS m3, 4 000 операций ввода-вывода в секунду). обеспеченный двигатель)
Эндрю Хилл

7

Если ваша база данных имеет уровень совместимости 130 или выше, вы можете использовать функцию STRING_SPLIT вместе с OFFSET FETCH чтобы получить конкретный элемент по индексу.

Чтобы получить элемент с индексом N (с нуля), вы можете использовать следующий код

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

Чтобы проверить уровень совместимости вашей базы данных , выполните этот код:

SELECT compatibility_level  
FROM sys.databases WHERE name = 'YourDBName';

Хитрость в смещении 1 ряд, который пропустит первый элемент и вернет второй элемент. Если ваши индексы основаны на 0, а @X - это переменная, содержащая индекс элемента, который вы хотите получить, вы можете выполнить OFFSET @X ROWS
Gorgi Rankovski

Хорошо, не использовал это раньше ... Приятно знать ... Я все же предпочел xmlбы подход, основанный на -split, так как он позволяет извлекать значение, безопасное с точки зрения типов и не требует подзапроса, но это Неплохо. +1 с моей стороны
Шнуго

3
проблема здесь в том, что STRING_SPLIT не гарантирует порядок возвращаемых результатов. Таким образом, ваш пункт 1 может или не может быть моим пунктом 1.
user1443098

@GorgiRankovski, Использование STRING_SPLIT требования для v2016 +. В этом случае гораздо лучше использовать OPENJSONили JSON_VALUE. Вы можете проверить мой ответ
Shnugo

6

Я искал решение в сети, и ниже работает для меня. ссылка .

И вы вызываете функцию так:

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

Вы не можете легко получить доступ к N-му элементу, используя эту функцию.
Бьорн Линдквист

6

Еще одна часть строки получает функцию delimeter:

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

и использование:

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

который возвращает:

c

Мне нравится это решение в качестве опции для возврата одной подстроки, а не для получения разобранной таблицы, из которой затем нужно выбрать. Использование результата таблицы имеет свое применение, но для того, что мне было нужно, это сработало отлично.
Джеймс Х

5

Попробуй это:

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

Проверьте это так:

select * from SplitWordList('Hello John Smith')

Я прошел через это, и это совершенно как то, что я хочу! даже я могу также настроить его для игнорирования специальных символов, которые я выбираю!
Викас

5

В следующем примере используется рекурсивный CTE

Обновление 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

Демо на SQLFiddle


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

2

Вы можете разбить строку в SQL без необходимости использования функции:

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

Если вам нужно поддерживать произвольные строки (с помощью специальных символов 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); 

1

Я знаю, что это старый вопрос, но я думаю, что кто-то может извлечь выгоду из моего решения.

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

SQL FIDDLE

Преимущества:

  • Он разделяет все 3 разделителя подстрок на ''.
  • Нельзя использовать цикл while, так как это снижает производительность.
  • Нет необходимости в Pivot, так как все результирующие подстроки будут отображаться в одной строке

Ограничения:

  • Нужно знать общее нет. пробелов (подстрока).

Примечание : решение может дать подстроку до N.

Чтобы преодолеть ограничение, мы можем использовать следующую ссылку .

Но опять же вышеупомянутое решение не может быть использовано в таблице (на самом деле я не смог его использовать).

Опять же, я надеюсь, что это решение может кому-то помочь.

Обновление: В случае записей> 50000 не рекомендуется использовать , LOOPSпоскольку это приведет к ухудшению производительности


1

Чистое множество на основе решения с использованием TVFрекурсивного CTE. Вы можете JOINи APPLYэту функцию для любого набора данных.

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

Применение:

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

Результат:

value   index
-------------
John    1

1

Почти все другие ответы заменяют разделяемую строку, которая тратит циклы ЦП и выполняет ненужные выделения памяти.

Я расскажу о гораздо лучшем способе разделения строк здесь: http://www.digitalruby.com/split-string-sql-server/

Вот код:

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.

0

Рекурсивное решение CTE с серверной болью, протестируйте его

Настройка схемы MS SQL Server 2008 :

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

Запрос 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

Результаты :

|    A_L | N |
|--------|---|
| Hello  | 0 |
|  John  | 1 |
| Smith  | 2 |

0

Несмотря на то, что аналогично ответу на основе xml от josejuan, я обнаружил, что обработка пути xml только один раз, а затем поворот был умеренно более эффективным:

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

побежал в 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

побежал в 9:20


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

И ИСПОЛЬЗУЙТЕ ЕГО

select *from dbo.fnSplitString('Querying SQL Server','')

0

если кто-то хочет получить только одну часть разделенного текста, можно использовать это

выберите * 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
  )

0

Я разработал это,

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

единственное внимание, которое вам следует уделить, это точка '.' этот конец @x всегда должен быть там.


0

Основываясь на решении @NothingsImpossible или, точнее, прокомментировав ответ с наибольшим количеством голосов (чуть ниже принятого), я обнаружил, что следующее быстрое и грязное решение отвечает моим собственным потребностям - оно имеет преимущество, заключающееся в том, что оно находится исключительно в домене SQL.

учитывая строку «первый; второй; третий; четвертый; пятый», скажем, я хочу получить третий токен. это работает только в том случае, если мы знаем, сколько токенов будет иметь строка - в данном случае это 5. поэтому мой способ действия - отрубить последние два токена (внутренний запрос), а затем отобрать первые два токена ( внешний запрос)

я знаю, что это уродливо и покрывает определенные условия, в которых я был, но я публикую это на тот случай, если кто-то посчитает это полезным. ура

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

это работает только в том случае, если мы знаем, сколько токенов будет в цепочке - ограничение на разрыв ...
Shnugo

0
declare @strng varchar(max)='hello john smith'
select (
    substring(
        @strng,
        charindex(' ', @strng) + 1,
        (
          (charindex(' ', @strng, charindex(' ', @strng) + 1))
          - charindex(' ',@strng)
        )
    ))

0

Начиная с SQL Server 2016 у нас есть string_split

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

SELECT value FROM string_split(@string, ',')

Это хорошо, но это не решает вопрос получения n-го результата.
Джони Карр,

STRING_SPLITне гарантирует возврат того же заказа. Но OPENJSONделает (см. Мой ответ (раздел обновления) )
Shnugo
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.