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

การ modeling ความสัมพันธ์

ทุกความสัมพันธ์ระหว่าง entity ตกอยู่ในหนึ่งในสามรูปร่างตาม cardinality — ของอย่างหนึ่งมีความสัมพันธ์กับของอีกอย่างหนึ่งจำนวนเท่าไร ผู้ใช้มี profile หนึ่งอัน (one-to-one) ผู้ใช้เขียน post หลายอัน (one-to-many) นักเรียนลงทะเบียนหลายวิชา และแต่ละวิชามีนักเรียนหลายคน (many-to-many) ใน relational database รูปร่างเป็นตัวกำหนดกลไกตายตัว คือ foreign key กับ join table ส่วนใน MongoDB รูปร่างแค่ช่วยจำกัดตัวเลือกให้แคบลง คุณยังต้องเลือกเองอยู่ดีว่าจะ embed หรือ reference โดยใช้ข้อแลกเปลี่ยนจากบทเรียนก่อนเป็นแนวทาง บทเรียนนี้จะพาไล่ทั้งสามรูปร่าง พร้อมข้อจำกัดด้านขนาดที่คุมทุกอย่างไว้

เมื่อ document หนึ่งสัมพันธ์กับอีก document หนึ่งเพียงหนึ่งเดียว embedding มักเป็นทางที่เหมาะตามธรรมชาติ — ข้อมูลที่เกี่ยวข้องมีขอบเขตจำกัด เป็นของ parent เดียว และถูกอ่านควบคู่กันไป ผู้ใช้และ profile ของเขากลายเป็น document เดียว:

{
"_id": "user_42",
"email": "[email protected]",
"profile": {
"displayName": "Ada Lovelace",
"bio": "Writes algorithms for engines that do not exist yet.",
"joinedYear": 2024
}
}
flowchart LR
  U["User document"] --> P["profile sub-object embedded inside"]
One-to-one: embed ข้อมูลที่เกี่ยวข้องเป็น sub-object ใน document เดียวกัน

คุณจะแยก one-to-one ออกเป็นสอง document ก็ต่อเมื่อส่วนที่เกี่ยวข้องนั้นใหญ่ ถูกอ่านไม่บ่อย หรือถูกเข้าถึงอย่างอิสระ — เช่น แยกไฟล์ binary ก้อนหนักหรือ audit block ที่แทบไม่เคยแตะออกไปอีก collection เพื่อให้ document ที่ร้อนยังคงเล็ก

ความสัมพันธ์แบบ one-to-many — parent หนึ่ง ลูกหลายตัว — มีสองรูปร่างที่ดี ถ้าลูกมีจำนวนน้อยและมีขอบเขต ให้ embed เป็น array ไปเลย อย่างบล็อกโพสต์กับ tag ไม่กี่ตัวก็เป็นการ embed ที่สะอาดมาก:

{
"_id": "post_310",
"title": "Why documents win",
"tags": ["mongodb", "modeling", "nosql"]
}

แต่เมื่อฝั่ง “many” สามารถโตได้ใหญ่ หรือถูก query ด้วยตัวเอง ให้ reference จากฝั่งลูก แทน: แต่ละ document ลูกเก็บ id ของ parent ผู้ใช้ที่อาจมี post เป็นพันควรเก็บ post ไว้อีก collection แล้วให้แต่ละ post ชี้กลับมา:

{ "_id": "post_310", "authorId": "user_42", "title": "Why documents win" }
flowchart TB
  subgraph FewBounded["Few and bounded — embed array"]
    A0["Post document"] --> A1["tags: array of values"]
  end
  subgraph ManyUnbounded["Many or unbounded — reference from child"]
    B0["User document"]
    B1["Post: authorId points to user"]
    B2["Post: authorId points to user"]
    B1 --> B0
    B2 --> B0
  end
One-to-many: embed เป็น array เมื่อลูกมีจำนวนน้อยและมีขอบเขต; reference จากฝั่งลูกเมื่อฝั่ง many โตได้ใหญ่

