- มักมีการใช้ การลบแบบซอฟต์โดยอิงคอลัมน์
archived_at เพื่อการกู้คืนข้อมูลและการปฏิบัติตามข้อกำหนด แต่เมื่อเวลาผ่านไป ความซับซ้อนและความไม่มีประสิทธิภาพ จะเพิ่มขึ้น
- วิธีนี้ทำให้ คิวรี, ดัชนี, การทำมิเกรชัน, และตรรกะการกู้คืน ซับซ้อนขึ้น และเนื่องจากข้อมูลที่เก็บถาวรส่วนใหญ่แทบไม่ถูกอ่านกลับ จึงทำให้เกิด ภาระที่ไม่จำเป็นต่อฐานข้อมูล
- ทางเลือกที่เสนอมีทั้ง การเก็บถาวรแบบอิงเหตุการณ์ของแอปพลิเคชัน, การเก็บถาวรแบบอิงทริกเกอร์, และ การเก็บถาวรแบบอิง WAL (Change Data Capture)
- แต่ละแนวทางแตกต่างกันในด้าน ความซับซ้อนในการปฏิบัติการ, ความต้องการด้านโครงสร้างพื้นฐาน, และความง่ายในการกู้คืน โดยเฉพาะแบบอิง WAL ที่ต้องการ การผสานรวมกับระบบภายนอกอย่าง Kafka
- หากเป็นโปรเจ็กต์ใหม่ แนวทางแบบอิงทริกเกอร์ คือ ตัวเลือกที่สมดุลที่สุดในแง่ความเรียบง่ายและการบำรุงรักษา
ปัญหาของการลบแบบซอฟต์
- โดยทั่วไปจะใช้คอลัมน์บูลีน
deleted หรือคอลัมน์ timestamp archived_at เพื่อลบข้อมูลในเชิงตรรกะ
- สามารถกู้คืนได้เมื่อลูกค้าลบข้อมูลโดยไม่ตั้งใจ
- บางกรณีก็จำเป็นต้องเก็บไว้เพื่อวัตถุประสงค์ด้านข้อบังคับหรือการตรวจสอบ
- แต่คอลัมน์
archived_at ทำให้เกิดความซับซ้อนทั่วทั้ง คิวรี, การดำเนินงาน, และโค้ดแอปพลิเคชัน
- ข้อมูลที่เก็บถาวรส่วนใหญ่แทบไม่ถูกอ่านกลับ
- ปัญหาการทำงานของ API หรือเครื่องมืออัตโนมัติ (เช่น Terraform) อาจทำให้มี แถวที่ไม่จำเป็นสะสมเป็นหลักล้าน
- หากไม่ได้ตั้งงานล้างข้อมูลที่เก็บถาวร จะเกิด ประสิทธิภาพลดลงระหว่างการสำรองและกู้คืนฐานข้อมูล
- ต้องกรองข้อมูลที่เก็บถาวรออกจากคิวรีและดัชนี และมี ความเสี่ยงต่อการรั่วไหลของข้อมูล
- ระหว่างมิเกรชันจะจัดการข้อมูลเก่าหรือแก้ค่าเริ่มต้นได้ยาก
- ตรรกะการกู้คืนซับซ้อนขึ้น และหากต้องเรียกระบบภายนอกก็อาจเกิดบั๊กได้
- สรุปแล้ว วิธี
archived_at แม้ดูเรียบง่าย แต่ มีต้นทุนการบำรุงรักษาระยะยาวสูง
การเก็บถาวรระดับแอปพลิเคชัน
- เมื่อมีการลบ จะ เผยแพร่เหตุการณ์ แล้วส่งไปยัง SQS เพื่อให้บริการอื่นนำไปเก็บถาวรใน S3
- ข้อดี
- ทำให้ฐานข้อมูลหลักและโค้ดแอปพลิเคชันเรียบง่ายขึ้น
- การล้างทรัพยากรภายนอกทำแบบ อะซิงโครนัส ช่วยเพิ่มประสิทธิภาพและความเสถียร
- สามารถ serialize เป็น JSON เพื่อเก็บในโครงสร้างที่ เหมาะกับแอปพลิเคชัน
- ข้อเสีย
- บั๊กในโค้ดแอปพลิเคชันอาจทำให้ ข้อมูลที่เก็บถาวรสูญหาย
- เพิ่ม ความซับซ้อนของโครงสร้างพื้นฐานในการปฏิบัติการ เช่น message queue
- ข้อมูลที่เก็บถาวรใน S3 ต้องมี เครื่องมือสำหรับค้นหาและกู้คืน
การเก็บถาวรแบบอิงทริกเกอร์
- ทริกเกอร์ก่อนลบจะคัดลอกแถวไปยัง ตาราง archive แยกต่างหาก ในรูปแบบ JSON
- ตัวอย่างตาราง:
archive(id, table_name, record_id, data, archived_at, caused_by_table, caused_by_id)
- ในกรณีลบด้วย foreign key แบบ cascade จะใช้ตัวแปรเซสชัน (
archive.cause_table, archive.cause_id) เพื่อ ติดตามสาเหตุของการลบ
- สามารถตรวจสอบได้ว่าเรคคอร์ดระดับบนตัวใดเป็นผู้ลบข้อมูลระดับล่าง
- ข้อดี
- ตารางใช้งานจริงสะอาดอยู่เสมอ ไม่ต้องมีคอลัมน์
archived_at
- การล้างตารางเก็บถาวร (
WHERE archived_at < NOW() - INTERVAL '90 days') ทำได้ง่าย
- รักษาประสิทธิภาพของคิวรีและดัชนี พร้อมทำให้มิเกรชันง่ายขึ้น
- ลดขนาดแบ็กอัป
- ตารางเก็บถาวรสามารถจัดการด้วย tablespace แยก หรือ time partitioning ได้
การเก็บถาวรแบบอิง WAL (Change Data Capture)
- อ่าน ล็อก WAL ของ PostgreSQL เพื่อสตรีมเหตุการณ์การลบไปยังระบบภายนอก
- เครื่องมือที่เป็นตัวแทน: Debezium (ทำงานร่วมกับ Kafka)
- ตัวอย่างเส้นทาง:
PostgreSQL → Debezium → Kafka → Consumer → Archive Storage
- ทางเลือกที่มีน้ำหนักเบากว่า
- pgstream: ส่ง WAL ไปยัง webhook หรือ message queue โดยตรง
- wal2json: แปลง WAL ออกมาเป็น JSON
- pg_recvlogical: เครื่องมือ logical replication ที่มีมาใน PostgreSQL
- ความซับซ้อนในการปฏิบัติการ
- แบบอิง Kafka ต้องมี การมอนิเตอร์, การรับมือเหตุขัดข้อง, และการปรับแต่ง
- หาก consumer ล่าช้าอาจเกิด ไฟล์ WAL สะสม → เสี่ยงพื้นที่ดิสก์ไม่พอ
- จำกัดได้ด้วยการตั้งค่า
max_slot_wal_keep_size ของ PostgreSQL 13+
- ต้องมีการมอนิเตอร์และแจ้งเตือนเรื่อง replication slot lag
- ข้อดี
- จับการเปลี่ยนแปลงทั้งหมดได้ โดยไม่ต้องแก้โค้ดแอปพลิเคชัน
- สตรีมไปยัง ปลายทางที่หลากหลาย (S3, data warehouse, search index) ได้
- ไม่เพิ่มภาระเพิ่มเติมให้ฐานข้อมูลหลัก
- ข้อเสีย
- ความซับซ้อนในการปฏิบัติการและต้นทุนโครงสร้างพื้นฐาน สูง
- หาก consumer ล่าช้าอาจเกิด ข้อมูลสูญหายหรือต้องซิงก์ใหม่
- เมื่อสคีมาเปลี่ยน ต้องมี การประสานงานระหว่างต้นทางกับ consumer
ไอเดียของรีพลิกาที่ไม่ประมวลผลการลบ
- มีการเสนอแนวคิดให้คง รีพลิกา PostgreSQL ที่เพิกเฉยต่อคำสั่ง DELETE
- สามารถสะสมเก็บข้อมูลทั้งหมดที่ไม่ถูกลบได้
- คิวรีข้อมูลที่เก็บถาวรได้โดยตรง
- ปัญหาที่อาจเกิดขึ้น
- อาจแยกไม่ออกว่าข้อมูลใดถูกลบไปแล้ว
- เสี่ยงเกิดความขัดแย้งเมื่อทำมิเกรชัน
- ต้นทุนด้านพื้นที่จัดเก็บและการปฏิบัติการเพิ่มขึ้น
บทสรุป
- สำหรับโปรเจ็กต์ใหม่ วิธีเก็บถาวรแบบอิงทริกเกอร์ เป็นตัวเลือกที่ใช้งานได้จริงที่สุด
- ตั้งค่าง่าย และช่วยให้ตารางใช้งานจริงสะอาด
- ค้นหาและจัดการข้อมูลที่เก็บถาวรได้ง่าย โดยไม่ต้องมีโครงสร้างพื้นฐานเพิ่มเติม
- หากมีโครงสร้างพื้นฐานที่ซับซ้อนอยู่แล้ว หรือต้องการสตรีมไปหลายปลายทาง แนวทางแบบอิง WAL จะเหมาะกว่า
4 ความคิดเห็น
ถ้าเป็นแบบอิงทริกเกอร์ ผมเคยเรียนมาว่ามันจะเพิ่มภาระให้กับ DB นี่นา...? กลับมาแนะนำให้ใช้ทริกเกอร์ซะงั้น
ถ้าโอเวอร์เฮดที่เกิดจากทริกเกอร์ระดับนั้นกลายเป็นปัญหา ต่อให้ไม่มีทริกเกอร์ก็เป็นสถานการณ์ที่เต็มไปด้วยปัญหาอยู่ดี
สุดท้ายแล้ว กฎระเบียบก็คือต้นทุนอยู่ดีนั่นแหละ แต่ยังไงซะก็เป็นภาระที่ผู้บริโภคต้องจ่ายอยู่แล้ว
ความคิดเห็นจาก Hacker News
ใน โดเมนธนาคาร ที่ฉันทำงานอยู่ กลับรู้สึกว่า soft delete มีข้อได้เปรียบมากกว่า
ถ้ามีคอลัมน์
deleted_atการเขียนคิวรีจะชัดเจน และทั้งคิวรีสำหรับวิเคราะห์กับหน้าแอดมินก็สามารถใช้ชุดข้อมูลเดียวกันได้การลบส่วนใหญ่เกิดขึ้นไม่บ่อย และแทบไม่เคยเห็นว่าแถวที่ถูก soft delete จะก่อให้เกิด ปัญหาด้านประสิทธิภาพ
อีกทั้งความสัมพันธ์ยังคงอยู่เหมือนเดิม ทำให้ การกู้คืน (undo) ทำได้ง่าย
ยิ่งไปกว่านั้น ฉันชอบแนวทางที่ทำให้แถว ไม่เปลี่ยนแปลง (immutable) อย่างสมบูรณ์ และเพิ่มแถวใหม่เมื่อมีการอัปเดต
ถ้าต้องการเก็บล็อก ฉันคิดว่าแนวทางที่ดีคือใช้ DB trigger เพื่อบันทึกลงตารางสำเนาเมื่อเกิด INSERT/UPDATE/DELETE
ในบางตารางที่ผมเคยเห็น มีข้อมูลถูก soft delete ถึง 50~70% และมีผลต่อประสิทธิภาพอย่างชัดเจน
สุดท้าย soft delete ก็ขึ้นอยู่กับบริบท และต้องมี การวิเคราะห์ล่วงหน้า
ส่วนใหญ่ไม่จำเป็น แต่ช่วย ประหยัด RAM ได้
ทางออกที่แท้จริงคือ Event Sourcing ที่บันทึกทุกการเปลี่ยนแปลงเป็นอีเวนต์
แม้ประสิทธิภาพจะลดลง แต่ชดเชยได้ด้วย snapshot และการซิงก์ (sync)
เพราะมีความสามารถแบบ time travel ที่ทำให้ตรวจสอบสถานะในอดีตได้อย่างสมบูรณ์
สถานะล่าสุดอยู่ที่แถวซึ่งมี timestamp มากที่สุด และสถานะในอดีตก็เรียกดูได้ด้วยตัวกรอง
วิธีนี้ทำให้มี การจัดการประวัติย้อนหลังที่แข็งแรง
กับดักใหญ่ที่สุดของ soft delete คือ ความซับซ้อนของคิวรี
ตอนแรกเหมือนแค่เพิ่ม
WHERE deleted_at IS NULLก็พอ แต่พอผ่านไปไม่กี่เดือน ข้อมูลผี ก็ไปโผล่ในรายงานเพราะลืมใส่เงื่อนไขกรองจะแก้ด้วย View ก็ได้ แต่สุดท้ายก็ยังต้องรักษา รูปแบบการเข้าถึงแบบขนาน ไว้ และเวลาต้องอ่านข้อมูลที่ถูกลบก็จำเป็นต้องข้าม abstraction นั้น
Event sourcing สะอาดกว่าก็จริง แต่มี ภาระในการปฏิบัติการ สูง เลยทำให้ส่วนใหญ่มักเลือกแนวทางแบบไฮบริด
ปัญหาคือ วิศวกร SWE และ BI จำนวนมากไม่คุ้นกับ SQL และการออกแบบสคีมา
ปัญหาที่พบได้บ่อยกว่า soft delete คือการจัดการ Type 2 Slowly Changing Dimension
คนส่วนใหญ่มักสร้าง audit table โดยไม่จำเป็น แล้วทำ UPDATE/INSERT ที่ไม่มีประสิทธิภาพซ้ำ ๆ
จริง ๆ แล้ว DB เป็นระบบที่สวยงามมาก แต่ก็น่าเสียดายที่มัน ไม่ได้รับความเคารพเท่าที่ควร
ฉันคิดว่าคงดีถ้า soft delete ถูก提供เป็น ฟีเจอร์ในตัวของ DB
ถ้าเปิดใช้ได้ในระดับตาราง และเลือกกลยุทธ์การจัดการการลบได้ ก็น่าจะเป็นอุดมคติ
แต่หลายทีมสุดท้ายก็ยังต้องทำเองด้วยแนวทาง SCD(Slowly Changing Dimension) เพราะมีความต้องการเฉพาะ
จากประสบการณ์ของฉัน แนวทางแบบ trigger-based เสถียรที่สุด
ตาราง archive ควรรักษาให้เป็นแบบ append-only และการกู้คืนควรจัดการที่ application layer
การอัปเดตให้ถือเป็น soft delete และให้ trigger จับสถานะก่อนหน้าไว้
trigger ควรรันในช่วง BEFORE เท่านั้น และ logic ควรเรียบง่าย
การแบ่งพาร์ทิชันมักใช้รายเดือนเป็นมาตรฐาน และถ้ามีภาระการเขียนสูงก็ควรแบ่งเป็นรายวัน
ฉันอยากให้ DB วิวัฒน์จาก stateful → stateless
ฉันชอบโครงสร้างที่บันทึกทุกการเปลี่ยนแปลงเป็น append-only event และแสดงข้อมูลที่ต้องการผ่าน view
จะยิ่งดีถ้า DB จัดการ materialized index ให้โดยอัตโนมัติ
DB รุ่นใหม่บางตัวมีฟีเจอร์ลักษณะนี้แล้ว แต่ตอนนี้การพัฒนาในฝั่ง OLTP ยังไม่มากพอ
น่าอ่านคำอธิบายของ Martin Fowler
บริษัทเก่าที่ฉันเคยทำงานใช้ soft delete กับทุกระบบ
ยังจำได้ว่าอาจารย์เคยพูดว่า “ในโลกธุรกิจ ข้อมูลไม่มีวันถูกลบจริง”
พื้นที่เก็บข้อมูลก็ถูกอยู่แล้ว เพราะงั้นไม่ควรลบข้อมูลทิ้งเลย
ฐานข้อมูลคือที่เก็บ ข้อเท็จจริง (fact)
การสร้างเรคอร์ดคือข้อเท็จจริงใหม่ การลบก็เป็นข้อเท็จจริงอีกแบบหนึ่ง
แต่ถ้าลบแถวออกทางกายภาพ ข้อเท็จจริงนั้นก็หายไป
โดยมากแล้วการลบแบบนี้ไม่ใช่สิ่งที่พึงประสงค์
ต้องคำนึงถึงต้นทุนการเก็บรักษาและความเสี่ยงด้านความปลอดภัย
การตัดสินใจเก็บข้อมูลไว้ถาวรจึงควรพิจารณาอย่างรอบคอบ
เพื่อให้ทำแบบนั้นได้ การเข้าใจ วงจรชีวิตของข้อมูล เป็นเรื่องสำคัญ
ที่ Firezone ตอนแรกใช้ soft delete เพื่อ audit log แต่ภายหลังเลิกใช้เพราะปัญหาเรื่อง migration
จากนั้นจึงเปลี่ยนไปใช้ Postgres CDC(Change Data Capture) เพื่อส่งอีเวนต์ออกไปยังตารางแยกที่ปรับให้เหมาะกับการเขียน
soft delete มีประโยชน์กับ ฟีเจอร์กู้คืนให้ผู้ใช้ แต่ไม่เหมาะกับงานด้าน การตรวจสอบหรือการปฏิบัติตามข้อกำหนด
การสร้าง View บนตารางที่มีฟิลด์ soft delete เพื่อซ่อนแถวที่ถูกลบ เป็นวิธีที่สะอาดดี
แบบนี้แอปพลิเคชันก็ไม่ต้องสนใจสถานะการลบ
แอปพลิเคชันยังคงอ่าน/เขียน/ลบกับตารางเดิมได้ตามปกติ
มีคำถามว่าแล้วจะจัดการ schema drift อย่างไร
ถ้าจะกู้คืนข้อมูลที่ serialize ไว้ตามสคีมาตอนถูกลบในภายหลัง การเปลี่ยนสคีมาก็จะกลายเป็นปัญหา
ส่วนใหญ่ถ้าจะกู้คืนก็มักเกิดภายในไม่กี่วันหลังลบ เลยได้รับผลจากการเปลี่ยนสคีมาน้อย
การ migrate archive เก่าให้เข้ากับโมเดลใหม่เป็นงานที่ ซับซ้อนและมีโอกาสผิดพลาดสูง
ท้ายที่สุดแล้ว วิธีรับมือก็ขึ้นอยู่กับรูปแบบการใช้งานของระบบ