সরাসরি পর্বে যান
পর্ব ০৮ · ৬৭-এর মধ্যে · মডিউল ২

Cardinality, participation আর weak entity

Mapping Cardinalities, Participation Constraints, (min,max) & Crow's Foot Notation, Weak Entities and Ternary Relationships

Taught in Bangla, this lesson covers mapping cardinalities and how to derive them from business rules by asking from both sides, total and partial participation, (min,max) and crow's foot notation, weak entities and identifying relationships, ternary relationships and their pitfalls, and where the foreign key goes when cardinality is turned into tables.

এই পর্বে যা শিখবেন

  • চার রকম mapping cardinality — 1:1, 1:N, N:1, M:N — আর ব্যবসার নিয়ম থেকে সেগুলো বের করার দুই-দিকের প্রশ্ন
  • Total আর partial participation, আর (min,max) structural constraint
  • Chen-এর চিহ্ন, crow's foot আর (min,max) — তিন notation পাশাপাশি পড়া, আর “কোন প্রান্তে লেখা” নিয়ে প্রচলিত বিভ্রান্তি
  • Weak entity — owner, identifying relationship, partial key, existence dependency; কখন weak, কখন surrogate key-সহ strong
  • Ternary relationship-এর ফাঁদ, আর cardinality থেকে টেবিলের নকশা — লাইভ SQL দিয়ে প্রমাণসহ

১ · Cardinality — “একজনের বিপরীতে কতজন?”

পর্ব ৭-এ (ER মডেল — entity, attribute আর relationship) দেখেছেন, ER model-এ বাস্তব জগতের জিনিসগুলো হয় entity — গ্রাহক, অর্ডার, শিক্ষার্থী, কোর্স। একই ধরনের সব entity মিলে একটি entity set, আর তাদের মধ্যের সম্পর্কগুলোর সংগ্রহ একটি relationship set — যেমন “গ্রাহক অর্ডার দেন” (places), “শিক্ষার্থী কোর্সে ভর্তি হন” (enrolls). কিন্তু শুধু “সম্পর্ক আছে” বলা যথেষ্ট নয়। ডিজাইনারকে জানতে হয় সম্পর্কটির আকার: একজন গ্রাহক কয়টি অর্ডার দিতে পারেন? একটি অর্ডার কয়জন গ্রাহকের হতে পারে?

এই প্রশ্নের উত্তরকেই বলে mapping cardinality বা cardinality ratio: একটি entity অন্য entity set-এর সর্বোচ্চ কতগুলো entity-র সাথে এই সম্পর্কে যুক্ত হতে পারে। পর্ব ৫-এ “cardinality” শব্দটি অন্য অর্থে দেখেছিলেন — একটি টেবিলে কয়টি সারি। ER মডেলে একই শব্দের মানে সম্পর্কের অনুপাত; প্রসঙ্গ দেখে অর্থ বুঝে নিতে হয়।

পাড়ার মুদি দোকানের বাকির খাতা ভাবুন। প্রতিটি পাতায় একজন খরিদ্দারের নাম, আর নিচে তাঁর অনেকগুলো কেনাকাটার লাইন। একজন খরিদ্দারের অনেক লাইন, কিন্তু একটি লাইন কখনো দুজনের পাতায় লেখা হয় না — এটিই 1:N. আবার খাতার শেষে দোকানদার রাখেন “কোন কোম্পানি কোন মাল দেয়” তালিকা: এক কোম্পানি অনেক মাল দেয়, একই চাল অনেক কোম্পানির কাছ থেকে আসে — এটি M:N. দোকানদার খাতা কীভাবে সাজাবেন, তা ঠিক করে এই অনুপাতগুলোই।

দুটি entity set A আর B-র মধ্যে একটি binary (দুই পক্ষের) relationship-এর জন্য চার রকম অনুপাত সম্ভব:

অনুপাতমানেবাংলাদেশি উদাহরণ
1:1 (one-to-one)একটি A-র সাথে সর্বোচ্চ একটি B, একটি B-র সাথে সর্বোচ্চ একটি Aনাগরিক — তাঁর বর্তমান NID কার্ড; বিভাগ — বিভাগীয় প্রধান
1:N (one-to-many)একটি A-র সাথে অনেক B, কিন্তু একটি B-র সাথে সর্বোচ্চ একটি Aবিভাগ — শিক্ষার্থী; গ্রাহক — অর্ডার; জেলা — উপজেলা
N:1 (many-to-one)1:N-কে উল্টো দিক থেকে পড়াঅর্ডার — গ্রাহক; উপজেলা — জেলা
M:N (many-to-many)একটি A-র সাথে অনেক B, একটি B-র সাথেও অনেক Aশিক্ষার্থী — কোর্স; অর্ডার — পণ্য; লেখক — বই

খেয়াল করুন, 1:N আর N:1 আসলে একই সম্পর্ক — শুধু কোন দিক থেকে বলছেন, তার পার্থক্য। “বিভাগ–শিক্ষার্থী 1:N” আর “শিক্ষার্থী–বিভাগ N:1” একই কথা। আরও খেয়াল করুন, cardinality শুধু সর্বোচ্চ সংখ্যা বলে। “প্রতিটি অর্ডারের কি গ্রাহক থাকতেই হবে?” — এই সর্বনিম্ন-এর প্রশ্নটি আলাদা, তার নাম participation, অংশ ৩-এ।

সংজ্ঞা

A আর B-র মধ্যে relationship set R. যদি প্রতিটি a ∈ A সর্বোচ্চ একটি b-এর সাথে R-এ যুক্ত হয়, তাহলে A থেকে B-র দিকে R “এক”; নয়তো “অনেক”। দুই দিকের উত্তর মিলিয়ে অনুপাত: দুটিই “এক” হলে 1:1, একটি “এক” আর একটি “অনেক” হলে 1:N বা N:1, দুটিই “অনেক” হলে M:N. এটি schema-র নিয়ম — R-এর যেকোনো বৈধ অবস্থায় সত্য হতে হবে।

এই পর্বের লাইভ SQL চলে দুটি নমুনা ডেটাবেসে। 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 — কোন অর্ডারে কোন পণ্য কতটি)। db-uni একটি বিশ্ববিদ্যালয়: departments (dept_id, name, building — ৪টি বিভাগ), instructors, students (student_id, name, dept_id, batch, cgpa — ৮ জন), courses (course_id, title, dept_id, credits, instructor_id) আর enrollments (student_id, course_id, term, grade — কে কোন term-এ কোন কোর্স নিয়েছেন)। প্রতিটি এডিটরের নিচে “নমুনা ডেটা” বাক্সে পুরো টেবিল দেখতে পাবেন; প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়।

২ · ব্যবসার নিয়ম থেকে cardinality — দুই দিক থেকে প্রশ্ন

Cardinality ঠিক করার একটি নির্ভরযোগ্য কৌশল আছে, যা পরীক্ষার খাতা থেকে অফিসের মিটিং পর্যন্ত সবখানে কাজ করে: সম্পর্কটিকে দুই দিক থেকে প্রশ্ন করুন, প্রতিবার একটি নির্দিষ্ট entity ধরে।

  1. “একটি X-এর জন্য সর্বোচ্চ কতটি Y?” — উত্তর “১” না “অনেক”?
  2. “একটি Y-এর জন্য সর্বোচ্চ কতটি X?” — আবার “১” না “অনেক”?
  3. উত্তর কে দেবেন? যিনি ব্যবসা চালান — দোকানের মালিক, রেজিস্ট্রার অফিস, রেলের টিকিট বিভাগ। ডেটা দেখে নয়।

প্রশ্ন দুটি আলাদা করে করা জরুরি। “শিক্ষার্থী আর কোর্সের সম্পর্ক কেমন?” — এমন অস্পষ্ট প্রশ্নে মানুষ প্রায়ই একটি দিকই ভাবেন। নিচের ডায়াগ্রামে পাঁচটি বাস্তব সম্পর্কে কৌশলটি প্রয়োগ করে দেখুন। শেষেরটি — ট্রেনের আসন — একটি ফাঁদ:

নমুনা সম্পর্কলাল = এই ধাপের সিদ্ধান্ত

            
সিদ্ধান্ত
সম্পর্ক
প্রথম প্রশ্নের উত্তর
দ্বিতীয় প্রশ্নের উত্তর
অনুপাত
  1. নাগরিক আর NID — 1:1. নিয়ম: একজন নাগরিকের একটিই বর্তমান জাতীয় পরিচয়পত্র, আর একটি NID নম্বর একজনেরই। দুই দিকেই উত্তর “১”, তাই 1:1. (পুরোনো, বাতিল কার্ডের ইতিহাস রাখতে চাইলে সম্পর্কটি বদলে যায় — তখন একজন নাগরিকের অনেক কার্ড, যার একটি সক্রিয়।)
  2. বিভাগ আর শিক্ষার্থী — 1:N. CSE-তে অনেক শিক্ষার্থী, কিন্তু এই বিশ্ববিদ্যালয়ের নিয়মে একজন শিক্ষার্থীর একটিই major বিভাগ। এক দিকে “অনেক”, অন্য দিকে “১” — তাই 1:N. কোনো বিশ্ববিদ্যালয় double major চালু করলে নিয়ম বদলাবে, আর অনুপাতও M:N হয়ে যাবে।
  3. শিক্ষার্থী আর কোর্স — M:N. Arif নিয়েছেন CSE201 আর CSE301, আবার CSE201-এ আছেন Arif, Bristy আর Dilruba. দুই দিকেই “অনেক”। এমন সম্পর্ক কোনো একটি টেবিলের একটি কলামে রাখা যায় না — অংশ ৭-এ দেখবেন কেন আলাদা টেবিল লাগে।
  4. অর্ডার আর গ্রাহক — N:1. db-shop-এর আসল ডেটা: অর্ডার 1001, 1003, 1009 তিনটিই Rahim-এর; 1002 আর 1007 Karima-র। একটি অর্ডার একজনেরই, একজনের অনেক অর্ডার। এটি “গ্রাহক–অর্ডার 1:N”-এরই উল্টো দিক থেকে পড়া — সম্পর্ক একটিই।
  5. ট্রেনের আসন — ফাঁদ। ১৬ ডিসেম্বরের যাত্রায় ক-১১ আর ক-১২ দুটিই Rokeya-র বুকিংয়ে (নিজের আর মায়ের জন্য)। এক যাত্রায় একটি আসন একজনের বুকিংয়েই থাকে। কিন্তু যাত্রা না বললে ক-১১-এর যাত্রী Rokeya, পরদিন Shila — অনেক। অর্থাৎ “আসন–যাত্রী” প্রশ্নটিই অসম্পূর্ণ; আসল সম্পর্কটি যাত্রা, আসন আর যাত্রী — তিন পক্ষের। এমন সম্পর্ক অংশ ৬-এ।
  6. সারসংক্ষেপ। পাঁচটি উত্তরই এসেছে নিয়ম থেকে: “একটি NID একজনের”, “একজনের একটি major”, “এক যাত্রায় এক আসন এক বুকিংয়ে”। ডেটা আপনাকে দেখাতে পারে নিয়মটি এখন পর্যন্ত ভাঙেনি, কিন্তু নিয়মটি কী, তা বলতে পারে না — পর্ব ৬-এ key-র বেলায় ঠিক এই শিক্ষাই পেয়েছিলেন।

