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

การ query array

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

ในบทนี้สมาชิกแต่ละคนมี array borrowed ของชื่อหนังสือ array scores ของตัวเลข และบางคนมี array tags ของป้ายความสนใจ ทุก output ด้านล่างจับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน)

ข้อมูลถูกเลือกให้ filter ต่างกันให้คำตอบ ต่างกัน — นั่นคือวิธีพิสูจน์ว่า operator ทำตามที่คุณคิด:

mongosh/arr-seed.js
db.members.insertMany([
{ name: "Ada", borrowed: ["Compilers"], scores: [70, 95], tags: ["js", "backend"] },
{ name: "Grace", borrowed: ["Compilers", "Algorithms", "Databases"], scores: [70, 85, 95], tags: ["backend"] },
{ name: "Linus", borrowed: [], scores: [], tags: [] },
{ name: "Margaret", borrowed: ["Algorithms", "Compilers"], scores: [88], tags: ["js"] },
{ name: "Edsger", borrowed: ["Databases"], scores: [60, 75] }, // no tags field
{ name: "Barbara", borrowed: "Compilers", scores: 85 }, // scalars, not arrays
]);
globalThis.names = (f) => db.members.find(f, { _id: 0, name: 1 }).sort({ name: 1 }).toArray().map((d) => d.name).join(", ");
assert.strictEqual(db.members.countDocuments(), 6);

เพื่อถามว่า “ใครกำลังยืม Compilers อยู่?” ให้กรอง field array ด้วยค่าเหมือนเป็น scalar MongoDB มองเข้าไปใน array ให้:

mongosh/arr-element.js
console.log("borrowed 'Compilers' :", names({ borrowed: "Compilers" }));
console.log("borrowed ['Compilers'] (whole array):", names({ borrowed: ["Compilers"] }));
console.log("borrowed ['Algorithms','Compilers'] :", names({ borrowed: ["Algorithms", "Compilers"] }));
console.log("borrowed ['Compilers','Algorithms'] :", names({ borrowed: ["Compilers", "Algorithms"] }));
assert.strictEqual(names({ borrowed: "Compilers" }), "Ada, Barbara, Grace, Margaret");

สมาชิกที่ array มีชื่อนั้นตรงกัน ไม่ว่าอยู่ตำแหน่งใดในลิสต์ — และสังเกต Barbara ด้วย ที่ borrowed เป็น string "Compilers" ธรรมดา (scalar ที่เท่ากับค่าก็ตรงเช่นกัน) การส่ง array ทั้งก้อนให้ filter แทนหมายถึง array เท่ากันทุกประการ รวมลำดับ:

borrowed 'Compilers' : Ada, Barbara, Grace, Margaret
borrowed ['Compilers'] (whole array): Ada
borrowed ['Algorithms','Compilers'] : Margaret
borrowed ['Compilers','Algorithms'] :

["Compilers"] ตรงกับ Ada เท่านั้น ซึ่ง array เป็นแบบนั้นพอดี; ["Compilers","Algorithms"] ไม่ตรงใครเพราะ array ของ Margaret เก็บในลำดับตรงข้าม

นี่คือกับดัก สมมติ scores เก็บ array ของตัวเลขและคุณต้องการ document ที่มี element มากกว่า 80 และ น้อยกว่า 90 การเขียน { scores: { $gt: 80, $lt: 90 } } ไม่ได้ หมายถึงอย่างนั้น — มันตรงถ้า element บางตัว เกิน 80 และ element บางตัว (ที่อาจเป็นคนละตัว) ต่ำกว่า 90 เพื่อตรึงทั้งสองเงื่อนไขกับ element เดียว ใช้ $elemMatch seed มีสมาชิกสองคนที่ array แค่ ดูคล้าย กัน Ada [70, 95] และ Grace [70, 85, 95]:

mongosh/arr-elemmatch.js
console.log("plain {$gt: 80, $lt: 90} :", names({ scores: { $gt: 80, $lt: 90 } }));
console.log("$elemMatch {$gt: 80, $lt: 90} :", names({ scores: { $elemMatch: { $gt: 80, $lt: 90 } } }));
assert.strictEqual(names({ scores: { $gt: 80, $lt: 90 } }).includes("Ada"), true); // 95 is >80 and 70 is <90: two DIFFERENT elements
assert.strictEqual(names({ scores: { $elemMatch: { $gt: 80, $lt: 90 } } }), "Grace, Margaret");

