การค้นหา document
การอ่านคือจุดที่ฐานข้อมูลพิสูจน์คุณค่า และ MongoDB ให้ประตูสองบานเข้าสู่ห้องเดียวกัน findOne ใช้เมื่อคุณคาดว่าจะได้ document เดียว — “สมาชิกชื่อ Grace คือใคร?” — และส่ง document นั้นมาหรือไม่ส่งอะไรเลย find ใช้เมื่ออาจได้หลายรายการ — “สมาชิกคนไหนเข้าร่วมในปี 2023?” — และส่ง cursor ที่คุณวนอ่านได้ ทั้งสองรับ filter ชนิดเดียวกัน คือ document ที่อธิบายว่าต้องการระเบียนไหน
ทุก output ด้านล่างจับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน) object id บนเครื่องของคุณจะต่างออกไป
ข้อมูลที่เรา query
หัวข้อที่มีชื่อว่า “ข้อมูลที่เรา query”แต่ละบทเรียน seed ข้อมูลของตัวเอง คุณจึงเปิดบทไหนก็ได้โดยลำพัง นี่คือสมาชิกห้องสมุดหกคน — รูปร่างเดียวกันตลอดคอร์ส:
db.members.insertMany([ { name: "Ada", joined: 2023, fines: 0, city: "Cambridge", borrowed: ["Compilers"] }, { name: "Grace", joined: 2024, fines: 0, city: "Arlington", borrowed: ["Compilers", "Algorithms"] }, { name: "Linus", joined: 2023, fines: 2, city: "Helsinki", borrowed: [] }, { name: "Margaret", joined: 2023, fines: 0, city: "Cambridge", borrowed: ["Algorithms"] }, { name: "Edsger", joined: 2022, fines: 5, city: "Austin", borrowed: ["Compilers", "Algorithms", "Databases"] }, { name: "Barbara", joined: 2025, fines: 1, city: "Boston", borrowed: [] },]);assert.strictEqual(db.members.countDocuments(), 6);await db.collection("members").insertMany([ { name: "Ada", joined: 2023, fines: 0, city: "Cambridge", borrowed: ["Compilers"] }, { name: "Grace", joined: 2024, fines: 0, city: "Arlington", borrowed: ["Compilers", "Algorithms"] }, { name: "Linus", joined: 2023, fines: 2, city: "Helsinki", borrowed: [] }, { name: "Margaret", joined: 2023, fines: 0, city: "Cambridge", borrowed: ["Algorithms"] }, { name: "Edsger", joined: 2022, fines: 5, city: "Austin", borrowed: ["Compilers", "Algorithms", "Databases"] }, { name: "Barbara", joined: 2025, fines: 1, city: "Boston", borrowed: [] },]);if ((await db.collection("members").countDocuments()) !== 6) throw new Error("expected 6");(สำหรับ Python, Go และ Rust ให้ insert หกรายการเดียวกันเหมือนบทก่อนหน้า)
findOne เทียบกับ find
หัวข้อที่มีชื่อว่า “findOne เทียบกับ find”ความต่างอยู่ที่รูปร่าง ไม่ใช่ไวยากรณ์ findOne คืน document ไม่เกินหนึ่งโดยตรง find คืน cursor — handle แบบ lazy ที่ driver วนดึง document เป็น batch — คุณจึงมักแปลงเป็น array (หรือ loop) เพื่อดูผลลัพธ์
const member = db.members.findOne({ name: "Grace" });console.log(member);console.log(db.members.findOne({ name: "Nobody" })); // no matchassert.strictEqual(member.name, "Grace");const member = await db.collection("members").findOne({ name: "Grace" });console.log(member);console.log(await db.collection("members").findOne({ name: "Nobody" }));if (member?.name !== "Grace") throw new Error("expected Grace");member = db.members.find_one({"name": "Grace"})print(member)var member bson.Merr := coll.FindOne(ctx, bson.M{"name": "Grace"}).Decode(&member)if err != nil { return err}fmt.Println(member)let member = coll.find_one(doc! { "name": "Grace" }).await?;println!("{:?}", member);document ที่ตรงกันกลับมาเต็ม รวม _id และการไม่พบคือ null (ไม่ใช่ error) ใน shell และ Node:
{ _id: ObjectId('6abca853ca24543a2bc8573d'), name: 'Grace', joined: 2024, fines: 0, city: 'Arlington', borrowed: [ 'Compilers', 'Algorithms' ]}nullแต่ละ driver รายงาน “ไม่มีสมาชิกคนนั้น” ต่างกันและสำคัญต่อการจัดการ error: Python คืน None, Rust คืน None ใน Option, Node คืน null — แต่ FindOne(...).Decode() ของ Go คืน error mongo: no documents in result (mongo.ErrNoDocuments) ซึ่งต้องถือเป็น “ไม่พบ” ไม่ใช่ความล้มเหลว
filter ว่างตรงกับทุกอย่าง นั่นคือวิธีอ่านทั้ง collection ด้วย find คุณได้ cursor ครอบทั้งหมด:
const cursor = db.members.find({});console.log(cursor.constructor.name); // a Cursor, not an arrayconst all = cursor.toArray();console.log(all.map((m) => m.name));assert.strictEqual(all.length, 6);const all = await db.collection("members").find({}).toArray();console.log(all.map((m) => m.name));let n = 0;for await (const m of db.collection("members").find({})) n++; // or iterate lazilyif (all.length !== 6 || n !== 6) throw new Error("expected 6");all_members = list(db.members.find({}))print(all_members)cur, err := coll.Find(ctx, bson.M{})if err != nil { return err}var all []bson.Mif err := cur.All(ctx, &all); err != nil { return err}fmt.Println(all)use futures::stream::TryStreamExt;
let mut cursor = coll.find(doc! {}).await?;while let Some(member) = cursor.try_next().await? { println!("{:?}", member);}Cursor[ 'Ada', 'Grace', 'Linus', 'Margaret', 'Edsger', 'Barbara' ]cursor ทำอะไรจริง ๆ: batch
หัวข้อที่มีชื่อว่า “cursor ทำอะไรจริง ๆ: batch”“handle แบบ lazy” ไม่ใช่คำเปรียบเทียบ เราเปิด profiler บน database scratch, insert document เล็ก ๆ 1000 รายการ แล้วนับ round trip ไปยัง server ของการอ่านสี่แบบ:
db.big.insertMany(Array.from({ length: 1000 }, (_, i) => ({ i, pad: "x".repeat(50) })));const trip = (label, fn) => { db.setProfilingLevel(2); // profile every operation of THIS database only const since = new Date(); const n = fn(); db.setProfilingLevel(0); const ops = db.system.profile.find({ ts: { $gte: since }, ns: db.getName() + ".big" }).sort({ ts: 1 }).toArray(); console.log(label, "docs:", n, "| server round trips:", ops.map((o) => `${o.op}(${o.nreturned})`).join(" ")); return ops;};const a = trip("find().toArray() ", () => db.big.find().toArray().length);const b = trip("find().batchSize(100) ", () => db.big.find().batchSize(100).toArray().length);const c = trip("find().limit(5) ", () => db.big.find().limit(5).toArray().length);const d = trip("findOne() ", () => (db.big.findOne() ? 1 : 0));assert.strictEqual(a.length, 2);assert.strictEqual(c.length, 1);find().toArray() docs: 1000 | server round trips: query(101) getmore(899)find().batchSize(100) docs: 1000 | server round trips: query(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(100) getmore(0)find().limit(5) docs: 5 | server round trips: query(5)findOne() docs: 1 | server round trips: query(1)คำตอบแรกพก 101 document getMore ถัดไปพกที่เหลือ (ถึงเพดานขนาดคำตอบ); batchSize(100) บังคับ round trip สิบเอ็ดครั้ง; limit(5) และ findOne ใช้พอดีหนึ่งครั้ง นั่นคือเหตุผลที่ shell พิมพ์ 20 document แล้วตามด้วย Type "it" for more: it คือ shell ขอ batch ถัดไปจาก cursor
filter แบบ equality
หัวข้อที่มีชื่อว่า “filter แบบ equality”filter ที่ระบุ field และค่าหมายถึง “field นี้เท่ากับค่านี้” วางหลาย field ใน filter เดียว MongoDB ถือเป็น implicit AND — document ต้องตรงทุกเงื่อนไขจึงถูกคืน ที่นี่เราขอสมาชิกที่เข้าร่วมปี 2023 และ ไม่มีค่าปรับค้าง:
const matches = db.members.find({ joined: 2023, fines: 0 }, { _id: 0, name: 1 }).toArray();console.log(matches);console.log(db.members.find({ joined: 2099, fines: 0 }).toArray()); // a miss: empty array, no errorassert.strictEqual(matches.map((m) => m.name).join(), "Ada,Margaret");const matches = await db .collection("members") .find({ joined: 2023, fines: 0 }) .toArray();console.log(matches.map((m) => m.name));if (matches.length !== 2) throw new Error("expected 2");matches = list(db.members.find({"joined": 2023, "fines": 0}))print(matches)cur, err := coll.Find(ctx, bson.M{"joined": 2023, "fines": 0})if err != nil { return err}var matches []bson.Mif err := cur.All(ctx, &matches); err != nil { return err}fmt.Println(matches)use futures::stream::TryStreamExt;
let mut cursor = coll.find(doc! { "joined": 2023, "fines": 0 }).await?;let matches: Vec<_> = cursor.try_collect().await?;println!("{:?}", matches);สมาชิกสองคนตรงทั้งสองเงื่อนไข; filter ที่ไม่มีใครตรงคืน list ว่าง:
[ { name: 'Ada' }, { name: 'Margaret' } ][](Node พิมพ์ [ 'Ada', 'Margaret' ]; Python, Go และ Rust คืน 2 document เดียวกันเช่นกัน)
ลองชิม projection
หัวข้อที่มีชื่อว่า “ลองชิม projection”โดยค่าเริ่มต้นการอ่านคืนทุก field ของทุก document ที่ตรง projection ให้คุณขอเฉพาะ field ที่สนใจ ซึ่งลดข้อมูลที่ส่งผ่านสาย คุณส่ง document ที่สองโดย 1 หมายถึง “รวม field นี้” ที่นี่เราต้องการแค่ชื่อ และปิด _id อย่างชัดเจนด้วยการตั้งเป็น 0:
console.log(db.members.find({ joined: 2023 }, { name: 1, _id: 0 }).toArray());console.log(db.members.find({}, { name: 1 }).limit(1).toArray()); // _id sneaks inlet err;try { db.members.find({}, { name: 1, fines: 0 }).toArray(); } catch (e) { err = e; } // mixing include and excludeconsole.log(err.name, err.code, err.message);assert.strictEqual(err.code, 31254);const names = await db .collection("members") .find({ joined: 2023 }, { projection: { name: 1, _id: 0 } }) .toArray();console.log(names);if (names.length !== 3 || "_id" in names[0]) throw new Error("expected 3 projected names");names = list(db.members.find({"joined": 2023}, {"name": 1, "_id": 0}))print(names)opts := options.Find().SetProjection(bson.M{"name": 1, "_id": 0})cur, err := coll.Find(ctx, bson.M{"joined": 2023}, opts)if err != nil { return err}var names []bson.Mif err := cur.All(ctx, &names); err != nil { return err}fmt.Println(names)use futures::stream::TryStreamExt;use mongodb::options::FindOptions;
let opts = FindOptions::builder() .projection(doc! { "name": 1, "_id": 0 }) .build();let mut cursor = coll.find(doc! { "joined": 2023 }).with_options(opts).await?;let names: Vec<_> = cursor.try_collect().await?;println!("{:?}", names);ตอนนี้แต่ละผลลัพธ์มีเฉพาะ field ที่ขอ — และกับดักสองข้อเป็นของจริง:
[ { name: 'Ada' }, { name: 'Linus' }, { name: 'Margaret' } ][ { _id: ObjectId('6abca853ca24543a2bc8573c'), name: 'Ada' } ]MongoServerError 31254 Cannot do exclusion on field fines in inclusion projection_id ถูกรวมเสมอเว้นแต่คุณปิดด้วย _id: 0 และการผสม inclusion (name: 1) กับ exclusion (fines: 0) ถูก server ปฏิเสธ (code 31254)
ใน Compass: query bar มีช่อง Filter และปุ่ม Find; drop-down Options เพิ่ม Project, Sort, Limit, Skip, Collation และ Max Time MS (เอกสาร Compass, mongodb.com/docs/compass/current/query/filter, เรียกอ่านเมื่อ 2026-09-30) สังเกตว่า filter bar ของ Compass รับทุก query operator ยกเว้น $text และ $expr
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”findคืน cursor ไม่ใช่ list ไม่มีอะไรถูกดึงนอกเหนือ batch แรกจนกว่าคุณจะวนอ่าน (หรือเรียก helper อย่างtoArray)findOneก็คือ cursor ที่หยุดหลัง document แรก- field
_idถูกรวมโดยค่าเริ่มต้น ในทุก projection ถ้าไม่ต้องการ ให้ปิดอย่างชัดเจนด้วย_id: 0 - อย่าผสม
1และ0ใน projection เดียวกัน ยกเว้น_idเลือกระบุ field ที่ต้องการ หรือระบุ field ที่ต้องการทิ้ง — ไม่ใช่ทั้งสองอย่าง - filter ที่ไม่ตรงอะไรเลยคืน cursor ว่าง (หรือสำหรับ
findOneคือnull/None; ใน Go เป็น errorErrNoDocuments) นั่นเป็นคำตอบปกติ ไม่ใช่ความล้มเหลว - filter เป็น object ธรรมดาในภาษาของคุณ ดังนั้น key ที่ซ้ำเก็บเฉพาะค่าสุดท้ายอย่างเงียบ ๆ (ดูด้านล่าง)
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ขอผู้ช่วย AI ว่า “สมาชิกที่มียอดค้างเล็กน้อย มากกว่า 0 และน้อยกว่า 5” คุณอาจได้:
🐛 หา bug ในโค้ดนี้
// AI comment: "fines greater than 0 AND less than 5"const owing = await db.collection("members").find({ fines: { $gt: 0 }, fines: { $lt: 5 } }).toArray();เฉลย
object literal ของ JavaScript ไม่มี key ซ้ำได้: fines ตัวที่สอง แทนที่ ตัวแรกอย่างเงียบ ๆ filter ที่ถึง server จึงเป็นแค่ { fines: { $lt: 5 } } บนสมาชิกหกคนของเรา มันคืน ห้า document รวม Ada, Grace และ Margaret ที่ไม่มีค้าง:
[ 'Ada 0', 'Grace 0', 'Linus 2', 'Margaret 0', 'Barbara 1' ] <- find({ fines: {$gt: 0}, fines: {$lt: 5} })[ 'Linus 2', 'Barbara 1' ] <- find({ fines: {$gt: 0, $lt: 5} })ใส่ทั้งสอง operator ใน sub-document เดียว หรือใช้ $and ที่ชัดเจน fence นี้พิสูจน์ว่าการแก้คืนพอดีสองคนที่ค้างเล็กน้อย:
db.members.drop();db.members.insertMany([ { name: "Ada", fines: 0 }, { name: "Grace", fines: 0 }, { name: "Linus", fines: 2 }, { name: "Margaret", fines: 0 }, { name: "Edsger", fines: 5 }, { name: "Barbara", fines: 1 },]);const wrong = db.members.find({ fines: { $gt: 0 }, fines: { $lt: 5 } }, { _id: 0, name: 1, fines: 1 }).toArray();const right = db.members.find({ fines: { $gt: 0, $lt: 5 } }, { _id: 0, name: 1, fines: 1 }).toArray();const explicit = db.members.find({ $and: [{ fines: { $gt: 0 } }, { fines: { $lt: 5 } }] }, { _id: 0, name: 1 }).toArray();console.log(wrong.map((d) => `${d.name} ${d.fines}`), right.map((d) => `${d.name} ${d.fines}`), explicit.length);assert.strictEqual(wrong.length, 5);assert.strictEqual(right.length, 2);assert.strictEqual(explicit.length, 2);[ 'Ada 0', 'Grace 0', 'Linus 2', 'Margaret 0', 'Barbara 1' ] [ 'Linus 2', 'Barbara 1' ] 2Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การอ่านคือ filter แล้วจัดรูป: filter (document) ตัดสิน document ไหน ที่ตรง, projection ตัดสิน field ไหน ที่กลับมา และ cursor ตัดสินว่าต้อง กี่ round trip find ให้ cursor; findOne คือ find().limit(1) ที่แกะห่อแล้ว เก็บ filter ไว้ฝั่ง server และเล็ก คุณก็เก็บสายให้เล็ก
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
filter แบบ equality ธรรมดา (field: value) | เขียนและอ่านง่าย ใช้ index ตรง ๆ และมีประสิทธิภาพ | ตอบได้เฉพาะคำถามแบบ “เท่ากับ” |
| filter หลาย field แบบ implicit AND | กรองได้คมและแม่นกว่าโดยไม่ต้องใช้ operator เพิ่ม | ยิ่งมี field มาก ยิ่งต้องมี compound index ที่ลำดับ field ตรงกัน ไม่เช่นนั้น query ช้าลง |
filter ที่ใช้ operator ซับซ้อน ($gt, $in, $regex ฯลฯ) | ยืดหยุ่นมาก ตอบคำถามช่วงหรือรูปแบบได้ | ออกแบบ index ยากกว่า; operator บางตัว (เช่น $regex ที่ไม่ anchor) ใช้ index ได้ไม่ดี |
batch ค่าเริ่มต้น (toArray) | 2 round trip สำหรับ 1000 document (101 + 899) | ผลลัพธ์ทั้งหมดอยู่ในหน่วยความจำ; ผลใหญ่มากควรวน cursor แทน |
batchSize เล็ก | หน่วยความจำต่อ batch มีขอบเขต | 11 round trip สำหรับ 1000 document เดียวกัน |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
findคืนผลลัพธ์ทันทีที่เรียก” — มันคืน cursor แบบ lazy การลืมtoArray()/วนอ่านไม่ทำให้เกิด error แต่ก็ไม่ได้ข้อมูล - “
find({})แล้วกรองในโค้ด client ก็ได้” — ส่ง filter ไปให้ MongoDB กรองระดับ query เพื่อส่งข้อมูลผ่านสายน้อยลงและให้ index ช่วยได้ - “
findกับfindOneใช้แทนกันได้” —findOneคืน document ธรรมดา (หรือ null);find().toArray()[0]ดึงทั้ง batch เพื่อใช้ document เดียว - “
{ a: {$gt: 0}, a: {$lt: 5} }คือช่วง” — key ที่สองแทนที่ตัวแรก; การรันจริงคืนห้า document แทนสอง - “การไม่พบคือ error” — ไม่ใช่ใน shell, Node, Python หรือ Rust; ใน Go
Decodeคืนmongo: no documents in result
ตัวอย่างจากของจริง
เอกสาร query bar ของ Compass แสดงโมเดล filter-document เดียวกันใน GUI: ช่อง Filter รับทุก query operator ยกเว้น
$textและ$exprโดยมี Project, Sort และ Limit ใต้ Options (Compass: filter documents, เรียกอ่านเมื่อ 2026-09-30)
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกัน (output ของ Go, Python และ Rust ตรงกัน: พบ Grace, 6 document, ตรง equality 2 รายการ, ชื่อที่ project 3 รายการ) แต่ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส