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

$group และ accumulator

$group คือจุดที่ aggregation สมชื่อจริง ๆ เพราะยุบ document จำนวนมากให้เหลือหนึ่ง document ต่อค่าที่แตกต่างกันแต่ละค่า ของคีย์ที่คุณเลือก แล้วคำนวณค่าสรุปของแต่ละกลุ่มด้วย accumulator

นี่คือ stage ที่ตอบคำถามอย่าง “แต่ละ genre ขายได้กี่เล่ม” “ราคาเฉลี่ยต่อผู้ซื้อเท่าไร” หรือ “ออเดอร์ที่ใหญ่ที่สุดของแต่ละชื่อเรื่องคืออันไหน” ถ้าคุณเคยเขียน GROUP BY ใน SQL แนวคิดก็อันเดียวกัน ต่างแค่วิธีเขียน

เรายังใช้ collection sales จากบทเรียนก่อนหน้าต่อ

_id ของ stage $group คือคีย์การจัดกลุ่ม — ฟิลด์ (หรือค่าที่คำนวณ) ที่ใช้แบ่ง document เป็นถัง ทุกฟิลด์อื่นใน stage คือ accumulator: expression ที่ยุบ document ทั้งหมดในกลุ่มให้เป็นค่าเดียว ที่นี่เรารวมจำนวนที่ขายต่อ genre และนับออเดอร์ด้วย:

flowchart LR
  In["sales documents"] --> G["$group _id genre"]
  G --> A["accumulators: $sum quantity, $sum 1"]
  A --> Out["one document per genre"]
document จำนวนมากยุบลงเป็นหนึ่ง document ผลลัพธ์สำหรับคีย์กลุ่มที่แตกต่างกันแต่ละค่า
db.sales.aggregate([
{ $group: {
_id: "$genre",
totalCopies: { $sum: "$quantity" },
orders: { $sum: 1 }
} }
])

ได้ document กลับมาหนึ่งอันต่อหนึ่ง genre พร้อมค่าสรุปของแต่ละกลุ่ม:

[
{ "_id": "fiction", "totalCopies": 11, "orders": 4 },
{ "_id": "science", "totalCopies": 5, "orders": 2 }
]

accumulator จำนวนหยิบมือเดียวครอบคลุมความต้องการส่วนใหญ่ ทุกตัวรับ expression ที่ประเมินกับแต่ละ document ในกลุ่ม:

  • $sum — บวก expression ตัวเลข หรือใช้นับด้วย $sum: 1
  • $avg — ค่าเฉลี่ยของ expression ตัวเลข
  • $min / $max — ค่าน้อยที่สุดและมากที่สุดที่พบ
  • $push — เก็บทุกค่าเข้า array รวมค่าซ้ำด้วย
  • $addToSet — เก็บค่าที่ แตกต่างกัน เข้า array

stage $count เฉพาะทางก็มีอยู่สำหรับกรณีพิเศษ “แค่นับ document” ที่นี่เราทำโปรไฟล์แต่ละ genre ให้สมบูรณ์ขึ้น — ราคาเฉลี่ย ช่วงราคา และเซตของผู้ซื้อ:

db.sales.aggregate([
{ $group: {
_id: "$genre",
avgPrice: { $avg: "$price" },
cheapest: { $min: "$price" },
priciest: { $max: "$price" },
buyers: { $addToSet: "$buyer" }
} }
])

ตอนนี้แต่ละ genre มีโปรไฟล์ครบถ้วน และ buyers เก็บแต่ละชื่อไว้เพียงครั้งเดียว:

[
{ "_id": "fiction", "avgPrice": 12.5, "cheapest": 9, "priciest": 15, "buyers": ["Ada", "Linus"] },
{ "_id": "science", "avgPrice": 20, "cheapest": 18, "priciest": 22, "buyers": ["Grace"] }
]

ใน Compass: stage $group ในแท็บ Aggregations ให้โครงสร้าง _id และ accumulator เดียวกันกับคุณ การดูตัวอย่างมีประโยชน์ตรงนี้ — เปลี่ยน expression ของ _id แล้วเฝ้าดูจำนวน document ผลลัพธ์เปลี่ยนไปเมื่อการจัดกลุ่มหยาบขึ้นหรือแยกย่อยลง

ตั้งค่า _id ของกลุ่มเป็น null แล้วทุก document จะตกลงในกลุ่ม เดียว ให้ยอดรวมทั้งหมดทั่วทั้ง stream นี่คือวิธีที่คุณคำนวณ “รายได้รวม” หรือ “ค่าเฉลี่ยโดยรวม” โดยไม่แยกตามคีย์:

db.sales.aggregate([
{ $group: {
_id: null,
totalRevenue: { $sum: { $multiply: ["$price", "$quantity"] } },
totalOrders: { $sum: 1 }
} }
])

document สรุปเดียวแทนทั้ง collection:

[
{ "_id": null, "totalRevenue": 248, "totalOrders": 6 }
]
  • หลังจาก $group มีแต่ฟิลด์ที่คุณสร้างขึ้นเท่านั้นที่ดำรงอยู่ ฟิลด์ document ต้นฉบับหายไป เว้นแต่คุณจะพาติดมาด้วย accumulator อย่าง $push หรือ $first
  • $sum: 1 คือสำนวน “นับแถวในกลุ่มนี้” ส่วน $sum: "$quantity" บวกค่าของฟิลด์แทน
  • $push เก็บค่าซ้ำและลำดับไว้ ส่วน $addToSet ลบค่าซ้ำและไม่รับรองลำดับ หยิบ $addToSet มาใช้เมื่อคุณต้องการลิสต์ที่ แตกต่างกัน
  • การจัดกลุ่มไม่เรียงลำดับ ถ้าคุณต้องการกลุ่มที่ใหญ่ที่สุดอยู่ก่อน ให้เพิ่ม stage $sort หลัง $group
ตัวเลือกBenefitCost
$group ใน pipelineคำนวณสรุปฝั่ง server ส่งกลับแค่ผลลัพธ์ที่ยุบแล้ว ประหยัด bandwidth และ memory ฝั่ง clientต้องคิดเป็น expression ของ MongoDB (accumulator) ซึ่งอาจจำกัดกว่า logic ที่เขียนใน application code
จัดกลุ่มใน application code (ดึง document ทั้งหมดมาแล้วสรุปเอง)ยืดหยุ่นเต็มที่ ใช้ library หรือ logic ใดก็ได้ต้องส่ง document ดิบทั้งหมดผ่านเครือข่ายก่อน ช้าและกิน memory มากเมื่อข้อมูลใหญ่
accumulator ที่แม่นยำ เช่น $sum, $avgผลลัพธ์ถูกต้อง 100% เหมาะกับตัวเลขทางการเงินหรือรายงานต้องสแกน/ประมวลผล document ทุกตัวในกลุ่ม ซึ่งหนักกว่าการประมาณค่า
  • คิดว่าฟิลด์ต้นฉบับยังอยู่หลัง $group โดยอัตโนมัติ — หลัง $group มีแค่ _id กับ accumulator ที่คุณประกาศไว้เท่านั้น ฟิลด์อื่นของ document ต้นฉบับหายไป เว้นแต่จะดึงมาด้วย $first, $last หรือ $push เอง
  • ใช้ $push เมื่อจริง ๆ ต้องการค่าที่ไม่ซ้ำ$push เก็บค่าซ้ำและรักษาลำดับ ถ้าต้องการ list ที่ไม่มีค่าซ้ำต้องใช้ $addToSet ไม่ใช่มา dedupe เองในภายหลัง
  • คาดหวังว่าผลลัพธ์จาก $group จะเรียงลำดับ$group ไม่รับประกันลำดับของกลุ่มที่ออกมา ต้องเติม $sort ต่อท้ายเองถ้าต้องการลำดับที่แน่นอน

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

Analytics dashboard — การคำนวณ “ยอดขายรวมต่อวัน” หรือ “จำนวนผู้ใช้ที่ active ต่อสัปดาห์” มักทำด้วย $group ที่มี _id เป็นวันที่/สัปดาห์ และ accumulator อย่าง $sum แทนที่จะดึง event ดิบทั้งหมดมานับใน application server

Keller Williams — รายงานยอดขายอสังหาริมทรัพย์ต่อนายหน้าใช้ $group พร้อม $sum และ $avg เพื่อสรุปมูลค่าธุรกรรมและราคาขายเฉลี่ยต่อคน โดยไม่ต้องดึงประวัติธุรกรรมทั้งหมดออกมาคำนวณนอกฐานข้อมูล

ฟิลด์ _id ของ stage $group ระบุอะไร?
คุณนับจำนวน document ในแต่ละกลุ่มอย่างไร?
accumulator ใดเก็บเฉพาะค่าที่แตกต่างกันเข้า array?
การตั้ง _id ของ $group เป็น null มีผลอย่างไร?