สองรูปแบบคืนคนละคน:

plain {$gt: 80, $lt: 90} : Ada, Barbara, Grace, Margaret
$elemMatch {$gt: 80, $lt: 90} : Grace, Margaret

มีเพียง Grace (85) และ Margaret (88) ที่มี element หนึ่งตัว อยู่ในช่วง 81 ถึง 89 รูปแบบธรรมดายังคืน Ada ซึ่ง array [70, 95] ไม่มี element ในช่วง — 95 ผ่าน > 80 และแยกกัน 70 ผ่าน < 90 — และ Barbara ซึ่ง scores เป็น scalar 85 (scalar ถูกทดสอบกับทั้งสองเงื่อนไขโดยตรง)

กฎเดียวกันใช้กับ array ของ sub-document กับ order ที่ items เก็บ { price, qty } “สินค้าที่ราคาเกิน 1000 และจำนวนอย่างน้อย 2”:

mongosh/arr-elemmatch-subdocs.js
db.orders.insertMany([
{ _id: 1, items: [{ sku: "a", price: 1500, qty: 1 }, { sku: "b", price: 20, qty: 5 }] },
{ _id: 2, items: [{ sku: "c", price: 1500, qty: 3 }] },
]);
const ids = (f) => db.orders.find(f, { _id: 1 }).toArray().map((d) => d._id);
console.log("plain dotted paths :", ids({ "items.price": { $gt: 1000 }, "items.qty": { $gte: 2 } }));
console.log("$elemMatch :", ids({ items: { $elemMatch: { price: { $gt: 1000 }, qty: { $gte: 2 } } } }));
console.log("$all of 2 $elemMatch:", ids({ items: { $all: [{ $elemMatch: { price: { $gt: 1000 } } }, { $elemMatch: { qty: { $gte: 2 } } }] } }));
assert.strictEqual(ids({ items: { $elemMatch: { price: { $gt: 1000 }, qty: { $gte: 2 } } } }).join(), "2");
plain dotted paths : [ 1, 2 ]
$elemMatch : [ 2 ]
$all of 2 $elemMatch: [ 1, 2 ]

order 1 มีสินค้าแพง (qty 1) และสินค้า อีกชิ้น ที่ถูกแต่ qty 5: รูปแบบธรรมดาตรงผิด; $elemMatch คืนเฉพาะ order 2 อย่างถูกต้อง และ $all ที่ห่อสอง $elemMatch แยกกัน ไม่ใช่ ทางแก้ — มันคือ “element บางตัวสำหรับแต่ละเงื่อนไข” อีกครั้ง order 1 จึงตรง

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

mongosh/arr-all.js
console.log("borrowed $all [Algorithms, Compilers]:", names({ borrowed: { $all: ["Algorithms", "Compilers"] } }));
console.log("tags $all [js, backend] :", names({ tags: { $all: ["js", "backend"] } }));
console.log("tags $in [js, backend] :", names({ tags: { $in: ["js", "backend"] } }));
console.log("borrowed $all [] :", names({ borrowed: { $all: [] } }));
assert.strictEqual(names({ borrowed: { $all: ["Algorithms", "Compilers"] } }), "Grace, Margaret");

เฉพาะสมาชิกที่ถือทั้งสองชื่อถูกคืน; มีเพียงอันเดียวไม่พอ Grace ตรงด้วย — $all เมิน element เกินและลำดับ — และ $all คือ AND ที่ $in คือ OR:

borrowed $all [Algorithms, Compilers]: Grace, Margaret
tags $all [js, backend] : Ada
tags $in [js, backend] : Ada, Grace, Margaret
borrowed $all [] :

$all: [] ว่างตรง ไม่มีอะไรเลย (ไม่ใช่ “ทุกอย่าง”) กับดักเล็ก ๆ เมื่อสร้างลิสต์แบบ dynamic

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

