T-SQL: В отличие от конкатенации строк - как разбить строку на несколько записей

Возможный дубликат:
Разделить строку в SQL

Я видел пару вопросов, связанных с конкатенацией строк в SQL. Интересно, как бы вы подойти к противоположной проблеме: разделить строку, разделенную запятыми, на строки данных:

Допустим, у меня есть таблицы:

userTypedTags(userID,commaSeparatedTags) 'one entry per user
tags(tagID,name)

И хотите вставить данные в таблицу

userTag(userID,tagID) 'multiple entries per user

На основе вопроса каких тегов в базе данных нет?

ИЗМЕНИТЬ

Спасибо за ответы, на самом деле более одного заслуживают того, чтобы быть принятым, но я могу выбрать только один, а решение, представленное Cade Roux с рекурсиями, мне кажется довольно понятным. Он работает на SQL Server 2005 и выше.

Для более ранней версии SQL Server решение предоставлено miies. Для работы с типом текстовых данных wcm answer будет вам полезен. Еще раз спасибо.


person kristof    schedule 24.11.2008    source источник
comment
Просто используйте для этого процедуру Split. Множество людей разместили код для этого на SO и в других местах.   -  person Kevin Fairchild    schedule 24.11.2008
comment
звучит так, как будто вам нужно разделить этот столбец в отдельную таблицу. Если теги хранятся только в списке с разделителями, как бы вы могли написать эффективный sql для поиска записей, связанных с определенным тегом?   -  person Joel Coehoorn    schedule 24.11.2008
comment
Кевин, не могли бы вы дать несколько ссылок?   -  person wcm    schedule 24.11.2008
comment
Это идея, стоящая за вопросом, разделить список тегов строкой, чтобы стать набором записей.   -  person kristof    schedule 25.11.2008
comment
Ладно, тогда в этом больше смысла. Так что я должен преследовать вашего предшественника за создание этого беспорядка;)   -  person Joel Coehoorn    schedule 25.11.2008
comment
См. Также stackoverflow.com/questions/2647/split-string-in-sql.   -  person Jarrod Dixon♦    schedule 08.03.2010
comment
Видно, что выполнение этого в SQL, возможно, является требованием, но не создает табличную функцию разделения CLR, которая просто вызывает string.split с параметрами, эффективное, элегантное и простое в развертывании и обслуживании решение (включая, но не ограничиваясь, cultureinfo, stringsplitoptions и т. д.)) люк   -  person luckyluke    schedule 04.03.2011
comment
В SQL Server 2016 у вас есть функция STRING_SPLIT: msdn.microsoft.com/en-us /library/mt684588.aspx   -  person Jovan MSFT    schedule 09.03.2016


Ответы (11)


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

CREATE FUNCTION dbo.Split (@sep char(1), @s varchar(512))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + 1, CHARINDEX(@sep, @s, stop + 1)
      FROM Pieces
      WHERE stop > 0
    )
    SELECT pn,
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
    FROM Pieces
  )
person Cade Roux    schedule 24.11.2008
comment
Фантастическая функция. Можно использовать nchar () и nvarchar (). Также см. Ниже предложения с разделителями переменной длины. - person Rory; 17.10.2009
comment
В SQL Server 2008 эта версия завершается ошибкой, если в списке более 101 элемента: оператор завершен. Максимальное количество рекурсии 100 было исчерпано до завершения оператора. - person Mike Schenk; 12.02.2010
comment
@MikeSchenk Вы можете использовать подсказку OPTION (MAXRECURSION n) (msdn.microsoft.com /en-us/library/ms181714.aspx), чтобы изменить уровень рекурсии - однако это не разрешено в определениях UDF. Этот вопрос (social.msdn .microsoft.com / forum / en-US / transactsql / thread /) будет означать, что вы можете указать его вне UDF, и он по-прежнему будет работать. - person Cade Roux; 12.02.2010
comment
Предупреждение: не работает с большими входными строками (более 1000 символов). Максимальное количество рекурсии 100 было исчерпано до завершения оператора. - person cbp; 14.04.2010
comment
Я понимаю, что вхожу в это невероятно поздно ... примерно на 1,5 года, но есть довольно интересное решение, не требующее рекурсии, о котором здесь не упоминалось. Я разместил это ниже. - person Nathan Wheeler; 15.05.2010
comment
Как я могу вызвать эту функцию из выбранного запроса? Я получаю следующую ошибку: не удается найти ни столбец dbo, ни пользовательскую функцию, ни агрегат dbo.fn_Split, либо имя неоднозначно. - person Lajos Arpad; 21.12.2012
comment
@LajosArpad Вы должны опубликовать весь свой пример (с фактическим кодом, который вы пытаетесь) в другом вопросе. - person Cade Roux; 22.12.2012

Вы также можете достичь этого эффекта с помощью XML, как показано здесь, что снимает ограничение предоставленных ответов, которые, похоже, в некоторой степени включают рекурсию. Конкретное использование, которое я здесь сделал, позволяет использовать до 32-значного разделителя, но его можно увеличить, насколько бы большим он ни был.

create FUNCTION [dbo].[Split] (@sep VARCHAR(32), @s VARCHAR(MAX))
RETURNS TABLE
AS
    RETURN
    (
        SELECT r.value('.','VARCHAR(MAX)') as Item
        FROM (SELECT CONVERT(XML, N'<root><r>' + REPLACE(REPLACE(REPLACE(@s,'& ','&amp; '),'<','&lt;'), @sep, '</r><r>') + '</r></root>') as valxml) x
        CROSS APPLY x.valxml.nodes('//root/r') AS RECORDS(r)
    )

Затем вы можете вызвать его, используя:

SELECT * FROM dbo.Split(' ', 'I hate bunnies')

Что возвращает:

-----------
|I        |
|---------|
|hate     |
|---------|
|bunnies  |
-----------


Должен заметить, я вообще-то не ненавижу кроликов ... это просто почему-то пришло мне в голову.
Следующее - самое близкое, что я мог придумать, используя тот же метод во встроенной функции с табличным значением. НЕ ИСПОЛЬЗУЙТЕ ЕГО, ЭТО УЖАСНО НЕЭФФЕКТИВНО! Это просто для справки.

CREATE FUNCTION [dbo].[Split] (@sep VARCHAR(32), @s VARCHAR(MAX))
RETURNS TABLE
AS
    RETURN
    (
        SELECT r.value('.','VARCHAR(MAX)') as Item
        FROM (SELECT CONVERT(XML, N'<root><r>' + REPLACE(@s, @sep, '</r><r>') + '</r></root>') as valxml) x
        CROSS APPLY x.valxml.nodes('//root/r') AS RECORDS(r)
    )
