รวมส่วนย่อยของตารางเป้าหมาย


71

ฉันกำลังพยายามใช้MERGEคำสั่งเพื่อแทรกหรือลบแถวออกจากตาราง แต่ฉันต้องการดำเนินการกับชุดย่อยของแถวเหล่านั้นเท่านั้น เอกสารสำหรับMERGEคำเตือนที่มีคำพูดค่อนข้างแรง:

สิ่งสำคัญคือต้องระบุเฉพาะคอลัมน์จากตารางเป้าหมายที่ใช้สำหรับจุดประสงค์ที่ตรงกัน นั่นคือระบุคอลัมน์จากตารางเป้าหมายที่เปรียบเทียบกับคอลัมน์ที่เกี่ยวข้องของตารางต้นฉบับ อย่าพยายามปรับปรุงประสิทธิภาพการสืบค้นด้วยการกรองแถวในตารางเป้าหมายในส่วนคำสั่ง ON เช่นโดยระบุ AND NOT target_table.column_x = value การทำเช่นนั้นอาจส่งคืนผลลัพธ์ที่ไม่คาดคิดและไม่ถูกต้อง

แต่นี่คือสิ่งที่ฉันต้องทำเพื่อให้MERGEงานของฉันปรากฏ

ข้อมูลที่ฉันมีคือตารางการเข้าร่วมหลายต่อหลายมาตรฐานสำหรับหมวดหมู่ (เช่นรายการที่รวมอยู่ในหมวดหมู่ใด) ดังนี้:

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

สิ่งที่ฉันต้องทำคือการแทนที่แถวทั้งหมดในหมวดหมู่เฉพาะด้วยรายการใหม่ ความพยายามครั้งแรกของฉันในการทำเช่นนี้ดูเหมือนว่า:

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

สิ่งนี้ดูเหมือนว่าจะทำงานในการทดสอบของฉัน แต่ฉันกำลังทำสิ่งที่ MSDN เตือนฉันอย่างชัดเจนไม่ให้ทำ สิ่งนี้ทำให้ฉันกังวลว่าฉันจะพบปัญหาที่ไม่คาดคิดในภายหลัง แต่ฉันไม่เห็นวิธีอื่นที่จะทำให้ฉันMERGEมีผลกับแถวที่มีค่าฟิลด์เฉพาะเท่านั้น ( CategoryId = 2) และละเว้นแถวจากหมวดหมู่อื่น ๆ

มีวิธี "ที่ถูกต้องมากกว่า" เพื่อให้ได้ผลลัพธ์เดียวกันนี้หรือไม่? และ "ผลลัพธ์ที่ไม่คาดคิดหรือไม่ถูกต้อง" ที่ MSDN เตือนฉันคืออะไร


ใช่เอกสารจะมีประโยชน์มากกว่าหากมีตัวอย่างที่ชัดเจนของผลลัพธ์ที่ไม่คาดคิดและไม่ถูกต้อง
AK

3
@AlexKuznetsov มีตัวอย่างที่นี่
พอลไวท์

@SQLKiwi ขอบคุณสำหรับลิงค์ - IMO เอกสารจะดีกว่ามากถ้ามันถูกอ้างอิงจากหน้าดั้งเดิม
AK

1
@AlexKuznetsov เห็นด้วย น่าเสียดายที่การปรับโครงสร้างองค์กรของ BOL ในปี 2555 นั้นมีหลายสิ่งหลายอย่าง มันถูกเชื่อมโยงค่อนข้างดีในเอกสาร 2008 R2
พอลไวท์

คำตอบ:


103

MERGEคำสั่งมีไวยากรณ์ที่ซับซ้อนและมีการใช้งานที่ซับซ้อนมากขึ้น แต่หลักความคิดที่จะเข้าร่วมสองตารางกรองลงไปแถวที่ต้องมีการเปลี่ยนแปลง (แทรกปรับปรุงหรือลบ) และจากนั้นจะดำเนินการเปลี่ยนแปลงที่ร้องขอ รับข้อมูลตัวอย่างต่อไปนี้:

DECLARE @CategoryItem AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL,

    PRIMARY KEY (CategoryId, ItemId),
    UNIQUE (ItemId, CategoryId)
);