তবু ডেটা কাজে লাগে — নিয়ম ভেঙেছে কি না দেখতে, আর অনুমানটি যাচাই করতে। গ্রাহক–অর্ডার সম্পর্কটি db-shop-এ পরীক্ষা করা যাক। প্রথম কোয়েরি প্রতিটি গ্রাহকের অর্ডার গোনে (COUNT আর GROUP BY, বিস্তারিত পর্ব ১৮-এ); দ্বিতীয়টি খোঁজে এমন অর্ডার, যার গ্রাহক সংখ্যা ১ নয়:

-- “অনেক” দিক: একজন গ্রাহকের কয়টি অর্ডার?
SELECT customer_id, COUNT(*) AS orders_count
FROM orders
GROUP BY customer_id
ORDER BY orders_count DESC, customer_id;

-- “এক” দিক: এমন অর্ডার আছে কি, যার গ্রাহক ১ জন নয়?
SELECT order_id, COUNT(customer_id) AS customers_per_order
FROM orders GROUP BY order_id
HAVING COUNT(customer_id) <> 1;

প্রথম ফলে ৭টি সারি: 1 | 3, 2 | 2, তারপর 3, 4, 6, 7 আর 8 — প্রত্যেকের 1. Rahim (1)-এর তিনটি, Karima (2)-এর দুটি অর্ডার — “অনেক” দিকটি ডেটাতেই দেখা যাচ্ছে। দ্বিতীয় ফল “(কোনো সারি নেই)”। এটি কাকতালীয় নয়: orders টেবিলের প্রতিটি সারিতে customer_id-এর একটিই ঘর, তাই একটি অর্ডারের দুজন গ্রাহক হওয়া গঠনগতভাবেই অসম্ভব। 1:N-এর “এক” দিকটি টেবিলের আকারই রক্ষা করছে — foreign key কেন “অনেক” দিকের টেবিলে বসে, এটিই তার কারণ (অংশ ৭)। আর তালিকায় গ্রাহক 5 (Tanvir Ahmed) নেই — কারণ তাঁর কোনো অর্ডারই নেই। তিনি কোথায় গেলেন, সেটি পরের অংশের বিষয়।

৩ · Participation — total আর partial

Cardinality বলে সর্বোচ্চ। কিন্তু ডিজাইনে সর্বনিম্নও সমান জরুরি: একটি entity-কে কি সম্পর্কটিতে অংশ নিতেই হবে? এই নিয়মের নাম participation constraint.

Total participation

Entity set-এর প্রতিটি entity অন্তত একবার সম্পর্কে থাকবে। “গ্রাহক ছাড়া অর্ডার হয় না” — orders-এর participation total. ER ডায়াগ্রামে দুই লাইন (══) দিয়ে আঁকা হয়।

min ≥ 1 · existence dependency-র কাছাকাছি

Partial participation

কিছু entity সম্পর্কের বাইরে থাকতে পারে। “নিবন্ধন করেছেন কিন্তু এখনো কিছু কেনেননি” — customers-এর participation partial. এক লাইন (──) দিয়ে আঁকা হয়।

min = 0

একই সম্পর্কের দুই প্রান্তে participation আলাদা হতে পারে, আর সাধারণত হয়ও। places সম্পর্কে orders-এর দিকে total participation, customers-এর দিকে partial participation. বিভাগ–শিক্ষার্থী সম্পর্কে বিশ্ববিদ্যালয় যদি বলে “প্রত্যেক শিক্ষার্থী কোনো না কোনো বিভাগে”, তাহলে শিক্ষার্থীর দিক total; আর নতুন খোলা বিভাগে এখনো কেউ ভর্তি না হলে বিভাগের দিক partial.

ডেটায় partial participation খুঁজতে হলে সেই entity-গুলোকেও দেখতে হবে, যাদের কোনো সম্পর্ক নেই। সাধারণ JOIN শুধু মিল-পাওয়া সারি রাখে, তাই অর্ডারহীন গ্রাহক হারিয়ে যান। LEFT JOIN বাঁ দিকের টেবিলের প্রতিটি সারি রাখে; ডান দিকে মিল না পেলে সেখানে NULL বসায় (পুরো আলোচনা পর্ব ১৯-এ)। তারপর COUNT(o.order_id) শুধু non-NULL মান গোনে, তাই অর্ডারহীন গ্রাহকের জন্য ফল আসে 0:

-- প্রত্যেক গ্রাহক, অর্ডার না থাকলেও
SELECT c.customer_id, c.name, COUNT(o.order_id) AS orders_count
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name
ORDER BY orders_count, c.customer_id;

-- অর্ডারের দিক: গ্রাহক ছাড়া কোনো অর্ডার আছে?
SELECT COUNT(*) AS orders_without_customer
FROM orders WHERE customer_id IS NULL;

প্রথম ফলে ৮টি সারি — প্রথমেই 5 | Tanvir Ahmed | 0, তারপর 3, 4, 6, 7, 8 প্রত্যেকের 1, তারপর 2 | Karima Begum | 2 আর 1 | Rahim Uddin | 3. Tanvir-এর 0 প্রমাণ করে customers-এর participation এখানে partial — ডেটাতেই একজন অংশ নেননি। দ্বিতীয় ফল 0. কিন্তু শুধু এই ডেটার জন্য নয়: orders.customer_id-এ NOT NULL আর REFERENCES customers আছে, তাই গ্রাহকহীন অর্ডার ঢোকানোই যায় না। Total participation-এর “অন্তত একজন” এখানে রক্ষা করছে NOT NULL foreign key. সাবধান: COUNT(o.order_id)-এর বদলে COUNT(*) লিখলে Tanvir-এর ফল হতো 1, কারণ LEFT JOIN তাঁর জন্য NULL-ভরা একটি সারি রাখে, আর COUNT(*) সারি গোনে, মান নয়।

একই প্রশ্ন বিশ্ববিদ্যালয়ে। প্রতিটি বিভাগে কতজন শিক্ষার্থী, আর এমন শিক্ষার্থী আছেন কি, যাঁর বিভাগ নেই?

-- বিভাগের দিক: সব বিভাগে কি শিক্ষার্থী আছে?
SELECT d.name, COUNT(s.student_id) AS students
FROM departments d
LEFT JOIN students s ON s.dept_id = d.dept_id
GROUP BY d.dept_id, d.name
ORDER BY d.dept_id;

-- শিক্ষার্থীর দিক: বিভাগহীন কেউ আছেন?
SELECT student_id, name FROM students WHERE dept_id IS NULL;

প্রথম ফল: CSE | 4, EEE | 2, BBA | 1, English | 0 — English বিভাগে কোনো শিক্ষার্থী নেই, তাই departments-এর participation partial. দ্বিতীয় ফলে একটি সারি: 2302 | Habib Ullah. তাঁর dept_id NULL, আর students.dept_id-এ NOT NULL নেই — অর্থাৎ এই schema শিক্ষার্থীর দিকেও partial participation অনুমতি দেয়। বিশ্ববিদ্যালয়ের নিয়ম যদি হয় “প্রত্যেক শিক্ষার্থী একটি বিভাগে”, তাহলে schema নিয়মের চেয়ে ঢিলা, আর ঠিক করতে হবে dept_id INTEGER NOT NULL REFERENCES departments. আবার নিয়ম যদি হয় “ভর্তির পর প্রথম বছর বিভাগ ঠিক হয় না”, তাহলে NULL-ই সঠিক। ডিজাইনারের কাজ এই প্রশ্ন জিজ্ঞেস করা।

সব total participation একটি NOT NULL দিয়ে রক্ষা হয় না। “প্রতিটি অর্ডারের গ্রাহক থাকবে” সহজ — foreign key টি orders-এর সারিতেই থাকে। কিন্তু উল্টো দিকের নিয়ম, যেমন “প্রতিটি অর্ডারে অন্তত একটি পণ্য-লাইন থাকবে”, রক্ষা করতে হয় অন্য টেবিলের সারি গুনে। সেটি CHECK বা NOT NULL দিয়ে লেখা যায় না; লাগে transaction-এর ভেতরে যাচাই (পর্ব ৪৪), trigger (পর্ব ২৯), বা অ্যাপের নিয়ম। ভাবনার প্রশ্ন ৪-এ বিস্তারিত।

৪ · (min,max) notation আর crow's foot — তিন ভাষায় একই কথা

এ পর্যন্ত দুটি আলাদা তথ্য পেয়েছেন: সর্বোচ্চ (cardinality) আর সর্বনিম্ন (participation). এ দুটিকে একসাথে একটি সংখ্যা-জোড়ায় লেখার রীতিকে বলে structural constraint বা (min,max) notation. কোনো entity set E আর relationship R-এর রেখার পাশে (min,max) লেখা মানে: E-এর প্রতিটি entity R-এ অন্তত min বার, সর্বোচ্চ max বার অংশ নেয়। min = 0 মানে partial, min ≥ 1 মানে total; max = 1 মানে “এক”, max = N মানে “অনেক”।

গ্রাহক–অর্ডার সম্পর্কের নিয়ম লিখলে: একজন গ্রাহক ০ থেকে অনেক অর্ডার দেন → CUSTOMER-এর পাশে (0,N); প্রতিটি অর্ডার ঠিক একজন গ্রাহকের → ORDER-এর পাশে (1,1). একই নিয়ম অন্য দুটি জনপ্রিয় notation-এ কেমন দেখায়, পাশাপাশি দেখুন। Chen notation-এ entity আয়তক্ষেত্র, relationship রম্বস (diamond); crow's foot notation-এ relationship-এর আলাদা চিহ্ন নেই, শুধু একটি রেখা, যার দুই প্রান্তে ছোট চিহ্ন:

একই নিয়ম, তিন notation

Every order has exactly one customer; a customer has zero or more orders

১ · Chen (তীর = “এক”, দুই লাইন = total) CUSTOMER places ORDER 1 N ২ · Crow's foot (চিহ্ন পড়ুন ওপারের প্রশ্নে) CUSTOMER ORDER places ঠিক ১ ০ বা অনেক ৩ · (min,max) (সংখ্যা পড়ুন নিজের প্রান্তে) CUSTOMER places ORDER (0,N) (1,1) min 0 = partial · min 1 = total · max 1 = এক · max N = অনেক
ছবি ৮.১ — তিনটি ছবি একই নিয়ম বলছে। খেয়াল করুন: ORDER-এর প্রান্তে crow's foot-এর চিহ্ন বলে “একজন গ্রাহকের অর্ডার ০ বা অনেক”, অথচ ঠিক সেই প্রান্তে (min,max) বলে “প্রতিটি অর্ডার (1,1) বার অংশ নেয়”।

ছবির শেষ লাইনটিই এই অংশের সবচেয়ে বড় ফাঁদ। Chen-এর 1/N আর crow's foot-এর চিহ্ন পড়তে হয় “ওপারে তাকিয়ে” (look-across): ORDER-এর পাশের চিহ্ন বলে একটি CUSTOMER-এর বিপরীতে কতগুলো ORDER. কিন্তু (min,max) পড়তে হয় “নিজের দিকে তাকিয়ে” (look-here): ORDER-এর পাশের জোড়া বলে একটি ORDER নিজে কতবার অংশ নেয়। ফলে একই সম্পর্কে সংখ্যাগুলোর জায়গা উল্টে যায়। ধাপে ধাপে মিলিয়ে দেখুন:

