Cómo funcionan Stuff y 'For Xml Path' en SQL Server

367

Tabla es:

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

Salida requerida:

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

Consulta:

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

Esta consulta está funcionando correctamente. Pero solo necesito la explicación de cómo funciona o si hay alguna otra forma de hacerlo.

Me estoy confundiendo mucho al entender esto.

Puneet Chawla
fuente
1
Hice una página SqlFiddle para esto, para verlo funcionando en la vida real. Espero que ayude a los demás.
Sabuncu
1
^ Quizás el IDes único en una tabla diferente de diferentes entidades, y esta tabla está almacenando cosas que les pertenecen.
Nick Rolando
Esta consulta no funciona si algunas de las filas tienen un ID diferente. por ejemplo, si 'ddd' y 'eee' tienen Id 2.
KevinVictor
10
Tiempo para mi visita mensual a esta página para ver dónde me equivoqué.
Taylor Ackley

Respuestas:

683

Así es como funciona:

1. Obtenga una cadena de elementos XML con FOR XML

Agregar FOR XML PATH al final de una consulta le permite generar los resultados de la consulta como elementos XML, con el nombre del elemento contenido en el argumento PATH. Por ejemplo, si tuviéramos que ejecutar la siguiente declaración:

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

Al pasar una cadena en blanco (FOR XML PATH ('')), obtenemos lo siguiente:

,aaa,bbb,ccc,ddd,eee

2. Elimine la coma inicial con COSAS

La instrucción STUFF literalmente "mete" una cadena en otra, reemplazando los caracteres dentro de la primera cadena. Sin embargo, la estamos usando simplemente para eliminar el primer carácter de la lista de valores resultante.

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

Los parámetros de STUFFson:

  • La cadena que se debe "rellenar" (en nuestro caso, la lista completa de nombres con una coma inicial)
  • La ubicación para comenzar a eliminar e insertar caracteres (1, estamos rellenando una cadena en blanco)
  • El número de caracteres a eliminar (1, siendo la coma inicial)

Entonces terminamos con:

aaa,bbb,ccc,ddd,eee

3. Únete a la identificación para obtener la lista completa

A continuación, solo unimos esto en la lista de ID en la tabla temporal, para obtener una lista de ID con nombre:

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;

Y tenemos nuestro resultado:

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

¡Espero que esto ayude!

FutbolFan
fuente
57
Debería trabajar para el equipo de documentación de Microsoft (si corresponde)
Fandango68
55
@ Fandango68, @ FutbolFan: no puede trabajar para el equipo de documentación de Microsoft. Sus explicaciones son demasiado claras y directas. ;-)
Chris
1
@ChrisProsser Estoy de acuerdo. Oracle ha estado por delante de Microsoft en esto al introducir la LISTAGGfunción en Oracle 11gR2. Echo de menos esa funcionalidad en los días en que tengo que usar esto en su lugar. techonthenet.com/oracle/functions/listagg.php
FutbolFan
2
Hola. En el paso 1, si lo hace: SELECCIONE el nombre de temp1 FOR XML PATH ('') ... obtendrá <name>aaa</name> <name> bbb </name> ... etc ... no lo hice ' No se dé cuenta al principio ... Al cambiarlo a SELECCIONAR '' + nombre ... etc ... elimina las etiquetas.
KevinVictor
1
@ChrisProsser: Sybase ASA ha tenido una listfunción durante décadas. Desafortunadamente, Microsoft basó SQLServer en el ASE de Sybase, y nunca se molestó con una función de lista hasta el año pasado. Estoy de acuerdo, es alucinante. Y luego lo hacen, lo llaman string_agg. Pensé que listera bastante obvio.
youcantryreachingme
75

Este artículo cubre varias formas de concatenar cadenas en SQL, incluida una versión mejorada de su código que no codifica en XML los valores concatenados.

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

Para comprender lo que sucede, comience con la consulta 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

Como está especificando FOR XML, obtendrá una única fila que contiene un fragmento XML que representa todas las filas.

Como no ha especificado un alias de columna para la primera columna, cada fila se ajustará en un elemento XML con el nombre especificado entre paréntesis después de FOR XML PATH. Por ejemplo, si lo hubiera hecho FOR XML PATH ('X'), obtendría un documento XML similar al siguiente:

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

Pero, como no ha especificado un nombre de elemento, solo obtiene una lista de valores:

,aaa,bbb,...

El .value('.', 'varchar(max)')simplemente recupera el valor del fragmento de XML resultante, sin XML que codifica todos los caracteres "especiales". Ahora tiene una cadena que se parece a:

',aaa,bbb,...'

La STUFFfunción luego elimina la coma inicial, lo que le da un resultado final que se parece a:

'aaa,bbb,...'

A primera vista, parece bastante confuso, pero tiende a funcionar bastante bien en comparación con algunas de las otras opciones.

