Home Database แนวทางการออกแบบ Schema ที่เกี่ยวข้องกับ Array

แนวทางการออกแบบ Schema ที่เกี่ยวข้องกับ Array

by khomkrit

มีแนวคิดข้อหนึ่งที่ต้องคำนึงถึงเวลาออกแบบโครงสร้างข้อมูลใน MongoDB ก็คือ ข้อมูลที่ถูกใช้ด้วยกันควรถูกเก็บไว้ด้วยกัน ด้วยแนวคิดนี้ เราสามารถเก็บข้อมูลเข้าไว้ด้วยกันได้ด้วยการ embed document ลงไปเป็น subdocument หรือเก็บเป็น array

เวลามีคนตั้งคำถามว่า “ฉันมีข้อมูลที่มีความสัมพันธ์กันแบบนี้จะออกแบบโครงสร้างข้อมูลอย่างไรดี” คำตอบก็คือ ยังตอบไม่ได้ เพราะนอกจากต้องดูความสัมพันธ์ของข้อมูลแล้ว ยังจำเป็นต้องดูลักษณะการใช้งานข้อมูลประกอบด้วย

นักพัฒนาบางคนอาจเริ่มออกแบบ database schema แบบคร่าวๆ แล้วก็เริ่มลุยสร้าง application เลย โดยแทบไม่ได้หยุดนึกถึง best practice ต่างๆ ที่มีอยู่เลยสักนิด เมื่อเวลาที่แอปมันโตขึ้น ปัญหาต่างๆ ก็ตามมา สุดท้ายก็ต้องมาเหนื่อยแก้กันภายหลังอีก แต่ในที่นี้ผมก็ไม่ได้หมายความว่าการออกแบบให้ดีตั้งแต่ทีแรกนั้นจะทำให้เราไม่ต้องมาแก้ไขอะไรกันอีกในภายหลังนะครับ เพราะเมื่อระบบมันโตขึ้น business logic บางอย่างอาจมีเปลี่ยนไปตามกาลเวลา อย่างไรก็ตาม การนึกถึงเรื่องการออกแบบให้ดีตั้งแต่แรกๆ เลยนั้นน่าจะดีกว่าเดินดุ่มๆ ทำมันไปแบบลูกทุ่งโดยไม่ได้คิดอะไรเลย

จากแนวคิดที่ยกมาข้างต้น เมื่อพูดถึงการ embed ปัญหาที่อาจตามมาอย่างหนึ่งก็คือการ embed ข้อมูลขนาดใหญ่จนเกินไป ทั้งที่ตอนแรกมันก็ไม่ได้ใหญ่หรอก

ความสัมพันธ์ของข้อมูลอย่าง 1-to-many นักพัฒนามักออกแบบโครงสร้างข้อมูลโดยการเก็บเป็น array ยกตัวอย่างเช่น เรามี app ที่แต่ละ app มีได้หลาย version เราก็เลือกที่จะ embed version ต่างๆ ลงไปใน app ด้วยเลย จะได้โครงสร้างข้อมูลออกมาเป็นแบบนี้

{
  title: 'demo',
  bundle_id: 'com.khomkrit.demo',
  description: 'This is a cool app in the AppStore',
  versions: [
    { version: 1, releaseDate: '1 Jan 2020', releaseNote: 'something' },
    { version: 2, releaseDate: '2 Jan 2020', releaseNote: 'something' },
    { version: 3, releaseDate: '3 Jan 2020', releaseNote: 'something' },
    { version: 4, releaseDate: '4 Jan 2020', releaseNote: 'something' },
    { version: 5, releaseDate: '5 Jan 2020', releaseNote: 'something' }
  ]
}

