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

ডিজাইন কেস স্টাডি — বিশ্ববিদ্যালয় আর ই-কমার্স ডেটাবেস

Database Design Case Studies — A University and an E-commerce Shop, from Requirements to a Reviewed Schema

Taught in Bangla, this lesson walks through two complete database design case studies, a university and an online shop, from gathering requirements to ER diagrams and reviewed schemas. It explains the hard decisions and rejected alternatives, CHECK constraints, extending the design for reviews, addresses and payment methods, and validating the design with sample data in live SQL.

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

  • পেশাদারদের মতো requirements সংগ্রহ — কী প্রশ্ন করবেন, বিশেষ্য-ক্রিয়া কৌশল, ব্যবসার নিয়মের তালিকা
  • কোর্সের বিশ্ববিদ্যালয় ডেটাবেস (db-uni)-এর প্রতিটি টেবিল, কলাম আর constraint কেন আছে, আর কোন বিকল্প কেন বাদ
  • অনলাইন দোকানের ডেটাবেস (db-shop)-এর সিদ্ধান্ত: দাম কপি করা, status আর stock-এর CHECK
  • নতুন চাহিদায় ডিজাইন বাড়ানো: রিভিউ, ঠিকানা, bKash/Nagad/কার্ড/ক্যাশ-অন-ডেলিভারি পেমেন্ট, ফেরত আর কুপন
  • নমুনা ডেটায় কোয়েরি চালিয়ে ডিজাইন যাচাই, রিভিউ চেকলিস্ট আর ডিজাইনের চারটি পরিচিত “দুর্গন্ধ”

১ · ডিজাইন শুরু হয় প্রশ্ন দিয়ে — requirements সংগ্রহ

মডিউল ২-এর আগের চারটি পর্বে আপনি যন্ত্রপাতি শিখেছেন: পর্ব ৭-এ entity, attribute আর relationship, পর্ব ৮-এ cardinality আর weak entity, পর্ব ৯-এ specialization আর aggregation, আর পর্ব ১০-এ ER থেকে টেবিলে রূপান্তরের নিয়ম। এই পর্বে সেই যন্ত্রগুলো দিয়ে দুটি পূর্ণ ডেটাবেস শুরু থেকে শেষ পর্যন্ত বানাব — ঠিক যেভাবে একজন পেশাদার বানান। আর সেই দুটি ডেটাবেসই এই কোর্সের সব লাইভ এডিটরে চলে: db-uni আর db-shop. অর্থাৎ এখানে আপনি জানবেন, পুরো কোর্সে যে টেবিলগুলোতে কোয়েরি চালাবেন, সেগুলো কেন এমন।

একজন ভালো দর্জি কাপড় কাটার আগে মাপ নেন আর প্রশ্ন করেন: “ঢিলা না আঁটসাঁট? পকেট লাগবে?” কাপড় কাটার পর পকেটের কথা মনে পড়লে জোড়াতালি ছাড়া উপায় নেই। ডেটাবেসও তাই: লাখো সারি ঢোকানোর পর “গ্রাহকের তো দুটি ঠিকানা থাকতে পারে!” আবিষ্কার করলে schema, পুরোনো ডেটা আর অ্যাপের কোড — তিনটিই বদলাতে হয়।

কার সাথে কথা বলবেন, কী জিজ্ঞেস করবেন

Requirements সংগ্রহ মানে যাঁরা সিস্টেমটি ব্যবহার করবেন — বিশ্ববিদ্যালয়ের রেজিস্ট্রার অফিস, বিভাগের প্রধান, শিক্ষার্থী; বা দোকানের মালিক, গুদামের কর্মী, হিসাবরক্ষক — তাঁদের কাছ থেকে জেনে নেওয়া কোন তথ্য রাখতে হবে, আর কোন প্রশ্নের উত্তর চাই। পেশাদার ডিজাইনাররা সাধারণত এই প্রশ্নগুলো করেন:

কী কী জিনিস?

“আপনার কাজে কোন কোন জিনিস বা মানুষের হিসাব রাখেন?” — উত্তরের বিশেষ্যগুলো entity-র প্রার্থী।

entity · attribute

কোন প্রশ্নের উত্তর চাই?

“মাস শেষে কোন রিপোর্ট লাগে? কোন তালিকা প্রায়ই খোঁজেন?” — রিপোর্টই বলে দেয় কোন তথ্য না রাখলে চলবে না।

report · query

কখনো কি…?

“একজন শিক্ষার্থী কি কখনো দুই বিভাগে? একটি অর্ডার কি কখনো দুই ভাগে পরিশোধ হয়?” — ব্যতিক্রমই cardinality আর key ঠিক করে।

cardinality · key

ইতিহাস লাগবে?

“দাম বদলালে পুরোনো দাম কি জানা দরকার? কিছু কি কখনো মোছা যাবে না?” — উত্তর ঠিক করে কী কপি করবেন আর ON DELETE কী হবে।

history · delete

সাথে আরও দুটি প্রশ্ন: পরিমাণ (কয়টি সারি — index বাছাইয়ে লাগে) আর কে কী দেখবেন (পর্ব ৫১-এর privilege). “কখনো কি” প্রশ্নগুলো সবচেয়ে দামি: মানুষ স্বাভাবিক ঘটনার কথা বলেন, ব্যতিক্রম ভুলে যান — আর ব্যতিক্রমই ডিজাইন ভাঙে।

বিশেষ্য-ক্রিয়া কৌশল

সাক্ষাৎকারের পর চাহিদাগুলো সহজ বাক্যে লিখে ফেলুন। তারপর একটি পুরোনো কিন্তু কার্যকর কৌশল: বাক্যের বিশেষ্য (noun) সাধারণত entity বা attribute, ক্রিয়া (verb) সাধারণত relationship, আর “একটি”, “একজন”, “কয়েকটি” ধরনের পরিমাণবাচক শব্দ cardinality-র ইঙ্গিত দেয়। নিচে বিশ্ববিদ্যালয়ের একটি ছোট চাহিদা-বর্ণনায় কৌশলটি ধাপে ধাপে প্রয়োগ করুন:

রেজিস্ট্রার অফিসের বর্ণনালাল = এই ধাপে যা খুঁজছি

            
যা পাওয়া গেল
Entity
Attribute
Relationship
নিয়ম
  1. বর্ণনাটি পড়ুন। রেজিস্ট্রার অফিসের সাথে কথা বলে লেখা এগারো লাইন। এখনো কোনো টেবিল নেই; একই লেখা চার রকম চশমায় পড়ব।
  2. বিশেষ্য → entity-র প্রার্থী। বারবার আসছে বিভাগ, শিক্ষক, কোর্স আর শিক্ষার্থী। প্রতিটির অনেক সদস্য আর নিজস্ব তথ্য আছে — তাই এরা entity, পরে টেবিল হবে।
  3. যে বিশেষ্য অন্য কিছুর গুণ বর্ণনা করে → attribute. ভবন বিভাগের গুণ, credit কোর্সের গুণ। কিন্তু term আর grade কার? এরা শিক্ষার্থী বা কোর্স কারও একার নয় — “কোন শিক্ষার্থী কোন কোর্স কোন term-এ নিলেন”, সেই ঘটনার গুণ। অর্থাৎ এরা relationship-এর attribute.
  4. ক্রিয়া → relationship. শিক্ষক বিভাগে চাকরি করেন, শিক্ষক আরেক শিক্ষককে mentor হিসেবে দেখেন (একই entity-র ভেতরে সম্পর্ক), বিভাগ কোর্স চালায়, শিক্ষক কোর্স পড়ান, শিক্ষার্থী বিভাগে ভর্তি হন, আর শিক্ষার্থী কোর্স নেন।
  5. পরিমাণবাচক শব্দ → cardinality. “প্রত্যেক শিক্ষক একটি বিভাগে” আর “কয়েকটি বিভাগ… প্রতিটি বিভাগের” মিলে বিভাগ–শিক্ষক 1:N. একই যুক্তিতে mentor, চালায়, পড়ান আর ভর্তি সবই 1:N. কিন্তু “প্রতি term-এ কয়েকটি কোর্স নেন” আর একটি কোর্সে অনেক শিক্ষার্থী — তাই শিক্ষার্থী–কোর্স M:N.
  6. শেষ বাক্যটি একটি নিয়ম। “F পেলে পরের term-এ আবার নেওয়া যায়” — কোনো নতুন entity নয়, কিন্তু ডিজাইনের সবচেয়ে গুরুত্বপূর্ণ বাক্য: একই শিক্ষার্থী আর একই কোর্সের জোড়া একাধিকবার আসতে পারে। অংশ ৩-এ দেখবেন, এই একটি বাক্যই enrollments-এর primary key ঠিক করে দেয়।

কৌশলটি শুরু, শেষ নয়। “ভবন” আজ attribute, কিন্তু ভবনের কক্ষ আর ধারণক্ষমতারও হিসাব রাখতে হলে সেটি নিজেই entity. নিয়মটি সহজ: যে জিনিসের নিজের তথ্য রাখতে হয়, সে entity.

ব্যবসার নিয়মের তালিকা

সাক্ষাৎকারের সবচেয়ে মূল্যবান ফল হলো নম্বর দেওয়া নিয়মের একটি তালিকা। প্রতিটি নিয়ম পরে কোনো একটি key, foreign key, NOT NULL বা CHECK হয় — অথবা স্পষ্ট লেখা থাকে “এটি ডেটাবেস নিজে পাহারা দিতে পারবে না”। বিশ্ববিদ্যালয়ের জন্য তালিকাটি এমন:

নংনিয়মডিজাইনে যা হলো
U1দুটি বিভাগের নাম এক হবে নাdepartments.name UNIQUE NOT NULL
U2শিক্ষক একটি বিভাগের; mentor থাকতেও পারেন, না-ও পারেনinstructors.dept_id FK; mentor_id নিজের টেবিলের দিকে FK, NULL চলে
U3বেতন ধনাত্মকCHECK (salary > 0)
U4নতুন শিক্ষার্থী বিভাগ পরে বাছতে পারেন; batch সবসময় জানাstudents.dept_id NULL চলে; batch NOT NULL
U5CGPA ০ থেকে ৪-এর মধ্যেCHECK (cgpa BETWEEN 0 AND 4)
U6কোর্স কোড (CSE301) কখনো বদলায় না, ট্রান্সক্রিপ্টে ছাপা হয়natural key course_id TEXT PRIMARY KEY
U7এক term-এ একই কোর্সে একবারই ভর্তি; F পেলে পরের term-এ আবারPRIMARY KEY (student_id, course_id, term)
U8term শেষ না হওয়া পর্যন্ত grade থাকে নাgrade NULL চলে
U9একজন শিক্ষার্থী এক term-এ ২১ credit-এর বেশি নেবেন নাCHECK-এ যায় না (অনেক সারি যোগ) — trigger বা অ্যাপ

U9 (সংখ্যাটি উদাহরণ মাত্র) এক সারিতে দেখা যায় না বলে CHECK-এ যায় না — পর্ব ৬-এ দেখেছেন। তবু তালিকায় রাখুন, যাতে কেউ ধরে না নেন যে ডেটাবেস এটি পাহারা দিচ্ছে।

১৯৭৬ সালে Peter Chen-এর প্রবন্ধ “The Entity-Relationship Model — Toward a Unified View of Data” (ACM Transactions on Database Systems) ER মডেলকে জনপ্রিয় করে। পরে, ১৯৮৩ সালে, Chen আরেকটি প্রবন্ধে দেখান ইংরেজি বাক্যের গঠন আর ER ডায়াগ্রামের মধ্যে মিল আছে — বিশেষ্য থেকে entity, ক্রিয়া থেকে relationship. একই বছর Russell Abbott প্রোগ্রাম ডিজাইনের জন্য প্রায় একই কৌশল প্রকাশ করেন, যা পরে object-oriented ডিজাইনে “noun–verb analysis” নামে ছড়িয়ে পড়ে। চার দশক পরেও প্রথম খসড়া বানাতে এটি সবচেয়ে সহজ পথ।

২ · কেস A: বিশ্ববিদ্যালয় — ER ডায়াগ্রাম থেকে db-uni

বিশেষ্য-ক্রিয়ার ফল আর নিয়মের তালিকা দিয়ে এবার ER ডায়াগ্রাম আঁকা যায়। আয়তক্ষেত্র entity, রম্বস relationship, ডিম্বাকৃতি relationship-এর attribute, আর রেখার পাশে 1, N, M হলো cardinality:

বিশ্ববিদ্যালয়ের ER ডায়াগ্রাম (db-uni)

Entities, relationships with cardinalities, and the attributes that belong to the enrolment relationship

