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

การ query array

field ของ document เก็บ array ได้ และการ query array ก็มีเซอร์ไพรส์น่ารักติดมาด้วย: เมื่อคุณ filter field ที่เป็น array กับค่าธรรมดา MongoDB จะตรวจ ทุก element และจับคู่ถ้า มีอันใด ผ่านเงื่อนไข กฎข้อเดียวนี้ทำให้ query array แบบง่ายอ่านได้เหมือนกับ query แบบ scalar ทุกประการ ความซับซ้อนจะปรากฏก็ต่อเมื่อคุณต้องการให้หลายเงื่อนไขเป็นจริงบน element เดียวกัน และตรงนั้นเองคือจุดที่ $elemMatch ได้ออกโรง

ในบทเรียนนี้สมาชิกแต่ละคนถือ array borrowed ของชื่อหนังสือ และบางคนถือ array tags ของป้ายความสนใจ

หากต้องการถามว่า “ใครยืม Compilers อยู่ตอนนี้?” ให้ filter field ที่เป็น array ด้วยค่านั้นเหมือนกับว่าเป็น scalar MongoDB จะมองเข้าไปใน array ให้คุณเอง:

db.members.find({ borrowed: "Compilers" })

สมาชิกคนใดก็ตามที่ array มีชื่อหนังสือนั้นจะ match หมด ไม่ว่าชื่อนั้นจะอยู่ตำแหน่งไหนในรายการ:

[
{
"_id": "65f0b3d4e4b0a1c2e4b0a1d2",
"name": "Margaret",
"borrowed": ["Algorithms", "Compilers"]
}
]

นี่คือกับดัก สมมติว่า 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 } }
})

เฉพาะ document ที่มี element เดียวอยู่ในช่วง 81–89 เท่านั้นที่ตรง — ตรงนี้ 85 ผ่านเกณฑ์ แม้ว่า array จะมีค่าที่อยู่นอกช่วงด้วย:

[
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "scores": [70, 85, 95] }
]

$all จับคู่เมื่อ array มี ทุกค่า ที่ระบุไว้ ไม่สนลำดับ พูดง่าย ๆ คือ AND ของการเป็นสมาชิก “ใครยืมทั้ง Algorithms และ Compilers พร้อมกัน?” ก็คือสิ่งนี้พอดี:

db.members.find({
borrowed: { $all: ["Algorithms", "Compilers"] }
})

เฉพาะสมาชิกที่ถือทั้งสองชื่อหนังสือเท่านั้นที่ส่งกลับมา การมีเพียงอันเดียวยังไม่พอ:

[
{
"_id": "65f0b3d4e4b0a1c2e4b0a1d2",
"name": "Margaret",
"borrowed": ["Algorithms", "Compilers"]
}
]

$size จับคู่ array ที่มีความยาวตรงเป๊ะ “ใครยืมหนังสืออยู่สองเล่มพอดีตอนนี้?” คือ query operator เดียว:

db.members.find({ borrowed: { $size: 2 } })

สมาชิกที่ array borrowed มี element สองตัวพอดีจะตรง:

[
{
"_id": "65f0b3d4e4b0a1c2e4b0a1d2",
"name": "Margaret",
"borrowed": ["Algorithms", "Compilers"]
}
]

ตำแหน่งใน 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"]
ตัวเลือกBenefitCost
$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 ก็พอ

คุณ filter field ที่เป็น array ชื่อ borrowed กับค่าธรรมดา "Compilers" อะไรจะตรง?
ทำไมจึงใช้ $elemMatch แทนที่จะใส่สองเงื่อนไขบน field ที่เป็น array โดยตรง?
{ borrowed: { $all: ["Algorithms", "Compilers"] } } จับคู่อะไร?
คุณจะจับคู่ document ที่ array มี element สามตัวพอดีได้อย่างไร?