2 คะแนน โดย GN⁺ 2026-02-24 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ธุรกรรม คือโครงสร้างสำหรับรันงานหลายอย่างในฐานข้อมูลเป็น หน่วยอะตอมมิก เดียว โดยรวมถึงการอ่าน การเขียน การอัปเดต และการลบ
  • MySQL และ Postgres ควบคุมธุรกรรมด้วย begin; และ commit; และยกเลิกการเปลี่ยนแปลงด้วย rollback; เมื่อเกิดความล้มเหลวหรือข้อผิดพลาด
  • ฐานข้อมูลทั้งสองต่างรับประกัน การอ่านแบบสอดคล้องกัน (consistent read) แต่ Postgres ใช้ การเก็บหลายเวอร์ชันของแถว (MVCC) ส่วน MySQL ใช้ undo log
  • ระดับการแยก (isolation level) ใช้ควบคุมการรบกวนกันของข้อมูลระหว่างธุรกรรม โดยแบ่งเป็น 4 ระดับตั้งแต่ Serializable ถึง Read Uncommitted
  • Postgres และ MySQL จัดการกับ ความขัดแย้งจากการเขียนพร้อมกัน ด้วยวิธีที่ต่างกัน โดย Postgres ใช้การตรวจสอบแบบมองโลกในแง่ดี ส่วน MySQL ใช้ การล็อกระดับแถว (row-level locking)

แนวคิดพื้นฐานของธุรกรรม

  • ธุรกรรมคือโครงสร้างที่รวมงาน SQL หลายรายการในฐานข้อมูลให้เป็น หน่วยการทำงานแบบอะตอมมิก เดียว
    • เริ่มต้นด้วย begin; และสิ้นสุดด้วย commit; โดยสามารถรันหลายคิวรีระหว่างนั้นได้
    • เมื่อ commit; การเปลี่ยนแปลงทั้งหมดจะถูกนำไปใช้พร้อมกันในครั้งเดียว
  • หากเกิดความขัดข้องที่ไม่คาดคิด (เช่น ไฟดับ ดิสก์เสีย เป็นต้น) หรือมีการยกเลิกโดยตั้งใจ สามารถย้อนการเปลี่ยนแปลงด้วย rollback;
    • Postgres รองรับการกู้คืนด้วย WAL (Write-Ahead Log)
  • ข้อมูลที่ถูกเปลี่ยนระหว่างธุรกรรมจะถูก แยกออกจากกัน จนมองไม่เห็นจากเซสชันอื่น
    • เมื่อ rollback; การเปลี่ยนแปลงทั้งหมดจะถูกยกเลิก และฐานข้อมูลจะกลับสู่สถานะเดิม

การอ่านแบบสอดคล้องกัน (Consistent Reads)

  • ธุรกรรมต้องรักษา มุมมองข้อมูลที่สอดคล้องกัน ซึ่งไม่ได้รับผลกระทบจากการเปลี่ยนแปลงภายนอกระหว่างการทำงาน
  • MySQL และ Postgres รองรับสิ่งนี้ตั้งแต่โหมด REPEATABLE READ ขึ้นไป แต่มีวิธีการทำงานต่างกัน
    • Postgres: จัดการเวอร์ชันของแต่ละแถวด้วย การเก็บหลายเวอร์ชันของแถว (MVCC)
    • MySQL: ใช้ undo log เพื่อสร้างเวอร์ชันย้อนหลังขึ้นมาใหม่

การเก็บหลายเวอร์ชันของแถวใน Postgres

  • ทุกครั้งที่มีการอัปเดตแถว จะมีการสร้างเวอร์ชันใหม่ โดยเวอร์ชันเก่าจะบันทึก transaction ID ใน xmax และเวอร์ชันใหม่ใน xmin
  • ก่อนที่ธุรกรรมจะคอมมิต เซสชันอื่นจะไม่สามารถเห็นการเปลี่ยนแปลงนั้นได้
  • หลังคอมมิต เวอร์ชันใหม่จะถูกสะท้อนไปยังทั้งฐานข้อมูล
  • เมื่อ rollback; การเปลี่ยนแปลงจะถูกทิ้งและข้อมูลเดิมยังคงอยู่
  • เวอร์ชันแถวเก่าจะถูกจัดการด้วยคำสั่ง VACUUM FULL เพื่อกู้คืนพื้นที่จัดเก็บ