বিভাগ (departments) শিক্ষক (instructors) কোর্স (courses) শিক্ষার্থী (students) ভর্তি term grade 1N 1N 1N 1N MN 1N চাকরি করেন চালায় সদস্য পড়ান mentor
term-এর নিচে দাগ: শিক্ষার্থী আর কোর্সের সাথে মিলে এটি একটি ভর্তিকে আলাদা করে চেনায়।

পর্ব ১০-এর mapping নিয়ম প্রয়োগ করলে এই ডায়াগ্রাম থেকে ঠিক পাঁচটি টেবিল আসে। প্রতিটি strong entity একটি টেবিল; প্রতিটি 1:N relationship “N” দিকের টেবিলে একটি foreign key; আর M:N relationship “ভর্তি” নিজেই একটি টেবিল — junction table — যার key দুই দিকের key আর relationship-এর নিজের key-attribute মিলে। নিচের এডিটরের “নমুনা ডেটা” বাক্সে পুরো db-uni দেখতে পাবেন; এখানে প্রতিটি টেবিলের সিদ্ধান্তের কারণ:

টেবিলকেন আছেমূল সিদ্ধান্ত আর কারণ
departments (dept_id, name, building)বিভাগ entity — ৪টি বিভাগsurrogate key dept_id; নাম UNIQUE (U1); building NULL চলে, কারণ English বিভাগের এখনো নিজের ভবন নেই
instructors (instructor_id, name, dept_id, salary, mentor_id)শিক্ষক entity — ৬ জন“চাকরি করেন” 1:N → dept_id FK; mentor সম্পর্ক একই টেবিলের ভেতরে, তাই mentor_id → instructors — একে বলে recursive relationship; তিনজন সিনিয়রের mentor_id NULL
students (student_id, name, dept_id, batch, cgpa)শিক্ষার্থী entity — ৮ জনstudent_id বিশ্ববিদ্যালয়ের দেওয়া আইডি; Habib Ullah-র dept_id NULL (বিভাগ বাছেননি, U4); Fahim Karim-এর cgpa NULL; cgpa একটি সংরক্ষিত হিসাব — অংশ ৩-এ এর ঝুঁকি
courses (course_id, title, dept_id, credits, instructor_id)কোর্স entity — ৭টিnatural key 'CSE301' (U6); credits REAL, কারণ ল্যাব 1.5 credit; ENG101-এর শিক্ষক এখনো ঠিক হয়নি, তাই instructor_id NULL
enrollments (student_id, course_id, term, grade)M:N “ভর্তি” — ১৩টি সারিদুই FK + term মিলে primary key (U7); grade NULL চলে (U8)

খেয়াল করুন, একই ডেটাবেসে দুই রকম key: students-এ সংখ্যা, courses-এ অর্থবহ কোড — পর্ব ৬-এর মাপকাঠি (স্থির, ছোট, কখনো NULL নয়) প্রতিটি টেবিলে আলাদা করে প্রয়োগ হয়েছে। আর mentor-এর জন্য আলাদা টেবিল লাগেনি: 1:N বলে “N” দিকের সারিতে একটি কলামই যথেষ্ট। নিয়ম “একাধিক mentor” হলে সম্পর্কটি M:N হতো, তখন আলাদা টেবিল (mentor_id, mentee_id) লাগত।

৩ · কেস A-র তিনটি কঠিন সিদ্ধান্ত — আর বাতিল হওয়া বিকল্প

সিদ্ধান্ত ১ — term কেন primary key-র অংশ

enrollments-এর key হিসেবে প্রথমে মনে আসে (student_id, course_id): “এই শিক্ষার্থী এই কোর্সে আছেন”। নিয়ম U7 না জানলে এটিই সঠিক মনে হয়। নিচের ডায়াগ্রামে একই ঘটনা দুটি নকশায় চালিয়ে দেখুন — Fahim Karim (2203) 2023-Spring-এ EEE101-এ F পেয়ে পরের বছর আবার কোর্সটি নিলেন:

দুটি বিকল্প নকশালাল = এই ধাপের পরিবর্তন

            
হিসাব
নকশা ক-তে সারি
নকশা খ-তে সারি
F-এর রেকর্ড (নকশা ক)
  1. দুটি খালি টেবিল। নকশা ক-এর key (student_id, course_id), নকশা খ-এর key (student_id, course_id, term). প্রথম সারি আসছে: Fahim (2203), EEE101, 2023-Spring, grade F.
  2. প্রথম ভর্তি — দুই নকশাতেই ঢোকে। কোনো টেবিলে এখনো কিছু নেই, তাই কোনো key-র সংঘাত নেই। দুই টেবিলেই ১টি সারি।
  3. পুনরায় ভর্তি — নকশা ক আটকে দেয়। 2024-Spring-এ একই কোর্স: (2203, EEE101) জোড়া নকশা ক-এ আগেই আছে, তাই নিয়মসম্মত একটি ঘটনা বাতিল। নকশা খ-এ term আলাদা, তাই সারিটি ঢোকে — এখন ২টি সারি, grade এখনো NULL.
  4. নকশা ক-এর “সমাধান”: পুরোনো সারি বদলে ফেলা। অনেকে term আর grade UPDATE করে বসান। সারি ১টিই থাকে, কিন্তু F-এর ঘটনা চিরতরে মুছে যায় — অথচ ট্রান্সক্রিপ্ট আর শিক্ষাবৃত্তির যোগ্যতা ওই ইতিহাসের ওপর নির্ভর করে।
  5. নকশা খ ভুলটি ঠিকই ধরে। একই term-এ (2024-Spring) একই কোর্সে দ্বিতীয় ভর্তির চেষ্টা: তিনটি কলামই মেলে, তাই বাতিল। term-সহ key বৈধ retake ঢুকতে দেয়, একই term-এ দুবার ভর্তি আটকায় — U7 পুরোপুরি।
  6. ফল আসার পর। 2024-Spring-এর সারিতে grade = 'B'. নকশা খ-এর ট্রান্সক্রিপ্টে আগে F, পরে B; নকশা ক-এ শুধু B. key-র আকার ঠিক করে কোন সত্য ডেটাবেসে রাখা সম্ভব।

এবার আসল db-uni-তে একই পরীক্ষা চালান। Fahim-এর 2023-Spring-এর F সারিটি নমুনা ডেটাতেই আছে:

-- Fahim Karim (2203)-এর বর্তমান রেকর্ড
SELECT * FROM enrollments WHERE student_id = 2203;

-- পরের term-এ retake: term আলাদা, তাই ঢুকবে
INSERT INTO enrollments VALUES (2203, 'EEE101', '2024-Spring', 'B');
SELECT * FROM enrollments WHERE student_id = 2203;

-- একই term-এ আবার: তিনটি key-কলামই মেলে, বাতিল
INSERT INTO enrollments VALUES (2203, 'EEE101', '2024-Spring', 'A');

প্রথম SELECT-এ একটি সারি: 2203 | EEE101 | 2023-Spring | F. INSERT-এর পর দ্বিতীয় SELECT-এ দুটি সারি — F-এর পাশে 2203 | EEE101 | 2024-Spring | B. শেষ INSERT-এ ত্রুটি: UNIQUE constraint failed: enrollments.student_id, enrollments.course_id, enrollments.term — বার্তায় তিনটি কলামের নাম, কারণ লঙ্ঘন হয়েছে পুরো composite key-র।

আরেকটি বিকল্প: surrogate enrollment_id আর পাশে UNIQUE (student_id, course_id, term). এটিও সঠিক, অনেক ORM-ভিত্তিক প্রজেক্টে এভাবেই হয় (পর্ব ৫০) — শুধু UNIQUE-টি ভুলে গেলে একই term-এ দুবার ভর্তি চুপচাপ ঢুকবে। db-uni-তে কোনো টেবিল enrollments-এর দিকে আঙুল তোলে না, তাই আলাদা id লাগেনি।

সিদ্ধান্ত ২ — grade কেন NULL হতে পারে

term চলাকালীন ভর্তি আছে, কিন্তু ফল নেই। Dilruba Khatun (2201)-এর 2024-Fall-এর CSE301 সারিতে grade NULL — অর্থাৎ “এখনো জানা নেই”। বিকল্প ছিল '' বা 'N/A' লেখা। দুটিই বাতিল: '' একটি মান, তাই “grade আছে কি না” বোঝাতে দুই রকম খালি (NULL আর '') আলাদা করতে হয়; আর 'N/A' দেখতে একটি grade-এর মতো, কেউ ভুল করে এটিকে গড়ে ধরতে পারেন। NULL-এর সুবিধা হলো aggregate ফাংশন (AVG, COUNT(grade)) এটিকে নিজে থেকেই বাদ দেয় — পর্ব ১৬ আর ১৮-এ বিস্তারিত।

সতর্কতা: NULL-এর অর্থ একটিই হওয়া চাই। “ফল আসেনি” আর “কোর্স ছেড়েছেন” দুটিই NULL হলে আর আলাদা করা যায় না — তাই W (withdrawn) আসল মান হিসেবে রাখুন। আর db-uni-র grade-এ কোনো CHECK নেই, তাই 'Z'-ও আটকায় না; অংশ ৭-এর রিভিউতে এটি একটি পর্যবেক্ষণ।

সিদ্ধান্ত ৩ — cgpa রাখা, নাকি প্রতিবার হিসাব করা

CGPA আসলে enrollments-এর grade আর courses-এর credit থেকে হিসাব করা যায়। এমন attribute-কে বলে derived attribute. db-uni ইচ্ছে করেই এটিকে students-এ রেখেছে — যাতে তালিকা দেখাতে প্রতিবার হিসাব না লাগে, আর SQL শেখার প্রথম পর্বগুলোতে একটি সহজ সংখ্যা-কলাম থাকে। কিন্তু একই সত্য দুই জায়গায় থাকলে প্রশ্ন ওঠে: কোনটি ঠিক? নিচে প্রতিটি grade-কে একটি point-এ বদলে credit দিয়ে ভারিত গড় বের করা হয়েছে (অনেক বাংলাদেশি বিশ্ববিদ্যালয়ে প্রচলিত একটি স্কেল ধরে নেওয়া: A+ = 4.00, A = 3.75, A- = 3.50 … F = 0). CASE আর GROUP BY এখনই না বুঝলেও চলবে — পর্ব ১৬ আর ১৮-এ আসবে; শুধু ফলের দুটি কলাম তুলনা করুন:

SELECT s.student_id, s.name, s.cgpa AS stored,
       ROUND(SUM(c.credits * CASE e.grade
             WHEN 'A+' THEN 4.0  WHEN 'A' THEN 3.75 WHEN 'A-' THEN 3.5
             WHEN 'B+' THEN 3.25 WHEN 'B' THEN 3.0  WHEN 'B-' THEN 2.75
             WHEN 'C+' THEN 2.5  WHEN 'C' THEN 2.25 WHEN 'D'  THEN 2.0
             ELSE 0 END) / SUM(c.credits), 2) AS computed
FROM students s
JOIN enrollments e ON e.student_id = s.student_id AND e.grade IS NOT NULL
JOIN courses c ON c.course_id = e.course_id
GROUP BY s.student_id, s.name, s.cgpa
ORDER BY s.student_id;

সাতটি সারি, আর একটিতেও দুই কলাম মেলে না: Arif Hossain 3.75 বনাম 3.65, Bristy Saha 3.4 বনাম 3.13, Chayon Roy 2.95 বনাম 2.5, Dilruba Khatun 3.9 বনাম 3.75, Emon Mia 3.1 বনাম 2.75, Fahim Karim NULL বনাম 0, Gulshan Ara 3.55 বনাম 3.38. Habib Ullah আসেননি, কারণ তাঁর কোনো ভর্তির সারি নেই। নমুনা ডেটায় enrollments-এ সব কোর্স নেই, তাই অমিল প্রত্যাশিত — কিন্তু প্রশ্নটি বাস্তব: বাস্তব সিস্টেমে এমন অমিল মানে কোথাও একটি বাগ। কেউ grade বদলেছেন কিন্তু cgpa আবার হিসাব হয়নি; বা উল্টো। সবচেয়ে চোখে পড়ার মতো Fahim: তাঁর একটি F আছে, অথচ সংরক্ষিত cgpa NULL.

তিনটি পথ খোলা, প্রতিটির দাম আলাদা:

  • রাখবেন না, দরকারে হিসাব করুন — একটি view (পর্ব ২৪) দিয়ে উপরের কোয়েরিটি নাম দিয়ে রাখুন। সত্যের উৎস একটিই; খরচ হলো প্রতিবার হিসাব।
  • রাখুন, কিন্তু স্বয়ংক্রিয়ভাবে হালনাগাদ করুন — grade বদলালে trigger (পর্ব ২৯) বা একই transaction-এ (পর্ব ৪৪) cgpa আবার হিসাব। পড়া দ্রুত, লেখা একটু ধীর।
  • রাখুন, আর স্পষ্ট লিখে দিন এটি একটি “স্ন্যাপশট” — যেমন প্রতি term শেষে রেজিস্ট্রার অফিসের প্রকাশ করা অফিসিয়াল cgpa, সাথে cgpa_as_of term. তখন অমিল ভুল নয়, পরিকল্পিত।