person Nathan Wheeler    schedule 14.05.2010
comment
@ md5sum: Отличное решение! - person p.campbell; 15.05.2010
comment
@ md5sum - если бы вы могли поместить это во встроенную функцию с табличным значением, я был бы на борту. Обычно скалярные функции ужасно работают на SQL Server. Я бы хотел увидеть это в сравнении с решением ITVF. - person Cade Roux; 15.05.2010
comment
@Cade Roux - извините, но я не думаю, что понимаю, о чем вы здесь спрашиваете ... это функция с табличным значением. Если вы просто не хотите, чтобы он возвращал выбранное значение, а не вставлял его во временную таблицу? - person Nathan Wheeler; 17.05.2010
comment
@ md5sum - встроенные функции с табличным значением (без BEGIN) обычно работают хуже и обрабатываются совершенно иначе, чем функции с несколькими инструкциями с табличным значением (как это есть). Конечно, вы никогда не сможете сказать, пока не проведете сравнительный анализ ... - person Cade Roux; 17.05.2010
comment
@ md5sum Я вижу, что упоминал скалярные функции - я, наверное, забыл закончить свою мысль - производительность на UDF обычно лучше всего на встроенном TVF, затем на TVF с несколькими операторами, затем на скалярных функциях (которые совершенно ужасны). - person Cade Roux; 17.05.2010
comment
@Cade Roux - встроенная версия (лучшее воплощение, которое я мог придумать) по сравнению с этим полная шутка. Я даже попробовал это еще пару раз, чтобы убедиться, что на сервере что-то не сработало, чтобы испортить мои результаты. Опубликованная выше версия, разделенная на 3168 строк I HATE BUNNIES, занимает ~ 110 мс. Встроенная версия с теми же параметрами постоянно занимает ~ 1: 25,175. Если хотите, я могу опубликовать встроенную версию. Возможно, я сделал что-то ужасно неправильное, когда построил его, но, насколько я могу судить, я сделал это единственно возможным правильным способом. - person Nathan Wheeler; 17.05.2010
comment
@ md5sum Я опоздал, голосую за вас, но хотел отметить, что мне было бы интересно посмотреть, как он масштабируется для гораздо большего количества строк. Я знаю, что даже простые скалярные функции могут ужасно работать по сравнению со встроенным кодом на сотнях тысяч строк. - person Cade Roux; 26.08.2010
comment
@Cade Roux - я не совсем уверен. Я знаю, что использовал его около 1000, и он работает нормально. Однако я бы сказал, что SQL Server в любом случае не место для манипуляций со строками (по крайней мере, ни в каких разумных шаблонах проектирования архитектуры приложений). - person Nathan Wheeler; 26.08.2010
comment
@ md5sum Я согласен, что это, вероятно, проблема моделирования / дизайна. Однако, если у вас есть терабайты данных, иногда вы не хотите втягивать их в еще одну систему, чтобы что-то с ними сделать. Очевидно, что структура данных должна быть улучшена, чтобы облегчить ожидаемую обработку. Иногда не так просто передать миллионы строк через что-то получше, чем T-SQL. Вот почему конструктивные дефекты в 1000 раз труднее компенсировать, если они обнаруживаются на стадии производства в течение жизненного цикла. - person Cade Roux; 27.08.2010
comment
@ md5sum благодарит за решения, но в первом примере параметры должны быть переименованы в @sep и @s соответственно. (сам не мог изменить) - person Tabloo Quijico; 10.11.2010
comment
@Tabloo Quijico - удивительно, что никто другой не уловил несовпадающие имена параметров. Спасибо! - person Nathan Wheeler; 10.11.2010
comment
В этом примере вы ограничиваете текст до 5 символов. Таким образом, ваш вывод должен возвращать I | hate | bunni - person arviman; 26.10.2011
comment
Если у вас есть символы amperstand (&) в ваших данных, это не сработает, если вы не замените их строкой на '' - person jkirkwood; 28.10.2011
comment
@arviman - Да, мой друг указал на это ... Я просто не удосужился обновить здесь ответ. - person Nathan Wheeler; 03.02.2012
comment
@jkirkwood - Да, поскольку вы конвертируете его в XML, вам нужно отформатировать все специальные символы XML в их соответствующее экранированное значение (прямо сейчас не могу придумать для этого подходящее слово). Таким образом, это применимо также к ‹и›. - person Nathan Wheeler; 03.02.2012
comment
Это отличное решение! Я просто изменил VARCHAR (5) на NVARHAR (MAX), и он отлично работает для меня! Спасибо! - person Ryan; 14.01.2013
comment
На самом деле он возвращает Ненавижу банни из-за VARCHAR(5) :) - person Jeremy Thompson; 18.04.2013
comment
@JeremyThompson - я мог бы поклясться, что когда-то это было исправлено ... вы можете видеть из комментариев выше, что это было упомянуто ранее :) - person Nathan Wheeler; 18.04.2013
comment
получил плохую производительность при изменении на uniqueidentifier, стоило мне 2 с, чтобы разделить 250 значений guid, не знаю почему ... - person Colin Niu; 15.11.2013
comment
@ColinNiu - Что вы изменили в uniqueidentifier? Вы можете проверить выполнение, но я держу пари, что процесс преобразования строк в уникальные идентификаторы - это то, что будет съедать вашу производительность, а не процесс их разделения. - person Nathan Wheeler; 01.03.2014
comment
Редактор номер 7 изменил первую функцию на встроенную. Это сбивает с толку, потому что в ответе обсуждаются различия между двумя решениями. На всякий случай, если кто-то еще запутается и увидит этот комментарий - person JoshBerke; 13.08.2014
comment
Странно, при тестировании я обнаружил, что версия функции с табличным значением (я вошел в историю этого ответа, чтобы получить ее) выполнялась более чем в 8 раз ХУЖЕ, чем встроенная версия. Я буду придерживаться встроенной версии, спасибо. - person Tim Friesen; 18.08.2015
comment
@JoshBerke, я отредактировал этот ответ, чтобы исправить указанную вами проблему. - person Tim Friesen; 18.08.2015
comment
Как бы то ни было, моя правка была отклонена, хотя цель моей правки заключалась в том, чтобы вернуть первоначальное намерение, которое выражал автор. На мой взгляд, плохая рецензия. - person Tim Friesen; 18.08.2015

