web-dev-qa-db-fra.com

T-SQL: Inverse de la concaténation de chaînes - Comment diviser une chaîne en plusieurs enregistrements

Duplicate possible:
Fractionner la chaîne en SQL

J'ai vu quelques questions liées à la concaténation de chaînes en SQL. Je me demande comment vous aborderiez le problème opposé: fractionner une chaîne délimitée par des virgules en lignes de données:

Disons que j'ai des tables:

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

Et veulent insérer des données dans la table

userTag(userID,tagID) 'multiple entries per user

Inspiré par Quelles étiquettes ne sont pas dans la base de données? question

[~ # ~] éditer [~ # ~]

Merci pour les réponses, en réalité plus qu’un mérite d’être accepté, mais je ne peux en choisir qu’une, et la solution présentée par Cade Roux avec récurrence me semble assez propre. Cela fonctionne sur SQL Server 2005 et supérieur.

Pour les versions antérieures de SQL Server, la solution fournie par miies peut être utilisée. Pour travailler avec le type de données texte wcm answer sera utile. Merci encore.

135
kristof

Il existe une grande variété de solutions à ce problème documenté ici , y compris ce petit bijou:

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
  )
147
Cade Roux

Vous pouvez également obtenir cet effet en utilisant XML, comme on le voit ici , ce qui supprime la limitation des réponses fournies, qui semblent toutes inclure une récursion. L’utilisation particulière que j’ai faite ici autorise jusqu’à un délimiteur de 32 caractères, mais elle pourrait être augmentée aussi grande qu’elle soit.

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

Ensuite, vous pouvez l'invoquer en utilisant:

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

Qui retourne:

-----------
|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)
    )
84
Nathan Wheeler

J'utilise cette fonction (SQL Server 2005 et supérieur).

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
18
user39603

Dans le cas particulier de la division de chaînes en mots, j'ai rencontré une autre solution pour 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

Résultats

display_term                   Cnt
------------------------------ -----------
the                            3
brown                          2
lorry                          2
sea                            2
11
Martin Smith

Légère modification de la solution ci-dessus afin qu'il fonctionne avec des délimiteurs de longueur variable.

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: J'ai utilisé datalength () car len () ne rapporte pas correctement s'il y a des espaces finaux.

7
Rory

Voici une fonction Split compatible avec les versions de SQL Server antérieures à 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
Tomalak

En utilisant CLR, voici une alternative beaucoup plus simple qui fonctionne dans tous les cas, mais 40% plus rapide que la réponse acceptée:

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);
    }
}

Bien sûr, il est toujours 8 fois plus lent que le regexp_split_to_table.

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

vous donnera la première balise. Vous pouvez procéder de la même manière pour obtenir le second et ainsi de suite en combinant sous-chaîne et charindex un calque plus profond à chaque fois. C'est une solution immédiate, mais cela ne fonctionne qu'avec très peu de balises, car la taille de la requête augmente très rapidement et devient illisible. Passez ensuite aux fonctions, comme indiqué dans d'autres réponses plus sophistiquées à ce message.

5
Yann Semet

J'ai voté "Nathan Wheeler" comme réponse car j'ai trouvé que "Cade Roux" ne fonctionnait pas au-dessus d'une certaine taille de chaîne.

Quelques points

-J'ai trouvé que l'ajout du mot clé DISTINCT améliorait les performances pour moi.

La réponse de -Nathan ne fonctionne que si vos identifiants ont 5 caractères ou moins, bien sûr, vous pouvez ajuster cela ... Si les éléments que vous divisez sont INT Je suis vous pouvez nous même comme moi ci-dessous:

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
2
Darren

J'ai écrit ceci quelque temps en arrière. Il suppose que le délimiteur est une virgule et que les valeurs individuelles ne dépassent pas 127 caractères. Il pourrait être modifié assez facilement.

Il a l'avantage de ne pas être limité à 4 000 caractères.

Bonne chance!

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 
2
wcm

Je le fais habituellement avec le code suivant:

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

Vous pouvez le tester avec cette requête:

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

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