Richard Deeming
fuente
2
¿De qué sirve Type en su consulta? Creo que para definir, el resultado de la ruta XML se almacenará en valor (no estoy seguro de explicarlo si es incorrecto).
Puneet Chawla
8
@PuneetChawla: la TYPEdirectiva le dice a SQL que devuelva los datos usando el xmltipo. Sin ella, los datos se devuelven como un nvarchar(max). Se usa aquí para evitar problemas de codificación XML si hay caracteres especiales en la namecolumna.
Richard Deeming el
2
@barlop: como explica el artículo de SimpleTalk , si suelta el TYPEy .value('.', 'varchar(max)'), puede terminar con entidades codificadas en XML en el resultado.
Richard Deeming
1
@ RichardDeeming, ¿quiere decir si los datos contienen o pueden contener corchetes angulares?
barlop
1
Pero, como no ha especificado un nombre de elemento, solo obtiene una lista de valores , esta es la información que me faltaba. Gracias.
Adam
44

El modo PATH se usa para generar XML a partir de una consulta 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>

La salida es XML centrado en elementos donde cada valor de columna en el conjunto de filas resultante se ajusta en un elemento de fila. Como la cláusula SELECT no especifica ningún alias para los nombres de columna, los nombres de elementos secundarios generados son los mismos que los nombres de columna correspondientes en la cláusula SELECT.

Para cada fila del conjunto de filas se agrega una etiqueta.

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>

Para el Paso 2: si especifica una cadena de longitud cero, el elemento de ajuste no se produce.

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

En el Paso 4 estamos concatenando los valores.

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

En el Paso 6 estamos agrupando la fecha por ID.

STUFF (source_string, start, length, add_string) Parámetros o argumentos source_string La cadena de origen a modificar. start La posición en source_string para eliminar caracteres de longitud y luego insertar add_string. longitud El número de caracteres para eliminar de source_string. add_string La secuencia de caracteres para insertar en source_string en la posición inicial.

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 |
-----------------------------------
Neha Chopra
fuente
1
Escribe "En el paso 4 estamos concatenando los valores". Pero no está claro por qué / cómo la ','columna especificada como, combinada con la ('')ruta xml posterior, hace que ocurra la concatenación
barlop
En el Paso 4, al realizar cualquier operación de cadena se utilizará el elemento de ajuste especificado que está en blanco ('') para este caso.
vCillusion
2
Para cualquiera que se pregunte sobre el punto 4 y por qué <Nombre> desaparece. Es porque después de la concatenación Nombre con coma ya no hay una columna, sino solo un valor, por lo que SQL Server no sabe qué nombre para la etiqueta xml debe usarse. Por ejemplo esta consulta SELECT 'a' FROM some_table FOR XML PATH('')producirá: 'aaaaaaa'. Pero si el nombre de la columna se especificará: SELECT 'a' AS Col FROM some_table FOR XML PATH('')se obtiene resultado:<Col>a</Col><Col>a</Col><Col>a</Col>
anth
23

Hay una funcionalidad muy nueva en Azure SQL Database y SQL Server (a partir de 2017) para manejar este escenario exacto. Creo que esto serviría como un método oficial nativo para lo que está tratando de lograr con el método XML / STUFF. Ejemplo:

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

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

EDITAR: Cuando publiqué esto originalmente, mencioné SQL Server 2016, ya que pensé que lo vi en una característica potencial que se incluiría. O lo recordaba incorrectamente o algo cambió, gracias por la edición sugerida que corrige la versión. Además, estoy bastante impresionado y no estaba completamente consciente del proceso de revisión de varios pasos que me llevó a una opción final.

Brian Jorden
fuente
3
STRING_AGG no está en SQL Server 2016. Se dice que viene en "vNext".
N8allan
Vaya, no quise sobrescribir la edición de @lostmylogin, perdón por eso ... Ese es el que realmente presionó la edición de corrección.
Brian Jorden el
5

En for xml path, si definimos cualquier valor como [ for xml path('ENVLOPE') ]entonces, estas etiquetas se agregarán con cada fila:

<ENVLOPE>
</ENVLOPE>
vikas
fuente
2
SELECT ID, 
    abc = STUFF(
                 (SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
               ) 
FROM temp1 GROUP BY id

Aquí, en la consulta anterior, la función STUFF se usa para eliminar la primera coma (,)de la cadena xml generada y (,aaa,bbb,ccc,ddd,eee)luego se convertirá (aaa,bbb,ccc,ddd,eee).

Y FOR XML PATH('')simplemente convierte los datos de la columna en una (,aaa,bbb,ccc,ddd,eee)cadena, pero en PATH estamos pasando '', por lo que no creará una etiqueta XML.

Y al final hemos agrupado los registros usando la columna ID .

Mahendra Singh Dhami
fuente
2

Hice la depuración y finalmente devolví mi consulta 'rellena' de la manera normal.

Simplemente

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

me da contenido de la tabla para escribir en una tabla de registro desde un desencadenador que depuro.

SlavaTT
fuente
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
Omkar Naik
fuente
-1

MATERIAL ((SELECCIONE distinto ',' + CAST (T.ID) DE la Tabla T donde T.ID = 1 PARA RUTA XML ('')), 1,1, '') COMO Nombre

B.Nishan
fuente
-3

Estoy usando frecuentemente con la cláusula 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,'')
sbaysal
fuente
2
No veo cómo esta es una respuesta, ¿podría dar algunas explicaciones, por favor?
Gar