mongosh/arr-size.js
console.log("borrowed $size 2 :", names({ borrowed: { $size: 2 } }));
console.log("borrowed $size 0 :", names({ borrowed: { $size: 0 } }));
console.log("tags $size 0 :", names({ tags: { $size: 0 } }), "| tags missing:", names({ tags: { $exists: false } }));
console.log("more than one book :", names({ "borrowed.1": { $exists: true } }));
let err;
try { db.members.find({ borrowed: { $size: { $gt: 1 } } }).toArray(); } catch (e) { err = e; }
console.log(err.name, err.code, err.message);
assert.strictEqual(err.code, 2);

สมาชิกที่ array borrowed มี element สองตัวพอดีตรงกัน $size รับ ตัวเลขตรง ๆ เท่านั้น — ช่วงคือ error จริง — และทางอ้อมสำหรับ “มากกว่าหนึ่ง” คือทดสอบว่า element ที่ index 1 มีอยู่:

borrowed $size 2 : Margaret
borrowed $size 0 : Linus
tags $size 0 : Linus | tags missing: Barbara, Edsger
more than one book : Grace, Margaret
MongoServerError 2 Failed to parse $size. Expected a number in: $size: { $gt: 1 }

สังเกตความต่างระหว่างว่างกับขาด: $size: 0 พบ Linus (array ว่าง) ไม่ใช่สมาชิกที่ไม่มี field tags

ตำแหน่งใน array เข้าถึงได้ด้วย path ตัวเลขคั่นจุด: borrowed.0 คือ element แรก borrowed.1 ตัวที่สอง เป็นต้น ใช้สิ่งนี้ query element ที่ตำแหน่งที่รู้ — “ใครที่หนังสือ เล่มแรก ที่ยืมคือ Algorithms?”:

mongosh/arr-index-path.js
console.log("borrowed.0 = Algorithms:", names({ "borrowed.0": "Algorithms" }));
assert.strictEqual(names({ "borrowed.0": "Algorithms" }), "Margaret");
borrowed.0 = Algorithms: Margaret

dot path เดียวกันใช้ได้เมื่อ array เก็บ sub-document — reviews.0.rating เข้าไปถึง field rating ของรีวิวแรก รูปแบบที่บทถัดไปสำรวจอย่างลึก

filter ตัดสิน document ที่กลับมา ไม่ใช่ element ของ array การตัด array ใช้ projection: positional $ (element แรกที่ filter ตรง), $elemMatch ใน projection หรือ $slice seed มี Grace ที่มีสามคะแนน:

mongosh/arr-project.js
const sorted = (c) => c.sort({ name: 1 }).toArray();
console.log("scores.$ :", sorted(db.members.find({ scores: { $gt: 80 } }, { _id: 0, name: 1, "scores.$": 1 })));
console.log("$elemMatch proj :", db.members.find({ name: "Grace" }, { _id: 0, name: 1, scores: { $elemMatch: { $gt: 80 } } }).toArray());
console.log("$slice 2 :", db.members.find({ name: "Grace" }, { _id: 0, name: 1, borrowed: { $slice: 2 } }).toArray());
assert.deepStrictEqual(Array.from(db.members.findOne({ name: "Grace", scores: { $gt: 80 } }, { "scores.$": 1 }).scores), [85]);
scores.$ : [
{ name: 'Ada', scores: [ 95 ] },
{ name: 'Barbara', scores: 85 },
{ name: 'Grace', scores: [ 85 ] },
{ name: 'Margaret', scores: [ 88 ] }
]
$elemMatch proj : [ { name: 'Grace', scores: [ 85 ] } ]
$slice 2 : [ { name: 'Grace', borrowed: [ 'Compilers', 'Algorithms' ] } ]

positional $ คืน element แรก ที่ผ่าน filter (95 สำหรับ Ada คะแนนแรกที่เกิน 80; 85 สำหรับ Grace แม้ 95 ก็ผ่านเช่นกัน); $elemMatch และ $slice ใน projection อิสระจาก filter positional $ ต้องการให้ array ปรากฏใน query

ใน Compass: filter ของ array ใส่ในช่อง Filter เหมือน scalar เช่น { borrowed: { $size: 2 } } (เอกสาร Compass, mongodb.com/docs/compass/current/query/filter, มีตัวอย่าง $elemMatch ด้วย); ขยาย array ที่ตรงในมุมมอง document เพื่อดูแต่ละ element

