การทำงานของ Stuff และ 'For Xml Path' ใน SQL Server เป็นอย่างไร


367

ตารางคือ

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

เอาต์พุตที่ต้องการ:

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

ค้นหา:

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

แบบสอบถามนี้ทำงานอย่างถูกต้อง แต่ฉันต้องการคำอธิบายว่ามันทำงานอย่างไรหรือมีวิธีอื่นหรือวิธีสั้น ๆ ในการทำเช่นนี้

ฉันสับสนมากที่จะเข้าใจสิ่งนี้



1
ฉันทำหน้า SqlFiddleสำหรับเรื่องนี้เพื่อดูว่ามันทำงานในชีวิตจริง หวังว่าจะช่วยผู้อื่น
Sabuncu

1
^ บางทีมันอาจIDจะไม่ซ้ำกันในตารางที่แตกต่างกันของเอนทิตีที่แตกต่างกันและตารางนี้จัดเก็บสิ่งต่าง ๆ ที่เป็นของพวกเขา
Nick Rolando

แบบสอบถามนี้ไม่ทำงานหากบางแถวมีรหัสที่แตกต่างกัน เช่นถ้า 'ddd' และ 'eee' มีรหัส 2
KevinVictor

10
เวลาที่ฉันเยี่ยมชมหน้านี้ทุกเดือนเพื่อดูว่าฉันผิดพลาดที่ไหน
Taylor Ackley

คำตอบ:


683

นี่คือวิธีการทำงาน:

1. รับองค์ประกอบสตริง XML ด้วย FOR XML

การเพิ่ม FOR PATH XML ให้กับส่วนท้ายของเคียวรีช่วยให้คุณสามารถแสดงผลลัพธ์ของแบบสอบถามเป็นองค์ประกอบ XML โดยมีชื่อองค์ประกอบอยู่ในอาร์กิวเมนต์ PATH ตัวอย่างเช่นถ้าเราต้องเรียกใช้คำสั่งต่อไปนี้:

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

โดยผ่านสตริงว่าง (สำหรับเส้นทาง XML ('')) เราจะได้รับสิ่งต่อไปนี้แทน:

,aaa,bbb,ccc,ddd,eee

2. ลบเครื่องหมายจุลภาคชั้นนำด้วย STUFF

คำสั่ง STUFF อย่างแท้จริง "ยัดไส้" หนึ่งสตริงเป็นอีกสตริงหนึ่งแทนที่อักขระภายในสตริงแรกอย่างไรก็ตามเราใช้เพียงเพื่อลบอักขระตัวแรกของรายการผลลัพธ์

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

พารามิเตอร์ของSTUFFคือ:

  • สตริงที่จะ "ยัด" (ในกรณีของเราคือรายการชื่อเต็มด้วยเครื่องหมายจุลภาคชั้นนำ)
  • ตำแหน่งที่จะเริ่มการลบและการแทรกตัวอักษร (1, เรากำลังบรรจุลงในสตริงว่าง)
  • จำนวนอักขระที่จะลบ (1 เป็นเครื่องหมายจุลภาคนำหน้า)

ดังนั้นเราจึงจบลงด้วย:

aaa,bbb,ccc,ddd,eee

3. เข้าร่วมที่ id เพื่อรับรายการทั้งหมด

ต่อไปเราเพิ่งเข้าร่วมในรายการรหัสในตารางชั่วคราวเพื่อรับรายการ ID ที่มีชื่อ:

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;

และเรามีผลลัพธ์ของเรา:

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

หวังว่านี่จะช่วยได้!


57
คุณควรทำงานให้กับทีมงานเอกสารของ Microsoft (ถ้ามี)
Fandango68

55
@ Fandango68, @ FutbolFan - เขาไม่สามารถทำงานให้กับทีมเอกสารของ Microsoft คำอธิบายของเขาชัดเจนเกินไปและตรงเกินไป ;-)
Chris

1
@ChrisProsser ฉันเห็นด้วย ออราเคิลเป็นผู้นำของ Microsoft ในเรื่องนี้โดยแนะนำLISTAGGฟังก์ชั่นใน Oracle 11gR2 ฉันคิดถึงฟังก์ชั่นนั้นในวันที่ฉันต้องใช้มันแทน techonthenet.com/oracle/functions/listagg.php
FutbolFan

2
สวัสดี. ในขั้นตอนที่ 1 ถ้าคุณทำ: เลือกชื่อจาก temp1 สำหรับเส้นทาง XML ('') ... คุณจะได้รับ <name>aaa</name> <name> bbb </name> ... ฯลฯ ฉันไม่ได้ ' ไม่ทราบสิ่งนี้ในตอนแรก ... การเปลี่ยนเป็น SELECT '' + ชื่อ ... ฯลฯ ... จะลบแท็ก
KevinVictor

1
@ChrisProsser - Sybase ASA มีlistฟังก์ชั่นมานานหลายทศวรรษ น่าเสียดายที่ Microsoft ใช้ SQLServer กับ ASE ของ Sybase แทนและไม่เคยใส่ใจกับฟังก์ชั่นรายการจนถึงปีที่แล้ว ฉันเห็นด้วย - มันเหลือเชื่อ string_aggและพวกเขาก็ทำพวกเขาเรียกว่า ฉันคิดว่าlistมันค่อนข้างชัดเจน
youcantryreachingme

75

บทความนี้จะกล่าวถึงวิธีการเชื่อมโยงสตริงต่างๆใน SQL รวมถึงโค้ดที่ปรับปรุงแล้วซึ่งไม่ได้เข้ารหัส XML ด้วยค่าที่ต่อกัน

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

เพื่อให้เข้าใจถึงสิ่งที่เกิดขึ้นให้เริ่มด้วยการสืบค้นภายใน:

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

เพราะคุณกำลังระบุ FOR XMLคุณจะได้รับแถวเดียวที่มีแฟรกเมนต์ XML แสดงถึงแถวทั้งหมด

เพราะคุณไม่ได้ระบุนามแฝงคอลัมน์คอลัมน์แรกแต่ละแถวจะถูกห่อในองค์ประกอบ XML FOR XML PATHที่มีชื่อระบุไว้ในวงเล็บหลัง ตัวอย่างเช่นถ้าคุณมีFOR XML PATH ('X')คุณจะได้รับเอกสาร XML ที่มีลักษณะดังนี้:

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

แต่เนื่องจากคุณไม่ได้ระบุชื่อองค์ประกอบคุณเพียงแค่ได้รับรายการค่า:

,aaa,bbb,...

.value('.', 'varchar(max)')เพียงแค่ดึงค่าจากส่วน XML ที่เกิดขึ้นโดยไม่ต้องใช้ XML การเข้ารหัสตัวอักษรใด "พิเศษ" ตอนนี้คุณมีสตริงที่มีลักษณะดังนี้:

',aaa,bbb,...'

STUFFฟังก์ชั่นแล้วเอาเครื่องหมายจุลภาคชั้นนำให้คุณมีผลสุดท้ายที่ดูเหมือนว่า:

'aaa,bbb,...'

มันดูค่อนข้างสับสนในแวบแรก แต่มันก็มีแนวโน้มที่จะทำงานได้ค่อนข้างดีเมื่อเทียบกับตัวเลือกอื่น ๆ


2
การใช้ประเภทในแบบสอบถามของคุณคืออะไร ฉันคิดว่ามันเป็นการกำหนดผลลัพธ์ของเส้นทาง XML จะถูกเก็บไว้ในค่า (ไม่แน่ใจอธิบายถ้าผิด)
Puneet Chawla

8
@PuneetChawla: สั่งบอก SQL เพื่อส่งกลับข้อมูลโดยใช้ชนิด ไม่ว่าข้อมูลจะถูกส่งกลับในฐานะที่เป็น มันถูกใช้ที่นี่เพื่อหลีกเลี่ยงปัญหาการเข้ารหัส XML หากมีอักขระพิเศษในคอลัมน์ TYPExmlnvarchar(max)name
Richard Deeming

2
@barlop: ตามที่บทความ SimpleTalkอธิบายไว้ถ้าคุณดรอปTYPEและ.value('.', 'varchar(max)')คุณสามารถจบด้วยเอนทิตีที่เข้ารหัส XML ในผลลัพธ์
Richard Deeming

1
@RichardDeeming คุณหมายความว่าข้อมูลมีหรืออาจมีวงเล็บเหลี่ยมหรือไม่?
barlop

1
แต่เนื่องจากคุณไม่ได้ระบุชื่อองค์ประกอบคุณเพียงแค่รับรายการค่านี่คือข้อมูลเชิงลึกที่ฉันพลาดไป ขอบคุณ.
อดัม

44

โหมด PATH ใช้ในการสร้าง XML จากแบบสอบถามแบบใช้เลือกข้อมูล

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>

เอาท์พุทเป็นองค์ประกอบเป็นศูนย์กลาง XML ที่แต่ละค่าของคอลัมน์ใน rowset ผลลัพธ์ถูกห่อในองค์ประกอบแถว เนื่องจากส่วนคำสั่ง SELECT ไม่ได้ระบุชื่อแทนใด ๆ สำหรับชื่อคอลัมน์ชื่อองค์ประกอบลูกที่สร้างขึ้นจะเหมือนกับชื่อคอลัมน์ที่เกี่ยวข้องในส่วนคำสั่ง SELECT

สำหรับแต่ละแถวใน rowset จะมีการเพิ่มแท็ก

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>

สำหรับขั้นตอนที่ 2: หากคุณระบุสตริงที่มีความยาวเป็นศูนย์องค์ประกอบการตัดจะไม่ถูกสร้างขึ้น

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

ในขั้นตอนที่ 4 เรากำลังเชื่อมโยงค่าต่างๆ

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

ในขั้นตอนที่ 6 เราจัดกลุ่มวันตาม ID

