วิธีหลีกเลี่ยงการใช้แบบสอบถามแบบผสานเมื่อทำซ้ำหลายข้อมูลโดยใช้พารามิเตอร์ xml


10

ฉันกำลังพยายามอัปเดตตารางด้วยอาร์เรย์ของค่าต่างๆ แต่ละรายการในอาร์เรย์มีข้อมูลที่ตรงกับแถวในตารางในฐานข้อมูล SQL Server หากแถวนั้นมีอยู่แล้วในตารางเราจะอัปเดตแถวนั้นด้วยข้อมูลในอาร์เรย์ที่กำหนด มิฉะนั้นเราจะแทรกแถวใหม่ในตาราง ฉันได้อธิบายโดยทั่วไปเพิ่มขึ้น

ตอนนี้ฉันกำลังพยายามทำให้สำเร็จในโพรซีเดอร์ที่เก็บไว้ซึ่งใช้พารามิเตอร์ XML เหตุผลที่ฉันใช้ XML และไม่ใช่พารามิเตอร์ที่มีค่าเป็นตารางเนื่องจากฉันจะต้องสร้างประเภทที่กำหนดเองใน SQL และเชื่อมโยงประเภทนี้กับกระบวนงานที่เก็บไว้ หากฉันเปลี่ยนบางสิ่งบางอย่างในขั้นตอนการจัดเก็บหรือ db schema ของฉันลงที่ถนนฉันจะต้องทำซ้ำทั้งขั้นตอนการจัดเก็บและประเภทที่กำหนดเอง ฉันต้องการหลีกเลี่ยงสถานการณ์นี้ นอกจากนี้ความเหนือกว่าที่ TVP มีเหนือ XML ไม่มีประโยชน์สำหรับสถานการณ์ของฉันเพราะขนาดอาร์เรย์ข้อมูลของฉันจะไม่เกิน 1,000 ซึ่งหมายความว่าฉันไม่สามารถใช้โซลูชันที่เสนอที่นี่: วิธีแทรกหลายระเบียนโดยใช้ XML ใน SQL Server 2008

นอกจากนี้ยังมีการอภิปรายที่คล้ายกันที่นี่ ( UPSERT - มีทางเลือกที่ดีกว่าสำหรับ MERGE หรือ @@ rowcount หรือไม่ ) แตกต่างจากที่ฉันขอเพราะฉันพยายามเพิ่มหลายแถวไปยังตาราง

ฉันหวังว่าฉันจะใช้ชุดคำสั่งต่อไปนี้เพื่อเพิ่มค่าจาก xml แต่นี่จะไม่ทำงาน วิธีการนี้ควรทำงานเมื่ออินพุตเป็นแถวเดียว

begin tran
   update table with (serializable) set select * from xml_param
   where key = @key

   if @@rowcount = 0
   begin
      insert table (key, ...) values (@key,..)
   end
commit tran

ทางเลือกถัดไปคือการใช้ IF EXISTS แบบละเอียดหรือแบบใดแบบหนึ่งของแบบฟอร์มต่อไปนี้ แต่ฉันปฏิเสธสิ่งนี้บนพื้นฐานของการมีประสิทธิภาพที่ดีที่สุดย่อย:

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

ตัวเลือกต่อไปคือการใช้คำสั่งผสานตามที่อธิบายไว้ที่นี่: http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html แต่แล้วผมอ่านเกี่ยวกับปัญหาเกี่ยวกับแบบสอบถามผสานนี่: http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ ด้วยเหตุนี้ฉันจึงพยายามหลีกเลี่ยงการรวม

ดังนั้นตอนนี้คำถามของฉันคือ: มีตัวเลือกอื่นหรือวิธีที่ดีกว่าเพื่อให้บรรลุหลาย upsert โดยใช้พารามิเตอร์ XML ใน SQL Server 2008 เก็บขั้นตอนหรือไม่