Я использую эту функцию (SQL Server 2005 и выше).

create function [dbo].[Split]
(
    @string nvarchar(4000),
    @delimiter nvarchar(10)
)
returns @table table
(
    [Value] nvarchar(4000)
)
begin
    declare @nextString nvarchar(4000)
    declare @pos int, @nextPos int

    set @nextString = ''
    set @string = @string + @delimiter

    set @pos = charindex(@delimiter, @string)
    set @nextPos = 1
    while (@pos <> 0)
    begin
        set @nextString = substring(@string, 1, @pos - 1)

        insert into @table
        (
            [Value]
        )
        values
        (
            @nextString
        )

        set @string = substring(@string, @pos + len(@delimiter), len(@string))
        set @nextPos = @pos
        set @pos = charindex(@delimiter, @string)
    end
    return
end
person user39603    schedule 24.11.2008
comment
Спасибо за это. Думаю, это тоже сработает в SQLServer 2000. - person kristof; 25.11.2008
comment
Ты прав. Я думал, что функции с табличным значением были введены в SQL Server 2005, но это не так. - person user39603; 26.11.2008
comment
@commaCheck не используется, кроме присваивания, поэтому его можно удалить. - person David; 04.05.2010
comment
Также необходимо установить @string = substring (@string, @pos + 1, len (@string)) @string = substring (@string, @pos + len (@delimiter), len (@string)) - person David; 04.05.2010

Для частного случая разделения строк на слова я нашел другое решение для SQL Server 2008.

with testTable AS
(
SELECT 1 AS Id, N'how now brown cow' AS txt UNION ALL
SELECT 2, N'she sells sea shells upon the sea shore' UNION ALL
SELECT 3, N'red lorry yellow lorry' UNION ALL
SELECT 4, N'the quick brown fox jumped over the lazy dog'
)

SELECT display_term, COUNT(*) As Cnt
 FROM testTable
CROSS APPLY sys.dm_fts_parser('"' + txt + '"', 1033, 0,0)
GROUP BY display_term
HAVING COUNT(*) > 1
ORDER BY Cnt DESC

Возврат

display_term                   Cnt
------------------------------ -----------
the                            3
brown                          2
lorry                          2
sea                            2
person Martin Smith    schedule 31.07.2010
comment
Интересно, но следует отметить, что он требует, чтобы полнотекстовый поиск был установлен и доступен - person quetzalcoatl; 08.01.2013
comment
@quetzalcoatl - И для этого также требуются sysadmin разрешения. Еще может быть кому-то полезно. - person Martin Smith; 08.01.2013
comment
Абсолютно идеально. Это похоже на противоположность команды Stuff. Нет необходимости тестировать множество разделенных функций только для того, чтобы сомневаться в том, действительно ли вы хотите установить его в производственной среде. Идеально подходит для моих требований. Спасибо! - person Peter PitLock; 15.10.2014

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

