Используя T-SQL, верните n-й разделительный элемент из строки

Мне нужно создать функцию, которая будет возвращать n-й элемент строки с разделителями.

Для проекта переноса данных я преобразую записи аудита JSON, хранящиеся в базе данных SQL Server, в структурированный отчет с использованием сценария SQL. Цель состоит в том, чтобы предоставить сценарий sql и функцию sql, используемые сценарием, без какого-либо кода.

(Это кратковременное исправление будет использоваться при добавлении новой функции аудита в приложение ASP.NET/MVC).

Нет недостатка в доступных примерах с разделителями. Я выбрал пример общего табличного выражения http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings

Пример: я хочу вернуть 67 из '1,222,2,67,888,1111'

Ответ 1

Вот мое первоначальное решение... Он основан на работе Аарона Бертранга http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings

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

Пример: SELECT dbo.GetSplitString_CTE ('1,222,2,67,888,1111', ',', 4)

CREATE FUNCTION dbo.GetSplitString_CTE
(
   @List       VARCHAR(MAX),
   @Delimiter  VARCHAR(255),
   @ElementNumber int
)
RETURNS VARCHAR(4000)
AS
BEGIN

   DECLARE @result varchar(4000)    
   DECLARE @Items TABLE ( position int IDENTITY PRIMARY KEY,
                          Item VARCHAR(4000)
                         )  

   DECLARE @ll INT = LEN(@List) + 1, @ld INT = LEN(@Delimiter);  

   WITH a AS
   (
       SELECT
           [start] = 1,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, @ld), 0), @ll),
           [value] = SUBSTRING(@List, 1, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, @ld), 0), @ll) - 1)
       UNION ALL
       SELECT
           [start] = CONVERT(INT, [end]) + @ld,
           [end]   = COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll),
           [value] = SUBSTRING(@List, [end] + @ld, 
                     COALESCE(NULLIF(CHARINDEX(@Delimiter, 
                       @List, [end] + @ld), 0), @ll)-[end][email protected])
       FROM a
       WHERE [end] < @ll
   )
   INSERT @Items SELECT [value]
   FROM a
   WHERE LEN([value]) > 0
   OPTION (MAXRECURSION 0);

   SELECT @result=Item
   FROM @Items
   WHERE [email protected]

   RETURN @result;
END
GO

Ответ 2

Это самый простой ответ, чтобы восстановить 67 (типобезопасный !!):

SELECT CAST('<x>' + REPLACE('1,222,2,67,888,1111',',','</x><x>') + '</x>' AS XML).value('/x[4]','int')

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

Легкий

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

Это настоящий однострочный для получения части 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:variable() или sql:column()

Конечно, вы можете использовать переменные для разделителя и позиции (используйте 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)')

Пограничный регистр с символами, запрещенными для XML

Если ваша строка может содержать запрещенные символы, вы все равно можете сделать это следующим образом. Просто используйте сначала 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-сервера 2016+

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

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

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

Когда 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,']'));

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

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

Ответ 3

Как насчет:

CREATE FUNCTION dbo.NTH_ELEMENT (@Input NVARCHAR(MAX), @Delim CHAR = '-', @N INT = 0)
RETURNS NVARCHAR(MAX)
AS
BEGIN
RETURN (SELECT VALUE FROM STRING_SPLIT(@Input, @Delim) ORDER BY (SELECT NULL) OFFSET @N ROWS FETCH NEXT 1 ROW ONLY)
END

Ответ 4

@a - значение (например, 'a/bb/ccc/dddd/ee/ff/....')

@p - желаемая позиция (1,2,3...)

@d - разделитель ('/')

обрезать (подстрока (заменить (@a, @d, повторить ('', len (@a))), (@p-1) * len (@a) +1, len (@a)))

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

Полностью основано на статье из https://exceljet.net/formula/split-text-with-delimiter

Ответ 5

В редкий момент безумия я просто подумал, что раскол намного проще, если мы используем XML для его анализа:

(Используя переменные из ответа @Gary Kindel)

declare @xml xml
set @xml = '<split><el>' + replace(@list,@Delimiter,'</el><el>') + '</el></split>'

select
    el = split.el.value('.','varchar(max)')
from  @xml.nodes('/split/el') split(el))

Здесь перечислены все элементы строки, разделенные указанным символом.

Мы можем использовать тест xpath, чтобы отфильтровать пустые значения, и еще один тест xpath, чтобы ограничить это элементом, который нас интересует. В полной функции Гэри становится:

