MERGE กับ OUTPUT ดีกว่า INSERT และ SELECT แบบมีเงื่อนไขหรือไม่?


12

เรามักจะพบกับสถานการณ์ "หากไม่มีอยู่แทรก" บล็อกของ Dan Guzmanมีการตรวจสอบที่ยอดเยี่ยมเกี่ยวกับวิธีทำให้เธรดกระบวนการนี้ปลอดภัย

SEQUENCEผมมีตารางพื้นฐานที่เพียงแคตตาล็อกสตริงจำนวนเต็มจากที่ ในกระบวนงานที่เก็บไว้ฉันต้องได้รับคีย์จำนวนเต็มสำหรับค่าหากมีอยู่หรือINSERTจากนั้นรับค่าผลลัพธ์ มีข้อ จำกัด ที่ไม่ซ้ำกันในdbo.NameLookup.ItemNameคอลัมน์ดังนั้นความถูกต้องของข้อมูลจึงไม่มีความเสี่ยง แต่ฉันไม่ต้องการพบข้อยกเว้น

มันไม่ใช่IDENTITYดังนั้นฉันไม่สามารถรับSCOPE_IDENTITYและคุณค่าอาจเป็นNULLในบางกรณี

ในสถานการณ์ของฉันฉันต้องจัดการกับINSERTความปลอดภัยบนโต๊ะเท่านั้นดังนั้นฉันจึงพยายามตัดสินใจว่าควรใช้MERGEแบบนี้ดีกว่า:

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

ฉันสามารถทำสิ่งนี้โดยใช้MERGEเพียงเงื่อนไขINSERTตามด้วยSELECT ฉันคิดว่าวิธีที่สองนี้ชัดเจนสำหรับผู้อ่าน แต่ฉันไม่เชื่อว่ามันเป็นการปฏิบัติที่ "ดีกว่า"

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

หรืออาจมีวิธีอื่นที่ดีกว่าที่ฉันไม่ได้พิจารณา

ฉันค้นหาและอ้างอิงคำถามอื่น ๆ อันนี้: /programming/5288283/sql-server-insert-if-not-exists-best-practiceเป็นสิ่งที่เหมาะสมที่สุดที่ฉันสามารถหาได้ แต่ดูเหมือนจะไม่เหมาะกับกรณีการใช้งานของฉัน คำถามอื่น ๆ เกี่ยวกับIF NOT EXISTS() THENวิธีการที่ฉันไม่คิดว่าเป็นที่ยอมรับ


คุณลองทดลองกับตารางที่มีขนาดใหญ่กว่าบัฟเฟอร์ของคุณหรือไม่ฉันเคยมีประสบการณ์ที่ประสิทธิภาพการผสานลดลงเมื่อตารางมีขนาดที่แน่นอน
pacreely

คำตอบ:


8

เนื่องจากคุณกำลังใช้ Sequence คุณสามารถใช้ฟังก์ชันNEXT VALUE FORเดียวกันกับที่คุณมีอยู่แล้วในข้อ จำกัด เริ่มต้นบนIdฟิลด์คีย์หลัก - เพื่อสร้างIdค่าใหม่ล่วงหน้า การสร้างคุณค่าก่อนหมายความว่าคุณไม่จำเป็นต้องกังวลเกี่ยวกับการไม่มีSCOPE_IDENTITYซึ่งหมายความว่าคุณไม่จำเป็นต้องใช้OUTPUTประโยคหรือทำเพิ่มเติมSELECTเพื่อรับค่าใหม่ คุณจะมีค่าก่อนที่จะทำINSERTและคุณไม่จำเป็นต้องยุ่งกับSET IDENTITY INSERT ON / OFF:-)

เพื่อดูแลส่วนหนึ่งของสถานการณ์โดยรวม INSERTอีกส่วนหนึ่งคือการจัดการกับปัญหาการทำงานพร้อมกันของทั้งสองกระบวนการในเวลาเดียวกันที่แน่นอนไม่ได้หาแถวที่มีอยู่สำหรับสายเดียวกันที่แน่นอนและการดำเนินการกับ ข้อกังวลคือเกี่ยวกับการหลีกเลี่ยงการละเมิดข้อ จำกัด ที่จะเกิดขึ้น

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

อีกวิธีหนึ่งในการจัดการกับการชนคือการยอมรับว่าบางครั้งพวกเขาจะเกิดขึ้นและจัดการกับพวกเขาแทนที่จะพยายามหลีกเลี่ยงพวกเขา ใช้TRY...CATCHสร้างคุณสามารถได้อย่างมีประสิทธิภาพกับดักข้อผิดพลาด (ในกรณีนี้ "ละเมิดข้อ จำกัด ที่ไม่ซ้ำกัน" ข่าวสารเกี่ยวกับ 2601) และอีกครั้งดำเนินการSELECTที่จะได้รับIdความคุ้มค่าเนื่องจากเรารู้ว่ามันตอนนี้มีอยู่เนื่องจากอยู่ในCATCHบล็อกที่มีเฉพาะที่ ความผิดพลาด ข้อผิดพลาดอื่น ๆ สามารถจัดการได้ตามปกติRAISERROR/ RETURNหรือTHROWลักษณะ

