Comparison และ logical operator
แอปจริงแทบไม่เคยถามแค่ “field นี้เท่ากับค่านี้ไหม” คำถามที่เจอบ่อยกว่าคือเรื่องช่วง เซ็ตของค่าที่ยอมรับได้ หรือเงื่อนไขหลายอย่างรวมกัน — สิ่งที่ filter แบบจับคู่ตรงเป๊ะเพียงอย่างเดียวตอบไม่ได้
filter แบบจับคู่ตรงเป๊ะ — field เท่ากับค่า — พาคุณไปได้แค่ระดับหนึ่ง คำถามจริง ๆ คือช่วงและเซ็ต: “ใครค้างค่าปรับมากกว่าสามรายการ?”, “ใครเข้าร่วมในปี 2022 หรือ 2023?”, “ระเบียนไหนไม่มี field นั้นเลย?” MongoDB ตอบคำถามเหล่านี้ด้วย query operator ที่เป็น key พิเศษที่ขึ้นต้นด้วยเครื่องหมายดอลลาร์และอยู่ ภายใน ตำแหน่งค่าของ field รูปแบบเป็นแบบเดียวกันเสมอ: แทนที่จะให้ค่าธรรมดากับ field คุณให้ document เล็ก ๆ ที่ key ของตัวเองคือ operator
เรายังคงแก้ไขสมาชิกห้องสมุด members ของเราต่อไป แต่ละตัวอย่างจะโชว์ filter คู่กับ document ที่ได้กลับมา
Comparison operator
หัวข้อที่มีชื่อว่า “Comparison operator”ตระกูล comparison เปรียบเทียบ field กับค่าหนึ่ง: $eq (เท่ากับ), $ne (ไม่เท่ากับ), $gt (มากกว่า), $gte (มากกว่าหรือเท่ากับ), $lt (น้อยกว่า) และ $lte (น้อยกว่าหรือเท่ากับ) การเขียนค่าธรรมดาก็เป็นเพียงรูปย่อของ $eq ตรงนี้เราขอสมาชิกทุกคนที่ค้างค่าปรับมากกว่าสองรายการ:
db.members.find({ fines: { $gt: 2 } })const docs = await db.collection("members") .find({ fines: { $gt: 2 } }) .toArray();console.log(docs);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);}เฉพาะสมาชิกที่เกินเกณฑ์เท่านั้นที่ส่งกลับมา:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 }, { "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "joined": 2021, "fines": 3 }]คุณสามารถรวมการเปรียบเทียบสองอันบน field เดียวกัน ได้โดยใส่ operator ทั้งสองไว้ใน document เดียว — ตรงนี้คือสมาชิกที่เข้าร่วมในช่วงปีที่กำหนด:
db.members.find({ joined: { $gte: 2022, $lte: 2023 } })การเป็นสมาชิก: $in และ $nin
หัวข้อที่มีชื่อว่า “การเป็นสมาชิก: $in และ $nin”$in จับคู่เมื่อ field เท่ากับค่า ใดก็ได้ ในรายการ ส่วน $nin จะ match เมื่อค่านั้น ไม่ตรงกับค่าใดเลย ในรายการ วิธีนี้สะอาดกว่าการต่อ $or ของความเท่ากันหลาย ๆ อันมาก ตรงนี้เราดึงสมาชิกที่เข้าร่วมในปี 2021 หรือ 2023:
db.members.find({ joined: { $in: [2021, 2023] } })const docs = await db.collection("members") .find({ joined: { $in: [2021, 2023] } }) .toArray();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?;ปีที่ match ทั้งสองค่าจะกลับมาด้วยกัน:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 }, { "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "joined": 2021, "fines": 3 }]การรวมเงื่อนไข: $and, $or, $not, $nor
หัวข้อที่มีชื่อว่า “การรวมเงื่อนไข: $and, $or, $not, $nor”คู่ field/value หลายคู่ใน filter เดียวมีความหมายเป็น AND อยู่แล้ว ดังนั้น $and จึงจำเป็นเฉพาะเมื่อคุณต้องเขียน field เดียวกันซ้ำสองครั้งเท่านั้น $or คือตัวที่ใช้กันประจำวัน โดยรับ list ของ filter และจับคู่ถ้า อันใดอันหนึ่ง เป็นจริง ตรงนี้เราหาสมาชิกที่ไม่ค้างค่าปรับเลย หรือเข้าร่วมในปี 2021:
db.members.find({ $or: [{ fines: 0 }, { joined: 2021 }]})const docs = await db.collection("members").find({ $or: [{ fines: 0 }, { joined: 2021 }]}).toArray();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?;ใครก็ตามที่ตรงกับสาขาใดสาขาหนึ่งจะปรากฏขึ้น:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "joined": 2023, "fines": 0 }, { "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "joined": 2021, "fines": 3 }]$not กลับด้านนิพจน์ operator เดียว และ $nor จับคู่ document ที่ ไม่มี สาขาใดเป็นจริงเลย — ตรงข้ามทาง logic ของ $or ตัวอย่างเช่น “ค้างค่าปรับเป็นจำนวนที่ไม่ใช่ศูนย์ และ ไม่ได้เข้าร่วมในปี 2021” อ่านได้อย่างเป็นธรรมชาติด้วย $nor:
db.members.find({ $nor: [{ fines: 0 }, { joined: 2021 }] })การทดสอบ field: $exists และ $type
หัวข้อที่มีชื่อว่า “การทดสอบ field: $exists และ $type”document ใน collection เดียวกันไม่จำเป็นต้องมี field เหมือนกันทั้งหมด $exists ถามว่ามี field อยู่หรือไม่ไม่ว่าค่าจะเป็นอะไร และ $type ถามว่าค่ามี BSON type อะไร ตรงนี้เราหาสมาชิกที่ยัง ไม่มี field fines บันทึกไว้:
db.members.find({ fines: { $exists: false } })const docs = await db.collection("members") .find({ fines: { $exists: false } }) .toArray();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?;เฉพาะ document ที่ไม่เคยมี field fines เลยเท่านั้นที่ส่งกลับมา:
[ { "_id": "65f0b3d4e4b0a1c2e4b0a1d5", "name": "Ada", "joined": 2024 }]หากต้องการจับคู่ตาม type แทน — เช่น ทุก document ที่ joined ถูกเก็บเป็นตัวเลขแทนที่จะเป็น string ที่บางคนพิมพ์ผิด — ให้ระบุชื่อ type:
db.members.find({ joined: { $type: "int" } })ใน Compass: พิมพ์ filter เหล่านี้ลงไปตรง ๆ ในแถบ filter ด้านบนของแท็บ Documents เช่น { fines: { $gt: 2 } } แล้วกด Find Compass แปลง syntax ของ operator แบบเดียวกับที่ shell ใช้
เคล็ดลับและจุดที่พลาดง่าย
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดที่พลาดง่าย”- ค่าธรรมดาคือ
$eqการเขียน{ fines: 0 }กับ{ fines: { $eq: 0 } }เหมือนกันทุกประการ ใช้ operator แบบชัดเจนเฉพาะเมื่อคุณต้องการการเปรียบเทียบอื่นนอกเหนือจากความเท่ากัน $neและ$ninจับคู่ field ที่หายไปด้วย document ที่ไม่มี fieldfinesถือว่า “ไม่เท่ากับ 5” จึงผ่านเงื่อนไข{ fines: { $ne: 5 } }ไปด้วย ถ้าคุณหมายถึง “มี field fines อยู่ และค่าไม่ใช่ 5” ให้รวมกับ$exists- operator สองตัวบน field เดียวกันอยู่ใน document เดียว range query เช่น
{ $gte: 2022, $lte: 2023 }เป็น value document ตัวเดียว ไม่ใช่ filter แยกสองอัน — คุณไม่สามารถใส่ key ของ field เดียวกันซ้ำที่ระดับบนสุดได้ - เลือก
$inแทนกอง$orของความเท่ากัน ทั้งสองมีความหมายเหมือนกัน แต่$inสั้นกว่า อ่านง่ายกว่า และช่วยให้ query planner ใช้ index ได้ตรงกว่า
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
equality filter ($eq หรือค่าตรง ๆ) | เร็วที่สุด ใช้ index ได้เต็มประสิทธิภาพเสมอ | ตอบคำถามเชิงช่วงหรือเซ็ตไม่ได้ |
range/set operator ($gt, $gte, $lt, $lte, $in) | ตอบคำถามเชิงช่วง/เซ็ตได้ ยังคง index ได้ถ้า field มี index ตรงกับที่ query | selectivity ต่ำกว่า equality โดยเฉพาะช่วงกว้าง อาจอ่าน document เกินความจำเป็น |
รวมด้วย $and/$or/$nor | ตอบคำถามเชิง logic ซับซ้อนได้ในคำสั่งเดียว | วางแผน index ยากขึ้น โดยเฉพาะ $or ที่อาจบังคับให้สแกนหลาย path หรือทั้ง collection |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- มองว่า
$ne/$ninใช้ index ได้มีประสิทธิภาพเหมือน$eq— ในทางปฏิบัติ$neมัก selectivity ต่ำ (ต้องพิจารณาแทบทุก document ที่ไม่ตรงเงื่อนไข) query planner จึงมักตัดสินใจไม่ใช้ index เลยแม้ field นั้นจะมี index อยู่ก็ตาม - ใช้
$orคนละ field ต่างกันในแต่ละ clause แล้วคาดหวังว่า index เดียวจะครอบคลุมทั้งหมด — MongoDB ต้องรัน index scan แยกต่างหากสำหรับแต่ละ clause ของ$orแล้วนำผลมารวมกัน ถ้า clause ไหนไม่มี index ที่เหมาะ clause นั้นจะ fallback ไปสแกนทั้ง collection - เขียน
$notครอบค่าตรง ๆ แทนที่จะครอบ operator expression —{ field: { $not: { $eq: 5 } } }ถูกต้อง แต่{ field: { $not: 5 } }ไม่ใช่รูปแบบที่$notรองรับ$notต้องห่อ operator expression เสมอ ไม่ใช่ค่าเปล่า ๆ
💡 ตัวอย่างจากของจริง
ระบบ e-commerce กรองช่วงราคา — slider ราคาต่ำสุด-สูงสุดบนหน้าสินค้าแปลงตรงเป็น
{ price: { $gte: min, $lte: max } }และ dropdown แบรนด์แบบเลือกได้หลายอันแปลงเป็น$inระบบตรวจจับ fraud ของธุรกิจ fintech — กฎแจ้งเตือนธุรกรรมต้องสงสัยมักเขียนด้วย
$orรวมหลายเงื่อนไข เช่น ยอดสูงผิดปกติ หรือมาจากประเทศที่ไม่เคยทำธุรกรรมมาก่อน หรือเกิดถี่ผิดปกติในเวลาสั้น ๆ