এই পর্বে যা শিখবেন
- Elmasri–Navathe ধাঁচের mapping অ্যালগরিদমের প্রতিটি ধাপ — নিয়ম, ER-এর টুকরো আর তার থেকে তৈরি CREATE TABLE
- Cardinality আর participation দেখে foreign key কোন টেবিলে বসবে, NOT NULL আর UNIQUE লাগবে কি না
- Weak entity, multi-valued attribute, n-ary, specialization, derived আর recursive relationship-এর সঠিক অনুবাদ
- একটি পুরো লাইব্রেরির ER থেকে কাজ-করা schema: PK/FK বাছাই, ON DELETE সিদ্ধান্ত, নমুনা ডেটায় কোয়েরি চালিয়ে যাচাই
- নামকরণের রীতি, NULL-ভরা ডিজাইন বনাম ভাগ করা, আর একটি বিদ্যমান schema থেকে ER উদ্ধার (reverse engineering)
১ · নকশা থেকে দালান — কেন নিয়ম মেনে অনুবাদ
পর্ব ৭-এ আপনি ER মডেলে entity, attribute আর relationship আঁকতে শিখেছেন; পর্ব ৮-এ cardinality, participation আর weak entity; পর্ব ৯-এ specialization আর generalization. সেগুলো ছিল কাগজের নকশা — কোন জিনিস আছে, কীভাবে জড়িত। কিন্তু DBMS ছবি বোঝে না, বোঝে টেবিল, কলাম, primary key আর foreign key. ER diagram থেকে এই টেবিলগুলো বানানোর কাজটিকে বলে ER-to-relational mapping. এটি conceptual নকশা থেকে logical নকশায় যাওয়ার সেতু (পর্ব ৪-এর conceptual → logical model).
একজন স্থপতি বাড়ির নকশা আঁকেন: কোথায় শোবার ঘর, কোথায় রান্নাঘর, কোন দরজা কোন ঘরে খোলে। রাজমিস্ত্রি সেই নকশা দেখে ইট গাঁথেন — আর তাঁর কিছু বাঁধা নিয়ম আছে: দরজার ওপরে লিন্টেল, দুই ঘরের মাঝে একটি দেয়াল। নকশায় “লিন্টেল” লেখা থাকে না, কিন্তু অভিজ্ঞ মিস্ত্রি জানেন কোথায় দিতে হবে। ER diagram হলো নকশা, mapping-এর নিয়ম হলো মিস্ত্রির বাঁধা নিয়ম। নিয়ম জানলে অনুবাদ প্রায় যন্ত্রের মতো হয় — আর যেখানে বিচার লাগে, সেখানে আপনি জানবেন যে বিচার লাগছে।
পাঠ্যবইয়ে এই অনুবাদকে কয়েকটি ধাপে সাজানো হয়। সবচেয়ে প্রচলিত রূপটি Ramez Elmasri ও Shamkant Navathe-এর Fundamentals of Database Systems বইয়ের: ER-এর জন্য ৭টি ধাপ, EER-এর জন্য আরও ২টি (specialization আর union type বা category). এই পর্বে আমরা প্রথম ৮টি ধাপ সেভাবেই নেব, আর ধাপ ৯-এ বইয়ের category-র বদলে বাস্তবে বেশি দরকারি দুটি বিষয় রাখব — derived attribute আর recursive relationship (বইয়ে recursive relationship ধাপ ৩–৫-এর ভেতরেই পড়ে)। ধাপগুলো এক নজরে:
| ধাপ | ER-এ যা আছে | Relational schema-য় যা হয় |
|---|---|---|
| ১ | Strong (regular) entity | একটি টেবিল; simple attribute কলাম, composite attribute ভেঙে কলাম, key → PRIMARY KEY |
| ২ | Weak entity | একটি টেবিল; মালিকের key (FK) + partial key = composite PRIMARY KEY, ON DELETE CASCADE |
| ৩ | Binary 1:1 | total দিকের টেবিলে FK + UNIQUE (বা দুই টেবিল এক করা) |
| ৪ | Binary 1:N | N দিকের টেবিলে FK; relationship-এর attribute-ও সেখানে |
| ৫ | Binary M:N | নতুন junction টেবিল: দুই FK মিলে PK, relationship-এর attribute এখানে |
| ৬ | Multi-valued attribute | নতুন টেবিল: মালিকের FK + মান, দুটি মিলে PK |
| ৭ | n-ary relationship (n > 2) | নতুন টেবিল: সব অংশগ্রহণকারীর FK |
| ৮ | Specialization / generalization | তিন কৌশলের একটি: superclass + subclass টেবিল, শুধু subclass, বা এক টেবিল |
| ৯ | Derived attribute, recursive relationship | derived সাধারণত জমা নয় (VIEW / generated column); recursive → একই টেবিলে FK |
ক্রমটি এলোমেলো নয়। Weak entity-র টেবিল বানাতে মালিকের key লাগে, তাই strong entity আগে। Relationship-এর FK বসাতে দুই পাশের টেবিল আগে থেকে থাকা চাই। আর M:N-এর junction টেবিলের key আসে দুই পাশের key থেকে — যার মধ্যে weak entity-র composite key-ও থাকতে পারে।
১৯৭৬ সালে Peter Chen ACM Transactions on Database Systems-এর প্রথম সংখ্যায় “The Entity-Relationship Model — Toward a Unified View of Data” প্রবন্ধে ER মডেল প্রস্তাব করেন। সেই প্রবন্ধেই তিনি দেখান, entity আর relationship-কে কীভাবে টেবিলে নেওয়া যায় — অর্থাৎ mapping-এর ধারণা ER-এর জন্মের সাথেই এসেছে। ১৯৮৬ সালে Toby Teorey, Dongqing Yang ও James Fry ACM Computing Surveys-এ extended ER থেকে relational ডিজাইনের একটি ধাপে-ধাপে পদ্ধতি প্রকাশ করেন, যা পরের পাঠ্যবইগুলোকে প্রভাবিত করে। আজ প্রায় সব database টেক্সটবুকে এই অ্যালগরিদমের কোনো না কোনো রূপ আছে, আর MySQL Workbench বা বিভিন্ন ER টুল একটি diagram থেকে নিজেই CREATE TABLE লিখে দিতে পারে — ভেতরে ঠিক এই নিয়মগুলোই চলে।
এই পর্বের বেশিরভাগ এডিটর নিজের ছোট schema নিজে বানিয়ে নেয় (data-setup) — একটি লাইব্রেরির টেবিল: members (সদস্য), books (বই), authors (লেখক), copies (একটি বইয়ের প্রতিটি কপি), loans (ধার)। প্রতিটি এডিটরের নিচে “নমুনা ডেটাবেস” বাক্সে ঠিক কোন টেবিল আছে দেখতে পাবেন। কয়েকটি এডিটর ব্যবহার করে কোর্সের বিশ্ববিদ্যালয় ডেটাবেস db-uni: departments (dept_id, name, building), instructors (instructor_id, name, dept_id, salary, mentor_id — mentor আরেকজন শিক্ষক), students (student_id, name, dept_id, batch, cgpa), courses (course_id, title, dept_id, credits, instructor_id) আর enrollments (student_id, course_id, term, grade). অংশ ৭-এ দেখবেন এই টেবিলগুলো তার ER থেকে কীভাবে এসেছে।
২ · ধাপ ১–২ · Strong entity আর weak entity
ধাপ ১ — প্রতিটি strong entity একটি টেবিল
প্রতিটি strong entity type E-র জন্য একটি টেবিল বানান। E-র প্রতিটি simple attribute একটি কলাম। composite attribute (যেমন name = first + last) নিজে কলাম হয় না — তার সরল অংশগুলো আলাদা কলাম হয়। E-র key-গুলোর একটি হয় PRIMARY KEY; বাকি key-গুলো UNIQUE (alternate key, পর্ব ৬)। Multi-valued আর derived attribute এই ধাপে বাদ — তাদের নিয়ম আলাদা (ধাপ ৬, ৯)।
┌──────────────────────────────┐
│ MEMBER │ (member_id) = key, নিচে দাগ
├──────────────────────────────┤
│ member_id │
│ name ─┬─ first_name │ composite: দুই অংশ
│ └─ last_name │
│ address ─┬─ area │ composite: দুই অংশ
│ └─ city │
│ joined_on │
└──────────────────────────────┘
⇩ ধাপ ১
members(member_id, first_name, last_name, area, city, joined_on)
খেয়াল করুন, “name” আর “address” নামের কোনো কলাম নেই। Relational model-এর প্রতিটি ঘরে একটি atomic value থাকে (পর্ব ৫-এর first normal form). “Rahim Uddin, Mirpur, Dhaka” এক ঘরে রাখলে “ঢাকার সব সদস্য” খুঁজতে লেখার ভেতর থেকে শহর কেটে বের করতে হতো। আলাদা কলামে রাখলে সেটি একটি সরল WHERE. পুরো নাম দরকার হলে কোয়েরিতে জুড়ে নিলেই হয়:
-- ER: MEMBER(member_id, name(first, last), address(area, city), joined_on)
CREATE TABLE members (
member_id INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
area TEXT,
city TEXT,
joined_on TEXT NOT NULL
);
INSERT INTO members VALUES (1, 'Rahim', 'Uddin', 'Mirpur', 'Dhaka', '2024-01-10'),
(2, 'Karima', 'Begum', 'Agrabad', 'Chattogram', '2024-03-05');
SELECT member_id, first_name || ' ' || last_name AS full_name, city
FROM members ORDER BY last_name;প্রথমে “✓ CREATE TABLE”, তারপর “✓ INSERT — ২টি সারি বদলেছে”। SELECT-এর ফলে দুটি সারি, last_name অনুযায়ী সাজানো: 2 | Karima Begum | Chattogram আর 1 | Rahim Uddin | Dhaka (B আগে, U পরে)। composite attribute ভেঙে রাখায় দুই দিকের সুবিধাই পাওয়া গেল: last_name দিয়ে সাজানো যায়, আবার || দিয়ে জুড়ে পুরো নামও দেখানো যায়। || হলো standard SQL-এর string জোড়ার অপারেটর; SQLite, PostgreSQL আর Oracle এটি বোঝে, MySQL ডিফল্টভাবে বোঝে CONCAT() (পর্ব ১৭)। NOT NULL কোথায় বসবে, তা ER থেকে আসে না — আসে ব্যবসার নিয়ম থেকে: নাম ছাড়া সদস্য হয় না, কিন্তু ঠিকানা পরে জানা যেতে পারে।
Composite attribute ভাঙার সময় একটি বিচার লাগে: কতটা ভাঙবেন? বাংলাদেশের ঠিকানা “বাড়ি ১২, রোড ৫, মিরপুর ১০, ঢাকা ১২১৬” — প্রতিটি অংশ আলাদা কলাম করবেন, নাকি “address_line” আর “city”? নিয়মটি হলো: যে অংশ দিয়ে আপনি খুঁজবেন, সাজাবেন বা গুনবেন, সেটি আলাদা কলাম হওয়া উচিত। যে অংশ শুধু ছাপা হবে, তা একসাথে থাকলেও ক্ষতি নেই।
ধাপ ২ — weak entity: মালিকের key + partial key
পর্ব ৮-এ দেখেছেন, weak entity-র নিজের পূর্ণ key নেই; তাকে চেনা যায় শুধু তার মালিক (owner entity) আর নিজের partial key মিলিয়ে। লাইব্রেরির প্রতিটি বইয়ের কয়েকটি কপি থাকে: “Learning SQL-এর কপি ২”। “কপি ২” একা কিছু বোঝায় না — প্রতিটি বইয়েরই কপি ২ থাকতে পারে। মালিক BOOK আর COPY-কে জোড়ে একটি identifying relationship, আর COPY-র অংশগ্রহণ সেখানে total.
প্রতিটি weak entity W-র জন্য একটি টেবিল বানান, W-র attribute-গুলো কলাম। মালিকের primary key-কে foreign key হিসেবে যোগ করুন। টেবিলের primary key = মালিকের key + W-র partial key (একটি composite key). FK-তে সাধারণত ON DELETE CASCADE, কারণ মালিক না থাকলে weak entity-র কোনো অস্তিত্ব থাকে না।
┌───────────┐ ◇◇───────◇◇ ╔═════════════╗
│ BOOK │──1──◇◇ HAS_COPY ◇◇══N══║ COPY ║ ══ = total participation
│ book_id │ ◇◇───────◇◇ ║ copy_no ┄┄┄ ║ ┄┄ = partial key
│ title │ ║ shelf ║
└───────────┘ ╚═════════════╝
⇩ ধাপ ২
copies(book_id, copy_no, shelf) book_id → books ON DELETE CASCADE
এবার চালিয়ে দেখুন। এই এডিটরের schema: books(book_id, title) — দুটি বই, আর copies(book_id, copy_no, shelf) — Learning SQL-এর ৩টি আর Database Design-এর ১টি কপি:
-- দুটি বইয়েরই 'copy 1' আছে: partial key একা যথেষ্ট নয়
SELECT book_id, copy_no, shelf FROM copies WHERE copy_no = 1;
-- মালিক (book 1) মুছলে তার কপিগুলোও যায়
DELETE FROM books WHERE book_id = 1;
SELECT * FROM copies;প্রথম SELECT-এ দুটি সারি: 1 | 1 | A-1 আর 2 | 1 | A-2. copy_no = 1 দুই বইয়েরই আছে — তাই copy_no একা key হতে পারে না, (book_id, copy_no) জোড়া লাগে। তারপর “✓ DELETE — ১টি সারি বদলেছে”, আর শেষ SELECT-এ শুধু 2 | 1 | A-2. Learning SQL মুছতেই তার তিনটি কপিও CASCADE-এ মুছে গেছে, যদিও বার্তায় গোনা হয়েছে শুধু books-এর ১টি সারি (পর্ব ৮-এ দেখেছেন, SQLite cascade-এ মোছা সারি গোনে না)। মালিকহীন কপি থাকলে সেটি হতো “কোন বইয়ের কপি, জানি না” — weak entity-র সংজ্ঞাই তা নিষেধ করে।
CASCADE এখানে ঠিক, সবখানে নয়। Weak entity মালিকের “অংশ”, তাই মালিকের সাথে চলে যাওয়াই স্বাভাবিক। কিন্তু অংশ ৬-এ দেখবেন, কপিটি যদি ধার-ইতিহাসে থাকে, তবে loans টেবিলের RESTRICT পুরো মোছাটাই আটকে দেবে — আর সেটাই চাই। একটি FK-র ON DELETE ঠিক করার সময় পুরো শৃঙ্খলটি দেখুন।
Weak entity-র মালিক নিজেও weak হতে পারে। ধরুন একটি বহুতল আবাসিক প্রকল্প: BUILDING → FLOOR (floor_no) → FLAT (flat_no). তখন flats-এর key হয় (building_id, floor_no, flat_no) — মালিকের পুরো composite key সাথে আসে। এজন্যই ধাপ ২-এ weak entity-গুলো মালিকের ক্রম অনুযায়ী বানাতে হয়।
৩ · ধাপ ৩–৫ · Binary relationship — 1:1, 1:N আর M:N
দুটি entity-র মধ্যে relationship-এর জন্য আলাদা টেবিল লাগবে কি না, আর লাগলে না হলে FK কোথায় বসবে — তা ঠিক করে cardinality ratio আর participation. আগে নিচের ডায়াগ্রামে তিন ধরনের সম্পর্কে FK বসানোর চেষ্টা ধাপে ধাপে দেখুন:
| সম্পর্ক | |
| FK কোথায় | |
| নতুন টেবিল |
- 1:N, ভুল দিক। একটি প্রকাশক অনেক বই ছাপে, প্রতিটি বই একটি প্রকাশকের। FK যদি publishers-এ রাখি, Tech Press-এর ঘরে লিখতে হবে “1, 2, 5” — এক ঘরে তিনটি মান। এটি atomic নয়, “বই 2 কোন প্রকাশকের?” খুঁজতে লেখা কাটতে হবে, আর FK constraint-ও বসানো যাবে না।
- 1:N, ঠিক দিক। FK books-এ: প্রতিটি বইয়ের ঘরে ঠিক একটি publisher_id. তিনটি বইয়ে 3, একটিতে 1 — মান পুনরাবৃত্তি হচ্ছে, কিন্তু প্রতিটি ঘরে একটিই মান। নিয়ম: 1:N-এ FK বসে N দিকের টেবিলে, কোনো নতুন টেবিল লাগে না।
- 1:1. একজন সদস্যের সর্বোচ্চ একটি কার্ড, প্রতিটি কার্ড ঠিক একজনের (card-এর দিকে total). FK রাখুন library_cards-এ, আর তাতে UNIQUE দিন — UNIQUE না দিলে এটি আসলে 1:N হয়ে যেত। উল্টো দিকে রাখলে কার্ডহীন Sabbir আর Tanvir-এর ঘরে NULL জমত।
- M:N, কোনো পাশে জায়গা নেই। Learning SQL-এর দুজন লেখক (1, 2), আবার Rafiq Ahmed দুটি বইয়ের লেখক (1, 4). books-এ FK রাখলে এক ঘরে দুটি মান, authors-এ রাখলেও তাই। 1:N-এর কৌশল এখানে দুই দিকেই ব্যর্থ।
- M:N, junction টেবিল। সম্পর্কটিকেই একটি টেবিল বানান: প্রতিটি (বই, লেখক) জোড়া একটি সারি। দুই FK মিলে primary key, আর relationship-এর attribute author_order (কে প্রথম লেখক) এখানেই থাকে — কারণ এটি বই বা লেখকের নয়, জোড়াটির গুণ।
ধাপ ৩ — 1:1: তিনটি পথ
1:1 relationship R(S, T)-এর জন্য তিনটি পথ। (ক) FK পথ (সবচেয়ে প্রচলিত): যে পাশের অংশগ্রহণ total, সেই টেবিলে অন্য পাশের primary key FK হিসেবে যোগ করুন, সাথে UNIQUE আর NOT NULL; R-এর attribute-ও এখানে। (খ) এক করা: দুই পাশই total হলে দুই entity-কে একটি টেবিলে মিলিয়ে দেওয়া যায়। (গ) আলাদা cross-reference টেবিল: দুই পাশই partial আর সম্পর্কটি কম ঘটে — তখন (s_id, t_id) টেবিল, দুই কলামেই UNIQUE.
লাইব্রেরি-কার্ডের উদাহরণটি চালিয়ে দেখুন। Schema: members(member_id, name) — Rahim, Karima, Sabbir; library_cards(card_no, member_id, issued_on), যেখানে member_id-তে NOT NULL UNIQUE. শুরুতে শুধু Rahim-এর কার্ড LC-1001 আছে:
-- Karima-র প্রথম কার্ড: ঢুকবে
INSERT INTO library_cards VALUES ('LC-1002', 2, '2024-03-05');
SELECT m.name, c.card_no FROM members m
LEFT JOIN library_cards c ON c.member_id = m.member_id;
-- Rahim-এর দ্বিতীয় কার্ড: UNIQUE এটি আটকাবে (1:1)
INSERT INTO library_cards VALUES ('LC-1003', 1, '2025-02-01');প্রথম INSERT-এ “✓ INSERT — ১টি সারি বদলেছে”। SELECT-এ তিনটি সারি: Rahim | LC-1001, Karima | LC-1002, Sabbir | NULL — LEFT JOIN কার্ডহীন সদস্যকেও দেখায় (পর্ব ১৯)। শেষ INSERT-এ ত্রুটি: UNIQUE constraint failed: library_cards.member_id. ER-এর “1” কথাটি টেবিলে টিকে আছে শুধু এই UNIQUE-এর জোরে। UNIQUE মুছে দিয়ে আবার চালান — দ্বিতীয় কার্ডটি ঢুকে যাবে, আর আপনার 1:1 নীরবে 1:N হয়ে যাবে। আর NOT NULL আছে কারণ কার্ডের দিকের অংশগ্রহণ total: মালিকহীন কার্ড হয় না।
ধাপ ৪ — 1:N: FK বসে N দিকে
1:N relationship-এ N দিকের টেবিলে (যে পাশের একটি সারি অন্য পাশের একটির সাথে যুক্ত) 1 দিকের primary key-কে FK হিসেবে যোগ করুন। Relationship-এর attribute-ও এই টেবিলে যায়। N দিকের অংশগ্রহণ total হলে FK-তে NOT NULL; partial হলে NULL চলবে।
CREATE TABLE publishers (
publisher_id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
city TEXT
);
CREATE TABLE books (
book_id INTEGER PRIMARY KEY,
isbn TEXT NOT NULL UNIQUE, -- natural key → alternate key
title TEXT NOT NULL,
pub_year INTEGER,
publisher_id INTEGER REFERENCES publishers(publisher_id) -- 1:N, N দিকে FK
);publisher_id-তে NOT NULL নেই — অর্থাৎ ডিজাইনার ধরে নিয়েছেন প্রকাশক অজানা এমন পুরোনো বইও লাইব্রেরিতে থাকতে পারে (BOOK-এর অংশগ্রহণ partial). নিয়ম বদলালে (প্রতিটি বইয়ের প্রকাশক জানতেই হবে) শুধু NOT NULL যোগ হবে। এভাবেই participation-এর তথ্য NOT NULL-এ অনুবাদ হয় — ER diagram-এর ডাবল লাইন টেবিলে হয়ে যায় চার অক্ষরের একটি শব্দ।
ধাপ ৫ — M:N: junction টেবিল
প্রতিটি M:N relationship R-এর জন্য একটি নতুন টেবিল বানান — একে junction table (associative বা bridge table-ও বলে)। দুই পাশের primary key দুটি FK হিসেবে আসে, আর সাধারণত দুটি মিলে টেবিলের primary key. R-এর নিজের attribute এই টেবিলের কলাম হয়।
এডিটরের schema: books (Learning SQL, Database Design, Data Structures), authors (Rafiq Ahmed, Nasreen Akter, Tanvir Alam) আর junction book_authors(book_id, author_id, author_order). Learning SQL আর Data Structures-এর দুজন করে লেখক:
-- কোন বইয়ের লেখক কারা, কোন ক্রমে?
SELECT b.title, ba.author_order, a.name
FROM book_authors ba
JOIN books b ON b.book_id = ba.book_id
JOIN authors a ON a.author_id = ba.author_id
ORDER BY b.title, ba.author_order;
-- উল্টো দিক: প্রত্যেক লেখকের কয়টি বই?
SELECT a.name, COUNT(*) AS books
FROM authors a JOIN book_authors ba ON ba.author_id = a.author_id
GROUP BY a.author_id ORDER BY a.name;প্রথম কোয়েরিতে ৫টি সারি: Data Structures | 1 | Rafiq Ahmed, Data Structures | 2 | Tanvir Alam, Database Design | 1 | Nasreen Akter, Learning SQL | 1 | Rafiq Ahmed, Learning SQL | 2 | Nasreen Akter. দ্বিতীয়টিতে ৩টি: Nasreen Akter | 2, Rafiq Ahmed | 2, Tanvir Alam | 1. একই junction টেবিল দুই দিকের প্রশ্নেরই উত্তর দেয় — M:N-এ কোনো “প্রধান” দিক নেই। Junction-এর দুই FK-র ON DELETE আলাদা: বই মুছলে তার লেখক-সারিগুলো যাক (CASCADE), কিন্তু যে লেখকের বই আছে তাঁকে মোছা চলবে না (RESTRICT). চাইলে INSERT INTO book_authors VALUES (1, 1, 3); যোগ করে দেখুন — composite PK একই জোড়া দ্বিতীয়বার ঢুকতে দেবে না।
1:1 আর 1:N-ও junction দিয়ে করা যায়। যেকোনো binary relationship-কে আলাদা টেবিলে নেওয়া সম্ভব (নিয়ম ৩-এর পথ গ)। 1:N-এর junction-এ শুধু N দিকের FK-টি primary key হয়, 1:1-এ দুটি কলামই আলাদাভাবে UNIQUE. লাভ: অনেক NULL এড়ানো যায় (যদি সম্পর্কটি খুব কম সারিতে থাকে), আর পরে সম্পর্কটি M:N হয়ে গেলে schema বদলাতে হয় না। ক্ষতি: প্রতিটি কোয়েরিতে একটি অতিরিক্ত JOIN. সাধারণ নিয়মে FK পথই ভালো।
৪ · ধাপ ৬–৭ · Multi-valued attribute আর n-ary relationship
ধাপ ৬ — এক সদস্যের অনেক ফোন নম্বর
বাংলাদেশে একজন মানুষের দুটি-তিনটি মোবাইল নম্বর থাকা খুব সাধারণ — একটি নিজের, একটি অফিসের, একটি বাড়ির জন্য। ER-এ phone তাই একটি multi-valued attribute (ডাবল ডিম্বাকার, পর্ব ৭)। কিন্তু relational টেবিলের এক ঘরে একটির বেশি মান রাখা যায় না।
প্রতিটি multi-valued attribute A-র জন্য একটি নতুন টেবিল বানান: মালিক entity-র primary key (FK) + A-র মান। দুটি মিলে primary key — একই সদস্যের একই নম্বর দুবার লেখার মানে নেই। FK-তে ON DELETE CASCADE: সদস্য না থাকলে তার নম্বরেরও মানে নেই।
┌────────────┐
│ MEMBER │──── (( phone )) (( )) = multi-valued
│ member_id │
└────────────┘
⇩ ধাপ ৬
member_phones(member_id, phone) member_id → members ON DELETE CASCADE
সহজ মনে হতে পারে “phone1, phone2, phone3” কলাম রাখা। কিন্তু তাতে তিনটি সমস্যা: চতুর্থ নম্বর এলে schema বদলাতে হয়; বেশিরভাগ সারিতে phone2, phone3 NULL থাকে; আর “এই নম্বরটি কার?” খুঁজতে তিন কলামে OR লিখতে হয়। আলাদা টেবিলে সব প্রশ্ন এক কলামে। এডিটরের schema: members(member_id, name) — Rahim, Karima, Sabbir; member_phones(member_id, phone) — Rahim-এর দুটি নম্বর, Karima-র একটি, Sabbir-এর নেই:
-- প্রতিটি নম্বর আলাদা সারি; নম্বর নেই এমন সদস্যও দেখাতে LEFT JOIN
SELECT m.name, p.phone
FROM members m LEFT JOIN member_phones p ON p.member_id = m.member_id
ORDER BY m.member_id, p.phone;
-- 'কোন সদস্যের নম্বর 01911000011?' — এক কলামে খোঁজ
SELECT m.name FROM members m
JOIN member_phones p ON p.member_id = m.member_id
WHERE p.phone = '01911000011';প্রথম কোয়েরিতে ৪টি সারি: Rahim | 01711000001, Rahim | 01911000011, Karima | 01811000002, Sabbir | NULL. Rahim দুবার এসেছেন কারণ তাঁর দুটি নম্বর — এটি duplicate নয়, দুটি আলাদা তথ্য। দ্বিতীয় কোয়েরির ফল একটি সারি: Rahim. একটি নকশা-প্রশ্ন রয়ে যায়: একই নম্বর কি দুজন সদস্যের হতে পারে (যেমন পরিবারের একটি সাধারণ নম্বর)? না হলে phone কলামে আলাদা UNIQUE দিন; হলে বর্তমান key-ই ঠিক। ER এই প্রশ্নের উত্তর দেয় না — ব্যবসা দেয়।
Multi-valued attribute যদি নিজেই composite হয় — যেমন প্রতিটি নম্বরের সাথে “ধরন” (personal/office) — তাহলে নতুন টেবিলে কলাম বাড়ে: member_phones(member_id, phone, phone_type). Key কী হবে, তা আবার নিয়ম দেখে: এক ধরনে একটিই নম্বর হলে (member_id, phone_type), নইলে (member_id, phone).
ধাপ ৭ — n-ary relationship: তিন বা তার বেশি অংশগ্রহণকারী
কখনো একটি ঘটনায় তিনটি entity একসাথে জড়িত, আর কোনো দুটি আলাদা করে সেটি বলতে পারে না। একটি গার্মেন্টস গ্রুপের কথা ভাবুন: কোন সরবরাহকারী (SUPPLIER) কোন কাপড় (FABRIC) কোন কারখানায় (FACTORY) কত গজ পাঠিয়েছে। এটি একটি ternary relationship SUPPLIES, যার attribute quantity_yards.
প্রতিটি n-ary relationship (n > 2)-এর জন্য একটি নতুন টেবিল: প্রতিটি অংশগ্রহণকারীর primary key একটি করে FK, আর relationship-এর attribute কলাম। Primary key সাধারণত সব FK মিলে। তবে কোনো অংশগ্রহণকারীর পাশে cardinality ১ হলে (বাকিদের একটি নির্দিষ্ট সমন্বয়ে সে একটিই), তার FK key থেকে বাদ যায়।
SUPPLIER ──M──◇ SUPPLIES ◇──N── FABRIC
│ (quantity_yards)
P
│
FACTORY
⇩ ধাপ ৭
supplies(supplier_id, fabric_id, factory_id, quantity_yards)
CREATE TABLE supplies (
supplier_id INTEGER REFERENCES suppliers(supplier_id),
fabric_id INTEGER REFERENCES fabrics(fabric_id),
factory_id INTEGER REFERENCES factories(factory_id),
quantity_yards INTEGER NOT NULL CHECK (quantity_yards > 0),
PRIMARY KEY (supplier_id, fabric_id, factory_id)
);একটি ternary-কে তিনটি binary দিয়ে বদলানো যায় না। “Karim Textiles ডেনিম সরবরাহ করে”, “Karim Textiles গাজীপুর কারখানায় মাল পাঠায়”, “গাজীপুর কারখানা ডেনিম পায়” — তিনটিই সত্য হতে পারে, অথচ Karim Textiles গাজীপুরে ডেনিম কখনো পাঠায়নি (ডেনিম সেখানে অন্য কেউ দিয়েছে)। তিনটি binary টেবিল JOIN করলে এই মিথ্যা সারিটি তৈরি হবে। ভাবনার প্রশ্ন ২-এ এর পুরো যুক্তি আর পর্ব ৩৭-এর 5NF-এর সাথে সম্পর্ক দেখবেন।
৫ · ধাপ ৮–৯ · Specialization, derived attribute আর recursive relationship
ধাপ ৮ — specialization: তিন কৌশল (পর্ব ৯-এর সংক্ষেপ)
পর্ব ৯-এ দেখেছেন, MEMBER-কে specialization করে STUDENT_MEMBER (প্রতিষ্ঠান, student ID) আর STAFF_MEMBER (পদবি) বানানো যায়। টেবিলে নেওয়ার প্রধান তিনটি কৌশল আছে (Elmasri–Navathe এগুলোকে ৮A, ৮B, ৮C বলেন; ৮D হলো একাধিক type-flag-ওয়ালা এক টেবিল)। নিচের ট্যাবে তিনটি পাশাপাশি দেখুন:
CREATE TABLE members (member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL, joined_on TEXT NOT NULL);
CREATE TABLE student_members (
member_id INTEGER PRIMARY KEY REFERENCES members(member_id) ON DELETE CASCADE,
institution TEXT NOT NULL, student_no TEXT NOT NULL);
CREATE TABLE staff_members (
member_id INTEGER PRIMARY KEY REFERENCES members(member_id) ON DELETE CASCADE,
designation TEXT NOT NULL);
-- যেকোনো specialization-এ চলে; subclass-এর তথ্য পেতে JOIN লাগেCREATE TABLE student_members (member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL, joined_on TEXT NOT NULL,
institution TEXT NOT NULL, student_no TEXT NOT NULL);
CREATE TABLE staff_members (member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL, joined_on TEXT NOT NULL,
designation TEXT NOT NULL);
-- শুধু total + disjoint হলে: প্রত্যেক সদস্য ঠিক একটি subclass-এ
-- 'সব সদস্য' পেতে UNION লাগে; member_id দুই টেবিলে আলাদা রাখার দায় আপনারCREATE TABLE members (member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL, joined_on TEXT NOT NULL,
member_type TEXT NOT NULL CHECK (member_type IN ('student', 'staff', 'general')),
institution TEXT, student_no TEXT, -- শুধু student-এর
designation TEXT, -- শুধু staff-এর
CHECK (member_type = 'student' OR (institution IS NULL AND student_no IS NULL)),
CHECK (member_type = 'staff' OR designation IS NULL));
-- JOIN নেই, দ্রুত; কিন্তু অনেক NULL, আর subclass-এর NOT NULL লিখতে CHECK লাগেকোনটি কখন? subclass কম আর তাদের নিজস্ব attribute অল্প হলে (গ) সরল ও দ্রুত। subclass-এর নিজস্ব attribute বেশি বা subclass-এর নিজের relationship থাকলে (ক) পরিষ্কার — যেমন শুধু STAFF_MEMBER বই অনুমোদন করতে পারে, তাহলে সেই FK staff_members-এর দিকে যাবে। (খ) কম ব্যবহৃত, কারণ total ও disjoint না হলে তথ্য হারায় বা দুই জায়গায় থাকে। এই তুলনা ORM-এর জগতেও আছে — ভাবনার প্রশ্ন ৩-এ দেখবেন।
ধাপ ৯ক — derived attribute: সাধারণত জমা নয়
ER-এ ড্যাশ-দেওয়া ডিম্বাকার derived attribute এমন মান, যা অন্য attribute থেকে হিসাব করা যায়: জন্মতারিখ থেকে বয়স, ধারের তারিখ থেকে ফেরতের শেষ দিন, কপি গুনে মোট কপি। এগুলো সাধারণত কলাম হিসেবে জমা রাখা হয় না, কারণ জমা রাখলে মূল মান বদলালে দুটি অমিল হতে পারে। তিনটি প্রচলিত পথ: কোয়েরিতে হিসাব, VIEW (পর্ব ২৪), আর generated column — যা DBMS নিজেই সূত্র থেকে হিসাব করে রাখে, হাতে লেখা যায় না:
CREATE TABLE loans (
loan_id INTEGER PRIMARY KEY,
member TEXT NOT NULL,
loaned_on TEXT NOT NULL,
returned_on TEXT,
due_on TEXT GENERATED ALWAYS AS (date(loaned_on, '+14 days')) VIRTUAL
);
INSERT INTO loans (loan_id, member, loaned_on, returned_on) VALUES
(1, 'Rahim', '2025-09-01', '2025-09-10'),
(2, 'Karima', '2025-09-05', '2025-09-25');
CREATE VIEW loan_status AS
SELECT loan_id, member, due_on,
CAST(MAX(0, julianday(returned_on) - julianday(due_on)) AS INTEGER) AS days_late
FROM loans;
SELECT * FROM loan_status;তিনটি ✓ (CREATE TABLE, INSERT — ২টি সারি বদলেছে, CREATE VIEW), তারপর ফল: 1 | Rahim | 2025-09-15 | 0 আর 2 | Karima | 2025-09-19 | 6. due_on কেউ লেখেনি — INSERT-এ কলামটি বাদ ছিল, SQLite নিজে loaned_on + ১৪ দিন হিসাব করেছে। Karima ১৯ সেপ্টেম্বরের বদলে ২৫ সেপ্টেম্বর ফেরত দিয়েছেন, তাই days_late = 6; এটি একটি দ্বিতীয় স্তরের derived মান, VIEW-এ হিসাব হচ্ছে। VIRTUAL মানে পড়ার সময় হিসাব (জায়গা লাগে না), STORED মানে লেখার সময় হিসাব করে ডিস্কে রাখা (পড়া দ্রুত, index-ও করা যায়)। SQLite-এ generated column এসেছে 3.31 (২০২০) সংস্করণে।
একটি সীমা মনে রাখুন: generated column-এর সূত্র deterministic হতে হবে — একই ইনপুটে সবসময় একই ফল। তাই “বয়স = আজকের তারিখ − জন্মতারিখ” generated column হতে পারে না; SQLite-এ date('now') ব্যবহার করলে সারি লেখার সময় ত্রুটি আসে: non-deterministic use of date() in a generated column. বয়সের মতো সময়-নির্ভর মান VIEW-এ বা কোয়েরিতে হিসাব করুন। অন্য DBMS-এ বাক্যগঠন একটু আলাদা:
-- PostgreSQL 12+: STORED (সাম্প্রতিক 18 সংস্করণে VIRTUAL-ও এসেছে)
due_on date GENERATED ALWAYS AS (loaned_on + 14) STORED
-- MySQL 5.7+: VIRTUAL বা STORED
due_on DATE GENERATED ALWAYS AS (loaned_on + INTERVAL 14 DAY) VIRTUAL
-- SQL Server: computed column, PERSISTED = জমা রাখা
due_on AS DATEADD(day, 14, loaned_on) PERSISTEDধাপ ৯খ — recursive relationship: একই টেবিলে FK
যে relationship-এ একই entity type দুটি ভিন্ন ভূমিকায় (role) অংশ নেয়, তাকে বলে recursive relationship. db-uni-তে MENTORS: একজন শিক্ষক আরেকজন শিক্ষকের mentor. Cardinality 1:N (একজন mentor-এর অনেক নবীন সহকর্মী, প্রত্যেকের সর্বোচ্চ একজন mentor), তাই নিয়ম ৪-ই খাটে — শুধু “N দিকের টেবিল” আর “1 দিকের টেবিল” একই টেবিল। FK কলামের নাম হয় ভূমিকার নামে: mentor_id → instructors(instructor_id). সম্পর্কটি M:N হলে (যেমন সদস্যরা একে অপরকে বই সুপারিশ করেন) নিয়ম ৫ খাটে: junction টেবিলের দুই FK-ই একই টেবিলের দিকে, যেমন recommendations(from_member_id, to_member_id).
-- MENTORS: instructors থেকে instructors-এ একটি 1:N সম্পর্ক
SELECT i.name AS instructor, m.name AS mentor
FROM instructors i
LEFT JOIN instructors m ON m.instructor_id = i.mentor_id
ORDER BY i.instructor_id;৬টি সারি: Dr. Anisur Rahman | NULL, Shahana Parvin | Dr. Anisur Rahman, Dr. Mahbub Alam | NULL, Rumana Islam | Dr. Mahbub Alam, Kamrul Hasan | NULL, Tasnim Chowdhury | Shahana Parvin. একই টেবিলকে দুই alias-এ (i = নবীন, m = mentor) দুবার পড়া হয়েছে — এটিই self join (পর্ব ২০)। সিনিয়রদের mentor_id NULL, তাই এখানে অংশগ্রহণ partial; mentor_id-তে NOT NULL দিলে কোনো প্রথম শিক্ষকই ঢোকানো যেত না। Tasnim → Shahana → Anisur — শৃঙ্খলটি কয়েক স্তর গভীর; পুরো শৃঙ্খল এক কোয়েরিতে বের করতে লাগে recursive CTE (পর্ব ২৫)।
৬ · পুরো উদাহরণ — একটি লাইব্রেরির ER থেকে কাজ-করা schema
এবার সব নিয়ম একসাথে। ধরুন একটি পাড়ার পাঠাগার তাদের খাতা-কলমের ক্যাটালগ ডেটাবেসে আনতে চায়। লাইব্রেরিয়ানের সাথে কথা বলে ব্যবসার নিয়মগুলো লেখা হলো:
- প্রতিটি বইয়ের একটি ISBN, শিরোনাম, প্রকাশের বছর আর (জানা থাকলে) একটি প্রকাশক।
- একটি বইয়ের একাধিক লেখক থাকতে পারে, আর লেখকের ক্রম (প্রথম, দ্বিতীয় লেখক) গুরুত্বপূর্ণ।
- একটি বইয়ের কয়েকটি কপি; প্রতিটি কপির একটি নম্বর (সেই বইয়ের ভেতরে ১, ২, ৩ …) আর একটি তাক।
- সদস্যের নাম (প্রথম ও শেষ অংশ), ঠিকানা (এলাকা, শহর), যোগদানের তারিখ আর এক বা একাধিক ফোন নম্বর।
- সদস্য একটি নির্দিষ্ট কপি ধার নেন; কবে নিলেন আর কবে ফেরত দিলেন লেখা থাকে। একই কপি একই সদস্য পরে আবার নিতে পারেন।
- ধারের ইতিহাস কখনো মোছা যাবে না — হিসাব আর হারানো বইয়ের খোঁজের জন্য দরকার।
লাইব্রেরির ER diagram
Chen notation (compact): attributes listed inside entities; key underlined, partial key dashed
LOAN-কে relationship হিসেবে আঁকা হয়েছে, কিন্তু একটি জিনিস খেয়াল করুন: একই (সদস্য, কপি) জোড়া একাধিকবার ঘটতে পারে। খাঁটি ER-এ relationship instance মানে একটি জোড়া — একই জোড়া দুবার হয় না। তাই ধার-এর মতো ঘটনাকে প্রায়ই associative entity (relationship-কে entity-তে উন্নীত করা) হিসেবে দেখা হয়, যার নিজের key থাকে। ম্যাপিংয়ে এর ফল নিচের দ্বিতীয় ডায়াগ্রামে দেখবেন। প্রথমে ধাপে ধাপে টেবিলগুলো:
| নিয়ম | |
| টেবিল | |
| foreign key |
- ধাপ ১ — চারটি strong entity. PUBLISHER, BOOK, AUTHOR, MEMBER প্রত্যেকে একটি টেবিল। MEMBER-এর composite name আর address ভেঙে চারটি কলাম। phone এখনো নেই — multi-valued, ধাপ ৬-এর কাজ। ৪টি টেবিল, কোনো FK নেই।
- ধাপ ২ — weak entity COPY. copies-এ মালিক BOOK-এর key book_id আসে FK হিসেবে, আর key হয় (book_id, copy_no). ৫টি টেবিল, ১টি FK.
- ধাপ ৩–৪ — 1:1 আর 1:N. এই ER-এ কোনো 1:1 নেই। 1:N PUBLISHES-এর জন্য N দিক books-এ publisher_id যোগ হলো — নতুন টেবিল নয়, নতুন কলাম। HAS_COPY-ও 1:N, কিন্তু সেটি ধাপ ২-এ ইতিমধ্যে হয়ে গেছে। ৫টি টেবিল, ২টি FK.
- ধাপ ৫ — M:N WRITES. junction book_authors: দুটি FK, আর relationship-এর attribute author_order. ৬টি টেবিল, ৪টি FK.
- ধাপ ৬ — multi-valued phone. member_phones(member_id, phone), key দুটি মিলে। ৭টি টেবিল, ৫টি FK.
- LOAN — M:N, কিন্তু ইতিহাসসহ। COPY আর MEMBER-এর M:N, তাই ধাপ ৫ অনুযায়ী নতুন টেবিল। COPY-র key composite বলে FK-ও দুই কলামের: (book_id, copy_no) → copies. একই জোড়া বারবার হতে পারে বলে নিজস্ব loan_id যোগ হলো — কেন, পরের ডায়াগ্রামে। ৮টি টেবিল, ৭টি FK.
- পুরো schema. ১টি ER diagram থেকে ৮টি টেবিল আর ৭টি FK — প্রতিটি সিদ্ধান্ত একটি নিয়মে বাঁধা। entity-র চেয়ে টেবিল বেশি হওয়াটাই স্বাভাবিক: M:N আর multi-valued attribute নিজেরাই টেবিল হয়।
এখন key বাছাই। বেশিরভাগ টেবিলে এটি সরল: strong entity-তে surrogate id (books-এ isbn থাকে UNIQUE alternate key হিসেবে, পর্ব ৬), weak entity-তে (মালিক, partial key), junction-এ দুই FK. কঠিন প্রশ্ন একটিই — loans. নমুনা ডেটা নিয়ে যাচাই করুন:
| যে সেট দেখছি | |
| সিদ্ধান্ত |
- ছয়টি ধার। খাঁটি M:N-এর নিয়মে key হতো দুই পাশের key — (member_id, book_id, copy_no). দেখা যাক তা টেকে কি না।
- দুই পাশের key — ব্যর্থ। Rahim (1) Learning SQL-এর কপি 1 সেপ্টেম্বরে নিয়ে ফেরত দিয়েছেন, অক্টোবরে আবার নিয়েছেন। সারি 1 আর 5 দুটিতেই (1, 1, 1). এই key রাখলে দ্বিতীয় ধারটি লেখাই যেত না, অথবা প্রথমটির ওপর লিখে ইতিহাস মুছে যেত।
- (book, copy, loaned_on) — প্রায়। একটি কপি একই দিনে দুবার ধার না হলে এটি অনন্য, আর এই ডেটায় তাই। কিন্তু সকালে নিয়ে দুপুরে ফেরত দিলে বিকেলে আরেকজন নিতেই পারেন। তারিখ-নির্ভর key এমন নিয়মের ওপর দাঁড়ায় যা কেউ নিশ্চিত করেনি, আর loans-এর দিকে ভবিষ্যতে অন্য টেবিলের FK (যেমন জরিমানা) এলে তিন কলাম কপি করতে হবে।
- আসল নিয়ম: একটি কপির একসাথে একটিই খোলা ধার। returned_on NULL এমন সারিতে (book, copy) অনন্য — 3/1, 1/2, 1/1, 5/1, সব আলাদা। এটি key নয়, একটি শর্তসাপেক্ষ অনন্যতা; SQLite আর PostgreSQL-এ partial unique index দিয়ে লেখা যায়।
- সিদ্ধান্ত। surrogate
loan_id INTEGER PRIMARY KEY; (book_id, copy_no) → copies আর member_id → members দুটি FK, দুটিই NOT NULL; আরCREATE UNIQUE INDEX one_open_loan ON loans(book_id, copy_no) WHERE returned_on IS NULL. M:N সম্পর্কটি এভাবে একটি associative entity হয়ে গেল।
শেষ সিদ্ধান্ত: প্রতিটি foreign key-র ON DELETE. প্রতিটির জন্য একটিই প্রশ্ন — “parent না থাকলে child সারির কি আলাদা অর্থ বা মূল্য আছে?” (পর্ব ৬)। নিচে নিয়মগুলো বসানো আর একটি DELETE-এর পরিণতি দেখুন:
| সিদ্ধান্ত হয়েছে | |
| মোছার জন্য চিহ্নিত |
- সাতটি FK, সাতটি প্রশ্ন। প্রতিটির জন্য ভাবুন: parent গেলে child-এর কী হওয়া উচিত — সাথে যাবে (CASCADE), থাকবে কিন্তু খালি হবে (SET NULL), নাকি parent-কেই যেতে দেওয়া হবে না (RESTRICT)?
- “অংশ” — CASCADE. একটি কপি বইয়ের অংশ (weak entity), একটি লেখক-সারি বইয়ের তথ্যের অংশ, একটি ফোন নম্বর সদস্যের অংশ। parent ছাড়া এদের কোনো স্বাধীন অর্থ নেই — তিনটিতেই CASCADE.
- “স্বাধীন বা ইতিহাস” — RESTRICT. যে লেখকের বই আছে তাঁকে মুছলে বইয়ের তথ্য অসম্পূর্ণ হবে; প্রকাশক মুছলে বইয়ের তথ্য হারাবে (চাইলে SET NULL-ও যুক্তিসংগত); আর ধার-ইতিহাস কখনো মোছা যাবে না — তাই loans-এর দুটি FK-তেই RESTRICT.
- পরীক্ষা: Golper Jhuli (3) মোছা। DBMS book 3-কে চিহ্নিত করল। এখন দেখতে হবে কারা এর দিকে আঙুল তুলে আছে: দুটি কপি, একটি লেখক-সারি — আর কপি (3, 1)-এর দিকে loan 2.
- প্রথম ঢেউ: CASCADE. copies আর book_authors-এ CASCADE, তাই কপি (3, 1), (3, 2) আর লেখক-সারি (3, 3) মোছার জন্য চিহ্নিত — মোট ৪টি সারি। এবার এই কপিগুলোর child-দের দেখার পালা।
- দ্বিতীয় স্তরে RESTRICT — সব বাতিল। কপি (3, 1)-কে loan 2 নির্দেশ করে, আর loans → copies-এ RESTRICT. একটি সারিও মোছা যাবে না, তাই পুরো DELETE বাতিল — বই, কপি, লেখক-সারি সব অক্ষত। CASCADE-এর ঢেউ নিচের কোনো স্তরের RESTRICT-এ ধাক্কা খেলে পুরো স্টেটমেন্ট ফিরে যায়: ইতিহাসওয়ালা বই এভাবে নিজে থেকেই সুরক্ষিত।
সব সিদ্ধান্ত মিলিয়ে সম্পূর্ণ DDL. নিচের দুটি এডিটর ঠিক এই schema আর নমুনা ডেটা নিয়ে চলে (“নমুনা ডেটাবেস” বাক্সে পুরোটা আছে):
CREATE TABLE publishers (publisher_id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, city TEXT);
CREATE TABLE books (book_id INTEGER PRIMARY KEY, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL,
pub_year INTEGER, publisher_id INTEGER REFERENCES publishers(publisher_id) ON DELETE RESTRICT);
CREATE TABLE authors (author_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE book_authors (book_id INTEGER REFERENCES books(book_id) ON DELETE CASCADE,
author_id INTEGER REFERENCES authors(author_id) ON DELETE RESTRICT,
author_order INTEGER NOT NULL, PRIMARY KEY (book_id, author_id));
CREATE TABLE copies (book_id INTEGER REFERENCES books(book_id) ON DELETE CASCADE,
copy_no INTEGER NOT NULL, shelf TEXT, PRIMARY KEY (book_id, copy_no));
CREATE TABLE members (member_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL,
last_name TEXT NOT NULL, area TEXT, city TEXT, joined_on TEXT NOT NULL);
CREATE TABLE member_phones (member_id INTEGER REFERENCES members(member_id) ON DELETE CASCADE,
phone TEXT NOT NULL, PRIMARY KEY (member_id, phone));
CREATE TABLE loans (loan_id INTEGER PRIMARY KEY, book_id INTEGER NOT NULL, copy_no INTEGER NOT NULL,
member_id INTEGER NOT NULL REFERENCES members(member_id) ON DELETE RESTRICT,
loaned_on TEXT NOT NULL, returned_on TEXT,
FOREIGN KEY (book_id, copy_no) REFERENCES copies(book_id, copy_no) ON DELETE RESTRICT);
CREATE UNIQUE INDEX one_open_loan ON loans(book_id, copy_no) WHERE returned_on IS NULL;নমুনা ডেটা: ৩টি প্রকাশক, ৫টি বই (Learning SQL, Database Design, Golper Jhuli, Data Structures, Python for All), ৪ জন লেখক, ৮টি কপি (Learning SQL-এর ৩টি, Golper Jhuli-র ২টি, বাকিদের ১টি করে), ৫ জন সদস্য (Rahim, Karima, Sabbir, Nusrat, Tanvir), ৪টি ফোন নম্বর আর উপরের ডায়াগ্রামের ৬টি ধার। ডিজাইনটি কাজ করে কি না, দুটি বাস্তব প্রশ্ন দিয়ে যাচাই করি:
-- ১. এখন কোন কোন কপি কার কাছে?
SELECT l.loan_id, b.title, l.copy_no,
m.first_name || ' ' || m.last_name AS member, l.loaned_on
FROM loans l
JOIN books b ON b.book_id = l.book_id
JOIN members m ON m.member_id = l.member_id
WHERE l.returned_on IS NULL
ORDER BY l.loan_id;
-- ২. প্রতিটি বইয়ের মোট কপি আর তাকে থাকা কপি
SELECT b.title, COUNT(*) AS total_copies,
COUNT(*) - COUNT(l.loan_id) AS on_shelf
FROM books b
JOIN copies c ON c.book_id = b.book_id
LEFT JOIN loans l ON l.book_id = c.book_id AND l.copy_no = c.copy_no
AND l.returned_on IS NULL
GROUP BY b.book_id ORDER BY b.book_id;প্রথম কোয়েরিতে ৪টি খোলা ধার: 2 | Golper Jhuli | 1 | Karima Begum | 2025-09-05, 3 | Learning SQL | 2 | Sabbir Hasan | 2025-09-12, 5 | Learning SQL | 1 | Rahim Uddin | 2025-10-02, 6 | Python for All | 1 | Nusrat Jahan | 2025-10-05. দ্বিতীয়টিতে ৫টি সারি: Learning SQL 3 | 1, Database Design 1 | 1, Golper Jhuli 2 | 1, Data Structures 1 | 1, Python for All 1 | 0. কৌশলটি দেখুন: LEFT JOIN-এর শর্তে returned_on IS NULL রাখায় শুধু খোলা ধার জোড়া লাগে; COUNT(l.loan_id) NULL গোনে না, তাই সেটি “বাইরে থাকা কপি”। শর্তটি WHERE-এ সরালে যে কপির শুধু ফেরত-আসা ধার আছে, তার সারি বাদ পড়ত — Data Structures পুরোপুরি হারিয়ে যেত, আর অন্য ডেটায় মোট কপির হিসাবও ভুল আসতে পারত। AI-এর লেখা কোয়েরিতে এটি খুব সাধারণ ভুল। Data Structures-এর কপিটি একবার ধার হয়ে ফেরত এসেছে, তাই সেটি তাকে।
এবার ON DELETE সিদ্ধান্তগুলো আসল ইঞ্জিনে পরীক্ষা করুন — একটি মোছা চলবে, একটি চলবে CASCADE-সহ, আর শেষটি উপরের ডায়াগ্রামের মতো আটকে যাবে:
-- Tanvir (5)-এর কোনো ধার নেই: সদস্যপদ মোছা যায়
DELETE FROM members WHERE member_id = 5;
-- 'Database Design' (2)-এর কপি কখনো ধার হয়নি: বই মুছলে কপিও যায় (CASCADE)
DELETE FROM books WHERE book_id = 2;
SELECT COUNT(*) AS copies_left FROM copies;
-- 'Golper Jhuli' (3)-এর কপি 1 ধার-ইতিহাসে আছে: পুরো DELETE বাতিল
DELETE FROM books WHERE book_id = 3;প্রথম দুটি DELETE-এ “✓ DELETE — ১টি সারি বদলেছে”। তারপর copies_left | 7 — ৮টি কপির মধ্যে Database Design-এর একমাত্র কপিটি CASCADE-এ গেছে (তার লেখক-সারি (2, 2)-ও)। শেষ DELETE-এ ত্রুটি: FOREIGN KEY constraint failed — ঠিক ডায়াগ্রামের ধাপ ৬. চাইলে DELETE FROM members WHERE member_id = 1; লিখে দেখুন: Rahim-এর তিনটি ধার-রেকর্ড আছে, তাই একই ত্রুটি। ডিজাইনটি নিজেই ব্যবসার নিয়ম “ধারের ইতিহাস কখনো মোছা যাবে না” রক্ষা করছে, অ্যাপের কোনো সাহায্য ছাড়াই।
৭ · db-uni কোথা থেকে এল — বিশ্ববিদ্যালয়ের ER-এর সাথে মিলিয়ে
এই কোর্সে যে বিশ্ববিদ্যালয় ডেটাবেস বারবার চালাচ্ছেন, সেটিও একটি ER থেকে এই নিয়মগুলো মেনেই তৈরি। পর্ব ৭-এ এর entity আর relationship-গুলো দেখেছেন (সেখানে relationship-এর নাম একটু ভিন্ন হতে পারে; অর্থ একই)। প্রতিটি ER উপাদান কোন ধাপে কোন টেবিল বা কলাম হয়েছে:
| ER উপাদান | ধরন | ধাপ | db-uni-তে ফল |
|---|---|---|---|
| DEPARTMENT, INSTRUCTOR, STUDENT, COURSE | strong entity | ১ | departments, instructors, students, courses — প্রতিটির নিজস্ব PK |
| INSTRUCTOR কোন বিভাগে কাজ করেন | 1:N | ৪ | instructors.dept_id → departments |
| STUDENT কোন বিভাগে পড়েন | 1:N, partial | ৪ | students.dept_id, NULL চলে (Habib Ullah-র বিভাগ এখনো ঠিক হয়নি) |
| COURSE কোন বিভাগের | 1:N | ৪ | courses.dept_id |
| COURSE কে পড়ান | 1:N (সরলীকৃত) | ৪ | courses.instructor_id, NULL চলে (ENG101) |
| MENTORS | recursive 1:N | ৯ | instructors.mentor_id → instructors |
| STUDENT ENROLLS IN COURSE (term, grade) | M:N, attribute-সহ | ৫ | enrollments(student_id, course_id, term, grade), PK তিন কলামের |
তিনটি জিনিস লক্ষ করার মতো। প্রথমত, enrollments-এর key-তে term আছে, যদিও ER-এ term relationship-এর একটি attribute. কারণ লাইব্রেরির loans-এর মতোই: একজন শিক্ষার্থী F পেয়ে একই কোর্স পরের term-এ আবার নিতে পারেন, তাই (student_id, course_id) জোড়া পুনরাবৃত্ত হয়। ডিজাইনার এখানে surrogate না নিয়ে term-কে key-তে নিয়েছেন — এটিও বৈধ, কারণ বিশ্ববিদ্যালয়ের নিয়মে একই term-এ একই কোর্স দুবার নেওয়া যায় না। দ্বিতীয়ত, “কোর্স কে পড়ান” এখানে 1:N ধরা হয়েছে (প্রতিটি কোর্সে একজন শিক্ষক)। বাস্তবে একটি কোর্সের একাধিক section আর একাধিক শিক্ষক থাকে; তখন এটি M:N বা SECTION নামে একটি weak entity হতো — পর্ব ১১-এর কেস স্টাডিতে এমন প্রশ্নই আসবে। তৃতীয়ত, কোনো 1:1 বা multi-valued attribute নেই, তাই সেই ধাপগুলো এখানে কিছু তৈরি করেনি। অ্যালগরিদমের প্রতিটি ধাপ প্রতিটি ER-এ কাজ পাবে, এমন নয়।
৮ · ডিজাইনের সিদ্ধান্ত — NULL, নামকরণ আর উল্টো পথ
NULL-ভরা এক টেবিল, নাকি ভাগ করা দুই টেবিল?
নিয়মগুলো মানলেও কিছু জায়গায় দুটি বৈধ নকশা থাকে, আর দুটির মধ্যে বেছে নিতে হয়। সবচেয়ে পরিচিত প্রশ্ন: একটি ঐচ্ছিক তথ্যগুচ্ছ (যেমন শুধু শিক্ষার্থী-সদস্যের প্রতিষ্ঠান আর student ID, বা শুধু কর্পোরেট গ্রাহকের ট্রেড লাইসেন্স, TIN, যোগাযোগ ব্যক্তি) মূল টেবিলে কলাম হবে, নাকি আলাদা 1:1 টেবিলে যাবে?
এক টেবিল, অনেক NULL
JOIN নেই, কোয়েরি সরল, এক সারিতে সব তথ্য। কিন্তু বেশিরভাগ সারিতে খালি ঘর, “শিক্ষার্থী হলে student ID বাধ্যতামূলক” লিখতে CHECK লাগে, আর NULL-এর তিন-মানের যুক্তি (পর্ব ১৬) কোয়েরিতে ফাঁদ পাতে।
ঐচ্ছিক কলাম কম, প্রায় সবার থাকেদুই টেবিল, 1:1
মূল টেবিল পরিষ্কার, ঐচ্ছিক টেবিলে NOT NULL সরাসরি লেখা যায়, আলাদা অনুমতি (privilege) দেওয়া যায়। কিন্তু প্রতিটি পূর্ণ তথ্যের জন্য JOIN, আর “এক সদস্যের সর্বোচ্চ এক সারি” রাখতে PK = FK.
ঐচ্ছিক গুচ্ছ বড়, অল্প সারিতে থাকেএকটি কার্যকর নিয়ম: যদি একগুচ্ছ কলাম একসাথে থাকে বা একসাথে না থাকে (সব NULL বা সব পূর্ণ), আর সেটি অর্ধেকের কম সারিতে প্রযোজ্য, তবে সেটি প্রায় নিশ্চিতভাবে একটি লুকানো subclass বা 1:1 entity — আলাদা টেবিল ভাবুন। আর গোপনীয় তথ্য (বেতন, NID-র ছবি) আলাদা টেবিলে রাখলে নিরাপত্তার নিয়ম সহজ হয় (পর্ব ৫১)।
নামকরণের রীতি
নাম ER থেকে আসে, কিন্তু টেবিলে নামের রীতি দলের সবাইকে একরকম মানতে হয়। প্রচলিত ভালো অভ্যাস:
- snake_case, ছোট হাতের অক্ষর:
member_phones,loaned_on. PostgreSQL উদ্ধৃতিহীন নাম ছোট হাতের অক্ষরে বদলে নেয় (Oracle বড় হাতের অক্ষরে), তাইMemberPhonesলিখলে পরে"MemberPhones"উদ্ধৃতিসহ লিখতে হতে পারে। - বহুবচন বা একবচন — একটি বেছে সবখানে: এই কোর্সে টেবিল বহুবচন (
books), ER-এর entity একবচন (BOOK). অনেক দল একবচন (book) পছন্দ করে; দুটিই চলে, মিশ্রণ চলে না। - Key-এর নাম
<entity>_id: শুধুidনা লিখেbook_id; তাহলে FK কলামের নাম PK-র সাথে মেলে, JOIN পড়তে সহজ হয়, আরUSING (book_id)লেখা যায়। ভূমিকা আলাদা হলে ভূমিকার নাম:mentor_id,from_member_id. - Junction টেবিল দুই পাশের নামে:
book_authors; সম্পর্কের নিজস্ব অর্থ থাকলে সেই নাম:enrollments,loans. - সংরক্ষিত শব্দ এড়ান:
order,group,user(PostgreSQL-এ সংরক্ষিত) টেবিলের নাম হলে প্রতিবার উদ্ধৃতি লাগে —orders,app_usersলিখুন। - তারিখের কলাম
_on/_at: শুধু তারিখ হলেjoined_on, সময়সহ হলেcreated_at— নাম দেখেই ধরন বোঝা যায়।
উল্টো পথ — schema দেখে ER উদ্ধার
বাস্তবে প্রায়ই নকশা আগে আসে না — আপনি হাতে পান দশ বছরের পুরোনো একটি ডেটাবেস, কোনো diagram ছাড়া। তখন mapping-এর নিয়মগুলো উল্টো দিকে চালিয়ে ER বের করা যায়; একে বলে reverse engineering. সূত্রগুলো:
| schema-য় যা দেখছেন | ER-এ সম্ভবত যা ছিল |
|---|---|
| এক কলামের PK, কোনো FK PK-র অংশ নয় | strong entity |
| সাধারণ কলামে FK (NULL চলে / চলে না) | 1:N, N দিকে partial / total |
| FK কলামে UNIQUE, অথবা PK-ই আরেক টেবিলের FK | 1:1 — বা specialization-এর subclass |
| composite PK = দুই FK, আর কিছু কলাম | M:N junction (বাকি কলাম relationship-এর attribute) |
| composite PK = এক FK + নিজের কলাম | weak entity (FK = মালিক, নিজের কলাম = partial key) বা multi-valued attribute |
| composite PK = তিন বা বেশি FK | n-ary relationship |
| FK যে টেবিলে আছে সেই টেবিলের দিকেই | recursive relationship |
এই সূত্র কাজে লাগাতে প্রথমে দরকার সব PK আর FK-র তালিকা, আর সেটি DBMS নিজেই দেয় — catalog থেকে (পর্ব ৩-এর system catalog). SQLite-এ pragma_foreign_key_list() আর pragma_table_info() টেবিলের মতো পড়া যায়। db-uni-র ওপর চালান:
-- schema থেকে ER পড়া: কোন টেবিল কোন টেবিলের দিকে আঙুল তোলে?
SELECT m.name AS child_table, f."from" AS fk_column,
f."table" AS parent_table, f."to" AS parent_column
FROM sqlite_master m, pragma_foreign_key_list(m.name) f
WHERE m.type = 'table'
ORDER BY m.name, f."from";
-- composite primary key = সম্ভবত weak entity বা M:N
SELECT p.name AS table_name, COUNT(*) AS pk_columns
FROM sqlite_master p, pragma_table_info(p.name) t
WHERE p.type = 'table' AND t.pk > 0
GROUP BY p.name ORDER BY p.name;প্রথম কোয়েরিতে ৭টি FK: courses.dept_id → departments, courses.instructor_id → instructors, enrollments.course_id → courses, enrollments.student_id → students, instructors.dept_id → departments, instructors.mentor_id → instructors, students.dept_id → departments. দ্বিতীয়টিতে ৫টি সারি: courses, departments, instructors আর students-এর PK ১ কলামের, enrollments-এর 3. সূত্র মিলিয়ে: চারটি strong entity, পাঁচটি 1:N (dept_id তিনবার, instructor_id, mentor_id), যার একটি recursive (child আর parent একই টেবিল), আর enrollments-এর PK-তে দুই FK + term — অর্থাৎ attribute-সহ একটি M:N. অংশ ৭-এর টেবিলটি এভাবে শুধু catalog থেকেই আবার তৈরি করা গেল। "from", "table" উদ্ধৃতিতে কারণ এগুলো SQL-এর সংরক্ষিত শব্দ — নামকরণের নিয়মটি কেন জরুরি, তার হাতে-কলমে প্রমাণ।
PostgreSQL, MySQL বা SQL Server-এ একই তথ্য আসে standard information_schema থেকে:
SELECT kcu.table_name, kcu.column_name,
ccu.table_name AS parent_table, ccu.column_name AS parent_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY';
-- PostgreSQL-এ composite FK-র জন্য pg_constraint বেশি নির্ভরযোগ্য;
-- MySQL-এ constraint_column_usage নেই — key_column_usage-এর referenced_table_name দেখুনSchema সব বলে না। পুরোনো ডেটাবেসে প্রায়ই FK constraint ঘোষণাই করা থাকে না (MyISAM যুগের MySQL, বা ইচ্ছে করে বাদ দেওয়া — পর্ব ৬-এর ভাবনার প্রশ্ন ৩)। তখন নামের মিল (customer_id দুই টেবিলে), ডেটার মিল (child-এর সব মান parent-এ আছে কি না, পর্ব ২০-এর anti join) আর অ্যাপের কোড দেখে সম্পর্ক অনুমান করতে হয়। আর cardinality-র “সর্বোচ্চ ১” বা participation-এর “total” schema-য় UNIQUE / NOT NULL না থাকলে কোথাও লেখা নেই — জানতে হবে মানুষের কাছ থেকে। MySQL Workbench, DBeaver বা SchemaSpy-র মতো টুল ঘোষিত FK থেকে স্বয়ংক্রিয়ভাবে diagram এঁকে দেয়, কিন্তু অঘোষিত সম্পর্ক তারাও দেখতে পায় না।
আরও গভীরে: Mapping অ্যালগরিদম ER-এর সব নিয়ম টেবিলে আনতে পারে না। “প্রতিটি বইয়ের অন্তত একটি কপি থাকবে” (BOOK-এর দিকে total participation, 1 দিকে) বা “প্রতিটি সদস্য ঠিক একটি subclass-এ” (total ও disjoint specialization)-এর মতো শর্ত একাধিক টেবিল জুড়ে, আর সাধারণ CHECK একটি সারির বাইরে দেখে না। SQL standard-এ এর জন্য CREATE ASSERTION আছে, কিন্তু প্রধান কোনো DBMS এটি বাস্তবায়ন করেনি; তাই এমন নিয়ম trigger (পর্ব ২৯), deferred constraint (PostgreSQL-এ DEFERRABLE INITIALLY DEFERRED FK, যা transaction শেষে যাচাই হয়) বা অ্যাপের transaction দিয়ে রক্ষা করতে হয়। একই কারণে partial unique index (SQLite, PostgreSQL; SQL Server-এ filtered index) বেশ শক্তিশালী — MySQL-এ এটি নেই, সেখানে generated column-এর ওপর UNIQUE দিয়ে কাছাকাছি ফল আনা হয়।
৯ · AI-এর যুগে ER থেকে schema
একটি ER diagram-এর ছবি বা বর্ণনা দিয়ে AI-কে “CREATE TABLE লিখে দাও” বললে কয়েক সেকেন্ডে ফল আসে, আর বেশিরভাগ সময় তা চলেও। কিন্তু mapping-এর ভুল চলার সময় ধরা পড়ে না — ধরা পড়ে মাস কয়েক পরে, যখন দ্বিতীয় ধার লেখা যায় না বা একটি DELETE ইতিহাস মুছে দেয়। AI প্রায়ই যা করে:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
M:N-এর জন্য junction না বানিয়ে author_ids TEXT বা JSON কলাম | FK নেই, খোঁজ ধীর, অনাথ id জমে | “এক ঘরে কি একাধিক মান?” — প্রতিটি কলামে প্রশ্ন |
| 1:1-এ FK দিয়েছে, UNIQUE দেয়নি | নীরবে 1:N | একই parent-এর দ্বিতীয় সারি ঢুকিয়ে দেখুন |
| weak entity-কে নিজস্ব id দিয়েছে, (owner, partial key)-তে UNIQUE নেই | একই বইয়ের “কপি ২” দুবার | partial key পুনরাবৃত্তি করে INSERT |
| ইতিহাসের M:N-এ (ধার, ভর্তি) দুই FK-কে PK করেছে | দ্বিতীয়বারের ঘটনা লেখা যায় না | একই জোড়া দুবার ঘটতে পারে কি না জিজ্ঞেস করুন |
সবখানে ON DELETE CASCADE | এক DELETE-এ ধার-ইতিহাস উধাও | প্রতিটি FK-র জন্য “অংশ নাকি ইতিহাস?” |
| derived মান (age, total) সাধারণ কলামে | মূল মান বদলালে অমিল | VIEW / generated column, বা জমা রাখার স্পষ্ট কারণ |
| ternary-কে তিনটি binary টেবিলে ভেঙেছে | JOIN-এ মিথ্যা সারি | তিনটি একসাথে ঘটে এমন উদাহরণ দিয়ে যাচাই |
AI-কে schema চাইলে শুধু “বানিয়ে দাও” না বলে প্রতিটি ধাপের যুক্তি চেয়ে নিন — তাহলে প্রতিটি সিদ্ধান্ত মিলিয়ে দেখতে পারবেন:
AI-এর দেওয়া schema হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- M:N relationship-কে টেবিলে কীভাবে নেবেন? — আলাদা junction টেবিল: দুই পাশের PK দুটি FK, সাধারণত দুটি মিলে PK; relationship-এর attribute সেখানে।
- 1:N-এ foreign key কোন দিকে? — N দিকের টেবিলে; 1 দিকে রাখলে এক ঘরে একাধিক মান লাগত।
- Weak entity-র primary key কী হয়? — মালিকের primary key + partial key; মালিকের FK সাধারণত ON DELETE CASCADE.
- Multi-valued attribute কেন আলাদা টেবিলে? — relational ঘরে একটিই atomic মান (1NF); phone1, phone2 কলাম NULL বাড়ায় আর সংখ্যা সীমিত করে।
- ন্যূনতম কয়টি টেবিল? (যেমন দুটি strong entity আর তাদের মধ্যে একটি M:N) — ৩টি; 1:N হলে ২টি।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। নিয়ম মুখস্থ নয়, নিয়মের পেছনের যুক্তিই এখানে আসল।
প্র ০১1:1 relationship-এ দুই entity-কে এক টেবিলে মিলিয়ে দেওয়া সহজ। তাহলে বাস্তব ডিজাইনে প্রায়ই দুটি আলাদা টেবিল রাখা হয় কেন?
তত্ত্বে দুই পাশের অংশগ্রহণ total হলে মিলিয়ে দেওয়াই সবচেয়ে সরল — প্রতিটি সারিতে দুই পাশের তথ্যই থাকবে, কোনো NULL নেই, কোনো JOIN নেই। কিন্তু বাস্তবে আলাদা রাখার কয়েকটি শক্ত কারণ আছে।
প্রথম কারণ জীবনচক্র। একজন সদস্য আর তাঁর লাইব্রেরি কার্ড একই সময়ে জন্মায় না: সদস্যপদ আগে, কার্ড ছাপা হয় পরে; কার্ড হারালে নতুন কার্ড হয়, পুরোনোটি বাতিল। মিলিয়ে রাখলে কার্ড বদলের ইতিহাস রাখার জায়গাই থাকে না। আজ যা 1:1, সময়ের মাত্রা যোগ হলেই তা 1:N হয়ে যায় (একজনের অনেক কার্ড, যার একটি সক্রিয়)। আলাদা টেবিল থাকলে এই পরিবর্তন শুধু একটি UNIQUE সরানো বা partial UNIQUE বসানো।
দ্বিতীয় কারণ নিরাপত্তা আর মালিকানা। কর্মচারীর সাধারণ তথ্য সবাই দেখবে, বেতন আর ব্যাংক হিসাব শুধু হিসাব বিভাগ। আলাদা টেবিলে আলাদা privilege দেওয়া সহজ (পর্ব ৫১), আর এক বিভাগের কলাম বদলালে অন্যের টেবিল ছুঁতে হয় না।
তৃতীয় কারণ কর্মক্ষমতা। খুব বড় কিন্তু কম পড়া কলাম (ছবি, দীর্ঘ লেখা) মূল সারিতে থাকলে প্রতিটি page-এ কম সারি ধরে, আর সাধারণ কোয়েরিও বেশি ডিস্ক পড়ে (পর্ব ৩৯)। একে বলে vertical partitioning, আর এটি ঠিক 1:1 টেবিলের রূপ। PostgreSQL বড় মান নিজেই TOAST নামে আলাদা জায়গায় সরিয়ে রাখে — DBMS নিজেও এই কৌশল ভেতরে ব্যবহার করে।
চতুর্থ কারণ partial participation. যদি দশ হাজার সদস্যের মাত্র দুশো জনের কর্পোরেট তথ্য থাকে, মিলিয়ে দিলে ৯,৮০০ সারিতে পাঁচ-ছয়টি NULL কলাম — আর “কর্পোরেট হলে TIN বাধ্যতামূলক” লিখতে জটিল CHECK.
উল্টো দিকে, মিলিয়ে দেওয়ার পক্ষেও যুক্তি আছে: প্রতিটি অনুরোধে দুই পাশের তথ্য একসাথে লাগলে প্রতিবারের JOIN অপ্রয়োজনীয় খরচ, আর দুই টেবিলে “ঠিক এক সারি” রক্ষা করা একটি বাড়তি দায়।
মূল উপলব্ধি: 1:1 মানে “আজ এক-এক”, সবসময় নয়। জীবনচক্র, অনুমতি, আকার আর participation আলাদা হলে আলাদা টেবিল; সব এক হলে এক টেবিল। সিদ্ধান্তটি cardinality নয়, এই চারটি প্রশ্নই নেয়।
প্র ০২একটি ternary relationship (সরবরাহকারী–কাপড়–কারখানা)-কে তিনটি binary relationship-এ ভাঙলে কী হারায়? আর কখন ভাঙা নিরাপদ?
ধরুন supplies টেবিলে তিনটি সারি: (Karim Textiles, ডেনিম, সাভার), (Rupsha Fabrics, ডেনিম, গাজীপুর) আর (Karim Textiles, পপলিন, গাজীপুর). এখন তিনটি binary টেবিল বানাই — supplier_fabric, supplier_factory, fabric_factory — প্রতিটিতে ternary থেকে দুই কলামের projection. supplier_fabric-এ (Karim, ডেনিম), (Rupsha, ডেনিম), (Karim, পপলিন); supplier_factory-তে (Karim, সাভার), (Rupsha, গাজীপুর), (Karim, গাজীপুর); fabric_factory-তে (ডেনিম, সাভার), (ডেনিম, গাজীপুর), (পপলিন, গাজীপুর).
এবার তিনটি JOIN করে আসল তথ্য ফেরাই। (Karim, ডেনিম) + (Karim, গাজীপুর) + (ডেনিম, গাজীপুর) — তিনটি শর্তই মেলে, তাই ফলাফলে আসে (Karim Textiles, ডেনিম, গাজীপুর). অথচ Karim কখনো গাজীপুরে ডেনিম পাঠায়নি! এটি একটি spurious (মিথ্যা) tuple. তিনটি binary সম্পর্কই আলাদাভাবে সত্য, কিন্তু তাদের সমন্বয় ternary ঘটনাটির সমান নয়। হিসাবের দিক থেকে এর মানে, গাজীপুর কারখানা ভুল সরবরাহকারীকে বিল পরিশোধ করতে পারে, বা মানের অভিযোগ ভুল কোম্পানির কাছে যেতে পারে।
quantity_yards-এর সমস্যা আরও সরল: এটি তিনটির সমন্বয়ের গুণ, কোনো জোড়ার নয়। কোন binary টেবিলে রাখবেন? যেখানেই রাখুন, অর্থ হারায়।
তাহলে কখন ভাঙা নিরাপদ? যখন ব্যবসার নিয়মেই একটি বিশেষ শর্ত থাকে: “কোনো সরবরাহকারী একটি কাপড় দেয়, আর কারখানাটি সেই কাপড় নেয়, আর সরবরাহকারী সেই কারখানায় মাল দেয় — তাহলে সে সেখানে সেই কাপড়ই দেয়।” এই চক্রাকার নিয়মকে তত্ত্বে join dependency বলা হয়। এটি থাকলে তিনটি projection-এর JOIN ঠিক আসল টেবিল ফেরত দেয় — lossless — আর তখন ভাঙলে redundancy কমে। এটিই fifth normal form (5NF)-এর মূল প্রশ্ন, যা পর্ব ৩৭-এ দেখবেন। এমন নিয়ম বাস্তবে বিরল, তাই ডিফল্ট উত্তর হলো: ternary-কে ternary রাখুন।
আরেকটি বিকল্প প্রায়ই সবচেয়ে পরিষ্কার: ঘটনাটিকে একটি entity বানানো — shipment(shipment_id, supplier_id, fabric_id, factory_id, quantity, date). বাস্তবে একই তিনজনের মধ্যে চালান বারবার হয়, তাই লাইব্রেরির loans-এর মতোই এখানেও surrogate key স্বাভাবিক।
মূল উপলব্ধি: জোড়ায় জোড়ায় সত্য মানেই তিনজন একসাথে সত্য নয়। ternary ভাঙা যায় শুধু তখনই, যখন একটি join dependency প্রমাণিত — নইলে JOIN মিথ্যা সারি বানাবে।
প্র ০৩Specialization-এর তিন কৌশল ORM-এর জগতেও আছে। Hibernate/JPA, Rails আর Django এগুলোকে কী নামে চেনে, আর ডিফল্ট পছন্দ কোনটি — কেন?
Object-oriented ভাষায় class inheritance খুব সাধারণ — Member থেকে StudentMember আর StaffMember. ORM-কে (পর্ব ৫০) এই class-গুলো টেবিলে বসাতে হয়, আর তারা ঠিক Elmasri–Navathe-এর তিন কৌশলই বেছে নেয়, শুধু নাম আলাদা।
Java-র JPA standard-এ @Inheritance-এর তিনটি strategy: SINGLE_TABLE (এক টেবিল + discriminator কলাম — আমাদের কৌশল গ), JOINED (superclass আর প্রতিটি subclass-এর টেবিল, PK = FK — কৌশল ক) আর TABLE_PER_CLASS (শুধু concrete class-এর টেবিল — কৌশল খ)। JPA-র ডিফল্ট SINGLE_TABLE. Ruby on Rails-এর পরিচিত রূপ Single Table Inheritance (STI): একটি type কলামে subclass-এর নাম লেখা থাকে। Django-তে তিনটি পথ: abstract base class (superclass-এর টেবিল নেই, প্রতিটি subclass-এ সব কলাম — কৌশল খ), multi-table inheritance (প্রতিটি class-এর টেবিল, একটি implicit OneToOne link — কৌশল ক) আর proxy model (একই টেবিল, শুধু আচরণ আলাদা)।
এক টেবিল কেন অনেক জায়গায় ডিফল্ট? কারণ object লোড করা সবচেয়ে সস্তা — একটি SELECT, কোনো JOIN নেই; আর “সব সদস্য” আনতে polymorphic কোয়েরিও সরল। দাম: subclass-এর কলামগুলো NOT NULL হতে পারে না (অন্য subclass-এ সেগুলো খালি), তাই integrity রক্ষার দায় অ্যাপে বা CHECK-এ যায়; subclass বাড়লে টেবিল চওড়া আর NULL-ভরা হয়।
JOINED সবচেয়ে normalized আর constraint-বান্ধব, কিন্তু গভীর hierarchy-তে প্রতিটি object আনতে কয়েক স্তরের JOIN, আর polymorphic কোয়েরিতে সব subclass টেবিলের সাথে LEFT JOIN. TABLE_PER_CLASS-এ polymorphic কোয়েরি মানে UNION, আর সব subclass জুড়ে id অনন্য রাখতে sequence ভাগ করতে হয়।
ডেটাবেস ডিজাইনারের দৃষ্টিতে প্রশ্নটি ORM-এর ডিফল্ট নয়, ডেটার আকার: subclass-এর নিজস্ব কলাম কয়টি, subclass-এর নিজের relationship আছে কি না (থাকলে JOINED-ই একমাত্র পরিষ্কার পথ, কারণ FK একটি নির্দিষ্ট subclass টেবিলের দিকে যেতে পারে), আর বেশিরভাগ কোয়েরি superclass স্তরে নাকি subclass স্তরে।
মূল উপলব্ধি: ORM নতুন কিছু আবিষ্কার করেনি — mapping-এর ধাপ ৮-কেই annotation-এর নামে ডাকে। ডিফল্ট নিলে জেনে নিন কোন কৌশল নিলেন, আর তার দাম কোথায় দিতে হবে।
প্র ০৪Junction টেবিলের key: দুই FK-র composite key, নাকি নিজস্ব surrogate id? ধার বা ভর্তির মতো সম্পর্কে উত্তর কেন বদলায়?
খাঁটি M:N-এ (বই–লেখক) প্রতিটি জোড়া একবারই ঘটে, আর জোড়াটিই তথ্য। তাই composite key (book_id, author_id) স্বাভাবিক: এটি “একই লেখক একই বইয়ে দুবার” নিজে থেকেই আটকায়, বাড়তি কলাম লাগে না, আর যে index PK-র জন্য তৈরি হয় সেটিই “এই বইয়ের লেখক কারা” খোঁজে কাজে লাগে।
কিন্তু সম্পর্কটি যদি ঘটনা হয় — ধার, ভর্তি, অর্ডার, ডাক্তারের অ্যাপয়েন্টমেন্ট — তাহলে একই জোড়া বারবার ঘটে। তখন তিনটি পথ: (১) সময়কে key-তে নেওয়া (db-uni-র term), (২) surrogate id (লাইব্রেরির loan_id), (৩) দুটো একসাথে — surrogate PK আর সময়সহ natural key-তে UNIQUE. db-uni-তে term স্বাভাবিক কারণ নিয়মেই একই term-এ একই কোর্স দুবার নেওয়া যায় না, আর term একটি পরিষ্কার, ছোট মান। লাইব্রেরিতে তারিখ-ভিত্তিক key দুর্বল, কারণ একই দিনে একই কপি দুবার ধার হতে পারে।
surrogate-এর পক্ষে আরেকটি যুক্তি হলো অন্য টেবিল থেকে আঙুল তোলা। ধার-এর ওপর জরিমানা (fines) টেবিল এলে FK হবে একটি কলাম loan_id, তিন-চার কলামের composite নয়। ঘটনা যত “entity-র মতো” হয় — নিজের attribute, নিজের সম্পর্ক, নিজের জীবনচক্র — surrogate তত যুক্তিসংগত। ER ভাষায় একে বলে associative entity.
ORM-এর জগতে এর পরিষ্কার প্রতিফলন আছে। Rails-এর has_and_belongs_to_many একটি সরল join টেবিল চায়, যার নিজের primary key নেই — খাঁটি M:N. কিন্তু সম্পর্কে attribute বা আচরণ লাগলে Rails-এর পরামর্শ হলো has_many :through, যেখানে junction-টি নিজেই একটি model, নিজস্ব id-সহ। অনেক দল শুরুতে প্রথমটি নিয়ে পরে দ্বিতীয়টিতে migration করতে বাধ্য হয়েছে, কারণ “এই সম্পর্কের তারিখটাও রাখতে হবে” প্রয়োজনটি প্রায় সবসময় আসে।
একটি সতর্কতা: surrogate নিলে natural অনন্যতা হারানো চলবে না। book_authors-এ surrogate id দিলেও (book_id, author_id)-এ UNIQUE রাখতেই হবে, নইলে একই লেখক দুবার ঢুকবে। loans-এ এর সমতুল্য হলো “একটি কপির একসাথে একটিই খোলা ধার” — partial unique index.
মূল উপলব্ধি: জোড়াই তথ্য হলে composite key; ঘটনাই তথ্য হলে surrogate key আর তার পাশে ব্যবসার অনন্যতা-নিয়ম। প্রশ্নটি হলো — “একই জোড়া কি দুবার ঘটতে পারে?”
প্র ০৫“Derived attribute জমা রাখবেন না” — নিয়মটি ঠিক হলে db-shop-এর order_items-এ unit_price কেন জমা রাখা হয়, যখন products টেবিলেই দাম আছে?
প্রশ্নটি গুরুত্বপূর্ণ, কারণ এখানে একটি সূক্ষ্ম পার্থক্য আছে: unit_price আসলে derived attribute নয়। Derived মানে “যেকোনো সময় অন্য মান থেকে একই ফল হিসাব করা যায়”। অর্ডারের দিনের দাম কিন্তু আজকের products.price থেকে হিসাব করা যায় না — দাম বদলায়। db-shop-এ এর প্রমাণ আছে: Mobile Phone-এর তালিকা-দাম 15000, কিন্তু অর্ডার 1009-এ বিক্রি হয়েছে 14500-এ (ছাড়), আর Headphone 1200-এর বদলে অর্ডার 1007-এ 1150. unit_price তাই একটি ঐতিহাসিক তথ্য — “বিক্রির মুহূর্তে কত টাকায় বিক্রি হয়েছিল” — যা হারালে চালান, ভ্যাট আর হিসাব মিলবে না। ER-এ এটি CONTAINS relationship-এর নিজস্ব attribute, derived নয়।
সত্যিকারের derived মান — বয়স, ধারের শেষ দিন, অর্ডারের মোট — জমা রাখার বিপদ হলো দুই জায়গার মান অমিল হওয়া। জন্মতারিখ সংশোধন হলো, কিন্তু age কলাম পুরোনো; একটি order_item মুছল, কিন্তু orders.total আগের মতো। এজন্যই ডিফল্ট পরামর্শ: হিসাব করুন, জমা রাখবেন না।
তবু বাস্তবে derived মান প্রায়ই জমা রাখা হয়, ইচ্ছে করে — একে বলে denormalization (পর্ব ৩৮)। একটি জনপ্রিয় পোস্টের মন্তব্যের সংখ্যা প্রতিবার COUNT করলে লক্ষ সারি গুনতে হয়, তাই comment_count কলাম রাখা হয় আর প্রতিটি নতুন মন্তব্যে বাড়ানো হয়। তখন সামঞ্জস্য রক্ষার একটি নির্দিষ্ট ব্যবস্থা চাই: generated column (একই সারির সূত্র হলে — DBMS নিজে রক্ষা করে), trigger (অন্য সারি বা টেবিল লাগলে, পর্ব ২৯), materialized view (পর্ব ২৪), বা একই transaction-এ দুটি লেখা। ব্যবস্থা ছাড়া জমা রাখা derived মান সময়ের সাথে নিশ্চিতভাবে ভুল হয়।
আইনি দিকও আছে। অনেক দেশে একটি চালান (invoice) জারি হওয়ার পর তা বদলানো যায় না — ভুল হলে নতুন সংশোধনী চালান। তাই চালানের মোট টাকা, ভ্যাট, দাম — সব সেই মুহূর্তে জমা রাখা হয়, পরে হিসাব করা নয়। এখানে “জমা রাখা” ডেটার সত্যতার শর্ত।
মূল উপলব্ধি: প্রশ্নটি “হিসাব করা যায় কি না” নয়, “সবসময় একই ফল আসবে কি না”। সময়ের সাথে বদলায় এমন উৎস থেকে নেওয়া মান ইতিহাস — জমা রাখুন। সত্যিকারের derived মান — হিসাব করুন, আর জমা রাখলে সামঞ্জস্যের ব্যবস্থা লিখে রাখুন।
অনুশীলন
খাতা-কলম নিন। একটি পুরো mapping, একটি কোয়েরি লেখা, একটি ফল আগে বলা আর একটি উল্টো পথ।
-
ম্যাপ করুন (হাতে): একটি ছোট হাসপাতালের ER: DEPARTMENT (dept_id, name) 1:N DOCTOR (doctor_id, name, phone — একাধিক হতে পারে); PATIENT (patient_id, name(first, last), birth_date, age — derived); PATIENT আর DOCTOR-এর M:N APPOINTMENT (attribute appt_at, fee; একই রোগী একই ডাক্তারকে বারবার দেখান); WARD (ward_no) আর তার weak entity BED (partial key bed_no). প্রতিটি টেবিল, PK, FK আর ON DELETE লিখুন।
- ধাপ ১: departments(dept_id, name); doctors(doctor_id, name, dept_id); patients(patient_id, first_name, last_name, birth_date) — age বাদ (derived, সময়-নির্ভর; VIEW-এ হিসাব); wards(ward_no).
- ধাপ ২: beds(ward_no, bed_no), ward_no → wards ON DELETE CASCADE.
- ধাপ ৪: doctors.dept_id → departments, ON DELETE RESTRICT (বিভাগ বন্ধ হলে ডাক্তারদের আগে অন্য বিভাগে সরাতে হবে); প্রতিটি ডাক্তার কোনো বিভাগে থাকলে NOT NULL.
- ধাপ ৫ (ইতিহাসসহ M:N): appointments(appointment_id, patient_id → patients, doctor_id → doctors, appt_at, fee), দুই FK NOT NULL আর RESTRICT — চিকিৎসার রেকর্ড মোছা যাবে না। চাইলে UNIQUE (doctor_id, appt_at): একজন ডাক্তারের একই সময়ে দুটি অ্যাপয়েন্টমেন্ট নয়।
- ধাপ ৬: doctor_phones(doctor_id, phone), doctor_id → doctors ON DELETE CASCADE.
মোট ৭টি টেবিল: departments, doctors, patients, wards, beds, appointments, doctor_phones.
-
কোয়েরি লিখুন: অংশ ৬-এর লাইব্রেরি schema-য় (প্রথম লাইব্রেরি এডিটরে চালাতে পারবেন) দুটি কোয়েরি লিখুন: (ক) যে সদস্যরা কখনো কোনো বই ধার নেননি; (খ) যেসব বইয়ের একাধিক লেখক। তারপর (গ) Learning SQL-এর কপি 1 (যা Rahim-এর কাছে আছে) Sabbir-কে ধার দেওয়ার INSERT লিখে কী হয় দেখুন।
-- (ক) SELECT m.member_id, m.first_name || ' ' || m.last_name AS member FROM members m WHERE NOT EXISTS (SELECT 1 FROM loans l WHERE l.member_id = m.member_id); -- (খ) SELECT b.title, COUNT(*) AS n_authors FROM books b JOIN book_authors ba ON ba.book_id = b.book_id GROUP BY b.book_id HAVING COUNT(*) > 1; -- (গ) INSERT INTO loans (loan_id, book_id, copy_no, member_id, loaned_on) VALUES (8, 1, 1, 3, '2025-10-11');(ক) একটি সারি:
5 | Tanvir Ahmed— anti join (পর্ব ২০, ২১)। (খ) দুটি:Learning SQL | 2,Data Structures | 2. (গ) ত্রুটি:UNIQUE constraint failed: loans.book_id, loans.copy_no— partial unique index one_open_loan বলছে কপিটি এখনো ফেরত আসেনি। একই INSERT copy_no 3 দিয়ে করলে ঢুকবে, কারণ সেই কপি তাকে আছে। -
ফল আগে বলুন (trace): লাইব্রেরি schema-র নিয়ম: book_authors.author_id → authors RESTRICT; copies.book_id → books CASCADE; loans → copies RESTRICT; books.publisher_id → publishers RESTRICT. নমুনা ডেটায় Shirin Sultana (author 3) শুধু Golper Jhuli-র লেখক; Data Structures (book 4)-এর একমাত্র কপি (4, 1) একবার ধার হয়ে ফেরত এসেছে (loan 4); Jamuna Books (publisher 2) শুধু Data Structures ছেপেছে। আলাদা আলাদা চালালে কী হবে: (ক)
DELETE FROM authors WHERE author_id = 3;(খ)DELETE FROM books WHERE book_id = 4;(গ)DELETE FROM publishers WHERE publisher_id = 2;(ক) বাতিল —
FOREIGN KEY constraint failed. book_authors-এ (3, 3) সারিটি Shirin-কে নির্দেশ করে, আর author_id-তে RESTRICT.(খ) বাতিল — একই ত্রুটি। প্রথমে CASCADE কপি (4, 1) মুছতে চায়, কিন্তু loan 4 সেই কপিকে নির্দেশ করে। ধারটি ফেরত এসেছে, তাতে কিছু বদলায় না — ইতিহাসের রেকর্ড এখনো আছে, আর RESTRICT “খোলা” আর “বন্ধ” ধারে পার্থক্য করে না।
(গ) বাতিল — books-এ Data Structures-এর publisher_id = 2, আর সেখানে RESTRICT. তিনটিই আটকায়, আর তিনটির পেছনেই একই নীতি: যার ওপর অন্য তথ্য দাঁড়িয়ে আছে, তা মোছা যায় না। লাইব্রেরি এডিটরে চালিয়ে মিলিয়ে নিন।
-
উল্টো পথ (reverse engineering): একটি পুরোনো রাইড-শেয়ার অ্যাপের schema পেলেন:
riders(rider_id PK, name),drivers(driver_id PK, name, referred_by REFERENCES drivers),vehicles(reg_no PK, driver_id UNIQUE NOT NULL REFERENCES drivers),trips(trip_id PK, rider_id NOT NULL REFERENCES riders, driver_id NOT NULL REFERENCES drivers, fare, started_at),rider_addresses(rider_id REFERENCES riders, label, address, PRIMARY KEY (rider_id, label)). ER-টি বর্ণনা করুন।- Strong entity: RIDER, DRIVER, VEHICLE, TRIP (প্রত্যেকের এক কলামের PK, কোনো FK PK-র অংশ নয়)।
- REFERS (recursive 1:N): drivers.referred_by → drivers; NULL চলে, তাই partial.
- OWNS / DRIVES (1:1): vehicles.driver_id-তে UNIQUE NOT NULL — প্রতিটি গাড়ি ঠিক একজন চালকের (total), একজন চালকের সর্বোচ্চ একটি গাড়ি।
- TRIP: RIDER আর DRIVER-এর মধ্যে একটি ঘটনা — M:N সম্পর্ক যা associative entity হয়েছে (নিজস্ব trip_id, attribute fare, started_at); দুই FK NOT NULL, তাই দুই দিকেই trip-এর অংশগ্রহণ total.
- rider_addresses: PK = FK + label — হয় multi-valued composite attribute “addresses(label, address)”, নয়তো weak entity ADDRESS (partial key label). schema দুটিকে আলাদা করতে পারে না; অন্য টেবিল এর দিকে FK দিলে weak entity ভাবাই ভালো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমElmasri–Navathe-এর ER-to-relational mapping-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Strong entity-র টেবিল বানানো
- Weak entity-র টেবিল — মালিকের key + partial key
- Binary 1:1 relationship — FK + UNIQUE
- Binary 1:N relationship — N দিকে FK
- Binary M:N relationship — junction টেবিল
- Multi-valued attribute — আলাদা টেবিল
DEPARTMENT আর EMPLOYEE-এর মধ্যে 1:N relationship WORKS_IN (একটি বিভাগে অনেক কর্মী, প্রত্যেক কর্মী একটি বিভাগে)। Relational schema-য় এটি কীভাবে আসবে?
- departments টেবিলে employee_id কলাম
- employees টেবিলে dept_id কলাম, departments-এর দিকে FK
- সবসময় একটি আলাদা works_in টেবিল লাগবে
- দুটি টেবিল মিলিয়ে একটি টেবিল
STUDENT আর COURSE-এর মধ্যে M:N relationship TAKES, যার attribute grade. দুটি entity আর এই relationship থেকে ন্যূনতম কয়টি টেবিল লাগবে, আর grade কোথায় যাবে?
- ২টি; grade students টেবিলে
- ২টি; grade courses টেবিলে
- ৩টি; grade junction টেবিলে
- ৪টি; grade-এর জন্য আলাদা টেবিল
Weak entity DEPENDENT (partial key dep_name) তার মালিক EMPLOYEE (key emp_id)-এর ওপর নির্ভরশীল। dependents টেবিলের primary key কী হবে?
- (emp_id, dep_name)
- শুধু dep_name
- শুধু emp_id
- একটি নতুন dependent_id, আর কোনো UNIQUE লাগবে না
CUSTOMER entity-র phone একটি multi-valued attribute (একজন গ্রাহকের যতগুলো খুশি নম্বর)। সবচেয়ে উপযুক্ত relational নকশা কোনটি?
- customers.phones কলামে কমা দিয়ে সব নম্বর
- customers-এ phone1, phone2, phone3 কলাম
- প্রতিটি নম্বরের জন্য customers-এ আলাদা সারি
- customer_phones(customer_id, phone) টেবিল, দুটি মিলে PK
MEMBER আর CARD-এর মধ্যে 1:1 relationship; প্রতিটি কার্ড ঠিক একজন সদস্যের, কিন্তু অনেক সদস্যের কার্ড নেই। FK পথে সবচেয়ে উপযুক্ত নকশা কোনটি?
- members টেবিলে card_no কলাম, NULL চলবে
- cards টেবিলে member_id, সাথে NOT NULL আর UNIQUE
- cards টেবিলে member_id, শুধু NOT NULL
- দুটি টেবিল মিলিয়ে একটি টেবিল
SUPPLIES নামে একটি ternary relationship SUPPLIER, PART আর PROJECT-কে যুক্ত করে (তিন দিকেই many), attribute quantity. এর জন্য তৈরি টেবিলের primary key কী?
- (supplier_id, part_id)
- তিনটি আলাদা binary টেবিল, প্রতিটির নিজস্ব PK
- (supplier_id, part_id, project_id)
- (quantity)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Entity → টেবিল: composite attribute ভেঙে কলাম, key → PRIMARY KEY; weak entity-র key = মালিকের key + partial key, FK-তে সাধারণত CASCADE.
- Cardinality ঠিক করে FK-র জায়গা: 1:N → N দিকে FK; 1:1 → total দিকে FK + UNIQUE; M:N → junction টেবিল, relationship-এর attribute সেখানে।
- Participation ঠিক করে NULL: total হলে FK-তে NOT NULL, partial হলে NULL চলে।
- এক ঘরে এক মান: multi-valued → আলাদা টেবিল; n-ary → সব FK-র টেবিল (তিনটি binary নয়); derived → VIEW বা generated column; recursive → একই টেবিলে ভূমিকার নামে FK.
- নিয়মের বাইরের সিদ্ধান্ত: একই জোড়া বারবার ঘটলে surrogate key; ON DELETE ঠিক হয় “অংশ নাকি ইতিহাস” দিয়ে; আর schema থেকে উল্টো নিয়মে ER উদ্ধার করা যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১১ · ডিজাইন কেস স্টাডি — বিশ্ববিদ্যালয় আর ই-কমার্স ডেটাবেসপরবর্তী পর্বER থেকে schema — দুটি পূর্ণ উদাহরণে প্রতিটি সিদ্ধান্ত।
- পর্ব ০৭ · ER মডেল — entity, attribute আর relationshipফিরে দেখুনযে নকশা থেকে এই পর্বের অনুবাদ শুরু।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Java · Spring Data JPAপাশের ট্র্যাক@OneToMany, @ManyToMany আর @Inheritance — ORM-এ একই mapping.
- Python · SQLAlchemy ORMহাতে-কলমেPython class থেকে টেবিল, relationship আর association table.