ข้ามไปยังเนื้อหา

ทำไม 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

ลองนึกภาพสองกลยุทธ์วางเทียบกัน เมื่อไม่มี 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 ส่วนการ seek ด้วย index กระโดดไปยังช่วงที่ตรงกันแล้วตามพอยน์เตอร์

เส้นทางด้านขวาแตะเฉพาะ document ที่ต้องการ ส่วนเส้นทางด้านซ้ายแตะทุกตัวแล้วโยนทิ้งเกือบหมด การอ่านที่สูญเปล่านั้นแหละคือสิ่งที่ index ขจัดออกไป

การสร้าง index เป็นการเรียกครั้งเดียว คุณตั้งชื่อฟิลด์และทิศทาง — 1 สำหรับจากน้อยไปมาก — แล้ว server จะสร้างโครงสร้างให้คุณ ตรงนี้เราทำ index บนฟิลด์ joined ของ members ในห้องสมุด เพื่อให้การกรองหรือเรียงตามปีที่เข้าร่วมกลายเป็นการ seek:

db.members.createIndex({ joined: 1 })

การเรียกนี้คืนชื่อที่ MongoDB ตั้งให้ index ใหม่ โดยอนุมานจากฟิลด์และทิศทาง เว้นแต่คุณจะตั้งชื่อเอง:

{ "createdIndexes": ["joined_1"], "ok": 1 }

ใน Compass: เปิด collection แล้วเลือกแท็บ Indexes คุณจะเห็น index _id_ ในตัวอยู่เสมอ และคุณเพิ่มอันใหม่ได้ด้วยปุ่ม Create Index โดยพิมพ์ชื่อฟิลด์และเลือกทิศทาง

บทเรียนต่อยอดจากฟิลด์เดียวไปสู่กล่องเครื่องมือเต็มชุด:

  1. ทำไม index จึงสำคัญ — scan เทียบกับ seek และต้นทุนของการไม่มีเลย
  2. single และ compound indexes — การทำ index ฟิลด์เดียว แล้วหลายฟิลด์ และ prefix rule ที่ตัดสินว่า compound index ให้บริการ query ใดได้บ้าง
  3. กฎ ESR — การจัดลำดับฟิลด์ (Equality, Sort, Range) ที่ทำให้ compound index คุ้มค่ากับที่ลงทุนไป
  4. explain และ query plan — การอ่านผลลัพธ์ของ explain เพื่อยืนยันว่ามีการใช้ index และจับ covered query
  5. ประเภทของ 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 ใดพึ่งพาออกไป
ตัวเลือกBenefitCost
สร้าง index บนฟิลด์ที่ query ใช้บ่อยquery เปลี่ยนจาก COLLSCAN เป็น seek ที่รวดเร็วเพิ่มงานให้ทุก insert/update/delete และกินพื้นที่ดิสก์กับ RAM
ไม่สร้าง index เลยไม่มีต้นทุนเพิ่มตอนเขียน ไม่ต้องดูแลquery ทุกตัวเสี่ยงเป็น COLLSCAN เมื่อ collection โต
compound index ครอบหลาย query shapeindex อันเดียวรับใช้หลาย 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

MongoDB ทำอะไรเพื่อตอบ query เมื่อไม่มี index ที่ใช้ประโยชน์ได้?
index คืออะไรในแก่นแท้?
ทุก collection มี index ใดโดยที่คุณไม่ต้องสร้าง?