ทำไม index จึงสำคัญ
query ที่คืนคำตอบถูกต้องก็ยังเป็น query ที่ช้าได้ เมื่อคุณขอให้ MongoDB หาสมาชิกทุกคนที่เข้าร่วมในปี 2023 แล้วไม่มีอะไรช่วยเลย server ก็มีทางเลือกเดียว นั่นคือเปิด collection ที่ document แรกแล้วอ่านไปข้างหน้า ตรวจทีละตัว จนกว่าจะดูครบทุก document การอ่านแบบกำลังดิบนี้เรียกว่า collection scan และ MongoDB ติดป้ายว่า COLLSCAN ในข้อมูลวินิจฉัย กับสมาชิกห้องสมุดไม่กี่คนคุณจะไม่รู้สึกอะไรเลย แต่บน collection ที่มี document สิบล้านรายการ นี่คือความต่างระหว่างตอบใน 1 มิลลิวินาที กับตอบแบบคลานต้วมเตี้ยม
index คือทางแก้ เป็นโครงสร้างข้อมูลแยกต่างหากที่เรียงลำดับไว้ และ server ดูแลควบคู่ไปกับ collection ของคุณ ข้างในเก็บค่าของฟิลด์หนึ่งหรือหลายฟิลด์เรียงตามลำดับ พร้อม pointer ชี้กลับไปยัง document ฉบับเต็ม เพราะค่าเรียงไว้แล้ว server จึงกระโดดตรงไปยังค่าที่ต้องการได้ แทนที่จะอ่านทั้งหมด ด้วยเหตุผลเดียวกับที่คุณพลิกไปท้ายเล่มตำราแทนที่จะอ่านทุกหน้าเพื่อหาว่าคำศัพท์หนึ่งถูกนิยามไว้ตรงไหน ทั้งโมดูลนี้ว่าด้วยการสร้าง index ที่ถูกต้องสำหรับ query ที่ถูกต้อง และพิสูจน์ชัยชนะด้วย explain
scan เทียบกับ seek
หัวข้อที่มีชื่อว่า “scan เทียบกับ seek”ลองนึกภาพสองกลยุทธ์วางเทียบกัน เมื่อไม่มี index นั้น query จะเดินผ่านทั้ง collection เมื่อมี index จะไต่ลงต้นไม้ที่เรียงไว้ แล้วลงตรงช่วงที่ match พอดี:
flowchart TB
subgraph NoIndex["Without an index — COLLSCAN"]
Q1["Query: joined equals 2023"] --> S0["Read doc 1"]
S0 --> S1["Read doc 2"]
S1 --> S2["Read doc 3"]
S2 --> S3["...read every document"]
S3 --> R1["Return the matches"]
end
subgraph WithIndex["With an index — IXSCAN"]
Q2["Query: joined equals 2023"] --> B0["Index root"]
B0 --> B1["Sorted branch"]
B1 --> B2["Leaf: joined equals 2023"]
B2 --> R2["Follow pointers to matching docs"]
end เส้นทางด้านขวาแตะเฉพาะ document ที่ต้องการ ส่วนเส้นทางด้านซ้ายแตะทุกตัวแล้วโยนทิ้งเกือบหมด การอ่านที่สูญเปล่านั้นแหละคือสิ่งที่ index ขจัดออกไป
index แรก
หัวข้อที่มีชื่อว่า “index แรก”การสร้าง index เป็นการเรียกครั้งเดียว คุณตั้งชื่อฟิลด์และทิศทาง — 1 สำหรับจากน้อยไปมาก — แล้ว server จะสร้างโครงสร้างให้คุณ ตรงนี้เราทำ index บนฟิลด์ joined ของ members ในห้องสมุด เพื่อให้การกรองหรือเรียงตามปีที่เข้าร่วมกลายเป็นการ seek:
db.members.createIndex({ joined: 1 })await db.collection("members").createIndex({ joined: 1 });db.members.create_index([("joined", 1)])_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{Keys: bson.D{{Key: "joined", Value: 1}}},)if err != nil { return err}coll.create_index( IndexModel::builder().keys(doc! { "joined": 1 }).build(),).await?;การเรียกนี้คืนชื่อที่ MongoDB ตั้งให้ index ใหม่ โดยอนุมานจากฟิลด์และทิศทาง เว้นแต่คุณจะตั้งชื่อเอง:
{ "createdIndexes": ["joined_1"], "ok": 1 }ใน Compass: เปิด collection แล้วเลือกแท็บ Indexes คุณจะเห็น index _id_ ในตัวอยู่เสมอ และคุณเพิ่มอันใหม่ได้ด้วยปุ่ม Create Index โดยพิมพ์ชื่อฟิลด์และเลือกทิศทาง
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”บทเรียนต่อยอดจากฟิลด์เดียวไปสู่กล่องเครื่องมือเต็มชุด:
- ทำไม index จึงสำคัญ — scan เทียบกับ seek และต้นทุนของการไม่มีเลย
- single และ compound indexes — การทำ index ฟิลด์เดียว แล้วหลายฟิลด์ และ prefix rule ที่ตัดสินว่า compound index ให้บริการ query ใดได้บ้าง
- กฎ ESR — การจัดลำดับฟิลด์ (Equality, Sort, Range) ที่ทำให้ compound index คุ้มค่ากับที่ลงทุนไป
- explain และ query plan — การอ่านผลลัพธ์ของ
explainเพื่อยืนยันว่ามีการใช้ index และจับ covered query - ประเภทของ index — multikey, text, TTL, partial, unique และ sparse index สำหรับกรณีที่ index ธรรมดาครอบคลุมไม่ได้
เคล็ดลับและจุดพลาด
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดพลาด”- ทุก collection เริ่มต้นด้วย index หนึ่งอันอยู่แล้ว นั่นคือ unique index บน
_idคุณไม่ต้องสร้างเอง และ drop ทิ้งก็ไม่ได้ - index ไม่ได้มาฟรี ช่วยให้อ่านเร็วขึ้นก็จริง แต่เพิ่มงานให้ทุก insert, update และ delete เพราะต้องคอยอัปเดต index ให้ตรงตลอด ทำ index เฉพาะฟิลด์ที่ query ของคุณใช้จริง ไม่ใช่ทุกฟิลด์
- index กินทั้งดิสก์และหน่วยความจำเหมือนข้อมูลที่ index นั้นอธิบายอยู่ กอง index ที่ไม่ได้ใช้ก็เปลืองทั้งสองอย่าง จึงคุ้มที่จะลบอันที่ไม่มี query ใดพึ่งพาออกไป
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| สร้าง index บนฟิลด์ที่ query ใช้บ่อย | query เปลี่ยนจาก COLLSCAN เป็น seek ที่รวดเร็ว | เพิ่มงานให้ทุก insert/update/delete และกินพื้นที่ดิสก์กับ RAM |
| ไม่สร้าง index เลย | ไม่มีต้นทุนเพิ่มตอนเขียน ไม่ต้องดูแล | query ทุกตัวเสี่ยงเป็น COLLSCAN เมื่อ collection โต |
| compound index ครอบหลาย query shape | index อันเดียวรับใช้หลาย query ได้ถ้าเรียงฟิลด์ถูกต้อง | ใหญ่กว่าและแพงกว่าต่อการดูแลเทียบกับ single-field index |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ทำ index ทุกฟิลด์ “เผื่อไว้” — แต่ละ index เพิ่มต้นทุนให้ทุก write และกิน RAM เพราะ MongoDB ต้องเก็บไว้ใน working set ทำ index เฉพาะฟิลด์ที่ query จริงใช้เท่านั้น
- ไม่เคยรัน
explain()ก่อนตัดสินใจเพิ่ม index — การเดาว่า index ไหนช่วยมักผิด ให้ดูผลลัพธ์explain()จริงก่อนสร้างหรือลบ index ใดๆ - ใช้ wildcard index เป็นค่าเริ่มต้นแทน index ที่เจาะจง — wildcard index สะดวกตอนเริ่มต้นแต่ใหญ่กว่าและช้ากว่า index ที่ออกแบบมาสำหรับ query shape จริง ควรใช้เมื่อ schema ไม่แน่นอนจริงๆ เท่านั้น
💡 ตัวอย่างจากของจริง
MongoDB Atlas Performance Advisor — วิเคราะห์ slow query log ของ collection แล้วเสนอ index ที่ควรสร้างโดยอัตโนมัติ ทีมงานจริงมักใช้เป็นจุดตั้งต้นแทนการเดาเอง แล้วค่อยยืนยันด้วย
explain()ก่อน applyทีม e-commerce ขนาดใหญ่ — ทำ index ทุกฟิลด์ใน collection สินค้าตอน migrate จากระบบเก่า แล้วพบว่า write throughput ตกฮวบ ต้องไล่ดรอป index ที่ query ไม่เคยใช้ทีละตัวโดยอ้างอิงจาก
$indexStats