Schema design patterns
เมื่อคุณคล่องในการเลือกระหว่าง embedding กับ referencing แล้ว คุณจะเริ่มพบปัญหาในการ modeling เดิม ๆ ซ้ำแล้วซ้ำเล่า: document ที่พาข้อมูลมามากกว่าที่หน้าจอใดหน้าจอหนึ่งต้องการ ค่าที่แพงในการคำนวณใหม่ในทุกการอ่าน array ที่อยากโตไปตลอดกาล reference ที่บังคับให้ต้องค้นหาเพียงเพื่อแสดง field เพิ่มอันเดียว
ชุมชน MongoDB ตั้งชื่อวิธีแก้ที่เจอซ้ำ ๆ พวกนี้ว่า schema design patterns ไม่ใช่ฟีเจอร์ที่กดเปิด แต่เป็นรูปร่างที่คุณตั้งใจออกแบบให้ document บทเรียนนี้ครอบคลุมสี่อันที่มีประโยชน์ที่สุด: Subset, Computed, Bucket และ Extended Reference
Subset pattern
หัวข้อที่มีชื่อว่า “Subset pattern”ปัญหา: document embed array ขนาดใหญ่ แต่การอ่านส่วนใหญ่ต้องการแค่ element ไม่กี่ตัวแรก การโหลดทั้งก้อนเปลืองหน่วยความจำและแบนด์วิดท์ วิธีแก้: เก็บ subset — element ไม่กี่ตัวที่คุณแสดงจริง ๆ — ไว้ใน document หลัก แล้วผลักรายการเต็มไปยัง collection แยกต่างหาก สินค้าตัวหนึ่งเก็บรีวิวล่าสุดไว้ inline:
{ "_id": "BK-204", "title": "Distributed Systems", "recentReviews": [ { "user": "Grace", "stars": 5, "text": "Indispensable." }, { "user": "Linus", "stars": 4, "text": "Dense but worth it." } ], "reviewCount": 1183}ใช้เมื่อ working set เป็นแค่เศษเล็ก ๆ ของ collection ลูกก้อนใหญ่ และคุณอยากให้การอ่านที่ใช้บ่อยยังเร็ว โดยที่ข้อมูลเต็มยังหยิบมาได้เมื่อต้องการ
Computed pattern
หัวข้อที่มีชื่อว่า “Computed pattern”ปัญหา: ค่าหนึ่งได้มาจาก source document หลายตัว และมีการอ่านบ่อยกว่าที่ source เปลี่ยนมาก การคำนวณใหม่ทุกครั้งที่อ่านจึงเปลืองเปล่า วิธีแก้: คำนวณครั้งเดียวตอนเขียน แล้วเก็บผลลัพธ์ไว้ คำสั่งซื้อรวมยอด total ของตัวเองแทนที่จะ re-sum รายการในทุกการอ่าน:
{ "_id": "order_5001", "itemCount": 3, "subtotal": 79.0, "tax": 6.32, "total": 85.32}ใช้เมื่อ งานอ่านมากกว่างานเขียนมหาศาล และการคำนวณไม่ใช่เรื่องเล็ก ๆ คุณแลกงานเพิ่มเล็กน้อยตอนเขียน — และความรับผิดชอบในการทำให้ field ที่คำนวณนั้นถูกต้องอยู่เสมอ — กับการอ่านที่ถูกและทันที
Bucket pattern
หัวข้อที่มีชื่อว่า “Bucket pattern”ปัญหา: คุณมี stream ของเรคคอร์ดเล็ก ๆ จำนวนมาก (sensor reading, log line, price tick) และการเก็บหนึ่ง document ต่อหนึ่ง reading สร้าง document เล็ก ๆ นับล้าน — สิ้นเปลืองทั้ง overhead และขนาด index วิธีแก้: bucket reading หลายตัวเข้าไว้ใน document เดียวที่จัดกลุ่มตามช่วงเวลา เช่น หนึ่งชั่วโมง หนึ่ง bucket เก็บการวัดหนึ่งชั่วโมง:
{ "_id": "sensor_9:2026-03-14T09", "sensorId": "sensor_9", "hour": "2026-03-14T09:00:00Z", "count": 60, "readings": [ { "t": "09:00:01Z", "celsius": 21.4 }, { "t": "09:01:01Z", "celsius": 21.5 } ]}ใช้เมื่อ คุณรับข้อมูล time-series หรือ streaming การ bucket จำกัดการเติบโตของแต่ละ document ไว้ที่ช่วงเวลาที่รู้แน่ ดังนั้นคุณจึงได้ประโยชน์ด้านการอ่านของ embedding โดยไม่ติดกับดัก unbounded-array จากบทเรียนก่อนหน้า
Extended Reference pattern
หัวข้อที่มีชื่อว่า “Extended Reference pattern”ปัญหา: คุณ reference อีก document หนึ่งแต่ต้องใช้ field ของ document นั้นหนึ่งสองตัวทุกครั้งที่ render parent จนต้องยิง query เพิ่มแค่เพื่อเอาชื่อมาโชว์ วิธีแก้: คัดลอก field ไม่กี่ตัวนั้นมาไว้ข้าง ๆ reference เป็นการ extend reference คำสั่งซื้อจึงเก็บ id ของลูกค้า และ field ลูกค้าไม่กี่ตัวที่ต้องแสดงเสมอ:
{ "_id": "order_5001", "customer": { "id": "cust_77", "name": "Grace Hopper", "city": "Cambridge" }, "status": "shipped"}ใช้เมื่อ field ที่ reference ไว้ถูกอ่านตลอดแต่แทบไม่เปลี่ยน คุณตัด query เพิ่มทิ้งได้ทุกครั้งที่อ่าน แลกกับการต้อง refresh field ที่คัดลอกมาเมื่อต้นทางเปลี่ยน — เป็นการแลกที่คุ้มค่าสำหรับข้อมูลที่นิ่งอย่างชื่อหรือเมือง
เลือก pattern ตามปัญหา
หัวข้อที่มีชื่อว่า “เลือก pattern ตามปัญหา”แต่ละ pattern ตอบความเจ็บปวดเฉพาะอย่าง จับคู่อาการที่เจอกับ pattern ที่รักษาตรงจุด:
flowchart LR P1["Large array, only top few read"] --> S["Subset pattern"] P2["Expensive value, read often"] --> C["Computed pattern"] P3["High-volume stream of tiny records"] --> B["Bucket pattern"] P4["Reference needs one or two extra fields"] --> E["Extended Reference pattern"]
pattern เหล่านี้ผสมกันได้อย่างอิสระ แอป time-series อาจ bucket reading เก็บค่าเฉลี่ยรายชั่วโมงที่คำนวณไว้บนแต่ละ bucket แล้วเก็บ extended reference ไปหาชื่อ sensor — สาม pattern ใน document เดียว แต่ละตัวทำหน้าที่ของตัวเอง
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- pattern เป็นข้อแลกเปลี่ยน ไม่ใช่ของฟรี Subset และ Extended Reference ต่างก็นำสำเนาที่คุณต้องคอยทำให้เป็นปัจจุบันเข้ามา ส่วน Computed เพิ่มงานตอนเขียน และ Bucket เพิ่ม logic ในการจัดกลุ่มเล็กน้อย
- Computed และ Extended Reference pattern ต่างก็ denormalize — ทั้งคู่เก็บข้อมูลที่คำนวณหรือคัดลอกมา ซึ่งใน MongoDB ไม่ผิด ตราบใดที่คุณมีแผนชัดเจนว่าจะ refresh เมื่อไร
- Bucket pattern คือคำตอบมาตรฐานสำหรับข้อมูล time-series และแก้ปัญหา unbounded array ตรง ๆ ด้วยการจำกัดการเติบโตต่อช่วงเวลา
- อย่าหยิบ pattern มาใช้แบบเผื่อไว้ก่อน ใช้ต่อเมื่อคุณชี้ได้ชัดว่าแก้ปัญหาอะไรใน workload ของคุณ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| Pattern | ประโยชน์ | ต้นทุน |
|---|---|---|
| Bucket pattern | ลดจำนวน document ต่อการอ่าน overhead ต่อ index น้อยลงมาก | ต้องออกแบบขนาด bucket ล่วงหน้า และ query ข้าม bucket ซับซ้อนขึ้น |
| Computed pattern | อ่านเร็วมาก ไม่ต้องคำนวณค่าเดิมซ้ำในทุกการอ่าน | เพิ่มงานตอนเขียน และเสี่ยง field ที่คำนวณไว้ล้าสมัย (stale) ถ้าลืม refresh |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้ pattern ก่อนที่จะมีปัญหาจริง — Bucket, Computed, Subset และ Extended Reference ล้วนเพิ่มความซับซ้อนให้ schema อย่าหยิบมาใช้แบบเก็งกำไรก่อนวัด workload จริง
- ใช้ Computed pattern แล้วลืม refresh ค่าที่คำนวณไว้ — field ที่ precompute จะเก่าค้างทันทีถ้า source เปลี่ยนแล้วไม่มี job หรือ trigger คอยอัปเดตตาม
- เลือกขนาด bucket ผิดใน Bucket pattern — bucket เล็กเกินไปแทบไม่ต่างจากการไม่ bucket เลย ส่วน bucket ใหญ่เกินไปก็เสี่ยงชนขนาด document เหมือน unbounded array เดิม
💡 ตัวอย่างจากของจริง
Bosch — ใช้ Bucket pattern เก็บข้อมูล telemetry จากอุปกรณ์ IoT โดยจัดกลุ่ม reading เป็น document ต่อช่วงเวลา (เช่นต่อชั่วโมง) แทนที่จะสร้าง document ใหม่ทุก reading ลดจำนวน document ลงหลายเท่าตัว
EA (Electronic Arts) — ใช้แนวทางแบบ Computed pattern precompute สถิติผู้เล่นและ leaderboard ไว้ล่วงหน้า แทนที่จะ aggregate คะแนนจากทุก match ใหม่ทุกครั้งที่มีคนเปิดดู