Funzionamento di Stuff e "For Xml Path" nel server SQL


367

La tabella è:

+----+------+
| Id | Name |
+----+------+    
| 1  | aaa  |
| 1  | bbb  |
| 1  | ccc  |
| 1  | ddd  |
| 1  | eee  |
+----+------+

Uscita richiesta:

+----+---------------------+
| Id |        abc          |
+----+---------------------+ 
|  1 | aaa,bbb,ccc,ddd,eee |
+----+---------------------+

Query:

SELECT ID, 
    abc = STUFF(
                 (SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
               ) 
FROM temp1 GROUP BY id

Questa query funziona correttamente. Ma ho solo bisogno della spiegazione di come funziona o esiste un altro o un modo breve per farlo.

Mi sto confondendo molto per capirlo.



1
Ho creato una pagina SqlFiddle per questo, per vederlo funzionare nella vita reale. Spero che aiuti gli altri.
Sabuncu,

1
^ Forse IDè unico in una diversa tabella di entità diverse, e questa tabella memorizza cose che appartengono a loro.
Nick Rolando,

Questa query non funziona se alcune delle righe hanno un ID diverso. ad es. se 'ddd' e 'eee' hanno Id 2.
KevinVictor

10
Tempo per la mia visita mensile a questa pagina per vedere dove ho sbagliato.
Taylor Ackley,

Risposte:


683

Ecco come funziona:

1. Ottieni la stringa dell'elemento XML con FOR XML

L'aggiunta di FOR XML PATH alla fine di una query consente di generare i risultati della query come elementi XML, con il nome dell'elemento contenuto nell'argomento PATH. Ad esempio, se dovessimo eseguire la seguente dichiarazione:

SELECT ',' + name 
              FROM temp1
              FOR XML PATH ('')

Passando una stringa vuota (FOR XML PATH ('')), otteniamo invece quanto segue:

,aaa,bbb,ccc,ddd,eee

2. Rimuovere la virgola iniziale con STUFF

L'istruzione STUFF "inserisce" letteralmente una stringa in un'altra, sostituendo i caratteri all'interno della prima stringa, ma la stiamo usando semplicemente per rimuovere il primo carattere dell'elenco di valori risultante.

SELECT abc = STUFF((
            SELECT ',' + NAME
            FROM temp1
            FOR XML PATH('')
            ), 1, 1, '')
FROM temp1

I parametri di STUFFsono:

  • La stringa da "imbottire" (nel nostro caso l'elenco completo dei nomi con una virgola iniziale)
  • La posizione per iniziare a eliminare e inserire caratteri (1, stiamo riempiendo una stringa vuota)
  • Il numero di caratteri da eliminare (1, essendo la virgola iniziale)

Quindi finiamo con:

aaa,bbb,ccc,ddd,eee

3. Iscriviti su id per ottenere l'elenco completo

Quindi ci uniamo a questo nell'elenco degli ID nella tabella temporanea, per ottenere un elenco di ID con nome:

SELECT ID,  abc = STUFF(
             (SELECT ',' + name 
              FROM temp1 t1
              WHERE t1.id = t2.id
              FOR XML PATH (''))
             , 1, 1, '') from temp1 t2
group by id;

E abbiamo il nostro risultato:

-----------------------------------
| Id        | Name                |
|---------------------------------|
| 1         | aaa,bbb,ccc,ddd,eee |
-----------------------------------

Spero che sia di aiuto!


57
Dovresti lavorare per il team di documentazione di Microsoft (se presente)
Fandango68,

55
@ Fandango68, @ FutbolFan - Non può lavorare per il team di documentazione di Microsoft. Le sue spiegazioni sono troppo chiare e troppo dirette. ;-)
Chris,

1
@ChrisProsser Sono d'accordo. Oracle ha anticipato Microsoft su questo introducendo la LISTAGGfunzione in Oracle 11gR2. Mi manca questa funzionalità nei giorni in cui devo usare questo invece. techonthenet.com/oracle/functions/listagg.php
FutbolFan

2
Ciao. Nel passaggio 1, se lo fai: SELEZIONA il nome DA temp1 PER XML PATH ('') ... ottieni <name>aaa</name> <name> bbb </name> ... ecc ... Non l'ho fatto all'inizio lo capisco ... Modificandolo in SELEZIONA '' + nome ... ecc ... rimuove i tag.
KevinVictor,

1
@ChrisProsser - Sybase ASA ha una listfunzione per decenni. Sfortunatamente, invece, SQL Server basato su Microsoft sull'ASE di Sybase non si è mai preoccupato di una funzione di elenco fino allo scorso anno. Sono d'accordo - è sbalorditivo. E poi lo fanno, lo chiamano string_agg. Pensavo listfosse abbastanza ovvio.
Youcantryreachingme,

75

Questo articolo illustra vari modi di concatenare le stringhe in SQL, inclusa una versione migliorata del codice che non codifica in XML i valori concatenati.

SELECT ID, abc = STUFF
(
    (
        SELECT ',' + name
        FROM temp1 As T2
        -- You only want to combine rows for a single ID here:
        WHERE T2.ID = T1.ID
        ORDER BY name
        FOR XML PATH (''), TYPE
    ).value('.', 'varchar(max)')
, 1, 1, '')
FROM temp1 As T1
GROUP BY id

Per capire cosa sta succedendo, inizia con la query interna:

SELECT ',' + name
FROM temp1 As T2
WHERE T2.ID = 42 -- Pick a random ID from the table
ORDER BY name
FOR XML PATH (''), TYPE

Poiché stai specificando FOR XML, otterrai una singola riga contenente un frammento XML che rappresenta tutte le righe.

Poiché non è stato specificato un alias di colonna per la prima colonna, ogni riga verrà racchiusa in un elemento XML con il nome specificato tra parentesi dopo il FOR XML PATH. Ad esempio, se lo avessi FOR XML PATH ('X'), otterrai un documento XML simile a:

<X>,aaa</X>
<X>,bbb</X>
...

Ma, dal momento che non hai specificato un nome di elemento, ottieni solo un elenco di valori:

,aaa,bbb,...

Il .value('.', 'varchar(max)')recupera semplicemente il valore dal frammento XML risultante, senza XML codifica caratteri "speciali". Ora hai una stringa che assomiglia a:

',aaa,bbb,...'

La STUFFfunzione rimuove quindi la virgola iniziale, dandoti un risultato finale simile a:

'aaa,bbb,...'

A prima vista sembra piuttosto confuso, ma tende ad avere prestazioni abbastanza buone rispetto ad alcune delle altre opzioni.


2
A che serve digitare nella tua query. Penso che per la definizione, il risultato del percorso XML verrà archiviato in valore (non sono sicuro di spiegarlo se sbagliato).
Puneet Chawla,

8
@PuneetChawla: la TYPEdirettiva dice a SQL di restituire i dati usando il xmltipo. Senza di essa, i dati vengono restituiti come nvarchar(max). Viene utilizzato qui per evitare problemi di codifica XML se nella namecolonna sono presenti caratteri speciali .
Richard Deeming,

2
@barlop: come spiegato nell'articolo di SimpleTalk , se si elimina TYPEe .value('.', 'varchar(max)'), è possibile finire con entità con codifica XML nel risultato.
Richard Deeming,

1
@RichardDeeming vuoi dire se i dati contengono o potrebbero contenere parentesi angolari?
barlop

1
Ma dal momento che non hai specificato un nome di elemento, ottieni solo un elenco di valori , questa è l'intuizione che mi mancava. Grazie.
Adam

44

La modalità PATH viene utilizzata per generare XML da una query SELECT

1. SELECT   
       ID,  
       Name  
FROM temp1
FOR XML PATH;  

Ouput:
<row>
<ID>1</ID>
<Name>aaa</Name>
</row>

<row>
<ID>1</ID>
<Name>bbb</Name>
</row>

<row>
<ID>1</ID>
<Name>ccc</Name>
</row>

<row>
<ID>1</ID>
<Name>ddd</Name>
</row>

<row>
<ID>1</ID>
<Name>eee</Name>
</row>

L'output è un XML incentrato sugli elementi in cui ogni valore di colonna nel set di righe risultante è racchiuso in un elemento di riga. Poiché la clausola SELECT non specifica alcun alias per i nomi delle colonne, i nomi degli elementi figlio generati sono gli stessi dei nomi delle colonne corrispondenti nella clausola SELECT.

Per ogni riga nel set di righe viene aggiunto un tag.

2.
SELECT   
       ID,  
       Name  
FROM temp1
FOR XML PATH('');

Ouput:
<ID>1</ID>
<Name>aaa</Name>
<ID>1</ID>
<Name>bbb</Name>
<ID>1</ID>
<Name>ccc</Name>
<ID>1</ID>
<Name>ddd</Name>
<ID>1</ID>
<Name>eee</Name>

Per il passaggio 2: se si specifica una stringa di lunghezza zero, l'elemento di avvolgimento non viene prodotto.

3. 

    SELECT   

           Name  
    FROM temp1
    FOR XML PATH('');

    Ouput:
    <Name>aaa</Name>
    <Name>bbb</Name>
    <Name>ccc</Name>
    <Name>ddd</Name>
    <Name>eee</Name>

4. SELECT   
        ',' +Name  
FROM temp1
FOR XML PATH('')

Ouput:
,aaa,bbb,ccc,ddd,eee

Nel passaggio 4 stiamo concatenando i valori.

5. SELECT ID,
    abc = (SELECT   
            ',' +Name  
    FROM temp1
    FOR XML PATH('') )
FROM temp1

Ouput:
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee
1   ,aaa,bbb,ccc,ddd,eee


6. SELECT ID,
    abc = (SELECT   
            ',' +Name  
    FROM temp1
    FOR XML PATH('') )
FROM temp1 GROUP by iD

Ouput:
ID  abc
1   ,aaa,bbb,ccc,ddd,eee

Nel passaggio 6 stiamo raggruppando la data per ID.

STUFF (source_string, start, length, add_string) Parametri o argomenti source_string La stringa di origine da modificare. inizio La posizione in source_string per eliminare i caratteri di lunghezza e quindi inserire add_string. lunghezza Il numero di caratteri da eliminare da source_string. add_string La sequenza di caratteri da inserire in source_string nella posizione iniziale.

SELECT ID,
    abc = 
    STUFF (
        (SELECT   
                ',' +Name  
        FROM temp1
        FOR XML PATH('')), 1, 1, ''
    )
FROM temp1 GROUP by iD

Output:
-----------------------------------
| Id        | Name                |
|---------------------------------|
| 1         | aaa,bbb,ccc,ddd,eee |
-----------------------------------

1
Scrivi "Nel passaggio 4 stiamo concatenando i valori". Ma non è chiaro perché / come la ','colonna specificata come, combinata con il ('')percorso after xml, causi la concatenazione
barlop

Nel passaggio 4, qualsiasi operazione di stringa utilizzerà l'elemento di wrapping specificato che è vuoto ('') per questo caso.
vCillusion,

1
Per chiunque si chieda il punto 4 e perché <Nome> scompare. È perché dopo la concatenazione Nome con virgola non c'è più colonna ma solo valore, quindi SQL Server non sa quale nome per il tag xml dovrebbe essere usato. Per esempio questa query SELECT 'a' FROM some_table FOR XML PATH('')produrrà: 'aaaaaaa'. Ma sarà specificato se il nome della colonna: SELECT 'a' AS Col FROM some_table FOR XML PATH('')si ottiene risultato:<Col>a</Col><Col>a</Col><Col>a</Col>
anth

23

Esistono funzionalità nuovissime nel database SQL di Azure e in SQL Server (a partire dal 2017) per gestire questo scenario esatto. Credo che questo servirebbe da metodo ufficiale nativo per ciò che stai cercando di realizzare con il metodo XML / STUFF. Esempio:

select id, STRING_AGG(name, ',') as abc
from temp1
group by id

STRING_AGG - https://msdn.microsoft.com/en-us/library/mt790580.aspx

EDIT: Quando ho pubblicato questo post, ho fatto menzione di SQL Server 2016 poiché pensavo di averlo visto su una potenziale funzionalità che doveva essere inclusa. O me lo sono ricordato in modo errato o qualcosa è cambiato, grazie per la modifica suggerita che fissa la versione. Inoltre, sono rimasto molto colpito e non ero pienamente consapevole del processo di revisione in più passaggi che mi ha appena portato a un'opzione finale.


3
STRING_AGG non si trova in SQL Server 2016. Si dice che arriverà in "vNext".
N8allan,

Spiacenti, non intendevo sovrascrivere la modifica da @lostmylogin, mi dispiace per quello ... Ecco chi ha effettivamente superato la modifica della correzione.
Brian Jorden,

5

In for xml path, se definiamo un valore simile, [ for xml path('ENVLOPE') ]questi tag verranno aggiunti con ogni riga:

<ENVLOPE>
</ENVLOPE>

2
SELECT ID, 
    abc = STUFF(
                 (SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
               ) 
FROM temp1 GROUP BY id

Qui nella query sopra la funzione STUFF viene utilizzata per rimuovere solo la prima virgola (,)dalla stringa xml generata, (,aaa,bbb,ccc,ddd,eee)quindi diventerà (aaa,bbb,ccc,ddd,eee).

E FOR XML PATH('')converte semplicemente i dati di colonna in (,aaa,bbb,ccc,ddd,eee)stringa ma in PATH stiamo passando '' in modo che non crei un tag XML.

E alla fine abbiamo raggruppato i record usando la colonna ID .


2

Ho eseguito il debug e alla fine ho restituito la mia query "imbottita" in modo normale.

Semplicemente

select * from myTable for xml path('myTable')

mi dà il contenuto della tabella per scrivere su una tabella di registro da un trigger che debug.


1
Declare @Temp As Table (Id Int,Name Varchar(100))
Insert Into @Temp values(1,'A'),(1,'B'),(1,'C'),(2,'D'),(2,'E'),(3,'F'),(3,'G'),(3,'H'),(4,'I'),(5,'J'),(5,'K')
Select X.ID,
stuff((Select ','+ Z.Name from @Temp Z Where X.Id =Z.Id For XML Path('')),1,1,'')
from @Temp X
Group by X.ID

-1

STUFF ((SELEZIONA distinto ',' + CAST (T.ID) DA Tabella T dove T.ID = 1 FOR XML PATH ('')), 1,1, '') AS Nome


-3

Sto usando frequentemente con la clausola where

SELECT 
TapuAda=STUFF(( 
SELECT ','+TBL.TapuAda FROM (
SELECT TapuAda FROM T_GayrimenkulDetay AS GD 
INNER JOIN dbo.T_AktiviteGayrimenkul AS AG ON  D.GayrimenkulID=AG.GayrimenkulID WHERE 
AG.AktiviteID=262
) AS TBL FOR XML PATH ('')
),1,1,'')

2
Non vedo come questa sia una risposta, potresti dare qualche spiegazione per favore?
Gar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.