CUSTOMER — places — ORDERলাল = এই ধাপে যে চিহ্ন পড়ছি

            
এই ধাপে
চিহ্ন
কী বলে
পড়ার দিক
  1. নিয়ম থেকে চারটি সংখ্যা। যেকোনো binary সম্পর্কে প্রতিটি প্রান্তে দুটি প্রশ্ন: সর্বনিম্ন কত, সর্বোচ্চ কত। গ্রাহকের দিকে (0, অনেক), অর্ডারের দিকে (1, 1). প্রতিটি notation এই চারটি সংখ্যাই আঁকে, শুধু আলাদা জায়গায়।
  2. Chen-এর অনুপাত (1 আর N). Elmasri–Navathe ধাঁচের Chen ডায়াগ্রামে রেখার পাশে 1, N বা M লেখা হয়। গ্রাহকের পাশের “1” মানে একটি অর্ডারের বিপরীতে একজন গ্রাহক — অর্থাৎ সংখ্যাটি ওপারের entity (অর্ডার) ধরে পড়তে হয়। এটি শুধু max দেখায়; min দেখাতে লাগে আলাদা চিহ্ন।
  3. Chen-এর তীর আর দুই লাইন. Silberschatz–Korth ধাঁচে লেখা থাকে না, থাকে তীর: relationship থেকে যে entity-র দিকে তীর, সেটি “এক” দিক — প্রতিটি অর্ডার সর্বোচ্চ একজন গ্রাহকের সাথে যুক্ত। আর দুই লাইন (══) মানে সেই entity-র total participation — প্রতিটি অর্ডার অবশ্যই কোনো গ্রাহকের।
  4. Crow's foot: অর্ডারের প্রান্ত. রেখার প্রান্তে দুটি চিহ্ন: entity-র বাক্সের গায়ে লাগানোটি max (কাকের পা = অনেক, খাড়া দাগ = এক), তার একটু আগেরটি (রেখার মাঝের দিকে) min (বৃত্ত = ০, দাগ = ১)। অর্ডারের প্রান্তে ○< মানে “একজন গ্রাহকের অর্ডার ০ থেকে অনেক” — Chen-এর মতোই ওপারে তাকিয়ে পড়া।
  5. Crow's foot: গ্রাহকের প্রান্ত. দুটি খাড়া দাগ (┼┼) মানে min ১, max ১ — “একটি অর্ডারের গ্রাহক ঠিক একজন”। এখানে min ১ থাকায় total participation-টি আসলে অর্ডারের, যদিও দাগ আঁকা গ্রাহকের প্রান্তে। এটিই সবচেয়ে বেশি গুলিয়ে যায়।
  6. (min,max). এখানে সংখ্যাগুলো নিজের দিকে তাকিয়ে পড়া হয়: গ্রাহকের পাশে (0,N) — প্রতিটি গ্রাহক places-এ ০ থেকে N বার; অর্ডারের পাশে (1,1) — প্রতিটি অর্ডার ঠিক একবার। সুবিধা: ternary বা আরও বড় সম্পর্কেও অর্থ পরিষ্কার থাকে, যেখানে “ওপার” একটি নয়, অনেক।
  7. তুলনা. অর্ডারের প্রান্তে Chen লেখে N, crow's foot আঁকে ○< — দুটিই গ্রাহক-প্রতি অর্ডার। অথচ (min,max) সেখানে লেখে (1,1) — অর্ডারের নিজের কথা। তাই একটি notation থেকে আরেকটিতে যেতে হলে সংখ্যা-জোড়াগুলোর প্রান্ত অদলবদল করতে হয়। পরীক্ষায় বা অফিসে কোনো ডায়াগ্রাম দেখলে আগে জেনে নিন কোন notation.

এবার db-shop থেকে গ্রাহকের দিকের (min,max) সরাসরি মেপে দেখা যাক — প্রতিটি গ্রাহকের অর্ডার গুনে তার সর্বনিম্ন আর সর্বোচ্চ। ভেতরের কোয়েরিটি আগের অংশের LEFT JOIN-ই; বাইরেরটি তার ওপর MIN আর MAX নেয় (এমন কোয়েরির ভেতরে কোয়েরি — subquery — পর্ব ২১-এ বিস্তারিত):

-- প্রতিটি গ্রাহক places-এ কতবার? তার min আর max
SELECT MIN(n) AS min_orders, MAX(n) AS max_orders
FROM (SELECT COUNT(o.order_id) AS n
      FROM customers c
      LEFT JOIN orders o ON o.customer_id = c.customer_id
      GROUP BY c.customer_id);

ফল একটি সারি: 0 | 3. এই ডেটায় গ্রাহকের দিকের জোড়া (0,3). এখান থেকে দুটি শিক্ষা। প্রথমত, min 0 প্রমাণ করে নিয়মে partial participation থাকতেই হবে — একজন গ্রাহক (Tanvir) সত্যিই অর্ডারহীন। দ্বিতীয়ত, max 3 থেকে “(0,3)” নিয়ম বানানো ভুল হবে। নিয়ম বলে গ্রাহক যত খুশি অর্ডার দিতে পারেন, তাই ডায়াগ্রামে লেখা হবে (0,N). ডেটা min-এর ক্ষেত্রে নিয়মের বিরুদ্ধে প্রমাণ দিতে পারে (“total বলেছিলেন, অথচ এই গ্রাহকের 0”), কিন্তু max-এর সীমা ঠিক করে ব্যবসা। কখনো সীমাটি সত্যিই সংখ্যা হয় — যেমন “একজন শিক্ষার্থী এক term-এ সর্বোচ্চ ৬টি কোর্স” হলে লেখা হয় (0,6) — আর তখন সেটি রক্ষার জন্য trigger বা অ্যাপের যাচাই লাগে।

৫ · Weak entity — যাকে একা চেনা যায় না

একটি ক্ষুদ্রঋণ প্রতিষ্ঠানের খাতা ভাবুন। Rokeya ৩০,০০০ টাকা ঋণ নিয়েছেন, ৩টি মাসিক কিস্তিতে শোধ করবেন; Jamal নিয়েছেন ১২,০০০ টাকা, ২টি কিস্তিতে। মাঠকর্মী অফিসে ফোন করে বললেন, “কিস্তি নং 2 জমা হয়েছে।” কার? Rokeya-র কিস্তি 2 আছে, Jamal-এরও আছে। শুধু কিস্তির নম্বর দিয়ে একটি কিস্তিকে চেনা যায় না; চেনা যায় “অমুক ঋণের কিস্তি নং 2” বললে।

এমন entity-কে বলে weak entity: যার নিজের attribute-গুলো দিয়ে একে আলাদা করে চেনা যায় না, চেনার জন্য অন্য একটি entity-র ওপর নির্ভর করতে হয়। যার ওপর নির্ভর করে, সেটি owner entity (বা identifying entity) — এখানে ঋণ। দুটিকে জোড়া সম্পর্কটি identifying relationship. আর weak entity-র যে attribute owner-এর ভেতরে তাকে আলাদা করে (কিস্তি নং), সেটি partial key বা discriminator. নিজের key আছে এমন সাধারণ entity-কে বলে strong entity.

Weak entity-র নিয়ম

Key: weak entity-র key = owner-এর primary key + partial key. কিস্তির key (loan_id, inst_no).

Participation: identifying relationship-এ weak entity-র participation সবসময় total, আর প্রতিটি weak entity-র owner ঠিক একটি — (min,max) = (1,1).

Existence dependency: owner না থাকলে weak entity-র অস্তিত্বই থাকে না। ঋণের রেকর্ড মুছে গেলে তার কিস্তিগুলোর আর কোনো অর্থ নেই।

Chen ডায়াগ্রামে weak entity আঁকা হয় দুই-রেখার আয়তক্ষেত্রে, identifying relationship দুই-রেখার রম্বসে, আর partial key-র নিচে ড্যাশ দেওয়া দাগ (সাধারণ key-র নিচে থাকে পূর্ণ দাগ):

LOAN আর তার INSTALLMENT

Weak entity (double rectangle), identifying relationship (double diamond), partial key (dashed underline)

loan_id LOAN has 1 N INSTALLMENT inst_no amount
ছবি ৮.২ — INSTALLMENT-এর দিকে দুই লাইন: প্রতিটি কিস্তি অবশ্যই একটি ঋণের (total). inst_no-র নিচে ড্যাশ দাগ: এটি শুধু একটি ঋণের ভেতরে অনন্য।

আরও দুটি পরিচিত weak entity: অর্ডারের লাইন-আইটেম (অর্ডার 1001-এর লাইন 1, লাইন 2 — অন্য অর্ডারেরও লাইন 1 আছে), আর কর্মীর নির্ভরশীল সদস্য (dependent) — অফিসের স্বাস্থ্যবিমায় কর্মী 507-এর স্ত্রী আর ছেলে; এঁদের চেনা হয় “কর্মী 507-এর dependent, নাম Nila” দিয়ে, আর কর্মী চাকরি ছাড়লে বিমার এই রেকর্ডও যায়। নিচের ডায়াগ্রামে দেখুন কিস্তির key কীভাবে গড়ে ওঠে:

installmentsলাল = এই ধাপে যা দেখছি

            
যাচাই
যে সেট দেখছি
সব সারিতে আলাদা?
ভূমিকা
  1. দুটি ঋণের পাঁচটি কিস্তি. ঋণ 1 (Rokeya, ৩০,০০০ টাকা) তিন কিস্তিতে, ঋণ 2 (Jamal, ১২,০০০ টাকা) দুই কিস্তিতে। মাঠকর্মী বললেন “কিস্তি নং 2 জমা হয়েছে” — এই তথ্য দিয়ে কি ঠিক একটি সারি বেছে নেওয়া যায়?
  2. {inst_no} একা — না. কিস্তি 1 আর 2 দুটি ঋণেই আছে। প্রতিটি ঋণ নিজের কিস্তি 1 থেকে গোনা শুরু করে। তাই inst_no শুধু একটি ঋণের ভেতরে অনন্য — এটিই partial key বা discriminator. ER ডায়াগ্রামে এর নিচে ড্যাশ দাগ।
  3. {loan_id, inst_no} — key. Owner-এর key (loan_id) যোগ করলে পাঁচটি জোড়াই আলাদা: (1,1), (1,2), (1,3), (2,1), (2,2). নিয়মেও তাই — একটি ঋণে কিস্তি নং 3 একবারই হয়। তাই টেবিলে এটি composite primary key, আর loan_id একই সাথে loans-এর দিকে foreign key.
  4. Existence dependency. ঋণ 1-এর রেকর্ড মুছে ফেললে (ধরুন ভুল করে খোলা হয়েছিল) তার তিনটি কিস্তি “কোন ঋণের কিস্তি?” প্রশ্নের উত্তরহীন হয়ে যায়। তাই weak entity-র foreign key-তে সাধারণত ON DELETE CASCADE — owner গেলে weak entity-ও যায়। (আসল আর্থিক প্রতিষ্ঠানে পরিশোধিত ঋণ মোছা হয় না, বন্ধ চিহ্নিত হয় — পর্ব ৬-এর আলোচনা মনে করুন।)
  5. বিকল্প: surrogate key দিয়ে strong entity. অনেক দল প্রতিটি টেবিলে একটি স্বয়ংক্রিয় id রাখেন — এখানে inst_id 501, 502… তখন কিস্তি “strong” হয়ে যায়। কিন্তু নিয়মটি হারায় না: (loan_id, inst_no)-এ UNIQUE না দিলে 506 নম্বর সারিতে ঋণ 1-এর কিস্তি 3 দ্বিতীয়বার ঢুকে যায়, আর DBMS কিছুই বলে না। Surrogate key বেছে নিলেও partial key-র নিয়ম UNIQUE হয়ে থাকতে হবে।

