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

กฎ ESR

บทเรียนก่อนทิ้งคำถามค้างไว้: เมื่อคุณสร้าง compound index สำหรับ query หนึ่ง ฟิลด์ควรเรียงลำดับอย่างไร คำตอบคือกฎสั้นๆ ที่จำง่ายชื่อ ESREquality, Sort, Range ระบุฟิลด์ที่ query ใช้สำหรับการจับคู่แบบเป๊ะก่อน แล้วตามด้วยฟิลด์ที่คุณเรียง แล้วตามด้วยฟิลด์ใดที่คุณ query เป็นช่วง ทำตามลำดับนั้นแล้ว index อันเดียวสามารถตอบสนองทั้งตัวกรอง การเรียง และการสแกนช่วงในรอบเดียว โดยไม่ต้องเรียงในหน่วยความจำเพิ่ม และไม่มีการอ่านที่สูญเปล่า

มาลองทำกันให้ตลอดด้วย query รูปธรรมกับ members ในห้องสมุดของเรา

สมมติว่าเราต้องการสมาชิกทุกคนใน city ที่กำหนด ที่มี fines มากกว่าสอง เรียงตามปีที่เข้าร่วม query นี้แตะสามฟิลด์ในสามรูปแบบที่ต่างกัน:

  • city — การจับคู่แบบ equality: city เท่ากับค่าเป๊ะค่าหนึ่ง
  • joined — ฟิลด์ sort: ผลลัพธ์กลับมาเรียงตามปีที่เข้าร่วม
  • fines — เป็น range: fines มากกว่าสอง เป็นช่วงค่ามากกว่าค่าเดียว
db.members
.find({ city: "Berlin", fines: { $gt: 2 } })
.sort({ joined: 1 })

ESR บอกว่า: ฟิลด์ equality ก่อน, ฟิลด์ sort ที่สอง, ฟิลด์ range ท้ายสุด นั่นให้ index { city: 1, joined: 1, fines: 1 }:

db.members.createIndex({ city: 1, joined: 1, fines: 1 })

นี่คือเหตุผลที่ลำดับนี้ใช้ได้:

flowchart TB
  E["Equality: city equals Berlin"] --> EX["Index seeks to one narrow city block"]
  EX --> S["Sort: joined ascending"]
  S --> SX["Inside that block, docs are already ordered by joined — no extra sort"]
  SX --> R["Range: fines greater than 2"]
  R --> RX["Walk the matching join order, keeping only fines above 2"]
  RX --> Out["Results: filtered, sorted, no in-memory sort step"]
Equality บีบลงเหลือบล็อกเดียว Sort ถูกตอบสนองด้วยลำดับของ index และ Range คือการเดินขั้นสุดท้าย

equality บน city ตรึงการค้นหาไว้ที่ชิ้นต่อเนื่องชิ้นเดียวของ index ภายในชิ้นนั้น document ถูกวางเรียงตามลำดับ joined อยู่แล้ว ดังนั้น MongoDB จึงคืน document แบบเรียงโดยไม่ต้องมีขั้นตอนเรียงแยกต่างหาก range บน fines มาท้ายสุดเพราะ range ทำให้ฟิลด์ของ index ที่ตามมาไม่เรียงลำดับ — ถ้า fines มาก่อน joined ปีที่เข้าร่วมภายในช่วงนั้นจะกระจัดกระจาย และ MongoDB จะต้องเรียง document ในหน่วยความจำหลังการอ่าน

เปรียบเทียบกับลำดับที่ดูน่าเลือกแต่ผิด { city: 1, fines: 1, joined: 1 } ซึ่งวาง range ไว้ก่อน sort equality บน city ยังคงใช้ได้ แต่ fines: { $gt: 2 } เป็น range และเมื่อ index เดินผ่าน range แล้ว ทุกฟิลด์ที่ตามมาจะไม่อยู่ในช่วงที่เรียงต่อเนื่องเดียวอีกต่อไป ดังนั้น joined จึงใช้สำหรับการเรียงไม่ได้ และ MongoDB ก็ถอยกลับไปเรียงในหน่วยความจำแบบ blocking ของชุดผลลัพธ์ทั้งหมด query ยังคงคืนคำตอบที่ถูกต้อง — เพียงแต่ช้ากว่าและใช้หน่วยความจำมากกว่า การเรียงที่ซ่อนอยู่นั้นแหละคือสิ่งที่ ESR มีไว้เพื่อหลีกเลี่ยง

วิธีจำที่มีประโยชน์: range “ผลาญ” การเรียงทั้งหมดของฟิลด์ที่ตามมา จึงต้องวางไว้ท้ายสุด หลังจากที่คุณได้การบีบด้วย equality และการเรียงมาฟรีแล้ว

ใน Compass: สร้าง index ผู้ท้าชิงในแท็บ Indexes แล้วรัน query ในแท็บ Explain Plan เมื่อ ESR ถูกตอบสนองคุณจะเห็นการเรียงถูกจัดการโดย index เมื่อไม่ใช่ plan จะแสดงขั้นตอน sort ที่ชัดเจนซึ่งกินผลลัพธ์เข้าไป

  • ESR เป็นค่าตั้งต้น ไม่ใช่กฎตายตัว เป็นจุดตั้งต้นที่ถูกต้องสำหรับ query รูปแบบ “filter, sort, range” ที่พบบ่อย แต่ยืนยันด้วย explain บนข้อมูลจริงของคุณเสมอ
  • ฟิลด์ equality มีได้มากกว่าหนึ่ง และทั้งหมดอยู่ด้านหน้า เช่นเดียวกับฟิลด์ range หลายฟิลด์ที่ท้ายสุด ส่วนฟิลด์ sort ฟิลด์เดียวอยู่ตรงกลาง
  • ผลตอบแทนของการทำ ESR ให้ถูกคือการกำจัดการเรียงในหน่วยความจำ plan ที่มีขั้นตอน sort ชัดเจนบนชุดผลลัพธ์ขนาดใหญ่คือสัญญาณว่าลำดับฟิลด์ของคุณกำลังต่อสู้กับ query
ตัวเลือกBenefitCost
เรียงฟิลด์ตาม ESR อย่างเคร่งครัดindex อันเดียวรองรับได้หลาย query shape ที่มี pattern filter/sort/range คล้ายกัน ลดจำนวน index ที่ต้องดูแลต้องมีวินัยตรวจสอบทุกครั้งที่ query pattern เปลี่ยน ไม่ใช่แค่สร้างแล้วจบ
เรียงฟิลด์แบบเฉพาะกิจตาม query ที่เขียนอยู่ตรงหน้าเขียนเร็ว ไม่ต้องคิดเยอะตอนเริ่มต้นindex ผูกติดกับ query shape เดียว เมื่อ query เปลี่ยนต้องสร้าง index ใหม่เพิ่มเรื่อยๆ
  • วางฟิลด์ range ไว้ก่อนฟิลด์ sort — range ทำให้ลำดับของฟิลด์ index ที่ตามมาไม่ต่อเนื่องอีกต่อไป ผลคือ MongoDB ต้องเรียงผลลัพธ์ในหน่วยความจำแม้จะมี index ก็ตาม
  • คิดว่า ESR เป็นกฎตายตัวที่ใช้ได้กับทุก query — ESR เป็นจุดเริ่มต้นที่ดีสำหรับ pattern filter-sort-range ทั่วไปเท่านั้น ต้องยืนยันด้วย explain() เสมอ โดยเฉพาะ query ที่ซับซ้อนกว่านั้น
  • ใส่ฟิลด์ equality หลายตัวปนกับฟิลด์ sort โดยไม่จัดกลุ่ม — ฟิลด์ equality ทั้งหมดต้องอยู่ด้านหน้าสุดของ index ด้วยกัน ถ้าแทรกฟิลด์ sort หรือ range ไว้ระหว่างฟิลด์ equality จะทำลาย prefix ที่ query equality หลายฟิลด์ต้องการ

💡 ตัวอย่างจากของจริง

ระบบ order management ที่ query ตาม { status: equality, createdAt: sort, totalAmount: range } — ทีมออกแบบ index ตาม ESR เป็น { status: 1, createdAt: 1, totalAmount: 1 } ทำให้ dashboard ที่กรองสถานะออเดอร์ เรียงตามวันที่ และกรองช่วงยอดเงิน ทำงานโดยไม่มีขั้นตอน sort ในหน่วยความจำเลย

ทีมที่ debug query ช้าลงหลัง requirement เปลี่ยน — เดิม query กรองเฉพาะ equality กับ sort ทำงานเร็ว แต่หลังเพิ่มเงื่อนไข range ใหม่เข้าไปโดยไม่ทบทวนลำดับ index เดิม ทำให้ query กลับไปมีขั้นตอน sort ที่ชัดเจนใน explain() ต้องสร้าง index ใหม่ตาม ESR ให้ตรงกับ query pattern ที่เปลี่ยนไป

ESR ย่อมาจากอะไร ในแง่ลำดับฟิลด์?
ทำไมฟิลด์ range จึงไปท้ายสุดใน ESR index?
คุณวางฟิลด์ range ไว้ก่อนฟิลด์ sort query plan จะแสดงอะไร?