การ modeling ความสัมพันธ์
ทุกความสัมพันธ์ระหว่าง entity ตกอยู่ในหนึ่งในสามรูปร่างตาม cardinality — ของอย่างหนึ่งมีความสัมพันธ์กับของอีกอย่างหนึ่งจำนวนเท่าไร ผู้ใช้มี profile หนึ่งอัน (one-to-one) ผู้ใช้เขียน post หลายอัน (one-to-many) นักเรียนลงทะเบียนหลายวิชา และแต่ละวิชามีนักเรียนหลายคน (many-to-many) ใน relational database รูปร่างเป็นตัวกำหนดกลไกตายตัว คือ foreign key กับ join table ส่วนใน MongoDB รูปร่างแค่ช่วยจำกัดตัวเลือกให้แคบลง คุณยังต้องเลือกเองอยู่ดีว่าจะ embed หรือ reference โดยใช้ข้อแลกเปลี่ยนจากบทเรียนก่อนเป็นแนวทาง บทเรียนนี้จะพาไล่ทั้งสามรูปร่าง พร้อมข้อจำกัดด้านขนาดที่คุมทุกอย่างไว้
One-to-one
หัวข้อที่มีชื่อว่า “One-to-one”เมื่อ document หนึ่งสัมพันธ์กับอีก document หนึ่งเพียงหนึ่งเดียว embedding มักเป็นทางที่เหมาะตามธรรมชาติ — ข้อมูลที่เกี่ยวข้องมีขอบเขตจำกัด เป็นของ parent เดียว และถูกอ่านควบคู่กันไป ผู้ใช้และ profile ของเขากลายเป็น document เดียว:
{ "_id": "user_42", "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 ออกเป็นสอง document ก็ต่อเมื่อส่วนที่เกี่ยวข้องนั้นใหญ่ ถูกอ่านไม่บ่อย หรือถูกเข้าถึงอย่างอิสระ — เช่น แยกไฟล์ binary ก้อนหนักหรือ audit block ที่แทบไม่เคยแตะออกไปอีก collection เพื่อให้ document ที่ร้อนยังคงเล็ก
One-to-many
หัวข้อที่มีชื่อว่า “One-to-many”ความสัมพันธ์แบบ 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 คำถามชี้ขาดคือ “รายการนี้จะโตได้ไม่จำกัดหรือไม่?” tag ของ post จะไม่ ส่วน post ของผู้ใช้อาจโต คำถามเดียวนั้นมักเลือก model ให้คุณได้
Many-to-many
หัวข้อที่มีชื่อว่า “Many-to-many”ในความสัมพันธ์แบบ 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
การเก็บ array ของ reference ไว้บน ทั้งสอง ฝั่ง (นักเรียนแสดงรายการวิชาของตน และ วิชาแสดงรายการนักเรียนของตน) ทำให้อ่านได้เร็วทั้งสองทิศทาง แต่เพิ่มงานเขียนเป็นสองเท่า — การลงทะเบียนนักเรียนหนึ่งคนตอนนี้หมายถึงการอัปเดตสอง document และคอยทำให้สอดคล้องกัน เลือก reference สองฝั่งเฉพาะเมื่อคุณอ่านทั้งสองทิศทางบ่อยจริง ๆ
ข้อจำกัด 16 MB และ array ที่ไม่มีขอบเขต
หัวข้อที่มีชื่อว่า “ข้อจำกัด 16 MB และ array ที่ไม่มีขอบเขต”ทุก 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 เสื่อมลงนานก่อนที่คุณจะไปถึง จงปฏิบัติต่อการเติบโตที่ไม่มีขอบเขตว่าเป็นศัตรูตัวจริง ไม่ใช่แค่เพดาน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| รูปแบบ | Benefit | Cost |
|---|---|---|
| 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 ทั้ง “คอร์สที่ผู้เรียนคนนี้ลง” และ “ผู้เรียนที่ลงคอร์สนี้” เร็วทั้งสองทิศทาง