คำตอบสั้น ๆ: อ่านผล EXPLAIN ให้เข้าใจว่าฐานข้อมูลทำงานอย่างไร แล้วออกแบบ Index ที่ตรงกับ Query จริง พร้อมกฎการเรียงคอลัมน์ที่คนมักทำผิด
คู่มือนี้อธิบาย EXPLAIN ทีละขั้นตอน พร้อมคำสั่งที่ใช้ได้จริงและปัญหาที่พบบ่อย
เมื่อเว็บช้าและพบว่าคอขวดอยู่ที่ฐานข้อมูล ทางแก้ที่ได้ผลที่สุดมักไม่ใช่การเพิ่ม CPU หรือ RAM แต่คือการเพิ่ม Index ที่ถูกต้องหนึ่งตัว ซึ่งเปลี่ยน Query จากที่ใช้เวลาสองวินาทีเหลือสองมิลลิวินาทีได้
คู่มือนี้สอนอ่านผล EXPLAIN ซึ่งเป็นเครื่องมือที่บอกว่าฐานข้อมูลตัดสินใจทำงานอย่างไร แล้วออกแบบ Index ให้ตรงกับที่ Query ต้องการจริง พร้อมกฎการเรียงลำดับคอลัมน์ที่เป็นสาเหตุให้ Index จำนวนมากไม่ถูกใช้งาน
สิ่งที่ต้องเตรียม (EXPLAIN)
- MySQL 8.0 หรือ MariaDB 10.5 ขึ้นไป
- Query ที่ช้าจริงจากระบบงาน ไม่ใช่ Query สมมติ
หาว่า Query ไหนช้าก่อนด้วย Slow Query Log
mysql -e "SET GLOBAL slow_query_log = ON;"
mysql -e "SET GLOBAL long_query_time = 1;"
mysql -e "SET GLOBAL log_queries_not_using_indexes = ON;"
sudo mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
ขั้นตอนที่ 1: อ่านผล EXPLAIN
EXPLAIN SELECT * FROM orders
WHERE customer_id = 1234 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
คอลัมน์ที่ต้องดูและความหมาย
type คือวิธีที่ใช้เข้าถึงข้อมูล เรียงจากดีที่สุดไปแย่ที่สุด (EXPLAIN)
constและeq_refดีที่สุด อ่านแถวเดียวrefดี ใช้ Index หาแถวที่ตรงเงื่อนไขrangeใช้ได้ อ่านช่วงของ Indexindexไม่ดีนัก อ่าน Index ทั้งตัวALLแย่ที่สุด อ่านทั้งตาราง คือเป้าหมายที่ต้องกำจัด
key คือ Index ที่ถูกเลือกใช้จริง
หากเป็น NULL แปลว่าไม่ได้ใช้ Index เลย
rows คือจำนวนแถวที่คาดว่าต้องอ่าน
ตัวเลขนี้คือสิ่งที่ต้องทำให้เล็กที่สุด ถ้าตารางมีล้านแถวและ rows แสดงเป็นล้าน แปลว่ากำลังสแกนทั้งตาราง
Extra คือรายละเอียดเพิ่มเติมที่สำคัญ
Using indexดีมาก อ่านจาก Index อย่างเดียวไม่ต้องแตะตารางจริงUsing whereปกติUsing filesortต้องเรียงข้อมูลเพิ่ม ซึ่งช้าUsing temporaryต้องสร้างตารางชั่วคราว ซึ่งช้ามาก
ดูแผนแบบละเอียดพร้อมเวลาที่ใช้จริง
EXPLAIN ANALYZE SELECT * FROM orders
WHERE customer_id = 1234 AND status = 'paid'
ORDER BY created_at DESC LIMIT 20;
คำสั่งนี้รัน Query จริงแล้วรายงานเวลาที่ใช้ในแต่ละขั้น ต่างจาก EXPLAIN ธรรมดาที่แสดงเพียงแผนที่คาดไว้
ขั้นตอนที่ 2: กฎการเรียงคอลัมน์ใน Index แบบผสม
นี่คือหัวใจของการออกแบบ Index และเป็นจุดที่ทำผิดกันมากที่สุด กฎคือเรียงตามลำดับนี้
- คอลัมน์ที่เปรียบเทียบแบบเท่ากับ (
=) ก่อน - คอลัมน์ที่ใช้เรียงลำดับ (
ORDER BY) ถัดมา - คอลัมน์ที่เปรียบเทียบแบบช่วง (
>,<,BETWEEN) ไว้ท้ายสุด
สำหรับ Query ตัวอย่างข้างต้น Index ที่ถูกต้องคือ
CREATE INDEX idx_orders_cust_status_created
ON orders (customer_id, status, created_at);
เหตุผลคือ customer_id และ status ใช้เปรียบเทียบแบบเท่ากับจึงมาก่อน แล้ว created_at ใช้เรียงจึงตามมา ผลคือฐานข้อมูลอ่าน Index ตามลำดับที่เรียงไว้แล้วได้เลย ไม่ต้อง Using filesort
หากสลับลำดับเป็น (created_at, customer_id, status) Index จะแทบไม่ถูกใช้ เพราะเงื่อนไขแรกใน Query ไม่ได้อยู่ที่คอลัมน์แรกของ Index
ขั้นตอนที่ 3: หลักการคำนำหน้าซ้ายสุด
Index แบบผสมใช้ได้เฉพาะเมื่อ Query ใช้คอลัมน์เรียงจากซ้ายไปขวาต่อเนื่องกัน สมมติมี Index (a, b, c)
WHERE a = 1ใช้ได้WHERE a = 1 AND b = 2ใช้ได้WHERE a = 1 AND b = 2 AND c = 3ใช้ได้เต็มที่WHERE b = 2ใช้ไม่ได้ เพราะข้ามaWHERE a = 1 AND c = 3ใช้ได้แค่aเท่านั้น
ผลที่ตามมาคือ Index (a, b) ทำให้ Index (a) ซ้ำซ้อนและควรลบทิ้ง เพราะตัวแรกครอบคลุมอยู่แล้ว
ขั้นตอนที่ 4: หา Index ที่ไม่ได้ใช้และที่ซ้ำซ้อน
-- Index ที่ไม่เคยถูกใช้เลย
SELECT object_schema, object_name, index_name
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
AND count_star = 0
AND object_schema NOT IN ('mysql','performance_schema','sys')
ORDER BY object_schema, object_name;
-- ดู Index ทั้งหมดของตาราง พร้อมความหลากหลายของค่า
SELECT index_name, seq_in_index, column_name, cardinality
FROM information_schema.statistics
WHERE table_schema = 'shop' AND table_name = 'orders'
ORDER BY index_name, seq_in_index;
ค่า cardinality คือจำนวนค่าที่ไม่ซ้ำกันโดยประมาณ ยิ่งสูงยิ่งดีสำหรับ Index เพราะกรองได้แคบ Index บนคอลัมน์ที่มีแค่สองค่า เช่น is_active มักไม่คุ้มที่จะสร้างเดี่ยว ๆ
Index ทุกตัวมีต้นทุน มันทำให้การ INSERT และ UPDATE ช้าลงและกินพื้นที่ ลบตัวที่ไม่ได้ใช้ทิ้ง
DROP INDEX idx_unused ON orders;