อ่าน EXPLAIN Plan ให้รู้ว่า Database ใช้เวลาไปกับอะไร
ตีความ scan, join, estimated rows, actual rows และ cost จาก execution plan เพื่อหาคอขวดของ query ก่อนตัดสินใจเพิ่ม index หรือปรับโครงสร้าง SQL

ภาพรวม
EXPLAIN Plan อ่านยังไงให้รู้ว่า database กำลังทำอะไร
เวลา query ช้า หลายคนเริ่มจากเดาว่า “ต้องเพิ่ม index แน่ ๆ” หรือดูตัวเลข cost แล้วสรุปทันทีว่า query นี้แย่
แต่ EXPLAIN Plan ไม่ได้มีไว้ให้ดูเลขเดียว มันคือแผนที่บอกว่า database “ตั้งใจจะทำอะไร” ก่อนจะคืนผลลัพธ์ให้เรา 🧭
ถ้าอ่านเป็น เราจะเห็นว่า query กำลัง อ่านตารางแบบไหน ใช้ index จริงไหม ต้อง sort เองหรือเปล่า join ตารางด้วยวิธีอะไร กรองข้อมูลช้าเพราะอะไร และ estimate ของ database เพี้ยนแค่ไหน
เริ่มจากคำว่า EXPLAIN ก่อน EXPLAIN ธรรมดามักบอก “แผนที่ optimizer เลือก” เช่น PostgreSQL จะโชว์ node อย่าง Seq Scan, Index Scan, Sort, Hash Join MySQL จะมีตารางผลลัพธ์ที่มี type, key, rows, filtered, Extra
ส่วน EXPLAIN ANALYZE จะไปไกลกว่า เพราะมันรัน query จริง แล้วเอา actual time / actual rows มาเทียบกับ estimate อันนี้มีประโยชน์มาก แต่ต้องระวังมากเหมือนกัน ⚠️
บาง database การใช้ ANALYZE คือ query ถูก execute จริง ถ้าเป็น SELECT อาจแค่หนักเครื่อง แต่ถ้าเป็นคำสั่งที่มี side effect หรืออยู่ใน transaction ผิดจังหวะ อาจทำให้เกิดผลกระทบจริงได้ ก่อนใช้ใน production ต้องอ่าน behavior ของ DB ตัวนั้นให้ชัด
จุดแรกที่ควรมองคือ scan type
ถ้าเห็น Seq Scan หรือ Full Table Scan ไม่ได้แปลว่าแย่เสมอไป ถ้าตารางเล็ก หรือ query ต้องอ่านข้อมูลส่วนใหญ่ของตาราง การไล่อ่านทั้งตารางอาจถูกกว่าใช้ index
แต่ถ้าตารางใหญ่ และ query กรองแค่ user_id เดียว แล้วยังเห็น full scan นั่นคือสัญญาณให้ถามต่อว่า มี index ไหม condition เขียนให้ใช้ index ได้หรือเปล่า ชนิดข้อมูลตรงกันไหม มี function ครอบ column จน index ใช้ไม่ได้ไหม
ตัวอย่าง pitfall ที่เจอบ่อยใน backend WHERE DATE(created_at) = '2026-07-07' แบบนี้หลายระบบใช้ index บน created_at ได้ไม่เต็มที่ เพราะเราเอา function ไปครอบ column
ทางที่ดีกว่าคือเขียนเป็นช่วงเวลา WHERE created_at >= '2026-07-07' AND created_at < '2026-07-08' แบบนี้ optimizer มีโอกาสใช้ index range scan ได้ดีกว่า
จุดที่สองคือ estimated rows vs actual rows
ถ้า EXPLAIN ANALYZE บอกว่า estimate 100 rows แต่ actual ได้ 500,000 rows แปลว่า database เข้าใจข้อมูลผิดมาก
ผลที่ตามมาคือมันอาจเลือก join ผิด เลือก index ผิด หรือคิดว่า sort เล็กนิดเดียว ทั้งที่จริงใหญ่จนต้อง spill ลง disk
สาเหตุอาจมาจาก statistics เก่า ข้อมูล distribution แปลก column มีค่า skew หนัก เช่น status = 'active' เกือบทั้งตาราง หรือ filter หลายตัวที่สัมพันธ์กัน แต่ optimizer ประเมินแยกกัน
workflow ง่าย ๆ คือ
- ดูว่า estimate กับ actual ต่างกันมากไหม
- ถ้าต่างมาก ให้เช็ก statistics / ANALYZE table
- ถ้ายังเพี้ยน ให้ดู data distribution และ index design
จุดที่สามคือ cost
cost ไม่ใช่เวลาเป็น millisecond และ cost ข้าม database เทียบกันตรง ๆ ไม่ได้ แม้แต่ใน database เดียวกัน cost ก็เป็นค่าประมาณตาม model ของ optimizer
อย่าดูแค่ว่า cost สูงหรือต่ำ ให้ดูว่า cost สูงเกิดตรง node ไหน เช่น Sort, Hash Join, Nested Loop, Seq Scan แล้ว node นั้นกิน rows เท่าไร
บางที cost รวมดูไม่เลว แต่ bottleneck จริงอยู่ที่ node เล็ก ๆ ที่ถูก loop ซ้ำหลายหมื่นครั้ง โดยเฉพาะ Nested Loop ที่ outer rows เยอะกว่าที่คิด
จุดที่สี่คือ filter และ rows removed
ใน PostgreSQL ถ้าเห็น Rows Removed by Filter เยอะมาก แปลว่า database อ่านข้อมูลเข้ามาเยอะ แล้วค่อยโยนทิ้งทีหลัง
นี่มักบอกว่า index อาจไม่ครอบ condition สำคัญ หรือ condition บางตัวถูก apply ช้าเกินไป
ใน MySQL ให้ดู filtered และ Extra ถ้าเห็น Using where, Using temporary, Using filesort ไม่ได้แปลว่าผิดทันที แต่เป็นจุดให้ตรวจว่า sort/group/filter เกิดหลังอ่านข้อมูลมากเกินไปไหม
จุดที่ห้าคือ sort
ORDER BY ช้าไม่ใช่เพราะ sort เสมอไป บางครั้งช้าเพราะต้องอ่าน rows จำนวนมากก่อนถึงจะ sort ได้
ถ้า query คือ WHERE user_id = ? ORDER BY created_at DESC LIMIT 20 index ที่ดีอาจไม่ใช่แค่ user_id แต่เป็น composite index เช่น (user_id, created_at) เพื่อให้ database ดึง 20 แถวตามลำดับได้เลย ไม่ต้องเอาหลายพันแถวมาจัดเรียงเอง
จุดที่หกคือ join
Join plan บอกวิธีคิดของ database ชัดมาก Nested Loop เหมาะเมื่อฝั่งนอกเล็ก และฝั่งใน lookup ด้วย index ได้เร็ว Hash Join มักเหมาะกับชุดข้อมูลใหญ่ที่ต้อง match กันจำนวนมาก Merge Join ต้องการข้อมูลที่เรียงตาม key
ถ้าเห็น Nested Loop แล้ว actual rows ฝั่งนอกเยอะมาก ให้สงสัยว่า query กำลังยิง lookup ซ้ำ ๆ จำนวนมหาศาล อาจต้องเพิ่ม index, เปลี่ยน query shape, หรือทำให้ filter เกิดเร็วขึ้น
วิธีอ่านแบบ practical สำหรับงาน Node.js/backend คือ อย่าเริ่มจากแก้ code ให้เริ่มจากเก็บ evidence ก่อน
ขั้นตอนที่แนะนำ:
- จด query จริงพร้อม parameter ตัวอย่าง
- รัน EXPLAIN ก่อน เพื่อดู plan แบบปลอดภัยกว่า
- ถ้าต้องการตัวเลขจริง ค่อยรัน EXPLAIN ANALYZE ใน environment ที่ควบคุมได้
- มองหา node ที่ rows เยอะ / loop เยอะ / sort หนัก / join แปลก
- ปรับทีละอย่าง เช่น index หรือ query condition
- เก็บ before/after plan ไว้เทียบ
ตัวอย่าง before/after ที่ดีไม่ใช่แค่ “cost ลดแล้ว”
แต่ควรตอบได้ว่า จาก Full Table Scan กลายเป็น Index Range Scan actual rows ที่อ่านลดจาก 800,000 เหลือ 120 Sort หายไปเพราะ index เรียงข้อมูลให้แล้ว Nested Loop ไม่ถูกเรียกซ้ำหลักแสนครั้ง latency p95 ลดจาก 900ms เหลือ 80ms
อีก pitfall ใหญ่คือทดสอบด้วยข้อมูลน้อยเกินไป ใน dev database ที่มี 200 rows ทุก plan ดูเร็วหมด แต่ production มี 20 ล้าน rows แล้ว pattern คนละโลก
ถ้าจะ optimize จริง ควรใช้ข้อมูลที่ใกล้ production หรืออย่างน้อยมี cardinality และ distribution คล้ายกัน ไม่งั้น optimizer อาจเลือก plan ที่ดูดีใน dev แต่พังในระบบจริง
สรุปสั้น ๆ EXPLAIN Plan คือภาษาที่ database ใช้อธิบายความตั้งใจของมัน ส่วน EXPLAIN ANALYZE คือใบเฉลยว่าเมื่อรันจริงแล้วเกิดอะไรขึ้น
อ่านให้ครบทั้ง scan, rows, cost, filter, sort, join, index usage แล้วเทียบ before/after ด้วยหลักฐาน คุณจะเลิก optimize แบบเดา และเริ่ม debug database แบบมีแผนจริง ๆ ✅
แนวทางนำไปใช้
- นำแนวคิดจากโพสต์นี้ไปทดลองกับงานจริงของคุณ
- แยกเป็นขั้นตอนเล็ก ๆ แล้วทำทีละส่วน
- บันทึกผลลัพธ์และสิ่งที่เรียนรู้เพื่อต่อยอด
สรุป
ตีความ scan, join, estimated rows, actual rows และ cost จาก execution plan เพื่อหาคอขวดของ query ก่อนตัดสินใจเพิ่ม index หรือปรับโครงสร้าง SQL