Covering Index: ออกแบบดัชนีให้ฐานข้อมูลตอบ Query โดยไม่อ่าน Table
Covering Index ช่วยให้ฐานข้อมูลตอบ Query จากดัชนีได้โดยไม่ต้องย้อนกลับไปอ่านแถวจริงในตาราง บทความนี้อธิบายวิธีออกแบบ ตรวจสอบ และประเมินความคุ้มค่าทั้งใน PostgreSQL และ MySQL

Covering Index คือเทคนิคออกแบบดัชนีให้มีข้อมูลครบตามที่ Query ต้องใช้ ฐานข้อมูลจึงมีโอกาสตอบผลลัพธ์จาก Index เพียงอย่างเดียวโดยไม่ต้องย้อนกลับไปอ่านแถวจริงใน Table ซึ่งช่วยลดงานอ่านข้อมูลและเพิ่มประสิทธิภาพให้ Query ที่ถูกเรียกบ่อยได้อย่างมาก
Covering Index คืออะไร
Index ทั่วไปเปรียบได้กับสารบัญที่ช่วยบอกว่าข้อมูลอยู่ตรงไหน เมื่อฐานข้อมูลพบตำแหน่งแล้ว อาจยังต้องกลับไปอ่านแถวจริงเพื่อดึงคอลัมน์อื่นที่ต้องส่งคืน กระบวนการนี้เรียกว่า Heap Lookup ใน PostgreSQL หรือ Table Lookup ในความหมายทั่วไป
Covering Index ต่างออกไปตรงที่ข้อมูลซึ่ง Query ต้องใช้ถูกเก็บไว้ใน Index ครบแล้ว จึงเปรียบเสมือนสารบัญที่ไม่ได้บอกแค่ตำแหน่ง แต่แนบเนื้อหาสำคัญที่ต้องการอ่านมาด้วย หากเงื่อนไขอื่นเอื้ออำนวย ฐานข้อมูลจึงสามารถตอบ Query ได้โดยอ่านจาก Index เท่านั้น
ตัวอย่าง Query รายการคำสั่งซื้อล่าสุด
สมมติว่าหน้า Admin ต้องแสดงคำสั่งซื้อล่าสุด 20 รายการของผู้ใช้หนึ่งคน โดยต้องใช้รหัสคำสั่งซื้อ สถานะ ยอดรวม และเวลาที่สร้างข้อมูล Query อาจเขียนได้ดังนี้
SELECT id, status, total, created_at
FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;
หากมี Index เฉพาะ user_id ฐานข้อมูลจะค้นหารายการของผู้ใช้ได้เร็วขึ้น แต่ยังต้องกลับไปอ่าน Table เพื่อดึง id, status, total และ created_at ของแต่ละแถว ยิ่งต้องตรวจสอบหลายแถว จำนวน Table Lookup ก็ยิ่งเพิ่มขึ้น
การออกแบบใน PostgreSQL
สำหรับ PostgreSQL สามารถสร้าง Composite Index โดยวางคอลัมน์ที่ใช้กรองและเรียงลำดับไว้เป็นส่วนหลัก แล้วเก็บคอลัมน์ที่ต้องส่งคืนด้วยคำสั่ง INCLUDE ได้
CREATE INDEX idx_orders_user_recent
ON orders (user_id, created_at DESC)
INCLUDE (id, status, total);
ใน Index นี้ user_id รองรับเงื่อนไข WHERE ส่วน created_at DESC รองรับลำดับข้อมูลที่ Query ต้องการ ขณะที่ id, status และ total เป็น Included Columns ซึ่งไม่ใช้กำหนดลำดับของ Index แต่ทำให้ข้อมูลที่ต้องส่งคืนอยู่ใน Index ครบ
เมื่อ Query และสภาพของตารางเหมาะสม PostgreSQL อาจเลือกแผนการทำงานแบบ Index Only Scan ฐานข้อมูลสามารถค้นหา เรียงลำดับ และคืนผลลัพธ์จาก Index ได้โดยไม่ต้องอ่าน Heap Page สำหรับทุกแถว
การออกแบบใน MySQL
ใน MySQL มักใช้คำว่า Covering Index โดยตรง ตัวอย่างนี้อาจสร้าง Index ที่ครอบคลุมคอลัมน์ทั้งหมดซึ่ง Query ใช้ค้นหา เรียงลำดับ และส่งคืน
CREATE INDEX idx_orders_user_recent
ON orders (user_id, created_at, id, status, total);
หาก Query เลือกใช้เฉพาะคอลัมน์ที่อยู่ใน Index นี้ MySQL อาจตอบผลลัพธ์จาก Index ได้ทั้งหมด เมื่อรัน EXPLAIN อาจพบข้อความ Using index ในรายละเอียดของแผนการทำงาน
อย่างไรก็ตาม การเห็นคำว่า Using index ไม่ได้ยืนยันว่า Query มีประสิทธิภาพดีในทุกด้าน ควรตรวจสอบจำนวนแถวที่ต้องอ่าน ค่า filtered และ Index ที่ Optimizer เลือกใช้ร่วมด้วย
เหตุผลที่ควรหลีกเลี่ยง SELECT *
ข้อผิดพลาดที่พบบ่อยคือใช้ SELECT * แม้หน้า UI หรือ API จะต้องการข้อมูลเพียงไม่กี่คอลัมน์ เครื่องหมายดอกจันหมายถึงการขอทุกคอลัมน์ในตาราง ทำให้ Index ต้องมีข้อมูลจำนวนมากจึงจะครอบคลุม Query ได้จริง
หากตารางมี Metadata, JSON Blob หรือคอลัมน์ขนาดใหญ่ที่ไม่ได้อยู่ใน Index ฐานข้อมูลยังต้องกลับไปอ่าน Table อยู่ดี การเพิ่มคอลัมน์เหล่านี้ทั้งหมดลงใน Index ก็อาจทำให้ Index ใหญ่เกินความจำเป็นและสร้างต้นทุนด้านอื่นตามมา
หลักการเดียวกันใช้กับแอปพลิเคชัน Node.js ได้เช่นกัน ไม่ควรดึงข้อมูลทุกฟิลด์ด้วย SELECT * แล้วค่อยตัดออกด้วย JavaScript แต่ควรเลือกเฉพาะฟิลด์ที่ UI หรือ API Response ใช้จริง เพื่อลดภาระของฐานข้อมูล ปริมาณข้อมูลบนเครือข่าย และหน่วยความจำของแอปพลิเคชัน
Visibility Map ของ PostgreSQL
การมีข้อมูลครบอยู่ใน Index ยังไม่เพียงพอที่จะรับประกันว่า PostgreSQL จะไม่อ่าน Heap เลย เพราะระบบต้องตรวจสอบด้วยว่าแต่ละแถวสามารถมองเห็นได้สำหรับ Transaction ปัจจุบันหรือไม่ เรื่องนี้เกี่ยวข้องกับกลไก MVCC และ Visibility Map
หาก Page ของตารางถูกระบุว่า all-visible PostgreSQL สามารถเชื่อข้อมูลใน Index และไม่ต้องอ่าน Heap Page เพื่อตรวจสอบ Visibility แต่ถ้า Page ยังไม่อยู่ในสถานะดังกล่าว ระบบอาจต้องกลับไปอ่าน Table บางส่วน แม้แผนการทำงานจะแสดงเป็น Index Only Scan ก็ตาม
ดังนั้นประสิทธิภาพของ Index-only Scan จึงขึ้นอยู่กับงาน Vacuum และรูปแบบการเปลี่ยนแปลงข้อมูลด้วย ตารางที่เน้นการอ่านและเปลี่ยนแปลงไม่บ่อยมักได้รับประโยชน์มากกว่า ส่วนตารางที่มีการอัปเดตต่อเนื่องอาจยังมี Heap Fetches จำนวนมาก
ต้นทุนและกรณีที่เหมาะสม
Covering Index ไม่ได้เพิ่มความเร็วโดยไม่มีต้นทุน เมื่อเก็บคอลัมน์มากขึ้น Index จะใช้พื้นที่บน Disk และ Cache มากขึ้น ขณะเดียวกันการ INSERT, UPDATE และ DELETE อาจช้าลง เพราะฐานข้อมูลต้องปรับปรุง Index เพิ่มเติมทุกครั้งที่ข้อมูลเปลี่ยน
Query ที่เหมาะกับ Covering Index มักมีลักษณะดังนี้
- เป็น Query ที่ถูกเรียกบ่อย เช่น List Page, Feed, Dashboard, Report Summary หรือ Autocomplete
- มีรูปแบบ
WHEREและORDER BYที่ชัดเจนและค่อนข้างคงที่ - ส่งคืนข้อมูลจำนวนไม่มาก เช่น Query ที่มี
LIMITต่ำแต่ถูกเรียกตลอดทั้งวัน - ใช้คอลัมน์สำหรับแสดงผลเพียงไม่กี่คอลัมน์และไม่มีข้อมูลขนาดใหญ่โดยไม่จำเป็น
ไม่ควรสร้าง Covering Index ครอบทุก Endpoint แบบหว่าน เพราะแต่ละ Index เพิ่มภาระด้านพื้นที่ Cache และการเขียนข้อมูล ควรเลือกเฉพาะ Query สำคัญที่วัดแล้วว่ามีปัญหาและได้รับประโยชน์อย่างคุ้มค่า
Workflow สำหรับออกแบบและตรวจสอบ
การออกแบบที่ดีควรเริ่มจาก Query จริง ไม่ใช่เริ่มจากรายชื่อคอลัมน์ในตาราง ต้องตรวจสอบว่า Endpoint ใช้อะไรกรองข้อมูล เรียงลำดับด้วยอะไร และต้องส่งคอลัมน์ใดกลับไปยังผู้ใช้จริง ๆ
ขั้นตอนที่นำไปใช้ได้มีดังนี้
- รัน
EXPLAINหรือEXPLAIN ANALYZEกับ Query จริง - ตรวจสอบว่ามี Table Lookup หรือ Heap Fetches มากเพียงใด
- เปลี่ยน
SELECT *ให้เหลือเฉพาะคอลัมน์ที่ใช้งานจริง - ออกแบบ Composite Index ตามคอลัมน์ใน
WHEREและORDER BY - เพิ่ม Included Columns หรือคอลัมน์สำหรับครอบคลุมผลลัพธ์เท่าที่จำเป็น
- วัดผลอีกครั้งบน Staging ที่มีปริมาณและการกระจายข้อมูลใกล้เคียง Production
ใน PostgreSQL ควรสังเกต Index Only Scan และค่า Heap Fetches จาก EXPLAIN ANALYZE ค่า Heap Fetches ที่ต่ำแสดงว่าฐานข้อมูลสามารถใช้ข้อมูลจาก Index ได้จริงมากขึ้น ส่วนใน MySQL ควรตรวจสอบ Using index ควบคู่กับจำนวนแถว ค่า filtered และ key ที่ถูกเลือก
สรุป
Covering Index ไม่ใช่ดัชนีวิเศษ แต่เป็นผลจากการออกแบบ Query และ Index ให้พอดีกัน เริ่มจากเลือกเฉพาะข้อมูลที่ต้องใช้จริง จัดลำดับ Index ตามรูปแบบการกรองและเรียงข้อมูล แล้วเพิ่มคอลัมน์สำหรับผลลัพธ์เท่าที่จำเป็น
หัวใจสำคัญคือการแลกประสิทธิภาพการอ่านกับพื้นที่จัดเก็บและต้นทุนการเขียนอย่างตั้งใจ หากจำได้เพียงข้อเดียว ให้เริ่มจากคำถามว่า “Query นี้ต้องอ่านข้อมูลอะไรจริง ๆ” เพราะคำตอบของ Covering Index อยู่ตรงนั้นเอง