এবার আসল SQLite-এ। এই দুটি এডিটর নিজেদের ছোট schema বানায়: loans (loan_id, borrower, amount) আর installments (loan_id, inst_no, due_date, amount), যেখানে primary key (loan_id, inst_no) আর loan_id REFERENCES loans ON DELETE CASCADE. শুরুর ডেটা উপরের ডায়াগ্রামের পাঁচটি কিস্তি। প্রথমে দেখুন partial key-র অর্থ — নতুন ঋণের কিস্তি নং 1 চলে, কিন্তু একই ঋণে একই নম্বর দুবার চলে না:

-- নতুন ঋণ 3: তার প্রথম কিস্তির নম্বরও 1 — চলবে
INSERT INTO loans VALUES (3, 'Shila', 5000);
INSERT INTO installments VALUES (3, 1, '2025-08-01', 5000);
SELECT * FROM installments WHERE inst_no = 1 ORDER BY loan_id;

-- ঋণ 1-এর কিস্তি 3 আগেই আছে: বাতিল হবে
INSERT INTO installments VALUES (1, 3, '2025-10-01', 10000);

দুটি INSERT-এর পর “✓ INSERT — ১টি সারি বদলেছে”। SELECT-এর ফলে তিনটি সারি: 1 | 1 | 2025-07-01 | 10000, 2 | 1 | 2025-07-15 | 6000, 3 | 1 | 2025-08-01 | 5000 — তিনটি আলাদা ঋণের তিনটি “কিস্তি 1”, একটুও সংঘাত নেই। শেষ INSERT-এ ত্রুটি: UNIQUE constraint failed: installments.loan_id, installments.inst_no. বার্তাতেই দুটি কলামের নাম — owner-এর key আর partial key মিলেই key.

এখন existence dependency — ঋণ 1 মুছলে তার কিস্তির কী হয়:

-- owner (ঋণ 1) মুছলে weak entity-ও যায়
DELETE FROM loans WHERE loan_id = 1;

SELECT * FROM loans;
SELECT loan_id, COUNT(*) AS installments_left
FROM installments GROUP BY loan_id;

DELETE-এর পর “✓ DELETE — ১টি সারি বদলেছে” (SQLite cascade-এ মোছা সারি এই সংখ্যায় গোনে না)। loans-এ শুধু 2 | Jamal | 12000, আর installments-এর হিসাবে শুধু 2 | 2 — ঋণ 1-এর তিনটি কিস্তি নিজে থেকেই মুছে গেছে। কোনো অনাথ কিস্তি থাকল না। এখানে CASCADE স্বাভাবিক, কারণ কিস্তি ঋণের অংশ; তবু আর্থিক রেকর্ডে মোছার বদলে “বন্ধ” চিহ্ন দেওয়াই নিরাপদ অভ্যাস।

কখন weak entity, কখন surrogate key-সহ strong entity?

ER মডেলের তত্ত্বে প্রশ্নটি সহজ: entity-র নিজের কোনো key না থাকলে সেটি weak. কিন্তু টেবিল বানানোর সময় আপনি চাইলে একটি surrogate id দিয়ে যেকোনো weak entity-কে strong বানাতে পারেন। কোনটি বেছে নেবেন?

বিষয়Weak (composite key)Strong (surrogate id + UNIQUE)
Key(loan_id, inst_no)inst_id, আর UNIQUE (loan_id, inst_no)
নিজের child আছে?থাকলে তাদের foreign key দুই কলামের — স্তর বাড়লে আরও লম্বাchild-রা একটি ছোট id দিয়েই আঙুল তোলে
অর্থkey দেখেই বোঝা যায় “কোন ঋণের কোন কিস্তি”id অর্থহীন; অর্থ UNIQUE-এ
owner বদলানোপ্রায় অসম্ভব — key-ই বদলাতে হয়সম্ভব (যদিও weak entity-তে এমন দরকার কমই)
ভালো যেখানেকিস্তি, অর্ডার-লাইন, dependent — owner স্থির, নিচে আর স্তর নেইযার নিজের child আছে, বা API/URL-এ আলাদা করে দেখাতে হয়

db-shop-এর order_items আসলে একটি weak-ধাঁচের টেবিল: তার key (order_id, product_id), যেখানে product_id-ই discriminator, আর অর্ডার ছাড়া লাইনের কোনো অর্থ নেই। যদি একই অর্ডারে একই পণ্য দুটি আলাদা দামে দুই লাইনে লেখার নিয়ম থাকত, তখন discriminator হতো একটি line_no.

আরও গভীরে: MySQL-এর InnoDB engine প্রতিটি টেবিলকে তার primary key-র ক্রমে সাজিয়ে রাখে (clustered index). ফলে weak entity-র key (loan_id, inst_no) হলে একটি ঋণের সব কিস্তি ডিস্কে পাশাপাশি পাতায় থাকে, আর “ঋণ 1-এর সব কিস্তি” কোয়েরি অল্প কয়েকটি page পড়েই শেষ হয়। Surrogate inst_id হলে সারিগুলো ঢোকার ক্রমে থাকে; বহু ঋণের কিস্তি মিশে যায়, আর একই কোয়েরির জন্য (loan_id, inst_no)-এর UNIQUE index থেকে একটি একটি করে মূল সারিতে যেতে হয়। PostgreSQL-এ টেবিল এমনিতে primary key-র ক্রমে সাজানো থাকে না, তাই পার্থক্য কম। Index-এর এই ভেতরের গঠন পর্ব ৩৯–৪১-এ।

৬ · Ternary relationship — তিন পক্ষের সম্পর্ক আর তার ফাঁদ

এ পর্যন্ত সব সম্পর্ক ছিল দুই পক্ষের — binary relationship. কিন্তু কিছু তথ্য তিনটি entity একসাথে না ধরলে বলাই যায় না। একটি হাসপাতালের ব্যবস্থাপত্র ভাবুন: “ডাক্তার Karim রোগী Babu-কে Omeprazole লিখেছেন।” এই একটি ঘটনায় তিন পক্ষ — ডাক্তার, রোগী, ওষুধ। এমন সম্পর্ককে বলে ternary relationship (degree ৩)। Chen ডায়াগ্রামে একটি রম্বস থেকে তিনটি রেখা তিনটি entity-তে যায়।

নতুনদের প্রায়ই মনে হয়, এটিকে তিনটি দুই-পক্ষের সম্পর্কে ভাঙলেই চলে: ডাক্তার–রোগী (কে কাকে দেখেছেন), রোগী–ওষুধ (কে কী খাচ্ছেন), ডাক্তার–ওষুধ (কে কী লেখেন)। দেখতে যুক্তিসংগত। কিন্তু এতে তথ্য হারায় — আর হাসপাতালে সেই হারানো তথ্য বিপজ্জনক। নিচের তিনটি ব্যবস্থাপত্র দিয়ে ধাপে ধাপে দেখুন:

prescribes (ডাক্তার, রোগী, ওষুধ)লাল = এই ধাপে নতুন

            
হিসাব
এই ধাপে সারি
কী করছি
  1. আসল তথ্য. তিনটি ব্যবস্থাপত্র। Dr. Karim লিখেছেন Rina-কে Paracetamol আর Babu-কে Omeprazole; Dr. Sultana লিখেছেন Babu-কে Paracetamol. প্রতিটি সারি একটি সম্পূর্ণ ঘটনা।
  2. প্রথম binary: কে কাকে দেখেছেন. প্রতিটি সারি থেকে ওষুধ বাদ দিয়ে ডাক্তার–রোগী জোড়া: (Karim, Rina), (Karim, Babu), (Sultana, Babu). প্রতিটি জোড়া সত্য।
  3. দ্বিতীয় binary: কে কী খাচ্ছেন. রোগী–ওষুধ জোড়া: Rina–Paracetamol, Babu–Omeprazole, Babu–Paracetamol. এটিও সত্য — Babu সত্যিই দুটি ওষুধই পাচ্ছেন।
  4. তৃতীয় binary: কে কী লেখেন. ডাক্তার–ওষুধ জোড়া: Karim–Paracetamol, Karim–Omeprazole, Sultana–Paracetamol. তিনটি টেবিলের প্রতিটি সারি সত্য — তবু দেখুন সামনে কী হয়।
  5. জোড়া লাগানো. আসল ঘটনা ফিরে পেতে ডাক্তার–রোগী আর রোগী–ওষুধকে রোগী মিলিয়ে join করি। Babu-র দুজন ডাক্তার আর দুটি ওষুধ, তাই তাঁর জন্য ২ × ২ = ৪টি সমন্বয় আসে, Rina-র জন্য ১টি — মোট ৫টি সারি। এর মধ্যে দুটি আসলে কখনো ঘটেনি।
  6. তৃতীয়টি দিয়ে ছাঁকা. এবার ডাক্তার–ওষুধ দিয়ে ছাঁকি। (Sultana, Babu, Omeprazole) বাদ পড়ে, কারণ Sultana কখনো Omeprazole লেখেননি। কিন্তু (Karim, Babu, Paracetamol) টিকে যায় — Karim সত্যিই Babu-কে দেখেছেন, Babu সত্যিই Paracetamol পান, Karim সত্যিই Paracetamol লেখেন। তিনটি শর্তই সত্য, অথচ ঘটনাটি মিথ্যা।
  7. শিক্ষা. ৩টি আসল সারির জায়গায় ৪টি। এমন বাড়তি সারিকে বলে spurious tuple. ternary সম্পর্কে যে তথ্য ছিল — “এই ডাক্তার এই রোগীকে এই ওষুধ” — তিনটি binary-তে তা নেই। ওষুধের প্রতিক্রিয়া হলে কোন ডাক্তারকে জানাবেন, সেটিই আর বলা যায় না।

একই পরীক্ষা SQLite-এ। এই এডিটরের setup-এ একটি prescribes (doctor, patient, medicine) টেবিল আর উপরের তিনটি সারি। কোডটি তিনটি binary টেবিল বানায় (CREATE TABLE … AS SELECT DISTINCT — একটি কোয়েরির ফল থেকে নতুন টেবিল), তারপর তিনটিকে join করে:

-- ternary থেকে তিনটি binary
CREATE TABLE dp AS SELECT DISTINCT doctor, patient FROM prescribes;
CREATE TABLE pm AS SELECT DISTINCT patient, medicine FROM prescribes;
CREATE TABLE dm AS SELECT DISTINCT doctor, medicine FROM prescribes;

-- তিনটি binary জুড়ে আসলটি ফেরত পাওয়ার চেষ্টা
SELECT dp.doctor, dp.patient, pm.medicine
FROM dp
JOIN pm ON pm.patient = dp.patient
JOIN dm ON dm.doctor = dp.doctor AND dm.medicine = pm.medicine
ORDER BY dp.doctor, dp.patient;

তিনটি CREATE TABLE-এর পর SELECT-এর ফলে ৪টি সারি: Dr. Karim | Babu | Omeprazole, Dr. Karim | Babu | Paracetamol, Dr. Karim | Rina | Paracetamol, Dr. Sultana | Babu | Paracetamol. দ্বিতীয় সারিটি ভুয়া — prescribes-এ এমন কিছু নেই। SQL নিজে কোনো ভুল করেনি; ভুলটি ডিজাইনের। তিনটি binary একসাথে আসল ternary-র চেয়ে কম তথ্য রাখে।

উল্টো দিকটিও সত্য — আর ternary-তে cardinality-র ফাঁদ