যেটিই বাছুন, সিদ্ধান্তটি লিখে রাখুন। ইচ্ছাকৃতভাবে একই তথ্য দুই জায়গায় রাখাকে বলে denormalization — পর্ব ৩৮-এ এর পুরো হিসাব দেখবেন।

৪ · কেস B: অনলাইন দোকান — db-shop-এর সিদ্ধান্ত

দ্বিতীয় কেস একটি ছোট অনলাইন দোকান। মালিকের সাথে কথা বলে পাওয়া মূল নিয়মগুলো:

নংনিয়মডিজাইনে যা হলো
S1একটি ফোন নম্বরে একটিই গ্রাহক-অ্যাকাউন্ট; নম্বর না দিয়েও কেনা যায়phone TEXT UNIQUE, NULL চলে
S2প্রতিটি পণ্য একটি শ্রেণির; শ্রেণির নাম অনন্যproducts.category_id FK; categories.name UNIQUE
S3দাম শূন্যের বেশি; স্টক কখনো ঋণাত্মক নয়CHECK (price > 0), CHECK (stock >= 0)
S4প্রতিটি অর্ডার একজন নিবন্ধিত গ্রাহকেরorders.customer_id NOT NULL FK
S5অর্ডারের অবস্থা চারটির একটি: pending, shipped, delivered, cancelledCHECK (status IN (…))
S6একটি অর্ডারে একই পণ্য একটি লাইনেই, পরিমাণ দিয়েPRIMARY KEY (order_id, product_id), CHECK (quantity > 0)
S7চালানে বিক্রির দিনের দাম থাকবে, পরে দাম বদলালেওorder_items.unit_price কপি
S8বিক্রির রেকর্ড কখনো মোছা যাবে নাকোনো FK-তে CASCADE নেই — ডিফল্ট NO ACTION

এর থেকে পাঁচটি টেবিল: 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 — ১৬টি লাইন)। orders আর products-এর মধ্যে M:N সম্পর্ক (“একটি অর্ডারে অনেক পণ্য, একটি পণ্য অনেক অর্ডারে”), আর order_items হলো তার junction table — ঠিক enrollments-এর মতো। পুরো ডেটা এডিটরের নিচের “নমুনা ডেটা” বাক্সে।

কেন unit_price কপি করা হয় — ঐতিহাসিক দাম

প্রথম দেখায় order_items.unit_price বাড়তি মনে হয়: দাম তো products.price-এই আছে, JOIN করলেই পাওয়া যায়। কিন্তু দুটি কলাম আলাদা সত্য ধরে রাখে। products.price হলো “আজকের দাম”, যা মালিক যেকোনো দিন বদলান। unit_price হলো “ওই অর্ডারে গ্রাহক কত দিয়ে কিনেছিলেন” — একবার লেখা হলে কখনো বদলায় না। মুদি দোকানের খাতার কথা ভাবুন: গত মাসে চালের কেজি যত টাকা ছিল, সেই হিসাবে বাকি লেখা আছে; আজ দাম বাড়লে পুরোনো বাকির অঙ্ক বদলায় না।

-- কোন লাইনে বিক্রির দাম আজকের দাম থেকে আলাদা?
SELECT oi.order_id, p.name, oi.unit_price AS sold_at, p.price AS price_now
FROM order_items oi JOIN products p ON p.product_id = oi.product_id
WHERE oi.unit_price <> p.price;

-- মোবাইলের দাম বাড়ল
UPDATE products SET price = 16000 WHERE product_id = 101;

-- অর্ডার 1001-এর চালান: কপি করা দামে, আর দাম কপি না করলে যা হতো
SELECT order_id, SUM(quantity * unit_price) AS invoice_total
FROM order_items WHERE order_id = 1001 GROUP BY order_id;
SELECT oi.order_id, SUM(oi.quantity * p.price) AS if_price_not_copied
FROM order_items oi JOIN products p ON p.product_id = oi.product_id
WHERE oi.order_id = 1001 GROUP BY oi.order_id;

প্রথম SELECT-এ দুটি সারি: 1007 | Headphone | 1150 | 1200 (সেদিন ছাড় ছিল) আর 1009 | Mobile Phone | 14500 | 15000. UPDATE-এর পর অর্ডার 1001-এর চালান 17400 — Rahim Uddin সেদিন যা দিয়েছিলেন, ঠিক তাই। কিন্তু দাম কপি না করে products থেকে নিলে একই চালান হয়ে যায় 18400 — একটি দাম বদলের কারণে এক মাস আগের বিক্রির হিসাব ১,০০০ টাকা বেড়ে গেল! হিসাবরক্ষণ, ভ্যাট রিটার্ন আর ফেরতের টাকা — সবই ভুল হতো।

এটি redundancy নয়: unit_price নির্ভর করে (order_id, product_id)-এর ওপর, শুধু product_id-এর ওপর নয় (ভাবনার প্রশ্ন ২)। একই কারণে ডেলিভারি ঠিকানা, ভ্যাটের হার আর ছাড়ও অর্ডারের সময়কার মান হিসেবে কপি হয়।

status-এর CHECK আর stock-এর CHECK

status: মুক্ত লেখা হলে কেউ লিখবেন 'Delivered', কেউ 'delivred', কেউ 'done' — তারপর “কয়টি অর্ডার পৌঁছেছে?” প্রশ্নের উত্তর তিনটি বানানে ভাগ হয়ে যাবে। CHECK (status IN ('pending', 'shipped', 'delivered', 'cancelled')) শব্দগুলোকে চারটিতে বেঁধে দেয়; SQLite-এ তুলনাটি ছোট-বড় হাতের অক্ষর আলাদা ধরে, তাই 'Delivered'-ও আটকায়। বিকল্প ছিল একটি order_statuses টেবিল আর তার দিকে FK — তালিকা ঘন ঘন বদলালে সেটি ভালো। কিন্তু অবস্থা বদলের ক্রম (“cancelled থেকে delivered নয়”) কোনোটিই পারে না; আগের আর নতুন মান দুটোই দেখতে trigger লাগে (পর্ব ২৯)।

stock: CHECK (stock >= 0) শেষ পাহারাদার। একই মুহূর্তে দুজন ক্রেতা শেষ Smart Watch-টি কিনতে গেলে দুজনই “১টি আছে” দেখতে পারেন; UPDATE যদি stock = stock - 1 ধরনে লেখা হয়, তবে দ্বিতীয় UPDATE-এ স্টক −1 হতে গেলে CHECK সেটি বাতিল করবে। (অ্যাপ যদি আগে পড়া মান থেকে হিসাব করে stock = 0 লেখে, দুটি লেখাই 0 বসাবে আর CHECK কিছুই ধরবে না — এজন্যই আপেক্ষিক UPDATE বা lock লাগে।) একসাথে-আসা অনুরোধের পুরো গল্প পর্ব ৪৬ আর ৪৭-এ।

৫ · নতুন চাহিদা এলে — ডিজাইন ধাপে ধাপে বাড়ানো

দোকান চালু হওয়ার ছয় মাস পর মালিক চারটি নতুন জিনিস চাইলেন: পণ্যের রিভিউ, গ্রাহকের একাধিক ঠিকানা, bKash/Nagad/কার্ড/ক্যাশ-অন-ডেলিভারিতে পেমেন্ট (ফেরতসহ), আর কুপন। পেশাদারের কাজ এখানে প্রতিটি চাহিদার জন্য আবার অংশ ১-এর প্রশ্নগুলো করা — বিশেষ করে “কখনো কি…?”। নিচে পুরো যাত্রাটি এক নজরে, তারপর প্রতিটি আলাদা করে:

দোকানের schemaলাল = এই ধাপে যোগ হলো

            
সিদ্ধান্ত
টেবিল
চাহিদা
মূল constraint
  1. শুরুর অবস্থা। পাঁচটি টেবিল (PK = primary key, FK = foreign key, UQ = UNIQUE). প্রতিটি চাহিদায় প্রথম প্রশ্ন: নতুন entity, নতুন relationship, নাকি নতুন কলাম?
  2. রিভিউ — নতুন M:N টেবিল। গ্রাহক আর পণ্যের মধ্যে নতুন সম্পর্ক, নিজের attribute (rating, body)-সহ। প্রশ্ন: “একজন গ্রাহক কি একই পণ্যে দুটি রিভিউ দিতে পারেন?” মালিক বললেন না, বদলাতে পারবেন। তাই (customer_id, product_id) নিজেই primary key.
  3. ঠিকানা — customers-এর একটি কলাম নয়, আলাদা টেবিল। একজনের বাসা, অফিস, গ্রামের বাড়ি — একাধিক মান মানে একটি ঘরে নয় (1NF). customers থেকে 1:N. সাথে নিয়ম: একজনের সর্বোচ্চ একটি default ঠিকানা — এটি সাধারণ UNIQUE দিয়ে হয় না, লাগে partial index.
  4. পেমেন্ট — orders থেকে 1:N. “একটি অর্ডার কি দুই ভাগে পরিশোধ হয়?” হ্যাঁ — কিছু bKash-এ, বাকিটা কার্ডে। তাই আলাদা টেবিল; গেটওয়ের transaction ID (provider_ref) UNIQUE, যাতে একই পেমেন্ট দুবার লেখা না হয়।
  5. ফেরত — নতুন টেবিল নয়, নতুন ধরনের সারি। ফেরত হলো উল্টো দিকের একটি পেমেন্ট: kind = 'refund', আর refund_of-এ কোন পেমেন্টের ফেরত — payments নিজের দিকেই FK.
  6. কুপন — entity আর একটি সংযোগ-টেবিল। coupons-এ কোড, ছাড়ের হার আর মেয়াদ। কোন অর্ডারে কোন কুপন, তা order_coupons-এ — আর নিয়ম “এক অর্ডারে একটিই কুপন” বলে order_id নিজেই সেখানে primary key. ছাড়ের টাকা কপি হয়, unit_price-এর মতোই। মোট টেবিল এখন ১০টি।

৫.১ · রিভিউ — composite primary key দিয়ে “একটিই”

CREATE TABLE reviews (
  customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
  product_id  INTEGER NOT NULL REFERENCES products(product_id),
  rating      INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
  body        TEXT,                    -- শুধু তারকা দিলেও চলে
  created_at  TEXT NOT NULL,
  PRIMARY KEY (customer_id, product_id)   -- এক গ্রাহক, এক পণ্য, একটি রিভিউ
);

নিচের এডিটর db-shop-এর সাথে এই টেবিল আর পাঁচটি রিভিউ আগে থেকে তৈরি করে নেয়: Rahim (1) → Mobile Phone 5★, Karima (2) → DSA Book 4★ আর Headphone 3★, Nusrat (4) → Panjabi 5★, Sabbir (3) → Smart Watch 1★।

-- প্রতিটি পণ্যের গড় রেটিং
SELECT p.name, COUNT(r.rating) AS reviews, AVG(r.rating) AS avg_rating
FROM products p JOIN reviews r ON r.product_id = p.product_id
GROUP BY p.product_id, p.name ORDER BY p.product_id;

-- মত বদলালে: নতুন সারি নয়, UPDATE
UPDATE reviews SET rating = 4, body = 'Good, battery ok'
WHERE customer_id = 1 AND product_id = 101;
SELECT * FROM reviews WHERE customer_id = 1;

-- একই পণ্যে দ্বিতীয় রিভিউ: বাতিল
INSERT INTO reviews VALUES (1, 101, 2, 'Second review', '2025-10-12');

প্রথম ফলে পাঁচটি সারি: Mobile Phone 5, Headphone 3, DSA Book 4, Panjabi 5, Smart Watch 1 — প্রতিটির একটি করে রিভিউ। UPDATE-এর পর Rahim-এর সারি 1 | 101 | 4 | Good, battery ok | 2025-09-05. শেষ INSERT-এ UNIQUE constraint failed: reviews.customer_id, reviews.product_id. নিয়মটি অ্যাপের কোডে নয়, key-তেই বসানো — তাই মোবাইল অ্যাপ, ওয়েবসাইট বা পুরোনো আমদানি-স্ক্রিপ্ট, কোনো পথেই দ্বিতীয় রিভিউ ঢুকবে না। একটি প্রশ্ন বাকি থাকল: Sabbir কি আসলে Smart Watch কিনেছিলেন? অংশ ৬-এ দেখব।

৫.২ · একাধিক ঠিকানা — আর “একটিই default”

