Upsert และ bulk write
สองท่าสุดท้ายในโมดูลนี้คือการทำมากขึ้นด้วย round trip น้อยลง upsert ตอบคำถามที่คุณเจอบ่อย: “อัปเดต document นี้ถ้ามีอยู่ ไม่เช่นนั้นสร้างใหม่” bulk write ให้คุณส่งรายการ operation ผสม — insert, update, delete — ไปยัง server ในการเรียกเดียวแทนที่จะเดินทางผ่านเครือข่ายทีละ operation ทั้งสองไม่ได้เพิ่ม operator ใหม่ แต่ประกอบเมธอดที่คุณรู้จักให้มีประสิทธิภาพมากขึ้น
เราจบด้วย collection members อีกครั้ง ทุก output ด้านล่างจับมาจาก mongo 8.3.11, mongosh 2.11.1 และ Node driver 7.7.0 (แท็บ Python, Go และ Rust รันหนึ่งครั้งในโปรแกรม scratch บน server เดียวกัน)
ข้อมูลที่เราเขียน
หัวข้อที่มีชื่อว่า “ข้อมูลที่เราเขียน”db.members.insertMany([ { name: "Grace", joined: 2024, fines: 0 }, { name: "Linus", joined: 2023, fines: 2 },]);assert.strictEqual(db.members.countDocuments(), 2);await db.collection("members").insertMany([ { name: "Grace", joined: 2024, fines: 0 }, { name: "Linus", joined: 2023, fines: 2 },]);if ((await db.collection("members").countDocuments()) !== 2) throw new Error("expected 2");Upsert: update หรือ insert
หัวข้อที่มีชื่อว่า “Upsert: update หรือ insert”เพิ่ม option upsert ให้การอัปเดต แล้วเมื่อ filter ไม่ตรงอะไรเลย MongoDB จะ insert document ใหม่ที่สร้างจาก filter บวก update เมื่อ filter ตรง มันทำตัวเหมือนการอัปเดตปกติ ที่นี่เราบันทึกการเข้าชมของ “Katherine” — ถ้าเธอยังไม่ใช่สมาชิก นี่จะสร้างเธอ:
const first = db.members.updateOne( { name: "Katherine" }, { $set: { joined: 2026 }, $inc: { visits: 1 } }, { upsert: true });console.log(first);console.log(db.members.findOne({ name: "Katherine" }));const second = db.members.updateOne( { name: "Katherine" }, { $set: { joined: 2026 }, $inc: { visits: 1 } }, { upsert: true });console.log(second);assert.strictEqual(first.upsertedCount, 1);assert.strictEqual(second.matchedCount, 1);assert.strictEqual(db.members.findOne({ name: "Katherine" }).visits, 2);const res = await db.collection("members").updateOne( { name: "Katherine" }, { $set: { joined: 2026 }, $inc: { visits: 1 } }, { upsert: true });console.log(res);console.log(res.upsertedId);if (res.upsertedCount !== 1) throw new Error("expected an insert");res = db.members.update_one( {"name": "Katherine"}, {"$set": {"joined": 2026}, "$inc": {"visits": 1}}, upsert=True,)print(res.upserted_id)opts := options.UpdateOne().SetUpsert(true)res, err := coll.UpdateOne( ctx, bson.M{"name": "Katherine"}, bson.M{"$set": bson.M{"joined": 2026}, "$inc": bson.M{"visits": 1}}, opts,)if err != nil { return err}fmt.Println(res.UpsertedID)use mongodb::options::UpdateOptions;
let opts = UpdateOptions::builder().upsert(true).build();let res = coll .update_one( doc! { "name": "Katherine" }, doc! { "$set": { "joined": 2026 }, "$inc": { "visits": 1 } }, ) .with_options(opts) .await?;println!("{:?}", res.upserted_id);เพราะไม่มี Katherine อยู่ การเรียกครั้งแรกจึงสร้างเธอ acknowledgement ต่างกันระหว่าง shell และ driver และเนื้อหาบทเก่าผิดตรงนี้: shell รายงาน _id ใหม่เป็น insertedId (ไม่มี field upsertedId ใน mongosh) พร้อม upsertedCount: 1 ขณะที่ Node driver รายงาน upsertedId และ upsertedCount: 1 ทั้งสองแบบ matchedCount: 0 คือสัญญาณว่าเกิดการ insert ไม่ใช่ update:
mongosh{ acknowledged: true, insertedId: ObjectId('6abcaadc73c3d0e445c6944e'), matchedCount: 0, modifiedCount: 0, upsertedCount: 1}
Node{ acknowledged: true, modifiedCount: 0, upsertedId: new ObjectId('6abcaae473c3d0e445c69452'), upsertedCount: 1, matchedCount: 0}
Python 6abcab0173c3d0e445c696c9 (res.upserted_id)Go ObjectID("6abcab0373c3d0e445c696ca") (res.UpsertedID)Rust Some(ObjectId("6abcab0673c3d0e445c696cb"))document ใหม่รวม filter และ update operator (findOne) และการรันการเรียกเดิมครั้งที่สองจะอัปเดตเธอแทน — matchedCount: 1, modifiedCount: 1, upsertedCount: 0 และ visits เพิ่มเป็น 2 (ใน Node ack ที่สองมี upsertedId: null):
{ _id: ObjectId('6abcaadc73c3d0e445c6944e'), name: 'Katherine', joined: 2026, visits: 1 }{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }อะไรเข้าไปอยู่ใน document ที่ insert: $setOnInsert
หัวข้อที่มีชื่อว่า “อะไรเข้าไปอยู่ใน document ที่ insert: $setOnInsert”document ที่ insert คือเงื่อนไข equality ของ filter บวก update เงื่อนไขที่ไม่ใช่ equality ไม่ถูก คัดลอก และ $setOnInsert เขียน field เฉพาะเมื่อ upsert insert (ไม่ทำอะไรในทางอัปเดต):
const r1 = db.people.updateOne( { name: "Ada", joined: { $gt: 2000 } }, // name is equality, joined is a range { $setOnInsert: { fines: 0, createdBy: "upsert" }, $inc: { visits: 1 } }, { upsert: true });console.log(r1.upsertedCount, db.people.findOne({ name: "Ada" }, { _id: 0 }));db.people.updateOne({ name: "Ada" }, { $setOnInsert: { fines: 99 }, $inc: { visits: 1 } }, { upsert: true });console.log(db.people.findOne({ name: "Ada" }, { _id: 0 })); // fines still 0: $setOnInsert skippedassert.strictEqual(db.people.findOne({ name: "Ada" }).fines, 0);assert.strictEqual("joined" in db.people.findOne({ name: "Ada" }), false);1 { name: 'Ada', createdBy: 'upsert', fines: 0, visits: 1 }{ name: 'Ada', createdBy: 'upsert', fines: 0, visits: 2 }equality ของ name มาจาก filter; range บน joined ไม่มา (ไม่มี field joined ถูกสร้าง); fines: 99 ในการเรียกครั้งที่สองถูกเมินเพราะ document มีอยู่แล้ว
bulkWrite: หลาย operation, round trip เดียว
หัวข้อที่มีชื่อว่า “bulkWrite: หลาย operation, round trip เดียว”bulkWrite รับรายการ operation และส่งไปพร้อมกัน แต่ละรายการระบุชื่อ operation และมี filter กับ update เดียวกับที่คุณจะส่งแยกกัน จึงผสม insert, update และ delete ได้อย่างอิสระ ที่นี่เรารับสมาชิกใหม่หนึ่งคน คิดค่าปรับ และลบสมาชิกที่หมดอายุ — ทั้งหมดในการเรียกเดียว:
const res = db.members.bulkWrite([ { insertOne: { document: { name: "Tim", joined: 2026, fines: 0 } } }, { updateOne: { filter: { name: "Grace" }, update: { $inc: { fines: 1 } } } }, { deleteOne: { filter: { name: "Linus" } } }]);console.log(res);assert.strictEqual(res.insertedCount, 1);assert.strictEqual(res.deletedCount, 1);const res = await db.collection("members").bulkWrite([ { insertOne: { document: { name: "Tim", joined: 2026, fines: 0 } } }, { updateOne: { filter: { name: "Grace" }, update: { $inc: { fines: 1 } } } }, { deleteOne: { filter: { name: "Linus" } } },]);console.log(res);if (res.insertedCount !== 1 || res.modifiedCount !== 1 || res.deletedCount !== 1) throw new Error("unexpected counts");from pymongo import InsertOne, UpdateOne, DeleteOne
res = db.members.bulk_write([ InsertOne({"name": "Tim", "joined": 2026, "fines": 0}), UpdateOne({"name": "Grace"}, {"$inc": {"fines": 1}}), DeleteOne({"name": "Linus"}),])print(res.inserted_count, res.modified_count, res.deleted_count)models := []mongo.WriteModel{ mongo.NewInsertOneModel().SetDocument(bson.M{"name": "Tim", "joined": 2026, "fines": 0}), mongo.NewUpdateOneModel(). SetFilter(bson.M{"name": "Grace"}). SetUpdate(bson.M{"$inc": bson.M{"fines": 1}}), mongo.NewDeleteOneModel().SetFilter(bson.M{"name": "Linus"}),}res, err := coll.BulkWrite(ctx, models)if err != nil { return err}fmt.Println(res.InsertedCount, res.ModifiedCount, res.DeletedCount)use mongodb::options::{DeleteOneModel, InsertOneModel, UpdateOneModel, WriteModel};
let ns = coll.namespace();let models = vec![ WriteModel::from( InsertOneModel::builder() .namespace(ns.clone()) .document(doc! { "name": "Tim", "joined": 2026, "fines": 0 }) .build(), ), WriteModel::from( UpdateOneModel::builder() .namespace(ns.clone()) .filter(doc! { "name": "Grace" }) .update(doc! { "$inc": { "fines": 1 } }) .build(), ), WriteModel::from( DeleteOneModel::builder() .namespace(ns.clone()) .filter(doc! { "name": "Linus" }) .build(), ),];let res = client.bulk_write(models).await?;println!("{} {} {}", res.inserted_count, res.modified_count, res.deleted_count);acknowledgement เดียวสรุปทุกอย่างที่เกิดขึ้นข้ามทั้งสาม operation รูปร่างของ shell และ Node (และ bulk_api_result ของ Python) ต่างกันในรายละเอียด:
mongosh{ acknowledged: true, insertedCount: 1, insertedIds: { '0': ObjectId('6abcaadcbdebc81d34174703') }, matchedCount: 1, modifiedCount: 1, deletedCount: 1, upsertedCount: 0, upsertedIds: {}}
NodeBulkWriteResult { insertedCount: 1, matchedCount: 1, modifiedCount: 1, deletedCount: 1, upsertedCount: 0, upsertedIds: {}, insertedIds: { '0': new ObjectId('6abcaae45031e43a4870ad5a') }}
Python {'writeErrors': [], 'writeConcernErrors': [], 'nInserted': 1, 'nUpserted': 0, 'nMatched': 1, 'nModified': 1, 'nRemoved': 1, 'upserted': []}Go, Rust: 1 1 1จุดสำคัญคือ round trip: แทนที่จะคุยกับ server สามครั้งแยกกัน driver รวมรายการเป็นชุดแล้วส่งครั้งเดียว operation ชนิดอื่นทำงานแบบเดียวกัน (updateMany, replaceOne, deleteMany และ upsert: true บนรายการ update); ในการรัน mongosh bulk ที่มี updateOne แบบ upsert, updateMany และ replaceOne คืน matchedCount: 4, modifiedCount: 4, upsertedCount: 1, upsertedIds: { '0': ObjectId('...') }
หมายเหตุเรื่อง Rust bulk_write ของ Rust driver อยู่บน client (ครอบคลุมหลาย namespace) และต้องใช้ server MongoDB 8.0+; ชนิด model เป็น builder (InsertOneModel::builder()...) ที่ห่อด้วย WriteModel รูป struct-literal (WriteModel::InsertOne { namespace: ..., document: ... }) ที่ร่างเก่าของบทนี้แสดงคอมไพล์ไม่ผ่านบน Rust driver 3.9.1 (variant WriteModel::InsertOne has no field named namespace) — โค้ดด้านบนคือสิ่งที่เรารัน
flowchart LR App["Your app"] -->|"one bulkWrite call"| Batch["Batched: insert + update + delete"] Batch -->|"single round trip"| Server["MongoDB server"] Server --> R1["insertedCount: 1"] Server --> R2["modifiedCount: 1"] Server --> R3["deletedCount: 1"]
bulkWrite เดียวใหญ่ได้แค่ไหน?
หัวข้อที่มีชื่อว่า “bulkWrite เดียวใหญ่ได้แค่ไหน?”คำแนะนำเก่าบอกให้แบ่ง batch ใหญ่เอง แต่ driver ทำให้แล้วบนสาย (เคารพ maxWriteBatchSize และ maxMessageSizeBytes ของ server ให้คุณ) การเรียกเดียวจึงรับรายการใหญ่มากได้ บน Mac เครื่องนี้กับ replica set โหนดเดียว:
const ops = Array.from({ length: 250000 }, (_, i) => ({ insertOne: { document: { n: i } } }));let t = Date.now();const r = await db.collection("bulk").bulkWrite(ops, { ordered: false });console.log("250k-op bulkWrite:", r.insertedCount, "in", Date.now() - t, "ms");t = Date.now();const r2 = await db.collection("many").insertMany(Array.from({ length: 100000 }, (_, i) => ({ n: i })));console.log("100k insertMany :", r2.insertedCount, "in", Date.now() - t, "ms");if (r.insertedCount !== 250000) throw new Error("expected all 250000");250k-op bulkWrite: 250000 in 833 ms100k insertMany : 100000 in 323 msเวลามาจากสองการรันบนเครื่องเดียว (862 ms และ 833 ms สำหรับ 250 000 operation) — ประเด็นคือ 250 000 operation ผ่านไปในการเรียกเดียวโดยไม่ต้องแบ่ง batch เอง สิ่งที่ยังต้องระวังคือขีดจำกัดที่ มองเห็นได้ เช่นขนาด 16 MiB ของ document หนึ่งใบ
Ordered เทียบ unordered
หัวข้อที่มีชื่อว่า “Ordered เทียบ unordered”โดยค่าเริ่มต้น bulkWrite เป็น ordered: รัน operation ตามลำดับและหยุดที่อันแรกที่ error ทิ้งที่เหลือไว้ไม่ได้รัน สลับเป็น unordered แล้ว server รัน operation ในลำดับใดก็ได้และไปต่อผ่านความล้มเหลว ใช้ทุก operation ที่สำเร็จได้ Unordered มักเร็วกว่าและเป็นตัวเลือกที่ถูกเมื่อ operation ไม่พึ่งพากัน ตัวอย่างเดิม insert สองชื่อที่ต่างกันจึงไม่มีทางล้มเหลวทั้งสองแบบ; เพื่อเห็นความต่างต้องมีสิ่งที่ล้มเหลว — unique index กับตัวซ้ำ:
db.roster.createIndex({ name: 1 }, { unique: true });db.roster.insertOne({ name: "Tim", joined: 2026 });const ops = () => [ { insertOne: { document: { name: "Barbara", joined: 2026 } } }, { insertOne: { document: { name: "Tim", joined: 2026 } } }, // duplicate: fails { insertOne: { document: { name: "Margaret", joined: 2026 } } },];for (const ordered of [true, false]) { db.roster.deleteMany({ name: { $in: ["Barbara", "Margaret"] } }); let err; try { db.roster.bulkWrite(ops(), { ordered }); } catch (e) { err = e; } console.log(`ordered=${ordered}:`, err.name, err.code, "inserted", err.result.insertedCount, "failed at", err.writeErrors.map((w) => w.index), "->", db.roster.find({}, { _id: 0, name: 1 }).sort({ name: 1 }).toArray().map((d) => d.name));}const roster = db.collection("roster");await roster.createIndex({ name: 1 }, { unique: true });await roster.insertOne({ name: "Tim", joined: 2026 });const ops = () => [ { insertOne: { document: { name: "Barbara", joined: 2026 } } }, { insertOne: { document: { name: "Tim", joined: 2026 } } }, { insertOne: { document: { name: "Margaret", joined: 2026 } } },];for (const ordered of [true, false]) { await roster.deleteMany({ name: { $in: ["Barbara", "Margaret"] } }); try { await roster.bulkWrite(ops(), { ordered }); } catch (e) { console.log(`ordered=${ordered}:`, e.name, e.code, "inserted", e.result.insertedCount, "failed at", e.writeErrors.map((w) => w.index), "->", (await roster.find().sort({ name: 1 }).toArray()).map((d) => d.name)); }}from pymongo import InsertOne
db.members.bulk_write( [ InsertOne({"name": "Barbara", "joined": 2026}), InsertOne({"name": "Tim", "joined": 2026}), ], ordered=False,)models := []mongo.WriteModel{ mongo.NewInsertOneModel().SetDocument(bson.M{"name": "Barbara", "joined": 2026}), mongo.NewInsertOneModel().SetDocument(bson.M{"name": "Tim", "joined": 2026}),}opts := options.BulkWrite().SetOrdered(false)_, err := coll.BulkWrite(ctx, models, opts)if err != nil { return err}use mongodb::options::{InsertOneModel, WriteModel};
let ns = coll.namespace();let models = vec![ WriteModel::from( InsertOneModel::builder() .namespace(ns.clone()) .document(doc! { "name": "Barbara", "joined": 2026 }) .build(), ), WriteModel::from( InsertOneModel::builder() .namespace(ns.clone()) .document(doc! { "name": "Tim", "joined": 2026 }) .build(), ),];client.bulk_write(models).ordered(false).await?;ผลจริง: ด้วย ordered: true insert ที่สอง (index 1 Tim ซ้ำ) ล้มเหลวด้วย code 11000 และ Margaret ที่ตามหลังไม่เคยรัน — มีเพียง Barbara ที่ถูก insert ด้วย ordered: false ตัวซ้ำยังถูกรายงาน แต่ Barbara และ Margaret เข้าไปทั้งคู่:
ordered=true: MongoBulkWriteError 11000 inserted 1 failed at [ 1 ] -> [ 'Barbara', 'Tim' ]ordered=false: MongoBulkWriteError 11000 inserted 2 failed at [ 1 ] -> [ 'Barbara', 'Margaret', 'Tim' ]ทั้งสองโหมด error บอกชัดว่า operation ไหนล้มเหลว (writeErrors[i].index) และสำเร็จกี่รายการ (result.insertedCount); ไม่มีโหมดใด rollback สิ่งที่ใช้ไปแล้ว
ใน Compass: ไม่มีแผง bulk-write; รัน bulkWrite ใน shell ที่ฝังอยู่ หรือใน driver ของคุณ
เคล็ดลับและข้อควรระวัง
หัวข้อที่มีชื่อว่า “เคล็ดลับและข้อควรระวัง”- upsert สร้าง document ใหม่จาก ทั้ง เงื่อนไข equality ของ filter และ update operator — เงื่อนไข range ไม่ถูกคัดลอก (ดู
$setOnInsertด้านบน) ทำ filter ให้เจาะจงเพื่อให้ document ที่ insert ออกมาตามที่คาด - หลัง upsert ตรวจ
matchedCount:0พร้อมinsertedId(shell) /upsertedId(driver) หมายถึงมี document ถูกสร้าง;1หมายถึงอัปเดตอันที่มีอยู่ - upsert ปลอดภัยจากการแข่งขันก็ต่อเมื่อมี unique index บน field ของ filter (พิสูจน์ด้านล่าง)
bulkWriteแบบ ordered หยุดที่ error แรก ถ้าต้องการให้ทุก operation ที่เป็นอิสระถูกลองไม่ว่าจะมีความล้มเหลว ตั้งordered: falsebulkWriteเกี่ยวกับประสิทธิภาพ ไม่ใช่ transaction ถ้าไม่ห่อด้วย transaction operation ไม่ all-or-nothing — บางอันสำเร็จขณะที่อันอื่นล้มเหลวได้ (inserted 1ด้านบน)- ผลใน shell มี
insertedCountบนbulkWriteแต่ไม่มีบนinsertMany; ตรวจว่าคุณถือ object ไหน
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ผู้ช่วย AI ที่ถูกขอตัวนับการเข้าชมหน้าจะหยิบ upsert และอธิบายว่า “รับประกันหนึ่งแถวต่อหน้า”:
🐛 หา bug ในโค้ดนี้
// AI comment: "upsert is atomic, so there can never be two rows for the same page"async function recordView(db, page) {await db.collection("counters").updateOne( { page }, { $inc: { views: 1 } }, { upsert: true });}เฉลย
การอัปเดตหนึ่ง document เป็น atomic แต่ “ไม่พบแล้ว insert” เป็นสองขั้น และเมื่อไม่มี unique index การเรียกพร้อมกันสองครั้งอาจไม่พบทั้งคู่และ insert ทั้งคู่ เรายิง upsert พร้อมกัน 100 ครั้งต่อหน้าเป็นเวลา 20 หน้าบน client เดียว: 17 ถึง 19 จาก 20 หน้ามี row ซ้ำ และหน้าหนึ่งสะสมได้ถึง 16 document (สามครั้งที่รัน: 19/20 สูงสุด 7 document, 17/20 สูงสุด 13, 17/20 สูงสุด 16 — จำนวนแน่นอนขึ้นกับจังหวะเวลา) ทุก upsert รายงานสำเร็จ และผลรวมยังถูกต้อง (2000 views) จึงไม่มีอะไรดูผิดปกติ เมื่อมี unique index บน page การรันเดียวกันให้ หนึ่ง document ต่อหน้าพอดี ไม่มี upsert ถูกปฏิเสธ รวม 2000 views (server retry ผู้แพ้การแข่งขันเป็น update):
const ROUNDS = 20, PARALLEL = 100;async function race(useUniqueIndex) { const c = db.collection(useUniqueIndex ? "counters_unique" : "counters_plain"); if (useUniqueIndex) await c.createIndex({ page: 1 }, { unique: true }); let roundsWithDuplicates = 0, maxDocs = 0, rejected = 0; for (let r = 0; r < ROUNDS; r++) { const page = `/home-${r}`; const results = await Promise.allSettled(Array.from({ length: PARALLEL }, () => c.updateOne({ page }, { $inc: { views: 1 } }, { upsert: true }))); rejected += results.filter((x) => x.status === "rejected").length; const docs = await c.countDocuments({ page }); if (docs > 1) roundsWithDuplicates++; maxDocs = Math.max(maxDocs, docs); } const total = (await c.aggregate([{ $group: { _id: null, v: { $sum: "$views" } } }]).next()).v; return { roundsWithDuplicates: `${roundsWithDuplicates}/${ROUNDS}`, maxDocsForOnePage: maxDocs, rejectedUpserts: rejected, totalViews: total };}const plain = await race(false);const unique = await race(true);console.log("without unique index:", plain);console.log("with unique index :", unique);if (unique.maxDocsForOnePage !== 1 || unique.totalViews !== ROUNDS * PARALLEL) throw new Error("unique index should give one document per page");without unique index: { roundsWithDuplicates: '17/20', maxDocsForOnePage: 16, rejectedUpserts: 0, totalViews: 2000}with unique index : { roundsWithDuplicates: '0/20', maxDocsForOnePage: 1, rejectedUpserts: 0, totalViews: 2000}วิธีแก้คือบรรทัดเดียว ทำครั้งเดียว: createIndex({ page: 1 }, { unique: true })
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”upsert คือ “ตรงก็ใช้ ไม่ตรงก็สร้าง” ที่ประเมินบน server แต่ ความไม่ซ้ำ ของสิ่งที่มันสร้างแข็งแรงเท่าข้อจำกัดที่อยู่เบื้องหลังเท่านั้น bulk write คือ รายการคำสั่งอิสระในซองเดียว: ซองประหยัด round trip, ordered ตัดสินว่ารายการหยุดที่ความล้มเหลวแรกหรือไม่ และไม่มีอันไหนให้ all-or-nothing รวมกัน: unique index ปกป้องความถูกต้อง ซอง bulk ปกป้อง latency
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
bulkWrite | รวมหลาย operation ใน round trip เดียว; insert 250 000 รายการในราว 850 ms ในการเรียกเดียว | ผลลัพธ์รวมเป็นชุดเดียว — ต้องแกะ writeErrors เองเมื่อบางอันล้มเหลว; ไม่ atomic |
bulkWrite ordered (ค่าเริ่มต้น) | คาดการณ์ได้: operation รันตามลำดับรายการและหยุดที่ error แรก | ความล้มเหลวหนึ่งครั้งทิ้ง operation ที่ตามมาทั้งหมดไม่ได้รัน (Margaret ด้านบน) |
bulkWrite unordered | ไปต่อผ่านความล้มเหลว; มักเร็วกว่า | ไม่รับประกันลำดับ; ต้องอ่าน writeErrors |
| upsert (มี unique index) | round trip เดียวครอบคลุมทั้ง insert-ถ้าไม่มี และ update-ถ้ามี; ปลอดภัยจากการแข่งขัน | ต้องมี unique index (ต้นทุนการเขียนและพื้นที่เพิ่ม) |
| upsert (ไม่มี unique index) | ไม่มีอะไรต้องดูแลเพิ่ม | การเรียกพร้อมกันทำ document ซ้ำ (17-19 จาก 20 หน้าในการรันของเรา) |
ตรวจก่อนเขียนแบบชัดเจน (findOne ก่อน) | ควบคุมละเอียดก่อนเขียน | สอง round trip และช่องว่างการแข่งขันเดียวกันระหว่าง find กับ write |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “upsert ป้องกัน duplicate เสมอ” — เฉพาะเมื่อมี unique index หลัง filter; ถ้าไม่มี เราได้ถึง 16 document สำหรับหนึ่งหน้า
- “
upsertedIdอยู่ในทุกผลลัพธ์” — mongosh รายงานinsertedIdและupsertedCount; driver รายงานupsertedId - “ต้องแบ่ง bulk write ใหญ่เอง” — driver แบ่ง batch บนสายให้; 250 000 operation รันในการเรียกเดียวในราว 850 ms แบ่ง batch เฉพาะเพื่อหน่วยความจำหรือการรายงานความคืบหน้าของคุณเอง
- “Unordered แปลว่าเร็วและปลอดภัย” — มันไปต่อผ่านความล้มเหลว แต่คุณยังต้องอ่าน
writeErrorsและไม่มีอะไรถูก rollback - “range ใน filter ของ upsert ไปอยู่ใน document ใหม่” — คัดลอกเฉพาะเงื่อนไข equality
- “
bulkWriteคือ transaction” — ไม่ใช่;inserted 1ยังคงอยู่หลังความล้มเหลว
ตัวอย่างจากของจริง
reference ของ
updateOneระบุข้อกำหนดที่เราวัด: “To avoid multiple upserts, ensure that thefilterfield(s) are uniquely indexed” (db.collection.updateOne, เรียกอ่านเมื่อ 2026-09-30) ในการรันของเรา unique index ไม่ทำให้ upsert ใดถูกปฏิเสธ — ผู้แพ้การแข่งขันถูก server retry เป็น update — ดังนั้น 2000 จาก 2000 การเพิ่มตกลงบน 20 document
แท็บ Python/Go/Rust สะท้อนพฤติกรรมที่พิสูจน์แล้วของ shell/Node; แต่ละแท็บถูกรันหนึ่งครั้งในโปรแกรม scratch กับ server เดียวกัน (options.Update() ของ Go ถูกแก้เป็น options.UpdateOne() สำหรับ driver v2 และแท็บ Rust bulk_write ถูกเขียนใหม่ — ทั้งคู่คอมไพล์ไม่ผ่านตามที่เขียนไว้เดิม) ไม่ถูกรันโดยตัวตรวจอัตโนมัติของคอร์ส