การตั้งค่าการทดสอบ: ลำดับตารางและดัชนีที่ไม่ซ้ำกัน

USE [tempdb];

CREATE SEQUENCE dbo.MagicNumber
  AS INT
  START WITH 1
  INCREMENT BY 1;

CREATE TABLE dbo.NameLookup
(
  [Id] INT NOT NULL
         CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
        CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
  [ItemName] NVARCHAR(50) NOT NULL         
);

CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
  ON dbo.NameLookup ([ItemName]);
GO

ทดสอบการตั้งค่า: กระบวนงานที่เก็บไว้

CREATE PROCEDURE dbo.GetOrInsertName
(
  @SomeName NVARCHAR(50),
  @ID INT OUTPUT,
  @TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;

BEGIN TRY
  SELECT @ID = nl.[Id]
  FROM   dbo.NameLookup nl
  WHERE  nl.[ItemName] = @SomeName
  AND    @TestRaceCondition = 0;

  IF (@ID IS NULL)
  BEGIN
    SET @ID = NEXT VALUE FOR dbo.MagicNumber;

    INSERT INTO dbo.NameLookup ([Id], [ItemName])
    VALUES (@ID, @SomeName);
  END;
END TRY
BEGIN CATCH
  IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
  BEGIN
    SELECT @ID = nl.[Id]
    FROM   dbo.NameLookup nl
    WHERE  nl.[ItemName] = @SomeName;
  END;
  ELSE
  BEGIN
    ;THROW; -- SQL Server 2012 or newer
    /*
    DECLARE @ErrorNumber INT = ERROR_NUMBER(),
            @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();

    RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
    RETURN;
    */
  END;

END CATCH;
GO

บททดสอบ

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT,
  @TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO

คำถามจาก OP

นี่คือเหตุผลที่ดีกว่าMERGE? ฉันจะไม่ได้รับฟังก์ชั่นเดียวกันโดยไม่ต้องTRYใช้WHERE NOT EXISTSประโยคหรือไม่?

MERGEมี "ปัญหา" ต่างๆ (การอ้างอิงหลายรายการเชื่อมโยงอยู่ในคำตอบของ @ SqlZim ดังนั้นไม่จำเป็นต้องทำซ้ำข้อมูลนั้นที่นี่) และไม่มีการล็อคเพิ่มเติมในวิธีนี้ (การช่วงชิงน้อยกว่า) ดังนั้นจึงควรดีกว่าในการใช้งานพร้อมกัน ในวิธีการนี้คุณจะไม่ได้รับการฝ่าฝืนข้อ จำกัด ที่ไม่เหมือนใครทั้งหมดโดยไม่มีข้อ จำกัดHOLDLOCKฯลฯ รับประกันได้เลยว่าทำงานได้จริง

เหตุผลเบื้องหลังแนวทางนี้คือ:

  1. หากคุณมีการประมวลผลขั้นตอนนี้มากพอจนคุณต้องกังวลเกี่ยวกับการชนคุณไม่ต้องการ:
    1. ทำตามขั้นตอนมากกว่าที่จำเป็น
    2. พักล็อคทรัพยากรใด ๆ นานกว่าที่จำเป็น
  2. เนื่องจากการชนสามารถเกิดขึ้นได้กับรายการใหม่เท่านั้น (รายการใหม่ที่ส่งในเวลาเดียวกันแน่นอน ) ความถี่ของการตกลงไปในCATCHบล็อกในสถานที่แรกจะค่อนข้างต่ำ มันเหมาะสมกว่าที่จะปรับโค้ดให้เหมาะสมซึ่งจะรัน 99% ของเวลาแทนที่จะเป็นโค้ดที่จะรัน 1% ของเวลา (นอกเสียจากว่าจะไม่มีค่าใช้จ่ายในการปรับให้เหมาะสมทั้งสอง แต่ไม่ใช่กรณีที่นี่)

ความคิดเห็นจากคำตอบของ @ SqlZim (เน้นการเพิ่ม)

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

ฉันจะเห็นด้วยกับประโยคแรกนี้หากมีการแก้ไขให้รัฐ "และ _ เมื่อระมัดระวัง" เพียงเพราะสิ่งที่เป็นไปได้ทางเทคนิคไม่ได้หมายความว่าสถานการณ์ (เช่นการใช้งานที่ตั้งใจ) จะได้รับประโยชน์จากมัน

ปัญหาที่ฉันเห็นด้วยวิธีนี้คือมันล็อคมากกว่าสิ่งที่ถูกแนะนำ มันเป็นสิ่งสำคัญที่จะอ่านเอกสารที่ยกมาอีกครั้งใน "serializable" โดยเฉพาะต่อไปนี้ (เน้นเพิ่ม):

  • ธุรกรรมอื่น ๆ ไม่สามารถแทรกแถวใหม่ด้วยค่าคีย์ที่จะอยู่ในช่วงของคีย์ที่อ่านโดยคำสั่งใด ๆ ในธุรกรรมปัจจุบันจนกว่าธุรกรรมปัจจุบันจะเสร็จสมบูรณ์

ตอนนี้นี่คือความคิดเห็นในรหัสตัวอย่าง:

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

คำผ่าตัดมี "ช่วง" การล็อคที่ใช้ไม่ได้เป็นเพียงแค่ค่า@vNameแต่ยังมีช่วงเริ่มต้นที่แม่นยำยิ่งขึ้นตำแหน่งที่ควรค่าใหม่นี้ (เช่นระหว่างค่าคีย์ที่มีอยู่ในทั้งสองด้านของค่าใหม่ที่เหมาะสม) แต่ไม่ใช่ค่าตัวเอง ความหมายกระบวนการอื่น ๆ จะถูกบล็อกไม่ให้แทรกค่าใหม่ขึ้นอยู่กับค่าที่ค้นหาอยู่ในปัจจุบัน หากการค้นหากำลังทำที่ด้านบนของช่วงจากนั้นให้แทรกสิ่งที่อาจครอบครองตำแหน่งเดียวกันนั้นจะถูกบล็อก ตัวอย่างเช่นหากมีค่า "a", "b" และ "d" อยู่หากกระบวนการหนึ่งกำลังเลือก SELECT บน "f" ดังนั้นจะไม่สามารถแทรกค่า "g" หรือแม้แต่ "e" ( ตั้งแต่หนึ่งในนั้นจะมาทันทีหลังจาก "d") แต่การแทรกค่า "c" จะเป็นไปได้เนื่องจากจะไม่อยู่ในช่วง "สงวน"

ตัวอย่างต่อไปนี้ควรแสดงให้เห็นถึงพฤติกรรมนี้:

(ในแท็บแบบสอบถาม (เช่นเซสชัน) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');

BEGIN TRAN;

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'test8';

--ROLLBACK;

(ในแท็บแบบสอบถาม (เช่นเซสชัน) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

ในทำนองเดียวกันหากมีค่า "C" อยู่และมีการเลือกค่า "A" (และถูกล็อกไว้) คุณสามารถแทรกค่าเป็น "D" แต่ไม่ใช่ค่า "B":

(ในแท็บแบบสอบถาม (เช่นเซสชัน) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');

BEGIN TRAN

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'testA';

--ROLLBACK;

(ในแท็บแบบสอบถาม (เช่นเซสชัน) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

เพื่อความเป็นธรรมในแนวทางที่แนะนำของฉันเมื่อมีข้อยกเว้นจะมี 4 รายการในบันทึกการทำธุรกรรมที่จะไม่เกิดขึ้นในแนวทาง "การทำธุรกรรมต่อเนื่อง" นี้ แต่อย่างที่ฉันได้กล่าวไว้ข้างต้นหากมีข้อยกเว้นเกิดขึ้น 1% (หรือ 5%) ของเวลานั่นส่งผลกระทบน้อยกว่ากรณีที่เป็นไปได้มากกว่าของ SELECT เริ่มต้นที่บล็อกการดำเนินการ INSERT ชั่วคราว

อีกอย่างหนึ่งถึงแม้ว่ารองลงมาปัญหาด้วยวิธีการ "การทำธุรกรรมแบบอนุกรม + ข้อออก OUTPUT" วิธีนี้ก็คือOUTPUTข้อ (ในการใช้งานในปัจจุบัน) ส่งข้อมูลกลับเป็นชุดผลลัพธ์ ชุดผลลัพธ์ต้องการค่าใช้จ่ายเพิ่มเติม (อาจเป็นทั้งสองด้าน: ใน SQL Server เพื่อจัดการเคอร์เซอร์ภายในและในเลเยอร์แอปเพื่อจัดการวัตถุ DataReader) กว่าOUTPUTพารามิเตอร์แบบง่าย เนื่องจากเรากำลังเผชิญกับค่าสเกลาร์เดียวและการสันนิษฐานนั้นเป็นความถี่สูงของการประหารชีวิตซึ่งค่าใช้จ่ายเพิ่มเติมของชุดผลลัพธ์อาจเพิ่มขึ้น

ในขณะที่OUTPUTข้อสามารถใช้ในลักษณะที่จะกลับOUTPUTพารามิเตอร์ที่จะต้องมีขั้นตอนเพิ่มเติมในการสร้างตารางชั่วคราวหรือตัวแปรตารางแล้วเพื่อเลือกค่าออกจากตัวแปรตาราง / ตาราง temp นั้นลงในOUTPUTพารามิเตอร์

ชี้แจงเพิ่มเติม: การตอบสนองต่อการตอบสนองของ @ SqlZim (คำตอบที่อัปเดต) ต่อการตอบสนองของฉันต่อการตอบสนองของ @ SqlZim (ในคำตอบเดิม) ต่อแถลงการณ์ของฉันเกี่ยวกับการเกิดพร้อมกันและประสิทธิภาพ ;-)

ขออภัยถ้าส่วนนี้ยาวนิดหน่อย แต่ ณ จุดนี้เราเพิ่งไปถึงความแตกต่างของสองวิธี

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

ใช่ฉันจะยอมรับว่าฉันลำเอียงแม้ว่าจะยุติธรรม:

  1. เป็นไปไม่ได้ที่มนุษย์จะไม่ลำเอียงอย่างน้อยก็ระดับเล็กและฉันพยายามทำให้เป็นอย่างน้อย
  2. ตัวอย่างที่ให้มานั้นง่าย แต่นั่นก็เพื่อจุดประสงค์ในการอธิบายพฤติกรรมโดยไม่ทำให้มันซับซ้อน การใช้คลื่นความถี่ที่มากเกินไปนั้นไม่ได้มีจุดมุ่งหมายถึงแม้ว่าฉันจะเข้าใจว่าฉันไม่ได้ระบุไว้เป็นอย่างอื่นอย่างชัดเจนและสามารถอ่านได้ว่าเป็นการบ่งบอกถึงปัญหาที่ใหญ่กว่าที่มีอยู่จริง ฉันจะพยายามชี้แจงว่าด้านล่าง
  3. ฉันได้รวมตัวอย่างของการล็อคช่วงระหว่างสองคีย์ที่มีอยู่แล้ว (ชุดที่สองของ "Query tab 1" และ "Query tab 2" block)
  4. ฉันพบ (และอาสาสมัคร) "ค่าใช้จ่ายแอบแฝง" ของวิธีการของฉันนั่นคือการบันทึก Tran Log สี่ครั้งในแต่ละครั้งที่INSERTล้มเหลวเนื่องจากการละเมิดข้อ จำกัด ที่ไม่ซ้ำกัน ฉันไม่ได้เห็นที่กล่าวถึงในคำตอบ / โพสต์อื่น ๆ

เกี่ยวกับวิธีการ "JFDI" ของ @ gbn, โพสต์ของ "Michael Pragmatism For The Win" ของ Michael J. Swart และความคิดเห็นของ Aaron Bertrand ในโพสต์ของ Michael (เกี่ยวกับการทดสอบของเขาแสดงให้เห็นว่าสถานการณ์มีประสิทธิภาพลดลง) และความคิดเห็นของคุณ การปรับตัวของสจ๊วตของขั้นตอนการลองจับ JFDI ของ @ gbn "ระบุ:

หากคุณกำลังแทรกค่าใหม่บ่อยกว่าการเลือกค่าที่มีอยู่สิ่งนี้อาจมีประสิทธิภาพมากกว่าเวอร์ชั่นของ @ srutzky มิฉะนั้นฉันจะชอบเวอร์ชันของ @ srutzky มากกว่าอันนี้

ด้วยความเคารพต่อการอภิปราย gbn / Michael / Aaron ที่เกี่ยวข้องกับวิธีการ "JFDI" มันจะไม่ถูกต้องที่จะเทียบคำแนะนำของฉันกับแนวทาง "JFDI" ของ gbn เนื่องจากลักษณะของการดำเนินการ "รับหรือแทรก" มีความจำเป็นอย่างชัดเจนที่จะต้องทำSELECTเพื่อรับIDค่าสำหรับระเบียนที่มีอยู่ SELECT นี้ทำหน้าที่เป็นการIF EXISTSตรวจสอบซึ่งทำให้วิธีการนี้มากขึ้นดังนั้นจึงถือเอาการเปลี่ยนแปลง "CheckTryCatch" ของการทดสอบของแอรอน โค้ดที่เขียนขึ้นใหม่ของไมเคิล (และการปรับตัวครั้งสุดท้ายของคุณเกี่ยวกับการปรับตัวของไมเคิล) ยังรวมถึงการWHERE NOT EXISTSตรวจสอบสิ่งเดียวกันก่อน ดังนั้นข้อเสนอแนะของฉัน (พร้อมด้วยรหัสสุดท้ายของไมเคิลและการดัดแปลงรหัสสุดท้ายของคุณ) จะไม่กระทบกับCATCHบล็อกทั้งหมดที่มักเกิดขึ้นจริง อาจเป็นสถานการณ์ที่มีสองเซสชันเท่านั้นItemNameINSERT...SELECTในช่วงเวลาเดียวกันนั้นทั้งสองเซสชันจะได้รับ "ความจริง" WHERE NOT EXISTSในช่วงเวลาเดียวกันและทำให้ทั้งคู่พยายามทำINSERTในช่วงเวลาเดียวกัน ว่าสถานการณ์ที่เฉพาะเจาะจงมากที่เกิดขึ้นมากน้อยบ่อยกว่าการเลือกตัวเลือกที่มีอยู่ItemNameหรือการใส่ใหม่ItemNameเมื่อไม่มีกระบวนการอื่น ๆ ที่เป็นความพยายามที่จะทำเช่นนั้นในขณะเดียวกันแน่นอน

กับทุกคนในใจ: ทำไมฉันถึงชอบแนวทางของฉัน?

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

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

หากเซสชัน 55 พยายามแทรกค่าคีย์ของ:

  • Aดังนั้นช่วง # 1 (จาก^ถึงC) ถูกล็อค: เซสชัน 56 ไม่สามารถแทรกค่าBแม้ว่าจะไม่ซ้ำกันและถูกต้อง (ยัง) แต่เซสชั่น 56 สามารถแทรกค่านิยมของD, และGM
  • Dจากนั้นช่วง # 2 (จากCถึงF) ถูกล็อค: เซสชัน 56 ไม่สามารถแทรกค่าE(ยัง) ได้ แต่เซสชั่น 56 สามารถแทรกค่านิยมของA, และGM
  • Mจากนั้นช่วง # 4 (จากJถึง$) ถูกล็อค: เซสชัน 56 ไม่สามารถแทรกค่าX(ยัง) ได้ แต่เซสชั่น 56 สามารถแทรกค่านิยมของA, และDG

เมื่อมีการเพิ่มค่าคีย์มากขึ้นช่วงระหว่างค่าคีย์จะแคบลงดังนั้นจึงลดความน่าจะเป็น / ความถี่ของค่าหลายค่าที่ถูกแทรกในเวลาเดียวกันเพื่อต่อสู้กับช่วงเดียวกัน เป็นที่ยอมรับว่านี่ไม่ใช่ปัญหาใหญ่และโชคดีที่มันดูเหมือนจะเป็นปัญหาที่ลดลงตามกาลเวลา

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

ถัดไปให้เราพิจารณาสองสถานการณ์และวิธีที่แต่ละวิธีจัดการกับสถานการณ์:

  1. คำขอทั้งหมดเป็นค่าคีย์ที่ไม่ซ้ำกัน:

    ในกรณีนี้CATCHบล็อกในคำแนะนำของฉันจะไม่ถูกป้อนดังนั้นจึงไม่มี "ปัญหา" (เช่นรายการบันทึก 4 tran และเวลาที่ใช้ในการทำเช่นนั้น) แต่ในแนวทาง "ต่อเนื่องได้" แม้จะมีเม็ดมีดทั้งหมดที่ไม่ซ้ำกัน แต่ก็มีความเป็นไปได้ที่จะบล็อกเม็ดมีดอื่น ๆ ในช่วงเดียวกันเสมอ (แม้ว่าจะไม่ได้ยาวมากก็ตาม)

  2. ความถี่สูงของการร้องขอค่าคีย์เดียวกันในเวลาเดียวกัน:

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

    (มีปัญหากับวิธี "อัปเดต" รุ่นก่อนหน้าที่อนุญาตให้ประสบปัญหาการหยุดชะงักได้updlockคำแนะนำเพิ่มไปยังที่อยู่นี้และไม่ได้รับการหยุดชะงักอีกต่อไป)แต่ในแนวทาง "ต่อเนื่องได้" (แม้จะเป็นเวอร์ชั่นที่ได้รับการปรับปรุงและปรับให้เหมาะสมที่สุด) การดำเนินการจะหยุดชะงัก ทำไม? เนื่องจากserializableพฤติกรรมป้องกันINSERTการดำเนินการในช่วงที่อ่านและถูกล็อก มันไม่ได้ป้องกันSELECTการดำเนินการในช่วงนั้น

    serializableวิธีการในกรณีนี้ก็ดูเหมือนจะไม่มีค่าใช้จ่ายเพิ่มเติมและอาจดำเนินการเล็กน้อยดีกว่าสิ่งที่ผมแนะนำ

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


7

อัปเดตคำตอบ


ตอบสนองต่อ @srutzky

อีกอย่างหนึ่งถึงแม้ว่ารองลงมาปัญหาด้วยวิธีการ "การทำธุรกรรมแบบอนุกรม + มาตรา OUTPUT" นี้ก็คือว่ามาตรา OUTPUT (ในการใช้งานในปัจจุบัน) ส่งข้อมูลกลับเป็นชุดผลลัพธ์ ชุดผลลัพธ์ต้องการค่าใช้จ่ายเพิ่มเติม (อาจเป็นทั้งสองด้าน: ใน SQL Server เพื่อจัดการเคอร์เซอร์ภายในและในเลเยอร์แอปเพื่อจัดการวัตถุ DataReader) กว่าพารามิเตอร์ OUTPUT อย่างง่าย เนื่องจากเรากำลังเผชิญกับค่าสเกลาร์เดียวและการสันนิษฐานนั้นเป็นความถี่สูงของการประหารชีวิตซึ่งค่าใช้จ่ายเพิ่มเติมของชุดผลลัพธ์อาจเพิ่มขึ้น

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

นี่คือขั้นตอนการแก้ไขโดยใช้พารามิเตอร์การแสดงผล, การเพิ่มประสิทธิภาพที่เพิ่มขึ้นพร้อมกับnext value forที่@srutzky อธิบายในคำตอบของเขา :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

บันทึกการปรับปรุง : รวมถึงupdlockการเลือกจะคว้าล็อคที่เหมาะสมในสถานการณ์นี้ ขอขอบคุณที่ @srutzky ที่ชี้ให้เห็นว่าเรื่องนี้อาจทำให้เกิดการหยุดชะงักเมื่อมีเพียงใช้ในserializableselect

หมายเหตุ: นี่อาจไม่เป็นกรณี แต่ถ้าเป็นไปได้ขั้นตอนจะถูกเรียกด้วยค่าสำหรับ@vValueIdรวมถึงset @vValueId = null;หลังจากset xact_abort on;มิฉะนั้นจะสามารถลบออกได้


เกี่ยวกับตัวอย่างของพฤติกรรมการล็อคช่วงคีย์ของ @ srutzky:

@srutzky ใช้เพียงหนึ่งค่าในตารางของเขาและล็อคกุญแจ "ถัดไป" / "อินฟินิตี้" สำหรับการทดสอบของเขาเพื่อแสดงการล็อคช่วงคีย์ ในขณะที่การทดสอบของเขาแสดงให้เห็นถึงสิ่งที่เกิดขึ้นในสถานการณ์เหล่านั้นฉันเชื่อว่าวิธีการนำเสนอข้อมูลอาจนำไปสู่สมมติฐานที่ผิดพลาดเกี่ยวกับปริมาณการล็อคที่คาดว่าจะเกิดขึ้นเมื่อใช้serializableในสถานการณ์ดังที่แสดงในคำถามเดิม

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


หลังจากการวิจัยมากขึ้นผมพบบทความบล็อกที่เกี่ยวข้องโดยเฉพาะอย่างยิ่งจากปี 2011 โดยไมเคิลเจผิวคล้ำ: mythbusting: พร้อมกันปรับปรุงแทรกโซลูชั่น ในนั้นเขาทดสอบหลายวิธีเพื่อความถูกต้องและการเกิดพร้อมกัน วิธีที่ 4: เพิ่ม Isolation + Fine Tuning Locksจะขึ้นอยู่กับการแทรกหรือการอัพเดตรูปแบบของ Sam Saffron สำหรับ SQL Serverและเป็นวิธีเดียวในการทดสอบดั้งเดิมเพื่อตอบสนองความคาดหวังของเขา (เข้าร่วมภายหลังmerge with (holdlock))

ในเดือนกุมภาพันธ์ปี 2559 Michael J. Swart โพสต์ปฏิบัตินิยมเพื่อการชนะอย่างน่าเกลียด ในโพสต์นั้นเขาครอบคลุมการปรับแต่งเพิ่มเติมบางอย่างที่เขาทำกับโพรซีเดอร์ Saffron ของเขาเพื่อลดการล็อค (ซึ่งฉันรวมอยู่ในขั้นตอนด้านบน)

หลังจากทำการเปลี่ยนแปลงเหล่านั้นแล้วไมเคิลไม่มีความสุขที่กระบวนการของเขาเริ่มดูซับซ้อนและปรึกษากับเพื่อนร่วมงานชื่อคริส คริสอ่านทั้งหมดของการโพสต์ Mythbusters เดิมและอ่านความคิดเห็นทั้งหมดและถามเกี่ยวกับ@ GBN ของ TRY รูปแบบที่จับ รูปแบบนี้คล้ายกับคำตอบของ @ srutzky และเป็นคำตอบที่ Michael ใช้ในกรณีนั้น

Michael J Swart:

เมื่อวานฉันเปลี่ยนใจเกี่ยวกับวิธีที่ดีที่สุดในการทำพร้อมกัน ฉันอธิบายวิธีการต่าง ๆ ใน Mythbusting: การแก้ไข / ปรับปรุงพร้อมกัน วิธีการที่ฉันชอบคือการเพิ่มระดับการแยกและล็อคการปรับแบบละเอียด

อย่างน้อยนั่นก็เป็นความชอบของฉัน ฉันเพิ่งเปลี่ยนวิธีการของฉันที่จะใช้วิธีการที่แนะนำใน gbn ความคิดเห็น เขาอธิบายถึงวิธีการของเขาในรูปแบบ“ ลองจับรูปแบบ JFDI” ปกติฉันจะหลีกเลี่ยงการแก้ปัญหาเช่นนั้น มีกฎง่ายๆที่บอกว่านักพัฒนาไม่ควรพึ่งพาการจับข้อผิดพลาดหรือข้อยกเว้นสำหรับการไหล แต่เมื่อวานนี้ฉันฝ่าฝืนกฎของหัวแม่มือ

อย่างไรก็ตามฉันชอบคำอธิบายของ gbn สำหรับรูปแบบ“ JFDI” มันทำให้ฉันนึกถึงวิดีโอสร้างแรงบันดาลใจของ Shia Labeouf


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

บางทีในอนาคตฉันก็จะได้ข้อสรุปแบบเดียวกับที่ Michael J. Swart ทำ แต่ฉันก็ยังไม่ได้ทำ


มันไม่ใช่ความชอบของฉัน แต่นี่คือสิ่งที่การปรับตัวของไมเคิลเจสจวร์ตการปรับกระบวนการ@ gbn's Try Catch JFDI ของฉันจะมีลักษณะดังนี้:

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

หากคุณกำลังใส่ค่าใหม่มักจะมากกว่าการเลือกค่าที่มีอยู่นี้อาจจะ performant มากกว่า@ รุ่น มิฉะนั้นฉันจะชอบเวอร์ชันของ @ srutzkyมากกว่าอันนี้

ความเห็นของ Aaron Bertrand เกี่ยวกับลิงก์โพสต์ของ Michael J Swart ไปยังการทดสอบที่เกี่ยวข้องที่เขาทำและนำไปสู่การแลกเปลี่ยนนี้ ข้อความที่ตัดตอนมาจากส่วนความคิดเห็นเกี่ยวกับการปฏิบัตินิยมอย่างน่าเกลียดสำหรับผู้ชนะ :

แม้ว่าบางครั้ง JFDI จะนำไปสู่ประสิทธิภาพโดยรวมที่แย่ลงโดยขึ้นอยู่กับว่าการโทร% ใดล้มเหลว การยกข้อยกเว้นมีค่าใช้จ่ายมากมาย ฉันแสดงสิ่งนี้ในสองโพสต์:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

ความคิดเห็นโดย Aaron Bertrand - 11 กุมภาพันธ์ 2016 @ 11:49 น

และคำตอบของ:

คุณพูดถูกแล้วและเราก็ทำการทดสอบ

ปรากฎว่าในกรณีของเราเปอร์เซ็นต์การโทรที่ล้มเหลวคือ 0 (เมื่อปัดเศษเป็นเปอร์เซ็นต์ใกล้เคียงที่สุด)

ฉันคิดว่าคุณแสดงให้เห็นถึงจุดที่มากที่สุดเท่าที่เป็นไปได้ประเมินสิ่งต่าง ๆ เป็นรายกรณีไปตามกฎของหัวแม่มือ

นอกจากนี้ยังเป็นเหตุผลว่าทำไมเราจึงเพิ่มส่วนคำสั่งที่ไม่เคร่งครัดซึ่งไม่จำเป็นต้องมีอยู่

ความคิดเห็นโดย Michael J. Swart - 11 กุมภาพันธ์ 2016 @ 11:57 am


ลิงค์ใหม่:


คำตอบเดิม


ฉันยังคงชอบวิธีการเพิ่มขึ้นของSam Saffronเทียบกับการใช้mergeโดยเฉพาะเมื่อต้องรับมือกับแถวเดียว

ฉันจะปรับวิธี upsert นั้นกับสถานการณ์เช่นนี้:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

ฉันจะสอดคล้องกับการตั้งชื่อของคุณและเช่นserializableเดียวกับการholdlockเลือกและสอดคล้องในการใช้งาน ฉันมักจะใช้เพราะมันเป็นชื่อเดียวกับที่ใช้เป็นเมื่อระบุserializableset transaction isolation level serializable

โดยการใช้serializableหรือholdlockล็อคช่วงจะขึ้นอยู่กับค่า@vNameที่ทำให้การดำเนินงานอื่น ๆ รอถ้าพวกเขาเลือกหรือแทรกค่าลงในdbo.NameLookupที่รวมถึงค่าในwhereข้อ

เพื่อให้การล็อคช่วงทำงานอย่างถูกต้องจะต้องมีดัชนีในItemNameคอลัมน์ที่ใช้เมื่อใช้mergeเช่นกัน


นี่คือสิ่งที่ขั้นตอนจะมีลักษณะส่วนใหญ่ต่อไปนี้whitepapers Erland Sommarskog สำหรับจัดการข้อผิดพลาดthrowโดยใช้ หากthrowไม่ใช่วิธีที่คุณแจ้งข้อผิดพลาดให้เปลี่ยนเพื่อให้สอดคล้องกับขั้นตอนที่เหลือของคุณ:

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

เพื่อสรุปสิ่งที่เกิดขึ้นในขั้นตอนข้างต้น: set nocount on; set xact_abort on;เช่นคุณมักจะทำแล้วถ้าตัวแปรอินพุตของเราis nullหรือว่างเปล่าselect id = cast(null as int)เป็นผล หากมันไม่ว่างเปล่าให้Idหาค่าตัวแปรของเราในขณะที่เก็บค่านั้นไว้ในกรณีที่ไม่มี หากIdมีอยู่ให้ส่งออกไป Idถ้ามันไม่ได้มีแทรกและส่งออกที่ใหม่

ในขณะเดียวกันการเรียกใช้โพรซีเดอร์นี้อื่น ๆ ที่พยายามค้นหา Id สำหรับค่าเดียวกันจะรอจนกว่าการทำธุรกรรมครั้งแรกจะเสร็จสิ้นจากนั้นเลือก & คืนค่า การเรียกใช้โพรซีเดอร์นี้หรือคำสั่งอื่น ๆ ที่ค้นหาค่าอื่น ๆ จะดำเนินต่อไปเนื่องจากสิ่งนี้ไม่ได้อยู่ในเส้นทาง

ในขณะที่ฉันเห็นด้วยกับ @srutzky ว่าคุณสามารถจัดการการชนและกลืนข้อยกเว้นสำหรับปัญหาประเภทนี้ได้ แต่ส่วนตัวแล้วฉันชอบที่จะลองและปรับแต่งโซลูชันเพื่อหลีกเลี่ยงการทำเช่นนั้นเมื่อเป็นไปได้ ในกรณีนี้ฉันไม่รู้สึกว่าการใช้ระบบล็อคserializableเป็นวิธีที่ถนัดและฉันมั่นใจว่ามันจะรองรับการทำงานพร้อมกันได้เป็นอย่างดี

อ้างจากเอกสารเซิร์ฟเวอร์ sql ในตารางคำแนะนำserializable/holdlock :

SERIALIZABLE

เทียบเท่ากับ HOLDLOCK ทำให้การล็อกแบบแบ่งใช้มีข้อ จำกัด มากขึ้นโดยการเก็บไว้จนกว่าการทำธุรกรรมจะเสร็จสมบูรณ์แทนที่จะปล่อยการล็อกแบบแบ่งใช้ทันทีที่ไม่จำเป็นต้องใช้ตารางหรือหน้าข้อมูลอีกต่อไปไม่ว่าธุรกรรมจะเสร็จสมบูรณ์หรือไม่ การสแกนจะดำเนินการด้วยความหมายเดียวกันกับธุรกรรมที่ดำเนินการในระดับการแยกแบบ SERIALIZABLE สำหรับข้อมูลเพิ่มเติมเกี่ยวกับระดับการแยกให้ดู SET TRANSACTION ISOLATION LEVEL (Transact-SQL)

อ้างจากเอกสารเซิร์ฟเวอร์ sql ในระดับแยกธุรกรรมserializable

SERIALIZABLE ระบุสิ่งต่อไปนี้:

  • งบไม่สามารถอ่านข้อมูลที่ได้รับการแก้ไข แต่ยังไม่ได้กระทำโดยการทำธุรกรรมอื่น ๆ

  • ไม่มีธุรกรรมอื่นใดที่สามารถแก้ไขข้อมูลที่อ่านโดยธุรกรรมปัจจุบันจนกว่าธุรกรรมปัจจุบันจะเสร็จสมบูรณ์

  • ธุรกรรมอื่น ๆ ไม่สามารถแทรกแถวใหม่ด้วยค่าคีย์ที่จะอยู่ในช่วงของคีย์ที่อ่านโดยคำสั่งใด ๆ ในธุรกรรมปัจจุบันจนกว่าธุรกรรมปัจจุบันจะเสร็จสมบูรณ์


ลิงค์ที่เกี่ยวข้องกับการแก้ปัญหาด้านบน:

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

หนึ่งลิงก์ล่าสุดKendra Littleทำการเปรียบเทียบอย่างคร่าวๆของmergevsinsert with left joinกับข้อแม้ที่เธอพูดว่า "ฉันไม่ได้ทำการทดสอบอย่างละเอียดเกี่ยวกับเรื่องนี้" แต่มันก็ยังอ่านได้ดี

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