Anti-patterns
schema ของ MongoDB ที่แย่ส่วนใหญ่ไม่ได้แย่เพราะใครเมินกฎ แต่แย่เพราะการตัดสินใจที่ดูสมเหตุสมผลค่อย ๆ เสื่อมไปตามเวลา array ที่ embed ไว้ไม่มีปัญหาตอนมีสิบ element แต่กลายเป็นภาระตอนมีหมื่น สัญชาตญาณ “เก็บทุกอย่างไว้ด้วยกัน” ที่ทำให้เวอร์ชันแรกดูเรียบง่าย กลับลาก database จมในเวอร์ชันสอง
บทเรียนสุดท้ายนี้รวม anti-pattern ที่เจอบ่อยที่สุด: แต่ละแบบหน้าตาเป็นอย่างไร เจ็บตรงไหน และแก้ยังไงให้เป็นรูปธรรม การมองออกตั้งแต่ตอน design review คือหนึ่งในทักษะ modeling ที่คุ้มค่าที่สุดที่คุณจะฝึกได้
array ขนาดมหึมาที่ไม่มีขอบเขต
หัวข้อที่มีชื่อว่า “array ขนาดมหึมาที่ไม่มีขอบเขต”anti-pattern ที่เจอบ่อยที่สุดคือการ embed array ที่โตแบบไม่มีเพดาน — ทุก comment บน post ยอดนิยม ทุก event ในประวัติของผู้ใช้ การ append แต่ละครั้งทำให้ document ใหญ่ขึ้นและโหลดช้าลง แล้วก็ค่อย ๆ ไต่เข้าหาลิมิต 16 MB
วิธีแก้: แยกลูกออกไปเป็น collection ต่างหาก แล้วให้ลูกแต่ละตัว reference กลับมาที่ parent หรือใช้ Bucket pattern เพื่อจำกัดการเติบโตต่อช่วงเวลา อย่างเช่น post ก้อนนี้ ไม่ควร embed ทุก comment:
{ "_id": "post_310", "title": "Why documents win", "commentCount": 8421 }comment อยู่แยก แต่ละตัวชี้กลับมาที่ post post จึงยังเล็กอยู่ และ comment ก็แบ่งหน้าได้:
{ "_id": "cmt_9", "postId": "post_310", "user": "Grace", "text": "Great read." }document เดียวที่อ้วนเกิน
หัวข้อที่มีชื่อว่า “document เดียวที่อ้วนเกิน”ปัญหาใกล้เคียงกันคือความอยากยัด ทุกอย่าง ของ entity หนึ่งเข้าไปใน document เดียว — ผู้ใช้พร้อม activity log ฉบับเต็ม ทุกคำสั่งซื้อ ทุก preference ทุก session nest ซ้อนกันอยู่ข้างใน จนสุดท้าย document ใหญ่เกินตัว อ่านทีเดียวแค่จะเอา email address ก็ลากข้อมูลเป็นเมกะไบต์ออกจากดิสก์
วิธีแก้: เก็บเฉพาะ field ร้อนที่อ่านบ่อยไว้ใน document หลัก แล้วย้ายข้อมูลเย็นหรือก้อนใหญ่ไปไว้อีก collection แล้ว reference เอา read locality ช่วยได้ก็ต่อเมื่อข้อมูลถูกอ่านพร้อมกันจริง ๆ การ embed ข้อมูลเย็นไม่ได้อะไรกลับมาเลย แต่คุณจ่ายค่าปรับทุกครั้งที่อ่าน
Over-referencing
หัวข้อที่มีชื่อว่า “Over-referencing”ความผิดพลาดฝั่งตรงข้ามคือแยกข้อมูลออกมากเกินไป จน render หน้าจอเดียวต้องยิง query สิบกว่าครั้ง เท่ากับสร้างปัญหา relational join ที่ MongoDB ตั้งใจจะเลี่ยงขึ้นมาใหม่ ถ้าการแสดงคำสั่งซื้อหนึ่งใบต้องดึงคำสั่งซื้อ แล้วลูกค้า แล้วสินค้าทีละตัว แล้ว category ของสินค้าแต่ละตัวอีก — คุณ over-reference แล้ว
วิธีแก้: embed ข้อมูลที่อ่านพร้อมกันจริงและมีขอบเขตชัดเจน หรือใช้ Extended Reference pattern คัดลอกแค่ field หนึ่งสองตัวที่คุณแสดงทุกครั้ง การอ่านที่ใช้บ่อยจะได้ไม่แตกออกเป็น join ฝั่งแอป
การจับคู่แบบไม่สนตัวพิมพ์ใหญ่เล็กโดยไม่ใช้ collation
หัวข้อที่มีชื่อว่า “การจับคู่แบบไม่สนตัวพิมพ์ใหญ่เล็กโดยไม่ใช้ collation”อันนี้แนบเนียนกว่า: คุณอยากได้การค้นหาแบบไม่สนตัวพิมพ์ใหญ่เล็ก (ให้ "Ada", "ada" และ "ADA" match กันหมด) เลยหันไปใช้ regular expression อย่าง /^ada$/i ผลลัพธ์ถูก แต่ใช้ index ปกติได้ไม่เต็มที่ query จึงค่อย ๆ ตกไปเป็น scan แบบเงียบ ๆ
วิธีแก้: ตั้ง collation แบบไม่สนตัวพิมพ์ใหญ่เล็กไว้ที่ collection หรือที่ index แล้ว query equality ธรรมดาก็จะ match ข้ามตัวพิมพ์ และ ใช้ index ได้ด้วย ทางที่ MongoDB รองรับอย่างเป็นทางการและ index ได้คือ collation ไม่ใช่ flag ของ regex
ทุกอย่างอยู่ใน collection เดียว
หัวข้อที่มีชื่อว่า “ทุกอย่างอยู่ใน collection เดียว”ข้อสุดท้ายคือความอยากโยน entity ที่ไม่เกี่ยวกันเลย — ผู้ใช้ คำสั่งซื้อ สินค้า log — ลงไปใน collection เดียว แล้วแยกกันด้วย field type อย่างเดียว ผลคือ index บวมด้วย document ที่ไม่เกี่ยวข้อง validation อธิบายรูปร่างที่สม่ำเสมอไม่ได้ และทุก query ต้อง filter type ก่อนเสมอ
วิธีแก้: ให้ entity แต่ละชนิดมี collection ของตัวเอง collection ราคาถูกมาก เลย์เอาต์แบบหนึ่ง entity ต่อหนึ่ง collection ทำให้ index กระชับ validator มีความหมาย และ query เรียบง่าย
anti-pattern และวิธีแก้
หัวข้อที่มีชื่อว่า “anti-pattern และวิธีแก้”แต่ละความผิดพลาดมีทางแก้ตรง ๆ ที่คุณเจอมาแล้วในโมดูลนี้:
flowchart LR A1["Massive unbounded array"] --> F1["Reference children or use the Bucket pattern"] A2["Bloated single document"] --> F2["Keep hot fields, reference cold and large data"] A3["Over-referencing everywhere"] --> F3["Embed co-accessed data or extend the reference"] A4["Case-insensitive via regex"] --> F4["Use a case-insensitive collation"] A5["Everything in one collection"] --> F5["One entity per collection"]
สังเกตว่าทุกวิธีแก้คือการเอาบทเรียนก่อนหน้ามาใช้อย่างมีวินัย: model ตาม access pattern จำกัดขอบเขต array ที่ embed reference สิ่งที่แชร์กันและก้อนใหญ่ embed สิ่งที่อ่านพร้อมกัน แล้วปล่อยให้ cardinality กับ access pattern เป็นคนตัดสินแต่ละความสัมพันธ์ ไม่ใช่ความเคยชิน
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- anti-pattern แทบไม่เคยดูผิดในวันแรก แต่จะโผล่ตอนสเกล ดังนั้น review schema โดยคิดถึงการเติบโต ไม่ใช่แค่ขนาดข้อมูลวันนี้
- “unbounded array” กับ “bloated document” คือโรคเดียวกันคนละหน้า: embed ข้อมูลที่ควรจะ reference วิธีรักษาก็อันเดียวกัน
- Over-referencing คือการแก้เกินตัว — แยกทุกอย่างเพราะเคยเจ็บจากการ embed มาครั้งหนึ่ง คำตอบคือใช้วิจารณญาณเป็นราย ๆ ไป ไม่ใช่ตั้งกฎเหมารวม
- MongoDB Atlas ดันหลายข้อพวกนี้ขึ้นมาเป็นคำแนะนำ Schema Anti-Pattern ในเครื่องมือด้าน performance ถ้าคุณรันบน Atlas ก็ปล่อยให้ Atlas ชี้ array ไร้ขอบเขตและ document ที่ใหญ่เกินให้
- collection ราคาถูก ถ้าลังเลว่าจะผสม entity หลายชนิดไว้ด้วยกันไหม ให้แยก — เลย์เอาต์แบบแยกต่อ entity มักเสื่อมสภาพช้ากว่าเสมอ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| แนวทาง | Benefit | Cost |
|---|---|---|
| มีวินัยในการ modeling ตั้งแต่ต้น (จำกัดขอบเขต array, เลือก embed/reference ตาม access pattern) | anti-pattern น้อยลง แก้ปัญหาตอนสเกลง่ายกว่ามาก | ใช้เวลาออกแบบล่วงหน้ามากขึ้นก่อนเริ่มเขียนโค้ด |
| model แบบรวดเร็วโดยไม่วางแผนล่วงหน้า | เริ่มต้นเร็ว ส่งของได้ไว | anti-pattern สะสมเงียบ ๆ แล้วเจ็บหนักตอนสเกล ต้อง migrate schema ทีหลังภายใต้แรงกดดัน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- คิดว่า anti-pattern เป็นความผิดพลาดของมือใหม่เท่านั้น — ส่วนใหญ่คือการตัดสินใจที่สมเหตุสมผลตอน design แรก แล้วค่อยเสื่อมตามการเติบโตของข้อมูล ไม่ใช่ความประมาท
- แก้ anti-pattern โดยเปลี่ยน schema ทันทีโดยไม่มีแผน migration — การย้ายจาก embedded array ไปเป็น reference บน collection ที่มีข้อมูล production อยู่แล้ว ต้องมี rollout ทีละขั้น (dual-write แล้วค่อย backfill) ไม่ใช่ deploy แล้วจบ
💡 ตัวอย่างจากของจริง
Wordnik — กรณีศึกษาที่รู้จักกันดีเรื่อง unbounded array: เก็บทุก occurrence ของคำในประโยคตัวอย่างไว้ใน array เดียวของ document คำศัพท์ จนบาง document โตจนเป็นปัญหา ต้องย้ายไป reference แยก collection ภายหลัง
Parse (แพลตฟอร์ม backend-as-a-service เดิมของ Facebook) — เจอปัญหา performance เสื่อมจาก document ที่อ้วนเกินและ query pattern ที่ over-reference ข้าม collection มากเกินไป จนต้อง refactor schema กลางคัน