กรณีนี้เราจะสังเกตได้ว่า เมื่อเวลาผ่านไป version จะมีจำนวนเพิ่มขึ้นเรื่อยๆ และไม่มีขอบเขตชัดเจนว่าจะเพิ่มขึ้นถึงเท่าไหร่ ส่งผลให้ array นี้โตขึ้นเรื่อยๆ และในวันหนึ่งข้างหน้าอาจใหญ่จน document นี้มีขนาดเกิน 16MB ได้ ซึ่งแน่นอนว่าถ้า use case เราเป็นแบบนี้ เราก็ไม่ควรออกแบบให้ embed array ไว้ใน object

Flip

วิธีแก้ไขที่ทำได้อย่างหนึ่งก็คือการสลับที่กัน (flip) โดยแทนที่จะ embed version ไว้ใน app ก็ให้ embed app ไว้ใน version แทน ดังนี้

{
  version: 1,
  releaseDate: '1 Jan 2020',
  releaseNote: 'something',
  app: {
    title: 'demo',
    bundle_id: 'com.khomkrit.demo',
    description: 'This is a cool app in the AppStore'
  }
},
{
  version: 2,
  releaseDate: '2 Jan 2020',
  releaseNote: 'something',
  app: {
    title: 'demo',
    bundle_id: 'com.khomkrit.demo',
    description: 'This is a cool app in the AppStore'
  }
},
{
  version: 3,
  releaseDate: '3 Jan 2020',
  releaseNote: 'something',
  app: {
    title: 'demo',
    bundle_id: 'com.khomkrit.demo',
    description: 'This is a cool app in the AppStore'
  }
}

จากตัวอย่างที่ยกมา เราก็ตัดปัญหาเรื่อง array ขนาดใหญ่ได้แล้ว แต่สิ่งที่ตามาก็คือ data จะ duplicate กันจำนวนมาก เพราะเราต้องแทรก app ไว้ในทุกๆ version และเมื่อไหร่ก็ตามหากเราต้องอัพเดทข้อมูลเกี่ยวกับ app เราก็จำเป็นต้องไล่ update ทุกๆ document ที่ embed app นี้ แต่ถ้าเราพิจารณาต่อไปแล้วพบว่า โดยปกติแล้ว app นั้นแทบจะไม่ถูกอัพเดท namebundleId, และ description เลย การออกแบบโครงสร้างข้อมูลแบบนี้ก็พอจะ make sense

Split

หากเราพิจารณาต่อไปอีกว่า ปกติแล้วเรามักจะดึงข้อมูลของ version ขึ้นมาพร้อมๆ กับ app หรือไม่? แล้วคำตอบที่ได้คือ ไม่ นั่นก็แปลว่าเราอาจต้องแก้ไขโครงสร้างข้อมูลกันใหม่ เพราะแมัข้อมูลจะสัมพันธ์กัน แต่แทบไม่ถูกใช้พร้อมกันเลย โดยการให้ version นั้นแยกจาก app แล้วให้ version เก็บ reference ไปยังแอป ซึ่งในที่นี้ก็คือ bundle_id ดังนี้

App Collection

{
  title: 'demo',
  bundle_id: 'com.khomkrit.demo',
  description: 'This is a cool app in the AppStore'
}

Version Collection

{
  version: 1,
  releaseDate: '1 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo'
},
{
  version: 2,
  releaseDate: '2 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo'
},
{
  version: 3,
  releaseDate: '3 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo'
}

กรณีนี้หากเราต้องการดึงข้อมูลเกี่ยวกับ version และ app ออกมาพร้อมๆ กัน เราสามารถใช้ $lookup เพื่อ join collection ได้ แต่โดยทั่วไปแล้ว เรามักหลีกเลี่ยงการใช้ $lookup โดยไม่จำเป็น ดังนั้นก่อนใช้วิธีนี้นี้ เราจึงจำเป็นต้องพิจารณาต่อไปว่าเราจำเป็นแค่ไหนที่เราจะเอา $lookup มาใช้