customers-এ address1, address2, address3 যোগ করা প্রলোভনের মতো, কিন্তু চতুর্থ ঠিকানায় আবার schema বদলাতে হবে। সঠিক পথ customers থেকে 1:N একটি টেবিল। “একজনের সর্বোচ্চ একটি default” নিয়মের জন্য UNIQUE (customer_id) চলবে না (তাহলে একটিই ঠিকানা রাখা যেত), আর UNIQUE (customer_id, is_default)-ও চলবে না (তাহলে non-default ঠিকানাও একটির বেশি রাখা যেত না)। সমাধান হলো partial unique index — শুধু is_default = 1 সারিগুলোর ওপর অনন্যতা:

CREATE TABLE addresses (
  address_id  INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customers(customer_id) ON DELETE CASCADE,
  label       TEXT NOT NULL,           -- 'Home', 'Office' …
  line1       TEXT NOT NULL,
  district    TEXT NOT NULL,
  is_default  INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1))
);
CREATE UNIQUE INDEX one_default_address ON addresses(customer_id) WHERE is_default = 1;

এখানে ON DELETE CASCADE ইচ্ছাকৃত: গ্রাহক না থাকলে তাঁর ঠিকানার কোনো আলাদা অর্থ নেই — পর্ব ৬-এর মাপকাঠি। এডিটরটি টেবিল, index আর তিনটি ঠিকানা আগে বানিয়ে নেয়: Rahim (1)-এর Home (default) আর Office, Karima (2)-এর Home (default).

-- তৃতীয় ঠিকানা, default নয়: চলবে
INSERT INTO addresses VALUES (4, 1, 'Village', 'Puthia', 'Rajshahi', 0);

SELECT customer_id, COUNT(*) AS addresses, SUM(is_default) AS defaults
FROM addresses GROUP BY customer_id;

-- Rahim-এর দ্বিতীয় default: index আটকাবে
INSERT INTO addresses VALUES (5, 1, 'Hostel', 'Shahbag', 'Dhaka', 1);

প্রথম INSERT সফল। SELECT-এ 1 | 3 | 1 আর 2 | 1 | 1 — Rahim-এর তিনটি ঠিকানা, তার একটি default. শেষ INSERT-এ UNIQUE constraint failed: addresses.customer_id, কারণ index-টিতে শুধু default সারি থাকে আর সেখানে customer_id 1 আগেই আছে। SQLite আর PostgreSQL partial index সমর্থন করে; MySQL করে না — সেখানে সাধারণত customers-এ default_address_id কলাম দিয়ে নিয়মটি লেখা হয়। আর অংশ ৪-এর শিক্ষা মনে রাখুন: অর্ডারের ডেলিভারি ঠিকানা orders-এ কপি করুন, নইলে গ্রাহক ঠিকানা বদলালে পুরোনো অর্ডারের ঠিকানাও বদলে যাবে।

৫.৩ · পেমেন্ট আর আংশিক ফেরত

বাংলাদেশের অনলাইন দোকানে পেমেন্টের পথ অনেক: bKash, Nagad, কার্ড, আর ক্যাশ-অন-ডেলিভারি (cod). মূল প্রশ্নগুলোর উত্তর থেকে ডিজাইন: একটি অর্ডারে একাধিক পেমেন্ট হতে পারে (1:N); ফেরত আংশিক হতে পারে আর একটি নির্দিষ্ট পেমেন্টের বিপরীতে হয়; গেটওয়ে যে transaction ID দেয়, সেটি কখনো দুবার আসবে না; আর টাকা রাখা হবে পয়সায় পূর্ণসংখ্যা হিসেবে (কেন, তা অংশ ৭-এ)।

CREATE TABLE payments (
  payment_id   INTEGER PRIMARY KEY,
  order_id     INTEGER NOT NULL REFERENCES orders(order_id),     -- CASCADE নয়
  method       TEXT NOT NULL CHECK (method IN ('bkash', 'nagad', 'card', 'cod')),
  kind         TEXT NOT NULL CHECK (kind IN ('charge', 'refund')),
  amount_paisa INTEGER NOT NULL CHECK (amount_paisa > 0),          -- 1 টাকা = 100
  refund_of    INTEGER REFERENCES payments(payment_id),
  provider_ref TEXT UNIQUE,            -- গেটওয়ের TrxID; cod-এ NULL
  paid_at      TEXT NOT NULL,          -- UTC, যেমন '2025-09-01T10:15:00Z'
  CHECK ((kind = 'refund') = (refund_of IS NOT NULL))
);

শেষ CHECK-টি খেয়াল করুন: “refund হলে refund_of থাকতেই হবে, charge হলে থাকবে না” — দুই কলামের সম্পর্ক এক সারির ভেতরে, তাই CHECK-এ লেখা যায়। আর amount সবসময় ধনাত্মক; দিক বোঝায় kind. এতে “−1200” আর “1200 refund” — একই জিনিসের দুই রকম লেখা তৈরি হয় না। অর্ডার 1001-এর পেমেন্টের ইতিহাস ধাপে ধাপে:

অর্ডার 1001-এর পেমেন্টলাল = নতুন সারি বা হিসাব

            
হিসাব (টাকা)
charge-এর যোগফল
refund-এর যোগফল
নিট পরিশোধ
অবস্থা
  1. অর্ডার আছে, পেমেন্ট নেই। Rahim Uddin-এর অর্ডার 1001: একটি Mobile Phone (15000) আর দুটি Headphone (2400), মোট 17400 টাকা। payments-এ এখনো কোনো সারি নেই।
  2. প্রথম ভাগ bKash-এ। Rahim ওয়ালেট থেকে 10000 টাকা দিলেন। সারি #1: kind = charge, amount_paisa = 1000000, provider_ref = bKash-এর TrxID. বাকি 7400.
  3. বাকিটা কার্ডে। সারি #2: card, 740000 পয়সা। charge-এর যোগফল 17400 — পুরো পরিশোধ। orders-এ একটি “payment” কলাম থাকলে দুটি পেমেন্ট লেখাই যেত না।
  4. একটি Headphone ফেরত। সারি #3: refund, 1200 টাকা, refund_of = 1 — পেমেন্ট #1-এর বিপরীতে। মূল পেমেন্ট বদলায়নি; শুধু নতুন সারি যোগ।
  5. ব্যালান্স কোথাও রাখা নেই — হিসাব হয়। charge-এর যোগফল 17400, refund-এর 1200, নিট 16200 টাকা। হিসাবরক্ষণের খাতার মতো এটি একটি ledger: শুধু যোগ হয়, কখনো মোছে না। “refund তার মূল পেমেন্টের বেশি হবে না” নিয়মটি অনেক সারি যোগ করে দেখতে হয় — CHECK-এ যায় না, transaction বা trigger লাগে।
-- প্রতিটি অর্ডারের মোট দাম, পরিশোধ আর ফেরত (টাকায়)
SELECT o.order_id, o.status,
  (SELECT SUM(quantity * unit_price) FROM order_items oi
    WHERE oi.order_id = o.order_id) AS total_taka,
  COALESCE(SUM(CASE p.kind WHEN 'charge' THEN p.amount_paisa END), 0) / 100.0 AS charged,
  COALESCE(SUM(CASE p.kind WHEN 'refund' THEN p.amount_paisa END), 0) / 100.0 AS refunded
FROM orders o LEFT JOIN payments p ON p.order_id = o.order_id
WHERE o.order_id IN (1001, 1003, 1009)
GROUP BY o.order_id, o.status;

তিনটি সারি: 1001 | delivered | 17400 | 17400 | 1200, 1003 | shipped | 1780 | 1780 | 0 আর 1009 | pending | 18000 | 0 | 0. 1009 ক্যাশ-অন-ডেলিভারি — পণ্য পৌঁছে টাকা নিলে তখন cod সারি যোগ হবে; LEFT JOIN আছে বলে পেমেন্ট-ছাড়া অর্ডারও তালিকায় এসেছে (JOIN-এর ধরন পর্ব ১৯-এ)। আর গেটওয়ে একই নিশ্চিতকরণ দুবার পাঠালে provider_ref UNIQUE দ্বিতীয়টি আটকাবে — পেমেন্ট দুবার গোনা হবে না।

৫.৪ · কুপন

কুপনের প্রশ্ন: “এক অর্ডারে কি একাধিক কুপন চলে?” (না), “একটি কুপন কতবার ব্যবহার করা যায়?” (সীমা আছে), “মেয়াদ?” (শুরু আর শেষের তারিখ)। কুপনের হার পরে বদলাতে পারে, তাই প্রতিটি অর্ডারে কত টাকা ছাড় হয়েছিল তা কপি হয় — আবার সেই unit_price-এর যুক্তি:

CREATE TABLE coupons (
  code        TEXT PRIMARY KEY,                     -- 'EID25'
  percent_off INTEGER NOT NULL CHECK (percent_off BETWEEN 1 AND 90),
  valid_from  TEXT NOT NULL,
  valid_to    TEXT NOT NULL,
  max_uses    INTEGER CHECK (max_uses > 0),          -- NULL = সীমাহীন
  CHECK (valid_from <= valid_to)
);
CREATE TABLE order_coupons (
  order_id       INTEGER PRIMARY KEY REFERENCES orders(order_id),  -- এক অর্ডারে একটি
  code           TEXT NOT NULL REFERENCES coupons(code),
  discount_paisa INTEGER NOT NULL CHECK (discount_paisa > 0)
);

order_coupons-এ order_id নিজেই primary key — এটিই “এক অর্ডারে একটি কুপন”; নিয়ম বদলালে key হবে (order_id, code). orders-এ একটি coupon_code কলামও চলত — এটি রুচির কাছাকাছি সিদ্ধান্ত। আর “max_uses অতিক্রম নয়” অনেক সারি গোনার নিয়ম, CHECK-এ যায় না।

৬ · নমুনা ডেটায় ডিজাইন যাচাই — ব্যবসার প্রশ্নের উত্তর মেলে কি?

কাগজে নিখুঁত ডিজাইনও আসল প্রশ্নে আটকে যেতে পারে। তাই সাক্ষাৎকারের প্রশ্নগুলোর জন্য বাস্তবসম্মত নমুনা ডেটায় কোয়েরি লিখে দেখুন। নিয়মটি সহজ: যে প্রশ্নের উত্তর দিতে কোয়েরিতে কসরত লাগে বা ডেটাই নেই, সেখানে ডিজাইনে ফাঁক। দোকানের মালিকের দুটি প্রশ্ন: “শ্রেণি অনুযায়ী আয় কত?” আর “কোন পণ্য কখনো বিক্রি হয়নি?”

-- শ্রেণিভিত্তিক আয় (বাতিল অর্ডার বাদে)
SELECT c.name AS category, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o     ON o.order_id = oi.order_id AND o.status <> 'cancelled'
JOIN products p   ON p.product_id = oi.product_id
JOIN categories c ON c.category_id = p.category_id
GROUP BY c.name ORDER BY revenue DESC;

-- কখনো বিক্রি হয়নি এমন পণ্য
SELECT product_id, name, stock FROM products
WHERE product_id NOT IN (SELECT product_id FROM order_items);

প্রথম ফল: Electronics 40050, Grocery 4920, Clothing 4400, Books 2950. Clothing-এ শুধু অর্ডার 1005-এর দুটি Panjabi — বাতিল অর্ডার 1004-এর Saree বাদ গেছে। দ্বিতীয় ফল: 103 | Power Bank | 0. দুটি প্রশ্নেরই সরল উত্তর এসেছে, অর্থাৎ ডিজাইন এদের সামলাতে পারে। কিন্তু প্রথম কোয়েরিটি একটি নতুন requirements-প্রশ্নও তুলেছে: “আয়” মানে কী? এখানে pending আর shipped অর্ডারও গোনা হয়েছে। হিসাবরক্ষক হয়তো শুধু delivered আর পরিশোধিত অর্ডার গুনতে চান। এই সংজ্ঞাটি লিখে না রাখলে দুজন বিশ্লেষক দুটি আলাদা “আয়” রিপোর্ট করবেন — যাচাইয়ের আসল লাভ এমন প্রশ্ন খুঁজে পাওয়া।

এবার অংশ ৫.১-এর বাকি প্রশ্ন: “শুধু ক্রেতারাই রিভিউ দেবেন” — ডিজাইন কি এটি পাহারা দেয়? reviews-এর FK শুধু বলে গ্রাহক আর পণ্য আছে; কেনা হয়েছিল কি না বলে না, আর রিভিউতে order_id নেই বলে order_items-এর দিকে FK-ও বসে না। তাই কোয়েরি দিয়ে যাচাই:

-- প্রতিটি রিভিউ: গ্রাহক কি পণ্যটি delivered অর্ডারে পেয়েছিলেন?
SELECT r.customer_id, r.product_id, r.rating,
       EXISTS (SELECT 1 FROM orders o
               JOIN order_items oi ON oi.order_id = o.order_id
               WHERE o.customer_id = r.customer_id
                 AND oi.product_id = r.product_id
                 AND o.status = 'delivered') AS verified
FROM reviews r ORDER BY r.customer_id, r.product_id;

পাঁচটি সারির চারটিতে verified = 1, আর 3 | 110 | 1 | 0. Sabbir Hasan (3)-এর একমাত্র অর্ডার 1004 (Saree), যা বাতিল — Smart Watch তিনি কখনো কেনেননি, অথচ 1★ দিয়েছেন। বিকল্প তিনটি: রিভিউর আগে trigger বা অ্যাপ এই EXISTS যাচাই করবে; reviews-এ order_id রেখে FOREIGN KEY (order_id, product_id) REFERENCES order_items; অথবা সবাইকে রিভিউ দিতে দিয়ে শুধু verified চিহ্ন দেখানো। কোনটি, সেটি আবার মালিকের প্রশ্ন।

ব্যবসার প্রশ্নকোন টেবিল লাগেডিজাইন পারে?
Fahim কোন কোর্স কতবার নিয়েছেন, প্রতিবার কী grade?enrollmentsহ্যাঁ — term key-তে থাকায়
কোন কোর্সের শিক্ষক এখনো ঠিক হয়নি?coursesহ্যাঁ — instructor_id IS NULL
গত বছরের অর্ডারের আসল চালানorder_itemsহ্যাঁ — unit_price কপি থাকায়
কোন অর্ডারে টাকা বাকি, কত ফেরত গেছে?payments + order_itemsহ্যাঁ — শুধু নতুন ডিজাইনে
পণ্যের দাম কবে কত ছিল (ইতিহাস)?—না — products-এ শুধু আজকের দাম; লাগলে price_history টেবিল
অর্ডারটি কোন ঠিকানায় গিয়েছিল?—না — orders-এ ঠিকানার কপি যোগ করতে হবে

৭ · ডিজাইন রিভিউ চেকলিস্ট — কোড রিভিউর মতো schema রিভিউ

পেশাদার দলে schema-ও কোডের মতো রিভিউ হয়। নিচের তালিকাটি db-shop আর db-uni-তে প্রয়োগ করা হলো — নমুনা ডেটাবেস দুটি শেখানোর জন্য সরল রাখা, তাই কিছু পর্যবেক্ষণ আসবেই।

প্রশ্নdb-shop / db-uni-তে যা পাওয়া গেল
Key: প্রতিটি টেবিলে primary key? প্রতিটি natural key-তে UNIQUE?সবখানে PK আছে; phone, categories.name, departments.name UNIQUE. ✓
FK আর ON DELETE: প্রতিটি সম্পর্কে REFERENCES? প্রতিটির ON DELETE-এর যুক্তি?সব সম্পর্কে FK আছে; কোথাও action লেখা নেই, তাই NO ACTION — বিক্রির রেকর্ডের জন্য সঠিক (S8). order_items → orders-এ CASCADE ভাবা যেত (লাইন অর্ডারেরই অংশ), কিন্তু তাতে লাইনসহ পুরো অর্ডার মোছার পথ খুলে যেত — S8-এর জন্য NO ACTION-ই নিরাপদ।
NULL: প্রতিটি NULL-যোগ্য কলামের অর্থ একটিই?grade NULL = ফল আসেনি; city NULL = জানা নেই। কিন্তু enrollments.student_id আর course_id-এ NOT NULL নেই — নিচের এডিটর দেখুন।
CHECK: প্রতিটি সীমিত মানের কলামে শর্ত?price, stock, quantity, status, salary, cgpa ✓। grade-এ CHECK নেই; credits-এ CHECK (credits > 0) নেই।
নামকরণ: একই ধাঁচ সবখানে?টেবিল বহুবচন, ছোট হাতের snake_case, key <entity>_id, FK-র নাম parent-এর key-র নামে (mentor_id ব্যতিক্রম, কারণ ভূমিকা আলাদা) ✓
FK কলামে index: parent মোছা বা JOIN-এ পুরো টেবিল পড়তে হবে না তো?SQLite আর PostgreSQL নিজে বানায় না; orders.customer_id, order_items.product_id, enrollments.course_id-এ index নেই (composite PK-র প্রথম কলাম ছাড়া)।
টাকা: কোন টাইপে?price আর unit_price REAL — সরলতার জন্য; নতুন payments-এ পয়সায় INTEGER.
তারিখ ও সময়: কোন ফরম্যাট, কোন টাইমজোন?ISO 'YYYY-MM-DD' লেখা — ভালো, কারণ লেখা হিসেবে সাজালেও ক্রম ঠিক থাকে। শুধু তারিখ, সময় নেই; payments-এ UTC সময়।

পর্যবেক্ষণ ১ — composite primary key-তে NULL (SQLite-এর ফাঁদ)

পর্ব ৬-এ দেখেছেন, SQLite-এ INTEGER PRIMARY KEY ছাড়া অন্য primary key-তে NULL ঢুকে যেতে পারে। enrollments-এর key composite, আর student_id ও course_id-এ আলাদা NOT NULL লেখা নেই। FK-ও আটকাবে না, কারণ NULL কোনো কিছুর দিকে আঙুল তোলে না:

-- শিক্ষার্থী ছাড়া একটি "ভর্তি" — দুবার!
INSERT INTO enrollments VALUES (NULL, 'CSE101', '2024-Fall', NULL);
INSERT INTO enrollments VALUES (NULL, 'CSE101', '2024-Fall', NULL);

SELECT student_id, course_id, term FROM enrollments
WHERE student_id IS NULL;

দুটি INSERT-ই সফল, আর SELECT-এ দুটি হুবহু একই সারি: NULL | CSE101 | 2024-Fall. entity integrity ভেঙেছে — এমন ভর্তি কার, কেউ জানে না — আর দুটি NULL “সমান” ধরা হয় না বলে primary key duplicate-ও আটকায়নি। PostgreSQL, MySQL, SQL Server বা Oracle-এ প্রথম INSERT-ই ত্রুটি দিত, কারণ সেখানে primary key-র প্রতিটি কলাম নিজে থেকেই NOT NULL. সমাধান: student_id INTEGER NOT NULL REFERENCES … আর course_id TEXT NOT NULL REFERENCES … লেখা। রিভিউর শিক্ষা: “এই DBMS ঠিক কী নিশ্চিত করে” ধরে নেবেন না, পরীক্ষা করে দেখুন।

পর্যবেক্ষণ ২ — foreign key কলামে index

Foreign key-তে সব DBMS নিজে index বানায় না (MySQL InnoDB বানায়; PostgreSQL আর SQLite না)। index না থাকলে “Rahim-এর সব অর্ডার” খুঁজতে বা গ্রাহক মোছার আগের যাচাইয়ে পুরো orders টেবিল পড়তে হয় — এক কোটি সারিতে টের পাবেন। সুপারিশ:

CREATE INDEX idx_orders_customer     ON orders(customer_id);
CREATE INDEX idx_order_items_product ON order_items(product_id);  -- PK-র প্রথম কলাম order_id, তাই এটি আলাদা লাগে
CREATE INDEX idx_products_category   ON products(category_id);
CREATE INDEX idx_enrollments_course  ON enrollments(course_id);
CREATE INDEX idx_payments_order      ON payments(order_id);

order_items(order_id)-এর আলাদা index লাগে না: composite primary key (order_id, product_id)-এর index প্রথম কলাম দিয়ে খোঁজায় কাজে লাগে। কিন্তু product_id দিয়ে খোঁজায় লাগে না — ফোনবুক নামের প্রথম অক্ষরে সাজানো থাকলে শুধু পদবি দিয়ে খোঁজার মতো। index-এর পুরো হিসাব পর্ব ৪০ আর ৪১-এ।

পর্যবেক্ষণ ৩ — টাকা কেন REAL-এ রাখা ঝুঁকিপূর্ণ

REAL (অন্য DBMS-এ FLOAT বা DOUBLE) হলো binary floating-point সংখ্যা। কম্পিউটার এটিকে ২-এর ঘাতে রাখে, তাই ০.১-এর মতো দশমিক ভগ্নাংশ ঠিকঠাক রাখতে পারে না — যেমন দশমিকে ১/৩ = ০.৩৩৩… কোথাও না কোথাও কেটে ফেলতে হয়। টাকার হিসাবে পয়সার এই ছোট ভুলগুলো জমে, তুলনায় গোলমাল করে, আর পয়সায় রূপান্তরের সময় এক পয়সা হারায়:

-- ০.১ টাকা + ০.২ টাকা কি ০.৩ টাকা?
SELECT 0.1 + 0.2 AS sum_real, 0.1 + 0.2 = 0.3 AS is_equal;

-- ১.১৫ টাকাকে পয়সায় বদলানো
SELECT 1.15 * 100 AS x,
       CAST(1.15 * 100 AS INTEGER) AS paisa_truncated,
       CAST(ROUND(1.15 * 100) AS INTEGER) AS paisa_rounded;

-- পয়সায় পূর্ণসংখ্যা: কোনো ভুল নেই
SELECT 10 + 20 AS paisa_sum, (10 + 20) = 30 AS is_equal;

প্রথম ফল 0.30000000000000004 | 0 — যোগফল ০.৩-এর খুব কাছে কিন্তু সমান নয়, তাই = 0.3 মিথ্যা (0). কোনো রিপোর্টে “যেসব অর্ডারের বাকি ঠিক ০” খুঁজলে এমন সারি বাদ পড়বে। দ্বিতীয় ফল 114.99999999999999 | 114 | 115 — ১.১৫ টাকা পয়সায় নিতে গিয়ে সরাসরি CAST করলে ১১৪ পয়সা হয়, এক পয়সা হারায়! ROUND করে নিলে ঠিক ১১৫। তৃতীয় ফল 30 | 1 — পূর্ণসংখ্যার যোগ সবসময় নিখুঁত। তাই দুটি নিরাপদ পথ: পয়সায় INTEGER (SQLite-এ এটিই সেরা), অথবা PostgreSQL/MySQL/Oracle/SQL Server-এ NUMERIC(12, 2) বা DECIMAL(12, 2) — যা দশমিক অঙ্ক হুবহু রাখে।

CREATE TABLE products (
  product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       TEXT NOT NULL,
  price      NUMERIC(12, 2) NOT NULL CHECK (price > 0),   -- হুবহু দশমিক
  stock      INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0)
);

db-shop তাহলে REAL কেন? শুধু সরলতার জন্য: প্রথম পর্বগুলোতে 15000-এর মতো সংখ্যা পড়া সহজ, আর এর সব দাম পূর্ণ টাকায়, যা REAL-এ হুবহু থাকে। কিন্তু বাস্তবে ভ্যাট বা শতাংশ-ছাড় হিসাব শুরু হলেই পয়সা আসবে — তখন REAL নয়।

আরও গভীরে: PostgreSQL-এর NUMERIC ভেতরে দশমিক অঙ্কগুলো ১০,০০০-ভিত্তিক “অঙ্ক”-এর একটি পরিবর্তনশীল-দৈর্ঘ্যের তালিকা হিসেবে রাখে, তাই নির্ভুল কিন্তু পূর্ণসংখ্যার চেয়ে ধীর। PostgreSQL-এ একটি money টাইপও আছে, কিন্তু তার আচরণ সার্ভারের locale-এর ওপর নির্ভর করে, তাই PostgreSQL-এর নিজস্ব wiki-র “Don't Do This” পাতায় এটি এড়াতে বলা হয়। আর SQLite 3.43 (২০২৩) থেকে SUM() ভেতরে Kahan–Babuška–Neumaier compensated summation ব্যবহার করে, ফলে অনেক REAL যোগ করলে ভুল কম জমে — কিন্তু তাতে ০.১-এর মতো সংখ্যার মূল প্রতিনিধিত্বের ভুল দূর হয় না।

পর্যবেক্ষণ ৪ — তারিখ, সময় আর টাইমজোন

বাংলাদেশের সময় UTC-এর চেয়ে ৬ ঘণ্টা এগিয়ে। সার্ভার UTC-তে চললে আর সময়গুলো মিশে গেলে রাত ১২টা থেকে সকাল ৬টার অর্ডার “আগের দিনের” রিপোর্টে চলে যায়। পেশাদার নিয়ম: মুহূর্ত রাখুন UTC-তে, দেখানোর সময় স্থানীয় সময়ে বদলান — PostgreSQL-এ TIMESTAMPTZ, SQLite-এ ISO লেখা '2025-09-01T10:15:00Z' (Z মানে UTC). কিন্তু শুধু তারিখ — জন্মদিন, joined_on, term — টাইমজোনে বদলাবেন না। কারণগুলোর বিস্তারিত ভাবনার প্রশ্ন ৫-এ।

৮ · ডিজাইনের চারটি পরিচিত “দুর্গন্ধ”