তার মানে কি সবসময় ternary-ই ভালো? না। কখনো binary এমন তথ্য রাখে, যা ternary পারে না। “Dr. Sultana Omeprazole লেখার অনুমতিপ্রাপ্ত” — এখনো কোনো রোগীকে লেখেননি — এই তথ্য prescribes-এ রাখা যায় না, কারণ সেখানে প্রতিটি সারিতে একজন রোগী লাগবে। তাই সঠিক ডিজাইনে প্রায়ই দুটোই থাকে: ঘটনার জন্য ternary, আর আলাদা স্বাধীন নিয়মের জন্য binary. আবার কখনো ternary সত্যিই তিনটি binary-তে নিখুঁতভাবে ভাঙা যায় — যদি ব্যবসার একটি বিশেষ নিয়ম থাকে (“কোনো ডাক্তার কোনো রোগীকে দেখেন, রোগী কোনো ওষুধ পান, আর ডাক্তার সেই ওষুধ লেখেন — তাহলে ডাক্তার রোগীকে সেটি লিখেছেনই”)। এই নিয়মকে বলে join dependency, আর এটি পর্ব ৩৭-এর 5NF-এর বিষয়।

Ternary-তে cardinality পড়াও কঠিন। ধরুন C. J. Date-এর পাঠ্যবইয়ের বিখ্যাত supplier–part–project উদাহরণ: “সরবরাহকারী S, প্রকল্প J-কে যন্ত্রাংশ P দেয়।” PART-এর রেখায় “1” লেখা মানে কী? Look-across ধরনে পড়লে এর মানে: প্রতিটি (supplier, project) জোড়ার জন্য সর্বোচ্চ একটি part — অর্থাৎ একটি সরবরাহকারী একটি প্রকল্পকে কেবল একটি যন্ত্রাংশই দিতে পারে। প্রায় কেউ এটা বোঝাতে চান না; তাঁরা সাধারণত ভাবেন “প্রতিটি part একটি supplier-এর”, যা আসলে একটি আলাদা binary নিয়ম। এই বিভ্রান্তি এড়াতে Silberschatz–Korth–Sudarshan-এর পাঠ্যবই পরামর্শ দেয়, একটি ternary বা বড় সম্পর্কে সর্বোচ্চ একটি তীর আঁকতে — একাধিক তীরের অর্থ দুই রকম পড়া যায়। (min,max) notation এখানে বেশি পরিষ্কার, কারণ সেটি শুধু বলে “প্রতিটি part কতবার অংশ নেয়”। আর crow's foot-এ ternary আঁকার সরাসরি উপায় নেই; সেখানে মাঝখানে একটি associative entity (যেমন PRESCRIPTION) বানিয়ে তিনটি 1:N রেখা টানা হয়।

৭ · Cardinality থেকে টেবিল — foreign key কোথায় বসবে

এই পর্বের সব সিদ্ধান্ত শেষ পর্যন্ত টেবিলের নকশায় গিয়ে দাঁড়ায়। পুরো mapping-এর নিয়ম পর্ব ১০-এ (ER থেকে টেবিল — mapping-এর নিয়ম); এখানে শুধু মূল ছবিটি, যাতে বোঝেন cardinality কেন এত গুরুত্বপূর্ণ:

ER-এটেবিলেউদাহরণ
1:N“অনেক” দিকের টেবিলে foreign key; total হলে NOT NULLorders.customer_id NOT NULL REFERENCES customers
M:Nআলাদা junction table: দুই দিকের দুটি foreign key, দুটি মিলে primary key; সম্পর্কের নিজের attribute এখানেorder_items (order_id, product_id, quantity, unit_price), enrollments
1:1এক দিকের টেবিলে foreign key + UNIQUE; সাধারণত total দিকটিতে, NOT NULL-সহnid_cards.citizen_id UNIQUE NOT NULL
Weak entityowner-এর key + partial key = composite primary key; owner-এর দিকে foreign key, প্রায়ই ON DELETE CASCADEinstallments PRIMARY KEY (loan_id, inst_no)
Ternaryতিনটি foreign key-সহ একটি টেবিল; key ঠিক হয় তিন প্রান্তের cardinality থেকেprescribes (doctor, patient, medicine)

1:N-এ foreign key কেন “অনেক” দিকে? অংশ ২-এর প্রথম এডিটর মনে করুন: orders-এর প্রতিটি সারিতে customer_id-এর একটিই ঘর, তাই “এক” দিকটি আপনাআপনি রক্ষা পায়। উল্টো করে customers-এ order_id রাখলে Rahim-এর তিনটি অর্ডারের জন্য তিনটি ঘর লাগত — কয়টি ঘর রাখবেন? এজন্যই M:N-এ কোনো দিকেই রাখা যায় না, আলাদা টেবিল লাগে। db-shop-এর order_items ঠিক এমন একটি junction table — অর্ডার আর পণ্যের M:N সম্পর্ক:

-- এক দিক: একটি অর্ডারে একাধিক পণ্য
SELECT order_id, COUNT(*) AS products
FROM order_items GROUP BY order_id
HAVING COUNT(*) > 1;

-- অন্য দিক: একটি পণ্য কয়টি অর্ডারে (০ হলেও)
SELECT p.product_id, p.name, COUNT(oi.order_id) AS in_orders
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.product_id
GROUP BY p.product_id, p.name
ORDER BY in_orders DESC, p.product_id;

প্রথম ফলে ৬টি অর্ডার — 1001, 1002, 1003, 1005, 1007, 1009 — প্রত্যেকটিতে 2টি পণ্য। দ্বিতীয় ফলে ১০টি সারি: Mobile Phone, Headphone, DSA Book, SQL Book, Rice 5kg, Mustard Oil 1L আর Smart Watch প্রত্যেকে 2টি অর্ডারে; Panjabi আর Saree 1টিতে; আর শেষে 103 | Power Bank | 0. দুই দিকেই “অনেক” দেখা যাচ্ছে — M:N. Power Bank-এর 0 বলছে পণ্যের দিকে participation partial (স্টক 0, কেউ কেনেনি)। আর অর্ডারের দিকে? “প্রতিটি অর্ডারে অন্তত একটি পণ্য” নিয়মটি এই ডেটায় সত্য, কিন্তু schema তা রক্ষা করে না — অংশ ৩-এর সতর্কতা।

বিশ্ববিদ্যালয়ে শিক্ষার্থী–কোর্সের M:N সম্পর্কের junction table হলো enrollments, যার key (student_id, course_id, term). একজন শিক্ষার্থী একই কোর্স দুই term-এ নিতে পারেন, তাই এখানে COUNT(DISTINCT …) দিয়ে আলাদা কোর্স বা শিক্ষার্থী গোনা হয়েছে:

-- একটি কোর্সে কতজন শিক্ষার্থী?
SELECT course_id, COUNT(DISTINCT student_id) AS students
FROM enrollments GROUP BY course_id
ORDER BY students DESC, course_id;

-- একজন শিক্ষার্থী কয়টি কোর্সে?
SELECT student_id, COUNT(DISTINCT course_id) AS courses
FROM enrollments GROUP BY student_id
ORDER BY courses DESC, student_id;

প্রথম ফল: CSE201 আর CSE301 প্রত্যেকে 3 জন, CSE101 আর EEE101 2 জন, BBA101, CSE302 আর ENG101 1 জন। দ্বিতীয় ফল: 2101 আর 2201 প্রত্যেকে 3টি কোর্সে, 2102 আর 2301 2টিতে, 2103, 2202, 2203 1টিতে। দুই দিকেই “অনেক” — M:N. আর 2302 (Habib Ullah) তালিকায় নেই: তাঁর কোনো ভর্তি নেই। শিক্ষার্থীর দিকে participation partial — কেন তালিকায় নেই, তা দেখাতে LEFT JOIN লাগত (অনুশীলন ২)।

শেষে 1:1 — নাগরিক আর NID কার্ড। এই এডিটরের ছোট schema: citizens (citizen_id, name) আর nid_cards (card_no, citizen_id, issued_on), যেখানে card_no primary key, আর citizen_id NOT NULL UNIQUE REFERENCES citizens. Rokeya আর Jamal-এর কার্ড আছে, Shila-র এখনো নেই:

-- Shila (3) প্রথম কার্ড পেলেন: চলবে
INSERT INTO nid_cards VALUES ('NID-1003', 3, '2024-01-20');
SELECT c.name, n.card_no FROM citizens c
JOIN nid_cards n ON n.citizen_id = c.citizen_id;

-- Rokeya (1)-র জন্য দ্বিতীয় কার্ড: 1:1 ভাঙবে
INSERT INTO nid_cards VALUES ('NID-1004', 1, '2025-05-05');

প্রথম INSERT-এর পর “✓ INSERT — ১টি সারি বদলেছে”; JOIN-এর ফলে তিনটি সারি: Rokeya | NID-1001, Jamal | NID-1002, Shila | NID-1003. শেষ INSERT-এ ত্রুটি: UNIQUE constraint failed: nid_cards.citizen_id. এক দিকের “এক” রক্ষা করছে card_no-র primary key (একটি কার্ড-সারিতে একটিই citizen_id), অন্য দিকের “এক” রক্ষা করছে UNIQUE. UNIQUE না দিলে টেবিলটি আসলে 1:N হয়ে যেত — নাগরিকের অনেক কার্ড। আর NOT NULL বলছে কার্ডের দিকে participation total (মালিকহীন কার্ড নেই); নাগরিকের দিকে partial, কারণ Shila আগে কার্ড ছাড়াই ছিলেন। পুরোনো কার্ডের ইতিহাস রাখতে চাইলে নিয়ম বদলে হয় 1:N, সাথে “একটিই সক্রিয়” শর্ত — PostgreSQL-এ WHERE active শর্তসহ partial unique index দিয়ে যা লেখা যায়।

৮ · ইতিহাস — Chen-এর রম্বস থেকে কাকের পা

১৯৭৬ সালে Peter Pin-Shan Chen-এর প্রবন্ধ “The Entity-Relationship Model — Toward a Unified View of Data” প্রকাশিত হয় ACM Transactions on Database Systems জার্নালের প্রথম খণ্ডে। সেখানেই আসে আয়তক্ষেত্র (entity), রম্বস (relationship), রেখার পাশে 1, n, m লিখে সম্পর্কের অনুপাত দেখানো, আর অন্যের ওপর নির্ভর করে চেনা যায় এমন “weak” entity-র ধারণা। Chen-এর লক্ষ্য ছিল network, hierarchical আর relational — তখনকার তিন প্রতিদ্বন্দ্বী মডেলের ওপরে একটি সাধারণ “ধারণাগত” ছবি দেওয়া, যা থেকে যেকোনো মডেলে যাওয়া যায়।

প্রায় একই সময়ে Gordon Everest “অনেক” বোঝাতে রেখার প্রান্তে তিন-কাঁটার একটি চিহ্ন ব্যবহার করেন, যা পরে “crow's foot” নামে পরিচিত হয়। ১৯৮০-এর দশকে Clive Finkelstein আর James Martin-এর Information Engineering পদ্ধতি এবং পরে Oracle-এর CASE পদ্ধতিতে Richard Barker-এর notation crow's foot-কে শিল্পক্ষেত্রে জনপ্রিয় করে। আজ বেশিরভাগ ডেটাবেস-ডিজাইন টুল crow's foot-ধাঁচের রেখাই আঁকে। (min,max) জোড়া লেখার রীতি এসেছে পরের গবেষণা থেকে, আর বিশেষভাবে ছড়িয়েছে Elmasri ও Navathe-র বহুল ব্যবহৃত পাঠ্যবইয়ের মাধ্যমে; ১৯৯৭ সালে আসা UML-এর class diagram-এ “0..*”, “1..1” লেখার multiplicity-ও একই ধারণার আরেক রূপ — তবে crow's foot-এর মতো ওপারে তাকিয়ে পড়তে হয়। তাই আজও বিশ্ববিদ্যালয়ের খাতায় Chen, অফিসের টুলে crow's foot — দুটোই জানা দরকার।

