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

Pipeline basics — $match และ $project

ก่อนจะไปถึงของหรู มีสอง stage ที่แบกงาน aggregation ไว้เกือบทั้งหมด: $match ทิ้ง document ที่คุณไม่ต้องการ ส่วน $project จัดรูป document ที่เหลือใหม่ เรียนสองตัวนี้ให้แม่น คุณก็มีเครื่องมือ query ที่ไปไกลกว่า find แล้ว และมีบทเรียนสำคัญซ่อนอยู่ในนั้น: ตำแหน่งที่คุณวาง stage เปลี่ยนทั้งความเร็วและความถูกต้องของ pipeline

เรายังใช้ collection sales ต่อ ออเดอร์หนึ่งรายการหน้าตาแบบนี้:

{
"_id": "a1",
"title": "Dune",
"genre": "fiction",
"quantity": 3,
"price": 12,
"buyer": "Ada"
}

$match เก็บเฉพาะ document ที่ field ตรงตามเงื่อนไข โดยใช้ syntax เดียวกับ query ของ find ที่คุณรู้จักอยู่แล้ว ดังนั้น $gt, $in, $and และตัวอื่น ๆ ใช้ได้หมด เหตุผลที่ต้องวางไว้ ก่อน ไม่ใช่เรื่องสไตล์: เมื่อ $match เป็น stage แรกสุด เอนจิน aggregation ใช้ index ของ collection ข้าม document ที่ไม่ตรงได้ทั้งหมด แทนที่จะ stream ทั้ง collection ผ่าน stage ถัดไป

flowchart LR
  In["all sales"] --> M["$match: price >= 10 (uses index)"]
  M --> P["$project: keep title, compute revenue"]
  P --> Out["slim, reshaped documents"]
filter ก่อนเพื่อให้ index ตัด stream ได้ แล้วค่อยจัดรูปสิ่งที่เหลือรอด

ตรงนี้เราเก็บเฉพาะออเดอร์ที่ราคาอย่างน้อยสิบดอลลาร์ต่อชิ้น:

db.sales.aggregate([
{ $match: { price: { $gte: 10 } } }
])

เฉพาะออเดอร์ที่ราคาตั้งแต่สิบขึ้นไปเท่านั้นที่รอดเข้าไปยัง stage ถัดไป:

[
{ "_id": "a1", "title": "Dune", "genre": "fiction", "quantity": 3, "price": 12, "buyer": "Ada" },
{ "_id": "a4", "title": "Foundation", "genre": "fiction", "quantity": 1, "price": 15, "buyer": "Linus" }
]

$project ตัดสินว่า field ไหนจะออกจาก stage และให้คุณสร้าง field ใหม่ จาก expression ได้ ตั้งค่า field เป็น 1 เพื่อเก็บไว้ 0 เพื่อทิ้ง หรือใส่ expression เพื่อคำนวณ การอ้าง field ภายใน expression ต้องนำหน้าด้วย $ ดังนั้น "$price" หมายถึง “ค่าของ field price” ตรงนี้เราเก็บ title ไว้ แล้วคำนวณ field revenue จาก price คูณ quantity:

db.sales.aggregate([
{ $match: { price: { $gte: 10 } } },
{ $project: {
_id: 0,
title: 1,
revenue: { $multiply: ["$price", "$quantity"] }
} }
])

document แต่ละอันที่รอดมาตอนนี้เหลือแค่ field ที่จำเป็น พร้อมค่าที่คำนวณแล้ว:

[
{ "title": "Dune", "revenue": 36 },
{ "title": "Foundation", "revenue": 15 }
]

ใน Compass: เพิ่ม stage $match แล้วตามด้วย stage $project ในแท็บ Aggregations ตัวอย่างสด ๆ ใต้แต่ละ stage จะโชว์จำนวน document ที่หดลงหลัง $match และ field ที่เปลี่ยนไปหลัง $project ทำให้เห็นผลของลำดับ stage ได้ในพริบตา