সফটওয়্যারে “code smell” মানে এমন লক্ষণ, যা দেখলেই সন্দেহ হয় ভেতরে সমস্যা আছে। ডেটাবেস ডিজাইনেও এমন কিছু পরিচিত anti-pattern আছে — শুরুতে সহজ মনে হয়, পরে চড়া দাম নেয়। চারটি সবচেয়ে প্রচলিত:

products-এ tags = 'phone,electronics,gift' — একটি ঘরে কয়েকটি মান। এটি 1NF ভাঙে (পর্ব ৫-এর first normal form). ফল: প্রতিটি tag-এ FK বসানো যায় না, বানান ভুল আটকায় না, index কাজে লাগে না, আর WHERE tags LIKE '%phone%' লিখলে 'headphone' ট্যাগের পণ্যও চলে আসে। একটি tag সরাতে হলে লেখা কেটে-জোড়া লাগাতে হয়। সমাধান: tags(tag_id, name UNIQUE) আর product_tags(product_id, tag_id, PRIMARY KEY (product_id, tag_id)) — একটি সাধারণ junction table, ঠিক order_items-এর মতো।

EAV (entity–attribute–value) মানে প্রতিটি গুণ আলাদা সারিতে: product_attrs(product_id, attr_name, value TEXT) — ('101', 'ram', '8GB'), ('106', 'size', 'XL'). বিভিন্ন পণ্যের বিভিন্ন গুণ থাকায় লোভনীয়। কিন্তু সব মান লেখা হয়ে যায় (সংখ্যা হিসেবে তুলনা বা যোগ করা কঠিন), “ram NOT NULL” বা “size এই তালিকার একটি” ধরনের কোনো constraint লেখা যায় না, attr_name-এর বানান ভুল ('Ram', 'RAM') আটকায় না, আর একটি পণ্যের পাঁচটি গুণ দেখাতে পাঁচবার JOIN বা pivot লাগে। বিকল্প: সাধারণ গুণ আসল কলামে; পণ্যের ধরনভেদে আলাদা subtype টেবিল (পর্ব ৯-এর specialization — যেমন phones(product_id, ram_gb, …)); আর সত্যিই অনিয়মিত গুণের জন্য একটি JSON কলাম (পর্ব ৩০)।

Excel থেকে আসা ডিজাইনে প্রায়ই একটিই টেবিল: sales(order_id, order_date, customer_name, customer_phone, customer_city, product1, price1, product2, price2, …)। ফল তিন গোলমাল (নাম পর্ব ৩৬-এ): update anomaly (Rahim-এর ফোন বদলালে প্রতিটি অর্ডারের সারিতে বদলাতে হয়), insert anomaly (অর্ডার ছাড়া নতুন গ্রাহক রাখা যায় না) আর delete anomaly (একমাত্র অর্ডার মুছলে গ্রাহকের পরিচয়ও যায়)। আর চতুর্থ পণ্য কোথায় যাবে? সমাধান: db-shop-এর মতো পাঁচটি টেবিলে ভাঙা — প্রতিটি সত্য এক জায়গায়।

একটি comments টেবিল, যেখানে মন্তব্য পণ্যের হতে পারে বা অর্ডারের: comments(comment_id, target_type, target_id, body) — target_type = 'product' হলে target_id মানে product_id, 'order' হলে order_id. কিছু web framework এটিকে “polymorphic association” নামে সহজে বানিয়ে দেয়। সমস্যা: একটি কলাম দুই টেবিলের দিকে আঙুল তুলতে পারে না, তাই REFERENCES লেখাই যায় না — অনাথ মন্তব্য, মুছে যাওয়া পণ্যের মন্তব্য, ভুল target_type — সব চুপচাপ ঢোকে। একে বলে polymorphic association. সমাধান: দুটি আলাদা nullable FK কলাম (product_id REFERENCES products, order_id REFERENCES orders) আর CHECK ((product_id IS NULL) <> (order_id IS NULL)) — ঠিক একটি থাকবে; অথবা আলাদা product_comments আর order_comments টেবিল।

আনুষ্ঠানিক যন্ত্র: normalization

এই পর্বের সিদ্ধান্তগুলো এসেছে অভিজ্ঞতা আর প্রশ্ন থেকে। কিন্তু “কোনো তথ্য কি দুই জায়গায় আছে?” — এর গাণিতিক উত্তরও আছে। পর্ব ৩৫-এ শিখবেন functional dependency (কোন কলাম জানলে কোন কলাম নির্ধারিত হয়), আর পর্ব ৩৬-এ normalization — 1NF, 2NF, 3NF — যা দিয়ে উপরের “এক বিশাল টেবিল” কেন খারাপ আর কীভাবে ভাঙতে হবে, তা প্রমাণসহ দেখানো যায়। পর্ব ৩৭-এ BCNF আর ভাঙার পর তথ্য না হারানোর (lossless) পরীক্ষা, আর পর্ব ৩৮-এ কখন cgpa বা unit_price-এর মতো ইচ্ছাকৃত নকল রাখা যুক্তিসংগত। ER দিয়ে আঁকা ভালো ডিজাইন সাধারণত শুরু থেকেই 3NF-এর কাছাকাছি থাকে; normalization সেটিকে যাচাই করার আনুষ্ঠানিক যন্ত্র।

৯ · AI-এর যুগে ডেটাবেস ডিজাইন

AI কয়েক সেকেন্ডে দশটি সুন্দর CREATE TABLE দেয়, আর সেগুলো চলবেও। কিন্তু AI আপনার দোকানের মালিকের সাথে কথা বলেনি — “কখনো কি…?” প্রশ্নের উত্তর না জেনে গড়পড়তা ধরে নেয়। প্রায়ই যা পাবেন:

AI-এর ভুলফলকীভাবে ধরবেন
order_items-এ দাম নেই, JOIN করে products.price নেয়দাম বদলালে পুরোনো চালান বদলে যায়দাম UPDATE করে পুরোনো অর্ডারের মোট দেখুন
টাকা FLOAT/REAL-এপয়সার ভুল, ভুল তুলনা0.1 + 0.2 = 0.3 চালিয়ে দেখুন
orders-এ একটি payment_method কলামভাগে পরিশোধ বা ফেরত লেখা যায় না“এক অর্ডারে দুটি পেমেন্ট” ঢোকানোর চেষ্টা
সবখানে ON DELETE CASCADEএকটি গ্রাহক মুছলে বিক্রির ইতিহাস উধাওপ্রতিটি FK-র জন্য “parent গেলে child-এর কী?”
enrollments-এর key (student_id, course_id)retake অসম্ভবএকই কোর্স দুই term-এ ঢুকিয়ে দেখুন
tags বা phone_numbers কমা দিয়ে এক কলামে1NF ভাঙে, খোঁজ ভুলপ্রতিটি কলাম: “এখানে কি একাধিক মান আসতে পারে?”
সময় টাইমজোন ছাড়া DATETIMEরাতের অর্ডার ভুল দিনেরাত ২টার একটি অর্ডারের “তারিখ” রিপোর্ট করে দেখুন
নেই এমন কলাম বা ব্যবসার নিয়ম কল্পনা করাডিজাইন আপনার ব্যবসার সাথে মেলে নানিজের নিয়মের তালিকা (S1–S8) পাশে রেখে মিলিয়ে নিন

সবচেয়ে ভালো ব্যবহার: AI-কে ডিজাইনার নয়, প্রশ্নকর্তা আর রিভিউয়ার বানান। আগে নিয়মের তালিকা নিজে লিখুন, তারপর AI-কে দিয়ে ফাঁক খোঁজান:

আমি একটি অনলাইন দোকানের ডেটাবেস ডিজাইন করছি (SQLite দিয়ে শুরু, পরে PostgreSQL). টেবিলগুলো আর আমার ব্যবসার নিয়ম (S1–S8) নিচে দিলাম। তুমি নিজে নতুন ডিজাইন বানিও না। বরং: ১. আমার নিয়মে কোন "কখনো কি…?" প্রশ্ন বাদ পড়েছে, তার ১০টি প্রশ্নের তালিকা দাও (পেমেন্ট, ফেরত, ঠিকানা, কুপন নিয়ে)। ২. প্রতিটি নিয়ম কোন key / FK / NOT NULL / CHECK দিয়ে রক্ষিত, আর কোনটি ডেটাবেস নিজে রক্ষা করতে পারে না — টেবিল করে দেখাও। ৩. প্রতিটি FK-র ON DELETE নিয়ম ঠিক আছে কি না, এক লাইনের ব্যবসায়িক যুক্তিসহ বলো। ৪. টাকা, তারিখ-সময় আর টাইমজোনের টাইপ নিয়ে কোনো ঝুঁকি থাকলে বলো। ৫. প্রতিটি সমস্যার জন্য একটি করে ছোট INSERT/UPDATE দাও, যা চালালে সমস্যাটি নমুনা ডেটায় দেখা যাবে। কোনো কলামের নাম অনুমান করবে না — শুধু আমার দেওয়া schema-র নাম ব্যবহার করবে। [এখানে CREATE TABLE আর নিয়মের তালিকা]

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

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

  • ই-কমার্সের order_items-এ দাম আবার রাখেন কেন? — বিক্রির সময়ের দাম একটি আলাদা ঐতিহাসিক তথ্য; পণ্যের দাম পরে বদলালেও চালান বদলাবে না।
  • শিক্ষার্থী–কোর্স M:N সম্পর্ক কীভাবে টেবিলে আনবেন, retake থাকলে key কী? — junction table; key (student_id, course_id, term).
  • টাকার জন্য কোন ডেটা টাইপ? — DECIMAL/NUMERIC বা ছোট এককে (পয়সা) INTEGER; FLOAT/REAL নয়, কারণ binary floating-point দশমিক ভগ্নাংশ হুবহু রাখে না।
  • Derived attribute (যেমন CGPA, বয়স) সংরক্ষণ করবেন? — সাধারণত না, হিসাব করুন; রাখলে হালনাগাদের ব্যবস্থা (trigger/transaction) আর কারণ লিখে রাখুন।
  • EAV বা polymorphic association-এর সমস্যা কী? — টাইপ, NOT NULL, CHECK আর foreign key লেখা যায় না, তাই ডেটার শুদ্ধতা ডেটাবেস রক্ষা করতে পারে না।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। ডিজাইনের প্রশ্নের একটিমাত্র সঠিক উত্তর কমই থাকে; থাকে যুক্তি আর দাম।

প্র ০১ব্যাংক প্রতিটি অ্যাকাউন্টের ব্যালান্স একটি কলামে রাখে, আবার সব লেনদেনও রাখে — এটিও তো derived value. তাহলে students.cgpa রাখা খারাপ হলে ব্যাংকের ব্যালান্স রাখা ভালো কেন?

প্রশ্নটি দারুণ, কারণ এটি দেখায় “derived value রাখা খারাপ” কোনো চিরন্তন নিয়ম নয় — নির্ভর করে কে, কীভাবে, কতবার হিসাবটি করে আর অমিল হলে কী হয়। ব্যালান্স আর লেনদেনের তালিকা একসাথে রাখার ধারণা পাঁচশো বছরেরও পুরোনো: ১৪৯৪ সালে ইতালীয় গণিতবিদ Luca Pacioli তাঁর বই Summa de arithmetica-য় ভেনিসের ব্যবসায়ীদের double-entry bookkeeping পদ্ধতি বর্ণনা করেন। সেখানে প্রতিটি লেনদেন দুবার লেখা হয় (এক হিসাবে ডেবিট, আরেকটিতে ক্রেডিট), আর নিয়মিত মিলিয়ে দেখা হয় যোগফলগুলো সমান কি না। অর্থাৎ নকল রাখা হয় ইচ্ছে করে, মিলিয়ে দেখার জন্য।

ব্যাংকের ব্যালান্সও একইভাবে নিয়ন্ত্রিত। প্রতিটি টাকা তোলা বা জমার সময় একই transaction-এ লেনদেনের সারি যোগ হয় আর ব্যালান্স বদলায় — হয় দুটোই হবে, নয়তো একটিও নয় (পর্ব ৪৪-এর atomicity). দিনশেষে বা নিয়মিত ব্যবধানে ব্যাচ-কাজ লেনদেন যোগ করে ব্যালান্সের সাথে মিলিয়ে দেখে (reconciliation). আর ব্যালান্স রাখার কারণ বাস্তব: প্রতিটি ATM বা bKash লেনদেনে “টাকা আছে কি?” প্রশ্নের উত্তর মিলিসেকেন্ডে চাই; বছরের পর বছরের লাখো লেনদেন যোগ করে প্রতিবার সেটি বের করা অসম্ভব।

