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