JOIN — দুই table একসাথে
এই পাঠে যা শিখবেন
- কেন JOIN দরকার — relational database-এর normalization-এর কারণ
- ৫টি JOIN type — সিনট্যাক্স, Venn diagram, ও বাস্তব use case
- JOIN-এর সাধারণ ভুল — Cartesian explosion, NULL behavior, duplicate row
- Multiple table chain JOIN — বাস্তব analytics query
১ · কেন JOIN দরকার — Normalization
একটি বড় table-এ সব ডেটা রাখলে সমস্যা — duplication, update anomaly, storage waste। তাই relational design-এ ডেটা ছোট ছোট table-এ ভাগ করা হয় — এটিই normalizationNormalizationEdgar Codd-এর তৈরি ডেটা design principle। ১NF, ২NF, ৩NF, BCNF — প্রতিটি স্তরে duplication কমে। লক্ষ্য: প্রতিটি fact একবারই save হবে। trade-off: query-তে JOIN লাগে।। তারপর প্রয়োজনে JOIN দিয়ে পুনরায় merge করা হয়।
customers (customer_id, name, district)
products (product_id, name, category, price)
orders (order_id, customer_id, product_id, quantity, order_date)
"কোন গ্রাহক কোন পণ্য কত বার কিনেছেন" — এই প্রশ্নে তিনটি table মিলাতে হয়। এখানেই JOIN আসে।
২ · INNER JOIN — match-only
সবচেয়ে সাধারণ JOIN। দু'টি table-এ key match করলেই সেই row return।
-- প্রতিটি অর্ডারের সাথে গ্রাহকের নাম
SELECT
o.order_id,
o.order_date,
o.total_amount,
c.name AS customer_name,
c.district
FROM orders AS o
INNER JOIN customers AS c
ON o.customer_id = c.customer_id;
-- "INNER" শব্দটি optional — শুধু "JOIN" লিখলেও INNER ধরা হয়
-- কিন্তু explicit লেখা best practice
AS o, AS c): table name short করার technique। long query-তে readability বাড়ায়। ON condition: দু'টি table কীভাবে match করবে — সাধারণত foreign key = primary key।
৩ · LEFT JOIN — বাঁ-পক্ষের সব
বাঁ-table-এর সব row রাখব, ডান-table-এর match থাকলে যোগ করব, না থাকলে NULL।
-- প্রতিটি গ্রাহকের সব অর্ডার (অর্ডার না করা গ্রাহকও দেখাব)
SELECT
c.customer_id,
c.name,
o.order_id,
o.total_amount
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id;
-- যারা কোনো অর্ডার করেননি — খুঁজে বের করা
SELECT c.customer_id, c.name
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
৪ · RIGHT JOIN ও FULL OUTER JOIN
RIGHT JOIN = LEFT-এর mirror। প্রায় কেউ ব্যবহার করে না — table order swap করে LEFT লিখলেই হয়। FULL OUTER দু'পক্ষের সব রাখে।
-- RIGHT — খুব কম use হয়
SELECT c.name, o.order_id
FROM orders AS o
RIGHT JOIN customers AS c
ON o.customer_id = c.customer_id;
-- FULL OUTER — দু'পক্ষের সব row, mismatch-এ NULL
SELECT
c.customer_id, c.name,
o.order_id, o.total_amount
FROM customers AS c
FULL OUTER JOIN orders AS o
ON c.customer_id = o.customer_id;
-- দু'পক্ষের mismatch (orphan rows)
SELECT
c.customer_id, c.name,
o.order_id
FROM customers AS c
FULL OUTER JOIN orders AS o
ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL OR o.order_id IS NULL;
LEFT JOIN UNION RIGHT JOIN। PostgreSQL, Oracle, SQL Server-এ direct support।
৫ · CROSS JOIN — Cartesian product
কোনো ON condition নেই — প্রতিটি row বাঁ-পক্ষের সাথে প্রতিটি row ডান-পক্ষের combine। সাধারণত accident-এ হয় (Cartesian explosion); intentionally কম।
-- ৪টি size × ৫টি color = ২০টি combination
SELECT s.size, c.color
FROM sizes AS s
CROSS JOIN colors AS c;
-- Date dimension তৈরি — calendar × stores
SELECT d.date, s.store_id
FROM date_dim AS d
CROSS JOIN stores AS s
WHERE d.date BETWEEN '2024-01-01' AND '2024-12-31';
৬ · Multiple JOIN — chain
বাস্তব query-তে প্রায়ই ৩-৫টি table একসাথে JOIN করতে হয়।
-- প্রতিটি অর্ডারের গ্রাহক নাম + পণ্য নাম + ক্যাটেগরি
SELECT
o.order_id,
o.order_date,
c.name AS customer,
p.name AS product,
p.category,
o.total_amount
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.customer_id
INNER JOIN products AS p ON o.product_id = p.product_id
WHERE o.order_date >= '2024-01-01'
ORDER BY o.order_date DESC
LIMIT 100;
৭ · Self-JOIN — একই table নিজের সাথে
Hierarchy বা peer-comparison-এর জন্য একই table-কে দু'বার alias করে JOIN। উদাহরণ — manager-employee:
-- প্রতিটি employee + তার manager-এর নাম
-- employees (id, name, manager_id)
SELECT
e.name AS employee,
m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m
ON e.manager_id = m.id;
৮ · Common pitfalls — সাধারণ ভুল
- Cartesian explosion: ON condition ভুল হলে row count multiply। ১ লক্ষ × ১ লক্ষ = ১০¹⁰।
- Duplicate rows: JOIN key non-unique হলে — যেমন এক customer-এর ৫টি order হলে — JOIN-এর পর ৫ গুণ row। DISTINCT use করতে হয় না; আগে data বুঝতে হয়।
- NULL in JOIN key: NULL = NULL → NULL (not true)। তাই NULL key-এর row INNER JOIN-এ বাদ পড়ে।
-
Type mismatch:
customer_id INTvscustomer_id VARCHARJOIN — implicit cast slow ও bug-prone। -
WHERE বনাম ON-এর পার্থক্য (LEFT JOIN-এ):
LEFT JOIN ... ON ... AND conditionবনামLEFT JOIN ... ON ... WHERE condition— ফলাফল ভিন্ন।
৯ · Performance considerations
- Index on JOIN keys: primary/foreign key-এ index — JOIN ১০-১০০x দ্রুত।
- Filter আগে JOIN পরে: বড় table-এ
WHEREদিয়ে শুধু relevant row, তারপর JOIN। - EXPLAIN দেখুন: Hash join, merge join, nested loop — কোনটি optimal কোন situation-এ।
- Subquery vs JOIN: অনেক query দু'ভাবে লেখা যায় — modern optimizer প্রায়ই rewrite করে। কিন্তু readability-এ JOIN preferred।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ "INNER vs LEFT JOIN — কোনটি use করব?" এই প্রশ্ন interview-এ অনেক আসে। শুধু "match চাই vs সব চাই" বললে যথেষ্ট না — বাস্তব decision কীভাবে নেবেন?
JOIN choice business question-এর উপর নির্ভর করে — purely technical না।
INNER JOIN-এর situation:
- "Completed orders + valid customer" — orphan order বা customer-হীন record বাদ চাই।
- Reporting where missing data = data quality problem (raise alert)।
- Aggregations where NULL inflate metric — যেমন "avg revenue per active customer"।
- Performance critical — INNER সাধারণত LEFT-এর চেয়ে দ্রুত।
LEFT JOIN-এর situation:
- "All customers with their orders" — অর্ডার না করা গ্রাহকও দেখতে চাই (churn)।
- "All products with sales count" — না-বিক্রি হওয়া পণ্যও important (dead stock)।
- Funnel analysis — drop-off identify করতে missing record দরকার।
- Data audit — orphan record খুঁজতে।
Hidden trap — accidental INNER:
-- ভুল: WHERE-এ NULL filter
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.order_date >= '2024-01-01';
-- এটি actually INNER হয়ে যায় কারণ
-- order-হীন customer-এর o.order_date NULL → WHERE-এ false
সঠিক — ON-এ filter:
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
AND o.order_date >= '2024-01-01';
-- এখন order-হীন customer থাকবে, NULL amount সহ
Decision framework:
- "Missing record-এ আমি কী করব?" - ignore (INNER) vs preserve (LEFT)?
- "Aggregation-এ NULL handle করব কীভাবে?" - COALESCE, FILTER।
- "Stakeholder কোন number আশা করছেন?" - mismatch বের হয়ে গেলে অস্বস্তিকর প্রশ্ন।
- "Data quality assumption সঠিক?" - foreign key constraints actually enforced?
Bangladesh-context: অনেক legacy database-এ FK constraint enforced নয়। তাই LEFT JOIN দিয়ে orphan ধরা — common audit pattern।
মূল উপলব্ধি: JOIN choice একটি business decision। "কোনটি দ্রুত" নয় — "কোনটি সঠিক"। সন্দেহ হলে LEFT দিয়ে শুরু — তারপর প্রয়োজন অনুযায়ী INNER।
প্র ০২ "JOIN-এর পর rows multiply হয়ে গেছে" — junior data scientist-দের সবচেয়ে common bug। এই issue কেন হয়, কীভাবে detect করব, এবং কীভাবে prevent করব?
"Row count exploded" — interview question ও real production bug দু'টোতেই।
কেন rows multiply:
- JOIN key এক table-এ unique, অন্যটিতে non-unique।
- "১ customer → ৫ orders" — JOIN-এর পর customer info ৫ বার duplicate।
- একে বলে fan-out। Many-to-many relationship-এ explosion বড়।
Numerical example:
- customers: ১,০০,০০০ row, প্রতিটি unique।
- orders: ৫,০০,০০০ row, প্রতি customer-এ গড়ে ৫।
- JOIN result: ৫,০০,০০০ row (orders-এর equal)।
- এখন SUM(customer.signup_date) = ভুল — কারণ প্রতি customer ৫ বার counted।
Detection strategies:
-
Pre-JOIN row count:
SELECT COUNT(*) FROM customers; -- 100000 SELECT COUNT(*) FROM orders; -- 500000 -- JOIN-এর পর if 500000 → orders-driven (LEFT/INNER from orders) -- যদি 1000000 → fan-out -
DISTINCT key check:
SELECT COUNT(*), COUNT(DISTINCT customer_id) FROM orders; -- দু'টি ভিন্ন হলে → many orders per customer → fan-out -
JOIN cardinality test:
SELECT customer_id, COUNT(*) FROM orders GROUP BY customer_id HAVING COUNT(*) > 1 LIMIT 5; - Sanity check post-JOIN: aggregate metric — pre-JOIN-এর সাথে match করছে?
Prevention:
-
Aggregate first, then JOIN:
-- ভুল: JOIN-এর পর SUM SELECT c.name, SUM(o.amount) FROM customers c JOIN orders o ON c.id = o.customer_id GROUP BY c.name; -- এটি ঠিক কাজ করে (GROUP BY-এ) -- কিন্তু মাঝে অপ্রয়োজনীয় rows -- alternate: pre-aggregate SELECT c.name, agg.total FROM customers c JOIN ( SELECT customer_id, SUM(amount) AS total FROM orders GROUP BY customer_id ) agg ON c.id = agg.customer_id; -
JOIN cardinality declaration: dbt-এর মতো tool-এ
relationshipstest। - Documentation: "this JOIN expects 1:1" — comment।
- Window function alternative: aggregate ছাড়াই row preserve করতে।
- Schema design: compound primary key, unique constraint।
Real production story:
- একটি e-commerce-এ "monthly revenue" calculate। JOIN customers + orders + payments।
- Payment table-এ retry due multiple row per order। JOIN-এর পর revenue ৩x inflate।
- ৩ মাস ধরে CFO-কে inflated number দেওয়া হচ্ছিল — board-এ presentation, investor pitch।
- একজন junior দেখে detect করেন। পরবর্তীতে policy: "every analytical query peer-review আগে stakeholder-এ যাবে।"
মূল উপলব্ধি: JOIN-এর পর row count মিলছে কিনা সবসময় check করুন। "গণিত ঠিক — সংখ্যাটা ভুল" এর সবচেয়ে common উৎস। Senior analyst-রা এটি instinct।
প্র ০৩ আধুনিক data warehouse-এ (BigQuery, Snowflake, Redshift) "denormalized" বা "wide table" trend দেখা যাচ্ছে — সব ডেটা এক বড় table-এ। এই trend কেন এবং কোথায় ঠিক, কোথায় ভুল?
Database design-এ এক বড় philosophical shift।
Traditional: normalize for OLTP
- OLTP (transaction processing): ছোট ছোট insert/update।
- Normalize → duplication কম, integrity strong।
- Disk expensive, RAM scarce।
- JOIN cost সহনীয়।
Modern: denormalize for OLAP
- OLAP (analytics): বড় aggregate query।
- Columnar storage — duplication compressed away।
- Distributed compute — JOIN-এ shuffle expensive।
- Storage cheap (BigQuery $20/TB/month), compute expensive।
- "Wide table" — JOIN পরিহার, scan optimize।
কেন wide table দ্রুত (modern context):
- Columnar — শুধু needed column read।
- JOIN avoidance — distributed shuffle এড়ানো।
- Predicate pushdown — partition pruning সহজ।
- Cache friendly — single scan।
Cost (যা trade off হয়):
- Storage: ১০-১০০x বেশি (compression-এর পরও)।
- Update complexity: এক fact-এ change → multiple row update।
- Schema evolution: column add কঠিন, migration painful।
- Data quality: single source of truth হারায়।
Modern hybrid pattern — Star Schema + Wide Mart:
- Raw layer: normalized — OLTP source অপরিবর্তিত।
- Staging: cleaned, lightly modeled।
- Mart: denormalized, business-ready, wide table।
- Tools: dbt, dataform — model build করে।
কোথায় normalize রাখুন:
- Production OLTP (orders, payments, inventory)।
- Frequent update with integrity (banking)।
- Limited storage (mobile, embedded)।
কোথায় denormalize:
- BI dashboard (Tableau, Power BI)।
- ML feature store।
- Pre-aggregated metrics (daily revenue)।
- Historical snapshot tables (SCD type-2)।
Bangladesh context:
- bKash/Daraz-এর modern stack: PostgreSQL OLTP → Snowflake/BigQuery analytics → wide mart।
- Smaller orgs: PostgreSQL-এ both OLTP + analytics। Materialized view দিয়ে compromise।
- dbt adoption-এ rapid growth — analytics engineer role emerge।
মূল উপলব্ধি: Normalize/denormalize religion নয়, tradeoff। Layered architecture — OLTP normalize, analytics denormalize — modern best practice। JOIN skill এখনো critical, কিন্তু "JOIN-heavy query" আজ smell।
প্র ০৪ "JOIN-এ NULL behavior tricky" — ৩-জনের একটি team-এ একজন বললেন। NULL-এর সাথে JOIN-এর কোন কোন subtle bug আছে?
NULL handling-এ SQL-এর সবচেয়ে nuanced জায়গা।
(১) NULL = NULL is NULL (not TRUE):
-- দু'টি table-এ customer_id NULL
-- INNER JOIN-এ এই row বাদ পড়বে
SELECT * FROM a INNER JOIN b ON a.customer_id = b.customer_id;
-- কারণ NULL = NULL → NULL (not match)
(২) NULL-safe equality (PostgreSQL IS NOT DISTINCT FROM):
-- NULL-NULL match করতে চাই
SELECT *
FROM a JOIN b
ON a.customer_id IS NOT DISTINCT FROM b.customer_id;
(৩) LEFT JOIN-এ NULL inflation:
SELECT c.name, AVG(o.amount)
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;
-- order-হীন customer-এ AVG(NULL) = NULL
-- COALESCE দিয়ে handle:
COALESCE(AVG(o.amount), 0)
(৪) WHERE clause "INNER-ifies" LEFT JOIN:
-- বিপজ্জনক:
SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.amount > 100;
-- এটি INNER হয়ে যায় — order-হীন customer NULL amount > 100 false
(৫) Aggregate functions ignore NULL:
SELECT
COUNT(*), -- সব row, NULL সহ
COUNT(amount), -- non-NULL only
AVG(amount) -- NULL ignore (denominator কম)
FROM orders;
(৬) NOT IN with NULL — silent bug:
-- যদি subquery-তে NULL থাকে
SELECT * FROM customers
WHERE customer_id NOT IN (SELECT id FROM blacklist);
-- blacklist.id-তে একটি NULL থাকলে → empty result!
-- কারণ "id NOT IN (1, 2, NULL)" → NULL → false
-- সমাধান: NOT EXISTS বা WHERE id IS NOT NULL
(৭) String comparison-এ NULL:
-- TRIM/LOWER NULL দিলে NULL
WHERE LOWER(name) = 'rahim'
-- name NULL → false → bypass
(৮) Arithmetic-এ NULL infectious:
SELECT price + tax FROM products;
-- tax NULL → result NULL
SELECT price + COALESCE(tax, 0) FROM products;
-- safer
(৯) ORDER BY NULL placement:
ORDER BY name ASC NULLS LAST -- PostgreSQL/Oracle
ORDER BY name ASC, name IS NULL -- MySQL workaround
Best practices:
- JOIN key column-এ NOT NULL constraint যেখানে possible।
- COALESCE/IFNULL/ISNULL সব JOIN/aggregate-এ explicit।
- NULL handling test case write করুন।
- Schema review-এ NULL behavior document।
- "Three-valued logic" (TRUE/FALSE/NULL) মাথায় রাখুন।
Bangladesh real-world:
- Customer phone NULL — registered without phone। JOIN-এ ঐ customer বাদ।
- Foreign student NID NULL — government data merge-এ silently drop।
- Ride-share-এ "rider rating" NULL (নতুন rider) — average inflate বা deflate।
মূল উপলব্ধি: SQL-এ NULL "অজানা" — boolean false নয়। Three-valued logic understand করলে JOIN-এর ৭০% bug avoid। Senior SQL practitioner-রা NULL handling-এ paranoid — এটাই professional discipline।
অনুশীলন
-
Customer + Order JOIN: Daraz-এ গত ৩০ দিনে অর্ডার করা সকল গ্রাহকের নাম, জেলা ও অর্ডার সংখ্যা — top ১০ active customer।
SELECT c.name, c.district, COUNT(o.order_id) AS order_count FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY c.name, c.district ORDER BY order_count DESC LIMIT 10;Note: GROUP BY (পাঠ ৫-এ বিস্তারিত)। INNER JOIN — শুধু order-করা গ্রাহক চাই।
-
Inactive customer খুঁজুন: এমন গ্রাহক যারা গত ৬০ দিনে কোনো অর্ডার করেননি।
-- LEFT JOIN + WHERE NULL pattern SELECT c.customer_id, c.name, c.signup_date FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id AND o.order_date >= CURRENT_DATE - INTERVAL '60 days' WHERE o.order_id IS NULL;Critical: recent date filter ON-এ, WHERE-এ নয়। এতে recent order না থাকা গ্রাহকও inactive হিসেবে আসবে।
-
3-table JOIN: "Electronics" ক্যাটেগরির পণ্য কিনেছেন এমন ঢাকার গ্রাহক — গ্রাহক নাম + পণ্য নাম + অর্ডার তারিখ।
SELECT c.name AS customer, p.name AS product, o.order_date, o.total_amount FROM orders o INNER JOIN customers c ON o.customer_id = c.customer_id INNER JOIN products p ON o.product_id = p.product_id WHERE c.district = 'Dhaka' AND p.category = 'Electronics' ORDER BY o.order_date DESC;Tip: Filter (district + category) JOIN-এর পর WHERE-এ — INNER JOIN-এ এটি ঠিক। LEFT JOIN-এ filter-এর জায়গা মাথায় রাখতে হয় (আগের প্রশ্ন)।
আরও পড়ুন
- পাঠ ০৫ · GROUP BY ও সংখ্যান পরবর্তী পাঠ JOIN + GROUP BY = analytics-এর সবচেয়ে শক্তিশালী combo।
- পাঠ ০৩ · SQL পরিচিতি আগের পাঠ SELECT/WHERE-এ দুর্বল লাগলে আগের পাঠ revisit।
- পাঠ ০৬ · Window function এই পাঠের সাথে সম্পর্কিত JOIN-এর পরের গভীর SQL skill।
- সব AI Courses দেখুন ABCL TECH সব AI কোর্স একসাথে।