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

Embedding กับ referencing

มีสองวิธีเท่านั้นในการแสดงความสัมพันธ์ระหว่าง document ใน MongoDB และเกือบทุกการตัดสินใจในการ modeling คือการเลือกระหว่างสองวิธีนี้ คุณสามารถ embed: วางข้อมูลที่เกี่ยวข้องไว้ภายใน parent document โดย nest เป็น sub-object หรือ array หรือ reference: เก็บข้อมูลที่เกี่ยวข้องไว้อีก collection แล้วเก็บตัวระบุ (ปกติคือ _id ของ document นั้น) ไว้ใน parent แล้วค่อยยิง query รอบสองไปดึงตอนต้องใช้ Embedding เอื้อต่อการอ่าน ส่วน referencing เอื้อต่อความยืดหยุ่นและหลีกเลี่ยงสำเนา การรู้ว่าควรหยิบอันไหนมาใช้คือสัญชาตญาณที่มีประโยชน์ที่สุดอย่างหนึ่งในการ modeling แบบ document

เราจะใช้คำสั่งซื้อและรายการสินค้าในนั้นเป็นตัวอย่างที่ใช้ต่อเนื่อง เพราะข้อมูลเดียวกันสามารถไปได้ทั้งสองทางอย่างสมเหตุสมผล ขึ้นอยู่กับว่าแอปพลิเคชันทำงานอย่างไร

เมื่อคุณ embed ข้อมูลที่เกี่ยวข้องจะอยู่ภายใน parent คำสั่งซื้อด้านล่างพารายการสินค้ามาด้วยเป็น array ที่ nest อยู่ ทำให้ document คือคำตอบที่สมบูรณ์ของ “แสดงคำสั่งซื้อนี้ให้ฉันดู”:

{
"_id": "order_5001",
"customer": "Grace Hopper",
"status": "shipped",
"items": [
{ "sku": "BK-204", "title": "Distributed Systems", "qty": 1, "price": 42.0 },
{ "sku": "BK-991", "title": "The Mythical Man-Month", "qty": 2, "price": 18.5 }
]
}

สิ่งที่ได้คือ read locality: ทุกอย่างที่คุณต้องการมาถึงในการอ่านครั้งเดียว ไม่มี join ไม่มี round trip รอบสอง ข้อมูลที่ embed ยัง update แบบ atomic ไปพร้อม parent ได้ด้วย — การเขียนครั้งเดียวแตะทั้งคำสั่งซื้อและรายการสินค้าทั้งหมดไปพร้อมกัน ต้นทุนมีสองอย่าง อย่างแรกคือ ความซ้ำซ้อน: ชื่อและราคาของสินค้าเป็นสำเนา หากราคาสินค้าตัวจริง (canonical) เปลี่ยน สำเนานี้จะไม่เปลี่ยนตาม อย่างที่สองคือ การเติบโตของ document: ทุกรายการที่ embed ทำให้คำสั่งซื้อใหญ่ขึ้น และ document ที่เติบโตโดยไม่มีขอบเขตในที่สุดจะชนเพดานแข็ง 16 MB

เมื่อคุณ reference parent จะเก็บแค่ตัวระบุ ส่วนข้อมูลจริงอยู่ที่อื่น ตรงนี้คำสั่งซื้อเก็บลูกค้าเป็น reference และเก็บ id ของสินค้าสำหรับรายการต่าง ๆ แทนที่จะคัดลอกรายละเอียดสินค้า:

{
"_id": "order_5001",
"customerId": "cust_77",
"status": "shipped",
"items": [
{ "productId": "BK-204", "qty": 1 },
{ "productId": "BK-991", "qty": 2 }
]
}

รายละเอียดสินค้าอยู่ที่เดียว คือใน collection ของสินค้า:

{ "_id": "BK-204", "title": "Distributed Systems", "price": 42.0 }

ตอนนี้ ไม่มีความซ้ำซ้อน — การเปลี่ยนราคาใน products collection ถูกเห็นทุกที่ทันที — และ document คำสั่งซื้อยังคงเล็ก ต้นทุนคือภาพสะท้อนกลับด้านของ embedding: การ render คำสั่งซื้อต้องใช้ การค้นหาครั้งที่สอง (join ฝั่งแอปพลิเคชัน หรือ $lookup ใน aggregation pipeline) เพื่อแปลง id เหล่านั้นให้เป็นชื่อและราคา คุณแลกการอ่านครั้งเดียวที่รวดเร็วกับการรับประกันว่าข้อมูลจะไม่มีวันเก่าค้าง

การตัดสินใจมาลงที่ว่าคุณอยากเจ็บแบบไหนมากกว่า: ต้นทุนของสำเนาที่อาจคลาดเคลื่อน หรือต้นทุนของการค้นหาเพิ่มในทุกการอ่าน

flowchart TB
  subgraph Embed["Embedding — one document"]
    E0["Read the order"] --> E1["Order document"]
    E1 --> E2["items array nested inside"]
    E2 --> E3["Done — one read, no join"]
  end
  subgraph Reference["Referencing — linked documents"]
    F0["Read the order"] --> F1["Order document with productId values"]
    F1 --> F2["Second lookup into products"]
    F2 --> F3["Join results to render"]
  end
Embedding ตอบการอ่านในครั้งเดียวแต่คัดลอกข้อมูล ส่วน referencing หลีกเลี่ยงสำเนาแต่ต้องการการค้นหาครั้งที่สอง

