การลบ document
การลบคือ operation ที่ไวยากรณ์น้อยที่สุดแต่เดิมพันสูงที่สุด ไม่มี operator ให้เรียนรู้ — คุณส่ง filter ให้ MongoDB แล้วมันลบ document ที่ตรงกัน ทักษะทั้งหมดอยู่ที่ filter: การลบแม่นยำเท่ากับเงื่อนไขที่คุณให้ และ filter ที่กว้างเกินไปจะลบข้อมูลที่ตั้งใจเก็บ ข่าวดีคือเมธอดสะท้อนสิ่งที่คุณรู้อยู่แล้ว: deleteOne ลบหนึ่งรายการที่ตรง deleteMany ลบทุกรายการที่ตรง
เรายังคงใช้ collection members แต่ละตัวอย่างแสดงว่า filter เลือกอะไรและ acknowledgement รายงานอะไร ทุก output จับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน)
ข้อมูลที่เราลบ
หัวข้อที่มีชื่อว่า “ข้อมูลที่เราลบ”แต่ละบทเรียน seed ข้อมูลของตัวเอง:
db.members.insertMany([ { name: "Edsger", joined: 2022, fines: 5 }, { name: "Linus", joined: 2023, fines: 2 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Grace", joined: 2024, fines: 0 }, { name: "Barbara", joined: 2023, fines: 1 },]);assert.strictEqual(db.members.countDocuments(), 5);await db.collection("members").insertMany([ { name: "Edsger", joined: 2022, fines: 5 }, { name: "Linus", joined: 2023, fines: 2 }, { name: "Margaret", joined: 2023, fines: 0 }, { name: "Grace", joined: 2024, fines: 0 }, { name: "Barbara", joined: 2023, fines: 1 },]);if ((await db.collection("members").countDocuments()) !== 5) throw new Error("expected 5");ลบ document เดียว
หัวข้อที่มีชื่อว่า “ลบ document เดียว”deleteOne ลบ document หนึ่งรายการ ที่ตรงกับ filter แล้วหยุด แม้จะมีรายการอื่นตรงด้วย รั้วนิรภัยของเมธอดเอกพจน์นี้คือเหตุผลที่คุณหยิบมันเมื่อตั้งใจลบระเบียนที่เจาะจงเพียงอันเดียว ที่นี่เราลบสมาชิกชื่อ Edsger:
const res = db.members.deleteOne({ name: "Edsger" });console.log(res);console.log(db.members.deleteOne({ name: "Nobody" })); // no matchconst many = db.members.deleteOne({ joined: 2023 }); // 3 members match...console.log(many.deletedCount, db.members.countDocuments({ joined: 2023 })); // ...only one goesassert.strictEqual(res.deletedCount, 1);assert.strictEqual(db.members.countDocuments({ joined: 2023 }), 2);const res = await db.collection("members").deleteOne({ name: "Edsger" });console.log(res.deletedCount);if (res.deletedCount !== 1) throw new Error("expected 1 deleted");res = db.members.delete_one({"name": "Edsger"})print(res.deleted_count)res, err := coll.DeleteOne(ctx, bson.M{"name": "Edsger"})if err != nil { return err}fmt.Println(res.DeletedCount)let res = coll.delete_one(doc! { "name": "Edsger" }).await?;println!("{}", res.deleted_count);acknowledgement รายงานว่ามี document ถูกลบกี่รายการ ถ้า filter ไม่ตรงอะไรเลย นั่นไม่ใช่ error — deletedCount เป็น 0 เฉย ๆ และเมื่อสมาชิกปี 2023 สามคนตรงกัน deleteOne ลบไปพอดีหนึ่ง:
{ acknowledged: true, deletedCount: 1 }{ acknowledged: true, deletedCount: 0 }1 2(บรรทัดสุดท้ายคือ many.deletedCount = 1 และสมาชิกปี 2023 ที่เหลือ = 2 ไม่ได้ระบุว่า document ไหนที่ตรงจะถูกลบ — อย่าพึ่งพา; ให้กรองด้วย _id เมื่อต้องเป็นระเบียนที่เจาะจง)
ใน Compass: เอาเมาส์ชี้ document ใน tab Documents แล้วคลิกไอคอนถังขยะ จากนั้นยืนยัน Compass ลบ document เดียวนั้น
ลบหลายรายการพร้อมกัน
หัวข้อที่มีชื่อว่า “ลบหลายรายการพร้อมกัน”deleteMany ลบ ทุก document ที่ตรงกับ filter ในหนึ่ง operation ที่นี่เรากวาดทุกคนที่เข้าร่วมก่อนปี 2024 — สังเกตว่า filter ทำงานความแม่นยำ:
const res = db.members.deleteMany({ joined: { $lt: 2024 } });console.log(res);assert.strictEqual(db.members.countDocuments(), 1); // only Grace (joined 2024) is leftconst res = await db .collection("members") .deleteMany({ joined: { $lt: 2024 } });console.log(res.deletedCount);if (res.deletedCount !== 3) throw new Error("expected 3 deleted");res = db.members.delete_many({"joined": {"$lt": 2024}})print(res.deleted_count)res, err := coll.DeleteMany(ctx, bson.M{"joined": bson.M{"$lt": 2024}})if err != nil { return err}fmt.Println(res.DeletedCount)let res = coll.delete_many(doc! { "joined": { "$lt": 2024 } }).await?;println!("{}", res.deleted_count);จำนวนสะท้อนทุก document ที่ filter กวาดไป (shell พิมพ์ { acknowledged: true, deletedCount: 2 } ที่นี่เพราะสมาชิกปี 2023 หนึ่งคนถูกลบไปแล้วในตัวอย่าง deleteOne ด้านบน; การรันใหม่ใน Node, Python, Go และ Rust รายงาน 3 สำหรับ seed ห้าคนลบ Edsger)
filter ว่าง
หัวข้อที่มีชื่อว่า “filter ว่าง”deleteMany({}) ตรงกับทุก document มันคือการเรียก CRUD ที่อันตรายที่สุด และเรื่องน่าประหลาดใจอยู่ที่ว่าแต่ละ client จัดการ filter ที่ หายไป อย่างไร เรารันทั้งหมดนี้บนข้อมูล scratch:
db.scratch.insertMany([{ name: "Ada" }, { name: "Grace" }]);db.scratch.createIndex({ name: 1 });
let missing;try { db.scratch.deleteMany(); } catch (e) { missing = e; } // no argument at allconsole.log(missing.name + ": " + missing.message);console.log(db.scratch.countDocuments()); // still 2
const res = db.scratch.deleteMany({}); // explicit empty filterconsole.log(res, db.scratch.countDocuments());console.log("collection still exists:", db.getCollectionNames().includes("scratch"));console.log("indexes still there:", db.scratch.getIndexes().map((i) => i.name));
db.scratch.drop();console.log("after drop, collection exists:", db.getCollectionNames().includes("scratch"));assert.strictEqual(db.scratch.countDocuments(), 0);MongoshInvalidInputError: [COMMON-10001] Missing required argument at position 0 (Collection.deleteMany)2{ acknowledged: true, deletedCount: 2 } 0collection still exists: trueindexes still there: [ '_id_', 'name_1' ]after drop, collection exists: falseดังนั้นใน mongosh deleteMany() ที่ไม่มี argument ถูกปฏิเสธ และ deleteMany({}) ล้าง collection แต่เก็บ collection และ index ไว้; drop() ลบ collection และ index ด้วย Node driver ไม่ให้อภัยเท่า:
const c = db.collection("scratch");await c.insertMany([{ a: 1 }, { a: 2 }]);const noFilter = await c.deleteMany(); // no argumentconsole.log("deleteMany():", noFilter, "left:", await c.countDocuments());
await c.insertMany([{ a: 1 }, { a: 2 }, { b: 3 }]);const undef = await c.deleteMany({ a: undefined }); // an undefined value inside the filterconsole.log("deleteMany({ a: undefined }):", undef.deletedCount, "left:", await c.countDocuments());if (noFilter.deletedCount !== 2) throw new Error("expected deleteMany() to delete everything");deleteMany(): { acknowledged: true, deletedCount: 2 } left: 0deleteMany({ a: undefined }): 1 left: 2ใน Node deleteMany() ที่ไม่มี argument ลบทั้งสอง document — driver ตั้ง filter เริ่มต้นเป็น {} — ขณะที่ mongosh โยน error และ { a: undefined } ถูก serialise เป็น { a: null } ซึ่งตรงกับ document ที่ a เป็น null หรือไม่มี: มันลบ { b: 3 } และปล่อยสอง document ที่มี a ทั้งสองอย่างคือวิธีที่ตัวแปรที่ไม่ได้ตั้งค่ากลายเป็นข้อมูลหาย
การล้างยังช้ากว่าการ drop บน 200 000 document (มีหนึ่ง index) บน Mac เครื่องนี้: deleteMany({}) ใช้ 1001 ms, drop() ใช้ 3 ms ถ้าคุณต้องการให้ collection หายจริงและสร้าง index ใหม่ได้ ให้ drop
ทางเลือก soft delete
หัวข้อที่มีชื่อว่า “ทางเลือก soft delete”บางครั้ง hard delete เป็นตัวเลือกที่ผิด: คุณอาจต้องการ audit trail ความสามารถในการย้อนกลับ หรือรักษารายงานย้อนหลังให้ถูกต้อง รูปแบบที่พบบ่อยคือ soft delete — แทนที่จะลบ document คุณทำเครื่องหมายด้วย flag (และมักมี timestamp) ผ่านการอัปเดต แล้วกันเอกสารที่มี flag ออกจากการอ่านปกติ
db.softies.insertMany([{ name: "Ada" }, { name: "Linus" }, { name: "Grace" }]);db.softies.updateOne( { name: "Linus" }, { $set: { deleted: true, deletedAt: new Date() } });console.log("no filter :", db.softies.find({}, { _id: 0, name: 1 }).toArray());console.log("deleted $ne true:", db.softies.find({ deleted: { $ne: true } }, { _id: 0, name: 1 }).toArray());console.log("deleted: false :", db.softies.find({ deleted: false }, { _id: 0, name: 1 }).toArray());assert.strictEqual(db.softies.countDocuments({ deleted: { $ne: true } }), 2);assert.strictEqual(db.softies.countDocuments({ deleted: false }), 0);await db.collection("members").insertOne({ name: "Linus", joined: 2023, fines: 2 });await db.collection("members").updateOne( { name: "Linus" }, { $set: { deleted: true, deletedAt: new Date() } });const linus = await db.collection("members").findOne({ name: "Linus" }, { projection: { _id: 0 } });console.log(linus);if (linus?.deleted !== true) throw new Error("expected the flag");from datetime import datetime, timezone
db.members.update_one( {"name": "Linus"}, {"$set": {"deleted": True, "deletedAt": datetime.now(timezone.utc)}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Linus"}, bson.M{"$set": bson.M{"deleted": true, "deletedAt": time.Now()}},)if err != nil { return err}use mongodb::bson::DateTime;
coll.update_one( doc! { "name": "Linus" }, doc! { "$set": { "deleted": true, "deletedAt": DateTime::now() } },).await?;document ยังอยู่ แค่ถูกติด flag; คุณกู้คืนภายหลังได้ด้วยการ unset flag ข้อควรระวังอยู่ที่ filter ตอน อ่าน — การอ่านสามแบบด้านบนคืน:
no filter : [ { name: 'Ada' }, { name: 'Linus' }, { name: 'Grace' } ]deleted $ne true: [ { name: 'Ada' }, { name: 'Grace' } ]deleted: false : []{ deleted: { $ne: true } } คือการกันออกที่ถูกต้อง: document ที่ ไม่เคยมี field ก็ตรงด้วย { deleted: false } คืน ว่าง เพราะไม่มีใครมี deleted: false — มันตรงเฉพาะ document ที่มี field นั้นด้วยค่านั้น (ใน Node document ที่ติด flag อ่านได้ { name: 'Linus', joined: 2023, fines: 2, deleted: true, deletedAt: 2026-09-30T06:19:12.301Z })
soft delete ตอนนี้ hard delete ทีหลัง: TTL index
หัวข้อที่มีชื่อว่า “soft delete ตอนนี้ hard delete ทีหลัง: TTL index”soft delete ทำให้ collection โตไม่หยุดถ้าไม่มีอะไรมาล้าง TTL index บน timestamp เปลี่ยน “กู้คืนได้ระยะหนึ่ง” ให้เป็นนโยบาย: MongoDB ลบ document หลังหน้าต่างเวลานั้น ที่นี่ใช้หน้าต่าง 1 วินาทีเพื่อให้เราดูได้ (นโยบายจริงใช้เป็นวัน):
// app/delete-soft-ttl.mjs @skip-verify waits for the TTL monitor, which wakes only every 60 sconst c = db.collection("members");await c.createIndex({ deletedAt: 1 }, { expireAfterSeconds: 1 });await c.insertMany([{ name: "Ada" }, { name: "Linus" }]);await c.updateOne({ name: "Linus" }, { $set: { deleted: true, deletedAt: new Date() } });console.log("before sweep:", (await c.find({}, { projection: { _id: 0, name: 1 } }).toArray()).map((d) => d.name));// ...poll until the background TTL thread runs...console.log("remaining:", (await c.find().toArray()).map((d) => d.name));before sweep: [ 'Ada', 'Linus' ]Linus gone after 18 s; remaining: [ 'Ada' ]document หายไป 18 วินาทีหลังติด flag ในการรันนี้ — TTL monitor ทำงานเป็นรอบตายตัว 60 วินาที ดังนั้นความล่าช้าอยู่ระหว่างเกือบศูนย์ถึงหนึ่งนาทีเหนือหน้าต่างเวลา document ที่ ไม่มี deletedAt (Ada) ไม่ถูกแตะ TTL index ทำงานกับค่า Date จริงเท่านั้น (โมดูล indexes พิสูจน์ว่า string วันที่ถูกเมิน)
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”deleteMany({})ล้างทั้ง collection mongosh ปฏิเสธdeleteMany()ที่ไม่มี argument แต่ Node driver ลบทุกอย่าง เมื่อเรียกแบบนั้น (พิสูจน์ด้านบน) อย่าสร้าง filter ของ delete จากตัวแปรที่อาจว่างหรือundefined- การลบเป็นถาวร ต่างจากการอัปเดต ไม่มีสำเนา “ก่อน” ทิ้งไว้เว้นแต่คุณจัดเตรียม เมื่อสงสัยให้รัน filter เดียวกันผ่าน
find/countDocumentsก่อนเพื่อดูว่าจะลบอะไรกันแน่ deleteOneคือรั้วนิรภัยสำหรับ “ลบระเบียนเจาะจงอันเดียว” ต่อให้ filter หละหลวมกว่าที่คิดก็เอาได้แค่หนึ่ง document — แต่ อันไหน ไม่ได้ระบุ จึงกรองด้วย_idเมื่อสำคัญ- การล้างด้วย
deleteMany({})เก็บ collection และ index ไว้และช้ากว่าdrop()มาก (1001 ms เทียบ 3 ms สำหรับ 200 000 document) - soft delete แลกความเรียบง่ายกับความปลอดภัย: ทุก query ปกติต้องจำ
deleted: { $ne: true }(ไม่ใช่deleted: false) แต่คุณได้ undo และ audit trail
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย AI ที่เขียน endpoint ผู้ดูแลมักส่ง input ของผู้ใช้ตรง ๆ เป็น filter:
🐛 หา bug ในโค้ดนี้
// AI comment: "delete the members matching the query string, e.g. DELETE /members?joined=2022"app.delete("/members", async (req, res) => {const result = await db.collection("members").deleteMany(req.query);res.json({ deleted: result.deletedCount });});เฉลย
req.query คือ { joined: "2022" } สำหรับ ?joined=2022 — แต่สำหรับ DELETE /members เฉย ๆ มันคือ {} และ deleteMany({}) ตรงกับ ทุกอย่าง เรารัน Node driver ด้วย filter ว่าง (และไม่มี argument): ทั้งสองลบทุก document ใน collection โดยไม่มี error ไม่มีการยืนยัน (แม้กรณีที่ตั้งใจก็ผิด: ค่าใน query string เป็น string ดังนั้น joined: "2022" ไม่มีทางตรงกับ field ที่เป็นตัวเลข) วิธีแก้คือสร้าง filter อย่างชัดเจน ตรวจสอบ และปฏิเสธอันที่ว่าง:
const c = db.collection("guard");await c.insertMany([{ name: "Ada", joined: 2022 }, { name: "Grace", joined: 2024 }]);
async function deleteMembers(query) { const joined = Number(query.joined); if (!Number.isInteger(joined)) throw new Error("joined is required and must be an integer"); return (await c.deleteMany({ joined })).deletedCount;}
let refused = null;try { await deleteMembers({}); } catch (e) { refused = e.message; }console.log({ refused, deleted: await deleteMembers({ joined: "2022" }), left: await c.countDocuments() });if (refused === null || (await c.countDocuments()) !== 1) throw new Error("guard failed");{ refused: 'joined is required and must be an integer', deleted: 1, left: 1}Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การลบคือ filter ที่ไม่มี undo: server ประเมิน predicate เดียวกับที่ find ใช้ แล้วลบสิ่งที่ตรง ดังนั้น workflow ที่ปลอดภัยที่สุดคือ predicate เดียวกันสามครั้ง: countDocuments(filter) เพื่อดูว่ากี่รายการ, find(filter) เพื่อดูว่าอันไหน แล้วจึง deleteMany(filter) deleteOne คือ find(filter).limit(1) ตามด้วยการลบ; การเลือกระหว่าง hard delete, soft delete และ TTL คือการเลือกว่าคุณต้องการให้ข้อมูล กู้คืนได้นานแค่ไหน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
Hard delete (deleteOne/deleteMany) | คืนพื้นที่ ไม่ทิ้งอะไรไว้ เข้าใจง่ายที่สุด | ถาวร — ไม่มี audit trail ไม่มี undo |
Soft delete (อัปเดต flag เช่น deleted: true) | กู้คืนได้ เก็บ audit trail และข้อมูลย้อนหลังครบ | ทุกการอ่านต้อง filter ด้วย $ne: true ไม่เช่นนั้น document ที่ “ลบแล้ว” จะโผล่กลับมา; collection โตต่อไป |
Soft delete + TTL index บน deletedAt | กู้คืนได้ในช่วงหนึ่ง แล้วถูกลบอัตโนมัติ | การล้างช้าได้ถึงหนึ่งนาทีเหนือหน้าต่างเวลา (รอบ TTL 60 วินาที); ใช้ได้กับค่า Date จริงเท่านั้น |
deleteMany({}) เพื่อล้าง | เก็บ collection และ index ไว้ | 1001 ms เทียบ 3 ms ของ drop() บน 200 000 document |
drop() | ทันที ลบ collection และ index | ต้องสร้าง index และ option ใหม่ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
deleteOneลบทุกรายการที่ตรง” — มันลบหนึ่ง (1 2ด้านบน: ตรงสาม ไปหนึ่ง) - “filter ว่างของการลบถูกปฏิเสธ” — ใน mongosh ใช่; ใน Node driver
deleteMany()และdeleteMany({})ลบทุกอย่าง - “ไม่มี error แปลว่าสำเร็จ” — field ที่พิมพ์ผิดให้
deletedCount: 0โดยไม่มี error ตรวจมันเสมอ - “
{ deleted: false }หา document ที่ยังไม่ถูกลบ” — ในการรันด้านบนไม่พบสักรายการ; ใช้{ deleted: { $ne: true } } - “
undefinedใน filter ถูกเมิน” — Node driver serialise เป็นnullซึ่งตรงกับ field ที่ไม่มีด้วย - “
deleteMany({})เหมือนการ drop” — มันเก็บ collection และ index และใช้ 1001 ms เทียบ 3 ms
ตัวอย่างจากของจริง
คู่มือ MongoDB ระบุกลไกที่เราสังเกต: “The background task that removes expired documents runs every 60 seconds” การลบจึงล่าช้ากว่าเวลาหมดอายุได้ และภายใต้โหลดข้อมูลที่หมดอายุอาจอยู่ “for some time beyond the 60 second period” (TTL indexes, เรียกอ่านเมื่อ 2026-09-30); field ที่ทำ index ต้องเป็นวันที่หรือ array ของวันที่ (Expire data)
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกัน (จำนวน 1 และ 3 แล้วอัปเดต soft delete) และไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส import ของแท็บ Rust ถูกแก้จาก use bson::DateTime (คอมไพล์ไม่ผ่านถ้าไม่เพิ่ม bson เป็น dependency ตรง) เป็น use mongodb::bson::DateTime