Undo Log ของ MySQL

  • MySQL เขียนทับแถวโดยตรง แต่จะบันทึกค่าก่อนหน้าไว้ใน undo log เพื่อให้กู้คืนได้เมื่อจำเป็น
  • แต่ละแถวมีเมตาดาตาเป็น xid (transaction ID ของการแก้ไขล่าสุด) และ ptr (พอยน์เตอร์ไปยัง undo log)
  • เมื่อมีหลายธุรกรรมทำงานพร้อมกัน แต่ละธุรกรรมจะเลือกอ่านเวอร์ชันที่ต้องการได้ผ่าน undo log
  • อาจมีบันทึก undo log หลายรายการสำหรับแถวเดียวกัน และจะเลือกเวอร์ชันที่เหมาะสมตาม transaction ID

ระดับการแยก (Isolation Levels)

  • เป็นการตั้งค่าสำหรับควบคุมการรบกวนกันของข้อมูลระหว่างธุรกรรม โดยผ่อนคลายลงตามลำดับ Serializable → Repeatable Read → Read Committed → Read Uncommitted
  • Serializable: ทำงานเสมือนว่าทุกธุรกรรมถูกรันแบบลำดับทีละรายการ
  • Repeatable Read: เมื่อรันคิวรีเดิมซ้ำจะได้ผลลัพธ์เหมือนเดิม แต่ยังอาจเกิด phantom read ได้
  • Read Committed: สามารถอ่านการเปลี่ยนแปลงจากธุรกรรมอื่นที่คอมมิตแล้วได้
  • Read Uncommitted: อนุญาต dirty read เป็นระดับการป้องกันต่ำที่สุด แต่ให้ประสิทธิภาพสูง

การเขียนพร้อมกัน (Concurrent Writes)

  • วิธีจัดการเมื่อธุรกรรมสองรายการแก้ไขแถวเดียวกันพร้อมกันนั้นแตกต่างกันไปตามแต่ละฐานข้อมูล

MySQL: การล็อกระดับแถว (Row-level Locking)

  • Shared lock (S lock) อนุญาตให้หลายธุรกรรมอ่านพร้อมกันได้
  • Exclusive lock (X lock) อนุญาตให้มีเพียงธุรกรรมเดียวที่แก้ไขแถวได้
  • ในโหมด SERIALIZABLE การอัปเดตทุกครั้งต้องได้ X lock และหากเกิดการชนกันอาจเกิด deadlock ได้
  • MySQL ตรวจจับ deadlock และยุติหนึ่งในธุรกรรม

Postgres: Serializable Snapshot Isolation

  • Postgres ใช้ predicate lock เพื่อติดตามการเข้าถึงในระดับชุดของแถว
    • ตัวอย่าง: การล็อกสำหรับเงื่อนไข WHERE id BETWEEN 10 AND 20
  • ไม่ได้บล็อกการเข้าถึงจริงทันที แต่จะตรวจจับความขัดแย้งและยุติธุรกรรมเมื่อมีการละเมิด
  • ใช้ การแก้ไขความขัดแย้งแบบมองโลกในแง่ดี (optimistic conflict resolution) เพื่อหลีกเลี่ยง deadlock
  • เช่นเดียวกับ MySQL เมื่อเกิดความขัดแย้ง ธุรกรรมหนึ่งจะถูกยุติ และแอปพลิเคชันต้องมีตรรกะสำหรับลองใหม่

บทสรุป

  • ธุรกรรมเป็นองค์ประกอบหลักของฐานข้อมูลที่รับประกัน ความเป็นอะตอม ความสอดคล้อง การแยกตัว และความคงทน (ACID)
  • Postgres และ MySQL บรรลุเป้าหมายเดียวกันผ่านโครงสร้างภายในที่แตกต่างกัน
  • หากเข้าใจระดับการแยกทั้ง 4 แบบและหลักการทำงานของธุรกรรม ก็จะดูแลฐานข้อมูลได้อย่างมั่นคงยิ่งขึ้น

1 ความคิดเห็น

 
GN⁺ 2026-02-24
ความคิดเห็นจาก Hacker News
  • รู้สึกว่าบทความนี้ยังอธิบายได้ไม่ค่อยครบ
    แทนที่จะอธิบายระดับการแยกกันทำงานผ่าน phenomena ตามที่นิยามไว้ในมาตรฐาน SQL ผมคิดว่าการเริ่มจากแนวคิด serializability จะเข้าใจได้ตรงไปตรงมามากกว่า
    serializability มองได้ว่าเป็นการขยายแนวคิดของ thread safety และเมื่อขาดสิ่งนี้ไป ก็จะเกิดบั๊กที่ผลลัพธ์เปลี่ยนไปตามลำดับการรัน
    ระดับ isolation ต่าง ๆ ของฐานข้อมูลก็เป็นเพียงการผ่อนคลายการรับประกันนี้ลงเท่านั้น และผู้ใช้ต้องหาวิธีรับประกันในแบบอื่นเอง
    phenomena เป็นเพียงเครื่องมือสำหรับทำภาพให้เห็นสถานการณ์ที่ไม่เป็น serializable ไม่ได้เชื่อมกับ serializability โดยตรง
    ยกตัวอย่างเช่น Kubernetes cluster ก็สามารถทำงานแบบ serializable ได้ หากใช้ controller ที่ออกแบบมาดี

    • ผู้เขียนเอง ขอบคุณสำหรับฟีดแบ็กดี ๆ
      การจะพูดถึง transaction, isolation level และ MVCC พร้อมทั้งเปรียบเทียบระหว่างหลาย DB ในคราวเดียวเป็นงานที่ใหญ่มาก
      ผมพยายามหาสมดุลระหว่างความลึกทางเทคนิค ความเข้าถึงง่าย และความยาวของบทความ
    • มีการแชร์ลิงก์ Jepsen: วิเคราะห์ MariaDB Galera Cluster
      เป็นความเห็นว่าอยากให้มี notation และ citation มากกว่านี้
    • RDBMS ส่วนใหญ่มี serializable isolation ให้ใช้อยู่แล้วถ้าจำเป็น
      แต่ถ้าใช้โดยไม่จำเป็น ต้นทุนในการประสานงาน ระหว่าง transaction จะสูงขึ้น ทำให้ concurrency และ throughput ลดลง
    • มีคนตอบกลับว่าถ้าอย่างนั้นก็ลองเสนอคำอธิบายที่ดีกว่ามาดู
  • เราอาจมอง transaction เหมือน snapshot ของ copy-on-write filesystem (btrfs, zfs) ได้ แต่ผมคิดว่าเปรียบกับ Git branch จะเข้าใจง่ายกว่า
    BEGIN คือการสร้าง branch, UPDATE คือ commit, ROLLBACK คือการลบ branch, และ COMMIT ก็เหมือน git merge
    ถ้ามี conflict DB จะพยายาม merge ในระดับ row และถ้าล้มเหลวก็จะ rollback หรือ force merge ตามการตั้งค่า
    READ UNCOMMITTED เน้น merge ให้เร็ว ส่วน SERIALIZABLE ให้ความสำคัญกับความถูกต้อง
    อุปมาแบบนี้อาจช่วยให้บางคนเข้าใจแนวคิดของ transaction แบบ “อ๋อ!” ได้

    • (คอมเมนต์สั้น ๆ) เป็นการตอบที่สื่อถึงเรื่อง concurrency
  • สิ่งที่ทำให้หลายคนแปลกใจคือ Postgres กับ MySQL ไม่ได้ใช้ serializable mode เป็นค่าเริ่มต้น แต่เป็น read-committed
    ความต่างด้านประสิทธิภาพไม่ได้แค่ “เล็กน้อย” แต่ในทางปฏิบัติใหญ่กว่านั้นมาก
    ถ้าใช้ read-committed ก็ต้องใส่ใจกับการจัดการ lock และ UNIQUE constraint ก็จำเป็นต่อการกัน race condition
    ถึงอย่างนั้นผมก็ยังชอบวิธีนี้มากกว่าการยอมรับภาระด้านประสิทธิภาพและปัญหา retry ของ serializable mode
    อ้างอิง: เอกสาร PostgreSQL

    • MySQL และ MariaDB (InnoDB) รุ่นใหม่ใช้ค่าเริ่มต้นเป็น repeatable-read
      ดู เอกสาร MySQL, เอกสาร MariaDB
      ทุกวันนี้แทบไม่มีใครใช้ MyISAM แล้ว
    • ปัญหาของ SERIALIZABLE ไม่ใช่แค่เรื่องประสิทธิภาพ แต่ยังรวมถึงการที่ transaction อาจล้มเหลวจาก conflict, deadlock, timeout
      แอปพลิเคชันต้องตรวจจับให้ได้และมีกลยุทธ์ retry
    • Oracle และ SQL Server ก็ใช้ read committed เป็นค่าเริ่มต้นเช่นกัน
      serializable mode ดูดีมากในตำรา แต่ของจริงแทบไม่มีใครใช้
  • ทุกวันนี้เครื่องมือฐานข้อมูลจำนวนมากให้ความสำคัญกับ การแชร์อัปเดตแบบเรียลไทม์ มากกว่า ACID
    ตัวอย่างเช่น Airtable จะแสดงผลการแก้ไขฟิลด์บนหน้าจอของเพื่อนร่วมงานทันที แต่เพราะไม่มี transaction จึงมี ความเสี่ยงเรื่องข้อมูลไม่สอดคล้องกัน
    ดูเพิ่มเติมได้ที่ บทความบล็อก VisualDB

    • มีคนตอบว่ามันดูเหมือน โฆษณาสินค้า ที่แฝงมาในรูปแบบการโจมตีคู่แข่ง
  • การอ่านบล็อก PlanetScale เป็นเรื่องที่เพลิดเพลินมากจริง ๆ
    สงสัยว่าใช้เครื่องมืออะไรทำภาพประกอบ

    • ผู้เขียนเอง ขอบคุณ!
      ภาพประกอบทำด้วย js + gsap (https://gsap.com)
  • ถ้าสนใจหัวข้อนี้ ขอแนะนำ 『Designing Data-Intensive Applications』 อย่างมาก
    มันไม่ได้ครอบคลุมแค่ isolation level หลายแบบ แต่ยังพูดถึง ความคลุมเครือของนิยาม ACID ด้วย
    ได้ยินมาว่าฉบับพิมพ์ครั้งที่ 2 กำลังจะออก

  • transaction ใน ระบบ MVCC อย่าง Postgres คล้ายกับ snapshot ของ copy-on-write filesystem
    ตอน BEGIN จะสร้าง snapshot ของข้อมูลขึ้นมา และ UPDATE จะสะท้อนอยู่แค่ในสำเนาส่วนตัว
    ถ้า ROLLBACK สำเนานั้นก็จะถูกทิ้งไป และเมื่อ COMMIT snapshot ใหม่จะกลายเป็นเวอร์ชันทางการ
    อุปมานี้อาจเป็นจุดที่ช่วยให้บางคนเข้าใจแนวคิด transaction ได้ชัดเจนขึ้น
    P.S. จะเปรียบเป็น Git branch ก็ได้เหมือนกัน

    • ไม่ได้ถูกต้องทั้งหมด DB ใช้ทั้ง branching และ locking ร่วมกัน
      ในกรณีอย่าง SELECT แล้วค่อย UPDATE อาจมี thread หนึ่งถูก block ได้
      วันนี้ผมว่าจะลองทดสอบใน MySQL ว่าจะเปลี่ยนให้เป็น query เดียวได้ไหม
  • สมัยก่อนในการสัมภาษณ์ backend มักจะถามเรื่อง transaction บ่อยมาก
    ทุกคนเคยใช้มัน แต่ระดับความเข้าใจต่างกันไปตามประสบการณ์
    ต่อให้จำ isolation level ได้ไม่ครบทั้งหมด แค่รู้ว่ามันทำงานต่างกันก็พอให้เห็น ความอยากรู้อยากเห็นและความเข้าใจระบบ ได้แล้ว

    • แม้จะใช้ชื่อ isolation level เหมือนกัน แต่แต่ละ DB ก็ทำงานไม่เหมือนกัน ดังนั้นต้อง ตรวจสอบพฤติกรรมรายละเอียดเป็นรายกรณี
  • คำอธิบายของ “phantom read” อาจทำให้เข้าใจผิดได้
    ใน repeatable read ค่าของ row เดิมจะไม่เปลี่ยน แต่ row ใหม่ สามารถถูกเพิ่มเข้ามาได้
    จะไม่มีกรณีที่ row เดิมถูกแก้ไขหรือลบ ดังนั้นควรอธิบายจุดนี้ให้ชัดเจน

  • ประโยคที่ว่า “ไม่เกี่ยวกับ xmin/xmax” ดูเหมือนจะอธิบายไม่ครบ
    และการทำภาพตอน commit ที่ชี้ไปยัง table header ก็ดูแปลก ๆ
    จริง ๆ แล้ว xmax/xmin คือกลไกหลักในการตัดสินสถานะ commit ไม่ใช่หรือ?
    ถ้ารวม subtransaction เข้าไปด้วยก็ยิ่งซับซ้อนขึ้น
    ถึงอย่างนั้นโดยรวมแล้วทั้งภาพและคำอธิบายก็อ่านสนุกมาก

    • ผมเองก็เสียดายที่ไม่มีการพูดถึง แนวคิด xmax/xmin
      มันเป็นหัวใจสำคัญในการเข้าใจ isolation level และให้ความรู้สึกเหมือนมีส่วนหนึ่งของบทความหายไป