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

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

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

Ответов (25)

Решение

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

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

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

Сначала создайте функцию (используя 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

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

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

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

    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]

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

По-моему, вы, ребята, слишком усложняете задачу. Просто создайте 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()));
  }
};

А как насчет использования 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

Я использую ответ Фредерика, но это не сработало в 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

Рекурсивное решение 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 |

Хотя это похоже на ответ на основе 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

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

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

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

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

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

В большинстве решений здесь используются циклы 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 . Я попросил сообщества помочь в улучшении встроенной функции здесь:

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

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

Однако в SQL Server 2016 или более поздней версии вам следует посмотреть STRING_SPLIT()и STRING_AGG():

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

опираясь на решение @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

Начиная с SQL Server 2016 мы string_split

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

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

Вот функция, которая выполнит задачу по разделению строки и доступу к элементу X:

CREATE FUNCTION [dbo].[SplitString]
(
   @List       VARCHAR(MAX),
   @Delimiter  VARCHAR(255),
   @ElementNumber INT
)
RETURNS VARCHAR(MAX)
AS
BEGIN

       DECLARE @inp VARCHAR(MAX)
       SET @inp = (SELECT REPLACE(@List,@Delimiter,'_DELMTR_') FOR XML PATH(''))

       DECLARE @xml XML
       SET @xml = '<split><el>' + REPLACE(@inp,'_DELMTR_','</el><el>') + '</el></split>'

       DECLARE @ret VARCHAR(MAX)
       SET @ret = (SELECT
              el = split.el.value('.','varchar(max)')
       FROM  @xml.nodes('/split/el[string-length(.)>0][position() = sql:variable("@elementnumber")]') split(el))

       RETURN @ret

END

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

SELECT dbo.SplitString('Hello John Smith', ' ', 2)

Результат:

John

ПРОСТОЕ РЕШЕНИЕ ДЛЯ РАЗБОРКИ ПЕРВОГО И ПОСЛЕДНЕГО ИМЕНИ

DECLARE @Name varchar(10) = 'John Smith'

-- Get First Name
SELECT SUBSTRING(@Name, 0, (SELECT CHARINDEX(' ', @Name)))

-- Get Last Name
SELECT SUBSTRING(@Name, (SELECT CHARINDEX(' ', @Name)) + 1, LEN(@Name))

В моем случае (и во многих других, кажется ...) у меня есть список имен и фамилий, разделенных одним пробелом. Это можно использовать непосредственно внутри оператора select для анализа имени и фамилии.

-- i.e. Get First and Last Name from a table of Full Names
SELECT SUBSTRING(FullName, 0, (SELECT CHARINDEX(' ', FullName))) as FirstName,
SUBSTRING(FullName, (SELECT CHARINDEX(' ', FullName)) + 1, LEN(FullName)) as LastName,
From FullNameTable

Я знаю, что это поздно, но недавно у меня было это требование, и я придумал приведенный ниже код. У меня нет выбора использовать пользовательскую функцию. Надеюсь это поможет.

SELECT 
    SUBSTRING(
                SUBSTRING('Hello John Smith' ,0,CHARINDEX(' ','Hello John Smith',CHARINDEX(' ','Hello John Smith')+1)
                        ),CHARINDEX(' ','Hello John Smith'),LEN('Hello John Smith')
            )

Я оставил это,

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 всегда должен быть там.

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

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

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

Получите часть 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 предложение:

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
CREATE TABLE test(
    id int,
    adress varchar(100)
);
INSERT INTO test VALUES(1, 'Ludovic Aubert, 42 rue de la Victoire, 75009, Paris, France'),(2, 'Jose Garcia, 1 Calle de la Victoria, 56500 Barcelona, Espana');

SELECT id, value, COUNT(*) OVER (PARTITION BY id) AS n, ROW_NUMBER() OVER (PARTITION BY id ORDER BY (SELECT NULL)) AS rn, adress
FROM test
CROSS APPLY STRING_SPLIT(adress, ',')

введите описание изображения здесь

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

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

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

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

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

Вот 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

You would call it like this:


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

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


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

Современный подход с использованием STRING_SPLIT требует SQL Server 2016 и выше.

DECLARE @string varchar(100) = 'Hello John Smith'

SELECT
    ROW_NUMBER() OVER (ORDER BY value) AS RowNr,
    value
FROM string_split(@string, ' ')

Результат:

RowNr   value
1       Hello
2       John
3       Smith

Теперь можно получить n-й элемент из номера строки.

Ответ Аарона Бертрана великолепен, но ошибочен. Он не точно обрабатывает пробел в качестве разделителя (как это было в примере в исходном вопросе), поскольку функция длины удаляет конечные пробелы.

Ниже приведен его код с небольшой поправкой, позволяющей использовать разделитель пробелов:

CREATE FUNCTION [dbo].[SplitString]
(
    @List NVARCHAR(MAX),
    @Delim VARCHAR(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_objects) AS x
          WHERE Number <= LEN(@List)
          AND SUBSTRING(@Delim + @List, [Number], LEN(@Delim+'x')-1) = @Delim
      ) AS y
    );