এই পর্বে যা শিখবেন
- Document, BSON-এর টাইপ, collection, database আর
_id— relational ধারণার সাথে মিলিয়ে - mongosh-এ insert, find, update, delete — filter, projection, sort, upsert আর array-র ফাঁদসহ
- db-shop-কে document-এ সাজানো: embed, reference আর পাঁচটি প্রচলিত schema pattern
- Aggregation pipeline দিয়ে ক্যাটাগরিভিত্তিক বিক্রি আর সেরা গ্রাহক — পাশে SQL
- Index ও ESR নিয়ম, schema validation, transaction, replica set, sharding, নিরাপত্তা — আর কখন PostgreSQL + JSONB-ই যথেষ্ট
১ · Document মডেল — BSON, collection আর _id
পর্ব ৫৬-এ চার ধরনের NoSQL ডেটাবেসের ছবি দেখেছেন। এই পর্বে সবচেয়ে প্রচলিত document database, MongoDB, নিয়ে হাতে-কলমে কাজ। MongoDB-র প্রথম সংস্করণ আসে ২০০৯ সালে, 10gen নামের একটি কোম্পানি থেকে (২০১৩ সালে নাম বদলে MongoDB Inc.)। নামটি এসেছে ইংরেজি “humongous” — বিশাল — থেকে।
পাড়ার দর্জির দোকানে দুই রকম খাতা ভাবুন। এক রকম দোকানে তিনটি আলাদা খাতা — গ্রাহকের খাতা, অর্ডারের খাতা, আর প্রতিটি অর্ডারের পোশাকের খাতা; একটি অর্ডার দেখতে তিন খাতা মেলাতে হয়। এটি relational পথ। আরেক দোকানে প্রতিটি অর্ডারের জন্য একটি করে কার্ড: ওপরে গ্রাহকের নাম আর ফোন, নিচে সব পোশাক, মাপ আর দাম — এক কার্ডেই সব। এই কার্ডটিই একটি document, আর কার্ডের বাক্সটি একটি collection.
একটি document দেখতে JSON-এর মতো (পর্ব ৩০): নাম-মানের জোড়া, যার মান হতে পারে আরেকটি document (embedded) বা একটি array. একই ধরনের document থাকে একটি collection-এ (টেবিলের মতো), আর কয়েকটি collection মিলে একটি database. কিন্তু দুটি বড় পার্থক্য আছে। এক, একই collection-এর document-গুলোর কাঠামো এক হতে হবে, এমন বাধ্যবাধকতা নেই — যদি না আপনি নিজে নিয়ম দেন (অংশ ৮)। দুই, ডিস্কে JSON লেখা থাকে না; থাকে BSON — Binary JSON, যাতে JSON-এর চেয়ে বেশি টাইপ আছে আর পড়তে পার্স করতে হয় না।
| Relational | MongoDB | লক্ষ করার বিষয় |
|---|---|---|
| database | database | প্রথম লেখার সময় নিজে থেকে তৈরি হয় |
| table | collection | আগে CREATE করতে হয় না; schema ঐচ্ছিক |
| row | document | ভেতরে nested document আর array থাকতে পারে; সর্বোচ্চ ১৬ MB |
| column | field | document-ভেদে থাকতে বা না থাকতে পারে |
| primary key | _id | প্রতিটি document-এ বাধ্যতামূলক, collection-এ অনন্য, বদলানো যায় না |
| JOIN | embed, বা $lookup | অংশ ৫ ও ৬ |
| foreign key | নেই | reference রাখা যায়, কিন্তু DBMS তা রক্ষা করে না |
BSON-এর টাইপ — বিশেষ করে তিনটি
JSON-এ সংখ্যা মাত্র এক রকম, তারিখের কোনো টাইপ নেই। BSON-এ আছে 32-bit ও 64-bit integer, double, string (UTF-8), boolean, null, array, object, binary — আর তিনটি টাইপ, যা প্রতিদিন লাগবে:
ObjectId
আপনি _id না দিলে ড্রাইভার বা সার্ভার একটি ObjectId বানায় — ১২ byte: প্রথম ৪ byte তৈরির সময় (সেকেন্ডে), তারপর ৫ byte এলোমেলো মান, শেষে ৩ byte বাড়তে থাকা কাউন্টার। কেন্দ্রীয় কাউন্টার ছাড়াই অনন্য, আর মোটামুটি সময়ের ক্রমে বাড়ে।
Date
Unix epoch থেকে মিলিসেকেন্ড, 64-bit integer — সবসময় UTC-তে। mongosh-এ ISODate("2025-09-01") বা new Date(). তারিখকে string-এ রাখলে তুলনা, TTL index আর তারিখের ফাংশন — সব হারাবেন।
Decimal128
Decimal128 (MongoDB ৩.৪ থেকে) — ১২৮ বিটের দশমিক সংখ্যা, ৩৪ অঙ্ক পর্যন্ত হুবহু। mongosh-এ NumberDecimal("1200.50"). টাকার জন্য এটিই; double-এ 0.1 + 0.2 হুবহু 0.3 হয় না (floating-point, পর্ব ১৩)।
এই পর্বের শুরুতে কোর্সের পরিচিত db-shop ডেটাবেস — customers (customer_id, name, city, phone, joined_on), categories, products (product_id, name, category_id, price, stock), orders (order_id, customer_id, order_date, status) আর order_items (order_id, product_id, quantity, unit_price). MongoDB এই পাতায় চলে না, কিন্তু SQLite-এর JSON ফাংশন দিয়ে ঠিক MongoDB-র মতো document বানানো যায়। নিচের এডিটরগুলোয় একটি বাড়তি টেবিল order_docs(id, doc) আগে থেকে তৈরি: প্রতিটি অর্ডার একটি JSON document — গ্রাহকের সংক্ষিপ্ত তথ্য আর সব item ভেতরে বসানো। দেখুন অর্ডার 1001 কেমন দেখায়:
-- তিনটি টেবিলের তথ্য, একটি document-এ
SELECT json_pretty(doc) AS doc FROM order_docs WHERE id = 1001;
-- document-এর আকার (byte)
SELECT id, length(doc) AS bytes FROM order_docs WHERE id IN (1001, 1010);{
"_id": 1001,
"customer": { "_id": 1, "name": "Rahim Uddin", "city": "Dhaka" },
"order_date": "2025-09-01",
"status": "delivered",
"items": [
{ "product_id": 101, "qty": 1, "unit_price": 15000.0 },
{ "product_id": 102, "qty": 2, "unit_price": 1200.0 }
]
}
id │ bytes
1001 │ 221
1010 │ 171
(জায়গা বাঁচাতে এখানে document-টি একটু ছোট করে সাজানো।) orders-এর একটি সারি, customers-এর একটি সারির অংশ আর order_items-এর দুটি সারি — একটি document-এ চলে এসেছে। অর্ডারের পাতা দেখাতে এখন একটিই পড়া, কোনো JOIN নেই। এটিই document মডেলের মূল প্রতিশ্রুতি: যা একসাথে পড়া হয়, তা একসাথে রাখুন। দাম পর্বের শেষ পর্যন্ত মনে রাখুন: এখানে unit_price একটি REAL (double) — আসল MongoDB-তে এর জায়গা Decimal128.
২০০০-এর দশকের শেষে ওয়েব অ্যাপ দ্রুত বাড়ছিল, আর প্রোগ্রামাররা প্রতিদিন object ও টেবিলের অমিল (impedance mismatch) সামলাতে ক্লান্ত। MongoDB-র প্রস্তাব ছিল: অ্যাপের object যেমন, ডেটাবেসেও তেমন রাখা, আর একটি সার্ভার ছাড়িয়ে গেলে বহু সার্ভারে ছড়িয়ে দেওয়া। পরের দশকে এটি ধীরে ধীরে relational জগতের অনেক কিছু গ্রহণ করেছে — schema validation (৩.২–৩.৬), multi-document transaction (৪.০, ২০১৮)। ২০১৮ সালের অক্টোবর থেকে Community Server-এর লাইসেন্স SSPL, যাকে OSI “open source” হিসেবে স্বীকৃতি দেয়নি — তাই একে অনেকে “source-available” বলেন।
২ · MongoDB চালানো — Atlas, Community Server, mongosh আর Compass
শেখা আর ছোট প্রকল্পের জন্য তিনটি প্রচলিত পথ। কোনটি “সেরা”, তা নির্ভর করে আপনার ইন্টারনেট, কম্পিউটার আর কাজের ধরনের ওপর:
MongoDB Atlas হলো MongoDB Inc.-এর নিজস্ব managed ক্লাউড সেবা (managed database). একটি বিনামূল্যের ছোট ক্লাস্টার (M0 নামে পরিচিত) দিয়ে শেখা শুরু করা যায়; সীমা আর শর্ত সময়ে সময়ে বদলায়, তাই সাইটে মিলিয়ে নিন। ইনস্টল লাগে না, কিন্তু ইন্টারনেট লাগে। অ্যাকাউন্ট খুলে ক্লাস্টার বানানোর পর দুটি কাজ অবশ্যই করবেন: একটি database user (শক্ত password) আর IP access list-এ শুধু নিজের IP — “সব জায়গা থেকে” (0.0.0.0/0) নয়।
# Atlas-এর "Connect" বোতাম থেকে পাওয়া ঠিকানা; password শেল নিজে জিজ্ঞেস করবে
mongosh "mongodb+srv://cluster0.example.mongodb.net/shop" --username rahimMongoDB Community Server Windows, macOS আর Linux-এ বিনামূল্যে চালানো যায় — অফিসিয়াল installer বা প্যাকেজ দিয়ে, অথবা Docker দিয়ে। Docker হলে port-টি শুধু নিজের কম্পিউটারের (127.0.0.1) সাথে বাঁধুন, আর admin password ফাইলে রাখুন, কমান্ডে নয়। ইমেজের সংস্করণ-ট্যাগ (যেমন 8.0) চালানোর আগে মিলিয়ে নিন।
# mongo.env ফাইলে: MONGO_INITDB_ROOT_USERNAME=admin আর MONGO_INITDB_ROOT_PASSWORD=...
docker run -d --name shopdb -p 127.0.0.1:27017:27017 \
-v shopdb-data:/data/db --env-file mongo.env mongo:8.0
mongosh "mongodb://127.0.0.1:27017" --username admin --authenticationDatabase adminশুধু ছোট পরীক্ষার জন্য একটি নোড চলে; কিন্তু transaction (অংশ ৮) চালাতে সার্ভারকে replica set হিসেবে চালু করতে হয় — এক সদস্যের replica set-ও চলে।
mongosh হলো MongoDB Shell — একটি JavaScript (Node.js-ভিত্তিক) কমান্ড-লাইন, যেখানে এই পর্বের সব কোড চলে। পুরোনো mongo শেল ৬.০ সংস্করণে বাদ পড়েছে, তাই ইন্টারনেটের পুরোনো টিউটোরিয়ালে db.coll.insert() বা update() দেখলে সতর্ক থাকুন। MongoDB Compass হলো বিনামূল্যের GUI — document দেখা, filter লেখা, index আর explain-এর ছবি দেখার জন্য সুবিধাজনক; এর ভেতরেও একটি mongosh আছে। VS Code-এর জন্যও MongoDB-র extension আছে।
৩ · তৈরি আর খোঁজা — insert, find, filter আর projection
mongosh-এ use shop লিখলে database বাছাই হয় (প্রথম লেখার সময় তৈরি হয়)। তারপর db.<collection>.<method>() — collection-ও প্রথম insert-এ নিজে থেকে তৈরি হয়। এই পর্বের কোডে relational আইডিগুলো (1, 101, 1001) _id হিসেবে রাখা হলো, যাতে SQL-এর সাথে মেলানো সহজ হয়। সংখ্যা, string — যেকোনো অনন্য মানই _id হতে পারে। নিচের কোড এই পাতায় চলবে না; mongosh-এ চালানোর জন্য:
use shop
db.customers.insertOne({
_id: 1, name: "Rahim Uddin", phone: "01711000001",
joined_on: ISODate("2025-01-15"),
addresses: [ // embedded, সীমিত সংখ্যক
{ label: "home", city: "Dhaka", area: "Mirpur" },
{ label: "office", city: "Dhaka", area: "Motijheel" }
]
})
// { acknowledged: true, insertedId: 1 }
db.products.insertMany([
{ _id: 101, name: "Mobile Phone", category: "Electronics", price: NumberDecimal("15000"),
stock: 12, attrs: { ram_gb: 8, colors: ["black", "blue"] } },
{ _id: 102, name: "Headphone", category: "Electronics", price: NumberDecimal("1200"), stock: 40 }
])
db.orders.insertOne({
_id: 1001,
customer: { _id: 1, name: "Rahim Uddin", city: "Dhaka" }, // extended reference
order_date: ISODate("2025-09-01"), status: "delivered",
items: [
{ product_id: 101, name: "Mobile Phone", qty: 1, unit_price: NumberDecimal("15000") },
{ product_id: 102, name: "Headphone", qty: 2, unit_price: NumberDecimal("1200") }
],
total: NumberDecimal("17400") // computed pattern
})
db.reviews.insertOne({ product_id: 101, rating: 5, body: "Battery lasts two days" })
// { acknowledged: true, insertedId: ObjectId('...') } ← _id দিইনি, তাই ObjectIdধরে নিন বাকি সাতজন গ্রাহক, দশটি পণ্য আর দশটি অর্ডার একইভাবে ঢোকানো হয়েছে (db-shop-এর মানগুলোই)। insertMany ডিফল্টে ক্রমানুসারে (ordered: true) ঢোকায় আর প্রথম error-এ থেমে যায়; আগের document-গুলো থেকে যায় — পুরো ব্যাচ মিলে একটি transaction নয়।
find আর query operator
find(filter, projection) ফেরত দেয় একটি cursor — mongosh প্রথম ২০টি দেখায়, বাকিটা দেখতে it. Filter নিজেই একটি document: { status: "delivered" } মানে status = 'delivered'. তুলনার জন্য query operator — $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin; একাধিক শর্ত একসাথে লিখলে নিজে থেকে AND, আর OR-এর জন্য $or. Embedded document-এর ভেতরে ঢুকতে dot notation — "customer.city" (উদ্ধৃতিচিহ্ন বাধ্যতামূলক)।
db.orders.find({ status: "delivered" }) // WHERE status = 'delivered'
db.orders.find({ total: { $gt: 3000 } }) // WHERE total > 3000
db.orders.find({ status: { $in: ["shipped", "pending"] }, // IN (...) AND
"customer.city": "Dhaka" }) // customer.city = 'Dhaka'
db.orders.find({ $or: [ { status: "pending" },
{ total: { $gte: 10000 } } ] }) // 1001, 1006, 1009
db.customers.find({ "addresses.city": "Chattogram" }) // array-র যেকোনো ঠিকানায়
db.products.find({ "attrs.colors": "blue" }) // array-তে "blue" থাকলেই
db.orders.countDocuments({ status: "delivered" }) // 5filter কীভাবে একটি document-এর সাথে মেলানো হয়, তা বোঝার সবচেয়ে ভালো উপায় নিজে একটি ছোট মিলানোর যন্ত্র লেখা। নিচের JavaScript কোডটি এই পাতাতেই চলে — ছয়টি অর্ডারের একটি array-র ওপর $eq, $gt, $gte, $lt, $in আর dot notation মানে এমন একটি খুদে find(). শর্ত বদলে চালিয়ে দেখুন:
const orders = [
{ _id: 1001, status: "delivered", total: 17400, customer: { city: "Dhaka" } },
{ _id: 1002, status: "delivered", total: 1750, customer: { city: "Chattogram" } },
{ _id: 1003, status: "shipped", total: 1780, customer: { city: "Dhaka" } },
{ _id: 1004, status: "cancelled", total: 4500, customer: { city: "Dhaka" } },
{ _id: 1005, status: "delivered", total: 5050, customer: { city: "Rajshahi" } },
{ _id: 1006, status: "pending", total: 3500, customer: { city: "Sylhet" } },
];
const get = (doc, path) => path.split(".").reduce((v, k) => (v == null ? undefined : v[k]), doc);
const ops = { $eq: (a, b) => a === b, $gt: (a, b) => a > b, $gte: (a, b) => a >= b,
$lt: (a, b) => a < b, $in: (a, list) => list.includes(a) };
const matches = (doc, filter) => Object.entries(filter).every(([path, cond]) => {
const v = get(doc, path); // "customer.city" = dot notation
if (cond === null || typeof cond !== "object") return v === cond; // {f: x} মানে {f: {$eq: x}}
return Object.entries(cond).every(([op, arg]) => ops[op](v, arg)); // একাধিক অপারেটর = AND
});
const find = (filter) => orders.filter((d) => matches(d, filter)).map((d) => d._id);
console.log(find({ status: "delivered" }));
console.log(find({ total: { $gt: 3000 } }));
console.log(find({ status: { $in: ["shipped", "pending"] }, "customer.city": "Dhaka" }));
console.log(find({ total: { $gte: 1000, $lt: 5000 }, "customer.city": { $eq: "Dhaka" } }));[ 1001, 1002, 1005 ] [ 1001, 1004, 1005, 1006 ] [ 1003 ] [ 1003, 1004 ]
প্রতিটি filter-এর প্রতিটি field একটি শর্ত, আর every মানে সব শর্ত সত্য হতে হবে — এজন্যই একাধিক field মানে AND. চতুর্থ লাইনে একই field-এ দুটি অপারেটর ($gte আর $lt) — দুটিই মানতে হবে, তাই 1000 থেকে 5000-এর নিচে ঢাকার অর্ডার: 1003 (1780) আর 1004 (4500). আসল MongoDB আরও দুটি কাজ করে, যা এই খুদে যন্ত্রে নেই। এক, field-টি array হলে শর্ত array-র যেকোনো উপাদানে মিললেই চলে। দুই, type bracketing: $gt: 3000 শুধু সংখ্যা-জাতীয় মানের সাথে তুলনা করে (int, long, double, Decimal128 সবই মানে-মানে তুলনা হয়); কোনো document-এ total: "4500" (string) থাকলে সেটি মিলবে না। JavaScript-এর > কিন্তু string আর সংখ্যা মিশিয়ে তুলনা করে ফেলে।
একই প্রশ্ন SQL-এ, JSON document-এর ওপর — dot notation হয়ে যায় JSON path ('$.customer.city'), আর ->> দেয় SQL মান (পর্ব ৩০):
-- find({ status: "delivered", "customer.city": "Dhaka" })
SELECT id FROM order_docs
WHERE doc ->> '$.status' = 'delivered'
AND doc ->> '$.customer.city' = 'Dhaka';
-- find({ status: { $in: ["shipped", "pending"] } })
SELECT id, doc ->> '$.status' AS status FROM order_docs
WHERE doc ->> '$.status' IN ('shipped', 'pending');
-- find({ "customer.city": null })
SELECT id, doc -> '$.customer' AS customer FROM order_docs
WHERE doc ->> '$.customer.city' IS NULL;id
1001
id │ status
1003 │ shipped
1006 │ pending
1008 │ shipped
1009 │ pending
id │ customer
1010 │ {"_id":7,"name":"Imran Hossain","city":null}
প্রথম প্রশ্নে শুধু 1001 — ঢাকার তিন গ্রাহকের (Rahim, Sabbir, Mitu) মধ্যে delivered অর্ডার শুধু Rahim-এর এটি। শেষ প্রশ্নটি গুরুত্বপূর্ণ: MongoDB-তে { "customer.city": null } মেলে দুই ধরনের document-এ — যেখানে মান null, আর যেখানে field-টিই নেই। SQL-এর IS NULL-ও এখানে দুটিকে এক করে, কারণ না-থাকা JSON path-ও NULL দেয়। শুধু “field-টি আছে কিন্তু null” চাইলে MongoDB-তে { "customer.city": { $type: "null" } }, আর শুধু “field নেই” চাইলে { $exists: false }.
Array-র ফাঁদ — dot notation বনাম $elemMatch
প্রশ্ন: “কোন অর্ডারে Headphone (102) অন্তত ২টি কেনা হয়েছে?” স্বাভাবিকভাবে লেখা যায় { "items.product_id": 102, "items.qty": { $gte: 2 } }. কিন্তু array-তে প্রতিটি শর্ত আলাদাভাবে যেকোনো উপাদানে মিললেই চলে — একটি item-এ 102, আর অন্য কোনো item-এ qty ≥ 2 থাকলেও document মিলে যাবে। একই উপাদানে দুটি শর্ত চাইলে $elemMatch:
db.orders.find({ "items.product_id": 102, "items.qty": { $gte: 2 } }) // 1001, 1007 (!)
db.orders.find({ items: { $elemMatch: { product_id: 102, qty: { $gte: 2 } } } }) // 1001-- dot notation-এর অর্থ: দুটি আলাদা EXISTS, দুটি আলাদা item হতে পারে
SELECT d.id FROM order_docs d
WHERE EXISTS (SELECT 1 FROM json_each(d.doc, '$.items') j WHERE j.value ->> 'product_id' = 102)
AND EXISTS (SELECT 1 FROM json_each(d.doc, '$.items') j WHERE j.value ->> 'qty' >= 2);
-- $elemMatch-এর অর্থ: একটিই EXISTS, একই item-এ দুই শর্ত
SELECT d.id FROM order_docs d
WHERE EXISTS (SELECT 1 FROM json_each(d.doc, '$.items') j
WHERE j.value ->> 'product_id' = 102 AND j.value ->> 'qty' >= 2);id id 1001 1001 1007
অর্ডার 1007-এ আছে Headphone ১টি আর Rice ৫টি। প্রথম কোয়েরিতে “102 আছে” শর্ত মিলেছে Headphone-এ, আর “qty ≥ 2” মিলেছে Rice-এ — দুই আলাদা item, তবু অর্ডারটি এসে গেছে। দ্বিতীয় কোয়েরি একই item-এ দুটি শর্ত চায়, তাই শুধু 1001 (Headphone ২টি)। SQL-এ পার্থক্যটা চোখে দেখা যায় — একটি বনাম দুটি semi join (পর্ব ২০); MongoDB-তে দুটির লেখা প্রায় একই রকম দেখায় বলেই এটি সবচেয়ে প্রচলিত ভুলগুলোর একটি।
Projection, sort, skip আর limit
db.orders.find(
{ status: { $in: ["delivered", "shipped"] } },
{ _id: 1, "customer.name": 1, total: 1 } // projection: শুধু এই field-গুলো
).sort({ total: -1, _id: 1 }).skip(0).limit(3) // -1 = বড় থেকে ছোট
// 1001 (17400), 1005 (5050), 1007 (3250)Projection-এ 1 মানে রাখা, 0 মানে বাদ দেওয়া; দুটি মেশানো যায় না, শুধু _id: 0 ব্যতিক্রম (_id ডিফল্টে সবসময় আসে)। লেখার ক্রম যা-ই হোক, সার্ভার সবসময় আগে sort, তারপর skip, তারপর limit করে। পর্ব ১৫-এর মতো এখানেও sort-এ একটি অনন্য field (_id) যোগ করুন, নইলে সমান total-এর document-গুলোর ক্রম নিশ্চিত নয়, আর পাতাভিত্তিক (pagination) ফলে ডুপ্লিকেট বা বাদ পড়া দেখা দেবে। বড় skip ধীর — বাদ দেওয়া document-গুলোও পড়তে হয়; তাই keyset pagination (“শেষ দেখা total ও _id-এর পরে”) এখানেও ভালো।
৪ · বদলানো আর মোছা — update operator, upsert আর delete
updateOne(filter, update, options) প্রথম মিলে যাওয়া document বদলায়, updateMany সবগুলো। দ্বিতীয় argument-টি নতুন document নয়, বরং কী বদলাতে হবে তার নির্দেশ — update operator দিয়ে লেখা:
// $set — field বসানো/বদলানো (না থাকলে যোগ হয়)
db.orders.updateOne({ _id: 1006 }, { $set: { status: "shipped", shipped_at: new Date() } })
// { acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
// $inc — শর্তসহ স্টক কমানো: একটি document-এর ভেতরে এটি atomic
db.products.updateOne({ _id: 101, stock: { $gte: 1 } }, { $inc: { stock: -1 } })
// matchedCount: 0 এলে স্টক শেষ — অর্ডার নেবেন না
// $push আর $pull — array-তে যোগ ও বাদ; total-ও একসাথে বদলাতে হবে
db.orders.updateOne({ _id: 1006 }, {
$push: { items: { product_id: 102, name: "Headphone", qty: 1, unit_price: NumberDecimal("1200") } },
$inc: { total: NumberDecimal("1200") }
})
db.orders.updateOne({ _id: 1006 }, {
$pull: { items: { product_id: 102 } },
$inc: { total: NumberDecimal("-1200") }
})
// updateMany — শর্তে মেলা সব document
db.products.updateMany({ stock: 0 }, { $set: { available: false } })অপারেটর ছাড়া পুরো document দিলে কী হয়? mongosh-এর updateOne সেটি প্রত্যাখ্যান করে (error: update document-এ atomic operator লাগবে)। পুরো document বদলাতে আলাদা মেথড replaceOne — আর সেটি _id বাদে বাকি সব field মুছে নতুনগুলো বসায়। পুরোনো update() মেথড অপারেটর ছাড়া দিলে নীরবে পুরো document বদলে ফেলত; অনেক পুরোনো কোড আর AI-এর উত্তরে এটি এখনো দেখা যায়।
Upsert (update + insert) — মিললে বদলানো, না মিললে নতুন বানানো। দিনভিত্তিক বিক্রির সারাংশের জন্য আদর্শ। আর মোছা — deleteOne আর deleteMany:
db.daily_sales.updateOne(
{ _id: "2025-09-01" },
{ $inc: { orders: 1, revenue: NumberDecimal("17400") } },
{ upsert: true }
)
// প্রথমবার: upsertedCount: 1 → { _id: "2025-09-01", orders: 1, revenue: 17400 }
// পরেরবার: matchedCount: 1 → orders আর revenue বাড়ে
db.orders.deleteOne({ _id: 1004 }) // { acknowledged: true, deletedCount: 1 }
db.orders.deleteMany({ status: "cancelled", order_date: { $lt: ISODate("2025-01-01") } })
db.orders.deleteMany({}) // সাবধান: সব document মুছে যায়!SQLite-এও JSON document এভাবে বদলানো যায় — json_set ≈ $set, path-এ [#] দিয়ে json_insert ≈ $push. তবে পার্থক্য আছে: SQLite প্রতিবার পুরো লেখাটি নতুন করে লেখে, আর টাইপ রক্ষার দায়িত্ব পুরোপুরি আপনার:
-- $set
UPDATE order_docs SET doc = json_set(doc, '$.status', 'shipped') WHERE id = 1006;
-- $inc (প্রথম item-এর qty)
UPDATE order_docs
SET doc = json_set(doc, '$.items[0].qty', (doc ->> '$.items[0].qty') + 1)
WHERE id = 1006;
-- $push
UPDATE order_docs
SET doc = json_insert(doc, '$.items[#]',
json_object('product_id', 102, 'qty', 1, 'unit_price', 1200))
WHERE id = 1006;
SELECT json_pretty(doc) AS doc FROM order_docs WHERE id = 1006;✓ UPDATE — ১টি সারি বদলেছে (তিনবার)
{
"_id": 1006,
"customer": { "_id": 6, "name": "Farhana Akter", "city": "Sylhet" },
"order_date": "2025-09-20",
"status": "shipped",
"items": [
{ "product_id": 110, "qty": 2, "unit_price": 3500.0 },
{ "product_id": 102, "qty": 1, "unit_price": 1200 }
]
}
তিনটি UPDATE-এ status, Smart Watch-এর qty (1 → 2) আর নতুন Headphone item — সব ঠিক। কিন্তু খেয়াল করুন unit_price: পুরোনো item-এ 3500.0 (REAL থেকে আসা), নতুনটিতে 1200 (integer). একই document-এর একই field, দুই টাইপ — কেউ বাধা দেয়নি। MongoDB-তেও ঠিক এভাবেই টাইপ মিশে যায়, যদি এক জায়গায় NumberDecimal("1200") আর আরেক জায়গায় শুধু 1200 লেখা হয়। এর প্রতিকার schema validation (অংশ ৮)।
৫ · db-shop-কে document-এ সাজানো — embed নাকি reference
Relational ডিজাইনে প্রশ্ন ছিল “ডেটা কী?” — তারপর normalization (পর্ব ৩৬)। Document ডিজাইনে প্রথম প্রশ্ন “অ্যাপ কোন প্রশ্নগুলো সবচেয়ে বেশি করে?” দুটি মূল হাতিয়ার: embed — সম্পর্কিত তথ্য একই document-এর ভেতরে (embedded document বা array); আর reference — শুধু অন্য document-এর _id রাখা (document reference), দরকারে আলাদা পড়া বা $lookup. db-shop-এর অর্ডার দিয়ে ছয়টি প্রশ্নে দুই পথ মিলিয়ে দেখুন:
// Embed: item-এর নাম আর দাম অর্ডারেই কপি
{ _id: 1001, customer: { _id: 1, name: 'Rahim Uddin' },
items: [ { product_id: 101, name: 'Mobile Phone',
qty: 1, unit_price: Decimal128('15000') }, … ] }
// Reference: শুধু আইডি
{ _id: 1001, customer_id: 1,
items: [ { product_id: 101, qty: 1 }, … ] }
// দাম? নাম? → products collection থেকে আনতে হবে
| প্রশ্ন | |
| Embed | |
| Reference | |
| বিজয়ী |
- অর্ডারের পাতা দেখানো। অ্যাপের সবচেয়ে ঘন ঘন প্রশ্ন: “অর্ডার 1001 দেখাও।” Embed হলে একটি document পড়লেই সব — একবার ডিস্ক বা নেটওয়ার্কে যাওয়া। Reference হলে অর্ডার পড়ার পর পণ্যগুলোর নাম-দাম আনতে আরেকটি কোয়েরি বা
$lookup; অ্যাপ থেকে প্রতিটি পণ্যের জন্য আলাদা কোয়েরি করলে তা পর্ব ৪৯-এর N+1 query সমস্যা। - পরের মাসে ফোনের দাম কমল। এটি সবচেয়ে গুরুত্বপূর্ণ ধাপ। ১ সেপ্টেম্বরের চালানে Rahim ১৫,০০০ টাকা দিয়েছেন — সেটি ইতিহাস, বদলাবে না। Embed নকশায়
unit_priceঅর্ডারেই কপি করা, তাই ঠিক। শুধু reference রাখলে পুরোনো অর্ডারের মোট অঙ্ক নিজে থেকে বদলে যাবে — হিসাবের খাতায় বড় ভুল। db-shop-এরorder_items.unit_price-ও ঠিক এই কারণেই আছে। - পণ্যের নামের বানান ঠিক করা। এখানে উত্তর ব্যবসার নিয়মের ওপর নির্ভর করে। চালান একটি স্ন্যাপশট — কেনার দিন পণ্যের নাম যা ছিল, তা থাকাই অনেক সময় ঠিক। কিন্তু সব জায়গায় নতুন নাম চাইলে কপিগুলো
updateManyদিয়ে বদলাতে হবে। কপি রাখা মানে এই সিদ্ধান্ত জেনে-বুঝে নেওয়া — পর্ব ৩৮-এর denormalization-এর মতোই। - স্টক কমানো। স্টক একটি জীবন্ত সংখ্যা, যার একটিই সত্য উৎস থাকতে হবে (source of truth). একে প্রতিটি অর্ডারে কপি করা অর্থহীন। তাই পণ্য থাকবে নিজের
productscollection-এ, আর অর্ডারে শুধুproduct_id— reference. স্টক কমানো হবেproducts-এ (অংশ ৪-এর শর্তসহ$inc). - গ্রাহকের সব অর্ডার customer document-এর ভেতরে? প্রলোভন জাগে — “একজন গ্রাহক, তার সব অর্ডার।” কিন্তু অর্ডারের সংখ্যা সময়ের সাথে সীমাহীন বাড়ে; document-এর সর্বোচ্চ আকার ১৬ MB, আর প্রতিটি নতুন অর্ডারে বিশাল document বারবার লেখা হবে। নিয়ম: সীমাহীন বাড়ে এমন array embed করবেন না। অর্ডার থাকবে আলাদা collection-এ, ভেতরে
customer._id. - চূড়ান্ত নকশা — দুটি মিলিয়ে।
orders: item-গুলো embed (নাম আরunit_priceকপিসহ), পণ্যে reference (product_id), গ্রাহকের_id-এর সাথে নাম আর শহরের কপি।customers: ঠিকানাগুলো embed (সংখ্যায় সীমিত, সবসময় গ্রাহকের সাথে পড়া হয়)।products: আলাদা collection, ক্যাটাগরির নাম ভেতরে। সারকথা: যা একসাথে পড়া হয় আর একসাথে বদলায়, তা embed; যা আলাদা বদলায় বা সীমাহীন বাড়ে, তা reference.
কপি রাখার দুই দিক নিজে চালিয়ে দেখুন। প্রথম কোয়েরি দেখায়, কোন অর্ডারে দেওয়া দাম আজকের দামের থেকে আলাদা — কপি না থাকলে এই ইতিহাস হারিয়ে যেত। তারপর গ্রাহকের নাম বদলানো হলো — কপিগুলো পুরোনোই থেকে গেল:
-- কপি করা দাম বনাম আজকের দাম
SELECT d.id, j.value ->> 'product_id' AS product_id,
j.value ->> 'unit_price' AS paid, p.price AS price_now
FROM order_docs d, json_each(d.doc, '$.items') j
JOIN products p ON p.product_id = j.value ->> 'product_id'
WHERE j.value ->> 'unit_price' <> p.price;
-- আসল সারিতে নাম বদলাল; document-এর কপি?
UPDATE customers SET name = 'Rahim Uddin Khan' WHERE customer_id = 1;
SELECT id, doc ->> '$.customer.name' AS name_in_order FROM order_docs
WHERE doc ->> '$.customer._id' = 1;id │ product_id │ paid │ price_now 1007 │ 102 │ 1150 │ 1200 1009 │ 101 │ 14500 │ 15000 ✓ UPDATE — ১টি সারি বদলেছে id │ name_in_order 1001 │ Rahim Uddin 1003 │ Rahim Uddin 1009 │ Rahim Uddin
দুটি অর্ডারে ছাড়ে কেনা হয়েছিল — Headphone ১,১৫০-এ আর Mobile Phone ১৪,৫০০-এ। শুধু reference রাখলে এই তথ্য কোথাও থাকত না। দ্বিতীয় অংশে customers-এ নাম বদলানোর পরও তিনটি অর্ডারে পুরোনো নাম — এটিই কপির দাম। চালানের জন্য এটি হয়তো কাঙ্ক্ষিত; কিন্তু “গ্রাহকের বর্তমান নাম” দেখাতে হলে আসল উৎস থেকে পড়তে হবে, বা কপিগুলো আলাদাভাবে হালনাগাদ করতে হবে। MongoDB-তে কোনো foreign key বা trigger এটি নিজে থেকে করবে না।
পাঁচটি প্রচলিত schema pattern
| Pattern | কী করে | db-shop-এ উদাহরণ |
|---|---|---|
| Embedding | একসাথে পড়া তথ্য একই document-এ | অর্ডারের item; গ্রাহকের ঠিকানা |
| Referencing | শুধু _id রাখা; বড়, আলাদা বদলায় বা ভাগ করা তথ্যের জন্য | item-এ product_id; রিভিউতে product_id |
| Extended reference | reference-এর সাথে প্রায়ই লাগে এমন কয়েকটি field-এর কপি | অর্ডারে customer: { _id, name, city } |
| Bucket | সময়ভিত্তিক বহু ছোট ঘটনা এক document-এ গুচ্ছ করা (যেমন ঘণ্টায় একটি) | গুদামের তাপমাত্রা সেন্সর: প্রতি ঘণ্টার ৬০টি মাপ একটি document-এ |
| Computed | বারবার লাগে এমন হিসাব লেখার সময় একবার করে রেখে দেওয়া | অর্ডারের total; daily_sales-এর upsert |
Bucket pattern এতটাই প্রচলিত যে MongoDB ৫.০ থেকে time series collection নামে এটি নিজেই দেয় — আপনি একটি একটি করে মাপ ঢোকান, ভেতরে সার্ভার সেগুলো গুচ্ছ করে আর সংকুচিত রাখে। Computed pattern-এর দাম হলো দায়িত্ব: অংশ ৪-এ $push-এর সাথে $inc: { total } ভুলে গেলে total আর item-এর যোগফল আর মিলবে না।
৬ · Aggregation pipeline — SQL-এর পাশে পাশে
find document বাছে আর সাজায়, কিন্তু যোগফল, গড় বা গ্রুপ করা হিসাব পারে না। তার জন্য aggregation pipeline: stage-এর একটি তালিকা, যেখানে প্রতিটি stage আগেরটির বের করা document-গুলো নেয়, কিছু করে, পরেরটিকে দেয় — কারখানার কনভেয়ার বেল্টের মতো। প্রধান stage: $match (বাছাই), $project (field বাছা বা নতুন field), $unwind (array-র প্রতিটি উপাদানকে আলাদা document), $lookup (অন্য collection থেকে মেলানো — left join-এর মতো), $group, $sort, $limit.
প্রশ্ন ১: delivered আর shipped অর্ডারে ক্যাটাগরিভিত্তিক বিক্রি কত? প্রথম ট্যাবে MongoDB, দ্বিতীয়টিতে SQL:
db.orders.aggregate([
{ $match: { status: { $in: ["delivered", "shipped"] } } },
{ $unwind: "$items" },
{ $lookup: { from: "products", localField: "items.product_id",
foreignField: "_id", as: "product" } },
{ $unwind: "$product" },
{ $group: { _id: "$product.category",
revenue: { $sum: { $multiply: ["$items.qty", "$items.unit_price"] } } } },
{ $sort: { revenue: -1 } },
{ $project: { _id: 0, category: "$_id", revenue: 1 } }
])SELECT c.name AS category, SUM(i.quantity * i.unit_price) AS revenue
FROM orders o -- db.orders
JOIN order_items i ON i.order_id = o.order_id -- $unwind "$items"
JOIN products p ON p.product_id = i.product_id -- $lookup + $unwind
JOIN categories c ON c.category_id = p.category_id
WHERE o.status IN ('delivered', 'shipped') -- $match
GROUP BY c.name -- $group
ORDER BY revenue DESC; -- $sortদশটি অর্ডার এই pipeline দিয়ে কীভাবে বয়ে যায়, ধাপে ধাপে দেখুন:
| stage | |
| ঢুকল | |
| বেরোল | |
| SQL-এ |
- শুরু — collection-এর ১০টি অর্ডার। প্রতিটি অর্ডারের ভেতরে item-এর array. এখনো কিছু বাদ পড়েনি; pipeline-এর প্রথম stage এই document-গুলোই পাবে।
$match— ১০ থেকে ৭। cancelled (1004) আর দুটি pending (1006, 1009) বাদ।$matchযত আগে, তত ভালো: পরের stage-গুলো কম document নিয়ে কাজ করে, আর pipeline-এর শুরুতে থাকলে এটি index-ও ব্যবহার করতে পারে — WHERE-এর মতোই।$unwind— ৭ থেকে ১২। প্রতিটি অর্ডার তার প্রতিটি item-এর জন্য একটি করে document হয়ে গেল;itemsএখন array নয়, একটি object. 1001-এর দুটি item থেকে দুটি document. সংখ্যা বাড়ল — ঠিক যেমনorders JOIN order_items-এ সারি বাড়ে।$lookup— অন্য collection থেকে পণ্য আনা। প্রতিটি document-এরitems.product_idদিয়েproducts-এর_idমেলানো হলো, ফল একটি arrayproduct-এ; পরের$unwindসেটিকে একক object বানাল। ১২টি ঢুকল, ১২টি বেরোল — প্রতিটি item-এর পণ্য পাওয়া গেছে। (মিল না পেলেproductহতো ফাঁকা array, আর সাধারণ$unwindসেই document ফেলে দিত — INNER JOIN-এর মতো।)$group— ১২ থেকে ৪।_id: "$product.category"মানে GROUP BY category. প্রতিটি গ্রুপে$sumজমা করল qty × unit_price: Electronics = ১৫,০০০ + ২,৪০০ + ১,১৫০; Books = ৬৫০ + ১,১০০ + ৬৫০ + ৫৫০; Grocery = ১,২৬০ + ৫২০ + ২,১০০ + ১,০৪০; Clothing = ৪,৪০০.$sortআর$project— চূড়ান্ত ফল। বিক্রি অনুযায়ী বড় থেকে ছোট: Electronics ১৮,৫৫০, Grocery ৪,৯২০, Clothing ৪,৪০০, Books ২,৯৫০.$project_id-এর নাম বদলেcategoryকরল। SQL এডিটরের ফলের সাথে মিলিয়ে নিন — একই সংখ্যা।
এবার SQL-এ চালিয়ে দেখুন — প্রথমে সাধারণ টেবিলে, তারপর document-এর ওপর। দ্বিতীয়টিতে json_each ঠিক $unwind-এর কাজ করছে:
SELECT c.name AS category,
SUM(i.quantity * i.unit_price) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.order_id
JOIN products p ON p.product_id = i.product_id
JOIN categories c ON c.category_id = p.category_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY c.name
ORDER BY revenue DESC;category │ revenue Electronics │ 18550 Grocery │ 4920 Clothing │ 4400 Books │ 2950
stepper-এর শেষ ধাপের সাথে হুবহু মিল। লক্ষ করুন, SQL-এ সব একটি ঘোষণামূলক (declarative) বাক্যে — কোন ক্রমে JOIN হবে, তা optimizer ঠিক করে (পর্ব ৪৩)। MongoDB-র pipeline-এ ধাপের ক্রম আপনি লেখেন; সার্ভার কিছু পুনর্বিন্যাস করে (যেমন $match আগে টেনে আনা), তবে মূল নকশা আপনার হাতে।
-- document-এর ওপর: json_each = $unwind, JOIN products = $lookup
SELECT c.name AS category,
SUM((j.value ->> 'qty') * (j.value ->> 'unit_price')) AS revenue,
COUNT(*) AS line_items
FROM order_docs d, json_each(d.doc, '$.items') j
JOIN products p ON p.product_id = j.value ->> 'product_id'
JOIN categories c ON c.category_id = p.category_id
WHERE d.doc ->> '$.status' IN ('delivered', 'shipped')
GROUP BY c.name
ORDER BY revenue DESC;category │ revenue │ line_items Electronics │ 18550 │ 3 Grocery │ 4920 │ 4 Clothing │ 4400 │ 1 Books │ 2950 │ 4
একই ফল। line_items-এর যোগফল ৩ + ৪ + ১ + ৪ = ১২ — stepper-এ $unwind-এর পর ঠিক ১২টি document ছিল। পাশাপাশি দেখলে বোঝা যায়, MongoDB আর SQL একই কাজের দুই ভাষা: একটিতে ধাপের তালিকা, অন্যটিতে একটি বাক্য।
pipeline-এর ভেতরের যুক্তিটা আরও স্পষ্ট হয়, যখন প্রতিটি stage-কে একটি সাধারণ ফাংশন হিসেবে লেখা হয় — document-এর তালিকা ঢোকে, document-এর তালিকা বেরোয়। নিচের Python প্রোগ্রামটি (শুধু standard library) db-shop-এর দশটি অর্ডারের ওপর একই pipeline চালায়:
from collections import defaultdict
category = {**dict.fromkeys([101, 102, 103, 110], "Electronics"), **dict.fromkeys([104, 105], "Books"),
**dict.fromkeys([106, 107], "Clothing"), **dict.fromkeys([108, 109], "Grocery")}
raw = [(1001, "delivered", [(101, 1, 15000), (102, 2, 1200)]), (1002, "delivered", [(104, 1, 650), (105, 2, 550)]),
(1003, "shipped", [(108, 3, 420), (109, 2, 260)]), (1004, "cancelled", [(107, 1, 4500)]),
(1005, "delivered", [(104, 1, 650), (106, 2, 2200)]), (1006, "pending", [(110, 1, 3500)]),
(1007, "delivered", [(102, 1, 1150), (108, 5, 420)]), (1008, "shipped", [(105, 1, 550)]),
(1009, "pending", [(101, 1, 14500), (110, 1, 3500)]), (1010, "delivered", [(109, 4, 260)])]
docs = [{"_id": o, "status": s, "items": [{"product_id": p, "qty": q, "unit_price": u} for p, q, u in it]} for o, s, it in raw]
def group_sum(docs): # {$group: {_id: "$category", revenue: {$sum: {$multiply: [qty, price]}}}}
acc = defaultdict(int)
for d in docs: acc[d["category"]] += d["items"]["qty"] * d["items"]["unit_price"]
return [{"_id": k, "revenue": v} for k, v in acc.items()]
pipeline = [("$match", lambda ds: [d for d in ds if d["status"] in ("delivered", "shipped")]),
("$unwind", lambda ds: [{**d, "items": x} for d in ds for x in d["items"]]),
("$lookup", lambda ds: [{**d, "category": category[d["items"]["product_id"]]} for d in ds]),
("$group", group_sum),
("$sort", lambda ds: sorted(ds, key=lambda d: -d["revenue"]))]
print(f"input -> {len(docs):2} docs")
for name, stage in pipeline:
docs = stage(docs); print(f"{name:8} -> {len(docs):2} docs")
for d in docs: print(d)input -> 10 docs
$match -> 7 docs
$unwind -> 12 docs
$lookup -> 12 docs
$group -> 4 docs
$sort -> 4 docs
{'_id': 'Electronics', 'revenue': 18550}
{'_id': 'Grocery', 'revenue': 4920}
{'_id': 'Clothing', 'revenue': 4400}
{'_id': 'Books', 'revenue': 2950}
stage-ভিত্তিক গণনা — ১০, ৭, ১২, ১২, ৪, ৪ — stepper-এর সাথে মেলে। প্রতিটি stage একটি তালিকা থেকে নতুন তালিকা বানায়, আগেরটি বদলায় না; তাই stage-গুলো স্বাধীনভাবে পরীক্ষা করা যায় — pipeline ডিবাগ করার সবচেয়ে ভালো কৌশলও এটিই: শেষের stage-গুলো সরিয়ে প্রতিটি ধাপের ফল দেখা। আসল সার্ভার অবশ্য সব document একসাথে memory-তে রাখে না, stream করে; শুধু $group আর $sort-এর মতো blocking stage-কে সব ইনপুট দেখতে হয়, আর বড় ডেটায় তারা ডিস্কে নামতে পারে।
প্রশ্ন ২: সবচেয়ে বেশি কেনাকাটা করা তিন গ্রাহক। অর্ডারে total (computed) আর customer-এর কপি (extended reference) থাকায় MongoDB-তে $lookup-ই লাগে না:
db.orders.aggregate([
{ $match: { status: { $in: ["delivered", "shipped"] } } },
{ $group: { _id: "$customer._id", name: { $first: "$customer.name" },
spent: { $sum: "$total" }, orders: { $sum: 1 } } },
{ $sort: { spent: -1, _id: 1 } },
{ $limit: 3 }
])SELECT c.customer_id, c.name,
SUM(i.quantity * i.unit_price) AS spent,
COUNT(DISTINCT o.order_id) AS orders
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items i ON i.order_id = o.order_id
WHERE o.status IN ('delivered', 'shipped')
GROUP BY c.customer_id, c.name
ORDER BY spent DESC, c.customer_id
LIMIT 3;customer_id │ name │ spent │ orders 1 │ Rahim Uddin │ 19180 │ 2 4 │ Nusrat Jahan │ 5050 │ 1 2 │ Karima Begum │ 5000 │ 2
Rahim-এর দুটি অর্ডার (১৭,৪০০ + ১,৭৮০); তাঁর pending অর্ডার 1009 বাদ পড়েছে। SQL-এ COUNT(DISTINCT o.order_id) লাগল, কারণ item-এর সাথে JOIN-এ প্রতিটি অর্ডার একাধিকবার আসে (পর্ব ১৯-এর fan-out). MongoDB-র সংস্করণে item-গুলো অর্ডারের ভেতরে, তাই $sum: 1 সরাসরি অর্ডার গোনে। একটি সতর্কতা: $first: "$customer.name" দেয় কপি করা নাম — অংশ ৫-এর মতো নাম বদলে থাকলে পুরোনোটিই আসবে।
৭ · Index, ESR নিয়ম আর explain()
Index ছাড়া প্রতিটি find পুরো collection পড়ে — explain-এ এর নাম COLLSCAN, SQL-এর full table scan. MongoDB-র সাধারণ index একটি B-tree (পর্ব ৪০), আর _id-এ একটি অনন্য index সবসময় নিজে থেকে থাকে। প্রধান ধরনগুলো:
db.orders.createIndex({ status: 1 }) // single field
db.orders.createIndex({ status: 1, order_date: -1, total: 1 }) // compound (ESR, নিচে)
db.orders.createIndex({ "items.product_id": 1 }) // multikey: array-র প্রতিটি মান
db.products.createIndex({ name: "text", description: "text" }) // text: প্রতি collection-এ একটি
db.sessions.createIndex({ last_seen: 1 }, { expireAfterSeconds: 1800 }) // TTL: ৩০ মিনিট পর মুছবে
db.customers.createIndex({ phone: 1 },
{ unique: true, partialFilterExpression: { phone: { $type: "string" } } })- Compound (composite index) — একাধিক field, ক্রম গুরুত্বপূর্ণ; পর্ব ৪১-এর leftmost prefix নিয়ম এখানেও খাটে।
- Multikey — array field-এ index দিলে MongoDB নিজেই প্রতিটি উপাদানের জন্য একটি key রাখে; “Headphone আছে এমন সব অর্ডার” দ্রুত হয়। একটি compound index-এ প্রতি document-এ সর্বোচ্চ একটি field array হতে পারে।
- Text — শব্দভিত্তিক খোঁজ (
$text), পর্ব ৩০-এর inverted index-এর ধারণা। Atlas-এ আরও শক্তিশালী Atlas Search আলাদা সেবা। - TTL — Date field-এ, নির্দিষ্ট সময় পর document নিজে থেকে মোছে; লগইন session বা OTP-র জন্য উপযোগী। একটি পটভূমির কাজ প্রায় প্রতি ৬০ সেকেন্ডে চলে, তাই মোছা ঠিক সেই মুহূর্তে নয়।
- Unique + partial — MongoDB-র unique index-এ null বা না-থাকা field-ও একটি মান ধরা হয়, তাই ফোনহীন দুজন গ্রাহক (Tanvir-এর মতো) রাখা যেত না। SQLite, PostgreSQL বা MySQL-এর UNIQUE একাধিক NULL মেনে নেয় (পর্ব ১৩; SQL Server ব্যতিক্রম) — এখানে নয়।
partialFilterExpressionশুধু string ফোনগুলোকে index-এ রাখে।
ESR নিয়ম — compound index-এ field-এর ক্রম
প্রশ্ন: “delivered অর্ডার, total অন্তত ২,০০০, নতুন তারিখ আগে।” MongoDB-র নিজস্ব নির্দেশিকা ESR rule: compound index-এ আগে Equality-র field (=), তারপর Sort-এর field, শেষে Range-এর field ($gt, $lt, $in-এর অনেক ক্ষেত্র)। দশটি অর্ডারে চারটি বিকল্প মিলিয়ে দেখুন:
db.orders.find({ status: "delivered", total: { $gte: 2000 } }).sort({ order_date: -1 })
// উত্তর: 1007 (2025-10-01), 1005 (2025-09-15), 1001 (2025-09-01)| index | |
| plan | |
| key পড়া | |
| document পড়া | |
| memory-তে sort? |
- Index নেই — COLLSCAN. দশটি document-এর প্রতিটি খুলে দেখা হলো; তিনটি মিলল। তারপর সেগুলো memory-তে তারিখ অনুযায়ী সাজানো (একটি blocking SORT stage). দশ লাখ অর্ডারে দশ লাখ document পড়া — আর বড় sort memory-র সীমা ছাড়ালে ডিস্কে নামে।
- শুধু
{ total: 1 }. Range-এর সীমা total ≥ 2000 থেকে ছয়টি key, ছয়টি document খুলে status দেখা — pending আর cancelled মিলিয়ে তিনটি বৃথা। তারপর আবার SORT. Index কাজে লাগল, কিন্তু সবচেয়ে বেছে নেওয়া শর্তটি (status) index-এ নেই। { status: 1, total: 1, order_date: -1 }— E, R, S. মাত্র তিনটি key, তিনটি document — সবচেয়ে কম পড়া! কিন্তু key-গুলো total-এর ক্রমে এল (3250, 5050, 17400), তারিখের ক্রমে নয়; range-এর পরে রাখা sort field ক্রম দিতে পারে না। তাই memory-তে SORT থেকেই গেল।{ order_date: -1, status: 1, total: 1 }— S, E, R. Index নিজেই তারিখের ক্রমে, তাই SORT লাগল না। কিন্তু status প্রথম field নয়, তাই delivered অংশে সরাসরি লাফ দেওয়া গেল না — দশটি key-ই পড়তে হলো। key-তে status আর total আছে বলে শুধু মিলে যাওয়া তিনটি document খোলা হলো।{ status: 1, order_date: -1, total: 1 }— E, S, R. status = delivered দিয়ে index-এর একটি অংশে লাফ (৫টি key), সেই অংশ আগে থেকেই তারিখের উল্টো ক্রমে সাজানো, আর total ≥ 2000 key দেখেই যাচাই (1010 আর 1002 বাদ, document না খুলে)। তিনটি document, কোনো SORT নেই। আর.limit(10)থাকলে প্রথম দশটি মিলেই থামা যায় — memory-তে sort হলে সব মিল আগে খুঁজতে হতো।- ESR — কেন এই ক্রম। Equality আগে, কারণ সমতা index-এর একটি সংকীর্ণ, নিজেই সাজানো অংশ বেছে দেয়। Sort তারপর, কারণ সেই অংশের ভেতরে ক্রম বিনা খরচে পাওয়া যায়। Range শেষে, কারণ range-এর পরের field-গুলো আর সাজানো ক্রমে থাকে না। ধাপ ৩-এ key কম পড়েছিল (৩ বনাম ৫) — ছোট ডেটায় সেটিও ভালো হতে পারে; নিয়মটি নির্দেশিকা, শেষ কথা
explain().
db.orders.find({ status: "delivered", total: { $gte: 2000 } })
.sort({ order_date: -1 })
.explain("executionStats")
// সংক্ষেপিত; গঠন সংস্করণভেদে আলাদা, আর সংখ্যাগুলো এই ছোট উদাহরণের আনুমানিক মান
// winningPlan: { stage: "FETCH", inputStage: { stage: "IXSCAN",
// indexName: "status_1_order_date_-1_total_1", ... } }
// executionStats: { nReturned: 3, totalKeysExamined: 5, totalDocsExamined: 3, ... }explain()-এ তিনটি জিনিস দেখুন: plan-এ COLLSCAN নাকি IXSCAN; আলাদা SORT stage আছে কি না; আর nReturned, totalKeysExamined, totalDocsExamined-এর অনুপাত — আদর্শ ক্ষেত্রে তিনটি কাছাকাছি। ফেরত ৩টি কিন্তু পড়া ১০,০০০টি হলে index ঠিক নেই। আর প্রতিটি index লেখাকে ধীর করে আর জায়গা নেয়, তাই পর্ব ৪১-এর মতো এখানেও অপ্রয়োজনীয় index বাদ দিন।
৮ · Schema validation আর transaction
$jsonSchema — “schema-less” মানে নিয়মহীন নয়
MongoDB-কে প্রায়ই “schema-less” বলা হয়, কিন্তু আসলে schema থাকে অ্যাপের কোডে — পর্ব ৪-এর schema-on-read. অংশ ৪-এ দেখেছেন, এতে টাইপ সহজেই মিশে যায়। তাই collection-এ schema validation দেওয়া যায় — JSON Schema-এর একটি রূপ দিয়ে, সাথে BSON টাইপের জন্য bsonType:
db.createCollection("orders", {
validator: { $jsonSchema: {
bsonType: "object",
required: ["customer", "order_date", "status", "items", "total"],
properties: {
order_date: { bsonType: "date" },
status: { enum: ["pending", "shipped", "delivered", "cancelled"] },
total: { bsonType: "decimal", minimum: 0 },
items: { bsonType: "array", minItems: 1, items: {
bsonType: "object", required: ["product_id", "qty", "unit_price"],
properties: {
qty: { bsonType: "int", minimum: 1 },
unit_price: { bsonType: "decimal" } // সাধারণ 1200 (int বা double) চলবে না
} } }
} } },
validationLevel: "strict", // প্রতিটি insert আর update যাচাই
validationAction: "error" // ভুল হলে লেখা প্রত্যাখ্যান ("warn" = শুধু লগ)
})
// নিয়ম ভাঙলে: MongoServerError: Document failed validationএটি SQL-এর CHECK আর NOT NULL-এর কাছাকাছি, কিন্তু foreign key নয় — product_id-এর পণ্যটি আদৌ আছে কি না, validator জানে না। একটি সূক্ষ্মতা: সাধারণ সংখ্যা (যেমন qty: 2) int হিসেবে যাবে নাকি double হিসেবে, তা শেল আর ড্রাইভারভেদে আলাদা (পুরোনো mongo শেল সব সংখ্যা double পাঠাত)। নিশ্চিত হতে NumberInt(2) লিখুন, অথবা নিয়ম শিথিল করে bsonType: ["int", "long", "double"] দিন। Validation এই অমিলগুলো শুরুতেই ধরে।
Transaction — কখন লাগে
MongoDB-তে একটি document-এর ভেতরে যেকোনো update atomic — $push আর $inc: { total } একসাথে হয়, নয়তো কোনোটিই না। Embed নকশার এটি বড় সুবিধা: অর্ডার আর তার item আলাদা টেবিলে না থাকায় অনেক ক্ষেত্রে transaction-ই লাগে না। কিন্তু একাধিক document বা collection একসাথে বদলাতে হলে — যেমন পণ্যের স্টক কমানো আর অর্ডার তৈরি — লাগবে multi-document transaction: replica set-এ ৪.০ (২০১৮) থেকে, sharded cluster-এ ৪.২ থেকে। Transaction চলে একটি session-এর ভেতরে:
const session = db.getMongo().startSession();
const shop = session.getDatabase("shop");
session.startTransaction({ readConcern: { level: "snapshot" },
writeConcern: { w: "majority" } });
try {
const r = shop.products.updateOne({ _id: 110, stock: { $gte: 1 } },
{ $inc: { stock: -1 } });
if (r.matchedCount === 0) throw new Error("out of stock");
shop.orders.insertOne({ _id: 1011, customer: { _id: 5, name: "Tanvir Ahmed", city: "Khulna" },
order_date: new Date(), status: "pending",
items: [ { product_id: 110, name: "Smart Watch", qty: NumberInt(1), unit_price: NumberDecimal("3500") } ],
total: NumberDecimal("3500") });
session.commitTransaction(); // দুটি লেখা একসাথে দৃশ্যমান
} catch (e) {
session.abortTransaction(); // কোনোটিই থাকবে না
throw e;
} finally {
session.endSession();
}এখানে পর্ব ৪৪-এর ACID-এর সব কটি গুণ পাওয়া যায় — snapshot read concern আর majority write concern মিলে isolation আর durability. বাস্তবে Node.js বা Python-এর ড্রাইভারে withTransaction() ব্যবহার করা ভালো: এটি সাময়িক error-এ (যেমন আরেক transaction-এর সাথে write conflict) নিজে থেকে আবার চেষ্টা করে। সীমা মনে রাখুন: ডিফল্টে একটি transaction ৬০ সেকেন্ডের বেশি চলতে পারে না, আর বহু document ছোঁয়া দীর্ঘ transaction সার্ভারের ওপর চাপ বাড়ায়। নিয়মটা উল্টো দিক থেকে ভাবুন — প্রায় প্রতিটি কাজে transaction লাগলে নকশাটি হয়তো আসলে relational.
৯ · Replica set, sharding, concern, নিরাপত্তা — আর কখন MongoDB
Replica set: একই ডেটার কয়েকটি কপি রাখা সার্ভারের দল — একটি primary (সব লেখা এখানে), বাকিরা secondary, যারা primary-র oplog (সব পরিবর্তনের একটি বিশেষ collection) পড়ে নিজেদের কপি হালনাগাদ রাখে। Primary বন্ধ হলে বাকিরা ভোট দিয়ে নতুন primary বেছে নেয় — সাধারণত কয়েক সেকেন্ড থেকে দশ-বারো সেকেন্ডের মধ্যে, আর ড্রাইভার নিজে নতুন primary খুঁজে নেয়। ভোটে সংখ্যাগরিষ্ঠতা লাগে বলে সদস্য সাধারণত বিজোড়, অন্তত তিন। এটি পর্ব ৫২-এর replication আর failover-এর MongoDB রূপ।
Sharding: ডেটা একটি সার্ভারের সামর্থ্য ছাড়ালে collection-কে shard key ধরে কয়েকটি shard-এ (প্রতিটি নিজেই একটি replica set) ভাগ করা হয়। অ্যাপ কথা বলে mongos router-এর সাথে, যে config server থেকে জানে কোন key-র পরিসর কোন shard-এ। Shard key-ই সব ঠিক করে: customer._id-এর মতো key হলে এক গ্রাহকের প্রশ্ন একটি shard-এই যায়; key-টি filter-এ না থাকলে প্রশ্ন সব shard-এ ছড়ায় (scatter-gather). একটানা বাড়তে থাকা key (যেমন শুধু order_date) হলে সব নতুন লেখা এক shard-এ জমে — পর্ব ৫৪-এর hot spot. ৫.০ থেকে shard key বদলানো (resharding) সম্ভব, কিন্তু ভারী কাজ — শুরুতেই ভেবে নিন।
Write concern আর read concern: লেখা কতটা নিরাপদ হলে “হয়েছে” বলা হবে — w: 1 (শুধু primary পেয়েছে) বনাম w: "majority" (সংখ্যাগরিষ্ঠ সদস্য পেয়েছে, তাই failover-এও হারাবে না; ৫.০ থেকে বেশিরভাগ ক্ষেত্রে এটিই ডিফল্ট)। Read concern বলে কোন ডেটা পড়া যাবে — "local" (ডিফল্ট, হয়তো পরে rollback হতে পারে এমনও), "majority" (শুধু সংখ্যাগরিষ্ঠে নিশ্চিত), "snapshot" (transaction-এ)। আর read preference দিয়ে secondary থেকে পড়লে ডেটা একটু পুরোনো হতে পারে — eventual consistency, পর্ব ৫৪-এর CAP আলোচনার বাস্তব রূপ।
নিরাপত্তা — খোলা দরজা বন্ধ রাখুন
বহু বছর ধরে ইন্টারনেটে খোলা, password ছাড়া চলা MongoDB সার্ভার থেকে বিপুল ব্যক্তিগত তথ্য ফাঁস হয়েছে, আর কিছু ক্ষেত্রে আক্রমণকারী ডেটা মুছে “মুক্তিপণ” দাবি করেছে বলে ব্যাপকভাবে জানা যায়। সমস্যাটা প্রায়ই সফটওয়্যারের দুর্বলতা নয় — কনফিগারেশন। ৩.৬ সংস্করণ থেকে সার্ভার ডিফল্টে শুধু localhost-এ শোনে, কিন্তু Docker-এ ভুল port, ক্লাউডে 0.0.0.0/0 বা authentication বন্ধ রাখা — এই একই ভুল এখনো ঘটে। ন্যূনতম তালিকা:
কখন MongoDB, কখন PostgreSQL + JSONB
MongoDB মানায়
ডেটা স্বভাবতই document-আকৃতির আর গভীর (পণ্য-ক্যাটালগ, কনটেন্ট, প্রোফাইল, IoT ইভেন্ট); প্রধান প্রশ্ন “এই আইডির পুরো জিনিসটি দাও”; কাঠামো ঘন ঘন বদলায়; শুরু থেকেই বহু সার্ভারে ছড়ানোর পরিকল্পনা; দল JavaScript/JSON-এ স্বচ্ছন্দ।
aggregate-কেন্দ্রিক নকশাPostgreSQL + JSONB সহজতর
ডেটার বড় অংশ কাঠামোবদ্ধ, সম্পর্ক অনেক (হিসাব, স্টক, বেতন); বহু টেবিল জুড়ে কঠোর নিয়ম আর foreign key দরকার; নানা রকম ad-hoc রিপোর্ট আর JOIN; নমনীয় অংশটুকু একটি jsonb কলামে (পর্ব ৩০) আর GIN index-এ সামলানো যায়।
এক ডেটাবেস, দুই মডেলdb-shop-এর মতো দোকানের জন্য — টাকা, স্টক আর বহু সম্পর্ক — PostgreSQL + JSONB প্রায়ই সহজ ও নিরাপদ বাছাই। কিন্তু একই দোকানের পণ্য-ক্যাটালগ আর রিভিউ, বা একটি কনটেন্ট-অ্যাপের পুরো ডেটা MongoDB-তে স্বাভাবিকভাবে বসে। প্রশ্নটা “কোন পণ্য সেরা” নয়, “আমার ডেটার আকার আর নিশ্চয়তার চাহিদা কী” (পর্ব ৫৬)।
আরও গভীরে: MongoDB-র ডিফল্ট storage engine WiredTiger (৩.২ থেকে ডিফল্ট) — ডেটা আর index দুটিই B-tree-তে, document-স্তরের concurrency, আর ভেতরে MVCC snapshot (পর্ব ৪৭)। প্রতিটি লেখা আগে journal-এ যায় (পর্ব ৪৮-এর write-ahead log-এর মতো), আর ডিফল্টে প্রায় প্রতি ৬০ সেকেন্ডে একটি checkpoint ডিস্কে স্থায়ী ছবি লেখে; ডেটা ব্লক ডিফল্টে snappy দিয়ে সংকুচিত। তাই “document ডেটাবেস” বাইরে থেকে নতুন দেখালেও ভেতরে এই কোর্সের পরিচিত কৌশলগুলোই কাজ করছে।
পরীক্ষা ও ইন্টারভিউতে — এমন প্রশ্ন প্রায়ই আসে:
- BSON আর JSON-এর পার্থক্য? — BSON binary, পার্স ছাড়াই পড়া যায়, আর Date, ObjectId, Decimal128, int/long-এর মতো বাড়তি টাইপ আছে।
- কখন embed, কখন reference? — একসাথে পড়া, সীমিত আকারের তথ্য embed; সীমাহীন বাড়ে, আলাদা বদলায় বা বহু জায়গায় ভাগ হয় এমন তথ্য reference.
- ESR rule কী? — compound index-এ Equality → Sort → Range ক্রমে field রাখা, যাতে কম key পড়া হয় আর memory-তে sort না লাগে।
- MongoDB কি ACID transaction দেয়? — একটি document-এ সবসময় atomic; ৪.০ থেকে replica set-এ আর ৪.২ থেকে sharded cluster-এ multi-document ACID transaction.
$elemMatchকেন লাগে? — array-র একই উপাদানে একাধিক শর্ত মেলাতে; dot notation-এ প্রতিটি শর্ত আলাদা উপাদানে মিলতে পারে।
১০ · AI-এর যুগে MongoDB
MongoDB-র কোড AI খুব সাবলীলভাবে লেখে — JSON-এর মতো সিনট্যাক্স ভাষা-মডেলের কাছে স্বাভাবিক। কিন্তু চলার মতো কোড আর সঠিক কোড এক নয়। SQL-এর মতোই এখানেও AI চলে কিন্তু ভুল ফল দেয় এমন কোয়েরি লেখে, field-এর নাম বানায়, আর index ও transaction-এর কথা ভুলে যায়:
| AI যা করে | আসলে সমস্যা |
|---|---|
| array-তে দুই শর্ত dot notation-এ লেখে | আলাদা উপাদানে মিলে যায় — অংশ ৩-এর 1007; $elemMatch লাগত |
| টাকা সাধারণ সংখ্যায় (double) রাখে | রাউন্ডিং ভুল; Decimal128 লাগবে |
পুরোনো insert(), update(), count() ব্যবহার করে | mongosh-এ deprecated; update অপারেটর ছাড়া পুরো document বদলে ফেলত |
| সব কিছু embed করে, গ্রাহকের ভেতরে সব অর্ডার | সীমাহীন array, ১৬ MB-র সীমা, প্রতি লেখায় বিশাল document |
নেই এমন field বা operator বানায় (customer_name, $contains) | নেই এমন field-এ filter নীরবে শূন্য ফল দেয়; ভুল operator error দেয় |
| compound index-এ field-এর ক্রম যেমন-তেমন, explain দেখায় না | memory-তে SORT, বহু key পড়া — ESR আর explain লাগে |
docker-compose-এ 27017:27017 খোলা, auth ছাড়া | সার্ভার পুরো নেটওয়ার্কে উন্মুক্ত — অংশ ৯-এর তালিকা |
প্রশ্ন করার সময় সংস্করণ, আসল document-এর নমুনা, প্রধান প্রশ্নগুলো আর যাচাইয়ের দাবি একসাথে দিন:
AI-এর লেখা MongoDB কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১MongoDB শুরুতে JOIN আর multi-document transaction ছাড়াই এল, পরে দুটিই যোগ করল। এটি কি ভুল স্বীকার, নাকি স্বাভাবিক বিবর্তন?
২০০৯ সালের প্রেক্ষাপট ভাবুন। ওয়েব কোম্পানিগুলো এমন মাপে বাড়ছিল যে একটি বড় relational সার্ভার যথেষ্ট হচ্ছিল না, আর বহু সার্ভারে relational ডেটাবেস ছড়ানো তখন কঠিন ও ব্যয়বহুল। JOIN আর বহু-সারির transaction বহু সার্ভারে করা সবচেয়ে কঠিন কাজ — দুটি ভিন্ন মেশিনের ডেটা মেলাতে নেটওয়ার্ক আর সমন্বয় লাগে। MongoDB-র মতো সিস্টেম জেনে-বুঝে এগুলো বাদ দিয়ে শুরু করেছিল: document-কে এমনভাবে নকশা করা যাতে এক document-ই একটি পূর্ণ একক হয়, তাহলে JOIN বা transaction কম লাগবে, আর document-গুলো স্বাধীনভাবে বিভিন্ন সার্ভারে রাখা যাবে।
কিন্তু বাস্তব অ্যাপ দেখাল, সব কাজ এক document-এ আঁটে না। স্টক কমানো আর অর্ডার তৈরি, বা দুটি অ্যাকাউন্টের মধ্যে টাকা সরানো — এসব কাজে ডেভেলপাররা অ্যাপের ভেতরে নিজেরাই “অর্ধেক transaction” লিখছিলেন, যা ভুলপ্রবণ। তাই ৩.২-তে $lookup, ৪.০ (২০১৮)-এ replica set-এ multi-document transaction, ৪.২-এ sharded cluster-এ। উল্টো দিকে relational জগৎও document-এর দিকে এসেছে: PostgreSQL-এর jsonb (২০১৪), MySQL-এর JSON টাইপ, আর NewSQL নামে পরিচিত distributed SQL সিস্টেম, যারা বহু সার্ভারেও SQL আর transaction দেয়।
তাই একে ভুল স্বীকার বলার চেয়ে অভিসারী বিবর্তন (convergent evolution) বলা বেশি যুক্তিসংগত — দুই দল দুই প্রান্ত থেকে শুরু করে মাঝের দিকে এসেছে। তবে মূল নকশার দর্শন রয়ে গেছে: MongoDB-তে transaction আছে, কিন্তু ভালো নকশায় তা ব্যতিক্রম; আর relational-এ JSON আছে, কিন্তু কাঠামোবদ্ধ কলামই মূল।
মূল উপলব্ধি: কোনো ডেটাবেসের “নেই” তালিকা প্রায়ই একটি সচেতন বিনিময়ের ফল, অজ্ঞতার নয়। প্রযুক্তি বাছার সময় দেখুন কোন বিনিময় আপনার কাজের সাথে মেলে — আর মনে রাখুন, সেই তালিকা সময়ের সাথে বদলায়।
প্র ০২ইন্টারনেটে খোলা MongoDB সার্ভার থেকে বহু তথ্য ফাঁসের ঘটনা থেকে ডেটাবেস নিরাপত্তার কী শিক্ষা পাওয়া যায়?
২০১০-এর দশকে নিরাপত্তা গবেষকরা বারবার দেখিয়েছেন, ইন্টারনেট স্ক্যান করলে হাজার হাজার MongoDB (এবং Elasticsearch, Redis-এর মতো অন্য) সার্ভার পাওয়া যায়, যেগুলো password ছাড়াই যে কেউ পড়তে পারে। ২০১৭ সালের শুরুতে এমন অনেক সার্ভারের ডেটা মুছে “ফেরত চাইলে মুক্তিপণ দাও” বার্তা রেখে যাওয়ার ঘটনা ব্যাপকভাবে প্রকাশিত হয়। এর পরেও বিভিন্ন সময়ে লাখ লাখ মানুষের ব্যক্তিগত তথ্য খোলা ডেটাবেস থেকে ফাঁসের খবর এসেছে — এর বড় অংশ কোনো জটিল হ্যাকিং নয়, শুধু ভুল কনফিগারেশন।
কেন এমন হলো? পুরোনো সংস্করণে সার্ভার ডিফল্টে সব নেটওয়ার্ক ঠিকানায় শুনত, আর authentication ডিফল্টে বন্ধ ছিল — নিজের কম্পিউটারে শেখার জন্য সুবিধাজনক, কিন্তু সেই একই সেটিং ক্লাউড সার্ভারে গেলে বিপজ্জনক। MongoDB ৩.৬ (২০১৭) থেকে ডিফল্টে শুধু localhost-এ শোনে, আর Atlas-এ authentication বন্ধ করা যায় না। কিন্তু Docker-এ -p 27017:27017, ক্লাউড firewall-এ “সব জায়গা থেকে অনুমতি” — এমন এক লাইনের ভুল আজও একই দরজা খুলে দেয়।
শিক্ষাগুলো সাধারণ, শুধু MongoDB-র নয়। এক, নিরাপদ ডিফল্ট (secure by default) সবচেয়ে কার্যকর নিরাপত্তা — ব্যবহারকারীকে কষ্ট করে অনিরাপদ হতে হবে, নিরাপদ হতে নয়। দুই, ডেটাবেস কখনো সরাসরি ইন্টারনেটে নয়; অ্যাপ সার্ভারের পেছনে, private network-এ। তিন, স্তরে স্তরে প্রতিরক্ষা (defense in depth) — network, authentication, least privilege, TLS, encryption, backup — একটি ব্যর্থ হলেও বাকিগুলো রক্ষা করে। চার, নিজের সার্ভার নিয়মিত বাইরে থেকে যাচাই করুন, ঠিক যেমন আক্রমণকারী করবে — অনুমতিপ্রাপ্ত পরিধির মধ্যে। বাংলাদেশেও ডিজিটাল সেবা বাড়ার সাথে ব্যক্তিগত তথ্য সুরক্ষার আলোচনা জোরালো হচ্ছে; প্রতিটি ডেভেলপারের দায়িত্ব এখানে সরাসরি।
মূল উপলব্ধি: বেশিরভাগ ডেটাবেস ফাঁস অত্যাধুনিক আক্রমণ নয়, খোলা রাখা দরজা। Deployment-এর আগে চেকলিস্ট মেনে চলা যেকোনো নতুন feature-এর চেয়ে বেশি গুরুত্বপূর্ণ।
প্র ০৩“MongoDB schema-less, তাই migration লাগে না” — এই দাবি কতটা সত্য?
দাবিটির অর্ধেক সত্য, অর্ধেক বিপজ্জনক। সত্য অংশ: নতুন field যোগ করতে ALTER TABLE লাগে না, collection লক হয় না, পুরোনো document-গুলো যেমন আছে থাকে। বড় relational টেবিলে কলাম যোগ বা টাইপ বদল একসময় ঘণ্টার পর ঘণ্টা লক রাখত (পর্ব ২৩-এর online schema change সেই সমস্যারই জবাব) — document ডেটাবেসের জনপ্রিয়তার একটি কারণ এই স্বাধীনতা।
বিপজ্জনক অংশ: schema হারিয়ে যায় না, জায়গা বদলায় — ডেটাবেস থেকে অ্যাপের কোডে। অ্যাপ ধরে নেয় total আছে আর তা Decimal128. এখন ভাবুন, তিন বছরে অ্যাপের পাঁচটি সংস্করণ এসেছে: প্রথমটি price লিখত double-এ, তৃতীয়টি unit_price নামে Decimal128-এ, পঞ্চমটি customer-কে string থেকে object বানিয়েছে। collection-এ পাঁচ প্রজন্মের document পাশাপাশি। প্রতিটি কোয়েরি আর রিপোর্টকে এখন পাঁচটি রূপই বুঝতে হবে — এটিই schema-on-read-এর আসল খরচ, যা relational ডেটাবেস migration-এর সময় একবারে মিটিয়ে দেয়।
তাই অভিজ্ঞ দলগুলো MongoDB-তেও migration করে, শুধু ভিন্নভাবে। এক, schema versioning: প্রতিটি document-এ schema_version field, আর কোড প্রতিটি সংস্করণ চিনে পড়ে। দুই, lazy migration: document পড়ার বা লেখার সময় পুরোনো রূপকে নতুনে বদলে দেওয়া। তিন, পটভূমিতে ব্যাচে updateMany দিয়ে সব বদলানো — পর্ব ২৩-এর expand-contract-এর মতোই ধাপে ধাপে। আর চার, অংশ ৮-এর $jsonSchema, যাতে নতুন ভুল রূপ ঢুকতেই না পারে; validationLevel: "moderate" পুরোনো ভুল document-কে ছাড় দিয়ে শুধু নতুন ও বৈধ document-এর লেখা যাচাই করে — স্থানান্তরের সময় উপযোগী।
মূল উপলব্ধি: “Schema-less” আসলে “schema ডেটাবেসের দায়িত্বে নয়, আপনার দায়িত্বে”। নমনীয়তা উপহার নয়, ধার — পরে শৃঙ্খলা দিয়ে শোধ করতে হয়।
প্র ০৪Normalization শেখার পর embed করে ডেটা কপি রাখা কি “ভুল” ডিজাইন নয়?
Normalization-এর লক্ষ্য মনে করুন (পর্ব ৩৬): একটি তথ্য এক জায়গায় রাখা, যাতে update anomaly না হয় — এক কপি বদলাল, আরেক কপি পুরোনো থাকল। এটি একটি লক্ষ্যের উপায়, নিজে লক্ষ্য নয়। E. F. Codd ১৯৭০-এর দশকে যখন normal form-এর তত্ত্ব দেন, তখন ডিস্ক ছিল অত্যন্ত দামি আর ডেটার সঠিকতা রক্ষার সবচেয়ে নির্ভরযোগ্য জায়গা ছিল ডেটাবেস নিজেই। Relational ডেটাবেসেও আমরা পর্ব ৩৮-এ দেখেছি — পড়ার গতির জন্য জেনে-বুঝে denormalize করা হয়।
Document ডিজাইনে প্রশ্নটা “কপি আছে কি না” নয়, “কপিটি বদলাবে কি না, বদলালে কী হবে”। অর্ডারের unit_price-এর কপি আসলে কপি নয় — এটি একটি ঐতিহাসিক সত্য (সেদিনের দাম), যা পণ্যের বর্তমান দাম থেকে আলাদা তথ্য। Normalization-এর ভাষাতেও এটি অর্ডার-লাইনের নিজস্ব attribute, তাই db-shop-এর order_items-এও আছে। গ্রাহকের নামের কপি ভিন্ন — এটি সত্যিকারের redundancy, আর অংশ ৫-এর এডিটরে দেখেছেন, নাম বদলালে কপি পুরোনো থাকে। এটি গ্রহণযোগ্য, যদি (ক) কপিটি কদাচিৎ বদলায়, (খ) পুরোনো থাকলে ক্ষতি সামান্য বা কাঙ্ক্ষিত (চালান), আর (গ) বদলানোর একটি পরিষ্কার পথ থাকে (একটি updateMany, বা change stream দিয়ে পটভূমিতে)।
যা কখনো কপি করবেন না: যে মান প্রতিনিয়ত বদলায় আর যার ভুল ক্ষতিকর — স্টক, ব্যালান্স, অনুমতি। এগুলোর একটিই উৎস থাকবে। bKash-এর মতো লেনদেন-সেবায় ব্যালান্সের দুই কপি মানে টাকার হিসাবে গরমিল; সেখানে relational নকশা আর কঠোর transaction-ই স্বাভাবিক।
মূল উপলব্ধি: Normalization নিয়ম নয়, একটি প্রশ্ন শেখায় — “এই তথ্য বদলালে কোথায় কোথায় বদলাতে হবে?” Document ডিজাইনেও সেই প্রশ্ন করুন; উত্তর “অনেক জায়গায়, প্রায়ই” হলে কপি করবেন না।
প্র ০৫MongoDB-র লাইসেন্স SSPL-এ বদলানোর পর “MongoDB-compatible” নানা সেবা আর প্রকল্প এসেছে। একটি দলের কাছে এর বাস্তব অর্থ কী?
২০১৮ সালের অক্টোবরে MongoDB Inc. Community Server-এর লাইসেন্স AGPL থেকে বদলে Server Side Public License (SSPL) করে। মূল শর্তটি: কেউ MongoDB-কে একটি সেবা হিসেবে অন্যদের দিলে (যেমন একটি ক্লাউড কোম্পানি), সেই সেবা চালানোর পুরো সফটওয়্যারের source code-ও প্রকাশ করতে হবে। কোম্পানির যুক্তি ছিল, বড় ক্লাউড সরবরাহকারীরা তাদের কাজ থেকে লাভ করছে কিন্তু ফেরত দিচ্ছে না। Open Source Initiative SSPL-কে open source লাইসেন্স হিসেবে অনুমোদন করেনি, আর কিছু Linux distribution MongoDB-কে তাদের প্যাকেজ থেকে সরিয়ে দেয়। পরের বছরগুলোতে Elastic, Redis-সহ আরও কিছু কোম্পানি একই ধরনের লাইসেন্স-পরিবর্তনের পথে যায় (পরে কেউ কেউ আবার একটি open-source বিকল্পও যোগ করেছে)।
এর পরে কয়েকটি বিকল্প আলোচনায় এসেছে। Amazon DocumentDB-র মতো ক্লাউড সেবা MongoDB-র API-র সাথে সামঞ্জস্যপূর্ণ বলে দাবি করে, কিন্তু ভেতরের engine আলাদা — তাই সব operator, index বা আচরণ হুবহু মেলে না। FerretDB-র মতো open-source প্রকল্প MongoDB-র wire protocol গ্রহণ করে ভেতরে PostgreSQL-এ ডেটা রাখে। “Compatible” শব্দটি তাই সবসময় একটি নির্দিষ্ট সংস্করণ আর বৈশিষ্ট্যের তালিকার সাথে মিলিয়ে পড়তে হয়।
একটি দলের জন্য বাস্তব প্রশ্নগুলো: আমরা কি নিজেরা চালাব (Community Server — নিজের অ্যাপের জন্য ব্যবহারে SSPL সাধারণত বাধা নয়), নাকি managed সেবা নেব? “Compatible” সেবায় আমাদের দরকারি বৈশিষ্ট্যগুলো — transaction, নির্দিষ্ট aggregation stage, change stream — সত্যিই আছে কি না, নিজের পরীক্ষায় যাচাই করেছি? আর ভবিষ্যতে সরে যেতে চাইলে খরচ কত (vendor lock-in)? লাইসেন্সের আইনি ব্যাখ্যার জন্য অবশ্য আইনজ্ঞের পরামর্শই নির্ভরযোগ্য।
মূল উপলব্ধি: ডেটাবেস বাছাই শুধু প্রযুক্তিগত নয় — লাইসেন্স, সরবরাহকারী আর সরে যাওয়ার পথও নকশার অংশ। “Compatible” দাবি নিজের workload-এ পরীক্ষা না করে বিশ্বাস করবেন না।
অনুশীলন
প্রথমটি কোয়েরি লেখার কাজ — SQL অংশটি উপরের order_docs আছে এমন যেকোনো এডিটরে চালাতে পারেন। বাকিগুলো হাতে-কলমে ভাবার।
-
কোয়েরি লিখুন: ঢাকার গ্রাহকদের delivered বা shipped অর্ডার, যাদের total অন্তত ১,০০০ — শুধু
_id, গ্রাহকের নাম আর total দেখান, total অনুযায়ী বড় থেকে ছোট। (ক) mongosh-এfindদিয়ে, (খ) SQLite-এorder_docs-এর ওপর (total-এর জায়গায় item-এর যোগফল)।db.orders.find( { "customer.city": "Dhaka", status: { $in: ["delivered", "shipped"] }, total: { $gte: 1000 } }, { "customer.name": 1, total: 1 } ).sort({ total: -1, _id: 1 })SELECT d.id, d.doc ->> '$.customer.name' AS name, SUM((j.value ->> 'qty') * (j.value ->> 'unit_price')) AS total FROM order_docs d, json_each(d.doc, '$.items') j WHERE d.doc ->> '$.customer.city' = 'Dhaka' AND d.doc ->> '$.status' IN ('delivered', 'shipped') GROUP BY d.id HAVING total >= 1000 ORDER BY total DESC, d.id;ফল দুটি: 1001 │ Rahim Uddin │ 17400, তারপর 1003 │ Rahim Uddin │ 1780. Mitu-র 1008 (৫৫০) total-এর শর্তে আর Sabbir-এর 1004 (cancelled) status-এর শর্তে বাদ। লক্ষ করুন, total-এর শর্ত SQLite-এ
HAVING-এ গেল, কারণ সেখানে total item থেকে হিসাব করতে হচ্ছে; MongoDB-তে computed pattern থাকায় এটি একটি সাধারণ filter — আর তাই index-ও ব্যবহার করতে পারে। -
হাতে হিসাব — ফল আগে বলুন:
cartscollection-এ একটিই document:{ _id: "c1", items: [ { p: 101, q: 1 }, { p: 102, q: 2 } ], n: 2 }. পরপর চারটি কমান্ড চলল: (ক)updateOne({ _id: "c1" }, { $push: { items: { p: 104, q: 1 } }, $inc: { n: 1 } })(খ)updateOne({ _id: "c1" }, { $pull: { items: { p: 102 } }, $inc: { n: -1 } })(গ)updateOne({ _id: "c2" }, { $set: { n: 0 } })(ঘ)updateOne({ _id: "c2" }, { $inc: { n: 1 } }, { upsert: true }). প্রতিটির matchedCount আর শেষে collection-এর অবস্থা কী?(ক) matchedCount 1, modifiedCount 1 — items-এ তিনটি, n = 3. (খ) matchedCount 1, modifiedCount 1 —
{ p: 102 }-এর সাথে মেলা item (q যা-ই হোক) সরে গেল; items: 101, 104; n = 2. (গ) matchedCount 0, modifiedCount 0 — c2 নেই, আর upsert না থাকায় কিছুই তৈরি হয়নি; কোনো error-ও নয়, তাই matchedCount দেখা জরুরি। (ঘ) matchedCount 0, upsertedCount 1, upsertedId "c2" — নতুন document{ _id: "c2", n: 1 }:$incনা-থাকা field-কে 0 ধরে বাড়ায়, আর filter-এর সমতার শর্ত (_id: "c2") নতুন document-এ বসে যায়। শেষ অবস্থা:{ _id: "c1", items: [ { p: 101, q: 1 }, { p: 104, q: 1 } ], n: 2 }আর{ _id: "c2", n: 1 }. -
ESR মেনে index: নিচের তিনটি কোয়েরির প্রতিটির জন্য একটি compound index লিখুন আর এক লাইনে কারণ দিন। (ক)
find({ "customer._id": 1 }).sort({ order_date: -1 })(খ)find({ status: "pending", order_date: { $lt: ISODate("2025-10-01") } })(গ)find({ category: "Electronics", price: { $lte: 5000 } }).sort({ name: 1 })(ক)
{ "customer._id": 1, order_date: -1 }— E তারপর S: একজন গ্রাহকের অর্ডার index-এ আগে থেকেই তারিখের ক্রমে, “আমার অর্ডার” পাতার জন্য আদর্শ। (খ){ status: 1, order_date: 1 }— E তারপর R; sort নেই, তাই range শেষে। উল্টো ক্রমে (order_dateআগে) পুরোনো সব অর্ডারের key পড়তে হতো, status যা-ই হোক। (গ){ category: 1, name: 1, price: 1 }— E, S, R. এতে Electronics-এর সব key পড়তে হয় (দাম যা-ই হোক), কিন্তু নামের ক্রম বিনা খরচে আসে আর দাম key দেখেই যাচাই হয়। ফল খুব কম হলে{ category: 1, price: 1 }+ memory-তে sort-ও ভালো হতে পারে — তাই দুটি বানিয়েexplain("executionStats")দিয়ে মিলিয়ে নিন। -
নকশার সিদ্ধান্ত: একটি বিশ্ববিদ্যালয়ের অ্যাপ MongoDB-তে বানানো হচ্ছে। প্রতিটির জন্য embed নাকি reference — এক লাইনে কারণসহ: (ক) শিক্ষার্থীর বর্তমান ও স্থায়ী ঠিকানা, (খ) একটি কোর্সে ভর্তি হওয়া সব শিক্ষার্থী (কোনো কোর্সে কয়েক হাজার পর্যন্ত), (গ) প্রতিটি enrollment-এর গ্রেড, (ঘ) বিভাগের নাম, যা প্রায় কখনো বদলায় না কিন্তু প্রতিটি শিক্ষার্থীর পাতায় দেখাতে হয়, (ঙ) ক্লাসরুমের দরজার সেন্সর থেকে প্রতি মিনিটে আসা উপস্থিতি-গণনা।
(ক) embed — দুটি, সীমিত, সবসময় শিক্ষার্থীর সাথে পড়া হয়। (খ) reference — কোর্সের ভেতরে হাজার শিক্ষার্থীর array সীমাহীন বাড়ে; বরং
enrollmentsনামে আলাদা collection:{ student_id, course_id, semester_code, grade }(এটি relational junction table-এর মতোই — many-to-many-তে document মডেলও প্রায়ই একই পথে ফেরে)। (গ) enrollment document-এর ভেতরেই — গ্রেড enrollment-এর নিজস্ব তথ্য। (ঘ) extended reference — শিক্ষার্থীর document-এdept: { _id, name }; নাম কদাচিৎ বদলায়, বদলালে একটিupdateMany. (ঙ) bucket pattern বা time series collection — প্রতি মিনিটে আলাদা document না রেখে প্রতি ঘণ্টা বা দিনে এক document-এ গুচ্ছ, অথবা MongoDB-র time series collection; সাথে TTL দিয়ে পুরোনো ডেটা মুছে ফেলা যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমতিন সদস্যের একটি MongoDB replica set-এ w: "majority" দিয়ে একটি insertOne — ধাপগুলো সাজান (▲▼ চেপে সরান)
- ড্রাইভার লেখার অনুরোধটি primary-তে পাঠায়
- Primary validation ও unique index যাচাই করে লেখাটি প্রয়োগ করে আর oplog-এ লেখে
- Secondary-রা primary-র oplog থেকে নতুন entry নিয়ে নিজেদের কপিতে প্রয়োগ করে
- অন্তত দুটি সদস্যে (primary + একটি secondary) লেখাটি পৌঁছালে তা majority-committed হয়
- ড্রাইভার সফলতার উত্তর (acknowledgement) পায়
MongoDB-র একটি orders collection-এ অর্ডার A-র items: [{product_id: 102, qty: 1}, {product_id: 108, qty: 5}], আর অর্ডার B-র items: [{product_id: 102, qty: 2}]. find({ "items.product_id": 102, "items.qty": { $gte: 2 } }) কোন অর্ডার ফেরত দেবে?
- শুধু B
- A আর B দুটিই
- শুধু A
- কোনোটিই নয়
MongoDB-তে কোয়েরি: find({ status: "delivered", total: { $gte: 2000 } }).sort({ order_date: -1 }). ESR নির্দেশিকা অনুযায়ী সবচেয়ে উপযুক্ত compound index কোনটি?
{ status: 1, total: 1, order_date: -1 }{ order_date: -1, status: 1, total: 1 }{ status: 1, order_date: -1, total: 1 }{ total: 1, status: 1, order_date: -1 }
MongoDB-তে পণ্যের দাম আর অর্ডারের মোট টাকা রাখার জন্য কোন BSON টাইপ সবচেয়ে উপযুক্ত?
- Double
- String (যেমন "1200.50")
- Int32
- Decimal128
MongoDB-তে find({ city: null }) কোন document-গুলো ফেরত দেয়?
- যেগুলোতে city-র মান null, আর যেগুলোতে city field-টিই নেই
- শুধু যেগুলোতে city-র মান null
- শুধু যেগুলোতে city field নেই
- কোনোটিই নয়, কারণ null-এর সাথে তুলনা সবসময় মিথ্যা
একটি অনলাইন দোকানে একজন গ্রাহক বছরের পর বছর হাজার হাজার অর্ডার দিতে পারেন। সব অর্ডার customer document-এর ভেতরে একটি array-তে embed করার প্রধান সমস্যা কী?
- Array-তে index দেওয়া যায় না
- Document সীমাহীন বড় হয় — ১৬ MB-র সীমা আর প্রতি লেখায় বিশাল document
- Embedded অর্ডার খোঁজা অসম্ভব
- Embed করলে প্রতিটি অর্ডারে multi-document transaction লাগে
একটি কাজে পণ্যের স্টক কমাতে (products collection) আর নতুন অর্ডার তৈরি করতে (orders collection) হবে — দুটি একসাথে, নয়তো কোনোটিই নয়। MongoDB-তে সঠিক উপায় কী?
- পরপর দুটি সাধারণ কমান্ড — MongoDB নিজে দুটিকে একসাথে রাখে
- দুটি কমান্ডেই w: "majority" দেওয়া
- একটি session-এ multi-document transaction, শেষে commit বা ভুলে abort
- orders-এ $jsonSchema validation দেওয়া
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Document → collection → database, জমা হয় BSON-এ; প্রতিটি document-এ অনন্য
_id(না দিলে ObjectId). টাকা Decimal128, সময় Date. - find-এর filter একটি document: একাধিক field = AND,
$gt/$in/$or, dot notation; array-র একই উপাদানে দুই শর্ত =$elemMatch.{ f: null }না-থাকা field-ও মেলায়। - update-এ অপারেটর —
$set,$inc,$push,$pull; upsert = মিললে বদলানো, নইলে বানানো। একটি document-এর ভেতরে সব update atomic. - একসাথে পড়া ও সীমিত → embed; সীমাহীন বা আলাদা বদলায় → reference. অর্ডারে দাম কপি করুন (ইতিহাস), স্টক কপি নয়। Extended reference, bucket, computed pattern.
- Pipeline ≈ SQL: $match = WHERE, $unwind + $lookup = JOIN, $group = GROUP BY, $sort = ORDER BY. Index-এ ESR, যাচাই
explain(). Auth, least privilege, ইন্টারনেটে খোলা নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৮ · Redis — in-memory key-value আর cacheপরবর্তী পর্বmemory-তে রাখা ডেটা, TTL আর cache-এর নকশা।
- পর্ব ৫৬ · NoSQL — key-value, document, wide-column আর graphআগের পর্বচার ধরনের NoSQL-এর বড় ছবি।
- পর্ব ৩০ · JSON, full-text search আর বিশেষ ডেটা টাইপফিরে দেখুনSQLite JSON আর PostgreSQL jsonb — relational ডেটাবেসে document.
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- JavaScript · Node.js থেকে ডেটাবেসপাশের ট্র্যাকঅ্যাপের কোড থেকে ডেটাবেসে সংযোগ, transaction আর ORM.
- DSA · B-treeগভীরে যানMongoDB-র index যে গাছে থাকে।