Comparison และ logical operator
filter แบบจับคู่ตรงเป๊ะ — field เท่ากับค่า — พาคุณไปได้แค่ระดับหนึ่ง คำถามจริง ๆ คือช่วงและเซ็ต: “ใครค้างค่าปรับมากกว่าสามครั้ง?”, “ใครเข้าร่วมปี 2022 หรือ 2023?”, “ระเบียนไหนขาด field ไปเลย?” MongoDB ตอบสิ่งเหล่านี้ด้วย query operator คือ key พิเศษที่ขึ้นต้นด้วยเครื่องหมายดอลลาร์และอยู่ ข้างใน ตำแหน่งค่าของ field รูปแบบเหมือนเดิมเสมอ: แทนที่จะให้ค่าธรรมดากับ field คุณให้ document เล็ก ๆ ที่ key คือ operator
เรายังคง query members ของห้องสมุดต่อไป ทุก output ด้านล่างจับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน) ข้อมูลของบทนี้จงใจให้ค่อนข้างรก — สมาชิกหนึ่งคนไม่มี field fines หนึ่งคนมี fines: null หนึ่งคนมี joined เป็น string — เพราะนั่นคือจุดที่ operator ทำให้คนแปลกใจ
ข้อมูลที่เรา query
หัวข้อที่มีชื่อว่า “ข้อมูลที่เรา query”db.members.insertMany([ { name: "Ada", joined: 2024 }, // no fines field at all { name: "Grace", joined: 2021, fines: 3 }, { name: "Linus", joined: 2023, fines: 5 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Edsger", joined: 2022, fines: 0 }, { name: "Barbara", joined: 2025, fines: null }, // fines exists, but null { name: "Alan", joined: "2020", fines: 1 }, // joined stored as a string by mistake]);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(), 7);await db.collection("members").insertMany([ { name: "Ada", joined: 2024 }, { name: "Grace", joined: 2021, fines: 3 }, { name: "Linus", joined: 2023, fines: 5 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Edsger", joined: 2022, fines: 0 }, { name: "Barbara", joined: 2025, fines: null }, { name: "Alan", joined: "2020", fines: 1 },]);if ((await db.collection("members").countDocuments()) !== 7) throw new Error("expected 7");Comparison operator
หัวข้อที่มีชื่อว่า “Comparison operator”ตระกูล comparison เทียบ field กับค่า: $eq (เท่ากับ), $ne (ไม่เท่ากับ), $gt (มากกว่า), $gte (มากกว่าหรือเท่ากับ), $lt (น้อยกว่า) และ $lte (น้อยกว่าหรือเท่ากับ) การเขียนค่าธรรมดาเป็นเพียงชวเลขของ $eq ที่นี่เราขอสมาชิกทุกคนที่ค้างค่าปรับมากกว่าสอง:
const docs = db.members.find({ fines: { $gt: 2 } }, { _id: 0 }).sort({ name: 1 }).toArray();console.log(docs);assert.strictEqual(docs.map((d) => d.name).join(), "Grace,Linus");const docs = await db.collection("members") .find({ fines: { $gt: 2 } }) .toArray();console.log(docs.map((d) => d.name).sort());if (docs.length !== 2) throw new Error("expected 2");docs = list(db.members.find({"fines": {"$gt": 2}}))print(docs)cur, err := coll.Find(ctx, bson.M{"fines": bson.M{"$gt": 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! { "fines": { "$gt": 2 } }) .await?;while let Some(doc) = cursor.try_next().await? { println!("{:?}", doc);}เฉพาะสมาชิกที่เกินเกณฑ์กลับมา (ค่าที่ขาด, null และค่าชนิดอื่นไม่เคยผ่าน $gt):
[ { name: 'Grace', joined: 2021, fines: 3 }, { name: 'Linus', joined: 2023, fines: 5 }]คุณรวมสอง comparison บน field เดียวกัน ได้โดยใส่ทั้งสอง operator ใน document เดียว — ที่นี่คือสมาชิกที่เข้าร่วมในช่วงปี และสังเกตว่า $gt: 2000 ทำอะไรกับสมาชิกที่ joined เป็น string "2020":
console.log("joined 2022..2023 :", names({ joined: { $gte: 2022, $lte: 2023 } }));console.log("joined > 2000 :", names({ joined: { $gt: 2000 } })); // Alan's "2020" is a stringconsole.log("joined type string :", names({ joined: { $type: "string" } }));assert.strictEqual(names({ joined: { $gt: 2000 } }).includes("Alan"), false);assert.strictEqual(names({ joined: { $type: "string" } }), "Alan");joined 2022..2023 : Edsger, Linus, Margaretjoined > 2000 : Ada, Barbara, Edsger, Grace, Linus, Margaretjoined type string : AlanMongoDB เทียบตัวเลขกับตัวเลขเท่านั้น "2020" ที่ชนิดผิดจึงหายไปเงียบ ๆ จากทุกช่วงตัวเลข — ไม่ error และไม่ถูกเรียงเป็น 2020 (กฎการแบ่งกลุ่มชนิดข้อมูลข้อเดียว: query แบบช่วงเห็นเฉพาะค่าที่ชนิดเดียวกับ operand)
การเป็นสมาชิก: $in และ $nin
หัวข้อที่มีชื่อว่า “การเป็นสมาชิก: $in และ $nin”$in ตรงเมื่อ field เท่ากับค่า ใดค่าหนึ่ง ในลิสต์ และ $nin ตรงเมื่อเท่ากับ ไม่มีค่าใดเลย สะอาดกว่าการต่อ $or ของ equality มาก ที่นี่เราดึงสมาชิกที่เข้าร่วมปี 2021 หรือ 2023:
console.log("joined $in [2021,2023]:", names({ joined: { $in: [2021, 2023] } }));console.log("fines $nin [0,5] :", names({ fines: { $nin: [0, 5] } }));assert.strictEqual(names({ joined: { $in: [2021, 2023] } }), "Grace, Linus, Margaret");const docs = await db.collection("members") .find({ joined: { $in: [2021, 2023] } }) .toArray();console.log(docs.map((d) => d.name).sort());if (docs.length !== 3) throw new Error("expected 3");docs = list(db.members.find({"joined": {"$in": [2021, 2023]}}))cur, err := coll.Find(ctx, bson.M{"joined": bson.M{"$in": bson.A{2021, 2023}}})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "joined": { "$in": [2021, 2023] } }) .await?;ทั้งสองปีที่ตรงกลับมาพร้อมกัน — และดู $nin: มันคืนสมาชิกสี่คน รวม Ada (ไม่มี fines เลย), Barbara (null) และ Alan (1) เพราะ “ไม่อยู่ใน [0, 5]” เป็นจริงกับทุกอย่างที่ไม่ใช่ 0 หรือ 5 เป๊ะ:
joined $in [2021,2023]: Grace, Linus, Margaretfines $nin [0,5] : Ada, Alan, Barbara, Graceการรวมเงื่อนไข: $and, $or, $not, $nor
หัวข้อที่มีชื่อว่า “การรวมเงื่อนไข: $and, $or, $not, $nor”หลายคู่ field/ค่าใน filter เดียวหมายถึง AND อยู่แล้ว $and จึงจำเป็นเมื่อคุณต้องเขียน field เดียวกันสองครั้ง $or คือตัวที่ใช้ทุกวัน — รับลิสต์ของ filter และตรงถ้า ข้อใดข้อหนึ่ง เป็นจริง ที่นี่เราหาสมาชิกที่ไม่ค้างเลยหรือเข้าร่วมปี 2021:
console.log("$or fines 0 / joined 2021:", names({ $or: [{ fines: 0 }, { joined: 2021 }] }));console.log("$nor (same branches) :", names({ $nor: [{ fines: 0 }, { joined: 2021 }] }));console.log("$not { $gt: 2 } :", names({ fines: { $not: { $gt: 2 } } }));assert.strictEqual(names({ $or: [{ fines: 0 }, { joined: 2021 }] }), "Edsger, Grace, Margaret");const docs = await db.collection("members").find({ $or: [{ fines: 0 }, { joined: 2021 }]}).toArray();console.log(docs.map((d) => d.name).sort());if (docs.length !== 3) throw new Error("expected 3");docs = list(db.members.find({ "$or": [{"fines": 0}, {"joined": 2021}]}))cur, err := coll.Find(ctx, bson.M{ "$or": bson.A{ bson.M{"fines": 0}, bson.M{"joined": 2021}, },})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "$or": [ doc! { "fines": 0 }, doc! { "joined": 2021 }, ] }) .await?;ใครก็ตามที่ตรงกิ่งใดกิ่งหนึ่งปรากฏ $nor คือด้านตรงข้ามเชิงตรรกะของ $or — และสังเกตว่ามัน รวมสมาชิกที่ไม่มี field เลย (Ada) หรือมันเป็น null (Barbara): “ไม่มีกิ่งใดเป็นจริง” เป็นจริงเมื่อไม่มีอะไรให้เทียบ $not กลับด้านนิพจน์ operator เดียวและตรงกับ field ที่ขาดเช่นกัน:
$or fines 0 / joined 2021: Edsger, Grace, Margaret$nor (same branches) : Ada, Alan, Barbara, Linus$not { $gt: 2 } : Ada, Alan, Barbara, Edsger, Margaret$not ห่อได้เฉพาะ นิพจน์ operator การส่งค่าเปล่า ๆ ให้ถูก server ปฏิเสธ:
> db.members.find({ fines: { $not: 5 } }).toArray()MongoServerError: $not argument must be a regex or an object> db.members.find({ fines: { $not: { $eq: 5 } } }) -> Ada, Alan, Barbara, Edsger, Grace, Margaretการทดสอบ field: $exists และ $type
หัวข้อที่มีชื่อว่า “การทดสอบ field: $exists และ $type”document ใน collection เดียวกันไม่จำเป็นต้องมี field เหมือนกันทั้งหมด $exists ถามว่า field มีอยู่หรือไม่ ไม่ว่าค่าเป็นอะไร และ $type ถามว่าค่าเป็น BSON type อะไร ที่นี่เราหาสมาชิกที่ ยังไม่มี field fines — และเทียบกับ null:
console.log("fines $exists false :", names({ fines: { $exists: false } }));console.log("fines null :", names({ fines: null }));console.log("fines $type null :", names({ fines: { $type: "null" } }));console.log("joined $type number :", names({ joined: { $type: "number" } }));console.log("joined $type [int,double]:", names({ joined: { $type: ["int", "double"] } }));assert.strictEqual(names({ fines: { $exists: false } }), "Ada");assert.strictEqual(names({ fines: null }), "Ada, Barbara");const docs = await db.collection("members") .find({ fines: { $exists: false } }) .toArray();console.log(docs.map((d) => d.name));if (docs.length !== 1) throw new Error("expected 1");docs = list(db.members.find({"fines": {"$exists": False}}))cur, err := coll.Find(ctx, bson.M{"fines": bson.M{"$exists": false}})if err != nil { return err}var docs []bson.Mif err := cur.All(ctx, &docs); err != nil { return err}let mut cursor = coll .find(doc! { "fines": { "$exists": false } }) .await?;สามคำถามต่างกัน สามคำตอบต่างกัน:
fines $exists false : Adafines null : Ada, Barbarafines $type null : Barbarajoined $type number : Ada, Barbara, Edsger, Grace, Linus, Margaretjoined $type [int,double]: Ada, Barbara, Edsger, Grace, Linus, Margaret{ fines: null } ตรงกับ ทั้ง field ที่ขาด (Ada) และ null ที่ระบุชัด (Barbara); $exists: false คือเฉพาะที่ขาด; $type: "null" คือเฉพาะ null ที่ระบุชัด $type รับได้ทั้ง alias ของ type ("int", "string", "number" = ตัวเลขทุกชนิด) หรือ array ของ alias
ใน Compass: พิมพ์ filter เหล่านี้ลงในช่อง Filter ที่ด้านบนของ tab Documents เช่น { fines: { $gt: 2 } } แล้วกด Find (Compass รับทุก query operator ยกเว้น $text และ $expr ตามเอกสารของมัน, mongodb.com/docs/compass/current/query/filter)
planner ทำอะไรกับแต่ละตัว
หัวข้อที่มีชื่อว่า “planner ทำอะไรกับแต่ละตัว”บทเก่าบอกว่า $ne ทำให้ planner “มักเลือกไม่ใช้ index” เราวัดบน 20 000 document ที่มี index เดี่ยวบน city และอีกอันบน joined; explain("executionStats") รายงาน key และ document ที่ตรวจ:
const cities = ["Berlin", "Cambridge", "Helsinki", "Austin", "Boston"];db.big.insertMany(Array.from({ length: 20000 }, (_, i) => ({ i, city: cities[i % 5], joined: 2020 + (i % 6), fines: i % 4 })));db.big.createIndex({ city: 1 });db.big.createIndex({ joined: 1 });const stage = (p) => p.inputStage ? p.stage + " > " + stage(p.inputStage) : (p.inputStages ? p.stage + " > [" + p.inputStages.map(stage).join(", ") + "]" : p.stage);const stats = (label, cursor) => { const e = cursor.explain("executionStats"), es = e.executionStats, wp = e.queryPlanner.winningPlan; console.log(label.padEnd(26), stage(wp.queryPlan ?? wp), "| returned", es.nReturned, "| keys", es.totalKeysExamined, "| docs", es.totalDocsExamined); return es;};stats("city = Berlin", db.big.find({ city: "Berlin" }));const ne = stats("city $ne Berlin", db.big.find({ city: { $ne: "Berlin" } }));stats("city $nin [Berlin,Boston]", db.big.find({ city: { $nin: ["Berlin", "Boston"] } }));stats("joined $gte 2025", db.big.find({ joined: { $gte: 2025 } }));stats("joined $in [2021,2023]", db.big.find({ joined: { $in: [2021, 2023] } }));stats("$or joined 2021 / 2023", db.big.find({ $or: [{ joined: 2021 }, { joined: 2023 }] }));const noIdx = stats("$or city / fines (no idx)", db.big.find({ $or: [{ city: "Berlin" }, { fines: 3 }] }));stats("$or city / joined", db.big.find({ $or: [{ city: "Berlin" }, { joined: 2021 }] }));assert.strictEqual(ne.totalKeysExamined, 16001);assert.strictEqual(noIdx.totalDocsExamined, 20000);city = Berlin FETCH > IXSCAN | returned 4000 | keys 4000 | docs 4000city $ne Berlin FETCH > IXSCAN | returned 16000 | keys 16001 | docs 16000city $nin [Berlin,Boston] FETCH > IXSCAN | returned 12000 | keys 12002 | docs 12000joined $gte 2025 FETCH > IXSCAN | returned 3333 | keys 3333 | docs 3333joined $in [2021,2023] FETCH > IXSCAN | returned 6667 | keys 6669 | docs 6667$or joined 2021 / 2023 FETCH > IXSCAN | returned 6667 | keys 6669 | docs 6667$or city / fines (no idx) SUBPLAN > COLLSCAN | returned 8000 | keys 0 | docs 20000$or city / joined SUBPLAN > FETCH > OR > [IXSCAN, IXSCAN] | returned 6668 | keys 7334 | docs 6668สิ่งที่แสดง:
$neและ$ninใช้ index (IXSCAN) — planner เดินช่วงทั้งสองข้างของค่าที่ยกเว้น สิ่งที่ทำไม่ได้คือ เลือกสรร:$ne "Berlin"ตรวจ 16 001 key และ fetch 16 000 document จาก 20 000 มันคือการสแกน index ที่ทำตัวเหมือนสแกนเกือบทั้งหมด ซึ่งเป็นเวอร์ชันที่ถูกต้องของคำเตือนเก่า$inบน field เดียวกับ$orของ equality บน field เดียวกันให้ plan เดียวกัน (FETCH > IXSCAN, 6 669 key) — optimizer เขียนอันหนึ่งเป็นอีกอัน เลือกอันที่อ่านง่ายกว่า$orต้องการ index สำหรับ ทุก กิ่งจึงจะใช้ได้: เมื่อมี index บนทั้งcityและjoinedมันรันORของสองIXSCAN; เมื่อกิ่งหนึ่ง (fines: 3) ไม่มี index ทั้ง query ถอยไปเป็นCOLLSCANและตรวจทั้ง 20 000 document
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- ค่าธรรมดาคือ
$eqการเขียน{ fines: 0 }และ{ fines: { $eq: 0 } }เหมือนกัน ใช้ operator ชัดเจนเมื่อต้องการเทียบแบบอื่นนอกจากเท่ากับ $ne,$nin,$norและ$notตรงกับ field ที่ขาดด้วย พิสูจน์ด้านบน:fines: { $ne: 0 }ตรง Ada (ไม่มี field) และ Barbara (null) ถ้าหมายถึง “มี field fines และไม่ใช่ 0” ให้ใช้$existsร่วมหรือใช้$gt- operator ช่วงเห็นเฉพาะชนิดของตัวเอง
{ joined: { $gt: 2000 } }ข้าม string"2020"; ค่าที่ชนิดผิดซ่อนจากช่วงตัวเลขอย่างเงียบ ๆ - สอง operator บน field เดียวใส่ใน document เดียว query ช่วงอย่าง
{ $gte: 2022, $lte: 2023 }คือ document ค่าเดียว ไม่ใช่สอง filter — การเขียน key field เดียวกันซ้ำที่ระดับบนทิ้งอันแรก (ดูบท find) - ใช้
$inแทน$orของ equality หลายตัว ได้ plan เดียวกันที่นี่ แต่$inสั้นกว่าและอ่านง่ายกว่า
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ขอผู้ช่วย AI ว่า “สมาชิกที่ติดหนี้” คุณมักเห็นรูปแบบปฏิเสธ:
🐛 หา bug ในโค้ดนี้
// AI comment: "everyone whose fines are not zero owes money"const debtors = await db.collection("members").find({ fines: { $ne: 0 } }).toArray();await sendReminderEmails(debtors);เฉลย
$ne: 0 ตรงกับทุก document ที่ fines ขาดหรือเป็น null ด้วย บนสมาชิกเจ็ดคนของเรา มันคืน ห้า ชื่อ — Ada (ไม่มี field), Alan, Barbara (null), Grace และ Linus — ทั้งที่มีเพียงสามคน (Alan 1, Grace 3, Linus 5) ที่ค้างจริง Ada และ Barbara จะได้อีเมลเตือนสำหรับหนี้ที่ไม่มี ถามคำถามเชิงบวกแทน:
const bug = names({ fines: { $ne: 0 } });const fixed = names({ fines: { $gt: 0 } });console.log("$ne 0 :", bug);console.log("$gt 0 :", fixed);assert.strictEqual(bug, "Ada, Alan, Barbara, Grace, Linus");assert.strictEqual(fixed, "Alan, Grace, Linus");$ne 0 : Ada, Alan, Barbara, Grace, Linus$gt 0 : Alan, Grace, Linus$gt: 0 ตรงเฉพาะตัวเลขที่มากกว่าศูนย์ ค่าที่ขาดและ null จึงหลุดไปเอง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”query operator ทุกตัวตอบว่า “ค่า ที่ path นี้ ผ่าน predicate ไหม?” — และ path ที่ขาดหรือค่าชนิดอื่นเป็น input ที่ถูกต้องซึ่ง predicate ต้องตัดสิน operator เชิงบวก ($eq, $gt, $in, $exists: true) ไม่ผ่านกับค่าที่ขาด/ชนิดผิด; operator เชิงลบ ($ne, $nin, $not, $nor) ผ่าน เมื่อผลลัพธ์กว้างเกินไป ให้มองหา operator เชิงลบ; เมื่อแคบเกินไป ให้มองหาชนิดที่ไม่ตรง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
filter แบบ equality ($eq หรือค่าธรรมดา) | เลือกสรรที่สุด ใช้ index เต็มที่ (4 000 key สำหรับ 4 000 ผลลัพธ์) | ตอบคำถามช่วงหรือเซ็ตไม่ได้ |
operator ช่วง/เซ็ต ($gt, $gte, $lt, $lte, $in) | ตอบคำถามช่วง/เซ็ต ยัง index ได้ถ้า field มี index ที่ตรงกัน | selectivity ต่ำกว่า equality โดยเฉพาะช่วงกว้าง — อ่าน key และ document มากกว่าจำเป็น |
operator เชิงลบ ($ne, $nin, $nor, $not) | แสดง “ทุกอย่างยกเว้น” ตรง ๆ | ตรงกับค่าที่ขาด/null/ชนิดผิด; ใช้ index แต่ตรวจเกือบทั้งหมด (16 001 key สำหรับ $ne) |
รวมด้วย $and/$or/$nor | ตอบคำถามตรรกะซับซ้อนในการเรียกเดียว | $or ต้องมี index ทุกกิ่ง ไม่เช่นนั้นถอยเป็นสแกนทั้ง collection (ตรวจ 20 000 document) |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
$ne/$ninข้าม index” — ใช้ แต่ selectivity เกือบศูนย์ (ตรวจ 16 001 key สำหรับ 16 000 ผลลัพธ์) - “
$orข้ามหลาย field ครอบคลุมด้วย index เดียว” — แต่ละกิ่งต้องมี index ของตัวเอง; กิ่งเดียวที่ไม่มี index ทำให้ทั้ง query เป็นCOLLSCAN - “
$not: 5ใช้ได้” — server ตอบ$not argument must be a regex or an object; ห่อ operator:$not: { $eq: 5 } - “
{ field: null }แปลว่า field เป็น null” — มันตรงกับ document ที่ field ขาดด้วย - “ตัวเลขที่เก็บเป็น string ยังอยู่ในช่วงตัวเลข” —
"2020"หายไปจาก$gt: 2000 - “
$norหา document ที่ไม่ตรง” — มันตรงกับ document ที่ไม่มีกิ่งใดเป็นจริง ซึ่งรวม document ที่ขาด field เหล่านั้นไปเลย
ตัวอย่างจากของจริง
หน้า query predicates ของคู่มือ MongoDB จัดกลุ่ม operator เหล่านี้พอดี — “Comparison” (
$eq,$gt,$in…), “Logical” ($and,$or,$nor,$not) และ “Data Type” ($exists,$typeคืนข้อมูล “based on field existence or data types”) — ควบคู่กับตระกูล array, bitwise และ geospatial ที่บทถัดไปและโมดูลหลังจะพบ (Query predicates, เรียกอ่านเมื่อ 2026-09-30)
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกันและคืนสมาชิกชุดเดียวกัน ($gt: Grace และ Linus; $in: Grace, Linus, Margaret; $or: Grace, Margaret, Edsger; $exists: false: Ada) แต่ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส