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

การลบ 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 ข้อมูลของตัวเอง:

mongosh/delete-seed.js
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);

deleteOne ลบ document หนึ่งรายการ ที่ตรงกับ filter แล้วหยุด แม้จะมีรายการอื่นตรงด้วย รั้วนิรภัยของเมธอดเอกพจน์นี้คือเหตุผลที่คุณหยิบมันเมื่อตั้งใจลบระเบียนที่เจาะจงเพียงอันเดียว ที่นี่เราลบสมาชิกชื่อ Edsger:

mongosh/delete-one.js
const res = db.members.deleteOne({ name: "Edsger" });
console.log(res);
console.log(db.members.deleteOne({ name: "Nobody" })); // no match
const many = db.members.deleteOne({ joined: 2023 }); // 3 members match...
console.log(many.deletedCount, db.members.countDocuments({ joined: 2023 })); // ...only one goes
assert.strictEqual(res.deletedCount, 1);
assert.strictEqual(db.members.countDocuments({ joined: 2023 }), 2);

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 ทำงานความแม่นยำ:

mongosh/delete-many.js
const res = db.members.deleteMany({ joined: { $lt: 2024 } });
console.log(res);
assert.strictEqual(db.members.countDocuments(), 1); // only Grace (joined 2024) is left

จำนวนสะท้อนทุก document ที่ filter กวาดไป (shell พิมพ์ { acknowledged: true, deletedCount: 2 } ที่นี่เพราะสมาชิกปี 2023 หนึ่งคนถูกลบไปแล้วในตัวอย่าง deleteOne ด้านบน; การรันใหม่ใน Node, Python, Go และ Rust รายงาน 3 สำหรับ seed ห้าคนลบ Edsger)

deleteMany({}) ตรงกับทุก document มันคือการเรียก CRUD ที่อันตรายที่สุด และเรื่องน่าประหลาดใจอยู่ที่ว่าแต่ละ client จัดการ filter ที่ หายไป อย่างไร เรารันทั้งหมดนี้บนข้อมูล scratch:

mongosh/delete-empty-filter.js
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 all
console.log(missing.name + ": " + missing.message);
console.log(db.scratch.countDocuments()); // still 2
const res = db.scratch.deleteMany({}); // explicit empty filter
console.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 } 0
collection still exists: true
indexes still there: [ '_id_', 'name_1' ]
after drop, collection exists: false

ดังนั้นใน mongosh deleteMany() ที่ไม่มี argument ถูกปฏิเสธ และ deleteMany({}) ล้าง collection แต่เก็บ collection และ index ไว้; drop() ลบ collection และ index ด้วย Node driver ไม่ให้อภัยเท่า:

app/delete-empty-filter.mjs
const c = db.collection("scratch");
await c.insertMany([{ a: 1 }, { a: 2 }]);
const noFilter = await c.deleteMany(); // no argument
console.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 filter
console.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: 0
deleteMany({ 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

บางครั้ง hard delete เป็นตัวเลือกที่ผิด: คุณอาจต้องการ audit trail ความสามารถในการย้อนกลับ หรือรักษารายงานย้อนหลังให้ถูกต้อง รูปแบบที่พบบ่อยคือ soft delete — แทนที่จะลบ document คุณทำเครื่องหมายด้วย flag (และมักมี timestamp) ผ่านการอัปเดต แล้วกันเอกสารที่มี flag ออกจากการอ่านปกติ

mongosh/delete-soft.js
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);

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 ทำให้ collection โตไม่หยุดถ้าไม่มีอะไรมาล้าง TTL index บน timestamp เปลี่ยน “กู้คืนได้ระยะหนึ่ง” ให้เป็นนโยบาย: MongoDB ลบ document หลังหน้าต่างเวลานั้น ที่นี่ใช้หน้าต่าง 1 วินาทีเพื่อให้เราดูได้ (นโยบายจริงใช้เป็นวัน):

// app/delete-soft-ttl.mjs @skip-verify waits for the TTL monitor, which wakes only every 60 s
const 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 ที่เขียน 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 อย่างชัดเจน ตรวจสอบ และปฏิเสธอันที่ว่าง:

app/delete-guard.mjs
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
}

การลบคือ filter ที่ไม่มี undo: server ประเมิน predicate เดียวกับที่ find ใช้ แล้วลบสิ่งที่ตรง ดังนั้น workflow ที่ปลอดภัยที่สุดคือ predicate เดียวกันสามครั้ง: countDocuments(filter) เพื่อดูว่ากี่รายการ, find(filter) เพื่อดูว่าอันไหน แล้วจึง deleteMany(filter) deleteOne คือ find(filter).limit(1) ตามด้วยการลบ; การเลือกระหว่าง hard delete, soft delete และ TTL คือการเลือกว่าคุณต้องการให้ข้อมูล กู้คืนได้นานแค่ไหน

ตัวเลือกBenefitCost
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 ใหม่
  • “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

deleteMany({}) ทำอะไรใน mongosh และ Node driver?
filter ของ deleteOne ไม่ตรง document ใดเลย จะรายงานอะไร?
แนวคิดหลักของ soft delete คืออะไร?
คุณ soft-delete Linus ด้วย { deleted: true } การอ่านแบบใดคืนสมาชิกที่ยังไม่ถูกลบ (Ada และ Grace ไม่เคยมี field นี้)?
ใน Node driver await collection.deleteMany() (ไม่มี argument) ทำอะไร?