৯ · AI-এর যুগে cardinality

AI-কে “একটি স্কুল ম্যানেজমেন্ট সিস্টেমের ER ডায়াগ্রাম দাও” বললে মুহূর্তে সুন্দর তালিকা আসে। কিন্তু AI আপনার ব্যবসার নিয়ম জানে না — সে অনুমান করে, আর অনুমানগুলো প্রায়ই চুপচাপ ভুল। Cardinality-র ভুল টেবিল চালু হওয়ার পরে ধরা পড়লে schema বদলাতে হয়, যা সবচেয়ে ব্যয়বহুল সংশোধন। AI প্রায়ই যা করে:

AI-এর ভুলফলকীভাবে ধরবেন
নিয়ম না জিজ্ঞেস করেই 1:N ধরে নেওয়া (“একজন শিক্ষকের একটি বিভাগ”)যৌথ নিয়োগ এলে schema বদলপ্রতিটি সম্পর্কে দুই দিকের প্রশ্ন নিজে করুন
M:N রাখল একটি কলামে কমা দিয়ে (course_ids = 'CSE101,CSE201')JOIN, foreign key, index — কিছুই চলে নাপ্রতিটি M:N-এর জন্য junction table আছে কি না দেখুন
1:1 বলল, কিন্তু foreign key-তে UNIQUE দিল নাআসলে 1:N; দ্বিতীয় সারি চুপচাপ ঢোকেএকই মালিকের দ্বিতীয় সারি INSERT করে দেখুন
Ternary-কে তিনটি binary বানালভুয়া সমন্বয় (spurious tuple)তিনটি জুড়ে সারির সংখ্যা আসলের সাথে মেলান
Weak entity-কে surrogate id দিল, partial key-তে UNIQUE দিল নাএকই কিস্তি দুবার(owner, partial key)-তে duplicate INSERT চেষ্টা
Participation গুনতে LEFT JOIN-এর সাথে COUNT(*)০-এর জায়গায় ১ — partial লুকিয়ে যায়যে entity-র সম্পর্ক নেই, তার ফল হাতে মিলিয়ে নিন
Crow's foot আর (min,max) গুলিয়ে ফেলাউল্টো প্রান্তে চিহ্ন, উল্টো অর্থপ্রতিটি চিহ্ন বাংলা বাক্যে পড়ে দেখুন

AI-কে ডায়াগ্রাম বানাতে না বলে আগে প্রশ্নগুলো বানাতে বলুন। তাহলে নিয়ম ঠিক করবেন আপনি, AI শুধু খাটুনিটা করবে:

আমি একটি ক্ষুদ্রঋণ প্রতিষ্ঠানের ডেটাবেস ডিজাইন করছি। entity: member (সদস্য), group (সমিতি), loan (ঋণ), installment (কিস্তি), field_officer (মাঠকর্মী)। ১. প্রতিটি সম্ভাব্য relationship-এর জন্য দুই দিকের প্রশ্ন লেখো ("একজন X-এর কয়টি Y?" আর উল্টোটা), আর সর্বনিম্ন সংখ্যার প্রশ্নও। উত্তর নিজে অনুমান করবে না — প্রশ্নগুলো আমাকে দাও, আমি অফিসকে জিজ্ঞেস করব। ২. আমার উত্তর পেলে প্রতিটি relationship (min,max) আর crow's foot দুই notation-এ লেখো, আর প্রতিটি চিহ্নের মানে একটি বাংলা বাক্যে বলো। ৩. কোনটি weak entity, তার partial key কী, বলো। ৪. কোনো ternary relationship থাকলে দেখাও তিনটি binary-তে ভাঙলে কোন তথ্য হারাবে। ৫. শেষে SQLite-এর CREATE TABLE দাও, আর প্রতিটি cardinality ভাঙার একটি করে INSERT দাও, যাতে আমি চালিয়ে দেখতে পারি ত্রুটি আসে।

AI-এর দেওয়া ER ডিজাইন হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • 1:N সম্পর্কে foreign key কোন টেবিলে? — “অনেক” দিকের টেবিলে, কারণ সেখানে প্রতিটি সারির একটিই অভিভাবক।
  • M:N কীভাবে টেবিলে আসে? — আলাদা junction table, যাতে দুই দিকের foreign key আর সেগুলো মিলে primary key.
  • Weak entity আর তার key কী? — যার নিজের key নেই; key = owner-এর primary key + partial key, আর identifying relationship-এ participation total.
  • Total আর partial participation-এর পার্থক্য? — Total-এ প্রতিটি entity অন্তত একবার সম্পর্কে (min ≥ 1, দুই লাইন), partial-এ কিছু বাইরে থাকতে পারে (min = 0).
  • একটি ternary কি সবসময় তিনটি binary-তে ভাঙা যায়? — না; সাধারণত তথ্য হারায় (spurious tuple), কেবল join dependency থাকলে নিখুঁতভাবে ভাঙা যায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১একটি বিশ্ববিদ্যালয়ের ডেটাবেসে শুরু থেকে “একজন শিক্ষার্থীর একটি বিভাগ” ধরে students.dept_id রাখা হয়েছিল। দশ বছর পর double major চালু হলো। কী কী বদলাতে হবে, আর শুরুতে কী করলে এটি সহজ হতো?

এটি cardinality ভুল অনুমানের সবচেয়ে পরিচিত গল্প, কারণ নিয়মটি শুরুতে ভুল ছিল না — নিয়মটি বদলেছে। 1:N থেকে M:N হওয়া মানে foreign key-টি আর students-এর একটি কলামে থাকতে পারে না। নতুন একটি junction table লাগবে, ধরুন student_majors (student_id, dept_id, is_primary), যার key (student_id, dept_id). পুরোনো dept_id-এর প্রতিটি মান সেখানে একটি সারি হিসেবে কপি করতে হবে।

কিন্তু আসল খরচ টেবিলে নয়, কোয়েরিতে। দশ বছরে যত রিপোর্ট, অ্যাপের পাতা, ফল-প্রকাশের স্ক্রিপ্ট students.dept_id ধরে লেখা হয়েছে, সব ভেঙে যাবে বা ভুল ফল দেবে। “বিভাগভিত্তিক শিক্ষার্থী সংখ্যা” রিপোর্টে double major শিক্ষার্থীকে দুবার গোনা হবে কি না — এটিও একটি নতুন ব্যবসায়িক প্রশ্ন। ফি হিসাব, CGPA গণনা, সমাবর্তনের তালিকা — প্রতিটির নিয়ম আবার ঠিক করতে হবে। এমন migration সাধারণত কয়েক ধাপে হয়: নতুন টেবিল বানানো, দুই জায়গায় একসাথে লেখা, কোয়েরিগুলো একে একে সরানো, তারপর পুরোনো কলাম তুলে দেওয়া (পর্ব ৫০-এর migration).

শুরুতে কী করা যেত? দুটি পথ। প্রথমত, নিয়ম জিজ্ঞেস করার সময় “এখন” নয়, “কখনো কি” প্রশ্ন করা: “কখনো কি কোনো শিক্ষার্থী দুটি বিভাগে থাকতে পারেন — minor, যৌথ ডিগ্রি, বিভাগ বদল?” উত্তর “হতে পারে” হলে শুরু থেকেই junction table. দ্বিতীয়ত, ঝুঁকিপূর্ণ জায়গায় একটি view (পর্ব ২৪) দিয়ে অ্যাপকে আসল টেবিল থেকে আলাদা রাখা, যাতে ভেতরের গঠন বদলালেও বাইরের কোয়েরি একই থাকে — পর্ব ৩-এর logical data independence ঠিক এই কাজের জন্যই। তবে সব 1:N-কে “ভবিষ্যতে লাগতে পারে” বলে M:N বানানোও ভুল: তখন “প্রতিটি শিক্ষার্থীর ঠিক একটি বিভাগ” নিয়মটি আর schema রক্ষা করে না।

মূল উপলব্ধি: Cardinality হলো ব্যবসার নিয়মের একটি ছবি, আর নিয়ম বদলায়। ভালো ডিজাইনার জিজ্ঞেস করেন “কখনো কি হতে পারে?”, আর যেখানে নিয়ম অস্থির, সেখানে বদলের দাম কমানোর ব্যবস্থা রাখেন।

প্র ০২Django বা Rails-এর মতো জনপ্রিয় framework প্রতিটি টেবিলে নিজে থেকে একটি id কলাম দেয়, তাই অনেক ডেভেলপার কখনো weak entity বা composite key দেখেনই না। এটি কি ভালো অভ্যাস?

Framework-গুলোর এই ডিফল্টের পেছনে বাস্তব যুক্তি আছে। প্রতিটি সারির একটি একক, ছোট, অপরিবর্তনীয় id থাকলে URL বানানো সহজ (/installments/503), ORM-এর কোড সরল হয়, আর child টেবিলগুলো একটি কলাম দিয়েই আঙুল তোলে। কয়েক স্তর নিচে গেলে পার্থক্য স্পষ্ট হয়: অর্ডার → লাইন → শিপমেন্ট-লাইন — composite key হলে শেষ টেবিলের foreign key তিন কলামের, আর প্রতিটি JOIN-এ তিনটি শর্ত। পর্ব ৬-এর surrogate key আলোচনার সব যুক্তি এখানেও খাটে।

সমস্যা হয় যখন id-টি একমাত্র key হয়ে যায়। ER মডেলের partial key — “একটি ঋণে কিস্তি নং 3 একবারই” — একটি সত্যিকারের নিয়ম। Surrogate id সেই নিয়ম জানে না। UNIQUE (loan_id, inst_no) না দিলে একই কিস্তি দুবার ঢোকে: মোবাইল অ্যাপ দুর্বল নেটওয়ার্কে “জমা দিন” অনুরোধ দুবার চলে গেলেই হলো। তখন হিসাবে একই কিস্তি দুবার জমা দেখায়, আর ডেটাবেস কিছুই বলে না। Framework-গুলো এটি জানে — Django-তে UniqueConstraint, Rails-এ uniqueness validation আর database index — কিন্তু ডেভেলপারকে নিজে লিখতে হয়। ডিফল্ট আপনাকে id দেয়, নিয়ম দেয় না।

দ্বিতীয় দিকটি existence dependency. weak entity হিসেবে ভাবলে স্বাভাবিকভাবেই প্রশ্ন আসে: owner গেলে কী হবে? surrogate id-র জগতে এই প্রশ্ন প্রায়ই ভুলে যাওয়া হয়, আর ফল হয় অনাথ সারি বা ভুল CASCADE. তৃতীয় দিকটি কর্মক্ষমতা: InnoDB-র মতো clustered storage-এ (loan_id, inst_no) key একটি ঋণের কিস্তিগুলো পাশাপাশি রাখে; surrogate id-তে সেই সুবিধা পেতে আলাদা index লাগে।

তাই উত্তরটি “হ্যাঁ, তবে”: surrogate id ব্যবহার করা ভালো অভ্যাস হতে পারে, যদি ER মডেলে weak entity চিনে তার partial key-কে UNIQUE হিসেবে, আর তার existence dependency-কে সচেতন ON DELETE নিয়ম হিসেবে টেবিলে রাখেন।

মূল উপলব্ধি: Weak entity একটি ধারণাগত সত্য, টেবিলের key বাছাই একটি বাস্তবায়নের সিদ্ধান্ত। বাস্তবায়ন যেটিই হোক, ধারণাগত নিয়মটি — owner-এর ভেতরে অনন্য, owner ছাড়া অস্তিত্বহীন — কোথাও না কোথাও constraint হয়ে থাকতে হবে।