DECLARE @DataSource AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL

    PRIMARY KEY (CategoryId, ItemId)
);

INSERT @CategoryItem
    (CategoryId, ItemId)
VALUES
    (1, 1),
    (1, 2),
    (1, 3),
    (2, 1),
    (2, 3),
    (3, 5),
    (3, 6),
    (4, 5);

INSERT @DataSource
    (CategoryId, ItemId)
VALUES
    (2, 2);

เป้า

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          1       1 
          2       1 
          1       2 
          1       3 
          2       3 
          3       5 
          4       5 
          3       6 
╚════════════╩════════╝

แหล่ง

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          2       2 
╚════════════╩════════╝

ผลลัพธ์ที่ต้องการคือการเปลี่ยนข้อมูลในเป้าหมายที่มีข้อมูลจากแหล่งที่มา CategoryId = 2แต่สำหรับ ทำตามคำอธิบายของที่MERGEให้ไว้ข้างต้นเราควรเขียนแบบสอบถามที่รวมแหล่งที่มาและเป้าหมายในคีย์เท่านั้นและกรองแถวเฉพาะในส่วนWHENคำสั่ง:

MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON 
    SOURCE.ItemId = TARGET.ItemId 
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE 
    AND TARGET.CategoryId = 2 
    THEN DELETE
WHEN NOT MATCHED BY TARGET 
    AND SOURCE.CategoryId = 2 
    THEN INSERT (CategoryId, ItemId)
        VALUES (CategoryId, ItemId)
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

สิ่งนี้ให้ผลลัพธ์ต่อไปนี้:

╔═════════╦════════════╦════════╗
 $ACTION  CategoryId  ItemId 
╠═════════╬════════════╬════════╣
 DELETE            2       1 
 INSERT            2       2 
 DELETE            2       3 
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          1       1 
          1       2 
          1       3 
          2       2 
          3       5 
          3       6 
          4       5 
╚════════════╩════════╝

แผนการดำเนินการคือ: ผสานแผน

สังเกตว่าทั้งสองตารางถูกสแกนอย่างสมบูรณ์ เราอาจคิดว่าสิ่งนี้ไม่มีประสิทธิภาพเพราะมีเพียงแถวที่CategoryId = 2จะได้รับผลกระทบในตารางเป้าหมาย นี่คือที่คำเตือนใน Books Online เข้ามาความพยายามที่ผิดพลาดอย่างหนึ่งในการปรับให้เหมาะสมเพื่อสัมผัสแถวที่จำเป็นเฉพาะในเป้าหมายคือ:

MERGE INTO @CategoryItem AS TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource AS ds 
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

ตรรกะในONประโยคถูกนำไปใช้เป็นส่วนหนึ่งของการเข้าร่วม ในกรณีนี้การเข้าร่วมเป็นการรวมภายนอกเต็มรูปแบบ (ดูรายการหนังสือออนไลน์สำหรับสาเหตุ) การใช้การตรวจสอบหมวดหมู่ 2 ในแถวเป้าหมายโดยเป็นส่วนหนึ่งของการรวมภายนอกสุดจะส่งผลให้แถวที่มีค่าอื่นถูกลบ (เนื่องจากไม่ตรงกับแหล่งที่มา):

╔═════════╦════════════╦════════╗
 $ACTION  CategoryId  ItemId 
╠═════════╬════════════╬════════╣
 DELETE            1       1 
 DELETE            1       2 
 DELETE            1       3 
 DELETE            2       1 
 INSERT            2       2 
 DELETE            2       3 
 DELETE            3       5 
 DELETE            3       6 
 DELETE            4       5 
╚═════════╩════════════╩════════╝

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          2       2 
╚════════════╩════════╝

สาเหตุที่แท้จริงเป็นเหตุผลเดียวกันที่เพรดิเคตทำงานในข้อต่อภายนอกด้านนอกแตกต่างไปจากที่ONระบุไว้ในWHEREข้อ MERGEไวยากรณ์ (และการดำเนินงานที่เข้าร่วมขึ้นอยู่กับคำสั่งที่ระบุไว้) เพียงแค่ทำให้มันยากขึ้นที่จะเห็นว่าเป็นเช่นนี้

