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

Anti-patterns

schema ของ MongoDB ที่แย่ส่วนใหญ่ไม่ได้แย่เพราะใครเมินกฎ แต่แย่เพราะการตัดสินใจที่ดูสมเหตุสมผลค่อย ๆ เสื่อมไปตามเวลา array ที่ embed ไว้ไม่มีปัญหาตอนมีสิบ element แต่กลายเป็นภาระตอนมีหมื่น สัญชาตญาณ “เก็บทุกอย่างไว้ด้วยกัน” ที่ทำให้เวอร์ชันแรกดูเรียบง่าย กลับลาก database จมในเวอร์ชันสอง

บทเรียนสุดท้ายนี้รวม anti-pattern ที่เจอบ่อยที่สุด: แต่ละแบบหน้าตาเป็นอย่างไร เจ็บตรงไหน และแก้ยังไงให้เป็นรูปธรรม การมองออกตั้งแต่ตอน design review คือหนึ่งในทักษะ modeling ที่คุ้มค่าที่สุดที่คุณจะฝึกได้

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." }

ปัญหาใกล้เคียงกันคือความอยากยัด ทุกอย่าง ของ entity หนึ่งเข้าไปใน document เดียว — ผู้ใช้พร้อม activity log ฉบับเต็ม ทุกคำสั่งซื้อ ทุก preference ทุก session nest ซ้อนกันอยู่ข้างใน จนสุดท้าย document ใหญ่เกินตัว อ่านทีเดียวแค่จะเอา email address ก็ลากข้อมูลเป็นเมกะไบต์ออกจากดิสก์

วิธีแก้: เก็บเฉพาะ field ร้อนที่อ่านบ่อยไว้ใน document หลัก แล้วย้ายข้อมูลเย็นหรือก้อนใหญ่ไปไว้อีก collection แล้ว reference เอา read locality ช่วยได้ก็ต่อเมื่อข้อมูลถูกอ่านพร้อมกันจริง ๆ การ embed ข้อมูลเย็นไม่ได้อะไรกลับมาเลย แต่คุณจ่ายค่าปรับทุกครั้งที่อ่าน

ความผิดพลาดฝั่งตรงข้ามคือแยกข้อมูลออกมากเกินไป จน 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

ข้อสุดท้ายคือความอยากโยน entity ที่ไม่เกี่ยวกันเลย — ผู้ใช้ คำสั่งซื้อ สินค้า log — ลงไปใน collection เดียว แล้วแยกกันด้วย field type อย่างเดียว ผลคือ index บวมด้วย document ที่ไม่เกี่ยวข้อง validation อธิบายรูปร่างที่สม่ำเสมอไม่ได้ และทุก query ต้อง filter type ก่อนเสมอ

วิธีแก้: ให้ entity แต่ละชนิดมี collection ของตัวเอง collection ราคาถูกมาก เลย์เอาต์แบบหนึ่ง entity ต่อหนึ่ง collection ทำให้ index กระชับ validator มีความหมาย และ query เรียบง่าย

แต่ละความผิดพลาดมีทางแก้ตรง ๆ ที่คุณเจอมาแล้วในโมดูลนี้:

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"]
แต่ละ anti-pattern ในการ modeling จับคู่กับวิธีแก้ที่เป็นรูปธรรมซึ่งดึงมาจากบทเรียนก่อนหน้า

สังเกตว่าทุกวิธีแก้คือการเอาบทเรียนก่อนหน้ามาใช้อย่างมีวินัย: 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 มักเสื่อมสภาพช้ากว่าเสมอ
แนวทางBenefitCost
มีวินัยในการ 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 กลางคัน

document embed array ที่โตโดยไม่มีขอบเขต วิธีแก้ที่แนะนำคืออะไร?
over-referencing สร้างปัญหาใดขึ้นมาใหม่?
วิธีที่ได้รับการสนับสนุนและเป็นมิตรกับ index สำหรับการจับคู่แบบไม่สนตัวพิมพ์ใหญ่เล็กใน MongoDB คืออะไร?
ทำไมการเก็บ entity ที่ไม่เกี่ยวข้องกันไว้ใน collection เดียวพร้อม field type จึงเป็น anti-pattern?