প্র ০৩কোন অবস্থায় একটি ternary relationship তিনটি binary-তে কোনো তথ্য না হারিয়ে ভাঙা যায়? আর ডিজাইনে এই সিদ্ধান্ত নেবেন কীভাবে?

অংশ ৬-এর ব্যবস্থাপত্রে ভাঙন ব্যর্থ হয়েছিল, কারণ তিনটি জোড়া-সম্পর্ক সত্য হলেও তিন-পক্ষের ঘটনাটি সত্য ছিল না। ভাঙন সফল হয় ঠিক তখনই, যখন উল্টোটা ব্যবসার নিয়ম: “যদি ডাক্তার d রোগী p-কে দেখেন, p ওষুধ m পান, আর d ওষুধ m লেখেন — তাহলে d অবশ্যই p-কে m লিখেছেন।” এই নিয়মকে relational তত্ত্বে বলে join dependency: relation-টি তার তিনটি projection-এর join-এর সমান। এমন নিয়ম থাকলে ternary টেবিলে পুনরাবৃত্তি থাকে, আর তিনটি টেবিলে ভাঙাই ভালো ডিজাইন — এটিই পর্ব ৩৭-এর 5NF (Project-Join Normal Form). C. J. Date-এর পাঠ্যবইয়ের supplier–part–project উদাহরণ এই আলোচনার জন্যই বিখ্যাত।

বাস্তবে এমন নিয়ম বিরল, আর ডেটা দেখে ধরা প্রায় অসম্ভব — আজকের ডেটায় join করে আসল সংখ্যা পেলেও কাল একটি নতুন সারি ভুয়া সমন্বয় তৈরি করতে পারে। তাই প্রশ্নটি করুন ব্যবসাকে: “একটি ঘটনার তিনটি অংশ কি স্বাধীনভাবে ঠিক হয়?” কিছু উদাহরণ ভাবুন। একটি কোচিং সেন্টারে: শিক্ষক কোন বিষয় পড়াতে পারেন, কোন ব্যাচে কোন বিষয় আছে, কোন শিক্ষক কোন ব্যাচে যান — যদি নিয়ম হয় “যোগ্য শিক্ষক ব্যাচে গেলে ব্যাচের সব বিষয়ের যেগুলো তিনি পারেন, সবই পড়ান”, তাহলে তিনটি binary যথেষ্ট। কিন্তু যদি নির্দিষ্ট করে বলা থাকে “Karim স্যার ব্যাচ A-তে শুধু পদার্থবিজ্ঞান পড়ান, যদিও রসায়নও পারেন”, তাহলে ternary লাগবে।

আরেকটি সূক্ষ্মতা: ternary-র প্রতিটি প্রান্তের cardinality থেকেই টেবিলের key ঠিক হয়। কোনো প্রান্তে “1” থাকলে (প্রতিটি (d, p) জোড়ার জন্য একটিই ওষুধ) key হয় বাকি দুটি কলাম; সব প্রান্ত “অনেক” হলে key তিনটি কলামই। আর “1” যদি আসলে কোনো binary নিয়ম হয়, তবে সেটি আলাদা binary হিসেবে মডেল করাই ঠিক।

মূল উপলব্ধি: ternary আর তিনটি binary আলাদা তথ্য বলে। ভাঙন নিরাপদ শুধু তখন, যখন ব্যবসা নিশ্চিত করে যে তিন-পক্ষের ঘটনাটি তিনটি জোড়া থেকে নির্ধারিত — আর সেই নিশ্চয়তা ডেটা থেকে নয়, নিয়ম থেকে আসে।

প্র ০৪“প্রতিটি অর্ডারে অন্তত একটি পণ্য থাকবে” — এই total participation SQL ডেটাবেসে কীভাবে রক্ষা করবেন? NOT NULL কেন যথেষ্ট নয়?

“প্রতিটি অর্ডারের গ্রাহক থাকবে” রক্ষা করা সহজ, কারণ তথ্যটি orders-এর সারিতেই থাকে: customer_id NOT NULL আর REFERENCES. কিন্তু “প্রতিটি অর্ডারের অন্তত একটি লাইন” নিয়মটি orders-এর কোনো কলামে নেই; এটি order_items-এ এমন সারির অস্তিত্ব দাবি করে। NOT NULL, CHECK বা foreign key — কোনোটিই অন্য টেবিলে সারি আছে কি না গুনতে পারে না।

এর সাথে আসে মুরগি-আগে-না-ডিম সমস্যা। লাইন ঢোকাতে আগে অর্ডার লাগে (order_items.order_id-এর foreign key), আর অর্ডার বৈধ হতে লাইন লাগে। কোনো একটি স্টেটমেন্টের শেষে দুটি নিয়ম একসাথে সত্য হতে পারে না। সমাধান হলো যাচাইকে স্টেটমেন্টের শেষ থেকে transaction-এর শেষে সরানো। SQL standard এর জন্য CREATE ASSERTION রেখেছে — “এমন কোনো অর্ডার থাকবে না যার লাইন নেই” — কিন্তু পর্ব ৬-এ দেখেছেন, প্রধান DBMS-গুলোর কোনোটিই এটি বাস্তবায়ন করেনি। PostgreSQL-এ foreign key-কে DEFERRABLE INITIALLY DEFERRED করা যায়, যাতে যাচাই হয় commit-এর সময়; আর “অন্তত একটি” যাচাইয়ের জন্য deferred constraint trigger (CREATE CONSTRAINT TRIGGER … DEFERRABLE) লেখা যায়, যা commit-এর আগে প্রতিটি নতুন অর্ডারের লাইন গোনে।

বাস্তবে বেশিরভাগ দল আরও সরল পথ নেয়: অর্ডার আর তার লাইনগুলো একটি transaction-এ একসাথে লেখা (পর্ব ৪৪), অ্যাপে “খালি কার্টে অর্ডার নয়” যাচাই, আর লাইন মোছার সময় শেষ লাইনটি মুছলে অর্ডারও বাতিল। সাথে একটি নিয়মিত পাহারাদার কোয়েরি — LEFT JOIN করে লাইনহীন অর্ডার খোঁজা (পর্ব ২০-এর anti join) — যাতে কোনো ফাঁক দিয়ে কিছু ঢুকলে ধরা পড়ে। একসাথে দুটি অনুরোধ এলে (একজন শেষ লাইন মুছছেন, আরেকজন অর্ডার নিশ্চিত করছেন) অ্যাপের যাচাই race condition-এ হারতে পারে, তাই এখানে isolation-এর প্রশ্নও আসে (পর্ব ৪৭)।

মূল উপলব্ধি: যে participation নিয়ম নিজের সারিতে লেখা যায় (foreign key-র দিক), তা NOT NULL রক্ষা করে। যেটি অন্য টেবিলে সারির অস্তিত্ব দাবি করে, তার জন্য transaction-স্তরের যাচাই লাগে — ER ডায়াগ্রামে দুটি একই রকম দুই লাইন, কিন্তু বাস্তবায়নের দাম একেবারে আলাদা।

প্র ০৫একই ডেটাবেসের জন্য পাঠ্যবইয়ে Chen notation, অফিসের টুলে crow's foot, আর কোনো দলে UML. এত notation কেন টিকে আছে, আর দলে কাজ করার সময় কী নিয়ম মানবেন?

প্রতিটি notation একটি আলাদা প্রয়োজন থেকে এসেছে। Chen-এর ১৯৭৬ সালের notation ছিল ধারণাগত — relationship নিজেই একটি চিহ্ন (রম্বস), তার attribute থাকতে পারে, ternary আঁকা স্বাভাবিক, আর weak entity-র জন্য আলাদা চিহ্ন। তাই তত্ত্ব শেখানো আর পরীক্ষার জন্য এটি আদর্শ। কিন্তু বড় ডায়াগ্রামে প্রতিটি attribute-এর ডিম্বাকৃতি আর প্রতিটি সম্পর্কের রম্বস জায়গা খেয়ে ফেলে; শ-খানেক টেবিলের ডায়াগ্রাম পড়া কঠিন হয়ে যায়।

Crow's foot এসেছে টেবিলের কাছাকাছি থাকার চাহিদা থেকে। প্রতিটি বাক্স প্রায় একটি টেবিল, ভেতরে কলামের তালিকা, আর রেখা প্রায় একটি foreign key. M:N সরাসরি আঁকা যায়, কিন্তু টুলগুলো সাধারণত একটি associative entity (junction table) বানিয়ে দুটি 1:N রেখায় ভেঙে দেয় — অর্থাৎ crow's foot ডায়াগ্রাম আসলে প্রায়ই logical model, পুরোপুরি conceptual নয়। Information Engineering আর Barker-এর notation শিল্পক্ষেত্রে এটিকে মানদণ্ড বানিয়েছে, তাই ডেটাবেস-ডিজাইন টুলগুলো সাধারণত এটিই আঁকে। UML এসেছে object-oriented সফটওয়্যারের দিক থেকে; ডেভেলপাররা ক্লাস আর টেবিল একই ভাষায় আঁকতে চেয়েছেন।

সবচেয়ে বিপজ্জনক পার্থক্যটি এই পর্বে দেখেছেন: (min,max) নিজের দিকে তাকিয়ে পড়ে, আর Chen-এর 1/N, crow's foot ও UML multiplicity পড়ে ওপারে তাকিয়ে। একই সংখ্যা ভুল প্রান্তে পড়লে নিয়মটি উল্টে যায় — “প্রতিটি অর্ডারের একজন গ্রাহক” হয়ে যায় “প্রতিটি গ্রাহকের একটি অর্ডার”। মিশ্র দলে, বা পাঠ্যবই থেকে অফিসে আসা নতুন কর্মীর হাতে, এমন ভুল খুব স্বাভাবিক।

দলে কাজের জন্য কয়েকটি নিয়ম: (১) প্রকল্পের শুরুতে একটি notation বেছে নিন, আর ডায়াগ্রামের কোণে একটি ছোট legend রাখুন। (২) প্রতিটি সম্পর্কের পাশে বা নথিতে একটি বাক্যে নিয়মটি লিখুন — “একটি অর্ডার ঠিক একজন গ্রাহকের; একজন গ্রাহকের অর্ডার শূন্য বা অনেক।” বাক্য কোনো notation-এর ওপর নির্ভর করে না। (৩) সম্ভব হলে ডায়াগ্রাম কোড থেকে বানান (যেমন schema থেকে স্বয়ংক্রিয় ডায়াগ্রাম), যাতে ছবি আর আসল টেবিল কখনো আলাদা না হয়।

মূল উপলব্ধি: Notation হলো ভাষা, নিয়মটি হলো বক্তব্য। ভাষা যত রকমই হোক, প্রতিটি সম্পর্কের নিয়ম একটি সাধারণ বাক্যে লিখে রাখলে কোনো চিহ্ন ভুল পড়ার সুযোগ থাকে না।

অনুশীলন

