অধ্যায় 5 · টেবিল জোড়া লাগানো
জয়েন: INNER JOIN আর টেবিল অ্যালিয়াস
- পৃষ্ঠা 16 / 22
- 13 মিনিট পড়া
ভালোভাবে ডিজাইন করা ডেটাবেস তার ডেটা কয়েকটা টেবিলে ভাগ করে রাখে: একজন কাস্টমারের নাম আর শহর customers-এ একবারই লেখা থাকে, আর প্রতিটা অর্ডারে থাকে শুধু একটা customer_id, যেটা সেই কাস্টমারের সারিটা দেখিয়ে দেয় (দেখুন টেবিল, কি আর রিলেশনশিপ)। এতে ডেটা ঠিক থাকে, কিন্তু মানুষ যে প্রশ্ন করে তা টেবিলের সীমা মানে না: "আমাদের অর্ডারগুলো কোন শহর থেকে আসে?", "কোন ক্যাটাগরি সবচেয়ে বেশি বিক্রি হয়?"। জয়েন (join) কিছু কপি না করেই একটা কোয়েরির জন্য টুকরোগুলো আবার জোড়া লাগায়।
AI আর ডেটার কাজে জয়েন রোজকার হাতিয়ার। চার্ন মডেলের ট্রেনিং টেবিলে প্রতিটা কাস্টমারের তথ্যের পাশে লাগে তার অর্ডারের ইতিহাস; ইভ্যালুয়েশন রিপোর্টে মডেলের প্রতিটা উত্তরের পাশে লাগে প্রম্পট আর কোন ইউজার সেটা পাঠিয়েছে; RAG সিস্টেম ডকুমেন্টের চাঙ্ক বাছাই করে অন্য টেবিলে রাখা মেটাডেটা দিয়ে। এগুলো সবই জয়েন।
যা শিখবেন
- জয়েন কেন দরকার, আর জয়েন ধাপে ধাপে কীভাবে সারি মেলায়
INNER JOIN … ON, এর ছোট রূপJOIN, আরUSING- টেবিল অ্যালিয়াস, আর জয়েনে কেন প্রতিটা কলামের আগে টেবিলের নাম দেবেন
- SQLite, MySQL আর PostgreSQL-এ "ambiguous column" এরর
WHERE,GROUP BY,ORDER BY-এর সাথে জয়েন, আর pandas-এ একই জয়েন
জয়েন কেন: তথ্য দুই জায়গায়
প্রথম কয়েকটা অর্ডার আলাদাভাবে দেখুন:
SELECT order_id, customer_id, order_date, status
FROM orders
ORDER BY order_id
LIMIT 4;+----------+-------------+------------+-----------+
| order_id | customer_id | order_date | status |
+----------+-------------+------------+-----------+
| 1 | 1 | 2026-01-12 | delivered |
| 2 | 2 | 2026-01-20 | delivered |
| 3 | 1 | 2026-02-03 | delivered |
| 4 | 3 | 2026-02-14 | delivered |
+----------+-------------+------------+-----------+কাস্টমার ১ কে? অর্ডার সেটা বলে না: এতে আছে শুধু ফরেন কি customer_id। নামটা আছে customers-এ, সেই সারিতে যার প্রাইমারি কি 1। জয়েন প্রতিটা সারির জন্য একসাথে এই সূত্র ধরে customers থেকে মিলিয়ে আনে।
জয়েন কীভাবে সারি মেলায়
চারজন কাস্টমার আর চারটা অর্ডার নিন। জয়েনের শর্ত হলো orders.customer_id = customers.customer_id।
আগে — দুটো আলাদা টেবিল:
| customers.customer_id | name |
|---|---|
| 1 | Nadia Rahman |
| 2 | Tanvir Ahmed |
| 3 | Farhana Akter |
| 8 | Karim Uddin |
| orders.order_id | orders.customer_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 4 | 3 |
INNER JOIN-এর পরে — শর্ত পূরণ করে এমন প্রতিটা জোড়ার জন্য একটা করে সারি:
| order_id | customer_id | name | কেন |
|---|---|---|---|
| 1 | 1 | Nadia Rahman | 1 = 1 |
| 2 | 2 | Tanvir Ahmed | 2 = 2 |
| 3 | 1 | Nadia Rahman | 1 = 1 (আবার Nadia) |
| 4 | 3 | Farhana Akter | 3 = 3 |
তিনটা জিনিস খেয়াল করুন, কারণ জয়েনের প্রায় সব চমকের ব্যাখ্যা এখানেই:
- একটা সারি কয়েকবার মিলতে পারে। Nadia-র দুটো অর্ডার, তাই তার নাম দুবার এসেছে। ওয়ান-টু-মেনি রিলেশনশিপে "এক" দিকের সারিটা "অনেক" দিকের প্রতিটা সারির সাথে বারবার আসে।
- যে সারির কোনো জোড়া নেই, সে হারিয়ে যায়। Karim (কাস্টমার ৮)-এর কোনো অর্ডার নেই, তাই ইনার জয়েন তাকে পুরোপুরি বাদ দেয়। কোনো এরর নেই, কোনো সতর্কবার্তাও নেই।
- ফলাফল একটা নতুন টেবিল, যাতে দুই দিকের কলামই আছে। অন্য যেকোনো টেবিলের মতো একে ফিল্টার, গ্রুপ আর সর্ট করা যায়।
মনে রাখার একটা ভালো ছবি: ডেটাবেস প্রতিটা (কাস্টমার, অর্ডার) জোড়া বিবেচনা করে, আর যেসব জোড়ায় ON-এর শর্ত সত্য সেগুলো রাখে। আসল ডেটাবেস ইনডেক্স আর হ্যাশ টেবিল ব্যবহার করে, তাই সত্যি সত্যি প্রতিটা জোড়া যাচাই করে না, কিন্তু ফলাফল হুবহু তেমনই হয়।
customers (4 rows) × orders (4 rows) = 16 possible pairs
keep pairs where orders.customer_id = customers.customer_id
→ 4 rowsINNER JOIN … ON
SELECT orders.order_id, orders.order_date, customers.name, customers.city
FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id
ORDER BY orders.order_id
LIMIT 5;+----------+------------+---------------+------------+
| order_id | order_date | name | city |
+----------+------------+---------------+------------+
| 1 | 2026-01-12 | Nadia Rahman | Dhaka |
| 2 | 2026-01-20 | Tanvir Ahmed | Chattogram |
| 3 | 2026-02-03 | Nadia Rahman | Dhaka |
| 4 | 2026-02-14 | Farhana Akter | Dhaka |
| 5 | 2026-02-25 | Rafiq Islam | Sylhet |
+----------+------------+---------------+------------+FROM orders INNER JOIN customersদুটো টেবিলের নাম বলে।ON …বলে কোন সারিগুলো একসাথে যাবে।orders.order_idহলো কোয়ালিফায়েড (qualified) কলাম: টেবিলের নাম, ডট, কলামের নাম। এটা ঠিক কোন টেবিলের কলাম তা স্পষ্ট করে।INNERনা লিখলেও চলে: সব ডায়ালেক্টেই শুধুJOINমানেINNER JOIN। বেশিরভাগ মানুষJOIN-ই লেখেন।- জয়েন আগে হয় (এটা
FROM-এর অংশ), তারপরWHERE,GROUP BY,SELECTআরORDER BYজোড়া লাগানো সারির ওপর কাজ করে — SQL কীভাবে কাজ করে: স্টেটমেন্ট, কমান্ডের ধরন আর কোয়েরির ক্রম পাতার সেই লজিক্যাল ক্রম।
টেবিল অ্যালিয়াস
প্রতিটা কলামের আগে customers. লিখতে লিখতে কোয়েরি লম্বা হয়ে যায়। টেবিল অ্যালিয়াস (table alias) কোয়েরি চলাকালীন টেবিলকে একটা ছোট নাম দেয়:
SELECT o.order_id, o.status, c.name
FROM orders AS o
JOIN customers AS c ON o.customer_id = c.customer_id
WHERE c.city = 'Dhaka'
ORDER BY o.order_id;+----------+-----------+---------------+
| order_id | status | name |
+----------+-----------+---------------+
| 1 | delivered | Nadia Rahman |
| 3 | delivered | Nadia Rahman |
| 4 | delivered | Farhana Akter |
| 8 | delivered | Farhana Akter |
| 10 | delivered | Nadia Rahman |
| 11 | delivered | Mitu Das |
| 14 | delivered | Farhana Akter |
+----------+-----------+---------------+WHERE এখানে অন্য টেবিলের একটা কলামে ফিল্টার করছে: জয়েনের পর দুই টেবিলের সব কলামই হাতের কাছে। SQLite, MySQL আর PostgreSQL-এ টেবিল অ্যালিয়াসের জন্য AS ঐচ্ছিক (FROM orders o); Oracle সেখানে AS একেবারেই নেয় না, তাই অনেক টিম এটা বাদই দেয়। ছোট আর অর্থবহ অ্যালিয়াস বাছুন (orders-এর জন্য o, customers-এর জন্য c), এলোমেলো a, b, c নয়।
কলাম কোয়ালিফাই করুন: ambiguous column এরর
products আর categories দুটোতেই name নামে কলাম আছে। কোনটা চান না বলে শুধু name চাইলে ডেটাবেস আন্দাজ করতে রাজি হয় না:
SELECT name, price
FROM products
JOIN categories ON products.category_id = categories.category_id;Error: ambiguous column name: nameMySQL আর PostgreSQL নিজেদের ভাষায় একই কথা বলে:
-- MySQL
SELECT name, price
FROM products
JOIN categories ON products.category_id = categories.category_id;Error: ERROR 1052: Column 'name' in field list is ambiguous-- PostgreSQL
SELECT name, price
FROM products
JOIN categories ON products.category_id = categories.category_id;Error: column reference "name" is ambiguousসমাধান হলো কলামের আগে টেবিল বলে দেওয়া, আর কলাম অ্যালিয়াস দিয়ে দুটো name কলামকে আলাদা নাম দেওয়া:
SELECT p.name AS product, c.name AS category, p.price
FROM products p
JOIN categories c ON p.category_id = c.category_id
ORDER BY p.product_id;+-----------------------------+-------------+-------+
| product | category | price |
+-----------------------------+-------------+-------+
| Python Crash Course | Books | 1200 |
| Hands-On Machine Learning | Books | 2500 |
| Mechanical Keyboard | Electronics | 4500 |
| Wireless Mouse | Electronics | 900 |
| 27-inch Monitor | Electronics | 18500 |
| SQL Masterclass | Courses | 3000 |
| AI Engineering Bootcamp | Courses | 15000 |
| Laptop Stand | Accessories | 1500 |
| USB-C Hub | Accessories | 2200 |
| Noise-Cancelling Headphones | Electronics | 7500 |
+-----------------------------+-------------+-------+price নিয়ে কোনো দ্বিধা ছিল না (শুধু products-এ আছে), তাই p. ছাড়াও কোয়েরি চলত। তবু দিন। পরে কেউ categories-এ price কলাম যোগ করলে কোয়ালিফাই না করা কোয়েরি হঠাৎ ভেঙে যাবে — অথবা তার চেয়েও খারাপ, পাঠক বুঝতে পারবেন না কলামটা কোন টেবিলের। জয়েনে প্রতিটা কলাম কোয়ালিফাই করুন।
Gift Card গেল কোথায়?
প্রোডাক্ট ১১টা, কিন্তু ওপরে সারি মাত্র ১০টা। দুটোই গুনে দেখুন:
SELECT COUNT(*) AS products FROM products;
SELECT COUNT(*) AS joined_rows
FROM products p
JOIN categories c ON p.category_id = c.category_id;+----------+
| products |
+----------+
| 11 |
+----------+
+-------------+
| joined_rows |
+-------------+
| 10 |
+-------------+প্রোডাক্ট ১১ (Gift Card)-এর category_id NULL। NULL = 1 সত্য নয় (এটা অজানা, দেখুন BETWEEN, IN, LIKE ও NULL), তাই Gift Card কোনো ক্যাটাগরির সাথে মেলে না আর ইনার জয়েন তাকে বাদ দেয়। ক্যাটাগরি ৫ (Furniture)-এ কোনো প্রোডাক্ট নেই, সেটাও বাদ পড়ে। ইনার জয়েন শুধু দুই দিকে মেলা সারিগুলো রাখে; না-মেলা সারিগুলোও চাইলে লাগবে আউটার জয়েন (পরের পাতা)। এটা ধরার সবচেয়ে ভালো অভ্যাস হলো জয়েনের আগে আর পরে সারির সংখ্যা মিলিয়ে দেখা।
USING: কলামের নাম যখন একই
জয়েনের কলামের নাম দুই টেবিলে একই হলে সমতার শর্তটা আরও ছোট করে লেখা যায় USING (column) দিয়ে:
SELECT category_id, p.name AS product, c.name AS category
FROM products p
JOIN categories c USING (category_id)
WHERE category_id = 3
ORDER BY p.product_id;+-------------+-------------------------+----------+
| category_id | product | category |
+-------------+-------------------------+----------+
| 3 | SQL Masterclass | Courses |
| 3 | AI Engineering Bootcamp | Courses |
+-------------+-------------------------+----------+USING (category_id) মানে ON p.category_id = c.category_id, আর এটা দুটো কলামকে মিলিয়ে একটা করে দেয় — সেজন্যই এখানে টেবিলের নাম ছাড়াই category_id লেখা গেছে। SQLite, MySQL আর PostgreSQL তিনটাই এটা সমর্থন করে; SQL Server করে না। নাম যখন সত্যিই মেলে, যেমন এই স্কিমায়, তখন ব্যবহার করুন।
NATURAL JOINএড়িয়ে চলুন। এটা দুই টেবিলে একই নামের সব কলাম দিয়ে জয়েন করে।products NATURAL JOIN categoriesজয়েন করেcategory_idআরnameদুটো দিয়েই, আর কোনো প্রোডাক্টের নাম "Books" নয়, তাই কিছুই আসে না — আর কেউ নতুন কলাম যোগ করলেই এর অর্থ চুপচাপ বদলে যায়।
SELECT COUNT(*) AS natural_rows FROM products NATURAL JOIN categories;+--------------+
| natural_rows |
+--------------+
| 0 |
+--------------+GROUP BY-এর সাথে জয়েন: আসল প্রশ্নের উত্তর
আগের পাতাগুলোর সবকিছুই জোড়া লাগানো সারিতে কাজ করে। প্রত্যেক কাস্টমার কয়টা অর্ডার করেছেন, আর প্রথমটা কবে?
SELECT c.name, COUNT(*) AS orders, MIN(o.order_date) AS first_order
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name
ORDER BY orders DESC, c.name;+-----------------+--------+-------------+
| name | orders | first_order |
+-----------------+--------+-------------+
| Farhana Akter | 3 | 2026-02-14 |
| Nadia Rahman | 3 | 2026-01-12 |
| Tanvir Ahmed | 3 | 2026-01-20 |
| Imran Hossain | 2 | 2026-04-02 |
| Mitu Das | 1 | 2026-05-06 |
| Rafiq Islam | 1 | 2026-02-25 |
| Sadia Chowdhury | 1 | 2026-03-15 |
+-----------------+--------+-------------+- নামের পাশাপাশি
c.customer_idদিয়েও গ্রুপ করেছি: দুজন আলাদা কাস্টমারের নাম এক হতে পারে, আইডি তাদের আলাদা রাখে। - কাস্টমার ৭ জন, ৮ জন নয়। Karim-এর কোনো অর্ডার নেই, তাই
GROUP BYপর্যন্ত পৌঁছানোর আগেই ইনার জয়েন তাকে বাদ দিয়েছে। চার্ন মডেলের জন্য যারা কখনো অর্ডার করেননি তারাই প্রায়ই সবচেয়ে আগ্রহের — পরের পাতায় দেখবেন তাদের কীভাবে রাখতে হয়।
পুরোনো কমা সিনট্যাক্স
এমন জয়েনও দেখবেন যেখানে FROM-এ কমা আর শর্তটা WHERE-এ। এর মানে ইনার জয়েনই: FROM orders o, customers c WHERE o.customer_id = c.customer_id।
JOIN … ON-ই ভালো: এটা "টেবিলগুলো কীভাবে যুক্ত" আর "কোন সারিগুলো চাই" আলাদা রাখে, আর শর্তটা থাকে ঠিক সেই টেবিলের পাশে, তাই বাদ পড়লে সহজেই চোখে পড়ে। (ON ছাড়া JOIN লিখলে PostgreSQL এরর দেয়; SQLite আর MySQL চুপচাপ একে ক্রস জয়েন ধরে নেয়, যা পরের পাতায় দেখবেন।) কমা রূপে শর্ত ভুলে গেলে আসে প্রতিটা জোড়া — ১৪টা অর্ডার × ৮ জন কাস্টমার:
SELECT COUNT(*) AS rows_without_condition
FROM orders, customers;+------------------------+
| rows_without_condition |
+------------------------+
| 112 |
+------------------------+pandas-এ একই জয়েন
Python for AI-তে আপনি merge দিয়ে DataFrame জোড়া লাগিয়েছেন। কাজটা একই: how="inner" হলো ইনার জয়েন, on= হলো USING। ডেটা যখন ডেটাবেসে, জয়েনটা ডেটাবেসকেই করতে দিন; সাধারণত সেটা দ্রুত, আর Python-এ কম ডেটা আনতে হয়।
import pandas as pd
from sqlhelp import con
joined = pd.read_sql("""
SELECT o.order_id, o.customer_id, c.name, c.city
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY o.order_id
""", con)
orders = pd.read_sql("SELECT order_id, customer_id FROM orders ORDER BY order_id", con)
customers = pd.read_sql("SELECT customer_id, name, city FROM customers", con)
merged = orders.merge(customers, on="customer_id", how="inner")
print(len(joined), len(merged))
print(merged.equals(joined))
print(merged.head(3))14 14
True
order_id customer_id name city
0 1 1 Nadia Rahman Dhaka
1 2 2 Tanvir Ahmed Chattogram
2 3 1 Nadia Rahman Dhakaসাধারণ ভুল
- কলাম কোয়ালিফাই না করা।
productsআরcategories-এর ওপরSELECT name …"ambiguous column" এরর দেয়। সমাধান:p.name AS product, c.name AS category, আর প্রতিটা জয়েনে প্রতিটা কলাম কোয়ালিফাই করুন। - অ্যালিয়াস দেওয়ার পর টেবিলের পুরো নাম ব্যবহার। টেবিলকে অ্যালিয়াস দিলে কোয়েরিতে অ্যালিয়াসটাই লিখতে হবে:
SELECT orders.order_id FROM orders o LIMIT 1;সমাধান:Error: no such column: orders.order_idSELECT o.order_id FROM orders o। - ভুল কলাম দিয়ে জয়েন। এটা কোনো এরর ছাড়াই চলে আর পুরোপুরি ভুল — অর্ডার ৮-কে জুড়ে দেয় কাস্টমার ৮-এর সাথে, যিনি কখনো অর্ডারই করেননি:
SELECT o.order_id, c.name FROM orders o JOIN customers c ON o.order_id = c.customer_id ORDER BY o.order_id;সমাধান: ফরেন কি-কে জয়েন করুন সেই প্রাইমারি কি-র সাথে যাকে সে রেফার করে (+----------+-----------------+ | order_id | name | +----------+-----------------+ | 1 | Nadia Rahman | | 2 | Tanvir Ahmed | | 3 | Farhana Akter | | 4 | Rafiq Islam | | 5 | Sadia Chowdhury | | 6 | Imran Hossain | | 7 | Mitu Das | | 8 | Karim Uddin | +----------+-----------------+o.customer_id = c.customer_id)। স্কিমারREFERENCESঅংশগুলোই বলে দেয় কোন জোড়া জয়েন করার জন্য। - হারিয়ে যাওয়া সারি খেয়াল না করা। যে সারির কোনো মিল নেই বা জয়েন কলামে NULL, ইনার জয়েন তাকে চুপচাপ বাদ দেয় (Gift Card, Karim)। সমাধান: জয়েনের আগে-পরে
COUNT(*)মেলান; সারিগুলো রাখতে হলেLEFT JOINব্যবহার করুন। - জয়েনের শর্ত বাদ পড়া।
WHEREছাড়াFROM orders, customersদেয় ১১২টা অর্থহীন সারি, আর এর ওপর যেকোনোSUM৮ গুণ বড় হয়। সমাধান:JOIN … ONলিখুন, যাতে শর্তটা টেবিলের ঠিক পাশেই থাকে।
নিজে চেষ্টা করুন
- সহজ: প্রতিটা পেমেন্টের সাথে তার অর্ডারের তারিখ দেখান:
payment_id,order_date,amount,method,payment_idঅনুযায়ী সাজিয়ে। (payments-কেorders-এর সাথে জয়েন করুন।) - মাঝারি: যেসব প্রোডাক্ট বিক্রি হয়েছে, প্রতিটার নাম আর মোট কতটা বিক্রি হয়েছে দেখান, সবচেয়ে বেশিটা আগে (সমান হলে নাম অনুযায়ী)। অ্যালিয়াস দিয়ে
order_itemsআরproductsব্যবহার করুন। - কঠিন:
USINGদিয়ে প্রতিটা ক্যাটাগরির নাম, তার প্রোডাক্টের সংখ্যা আর ০ দশমিকে রাউন্ড করা গড় দাম দেখান, শুধু যেসব ক্যাটাগরিতে অন্তত ২টা প্রোডাক্ট আছে, গড় দাম বেশি থেকে কম ক্রমে (সমান হলে ক্যাটাগরির নাম অনুযায়ী)। তারপর এক বাক্যে বলুন Furniture আর Gift Card কেন আসতে পারে না।
উত্তর
-- 1
SELECT p.payment_id, o.order_date, p.amount, p.method
FROM payments p
JOIN orders o ON p.order_id = o.order_id
ORDER BY p.payment_id;-- 2
SELECT pr.name, SUM(oi.quantity) AS total_quantity
FROM order_items oi
JOIN products pr ON oi.product_id = pr.product_id
GROUP BY pr.product_id, pr.name
ORDER BY total_quantity DESC, pr.name;-- 3
SELECT c.name AS category, COUNT(*) AS products, ROUND(AVG(p.price), 0) AS avg_price
FROM products p
JOIN categories c USING (category_id)
GROUP BY c.name
HAVING COUNT(*) >= 2
ORDER BY avg_price DESC, category;Furniture-এ কোনো প্রোডাক্ট নেই আর Gift Card-এর কোনো ক্যাটাগরি নেই, তাই কারোরই অন্য দিকে জোড়া নেই, আর ইনার জয়েন শুধু মেলা জোড়াগুলোই রাখে।
সারসংক্ষেপ
- কি-এর মাধ্যমে যুক্ত টেবিলগুলো থেকে জয়েন আবার একটা মিলিত ছবি তৈরি করে: সাধারণত একটা ফরেন কি-কে মেলানো হয় সে যে প্রাইমারি কি-কে রেফার করে তার সাথে।
JOIN … ON শর্ত(INNER JOIN-এর সমান) যেসব সারির জোড়ায় শর্ত সত্য সেগুলো সব রাখে; একটা সারি অনেকবার মিলতে পারে, আবার একবারও না।- না-মেলা সারি আর জয়েন কলামে NULL থাকা সারি ইনার জয়েন থেকে কোনো সতর্কতা ছাড়াই হারিয়ে যায় — সারির সংখ্যা মিলিয়ে দেখুন।
- ছোট টেবিল অ্যালিয়াস দিন আর প্রতিটা কলাম কোয়ালিফাই করুন; দুই টেবিলে থাকা কলাম কোয়ালিফাই না করলে তিন ডায়ালেক্টেই "ambiguous column" এরর।
- নাম মিললে
USING (col)একটা ছোট রূপ;NATURAL JOINএড়িয়ে চলুন।
এরপর: LEFT, RIGHT, FULL, CROSS আর সেলফ জয়েন সেই সারিগুলো রাখে যা ইনার জয়েন ফেলে দেয় — যে কাস্টমাররা কখনো অর্ডার করেননি, যে প্রোডাক্ট কখনো বিক্রি হয়নি — আর দেখায় এমন জয়েন যা প্রতিটা সারিকে প্রতিটা সারির সাথে জোড়ে, কিংবা একটা টেবিলকে নিজের সাথেই।