J'utilise SQL Server 2014 et j'ai une table avec une colonne contenant une chaîne CSV :
110,200,310,130,null
La sortie du tableau ressemble à ceci:
Je veux sélectionner la deuxième colonne comme plusieurs colonnes, en plaçant chaque élément de la chaîne CSV dans une colonne distincte, comme ceci:
J'ai donc créé une fonction pour fractionner une chaîne:
create FUNCTION [dbo].[fn_splitstring]
(
@List nvarchar(2000),
@SplitOn nvarchar(5)
)
RETURNS @RtnValue table
(
Id int identity(1,1),
Value nvarchar(100)
)
AS
BEGIN
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))
return
END
Je voudrais l'utiliser comme ceci:
select Val , (select value from tvf_split_string(cchar1,',')) from table1
Mais le code ci-dessus ne fonctionnera évidemment pas, car la fonction renverra plus d'une ligne, ce qui provoquera le renvoi de plus d'une valeur par la sous-requête et la rupture du code.
Je peux utiliser quelque chose comme:
select Val ,
(select value from tvf_split_string(cchar1,',') order by id offset 0 rows fetch next 1 rows only ) as col1,
(select value from tvf_split_string(cchar1,',') order by id offset 1 rows fetch next 1 rows only ) as col2,
................
from table1
mais je ne pense pas que ce soit une bonne approche.
Quelle est la bonne façon de procéder?
Pour résoudre ce problème, vous aurez probablement besoin d'un code de procédure supplémentaire. Différentes bases de données ont différents ensembles de fonctions de chaîne intégrées (comme vous le savez). Ainsi, pour trouver une solution à cela, j'ai écrit un code de "preuve de concept" qui est plutôt générique et utilise uniquement la fonction SUBSTR () (l'équivalent est SUBSTRING () dans MS SQL).
Lorsque vous regardez LOAD DATA ... (MySQL), vous pouvez voir que nous avons besoin d'un fichier .csv et d'une table existante. En utilisant ma fonction, vous pourrez sélectionner des sections du fichier .csv, en passant le nom de la colonne plus 2 entiers: un pour le numéro du "délimiteur de gauche", et un pour le numéro de "droite- délimiteur côté main ". (Ça a l'air horrible ...).
Exemple: supposons que nous ayons une valeur séparée par des virgules qui ressemble à ceci, et qu'elle soit stockée dans une colonne appelée csv:
aaa,111,zzz
Si nous voulons "extraire" 111 de cela, nous appelons la fonction comme ceci:
select split(csv, 1, 2) ... ;
-- 1: start at the first comma
-- 2: end at the second comma
Le "début" et la "fin" de la chaîne peuvent être sélectionnés comme ceci:
select split(csv, 0, 1) ... ; -- from the start of the string (no comma) up to the first comma
select split(csv, 2, 0) ... ; -- from the second comma right up to the end of the string
Je sais que le code de la fonction n'est pas parfait et qu'il peut être simplifié par endroits (par exemple, dans Oracle, nous devrions utiliser INSTR (), qui peut trouver une occurrence particulière d'une partie d'une chaîne). De plus, il n'y a aucune exception pour le moment. Ce n'est qu'une première ébauche. Voici ...
create or replace function split(
csvstring varchar2
, lcpos number
, rcpos number )
return varchar2
is
slen pls_integer := 0 ; -- string length
comma constant varchar2(1) := ',' ;
currentchar varchar2(1) := '' ;
commacount pls_integer := 0 ;
firstcommapos pls_integer := 0 ;
secondcommapos pls_integer := 0 ;
begin
slen := length(csvstring);
-- special case: leftmost value
if lcpos = 0 then
firstcommapos := 0 ;
for i in 1 .. slen
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
secondcommapos := i - 1 ;
exit ;
end if ;
end loop ;
return substr(csvstring, 1, secondcommapos) ;
end if ;
-- 2 commas somewhere in the middle of the string
if lcpos > 0 and rcpos > 0 then
for i in 1 .. slen
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
commacount := commacount + 1;
if commacount = lcpos then
firstcommapos := i ;
end if ;
if commacount = rcpos then
secondcommapos := i ;
end if ;
end if ;
end loop ;
return substr(csvstring, firstcommapos + 1, (secondcommapos-1) - firstcommapos ) ;
end if ;
-- special case: rightmost value
if rcpos = 0 then
secondcommapos := slen ;
for i in reverse 1 .. slen -- caution: count DOWN!
loop
currentchar := substr(csvstring, i, 1) ;
if currentchar = comma then
firstcommapos := i + 1 ;
exit ;
end if ;
end loop ;
return substr(csvstring, firstcommapos, secondcommapos-(firstcommapos-1)) ;
end if ;
end split;
Essai:
-- test table, test data
create table csv (
id number generated always as identity primary key
, astring varchar2(256)
);
-- insert some test data
begin
insert into csv (astring) values ('123,456,88789,null,null');
insert into csv (astring) values ('123,456,99789,1234,null');
insert into csv (astring) values ('123,456,00789,1234,null');
insert into csv (astring) values ('1,2222,77789,null,null');
insert into csv (astring) values ('11,222,88789,null,');
insert into csv (astring) values ('111,22,99789,,');
insert into csv (astring) values ('1111,2,00789,oooo,null');
end;
-- testing:
select
split(astring,0,1) col1
, split(astring,1,2) col2
, split(astring,2,3) col3
, split(astring,3,4) col4
, split(astring,4,0) col5
from csv
-- output
COL1 COL2 COL3 COL4 COL5
123 456 88789 null null
123 456 99789 1234 null
123 456 00789 1234 null
1 2222 77789 null null
11 222 88789 null -
111 22 99789 - -
1111 2 00789 oooo null
... La fonction semble exagérée. Cependant, si nous écrivons plus de code procédural, le SQL en fonction de celui-ci devient plutôt "élégant". Bonne chance dans le traitement de votre csv!
De manière générale, les fonctions scalaires et les mTVF (Multi-Statement Table Valued Functions) sont un peu un problème de performances intégré. Il est préférable, si vous le pouvez, d'utiliser les iTVF (Inline Table Valued Functions) car leur code est réellement inclus dans le plan d'exécution (un peu comme une VUE mais paramétré) au lieu d'être exécuté séparément. Plutôt que de reproduire un article entier ici, veuillez consulter ce qui suit pour une preuve même lorsqu'un iTVF est utilisé comme iSF (fonction scalaire en ligne). Dans de nombreux cas, les fonctions scalaires peuvent être 7 fois plus lentes que les iTVF même lorsque le code est identique. Voici le lien, auquel vous pouvez désormais accéder sans avoir à vous inscrire pour devenir membre. Pour résumer, si votre fonction contient le mot "BEGIN", ce ne sera PAS un iTVF haute performance.
Comment rendre les FDU scalaires plus rapides (Spackle SQL)
En passant au problème d'origine signalé par @Reza et en empruntant fortement au code fourni par @stefan pour générer des exemples de lignes de données, voici les données de test que nous utiliserons. Je le fais dans une table temporaire afin que nous puissions facilement supprimer la table de test sans avoir à craindre de supprimer accidentellement une vraie table.
--===== If the test table exists, drop it to make reruns in SSMS eaiser
IF OBJECT_ID('tempdb..#CSV','U') IS NOT NULL
DROP TABLE #CSV
;
--===== Create the test table.
CREATE TABLE #CSV --Using a Temp Table just for demo purposes
(
ID INT IDENTITY(1,1) --or whatever your PK is
,AString VARCHAR(8000)
)
;
--===== Insert some test data
INSERT INTO #CSV
(AString)
SELECT AString
FROM (
VALUES ('123,456,88789,null,null')
,('123,456,99789,1234,null')
,('123,456,00789,1234,null')
,('1,2222,77789,null,null')
,('11,222,88789,null,')
,('111,22,99789,,')
,('1111,2,00789,oooo,null')
) v (AString)
;
Si vous utilisez toujours SQL Server 2005, ne désespérez pas. Tout le code y fonctionnera toujours. Nous avons juste besoin de changer la façon dont nous générons les données de test car la clause VALUES ne pouvait pas gérer la façon dont nous avons généré les données ci-dessus jusqu'en 2008. À la place, vous devez utiliser une série d'instructions SELECT/UNION ALL. Il n'y a pratiquement aucune différence de performances entre les deux si vous avez besoin d'un plus grand volume de données et la méthode SELECT/UNION ALL fonctionne toujours très bien jusqu'en 2016.
Voici le code de génération de table de test modifié pour fonctionner pour 2005.
--===== If the test table exists, drop it to make reruns in SSMS eaiser
IF OBJECT_ID('tempdb..#CSV','U') IS NOT NULL
DROP TABLE #CSV
;
--===== Create the test table.
CREATE TABLE #CSV --Using a Temp Table just for demo purposes
(
ID INT IDENTITY(1,1) --or whatever your PK is
,AString VARCHAR(8000)
)
;
--===== Insert some test data
INSERT INTO #CSV
(AString)
SELECT AString
FROM (
SELECT '123,456,88789,null,null' UNION ALL
SELECT '123,456,99789,1234,null' UNION ALL
SELECT '123,456,00789,1234,null' UNION ALL
SELECT '1,2222,77789,null,null' UNION ALL
SELECT '11,222,88789,null,' UNION ALL
SELECT '111,22,99789,,' UNION ALL
SELECT '1111,2,00789,oooo,null'
) v (AString)
;
Tout d'abord, SQL Server n'est pas le meilleur outil à utiliser pour les manipulations de chaînes. Il a été prouvé à plusieurs reprises qu'un SQLCLR correctement écrit fumera à peu près n'importe quelle tentative de fractionnement de chaîne par rapport aux solutions T-SQL pures. Les problèmes associés aux solutions SQLCLR sont 1) de trouver celui qui est correctement écrit (renvoie correctement l'ensemble de résultats attendu) et 2) il est parfois difficile de convaincre les DBA résidents d'autoriser l'utilisation de SQLCLR. Si vous en trouvez un qui est correctement écrit pour renvoyer les résultats attendus, cela vaut vraiment la peine de passer du temps à essayer de convaincre le DBA car il gérera généralement les types de données MAX et se souciera peu si vous le transmettez les données VARCHAR ou NVARCHAR, sans parler d'être plus rapides que toute solution T-SQL que j'ai jamais vue.
Si vous n'avez pas à vous soucier de l'utilisation de NVARCHAR et que vous n'avez pas à vous soucier des délimiteurs à plusieurs caractères (ou pouvez changer le délimiteur "en cours de route") et que vous n'avez pas à vous soucier des types de données MAX (limité à 8K chaînes), alors la fonction DelimitedSplit8K peut être pour vous. Vous pouvez trouver l'article correspondant ainsi que les tests de "cardinalité élevée" appropriés à l'URL suivante.
Tally OH! Une fonction améliorée "Splitter CSV" SQL 8K
Je vous dirai également que le code de génération de données de test à cardinalité élevée dans cet article s'est rompu lorsque SQL Server 2012 est sorti en raison de modifications apportées à la façon dont XML peut être utilisé pour concaténer. Wayne Sheffield a effectué des tests distincts pour le comparer à la nouvelle fonction de séparateur en 2016 et a effectué une réparation du code de génération de données de test dans l'article suivant. Notez qu'il y a également 3 autres répartiteurs qui sont disponibles dans l'article, dont 2 ne sont disponibles qu'en 2016. Il teste également par rapport à un SQLCLR et répertorie certaines des différences fonctionnelles (lacunes dans certains cas) de chaque répartiteur.
Fractionnement de chaînes dans SQL Server 2016
Il y a aussi un article par Eirikur Eiriksson qui explore non seulement une amélioration sérieuse des performances, mais explore également le fractionnement des "vrais CSV" qui peuvent être inclus des délimiteurs entre les "qualificateurs de texte" (bien qu'à des frais considérables de performances). Voici le lien vers cet article.
Profiter des avantages des fonctions Window dans T-SQL
Malgré tout, même il démontre qu'un SQLCLR est beaucoup plus rapide que tout ce que vous pouvez faire en utilisant uniquement T-SQL. Je recommande cette méthode au lieu de toute méthode T-SQL, mais FAITES des tests de fonctionnalité étendus pour vous assurer qu'elle retournera ce que vous attendez dans certains des cas "étranges".
D'accord. Désolé pour le détournement de longue haleine sur les séparateurs. Revenons au problème d'origine.
Nous devons faire deux choses:
Fractionnez les CSV de manière à savoir de quelle ligne provient chaque élément fractionné.
Réassemblez les éléments divisés en colonnes distinctes dans la même ligne pour chaque ligne que nous divisons.
Tout d'abord, nous devons créer une fonction de séparation. Voici une modification de DelimitedSplit8K sur laquelle je travaille (enfin, pas récemment ;-)) qui supportera toujours SQL Server 2005. La grande différence est d'utiliser un classement binaire pour les performances supplémentaires. Ce rendu n'est pas encore entièrement testé (je l'utilise personnellement depuis septembre 2013) mais, compte tenu de la simplicité des changements et du fait qu'aucun d'entre eux ne modifie réellement la fonctionnalité, je pense que vous le trouverez satisfaisant pour vos besoins.
CREATE FUNCTION [dbo].[DelimitedSplit8K]
/**********************************************************************************************************************
Purpose:
Given a string containing multiple elements separated by a single character delimiter and that single character
delimiter, this function will split the string and return a table of the single elements (Item) and the element
position within the string (ItemNumber).
Notes:
1. Performance of this function approaches that of a CLR.
2. Note that this code implicitly converts NVARCHAR to VARCHAR and that conversion may NOT be faithful.
Revision History:
Note that this code is a modification of a well proven function created as a community effort and initially documented
at the following URL (http://www.sqlservercentral.com/articles/Tally+Table/72993/). This code is still undergoing
tests. Although every care has certainly been taken to ensure its accuracy, you are reminded to do your own tests to
ensure that this function is suitable for whatever application you might use it for.
--Jeff Moden, 01 Sep 2013
**********************************************************************************************************************/
--===== Define I/O parameters
(@pString VARCHAR(8000) , @pDelimiter CHAR(1)) --DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000).
WITH E1(N) AS (--==== Itzik Ben-Gan style of a cCTE (Cascading CTE) and
-- should not be confused with a much slower rCTE (Recursive CTE).
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E4(N) AS (SELECT 1 FROM E1 a, E1 b, E1 c, E1 d), --10E+4 or 10,000 rows max
cteTally(N) AS ( --=== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS ( --=== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT CASE WHEN SUBSTRING(@pString,t.N,1) = @pDelimiter COLLATE Latin1_General_BIN THEN t.N+1 END --added short circuit for casting
FROM cteTally t
WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter COLLATE Latin1_General_BIN
),
cteLen(N1,L1)AS ( --=== Return start position and length (for use in substring).
-- The ISNULL/NULLIF combo handles the length for the final of only element.
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(@pDelimiter ,@pString COLLATE Latin1_General_BIN,s.N1) ,0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(@pString, l.N1, l.L1)
FROM cteLen l
;
GO
Une fois que ce mauvais garçon est en place, des problèmes comme ceux du message d'origine deviennent triviaux à résoudre en utilisant soit un PIVOT soit un CROSS TAB. Je préfère la méthode CROSS TAB plutôt ancienne car elle est souvent plus rapide (parfois de manière significative) qu'un PIVOT réel. Veuillez consulter l'article suivant sur les tests qui y sont effectués (il est assez ancien et devrait probablement être mis à jour pour les mesures de performances, bien qu'il explique comment utiliser les deux méthodes).
Tableaux croisés et pivots, Partie 1 - Conversion de lignes en colonnes
Voici le code qui utilise la fonction ci-dessus et un onglet CROSS pour résoudre le problème d'origine publié sur ce fil.
--===== Do the split for each row and repivot to columns using a
-- high performance CROSS TAB. It uses the function only once
-- for each row, which is another advantage iTVFs have over
-- Scalare Functions.
SELECT csv.ID
,Col1 = MAX(CASE WHEN ca.ItemNumber = 1 THEN Item ELSE '' END)
,Col2 = MAX(CASE WHEN ca.ItemNumber = 2 THEN Item ELSE '' END)
,Col3 = MAX(CASE WHEN ca.ItemNumber = 3 THEN Item ELSE '' END)
,Col4 = MAX(CASE WHEN ca.ItemNumber = 4 THEN Item ELSE '' END)
,Col5 = MAX(CASE WHEN ca.ItemNumber = 5 THEN Item ELSE '' END)
FROM #CSV csv
CROSS APPLY dbo.DelimitedSplit8K(csv.AString,',') ca
GROUP BY csv.ID
;
GO
Voici l'ensemble de résultats du code ci-dessus en utilisant les données de test données.
Si vous avez des questions ou avez besoin d'éclaircissements à ce sujet, n'hésitez pas à les poser.