โปรดทราบว่าข้อมูลในพารามิเตอร์ XML อาจมีบางระเบียนที่ไม่ควรเป็น UPSERTed เนื่องจากมีอายุมากกว่าระเบียนปัจจุบัน มีModifiedDateฟิลด์ในทั้ง XML และตารางปลายทางที่ต้องเปรียบเทียบเพื่อพิจารณาว่าควรอัปเดตหรือทิ้งเรคคอร์ดหรือไม่


การพยายามหลีกเลี่ยงการเปลี่ยนแปลง proc ในอนาคตไม่ใช่เหตุผลที่ดีที่จะไม่ใช้ TVP หากข้อมูลที่ส่งผ่านไปในการเปลี่ยนแปลงคุณจะต้องทำการเปลี่ยนแปลงโค้ดด้วยวิธีใดวิธีหนึ่ง
Max Vernon

1
@MaxVernon ผมมีความคิดเหมือนกันในตอนแรกและเกือบจะได้แสดงความคิดเห็นที่คล้ายกันมากเพราะเห็นว่าอยู่คนเดียวไม่ได้เป็นเหตุผลที่จะหลีกเลี่ยงการ TVP แต่พวกเขาใช้ความพยายามเพิ่มขึ้นเล็กน้อยและด้วยข้อแม้ของ "ไม่เกิน 1,000 แถว" (โดยนัยบางครั้งหรืออาจจะบ่อยครั้ง) เป็นบิตของการโยน อย่างไรก็ตามฉันคิดว่าฉันควรมีคุณสมบัติตอบคำตอบของฉันว่า <1000 แถวต่อครั้งไม่ได้แตกต่างจาก XML มากเกินไปตราบใดที่มันไม่ได้เรียกว่า 10k ครั้งในแถว จากนั้นความแตกต่างของประสิทธิภาพเล็กน้อยจะเพิ่มขึ้นอย่างแน่นอน
โซโลมอน Rutzky

ปัญหาของMERGEเบอร์แทรนด์ชี้ให้เห็นว่าส่วนใหญ่เป็นคดีและความไร้ประสิทธิภาพไม่แสดง stoppers - MS คงไม่ปล่อยถ้ามันเป็นเขตทุ่นระเบิดที่แท้จริง คุณแน่ใจหรือไม่ว่าการโน้มน้าวใจที่คุณกำลังเผชิญเพื่อหลีกเลี่ยงMERGEไม่ได้สร้างข้อผิดพลาดที่อาจเกิดขึ้นได้มากกว่าการประหยัด?
Jon of All Trades

@JonofAllTrades MERGEเป็นธรรมสิ่งที่ผมเสนอไม่ได้จริงๆที่ซับซ้อนเมื่อเทียบกับ ขั้นตอน INSERT และ UPDATE ของ MERGE ยังคงดำเนินการแยกกัน ข้อแตกต่างที่สำคัญในแนวทางของฉันคือตัวแปรตารางที่เก็บ ID เร็กคอร์ดที่อัพเดตและเคียวรี DELETE ที่ใช้ตัวแปรตารางนั้นเพื่อลบเร็กคอร์ดเหล่านั้นออกจากตาราง temp ของข้อมูลขาเข้า และฉันคิดว่าแหล่งที่มาอาจถูกส่งโดยตรงจาก @ XMLparam.nodes () แทนที่จะทิ้งลงในตารางชั่วคราว แต่ถึงกระนั้นก็ยังไม่มีอะไรพิเศษอีกมากที่ไม่ต้องกังวลเกี่ยวกับการค้นพบตัวเองในกรณีขอบเหล่านั้น - )
โซโลมอน Rutzky

คำตอบ:


12

ไม่ว่าแหล่งที่มาจะเป็น XML หรือ TVP ไม่ได้สร้างความแตกต่างอย่างมาก การดำเนินงานโดยรวมเป็นหลัก:

  1. อัปเดตแถวที่มีอยู่
  2. INSERT แถวที่ขาดหายไป

คุณทำตามลำดับนั้นเพราะถ้าคุณใส่ก่อนแล้วแถวทั้งหมดจะได้รับการอัพเดทและคุณจะทำงานซ้ำสำหรับแถวใด ๆ ที่เพิ่งแทรกเข้าไป