ได้ — index บน field array กลายเป็น multikey index มีหนึ่งรายการต่อ element สร้าง { borrowed: 1 } แล้ว explain query แบบ element บนหกเอกสารของเรา:

mongosh/arr-multikey.js
db.members.createIndex({ borrowed: 1 });
const e = db.members.find({ borrowed: "Compilers" }).explain("executionStats");
const ix = e.queryPlanner.winningPlan.inputStage;
console.log("stage:", ix.stage, "| isMultiKey:", ix.isMultiKey, "| keys", e.executionStats.totalKeysExamined, "| docs", e.executionStats.totalDocsExamined);
assert.strictEqual(ix.isMultiKey, true);
stage: IXSCAN | isMultiKey: true | keys 4 | docs 4

โมดูล indexes ครอบคลุมกฎ multikey (รวม “cannot index parallel arrays”)

  • ทำไม $elemMatch สำคัญ: ถ้าไม่มี หลายเงื่อนไขบน field array ถูกตรวจ ข้ามทุก element { scores: { $gt: 80, $lt: 90 } } จึงตรงกับ [70, 95] (Ada) $elemMatch บังคับทุกเงื่อนไขกับ element เดียว ซึ่งเกือบทุกครั้งคือสิ่งที่คุณตั้งใจ
  • ค่า array ทั้งก้อนหมายถึงความเท่ากัน ไม่ใช่การบรรจุ { borrowed: ["Compilers"] } ตรงเฉพาะ array ที่เป็นแบบนั้นพอดีในลำดับนั้น
  • $all ไม่สนลำดับ และเมิน element เกิน; $all: [] ไม่ตรงอะไร $size ในทางตรงข้าม กำหนดความยาวพอดี
  • $size รับตัวเลขตรง ๆ เท่านั้น (Failed to parse $size. Expected a number) สำหรับ “มากกว่าหนึ่ง element” ทดสอบ "field.1": { $exists: true }
  • filter ค่าเดียวบน array ตรงแบบบรรจุ — และตรง scalar ที่เท่ากับค่านั้นด้วย (Barbara)
  • filter ไม่ตัด array ใช้ positional $, $elemMatch ใน projection หรือ $slice

ผู้ช่วย AI ที่ถูกขอ “สมาชิกที่มีคะแนนแบนด์ B ระหว่าง 80 ถึง 90” จะเขียน filter scalar ที่เป็นธรรมชาติ:

🐛 หา bug ในโค้ดนี้

// AI comment: "students with a score between 80 and 90"
const bBand = await db.collection("members")
.find({ scores: { $gt: 80, $lt: 90 } })
.toArray();
await sendCongratulations(bBand);
เฉลย

บน field array สองเงื่อนไขถูกทำให้เป็นจริงแยกกันโดยคนละ element บนข้อมูลของเรามันคืน สี่ สมาชิก — Ada [70, 95], Barbara 85, Grace [70, 85, 95] และ Margaret [88] — ทั้งที่มีเพียง Grace และ Margaret ที่มีคะแนนในช่วง Ada จะได้รับคำแสดงความยินดีสำหรับคะแนน 95 และ 70 แต่ไม่มี B ทางแก้คือตรึงทั้งสองขอบกับ element เดียว:

mongosh/arr-bband-fix.js
const bug = names({ scores: { $gt: 80, $lt: 90 } });
const fixed = names({ scores: { $elemMatch: { $gt: 80, $lt: 90 } } });
console.log("plain :", bug);
console.log("$elemMatch :", fixed);
assert.strictEqual(bug, "Ada, Barbara, Grace, Margaret");
assert.strictEqual(fixed, "Grace, Margaret");
plain : Ada, Barbara, Grace, Margaret
$elemMatch : Grace, Margaret

(scalar 85 ของ Barbara หลุดจาก $elemMatch เพียงเพราะไม่ใช่ element ของ array; ถ้ามี scalar ปนกับ array ใน field เดียว ให้ทำ field ให้เป็นปกติก่อน — schema validation ในโมดูลถัดไปป้องกันได้)

