VACUUM และ statistics
index และแผนที่ดีเป็นเพียงครึ่งหนึ่งของเรื่องประสิทธิภาพ อีกครึ่งหนึ่งคือการรักษาตัว table เองให้แข็งแรง PostgreSQL มีวิธีจัดการการ update และ delete ที่ไม่ธรรมดา และถ้าคุณเข้าใจกลไกนี้ คุณก็จะเข้าใจว่าทำไมงานเบื้องหลังสองอย่าง — VACUUM และ ANALYZE — จึงจำเป็นต่อการรักษา query ให้เร็ว
MVCC ทิ้ง dead tuple ไว้ข้างหลัง
หัวข้อที่มีชื่อว่า “MVCC ทิ้ง dead tuple ไว้ข้างหลัง”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]
VACUUM คืนพื้นที่ ANALYZE แจ้งข้อมูล
หัวข้อที่มีชื่อว่า “VACUUM คืนพื้นที่ ANALYZE แจ้งข้อมูล”งานบำรุงรักษาสองอย่างทำความสะอาดหลัง MVCC:
VACUUMสแกน table และทำเครื่องหมายพื้นที่ที่ dead tuple ใช้อยู่ว่าว่างพร้อมนำกลับมาใช้ใหม่ได้ เพื่อให้การ insert และ update ในอนาคตเข้ามาเติมเต็มแทนที่จะทำให้ไฟล์ขยายตัวVACUUMแบบธรรมดาไม่ได้คืนดิสก์ให้ระบบปฏิบัติการ แต่ทำให้พื้นที่กลับมาใช้ใหม่ได้ ภายใน tableANALYZEสุ่มตัวอย่างจาก table และปรับปรุง planner statistics — จำนวน row, การกระจายของค่า, column หนึ่งมีค่าที่แตกต่างกันกี่ค่า ตัวเลขเหล่านี้คือสิ่งที่ planner พึ่งพาในการเลือกระหว่าง seq scan กับ index scan ดังที่คุณเห็นในบทเรียน EXPLAIN
คุณสั่งรันเองด้วยมือก็ได้ ทั้งพร้อมกันหรือแยกกัน:
-- reclaim dead-tuple space in one tableVACUUM books;
-- refresh statistics so the planner estimates wellANALYZE books;
-- do both at onceVACUUM ANALYZE books;จากโค้ดแอปพลิเคชันก็เป็นคำสั่งเดียวกันผ่าน driver ใด ๆ
VACUUM ANALYZE books;await pool.query('VACUUM ANALYZE books');# VACUUM cannot run inside a transaction block,# so use autocommit for this statementold = conn.autocommitconn.autocommit = Truecur.execute("VACUUM ANALYZE books")conn.autocommit = old_, err := conn.Exec(ctx, "VACUUM ANALYZE books")sqlx::query("VACUUM ANALYZE books") .execute(&pool) .await?;ข้อสังเกตสำหรับ driver: VACUUM ไม่สามารถรันภายใน transaction block ได้ ดังนั้นต้องสั่งบน connection ที่อยู่ในโหมด autocommit แทนที่จะห่อไว้ใน BEGIN ... COMMIT
autovacuum ทำสิ่งนี้ให้คุณ
หัวข้อที่มีชื่อว่า “autovacuum ทำสิ่งนี้ให้คุณ”คุณแทบจะไม่ต้องรัน 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_autovacuumFROM pg_stat_user_tablesWHERE relname = 'books'; relname | n_live_tup | n_dead_tup | last_autovacuum---------+------------+------------+------------------------------- books | 48213 | 1502 | 2026-06-25 09:14:02.118431+00n_dead_tup ที่แข็งแรงคือค่าที่เล็กเมื่อเทียบกับ n_live_tup; ค่าที่ใหญ่และไม่เคยลดลงเลยหมายความว่าการ vacuum ตามไม่ทัน
VACUUM FULL เขียน table ใหม่
หัวข้อที่มีชื่อว่า “VACUUM FULL เขียน table ใหม่”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 ตามทันปริมาณการเขียนของคุณจริง ๆ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
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 จะช้าลงจริง แทนที่จะรอให้ผู้ใช้ร้องเรียนก่อน