পাঠ ০৪ · ৩০-এর মধ্যে · মডিউল ১

JOIN — দুই table একসাথে

SQL JOINs — combining tables
৮ মিনিট পড়া মাঝারি · Intermediate 5 JOIN types

এই পাঠে যা শিখবেন

  • কেন 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 করা হয়।

Daraz-এর normalized schema

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।

SQL
-- প্রতিটি অর্ডারের সাথে গ্রাহকের নাম
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

    
Alias (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।

SQL
-- প্রতিটি গ্রাহকের সব অর্ডার (অর্ডার না করা গ্রাহকও দেখাব)
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;

    
LEFT JOIN-এর pattern সবচেয়ে important: "এই table-এর সব record দেখাও, যদি অন্য table-এ data থাকে যোগ করো।" Customer churn analysis, missing inventory, incomplete user profile — সব এই pattern follow করে।

৪ · RIGHT JOIN ও FULL OUTER JOIN

RIGHT JOIN = LEFT-এর mirror। প্রায় কেউ ব্যবহার করে না — table order swap করে LEFT লিখলেই হয়। FULL OUTER দু'পক্ষের সব রাখে।

SQL
-- 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;

    
MySQL-এ FULL OUTER নেই — workaround: LEFT JOIN UNION RIGHT JOIN। PostgreSQL, Oracle, SQL Server-এ direct support।

৫ · CROSS JOIN — Cartesian product

কোনো ON condition নেই — প্রতিটি row বাঁ-পক্ষের সাথে প্রতিটি row ডান-পক্ষের combine। সাধারণত accident-এ হয় (Cartesian explosion); intentionally কম।

SQL
-- ৪টি 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';

    
সবচেয়ে dangerous bug: ON ভুলে গেলে SQL Cartesian product করে। ১০ লক্ষ × ১০ লক্ষ = ১০¹² row — production crash। সবসময় ON condition double-check করুন।
JOIN types — কোনটি কী রাখে Venn diagram intuition INNER JOIN ∩ A B match-only LEFT JOIN A B A + match RIGHT JOIN A B B + match FULL OUTER A B all rows কোনটি কখন? INNER: matching only · "completed orders with valid customer" LEFT: keep all from main · "all customers, with their orders if any" FULL OUTER: data audit, reconciliation between two systems
Venn diagram — চারটি JOIN-এর intuitive পার্থক্য। শেডেড অংশ = ফলাফলে যা থাকবে।

৬ · Multiple JOIN — chain

বাস্তব query-তে প্রায়ই ৩-৫টি table একসাথে JOIN করতে হয়।

SQL
-- প্রতিটি অর্ডারের গ্রাহক নাম + পণ্য নাম + ক্যাটেগরি
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:

SQL
-- প্রতিটি 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 INT vs customer_id VARCHAR JOIN — 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:

  1. "Missing record-এ আমি কী করব?" - ignore (INNER) vs preserve (LEFT)?
  2. "Aggregation-এ NULL handle করব কীভাবে?" - COALESCE, FILTER।
  3. "Stakeholder কোন number আশা করছেন?" - mismatch বের হয়ে গেলে অস্বস্তিকর প্রশ্ন।
  4. "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:

  1. 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
  2. DISTINCT key check:
    SELECT COUNT(*), COUNT(DISTINCT customer_id)
    FROM orders;
    -- দু'টি ভিন্ন হলে → many orders per customer → fan-out
  3. JOIN cardinality test:
    SELECT customer_id, COUNT(*)
    FROM orders
    GROUP BY customer_id
    HAVING COUNT(*) > 1
    LIMIT 5;
  4. Sanity check post-JOIN: aggregate metric — pre-JOIN-এর সাথে match করছে?

Prevention:

  1. 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;
  2. JOIN cardinality declaration: dbt-এর মতো tool-এ relationships test।
  3. Documentation: "this JOIN expects 1:1" — comment।
  4. Window function alternative: aggregate ছাড়াই row preserve করতে।
  5. 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):

  1. Columnar — শুধু needed column read।
  2. JOIN avoidance — distributed shuffle এড়ানো।
  3. Predicate pushdown — partition pruning সহজ।
  4. 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।

অনুশীলন

  1. 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-করা গ্রাহক চাই।

  2. 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. 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-এর জায়গা মাথায় রাখতে হয় (আগের প্রশ্ন)।

আরও পড়ুন

JOIN visualize করতে চান? SQL Joins Visualizer ব্যবহার করুন — interactive Venn diagram।
পূর্ববর্তী পাঠ
পাঠ ০৩ · SQL পরিচিতি