การการออกแบบ schema ที่ไม่ดี มันส่งผลต่อประสิทธิภาพ ในการจัดโครงสร้าง schema ของ database ของเรา รวมไปถึงการสร้างความซับซ้อนที่ไม่จำเป็น มันมักก่อให้เกิดปัญหาด้านประสิทธิภาพได้
ดังนั้นการรับรู้ และ หลีกเลี่ยงรูปแบบการออกแบบ schema ที่ไม่ดี สามารถช่วยสร้าง application ที่มีประสิทธิภาพที่ดียิ่งขึ้นได้
ช่วงนี้ได้ทำงานกับระบบที่ใช้งาน MongoDB และ ได้แนะนำน้องๆ ในทีมไป บทความนี้เราจะมาพูดถึง anti-patterns ของ mongo schema กัน

Avoid Unbounded Arrays
การออกแบบ schema ที่มีการเก็บข้อมูลเป็น arrays ลงไปด้วย (embed data) หากเราไม่ได้จำกัด limit ของ arrays ตัวนั้นไว้ มันจะส่งผลต่อประสิทธิภาพ
ที่สำคัญ documents จะมี limit ของการเก็บข้อมูลอยู่ 16MB BSON document size limit
array ที่ไม่มีขอบเขตนั้น มันสามารถทำให้เกิดการใช้ resource ของ application สูงขึ้น และ ลดประสิทธิภาพของ index ลงได้
แทนที่จะใส่ชุดข้อมูลทั้งหมดไว้ ควรใช้รูปแบบการออกแบบที่เรียกว่า subsetting และ referencing เพื่อกำหนดขอบเขตของ array ซึ่งมันจะช่วยปรับปรุงประสิทธิภาพ และ รักษาขนาดเอกสารให้อยู่ในระดับที่จัดการได้
ตัวอย่างเช่น
ถ้าเรามั schema ที่ใช้ในการเก็บหนังสือ books
โดยที่เรานำเอาข้อมูล reviews
มาเก็บไว้ใน schema นั้นด้วย
{
title: "Harry Potter",
author: "J.K. Rowling",
publisher: "Scholastic",
reviews: [
{
user: "Alice",
review: "Great book!",
rating: 5
},
{
user: "Bob",
review: "Didn't like it!",
rating: 1
},
{
user: "Charlie",
review: "Not bad, but could be better.",
rating: 3
}
]
}
การเก็บข้อมูลแบบนี้มันมีผลต่อ performance และมีโอกาศติด limit ของ document ได้ เนื่องจาก reviews
สามารถเพิ่มได้ไม่จำกัด
ดังนั้น เราควรปรับไปใช้ subsetting
และ referencing
แทน
Subset Pattern
Subsetting เป็น การเลือกเฉพาะส่วนที่จำเป็นของข้อมูล มาทำงานด้วย แทนที่จะฝังชุดข้อมูลทั้งหมด (เช่น การฝังรีวิวทั้งหมดของหนังสือเล่มหนึ่ง) เราจะฝังเพียงส่วนย่อย (subset) หรือ ส่วนที่จำกัดจำนวนของข้อมูลนั้นไว้ใน documents หลัก โดยตรง (books collection
) ส่วนข้อมูลที่เหลือจะถูกจัดเก็บไว้ใน collection
แยกต่างหาก review collection
รูปแบบ Subsetting นี้
- เหมาะที่สุดสำหรับข้อมูลที่ไม่ได้อัปเดตบ่อย
- เมื่อเราต้องการ เข้าถึงข้อมูลอย่างรวดเร็ว
ประโยชน์ของการใช้ Subsetting
- กำจัด array ที่ไม่มีขอบเขต
- ควบคุมขนาดเอกสาร
- ลดการใช้หน่วยความจำและเวลาประมวลผล โดยเน้นเฉพาะข้อมูลที่เกี่ยวข้อง
- ช่วยให้เราสามารถ return ข้อมูลที่จำเป็นทั้งหมดได้ในการดำเนินการเดียว
- หลีกเลี่ยงการใช้ query หลายรายการ ในการเข้าถึงข้อมูล (เมื่อเทียบกับรูปแบบ
Referencing
ที่ต้องใช้$lookup
)
Book Collection
db.books.insertOne( [
{
title: "Harry Potter",
author: "J.K. Rowling",
publisher: "Scholastic",
reviews: [
{
reviewer: "Alice",
review: "Great book!",
rating: 5
},
{
reviewer: "Charlie",
review: "Didn't like it.",
rating: 1
},
{
reviewer: "Bob",
review: "Not bad, but could be better.",
rating: 3
}
],
}
] )
Review Collection
db.reviews.insertMany( [
{
reviewer: "Jason",
review: "Did not enjoy!",
rating: 1
},
{
reviewer: "Pam",
review: "Favorite book!",
rating: 5
},
{
reviewer: "Bob",
review: "Not bad, but could be better.",
rating: 3
}
] )
Reference Data
รูปแบบการออกแบบ Referencing
นี้ เหมาะที่สุดสำหรับการจัดการกับชุดข้อมูลที่มีขนาดใหญ่ หรือ มีการอัปเดตบ่อยๆ โดยไม่ทำให้ขนาด documents หลัก เพิ่มขึ้นจนเกินขีดจำกัด ซึ่งเป็นข้อจำกัดหนึ่งของ array ที่ไม่มีขอบเขตที่อาจทำให้ documents มีขนาดเกินขีดจำกัด (BSON 16MB)
ประโยชน์และข้อควรพิจารณาของการใช้ Referencing
- แก้ปัญหาอาเรย์ที่ไม่มีขอบเขต ได้
- ช่วย ควบคุมขนาดเอกสาร หลักไม่ให้ใหญ่เกินไป โดยเฉพาะเมื่อข้อมูลที่เกี่ยวข้องมีจำนวนมาก
- เหมาะกับข้อมูลที่ มีการอัปเดตบ่อยๆ เนื่องจากลดปัญหาการอัปเดตข้อมูลซ้ำซ้อนที่อาจเกิดขึ้นในรูปแบบ Subsetting
- อย่างไรก็ตาม การใช้ Referencing จะ เพิ่มความหน่วง (latency) ในการเข้าถึงข้อมูล เนื่องจากคุณจำเป็นต้องทำการ query ไปยัง collection ที่เก็บข้อมูลที่ถูกอ้างอิงไว้ เพื่อดึงข้อมูลฉบับเต็มมาแสดง
- การดึงข้อมูลที่ใช้ Referencing มักจะต้องใช้การดำเนินการรวมข้อมูล เช่น
$lookup
เพื่อเชื่อม (join) ข้อมูลจาก collection หลัก กับ collection ที่ถูกอ้างอิงถึง
ตัวอย่าง
ถ้าหากเราเก็บข้อมูลแยก collection กัน เราต้องใช้ $lookup
เพื่อทำการ join ข้อมูลเข้าหากัน
db.books.insertMany( [
{
title: "Harry Potter",
author: "J.K. Rowling",
publisher: "Scholastic",
reviews: ["review1", "review2", "review3"]
},
{
title: "Pride and Prejudice",
author: "Jane Austen",
publisher: "Penguin",
reviews: ["review4", "review5"]
}
] )
Review Collection
db.reviews.insertMany( [
{
review_id: "review1",
reviewer: "Jason",
review: "Did not enjoy!",
rating: 1
},
{
review_id: "review2",
reviewer: "Pam",
review: "Favorite book!",
rating: 5
},
{
review_id: "review3",
reviewer: "Bob",
review: "Not bad, but could be better.",
rating: 3
},
{
review_id: "review4",
reviewer: "Tina",
review: "Amazing!",
rating: 5
},
{
review_id: "review5",
reviewer: "Jacob",
review: "A little overrated",
rating: 4,
}
] )
ใช้ $lookup สำหรับการ Join Array Field
db.books.aggregate( [
{
$lookup: {
from: "reviews",
localField: "reviews",
foreignField: "review_id",
as: "reviewDetails"
}
}
] )
เราจะได้ข้อมูลนี้
[
{
_id: ObjectId('665de81eeda086b5e22dbcc9'),
title: 'Harry Potter',
author: 'J.K. Rowling',
publisher: 'Scholastic',
reviews: [ 'review1', 'review2', 'review3' ],
reviewDetails: [
{
_id: ObjectId('665de82beda086b5e22dbccb'),
review_id: 'review1',
reviewer: 'Jason',
review: 'Did not enjoy!',
rating: 1
},
{
_id: ObjectId('665de82beda086b5e22dbccc'),
review_id: 'review2',
reviewer: 'Pam',
review: 'Favorite book!',
rating: 5
},
{
_id: ObjectId('665de82beda086b5e22dbccd'),
review_id: 'review3',
reviewer: 'Bob',
review: 'Not bad, but could be better.',
rating: 3
} ]
},
{
_id: ObjectId('665de81eeda086b5e22dbcca'),
title: 'Pride and Prejudice',
author: 'Jane Austen',
publisher: 'Penguin',
reviews: [ 'review4', 'review5' ],
reviewDetails: [
{
_id: ObjectId('665de82beda086b5e22dbcce'),
review_id: 'review4',
reviewer: 'Tina',
review: 'Amazing!',
rating: 5
},
{
_id: ObjectId('665de82beda086b5e22dbccf'),
review_id: 'review5',
reviewer: 'Jacob',
review: 'A little overrated',
rating: 4
} ]
}
]
Reduce the Number of Collections
การสร้าง collection มากเกินไป เป็นรูปแบบของการออกแบบที่ควรหลีกเลี่ยง เนื่องจากแต่ละ collection มันจะสร้าง _id index ขึ้นมา ซึ่งมันใช้พื้นที่ในการจัดเก็บ และ resource ทำให้ performance ลดลง
ตัวอย่าง เช่น การจัดเก็บข้อมูลอุณหภูมิรายวันที่ แต่ละวันอยู่ในคอลเลกชันแยกกัน
// Temperatures for May 10, 2024
{
_id: 1,
timestamp: "2024-05-10T010:00:00Z",
temperature: 60
},
{
_id: 2
timestamp: "2024-05-10T011:00:00Z",
temperature: 61
},
{
_id: 3
timestamp: "2024-05-10T012:00:00Z",
temperature: 64
}
...
// Temperatures for May 11, 2024
{
_id: 1,
timestamp: "2024-05-11T010:00:00Z",
temperature: 68
},
{
_id: 2
timestamp: "2024-05-11T011:00:00Z",
temperature: 72
},
{
_id: 3
timestamp: "2024-05-11T012:00:00Z",
temperature: 72
}
...
ซึ่งเวลาที่เราจะดึงข้อมูลทั้งหมดออกมา จำเป็นต้องใช้ $lookup
ในการสอบถามข้าม collections ซึ่งซับซ้อน และ ใช้ทรัพยากรสูง
การแก้ปัญหา คือ การ รวมข้อมูลที่เกี่ยวข้อง ไว้ใน collection เดียว โดยใช้ embedded documents ช่วยลดจำนวน index และ เพิ่ม performance
ดังนั้นเราสามารถแก้ไขได้เป็นแบบนีั้
db.dailyTemperatures.insertMany( [
{
_id: ISODate("2024-05-10T00:00:00Z"),
readings: [
{
timestamp: "2024-05-10T10:00:00Z",
temperature: 60
},
{
timestamp: "2024-05-10T11:00:00Z",
temperature: 61
},
{
timestamp: "2024-05-10T12:00:00Z",
temperature: 64
}
]
},
{
_id: ISODate("2024-05-11T00:00:00Z"),
readings: [
{
timestamp: "2024-05-11T10:00:00Z",
temperature: 68
},
{
timestamp: "2024-05-11T11:00:00Z",
temperature: 72
},
{
timestamp: "2024-05-11T12:00:00Z",
temperature: 72
}
]
}
] )
วิธีนี้ทำให้การอัปเดต schema ต้องการทรัพยากรน้อยกว่าตัวต้นฉบับ แทนที่จะต้องมี index แยกสำหรับแต่ละวัน _id index เราสามารถใช้ _id เป็นวันที่เริ่มต้นไปเลย มันช่วยให้เราสามารถค้นหาตามวันที่ได้
Remove Unnecessary Indexes
การสร้าง index สำหรับทุกๆ query อาจทำให้เกิด index ที่ไม่จำเป็นได้ ซึ่งมันจะส่งผลให้ประสิทธิภาพโดยรวมของฐานข้อมูลลดลง
index ที่ไม่จำเป็นอาจเกิดจากสิ่งที่ไม่ค่อยได้ใช้งาน หรือ ทำงานซ้ำซ้อน กับ index แบบ compound ที่ครอบคลุมข้อมูลเดียวกัน หรือ บางครั้งก็ไม่มีการใช้งานเลย
เพื่อให้ฐานข้อมูลทำงานได้อย่างมีประสิทธิภาพ เราควรลดจำนวน index ที่มีอยู่ โดยการระบุ และ ลบ index ที่ไม่จำเป็นออก
ตัวอย่าง
ถ้าเราพิจารณา collection ที่ชื่อ courses
ซึ่งเก็บข้อมูลเกี่ยวกับหลักสูตรของแต่ละวิชา ตัวอย่างเอกสารใน collection จะมีหน้าตาประมาณนี้
// Biology course document
db.courses.insertOne(
{
_id: 1,
course_name: "Biology 101",
professor: "Tate",
semester: "Fall",
days: "Monday, Friday",
time: "12:00",
building: "Olson"
}
)
ในตัวอย่างนี้ collection courses
จะถูกสร้าง index สำหรับทุกๆ field ดังนี้
- Field
_id
จะมี index เป็นค่าเริ่มต้น - สร้าง index สำหรับ field:
{ course_name: 1 }
{ professor: 1 }
{ semester: 1 }
{ building: 1 }
{ days: 1 }
{ time: 1 }
- และสร้าง compound index
{ day: 1, time: 1 }
การสร้าง index สำหรับ field ทุก field ใน collection อาจทำให้ collection มีขนาดใหญ่เกินจำเป็นและส่งผลกระทบต่อประสิทธิภาพการเขียนข้อมูล (write performance)
ขั้นตอนในการระบุและลบ Index ที่ไม่จำเป็น
1.ประเมินการใช้งานของ Index
เพื่อหาว่า index ใดที่ถูกใช้งานน้อย หรือ ไม่ได้ใช้งานเลย เราสามารถใช้ aggregation stage $indexStats
ได้โดยการเรียกใช้งาน
db.courses.aggregate([
{ $indexStats: {} }
])
คำสั่งนี้จะคืนค่าข้อมูลสถิติของ index แต่ละตัว ซึ่งรวมไปถึงชื่อ index (name
), key, host, และสถิติการเข้าถึง (accesses
)
[
{
name: "building_1",
key: { "building": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('0')", "since": "ISODate('2024-06-24T17:35:00.000Z')" },
spec: { "v": 2, "key": { "building": 1 }, "name": "building_1" }
},
{
name: "day_1",
key: { "day": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('1')", "since": "ISODate('2024-06-24T17:35:30.000Z')" },
spec: { "v": 2, "key": { "day": 1 }, "name": "day_1" }
},
{
name: "time_1",
key: { "time": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('1')", "since": "ISODate('2024-06-24T17:36:00.000Z')" },
spec: { "v": 2, "key": { "time": 1 }, "name": "time_1" }
},
{
name: "day_1_time_1",
key: { "day": 1, "time": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('110')", "since": "ISODate('2024-06-24T17:31:21.800Z')" },
spec: { "v": 2, "key": { "day": 1, "time": 1 }, "name": "day_1_time_1" }
},
{
name: "_id_",
key: { "_id": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('150')", "since": "ISODate('2024-06-24T15:31:49.463Z')" },
spec: { "v": 2, "key": { "_id": 1 }, "name": "_id_" }
},
{
name: "course_name_1",
key: { "course_name": 1 },
host: "M-C02FJ3BDML85:27017",
accesses: { "ops": "Long('120')", "since": "ISODate('2024-06-24T17:29:26.344Z')" },
spec: { "v": 2, "key": { "course_name": 1 }, "name": "course_name_1" }
},
...
]
โดยตัวอย่าง ผลลัพธ์ที่ได้อาจแสดงว่า index "building_1"
มีการเข้าถึงเป็น 0 (หมายความว่าไม่มี query ใดที่ใช้ index นี้)
2.ตัดสินใจลบ Index
- หาก index
"building_1"
มีจำนวนการเข้าถึงเป็น 0 นั่นหมายความว่า มันถูกใช้งานน้อย หรือ ไม่ถูกใช้งานเลย เราจึงสามารถลบมันออกได้ - ในกรณีที่มี index สำหรับ
{ days: 1 }
และ{ time: 1 }
ซึ่งถูกครอบคลุมโดย compound index{ day: 1, time: 1 }
(ซึ่งสามารถรองรับ query ที่เกี่ยวกับเวลาหรือวันที่ได้) ก็สามารถลบ index สองตัวนี้ออกได้เช่นกัน
การลบ index ที่ไม่จำเป็นจะช่วยให้ฐานข้อมูลสามารถ query ได้อย่างมีประสิทธิภาพมากขึ้น และ ช่วยลดการใช้ทรัพยากรโดยรวมอีกด้วย
3.ซ่อน index (Hire Index)
หลังจากเราระบุ index ที่ไม่จำเป็นได้แล้ว ก่อนที่เราจะลบ index ออกนอก เราสามารถใช้เมธอด db.collection.hideIndex()
เพื่อซ่อน index และ ประเมินผลกระทบของ index เหล่านี้ต่อฐานข้อมูลก่อนที่เราจะลบ index ออกไป
db.courses.hideIndex( "days_1" )
db.courses.hideIndex( "time_1" )
db.courses.hideIndex( "building_1" )
4.ลบ Index
หากเราพบแล้วว่ามี index ที่ไม่จำเป็น และ มีผลกระทบเชิงลบต่อประสิทธิภาพ ให้ลบ index นั้นออกไป โดยใช้เมธอด db.collection.dropIndexes()
db.courses.dropIndexes( [ "days_1", "time_1", "building_1" ] )
ในตัวอย่าง มีเพียง index เหล่านี้เท่านั้นที่ยังคงอยู่ เนื่องจาก index เหล่านี้ถูกใช้บ่อยที่สุด และ ช่วยเพิ่มประสิทธิภาพการค้นหาได้ นั่นก็คือ
_id
ที่เป็น default indexed{ course_name: 1 }
{ professor: 1 }
{ semester: 1 }
{ day: 1, time: 1 }
Bloated Documents
อีกหนึ่งปัญหาที่อาจจะเจอกันบ่อยเลย คือ การเก็บข้อมูลที่มีความสัมพันธ์กันไว้ในเอกสารเดียวกัน แม้ว่าข้อมูลเหล่านั้น จะไม่ได้ถูกเรียกใช้งานร่วมกันในบางครั้ง
การทำเช่นนี้ ทำให้เอกสารมีขนาดใหญ่เกินความจำเป็น ส่งผลให้เกิดการใช้งาน RAM และ Bandwidth สูงขึ้น โดยเฉพาะอย่างยิ่ง เมื่อชุดของข้อมูลที่มีการเข้าถึงบ่อย (working set) ไม่สามารถยัดลงในหน่วยความจำ RAM ได้
อย่างที่เรารู้กันว่า หากชุดข้อมูลนี้อยู่ใน RAM ของ MongoDB แล้ว จะสามารถ search ข้อมูลได้เร็วขึ้น เพราะจะไปหาจากหน่วยความจำเลย แต่หากเอกสารมีขนาดใหญ่เกินไป ตัว MognoDB ต้องไปเข้าถึงข้อมูลจาก disk แทน ซึ่งมันทำให้ประสิทธิภาพลดลง
วิธีแก้ไขปัญหา
เพื่อป้องกันไม่ให้เอกสารมีขนาดใหญ่เกินไป เราควรปรับโครงสร้าง schema โดยแบ่งข้อมูลออกเป็นเอกสารที่มีขนาดเล็กลง
พร้อมทั้งใช้การอ้างอิงเอกสาร (document references) เพื่อแยก fields ที่ไม่ถูกเรียกใช้งานพร้อมกันออกจากกัน วิธีนี้จะช่วยลดขนาดของชุดข้อมูลการทำงาน (working set) และเพิ่มประสิทธิภาพในการ search ได้
ตัวอย่าง
ลองดู schema ที่ใช้เก็บข้อมูลหนังสือสำหรับหน้าแรกของเว็บไซต์ร้านหนังสือ โดยที่หน้าแรกจะแสดงเพียงชื่อหนังสือ ผู้เขียน และ ภาพหน้าปก
หมายเหตุ: ผู้ใช้จำเป็นต้องคลิกที่หนังสือ เพื่อดูรายละเอียดเพิ่มเติม
ตัวอย่าง ของ document
{
title: "Tale of Two Cities",
author: "Charles Dickens",
genre: "Historical Fiction",
cover_image: "<url>",
year: 1859,
pages: 448,
price: 15.99,
description: "A historical novel set during the French Revolution."
}
ใน schema ปัจจุบันหากเราต้องการแสดงข้อมูลบนหน้าแรกของเว็บไซต์ ต้องสืบค้นข้อมูลทั้งหมดที่มีในเอกสารนั้น ซึ่งอาจทำให้เอกสารมีขนาดใหญ่เกินความจำเป็น
เพื่อให้ขนาดของเอกสารเล็กลง และ ช่วยให้การ query ทำงานได้รวดเร็วขึ้น เราสามารถแยกเอกสารที่มีขนาดใหญ่เหล่านี้ออกเป็น 2 collection ได้
การแบ่งข้อมูลออกเป็น Collection ย่อย
ในตัวอย่างนี้ ข้อมูลหนังสือถูกแยกออกเป็น 2 collection:
- mainBookInfo: ประกอบด้วย ข้อมูลที่จะแสดงบนหน้าแรกของเว็บไซต์ (เช่น ชื่อหนังสือ, ผู้เขียน, ประเภท และภาพหน้าปก)
- additionalBookDetails: ประกอบด้วย รายละเอียดเพิ่มเติมที่จะแสดง เมื่อมีการคลิกเลือกหนังสือ
Collection mainBookInfo:
db.mainBookInfo.insertOne({
_id: 1234,
title: "Tale of Two Cities",
author: "Charles Dickens",
genre: "Historical Fiction",
cover_image: "<url>"
});
Collection additionalBookDetails:
db.additionalBookDetails.insertOne({
title: "Tale of Two Cities",
bookId: 1234,
year: 1859,
pages: 448,
price: 15.99,
description: "A historical novel set during the French Revolution."
});
ในตัวอย่างนี้
- ทั้ง 2 collection เชื่อมโยงกันโดยใช้ field
_id
ใน collection mainBookInfo และ fieldbookId
ใน collection additionalBookDetails - หน้าแรกของเว็บไซต์จะใช้ข้อมูลจาก collection mainBookInfo เท่านั้น
- เมื่อผู้ใช้เลือกหนังสือ เพื่อดูรายละเอียดเพิ่มเติม ระบบจะทำการ query ไปยัง additionalBookDetails โดยเชื่อมโยงผ่าน
_id
กับbookId
การแบ่งข้อมูลออกเป็น 2 ส่วนแบบนี้ จะช่วยให้เอกสารไม่เติบโตจนเกินความจำเป็น และ ไม่เกินขีดจำกัดของ RAM
รวม Collection ด้วย $lookup
หากจำเป็นต้องรวมข้อมูลจาก mainBookInfo และ additionalBookDetails เราสามารถใช้คำสั่ง $lookup
เพื่อ join ข้อมูลจาก 2 collection เข้าด้วยกันได้
db.mainBookInfo.aggregate( [
{
$lookup: {
from: "additionalBookDetails",
localField: "_id",
foreignField: "bookId",
as: "details"
}
},
{
$replaceRoot: {
newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$details", 0 ] }, "$$ROOT" ] }
}
},
{
$project: { details: 0 }
}
] )
เราก็จะได้ข้อมูลแบบนี้
[
{
_id: ObjectId('666b1235eda086b5e22dbcf1'),
title: 'Tale of Two Cities',
author: 'Charles Dickens',
genre: 'Historical Fiction',
cover_image: '<url>',
bookId: 1234,
year: 1859,
pages: 448,
price: 15.99,
description: 'A historical novel set during the French Revolution.'
}
]
Reduce $lookup Operations
ช่วงท้ายของหัวข้อก่อนหน้า ได้พูดถึงการรวมเอกสาร (collection) โดยใช้ $lookup
ซึ่งเป็นคำสั่งใน MongoDB ที่ถูกใช้เพื่อรวมข้อมูลจากหลายๆ collection เข้าด้วยกันในเอกสารเดียว
แม้ว่า $lookup
จะมีประโยชน์ในกรณีที่ต้องรวมข้อมูลในบางสถานการณ์ แต่การใช้งานบ่อยๆ ก็ไม่ใช้เรื่องที่ดี มันอาจทำให้ประสิทธิภาพของ query ช้าลง และ ใช้ทรัพยากรระบบมากกว่าการ query กับเพียง collection เดียว
หากเราพบว่ามีการใช้งาน $lookup
ซ้ำๆ ในระบบของเรา แนะนำให้ปรับโครงสร้าง schema ใหม่โดยจัดเก็บข้อมูลที่มีความเกี่ยวข้องกันไว้ใน collection เดียว วิธีนี้จะช่วยเพิ่มประสิทธิภาพในการ query และ ลดต้นทุนของการดำเนินการได้
ตัวอย่าง
ลองดู schema ที่มี 2 collection แยกกัน คือ products
และ orders
ซึ่งในแต่ละออร์เดอร์อาจมีรายการสินค้าได้หลายรายการ
สมมติว่าเราต้องการให้ข้อมูลรายละเอียดสินค้าในแต่ละออร์เดอร์แสดงผลได้อย่างรวดเร็ว ด้วย schema นี้ เราจะต้องใช้ $lookup
ทุกครั้งที่มีการเข้าถึงข้อมูลออร์เดอร์ เพื่อ join ข้อมูลจาก collection ที่แตกต่างกัน
// Collection products
db.products.insertMany([
{ _id: 1, name: "Laptop", price: 1000, manufacturer: "TechCorp", category: "Electronics", description: "Fastest computer on the market." },
{ _id: 2, name: "Headphones", price: 100, manufacturer: "Sound", category: "Accessories", description: "The latest sound technology." },
{ _id: 3, name: "Tablet", price: 200, manufacturer: "TechCorp", category: "Electronics", description: "The most compact tablet." }
]);
// Collection orders
db.orders.insertMany([
{ _id: 101, customer_name: "John Doe", timestamp: "2024-05-11T01:00:00Z", product_ids: [1, 2], total: 1200 },
{ _id: 102, customer_name: "Jane Smith", timestamp: "2024-05-11T01:00:00Z", product_ids: [2], total: 100 }
]);
ใน schema นี้ ทุกครั้งที่ต้องเข้าถึงข้อมูลออร์เดอร์ เราจะต้องใช้ $lookup
เพื่อดึงข้อมูลรายละเอียดสินค้าจาก collection products
ทำให้เกิดความซับซ้อนในการ query และ ส่งผลต่อประสิทธิภาพในการทำงานของระบบ
ใช้ Subset Schema Design Pattern
เพื่อแก้ปัญหาการเรียกใช้งาน $lookup
บ่อยๆ เราสามารถการออกแบบ schema ใหม่ ให้ข้อมูลที่ถูกเข้าถึงพร้อมกันเก็บอยู่ใน collection เดียวกัน
ตัวอย่าง เช่น เราอาจฝังข้อมูลรายละเอียดสินค้าบางส่วน (subset) ลงใน collection orders
ซึ่งจะช่วยให้สามารถ query ข้อมูลที่ต้องการได้จาก collection เดียว โดยที่รายละเอียดที่ไม่เกี่ยวข้องกับการแสดงผลในออร์เดอร์ก็ยังคงถูกจัดเก็บไว้ใน collection products
ตามเดิม
// Collection orders ที่ฝังข้อมูลสินค้าบางส่วนไว้ในตัว
db.orders.insertMany([
{
_id: 101,
customer_name: "John Doe",
timestamp: "2024-05-11T10:00:00Z",
products: [
{ product_id: 1, name: "Laptop", price: 1000 },
{ product_id: 2, name: "Headphones", price: 100 }
],
total: 1100
},
{
_id: 102,
customer_name: "Jane Smith",
timestamp: "2024-05-11T12:00:00Z",
products: [
{ product_id: 2, name: "Headphones", price: 100 }
],
total: 100
}
]);
// Collection products ยังคงเก็บข้อมูลรายละเอียดสินค้าฉบับเต็มไว้
db.products.insertMany([
{ _id: 1, name: "Laptop", price: 1000, manufacturer: "TechCorp", category: "Electronics", description: "Fastest computer on the market." },
{ _id: 2, name: "Headphones", price: 100, manufacturer: "Sound", category: "Accessories", description: "The latest sound technology." },
{ _id: 3, name: "Tablet", price: 200, manufacturer: "TechCorp", category: "Electronics", description: "The most compact tablet." }
]);
ด้วยวิธีนี้ เมื่อเราต้องการดึงข้อมูลออร์เดอร์ที่ประกอบด้วยรายละเอียดสินค้าที่จำเป็นสำหรับการแสดงผล
เราจะก็สามารถ query เพียง collection orders
ได้โดยตรง โดยไม่ต้องใช้ $lookup
ซึ่งจะลดความซับซ้อนของ query และเพิ่มประสิทธิภาพในการเข้าถึงข้อมูลได้อย่างมาก
ข้อควรระวัง
ส่วนสิ่งที่ต้องระวังคือ พยายามอย่าเอาข้อมูลที่ไม่จำเป็นมาเก็บไว้เยอะจนเกินไป เพราะมันจะทำให้ document มีขนาดใหญ่เกินไปได้
นี่ คือ ทั้งหมดในเอกสารของ MongoDB ที่ได้พูดถึง Anti-Pattern เอาไว้ เมื่อเราต้องออกแบบการเก็บข้อมูลบนฐานข้อมูล MongoDB ก็สามารถมาเปิดอ่านบทความนี้ได้