create FUNCTION dbo.fn_Split2 (@sep nvarchar(10), @s nvarchar(4000))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + (datalength(@sep)/2), CHARINDEX(@sep, @s, stop + (datalength(@sep)/2))
      FROM Pieces
      WHERE stop > 0
    )
    SELECT pn,
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 4000 END) AS s
    FROM Pieces
  )

NB: я использовал datalength (), так как len () неправильно сообщает, если есть конечные пробелы.

person Rory    schedule 17.10.2009

Вот функция Split, совместимая с версиями SQL Server до 2005 года.

CREATE FUNCTION dbo.Split(@data nvarchar(4000), @delimiter nvarchar(100))  
RETURNS @result table (Id int identity(1,1), Data nvarchar(4000)) 
AS  
BEGIN 
    DECLARE @pos   INT
    DECLARE @start INT
    DECLARE @len   INT
    DECLARE @end   INT

    SET @len   = LEN('.' + @delimiter + '.') - 2
    SET @end   = LEN(@data) + 1
    SET @start = 1
    SET @pos   = 0

    WHILE (@pos < @end)
    BEGIN
        SET @pos = CHARINDEX(@delimiter, @data, @start)
        IF (@pos = 0) SET @pos = @end

        INSERT @result (data) SELECT SUBSTRING(@data, @start, @pos - @start)
        SET @start = @pos + @len
    END

    RETURN
END
person Tomalak    schedule 24.03.2011
comment
+1 для предотвращения рекурсии (поскольку SQL Server делает это так плохо), избегания XML (поскольку SQL не имеет простого API для экранирования специальных символов XML), а также избегания кода CLR (поскольку центры обработки данных некоторых компаний не позволяют настраивать код на общих экземплярах SQL Server). - person Zarepheth; 19.12.2013

Используя CLR, есть гораздо более простая альтернатива, которая работает во всех случаях, но на 40% быстрее, чем принятый ответ:

using System;
using System.Collections;
using System.Data.SqlTypes;
using System.Text.RegularExpressions;
using Microsoft.SqlServer.Server;

public class UDF
{
    [SqlFunction(FillRowMethodName="FillRow")]
    public static IEnumerable RegexSplit(SqlString s, SqlString delimiter)
    {
        return Regex.Split(s.Value, delimiter.Value);
    }

    public static void FillRow(object row, out SqlString str)
    {
        str = new SqlString((string) row);
    }
}

Конечно, он все равно в 8 раз медленнее, чем regexp_split_to_table PostgreSQL.

person sayap    schedule 22.07.2011
comment
EXPLAIN ANALYZE с PostgreSQL и его версией для бедняков, то есть проверкой Включить фактический план выполнения в SSMS с SQL Server. Точная таблица с миллионами записей в обеих базах данных. - person sayap; 19.03.2012
comment
Мне это нравится (хотя я еще не тестировал его), но вы должны включить свойство TableDefinition в атрибут SqlFunction, чтобы инструменты обработки данных могли генерировать правильное определение функции. - person karaken12; 30.07.2012

SELECT substring(commaSeparatedTags,0,charindex(',',commaSeparatedTags))

даст вам первый тег. Вы можете поступить аналогичным образом, чтобы получить второй и так далее, комбинируя substring и charindex каждый раз на один уровень глубже. Это немедленное решение, но оно работает только с очень небольшим количеством тегов, поскольку размер запроса очень быстро увеличивается и становится нечитаемым. Затем переходите к функциям, как указано в других, более сложных ответах на этот пост.

person Yann Semet    schedule 24.11.2008

Я написал это некоторое время назад. Предполагается, что разделителем является запятая, а отдельные значения не превышают 127 символов. Его можно довольно легко модифицировать.

Его преимущество состоит в том, что он не ограничен 4 000 символов.

Удачи!