alter FUNCTION dbo.GetSplitString_CTE
(
   @List       VARCHAR(MAX),
   @Delimiter  VARCHAR(255),
   @ElementNumber int
)
RETURNS VARCHAR(max)
AS
BEGIN

       declare @xml xml
       set @xml = '<split><el>' + replace(@list,@Delimiter,'</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

Ответ 6

вы можете поместить этот выбор в UFN. если вам нужно, вы можете настроить его и для указания разделителя. в этом случае ваш ufn будет иметь два входа. номер N и разделитель.

    DECLARE @tlist varchar(max)='10,20,30,40,50,60,70,80,90,100'
    DECLARE @i INT=1, @nth INT=3
    While len(@tlist) <> 0
    BEGIN
            IF @[email protected]
            BEGIN
              select Case when charindex(',',@tlist) <> 0 Then LEFT(@tlist,charindex(',',@tlist)-1)
                          Else @tlist
                    END
            END

              Select @tlist = Case when charindex(',',@tlist) <> 0 Then substring(@tlist,charindex(',',@tlist)+1,len(@tlist))
                          Else ''
                          END

            SELECT @[email protected]+1
    END

Ответ 7

Я бы предпочел создать временную таблицу со столбцом идентификаторов и заполнить ее выводом из функции SPLIT.

  CREATE TABLE #tblVals(Id INT IDENTITY(1,1), Val NVARCHAR(100))
  INSERT INTO #tblVals (Val)
  SELECT [value] FROM STRING_SPLIT('Val1-Val3-Val2-Val5', '-')
  SELECT * FROM #tblVals

Теперь вы можете легко сделать что-то вроде ниже.

DECLARE @val2 NVARCHAR(100) = (SELECT TOP 1 Val FROM #tblVals WHERE Id = 2)

Смотрите снимок ниже:

see the snapshot

Ответ 8

Я не могу комментировать решение Gary из-за моей низкой репутации

Я знаю, что Гэри ссылался на другую ссылку.

Я изо всех сил пытался понять, зачем нужна эта переменная

@ld INT = LEN(@Delimiter)

Я также не понимаю, почему charindex должен начинаться с позиции длины разделителя, @ld

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

В этом случае следующий случай не будет выполнен

11,, 22,, 33,, 44,,, 55

Я клонировал из кодов из этой ссылки. http://codebetter.com/raymondlewallen/2005/10/26/quick-t-sql-to-parse-a-delimited-string/

Я тестировал различные сценарии, включая разделители, которые имеют более одного символа.

alter FUNCTION [dbo].[split1]
(
    @string1 VARCHAR(8000) -- List of delimited items
    , @Delimiter VARCHAR(40) = ',' -- delimiter that separates items
    , @ElementNumber int
)
RETURNS varchar(8000)
AS
BEGIN
    declare @position int
    declare @piece varchar(8000)=''
    declare @returnVal varchar(8000)=''
    declare @Pattern varchar(50) = '%' + @Delimiter + '%'
    declare @counter int =0
    declare @ld int = len(@Delimiter)
    declare @ls1 int = len (@string1)
    declare @foundit int = 0

    if patindex(@Pattern , @string1) = 0
        return  ''

    if right(rtrim(@string1),1) <> @Delimiter
        set @string1 = @string1  + @Delimiter

    set @position =  patindex(@Pattern , @string1) + @ld  -1  
    while @position > 0
    begin
        set @counter = @counter +1 
        set @ls1  = len (@string1)
        if (@ls1 >= @ld)
            set @piece = left(@string1, @position - @ld)
        else
            break
        if (@counter = @ElementNumber)
        begin
            set @foundit = 1
                break
        end
        if len(@string1) > 0
        begin
            set @string1 = stuff(@string1, 1, @position, '')
            set @position =  patindex(@Pattern , @string1) + @ld  -1  
        end
        else
        set @position = -1
    end 


    if @foundit =1
        set @returnVal = @piece
    else 
        set @returnVal =  ''
    return @returnVal

Ответ 9

В качестве альтернативы можно использовать xml, nodes() и ROW_NUMBER. Мы можем заказать элементы в соответствии с порядком их документов. Например:

DECLARE @Input VARCHAR(100) = '1a,2b,3c,4d,5e,6f,7g,8h'
       ,@Number TINYINT = 3

DECLARE @XML XML;
DECLARE @value VARCHAR(100);

SET @XML = CAST('<x>' + REPLACE(@Input,',','</x><x>') + '</x>' AS XML);

WITH DataSource ([rowID], [rowValue]) AS
(
    SELECT ROW_NUMBER() OVER (ORDER BY T.c ASC) 
            ,T.c.value('.', 'VARCHAR(100)')
    FROM @XML.nodes('./x') T(c)
)
SELECT @value = [rowValue]
FROM DataSource
WHERE [rowID] = @Number;

SELECT @value;

Ответ 10

У меня недостаточно репутации для комментариев, поэтому я добавляю ответ. При необходимости отрегулируйте.

У меня проблема с ответом Гэри Кинделя для случаев, когда между двумя разделителями

Если вы это сделаете выберите * из dbo.GetSplitString_CTE ('abc ^ def ^^ ghi', '^', 3) Вы получаете ГХИ вместо пустой строки

Если вы закомментируете  WHERE LEN ([значение]) > 0 line, вы получите желаемый результат