วาง $match ไว้ หลัง stage ที่หนัก คุณก็ต้องจ่ายค่าประมวลผล document ที่ยังไงก็จะโดนทิ้งอยู่ดี วางไว้ ก่อน เอนจินจะตัด stream — บางครั้งด้วย index — ก่อนที่งานอื่นจะเริ่ม หลักการง่าย ๆ คือ ** filter ให้เร็วที่สุด จัดรูปให้ช้าที่สุด** การใช้ $project ทิ้ง field ก้อนใหญ่ก็ช่วย stage ถัดไปได้เหมือนกัน เพราะทำให้แต่ละ document เล็กลง แต่ที่ได้ผลที่สุดเกือบทุกครั้งคือ $match ที่วางไว้ต้น pipeline

  • $match ที่อยู่ต้น pipeline ใช้ index ได้ ส่วน $match ที่มาหลัง $group หรือ $project มักใช้ไม่ได้ เพราะ document ณ จุดนั้นเป็นของที่คำนวณขึ้นใหม่ ไม่ใช่ของที่เก็บอยู่บนดิสก์
  • ใน $project การผสม inclusion (1) กับ exclusion (0) ใน stage เดียวกันทำได้เฉพาะกับ _id เท่านั้น ให้เลือกว่าจะระบุ field ที่ต้องการ หรือทิ้ง field ที่ไม่ต้องการ — ไม่ใช่ทั้งสองอย่าง
  • field path ภายใน expression ต้องนำหน้าด้วย $: "$price" คือค่าใน field ส่วน "price" คือ string ตัวอักษรนั้นตรง ๆ
  • ใช้ $project (หรือ $set ที่ทำหน้าที่คล้ายกัน) คำนวณครั้งเดียวแล้วเอาผลไปใช้ซ้ำทีหลัง ดีกว่าเขียน expression เดิมซ้ำในหลาย stage
ตัวเลือกBenefitCost
$match ไว้ต้น pipelineใช้ index ได้ ตัด document ทิ้งก่อนงานหนัก ประหยัด CPU และ memoryต้องเขียนเงื่อนไข filter ให้ตรงกับ field ที่ยังไม่แปลงรูป บางครั้งซับซ้อนกว่าการ filter หลังคำนวณ
$match ไว้ท้าย pipeline (หลังคำนวณ)เขียนเงื่อนไขบน field ที่คำนวณแล้วได้ตรงไปตรงมาใช้ index ไม่ได้ และเสียแรงประมวลผล document ที่สุดท้ายก็โดนทิ้ง
$project ระบุ field ทีละตัว (explicit list)คุม shape ของผลลัพธ์ได้แม่นยำ ลดขนาด document ที่ส่งต่อพอ schema เปลี่ยน (เพิ่ม/ลบ field) ต้องกลับมาแก้ $project ทุกจุดที่ list field ไว้
  • วาง $match หลัง $group หรือ $project แล้วหวังว่า index จะยังช่วย — ทันทีที่ document ผ่าน $group/$project ก็ไม่ใช่ document ที่เก็บบนดิสก์อีกต่อไป index ของ collection จึงใช้ไม่ได้แล้ว
  • ลืมใส่ $project ท้าย pipeline — ถ้าไม่จัดรูปผลลัพธ์สุดท้าย ฝั่ง client จะได้ document ที่มี field เกินความจำเป็น รวมถึง field ภายในที่ตั้งใจใช้แค่ตอนคำนวณ ทำให้ payload บวมโดยเปล่าประโยชน์
  • ผสม inclusion (1) กับ exclusion (0) ใน $project เดียวกัน แล้วงงว่าทำไม error — MongoDB ยอมให้ผสมได้เฉพาะกับ _id เท่านั้น field อื่นต้องเลือกอย่างใดอย่างหนึ่ง

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

Analytics dashboard — query ที่ filter ตามช่วงวันที่และ status ก่อน ($match ต้น pipeline บน field ที่มี index) แล้วค่อย $project ให้เหลือแค่ field ที่กราฟต้องใช้ ช่วยให้ dashboard ที่อ่านข้อมูลหลายล้าน document ต่อวันตอบกลับได้ในไม่กี่ร้อย millisecond

Keller Williams — รายงานอสังหาริมทรัพย์ใช้ $match filter บน field อย่าง region และ status ก่อนเสมอ เพื่อให้ index ตัด document ส่วนใหญ่ทิ้งได้ทันที แล้วค่อย $project ให้เหลือเฉพาะ field ที่ทีมขายต้องเห็นในรายงาน

ทำไมการวาง $match เป็น stage แรกของ pipeline ถึงดีที่สุด?
ใน stage $project, "$price" อ้างถึงอะไร?
รูปแบบไหนของ $project ที่ทำใน stage เดียวไม่ได้?