STUFF (source_string, start, length, add_string) พารามิเตอร์หรืออาร์กิวเมนต์ source_string สตริงต้นทางที่จะแก้ไข start ตำแหน่งใน source_string เพื่อลบอักขระความยาวแล้วใส่ add_string ความยาวจำนวนอักขระที่จะลบจาก source_string add_string ลำดับของอักขระที่จะแทรกลงใน source_string ที่ตำแหน่งเริ่มต้น

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
คุณเขียน "ในขั้นตอนที่ 4 เรากำลังเชื่อมต่อค่าต่างๆ" แต่ยังไม่ชัดเจนว่าทำไม','คอลัมน์ที่ระบุว่าเป็นอย่างไรรวมกับ('')เส้นทาง xml หลังทำให้การตัดแบ่งเกิดขึ้น
barlop

ในขั้นตอนที่ 4 การดำเนินการกับสตริงจะใช้องค์ประกอบการตัดที่ระบุซึ่งเป็นค่าว่าง ('') สำหรับกรณีนี้
vCillusion

1
สำหรับทุกคนที่สงสัยเกี่ยวกับจุดที่ 4 และสาเหตุที่ <Name> หายไป เป็นเพราะหลังจากชื่อการต่อข้อมูลที่มีเครื่องหมายจุลภาคไม่มีคอลัมน์อีกต่อไป แต่เป็นเพียงค่าดังนั้น SQL Server จึงไม่ทราบว่าควรใช้ชื่อใดสำหรับแท็ก xml ยกตัวอย่างเช่นแบบสอบถามนี้จะผลิต:SELECT 'a' FROM some_table FOR XML PATH('') 'aaaaaaa'แต่ถ้าชื่อคอลัมน์จะได้รับการระบุSELECT 'a' AS Col FROM some_table FOR XML PATH('')คุณจะได้รับผล:<Col>a</Col><Col>a</Col><Col>a</Col>
Anth

23

มีฟังก์ชันการทำงานใหม่ ๆ ในฐานข้อมูล Azure SQL และ SQL Server (เริ่มต้นด้วย 2017) เพื่อจัดการสถานการณ์สมมตินี้ ฉันเชื่อว่านี่จะเป็นวิธีที่เป็นทางการสำหรับสิ่งที่คุณพยายามทำด้วยวิธี XML / STUFF ตัวอย่าง:

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

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

แก้ไข:เมื่อฉันโพสต์ครั้งแรกนี้ฉันได้กล่าวถึง SQL Server 2016 เพราะฉันคิดว่าฉันเห็นว่าในคุณสมบัติที่เป็นไปได้ที่จะรวม ฉันจำได้ว่าไม่ถูกต้องหรือมีอะไรเปลี่ยนแปลงขอบคุณสำหรับการแก้ไขที่แนะนำในการแก้ไขเวอร์ชัน นอกจากนี้ยังค่อนข้างประทับใจและไม่ได้ตระหนักถึงขั้นตอนการตรวจสอบหลายขั้นตอนที่เพิ่งดึงฉันเข้าสู่ตัวเลือกสุดท้าย


3
STRING_AGG ไม่ได้อยู่ใน SQL Server 2016 มีการกล่าวกันว่ากำลังมาใน "vNext"
N8allan

โอ๊ะฉันไม่ได้ตั้งใจจะเขียนทับการแก้ไขจาก @lostmylogin ขอโทษด้วย ... นั่นคือผู้ที่ผลักดันผ่านการแก้ไขการแก้ไขจริง
Brian Jorden

5

ในfor xml pathหากเรากำหนดค่าใด ๆ เช่น[ for xml path('ENVLOPE') ]นั้นแท็กเหล่านี้จะถูกเพิ่มเข้าไปในแต่ละแถว:

<ENVLOPE>
</ENVLOPE>

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

ที่นี่ในฟังก์ชันแบบสอบถามSTUFFด้านบนใช้เพื่อลบเครื่องหมายจุลภาคแรก(,)จากสตริง xml ที่สร้างขึ้น(,aaa,bbb,ccc,ddd,eee)จากนั้นจะกลายเป็น(aaa,bbb,ccc,ddd,eee)แล้วมันจะกลายเป็น

และFOR XML PATH('')เพียงแปลงข้อมูลคอลัมน์เป็น(,aaa,bbb,ccc,ddd,eee)สตริง แต่อยู่ในPATHเรากำลังส่งผ่าน '' ดังนั้นมันจะไม่สร้างแท็ก XML

และในตอนท้ายเราได้จัดกลุ่มระเบียนโดยใช้คอลัมน์ID


2

ฉันทำการดีบั๊กและในที่สุดก็ส่งคืนข้อความค้นหา 'ยัดไส้' ของฉันไปเป็นวิธีปกติ

ง่ายดาย

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

ให้เนื้อหาของตารางแก่ฉันในการเขียนไปยังตารางบันทึกจากทริกเกอร์ที่ฉันดีบัก


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 ((SELECT ชัดเจน ',' + CAST (T.ID)) จากตาราง T โดยที่ T.ID = 1 สำหรับเส้นทาง XML ('')), 1,1, '') ชื่อ AS


-3

ฉันมักจะใช้กับที่ข้อ

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
ฉันไม่เห็นว่าคำตอบนี้เป็นอย่างไรคุณช่วยโยนคำอธิบายบางอย่างได้ไหม?
Gar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.