অধ্যায় 2 · ডিজাইন ও ডেটার শুদ্ধতা
ডেটাবেস ডিজাইন: কি, ER ডায়াগ্রাম আর নরমালাইজেশন
- পৃষ্ঠা 5 / 22
- 15 মিনিট পড়া
এখন পর্যন্ত আপনার লেখা প্রতিটা কোয়েরি এমন একটা স্কিমার ওপর ভরসা করেছে, যা কেউ যত্ন করে ডিজাইন করেছেন: প্রত্যেক কাস্টমারের জন্য একটা সারি, দাম শুধু এক জায়গায়, অর্ডার লাইনের জন্য একটা জাংশন টেবিল। এই পাতার বিষয় হলো সেই ডিজাইন কীভাবে করা হয়। খারাপ ডিজাইন কোনো এরর মেসেজ দেখায় না। তার ফল ধরা পড়ে কয়েক মাস পরে — একই কাস্টমারের দুটো শহর, রেভিনিউ দুবার গুনে ফেলা রিপোর্ট, কিংবা এমন ট্রেনিং ডেটাসেট, যার লেবেলগুলো নিঃশব্দে একটা আরেকটার সাথে মেলে না।
ভালো ডিজাইনের হাতিয়ার হলো নরমালাইজেশন (normalization): প্রতিটা তথ্য ঠিক একবার রাখা, যাতে একই তথ্যের দুটো কপি কখনো দুরকম কথা বলতে না পারে। আপনি একটা এলোমেলো, স্প্রেডশিট-ধাঁচের এক্সপোর্ট নেবেন, SQL দিয়ে তার সমস্যাগুলো খুঁজবেন, আর ধাপে ধাপে নরমালাইজ করবেন — তারপর দেখবেন কখন অ্যানালিটিক্স আর ML ইচ্ছে করেই আবার ডিনরমালাইজ করে।
যা শিখবেন
- চাহিদা থেকে এনটিটি, অ্যাট্রিবিউট আর সম্পর্ক বের করা, আর সেগুলোকে টেক্সট ER ডায়াগ্রামে আঁকা
- ক্যান্ডিডেট, কম্পোজিট, ন্যাচারাল আর সারোগেট কি, আর রেফারেনশিয়াল ইন্টেগ্রিটি
- ফাংশনাল ডিপেনডেন্সি, আর SQL দিয়ে আসল ডেটায় সেটা যাচাই করা
- একটা এলোমেলো টেবিলে ধাপে ধাপে 1NF, 2NF, 3NF আর BCNF
- জাংশন টেবিল, আর অ্যানালিটিক্সের জন্য কখন ডিনরমালাইজ করবেন
চাহিদা থেকে এনটিটি
ডিজাইন শুরু হয় বাক্য দিয়ে, টেবিল দিয়ে নয়। ধরুন শপের চাহিদায় লেখা আছে:
কাস্টমাররা অর্ডার দেয়। একটা অর্ডারের একটা তারিখ আর একটা স্ট্যাটাস থাকে, আর তাতে এক বা একাধিক প্রোডাক্ট থাকে — প্রতিটার পরিমাণ আর আসলে নেওয়া দামসহ। একটা প্রোডাক্ট বড়জোর একটা ক্যাটাগরিতে থাকে। একটা অর্ডারের টাকা কয়েকটা পেমেন্টে শোধ করা যায়।
শুরুতে কাজে লাগে এমন একটা সহজ নিয়ম: যে বিশেষ্যগুলোর নিজস্ব পরিচয় আছে, সেগুলো হয় এনটিটি (entity) (কাস্টমার, অর্ডার, প্রোডাক্ট, ক্যাটাগরি, পেমেন্ট); যে বিশেষ্য শুধু কিছু একটার বর্ণনা দেয়, সেগুলো অ্যাট্রিবিউট (তারিখ, স্ট্যাটাস, পরিমাণ); আর ক্রিয়াগুলো হয় সম্পর্ক (relationship) (অর্ডার দেয়, ধারণ করে, অন্তর্ভুক্ত থাকে, পরিশোধ করে)। প্রতিটা সম্পর্কের জন্য জিজ্ঞেস করুন "প্রতিটা পাশে কয়টা করে?" — এটাই ঠিক করে ফরেন কি কোথায় বসবে। "ধারণ করে" হলো মেনি-টু-মেনি (একটা অর্ডারে অনেক প্রোডাক্ট, একটা প্রোডাক্ট অনেক অর্ডারে), আর "পরিমাণ" ও "নেওয়া দাম" কোনো একটা পাশের একার নয়: এরা জোড়াটার বর্ণনা দেয়। এজন্যই order_items আছে।
টেক্সটে একটা ER ডায়াগ্রাম
এনটিটি-রিলেশনশিপ (ER) ডায়াগ্রাম দেখায় এনটিটিগুলো কী কী আর কীভাবে যুক্ত। আঁকার টুল লাগে না; এই ক্রো'স-ফুট নোটেশনই যথেষ্ট (এটা Mermaid-এর erDiagram-এর সিনট্যাক্স, তাই GitHub README-তেও ছবি হয়ে দেখায়):
CUSTOMERS ||--o{ ORDERS : places এক কাস্টমার, শূন্য বা বেশি অর্ডার
ORDERS ||--|{ ORDER_ITEMS : contains এক অর্ডার, এক বা বেশি লাইন
PRODUCTS ||--o{ ORDER_ITEMS : "sold as" এক প্রোডাক্ট, শূন্য বা বেশি লাইন
CATEGORIES |o--o{ PRODUCTS : groups প্রোডাক্টের শূন্য বা একটা ক্যাটাগরি
ORDERS ||--o{ PAYMENTS : "paid by" এক অর্ডার, শূন্য বা বেশি পেমেন্ট
EMPLOYEES |o--o{ EMPLOYEES : manages নিজের সাথেই সম্পর্ক
|| ঠিক একটা |o শূন্য বা একটা |{ এক বা বেশি o{ শূন্য বা বেশিপ্রতিটা টেবিলের পাশের চিহ্ন পড়লেই ব্যবসার নিয়মগুলো বোঝা যায়: একটা অর্ডারের ঠিক একজন কাস্টমার থাকতেই হবে (customer_id NOT NULL), কিন্তু প্রোডাক্টের ক্যাটাগরি না-ও থাকতে পারে (category_id NULL হতে পারে — প্রোডাক্ট ১১, Gift Card)।
কি (key)
| ধরন | মানে | শপে |
|---|---|---|
| ক্যান্ডিডেট কি | যেকোনো ন্যূনতম কলাম-সেট, যা প্রতিটা সারিকে আলাদা করে চিনিয়ে দেয় | customer_id আর email দুটোই ক্যান্ডিডেট |
| প্রাইমারি কি | যে ক্যান্ডিডেটকে মূল পরিচয় হিসেবে বেছে নেন | customer_id |
| কম্পোজিট কি | কয়েকটা কলাম মিলে একটা কি | order_items-এ (order_id, product_id) |
| ন্যাচারাল কি | বাস্তব জগতে যার একটা অর্থ আছে | email, জাতীয় পরিচয়পত্র নম্বর, ISBN |
| সারোগেট কি | অর্থহীন, তৈরি করা একটা সংখ্যা | customer_id, order_id |
ন্যাচারাল কি বদলায় (মানুষ ইমেইল বদলায়), আর ফরেন কি হিসেবে ব্যবহার করলে অন্য টেবিলেও ছড়িয়ে পড়ে। তাই বেশিরভাগ সিস্টেম একটা সারোগেট প্রাইমারি কি রাখে, সাথে ন্যাচারাল কি-তে একটা UNIQUE কনস্ট্রেইন্ট — customers ঠিক এটাই করে। জাংশন টেবিল এর ব্যতিক্রম: এদের নিজস্ব কম্পোজিট কি (order_id, product_id)-ই আদর্শ, কারণ এটা একই অর্ডারে একই প্রোডাক্ট দুবার থাকাও আটকায়।
রেফারেনশিয়াল ইন্টেগ্রিটি
ফরেন কি প্রতিশ্রুতি দেয় যে প্রতিটা রেফারেন্স এমন একটা সারিকে নির্দেশ করে, যেটা সত্যিই আছে। ফরেন কি চালু থাকলে ডেটাবেস নিজেই সেই প্রতিশ্রুতি রক্ষা করে:
INSERT INTO orders (order_id, customer_id, order_date, status)
VALUES (99, 42, '2026-07-01', 'pending');Error: FOREIGN KEY constraint failedকাস্টমার ৪২ নেই, তাই ইনসার্টটা বাতিল হলো। কনস্ট্রেইন্ট না থাকলে একটা "এতিম" অর্ডার তৈরি হতো, যা প্রতিটা INNER JOIN রিপোর্ট থেকে নিঃশব্দে উধাও হয়ে যেত। যে সারির দিকে অন্য সারি নির্দেশ করছে, সেটাই মুছে ফেললে কী হয় (আটকানো, ক্যাসকেড, NULL করা), তা আছে ট্রানজ্যাকশন, ACID, আইসোলেশন আর লকিং পাতায়।
একটা এলোমেলো টেবিল
স্প্রেডশিট এক্সপোর্ট হিসেবে এ ধরনের টেবিলই আসে: প্রতিটা অর্ডার লাইন, তার সব খুঁটিনাটি কপি করে বসানো। শপের ডেটা থেকে কাস্টমার ১ আর ২-এর জন্য এমন একটা টেবিল বানানো যাক:
CREATE TABLE order_sheet AS
SELECT o.order_id, o.order_date,
c.email AS customer_email, c.name AS customer_name, c.city AS customer_city,
p.name AS product, cat.name AS category, p.price AS list_price,
oi.quantity, oi.unit_price
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN products AS p ON p.product_id = oi.product_id
JOIN categories AS cat ON cat.category_id = p.category_id
WHERE o.customer_id IN (1, 2);
SELECT order_id, order_date, customer_name, customer_city, product, category, quantity
FROM order_sheet
ORDER BY order_id, product;+----------+------------+---------------+---------------+-------------------------+-------------+----------+
| order_id | order_date | customer_name | customer_city | product | category | quantity |
+----------+------------+---------------+---------------+-------------------------+-------------+----------+
| 1 | 2026-01-12 | Nadia Rahman | Dhaka | Python Crash Course | Books | 1 |
| 1 | 2026-01-12 | Nadia Rahman | Dhaka | Wireless Mouse | Electronics | 1 |
| 2 | 2026-01-20 | Tanvir Ahmed | Chattogram | Mechanical Keyboard | Electronics | 1 |
| 3 | 2026-02-03 | Nadia Rahman | Dhaka | SQL Masterclass | Courses | 1 |
| 6 | 2026-03-08 | Tanvir Ahmed | Chattogram | AI Engineering Bootcamp | Courses | 1 |
| 10 | 2026-04-19 | Nadia Rahman | Dhaka | 27-inch Monitor | Electronics | 1 |
| 12 | 2026-05-21 | Tanvir Ahmed | Chattogram | Laptop Stand | Accessories | 2 |
+----------+------------+---------------+---------------+-------------------------+-------------+----------+পড়তে সহজ, pandas-এ লোড করতেও সহজ। কিন্তু এটা একটা ফাঁদও।
তিনটা অসংগতি (anomaly)
- আপডেট অসংগতি: Tanvir-এর শহর তিনবার রাখা আছে। একটা কপি বদলালেই বাকিগুলোর সাথে আর মেলে না।
- ইনসার্ট অসংগতি: কেউ অর্ডার না করা পর্যন্ত নতুন প্রোডাক্টের তথ্য রাখাই যায় না — রাখার মতো কোনো সারি নেই।
- ডিলিট অসংগতি: Tanvir-এর তিনটা অর্ডার মুছুন (ধরুন ডেটা সংরক্ষণ নীতির কারণে), সাথে হারাবেন এই তথ্যও যে তিনি চট্টগ্রামের একজন কাস্টমার।
প্রথমটা চোখের সামনে ঘটতে দেখুন। Tanvir ঢাকায় চলে গেছেন; সাপোর্ট টুল শুধু খোলা থাকা অর্ডারটাই আপডেট করল:
UPDATE order_sheet SET customer_city = 'Dhaka' WHERE order_id = 12;
SELECT customer_email, COUNT(DISTINCT customer_city) AS cities,
GROUP_CONCAT(DISTINCT customer_city) AS which
FROM order_sheet
GROUP BY customer_email
ORDER BY customer_email;+--------------------+--------+------------------+
| customer_email | cities | which |
+--------------------+--------+------------------+
| nadia@example.com | 1 | Dhaka |
| tanvir@example.com | 2 | Chattogram,Dhaka |
+--------------------+--------+------------------+এই টেবিলে ট্রেন করা মডেল দেখবে Tanvir একসাথে দুই শহরে। "শহরভিত্তিক রেভিনিউ" রিপোর্ট তাঁর কেনাকাটা দুই শহরে ভাগ করে ফেলবে।
ফাংশনাল ডিপেনডেন্সি
ফাংশনাল ডিপেনডেন্সি (functional dependency) A → B ("A, B-কে নির্ধারণ করে") মানে: যেসব সারির A একই, তাদের B-ও সবসময় একই। নিচের ডিজাইনের নিয়মগুলো সবই ডিপেনডেন্সি নিয়ে, তাই আগে এগুলোর তালিকা করুন। order_sheet-এর কি হলো (order_id, product):
order_id → order_date, customer_email
customer_email → customer_name, customer_city
product → category, list_price
(order_id, product) → quantity, unit_priceডিপেনডেন্সি আসে ব্যবসার নিয়ম থেকে, কিন্তু ডেটা দিয়ে যেকোনো ডিপেনডেন্সি যাচাই করা যায়: বাঁ পাশ দিয়ে গ্রুপ করুন আর ডান পাশের আলাদা মানগুলো গুনুন। কোনো গ্রুপে একাধিক মান থাকলে নিয়ম ভেঙেছে — ওপরের কোয়েরি customer_email → customer_city-এর ক্ষেত্রে ঠিক এটাই পেয়েছে। যেকোনো ডেটাসেটে ট্রেন করার আগে এটা চালিয়ে দেখার মতো একটা দ্রুত ডেটা-মানের পরীক্ষা:
SELECT product, COUNT(DISTINCT list_price) AS prices, COUNT(DISTINCT unit_price) AS charged
FROM order_sheet
GROUP BY product
HAVING COUNT(DISTINCT unit_price) > 1 OR COUNT(DISTINCT list_price) > 1;+---------+--------+---------+
| product | prices | charged |
+---------+--------+---------+
+---------+--------+---------+কোনো সারি নেই: এখানে product → list_price টিকে আছে, আর unit_price-ও কাকতালীয়ভাবে স্থির। কিন্তু অর্ডার ৯ থেকে জানা আছে, নেওয়া দাম লিস্ট প্রাইস থেকে আলাদা হতে পারে (ডিসকাউন্ট), তাই unit_price নির্ভর করে অর্ডার লাইনের ওপর, প্রোডাক্টের ওপর নয়। ডেটা শুধু দেখাতে পারে যে একটা ডিপেনডেন্সি ভেঙেছে; সেটা থাকা উচিত কি না, তা ব্যবসার প্রশ্ন।
ধাপে ধাপে নরমাল ফর্ম
1NF: পারমাণবিক মান, পুনরাবৃত্ত গ্রুপ নেই
একটা টেবিল ফার্স্ট নরমাল ফর্মে থাকে, যখন প্রতিটা ঘরে একটাই মান আর product1, product2-এর মতো পুনরাবৃত্ত কলাম নেই। মূল এক্সপোর্টে অর্ডার ১-এর জন্য একটা ঘরে ছিল "Python Crash Course, Wireless Mouse"; প্রতিটা প্রোডাক্টকে আলাদা সারি দিলে (order_sheet যেমন করে) সেটা ঠিক হয়। কমা দিয়ে আলাদা করা স্ট্রিংয়ের ভেতরের অংশে JOIN, GROUP BY বা ইনডেক্স কিছুই চলে না — তাই "শুধু অ্যানালিটিক্সের" জন্যও 1NF জরুরি।
2NF: আংশিক ডিপেনডেন্সি নেই
সেকেন্ড নরমাল ফর্ম: 1NF, আর কোনো নন-কি কলাম (যে কলাম কোনো ক্যান্ডিডেট কি-র অংশ নয়) কম্পোজিট কি-র শুধু একটা অংশের ওপর নির্ভর করে না। যে টেবিলের কি একটাই কলাম, সেটা আপনা-আপনিই 2NF-এ থাকে; সমস্যাটা দেখা দেয় শুধু (order_id, product)-এর মতো কম্পোজিট কি-তে। order_date আর customer_email নির্ভর করে শুধু order_id-এর ওপর; category আর list_price শুধু product-এর ওপর। প্রতিটা দলকে এমন টেবিলে সরান, যার কি হলো ঠিক সেই অংশটা, যার ওপর দলটা নির্ভর করে:
order_sheet(order_id, product, order_date, customer_email, customer_name, customer_city,
category, list_price, quantity, unit_price)
↓ প্রতিটা কলাম কিসের ওপর নির্ভর করে, সেই অনুযায়ী ভাগ
orders(order_id PK, order_date, customer_email, customer_name, customer_city)
products(product PK, category, list_price)
order_items(order_id, product, quantity, unit_price) PK (order_id, product)3NF: ট্রানজিটিভ ডিপেনডেন্সি নেই
থার্ড নরমাল ফর্ম: 2NF, আর কোনো নন-কি কলাম অন্য কোনো নন-কি কলামের মধ্য দিয়ে কি-র ওপর নির্ভর করে না (একে বলে ট্রানজিটিভ বা পরোক্ষ ডিপেনডেন্সি: কি → X → Y)। নতুন orders-এ customer_name আর customer_city নির্ভর করে customer_email-এর ওপর, যা নির্ভর করে order_id-এর ওপর: পরোক্ষ নির্ভরতার একটা শিকল। আবার ভাগ করুন — category-র ক্ষেত্রেও তাই; এর জন্য আলাদা টেবিল দরকার, যাতে প্রোডাক্টহীন ক্যাটাগরিও (Furniture) থাকতে পারে:
customers(customer_id PK, email UNIQUE, name, city)
orders(order_id PK, customer_id FK, order_date, status)
categories(category_id PK, name UNIQUE)
products(product_id PK, name, category_id FK, price, stock)
order_items(order_id FK, product_id FK, quantity, unit_price) PK (order_id, product_id)এটাই সেই শপ স্কিমা, যা আপনি শুরু থেকে ব্যবহার করছেন। নিয়মটার ছোট রূপ: প্রতিটা নন-কি কলাম নির্ভর করবে কি-র ওপর, পুরো কি-র ওপর, আর কি ছাড়া আর কিছুর ওপর নয়। আর ভাগ করায় কিছু হারায় না — টুকরোগুলো জয়েন করলে মূল সারিগুলো ফিরে আসে:
SELECT COUNT(*) AS rebuilt_rows
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN products AS p ON p.product_id = oi.product_id
JOIN categories AS cat ON cat.category_id = p.category_id
WHERE o.customer_id IN (1, 2);+--------------+
| rebuilt_rows |
+--------------+
| 7 |
+--------------+৭টা সারি, order_sheet-এর মতোই। যে ভাগকে হুবহু জয়েন করে ফেরত আনা যায়, তাকে বলে লসলেস (lossless)। ফাংশনাল ডিপেনডেন্সি ধরে এমনভাবে ভাগ করলে, যাতে দুই টুকরোর সাধারণ কলামটা একটা টুকরোর কি হয় (যেমন customers-এর জন্য customer_id), এটা নিশ্চিত হয়।
BCNF: প্রতিটা নির্ধারক একটা কি
বয়েস–কড নরমাল ফর্ম 3NF-কে আরও কড়া করে: প্রতিটা ডিপেনডেন্সি A → B-এর জন্য A-কে একটা ক্যান্ডিডেট কি হতে হবে। 3NF-এ আছে কিন্তু BCNF-এ নেই — এমন টেবিল বিরল, তবে এই হলো ক্লাসিক উদাহরণ। শিক্ষার্থীদের প্রতি কোর্সে একজন মেন্টর দেওয়া হয়, আর প্রত্যেক মেন্টর ঠিক একটা কোর্স দেখেন:
CREATE TABLE mentoring (
student TEXT NOT NULL,
course TEXT NOT NULL,
mentor TEXT NOT NULL,
PRIMARY KEY (student, course)
);
INSERT INTO mentoring VALUES
('Nadia', 'SQL', 'Rina'),
('Tanvir', 'SQL', 'Rina'),
('Nadia', 'ML', 'Joy'),
('Mitu', 'ML', 'Sami');
UPDATE mentoring SET course = 'Python' WHERE mentor = 'Rina' AND student = 'Nadia';
SELECT mentor, GROUP_CONCAT(DISTINCT course) AS courses
FROM mentoring
GROUP BY mentor
ORDER BY mentor;+--------+------------+
| mentor | courses |
+--------+------------+
| Joy | ML |
| Rina | Python,SQL |
| Sami | ML |
+--------+------------+নিয়ম অনুযায়ী mentor → course সত্য, কিন্তু mentor কোনো কি নয়, তাই টেবিলটা "Rina SQL পড়ান" দুবার রাখে — আর একটা আপডেটেই Rina দুটো কোর্সের মেন্টর হয়ে গেলেন। এখানে প্রতিটা কলাম কোনো না কোনো ক্যান্ডিডেট কি-র অংশ, তাই টেবিলটা 3NF-এ আছে; BCNF-এ নেই। সমাধান: mentors(mentor PK, course) আর student_mentors(student, mentor)। সত্যি বলতে এর একটা দাম আছে: ভাগের পর "প্রতি কোর্সে একজন শিক্ষার্থীর একজনই মেন্টর" নিয়মটা আর একটা সাধারণ প্রাইমারি কি দিয়ে নিশ্চিত করা যায় না। নরমাল ফর্ম হাতিয়ার, আইন নয়।
জাংশন টেবিল
মেনি-টু-মেনি সম্পর্ক সবসময় একটা জাংশন টেবিল হয়ে যায়, যাতে থাকে দুটো ফরেন কি — সাধারণত কম্পোজিট প্রাইমারি কি হিসেবে — আর জোড়াটা সম্পর্কে যেকোনো তথ্য। প্রোডাক্টের ট্যাগ এর একটা চেনা উদাহরণ (পরে সার্চে মেটাডেটা ফিল্টার হিসেবে এগুলো কাজে লাগবে):
CREATE TABLE product_tags (
product_id INTEGER NOT NULL,
tag TEXT NOT NULL,
PRIMARY KEY (product_id, tag),
FOREIGN KEY (product_id) REFERENCES products (product_id)
);
INSERT INTO product_tags VALUES (1, 'python'), (2, 'ml'), (2, 'python'), (7, 'ml'), (6, 'sql');
SELECT p.name, GROUP_CONCAT(t.tag, ', ') AS tags
FROM products AS p
JOIN product_tags AS t ON t.product_id = p.product_id
GROUP BY p.product_id, p.name
ORDER BY p.product_id;
INSERT INTO product_tags VALUES (2, 'ml');+---------------------------+------------+
| name | tags |
+---------------------------+------------+
| Python Crash Course | python |
| Hands-On Machine Learning | ml, python |
| SQL Masterclass | sql |
| AI Engineering Bootcamp | ml |
+---------------------------+------------+
Error: UNIQUE constraint failed: product_tags.product_id, product_tags.tagকম্পোজিট কি সত্যিকারের কাজ করছে: ডুপ্লিকেট ট্যাগটা বাতিল হলো। ট্যাগগুলো একটা স্ট্রিং কলামে ('ml,python') রাখলে 1NF ভাঙত, আর "ml ট্যাগের সব প্রোডাক্ট" হয়ে যেত একটা ভঙ্গুর LIKE কোয়েরি।
ডিনরমালাইজেশন: অ্যানালিটিক্স যখন সমতল টেবিল চায়
যে সিস্টেম ডেটা লেখে, তার জন্য নরমালাইজড টেবিলই ঠিক: প্রতিটা পরিবর্তন লিখতে হয় শুধু এক জায়গায়। অ্যানালিটিক্স আর ML লেখার চেয়ে অনেক বেশি পড়ে, আর তারা চায় প্রতিটা জিনিসের জন্য একটা চওড়া সারি — একটা pandas DataFrame, একটা ফিচার টেবিল, একটা BI ডেটাসেট। তাই তাদের জন্য ইচ্ছে করেই ডিনরমালাইজ করা হয় — নরমালাইজড উৎস থেকে সমতল রূপটা বানিয়ে:
CREATE VIEW sales_wide AS
SELECT o.order_id, o.order_date, c.city, cat.name AS category, p.name AS product,
oi.quantity * oi.unit_price AS revenue
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN products AS p ON p.product_id = oi.product_id
LEFT JOIN categories AS cat ON cat.category_id = p.category_id
WHERE o.status <> 'cancelled';
SELECT COALESCE(city, '(none)') AS city, SUM(revenue) AS revenue
FROM sales_wide
GROUP BY city
ORDER BY revenue DESC, city;+------------+---------+
| city | revenue |
+------------+---------+
| Dhaka | 38600 |
| Chattogram | 22500 |
| Khulna | 19950 |
| (none) | 4000 |
+------------+---------+| নরমালাইজড (OLTP উৎস) | ডিনরমালাইজড (অ্যানালিটিক্সের কপি) | |
|---|---|---|
| প্রতিটা তথ্য রাখা হয় | একবার | বহুবার |
| লেখা | সহজ আর নিরাপদ | আবার বানাতে বা রিফ্রেশ করতে হয় |
| পড়া | জয়েন লাগে | একটাই টেবিল, দ্রুত স্ক্যান |
| সাধারণত থাকে | অ্যাপের ডেটাবেসে | ভিউ, ওয়্যারহাউস টেবিল, Parquet ট্রেনিং স্ন্যাপশট |
যে নিয়ম আপনাকে নিরাপদ রাখে: কপিকে ডিনরমালাইজ করুন, মূল উৎসকে (source of truth) কখনো নয়। হয় একটা ভিউ (সবসময় হালনাগাদ), নয়তো পাইপলাইনে আবার বানানো টেবিল (দ্রুত, কিন্তু একটা স্ন্যাপশট) — আর ওয়্যারহাউস, লেক, স্ট্রিম আর স্কেল পাতার স্টার স্কিমা এই ধারণারই গোছানো রূপ।
সাধারণ ভুল
- একটা কলামে কমা দিয়ে আলাদা করা তালিকা।
tags = 'ml,python'-এর ওপর জয়েন, ইনডেক্স বা কনস্ট্রেইন্ট কিছুই চলে না। জাংশন টেবিল ব্যবহার করুন। - "সুবিধার জন্য" বর্ণনামূলক কলাম চাইল্ড টেবিলে কপি করা।
orders.customer_cityএকসময়customers.city-এর সাথে আর মিলবে না। জয়েন করুন, বা একটা ভিউ বানান। - বদলাতে পারে এমন ন্যাচারাল কি-কে প্রাইমারি কি বানানো। ইমেইল আর ফোন নম্বর বদলায়, আর প্রতিটা ফরেন কি-তে কপি হয়। সারোগেট কি নিন, সাথে ন্যাচারাল কি-তে
UNIQUE। - ইতিহাস যে আলাদা তথ্য, তা ভুলে যাওয়া।
order_items.unit_priceproducts.price-এর ডুপ্লিকেট নয়: এটা বিক্রির সময়ের দাম। এটা নরমালাইজ করে বাদ দিলে দাম বদলালেই অতীতের সব অর্ডার বদলে যেত। - অ্যানালিটিক্স লেয়ারকে মাত্রাতিরিক্ত নরমালাইজ করা। যে ফিচার টেবিল প্রতিটা ট্রেনিং রানে নয়টা জয়েন চায়, তা ধীর আর ভুলপ্রবণ। উৎস নরমালাইজড রাখুন, আর অ্যানালিটিক্সকে একটা সমতল, আবার-বানানো-যায় এমন কপি দিন।
নিজে চেষ্টা করুন
- সহজ:
order_sheet-এorder_id → order_dateটিকে আছে কি না দেখুন (একাধিক তারিখওয়ালা কোনো অর্ডার থাকলে দেখান)। - মাঝারি:
(student_email, course_code)কি-ওয়ালাenrolments(student_email, student_name, course_code, course_title, enrolled_on, score)টেবিলের ফাংশনাল ডিপেনডেন্সিগুলো লিখুন, তারপর 3NF টেবিলগুলোর নাম দিন। - কঠিন:
mentoringটেবিলটা ঠিক করুন:mentorsআরstudent_mentorsবানান, মূল চারটা সারির নিয়ম অনুযায়ী ভরুন (Rina→SQL, Joy→ML, Sami→ML), আর জয়েন করে প্রত্যেক শিক্ষার্থীর কোর্স ও মেন্টর দেখান।
উত্তর
-- সহজ: কোনো সারি না এলে ডিপেনডেন্সি টিকে আছে
SELECT order_id, COUNT(DISTINCT order_date) AS dates
FROM order_sheet
GROUP BY order_id
HAVING COUNT(DISTINCT order_date) > 1;মাঝারি — ডিপেনডেন্সি আর টেবিলগুলো:
student_email → student_name
course_code → course_title
(student_email, course_code) → enrolled_on, score
-- 3NF: students(student_email PK, student_name)
-- courses(course_code PK, course_title)
-- enrolments(student_email FK, course_code FK, enrolled_on, score) PK (student_email, course_code)-- কঠিন
CREATE TABLE mentors (mentor TEXT PRIMARY KEY, course TEXT NOT NULL);
CREATE TABLE student_mentors (
student TEXT NOT NULL,
mentor TEXT NOT NULL,
PRIMARY KEY (student, mentor),
FOREIGN KEY (mentor) REFERENCES mentors (mentor)
);
INSERT INTO mentors VALUES ('Rina', 'SQL'), ('Joy', 'ML'), ('Sami', 'ML');
INSERT INTO student_mentors VALUES ('Nadia', 'Rina'), ('Tanvir', 'Rina'), ('Nadia', 'Joy'), ('Mitu', 'Sami');
SELECT sm.student, m.course, m.mentor
FROM student_mentors AS sm
JOIN mentors AS m ON m.mentor = sm.mentor
ORDER BY sm.student, m.course;সারসংক্ষেপ
- চাহিদা → এনটিটি (নিজস্ব পরিচয়ওয়ালা বিশেষ্য), অ্যাট্রিবিউট, সম্পর্ক (ক্রিয়া); মেনি-টু-মেনির জন্য জাংশন টেবিল লাগে।
- সারোগেট প্রাইমারি কি নিন, সাথে ন্যাচারাল কি-তে
UNIQUE; ফরেন কি রেফারেন্সগুলোকে সৎ রাখে। - নরমালাইজেশন চলে ফাংশনাল ডিপেনডেন্সি ধরে, আর একটা
GROUP BY … COUNT(DISTINCT …)কোয়েরি আসল ডেটায় সেগুলো যাচাই করে। - 1NF: পারমাণবিক মান। 2NF: কোনো আংশিক ডিপেনডেন্সি নেই। 3NF: কোনো ট্রানজিটিভ ডিপেনডেন্সি নেই। BCNF: প্রতিটা নির্ধারক একটা কি।
- উৎস নরমালাইজড রাখুন; অ্যানালিটিক্স আর ML-এর জন্য কপি (ভিউ, ওয়্যারহাউস টেবিল, ট্রেনিং স্ন্যাপশট) ডিনরমালাইজ করুন।
এরপর: আসল স্কিমা ডিজাইন, আর নিরাপদে তা বদলানো পাতায় এসবই প্রয়োগ করা হবে একটা লার্নিং-প্ল্যাটফর্মের ডেটাবেসে — চাহিদা থেকে DDL পর্যন্ত — তারপর দেখানো হবে ভার্সন করা মাইগ্রেশন দিয়ে চালু স্কিমা কীভাবে বদলাতে হয়।