ALTER Function [dbo].[SplitStr] ( 
        @txt text 
) 
Returns @tmp Table 
        ( 
                value varchar(127)
        ) 
as 
BEGIN 
        declare @str varchar(8000) 
                , @Beg int 
                , @last int 
                , @size int 

        set @size=datalength(@txt) 
        set @Beg=1 


        set @str=substring(@txt,@Beg,8000) 
        IF len(@str)<8000 set @Beg=@size 
        ELSE BEGIN 
                set @last=charindex(',', reverse(@str)) 
                set @str=substring(@txt,@Beg,8000-@last) 
                set @Beg=@Beg+8000-@last+1 
        END 

        declare @workingString varchar(25) 
                , @stringindex int 



        while @Beg<=@size Begin 
                WHILE LEN(@str) > 0 BEGIN 
                        SELECT @StringIndex = CHARINDEX(',', @str) 

                        SELECT 
                                @workingString = CASE 
                                        WHEN @StringIndex > 0 THEN SUBSTRING(@str, 1, @StringIndex-1) 
                                        ELSE @str 
                                END 

                        INSERT INTO 
                                @tmp(value)
                        VALUES 
                                (cast(rtrim(ltrim(@workingString)) as varchar(127)))
                        SELECT @str = CASE 
                                WHEN CHARINDEX(',', @str) > 0 THEN SUBSTRING(@str, @StringIndex+1, LEN(@str)) 
                                ELSE '' 
                        END 
                END 
                set @str=substring(@txt,@Beg,8000) 

                if @Beg=@size set @Beg=@Beg+1 
                else IF len(@str)<8000 set @Beg=@size 
                ELSE BEGIN 
                        set @last=charindex(',', reverse(@str)) 
                        set @str=substring(@txt,@Beg,8000-@last) 
                        set @Beg=@Beg+8000-@last+1 

                END 
        END     

        return
END 
person wcm    schedule 24.11.2008
comment
Отлично + без ограничений !! Спасибо - person 321X; 05.06.2012

Я поддержал ответ «Натан Уиллер», так как обнаружил, что ответ «Кейд Ру» не работает при превышении определенного размера строки.

Пара очков

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

-Ответ Натана работает только в том случае, если ваши идентификаторы состоят из 5 символов или меньше, конечно, вы можете это настроить ... Если элементы, которые вы разделяете, являются идентификаторами INT, как и я, вы можете использовать то же, что и я, ниже:

CREATE FUNCTION [dbo].Split
(
    @sep VARCHAR(32), 
    @s VARCHAR(MAX)
)
RETURNS 
    @result TABLE (
        Id INT NULL
    )   
AS
BEGIN
    DECLARE @xml XML
    SET @XML = N'<root><r>' + REPLACE(@s, @sep, '</r><r>') + '</r></root>'

    INSERT INTO @result(Id)
    SELECT DISTINCT r.value('.','int') as Item
    FROM @xml.nodes('//root//r') AS RECORDS(r)

    RETURN
END
person Darren    schedule 15.03.2012
comment
В зависимости от того, что разделяется, использование DISTINCT может иметь негативные последствия. А именно, что, возможно, итоговая таблица должна содержать несколько повторяющихся значений, но с DISTINCT она имеет только уникальные значения. - person Zarepheth; 19.12.2013

Обычно я делаю это с помощью следующего кода:

create function [dbo].[Split](@string varchar(max), @separator varchar(10))
returns @splited table ( stringPart varchar(max) )
with execute as caller
as
begin
    declare @stringPart varchar(max);
    set @stringPart = '';

    while charindex(@separator, @string) > 0
    begin
        set @stringPart = substring(@string, 0, charindex(@separator, @string));
        insert into @splited (stringPart) values (@stringPart);
        set @string = substring(@string, charindex(@separator, @string) + len(@separator), len(@string) + 1);
    end

    return;
end
go

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

declare @example varchar(max);
set @example = 'one;string;to;rule;them;all;;';

select * from [dbo].[Split](@example, ';');
person Marek    schedule 24.04.2012