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. Еще раз спасибо.

Ответ 1

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

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
  )

Ответ 2

Вы также можете добиться этого эффекта, используя 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)
    )

Ответ 3

Я использую эту функцию (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

Ответ 4

Для частного случая разделения строк на слова я столкнулся с другим решением для 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

Ответ 5

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

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() неверно сообщает, если есть конечные пробелы.

Ответ 6

Здесь функция 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

Ответ 7

Используя 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 раз медленнее PostgreSQL regexp_split_to_table.

Ответ 8

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

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

Ответ 9

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

Он имеет преимущество не ограничиваться 4000 символами.

Удачи!

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 @[email protected] 
        ELSE BEGIN 
                set @last=charindex(',', reverse(@str)) 
                set @str=substring(@txt,@Beg,[email protected]) 
                set @[email protected][email protected]+1 
        END 

        declare @workingString varchar(25) 
                , @stringindex int 



        while @Beg<[email protected] 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 @[email protected] set @[email protected]+1 
                else IF len(@str)<8000 set @[email protected] 
                ELSE BEGIN 
                        set @last=charindex(',', reverse(@str)) 
                        set @str=substring(@txt,@Beg,[email protected]) 
                        set @[email protected][email protected]+1 

                END 
        END     

        return
END 

Ответ 10

Я проголосовал за ответ "Натан Уилер", поскольку я нашел ответ "Cade Roux" не работал над определенным размером строки.

Несколько пунктов

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

-Nathan answer работает только в том случае, если ваши идентификаторы имеют 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

Ответ 11

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

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