นอกเหนือจากนั้นมีวิธีการที่แตกต่างกันในการทำสิ่งนี้และวิธีการต่าง ๆ เพื่อปรับแต่งประสิทธิภาพเพิ่มเติม

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

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

จากนั้นเราจะอัพเดทและจากนั้นใส่:

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

ตอนนี้เรามีการดำเนินการขั้นพื้นฐานแล้วเราสามารถทำสิ่งต่าง ๆ เพื่อเพิ่มประสิทธิภาพ:

  1. จับภาพ @@ ROWCOUNT ของการแทรกลงในตารางอุณหภูมิและเปรียบเทียบกับ @@ ROWCOUNT ของ UPDATE หากเหมือนกันเราก็สามารถข้าม INSERT

  2. จับค่า ID ที่อัพเดตผ่านส่วนคำสั่ง OUTPUT และลบค่าเหล่านั้นจากตารางอุณหภูมิ จากนั้น INSERT ไม่จำเป็นต้องใช้WHERE NOT EXISTS(...)

  3. หากมีแถวใด ๆ ในข้อมูลขาเข้าที่ไม่ควรซิงค์ (เช่นไม่มีการแทรกหรืออัปเดต) ดังนั้นบันทึกเหล่านั้นควรถูกลบออกก่อนที่จะทำการ UPDATE

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

ฉันใช้โมเดลนี้หลายครั้งกับ Imports / ETLs ที่มีมากกว่า 1,000 แถวหรือ 500 แถวในชุดทั้งหมด 20k - มากกว่าหนึ่งล้านแถว อย่างไรก็ตามฉันยังไม่ได้ทดสอบความแตกต่างด้านประสิทธิภาพระหว่าง DELETE ของแถวที่อัปเดตออกจากตารางชั่วคราวและเพิ่งอัปเดตฟิลด์ [IsUpdate]


โปรดทราบเกี่ยวกับการตัดสินใจใช้ XML บน TVP เนื่องจากมีจำนวนไม่เกิน 1,000 แถวที่จะนำเข้าในแต่ละครั้ง (กล่าวถึงในคำถาม):

หากสิ่งนี้ถูกเรียกสองสามครั้งที่นี่และที่นั่นอาจเป็นไปได้ว่าประสิทธิภาพเล็กน้อยใน TVP อาจไม่คุ้มค่าการบำรุงรักษาเพิ่มเติม (จำเป็นต้องลดลง proc ก่อนที่จะเปลี่ยนประเภทตารางที่กำหนดโดยผู้ใช้การเปลี่ยนแปลงรหัสแอป ฯลฯ ) . แต่ถ้าคุณนำเข้า 4 ล้านแถวส่ง 1,000 ครั้งต่อครั้งนั่นคือการประมวลผล 4000 ครั้ง (และ 4 ล้านแถวของ XML เพื่อแยกวิเคราะห์ไม่ว่าจะเสียอย่างไร) และแม้แต่ประสิทธิภาพที่ต่างกันเล็กน้อยเมื่อดำเนินการเพียงไม่กี่ครั้งเท่านั้น เพิ่มความแตกต่างที่เห็นได้ชัดเจน

ที่ถูกกล่าวว่าวิธีการที่ฉันได้อธิบายไม่เปลี่ยนแปลงนอกจากการแทนที่ SELECT FROM @XmlInputParam เป็น SELECT FROM @TVP เนื่องจาก TVP เป็นแบบอ่านอย่างเดียวคุณจะไม่สามารถลบออกได้ ฉันเดาว่าคุณสามารถเพิ่มคำWHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)สั่ง SELECT สุดท้าย (เชื่อมโยงกับ INSERT) แทนการใช้WHERE IsUpdate = 0วิ หากคุณต้องใช้@UpdateIDsตัวแปรตารางในลักษณะนี้คุณสามารถหลีกเลี่ยงได้โดยไม่ทิ้งแถวที่เข้ามาลงในตารางชั่วคราว

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