Как с помощью SQL Server разбить строку, чтобы получить доступ к элементу x?
Возьмите строку «Привет, Джон Смит». Как я могу разделить строку по пробелу и получить доступ к элементу с индексом 1, который должен вернуть «Джон»?
встроенный с sql server 2016 msdn.microsoft.com/en-us/library/mt684588.aspx
Ответы наибольший здесь - по крайней мере, для меня - довольно старомодны и довольно устаревшие. Процедурный указатель, циклы, рекурсии, CLR, функции, много строк кода ... Было бы интересно прочитать "активные" ответы, чтобы найти больше подходов своевременно.
Я добавил новый ответ с более современным подходом: stackoverflow.com/a/49669994/632604
Попробуйте Получить n-й элемент списка -> portosql.wordpress.com/2019/05/27/enesimo-elemento-lista
Некоторые комментарии (в том числе и мой) были удалены. Кто-нибудь из модераторов хочет прокомментировать?
@TimAbell, в документации указано, что «Порядок не гарантированно соответствует порядку подстрок во входной строке».


Вы можете найти решение в Пользовательская функция 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
почему 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)?
@GateKiller Это решение не поддерживает Unicode и использует жестко заданные числа (18,3), что не делает его жизнеспособной функцией "многократного использования".
Это работает, но выделяет много памяти и расходует ресурсы ЦП.
Начиная с SQL Server 2016, теперь есть встроенная функция STRING_SPLIT, которая разбивает строку и возвращает результат таблицы с одним столбцом, который вы можете использовать в инструкции SELECT или где-либо еще.
Жаль, что парни, с которыми я работаю, не в 2016 году. Но я буду иметь это в виду на случай, если они когда-нибудь вырвутся вперед из своей обуви. Отличное решение тем временем. Я реализовал это как функцию и добавил разделитель в качестве аргумента.
@Jonesinator Спасибо. Это мне второй раз, когда я приезжаю сюда. И, наконец, сделали функцию со значением таблицы SplitString с этим для сервера Sql 2008, 2010, 2014 Здесь функция SplitString и ее механизм вызова
@qJake Как бы вы использовали STRING_SPLIT с индексом? Вопрос спрашивает, как получить конкретное значение, а не полный список.
Вы можете мне помочь с тем, как изменить эту логику, если input = | 1 | 20 | 3 | 343 | 44 | 6 | 8765 |
Ну, у меня не все так просто, но вот код, который я использую, чтобы разделить входную переменную с разделителями-запятыми на отдельные значения и поместить их в переменную таблицы. Я уверен, что вы могли бы немного изменить это, чтобы разделить на основе пробела, а затем выполнить базовый запрос SELECT для этой табличной переменной, чтобы получить свои результаты.
-- Create temporary table to parse the list of accounting cycles.
DECLARE @tblAccountingCycles table
(
AccountingCycle varchar(10)
)
DECLARE @vchAccountingCycle varchar(10)
DECLARE @intPosition int
SET @vchAccountingCycleIDs = LTRIM(RTRIM(@vchAccountingCycleIDs)) + ','
SET @intPosition = CHARINDEX(',', @vchAccountingCycleIDs, 1)
IF REPLACE(@vchAccountingCycleIDs, ',', '') <> ''
BEGIN
WHILE @intPosition > 0
BEGIN
SET @vchAccountingCycle = LTRIM(RTRIM(LEFT(@vchAccountingCycleIDs, @intPosition - 1)))
IF @vchAccountingCycle <> ''
BEGIN
INSERT INTO @tblAccountingCycles (AccountingCycle) VALUES (@vchAccountingCycle)
END
SET @vchAccountingCycleIDs = RIGHT(@vchAccountingCycleIDs, LEN(@vchAccountingCycleIDs) - @intPosition)
SET @intPosition = CHARINDEX(',', @vchAccountingCycleIDs, 1)
END
END
Концепция почти такая же. Еще одна альтернатива - использовать совместимость .NET в самом SQL Server 2005. По сути, вы можете написать себе простой метод в .NET, который разделит строку, а затем предоставит ее как хранимую процедуру / функцию.
пример выполнения этого в .NET (процедуры / функции CLR) можно найти здесь cstruter.com/blog/260
Вот 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 * from dbo.ethos_SplitString_fn ('парень, фитиль, был здесь', ',') часть идентификатора ----------- ------------ -------------------------------------- 1 парень 2 фитиля
будьте осторожны с len (), так как он не вернет правильное число, если его аргумент имеет конечные пробелы., например len ('-') = 2.
Не работает на: select * from dbo.SplitString ('foo, foo test ,,,, foo', ',')
Исправление для cbp. Выберите @myString = substring (@ mystring, @ iSpaces + len (@deliminator), len (@myString) - charindex (@ deliminator, @ myString, 0))
Попробуй это:
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')
Я прошел через это, и это совершенно похоже на то, что я хочу! даже я могу настроить его так, чтобы игнорировать специальные символы, которые я выбираю!
Я не верю, что в SQL Server есть встроенная функция разделения, поэтому, кроме UDF, единственный известный мне ответ - захватить функцию PARSENAME:
SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 2)
PARSENAME берет строку и разбивает ее на символ точки. Он принимает число в качестве второго аргумента, и это число указывает, какой сегмент строки следует вернуть (работая от конца к началу).
SELECT PARSENAME(REPLACE('Hello John Smith', ' ', '.'), 3) --return Hello
Очевидная проблема - когда строка уже содержит точку. Я все еще думаю, что использование UDF - лучший способ ... есть другие предложения?
Спасибо, Сол ... Я должен отметить, что это решение действительно плохое решение для реальной разработки. PARSENAME ожидает только четыре части, поэтому использование строки с более чем четырьмя частями приводит к возврату NULL. Решения UDF явно лучше.
Это отличный хакер, и он заставляет меня плакать, что что-то подобное необходимо для чего-то такого чертовски простого на реальных языках.
Чтобы индексы работали «правильным» образом, то есть начиная с 1, я перехватил ваш взлом с помощью REVERSE: REVERSE (PARSENAME (REPLACE (REVERSE ('Hello John Smith'), '', '.') , 1)) - Возвращает Hello
@FactorMystic Первая нормальная форма требует, чтобы вы не помещали несколько значений в одно поле. Это буквально первое правило СУБД. Функция SPLIT() не предоставляется, поскольку она способствует плохому проектированию базы данных, и база данных никогда не будет оптимизирована для использования данных, хранящихся в этом формате. РСУБД не обязана помогать разработчикам делать глупые вещи, для обработки которых она предназначена нет. Правильный ответ будет всегда: «Нормализовать вашу базу данных, как мы говорили вам 40 лет назад». Ни SQL, ни СУБД не виноваты в плохом дизайне.
Это супер круто. Так уж получилось, что данные, с которыми я работаю, состоят из четырех элементов.
+1 всегда может придумать быстрое и грязное рабочее решение для> 4 токенов с комбинацией reverse (), charindex (), substring ()
@BaconBits, хотя теоретически я согласен, на практике такие инструменты полезны при нормализации плохого дизайна, созданного кем-то, кто был до вас.
Когда количество разбиений в строке меняется слева направо, тогда путь @NothingsImpossible - это правильный путь.
@NothingsImpossible Производительность решения REVERSE (PARSENAME (REPLACE (REVERSE ('Hello John Smith'), '', '.'), 1)) превосходна, и ограничение на 4 поля подходит для моей проблемы. Спасибо!!!
@hello_earth Мне бы хотелось увидеть, что вы придумали. Пожалуйста, покажите нам.
@wwmbes Я опубликовал отдельный ответ - он очень быстрый, грязный и некрасивый, и основан на тех же принципах, но в любом случае, поскольку вы спросили. ваше здоровье
Для осведомленности: поскольку функция parsename предназначена для возврата идентификаторов базы данных, возвращаемое значение ограничено 128 символами (тип данных sysname, который соответствует nvarchar (128)). Если это выходит за рамки этого, вместо этого будет возвращено NULL.
Для осведомленности: он работает только с четырьмя элементами или меньше. Если у вас более четырех элементов, PARSENAME всегда возвращает NULL, даже для индекса ниже 5. docs.microsoft.com/it-it/sql/t-sql/functions/…
Сначала создайте функцию (с использованием 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
Использование остается прежним.
Это элегантно, но работает только для 100 элементов из-за ограничения глубины рекурсии.
@Pking, нет, значение по умолчанию - 100 (для предотвращения бесконечного цикла). Используйте Подсказка MAXRECURSION, чтобы определить количество уровней рекурсии (от 0 до 32767, 0 - «без ограничений» - может привести к сбою сервера). Кстати, гораздо лучший ответ, чем PARSENAME, потому что он универсален :-). +1
Добавляя maxrecursion к этому решению, имейте в виду этот вопрос и его ответы Как настроить параметр maxrecursion для CTE внутри функции с табличным значением.
В частности, обратитесь к ответ Crisfole - его метод несколько замедляет его, но он проще, чем большинство других вариантов.
второстепенный момент, но использование не остается прежним, потому что вы изменили имя столбца, поэтому s больше не определен
Я избегаю ограничения рекурсии, используя этот код, который, если бы я не перешел на SQL Server 2016, я бы сделал функцию с табличным значением или скалярную функцию, в зависимости от моих реальных потребностей. Путем зацикливания вместо самосоединения теоретически можно использовать строку любого размера.
Вы можете использовать числовую таблицу для синтаксического анализа строк.
Создайте таблицу физических номеров:
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]
Производительность здесь не впечатляет, но вызов функции с таблицей из миллиона строк - не лучшая идея. При выполнении разбиения строки на несколько строк я бы избегал этой функции.
Лучшее решение IMO, у других есть какое-то ограничение .. это быстро и может анализировать длинные строки со многими элементами.
Почему вы заказываете n по убыванию? Если там было три элемента, и мы начали нумерацию с 1, то первый элемент будет под номером 3, а последний - под номером 1. Разве это не дало бы более интуитивных результатов, если бы desc был удален?
Согласен, было бы по возрастанию нагляднее. Я следовал соглашению parsename (), в котором используется desc
какое-то объяснение того, как это работает, было бы здорово
В тесте со 100 миллионами строк до 3 полей для анализа ufn_ParseArray не завершился через 25 минут, а REVERSE(PARSENAME(REPLACE(REVERSE('Hello John Smith'), ' ', '.'), 1)) из @NothingsImpossible завершился за 1,5 минуты. @hello_earth Как ваше решение будет сравниваться с более длинными строками с более чем 4 полями?
При дальнейшем исследовании, когда версия @NothingsImpossible встроена в функцию и используется оттуда, она работает в 25 раз хуже, чем при использовании непосредственно в запросе. Кто-нибудь может прокомментировать, почему?
@wwmbes Попробуйте использовать таблицу физических номеров с кластеризованным индексом. Использование master..spt_values только для иллюстрации
@wwmbes добавил пример таблицы физических номеров
Я искал решение в сети, и приведенное ниже работает для меня. Ссылка.
И вы вызываете функцию так:
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-му элементу с помощью этой функции.
На мой взгляд, вы, ребята, слишком усложняете задачу. Просто создайте 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()));
}
};
Я думаю, это слишком сложно, потому что мне нужно иметь Visual Studio, затем включить CLR на сервере, затем создать и скомпилировать проект и, наконец, добавить сборки в базу данных, чтобы использовать его. Но все же это интересный ответ.
@ guillegr123, это не должно быть сложно. Вы можете просто скачать и установить (бесплатно!) SQL #, который представляет собой библиотеку функций и процедур SQLCLR. Вы можете получить его от SQLsharp.com. Да, я являюсь автором, но String_Split включен в бесплатную версию.
Я знаю, что это старый вопрос, но я думаю, что кто-то может извлечь выгоду из моего решения.
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
Преимущества:
Ограничения:
Примечание: решение может выдавать подстроку до N.
Чтобы преодолеть ограничение, мы можем использовать следующий ссылка.
Но снова вышеуказанный решение нельзя использовать в таблице (на самом деле я не мог его использовать).
Опять же, я надеюсь, что это решение может кому-то помочь.
Обновлять: В случае записей> 50000, желательно не будет использовать LOOPS, так как это ухудшит Спектакль
Здесь я публикую простой способ решения
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',' ')
Мне понравилось это решение. Расширен, чтобы вернуть скалярное значение на основе указанного столбца в результатах.
Я получил ожог с '&' в строке, которую нужно разделить с помощью этого
А как насчет использования 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
Я использовал ваш ответ, но не работал, но я изменил, и это сработало с union all, я использую sql 2005
В следующем примере используется рекурсивный 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
Вот мое решение, которое может кому-то помочь. Модификация ответа Jonesinator выше.
Если у меня есть строка значений INT с разделителями и я хочу, чтобы была возвращена таблица значений INT (к которой я затем могу присоединиться). например '1,20,3,343,44,6,8765'
Создайте UDF:
IF OBJECT_ID(N'dbo.ufn_GetIntTableFromDelimitedList', N'TF') IS NOT NULL
DROP FUNCTION dbo.[ufn_GetIntTableFromDelimitedList];
GO
CREATE FUNCTION dbo.[ufn_GetIntTableFromDelimitedList](@String NVARCHAR(MAX), @Delimiter CHAR(1))
RETURNS @table TABLE
(
Value INT NOT NULL
)
AS
BEGIN
DECLARE @Pattern NVARCHAR(3)
SET @Pattern = '%' + @Delimiter + '%'
DECLARE @Value NVARCHAR(MAX)
WHILE LEN(@String) > 0
BEGIN
IF PATINDEX(@Pattern, @String) > 0
BEGIN
SET @Value = SUBSTRING(@String, 0, PATINDEX(@Pattern, @String))
INSERT INTO @table (Value) VALUES (@Value)
SET @String = SUBSTRING(@String, LEN(@Value + @Delimiter) + 1, LEN(@String))
END
ELSE
BEGIN
-- Just the one value.
INSERT INTO @table (Value) VALUES (@String)
RETURN
END
END
RETURN
END
GO
Затем получите результаты таблицы:
SELECT * FROM dbo.[ufn_GetIntTableFromDelimitedList]('1,20,3,343,44,6,8765', ',')
1
20
3
343
44
6
8765
И в заявлении соединения:
SELECT [ID], [FirstName]
FROM [User] u
JOIN dbo.[ufn_GetIntTableFromDelimitedList]('1,20,3,343,44,6,8765', ',') t ON u.[ID] = t.[Value]
1 Elvis
20 Karen
3 David
343 Simon
44 Raj
6 Mike
8765 Richard
Если вы хотите вернуть список NVARCHAR вместо INT, просто измените определение таблицы:
RETURNS @table TABLE
(
Value NVARCHAR(MAX) NOT NULL
)
Я использую ответ Фредерика, но это не сработало в 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#comme nts, которая делает это, и потребовалось полторы минуты, чтобы разделить данные и вернуть строки при использовании только 4 номеров счетов. Я протестировал вашу версию с левым соединением в таблице с данными о номерах счетов, и это заняло примерно 2 или 3 секунды! Огромная разница и работает безотказно! Если можно, я бы отдал за это 20 голосов!
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
В большинстве решений здесь используются циклы 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():
Лучший ответ, ИМХО. В некоторых других ответах есть проблема ограничения рекурсии SQL 100, но не в этом случае. Очень быстрая и очень простая реализация. Где кнопка +2?
Спасибо Аарону; Но может ли кто-нибудь объяснить, почему, если я передаю varchar (не varchar(max)) в качестве аргумента, эта функция возвращает пустой список? как declare @list varchar = 'something'; select from dbo.SplitString(@list, ';');
@Mikhail Потому что varchar без длины может быть varchar(30) или varchar(1) в зависимости от контекста. Не пытайтесь понять эту проблему - просто не используйте этот синтаксис. Всегда.
Я попробовал эту функцию дословно с использованием: select * from DBO.SplitString('Hello John smith', ' ');, и на выходе получилось: Ценить Hello ello llo lo o John ohn hn n smith mith th h
@wwmbes Попробуйте использовать разделитель, отличный от пробела. Многие функции будут иметь проблему с отбрасыванием конечных пробелов.
@AaronBertrand Исходная проблема, опубликованная GateKiller, включает в себя разделитель пробелов.
Функция "len" имеет неявный встроенный "rtrim", который появился из 80-х, когда в SQL был только char (не varchar), поэтому попробуйте следующее: select len( ' ' ) as len_space, len( '* ' ) as len_star_space, len( ' *' ) as len_space_star, datalength( ' ' ) as datalength_space, datalength( '* ' ) as datalength_star_space, datalength( ' *' ) as datalength_space_star, case when '' = ' ' then 'yes' else 'no' end as nospace_equals_space Исправление: замените "len" на "datalength" (но будьте осторожны, n (var) символ)
Это разбивает строку. как получить конкретный n-й элемент?
@ user1255933 Адресовано.
@AaronBertrand Большое спасибо, я могу воспроизвести это.
@AaronBertrand пример добавления для применения ко всей таблице: SELECT o.myfield, u.value из mytable o cross apply dbo.SplitString (o.myfield, ';') u, где u.idx = 4
Это не сработает, если у вас нет разрешения «СОЗДАТЬ ФУНКЦИЮ» ...
@ Майкл Да, это правда. У вас также не будет таблицы для выбора, если у вас нет разрешения ALTER SCHEMA, и вы не сможете выбирать из нее, если у вас нет разрешения SELECT. Вы всегда можете Спроси кого-нибудь создать функцию для вас. Или создайте его где-нибудь, где сможете (даже временно, например, в tempdb). И в 2016+ вы должны использовать STRING_SPLIT (), а не функцию, которую вы все равно должны создать.
@AaronBertrand Я нахожусь в 2016 году, но, по-видимому, не уровень совместимости> = 130 (администратор базы данных не был уверен и не сможет изучить это позже)
@AaronBertrand "А в 2016+ вам следует использовать STRING_SPLIT ()": я не могу представить себе какое-либо реальное использование STRING_SPLIT, поскольку он не возвращает индекс для частей строки, которые были разделены, и согласно это документы , «порядок вывода может отличаться, поскольку порядок не гарантированно соответствует порядку подстрок во входной строке».
Мне нравится, как этот элемент обратной связи Azure выражает это: «STRING_SPLIT не является полной функцией» и «Жалко, что это было отправлено как просто« предложение ». Фактически это должно быть указано как «ошибка», потому что существует лишь сравнительно небольшой набор вариантов использования, в которых перечисление результирующего набора элементов не имеет значения ».
@ReversedEngineer Существует множество вариантов использования, которые не заботятся (и не должны) относиться к исходному порядку в списке. «Найдите всех клиентов в этом списке». Почему это важно, если в списке есть 3,5,2,8 или 8,3,5,2? Я думаю, что это довольно непонятный крайний случай, когда требование было бы «Найти всех клиентов в этом списке, и отобразит их именно в этом порядке». (Я не возражаю против исправления этого, я просто предполагаю, что мое впечатление о большинстве случаев использования функции отличается от вашего. Это неофициальное свидетельство из числа принятых ответов, показывающих функции без порядкового номера.)
@ReversedEngineer В любом случае обновил свой ответ с помощью некоторые из моих мыслей по этому поводу. Кроме того, я был одним из тех, кто настаивал на ясности в документации, что порядок вывода может не совпадать с порядком ввода.
@AaronBertrand «Кроме того, я был одним из тех, кто настаивал на ясности в документации, что порядок вывода может не совпадать с порядком ввода». - Спасибо! Я ценю то, что вы делаете, чтобы «требовать от Microsoft улучшений», а также помогаете прояснить эту расплывчатую «документацию Microsoft для чайников». Также я понимаю вашу точку зрения о том, что бывают случаи, когда порядок не имеет значения, и в этом случае STRING_SPLIT действительно отличное решение.
Рекурсивное решение 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 |
Вот SQL UDF, который может разбить строку и захватить только определенный фрагмент.
create FUNCTION [dbo].[udf_SplitParseOut]
(
@List nvarchar(MAX),
@SplitOn nvarchar(5),
@GetIndex smallint
)
returns varchar(1000)
AS
BEGIN
DECLARE @RtnValue table
(
Id int identity(0,1),
Value nvarchar(MAX)
)
DECLARE @result varchar(1000)
While (Charindex(@SplitOn,@List)>0)
Begin
Insert Into @RtnValue (value)
Select Value = ltrim(rtrim(Substring(@List,1,Charindex(@SplitOn,@List)-1)))
Set @List = Substring(@List,Charindex(@SplitOn,@List)+len(@SplitOn),len(@List))
End
Insert Into @RtnValue (Value)
Select Value = ltrim(rtrim(@List))
select @result = value from @RtnValue where ID = @GetIndex
Return @result
END
Простой оптимизированный алгоритм:
ALTER FUNCTION [dbo].[Split]( @Text NVARCHAR(200),@Splitor CHAR(1) )
RETURNS @Result TABLE ( value NVARCHAR(50))
AS
BEGIN
DECLARE @PathInd INT
Set @Text+=@Splitor
WHILE LEN(@Text) > 0
BEGIN
SET @PathInd=PATINDEX('%'+@Splitor+'%',@Text)
INSERT INTO @Result VALUES(SUBSTRING(@Text, 0, @PathInd))
SET @Text= SUBSTRING(@Text, @PathInd+1, LEN(@Text))
END
RETURN
END
Я использовал ответ vzczc с использованием рекурсивных cte в течение некоторого времени, но хотел обновить его для обработки разделителя переменной длины, а также для обработки строк с ведущими и запаздывающими «разделителями», например, когда у вас есть файл csv с такими записями, как :
«Боб», «Смит», «Саннивейл», «Калифорния»
или когда вы имеете дело с шестью частями fqn, как показано ниже. Я широко использую их для ведения журнала subject_fqn для аудита, обработки ошибок и т.д., а parsename обрабатывает только четыре части:
[netbios_name].[machine_name].[instance].[database].[schema].[table].[column]
Вот моя обновленная версия и спасибо vzczc за его оригинальный пост!
select * from [utility].[split_string](N'"this"."string"."gets"."split"."and"."removes"."leading"."and"."trailing"."quotes"', N'"."', N'"', N'"');
select * from [utility].[split_string](N'"this"."string"."gets"."split"."but"."leaves"."leading"."and"."trailing"."quotes"', N'"."', null, null);
select * from [utility].[split_string](N'[netbios_name].[machine_name].[instance].[database].[schema].[table].[column]', N'].[', N'[', N']');
create function [utility].[split_string] (
@input [nvarchar](max)
, @separator [sysname]
, @lead [sysname]
, @lag [sysname])
returns @node_list table (
[index] [int]
, [node] [nvarchar](max))
begin
declare @separator_length [int]= len(@separator)
, @lead_length [int] = isnull(len(@lead), 0)
, @lag_length [int] = isnull(len(@lag), 0);
--
set @input = right(@input, len(@input) - @lead_length);
set @input = left(@input, len(@input) - @lag_length);
--
with [splitter]([index], [starting_position], [start_location])
as (select cast(@separator_length as [bigint])
, cast(1 as [bigint])
, charindex(@separator, @input)
union all
select [index] + 1
, [start_location] + @separator_length
, charindex(@separator, @input, [start_location] + @separator_length)
from [splitter]
where [start_location] > 0)
--
insert into @node_list
([index],[node])
select [index] - @separator_length as [index]
, substring(@input, [starting_position], case
when [start_location] > 0
then
[start_location] - [starting_position]
else
len(@input)
end) as [node]
from [splitter];
--
return;
end;
go
Почти все другие ответы заменяют разделяемую строку, которая тратит циклы ЦП и выполняет ненужное выделение памяти.
Я описываю гораздо лучший способ разбить строку здесь: 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.
Этот шаблон отлично работает, и вы можете обобщить
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, 4k iops подготовленный диск)
Хотя это похоже на ответ на основе 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','')
Решение на основе чистого набора с использованием 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
если кто-то хочет получить только одну часть отложенного текста, может использовать это
выберите * 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
)
Я оставил это,
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 всегда должен быть там.
Вы можете разделить строку в 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);
Еще одна функция получения n-й части строки с помощью функции-разделителя:
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
Мне нравится это решение как возможность вернуть одну подстроку, а не получить проанализированную таблицу, из которой вам затем нужно будет выбрать. Использование результата таблицы имеет свои применения, но для того, что мне было нужно, это сработало отлично.
Это вопрос не о подходе с разделением строк, но про как получить n-й элемент.
Все ответы здесь выполняют какое-то разделение строк с использованием рекурсии, CTE, нескольких CHARINDEX, REVERSE и PATINDEX, изобретают функции, вызывают методы CLR, таблицы номеров, CROSS APPLY ... Большинство ответов охватывают много строк кода.
Но - если вы действительно не хочу ничего, кроме подхода к получению 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)');
К сожалению, разработчики забыли вернуть индекс детали с STRING_SPLIT. Но, используя SQL-Server 2016+, есть JSON_VALUE и OPENJSON.
С JSON_VALUE мы можем передать позицию как индексный массив.
Для OPENJSON в документация четко указано:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
Строке вроде 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 () ...
Мы можем использовать массив внутри массива, просто используя удвоенный [[]]. Это позволяет использовать типизированное предложение 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
Re: если ваша строка может включать запрещенные символы ... вы можете просто обернуть подстроки, например, <x><![CDATA[x<&>x]]></x>.
@SalmanA, да, CDATA-разделы тоже могут справиться с этим ... Но после преобразования они исчезли (неявно изменены на экранированный text()). Мне не нравится магия под капотом, поэтому я бы предпочел подход (SELECT 'Text with <&>' AS [*] FOR XML PATH('')). Мне это кажется более чистым и все равно происходит ... (Еще немного о CDATA и XML).
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, ',')
Это хорошо, но не решает вопрос о получении n-го результата.
STRING_SPLIT не гарантирует возврат того же заказа. Но OPENJSON делает (см. Мой ответ (раздел обновления))
Современный подход с использованием 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-й элемент из номера строки.
STRING_SPLIT не гарантирует возврат того же заказа. Но OPENJSON делает (см. Мой ответ (раздел обновления))
Ответ Аарона Бертрана великолепен, но ошибочен. Он не точно обрабатывает пробел в качестве разделителя (как это было в примере в исходном вопросе), поскольку функция длины удаляет конечные пробелы.
Ниже приведен его код с небольшой поправкой, позволяющей использовать разделитель пробелов:
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
);
Если ваша база данных имеет уровень совместимости 130 или выше, вы можете использовать функцию STRING_SPLIT вместе с предложениями СМЕЩЕНИЕ ВЫВОДА, чтобы получить конкретный элемент по индексу.
Чтобы получить элемент по индекс 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';
Уловка заключается в OFFSET 1 ROWS, который пропустит первый элемент и вернет второй элемент. Если ваши индексы начинаются с 0 и @X - это переменная, содержащая индекс элемента, который вы хотите получить, вы можете обязательно сделать OFFSET @X ROWS
Хорошо, не использовал это раньше ... Приятно знать ... Я все же предпочел бы подход, основанный на разделении xml, поскольку он позволяет извлекать значение типобезопасно и не требует подзапроса, но это хороший. +1 с моей стороны
проблема здесь в том, что STRING_SPLIT не гарантирует порядок возвращаемых результатов. Таким образом, ваш предмет 1 может быть или не быть моим предметом 1.
@GorgiRankovski, Использование требований STRING_SPLIT для v2016 +. В этом случае лучше использовать OPENJSON или JSON_VALUE. Вы можете захотеть проверь мой ответ
Вот функция, которая выполнит задачу по разделению строки и доступу к элементу 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
Это слишком сложно ... .nodes() не нужен. Вы можете поместить XQuery непосредственно в .value() (см. Мой ответ). Кстати: скалярные функции очень плохие исполнители. Намного лучше было встроенный TVF, даже если он возвращает только одну ячейку в одной строке ...
ПРОСТОЕ РЕШЕНИЕ ДЛЯ РАЗБОРКИ ПО ПЕРВОЕ И ПОСЛЕДНЕЕ ИМЯ
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')
)
Я понимаю, что это действительно старый вопрос, но, начиная с SQL Server 2016, есть функции для анализа данных JSON, которые можно использовать для конкретного решения вопроса OP - и без разделения строк или использования определяемой пользователем функции. Чтобы получить доступ к элементу по определенному индексу в строке с разделителями, используйте функцию JSON_VALUE. Однако требуются правильно отформатированные данные JSON: строки должны быть заключены в двойные кавычки ", а разделителем должна быть запятая ,, а вся строка должна быть заключена в квадратные скобки [].
DECLARE @SampleString NVARCHAR(MAX) = '"Hello John Smith"';
--Format as JSON data.
SET @SampleString = '[' + REPLACE(@SampleString, ' ', '","') + ']';
SELECT
JSON_VALUE(@SampleString, '$[0]') AS Element1Value,
JSON_VALUE(@SampleString, '$[1]') AS Element2Value,
JSON_VALUE(@SampleString, '$[2]') AS Element3Value;
Выход
Element1Value Element2Value Element3Value
--------------------- ------------------- ------------------------------
Hello John Smith
(1 row affected)
Использование SQL Server 2016 и выше. Используйте этот код для ОБРЕЗКИ строк, игнорируйте значения NULL и применяйте индекс строки в правильном порядке. Он также работает с разделителем пробелов:
DECLARE @STRING_VALUE NVARCHAR(MAX) = 'one, two,,three, four, five'
SELECT ROW_NUMBER() OVER (ORDER BY R.[index]) [index], R.[value] FROM
(
SELECT
1 [index], NULLIF(TRIM([value]), '') [value] FROM STRING_SPLIT(@STRING_VALUE, ',') T
WHERE
NULLIF(TRIM([value]), '') IS NOT NULL
) R
Если вы просмотрите следующий учебник SQL по разделение строки с помощью SQL, вы найдете ряд функций, которые можно использовать для разделения заданной строки на SQL Server.
Например, UDF-функцию SplitAndReturnNth можно использовать для разделения текста с помощью разделителя и возврата N-го фрагмента в качестве вывода функции.
select dbo.SplitAndReturnNth('Hello John Smith',' ',2)
См. Также stackoverflow.com/questions/314824/…