db-uni-র cgpa-র কোনো হালনাগাদের নিয়ম নেই — trigger নেই, transaction-নিয়ম নেই, মিলিয়ে দেখা নেই; অংশ ৩-এ সাতজনের একজনেরও মেলেনি। আর হিসাবটি সস্তা: একজনের বছরে কয়েক ডজন grade. তাই সিদ্ধান্তের চার প্রশ্ন: হিসাব কতটা ব্যয়বহুল? সংরক্ষিত মান কে হালনাগাদ করে — ডেটাবেস নিজে, নাকি “কেউ মনে করে”? অমিল ধরার পরীক্ষা আছে? অমিল হলে কোনটি সত্যের উৎস? ব্যাংকে চারটির উত্তরই স্পষ্ট; db-uni-তে একটিরও নয়।

মূল উপলব্ধি: derived value রাখা মানে একটি প্রতিশ্রুতি দেওয়া — “এটি সবসময় মূল ডেটার সাথে মিলবে।” প্রতিশ্রুতি রক্ষার ব্যবস্থা (transaction, trigger, reconciliation) না থাকলে সেটি রাখবেন না; থাকলে লিখে রাখুন কোনটি সত্যের উৎস।

প্র ০২order_items.unit_price-এ পণ্যের দাম কপি করা কি normalization-এর নিয়ম ভাঙে? আর যদি পণ্যের দামের পুরো ইতিহাস লাগে — কবে কত ছিল — তাহলে কী করবেন?

Normalization-এর লক্ষ্য একই তথ্য দুই জায়গায় না রাখা, একই সংখ্যা নয়। প্রশ্ন হলো কোন কলাম কীসের ওপর নির্ভর করে। products.price নির্ভর করে product_id-এর ওপর আর সময়ের ওপর — এটি “এই মুহূর্তের তালিকা-দাম”। unit_price নির্ভর করে (order_id, product_id)-এর ওপর — “এই অর্ডারে এই পণ্য কত দামে বিক্রি হয়েছিল”। পর্ব ৩৫-এর ভাষায়, order_items-এ product_id → unit_price এই functional dependency সত্য নয়: একই পণ্য 101 অর্ডার 1001-এ 15000 আর 1009-এ 14500 টাকায় বিক্রি হয়েছে। যেখানে FD-ই নেই, সেখানে সেই FD-ভিত্তিক redundancy-ও নেই। তাই এটি 3NF ভাঙে না; বরং দাম কপি না করলে একটি সত্য (বিক্রির দাম) ডেটাবেস থেকে হারিয়ে যায়।

দামের ইতিহাস চাইলে দুটি প্রচলিত পথ। প্রথমটি নিজে বানানো: price_history(product_id, price, valid_from, valid_to), যেখানে প্রতিটি দাম-বদলে আগের সারির valid_to বসে আর নতুন সারি যোগ হয়; “২০২৫-০৯-০১ তারিখে দাম কত ছিল” প্রশ্নের উত্তর তারিখের পরিসর দিয়ে খোঁজা যায়। ডেটা ওয়্যারহাউসে একই ধারণার নাম slowly changing dimension (Type 2), যা পর্ব ৫৫-এ আসবে।

দ্বিতীয়টি DBMS-এর নিজস্ব সুবিধা: SQL:2011 standard-এর system-versioned temporal table, যেখানে DBMS নিজেই সারির পুরোনো সংস্করণ রাখে আর FOR SYSTEM_TIME AS OF … দিয়ে অতীত দেখা যায় — SQL Server 2016 আর MariaDB 10.3 থেকে আছে। তবু unit_price কপি রাখুন: বিক্রির দাম সবসময় তালিকা-দাম নয় — অর্ডার 1007-এর Headphone 1150-এ বিক্রি, তালিকা-দাম 1200.

মূল উপলব্ধি: একই সংখ্যা দুই জায়গায় থাকা মানেই redundancy নয়; দেখতে হবে দুটি কলাম একই প্রশ্নের উত্তর কি না। “আজকের দাম” আর “সেদিনের বিক্রির দাম” দুটি আলাদা প্রশ্ন।

প্র ০৩কমা-দেওয়া তালিকা, EAV আর polymorphic association এত পরিচিত ভুল হলে জনপ্রিয় framework আর বাস্তব প্রজেক্টে এগুলো এত দেখা যায় কেন?

কারণ প্রতিটি ভুল প্রথম দিনে একটি বাস্তব সমস্যা সহজে সমাধান করে, আর দাম আসে অনেক পরে, অন্য কারও ঘাড়ে। ২০১০ সালে Bill Karwin-এর বই SQL Antipatterns এই ভুলগুলোর নাম দিয়ে তালিকা করে: কমা-দেওয়া তালিকার নাম দেন “Jaywalking” (রাস্তা পার হতে নিয়ম ভেঙে শর্টকাট), আর আলাদা অধ্যায় আছে Entity-Attribute-Value, Polymorphic Associations, এমনকি টাকার জন্য FLOAT ব্যবহারের (“Rounding Errors”). বইটির মূল যুক্তি: প্রতিটি anti-pattern একটি বৈধ লক্ষ্য থেকে আসে, শুধু ভুল উপায়ে।

কমা-দেওয়া তালিকা আসে কারণ নতুন টেবিল আর JOIN-এর চেয়ে একটি TEXT কলাম দ্রুত। EAV আসে বৈধ প্রয়োজন থেকে — বিভিন্ন পণ্যের বিভিন্ন গুণ, আর প্রতিবার schema বদলানোর ভয়; চিকিৎসা-তথ্যের মতো ক্ষেত্রে, যেখানে হাজারো সম্ভাব্য পরীক্ষার অল্প কয়েকটিই একজন রোগীর থাকে, এমন নকশা ভেবেচিন্তেই ব্যবহার হয়েছে। Polymorphic association-কে কিছু web framework (যেমন Ruby on Rails) একটি লাইনেই বানিয়ে দেয় — “মন্তব্য যেকোনো কিছুর হতে পারে” লেখার সহজ উপায়।

তিনটিরই সাধারণ দাম: টাইপ, FK, CHECK নেই — শুদ্ধতার সব দায় অ্যাপের কোডে, যা সবচেয়ে দুর্বল পাহারাদার (পর্ব ৬)। আধুনিক বিকল্পও আছে: PostgreSQL 9.4 (২০১৪) থেকে jsonb টাইপ আর তার ওপর index, MySQL 5.7 থেকে JSON — অনিয়মিত গুণের জন্য EAV-এর চেয়ে প্রায়ই ভালো (পর্ব ৩০)। তবু যে গুণের ওপর নিয়ম আছে (দাম, স্টক), তা আসল কলামেই রাখুন।

মূল উপলব্ধি: anti-pattern-কে চেনার প্রশ্ন একটিই — “এই নকশায় ডেটাবেস কি আমার নিয়মগুলো নিজে রক্ষা করতে পারবে?” না পারলে শর্টকাটের দাম পরে, সুদসহ দিতে হবে।

প্র ০৪floating-point-এর ছোট ভুল তো ১০−১৬-এর মতো — বাস্তবে এতে কি সত্যিই কোনো বড় ক্ষতি হয়েছে? টাকার হিসাবে INTEGER পয়সা আর DECIMAL-এর মধ্যে কোনটি বাছবেন?

একটি যোগে ভুল নগণ্য, কিন্তু ভুল জমে, আর তুলনা (=) একটি ভুলেই ভেঙে যায়। ইতিহাসে দুটি পরিচিত ঘটনা আছে — দুটিই ঠিক টাকার নয়, কিন্তু একই মূল সমস্যার। ১৯৮২ সালে কানাডার Vancouver Stock Exchange একটি নতুন সূচক চালু করে, মান ১০০০ থেকে। প্রতিটি লেনদেনের পর সূচক হিসাব করে তিন দশমিক ঘরে কেটে (round না করে) রাখা হতো। প্রায় দুই বছরে দিনে হাজারো হিসাবের ছোট ছোট কাটা জমে সূচক প্রায় ৫২০-এ নেমে আসে, অথচ বাজার আসলে বেড়েছিল; ১৯৮৩-র শেষে ঠিকভাবে আবার হিসাব করলে মান দাঁড়ায় প্রায় ১০৯৮। দ্বিতীয় ঘটনা ১৯৯১ সালের উপসাগরীয় যুদ্ধে একটি Patriot ক্ষেপণাস্ত্র-প্রতিরক্ষা ব্যবস্থার: সময় গোনা হতো এক সেকেন্ডের দশমাংশে, আর ০.১ binary-তে হুবহু রাখা যায় না বলে টানা প্রায় ১০০ ঘণ্টা চলার পর ঘড়ি প্রায় এক-তৃতীয়াংশ সেকেন্ড পিছিয়ে যায় — যুক্তরাষ্ট্রের GAO-র পরের প্রতিবেদন এই ভুলকে একটি ব্যর্থ প্রতিরোধের কারণ হিসেবে চিহ্নিত করে।

টাকায় ঝুঁকি: পয়সা হারানো (অংশ ৭), “বাকি = 0” খোঁজায় সারি বাদ পড়া, আর মোট হিসাব খাতার সাথে না মেলা — যা নিজেই অডিট আর আস্থার খরচ।

বাছাই: INTEGER পয়সা সবচেয়ে দ্রুত, সব DBMS-এ একই আচরণ, SQLite-এ একমাত্র নিখুঁত পথ — কলামের নামে _paisa লিখে একক স্পষ্ট রাখুন। DECIMAL/NUMERIC পড়তে স্বাভাবিক (1234.50), কিন্তু ধীর, আর অ্যাপে আনার সময় float-এ বদলে ফেলার ঝুঁকি (Java-তে BigDecimal, Python-এ decimal লাগে)। একাধিক মুদ্রা থাকলে মুদ্রার কোডও আলাদা কলামে রাখুন।

মূল উপলব্ধি: floating-point মাপের জন্য, গণনার জন্য নয়। টাকা গোনা হয় — তাই হয় সবচেয়ে ছোট এককে পূর্ণসংখ্যা, নয়তো হুবহু দশমিক।

প্র ০৫বাংলাদেশের সব ব্যবহারকারী একই টাইমজোনে — তাহলে ডেটাবেসে সময় UTC-তে রাখার এত ঝামেলা কেন? স্থানীয় সময় রাখলে কী সমস্যা?

“সবাই একই টাইমজোনে” কথাটি আজ সত্য, কিন্তু ডেটাবেস বহু বছর বাঁচে, আর তিনটি জিনিস বদলায়: সার্ভারের অবস্থান, ব্যবসার পরিধি, আর খোদ টাইমজোনের নিয়ম। শেষটির একটি দেশীয় উদাহরণ আছে: ২০০৯ সালে বিদ্যুৎ সাশ্রয়ের জন্য বাংলাদেশে কয়েক মাস ঘড়ি এক ঘণ্টা এগিয়ে রাখা হয়েছিল (daylight saving), তারপর বছর শেষে আবার আগের সময়ে ফেরানো হয়। সেই রাতে স্থানীয় ঘড়িতে একটি ঘণ্টা দুবার এসেছিল — স্থানীয় সময়ে রাখা দুটি লেনদেনের কোনটি আগে, শুধু ঘড়ির মান দেখে বলা যায় না। UTC-তে এমন কিছু ঘটে না, কারণ UTC কখনো এগোয়-পেছায় না।

দ্বিতীয় সমস্যা সার্ভার: cloud সার্ভার প্রায়ই বিদেশে থাকে আর ডিফল্টে UTC-তে চলে। অ্যাপের এক অংশ স্থানীয় সময় লেখে, আরেক অংশ সার্ভারের now() — একই টেবিলে দুই টাইমজোন মিশে যায়, আর পরে আলাদা করার উপায় থাকে না।

তাই মুহূর্ত রাখুন UTC-তে; দেখানো আর “কোন দিনের বিক্রি” হিসাব করুন Asia/Dhaka ধরে। PostgreSQL-এর TIMESTAMPTZ ভেতরে সবকিছু UTC-তে রাখে আর সংযোগের টাইমজোন অনুযায়ী দেখায়। MySQL-এর TIMESTAMP-ও UTC-তে জমা হয়, কিন্তু এর সীমা ২০৩৮ সালের ১৯ জানুয়ারি (৩২-বিট সেকেন্ড-গণনার শেষ) — দীর্ঘমেয়াদি তারিখ, যেমন ৩০ বছরের ঋণের শেষ কিস্তি, সেখানে রাখা যায় না; তখন DATETIME আর UTC রাখার নিজস্ব নিয়ম লাগে।

আর উল্টো ভুলটিও এড়ান: যা মুহূর্ত নয় — জন্মতারিখ, পরীক্ষার তারিখ, term — তাকে টাইমজোনে বদলাবেন না। একজন শিক্ষার্থী বিদেশ থেকে লগইন করলে তাঁর জন্মদিন একদিন সরে যাওয়া উচিত নয়।

