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

Schema design patterns

เมื่อคุณคล่องในการเลือกระหว่าง embedding กับ referencing แล้ว คุณจะเริ่มพบปัญหาในการ modeling เดิม ๆ ซ้ำแล้วซ้ำเล่า: document ที่พาข้อมูลมามากกว่าที่หน้าจอใดหน้าจอหนึ่งต้องการ ค่าที่แพงในการคำนวณใหม่ในทุกการอ่าน array ที่อยากโตไปตลอดกาล reference ที่บังคับให้ต้องค้นหาเพียงเพื่อแสดง field เพิ่มอันเดียว

ชุมชน MongoDB ตั้งชื่อวิธีแก้ที่เจอซ้ำ ๆ พวกนี้ว่า schema design patterns ไม่ใช่ฟีเจอร์ที่กดเปิด แต่เป็นรูปร่างที่คุณตั้งใจออกแบบให้ document บทเรียนนี้ครอบคลุมสี่อันที่มีประโยชน์ที่สุด: Subset, Computed, Bucket และ Extended Reference

ปัญหา: 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 ลูกก้อนใหญ่ และคุณอยากให้การอ่านที่ใช้บ่อยยังเร็ว โดยที่ข้อมูลเต็มยังหยิบมาได้เมื่อต้องการ

ปัญหา: ค่าหนึ่งได้มาจาก source document หลายตัว และมีการอ่านบ่อยกว่าที่ source เปลี่ยนมาก การคำนวณใหม่ทุกครั้งที่อ่านจึงเปลืองเปล่า วิธีแก้: คำนวณครั้งเดียวตอนเขียน แล้วเก็บผลลัพธ์ไว้ คำสั่งซื้อรวมยอด total ของตัวเองแทนที่จะ re-sum รายการในทุกการอ่าน:

{
"_id": "order_5001",
"itemCount": 3,
"subtotal": 79.0,
"tax": 6.32,
"total": 85.32
}

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

ปัญหา: คุณมี 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 จากบทเรียนก่อนหน้า

ปัญหา: คุณ 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 ที่รักษาตรงจุด:

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"]
แต่ละ design pattern จับคู่กับปัญหาในการ modeling ที่เกิดซ้ำเฉพาะอย่าง

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 ใหม่ทุกครั้งที่มีคนเปิดดู

pattern ใดเก็บเฉพาะ element ไม่กี่อันที่ถูกอ่านบ่อยไว้ใน document หลัก และเก็บรายการเต็มไว้ที่อื่น?
คุณเก็บ total ของคำสั่งซื้อแทนที่จะ re-sum รายการในทุกการอ่าน นั่นคือ pattern ใด?
pattern ใดจัดกลุ่มเรคคอร์ด time-series เล็ก ๆ จำนวนมากเข้าไว้ใน document เดียวต่อช่วงเวลาเพื่อจำกัดการเติบโต?
คุณคัดลอกชื่อลูกค้าไว้ข้าง customerId เพื่อหลีกเลี่ยงการค้นหาเมื่อ render คำสั่งซื้อ นั่นคือ pattern ใด?