ประเภทของ index
index แบบจากน้อยไปมากธรรมดาครอบคลุมความต้องการส่วนใหญ่ แต่ MongoDB มาพร้อม index ประเภทเฉพาะทางหลายชนิดสำหรับกรณีที่ index ธรรมดาจัดการได้ไม่สง่างาม: ฟิลด์ที่ถือ array, การค้นหาแบบ full-text, document ที่ควรหมดอายุเอง, index ที่ครอบเพียงชิ้นหนึ่งของ collection และ index ที่บังคับความไม่ซ้ำ แต่ละชนิดเป็นการปรับเปลี่ยนเล็กน้อยจาก createIndex และแค่รู้ว่ามีของพวกนี้อยู่ ก็ช่วยให้คุณไม่ต้องไปแก้ขัดแบบเก้งก้าง เราจะไล่ไปทีละชนิด โดยยังคงใช้ collection members ของห้องสมุดของเรา
Multikey: การทำ index ให้ array
หัวข้อที่มีชื่อว่า “Multikey: การทำ index ให้ array”เมื่อคุณทำ index บนฟิลด์ที่ถือ array MongoDB จะทำให้เป็น multikey index อัตโนมัติ คือทำ index ให้ทุกสมาชิกของ array แยกกัน query หาสมาชิกตัวใดตัวหนึ่งจึง seek ตรงไปยัง document ที่ match ได้ คุณไม่ได้สั่งให้เป็น multikey แต่เกิดขึ้นเองเพราะค่าของฟิลด์เป็น array ทำ index บน array borrowed ของชื่อหนังสือ:
db.members.createIndex({ borrowed: 1 })await db.collection("members").createIndex({ borrowed: 1 });db.members.create_index([("borrowed", 1)])_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{Keys: bson.D{{Key: "borrowed", Value: 1}}},)if err != nil { return err}coll.create_index( IndexModel::builder().keys(doc! { "borrowed": 1 }).build(),).await?;query อย่างการหาทุกคนที่ยืม "Compilers" ตอนนี้ seek แทนที่จะสแกน แม้ค่าจะอยู่ภายใน array
Text: การค้นหาคำ
หัวข้อที่มีชื่อว่า “Text: การค้นหาคำ”text index ตัดคำ (tokenize) ฟิลด์ string เพื่อให้คุณค้นหาคำด้วยตัวดำเนินการ $text แทนการ match string แบบเป๊ะ ลองทำ text index บนฟิลด์ notes ของสมาชิกแล้ว query ดู:
db.members.createIndex({ notes: "text" })db.members.find({ $text: { $search: "overdue reminder" } })await db.collection("members").createIndex({ notes: "text" });await db.collection("members") .find({ $text: { $search: "overdue reminder" } }) .toArray();db.members.create_index([("notes", "text")])list(db.members.find({"$text": {"$search": "overdue reminder"}}))_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{Keys: bson.D{{Key: "notes", Value: "text"}}},)if err != nil { return err}cur, err := coll.Find(ctx, bson.M{"$text": bson.M{"$search": "overdue reminder"}})coll.create_index( IndexModel::builder().keys(doc! { "notes": "text" }).build(),).await?;let cur = coll.find(doc! { "$text": { "$search": "overdue reminder" } }).await?;การค้นหาจับคู่คำใดคำหนึ่งและจัดอันดับผลลัพธ์ตามความเกี่ยวข้อง collection หนึ่งมี text index ได้อันเดียวเท่านั้น แม้ index อันนั้นจะครอบหลายฟิลด์ string ก็ตาม
TTL: document ที่หมดอายุ
หัวข้อที่มีชื่อว่า “TTL: document ที่หมดอายุ”TTL (time to live) index ลบ document อัตโนมัติเมื่อฟิลด์วันที่เก่ากว่าจำนวนวินาทีที่กำหนด สร้างได้ด้วยการทำ index บนฟิลด์วันที่พร้อม option expireAfterSeconds ตรงนี้ ระเบียน session ของสมาชิกจะหมดอายุหนึ่งชั่วโมงหลังจาก timestamp createdAt:
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })await db.collection("sessions").createIndex( { createdAt: 1 }, { expireAfterSeconds: 3600 });db.sessions.create_index([("createdAt", 1)], expireAfterSeconds=3600)_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{ Keys: bson.D{{Key: "createdAt", Value: 1}}, Options: options.Index().SetExpireAfterSeconds(3600), },)if err != nil { return err}coll.create_index( IndexModel::builder() .keys(doc! { "createdAt": 1 }) .options( IndexOptions::builder() .expire_after(Duration::from_secs(3600)) .build(), ) .build(),).await?;งานเบื้องหลังกวาดและลบ document ที่หมดอายุออก การกวาดทำงานราวหนึ่งครั้งต่อนาที ดังนั้นการลบจึงฉับไวแต่ไม่ทันที
Partial: การทำ index ชุดย่อย
หัวข้อที่มีชื่อว่า “Partial: การทำ index ชุดย่อย”partial index ครอบเฉพาะ document ที่ตรงกับนิพจน์ตัวกรอง ซึ่งทำให้ index เล็กเมื่อคุณ query เพียงชิ้นหนึ่งของ collection ทำ index เฉพาะสมาชิกที่ค้าง fines จริง โดยแนบ partialFilterExpression:
db.members.createIndex( { fines: 1 }, { partialFilterExpression: { fines: { $gt: 0 } } })await db.collection("members").createIndex( { fines: 1 }, { partialFilterExpression: { fines: { $gt: 0 } } });db.members.create_index( [("fines", 1)], partialFilterExpression={"fines": {"$gt": 0}},)_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{ Keys: bson.D{{Key: "fines", Value: 1}}, Options: options.Index().SetPartialFilterExpression( bson.M{"fines": bson.M{"$gt": 0}}, ), },)if err != nil { return err}coll.create_index( IndexModel::builder() .keys(doc! { "fines": 1 }) .options( IndexOptions::builder() .partial_filter_expression(doc! { "fines": { "$gt": 0 } }) .build(), ) .build(),).await?;สมาชิกที่มี fines เป็นศูนย์ไม่เคยเข้าสู่ index index จึงเล็กกระชับและยังรับใช้ query “ใครค้างเงิน” ได้ โดยไม่ต้องทำ index ทุกคน
Unique และ sparse
หัวข้อที่มีชื่อว่า “Unique และ sparse”unique index ปฏิเสธทุก insert หรือ update ที่จะทำให้ค่าที่ทำ index ซ้ำกัน — เป็นวิธีที่คุณรับประกัน เช่น ว่าไม่มีสมาชิกสองคนใช้ cardNumber ร่วมกัน:
db.members.createIndex({ cardNumber: 1 }, { unique: true })await db.collection("members").createIndex({ cardNumber: 1 }, { unique: true });db.members.create_index([("cardNumber", 1)], unique=True)_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{ Keys: bson.D{{Key: "cardNumber", Value: 1}}, Options: options.Index().SetUnique(true), },)if err != nil { return err}coll.create_index( IndexModel::builder() .keys(doc! { "cardNumber": 1 }) .options(IndexOptions::builder().unique(true).build()) .build(),).await?;sparse index ก้าวไปอีกขั้นด้วยการข้าม document ที่ขาดฟิลด์ที่ทำ index ไปเลย แทนที่จะเก็บเป็นค่า null การใช้ sparse คู่กับ unique จึงยอมให้ document จำนวนมากไม่มี cardNumber ได้ แต่ยังห้ามค่าซ้ำในกลุ่มที่มีค่าอยู่:
db.members.createIndex({ cardNumber: 1 }, { unique: true, sparse: true })await db.collection("members").createIndex( { cardNumber: 1 }, { unique: true, sparse: true });db.members.create_index([("cardNumber", 1)], unique=True, sparse=True)_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{ Keys: bson.D{{Key: "cardNumber", Value: 1}}, Options: options.Index().SetUnique(true).SetSparse(true), },)if err != nil { return err}coll.create_index( IndexModel::builder() .keys(doc! { "cardNumber": 1 }) .options( IndexOptions::builder().unique(true).sparse(true).build(), ) .build(),).await?;ใน Compass: ไดอะล็อก Create Index ในแท็บ Indexes เปิดเผยสิ่งเหล่านี้เป็นตัวเลือก — คุณติ๊ก Unique ตั้งค่า TTL หรือให้ Partial Filter Expression ได้โดยไม่ต้องออกจาก GUI
เคล็ดลับและจุดพลาด
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดพลาด”- multikey index มีข้อจำกัด: compound index รวมฟิลด์ array ได้มากที่สุดหนึ่งฟิลด์ เพราะการทำ index สอง array พร้อมกันจะคูณกันออกมาเป็นรายการ index ที่ระเบิดมหาศาล
- การลบของ TTL จับเวลาแบบโดยประมาณ การกวาดเบื้องหลังทำงานราวหนึ่งครั้งต่อนาที document ที่หมดอายุจึงอาจค้างอยู่เกินกำหนดได้ถึงราวหนึ่งนาทีก่อนโดนลบ
- partial index ให้บริการ query ได้ก็ต่อเมื่อ query นั้นรับประกันว่าตกอยู่ภายในตัวกรองของ index query ที่อาจตรงกับ document ที่ถูกยกเว้นใช้ partial index ไม่ได้ แม้กับส่วนที่ทับซ้อนกันก็ตาม
- unique index นับฟิลด์ที่หายไปว่าเป็นค่า
nullถ้าไม่ใส่sparseจึงมี document ที่ไม่มีฟิลด์นั้นได้แค่อันเดียว เพิ่มsparseเมื่อ document จำนวนมากจะขาดฟิลด์นั้น
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| text index | ค้นหาคำแบบ full-text สะดวก ไม่ต้อง regex | หนึ่ง collection มี text index ได้แค่อันเดียว |
| TTL index | ลบ document ที่หมดอายุอัตโนมัติ ไม่ต้องเขียน job แยก | เวลาลบไม่แม่นยำ เพราะงานกวาดพื้นหลังทำงานราวหนึ่งครั้งต่อนาที |
| partial index | เล็กกว่า index เต็ม เพราะครอบเฉพาะ document ที่ตรงตัวกรอง | ช่วยเฉพาะ query ที่การันตีว่าตกอยู่ในตัวกรองของ index เท่านั้น |
| unique index | รับประกันความไม่ซ้ำของข้อมูลในระดับ database | insert หรือ update ที่ทำให้ค่าซ้ำจะโดน reject ทันที ต้องเขียน error path รองรับเพิ่ม |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- สร้าง text index สองอันบน collection เดียว — MongoDB อนุญาตให้มี text index ได้แค่อันเดียวต่อ collection แม้จะครอบหลายฟิลด์ก็ตาม ต้องรวมฟิลด์ที่ต้องการค้นหาไว้ใน index เดียว
- คาดหวังว่า TTL index ลบ document ตรงเวลาเป๊ะ — งานกวาดพื้นหลังทำงานราวหนึ่งครั้งต่อนาที document อาจค้างเกินกำหนดได้นานถึงหนึ่งนาที อย่าพึ่ง TTL สำหรับ use case ที่ต้องการความแม่นยำระดับวินาที
- ลืมใส่
sparseคู่กับuniqueเมื่อฟิลด์เป็น optional — unique index นับค่าที่หายไปเป็นnullเหมือนกันหมด ดังนั้นถ้าไม่ใส่sparseจะมี document ได้แค่หนึ่งฉบับเท่านั้นที่ขาดฟิลด์นั้นได้
💡 ตัวอย่างจากของจริง
แพลตฟอร์ม SaaS ที่เก็บ session/token ชั่วคราว — ใช้ TTL index บนฟิลด์
expiresAtเพื่อให้ session หมดอายุลบตัวเองโดยไม่ต้องมี cron job แยกไว้ล้างข้อมูลเก่าระบบ support ticket ที่ทำ full-text search บนเนื้อหา ticket — ใช้ text index บนฟิลด์
subjectและbodyรวมกัน เพื่อให้ agent ค้นหา ticket ด้วยคำสำคัญได้โดยไม่ต้องพึ่ง search engine แยกต่างหากอย่าง Elasticsearch สำหรับข้อมูลปริมาณไม่ใหญ่มาก