การลบ document
การลบคือ operation ที่มี syntax น้อยที่สุดแต่เดิมพันสูงที่สุด ไม่มี operator ให้เรียนรู้ — คุณยื่น filter ให้ MongoDB แล้ว document ที่ match ก็หายไป ทักษะทั้งหมดจึงอยู่ที่ filter เพราะการลบจะแม่นยำได้เท่าที่เงื่อนไขคุณแม่นเท่านั้น และ filter ที่กว้างเกินไปจะลบข้อมูลที่คุณตั้งใจจะเก็บไว้ ข่าวดีคือ method เหล่านี้สะท้อนทุกสิ่งที่คุณรู้อยู่แล้ว นั่นคือ deleteOne ลบรายการแรกที่ตรงกัน ส่วน deleteMany ลบทุกรายการที่ตรงกัน
เราดำเนินต่อด้วย collection members ทุกตัวอย่างแสดงว่า filter เลือกอะไรและ acknowledgement รายงานอะไร
การลบ document เดียว
หัวข้อที่มีชื่อว่า “การลบ document เดียว”deleteOne ลบ document แรก ที่ตรงกับ filter แล้วหยุด แม้ว่าจะมีรายการอื่นตรงกันด้วยก็ตาม ราวกันตกของ method แบบลบทีละอันนี่แหละคือเหตุผลที่ควรหยิบมาใช้เวลาตั้งใจลบระเบียนเดียวแบบเจาะจง ในที่นี้เราลบสมาชิกชื่อ Edsger:
db.members.deleteOne({ name: "Edsger" })const res = await db.collection("members").deleteOne({ name: "Edsger" });console.log(res.deletedCount);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 ถูกลบไปกี่รายการ:
{ "acknowledged": true, "deletedCount": 1 }ถ้า filter ไม่ตรงกับอะไรเลย นั่นไม่ใช่ error — deletedCount ก็เพียงแค่เป็น 0:
{ "acknowledged": true, "deletedCount": 0 }ใน Compass: เอาเมาส์ไปวางบน document ในแท็บ Documents แล้วคลิกไอคอนถังขยะ จากนั้นยืนยัน Compass จะลบ document รายการเดียวนั้น
การลบหลายรายการในครั้งเดียว
หัวข้อที่มีชื่อว่า “การลบหลายรายการในครั้งเดียว”deleteMany ลบ document ทุก รายการที่ตรงกับ filter ใน operation เดียว ในที่นี้เรากวาดล้างทุกคนที่เข้าร่วมก่อนปี 2024 — สังเกตว่า filter เป็นตัวทำงานเรื่องความแม่นยำ:
db.members.deleteMany({ joined: { $lt: 2024 } })const res = await db .collection("members") .deleteMany({ joined: { $lt: 2024 } });console.log(res.deletedCount);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 กวาดเข้ามา:
{ "acknowledged": true, "deletedCount": 2 }ทางเลือกแบบ soft-delete
หัวข้อที่มีชื่อว่า “ทางเลือกแบบ soft-delete”บางครั้งการลบแบบถาวร (hard delete) เป็นตัวเลือกที่ผิด คุณอาจต้องการเส้นทางการตรวจสอบ (audit trail) ความสามารถในการ undo หรือต้องการให้รายงานเชิงประวัติแม่นยำ รูปแบบที่พบบ่อยคือ soft delete — แทนที่จะลบ document คุณติดแฟล็กไว้บน document (ส่วนใหญ่พร้อม timestamp) ด้วยการ update แล้วกัน document ที่ถูกทำเครื่องหมายออกจากการอ่านปกติของคุณ
db.members.updateOne( { name: "Linus" }, { $set: { deleted: true, deletedAt: new Date() } })await db.collection("members").updateOne( { name: "Linus" }, { $set: { deleted: true, deletedAt: new Date() } });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 bson::DateTime;
coll.update_one( doc! { "name": "Linus" }, doc! { "$set": { "deleted": true, "deletedAt": DateTime::now() } },).await?;document ยังคงอยู่ตรงนั้น เพียงแค่ถูกทำเครื่องหมายไว้ — การอ่านปกติแค่กรองออกไป และคุณกู้คืนทีหลังได้ด้วยการ unset แฟล็กนั้น:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "deleted": true, "deletedAt": "2026-06-25T10:15:00.000Z"}เคล็ดลับและจุดที่ต้องระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและจุดที่ต้องระวัง”deleteManyด้วย filter ที่ว่างเปล่า — ตรงกับทุก document — ล้าง collection ทั้งก้อน นี่คือการเรียก CRUD ที่อันตรายที่สุด ให้พิมพ์ filter ก่อนแล้วค่อยพิมพ์ method ทีหลัง จะได้ไม่เผลอรันแบบ filter ว่าง- การลบเป็นแบบถาวร ไม่เหมือนการ update ตรงที่ไม่มีสำเนา “ก่อนหน้า” หลงเหลือไว้ เว้นแต่คุณจะจัดเตรียมไว้ เมื่อไม่แน่ใจ ให้รัน filter เดียวกันผ่าน
findก่อน เพื่อดูให้ชัดว่าคุณกำลังจะลบอะไร deleteOneคือราวกันตกของคุณสำหรับ “ลบระเบียนหนึ่งรายการที่เจาะจง” ต่อให้ filter หลวมกว่าที่คิด ก็ยังลบได้แค่ document เดียวอยู่ดี- soft delete แลกความเรียบง่ายเพื่อความปลอดภัย ทุก query ปกติต้องจำที่จะกัน document ที่ถูกทำเครื่องหมายออก แต่คุณได้ undo และเส้นทางการตรวจสอบมาแทน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
hard delete (deleteOne/deleteMany) | คืนพื้นที่จัดเก็บทันที ไม่มีขยะตกค้าง เรียบง่ายที่สุด | ลบถาวร ไม่มี audit trail ไม่มี undo |
soft delete (update flag เช่น deleted: true) | กู้คืนได้ เก็บ audit trail และ historical data ไว้ครบ | ต้องกรอง flag ใน query ทุกจุดที่อ่านข้อมูล ไม่งั้น document ที่ “ลบ” แล้วโผล่มาอีก และ collection โตขึ้นเรื่อย ๆ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้
deleteOneเมื่อจริง ๆ ต้องการลบทุกรายการที่ตรง filter — เผลอใช้deleteOneคิดว่าลบหมดแล้ว ทั้งที่จริง ๆ หยุดแค่ document แรก เหลือ document อื่นที่ตรง filter เดิมค้างอยู่ใน collection - ไม่ตรวจ
deletedCountหลังลบ — สมมติว่าลบสำเร็จเพราะไม่มี error โยนขึ้นมา ทั้งที่deletedCountอาจเป็น 0 เพราะ filter สะกดผิดหรือพิมพ์ field ผิด - ทำ soft delete แล้วลืมปรับ query ปกติให้กัน document ที่ถูกทำเครื่องหมายออก — เขียน find ปกติโดยไม่เติมเงื่อนไข
deleted: { $ne: true }ทำให้ข้อมูลที่ “ลบ” ไปแล้วโผล่กลับมาในผลการค้นหาให้ผู้ใช้เห็น
💡 ตัวอย่างจากของจริง
ตะกร้าสินค้าที่ค้างอยู่ (abandoned cart) — job รายวันเรียก
deleteMany({ status: "cart", updatedAt: { $lt: thirtyDaysAgo } })กวาดตะกร้าที่ไม่มีการเคลื่อนไหวเกิน 30 วันทิ้ง เพื่อไม่ให้ collection บวมด้วยข้อมูลที่ไม่มีใครจะ checkout แล้วบัญชีผู้ใช้ (user account) — เมื่อผู้ใช้กด “ลบบัญชี” ระบบไม่เรียก
deleteOneทันที แต่ทำ soft delete ด้วยupdateOneตั้งdeleted: trueและdeletedAtเพื่อให้มีช่วงเวลากู้คืนได้ (เช่น 30 วัน) ก่อนที่ job เบื้องหลังจะdeleteOneจริงเมื่อพ้นกำหนด