กฎคร่าว ๆ ที่มีประโยชน์: embed ข้อมูลที่ อ่านด้วยกัน เขียนด้วยกัน และมีขนาดจำกัด ส่วน reference ข้อมูลที่ ใหญ่ ถูกแชร์ข้าม parent หลายตัว ถูกอัปเดตบ่อยด้วยตัวเอง หรือมีจำนวนไม่จำกัด ชื่อลูกค้าและรายการสินค้าที่เป็นของคำสั่งซื้อเดียวเท่านั้นคือการ embed ที่เป็นธรรมชาติ ส่วน product catalog ที่แชร์โดยคำสั่งซื้อนับพัน หรือรายการ comment ที่อาจโตได้ไม่จำกัด คือ reference ที่เป็นธรรมชาติ

ใน Compass: เปิดแท็บ Schema บน collection แล้ว Compass จะสุ่มตัวอย่าง document ของคุณเพื่อบอกว่ามี field อะไรบ้าง เป็น type ไหน และ array กับ nested object โผล่บ่อยแค่ไหน — เป็นวิธีรวดเร็วในการดูว่าจริง ๆ แล้วคุณได้ embedding หรือ referencing อยู่ และ array ที่ embed ของคุณโตขึ้นมากแค่ไหน

  • Embedding ให้การเขียนแบบ atomic แก่คุณฟรี ๆ: parent และลูกที่ embed ไว้เปลี่ยนในการทำงานครั้งเดียว โดยไม่ต้องประสานงานแบบ multi-document
  • Referencing เก็บข้อมูลที่แชร์กันไว้ที่เดียว ดังนั้นการอัปเดตจึงถูกเห็นทุกที่ — แต่ทุกการอ่านที่ต้องการข้อมูลที่ resolve แล้วต้องจ่ายค่าค้นหาเพิ่ม
  • คุณสามารถผสมทั้งสองแบบใน document เดียวได้ เป็นเรื่องปกติที่จะ embed snapshot ขนาดเล็กแบบ denormalize (ไม่กี่ field ที่คุณแสดงบ่อย ๆ) และ เก็บ reference ไปยังเรคคอร์ดฉบับเต็มไว้ใช้เมื่อต้องการทุกอย่าง
  • จับตาการเติบโตของ document บน array ที่ embed array ที่มีแต่โตขึ้นเรื่อย ๆ คือวิธีที่พบบ่อยที่สุดที่การตัดสินใจ embed จะค่อย ๆ กลายเป็นปัญหาอย่างเงียบ ๆ
สถานการณ์Benefit ของตัวเลือกที่แนะนำCost
1:1 (เช่น user + profile) — embedอ่านครั้งเดียว เขียนแบบ atomicdocument ใหญ่ขึ้นเล็กน้อย แยกออกทีหลังยากถ้าส่วนนั้นต้อง scale เดี่ยว ๆ
1:many bounded (เช่น post + tags) — embedเร็ว เรียบง่าย ไม่ต้อง joinต้องมั่นใจว่าขอบเขตจำกัดจริง ไม่งั้นย้อนกลับมาแก้ยากภายหลัง
1:many unbounded (เช่น user + posts) — reference จากฝั่งลูกparent เล็กตลอดเวลา ลูก query และแบ่งหน้าได้อิสระต้อง query ที่สองเพื่อดึงลูกทั้งหมด ไม่มี atomic write ข้าม parent-child
many:many (เช่น student ↔ course) — reference array ทั้งสองฝั่งอ่านเร็วทั้งสองทิศทางโดยไม่ต้อง $lookupเขียนสองเท่า ต้องคอย sync ทั้งสองฝั่งให้ตรงกันเสมอ
  • คิดว่า embedding เร็วกว่า referencing เสมอ — ถ้า array ที่ embed โตไม่มีขอบเขต การอ่านและเขียนจะช้าลงเรื่อย ๆ เพราะ server ต้องจัดการ document ทั้งก้อนทุกครั้ง สุดท้ายช้ากว่าการ reference เสียอีก
  • คิดว่า $lookup ทำงานเหมือน JOIN ใน SQL ทุกประการ$lookup ไม่มี query optimizer แบบ relational database คอยเลือก join strategy ให้ ยิง $lookup บน hot path บ่อย ๆ แล้ว performance จะแย่กว่าที่คิดไว้เยอะ
  • ยึดกฎ embed-or-reference เดียวกับทุกความสัมพันธ์ใน schema — ต้องตัดสินทีละความสัมพันธ์ตาม access pattern และการเติบโตของความสัมพันธ์นั้น ๆ ไม่ใช่นโยบายเดียวที่ใช้กับทุก field

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

eBay — จัดการ product listing จำนวนมหาศาลด้วยการ reference แทนที่จะ embed ทุกอย่างไว้ที่เดียว เพื่อให้ seller อัปเดตราคาหรือสต๊อกได้โดยไม่ต้องแก้ document อื่นที่ point มาที่สินค้าเดียวกัน

Forbes — embed field ของบทความ เช่น tag และ metadata สั้น ๆ ไว้ใน document เดียวกัน เพื่อให้หน้าแสดงบทความโหลดได้ในการอ่านครั้งเดียว โดยไม่ต้อง join ไปหลาย collection

ข้อได้เปรียบหลักของการ embed ข้อมูลที่เกี่ยวข้องไว้ใน document คืออะไร?
ต้นทุนหลักของการ reference แทนที่จะ embed คืออะไร?
ข้อมูลใดเป็นตัวเลือกที่เป็นธรรมชาติที่สุดสำหรับการ reference มากกว่าการ embed?
ทำไมความซ้ำซ้อนจาก embedding บางครั้งจึงเป็นปัญหา?