ข้ามไปยังเนื้อหา

VACUUM และ statistics

index และแผนที่ดีเป็นเพียงครึ่งหนึ่งของเรื่องประสิทธิภาพ อีกครึ่งหนึ่งคือการรักษาตัว table เองให้แข็งแรง PostgreSQL มีวิธีจัดการการ update และ delete ที่ไม่ธรรมดา และถ้าคุณเข้าใจกลไกนี้ คุณก็จะเข้าใจว่าทำไมงานเบื้องหลังสองอย่าง — VACUUM และ ANALYZE — จึงจำเป็นต่อการรักษา query ให้เร็ว

PostgreSQL ใช้ MVCC หรือ multi-version concurrency control เพื่อให้ผู้อ่านไม่เคยบล็อกผู้เขียน ผลที่ตามมาคือ UPDATE ไม่ได้เขียนทับ row ที่ตำแหน่งเดิม และ DELETE ก็ไม่ได้ลบ row ทิ้งทันที แต่กลับเป็นว่า:

  • UPDATE เขียนเวอร์ชัน ใหม่ ของ row ขึ้นมา และทำเครื่องหมายเวอร์ชันเก่าว่าไม่ใช่เวอร์ชันปัจจุบันอีกต่อไป
  • DELETE ทำเครื่องหมาย row ว่าหายไปแล้ว แต่ยังทิ้งไบต์ของ row นั้นไว้บนดิสก์

เวอร์ชัน row ที่หลงเหลือและมองไม่เห็นแล้วเหล่านั้นเรียกว่า dead tuple — query ของคุณมองไม่เห็น แต่ยังกินพื้นที่อยู่ และ scan ก็ยังต้องข้ามผ่านทุกครั้ง

flowchart LR
  A[Row version v1] -->|UPDATE| B[Row version v2 live]
  A -->|marked obsolete| D[Dead tuple]
  D -->|VACUUM| R[Space reclaimed and reusable]
  B --> Q[Visible to queries]
การ update สร้าง dead tuple ที่ VACUUM จะมาคืนพื้นที่ในภายหลัง

งานบำรุงรักษาสองอย่างทำความสะอาดหลัง MVCC:

  • VACUUM สแกน table และทำเครื่องหมายพื้นที่ที่ dead tuple ใช้อยู่ว่าว่างพร้อมนำกลับมาใช้ใหม่ได้ เพื่อให้การ insert และ update ในอนาคตเข้ามาเติมเต็มแทนที่จะทำให้ไฟล์ขยายตัว VACUUM แบบธรรมดาไม่ได้คืนดิสก์ให้ระบบปฏิบัติการ แต่ทำให้พื้นที่กลับมาใช้ใหม่ได้ ภายใน table
  • ANALYZE สุ่มตัวอย่างจาก table และปรับปรุง planner statistics — จำนวน row, การกระจายของค่า, column หนึ่งมีค่าที่แตกต่างกันกี่ค่า ตัวเลขเหล่านี้คือสิ่งที่ planner พึ่งพาในการเลือกระหว่าง seq scan กับ index scan ดังที่คุณเห็นในบทเรียน EXPLAIN

คุณสั่งรันเองด้วยมือก็ได้ ทั้งพร้อมกันหรือแยกกัน:

-- reclaim dead-tuple space in one table
VACUUM books;
-- refresh statistics so the planner estimates well
ANALYZE books;
-- do both at once
VACUUM ANALYZE books;

จากโค้ดแอปพลิเคชันก็เป็นคำสั่งเดียวกันผ่าน driver ใด ๆ

VACUUM ANALYZE books;

ข้อสังเกตสำหรับ driver: VACUUM ไม่สามารถรันภายใน transaction block ได้ ดังนั้นต้องสั่งบน connection ที่อยู่ในโหมด autocommit แทนที่จะห่อไว้ใน BEGIN ... COMMIT

คุณแทบจะไม่ต้องรัน VACUUM ด้วยมือเลย เพราะ PostgreSQL มาพร้อมกับ autovacuum — กระบวนการเบื้องหลังที่คอยจับตาดูว่าแต่ละ table มี row เปลี่ยนแปลงไปกี่ row และรัน VACUUM และ ANALYZE โดยอัตโนมัติเมื่อความปั่นป่วน (churn) ข้ามค่า threshold ค่าเริ่มต้นคือเปิดไว้ และคุณก็ควรปล่อยให้เปิดไว้แบบนั้น