ถ้าหากพิจารณาดูแล้วพบว่าใน application ของเรา จำเป็นต้องแสดง version ไปพร้อมๆ กับ bundle_id และ app title อยู่เสมอ ส่วน app description นั้นจะถูกดึงมาแสดงก็ต่อเมื่อ client ของดู application detail เท่านั้น ไม่ได้ถูกดึงมาพร้อมกับ version หรือถ้าจะดูพร้อมกันก็ไม่บ่อยนัก ถ้าอย่างนี้ก็ไม่ยาก เราสามารถออกแบบโครงสร้างข้อมูลกึ่ง flip กึ่ง split ได้โดยใช้ Extended Reference Pattern

Mixed

Extended Reference Pattern นั้นก็คือการรวมทั้ง 2 การออกแบบที่ยกมาไว้ด้วยกัน คือ เราจะ duplicate ข้อมูลบางส่วน และจะยังแยกกันเก็บคนละ collection อยู่ โดยข้อมูลที่เราจะเลือก duplicate นั้น ควรเป็นข้อมูลที่เรามักใช้งานร่วมกัน

เราออกแบบโดยใช้แนวคิดดังกล่าวได้ดังนี้

App Collection

{
  title: 'demo',
  bundle_id: 'com.khomkrit.demo',
  description: 'This is a cool app in the AppStore'
}

Version Collection

{
  version: 1,
  releaseDate: '1 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo',
  title: 'demo'
},
{
  version: 2,
  releaseDate: '2 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo',
  title: 'demo'
},
{
  version: 3,
  releaseDate: '3 Jan 2020',
  releaseNote: 'something',
  bundle_id: 'com.khomkrit.demo',
  title: 'demo'
}

ก่อนหน้านี้หากต้องดึง version ขึ้นมาแสดงให้มีข้อมูลเพียงพอต่อการนำไปใช้งาน เราจำเป็นต้อง $lookup ทุกครั้ง แต่พอเปลี่ยนแนวทางการออกแบบใหม่ เราก็ไม่จำเป็นต้องใช้ $lookup ทุกครั้งแล้ว เราจะใช้ก็ต่อเมื่อเราต้องการ version พร้อมกับข้อมูลของ app แบบครบครันเท่านั้น เช่นต้องการดึง description ขึ้นมาด้วย

อีกทั้งพอเราพิจารณาเพิ่มไปอีก เราก็พบว่าการ duplicate data ของ app ในการออกแบบนี้ เป็นการ duplicate ข้อมูลที่แทบจะไม่มีการอัพเดทเลย โดยเฉพาะ bundle_id อันนี้ปกติก็คือตายตัวอยู่แล้ว ส่วนชื่อแอปนั้นโดยปกติก็แทบไม่มีการเปลี่ยนชื่อกันอยู่แล้ว หากต้องการอัพเดท app description (ซึ่งมีโอกาสถูกอัพเดทเรื่อยๆ แล้วแต่ฝ่ายการตลาด) ก็ไม่ต้องไล่อัพเดทหลาย document เพราะเราได้แยกออกไปอีก collection หนึ่งต่างหากแล้ว ดังนั้นออกแบบโครงสร้างข้อมูลแบบนี้ก็ถือว่าค่อนข้างเวิร์คเลยทีเดียว

สรุป

การเก็บข้อมูลที่มีความสัมพันธ์กันที่ที่ข้อมูลมักถูกใช้ด้วยกันบ่อยๆ ไว้ด้วยกันนั้นดี อย่างไรก็ตามการเก็บมันลงไปใน array ใหญ่ๆ ที่จำนวนสมาชิกใน array นั้นโตขึ้นเรื่อยๆ แบบไม่มีขอบเขตจำกัดนั้นอาจสร้างปัญหาให้เราได้ในอนาคต แต่ไม่ว่าจะมี pattern, best practice หรือแนวทางการออกแบบที่ดีเลิศอยู่แล้วอย่างไร เราก็ต้องเลือกใช้ให้เหมาะสมกับสถานการณ์ และลักษณะการใช้งานของเราไว้ก่อน

You may also like