คำแนะนำในการหนังสือออนไลน์ (ขยายตัวในการปฏิบัติงานเพิ่มประสิทธิภาพรายการ) มีคำแนะนำว่าจะให้ความหมายที่ถูกต้องจะแสดงโดยใช้MERGEไวยากรณ์โดยที่ผู้ใช้จำเป็นต้องเข้าใจทุกรายละเอียดการดำเนินการหรือการบัญชีสำหรับวิธีการที่เพิ่มประสิทธิภาพถูกต้องตามกฎหมายอาจจัดเรียง สิ่งต่าง ๆ สำหรับเหตุผลประสิทธิภาพการดำเนินการ

เอกสารประกอบมีสามวิธีที่เป็นไปได้ในการนำการกรองเริ่มต้นไปใช้:

การระบุเงื่อนไขการกรองในWHENข้อรับประกันผลลัพธ์ที่ถูกต้อง แต่อาจหมายความว่ามีการอ่านและประมวลผลแถวเพิ่มเติมจากตารางต้นทางและตารางเป้าหมายเกินความจำเป็นอย่างเคร่งครัด (ดังที่เห็นในตัวอย่างแรก)

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

การใช้นิพจน์ตารางทั่วไปมีความเสี่ยงที่คล้ายคลึงกันในการเพิ่มเพรดิเคตให้กับONข้อ แต่ด้วยเหตุผลที่แตกต่างกันเล็กน้อย ในหลาย ๆ กรณีมันจะปลอดภัย แต่ต้องมีการวิเคราะห์ผู้เชี่ยวชาญของแผนปฏิบัติการเพื่อยืนยันเรื่องนี้ (และการทดสอบภาคปฏิบัติที่ครอบคลุม) ตัวอย่างเช่น:

WITH TARGET AS 
(
    SELECT * 
    FROM @CategoryItem
    WHERE CategoryId = 2
)
MERGE INTO TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

สิ่งนี้สร้างผลลัพธ์ที่ถูกต้อง (ไม่ซ้ำ) พร้อมกับแผนการที่เหมาะสมกว่า:

ผสานแผน 2

แผนอ่านเฉพาะแถวสำหรับหมวดหมู่ 2 จากตารางเป้าหมาย นี่อาจเป็นการพิจารณาประสิทธิภาพที่สำคัญหากตารางเป้าหมายมีขนาดใหญ่ แต่ทั้งหมดนั้นง่ายเกินไปที่จะทำให้เกิดข้อผิดพลาดนี้โดยใช้MERGEไวยากรณ์

บางครั้งการเขียนการMERGEดำเนินการ DML แยกเป็นเรื่องง่ายขึ้น วิธีนี้สามารถทำได้ดีกว่าMERGEความจริงซึ่งมักจะทำให้คนประหลาดใจ

DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS 
(
    SELECT 1 
    FROM @DataSource AS ds 
    WHERE 
        ds.ItemId = ci.ItemId
        AND ds.CategoryId = ci.CategoryId
);

INSERT @CategoryItem
SELECT 
    ds.CategoryId, 
    ds.ItemId
FROM @DataSource AS ds
WHERE
    ds.CategoryId = 2;

ฉันรู้ว่านี่เป็นคำถามที่เก่ามาก ... แต่โอกาสใด ๆ ที่คุณสามารถอธิบายอย่างละเอียดเกี่ยวกับ "การใช้ตารางนิพจน์ทั่วไปมีความเสี่ยงที่คล้ายคลึงกันในการเพิ่มภาคแสดงในประโยค ON แต่ด้วยเหตุผลที่แตกต่างกันเล็กน้อย" ฉันรู้ว่า BOL ยังมีคำเตือนที่คลุมเครือในทำนองเดียวกัน "วิธีนี้คล้ายกับการระบุเกณฑ์การค้นหาเพิ่มเติมในคำสั่งย่อย ON และอาจให้ผลลัพธ์ที่ไม่ถูกต้องเราขอแนะนำให้คุณหลีกเลี่ยงการใช้วิธีนี้ ... " วิธี CTE ปรากฏขึ้นเพื่อแก้ไขกรณีการใช้งานของฉัน แต่ฉันสงสัยว่ามีสถานการณ์ที่ฉันไม่ได้พิจารณา
เฮนรี่ลี
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.