মূল উপলব্ধি: “কখন ঘটেছিল” (মুহূর্ত) আর “কোন তারিখে” (ক্যালেন্ডারের দিন) দুটি আলাদা ধরনের তথ্য। প্রথমটি UTC-তে, দ্বিতীয়টি টাইমজোন ছাড়া — আর দেখানোর সময়ের রূপান্তর অ্যাপের দায়িত্ব।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে হিসাব, একটি নতুন ডিজাইন আর একটি রিভিউ।

  1. কোয়েরি লিখুন: দোকানের মালিক জানতে চান, প্রতিজন গ্রাহক delivered অবস্থার অর্ডারে মোট কত টাকা খরচ করেছেন আর কয়টি delivered অর্ডার — বেশি খরচ আগে। db-shop-এ (customers, orders, order_items) কোয়েরিটি লিখুন আর অংশ ৬-এর প্রথম এডিটরে চালান। কতজন গ্রাহক আসবেন?
    SELECT c.customer_id, c.name,
           COUNT(DISTINCT o.order_id) AS delivered_orders,
           SUM(oi.quantity * oi.unit_price) AS spent
    FROM customers c
    JOIN orders o ON o.customer_id = c.customer_id AND o.status = 'delivered'
    JOIN order_items oi ON oi.order_id = o.order_id
    GROUP BY c.customer_id, c.name
    ORDER BY spent DESC;

    চারজন: Rahim Uddin — ১টি অর্ডার, 17400; Nusrat Jahan — ১টি, 5050; Karima Begum — ২টি, 5000; Imran Hossain — ১টি, 1040. খেয়াল করুন COUNT(DISTINCT o.order_id): JOIN-এর পর প্রতিটি অর্ডার তার লাইনের সংখ্যা যতবার ততবার আসে (Karima-র দুটি অর্ডারে চারটি লাইন), তাই শুধু COUNT(*) লিখলে ৪ দেখাত — AI-এর লেখা কোয়েরিতে এটি খুব প্রচলিত ভুল। দাম নেওয়া হয়েছে unit_price থেকে, products.price থেকে নয়।

  2. ফল আগে বলুন (trace): একটি টেবিল reviews(customer_id, product_id, rating), যেখানে PRIMARY KEY (customer_id, product_id) আর CHECK (rating BETWEEN 1 AND 5); শুরুতে খালি। ক্রমানুসারে চালানো হলো: (ক) INSERT (5, 104, 4) (খ) INSERT (5, 105, 6) (গ) INSERT (6, 104, 5) (ঘ) INSERT (5, 104, 2) (ঙ) UPDATE reviews SET rating = 2 WHERE customer_id = 5 AND product_id = 104. কোনগুলো ব্যর্থ, কেন, আর শেষে টেবিলে কী থাকে? Product 104-এর গড় রেটিং কত?

    (ক) সফল। (খ) ব্যর্থ — rating 6, CHECK ভাঙে। (গ) সফল — একই পণ্য, অন্য গ্রাহক, জোড়াটি নতুন। (ঘ) ব্যর্থ — (5, 104) জোড়া আগেই আছে, primary key ভাঙে। (ঙ) সফল, ১টি সারি বদলায়: গ্রাহক ৫ তাঁর মত বদলালেন, নতুন সারি নয়।

    শেষে দুটি সারি: (5, 104, 2) আর (6, 104, 5). Product 104-এর গড় (২ + ৫) ÷ ২ = ৩.৫। শিক্ষা: “একটিই রিভিউ” নিয়মটি key দিয়ে রক্ষা হয়, আর মত বদলানোর পথ UPDATE.

  3. ডিজাইন করুন: একটি বিশ্ববিদ্যালয় লাইব্রেরি। নিয়ম: একই বইয়ের (একই ISBN) একাধিক কপি থাকে, প্রতিটি কপির নিজস্ব accession নম্বর; সদস্য (শিক্ষার্থী বা শিক্ষক) কপি ধার নেন, ফেরতের শেষ তারিখ থাকে; একটি কপি একসাথে একজনের কাছেই থাকতে পারে; দেরিতে ফেরতে জরিমানা। টেবিল, key, FK, ON DELETE আর গুরুত্বপূর্ণ constraint লিখুন। কোন নিয়মটি সবচেয়ে কঠিন?
    • titles(isbn PRIMARY KEY, title NOT NULL, author) আর copies(accession_no PRIMARY KEY, isbn NOT NULL REFERENCES titles) — “বই” আর “কপি” আলাদা entity; এটিই মূল অন্তর্দৃষ্টি।
    • members(member_id PRIMARY KEY, name NOT NULL, kind CHECK (kind IN ('student', 'teacher')))
    • loans(loan_id PRIMARY KEY, accession_no NOT NULL REFERENCES copies, member_id NOT NULL REFERENCES members, borrowed_on, due_on, returned_on NULL, CHECK (due_on >= borrowed_on)) — returned_on NULL মানে বই এখনো বাইরে; ইতিহাস থাকে, তাই CASCADE নয়।
    • সবচেয়ে কঠিন নিয়ম “একটি কপি একসাথে একজনের কাছেই”: CREATE UNIQUE INDEX one_open_loan ON loans(accession_no) WHERE returned_on IS NULL; — অংশ ৫.২-এর partial index.
    • জরিমানা derived — হিসাব করুন; আদায় হলে fine_payments-এ পয়সায় INTEGER.
  4. রিভিউ করুন: একজন নবীন সহকর্মী এই টেবিলটি দিয়েছেন। অন্তত পাঁচটি সমস্যা খুঁজুন আর প্রতিটির সমাধান বলুন: CREATE TABLE sales (id INTEGER PRIMARY KEY, customer_name TEXT, customer_phone TEXT, products TEXT, total REAL, paid_by TEXT, note_target TEXT, note_target_id INTEGER, created TEXT) — যেখানে products-এ লেখা হয় 'Rice 5kg x2, Mustard Oil 1L x1', paid_by-তে 'bkash', আর created-এ '05/10/2025 9:30 PM'.
    • এক বিশাল টেবিল: গ্রাহকের নাম-ফোন প্রতিটি বিক্রিতে — update anomaly. → customers টেবিল আর customer_id FK.
    • কমা-দেওয়া তালিকা: products একটি লেখা — পণ্যভিত্তিক বিক্রি গোনা অসম্ভব, দাম নেই। → sale_items(sale_id, product_id, quantity, unit_price).
    • টাকা REAL-এ, আর total derived (লাইনগুলো থেকে হিসাব করা যায়)। → পয়সায় INTEGER; total রাখলে কেন রাখছেন লিখে রাখুন।
    • একটি paid_by: ভাগে পেমেন্ট বা ফেরত লেখা যায় না, বানানের কোনো CHECK নেই। → payments টেবিল, CHECK (method IN (…))।
    • Polymorphic FK: note_target / note_target_id — REFERENCES লেখা যায় না। → আলাদা nullable FK কলাম আর “ঠিক একটি” CHECK, বা আলাদা নোট-টেবিল।
    • সময়ের ফরম্যাট: '05/10/2025' — ৫ অক্টোবর না ১০ মে? লেখা হিসেবে সাজালে ক্রমও ভুল, টাইমজোনও নেই। → ISO 8601 UTC '2025-10-05T15:30:00Z'.
    • আর কোথাও NOT NULL নেই — নামহীন, দামহীন বিক্রিও ঢুকবে।

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

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

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

  1. ব্যবহারকারীদের সাক্ষাৎকার — রিপোর্ট আর “কখনো কি…?” প্রশ্ন
  2. বিশেষ্য-ক্রিয়া কৌশলে entity, attribute আর relationship খোঁজা
  3. নম্বর দেওয়া ব্যবসার নিয়মের তালিকা লেখা
  4. ER ডায়াগ্রাম এঁকে টেবিলে রূপান্তর — key, FK, NOT NULL, CHECK
  5. নমুনা ডেটা ঢুকিয়ে ব্যবসার প্রশ্নগুলোর কোয়েরি চালানো
  6. রিভিউ চেকলিস্ট আর normalization দিয়ে চূড়ান্ত যাচাই

একটি বিশ্ববিদ্যালয়ে শিক্ষার্থী F পেলে পরের term-এ একই কোর্স আবার নিতে পারেন, কিন্তু এক term-এ একই কোর্সে দুবার ভর্তি হতে পারেন না। টেবিল enrollments(student_id, course_id, term, grade)-এর সঠিক primary key কোনটি?

  • (student_id)
  • (student_id, course_id)
  • (student_id, course_id, term)
  • (student_id, course_id, term, grade)

একটি অনলাইন দোকানের order_items টেবিলে products টেবিলে দাম থাকা সত্ত্বেও unit_price কলাম রাখা হয়। এর প্রধান কারণ কোনটি?

  • JOIN এড়িয়ে কোয়েরি দ্রুত করা
  • পণ্যের দাম পরে বদলালেও বিক্রির সময়ের দাম যাতে অপরিবর্তিত থাকে
  • products টেবিলের দিকে foreign key বানাতে
  • normalization-এর নিয়ম মানতে

SQLite-এ SELECT 0.1 + 0.2 = 0.3; চালালে ফল 0 আসে। টাকার পরিমাণ রাখার জন্য তাহলে কোনটি সবচেয়ে নিরাপদ?

  • REAL কলামে টাকা
  • TEXT কলামে টাকা, যেমন '1234.50'
  • DOUBLE কলামে টাকা
  • INTEGER কলামে পয়সা (১ টাকা = 100)

একটি products টেবিলের tags কলামে লেখা থাকে 'phone,electronics,gift'. এই নকশার সবচেয়ে মৌলিক সমস্যা কোনটি?

  • একটি ঘরে একাধিক মান — 1NF ভাঙে, তাই FK, index আর সঠিক খোঁজ অসম্ভব
  • লেখাটি বেশি জায়গা নেয়
  • SQL-এ কমা-সহ লেখা রাখা যায় না
  • tags কলামে NULL রাখা যায় না

নিয়ম: একজন গ্রাহক একটি পণ্যে সর্বোচ্চ একটি রিভিউ দিতে পারেন, পরে বদলাতে পারেন। টেবিল reviews(customer_id, product_id, rating, body)-এ নিয়মটি ডেটাবেসে রক্ষা করার সঠিক উপায় কোনটি?

  • অ্যাপে INSERT-এর আগে SELECT করে দেখা
  • (customer_id, product_id)-কে primary key বা UNIQUE করা
  • customer_id-এ UNIQUE
  • CHECK (customer_id <> product_id)

একটি comments টেবিলে মন্তব্য পণ্যের বা অর্ডারের হতে পারে, তাই নকশা: comments(comment_id, target_type, target_id, body), যেখানে target_type 'product' বা 'order'. এই নকশার প্রধান দুর্বলতা কোনটি?

  • এতে primary key দেওয়া যায় না
  • target_type-এ CHECK লেখা যায় না
  • target_id-এ foreign key দেওয়া যায় না, তাই অনাথ মন্তব্য ঢুকতে পারে
  • একটি পণ্যে একটির বেশি মন্তব্য রাখা যায় না

শব্দকোষ

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

মনে রাখুন

  1. ডিজাইন শুরু প্রশ্ন দিয়ে: রিপোর্ট, “কখনো কি…?”, ইতিহাস; বিশেষ্য → entity, ক্রিয়া → relationship; ফল হলো নম্বর দেওয়া ব্যবসার নিয়মের তালিকা।
  2. প্রতিটি key আর constraint একটি নিয়ম থেকে আসে: retake আছে বলে enrollments-এর key-তে term; ফল আসেনি বলে grade NULL.
  3. যা বদলায়, তার লেনদেনের সময়কার মান কপি করুন (unit_price, ঠিকানা, ছাড়); derived value (cgpa) রাখলে হালনাগাদের ব্যবস্থা আর কারণ লিখে রাখুন।
  4. নতুন চাহিদায় আবার প্রশ্ন: রিভিউ → composite PK, একাধিক ঠিকানা → 1:N + partial index, পেমেন্ট → 1:N ledger আর refund_of, কুপন → ছাড়ের কপি।
  5. যাচাই করুন নমুনা ডেটায় আর চেকলিস্টে: FK index, composite key-তে NOT NULL, টাকা পয়সায় INTEGER, সময় UTC; কমা-তালিকা, EAV, বিশাল টেবিল আর polymorphic FK এড়িয়ে চলুন।

পরের পর্বে: মডিউল ২ শেষ — আপনি এখন একটি ডেটাবেস ডিজাইন করতে আর রিভিউ করতে পারেন। মডিউল ৩-এ সেই ডিজাইন হাতে-কলমে SQL-এ: পর্ব ১২-তে SQL-এর পরিচয়, আর নিজের কম্পিউটারে SQLite, MySQL ও PostgreSQL বসানো।

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

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