এই পর্বে যা শিখবেন
- Redundancy থেকে কীভাবে update, insertion আর deletion anomaly জন্মায় — আসল SQLite-এ চালিয়ে দেখা
- একটি কোচিং সেন্টারের এলোমেলো খাতাকে ধাপে ধাপে 1NF, 2NF আর 3NF-এ আনা
- 3NF-এর দুই সংজ্ঞা (Codd আর Zaniolo) এবং কেন দুটি একই কথা বলে
- Key আর FD দেখে যেকোনো relation-এর সর্বোচ্চ normal form বের করার নিয়মমাফিক পদ্ধতি, আর 3NF synthesis algorithm
- ভাঙা টেবিল JOIN করে মূল ডেটা ফেরত আনা — আর ভুল ভাঙনে কীভাবে ভুয়া সারি তৈরি হয়
১ · Normalization কী, আর কেন — কোচিং সেন্টারের একটি খাতা
মিরপুরের একটি কোচিং সেন্টারের অফিস সহকারী সব হিসাব রাখেন একটি স্প্রেডশিটে। প্রতিটি সারি একজন শিক্ষার্থী। নামের পাশে তাঁর ফোন নম্বরগুলো কমা দিয়ে, তারপর কোন কোন কোর্সে ভর্তি, প্রতিটি কোর্সের শিক্ষক, শিক্ষকের ফোন, মাসিক ফি আর ভর্তির তারিখ — সবই কমা দিয়ে একই ঘরে। দেখতে সুবিধাজনক: একজনের সব তথ্য এক সারিতে।
সমস্যা শুরু হয় প্রশ্ন করলে। “Math কোর্সে কে কে আছেন?” “প্রত্যেকের মোট মাসিক ফি কত?” “Kamal স্যারের নতুন ফোন নম্বর সবখানে বসান।” নিচের এডিটরে এই খাতাটিই একটি টেবিল coaching_center_records: কলামগুলো student_id, student_name, area, phones, courses, teachers, teacher_phones, fees আর enrolled_on; শেষের পাঁচটিতে কমা দিয়ে লেখা তালিকা। চারজন শিক্ষার্থী: Rafi (Math, Physics), Mim (Higher Math, Physics), Tonmoy (Math, Higher Math) আর Sumaiya (English).
-- Math কোর্সে কে কে? (তালিকার ভেতরে খোঁজা)
SELECT student_name, courses
FROM coaching_center_records
WHERE courses LIKE '%Math%';
-- প্রত্যেকের মোট ফি? (লেখাকে সংখ্যা বানানোর চেষ্টা)
SELECT student_name, fees, fees + 0 AS naive_total
FROM coaching_center_records;প্রথম কোয়েরিতে তিনটি সারি: Rafi Islam, Mim Chowdhury আর Tonmoy Das. কিন্তু Mim তো Math নেননি, নিয়েছেন Higher Math! LIKE '%Math%' লেখার ভেতরে “Math” শব্দাংশ খোঁজে, কোর্স চেনে না। দ্বিতীয় কোয়েরি আরও বিপজ্জনক: Rafi-র naive_total এসেছে 3000, অথচ আসল মোট 6000. Mim-এর 3500 (আসলে 6500), Tonmoy-র 3000 (আসলে 6500), শুধু Sumaiya-র 2500 ঠিক। SQLite '3000, 3000' লেখাটিকে সংখ্যা বানাতে গিয়ে শুরুর অংশ 3000 নিয়ে বাকিটা চুপচাপ ফেলে দিয়েছে — কোনো ত্রুটি দেখায়নি। (এটি SQLite-এর নিজস্ব শিথিলতা; PostgreSQL এমন রূপান্তরে ত্রুটি দেয়।) একটি ঘরে একাধিক মান থাকলে SQL আর সেগুলোকে আলাদা তথ্য হিসেবে দেখতে পারে না।
এই খাতার রোগ একটিই: একই তথ্য বহু জায়গায়, আর একাধিক তথ্য এক জায়গায়। Kamal Hossain-এর ফোন নম্বর তিনটি সারিতে চারবার লেখা। এটিই data redundancy, আর এর ফল অমিল (data inconsistency). Normalization হলো functional dependency (FD) দেখে টেবিলকে নিয়মমাফিক ভেঙে সাজানোর পদ্ধতি, যাতে প্রতিটি তথ্য ঠিক এক জায়গায় থাকে। FD, closure আর canonical cover পর্ব ৩৫-এ (Functional dependency) পড়েছেন; এখানে সেগুলোই কাজে লাগবে।
বিশ্ববিদ্যালয়ের অফিস প্রতিটি শিক্ষার্থীর ফর্মে বিভাগীয় প্রধানের নাম আর ফোন নম্বর হাতে লিখে রাখলে কী হবে ভাবুন। প্রধান বদলালে হাজারটি ফর্ম ঘষে ঠিক করতে হবে, আর কয়েকটি বাদ পড়বেই। বুদ্ধিমান অফিস বিভাগের তথ্য রাখে একটি আলাদা রেজিস্টারে, আর শিক্ষার্থীর ফর্মে লেখে শুধু বিভাগের কোড। Normalization ঠিক এই কাজটিকে নিয়ম আর প্রমাণের ভাষায় বাঁধে।
Normal form — মানের সিঁড়ি
একটি normal form হলো একটি relation schema-র মানের স্তর, যা কিছু শর্ত দিয়ে সংজ্ঞায়িত। প্রতিটি উঁচু ধাপ নিচের সব ধাপের শর্ত মানে, আর আরও এক ধরনের redundancy সরায়:
| ধাপ | শর্ত (সংক্ষেপে) | যা সরায় |
|---|---|---|
| 1NF | প্রতিটি ঘরে একটি atomic মান | তালিকা, repeating group |
| 2NF | 1NF + কোনো partial dependency নেই | key-র অংশের ওপর নির্ভরতা |
| 3NF | 2NF + কোনো transitive dependency নেই | non-key থেকে non-key নির্ভরতা |
| BCNF, 4NF, 5NF | আরও কড়া শর্ত | বাকি redundancy (পর্ব ৩৭) |
William Kent ১৯৮৩ সালে 3NF পর্যন্ত পুরো ধারণাটিকে একটি বাক্যে বেঁধেছিলেন, যা আজও পাঠ্যবইয়ে উদ্ধৃত হয়: প্রতিটি non-key attribute তথ্য দেবে key সম্পর্কে (1NF), পুরো key সম্পর্কে (2NF), আর key ছাড়া অন্য কিছু সম্পর্কে নয় (3NF).
২ · তিন রকম anomaly — চালিয়ে দেখুন
স্প্রেডশিটটিকে সামান্য গুছিয়ে প্রতিটি (শিক্ষার্থী, কোর্স) জোড়ার জন্য একটি সারি করা যাক (কীভাবে, তা অংশ ৩-এ ধাপে ধাপে দেখবেন)। টেবিল coaching_records: student_id, student_name, area, course_code, course_title, teacher_id, teacher_name, teacher_phone, fee, enrolled_on — ৭টি সারি, primary key (student_id, course_code). প্রতিটি ঘরে এখন একটি মান, তবু redundancy রয়ে গেছে। এর তিনটি পরিণতিকে বলে তিন anomaly.
Update anomaly
একটি তথ্য বদলাতে বহু সারি বদলাতে হয়; একটিও বাদ পড়লে ডেটা পরস্পরবিরোধী।
Kamal-এর ফোন ৪ সারিতেInsertion anomaly
একটি তথ্য ঢোকাতে অন্য একটি অপ্রাসঙ্গিক তথ্যও লাগে।
শিক্ষার্থী ছাড়া নতুন কোর্স নয়Deletion anomaly
একটি তথ্য মুছলে অন্য একটি দরকারি তথ্যও হারিয়ে যায়।
শেষ শিক্ষার্থী গেলে শিক্ষকও গেলেনপ্রথমে update anomaly. Kamal Hossain (T1) নতুন সিম নিয়েছেন। অফিস সহকারী সামনে থাকা একটি সারিই বদলালেন:
-- Kamal Hossain (T1)-এর নতুন নম্বর: শুধু একটি সারি বদলানো হলো
UPDATE coaching_records SET teacher_phone = '01700000099'
WHERE student_id = 'S1' AND course_code = 'MATH';
-- এখন একজন শিক্ষকের কয়টি নম্বর?
SELECT DISTINCT teacher_id, teacher_name, teacher_phone
FROM coaching_records ORDER BY teacher_id, teacher_phone;
-- ঠিকভাবে করতে হলে কয়টি সারি বদলাতে হতো?
SELECT COUNT(*) AS rows_for_T1 FROM coaching_records WHERE teacher_id = 'T1';UPDATE-এর পর “১টি সারি বদলেছে”। DISTINCT-এর ফলে চারটি সারি: T1 | Kamal Hossain | 01700000001, T1 | Kamal Hossain | 01700000099, T2 | Nasrin Akter | 01700000002 আর T3 | Jahid Hasan | 01700000003. একজন মানুষের দুটি “সঠিক” নম্বর — কোনটিতে ফোন করবেন, ডেটাবেস বলতে পারে না। শেষ কোয়েরি দেখায় rows_for_T1 = 4: একটি তথ্য বদলাতে চারটি সারি ছুঁতে হতো। কোনো constraint এই অমিল আটকায়নি, কারণ টেবিলের কোনো নিয়মই বলে না “এক teacher_id-এর একটিই ফোন”।
এবার Sumaiya Khan কোচিং ছেড়ে দিলেন, আর ঠিক পরেই সেন্টার একটি নতুন Biology কোর্স খুলতে চায়, শিক্ষক Sadia Rahman (T4), কিন্তু এখনো কোনো শিক্ষার্থী ভর্তি হননি:
-- Sumaiya (S4) কোচিং ছাড়লেন: তাঁর একমাত্র সারি মুছল
DELETE FROM coaching_records WHERE student_id = 'S4';
-- কোন কোন শিক্ষকের তথ্য এখনো আছে?
SELECT DISTINCT teacher_id, teacher_name, teacher_phone
FROM coaching_records ORDER BY teacher_id;
-- শিক্ষার্থী ছাড়া নতুন কোর্স Biology ঢোকানোর চেষ্টা
INSERT INTO coaching_records (student_id, course_code, course_title,
teacher_id, teacher_name, teacher_phone, fee)
VALUES (NULL, 'BIO', 'Biology', 'T4', 'Sadia Rahman', '01700000004', 3000);DELETE-এ “১টি সারি বদলেছে”, আর SELECT-এ এখন মাত্র দুজন শিক্ষক: T1 Kamal Hossain আর T2 Nasrin Akter. Jahid Hasan (T3)-এর নাম, ফোন আর English কোর্সের ফি — সব উধাও, অথচ আমরা শুধু একজন শিক্ষার্থীকে মুছতে চেয়েছিলাম। এটি deletion anomaly. শেষ INSERT-এ ত্রুটি: NOT NULL constraint failed: coaching_records.student_id. Primary key-র অংশ student_id ফাঁকা রাখা যায় না, তাই শিক্ষার্থী ছাড়া কোর্স ঢোকানোই অসম্ভব — insertion anomaly. নকল একজন “শিক্ষার্থী” বানিয়ে ঢোকানো যেত, কিন্তু সেটি ডেটায় মিথ্যা যোগ করা।
তিনটি anomaly-র মূল কারণ এক: একটি টেবিলে কয়েকটি আলাদা “বিষয়” (শিক্ষার্থী, কোর্স, শিক্ষক, ভর্তি) একসাথে গাদা করা। প্রতিটি বিষয়ের নিজের key আছে, আর সেই key-র ওপর নির্ভর করা তথ্য তার নিজের টেবিলে থাকা উচিত। Normal form-গুলো ঠিক এই “নির্ভরতা”-কে FD-র ভাষায় মাপে।
৩ · 1NF — প্রতিটি ঘরে একটি মান
1NF: একটি relation 1NF-এ আছে, যদি প্রতিটি attribute-এর domain atomic হয় — প্রতিটি ঘরে domain থেকে নেওয়া ঠিক একটি মান, কোনো তালিকা, সেট বা ঘরের ভেতরে টেবিল নয় — আর কোনো repeating group (course1, course2, course3 ধরনের পুনরাবৃত্ত কলামের দল) নেই।
Codd-এর relational model-এ relation সংজ্ঞাগতভাবেই 1NF-এ (পর্ব ৫)। তাই 1NF আসলে “টেবিলকে আসল relation বানানো”-র ধাপ। নিচের ডায়াগ্রামে খাতাটিকে 1NF-এ আনুন:
| টেবিল | |
| মোট সারি | |
| 1NF? |
- শুরুর খাতা (unnormalized). চারটি সারি, প্রতিটি একজন শিক্ষার্থী। Rafi-র সারিতে দুটি ফোন, দুটি কোর্স, আর প্রতিটি কোর্সের শিক্ষক, শিক্ষকের ফোন, ফি ও তারিখ — সব কমা দিয়ে একেকটি ঘরে।
- দুই রকম সমস্যা চিহ্নিত। phones একটি সরল তালিকা। আর courses, teachers, t_phones, fees, enrolled — এই পাঁচটি তালিকা আসলে একটি দল: প্রতিটির প্রথম মান মিলে একটি ভর্তি (Math, Kamal, 3000…), দ্বিতীয় মান মিলে আরেকটি। একে বলে repeating group.
- ফোনের তালিকা আলাদা টেবিলে। student_phones-এ প্রতিটি (শিক্ষার্থী, ফোন) জোড়ার একটি সারি: Rafi-র দুটি, Sumaiya-র একটি — মোট ৬টি। ফোনগুলোকে কোর্সের সারিতেই ছড়িয়ে দিলে Rafi-র ২ ফোন × ২ কোর্স = ৪টি সারি হতো, প্রতিটি ফোন দুবার। দুটি স্বাধীন তালিকা এক টেবিলে মেশানোর এই সমস্যা পর্ব ৩৭-এর 4NF-এর বিষয়।
- Repeating group থেকে সারি। দলটির প্রতিটি “অবস্থান” একটি আলাদা সারি হলো, সাথে শিক্ষার্থীর তথ্য কপি হলো। Rafi এখন দুটি সারিতে: MATH আর PHY. কোর্সের নামের পাশে একটি কোড (course_code) আর শিক্ষকের একটি id (teacher_id) যোগ করা হলো, যাতে নাম না মিলিয়ে কোড দিয়ে চেনা যায়।
- ফল: 1NF. coaching_records-এ ৭টি সারি, key (student_id, course_code); student_phones-এ ৬টি সারি। প্রতিটি ঘরে একটি মান — 1NF ঠিক। কিন্তু খেয়াল করুন, redundancy বেড়েছে: Rafi-র নাম দুবার, Kamal-এর ফোন চারবার। অংশ ২-এর anomaly-গুলো এই টেবিলেরই। 1NF শুধু প্রথম ধাপ।
“Atomic” মানে কী — ব্যবহারই ঠিক করে
একটি তারিখ 2026-01-05-এর ভেতরে বছর, মাস আর দিন আছে। তাহলে কি এটি atomic নয়? আবার একটি নাম “Rafi Islam”-এ দুটি অংশ। উত্তর: atomic মানে পদার্থবিদ্যার “অবিভাজ্য” নয়, মানে ডেটাবেস যেভাবে ব্যবহার করে, তাতে মানটি একক। তারিখকে আমরা প্রায় সবসময় একটি একক হিসেবে তুলনা, সাজানো আর বিয়োগ করি; DBMS-এর date ফাংশন ভেতরের অংশ বের করে দিতে পারে, তাই এটি atomic ধরা হয়। কিন্তু phones-এর তালিকা থেকে আমরা আলাদা আলাদা নম্বর খুঁজি, যাচাই করি, একটি বাদ দিই — সেখানে প্রতিটি নম্বরই আলাদা তথ্য, তাই তালিকাটি atomic নয়।
একই যুক্তিতে “Rafi Islam” একটি দোকানের জন্য atomic হতে পারে, কিন্তু যে ব্যাংক পদবি ধরে চিঠি সাজায়, তার জন্য first_name আর last_name আলাদা রাখাই ভালো। একটি পরীক্ষা কাজে দেয়: যদি কখনো ঘরের একটি অংশ ধরে খুঁজতে, বদলাতে, গুনতে বা foreign key দিয়ে জুড়তে হয়, তাহলে সেটি atomic নয়।
Array আর JSON কলাম — 1NF কি ভাঙে?
আধুনিক DBMS একটি ঘরেই তালিকা রাখতে দেয়। SQL:1999 standard-এ ARRAY টাইপ আসে, আর PostgreSQL-এ array ও jsonb কলাম জনপ্রিয় (পর্ব ৩০)। নিচের ব্লকটি চালানো যায় না, শুধু PostgreSQL-এর ধরন দেখানোর জন্য:
CREATE TABLE students (
student_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
phones TEXT[] -- একটি ঘরে নম্বরের তালিকা
);
-- তালিকার ভেতরে খোঁজা যায়; @> (contains) লিখলে GIN index দিয়ে দ্রুতও করা যায়
SELECT name FROM students WHERE phones @> ARRAY['01511000103'];তাত্ত্বিকদের মধ্যে এ নিয়ে বিতর্ক আছে। C. J. Date-এর মতো লেখকেরা যুক্তি দিয়েছেন, domain-এর একটি মান নিজেই একটি তালিকা বা relation হতে পারে, আর তাতে relation 1NF-এর বাইরে যায় না — সমস্যা হলো সেই মানের ভেতরে ঢুকে প্রশ্ন করা। ব্যবহারিক নিয়মটি তাই সহজ: যদি তালিকাটি সবসময় একসাথে পড়া-লেখা হয় (একটি পোস্টের ট্যাগ দেখানো, একটি API-র সম্পূর্ণ উত্তর জমা রাখা), array বা JSON ঠিক আছে। কিন্তু যদি ভেতরের মান ধরে JOIN, foreign key, UNIQUE বা গণনা লাগে — “এই নম্বর আর কারও নামে আছে কি?” — তাহলে আলাদা টেবিলই সঠিক, কারণ তালিকার ভেতরের মানে foreign key বা UNIQUE বসানো যায় না।
৪ · 2NF — পুরো key-র ওপর নির্ভরতা
2NF আর 3NF বুঝতে দুটি শব্দ লাগে। যে attribute কোনো একটি candidate key-র অংশ, সেটি prime attribute; বাকিরা non-prime attribute. coaching_records-এর একমাত্র candidate key {student_id, course_code}, তাই prime শুধু এই দুটি, বাকি আটটি non-prime.
এবার এই টেবিলের FD-গুলো লিখি — ব্যবসার নিয়ম থেকে, ডেটা থেকে নয় (প্রতিটি কোর্সের একজন শিক্ষক আর একটি নির্দিষ্ট ফি; প্রতিটি শিক্ষকের একটি নাম আর একটি অফিসিয়াল ফোন):
F2: course_code → course_title, teacher_id, fee
F3: teacher_id → teacher_name, teacher_phone
F4: student_id, course_code → enrolled_on
Full আর partial dependency: X → A-তে যদি X-এর কোনো প্রকৃত উপসেট Y থেকেও Y → A হয়, তাহলে A, X-এর ওপর partially dependent; না হলে fully dependent.
2NF: relation 1NF-এ আছে, আর প্রতিটি non-prime attribute প্রতিটি candidate key-র ওপর fully dependent. অর্থাৎ কোনো non-prime attribute কোনো key-র অংশ থেকে নির্ধারিত হয় না।
একটি সরাসরি ফল: যে relation-এর প্রতিটি candidate key একটিমাত্র attribute, সেটি 1NF-এ থাকলেই 2NF-এ, কারণ একটি attribute-এর প্রকৃত উপসেট শুধু খালি সেট। 2NF-এর প্রশ্ন ওঠে কেবল composite key-তে। নিচের ডায়াগ্রামে coaching_records-কে 2NF, তারপর 3NF-এ আনুন:
| টেবিল (student_phones বাদে) | |
| সর্বোচ্চ normal form | |
| Kamal-এর ফোন কয়বার লেখা |
- শুরু: 1NF টেবিল আর চারটি FD. F1 থেকে F4 লেখা আছে। (student_id, course_code)-এর closure-এ F1, F2, F3, F4 মিলে দশটি attribute-ই আসে, তাই এটি key; আর কোনো একটি অংশ একা সব নির্ধারণ করে না, তাই এটিই candidate key.
- F1 — partial dependency. student_name আর area নির্ধারিত হয় শুধু student_id দিয়ে, যা key-র অর্ধেক। তাই Rafi-র নাম তাঁর প্রতিটি কোর্সের সারিতে কপি হয়। এটি 2NF ভাঙে।
- F2 আর F3 — আরও partial. course_title, teacher_id আর fee শুধু course_code-এর ওপর নির্ভর করে। আর course_code → teacher_id → teacher_name হওয়ায় teacher_name ও teacher_phone-ও শুধু course_code থেকে আসে। একমাত্র enrolled_on পুরো key-র ওপর নির্ভর করে (F4).
- 2NF-এর ভাঙন। প্রতিটি partial dependency-র জন্য একটি আলাদা টেবিল: key-র সেই অংশ আর তার ওপর নির্ভর করা attribute-গুলো। students আর courses তৈরি হলো; পুরো key-র ওপর নির্ভর করা enrolled_on থাকল enrollments-এ। এখন Kamal-এর ফোন দুবার — MATH আর HMATH দুটি কোর্সের সারিতে।
- 3NF পরীক্ষা — courses-এ সমস্যা। courses-এর key course_code. কিন্তু teacher_name আসে teacher_id থেকে, আর teacher_id নিজে key নয়: একজন শিক্ষক দুটি কোর্স পড়ালে তাঁর নাম-ফোন দুবার লেখা হয়। একটি non-key attribute থেকে আরেকটি non-key attribute নির্ধারিত — এটি transitive dependency.
- 3NF-এর ভাঙন। teacher_id আর তার ওপর নির্ভর করা attribute-গুলো নিয়ে teachers টেবিল; courses-এ থাকল শুধু teacher_id (foreign key). ফল: চারটি টেবিল আর student_phones, সবকটি 3NF-এ। Kamal-এর ফোন এখন ঠিক একবার লেখা।
প্রচলিত ভুল: “টেবিলে id কলাম আছে, তাই 2NF ঠিক।” যদি টেবিলে surrogate id থাকে কিন্তু (student_id, course_code)-ও একটি candidate key হয়, তাহলে student_name সেই candidate key-র অংশের ওপর নির্ভর করে — 2NF এখনো ভাঙা। সংজ্ঞায় শুধু primary key নয়, প্রতিটি candidate key দেখতে হয়। একটি নতুন id কলাম redundancy সরায় না, শুধু লুকায়।
৫ · 3NF — transitive dependency আর Zaniolo-র সংজ্ঞা
3NF-এর দুটি প্রচলিত সংজ্ঞা আছে। পরীক্ষায় দুটিই আসে, তাই দুটিই জানুন।
Relation 2NF-এ আছে, আর কোনো non-prime attribute কোনো candidate key-র ওপর transitively dependent নয়। Transitive dependency মানে: K → X আর X → A, যেখানে X ↛ K (X থেকে key আসে না), A ∉ X, আর A non-prime.
F⁺-এর প্রতিটি nontrivial FD X → A-র জন্য অন্তত একটি সত্য: (ক) X একটি superkey, অথবা (খ) A একটি prime attribute.
দ্বিতীয়টি যাচাইয়ে অনেক সহজ: প্রতিটি FD দেখুন, বামপাশ superkey কি না দেখুন, না হলে ডানপাশ prime কি না দেখুন। কিন্তু দুটি কি একই কথা বলে? চলুন উভয় দিক দেখি।
Zaniolo ভাঙলে Codd-ও ভাঙে। ধরুন X → A এমন যে X superkey নয় আর A non-prime. যেকোনো candidate key K নিন। K সব কিছু নির্ধারণ করে, তাই K → X. X superkey নয়, তাই X ↛ K. A non-prime বলে A ∉ K, আর X → A nontrivial বলে A ∉ X. তাহলে K → X → A একটি transitive dependency — Codd-এর শর্ত ভাঙল। (বিশেষ ঘটনা: X যদি K-র প্রকৃত উপসেট হয়, এটি আসলে partial dependency — তাই Zaniolo-র একটি শর্তই 2NF আর 3NF দুটিকে একসাথে ধরে।)
Codd ভাঙলে Zaniolo-ও ভাঙে। Partial dependency Y → A (Y ⊂ K, A non-prime) থাকলে Y superkey হতে পারে না — তাহলে K minimal হতো না — তাই Y → A Zaniolo ভাঙে। আর transitive K → X → A (X ↛ K) থাকলে X superkey নয়, A non-prime, তাই X → A Zaniolo ভাঙে। দুই দিকেই মিলল, অতএব সংজ্ঞা দুটি সমতুল্য। ∎
ট্রেনের টিকিটের কথা ভাবুন। টিকিট নম্বর (key) থেকে জানা যায় ট্রেন নম্বর, আর ট্রেন নম্বর থেকে জানা যায় ছাড়ার সময়। ছাড়ার সময় টিকিটের তথ্য নয়, ট্রেনের তথ্য — টিকিটের ঘরে লিখলে একটি ট্রেনের সময় বদলালে হাজার টিকিট বদলাতে হবে। Zaniolo-র ভাষায়: “ট্রেন নম্বর → ছাড়ার সময়” FD-র বামপাশ টিকিট-টেবিলের superkey নয়, আর ছাড়ার সময় কোনো key-র অংশ নয়। তাই ভাঙতে হবে।
শর্ত (খ)-টি কেন রাখা হলো? কারণ এটি না থাকলে কিছু relation ভাঙার পর কোনো একটি FD আর কোনো একক টেবিলের ভেতরে যাচাই করা যায় না। শর্তটি তুলে দিলে যে কড়া রূপ পাওয়া যায়, সেটিই BCNF: প্রতিটি nontrivial X → A-তে X superkey. পর্ব ৩৭-এ দেখবেন, BCNF সবসময় dependency রক্ষা করে পাওয়া যায় না, কিন্তু 3NF যায় — এজন্যই 3NF বাস্তবের সাধারণ লক্ষ্য।
১৯৭০ সালে Edgar F. Codd relational model-এর প্রবন্ধে “nonsimple domain” দূর করার প্রক্রিয়াকে normalization বলেন — আজকের 1NF. পরের বছর, ১৯৭১ সালে “Further Normalization of the Data Base Relational Model” প্রবন্ধে তিনি 2NF আর 3NF-এর সংজ্ঞা দেন (প্রবন্ধটি ১৯৭২ সালে বই আকারে প্রকাশিত হয়)। ১৯৭৪ সালে Raymond Boyce-এর সাথে আসে BCNF. ১৯৭৬ সালে Philip Bernstein FD থেকে সরাসরি 3NF schema বানানোর synthesis algorithm প্রকাশ করেন, আর ১৯৭৯ সালে Biskup, Dayal ও Bernstein দেখান, একটি key-ধারী schema যোগ করলে ভাঙনটি lossless-ও হয়। Carlo Zaniolo-র superkey-বা-prime সংজ্ঞাটি আসে ১৯৮২ সালে। Codd কেন “normalization” শব্দটি বেছে নিয়েছিলেন, তা নিয়ে একটি জনপ্রিয় গল্প আছে — সে সময় সংবাদপত্রে যুক্তরাষ্ট্র-চীন সম্পর্ক “normalize” করার খবর চলছিল; গল্পটি Codd-এর এক সাক্ষাৎকার থেকে ছড়ালেও একে ইতিহাসের মজার টীকা হিসেবেই নিন।
৬ · ভাঙার পরে — anomaly নেই, তথ্যও হারায়নি
এবার 3NF-এ ভাঙা পাঁচটি টেবিল আসল SQLite-এ। একই ডেটা: students (student_id, name, area — ৪ জন), student_phones (student_id, phone — ৬টি), teachers (teacher_id, name, phone — ৩ জন), courses (course_code, title, teacher_id, fee — ৪টি) আর enrollments (student_id, course_code, enrolled_on — ৭টি)। অংশ ২-এর তিনটি কাজ আবার করুন:
-- update: Kamal-এর নতুন নম্বর, এক জায়গায়
UPDATE teachers SET phone = '01700000099' WHERE teacher_id = 'T1';
-- delete: Sumaiya-র ভর্তি বাতিল
DELETE FROM enrollments WHERE student_id = 'S4';
-- insert: শিক্ষার্থী ছাড়াই নতুন শিক্ষক আর কোর্স
INSERT INTO teachers VALUES ('T4', 'Sadia Rahman', '01700000004');
INSERT INTO courses VALUES ('BIO', 'Biology', 'T4', 3000);
SELECT c.course_code, c.title, t.name, t.phone
FROM courses c JOIN teachers t ON t.teacher_id = c.teacher_id
ORDER BY c.course_code;চারটি পরিবর্তনের প্রতিটিতে “১টি সারি বদলেছে”। শেষ SELECT-এ পাঁচটি কোর্স: BIO | Biology | Sadia Rahman | 01700000004, ENG | English | Jahid Hasan | 01700000003, HMATH | Higher Math | Kamal Hossain | 01700000099, MATH | Math | Kamal Hossain | 01700000099 আর PHY | Physics | Nasrin Akter | 01700000002. তিনটি anomaly-ই উধাও: Kamal-এর নম্বর একটি সারি বদলেই দুটি কোর্সে নতুন; Sumaiya-র ভর্তি গেলেও Jahid আর English কোর্স অক্ষত (Sumaiya নিজেও students-এ আছেন); আর Biology ঢুকল কোনো নকল শিক্ষার্থী ছাড়াই।
JOIN করে মূল টেবিল ফেরত — কোনো তথ্য হারায়নি তো?
ভাঙন তখনই নিরাপদ, যখন ভাঙা টেবিলগুলো আবার জুড়ে হুবহু মূল টেবিলটি পাওয়া যায় — একটি সারিও কম নয়, একটিও বেশি নয়। নিচের এডিটরে মূল coaching_records আর ভাঙা টেবিল দুটোই আছে। JOIN দিয়ে একটি view বানিয়ে দুই দিকে EXCEPT চালানো হলো:
CREATE VIEW rebuilt AS
SELECT e.student_id, s.name AS student_name, s.area, e.course_code,
c.title AS course_title, c.teacher_id, t.name AS teacher_name,
t.phone AS teacher_phone, c.fee, e.enrolled_on
FROM enrollments e
JOIN students s ON s.student_id = e.student_id
JOIN courses c ON c.course_code = e.course_code
JOIN teachers t ON t.teacher_id = c.teacher_id;
SELECT * FROM rebuilt ORDER BY student_id, course_code;
SELECT
(SELECT COUNT(*) FROM (SELECT * FROM coaching_records EXCEPT SELECT * FROM rebuilt)) AS missing,
(SELECT COUNT(*) FROM (SELECT * FROM rebuilt EXCEPT SELECT * FROM coaching_records)) AS extra;CREATE VIEW-এ ✓. দ্বিতীয় ফলে ৭টি সারি, প্রথমটি S1 | Rafi Islam | Mirpur | MATH | Math | T1 | Kamal Hossain | 01700000001 | 3000 | 2026-01-05, শেষটি S4 | Sumaiya Khan | Dhanmondi | ENG | English | T3 | Jahid Hasan | 01700000003 | 2500 | 2026-01-12 — হুবহু মূল টেবিলের মতো। শেষ সারিতে missing = 0 আর extra = 0: মূলের কোনো সারি হারায়নি, কোনো ভুয়া সারিও তৈরি হয়নি। এমন ভাঙনকে বলে lossless-join decomposition. মনে রাখুন, এটি একটি নির্দিষ্ট ডেটায় পরীক্ষা মাত্র; সব সম্ভাব্য ডেটায় lossless কি না, তা প্রমাণ হয় FD দিয়ে (নিচে আর পর্ব ৩৭-এ)। অবশ্য ভাঙা টেবিল এখন এমন তথ্যও রাখতে পারে যা মূল টেবিল পারত না — যেমন শিক্ষার্থীহীন কোর্স।
যে প্রশ্নগুলো এখন সহজ
অংশ ১-এর যে প্রশ্নে LIKE ভুল উত্তর দিয়েছিল আর ফি যোগ করা যায়নি, সেগুলো এখন সাধারণ JOIN আর GROUP BY:
-- 1. কোন কোর্সে কতজন
SELECT c.title, COUNT(*) AS students
FROM enrollments e JOIN courses c ON c.course_code = e.course_code
GROUP BY c.course_code ORDER BY students DESC, c.title;
-- 2. শুধু Math-এর শিক্ষার্থী (Higher Math নয়)
SELECT s.name FROM students s
JOIN enrollments e ON e.student_id = s.student_id WHERE e.course_code = 'MATH';
-- 3. প্রত্যেকের মোট মাসিক ফি
SELECT s.name, SUM(c.fee) AS total_fee
FROM students s JOIN enrollments e ON e.student_id = s.student_id
JOIN courses c ON c.course_code = e.course_code
GROUP BY s.student_id ORDER BY s.student_id;
-- 4. ফোন নম্বর থেকে শিক্ষার্থী
SELECT s.name FROM student_phones p
JOIN students s ON s.student_id = p.student_id WHERE p.phone = '01511000103';প্রথম ফল: Higher Math | 2, Math | 2, Physics | 2, English | 1. দ্বিতীয়টিতে শুধু Rafi Islam আর Tonmoy Das — Mim আর ভুল করে আসেননি, কারণ এখন তুলনা হচ্ছে পুরো কোড দিয়ে, লেখার টুকরো দিয়ে নয়। তৃতীয়টিতে সঠিক মোট: Rafi Islam 6000, Mim Chowdhury 6500, Tonmoy Das 6500, Sumaiya Khan 2500. চতুর্থটিতে Tonmoy Das — তাঁর দ্বিতীয় নম্বর দিয়েও খোঁজা গেল। আর student_phones-এর key (student_id, phone) এখন নিশ্চিত করে, একজনের একই নম্বর দুবার লেখা হবে না।
ভুল ভাঙন — ভুয়া সারি (lossless পরীক্ষার পূর্বাভাস)
সব ভাঙন নিরাপদ নয়। ধরুন কেউ ভর্তির তথ্যকে (student_id, teacher_id) আর (teacher_id, course_code) — দুটি টেবিলে ভাঙলেন, ভাবলেন teacher_id দিয়ে আবার জোড়া যাবে:
CREATE TABLE st AS SELECT DISTINCT student_id, teacher_id FROM coaching_records;
CREATE TABLE tc AS SELECT DISTINCT teacher_id, course_code FROM coaching_records;
SELECT COUNT(*) AS original FROM coaching_records;
SELECT COUNT(*) AS rejoined FROM st JOIN tc ON tc.teacher_id = st.teacher_id;
-- যে সারিগুলো আসলে কখনো ছিল না
SELECT st.student_id, tc.course_code
FROM st JOIN tc ON tc.teacher_id = st.teacher_id
EXCEPT SELECT student_id, course_code FROM coaching_records;original = 7, কিন্তু rejoined = 9. শেষ কোয়েরিতে দুটি spurious tuple: S1 | HMATH আর S2 | MATH. Kamal (T1) দুটি কোর্স পড়ান; Rafi তাঁর কাছে শুধু Math পড়েন, কিন্তু জোড়ার সময় “Rafi–Kamal” আর “Kamal–Higher Math” মিলে বানিয়ে দিল “Rafi Higher Math পড়েন” — যা কখনো ঘটেনি। বেশি সারি মানে বেশি তথ্য নয়, বরং আসল তথ্য হারানো: কে আসলে কোন কোর্সে, সেটি আর বলা যায় না। কারণ সাধারণ টেবিল দুটির মিল teacher_id, আর teacher_id কোনো দিকেরই key নয়।
এখান থেকেই lossless ভাঙনের পরীক্ষা: R-কে R₁ আর R₂-তে ভাঙন lossless, যদি এবং কেবল যদি F⁺-এ R₁ ∩ R₂ → R₁ অথবা R₁ ∩ R₂ → R₂ থাকে — অর্থাৎ সাধারণ attribute-গুলো অন্তত একটি টেবিলের superkey. students আর enrollments-এর মিল student_id, যা students-এর key — তাই lossless. উপরের st আর tc-র মিল teacher_id, যা কোনো দিকের key নয় — তাই lossy. প্রমাণ, দুইয়ের বেশি টেবিলের ভাঙন আর dependency preservation পর্ব ৩৭-এ।
৭ · নিয়মমাফিক যাচাই আর 3NF synthesis algorithm
কোচিং সেন্টারের উদাহরণে অনুমান দিয়ে চলেছি। পরীক্ষায় আর বড় ডিজাইনে লাগে একটি যান্ত্রিক পদ্ধতি — R আর F দেওয়া থাকলে প্রতিটি ধাপ নিশ্চিতভাবে করা যায় এমন:
১. F-এর প্রতিটি FD-র ডানপাশ একক attribute-এ ভাঙুন (X → AB হলে X → A, X → B).
২. সব candidate key খুঁজুন। যে attribute কোনো FD-র ডানপাশে নেই, সেটি প্রতিটি key-তে থাকবেই — তাদের দিয়ে শুরু করে attribute closure বাড়িয়ে দেখুন।
৩. Prime আর non-prime attribute আলাদা করুন।
৪. 2NF: প্রতিটি key K-র প্রতিটি প্রকৃত উপসেট Y-র closure Y⁺ বের করুন; Y⁺-এ Y-এর বাইরের কোনো non-prime attribute থাকলে 2NF ভাঙা।
৫. 3NF: প্রতিটি X → A দেখুন; X superkey নয় এবং A non-prime হলে 3NF ভাঙা।
৬. BCNF: প্রতিটি nontrivial X → A-তে X superkey কি না (পর্ব ৩৭)।
ধাপ ৫-এ শুধু দেওয়া F দেখলেই চলে, পুরো F⁺ লাগে না — মূল relation R-এর জন্য এটি প্রমাণিত সত্য। কিন্তু ভাঙার পরের ছোট টেবিল যাচাইয়ের সময় সেই টেবিলে প্রক্ষিপ্ত FD-গুলো (F⁺ থেকে যেগুলোর সব attribute টেবিলটির ভেতরে) দেখতে হয়। একটি উদাহরণে পুরো পদ্ধতি চালান:
| এই ধাপে | |
| এ পর্যন্ত জানা |
- দেওয়া আছে। পাঁচটি attribute আর চারটি FD. দুটি প্রশ্ন: candidate key কী, আর R সর্বোচ্চ কোন normal form-এ আছে?
- Key খোঁজা। A কোনো FD-র ডানপাশে নেই, তাই কোনো FD A-কে “তৈরি” করতে পারে না — প্রতিটি key-তে A থাকতেই হবে। A⁺ বের করলে A → BC, তারপর AB → D, তারপর D → E দিয়ে পুরো R আসে। A একাই key, আর A থাকা যেকোনো বড় সেট minimal নয় — তাই {A} একমাত্র candidate key.
- Prime ভাগ। prime শুধু A; B, C, D, E non-prime. ডানপাশ একক করলে পাঁচটি FD: A → B, A → C, B → C, AB → D, D → E.
- 2NF. key একটিমাত্র attribute, তার প্রকৃত উপসেট শুধু খালি সেট, যা কিছুই নির্ধারণ করে না। তাই partial dependency অসম্ভব — R 2NF-এ আছে। (AB → D দেখে ভয় পাবেন না: AB key নয়, superkey; partial dependency মাপা হয় candidate key-র অংশ দিয়ে।)
- 3NF. A → B, A → C, AB → D ঠিক আছে, কারণ বামপাশ superkey. কিন্তু B → C-তে B⁺ = {B, C}, B superkey নয় আর C non-prime — লঙ্ঘন। D → E-তেও একই অবস্থা। দুটিই transitive: A → B → C আর A → D → E.
- সিদ্ধান্ত। candidate key {A}; সর্বোচ্চ normal form 2NF. 3NF-এ আনতে হলে ভাঙতে হবে — নিচের synthesis algorithm সেটিই করে।
এই relation-এর canonical cover: A → BC-তে C বাড়তি (A → B আর B → C থেকেই আসে), আর AB → D-তে B বাড়তি (A⁺-এ D আছে)। তাই Fc = {A → BD, B → C, D → E}, আর 3NF ভাঙন (A, B, D), (B, C), (D, E) — প্রথমটিতে key A আছে। কাজটি যে পদ্ধতিতে হলো, সেটিই এবার আনুষ্ঠানিকভাবে।
3NF synthesis algorithm
3NF-SYNTHESIS(R, F):
Fc ← canonical cover of F -- পর্ব ৩৫
S ← { }
for each X → Y in Fc:
S ← S ∪ { X ∪ Y } -- প্রতিটি FD থেকে একটি schema
remove every schema in S that is a subset of another schema in S
if no schema in S contains a candidate key of R:
S ← S ∪ { any one candidate key of R }
return Sএখানে canonical cover হলো F-এর সমতুল্য এমন একটি ছোট সেট, যার কোনো FD বাড়তি নয়, কোনো বামপাশে বাড়তি attribute নেই, আর একই বামপাশের FD-গুলো একসাথে লেখা। একটি বড় উদাহরণে প্রতিটি ধাপ দেখুন — এখানে সব ধাপই কাজে লাগে:
| ধাপ | |
| schema সংখ্যা |
- Key আগে। A আর G কোনো ডানপাশে নেই, তাই দুটিই প্রতিটি key-তে। (AG)⁺ = {A, G, D, E} — B আর C বাকি। B যোগ করলে AB → C দিয়ে সব আসে; C যোগ করলে C → B, তারপর সব। candidate key দুটি: {A, B, G} আর {A, C, G}.
- বাড়তি attribute. AD → E-তে D কি দরকারি? A⁺ = {A, D, E}-এ E আগেই আছে (A → D, তারপর AD → E), তাই D বাড়তি — FD-টি হয় A → E. AB → C-তে A বা B কোনোটিই বাড়তি নয়: B⁺ বা A⁺ কোনোটিতে C নেই।
- একত্র করা আর বাড়তি FD বাদ। একই বামপাশ A, তাই A → D আর A → E মিলে A → DE. প্রতিটি FD বাদ দিয়ে দেখা হলো বাকিগুলো থেকে সেটি আসে কি না — আসে না। তাই Fc = {AB → C, C → B, A → DE}.
- প্রতিটি FD একটি schema. AB → C থেকে R1(A, B, C), C → B থেকে R2(B, C), A → DE থেকে R3(A, D, E). প্রতিটি FD পুরোপুরি একটি schema-র ভেতরে, তাই প্রতিটিকে একটি টেবিলেই যাচাই করা যাবে।
- উপসেট বাদ। R2(B, C)-এর সব attribute R1-এ আছে, আর C → B, R1-এর ভেতরেই যাচাই হবে। তাই R2 বাড়তি — বাদ।
- Key-ধারী schema. R1 বা R3 কোনোটিতে একটি পুরো candidate key নেই — G কোথাও নেই! G-কে কোথাও না রাখলে তথ্য হারাবে, আর key ছাড়া জোড়া lossless নাও হতে পারে। তাই একটি key, ধরুন {A, B, G}, নিয়ে R4(A, B, G) যোগ করা হলো।
- ফল। R1(A, B, C), R3(A, D, E), R4(A, B, G). R1-এর key AB আর AC; C → B-তে C superkey নয় কিন্তু B prime, তাই 3NF ঠিক (BCNF নয় — পর্ব ৩৭)। R3-এর key A. R4-এ কোনো nontrivial FD নেই। আর মূল R? A → D-তে A একটি key-র অংশ, D non-prime — R ছিল শুধু 1NF-এ।
কেন algorithm-টি কাজ করে — তিনটি নিশ্চয়তা
- Dependency-preserving: Fc-এর প্রতিটি FD X → Y কোনো একটি schema-র (X ∪ Y বা তার বড় কোনোটির) ভেতরে। তাই প্রতিটি FD একটি টেবিলের UNIQUE/PRIMARY KEY দিয়েই রক্ষা করা যায়, JOIN ছাড়া। আর Fc ≡ F, তাই পুরো F রক্ষা পায়।
- Lossless: ধরুন Rk একটি candidate key K ধারণ করে। K⁺ = R, আর K⁺ বের করার প্রতিটি ধাপে একটি FD X → Y ব্যবহার হয়, যার X আগেই পাওয়া attribute-এর মধ্যে। Rk-এর একটি সারির সাথে X ∪ Y schema-র টেবিল X দিয়ে জুড়লে X → Y-এর কারণে ঠিক একটি মিল পাওয়া যায় — নতুন কোনো সারি তৈরি হয় না। এভাবে ধাপে ধাপে পুরো R জোড়া লাগে কোনো ভুয়া সারি ছাড়া। (এটি chase নামের আনুষ্ঠানিক প্রমাণের সারাংশ।)
- প্রতিটি schema 3NF: X ∪ Y schema-তে X একটি key (Fc-এ X-এর কোনো বাড়তি attribute নেই)। কোনো B → A যদি এখানে 3NF ভাঙত (B superkey নয়, A non-prime), তাহলে A ∈ Y, আর X → Y থেকে A-কে বাদ দিলেও A অন্য পথে আসত — অর্থাৎ Fc-এ বাড়তি কিছু ছিল, যা canonical cover-এর সংজ্ঞার বিরোধী। Key-ধারী schema-টিও 3NF-এ থাকে।
Data থেকে একটি FD প্রমাণ করা যায় না, কিন্তু খণ্ডন করা যায় — পর্ব ৬-এ key-র বেলায় যা দেখেছিলেন। coaching_records-এ কোন FD ডেটায় ভাঙে, দেখুন:
-- course_code → teacher_id: কোনো কোর্সের একাধিক শিক্ষক?
SELECT course_code, COUNT(DISTINCT teacher_id) AS n FROM coaching_records
GROUP BY course_code HAVING COUNT(DISTINCT teacher_id) > 1;
-- teacher_id → course_code: কোনো শিক্ষকের একাধিক কোর্স?
SELECT teacher_id, COUNT(DISTINCT course_code) AS n FROM coaching_records
GROUP BY teacher_id HAVING COUNT(DISTINCT course_code) > 1;
-- area → student_name: এক এলাকায় একাধিক নাম?
SELECT area, COUNT(DISTINCT student_name) AS n FROM coaching_records
GROUP BY area HAVING COUNT(DISTINCT student_name) > 1;প্রথম ফল “(কোনো সারি নেই)” — course_code → teacher_id এই ডেটায় ভাঙেনি (নিয়মেও সত্য, তাই এটি F2-তে আছে)। দ্বিতীয়টিতে T1 | 2 — Kamal দুটি কোর্স পড়ান, তাই teacher_id → course_code নয়; এজন্যই st/tc ভাঙন lossy হয়েছিল। তৃতীয়টিতে Mirpur | 2 — Rafi আর Tonmoy দুজনেই মিরপুরের, তাই area → student_name নয়। মনে রাখুন, প্রথম কোয়েরি খালি এলেও FD প্রমাণিত হয় না; নিয়মটি আসে ব্যবসা থেকে।
আরও গভীরে: 3NF যাচাই করা কঠিন, কিন্তু 3NF বানানো সহজ — এক মজার বৈপরীত্য। একটি attribute prime কি না, তা জানা NP-complete (Lucchesi ও Osborn, ১৯৭৮), তাই সাধারণভাবে “R কি 3NF-এ?” প্রশ্নটিও NP-complete (Jou ও Fischer, ১৯৮২)। অথচ synthesis algorithm polynomial সময়ে চলে, কারণ এটি কখনো সব key খোঁজে না — শুধু একটি key লাগে, যা একটি superkey থেকে attribute ছেঁটে সহজেই পাওয়া যায়। বাস্তবে attribute-এর সংখ্যা ছোট, তাই এই কঠিনতা টের পাওয়া যায় না।
৮ · db-shop কি 3NF-এ? আর প্রচলিত ভুল ধারণা
এই কোর্সের দোকানের ডেটাবেস (db-shop) পরীক্ষা করা যাক। পাঁচটি টেবিল: customers (customer_id, name, city, phone, joined_on), categories (category_id, name), 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).
| টেবিল | Candidate key | Nontrivial FD (নিয়ম থেকে) | ফল |
|---|---|---|---|
| customers | customer_id; phone (UNIQUE) | customer_id → বাকি সব; phone → বাকি সব | প্রতিটি বামপাশ superkey ⇒ BCNF |
| categories | category_id; name | category_id → name; name → category_id | BCNF |
| products | product_id | product_id → name, category_id, price, stock | BCNF (category-র নাম এখানে নেই) |
| orders | order_id | order_id → customer_id, order_date, status | BCNF (গ্রাহকের নাম-শহর এখানে নেই) |
| order_items | (order_id, product_id) | (order_id, product_id) → quantity, unit_price | BCNF |
প্রতিটি টেবিলে প্রতিটি nontrivial FD-র বামপাশ superkey — তাই সবকটি 3NF-এ, এমনকি BCNF-এ। এটি কাকতালীয় নয়: পর্ব ১০-এর ER থেকে টেবিল বানানোর নিয়ম মানলে প্রতিটি entity নিজের টেবিলে যায়, আর ফল সাধারণত সরাসরি 3NF হয়। Normalization তখন ডিজাইনের যাচাই-এর কাজ করে।
order_items.unit_price কি 3NF ভাঙে?
প্রথম দেখায় মনে হয়, দাম তো products.price-এ আছে, order_items-এ আবার কেন? এটি কি product_id → unit_price ধরনের transitive বা partial dependency? ডেটা দেখুন:
-- কোন অর্ডার-লাইনের দাম আজকের দামের সাথে মেলে না?
SELECT oi.order_id, p.name, p.price AS price_now, oi.unit_price
FROM order_items oi JOIN products p ON p.product_id = oi.product_id
WHERE oi.unit_price <> p.price;
-- product_id → unit_price কি সত্য? একই পণ্যের একাধিক দাম?
SELECT product_id, COUNT(DISTINCT unit_price) AS prices
FROM order_items GROUP BY product_id HAVING COUNT(DISTINCT unit_price) > 1;প্রথম ফলে দুটি সারি: 1007 | Headphone | 1200 | 1150 আর 1009 | Mobile Phone | 15000 | 14500. দ্বিতীয়টিতে 101 | 2 আর 102 | 2 — একই পণ্য ভিন্ন অর্ডারে ভিন্ন দামে বিক্রি হয়েছে (ছাড় বা দাম বদলের কারণে)। তাই product_id → unit_price FD-ই নয়। unit_price হলো “এই অর্ডারে এই পণ্য কত দামে বিক্রি হয়েছিল” — একটি ঐতিহাসিক ঘটনা, যা পুরো key (order_id, product_id)-এর ওপর নির্ভর করে। এটি redundancy নয়; products.price বদলালেও পুরোনো চালান বদলানো উচিত নয়। উল্টো, যদি কেউ order_items-এ “আজকের দাম” কপি রাখত, সেটিই হতো redundancy.
প্রচলিত ভুল ধারণা
- “বেশি টেবিল মানেই ভালো ডিজাইন।” না। students-কে (student_id, name) আর (student_id, area)-তে ভাঙা lossless, কিন্তু কোনো redundancy সরায় না — শুধু JOIN বাড়ায়। Normal form-এর শর্ত পূরণ হলে থামুন।
- “3NF মানে কোনো redundancy নেই।” প্রায়, কিন্তু পুরো নয়। 3NF-এ থেকেও কিছু redundancy থাকে (যেমন R1(A, B, C)-এ C → B), আর দুটি স্বাধীন তালিকা এক টেবিলে মেশালে 4NF লাগে — পর্ব ৩৭।
- “Normal form ডেটা দেখে ঠিক হয়।” না, FD দেখে, আর FD আসে নিয়ম থেকে। ডেটা শুধু একটি FD খণ্ডন করতে পারে।
- “Normalization ধীর করে, তাই বাদ দিন।” প্রথমে normalize করুন, তারপর মেপে দেখুন কোথায় সত্যিই ধীর; প্রয়োজনে সচেতনভাবে denormalization — পর্ব ৩৮-এর বিষয়।
- “কপি থাকলেই normalization ভাঙা।” ঐতিহাসিক মান (unit_price, চালানের ঠিকানা) কপি নয়, আলাদা তথ্য।
৯ · পরীক্ষার ধাঁচের সমস্যা — সম্পূর্ণ সমাধান
বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর লিখিত পরীক্ষায় প্রশ্নটি প্রায় একই রূপে আসে: “R আর F দেওয়া আছে। candidate key বের করুন, সর্বোচ্চ normal form বলুন, প্রয়োজনে 3NF-এ ভাঙুন।” তিনটি ধরন দেখুন।
সমস্যা ১. R(A, B, C, D), F = {AB → C, B → D}.
- Key: A আর B কোনো ডানপাশে নেই, তাই দুটিই প্রতিটি key-তে। (AB)⁺ = {A, B, C, D} = R. candidate key: {A, B}. prime: A, B.
- Normal form: B → D-তে B একটি key-র প্রকৃত অংশ, D non-prime — partial dependency. তাই 2NF নয়; সর্বোচ্চ 1NF.
- 3NF ভাঙন: F-এ কোনো বাড়তি কিছু নেই (AB → C-তে A⁺ = {A}, B⁺ = {B, D} — C নেই), তাই Fc = F. schema: (A, B, C) আর (B, D). প্রথমটিতে key AB আছে, আর কেউ কারও উপসেট নয় — ফল R1(A, B, C), R2(B, D).
সমস্যা ২. R(A, B, C), F = {AB → C, C → A}.
- Key: B কোনো ডানপাশে নেই। B⁺ = {B}. (AB)⁺ = {A, B, C}, (BC)⁺ = {B, C, A}. candidate key: {A, B} আর {B, C}. prime: A, B, C — সব!
- Normal form: সব attribute prime হলে Zaniolo-র শর্ত (খ) সবসময় সত্য, তাই R 3NF-এ — ভাঙার দরকার নেই। তবে C → A-তে C superkey নয়, তাই BCNF নয়। এটিই পর্ব ৩৭-এর বিখ্যাত উদাহরণের কাঠামো।
সমস্যা ৩. R(emp_id, dept_id, dept_name, project_id, hours), F = {emp_id → dept_id, dept_id → dept_name, (emp_id, project_id) → hours}.
- Key: emp_id আর project_id কোনো ডানপাশে নেই; (emp_id, project_id)⁺ = সব। candidate key: {emp_id, project_id}.
- Normal form: emp_id → dept_id partial (dept_id non-prime), তাই সর্বোচ্চ 1NF. (dept_id → dept_name একটি transitive dependency-ও।)
- 3NF ভাঙন: Fc = F. schema: (emp_id, dept_id), (dept_id, dept_name), (emp_id, project_id, hours) — শেষটিতে key আছে। ফল: কর্মী-বিভাগ, বিভাগ, আর কর্মী-প্রকল্পের ঘণ্টা — তিনটি টেবিল।
পরীক্ষায় নম্বর হারানোর সাধারণ কারণ: (১) একটি candidate key পেয়েই থেমে যাওয়া — সমস্যা ২-এ দ্বিতীয় key না পেলে A-কে non-prime ভেবে ভুল সিদ্ধান্ত আসে; (২) canonical cover না করেই schema বানানো, ফলে বাড়তি টেবিল; (৩) key-ধারী schema যোগ করতে ভুলে যাওয়া; (৪) superkey আর candidate key গুলিয়ে partial dependency খোঁজা।
১০ · AI-এর যুগে normalization
AI-কে “এই স্প্রেডশিট normalize করে দাও” বললে সুন্দর কয়েকটি CREATE TABLE আসে। কিন্তু AI আপনার ব্যবসার নিয়ম জানে না, তাই FD অনুমান করে — আর অনুমানই ভুলের উৎস। প্রায়ই দেখা যায়: কলামের নাম দেখে FD ধরে নেওয়া (“city → zip” সব দেশে সত্য ভাবা), ঐতিহাসিক দাম বা ঠিকানাকে redundancy ভেবে মুছে ফেলা, প্রয়োজনের চেয়ে বেশি টেবিল, অস্তিত্বহীন কলামের নাম দিয়ে কোয়েরি, আর নতুন টেবিলে foreign key বা UNIQUE ভুলে যাওয়া। ভাঙার পর JOIN কোয়েরি চলে, কিন্তু সারির সংখ্যা ভুল — duplicate বা ভুয়া সারি চোখে পড়ে না।
AI-কে কাজ দেওয়ার আগে FD আপনি নিজে লিখে দিন, আর প্রতিটি সিদ্ধান্তের যুক্তি চেয়ে নিন:
AI-এর দেওয়া normalization হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Normalization কেন করা হয়? — Redundancy আর তা থেকে আসা update, insertion ও deletion anomaly দূর করতে; প্রতিটি তথ্য এক জায়গায় রাখতে।
- 2NF আর 3NF-এর পার্থক্য? — 2NF partial dependency (key-র অংশ → non-prime) সরায়; 3NF transitive dependency (non-key → non-prime) সরায়।
- একক-attribute key থাকলে টেবিল কি 2NF-এ? — হ্যাঁ, যদি 1NF-এ থাকে আর সব candidate key একক হয়; partial dependency-র জন্য composite key লাগে।
- 3NF-এর Zaniolo সংজ্ঞা? — প্রতিটি nontrivial X → A-তে X superkey, নয়তো A prime.
- 3NF synthesis-এ key-ধারী schema কেন যোগ করা হয়? — ভাঙনটিকে lossless করতে; FD-ভিত্তিক schema-গুলো একা জুড়লে ভুয়া সারি আসতে পারে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোয়েরি-লেখক থেকে ডেটাবেস-ডিজাইনার বানাবে।
প্র ০১১৯৭০-এর দশকে ডিস্ক ছিল ভীষণ দামি, তাই অনেকে ভাবেন normalization-এর আসল উদ্দেশ্য ছিল জায়গা বাঁচানো। আজ স্টোরেজ সস্তা — তাহলে কি normalization অপ্রয়োজনীয়?
ধারণাটি জনপ্রিয় হলেও Codd-এর প্রবন্ধগুলো পড়লে বোঝা যায়, তাঁর মূল চিন্তা ছিল সঠিকতা, জায়গা নয়। ১৯৭১ সালের “Further Normalization” প্রবন্ধে তিনি insertion, update আর deletion-এর সময় অবাঞ্ছিত নির্ভরতার কথা বলেন — অর্থাৎ ডেটা বদলানোর সময় কী ভুল হতে পারে। জায়গা বাঁচা একটি পার্শ্ব-সুবিধা মাত্র।
এই পর্বের update anomaly এডিটরটি আবার ভাবুন। Kamal-এর ফোন নম্বর চারবার লেখা থাকায় জায়গা নষ্ট হয়েছে মাত্র কয়েকটি বাইট। কিন্তু একটি সারি বদলানোর পর ডেটাবেস দুটি পরস্পরবিরোধী সত্য বহন করছে, আর কোনটি ঠিক তা আর কেউ জানে না। বড় সিস্টেমে এমন অমিল ধীরে ধীরে জমে: একটি ব্যাংকের বিভিন্ন টেবিলে একই গ্রাহকের তিন রকম ঠিকানা, একটি ই-কমার্স সাইটে একই পণ্যের দুই রকম নাম। শেষে কাউকে বসে “কোনটি সঠিক” নির্ধারণের জন্য আলাদা data cleaning প্রকল্প চালাতে হয় — যা ডিস্কের দামের চেয়ে বহুগুণ ব্যয়বহুল।
আরেকটি দিক হলো constraint. Normalized ডিজাইনে প্রতিটি FD একটি টেবিলের key হয়ে যায়, তাই DBMS নিজেই নিয়ম রক্ষা করে — teachers-এর primary key নিশ্চিত করে এক শিক্ষকের একটিই ফোন। Unnormalized টেবিলে একই নিয়ম রক্ষা করতে trigger বা অ্যাপের কোড লাগে, যা ভুলে যাওয়া সহজ।
তবে স্টোরেজ সস্তা হওয়ায় একটি জিনিস সত্যিই বদলেছে: পড়ার গতির জন্য ইচ্ছাকৃত কপি রাখা এখন বেশি যুক্তিসংগত। Data warehouse-এর star schema (পর্ব ৫৫) বা পড়া-ভারী অ্যাপের cache-টেবিল তার উদাহরণ। কিন্তু সেটি সচেতন সিদ্ধান্ত, যেখানে কপিগুলো কে মেলাবে তা আগেই ঠিক করা থাকে।
মূল উপলব্ধি: Normalization-এর উদ্দেশ্য সবসময়ই ছিল “একটি তথ্যের একটি সত্য সংস্করণ”। স্টোরেজের দাম কমায় সেই লক্ষ্য বদলায়নি, শুধু denormalization-এর হিসাব একটু সস্তা হয়েছে।
প্র ০২PostgreSQL-এ array আর jsonb কলাম, MongoDB-তে ডকুমেন্টের ভেতরে তালিকা — আধুনিক সিস্টেম 1NF ভাঙাকে উৎসাহ দিচ্ছে নাকি 1NF-এর ধারণাটিই পুরোনো?
প্রথমে ইতিহাস। Codd ১৯৭০ সালে “nonsimple domain” — অর্থাৎ মানের ভেতরে আরেকটি relation — দূর করতে বলেছিলেন মূলত একটি সরল, গাণিতিকভাবে পরিষ্কার query ভাষার জন্য। কিন্তু পরে C. J. Date-সহ কয়েকজন তাত্ত্বিক যুক্তি দেন, “atomic” শব্দটির কোনো নিখুঁত সংজ্ঞা নেই: একটি লেখা আসলে অক্ষরের তালিকা, একটি তারিখ তিনটি সংখ্যা। তাঁদের মতে 1NF মানে শুধু “প্রতিটি ঘরে domain-এর ঠিক একটি মান”, আর domain নিজেই তালিকা বা relation হতে পারে। SQL standard-ও এই দিকে গেছে: SQL:1999-এ ARRAY, পরে MULTISET, আর SQL:2016-এ JSON-এর ফাংশন।
তাহলে ভুলটা কোথায়? সমস্যা তালিকা রাখায় নয়, তালিকার ভেতরের মান ধরে কাজ করায়। jsonb-এর ভেতরের একটি customer_id-তে foreign key বসানো যায় না; array-র একটি উপাদানে UNIQUE বসানো যায় না (exclusion constraint দিয়ে কিছুটা সম্ভব, কিন্তু জটিল)। ভেতরের মান বদলাতে পুরো মান নতুন করে লিখতে হয়, আর অনেকে একসাথে একই ডকুমেন্টের তালিকা বদলালে সংঘাত বাড়ে। অংশ ১-এর LIKE '%Math%' ভুলটি এই সমস্যার সবচেয়ে সরল রূপ।
MongoDB-র মতো document ডেটাবেস (পর্ব ৫৭) ইচ্ছাকৃতভাবে embed করাকে উৎসাহ দেয়, কারণ একটি ডকুমেন্ট একবারে পড়া দ্রুত, আর অনেক অ্যাপ ঠিক সেভাবেই ডেটা পড়ে। কিন্তু MongoDB-র নিজের ডিজাইন নির্দেশিকাও বলে, যে তালিকা অসীমভাবে বাড়ে বা যার উপাদান অন্য জায়গা থেকে আলাদা করে খোঁজা হয়, সেটি আলাদা collection-এ রেখে reference দিন — যা আসলে normalization-এর যুক্তিই।
একটি কাজের নিয়ম: তালিকাটি কি সবসময় মালিকের সাথে একসাথে পড়া-লেখা হয়, ছোট থাকে, আর ভেতরের কোনো মানে নিয়ম (UNIQUE, foreign key) লাগে না? তাহলে array/JSON চমৎকার। নইলে আলাদা টেবিল।
মূল উপলব্ধি: 1NF পুরোনো হয়নি; “atomic” শব্দটি ব্যবহারের প্রেক্ষিতে বোঝা দরকার। যে মানের ভেতরে আপনি প্রশ্ন করবেন বা নিয়ম চাপাবেন, সেটি একটি ঘরে লুকিয়ে রাখবেন না।
প্র ০৩BCNF 3NF-এর চেয়ে কড়া আর বেশি redundancy সরায়। তবু বাস্তব ডিজাইনে আর পাঠ্যবইয়ে 3NF-কেই কেন সাধারণ লক্ষ্য বলা হয়?
উত্তরটি লুকিয়ে আছে Zaniolo-র সংজ্ঞার শর্ত (খ)-তে: “অথবা A prime”. এই ছাড় না থাকলে, অর্থাৎ BCNF চাইলে, কিছু relation-কে এমনভাবে ভাঙতে হয় যে একটি FD আর কোনো একক টেবিলের ভেতরে থাকে না। সমস্যা ২-এর কাঠামো R(A, B, C), F = {AB → C, C → A} ভাবুন। BCNF-এর জন্য C → A ভাঙতে হবে: (C, A) আর (B, C). ভাঙনটি lossless, কিন্তু AB → C এখন কোনো টেবিলে পুরোপুরি নেই। এই নিয়ম রক্ষা করতে প্রতিটি INSERT-এর সময় দুটি টেবিল JOIN করে যাচাই করতে হবে — trigger বা অ্যাপের কোডে, যা ধীর আর ভঙ্গুর।
১৯৭৬ সালে Bernstein-এর synthesis algorithm আর ১৯৭৯ সালের Biskup–Dayal–Bernstein-এর ফলাফল প্রমাণ করে, 3NF-এ সবসময় এমন ভাঙন পাওয়া যায় যা একসাথে lossless আর dependency-preserving. BCNF-এ এই দুটি একসাথে সবসময় পাওয়া যায় না — এটি একটি প্রমাণিত সীমাবদ্ধতা, শুধু অ্যালগরিদমের দুর্বলতা নয়। ডিজাইনারকে তখন বেছে নিতে হয়: সামান্য redundancy (3NF), নাকি একটি নিয়ম রক্ষার ঝামেলা (BCNF).
বাস্তবে অবশ্য পার্থক্যটি বিরল। 3NF-এ থেকেও BCNF ভাঙে কেবল তখন, যখন একাধিক overlapping candidate key আছে — যেমন সমস্যা ২-এ {A, B} আর {B, C}, যাদের মধ্যে B সাধারণ। বেশিরভাগ ব্যবসায়িক টেবিলে একটি key বা আলাদা আলাদা key থাকে, তাই 3NF পেলেই সাধারণত BCNF-ও মেলে — যেমন db-shop-এর সব টেবিল।
আরেকটি ব্যবহারিক কারণ: 3NF-এর পরীক্ষা FD ধরে সরল, আর বেশি ভাঙলে JOIN বাড়ে। তাই অনেক প্রতিষ্ঠানের ডিজাইন-নির্দেশিকা বলে “কমপক্ষে 3NF, BCNF যেখানে খরচ ছাড়া পাওয়া যায়”।
মূল উপলব্ধি: 3NF হলো এমন সর্বোচ্চ স্তর, যেখানে তথ্য না হারিয়ে আর সব নিয়ম টেবিলের key দিয়ে রক্ষা করে পৌঁছানোর নিশ্চয়তা আছে। পর্ব ৩৭-এ এই trade-off-টির পুরো প্রমাণ দেখবেন।
প্র ০৪চালানে গ্রাহকের ঠিকানা, অর্ডার-লাইনে বিক্রির দাম, বেতন-স্লিপে সেই মাসের পদবি — এগুলো কপি করে রাখা কি normalization-এর লঙ্ঘন? কখন কপি করাই সঠিক?
মূল প্রশ্ন হলো, তথ্যটি কোন “মুহূর্তের” সত্য। products.price বলে “আজ এই পণ্যের দাম কত”, আর order_items.unit_price বলে “সেপ্টেম্বরের ওই অর্ডারে পণ্যটি কত দামে বিক্রি হয়েছিল”। দুটি আলাদা তথ্য, আলাদা FD: প্রথমটি product_id → price, দ্বিতীয়টি (order_id, product_id) → unit_price. এই পর্বের db-shop এডিটর দেখিয়েছে, Headphone এক অর্ডারে 1150, তালিকায় 1200 — দুটিই সত্য, দুটিই দরকারি।
যদি order_items-এ দাম না রেখে প্রতিবার products থেকে পড়তেন, তাহলে দাম বদলের সাথে সাথে পুরোনো সব চালানের মোট অঙ্ক বদলে যেত। হিসাবরক্ষণ, ভ্যাট রিটার্ন আর অডিটের জন্য এটি মারাত্মক — একবার ইস্যু করা চালান বদলানো যায় না। একই যুক্তি চালানের ঠিকানায়: গ্রাহক পরে বাসা বদলালে পুরোনো ডেলিভারির ঠিকানা বদলে যাওয়া উচিত নয়।
কপি তখনই redundancy, যখন দুটি মান সবসময় সমান থাকার কথা। যেমন order_items-এ product_name রাখা: পণ্যের নামের বানান ঠিক করলে সেটি সব জায়গায় বদলানোই উদ্দেশ্য — সেখানে কপি anomaly আনে। পরীক্ষা হলো: “মূল মান বদলালে কপিটিও কি বদলানো উচিত?” হ্যাঁ হলে redundancy, না হলে আলাদা ঐতিহাসিক তথ্য।
এই চিন্তা আরও বড় ধারণায় যায়। কিছু সিস্টেম মূল টেবিলেই ইতিহাস রাখে — প্রতিটি দামের সাথে valid_from আর valid_to কলাম। SQL:2011 standard-এ temporal table এসেছে, আর SQL Server, MariaDB-র মতো সিস্টেমে system-versioned table আছে। তখন অর্ডারে শুধু product_id আর সময় থাকলেই সেই মুহূর্তের দাম বের করা যায়, যদিও কোয়েরি জটিল হয়। বেশিরভাগ দোকান-সফটওয়্যার তাই সরল পথটিই নেয়: বিক্রির মুহূর্তে দাম কপি।
মূল উপলব্ধি: একই রকম দেখতে দুটি কলাম মানেই redundancy নয়। FD লিখুন: যদি মানটি ভিন্ন key-র ওপর নির্ভর করে, সেটি আলাদা তথ্য, আর 3NF তাকে পুরোপুরি অনুমোদন করে।
প্র ০৫একই R আর F থেকে দুজন শিক্ষার্থী synthesis algorithm চালিয়ে ভিন্ন 3NF ডিজাইন পেলেন। দুজনেই কি ঠিক হতে পারেন? আর বাস্তবে ডিজাইনাররা কি সত্যিই এই অ্যালগরিদম চালান?
হ্যাঁ, দুজনেই ঠিক হতে পারেন। Canonical cover অনন্য নয়: বাড়তি attribute বা FD কোন ক্রমে পরীক্ষা করছেন, তার ওপর ফল বদলাতে পারে। যেমন F = {A → BC, B → AC, C → AB} হলে একটি canonical cover {A → B, B → C, C → A}, আরেকটি {A → BC, B → A, C → A}. প্রথমটি থেকে তিনটি schema আসে — (A, B), (B, C), (C, A); দ্বিতীয়টি থেকে উপসেট বাদ দেওয়ার পর একটিই — (A, B, C). আবার শেষ ধাপে একাধিক candidate key থাকলে যেকোনোটি যোগ করা যায় — অংশ ৭-এর উদাহরণে {A, B, G}-এর বদলে {A, C, G} নিলেও সঠিক উত্তর। পরীক্ষকেরা তাই সাধারণত প্রক্রিয়া আর শেষ ফলের গুণ (3NF, lossless, dependency-preserving) দেখেন, হুবহু একই টেবিল নয়।
১৯৭০-এর দশকের শেষে এই “bottom-up” পদ্ধতি নিয়ে বেশ আশা ছিল: সব attribute আর FD লিখে দিন, অ্যালগরিদম ডিজাইন বানিয়ে দেবে। বাস্তবে দুটি বাধা আছে। প্রথমত, শত শত attribute-এর সব FD সঠিকভাবে লেখা নিজেই কঠিন, আর একটি FD ভুলে গেলে ডিজাইন ভুল হয়। দ্বিতীয়ত, অ্যালগরিদম টেবিলের অর্থবোধক নাম বা ব্যবসার ধারণা জানে না।
তাই প্রচলিত পথটি হলো top-down: ER মডেল (পর্ব ৭–১০) দিয়ে entity আর relationship ভেবে টেবিল বানানো, তারপর normalization দিয়ে যাচাই — “এই টেবিলে কি কোনো non-key থেকে non-key FD আছে?” ER ভালোভাবে করলে ফল সাধারণত 3NF-এ থাকে, যেমন db-shop. Normalization ধরতে সাহায্য করে সেই জায়গাগুলো, যেখানে একটি entity আরেকটির ভেতরে লুকিয়ে ছিল — coaching_records-এ শিক্ষক যেমন কোর্সের ভেতরে লুকিয়ে ছিলেন।
তবু algorithm জানা জরুরি। এটি প্রমাণ করে যে 3NF সবসময় অর্জনযোগ্য, আর পুরোনো স্প্রেডশিট বা legacy টেবিল ভাঙার সময় একটি নির্ভরযোগ্য, যান্ত্রিক পথ দেয়।
মূল উপলব্ধি: 3NF ডিজাইন একাধিক হতে পারে; সঠিকতার মাপকাঠি হলো তিনটি গুণ। আর বাস্তবে ER দিয়ে ডিজাইন, normalization দিয়ে যাচাই — দুটি মিলেই ভালো schema.
অনুশীলন
খাতা-কলম নিন। দুটি হাতে হিসাব, একটি কোয়েরি লেখা, একটি ডিজাইন।
-
পুরো সমাধান করুন (হাতে): R(P, Q, R, S, T), F = {P → Q, Q → R, PS → T}. candidate key, সর্বোচ্চ normal form আর 3NF ভাঙন বের করুন।
Key: P আর S কোনো ডানপাশে নেই। (PS)⁺: P → Q, Q → R, PS → T ⇒ {P, Q, R, S, T}. candidate key {P, S}; prime P, S.
Normal form: P → Q-তে P key-র প্রকৃত অংশ, Q non-prime — partial. তাই সর্বোচ্চ 1NF. (Q → R transitive-ও।)
3NF: PS → T-তে P⁺ = {P, Q, R}, S⁺ = {S} — কোনোটিতে T নেই, তাই বাড়তি কিছু নেই; Fc = F. schema: (P, Q), (Q, R), (P, S, T) — শেষটিতে key আছে। ফল: তিনটি টেবিল।
-
কোয়েরি লিখুন: coaching_records (student_id, student_name, area, course_code, course_title, teacher_id, teacher_name, teacher_phone, fee, enrolled_on) টেবিলে course_code → fee FD ডেটায় ভাঙে কি না, তা পরীক্ষার কোয়েরি লিখুন (অংশ ৭-এর শেষ এডিটরে চালাতে পারবেন)। ফল খালি এলে কী সিদ্ধান্ত নেবেন?
SELECT course_code, COUNT(DISTINCT fee) AS n FROM coaching_records GROUP BY course_code HAVING COUNT(DISTINCT fee) > 1;ফল “(কোনো সারি নেই)” — প্রতিটি কোর্সের সব সারিতে একই ফি। কিন্তু এতে FD প্রমাণ হয় না, শুধু খণ্ডিত হয়নি। কোচিং সেন্টার যদি কাউকে ছাড় দেয় (একই কোর্সে কারও 2500), তাহলে fee আসলে (student_id, course_code)-এর ওপর নির্ভর করবে আর enrollments টেবিলে যাবে। সিদ্ধান্ত নিতে হবে নিয়ম জেনে: “ফি কি কোর্সের, নাকি প্রতিটি ভর্তির?”
-
ফল আগে বলুন (trace): একটি টেবিল (student, course, teacher)-এ তিনটি সারি: (Rafi, Math, Kamal), (Mim, Higher Math, Kamal), (Mim, Physics, Nasrin). একে (student, teacher) আর (teacher, course)-এ ভেঙে teacher দিয়ে আবার JOIN করা হলো। ফলে কয়টি সারি আসবে, কোনগুলো ভুয়া, আর কেন?
(student, teacher): (Rafi, Kamal), (Mim, Kamal), (Mim, Nasrin). (teacher, course): (Kamal, Math), (Kamal, Higher Math), (Nasrin, Physics).
JOIN: Rafi–Kamal দুটি কোর্সের সাথে মেলে → (Rafi, Math), (Rafi, Higher Math); Mim–Kamal → (Mim, Math), (Mim, Higher Math); Mim–Nasrin → (Mim, Physics). মোট ৫টি সারি, যার দুটি ভুয়া: (Rafi, Higher Math) আর (Mim, Math). কারণ সাধারণ attribute teacher কোনো টেবিলের key নয় — Kamal দুটি কোর্স পড়ান, আর তাঁর কাছে দুজন শিক্ষার্থী। lossless শর্ত (teacher → student বা teacher → course) কোনোটিই সত্য নয়।
-
ডিজাইন করুন: একটি মুদি দোকানের ক্যাশ মেমোর খাতা: sales(memo_no, memo_date, customer_phone, customer_name, product_code, product_name, qty, price_at_sale). নিয়ম: প্রতিটি মেমোর একটি তারিখ আর একজন গ্রাহক; একটি ফোন নম্বর একজন গ্রাহকের; প্রতিটি পণ্য-কোডের একটি নাম; একটি মেমোতে একটি পণ্য একবারই লেখা হয়। FD লিখুন, key আর normal form বের করুন, 3NF-এ ভাঙুন।
- FD: memo_no → memo_date, customer_phone; customer_phone → customer_name; product_code → product_name; (memo_no, product_code) → qty, price_at_sale.
- Key: (memo_no, product_code). memo_no → memo_date partial, তাই সর্বোচ্চ 1NF.
- 3NF: memos(memo_no, memo_date, customer_phone), customers(customer_phone, customer_name), products(product_code, product_name), memo_lines(memo_no, product_code, qty, price_at_sale) — শেষটিতে key আছে।
- ফাঁদ: price_at_sale-কে products-এ সরাবেন না — এটি বিক্রির মুহূর্তের দাম, পুরো key-র ওপর নির্ভর করে, ঠিক db-shop-এর unit_price-এর মতো।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রম3NF synthesis algorithm-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- F-এর canonical cover Fc বের করা
- Fc-এর প্রতিটি X → Y থেকে X ∪ Y নিয়ে একটি schema বানানো
- অন্য schema-র উপসেট এমন schema বাদ দেওয়া
- কোনো schema-তে candidate key না থাকলে একটি key দিয়ে নতুন schema যোগ করা
R(A, B, C, D)-এর একমাত্র candidate key {A, B}, আর F-এ একটি FD হলো B → D. R কোন normal form ভাঙে?
- 1NF
- 2NF
- শুধু 3NF, 2NF ঠিক আছে
- কোনোটিই নয়
Zaniolo-র সংজ্ঞা অনুযায়ী একটি nontrivial FD X → A কখন 3NF-এ অনুমোদিত?
- শুধু যখন X একটি superkey
- যখন A non-prime
- যখন X superkey, অথবা A prime attribute
- যখন X একটি candidate key আর A non-prime
একটি টেবিলে প্রতিটি সারি একটি (student, course) ভর্তি, আর শিক্ষকের নাম-ফোনও প্রতিটি সারিতে লেখা। কোনো কোর্সের একমাত্র শিক্ষার্থীর সারি মুছলে সেই কোর্সের শিক্ষকের তথ্যও হারিয়ে যায়। এটি কোন সমস্যা?
- Update anomaly
- Insertion anomaly
- Spurious tuple
- Deletion anomaly
3NF synthesis algorithm-এ, কোনো schema-তে candidate key না থাকলে একটি key-ধারী schema কেন যোগ করা হয়?
- ভাঙনটি lossless করতে
- ভাঙনটি dependency-preserving করতে
- প্রতিটি schema-কে BCNF-এ আনতে
- টেবিলের সংখ্যা কমাতে
order_items(order_id, product_id, quantity, unit_price) টেবিলে unit_price হলো বিক্রির মুহূর্তের দাম, আর একই পণ্য ভিন্ন অর্ডারে ভিন্ন দামে বিক্রি হতে পারে। key (order_id, product_id). unit_price কি 3NF ভাঙে?
- হ্যাঁ, কারণ product_id → unit_price একটি partial dependency
- না, কারণ unit_price পুরো key-র ওপর নির্ভর করে
- হ্যাঁ, কারণ দাম products টেবিলে আগেই আছে
- হ্যাঁ, কারণ composite key-ওয়ালা টেবিল কখনো 3NF-এ থাকে না
R(A, B, C)-এ F = {AB → C, C → A}. R-এর সর্বোচ্চ normal form কোনটি?
- 1NF
- 2NF
- 3NF
- BCNF
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Redundancy থেকে update, insertion আর deletion anomaly; normalization প্রতিটি তথ্য এক জায়গায় রাখে।
- 1NF = atomic মান, তালিকা বা repeating group নেই; “atomic” ঠিক হয় ব্যবহার দেখে।
- 2NF = কোনো non-prime attribute key-র অংশের ওপর নির্ভর করে না — শুধু composite key-তে প্রশ্ন ওঠে।
- 3NF = প্রতিটি X → A-তে X superkey অথবা A prime (Zaniolo) — Codd-এর “transitive dependency নেই”-র সমতুল্য।
- Synthesis: canonical cover → প্রতি FD-তে schema → উপসেট বাদ → দরকারে key-ধারী schema; ফল lossless আর dependency-preserving.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৭ · BCNF, 4NF, 5NF আর decomposition-এর গুণপরবর্তী পর্ব3NF-এর পরের normal form আর lossless-join পরীক্ষার প্রমাণ।
- পর্ব ৩৫ · Functional dependency — closure, key আর canonical coverভিত্তিএই পর্বের প্রতিটি ধাপের গাণিতিক যন্ত্রপাতি।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Python · SQLite ডেটাবেসহাতে-কলমেনিজের Python অ্যাপে normalized টেবিল বানিয়ে ব্যবহার করুন।
- DSA · Hash tableপাশের ট্র্যাকFD খণ্ডনের GROUP BY অনেক DBMS-এ যে কাঠামো (hash aggregation) দিয়ে চলে।