☕ 5-Min Tech Story #32

PostgreSQL Dead Tuples: สั่งลบข้อมูล 10 ล้านแถว แต่ทำไมพื้นที่ Disk ไม่ยอมคืนมาสักไบต์?

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 25 วินาที

พื้นที่บน Harddisk ของเซิร์ฟเวอร์ Database กำลังจะเต็มปริ่มที่ 95%...

คุณรีบเขียนสคริปต์กอบกู้สถานการณ์ สั่งลบ Log เก่าทิ้งทันที: DELETE FROM logs WHERE created_at < NOW() - INTERVAL '6 months'

ผ่านไป 5 นาที หน้าจอตอบกลับมาอย่างสวยงาม: DELETE 10000000 (ลบไปแล้วสิบล้านแถว!) คุณเปิด Terminal พิมพ์คำสั่ง df -h เพื่อชื่นชมพื้นที่ว่างที่ควรจะได้คืนมาสัก 30 GB...

แต่ความจริงกลับชวนช็อก: พื้นที่ดิสก์ยังคงค้างอยู่ที่ 95% เท่าเดิมเป๊ะ! ไม่ลดลงเลยแม้แต่วินาทีเดียว! มิหนำซ้ำ คำสั่ง SELECT ธรรมดาๆ กลับทำงาน ช้าลงกว่าเดิมเป็นเท่าตัว... ข้อมูลสิบล้านแถวมันหายไปไหน และทำไมดิสก์ถึงไม่ยอมคืนพื้นที่?

👻 กลไก MVCC: PostgreSQL ไม่เคยลบข้อมูลทิ้งจริง

หัวใจเบื้องหลังความเร็วในการอ่านเขียนพร้อมกันของ PostgreSQL คือ Multi-Version Concurrency Control (MVCC)

กฎเหล็กของ MVCC คือ: "คนอ่านต้องไม่ขวางคนเขียน และคนเขียนต้องไม่ขวางคนอ่าน" ดังนั้น ทุกแถวข้อมูล (Tuple) ใน PostgreSQL จะมี Header ล่องหนกำกับไว้เสมอ ได้แก่ xmin (Transaction ที่สร้าง) และ xmax (Transaction ที่ลบ)

"เมื่อคุณสั่ง DELETE ข้อมูล... PostgreSQL ไม่ได้ลบแถวนั้นออกจากหน้าดิสก์ (Disk Page) เลยแม้แต่น้อย มันแค่เอาตรายางไปประทับที่ค่า xmax ว่า 'แถวนี้หมดอายุแล้วนะ' เท่านั้น!"

และถ้าเป็นการสั่ง UPDATE ยิ่งน่ากลัวกว่านั้น เพราะมันคือการเอาตรายางไปสั่งตายแถวเก่า แล้ว เขียนแถวใหม่เพิ่มต่อท้ายลงไปอีกแถว ทันที!

🧟‍♂️ วิญญาณหลอนในตาราง: ปรากฏการณ์ Table Bloat

แถวข้อมูลที่หมดอายุแล้วแต่ยังนอนขวางอยู่ในไฟล์บนดิสก์ เราเรียกว่า Dead Tuples (ซากข้อมูล)

ลองจินตนาการว่า หน้ากระดาษ 1 แผ่น (Page ขนาด 8KB) เคยมีข้อมูลจริงอยู่ 100 แถว... คุณสั่งลบไป 99 แถว เหลือข้อมูลจริงแค่ 1 แถว:

🧹 ไม้กวาด VACUUM และความแตกต่างที่ต้องจำให้ขึ้นใจ

PostgreSQL มีกลไกเก็บกวาดซากข้อมูลที่ชื่อว่า VACUUM ซึ่งแบ่งออกเป็น 2 ระดับ:

1. Standard VACUUM (หรือ Autovacuum ที่ทำงานอัตโนมัติ)

มันจะเดินไล่ตรวจดูหน้ากระดาษ แล้วทำเครื่องหมายลงใน Free Space Map (FSM) ว่า "พื้นที่ตรงซากพวกนี้ ว่างให้เขียนทับได้แล้วนะ":

2. VACUUM FULL (การรื้อสร้างใหม่ทั้งตาราง)

สร้างไฟล์ตารางใหม่เอี่ยมขึ้นมาข้างๆ ก๊อปปี้เฉพาะแถวที่ยังมีชีวิต (Live Tuples) ไปจัดเรียงใหม่อย่างแน่นหนา แล้วลบไฟล์เก่าทิ้ง:

VACUUM FULL logs;
// ได้พื้นที่ดิสก์คืนสู่ OS ทันที 100% ครบทุกกิกะไบต์!
// ⚠️ คำเตือน: มันจะล็อกตารางแบบ Exclusive Lock ห้ามอ่านห้ามเขียนเด็ดขาดจนกว่าจะเสร็จ!

ในระบบ Production ที่ห้าม Downtime วิศวกรมักจะเลี่ยง VACUUM FULL แล้วหันไปใช้เครื่องมืออย่าง pg_repack ซึ่งสามารถจัดระเบียบตารางและคืนพื้นที่ให้ OS ได้แบบออนไลน์ โดยไม่ล็อกการทำงานของแอปพลิเคชัน

💡 5-Min Takeaway

"ใน PostgreSQL การสั่ง DELETE หรือ UPDATE คือการสร้าง 'ภาระงานกวาดขยะ' ล่วงหน้าเสมอ" การลบข้อมูลปริมาณมหาศาลในคราวเดียวไม่ได้แปลว่าได้พื้นที่คืนทันที อย่าลืมปรับจูน autovacuum_vacuum_scale_factor ให้ทำงานขยันขึ้นในตารางที่มีการอัปเดตบ่อย หรือพิจารณาแบ่งตารางด้วย Table Partitioning เพื่อให้สามารถสั่ง DROP TABLE ทิ้งได้ทั้งก้อน ซึ่งคืนพื้นที่ดิสก์ให้ OS ทันทีโดยไม่ต้องเสียเวลาตามเก็บกวาด Dead Tuples ทีละแถว