Document เทียบกับ relational
ถ้าคุณรู้จัก relational database อยู่แล้ว วิธีที่เร็วที่สุดในการคล่อง MongoDB คือการแปลคำที่คุณรู้ให้เป็นคำที่ MongoDB ใช้ แนวคิดส่วนใหญ่เรียงตรงกัน มีสองสามอันที่ต่างออกไปในแบบที่เป็นแก่นของโมเดล document บทเรียนนี้คือตารางแปลนั้น บวกกับการมองครั้งแรกไปที่คำถามที่โมดูล Data Modeling ตอบไว้อย่างครบถ้วน: เมื่อไรควร embed ข้อมูลที่เกี่ยวข้อง และเมื่อไรควรเก็บไว้ใน collection แยกต่างหาก
คำศัพท์ที่ map แล้ว
หัวข้อที่มีชื่อว่า “คำศัพท์ที่ map แล้ว”ฝั่งซ้ายคือโลก relational ฝั่งขวาคือของที่เทียบเคียงกันใน MongoDB:
- Database → Database อันนี้คำเดียวกันและแนวคิดเดียวกัน: ภาชนะที่มีชื่อสำหรับบรรจุ collection
- Table → Collection collection เก็บ document แบบเดียวกับที่ table เก็บ row แต่ไม่ได้บังคับให้ทุก document มี column เหมือนกันเป๊ะ
- Row → Document document คือหนึ่งระเบียน ต่างจาก row ตรงที่ซ้อน array และ sub-object ได้ ไม่ใช่แค่แถวของ cell แบน ๆ
- Column → Field field คือคู่ key-value ภายใน document field เป็นแบบต่อ document ดังนั้นสอง document ใน collection ไม่จำเป็นต้องมี field ชุดเดียวกัน
- JOIN → Embedding หรือ
$lookupความสัมพันธ์ถูกจัดการด้วยการ embed ข้อมูลที่เกี่ยวข้องไว้ภายใน document เดียว หรือลิงก์ document ไว้แล้วค่อยรวมตอนอ่านด้วย aggregation stage$lookup - Schema → Flexible schema (พร้อม validation ที่เป็น optional) รูปร่างของข้อมูลคุณยืดหยุ่นโดยค่าเริ่มต้น และคุณสามารถเพิ่มกฎ validation เมื่อต้องการให้ database บังคับโครงสร้าง
- Primary key →
_idทุก document มี_idที่ไม่ซ้ำกัน บทบาทเดียวกับที่ primary key เล่นให้กับ row
flowchart LR
subgraph SQL["Relational"]
direction TB
R1["Database"]
R2["Table"]
R3["Row"]
R4["Column"]
R5["JOIN"]
end
subgraph MDB["MongoDB"]
direction TB
M1["Database"]
M2["Collection"]
M3["Document"]
M4["Field"]
M5["Embedding or lookup"]
end
R1 --> M1
R2 --> M2
R3 --> M3
R4 --> M4
R5 --> M5 ความแตกต่างใหญ่: ความสัมพันธ์
หัวข้อที่มีชื่อว่า “ความแตกต่างใหญ่: ความสัมพันธ์”ใน relational database คุณเกือบจะ normalise เสมอ: สมาชิกอยู่ในตาราง members และการยืมของพวกเขาอยู่ในตาราง loans ที่ join กันด้วย key คุณประกอบภาพเต็มด้วย JOIN ทุกครั้งที่ query
MongoDB ให้ทางเลือกแก่คุณ คุณ embed ข้อมูลที่เกี่ยวข้องไว้ภายใน document พ่อแม่โดยตรง ดังนั้นการอ่านครั้งเดียวคืนทุกอย่าง หรือเก็บข้อมูลใน collection แยก แล้วลิงก์ด้วย id จากนั้นรวมกันตอน query ด้วย $lookup — join ตอนอ่านของ MongoDB fence นี้สร้างทั้งสองรูปแบบสำหรับสมาชิกคนเดียวกันและอ่านกลับ (output จาก mongo 8.3.11, mongosh 2.11.1, Mac เครื่องนี้):
const ada = ObjectId("65f0b3d4e4b0a1c2e4b0a1d1");
// Shape 1: embedded - one document holds the loansdb.embedded.insertOne({ _id: ada, name: "Ada", borrowed: [ { title: "Compilers", due: "2026-07-01" }, { title: "Algorithms", due: "2026-07-15" },] });
// Shape 2: referenced - two collections linked by member_iddb.members.insertOne({ _id: ada, name: "Ada" });db.loans.insertMany([ { member_id: ada, title: "Compilers", due: "2026-07-01" }, { member_id: ada, title: "Algorithms", due: "2026-07-15" },]);
console.log(db.embedded.findOne({ name: "Ada" })); // one readconst joined = db.members.aggregate([ { $lookup: { from: "loans", localField: "_id", foreignField: "member_id", as: "borrowed" } }, { $project: { "borrowed._id": 0, "borrowed.member_id": 0 } },]).toArray(); // one aggregation, two collectionsconsole.log(joined);console.log("embedded doc bytes:", bsonsize(db.embedded.findOne()));assert.strictEqual(joined[0].borrowed.length, 2);{ _id: ObjectId('65f0b3d4e4b0a1c2e4b0a1d1'), name: 'Ada', borrowed: [ { title: 'Compilers', due: '2026-07-01' }, { title: 'Algorithms', due: '2026-07-15' } ]}[ { _id: ObjectId('65f0b3d4e4b0a1c2e4b0a1d1'), name: 'Ada', borrowed: [ { title: 'Compilers', due: '2026-07-01' }, { title: 'Algorithms', due: '2026-07-15' } ] }]embedded doc bytes: 150การอ่านทั้งสองคืน ภาพเดียวกัน — ต่างกันที่งานเกิดขึ้นที่ไหน: รูปแบบ embed ทำ findOne ครั้งเดียว ส่วนรูปแบบอ้างอิงรัน $lookup ข้ามสอง collection
ไม่มี foreign key
หัวข้อที่มีชื่อว่า “ไม่มี foreign key”สัญชาตญาณของ relational คือ “database จะกันไม่ให้ฉันทิ้ง loan กำพร้า” MongoDB ไม่ทำ:
db.members.drop(); db.loans.drop(); // start clean: this fence reuses the names aboveconst ada = ObjectId("65f0b3d4e4b0a1c2e4b0a1d1");db.members.insertOne({ _id: ada, name: "Ada" });db.loans.insertMany([{ member_id: ada, title: "Compilers" }, { member_id: ada, title: "Algorithms" }]);
// 1. insert a loan for a member that never existed - acceptedconsole.log(db.loans.insertOne({ member_id: ObjectId("000000000000000000000001"), title: "Orphan" }).acknowledged);// 2. delete the member while loans still point at them - accepteddb.members.deleteOne({ _id: ada });// 3. $lookup does not error - it returns an empty arrayconsole.log(db.loans.aggregate([ { $lookup: { from: "members", localField: "member_id", foreignField: "_id", as: "member" } }, { $project: { _id: 0, title: 1, member: 1 } },]).toArray());console.log("loans still pointing at Ada:", db.loans.countDocuments({ member_id: ada }));assert.strictEqual(db.loans.countDocuments({ member_id: ada }), 2);true[ { title: 'Compilers', member: [] }, { title: 'Algorithms', member: [] }, { title: 'Orphan', member: [] }]loans still pointing at Ada: 2“การละเมิด” ทั้งสามสำเร็จอย่างเงียบ ๆ ความสมบูรณ์ระหว่าง collection เป็นหน้าที่ของ คุณ (ในโค้ดแอปพลิเคชัน หรือด้วยการ embed เพื่อให้ลูกอยู่รอดเกินพ่อแม่ไม่ได้)
เมื่อไรควร embed เทียบกับเก็บแยก
หัวข้อที่มีชื่อว่า “เมื่อไรควร embed เทียบกับเก็บแยก”การลงรายละเอียดเต็มคือโมดูล Data Modeling แต่สัญชาตญาณที่ต้องสร้างตอนนี้คือ:
- Embed เมื่อ ข้อมูลที่เกี่ยวข้องเป็นของพ่อแม่อันเดียว ถูกอ่านไปพร้อมกัน และไม่โตแบบไร้ขีดจำกัด หนังสือที่สมาชิกยืม, รายการสินค้าในออเดอร์, ความคิดเห็นล่าสุดของโพสต์ — เหล่านี้อ่านเป็น document เดียวในการเดินทางครั้งเดียว
- เก็บแยกเมื่อ ข้อมูลถูกแชร์ระหว่างพ่อแม่หลายตัว ถูกอ้างอิงอย่างอิสระ หรือเติบโตใหญ่ แคตตาล็อกหนังสือที่สมาชิกหลายคนยืมควรเป็น collection ของตัวเอง อ้างอิงด้วย id ไม่ใช่คัดลอกเข้าไปในทุกสมาชิก
คำถามนำทางคือ ฉันอ่านข้อมูลนี้อย่างไร? ถ้าคุณเกือบจะต้องการพ่อแม่และลูกพร้อมกันเสมอ การ embed ก็คุ้มค่า ถ้าลูกมีชีวิตเป็นของตัวเอง ก็แยกออกไปแล้วลิงก์
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- อย่า normalise โดยอัตโนมัติ เมื่อมาจาก SQL แรงกระตุ้นคือการแยกทุกอย่างออกเป็น collection ที่ต่างกัน ใน MongoDB ข้อมูลที่ถูกอ่านพร้อมกันมักจะควรอยู่ด้วยกัน ใน document เดียว
- collection ไม่ใช่ table เพราะไม่ได้บังคับชุด column ตายตัว สอง document สามารถพก field ต่างกันได้ และนั่นเป็นสิ่งที่ออกแบบให้อนุญาต
$lookupมีให้ใช้ แต่ไม่ใช่ join ฟรี ๆ การ embed โดยทั่วไปเร็วกว่าสำหรับข้อมูลที่คุณอ่านพร้อมกัน; เอื้อมไปหา$lookupเมื่อข้อมูลควรอยู่ใน collection แยกต่างหากจริง ๆ_idคือ primary key ของคุณแบบฟรี ๆ คุณได้ identifier ที่ไม่ซ้ำและถูก index บนทุก document โดยไม่ต้องประกาศ
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย AI ที่พอร์ต schema จาก SQL มักเขียน delete ราวกับมี ON DELETE CASCADE:
🐛 หา bug ในโค้ดนี้
// AI comment: "loans reference members; deleting the member removes their loans (like ON DELETE CASCADE)"await db.collection("members").deleteOne({ _id: memberId });// loans are cleaned up automatically by the foreign keyเฉลย
MongoDB ไม่มี foreign key และไม่มี cascade เรารันโค้ดนี้จริง: สมาชิกถูกลบ แต่ loan สองรายการยังอยู่ (loans still pointing at Ada: 2 ด้านบน) และทุก $lookup จาก loans ไป members ตอนนี้คืน member: [] วิธีแก้คือทำการล้างให้ชัดเจน — ลบลูกใน unit of work เดียวกัน (หรือ embed ให้ลูกไปพร้อมพ่อแม่):
db.members.drop(); db.loans.drop();const ada = ObjectId("65f0b3d4e4b0a1c2e4b0a1d1");db.members.insertOne({ _id: ada, name: "Ada" });db.loans.insertMany([{ member_id: ada, title: "Compilers" }, { member_id: ada, title: "Algorithms" }]);const loansGone = db.loans.deleteMany({ member_id: ada }).deletedCount; // children firstconst memberGone = db.members.deleteOne({ _id: ada }).deletedCount;console.log({ loansGone, memberGone, orphans: db.loans.countDocuments({ member_id: ada }) });assert.strictEqual(db.loans.countDocuments({ member_id: ada }), 0);{ loansGone: 2, memberGone: 1, orphans: 0 }การลบสองครั้งแยกกันไม่ atomic — ถ้า crash ระหว่างสองอันก็ยังมีกำพร้า บท transactions แสดงวิธีทำให้เป็นหน่วยเดียว
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”relational database บังคับความสัมพันธ์ระหว่างตาราง; MongoDB เก็บความสัมพันธ์ไว้ข้างในหรือข้าง ๆ document และปล่อยให้คุณบังคับเอง การ embed ทำให้ความสัมพันธ์เป็นกายภาพ (ไบต์ของลูกอยู่ในไบต์ของพ่อแม่ 150 ไบต์สำหรับ Ada และ loan สองรายการ) การอ้างอิงทำให้เป็นตรรกะ (ค่า member_id ที่ไม่มีอะไรตรวจ) เลือกตามวิธีอ่าน: อ่านด้วยกัน -> embed; อ่านอิสระ -> อ้างอิง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Denormalized document model (embed) | อ่าน document เดียวได้ทุกอย่าง ไม่ต้อง join — เร็วสำหรับ workload ที่อ่านพ่อแม่และลูกพร้อมกันเสมอ | ข้อมูลที่ซ้ำข้าม document ต้องอัปเดตหลายที่เมื่อเปลี่ยน และ document อาจโตเข้าใกล้ขีดจำกัด 16 MiB ถ้า array ไม่จำกัด |
Referenced model + $lookup | ข้อเท็จจริงแต่ละอย่างมีสำเนาเดียว ลูกอ่านแยกได้ | ไม่มี referential integrity (กำพร้าพิสูจน์ด้านบน); join ตอนอ่านทุก query ที่ต้องการทั้งสอง |
| Normalized relational model (foreign key) | ไม่มีข้อมูลซ้ำ แก้ที่เดียว และ foreign key ให้ referential integrity จาก database | เกือบทุก query ต้อง join หลายตาราง ซึ่งแพงขึ้นเมื่อจำนวนตารางที่เกี่ยวข้องเพิ่ม |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
$lookupคือ SQL JOIN ที่มีการรับประกันเหมือนกัน” — ไม่มี foreign-key integrity: การรันจริงด้านบน insert loan กำพร้า ลบสมาชิกที่ถูกอ้างอิง และ$lookupคืนmember: []โดยไม่มี error - “embed เร็วกว่าเสมอ ดังนั้น embed ทุกอย่าง” — array ลูกที่ไม่จำกัด (log กิจกรรมที่โตไม่หยุด) จะชนขีดจำกัด document 16 MiB ในที่สุดและทำให้พ่อแม่อ่าน/เขียนช้า เก็บแยกตั้งแต่แรก
- “การเรียงตาม
_idเหมือนการเรียงตามเวลาสร้างที่แม่นยำ” — ภายใน process เดียว id เพิ่มขึ้นตามลำดับการสร้าง (บท documents-and-bson) ข้าม process counter 3 ไบต์เริ่มจากค่าสุ่ม ดังนั้นสอง id ที่สร้างใน วินาทีเดียวกัน โดยต่าง process เรียงได้ทั้งสองทาง: ใน 40 คู่ของ id จากสอง process Node ที่แยกกัน 37 คู่ใช้ timestamp วินาทีเดียวกัน และใน 20 จาก 37 นั้น id ที่ตามมาเรียงมาก่อน เก็บ field timestamp ของตัวเองเมื่อลำดับสำคัญ - “collection คือ table” — ไม่บังคับชุด column ตายตัว สอง document พก field ต่างกันได้
- “มาจาก SQL ต้อง normalise โดยอัตโนมัติ” — ข้อมูลที่อ่านพร้อมกันมักควรอยู่ด้วยกันใน document เดียว
ตัวอย่างจากของจริง
คู่มือ data modeling ของ MongoDB เองวางการตัดสินใจเดียวกัน: embed สำหรับความสัมพันธ์แบบ “contains” และ “one-to-few” อ้างอิงเมื่อข้อมูลถูกแชร์หรือไม่จำกัด (Data modeling introduction, อ้างอิงเมื่อ 2026-09-30) โมดูล Data Modeling ของคอร์สนี้ผ่านแต่ละกรณีพร้อมการวัด
บทนี้ไม่ใช้ Python/Go/Rust; fence รันใน mongosh