คำถามชี้ขาดคือ “รายการนี้จะโตได้ไม่จำกัดหรือไม่?” tag ของ post จะไม่ ส่วน post ของผู้ใช้อาจโต คำถามเดียวนั้นมักเลือก model ให้คุณได้

ในความสัมพันธ์แบบ many-to-many แต่ละฝั่งสัมพันธ์กับอีกฝั่งหลายตัว — นักเรียนกับวิชา บทความกับ tag นักแสดงกับภาพยนตร์ แนวทางที่สะอาดคือ referencing ด้วย array ของ id บนฝั่งใดฝั่งหนึ่งหรือทั้งสองฝั่ง การวาง array ไว้บนฝั่งที่คุณอ่านบ่อยที่สุดให้ query ที่ใช้ทั่วไปเร็วที่สุด ตรงนี้นักเรียนแต่ละคนพาวิชาที่ตนลงทะเบียนติดตัวมา:

{ "_id": "student_7", "name": "Linus", "courseIds": ["c_db", "c_os", "c_net"] }

และ document วิชายืนด้วยตัวเอง ถูก reference ด้วย id:

{ "_id": "c_db", "title": "Database Systems", "term": "Fall 2026" }
flowchart TB
  S1["Student: courseIds array"]
  S2["Student: courseIds array"]
  C1["Course c_db"]
  C2["Course c_os"]
  S1 --> C1
  S1 --> C2
  S2 --> C1
Many-to-many: เก็บ array ของ id ที่ถูก reference ไว้บนฝั่งที่คุณอ่านบ่อยที่สุด

การเก็บ array ของ reference ไว้บน ทั้งสอง ฝั่ง (นักเรียนแสดงรายการวิชาของตน และ วิชาแสดงรายการนักเรียนของตน) ทำให้อ่านได้เร็วทั้งสองทิศทาง แต่เพิ่มงานเขียนเป็นสองเท่า — การลงทะเบียนนักเรียนหนึ่งคนตอนนี้หมายถึงการอัปเดตสอง document และคอยทำให้สอดคล้องกัน เลือก reference สองฝั่งเฉพาะเมื่อคุณอ่านทั้งสองทิศทางบ่อยจริง ๆ

ทุก document ของ MongoDB มีเพดานแข็งที่ 16 เมกะไบต์ ซึ่งเหลือเฟือสำหรับข้อมูลทั่วไป แต่กลายเป็นกับดักทันทีเมื่อ array ที่ embed ไว้ไม่มีเพดานตามธรรมชาติ คำสั่งซื้อที่มีรายการสินค้าไม่กี่อันปลอดภัยตลอดไป ส่วนห้องแชทที่ embed ทุกข้อความ หรือ stock ticker ที่ embed ทุก price tick ในที่สุดจะกระแทกข้อจำกัด — และก่อนหน้านั้นนานแล้ว document ที่โตขึ้นเรื่อย ๆ จะกลายเป็นอ่านและเขียนใหม่ได้ช้า เพราะ server ย้ายทั้ง document ในแต่ละครั้งที่อัปเดต

กฎที่ตามมาคือ อย่า embed array ที่สามารถโตได้ไม่จำกัด ถ้าจำนวนเปิดกว้างไม่จำกัด ให้แยกลูกไปอีก collection แล้ว reference หรือใช้ Bucket pattern จากบทเรียนถัดไปเพื่อจำกัดการเติบโตของแต่ละ document Embed เฉพาะสิ่งที่จำกัดตามธรรมชาติ

  • รูปร่าง cardinality จำกัดตัวเลือกของคุณให้แคบลง แต่คุณยังคงเลือก embed หรือ reference สำหรับแต่ละความสัมพันธ์ตามขนาด การเติบโต และการเข้าถึง — ไม่มีคำตอบอัตโนมัติ
  • One-to-one เกือบทุกครั้ง embed; ข้อยกเว้นคือส่วนย่อยที่ใหญ่หรือแทบไม่ถูกอ่าน ซึ่งคุ้มที่จะแยกออกไปอีก collection
  • สำหรับ one-to-many ถามว่า “ฝั่ง many โตได้ไม่จำกัดหรือไม่?” ถ้าใช่ ให้ reference จากฝั่งลูก ถ้าไม่ array ที่ embed นั้นง่ายและเร็วกว่า
  • Reference สองฝั่งใน many-to-many ทำให้อ่านได้เร็วทั้งสองทิศทาง แต่ทุกการเขียนต้องอัปเดตทั้งสองฝั่ง — คุ้มเมื่อทั้งสองทิศทางร้อนเท่านั้น
  • ข้อจำกัด 16 MB เป็นกำแพงแข็ง แต่ ประสิทธิภาพ ของ document เสื่อมลงนานก่อนที่คุณจะไปถึง จงปฏิบัติต่อการเติบโตที่ไม่มีขอบเขตว่าเป็นศัตรูตัวจริง ไม่ใช่แค่เพดาน
รูปแบบBenefitCost
One-to-few embeddingอ่าน parent กับ child ได้ในครั้งเดียว เขียนแบบ atomicใช้ได้ดีเฉพาะเมื่อจำนวนลูกมีขอบเขตจริง ๆ
One-to-many referencing (จากฝั่งลูก)parent เล็กตลอดเวลา ลูกโตได้ไม่จำกัดและ query แยกได้อิสระต้อง query ที่สองเพื่อดึงลูกทั้งหมดของ parent
Many-to-many ด้วย reference array ทั้งสองฝั่งอ่านเร็วทั้งสองทิศทางโดยไม่ต้อง $lookupทุกการเขียนต้องอัปเดตสอง document พร้อมกัน เสี่ยง inconsistency ถ้าล้มเหลวกลางทาง
  • คิดว่า cardinality (one-to-many, many-to-many) ตัดสินใจ embed หรือ reference ให้เองโดยอัตโนมัติ — cardinality บอกแค่รูปร่างที่เป็นไปได้ ส่วน access pattern และการเติบโตต่างหากที่ตัดสินระหว่างตัวเลือกที่เหลือ
  • มองข้ามว่า one-to-many ที่ “ดูน้อย” วันนี้อาจไม่มีขอบเขตในอนาคต — tag ของ post ไม่โต แต่ comment ของ post โตได้ไม่จำกัด ต้องถามคำถาม “โตได้ไม่จำกัดหรือไม่” ทุกครั้ง ไม่ใช่แค่ดูจำนวนปัจจุบัน
  • ทำ many-to-many แบบ reference สองฝั่งโดยไม่จำเป็น — ถ้าคุณอ่านจากทิศทางเดียวเป็นหลัก การเก็บ reference array แค่ฝั่งเดียวก็พอ การเก็บสองฝั่งเพิ่มต้นทุนการเขียนโดยไม่มีประโยชน์ถ้าไม่มีใครอ่านอีกทิศทาง

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

MetLife — รวมข้อมูลลูกค้าจากระบบเดิมหลายระบบเข้าเป็น “single view” ด้วยการ embed ข้อมูลที่เกี่ยวข้องกับลูกค้าคนเดียว (one-to-few) ไว้ใน document เดียว แทนที่จะกระจายอยู่หลายตารางเหมือนระบบเดิม

Codecademy — model ความสัมพันธ์แบบ many-to-many ระหว่างผู้เรียนกับคอร์สด้วย reference array บนทั้งสองฝั่ง เพื่อให้ query ทั้ง “คอร์สที่ผู้เรียนคนนี้ลง” และ “ผู้เรียนที่ลงคอร์สนี้” เร็วทั้งสองทิศทาง

สำหรับความสัมพันธ์แบบ one-to-one ที่ข้อมูลที่เกี่ยวข้องมีขนาดเล็กและถูกอ่านด้วยกัน อะไรมักดีที่สุด?
ในความสัมพันธ์แบบ one-to-many เมื่อใดที่คุณควร reference จากฝั่งลูกแทนที่จะ embed เป็น array?
ขนาดสูงสุดแบบแข็งของ document หนึ่งใน MongoDB คือเท่าไร?
ทำไมการ embed array แบบไม่มีขอบเขตจึงอันตราย ทั้งที่ยังไม่ถึง 16 MB?