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 ฟิลเตอร์สตรีม — วางไว้ก่อน
หัวข้อที่มีชื่อว่า “$match ฟิลเตอร์สตรีม — วางไว้ก่อน”$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"]
ตรงนี้เราเก็บเฉพาะออเดอร์ที่ราคาอย่างน้อยสิบดอลลาร์ต่อชิ้น:
db.sales.aggregate([ { $match: { price: { $gte: 10 } } }])const rows = await db.collection("sales").aggregate([ { $match: { price: { $gte: 10 } } },]).toArray();rows = list(db.sales.aggregate([ {"$match": {"price": {"$gte": 10}}},]))pipeline := mongo.Pipeline{ {{"$match", bson.D{{"price", bson.D{{"$gte", 10}}}}}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$match": { "price": { "$gte": 10 } } },];let mut cursor = coll.aggregate(pipeline).await?;เฉพาะออเดอร์ที่ราคาตั้งแต่สิบขึ้นไปเท่านั้นที่รอดเข้าไปยัง 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 จัดรูปและคำนวณ
หัวข้อที่มีชื่อว่า “$project จัดรูปและคำนวณ”$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"] } } }])const rows = await db.collection("sales").aggregate([ { $match: { price: { $gte: 10 } } }, { $project: { _id: 0, title: 1, revenue: { $multiply: ["$price", "$quantity"] }, } },]).toArray();rows = list(db.sales.aggregate([ {"$match": {"price": {"$gte": 10}}}, {"$project": { "_id": 0, "title": 1, "revenue": {"$multiply": ["$price", "$quantity"]}, }},]))pipeline := mongo.Pipeline{ {{"$match", bson.D{{"price", bson.D{{"$gte", 10}}}}}}, {{"$project", bson.D{ {"_id", 0}, {"title", 1}, {"revenue", bson.D{{"$multiply", bson.A{"$price", "$quantity"}}}}, }}},}cursor, err := coll.Aggregate(ctx, pipeline)if err != nil { return err}let pipeline = vec![ doc! { "$match": { "price": { "$gte": 10 } } }, doc! { "$project": { "_id": 0, "title": 1, "revenue": { "$multiply": ["$price", "$quantity"] } } },];let mut cursor = coll.aggregate(pipeline).await?;document แต่ละอันที่รอดมาตอนนี้เหลือแค่ field ที่จำเป็น พร้อมค่าที่คำนวณแล้ว:
[ { "title": "Dune", "revenue": 36 }, { "title": "Foundation", "revenue": 15 }]ใน Compass: เพิ่ม stage $match แล้วตามด้วย stage $project ในแท็บ Aggregations ตัวอย่างสด ๆ ใต้แต่ละ stage จะโชว์จำนวน document ที่หดลงหลัง $match และ field ที่เปลี่ยนไปหลัง $project ทำให้เห็นผลของลำดับ stage ได้ในพริบตา
ทำไมลำดับของ stage จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมลำดับของ 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
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
$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 ต่อวันตอบกลับได้ในไม่กี่ร้อย millisecondKeller Williams — รายงานอสังหาริมทรัพย์ใช้
$matchfilter บน field อย่าง region และ status ก่อนเสมอ เพื่อให้ index ตัด document ส่วนใหญ่ทิ้งได้ทันที แล้วค่อย$projectให้เหลือเฉพาะ field ที่ทีมขายต้องเห็นในรายงาน