field array คือ เซ็ตของค่าอิสระที่ filter เห็นพร้อมกันหมด แต่ละเงื่อนไขถาม “element ใด ผ่านฉันไหม?” และเงื่อนไขต่างกันถูกทำให้เป็นจริงโดยคนละ element ได้ $elemMatch รวมเงื่อนไขเป็นคำถามเดียวเกี่ยวกับ element เดียว; $all AND คำถามการเป็นสมาชิก; $size และ index path ถามเรื่องรูปร่าง query เลือก document; projection ($, $elemMatch, $slice) เลือก element

ตัวเลือกBenefitCost
$elemMatch สำหรับหลายเงื่อนไขบังคับทุกเงื่อนไขเป็นจริงบน element เดียว ตรงเจตนาจริงเกือบทุกครั้งเขียนมากขึ้นเล็กน้อย; ต้องคิดเรื่อง “element เดียวกัน” ตั้งแต่แรก
operator แยกบน field เดียวกัน (ไม่มี $elemMatch)เขียนสั้นกว่า อ่านเหมือน query scalar ธรรมดาแต่ละเงื่อนไขผ่านด้วยคนละ element ได้ ทำให้ตรงโดยไม่ตั้งใจ (Ada ด้านบน) — bug เงียบที่พบบ่อย
$allแสดง “มีครบทุกค่าเหล่านี้”เมิน element เกินและลำดับ; ลิสต์ว่างไม่ตรงอะไร
query ตำแหน่ง/ความยาว ($size, "field.0")เจาะตำแหน่งหรือความยาวโดยตรงในการเรียกเดียวเปราะเมื่อ array เรียงลำดับใหม่ และ $size รับแค่ค่าตรง ๆ ไม่ใช่ช่วง
multikey index บน arrayquery แบบ element ใช้ IXSCAN (4 key สำหรับ 4 ผลลัพธ์)ขนาด index และต้นทุนการเขียนโตตามความยาว array; ข้อจำกัดของ compound multikey index
  • “filter ตัด array ที่คืนให้เหลือ element ที่ตรง” — มันแค่ตัดสินว่าคืนทั้ง document หรือไม่ ใช้ scores.$, $elemMatch ใน projection หรือ $slice
  • “borrowed.0 มีความหมายทางธุรกิจคงที่” — ลำดับ array เปลี่ยนทุก $push/$pull; query ตามค่าทนทานกว่า
  • “$all กับหลาย $elemMatch ตรึง element เดียว” — แต่ละ clause ถูกทำให้เป็นจริงโดยคนละ element ได้ (order 1 ในการรันด้านบนตรง)
  • “{ field: ['a'] } แปลว่ามี ‘a’” — แปลว่า array เท่ากับ ['a']
  • “$size รับการเปรียบเทียบได้” — ไม่ได้: Failed to parse $size. Expected a number
  • “$size: 0 พบ document ที่ไม่มี field” — พบเฉพาะ array ว่าง; $exists: false พบอันที่ไม่มี

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

บทช่วยสอน array ของคู่มือ MongoDB อธิบายทั้งสองพฤติกรรมที่เราวัด: compound filter ตรงกับ array ที่ “elements … in some combination satisfy the query conditions. One element can satisfy the greater than 15 condition and another element can satisfy the less than 20 condition, or a single element can satisfy both”; ส่วน $elemMatch คือ “so that at least one array element satisfies all the specified criteria” (Query an Array, เรียกอ่านเมื่อ 2026-09-30)

แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกันและคืนสมาชิกชุดเดียวกัน (borrowed: "Compilers", $elemMatch, $all, $size) แต่ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส

คุณกรอง field array borrowed ด้วยค่าธรรมดา "Compilers" อะไรตรง?
ทำไมใช้ $elemMatch แทนสองเงื่อนไขโดยตรงบน field array?
{ borrowed: { $all: ["Algorithms", "Compilers"] } } ตรงกับอะไร?
จะตรงกับ document ที่ array มีสาม element พอดีอย่างไร?
order 1 มี items [{price 1500, qty 1}, {price 20, qty 5}] { "items.price": {$gt: 1000}, "items.qty": {$gte: 2} } ตรงกับมันไหม?
ต้องการให้ array ถูกตัดเหลือ element แรกที่ตรง scores > 80 ใช้อะไร?