เหตุใดจึงไม่สามารถแทรกแถวใน CTE ไม่สามารถอัปเดตในคำสั่งเดียวกันได้


13

ใน PostgreSQL 9.5 ให้สร้างตารางง่ายๆด้วย:

create table tbl (
    id serial primary key,
    val integer
);

ฉันเรียกใช้ SQL เพื่อแทรกค่าจากนั้นอัปเดตในคำสั่งเดียวกัน:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

ผลลัพธ์คือ UPDATE ถูกละเว้น:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

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

คำตอบ:


15

งบทั้งหมดใน CTE เกิดขึ้นจริงในเวลาเดียวกัน เช่นพวกเขาจะขึ้นอยู่กับภาพรวมของฐานข้อมูลเดียวกัน

UPDATEเห็นรัฐเดียวกันของตารางต้นแบบเป็นINSERTซึ่งหมายความว่าแถวที่มีval = 1ไม่ได้มี แต่ คู่มือชี้แจงที่นี่:

งบทั้งหมดจะถูกดำเนินการด้วยภาพรวมเดียวกัน(ดูบทที่ 13 ) ดังนั้นพวกเขาไม่สามารถ "เห็น" ผลกระทบของอีกคนหนึ่งในตารางเป้าหมาย

แต่ละคำสั่งสามารถดูสิ่งที่ CTE อื่นส่งคืนในส่วนRETURNINGคำสั่ง แต่ตารางต้นแบบนั้นมีลักษณะเหมือนกันทั้งหมด

คุณจะต้องมีสองข้อความ (ในธุรกรรมเดียว) สำหรับสิ่งที่คุณพยายามทำ ตัวอย่างที่ให้มาควรเป็นเพียงหนึ่งเดียวที่INSERTจะเริ่มต้นด้วย แต่นั่นอาจเป็นเพราะตัวอย่างที่ง่าย


15

นี่คือการตัดสินใจดำเนินการ มันเป็นเรื่องที่อธิบายไว้ในเอกสาร Postgres, WITHแบบสอบถาม (Common ตารางนิพจน์) มีสองย่อหน้าที่เกี่ยวข้องกับปัญหานี้

ก่อนเหตุผลของพฤติกรรมที่สังเกต:

ย่อยงบในการWITHที่จะดำเนินการควบคู่กันไปกับแต่ละอื่น ๆและมีแบบสอบถามหลัก ดังนั้นเมื่อใช้คำสั่ง data-modifying ในWITHลำดับที่การอัพเดทที่ระบุเกิดขึ้นจริงจะไม่สามารถคาดเดาได้ งบทั้งหมดจะถูกดำเนินการด้วยภาพรวมเดียวกัน (ดูบทที่ 13) ดังนั้นพวกเขาไม่สามารถ "เห็น" ผลกระทบของอีกคนหนึ่งในตารางเป้าหมาย สิ่งนี้ช่วยลดผลกระทบของความไม่แน่นอนของลำดับการอัพเดตแถวจริงและหมายความว่าRETURNINGข้อมูลเป็นวิธีเดียวที่จะสื่อสารการเปลี่ยนแปลงระหว่างWITHคำสั่งย่อยต่างๆและแบบสอบถามหลัก ตัวอย่างของสิ่งนี้คือใน ...

หลังจากที่ฉันโพสต์ข้อเสนอแนะพร้อมกับpgsql-docsมาร์โค Tiikkaja อธิบาย (ซึ่งเห็นด้วยกับคำตอบของเออร์วิน):

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

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

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

เรามี 2 ส่วนคือ CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

และแบบสอบถามหลัก:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

การไหลของการดำเนินการเป็นดังนี้:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

ดังนั้นเมื่อแบบสอบถามหลักเข้าร่วมtbl(ตามที่เห็นในภาพรวม) กับnewvalตารางนั้นจะรวมตารางว่างที่มีตาราง 1 แถว เห็นได้ชัดว่ามันอัปเดต 0 แถว ดังนั้นคำสั่งไม่เคยมาแก้ไขแถวที่แทรกใหม่และนั่นคือสิ่งที่คุณเห็น

ทางออกในกรณีของคุณคือการเขียนคำสั่งใหม่เพื่อแทรกค่าที่ถูกต้องในสถานที่แรกหรือใช้ 2 คำสั่ง หนึ่งที่แทรกและที่สองในการปรับปรุง


มีสถานการณ์อื่น ๆ ที่คล้ายกันเช่นถ้าข้อความมีINSERTและจากนั้นDELETEในแถวเดียวกัน การลบจะล้มเหลวด้วยเหตุผลเดียวกันทั้งหมด

บางกรณีมีการอัปเดตและอัปเดตลบและพฤติกรรมของพวกเขาอธิบายไว้ในย่อหน้าต่อไปนี้ในหน้าเอกสารเดียวกัน

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

และในการตอบกลับจาก Marko Tiikkaja:

เคส update-update และ update-delete ไม่ได้เกิดจากรายละเอียดการใช้งานที่เหมือนกันอย่างชัดเจน(เช่น case-update และ insert-delete cases)
เคส update-update ไม่ทำงานเพราะภายในดูเหมือนว่าปัญหาฮัลโลวีนและ Postgres ไม่มีทางรู้ว่าทูเปิลตัวไหนที่จะโอเคที่จะอัปเดตสองครั้งและอันไหนที่สามารถนำปัญหาฮัลโลวีนกลับมาใช้ได้

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

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


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

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