খাতা-কলম নিন। একটি নিয়ম থেকে সিদ্ধান্ত, একটি কোয়েরি লেখা, একটি হাতে হিসাব, একটি ডিজাইন।

  1. নিয়ম থেকে cardinality আর participation: নিচের প্রতিটির জন্য অনুপাত (1:1 / 1:N / M:N) আর দুই দিকের participation (total / partial) লিখুন, আর (min,max) জোড়া দিন। (ক) জেলা — উপজেলা: প্রতিটি উপজেলা একটি জেলার, প্রতিটি জেলায় অন্তত একটি উপজেলা। (খ) মোবাইল ওয়ালেট অ্যাকাউন্ট — নিবন্ধিত সিম নম্বর: একটি অ্যাকাউন্ট একটি নম্বরে, একটি নম্বরে সর্বোচ্চ একটি অ্যাকাউন্ট, সব নম্বরে অ্যাকাউন্ট নেই। (গ) লেখক — বই: একটি বইয়ের অন্তত একজন লেখক, একজন লেখক অনেক বই লিখতে পারেন, ডেটাবেসে এমন লেখকও আছেন যাঁর বই এখনো তালিকাভুক্ত হয়নি।

    (ক) 1:N (জেলা : উপজেলা)। উপজেলার দিক total (প্রতিটি উপজেলা একটি জেলার) — (1,1); জেলার দিকও total (অন্তত একটি উপজেলা) — (1,N). টেবিলে upazilas.district_id NOT NULL REFERENCES districts; জেলার “অন্তত একটি” নিয়মটি NOT NULL দিয়ে রক্ষা হয় না (ভাবনার প্রশ্ন ৪)।

    (খ) 1:1. অ্যাকাউন্টের দিক total — (1,1); নম্বরের দিক partial — (0,1). টেবিলে wallet_accounts.sim_id NOT NULL UNIQUE REFERENCES sims: foreign key বসেছে total দিকে, UNIQUE দিয়ে 1:1.

    (গ) M:N. বইয়ের দিক total — (1,N); লেখকের দিক partial — (0,N). টেবিলে junction table book_authors (book_id, author_id), key দুটি মিলে। বইয়ের “অন্তত একজন লেখক” নিয়মটি transaction-স্তরে যাচাই করতে হবে।

  2. কোয়েরি লিখুন: db-uni-তে এমন একটি কোয়েরি লিখুন, যা প্রত্যেক শিক্ষার্থীর নাম আর তিনি কয়টি ভর্তি-সারিতে (enrollments) আছেন তা দেখায় — যাঁর কোনো ভর্তি নেই, তাঁকেও 0 দিয়ে। ফলে কয়টি সারি আসবে, আর কে 0 পাবেন? এ থেকে students–enrollments সম্পর্কে শিক্ষার্থীর participation সম্পর্কে কী বলা যায়? (উপরের db-uni এডিটরের কোড বদলে চালাতে পারবেন।)
    SELECT s.student_id, s.name, COUNT(e.course_id) AS enrollments
    FROM students s
    LEFT JOIN enrollments e ON e.student_id = s.student_id
    GROUP BY s.student_id, s.name
    ORDER BY enrollments, s.student_id;

    ৮টি সারি আসবে (৮ জন শিক্ষার্থী)। প্রথম সারি 2302 | Habib Ullah | 0; বাকিরা 1 থেকে 3 (2101 আর 2201-এর 3টি করে)। Habib-এর 0 প্রমাণ করে এই ডেটায় শিক্ষার্থীর দিকে participation partial. এটি নিয়মের লঙ্ঘন কি না, তা নির্ভর করে বিশ্ববিদ্যালয়ের নিয়মে — সদ্য ভর্তি হওয়া শিক্ষার্থীর কোর্স নিবন্ধন না-ও থাকতে পারে। COUNT(*) লিখলে Habib-এর ফল ভুল করে 1 আসত।

  3. ফল আগে বলুন (trace): teams টেবিলে তিনটি দল: (1, Rajshahi), (2, Sylhet), (3, Khulna). players (player_id, name, team_id)-এ চারজন: (10, Arif, 1), (11, Babu, 1), (12, Chayon, 2), (13, Dipu, NULL). হাতে হিসাব করে বলুন: (ক) SELECT t.name, COUNT(*) FROM teams t LEFT JOIN players p ON p.team_id = t.team_id GROUP BY t.team_id; আর (খ) একই কোয়েরিতে COUNT(*)-এর বদলে COUNT(p.player_id) — দুটির ফল কী? কোনটি ঠিক, আর Dipu কোথাও আসেন কি না?

    LEFT JOIN-এর পর সারিগুলো: (Rajshahi, Arif), (Rajshahi, Babu), (Sylhet, Chayon), (Khulna, NULL) — Khulna-র কোনো খেলোয়াড় নেই, তাই NULL-ভরা একটি সারি।

    (ক) COUNT(*) সারি গোনে: Rajshahi 2, Sylhet 1, Khulna 1 — ভুল! (খ) COUNT(p.player_id) শুধু non-NULL মান গোনে: Rajshahi 2, Sylhet 1, Khulna 0 — সঠিক। Dipu কোনো ফলে আসেন না: তাঁর team_id NULL, তাই তিনি কোনো দলের সাথে মেলেন না, আর LEFT JOIN-এর বাঁ দিকে আছে teams, players নয়। দলের দিকে participation partial (Khulna), খেলোয়াড়ের দিকেও partial (Dipu).

  4. ডিজাইন করুন: একটি হাসপাতালের ভর্তি-ব্যবস্থা: রোগী (patient), ভর্তি (admission — একজন রোগী বিভিন্ন সময়ে অনেকবার ভর্তি হন, প্রতিবার ভর্তির নম্বর 1, 2, 3… রোগীভিত্তিক), ওয়ার্ড (ward), আর বেড (bed — প্রতিটি ওয়ার্ডে বেড নং 1 থেকে শুরু)। প্রতিটি ভর্তিতে একটি বেড বরাদ্দ হয়। কোনগুলো weak entity, তাদের owner আর partial key কী, প্রতিটি সম্পর্কের cardinality আর participation কী, আর টেবিলগুলোর primary key কী হবে?
    • Weak entity: ADMISSION (owner PATIENT, partial key admission_no) আর BED (owner WARD, partial key bed_no). দুটির ক্ষেত্রেই নম্বর owner-এর ভেতরে শুরু হয় 1 থেকে।
    • PATIENT — ADMISSION: 1:N; admission-এর দিক total (1,1), রোগীর দিক partial (0,N) — শুধু বহির্বিভাগে দেখানো রোগীর ভর্তি নেই।
    • WARD — BED: 1:N; বেডের দিক total (1,1), ওয়ার্ডের দিক সাধারণত total (1,N).
    • ADMISSION — BED: প্রতিটি ভর্তিতে একটি বেড (1,1), একটি বেড সময়ের সাথে অনেক ভর্তিতে (0,N) — N:1. “একই সময়ে একটি বেডে একজনই” নিয়মটি সময়-নির্ভর; এটি সাধারণ UNIQUE দিয়ে লেখা যায় না (PostgreSQL-এর exclusion constraint বা অ্যাপের যাচাই লাগে)।
    • টেবিল: patients (patient_id PK), admissions (patient_id, admission_no, admitted_on, ward_id, bed_no, PK (patient_id, admission_no)), wards (ward_id PK), beds (ward_id, bed_no, PK (ward_id, bed_no)). admissions থেকে beds-এ foreign key দুই কলামের: (ward_id, bed_no) REFERENCES beds. রোগীর রেকর্ড চিকিৎসা-ইতিহাস, তাই admissions → patients-এ CASCADE নয়, RESTRICT.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি নতুন relationship-কে ডিজাইন থেকে টেবিলে আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. সম্পর্কের পক্ষ (entity set) আর ব্যবসার নিয়ম চিহ্নিত করা
  2. “একটি X-এর জন্য সর্বোচ্চ কতটি Y?” — প্রথম দিকের প্রশ্ন
  3. “একটি Y-এর জন্য সর্বোচ্চ কতটি X?” — উল্টো দিকের প্রশ্ন
  4. প্রতিটি দিকের সর্বনিম্ন ঠিক করা — total না partial
  5. Cardinality দেখে foreign key, junction table বা UNIQUE বসানো

একটি কোম্পানির নিয়ম: প্রতিটি কর্মী ঠিক একটি বিভাগে কাজ করেন, আর একটি বিভাগে অনেক কর্মী। employees আর departments টেবিলে এই সম্পর্ক রাখার সঠিক উপায় কোনটি?

  • departments টেবিলে employee_id কলাম
  • employees টেবিলে dept_id foreign key, NOT NULL-সহ
  • আলাদা employee_departments junction table
  • employees.dept_id-এ UNIQUE constraint

CUSTOMER আর ORDER-এর places সম্পর্কের নিয়ম: প্রতিটি অর্ডার ঠিক একজন গ্রাহকের, আর একজন গ্রাহকের অর্ডার শূন্য বা অনেক। (min,max) notation-এ ORDER-এর পাশে কী লেখা হবে?

  • (0,N)
  • (0,1)
  • (1,1)
  • (1,N)

একটি ঋণের কিস্তিগুলোর নম্বর প্রতিটি ঋণে 1 থেকে শুরু হয়। INSTALLMENT-কে weak entity ধরলে তার key কী?

  • (loan_id, inst_no)
  • inst_no
  • loan_id
  • (due_date, amount)

citizens আর nid_cards টেবিলে নাগরিক–NID সম্পর্কটি 1:1 রাখতে হবে। nid_cards-এ card_no primary key আর citizen_id একটি foreign key. আর কোনটি অবশ্যই লাগবে?

  • citizen_id-এ ON DELETE CASCADE
  • একটি আলাদা junction table
  • citizen_id-এ CHECK (citizen_id > 0)
  • citizen_id-এ UNIQUE constraint

prescribes (doctor, patient, medicine) একটি ternary সম্পর্ক। একে doctor–patient, patient–medicine আর doctor–medicine — তিনটি binary টেবিলে ভেঙে পরে তিনটিকে join করলে সাধারণভাবে কী ঘটে?

  • সবসময় আসল টেবিলটিই হুবহু ফিরে আসে
  • আসল সব সারি থাকে, কিন্তু কখনো কখনো এমন সারিও আসে যা কখনো ঘটেনি
  • কিছু আসল সারি হারিয়ে যায়, বাড়তি কিছু আসে না
  • join করাই যায় না, কারণ কোনো সাধারণ কলাম নেই

customers (৩ জন) আর orders টেবিল: গ্রাহক 1-এর ২টি অর্ডার, গ্রাহক 2-এর ১টি, গ্রাহক 3-এর কোনো অর্ডার নেই। SELECT c.customer_id, COUNT(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id GROUP BY c.customer_id; চালালে গ্রাহক 3-এর পাশে কী আসবে?

  • 0
  • NULL
  • 1
  • গ্রাহক 3 ফলে আসবেন না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Cardinality = সর্বোচ্চ (1:1, 1:N, M:N); বের করুন দুই দিকের প্রশ্নে, উত্তর নিন ব্যবসার নিয়ম থেকে।
  2. Participation = সর্বনিম্ন: total (দুই লাইন, min ≥ 1) বা partial (min = 0); ডেটায় খুঁজুন LEFT JOIN + COUNT(column) দিয়ে।
  3. (min,max) পড়ে নিজের দিকে, crow's foot ও Chen-এর 1/N পড়ে ওপারে — প্রান্ত উল্টে যায়।
  4. Weak entity-র key = owner-এর key + partial key; owner ছাড়া অস্তিত্ব নেই (প্রায়ই CASCADE); surrogate id নিলেও partial key-তে UNIQUE.
  5. 1:N → FK “অনেক” দিকে; M:N → junction table; 1:1 → UNIQUE FK; ternary ≠ তিনটি binary.

পরের পর্বে: সব শিক্ষার্থী কি একই রকম? স্নাতক আর স্নাতকোত্তর শিক্ষার্থীর কিছু তথ্য আলাদা, কিছু একই। পর্ব ০৯-এ EER — specialization, generalization আর aggregation দিয়ে “ধরনের ভেতরে ধরন” আঁকা, আর এই পর্বের participation ধারণা সেখানে কীভাবে আবার ফিরে আসে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান