การ query array
field ของ document เก็บ array ได้ และการ query array ก็มีเซอร์ไพรส์น่ารักติดมาด้วย: เมื่อคุณ filter field ที่เป็น array กับค่าธรรมดา MongoDB จะตรวจ ทุก element และจับคู่ถ้า มีอันใด ผ่านเงื่อนไข กฎข้อเดียวนี้ทำให้ query array แบบง่ายอ่านได้เหมือนกับ query แบบ scalar ทุกประการ ความซับซ้อนจะปรากฏก็ต่อเมื่อคุณต้องการให้หลายเงื่อนไขเป็นจริงบน element เดียวกัน และตรงนั้นเองคือจุดที่ $elemMatch ได้ออกโรง
ในบทเรียนนี้สมาชิกแต่ละคนถือ array borrowed ของชื่อหนังสือ และบางคนถือ array tags ของป้ายความสนใจ
การจับคู่ element เดียว
หัวข้อที่มีชื่อว่า “การจับคู่ element เดียว”หากต้องการถามว่า “ใครยืม Compilers อยู่ตอนนี้?” ให้ filter field ที่เป็น array ด้วยค่านั้นเหมือนกับว่าเป็น scalar MongoDB จะมองเข้าไปใน array ให้คุณเอง:
db.members.find({ borrowed: "Compilers" })const docs = await db.collection("members") .find({ borrowed: "Compilers" }) .toArray();docs = list(db.members.find({"borrowed": "Compilers"}))cur, err := coll.Find(ctx, bson.M{"borrowed": "Compilers"})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "borrowed": "Compilers" }) .await?;สมาชิกคนใดก็ตามที่ array มีชื่อหนังสือนั้นจะ match หมด ไม่ว่าชื่อนั้นจะอยู่ตำแหน่งไหนในรายการ:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "borrowed": ["Algorithms", "Compilers"] }]หลายเงื่อนไขบน element เดียวกัน: $elemMatch
หัวข้อที่มีชื่อว่า “หลายเงื่อนไขบน element เดียวกัน: $elemMatch”นี่คือกับดัก สมมติว่า scores เก็บ array ของตัวเลข และคุณต้องการ document ที่มี element มากกว่า 80 และ น้อยกว่า 90 การเขียน { scores: { $gt: 80, $lt: 90 } } ไม่ได้ แปลอย่างนั้น แต่จะ match เมื่อ บาง element เกิน 80 และ บาง element (อาจคนละตัวกัน) ต่ำกว่า 90 หากต้องการตรึงทั้งสองเงื่อนไขไว้บน หนึ่ง element ให้ใช้ $elemMatch:
db.members.find({ scores: { $elemMatch: { $gt: 80, $lt: 90 } }})const docs = await db.collection("members").find({ scores: { $elemMatch: { $gt: 80, $lt: 90 } }}).toArray();docs = list(db.members.find({ "scores": {"$elemMatch": {"$gt": 80, "$lt": 90}}}))cur, err := coll.Find(ctx, bson.M{ "scores": bson.M{"$elemMatch": bson.M{"$gt": 80, "$lt": 90}},})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "scores": { "$elemMatch": { "$gt": 80, "$lt": 90 } } }) .await?;เฉพาะ document ที่มี element เดียวอยู่ในช่วง 81–89 เท่านั้นที่ตรง — ตรงนี้ 85 ผ่านเกณฑ์ แม้ว่า array จะมีค่าที่อยู่นอกช่วงด้วย:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "scores": [70, 85, 95] }]การบังคับให้มีหลายค่า: $all
หัวข้อที่มีชื่อว่า “การบังคับให้มีหลายค่า: $all”$all จับคู่เมื่อ array มี ทุกค่า ที่ระบุไว้ ไม่สนลำดับ พูดง่าย ๆ คือ AND ของการเป็นสมาชิก “ใครยืมทั้ง Algorithms และ Compilers พร้อมกัน?” ก็คือสิ่งนี้พอดี:
db.members.find({ borrowed: { $all: ["Algorithms", "Compilers"] }})const docs = await db.collection("members").find({ borrowed: { $all: ["Algorithms", "Compilers"] }}).toArray();docs = list(db.members.find({ "borrowed": {"$all": ["Algorithms", "Compilers"]}}))cur, err := coll.Find(ctx, bson.M{ "borrowed": bson.M{"$all": bson.A{"Algorithms", "Compilers"}},})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "borrowed": { "$all": ["Algorithms", "Compilers"] } }) .await?;เฉพาะสมาชิกที่ถือทั้งสองชื่อหนังสือเท่านั้นที่ส่งกลับมา การมีเพียงอันเดียวยังไม่พอ:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "borrowed": ["Algorithms", "Compilers"] }]การ filter ตามความยาว: $size
หัวข้อที่มีชื่อว่า “การ filter ตามความยาว: $size”$size จับคู่ array ที่มีความยาวตรงเป๊ะ “ใครยืมหนังสืออยู่สองเล่มพอดีตอนนี้?” คือ query operator เดียว:
db.members.find({ borrowed: { $size: 2 } })const docs = await db.collection("members") .find({ borrowed: { $size: 2 } }) .toArray();docs = list(db.members.find({"borrowed": {"$size": 2}}))cur, err := coll.Find(ctx, bson.M{"borrowed": bson.M{"$size": 2}})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "borrowed": { "$size": 2 } }) .await?;สมาชิกที่ array borrowed มี element สองตัวพอดีจะตรง:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "borrowed": ["Algorithms", "Compilers"] }]การเข้าถึง element ตาม index
หัวข้อที่มีชื่อว่า “การเข้าถึง element ตาม index”ตำแหน่งใน array สามารถระบุที่อยู่ได้ด้วย dotted numeric path: borrowed.0 คือ element แรก, borrowed.1 คือตัวที่สอง และต่อ ๆ ไป ใช้สิ่งนี้เพื่อ query element ที่ตำแหน่งที่รู้แน่ — “หนังสือเล่ม แรก ที่ใครยืมคือ Algorithms?”:
db.members.find({ "borrowed.0": "Algorithms" })dot path เดียวกันใช้ได้เมื่อ array เก็บ sub-document — reviews.0.rating เข้าถึง field rating ของรีวิวแรก ที่เป็นรูปแบบที่บทเรียนถัดไปจะเจาะลึก
ใน Compass: filter สำหรับ array ใส่ในแถบ filter เหมือนกับ scalar เลย เช่น { borrowed: { $size: 2 } } Compass แสดง array ที่ตรงแบบ inline และให้คุณกางออกเพื่อดูแต่ละ element ได้
เคล็ดลับและจุดที่พลาดง่าย
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดที่พลาดง่าย”- ทำไม
$elemMatchถึงสำคัญ: ถ้าไม่มี$elemMatchหลายเงื่อนไขบน field ที่เป็น array จะถูกไล่ตรวจ กระจายข้าม element ดังนั้น{ scores: { $gt: 80, $lt: 90 } }อาจตรงกับ array อย่าง[70, 95]ได้ — แต่ละ element ผ่านเกณฑ์คนละอัน$elemMatchบังคับให้ทุกเงื่อนไขอยู่บน element เดียว ซึ่งแทบจะตรงกับสิ่งที่คุณตั้งใจจริง ๆ เสมอ $allไม่สนลำดับ แค่ตรวจว่าแต่ละค่ามีอยู่ที่ไหนสักที่ ไม่ได้บอกอะไรเรื่องตำแหน่งหรือ element ส่วนเกิน ส่วน$sizeตรึงความยาวเป๊ะ ๆ$sizeรับเฉพาะตัวเลขตรง ๆ เท่านั้น คุณเขียน{ $size: { $gt: 1 } }ไม่ได้ หากต้องการ query “มากกว่าหนึ่ง element” ให้สร้าง field ความยาวหรือใช้ aggregation framework- filter ค่าเดียวบน array จับคู่แบบการบรรจุ (containment)
{ borrowed: "Compilers" }match ทุก array ที่ มี ชื่อหนังสือนั้นอยู่ ไม่ได้บังคับว่า array ต้องเท่ากับ["Compilers"]
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
$elemMatch สำหรับหลายเงื่อนไข | บังคับให้ทุกเงื่อนไขเป็นจริงบน element เดียวกัน ผลลัพธ์ตรงกับเจตนาที่แท้จริงเกือบทุกครั้ง | เขียนยาวกว่าเล็กน้อย ต้องคิดเรื่อง “element เดียวกัน” ให้ชัดตั้งแต่ต้น |
operator หลายตัวแยกกันบน field เดียวกัน (ไม่ใช้ $elemMatch) | เขียนสั้น อ่านเหมือน scalar query ทั่วไป | แต่ละเงื่อนไขอาจผ่านคนละ element ทำให้ได้ผลลัพธ์ที่ไม่ตรงเจตนา — บั๊กเงียบที่พบบ่อย |
positional/length query ($size, "field.0") | เจาะจงตำแหน่งหรือความยาวได้ตรง ๆ ในคำสั่งเดียว | เปราะบางต่อการเปลี่ยนลำดับ array และ $size รับเฉพาะค่าตรง ๆ ใช้กับช่วงไม่ได้ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- คิดว่า filter บน array field จะทำให้ผลลัพธ์มีเฉพาะ element ที่ตรงเงื่อนไข — query แค่ตัดสินว่าจะส่ง document ทั้งก้อนกลับมาหรือไม่ ตัว array field ที่ส่งกลับมายังคงมีครบทุก element เหมือนเดิม หากต้องการเฉพาะ element ที่ตรง ต้องใช้ positional projection operator
$หรือ$elemMatchใน projection แยกต่างหาก - ใช้ positional dot path อย่าง
borrowed.0เหมือนว่าตำแหน่งนั้นมีความหมายทางธุรกิจที่แน่นอน — ลำดับใน array เปลี่ยนได้ทุกครั้งที่แอปทำ$push,$pullหรือเขียนทับ array ทั้งก้อน การอิงตำแหน่งจึงเปราะบางกว่าการ query ด้วยเงื่อนไขค่า - สับสนระหว่าง
$allปกติกับ$allที่ครอบด้วย$elemMatchหลายตัว —$all: [{ $elemMatch: {...} }, { $elemMatch: {...} }]ตรวจว่ามี element (อาจคนละตัว) ที่ผ่านแต่ละเงื่อนไขแยกกัน ไม่ใช่การรวมเงื่อนไขทั้งหมดไว้บน element เดียว
💡 ตัวอย่างจากของจริง
ระบบตะกร้าสินค้าที่มีโปรโมชั่นตามเงื่อนไข — เช่น “มีสินค้าชิ้นเดียวที่ทั้งราคาเกิน 1000 และจำนวนอย่างน้อย 2 ชิ้น” ต้องใช้
$elemMatchเพราะถ้าใช้เงื่อนไขแยกกัน ระบบอาจให้ส่วนลดผิดกับตะกร้าที่มีสินค้าราคาแพงชิ้นหนึ่งกับสินค้าจำนวนเยอะอีกชิ้นหนึ่งซึ่งไม่ใช่ชิ้นเดียวกันระบบ tag ของบทความหรือสินค้า — ค้นหาบทความที่ติด tag ครบทุกอันที่ผู้ใช้เลือก (เช่น “javascript” และ “backend” พร้อมกัน) ใช้
$allตรงตัว ส่วน recommendation engine ที่แค่เช็คว่า tag ไหนบ้าง “มีอยู่” ใช้ query ค่าเดียวแบบ containment ก็พอ