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

Comparison และ logical operator

แอปจริงแทบไม่เคยถามแค่ “field นี้เท่ากับค่านี้ไหม” คำถามที่เจอบ่อยกว่าคือเรื่องช่วง เซ็ตของค่าที่ยอมรับได้ หรือเงื่อนไขหลายอย่างรวมกัน — สิ่งที่ filter แบบจับคู่ตรงเป๊ะเพียงอย่างเดียวตอบไม่ได้

filter แบบจับคู่ตรงเป๊ะ — field เท่ากับค่า — พาคุณไปได้แค่ระดับหนึ่ง คำถามจริง ๆ คือช่วงและเซ็ต: “ใครค้างค่าปรับมากกว่าสามรายการ?”, “ใครเข้าร่วมในปี 2022 หรือ 2023?”, “ระเบียนไหนไม่มี field นั้นเลย?” MongoDB ตอบคำถามเหล่านี้ด้วย query operator ที่เป็น key พิเศษที่ขึ้นต้นด้วยเครื่องหมายดอลลาร์และอยู่ ภายใน ตำแหน่งค่าของ field รูปแบบเป็นแบบเดียวกันเสมอ: แทนที่จะให้ค่าธรรมดากับ field คุณให้ document เล็ก ๆ ที่ key ของตัวเองคือ operator

เรายังคงแก้ไขสมาชิกห้องสมุด members ของเราต่อไป แต่ละตัวอย่างจะโชว์ filter คู่กับ document ที่ได้กลับมา

ตระกูล comparison เปรียบเทียบ field กับค่าหนึ่ง: $eq (เท่ากับ), $ne (ไม่เท่ากับ), $gt (มากกว่า), $gte (มากกว่าหรือเท่ากับ), $lt (น้อยกว่า) และ $lte (น้อยกว่าหรือเท่ากับ) การเขียนค่าธรรมดาก็เป็นเพียงรูปย่อของ $eq ตรงนี้เราขอสมาชิกทุกคนที่ค้างค่าปรับมากกว่าสองรายการ:

db.members.find({ fines: { $gt: 2 } })

เฉพาะสมาชิกที่เกินเกณฑ์เท่านั้นที่ส่งกลับมา:

[
{ "_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 จับคู่เมื่อ field เท่ากับค่า ใดก็ได้ ในรายการ ส่วน $nin จะ match เมื่อค่านั้น ไม่ตรงกับค่าใดเลย ในรายการ วิธีนี้สะอาดกว่าการต่อ $or ของความเท่ากันหลาย ๆ อันมาก ตรงนี้เราดึงสมาชิกที่เข้าร่วมในปี 2021 หรือ 2023:

db.members.find({ joined: { $in: [2021, 2023] } })

ปีที่ match ทั้งสองค่าจะกลับมาด้วยกัน:

[
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 },
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d4", "name": "Grace", "joined": 2021, "fines": 3 }
]

คู่ field/value หลายคู่ใน filter เดียวมีความหมายเป็น AND อยู่แล้ว ดังนั้น $and จึงจำเป็นเฉพาะเมื่อคุณต้องเขียน field เดียวกันซ้ำสองครั้งเท่านั้น $or คือตัวที่ใช้กันประจำวัน โดยรับ list ของ filter และจับคู่ถ้า อันใดอันหนึ่ง เป็นจริง ตรงนี้เราหาสมาชิกที่ไม่ค้างค่าปรับเลย หรือเข้าร่วมในปี 2021:

db.members.find({
$or: [{ fines: 0 }, { joined: 2021 }]
})

ใครก็ตามที่ตรงกับสาขาใดสาขาหนึ่งจะปรากฏขึ้น:

[
{ "_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 }] })

document ใน collection เดียวกันไม่จำเป็นต้องมี field เหมือนกันทั้งหมด $exists ถามว่ามี field อยู่หรือไม่ไม่ว่าค่าจะเป็นอะไร และ $type ถามว่าค่ามี BSON type อะไร ตรงนี้เราหาสมาชิกที่ยัง ไม่มี field fines บันทึกไว้:

db.members.find({ fines: { $exists: false } })

เฉพาะ 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 ที่ไม่มี field fines ถือว่า “ไม่เท่ากับ 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 ได้ตรงกว่า
ตัวเลือกBenefitCost
equality filter ($eq หรือค่าตรง ๆ)เร็วที่สุด ใช้ index ได้เต็มประสิทธิภาพเสมอตอบคำถามเชิงช่วงหรือเซ็ตไม่ได้
range/set operator ($gt, $gte, $lt, $lte, $in)ตอบคำถามเชิงช่วง/เซ็ตได้ ยังคง index ได้ถ้า field มี index ตรงกับที่ queryselectivity ต่ำกว่า 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 รวมหลายเงื่อนไข เช่น ยอดสูงผิดปกติ หรือมาจากประเทศที่ไม่เคยทำธุรกรรมมาก่อน หรือเกิดถี่ผิดปกติในเวลาสั้น ๆ

คุณจะหา document ที่ fines มากกว่า 2 ได้อย่างไร?
document ที่ไม่มี field fines จะ match กับ { fines: { $ne: 5 } } ไหม?
operator ใดจับคู่เมื่อ field เท่ากับค่าใดก็ได้ในรายการ?
logical operator อย่าง $or ควรอยู่ตรงไหนใน filter?