autovacuum ป้องกันไม่ให้ dead tuple กองพะเนินและรักษา statistics ให้สดใหม่โดยที่คุณไม่ต้องลงแรงเลย เมื่อคนบ่นว่า “Postgres ช้าลงเมื่อเวลาผ่านไป” สาเหตุมักจะเป็นเพราะ autovacuum ถูกปิดไว้หรือตามไม่ทัน

เมื่อ dead tuple สะสมเร็วกว่าที่ถูกคืนพื้นที่ — เพราะ autovacuum ถูกปิด, ช้าเกินไป หรือมี transaction ที่รันยาวคอยถือเวอร์ชันเก่าให้ยังมองเห็นได้ — table จะขยายตัวใหญ่กว่าที่ข้อมูลที่มีชีวิตจริงสมควรจะเป็น นี่คือ bloat table ที่บวม (bloated) มีหน้าให้อ่านมากขึ้นสำหรับจำนวน row ที่มีชีวิตเท่าเดิม ดังนั้นแม้แต่ seq scan หรือ index scan ก็ต้องทำ I/O เพิ่มขึ้น bloat ส่งผลต่อ index ด้วย

คุณสามารถดูสถานะคร่าว ๆ ของ table ได้ด้วย:

SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'books';
relname | n_live_tup | n_dead_tup | last_autovacuum
---------+------------+------------+-------------------------------
books | 48213 | 1502 | 2026-06-25 09:14:02.118431+00

n_dead_tup ที่แข็งแรงคือค่าที่เล็กเมื่อเทียบกับ n_live_tup; ค่าที่ใหญ่และไม่เคยลดลงเลยหมายความว่าการ vacuum ตามไม่ทัน

VACUUM แบบธรรมดาทำให้พื้นที่นำกลับมาใช้ใหม่ได้ แต่ไม่ได้ทำให้ไฟล์เล็กลง หากต้องการคืนดิสก์ให้ระบบปฏิบัติการจริง ๆ และอัด table ที่บวมอย่างหนักให้กระชับ มี VACUUM FULL ซึ่งเขียนทั้ง table ใหม่ลงในไฟล์ที่สดใหม่และถูกอัดแน่น

VACUUM FULL books;

ข้อเสียนั้นรุนแรง: VACUUM FULL จับ exclusive lock บน table ตลอดการเขียนใหม่ บล็อกการอ่านและเขียนทุกอย่างจนกว่าจะเสร็จ บน table ขนาดใหญ่นั่นอาจหมายถึงช่วงดาวน์ไทม์ที่ยาวนาน เก็บไว้ใช้กู้คืนจาก bloat รุนแรงแบบครั้งเดียวในช่วง maintenance window เท่านั้น — อย่าใช้เป็นงานบ้านประจำ การทำความสะอาดประจำเป็นหน้าที่ของ autovacuum

ใน pgAdmin: คลิกขวาที่ table แล้วเลือก Maintenance เพื่อรัน VACUUM, ANALYZE หรือ VACUUM FULL จากกล่องโต้ตอบ พร้อม checkbox สำหรับตัวเลือกต่าง ๆ กล่อง Maintenance เดียวกันนี้ยังสามารถ reindex index ที่บวมได้ด้วย

  • จงเปิด autovacuum ไว้ การปิด autovacuum เป็นสาเหตุที่พบบ่อยที่สุดของฐานข้อมูลที่ค่อย ๆ เสื่อมลงสู่ bloat และแผนที่ไม่ดี
  • จงรัน ANALYZE (หรือ VACUUM ANALYZE) ด้วยมือหลังจากการโหลดข้อมูลจำนวนมาก (bulk load), การ import หรือการ migration autovacuum อาจไม่ทำงานทันที และ statistics ที่เก่าล้าสมัยทันทีหลังการเปลี่ยนแปลงครั้งใหญ่จะนำ planner ไปผิดทาง
  • VACUUM แบบธรรมดาไม่บล็อกและปลอดภัยที่จะรันเมื่อใดก็ได้; VACUUM FULL บล็อกและก่อกวน — เก็บไว้ใช้ใน maintenance window เท่านั้น
  • transaction ที่รันยาวจะขัดขวาง vacuum ไม่ให้ทำความสะอาด tuple ที่ transaction นั้นอาจยังต้องเห็น จงระวัง transaction ที่เปิดค้างไว้และถูกลืม เมื่อ bloat โตขึ้นทั้งที่ autovacuum แข็งแรง
  • จงเฝ้าดู n_dead_tup และ last_autovacuum ใน pg_stat_user_tables เพื่อยืนยันว่าการ vacuum ตามทันปริมาณการเขียนของคุณจริง ๆ
ตัวเลือกBenefitCost
VACUUM ธรรมดาไม่ block การอ่านหรือเขียน ปลอดภัยที่จะรันได้ทุกเมื่อคืนพื้นที่ให้ใช้ซ้ำได้ภายใน table เท่านั้น ไม่คืนดิสก์ให้ระบบปฏิบัติการ
VACUUM FULLเขียน table ใหม่ให้กระชับและคืนดิสก์ให้ระบบปฏิบัติการจริง ๆจับ exclusive lock บล็อกทั้งการอ่านและเขียนตลอดการทำงาน
autovacuum (อัตโนมัติ)รันเบื้องหลังให้เองต่อเนื่อง แทบไม่ต้องดูแลควบคุมจังหวะเวลาไม่ได้ละเอียดเท่าการรันมือ อาจตามไม่ทันช่วง bulk load ที่หนักมาก
vacuum แบบรันมือควบคุมได้ว่าจะรันเมื่อไร เหมาะกับหลัง bulk loadเพิ่มภาระงานปฏิบัติการ (ops) ต้องมีคนคอยเฝ้าและสั่งเอง
  • มองว่า VACUUM FULL เป็นงานบำรุงรักษาประจำVACUUM FULL จับ exclusive lock ที่บล็อกทุกการอ่านและเขียน เก็บไว้ใช้กู้คืนจาก bloat รุนแรงในช่วง maintenance window เท่านั้น ไม่ใช่งานประจำวัน
  • ปิดหรือปรับ autovacuum ให้อนุรักษ์นิยมเกินไป — การตั้งค่า threshold ที่หลวมเกินไปหรือปิด autovacuum ไปเลย ทำให้ dead tuple สะสมโดยไม่มีใครรู้ตัว จนกลายเป็น bloat ที่แก้ยากในภายหลัง
  • ไม่รัน ANALYZE หลัง bulk load ขนาดใหญ่ — หลังโหลดข้อมูลจำนวนมาก, import หรือ migration statistics เดิมจะเก่าล้าสมัยทันที ถ้า autovacuum ยังไม่ทัน planner จะได้ค่าประมาณผิดและเลือก plan ที่แย่ จงรัน ANALYZE ด้วยมือทันทีหลังงานเหล่านี้

💡 ตัวอย่างจากของจริง

แพลตฟอร์ม SaaS ที่มี write โหลดสูง — ทีมที่รัน PostgreSQL งานหนักด้าน write เฝ้าติดตาม autovacuum lag และสัดส่วน dead tuple เป็น metric หลักในการดำเนินงาน (core operational metric) เพราะ table bloat ที่ไม่ถูกควบคุมเป็นสาเหตุอันดับต้น ๆ ของประสิทธิภาพ PostgreSQL ที่เสื่อมลงใน production

ทีม DBA ที่ใช้ pg_stat_user_tables — หลายทีมตั้ง alert อัตโนมัติจาก n_dead_tup และ last_autovacuum เพื่อรู้ล่วงหน้าก่อนที่ query จะช้าลงจริง แทนที่จะรอให้ผู้ใช้ร้องเรียนก่อน

ทำไม UPDATE จึงทิ้ง dead tuple ไว้ข้างหลังใน PostgreSQL?
ANALYZE ทำอะไรที่ VACUUM ไม่ทำ?
ทำไม VACUUM FULL จึงควรถูกสงวนไว้สำหรับ maintenance window?