এই পর্বে যা শিখবেন
- ডেটা কেন ইচ্ছে করে কয়েকটি টেবিলে ভাগ থাকে, আর join কীভাবে Cartesian product থেকে ছেঁকে তৈরি হয়
- INNER JOIN … ON, পুরোনো কমা-join, table alias, আর তিন-চারটি টেবিল একসাথে জোড়া
- LEFT, RIGHT আর FULL OUTER JOIN — NULL কোথা থেকে আসে, আর “যার কিছু নেই” খোঁজার anti-join
- WHERE বনাম ON-এর ফাঁদ, NATURAL JOIN-এর ঝুঁকি, key ছাড়া অন্য কলামে join
- Fan-out-এ দ্বিগুণ যোগফল ধরা ও সারানো, Venn diagram-এর সীমা, আর join দ্রুত করতে FK-তে index
১ · JOIN কেন — ডেটা ইচ্ছে করেই ভাগ করা
পর্ব ১০-এ ER ডায়াগ্রাম থেকে টেবিল বানানোর নিয়ম দেখেছেন: প্রতিটি entity নিজের টেবিলে, আর 1:N সম্পর্ক “N” দিকের টেবিলে একটি foreign key. ফলে একটি অর্ডারের পুরো ছবি — কে কিনলেন, কী কিনলেন, কত দামে — কোনো একটি টেবিলে নেই। গ্রাহকের নাম আছে customers-এ, অর্ডারের তারিখ orders-এ, পণ্যের নাম products-এ। এটি ভুল নয়, পরিকল্পনা: প্রতিটি সত্য এক জায়গায় থাকে, তাই Rahim Uddin-এর ফোন নম্বর বদলাতে একটিই সারি বদলাতে হয়।
পাড়ার মুদি দোকানের কথা ভাবুন। দোকানির একটি খাতায় গ্রাহকদের নাম-ঠিকানা, পাশে নম্বর: ১ নম্বর Rahim ভাই, ২ নম্বর Karima আপা। আরেকটি খাতায় প্রতিদিনের বাকি: “১২ তারিখ, গ্রাহক ১, চাল ৫ কেজি”। মাস শেষে Rahim ভাইয়ের বিল বানাতে দোকানি বাকির খাতায় “১” খোঁজেন আর প্রথম খাতা থেকে নাম-ঠিকানা মিলিয়ে নেন। এই “নম্বর ধরে দুই খাতা মেলানো”-ই JOIN.
এই পর্বে সবচেয়ে বেশি ব্যবহার হবে কোর্সের দোকান ডেটাবেস db-shop: customers (customer_id, name, city, phone, joined_on — ৮ জন), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার), order_items (order_id, product_id, quantity, unit_price — ১৬টি লাইন) আর products (product_id, name, category_id, price, stock — ১০টি পণ্য)। orders.customer_id আঙুল তোলে customers-এর দিকে, আর order_items-এর দুটি কলাম orders আর products-এর দিকে। পরের দিকে বিশ্ববিদ্যালয় ডেটাবেস db-uni-ও লাগবে। প্রতিটি এডিটরের নিচে “নমুনা ডেটা” বাক্সে পুরো ডেটা দেখতে পাবেন।
Join শিখলে আপনি প্রায় যেকোনো ব্যবসায়িক প্রশ্নের উত্তর দিতে পারবেন: “ঢাকার গ্রাহকেরা কোন কোন পণ্য কিনেছেন?”, “কোন পণ্য কখনো বিক্রি হয়নি?”, “কোন কোর্সে এখনো শিক্ষক নেই?” — প্রতিটির উত্তর দুই বা ততোধিক টেবিলে ছড়িয়ে আছে।
২ · Cartesian product থেকে INNER JOIN — সব জোড়া, তারপর ছাঁকনি
Join বোঝার সবচেয়ে পরিষ্কার পথ একটি দুই-ধাপের ছবি। প্রথম ধাপে দুই টেবিলের প্রতিটি সারির সাথে প্রতিটি সারি জোড়া দিন — একে বলে Cartesian product. বাঁ টেবিলে mটি আর ডান টেবিলে nটি সারি থাকলে জোড়া হয় ঠিক m × nটি, কারণ বাঁয়ের প্রতিটি সারি ডানের nটি সারির প্রত্যেকটির সাথে একবার করে বসে। দ্বিতীয় ধাপে শুধু সেই জোড়াগুলো রাখুন, যেখানে শর্ত (যেমন দুই customer_id সমান) সত্য। পর্ব ৩৩-এর relational algebra-র ভাষায়: R ⋈θ S = σθ(R × S). নিচে তিনজন গ্রাহক আর তিনটি অর্ডারের ছোট টেবিলে ধাপ দুটি দেখুন:
| সারি / জোড়া | |
| শর্ত | |
| Rahim কতবার | |
| Tanvir কতবার |
- দুটি ছোট টেবিল। তিনজন গ্রাহক (Rahim ১, Karima ২, Tanvir ৫) আর তিনটি অর্ডার। অর্ডার 1001 আর 1003 Rahim-এর, 1002 Karima-র; Tanvir-এর কোনো অর্ডার নেই।
- Cartesian product — ৯টি জোড়া। কোনো শর্ত নেই, তাই প্রতিটি গ্রাহক প্রতিটি অর্ডারের সাথে একবার বসেছেন: ৩ × ৩ = ৯। অর্থহীন জোড়াও আছে, যেমন “Tanvir আর অর্ডার 1001”.
- প্রতিটি জোড়ায় শর্ত পরীক্ষা। c.id আর o.cust সমান হলে ✓। নয়টির মধ্যে তিনটি ✓: (Rahim, 1001), (Rahim, 1003), (Karima, 1002).
- শুধু ✓ জোড়া রাখা = join-এর ফল। তিনটি সারি, প্রতিটি একটি আসল অর্ডার আর তার আসল গ্রাহক। Rahim দুবার এসেছেন, কারণ তাঁর দুটি অর্ডার।
- যার জোড়া টেকেনি, সে নেই। Tanvir-এর তিনটি জোড়ার একটিও শর্ত মানেনি, তাই ফলে তিনি অনুপস্থিত। এটিই INNER JOIN-এর স্বভাব — আর অংশ ৪-এর LEFT JOIN ঠিক এই সমস্যা সমাধান করে।
এটি যৌক্তিক ছবি, আসল কাজের ধারা নয়। ১০ লাখ গ্রাহক আর ১ কোটি অর্ডারে Cartesian product হতো ১০১৩টি জোড়া — কোনো DBMS এভাবে চালায় না। ফল একই রেখে query optimizer অনেক দ্রুত পথ বাছে (অংশ ৮ আর পর্ব ৪২)। কিন্তু ফল কী হবে বোঝার জন্য “সব জোড়া, তারপর ছাঁকনি” ছবিটি সবসময় সঠিক।
INNER JOIN … ON — লেখার নিয়ম
SQL-এ ওপরের দুই ধাপ একটি বাক্যে লেখা হয়: FROM orders INNER JOIN customers ON শর্ত. INNER JOIN শুধু মিল-পাওয়া জোড়া রাখে; INNER শব্দটি ঐচ্ছিক, শুধু JOIN লিখলেও একই। ON-এর পরের অংশটি join condition — সাধারণত foreign key আর primary key সমান হওয়ার শর্ত। সমান (=) চিহ্নের এমন join-কে বলে equi-join.
DBMS ভেতরে কীভাবে মেলায়, তার সবচেয়ে সরল পদ্ধতি দুটি নেস্টেড লুপ: বাঁ টেবিলের প্রতিটি সারি ধরে ডান টেবিলের সব সারির সাথে শর্ত পরীক্ষা। ধাপে ধাপে দেখুন:
| বাঁয়ের সারি | |
| মোট তুলনা | |
| ফলের সারি |
- শুরু। বাইরের লুপ চলবে customers-এর ওপর, ভেতরের লুপ orders-এর ওপর। ফল এখনো খালি।
- Rahim বনাম 1001. অর্ডার 1001-এর customer_id ১, Rahim-এরও ১ — শর্ত সত্য, ফলে প্রথম সারি: (Rahim, 1001). এখন পর্যন্ত ১টি তুলনা।
- Rahim বনাম বাকি দুটি অর্ডার। 1002 Karima-র (✗), 1003 আবার Rahim-এর (✓). ফলে দ্বিতীয় সারি (Rahim, 1003). Rahim-এর পালা শেষ — মোট ৩টি তুলনা।
- Karima-র পালা। আবার তিনটি অর্ডারই দেখা হলো; শুধু 1002 মেলে। ফলে তৃতীয় সারি, তুলনা হলো মোট ৬টি।
- Tanvir-এর পালা। তিনটি তুলনাই ✗ — INNER JOIN-এ তাঁর জন্য কিছুই যোগ হয় না। মোট তুলনা ৯টি, ফল ৩টি সারি।
- খরচের হিসাব। সরল নেস্টেড লুপে তুলনা = বাঁয়ের সারি × ডানের সারি। ডান টেবিলের join-কলামে index থাকলে ভেতরের লুপ পুরো টেবিল না ঘুরে সরাসরি মেলা সারিতে যায় — অংশ ৮-এ এর হিসাব।
এবার আসল db-shop-এ। নিচের এডিটরে তিনটি কোয়েরি: শর্ত ছাড়া জোড়ার সংখ্যা, পুরোনো কমা-লেখায় শর্তসহ সংখ্যা, আর আধুনিক INNER JOIN … ON দিয়ে প্রতিটি অর্ডারের পাশে গ্রাহকের নাম:
-- ১) শর্ত ছাড়া: সব জোড়া (Cartesian product)
SELECT COUNT(*) AS all_pairs FROM customers, orders;
-- ২) পুরোনো কমা-join: শর্ত WHERE-এ
SELECT COUNT(*) AS matched FROM customers, orders
WHERE customers.customer_id = orders.customer_id;
-- ৩) আধুনিক লেখা: INNER JOIN … ON, alias সহ
SELECT o.order_id, o.order_date, c.name, c.city, o.status
FROM orders AS o
INNER JOIN customers AS c ON c.customer_id = o.customer_id
ORDER BY o.order_id;প্রথম ফল 80 — ৮ জন গ্রাহক × ১০টি অর্ডার। দ্বিতীয় ফল 10: আশিটি জোড়ার মধ্যে মাত্র দশটিতে customer_id মেলে, প্রতিটি অর্ডারের জন্য ঠিক একটি, কারণ orders.customer_id NOT NULL আর একটিই গ্রাহকের দিকে নির্দেশ করে। তৃতীয় কোয়েরিতে সেই দশটি সারি: 1001 | 2025-09-01 | Rahim Uddin | Dhaka | delivered থেকে 1010 | 2025-10-09 | Imran Hossain | NULL | delivered পর্যন্ত। Rahim Uddin তিনবার (1001, 1003, 1009), Karima Begum দুবার (1002, 1007). Tanvir Ahmed একবারও নেই — তাঁর কোনো অর্ডার নেই। Imran-এর city NULL, কিন্তু তিনি আছেন, কারণ join-শর্ত city নয়, customer_id.
পুরোনো কমা-join বনাম আধুনিক JOIN
দ্বিতীয় আর তৃতীয় কোয়েরির join একই কাজ করে, আর DBMS দুটিকে সাধারণত একই plan-এ চালায়। কমা দিয়ে টেবিল লিখে শর্ত WHERE-এ দেওয়া SQL-এর প্রথম দিকের রীতি; JOIN … ON আসে ১৯৯২ সালের SQL-92 standard-এ। আজ প্রায় সব style guide আধুনিক লেখাটি পছন্দ করে, তিনটি কারণে:
- শর্ত ভুলে যাওয়া সহজে ধরা পড়ে। পাঁচটি টেবিল কমায় লিখে একটি শর্ত ভুলে গেলে কোনো ত্রুটি আসে না — চুপচাপ Cartesian product, ফল শতগুণ। আধুনিক লেখায় প্রতিটি
JOIN-এর পাশে একটিONথাকার কথা, তাই অভাবটি চোখে পড়ে; PostgreSQL তোONছাড়াJOIN-কে সরাসরি syntax error বলে (MySQL আর SQLite অবশ্য সেটিকে CROSS JOIN ধরে চালিয়ে দেয়)। - সম্পর্ক আর ছাঁকনি আলাদা থাকে। ON-এ লেখা থাকে “টেবিলগুলো কীভাবে যুক্ত”, WHERE-এ “কোন সারি চাই”। পড়তে, রিভিউ করতে সহজ।
- Outer join কমা দিয়ে লেখাই যায় না। LEFT/RIGHT/FULL-এর জন্য JOIN লেখা লাগবেই, তাই এক কোয়েরিতে দুই রীতি না মিশিয়ে সবখানে একটি রাখাই ভালো।
Standard আসার আগে প্রতিটি কোম্পানি নিজের outer join চিহ্ন বানিয়েছিল। পুরোনো কোডে এখনো দেখতে পাবেন — চিনে রাখুন, নতুন করে লিখবেন না:
-- Oracle-এর পুরোনো outer join: (+) চিহ্ন যেদিকে, সেদিকে NULL বসতে পারে
SELECT c.name, o.order_id
FROM customers c, orders o
WHERE c.customer_id = o.customer_id(+);
-- আজকের standard লেখা, একই ফল
SELECT c.name, o.order_id
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id;Table alias — ছোট নাম
তৃতীয় কোয়েরিতে orders AS o আর customers AS c হলো table alias — কোয়েরির ভেতরে টেবিলের ডাকনাম। AS শব্দটি ঐচ্ছিক (orders o), আর Oracle-এ টেবিল-alias-এর আগে AS লেখাই যায় না, তাই অনেকে AS বাদ দিয়ে অভ্যাস করেন। Alias তিনটি কাজ করে: লেখা ছোট করে; একই নামের কলাম আলাদা করে (দুই টেবিলেই customer_id — শুধু customer_id লিখলে ত্রুটি আসে “ambiguous column name”); আর একই টেবিলকে দুবার ব্যবহার করতে দেয়, যা পর্ব ২০-এর self join-এর ভিত্তি। ভালো অভ্যাস: join থাকলে প্রতিটি কলামের আগে alias লিখুন, এমনকি নাম অনন্য হলেও — কাল কেউ অন্য টেবিলে একই নামের কলাম যোগ করলে কোয়েরি ভাঙবে না।
৩ · তিন-চারটি টেবিল একসাথে — সারি কোথায় গুণ হয়
বাস্তবের প্রশ্নে প্রায়ই চার-পাঁচটি টেবিল লাগে। “Rahim Uddin কোন কোন পণ্য কিনেছেন, কোন অর্ডারে, কত টাকার?” — নাম customers-এ, অর্ডার orders-এ, পরিমাণ আর দাম order_items-এ, পণ্যের নাম products-এ। JOIN একটির পর একটি যোগ হয়: প্রথম দুই টেবিলের join-এর ফলের সাথে তৃতীয় টেবিল, তার ফলের সাথে চতুর্থ। প্রতিটি ধাপে সারির সংখ্যা কী হয়, লক্ষ করুন:
| সারি | |
| এই join-এর দিক | |
| Rahim কতবার |
- শুরু এক সারিতে। বোঝার সুবিধায় শুরুতেই Rahim Uddin-এ সীমিত রাখছি (যৌক্তিকভাবে WHERE পরে আসে, কিন্তু ফল একই — optimizer-ও প্রায়ই এভাবে ছাঁকনি আগে প্রয়োগ করে)।
- orders যোগ — ১ থেকে ৩। একজন গ্রাহকের অনেক অর্ডার (1:N), তাই Rahim-এর সারিটি তাঁর তিনটি অর্ডারের প্রতিটির জন্য একবার করে আসে।
- order_items যোগ — ৩ থেকে ৬। প্রতিটি অর্ডারে দুটি করে লাইন, তাই প্রতিটি অর্ডার-সারি দুবার। এখন নাম “Rahim” ছয়বার, প্রতিটি অর্ডার নম্বর দুবার করে।
- products যোগ — ৬-ই থাকে। প্রতিটি লাইনের একটিই পণ্য (N:1), তাই নতুন সারি তৈরি হয় না, শুধু পণ্যের নাম যোগ হয়। একই পণ্য (Mobile Phone) দুটি অর্ডারে থাকায় দুবার এসেছে — সেটি আলাদা দুটি লাইন।
- নিয়ম। FK ধরে “অনেক” দিকে গেলে সারি গুণ হয়, “এক” দিকে গেলে বদলায় না। ফলের প্রতিটি সারি কী বোঝায় — এখানে “একটি অর্ডারের একটি লাইন” — একে বলে ফলের grain. অংশ ৭-এর fan-out ফাঁদ এই grain ভুলে যাওয়া থেকেই আসে।
-- Rahim Uddin-এর সব কেনা: চারটি টেবিল
SELECT o.order_id, p.name AS product, oi.quantity,
oi.quantity * oi.unit_price AS line_total
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE c.name = 'Rahim Uddin'
ORDER BY o.order_id, p.name;ছয়টি সারি — ঠিক স্টেপারের মতো: 1001 | Headphone | 2 | 2400, 1001 | Mobile Phone | 1 | 15000, 1003 | Mustard Oil 1L | 2 | 520, 1003 | Rice 5kg | 3 | 1260, 1009 | Mobile Phone | 1 | 14500, 1009 | Smart Watch | 1 | 3500. লক্ষ করুন দাম নেওয়া হয়েছে oi.unit_price থেকে (বিক্রির দিনের দাম — পর্ব ১১), তাই 1009-এর মোবাইল 14500. WHERE সরিয়ে দিন: ফল হবে ১৬টি সারি — order_items-এর প্রতিটি লাইন একবার, কারণ বাকি তিনটি join-ই “এক” দিকে।
N টেবিল জুড়তে সাধারণত N − 1টি join-শর্ত লাগে — প্রতিটি নতুন টেবিলের জন্য একটি। শর্ত কম হলে কোথাও Cartesian product লুকিয়ে আছে। এটি একটি দ্রুত পরীক্ষা, AI-এর লেখা কোয়েরিতেও কাজে লাগে।
৪ · LEFT JOIN — বাঁ দিকের কেউ বাদ যাবে না
INNER JOIN প্রশ্ন করে “কোন জোড়া মেলে?” কিন্তু অনেক প্রশ্ন আসলে “সব গ্রাহকের তালিকা দাও, অর্ডার থাকলে পাশে লিখে দাও”। এর জন্য outer join, আর সবচেয়ে বেশি ব্যবহৃত রূপ LEFT JOIN (পুরো নাম LEFT OUTER JOIN; OUTER শব্দটি ঐচ্ছিক)। নিয়ম: প্রথমে INNER JOIN-এর মতো সব মিল-পাওয়া জোড়া; তারপর বাঁ টেবিলের যে সারি একটিও জোড়া পায়নি, তাকে একবার যোগ করা — ডান দিকের সব কলামে NULL বসিয়ে।
| বাঁয়ের সারি | |
| মিল-পাওয়া সারি | |
| NULL-ভরা সারি | |
| ফলের সারি |
- একই দুটি টেবিল, এবার LEFT JOIN. বাঁ টেবিল customers — কারণ প্রশ্নটি গ্রাহকদের নিয়ে, আর কোনো গ্রাহক হারানো চলবে না।
- Rahim — INNER-এর মতোই। দুটি অর্ডার মেলে, দুটি সারি। মিল থাকলে LEFT আর INNER-এ কোনো পার্থক্য নেই।
- Karima — একটি মিল, একটি সারি। এখনো ফল INNER JOIN-এর সাথে হুবহু এক।
- Tanvir — পার্থক্য এখানেই। তিনটি তুলনাই ব্যর্থ। INNER JOIN তাঁকে বাদ দিত; LEFT JOIN তাঁকে একবার রাখে, আর orders-এর সব কলামে NULL বসায় — “এখানে মেলানোর মতো কিছু ছিল না”।
- পূর্ণ ফল: ৪টি সারি। = ৩টি মিল-পাওয়া জোড়া + ১টি NULL-ভরা সারি। নিশ্চয়তা: বাঁ টেবিলের প্রতিটি সারি ফলে অন্তত একবার আছে।
- IS NULL দিয়ে “যার নেই” খোঁজা। orders-এর primary key order_id কখনো NULL হয় না, তাই এখানে NULL মানেই “কোনো অর্ডার মেলেনি”। ফলে শুধু Tanvir. এই কৌশলের নাম anti-join.
বিশ্ববিদ্যালয় ডেটাবেসে একই পার্থক্য দেখুন। db-uni-র students (student_id, name, dept_id, batch, cgpa — ৮ জন) আর departments (dept_id, name, building — ৪টি বিভাগ)। Habib Ullah এখনো বিভাগ বাছেননি, তাঁর dept_id NULL:
-- INNER: বিভাগ মিললে তবেই
SELECT s.name, d.name AS dept
FROM students s INNER JOIN departments d ON d.dept_id = s.dept_id
ORDER BY s.student_id;
-- LEFT: সব শিক্ষার্থী, বিভাগ না থাকলে NULL
SELECT s.name, d.name AS dept
FROM students s LEFT JOIN departments d ON d.dept_id = s.dept_id
ORDER BY s.student_id;প্রথম ফলে ৭টি সারি — Arif Hossain (CSE) থেকে Gulshan Ara (CSE); Habib Ullah নেই। কারণ NULL = 1, NULL = 2 … প্রতিটি তুলনার ফল UNKNOWN (পর্ব ১৬-এর তিন-মানের যুক্তি), আর join শুধু TRUE জোড়া রাখে। দ্বিতীয় ফলে ৮টি সারি, শেষটি Habib Ullah | NULL. “আমাদের কতজন শিক্ষার্থী?” প্রশ্নের উত্তর INNER JOIN দিয়ে গুনলে ৭ আসত — একজন চুপচাপ হারিয়ে যেতেন। আর English বিভাগ (dept_id 4) কোনো ফলেই নেই: সেখানে কোনো শিক্ষার্থী নেই, আর departments এখানে ডান টেবিল।
যার কিছু নেই — anti-join
“কোন গ্রাহক কখনো অর্ডার করেননি?” আর “কোন পণ্য কখনো বিক্রি হয়নি?” — দুটিই একই ছাঁচ: LEFT JOIN, তারপর WHERE ডান_টেবিলের_NOT_NULL_কলাম IS NULL. মনে রাখবেন = NULL নয়, IS NULL — = NULL সবসময় UNKNOWN, তাই কোনো সারিই আসে না।
-- কোন গ্রাহক কখনো অর্ডার করেননি?
SELECT c.customer_id, c.name
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
-- কোন পণ্য কখনো বিক্রি হয়নি?
SELECT p.product_id, p.name, p.stock
FROM products p LEFT JOIN order_items oi ON oi.product_id = p.product_id
WHERE oi.product_id IS NULL;প্রথম ফল একটি সারি: 5 | Tanvir Ahmed. দ্বিতীয় ফলও একটি: 103 | Power Bank | 0 — দশটি পণ্যের নয়টি অন্তত একবার বিক্রি হয়েছে, Power Bank একবারও নয়, আর স্টকও শূন্য। দোকানের মালিকের জন্য এটি কাজের তথ্য: হয় পণ্যটি আবার আনবেন, নয়তো তালিকা থেকে সরাবেন। IS NULL-এর কলাম বাছুন ডান টেবিলের এমন কলাম, যা কখনো NULL হয় না (primary key বা join-কলাম) — নইলে “মিল নেই” আর “মিল আছে কিন্তু মানটি NULL” গুলিয়ে যাবে। একই প্রশ্ন NOT EXISTS দিয়েও লেখা যায় — পর্ব ২০ আর ২১-এ।
সবচেয়ে প্রচলিত ফাঁদ — ডান টেবিলের শর্ত WHERE-এ
প্রশ্ন: “প্রত্যেক গ্রাহকের কয়টি delivered অর্ডার? যাঁর একটিও নেই, তাঁর পাশে 0.” স্বাভাবিকভাবে মনে আসে LEFT JOIN, তারপর WHERE o.status = 'delivered'. দুটি লেখা পাশাপাশি চালান:
-- ভুল: ডান টেবিলের শর্ত WHERE-এ
SELECT c.name, COUNT(o.order_id) AS delivered
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered'
GROUP BY c.customer_id, c.name ORDER BY c.customer_id;
-- ঠিক: শর্ত ON-এ, join-এর অংশ হিসেবে
SELECT c.name, COUNT(o.order_id) AS delivered
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
AND o.status = 'delivered'
GROUP BY c.customer_id, c.name ORDER BY c.customer_id;প্রথম কোয়েরিতে মাত্র ৪টি সারি: Rahim Uddin 1, Karima Begum 2, Nusrat Jahan 1, Imran Hossain 1. দ্বিতীয়টিতে ৮ জনই — সাথে Sabbir Hasan 0, Tanvir Ahmed 0, Farhana Akter 0, Mitu Rani 0. প্রথমটিতে কী ঘটল? যৌক্তিক ক্রম মনে করুন (পর্ব ১৫): আগে FROM আর JOIN পুরো হয় — Tanvir-এর সারি NULL-ভরা হয়ে যোগ হয়; তারপর WHERE চলে। NULL = 'delivered' UNKNOWN, তাই Tanvir বাদ; Sabbir-এর একমাত্র অর্ডার cancelled, তাই তিনিও বাদ। LEFT JOIN যে সারিগুলো যত্ন করে রেখেছিল, WHERE সেগুলোই ফেলে দিল — ফল হুবহু INNER JOIN-এর মতো। দ্বিতীয়টিতে status-শর্ত মেলানোর অংশ: “delivered অর্ডার থাকলে জুড়ে দাও”; না থাকলে গ্রাহক NULL-সহ থাকেন, আর COUNT(o.order_id) NULL গোনে না বলে 0 আসে (পর্ব ১৮)।
LEFT JOIN-এ ডান টেবিলের শর্ত (কোন অর্ডার জুড়ব) → ON-এ। বাঁ টেবিলের শর্ত (কোন গ্রাহক দেখাব) → WHERE-এ। উল্টোটিও ফাঁদ: বাঁ টেবিলের শর্ত ON-এ লিখলে (ON … AND c.city = 'Dhaka') কোনো গ্রাহক বাদ যান না — শুধু ঢাকার বাইরের সবার অর্ডার NULL হয়ে যায়। ব্যতিক্রম একটিই: ইচ্ছে করে WHERE ডান.key IS NULL লেখা anti-join.
৫ · RIGHT JOIN আর FULL OUTER JOIN
RIGHT JOIN হলো LEFT JOIN-এর আয়নার প্রতিবিম্ব: ডান টেবিলের প্রতিটি সারি থাকে, বাঁ দিকে মিল না পেলে NULL. A RIGHT JOIN B আর B LEFT JOIN A একই সারি দেয় (শুধু কলামের ক্রম আলাদা হতে পারে)। তাই বেশিরভাগ দল একটি রীতি মানে: সবসময় LEFT JOIN, যে টেবিলের কাউকে হারানো চলবে না সেটি বাঁয়ে। FULL OUTER JOIN দুই দিকের সবাইকে রাখে: মিল-পাওয়া জোড়া, বাঁয়ের অমিল সারি (ডানে NULL), আর ডানের অমিল সারি (বাঁয়ে NULL). সারির সংখ্যা তাই = INNER-এর সারি + বাঁয়ের অমিল + ডানের অমিল।
db-uni-র courses (course_id, title, dept_id, credits, instructor_id) আর instructors (instructor_id, name, dept_id, salary, mentor_id) নিন। ENG101-এর শিক্ষক এখনো ঠিক হয়নি (instructor_id NULL), আবার একজন শিক্ষক কোনো কোর্স পড়ান না। দুটিই একসাথে দেখতে FULL লাগে:
-- RIGHT: সব কোর্স, শিক্ষক না থাকলে NULL
SELECT c.course_id, c.title, i.name AS instructor
FROM instructors i RIGHT JOIN courses c ON c.instructor_id = i.instructor_id
ORDER BY c.course_id;
-- FULL: সব কোর্স আর সব শিক্ষক
SELECT c.course_id, i.name AS instructor
FROM courses c FULL OUTER JOIN instructors i ON i.instructor_id = c.instructor_id
ORDER BY c.course_id, i.name;RIGHT JOIN-এ ৭টি সারি — সব কোর্স, শেষটি ENG101 | English Language | NULL. FULL OUTER JOIN-এ ৮টি সারি: সেই ৭টি, আর প্রথমে NULL | Rumana Islam — একজন শিক্ষক, যাঁর কোনো কোর্স নেই (SQLite-এ ছোট থেকে বড় সাজালে NULL সবার আগে আসে)। হিসাব: মিল-পাওয়া জোড়া ৬, বাঁয়ের (courses) অমিল ১ (ENG101), ডানের (instructors) অমিল ১ (Rumana) — মোট ৮। Tasnim Chowdhury দুবার আছেন, কারণ তিনি CSE301 আর CSE302 দুটিই পড়ান।
SQLite বহু বছর শুধু INNER আর LEFT JOIN চালাত; RIGHT বা FULL লিখলে ত্রুটি দিত “RIGHT and FULL OUTER JOINs are not currently supported”. ২০২২ সালের জুনে SQLite 3.39.0-এ দুটিই যোগ হয়। তাই পুরোনো ফোন বা পুরোনো Python-এর সাথে আসা SQLite-এ ওপরের কোয়েরি ত্রুটি দিতে পারে — SELECT sqlite_version(); চালিয়ে দেখে নিন। এই পাতার এডিটর নতুন সংস্করণে চলে। PostgreSQL, SQL Server আর Oracle অনেক আগে থেকেই তিনটি outer join চালায়; MySQL আর MariaDB এখনো FULL OUTER JOIN চালায় না।
MySQL-এ FULL OUTER JOIN — UNION দিয়ে বানানো
MySQL-এ FULL লিখলে syntax error আসে। প্রচলিত সমাধান: LEFT JOIN-এর ফল, আর তার সাথে উল্টো দিকের anti-join (ডানের যে সারির বাঁয়ে কিছু নেই) — UNION ALL দিয়ে জোড়া। UNION দুই কোয়েরির ফল একটির নিচে আরেকটি বসায় (পর্ব ২২)। নিচের কোয়েরিটি MySQL, PostgreSQL আর SQLite — তিন জায়গাতেই চলে:
-- ভাগ ১: সব কোর্স (শিক্ষক থাকুক বা না থাকুক)
SELECT c.course_id, i.name AS instructor
FROM courses c LEFT JOIN instructors i ON i.instructor_id = c.instructor_id
UNION ALL
-- ভাগ ২: শুধু কোর্সহীন শিক্ষক (anti-join), যাতে কেউ দুবার না আসে
SELECT c.course_id, i.name
FROM instructors i LEFT JOIN courses c ON c.instructor_id = i.instructor_id
WHERE c.course_id IS NULL
ORDER BY 1, 2;ফল FULL OUTER JOIN-এর সাথে হুবহু এক — ৮টি সারি, শুরুতে NULL | Rumana Islam, শেষে ENG101 | NULL. অনেক টিউটোরিয়ালে লেখা থাকে LEFT JOIN … UNION … RIGHT JOIN (ALL ছাড়া)। সেটিও সাধারণত কাজ করে, কারণ UNION ডুপ্লিকেট সরায় — কিন্তু যদি ফলে আসল ডুপ্লিকেট সারি থাকে (যেমন একই শিক্ষক একই নামের দুটি কোর্সে), UNION সেগুলোও মুছে দেবে। ওপরের UNION ALL + anti-join ধরন কোনো সারি মোছে না, আর ডুপ্লিকেট সরানোর বাড়তি কাজও করে না।
৬ · USING, NATURAL JOIN আর key ছাড়া অন্য কলামে join
USING — একই নামের কলামে join-এর সংক্ষেপ
দুই টেবিলে join-কলামের নাম এক হলে (students.dept_id আর departments.dept_id) লেখা যায় JOIN departments USING (dept_id), যা ON s.dept_id = d.dept_id-এর সমান। বাড়তি সুবিধা: ফলে dept_id কলামটি একবারই থাকে। USING নিরাপদ, কারণ কোন কলাম মেলানো হচ্ছে তা আপনি নিজে লিখে দেন। SQL Server এটি চালায় না; বাকি বড় DBMS-গুলো চালায়।
NATURAL JOIN — সুবিধাজনক, কিন্তু বিপজ্জনক
NATURAL JOIN নিজেই খুঁজে নেয় দুই টেবিলে যত কলামের নাম এক, আর সবগুলোর সমতাকে শর্ত বানায়। কাগজে সুন্দর, বাস্তবে ফাঁদ — শর্তটি কোয়েরিতে লেখা থাকে না, থাকে schema-য়, আর schema বদলায়। db-uni-তে চালিয়ে দেখুন:
-- USING: শুধু dept_id মেলাও
SELECT student_id, s.name, d.name AS dept
FROM students s JOIN departments d USING (dept_id)
ORDER BY student_id;
-- NATURAL: নামের মিল থাকা সব কলাম — কোনগুলো?
SELECT COUNT(*) AS natural_rows FROM students NATURAL JOIN departments;
-- courses-এ আজ dept_id-ই একমাত্র মিল
SELECT COUNT(*) AS before_change FROM courses NATURAL JOIN departments;
-- কেউ courses-এ একটি name কলাম যোগ করলেন…
ALTER TABLE courses ADD COLUMN name TEXT;
SELECT COUNT(*) AS after_change FROM courses NATURAL JOIN departments;USING-এ ৭টি সারি (Habib-এর dept_id NULL বলে বাদ)। কিন্তু natural_rows = 0! students আর departments দুটিতেই আছে dept_id আর name, তাই NATURAL JOIN শর্ত বানিয়েছে “dept_id সমান এবং শিক্ষার্থীর নাম = বিভাগের নাম” — কোনো শিক্ষার্থীর নাম 'CSE' নয়। courses-এ আজ মিল শুধু dept_id, তাই before_change = 7. তারপর একটি নিরীহ ALTER TABLE — আর after_change = 0. কোয়েরির একটি অক্ষরও বদলায়নি, কোনো ত্রুটিও আসেনি, অথচ রিপোর্ট খালি। এ কারণেই production কোডে NATURAL JOIN প্রায় নিষিদ্ধ; ON বা USING লিখুন।
key নয় এমন কলামে join — আর অসমতার শর্ত
Join-শর্ত FK = PK হতেই হবে, এমন কোনো নিয়ম নেই — যেকোনো সত্য-মিথ্যা শর্ত চলে। যেমন দামের ভিত্তিতে পণ্যকে শ্রেণিতে ফেলা: একটি ছোট টেবিল price_bands (band, min_price, max_price) — Budget 0–999, Mid 1000–4999, Premium 5000–1000000 টাকা — এডিটরের সাথে তৈরি হয়। শর্তে = নেই, আছে BETWEEN; এমন join-কে বলে theta join বা non-equi join:
-- দামের পরিসরে join
SELECT b.band, COUNT(*) AS products, MIN(p.price) AS cheapest, MAX(p.price) AS costliest
FROM products p
JOIN price_bands b ON p.price BETWEEN b.min_price AND b.max_price
GROUP BY b.band ORDER BY MIN(p.price);
-- কেউ একটি পরিসর যোগ করলেন, যা Mid-এর সাথে মিশে যায়
INSERT INTO price_bands VALUES ('Gift', 1000, 2500);
SELECT COUNT(*) AS rows_now
FROM products p
JOIN price_bands b ON p.price BETWEEN b.min_price AND b.max_price;প্রথম ফল: Budget | 4 | 260 | 650, Mid | 5 | 1200 | 4500, Premium | 1 | 15000 | 15000 — মোট ১০টি পণ্য, প্রতিটি ঠিক একবার। তারপর Gift (1000–2500) যোগ হলো, আর rows_now = 13: Headphone (1200), Power Bank (1800) আর Panjabi (2200) এখন দুটি পরিসরেই পড়ে, তাই দুবার করে। PK-FK join-এ “এক” দিকে যাওয়া মানে সর্বোচ্চ একটি মিল — primary key সেটি নিশ্চিত করে। key নয় এমন কলামে সেই নিশ্চয়তা নেই; পরিসর না-মেশার নিয়মটি ডেটাবেস নিজে পাহারা দেয় না। শহর ধরে join (customers.city = delivery_zones.city)-এও একই কথা — আর Imran-এর মতো NULL শহর কখনো মিলবে না।
৭ · Fan-out — join-এর পর যোগফল দ্বিগুণ
অংশ ৩-এ দেখেছেন: orders থেকে order_items-এ গেলে প্রতিটি অর্ডার-সারি তার লাইনের সংখ্যা যতবার, ততবার আসে। লাইন-স্তরের হিসাব (পরিমাণ × দাম) তাতে ঠিকই থাকে, কারণ প্রতিটি লাইন একবারই আছে। কিন্তু অর্ডার-স্তরের কোনো সংখ্যা — ডেলিভারি চার্জ, অর্ডারের ছাড়, পরিশোধিত টাকা — এখন বারবার এসেছে। তার ওপর SUM চালালে ফল গুণ হয়ে যায়। একে বলে fan-out: এক সারি পাখার মতো ছড়িয়ে অনেক সারি। ধরুন প্রতিটি অর্ডারে ৬০ টাকা ডেলিভারি চার্জ (একটি delivery_fee কলাম); Karima Begum-এর দুটি অর্ডার, প্রতিটিতে দুটি লাইন:
| সারি | |
| ফলের grain | |
| মোট চার্জ |
- শুধু orders-এ সব ঠিক। Karima-র দুটি অর্ডার, প্রতিটিতে ৬০ টাকা চার্জ। প্রতিটি সারি একটি অর্ডার, তাই যোগফল ১২০ — সঠিক।
- order_items যোগ — ২ থেকে ৪ সারি। প্রতিটি অর্ডারে দুটি লাইন, তাই প্রতিটি অর্ডারের সারি (আর তার ৬০ টাকা) দুবার। ফলের grain বদলে “অর্ডারের লাইন” হয়ে গেছে।
- SUM-এ দ্বিগুণ। চার্জের যোগ ২৪০ — দ্বিগুণ, কারণ প্রতিটি ৬০ দুবার গোনা হয়েছে। অথচ লাইনের মোট ৫০০০ সঠিক: লাইন-স্তরের কলাম এই grain-এরই। একই কোয়েরির দুটি কলামের একটি সঠিক, একটি ভুল — এ কারণেই ভুলটি সহজে চোখে পড়ে না।
- SUM(DISTINCT) কোনো সমাধান নয়। DISTINCT মান দেখে, অর্ডার দেখে না। দুটি অর্ডারের চার্জই ৬০ বলে একবার গোনা হলো — ফল ৬০, এবার অর্ধেক। মান কাকতালীয়ভাবে আলাদা হলে ফল ঠিক দেখাত, আর বাগটি লুকিয়ে থাকত।
- সমাধান: আগে যোগ, তারপর join. order_items-কে অর্ডার-প্রতি এক সারিতে নামিয়ে আনুন (একে বলে pre-aggregation), তারপর orders-এর সাথে join করুন। এখন দুই পাশই অর্ডার-স্তরের, join “এক → এক”, কোনো সারি গুণ হয় না: চার্জ ১২০, পণ্য ৫০০০ — দুটিই সঠিক।
এডিটরের সাথে orders টেবিলে delivery_fee কলাম যোগ হয় (সব অর্ডারে ৬০)। প্রথম কোয়েরি ফাঁদটি দেখায়, দ্বিতীয়টি সমাধান। দ্বিতীয়টির FROM-এ বন্ধনীর ভেতরের SELECT হলো একটি derived table — একটি কোয়েরির ফলকে টেবিলের মতো ব্যবহার; বিস্তারিত পর্ব ২১-এ, আর একই কাজ CTE দিয়ে পর্ব ২৫-এ।
-- ফাঁদ: লাইনের সাথে join করে অর্ডার-স্তরের চার্জ যোগ
SELECT c.name, COUNT(*) AS rows_seen, SUM(o.delivery_fee) AS fees_wrong,
SUM(DISTINCT o.delivery_fee) AS fees_distinct
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
WHERE c.customer_id IN (1, 2) GROUP BY c.customer_id, c.name;
-- সমাধান: আগে অর্ডার-প্রতি লাইনের যোগ, তারপর join
SELECT c.name, COUNT(*) AS orders, SUM(o.delivery_fee) AS fees, SUM(t.items) AS items
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
JOIN (SELECT order_id, SUM(quantity * unit_price) AS items
FROM order_items GROUP BY order_id) t ON t.order_id = o.order_id
WHERE c.customer_id IN (1, 2) GROUP BY c.customer_id, c.name;ফাঁদের ফল: Rahim Uddin | 6 | 360 | 60 আর Karima Begum | 4 | 240 | 60. Rahim-এর তিনটি অর্ডারে আসল চার্জ ১৮০, কিন্তু ছয়টি লাইনে ছড়িয়ে ৩৬০; DISTINCT দিয়ে ৬০। সমাধানের ফল: Rahim Uddin | 3 | 180 | 37180 আর Karima Begum | 2 | 120 | 5000 — চার্জ আর পণ্যের মোট দুটিই সঠিক, আর COUNT(*) এখন সত্যিই অর্ডারের সংখ্যা। ফাঁদ ধরার সহজ অভ্যাস: join যোগ করার আগে আর পরে COUNT(*) দেখুন। সারি বাড়লে নিজেকে জিজ্ঞেস করুন, “আমার প্রতিটি SUM-এর কলাম কি এই নতুন grain-এর?”
দুটি “অনেক” দিক একসাথে হলে আরও খারাপ। একটি অর্ডারের ৩টি লাইন আর ২টি পেমেন্ট থাকলে orders-এর সাথে order_items আর payments দুটিই join করলে সেই অর্ডারের ৩ × ২ = ৬টি সারি হয় — লাইনের মোট দ্বিগুণ, পেমেন্টের মোট তিনগুণ। দুটি স্বাধীন 1:N শাখা কখনো এক join-এ মেলাবেন না; প্রতিটিকে আলাদা করে অর্ডার-স্তরে যোগ করে তারপর জুড়ুন।
৮ · Venn diagram-এর সীমা, আর join-এর গতি
Join আসলে Venn diagram নয়
ইন্টারনেটে join বোঝাতে প্রায়ই দুটি মেশানো বৃত্ত আঁকা হয়: INNER মানে মাঝের অংশ, LEFT মানে বাঁ বৃত্ত পুরোটা। প্রথম অনুভূতির জন্য ছবিটি মন্দ নয়, কিন্তু এটি তিনটি জায়গায় ভুল শেখায়:
| Venn diagram যা বলে | আসলে যা ঘটে | এই পর্বের প্রমাণ |
|---|---|---|
| দুই বৃত্তে একই ধরনের জিনিস, মাঝে “সাধারণ সদস্য” | দুই টেবিলের সারি আলাদা ধরনের; ফলের প্রতিটি সারি একটি নতুন জোড়া (গ্রাহক + অর্ডার) | অংশ ২: ফলের কলাম দুই টেবিলের মিলিত |
| INNER-এর ফল কোনো বৃত্তের চেয়ে বড় হতে পারে না | এক সারি অনেক সারির সাথে মিলতে পারে; ফল দুই টেবিলের চেয়েও বড় হতে পারে (সর্বোচ্চ m × n) | Rahim ১ সারি থেকে ৬ সারি; fan-out |
| শর্তের কোনো ভূমিকা নেই, শুধু “সাধারণ” | ফল পুরোপুরি ON-শর্তের ওপর নির্ভর করে; অসমতার শর্তও চলে | অংশ ৬: price_bands, NATURAL JOIN-এ ০ সারি |
Venn diagram আসলে মানায় পর্ব ২২-এর UNION, INTERSECT আর EXCEPT-এ, যেখানে দুই কোয়েরির ফল একই গঠনের সারির set. Join-এর জন্য সঠিক মানসিক ছবি এই পর্বের স্টেপারগুলো: বাঁয়ের প্রতিটি সারি ধরে ডানে মিল খোঁজা, যতগুলো মেলে ততগুলো জোড়া, আর outer join-এ মিল না পেলে একটি NULL-জোড়া।
Join দ্রুত হয় কীভাবে — একটি প্রাথমিক ধারণা
অংশ ২-এর সরল নেস্টেড লুপে তুলনা হয় m × nটি — দুই টেবিলে এক লাখ করে সারি থাকলে ১০০০ কোটি তুলনা। কিন্তু ডান টেবিলের join-কলামে index (সাধারণত B+ tree) থাকলে বাঁয়ের প্রতিটি সারির জন্য পুরো ডান টেবিল না ঘুরে index-এ খোঁজা যায়, যার খরচ মোটামুটি log n-এর সমানুপাতিক। মোট খরচ তখন প্রায় m × log n + মিল-পাওয়া সারির সংখ্যা — এক লাখ সারিতে ২০-এর কাছাকাছি ধাপ, এক লাখ নয়। একে বলে index nested loop join. এর বাইরে বড় টেবিলের জন্য আছে hash join (ছোট টেবিল দিয়ে একটি hash table বানিয়ে বড়টি একবার পড়া) আর sort-merge join; কোনটি কখন বাছা হয় আর তার I/O খরচ — পর্ব ৪২-এ।
ব্যবহারিক শিক্ষাটি এখনই নিন: foreign key কলামে index রাখুন। join প্রায় সবসময় FK = PK, আর PK-তে index নিজে থেকেই থাকে, কিন্তু FK-তে সব DBMS নিজে বানায় না:
-- SQLite আর PostgreSQL FK কলামে নিজে index বানায় না — নিজে বানান
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_items_product ON order_items(product_id);
-- order_items(order_id, product_id)-এর primary key-ই order_id দিয়ে খোঁজায় কাজে লাগে
-- MySQL (InnoDB) FK-র জন্য index না থাকলে নিজেই একটি বানিয়ে নেয়index ফল বদলায় না, শুধু গতি — তাই ছোট নমুনা ডেটায় পার্থক্য দেখবেন না। পর্ব ৪০–৪১-এ index আর EXPLAIN দিয়ে plan পড়া শিখবেন।
আরও গভীরে: optimizer INNER JOIN-গুলো যেকোনো ক্রমে সাজাতে পারে, কারণ INNER JOIN commutative আর associative — কিন্তু outer join-এ সেই স্বাধীনতা নেই, (A LEFT JOIN B) JOIN C আর A LEFT JOIN (B JOIN C) সাধারণভাবে আলাদা ফল দেয়। তবে PostgreSQL আর MySQL-এর মতো optimizer অংশ ৪-এর ফাঁদটি নিজেই চেনে: WHERE-এর শর্ত যদি ডান টেবিলের NULL-ভরা সারিকে কখনো TRUE হতে না দেয় (“null-rejecting” শর্ত), তবে LEFT JOIN-কে ভেতরে INNER JOIN-এ বদলে নেয় (MySQL-এর ডকুমেন্টেশনে নাম “outer join simplification”) — কারণ ফল দুটিতে একই। অর্থাৎ ভুল কোয়েরিটি দ্রুতও চলে, শুধু আপনার প্রশ্নের উত্তর দেয় না।
৯ · AI-এর যুগে JOIN
AI-কে “প্রত্যেক গ্রাহকের মোট খরচ দেখাও” বললে কয়েক সেকেন্ডে একটি চার-টেবিলের join পাবেন, আর সেটি চলবেও। সমস্যা হলো, join-এর ভুল প্রায় কখনো ত্রুটি দেয় না — দেয় ভুল সংখ্যা, দেখতে একদম স্বাভাবিক। AI-এর লেখা join-এ যা প্রায়ই দেখা যায়:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| “সব গ্রাহক” চাইলেও INNER JOIN | অর্ডারহীন বা NULL-FK সারি চুপচাপ বাদ | বাঁ টেবিলের COUNT(*) আর ফলের আলাদা গ্রাহক-সংখ্যা মেলান |
| LEFT JOIN-এর পর ডান টেবিলের শর্ত WHERE-এ | LEFT JOIN আসলে INNER হয়ে যায়; 0-ওয়ালা সারি নেই | যাঁর ফল 0 হওয়া উচিত, তিনি তালিকায় আছেন কি? |
লাইনের সাথে join করে অর্ডার-স্তরের টাকা SUM, বা অর্ডার গুনতে COUNT(*) | fan-out: দ্বিগুণ-তিনগুণ যোগফল | join-এর আগে-পরে সারির সংখ্যা; একটি অর্ডার হাতে হিসাব করুন |
“সমাধান” হিসেবে SUM(DISTINCT …) বা SELECT DISTINCT | সমান মানের আলাদা সারি মুছে যায় | দুটি অর্ডারে একই চার্জ রেখে পরীক্ষা করুন |
নেই এমন কলাম কল্পনা (orders.total, customer_name) | ত্রুটি, বা ভুল টেবিলে ভুল কলাম | schema পাশে রেখে প্রতিটি কলামের নাম মিলান |
| NATURAL JOIN বা alias ছাড়া কলাম | schema বদলালে নীরবে ভুল বা “ambiguous” ত্রুটি | প্রতিটি join-এ স্পষ্ট ON, প্রতিটি কলামে alias |
| FK কলামে index-এর কথা না বলা | বড় ডেটায় কোয়েরি ধীর | EXPLAIN দেখুন (পর্ব ৪১) |
AI-কে কোয়েরি লিখতে দিন, কিন্তু প্রশ্নটি এমনভাবে দিন যাতে সে grain, NULL আর অমিল সারি নিয়ে স্পষ্ট হতে বাধ্য হয় — আর নিজে যাচাইয়ের পরীক্ষাও লিখে দেয়:
AI-এর লেখা join হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- INNER JOIN আর LEFT JOIN-এর পার্থক্য কী? — INNER শুধু মিল-পাওয়া জোড়া রাখে; LEFT বাঁ টেবিলের প্রতিটি সারি রাখে, মিল না পেলে ডান দিকের কলামে NULL বসায়।
- যাঁর কোনো অর্ডার নেই, এমন গ্রাহক খুঁজুন। —
customers LEFT JOIN orders … WHERE o.order_id IS NULL, বাNOT EXISTS. - A-তে ৫টি, B-তে ৪টি সারি; CROSS JOIN-এ কয়টি? INNER JOIN-এ সর্বোচ্চ কয়টি? — CROSS JOIN-এ ঠিক ৫ × ৪ = ২০টি; INNER JOIN-এ শর্ত অনুযায়ী ০ থেকে সর্বোচ্চ ২০টি।
- LEFT JOIN-এ শর্ত ON-এ আর WHERE-এ রাখার পার্থক্য কী? — ON-এর শর্ত শুধু কোন ডান-সারি জুড়বে তা ঠিক করে, বাঁ-সারি হারায় না; WHERE join-এর পরে চলে, তাই ডান টেবিলের শর্ত NULL-ভরা সারি ফেলে দিয়ে INNER JOIN বানায়।
- MySQL-এ FULL OUTER JOIN কীভাবে লিখবেন? —
A LEFT JOIN BUNION ALLB LEFT JOIN A … WHERE A.key IS NULL(বা LEFT ও RIGHT JOIN-এর UNION).
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। join-এর প্রায় প্রতিটি নিয়মের পেছনে কোনো এক সময়ের ভুল রিপোর্টের গল্প আছে।
প্র ০১কমা দিয়ে টেবিল লিখে শর্ত WHERE-এ দিলে INNER JOIN-এর একই ফল আসে। তাহলে SQL standard-এ আলাদা JOIN … ON লেখা আনার দরকার পড়ল কেন?
INNER JOIN-এর জন্য সত্যিই নতুন কিছুর দরকার ছিল না — সমস্যা ছিল outer join-এ। ১৯৭০-এর দশকে relational মডেলে outer join-এর ধারণা আসে, আর E. F. Codd ১৯৭৯ সালের একটি প্রবন্ধে NULL-সহ একে আনুষ্ঠানিক রূপ দেন। কিন্তু তখনকার SQL-এ একে লেখার কোনো standard উপায় ছিল না, তাই প্রতিটি কোম্পানি নিজের চিহ্ন বানাল: Oracle কলামের পাশে (+), Sybase আর Microsoft SQL Server তুলনার চিহ্নে *= আর =*.
এই চিহ্নগুলো WHERE-এর ভেতরে বসত — ঠিক যেখানে সাধারণ ছাঁকনিও থাকে। ফলে অংশ ৪-এর প্রশ্নটি অস্পষ্ট হয়ে যেত: WHERE c.id *= o.cust AND o.status = 'delivered' লিখলে status-শর্তটি join-এর আগে খাটবে (ON-এর মতো), নাকি পরে (WHERE-এর মতো)? বিভিন্ন পণ্য, এমনকি একই পণ্যের বিভিন্ন সংস্করণ, এমন ক্ষেত্রে আলাদা আচরণ করত বলে জানা যায়, আর একাধিক টেবিলে outer join শৃঙ্খল প্রায় ব্যাখ্যার অযোগ্য হয়ে উঠত।
১৯৯২ সালের SQL-92 standard সমস্যাটি গঠন দিয়েই মেটাল: LEFT/RIGHT/FULL OUTER JOIN … ON, যেখানে ON-এর শর্ত মেলানোর অংশ আর WHERE-এর শর্ত পরের ছাঁকনি — দুটির অর্থ আলাদা, আর লেখাতেই আলাদা। সাথে এল CROSS JOIN, NATURAL JOIN আর USING. বড় কোম্পানিগুলো ধীরে ধীরে মেনে নিল: Oracle ANSI-রীতির join চালাতে শুরু করে 9i (২০০১) থেকে, আর SQL Server পুরোনো *= চিহ্ন ২০০৫ সংস্করণের নতুন compatibility level থেকে আর চালায় না। Oracle-এর (+) এখনো চলে, কিন্তু Oracle নিজেই ডকুমেন্টেশনে ANSI-রীতি ব্যবহারের পরামর্শ দেয়।
আজ INNER JOIN-এর জন্যও আধুনিক লেখা পছন্দ করার কারণ সেই একই দর্শন: “কীভাবে জুড়ব” আর “কোনটি চাই” আলাদা রাখলে কোয়েরি পড়া, রিভিউ করা আর বদলানো সহজ, আর ON ভুলে গেলে DBMS ধরিয়ে দেয়।
মূল উপলব্ধি: SQL-92-এর JOIN শুধু নতুন বানান নয়, একটি অর্থের পার্থক্যকে লেখায় স্থায়ী করা — মেলানোর শর্ত (ON) আর ফলের ছাঁকনি (WHERE) দুটি আলাদা কাজ, আর outer join-এ এই পার্থক্যই সঠিক ফল ঠিক করে।
প্র ০২Join বোঝাতে Venn diagram এত জনপ্রিয় হলো কেন, আর অভিজ্ঞ অনেকে কেন একে ক্ষতিকর বলেন? বিকল্প হিসেবে কোন ছবি ভালো?
Venn diagram জনপ্রিয় হওয়ার কারণ সহজ: এক নজরে সাতটি join-এর “আকার” দেখা যায়, আর মানুষ স্কুল থেকেই set-এর ছবি চেনেন। ২০০৭ সালে Jeff Atwood-এর একটি ব্লগ পোস্ট (“A Visual Explanation of SQL Joins”) এই ছবিগুলো ব্যাপকভাবে ছড়িয়ে দেয়, আর তারপর অসংখ্য পোস্টার, টিউটোরিয়াল আর ইন্টারভিউ-নোটে একই বৃত্ত। পরে অনেকে এর সমালোচনা করেন — যেমন jOOQ লাইব্রেরির নির্মাতা Lukas Eder ২০১৬ সালের একটি লেখায় সরাসরি বলেন, join বোঝাতে Venn diagram ব্যবহার বন্ধ করা উচিত।
সমালোচনার মূল যুক্তি গাণিতিক। Venn diagram দেখায় একই ধরনের সদস্যের দুটি set-এর ছেদ বা মিলন — এটি INTERSECT আর UNION-এর ছবি। Join-এর ফল কিন্তু কোনো set-এর সদস্য নয়; এটি Cartesian product-এর একটি উপসেট, যার প্রতিটি সদস্য একটি জোড়া. বৃত্তের ছবি থেকে কখনো বোঝা যায় না যে Rahim-এর এক সারি ছয় সারি হয়ে যেতে পারে, বা দুটি টেবিলের join-এর ফল দুটির যেকোনোটির চেয়ে বড় হতে পারে। অংশ ৭-এর fan-out, যা বাস্তব রিপোর্টের সবচেয়ে ব্যয়বহুল ভুলগুলোর একটি, Venn diagram-এ অদৃশ্য। CROSS JOIN বা অসমতার শর্ত (price_bands) বৃত্তে আঁকাই যায় না।
বিকল্প হিসেবে অনেক শিক্ষক দুটি ছোট টেবিল পাশাপাশি এঁকে রেখা টেনে মিল দেখান — এই পর্বের স্টেপারগুলোর মতো। কেউ কেউ ছক ব্যবহার করেন: সারিতে বাঁ টেবিল, কলামে ডান টেবিল, প্রতিটি ঘর একটি জোড়া; শর্ত সত্য এমন ঘর রঙিন। এতে Cartesian product, মিল আর একাধিক মিল — তিনটিই একসাথে দেখা যায়। LEFT JOIN-এর জন্য ছকের পাশে একটি বাড়তি “NULL” কলাম যোগ করলেই চলে।
তবু Venn diagram-এর একটি ছোট ব্যবহার আছে: কোন দিকের অমিল সারি টিকে থাকে, সেটি মনে রাখতে। শুধু মনে রাখবেন, বৃত্তের ভেতরের “আকার” সারির সংখ্যা নয়।
মূল উপলব্ধি: join হলো জোড়া বানানো আর ছাঁকা, set-এর ছেদ নয়। যে ছবি সারির গুণ দেখাতে পারে না, সেটি দিয়ে join-এর সবচেয়ে বিপজ্জনক ভুলগুলো বোঝা যায় না।
প্র ০৩প্রতিটি রিপোর্টে যদি চার-পাঁচটি টেবিল join করতেই হয়, তবে ডেটা প্রথমেই একটি বড় টেবিলে বা একটি document-এ রাখলে কি ভালো হতো না? NoSQL ডেটাবেস তো অনেক সময় তাই করে।
এটি ডেটাবেস ডিজাইনের সবচেয়ে পুরোনো টানাপোড়েনগুলোর একটি। ১৯৭০ সালে Codd-এর relational মডেলের মূল প্রস্তাব ছিল: ডেটা এমনভাবে রাখুন যাতে প্রতিটি সত্য একবার থাকে, আর প্রশ্নের সময় join দিয়ে যেকোনো দৃষ্টিভঙ্গি তৈরি করুন। তার আগের hierarchical আর network ডেটাবেসে (পর্ব ৪) প্রোগ্রামারকে আগে থেকে ঠিক করা “পথ” ধরে ডেটায় যেতে হতো; নতুন প্রশ্ন মানে নতুন পথ আর নতুন প্রোগ্রাম। Join সেই বাঁধন খুলে দিয়েছিল — আজকের যেকোনো ad-hoc রিপোর্ট তারই ফল।
এক বড় টেবিলে রাখলে join বাঁচে, কিন্তু ফেরত আসে পর্ব ১১ আর ৩৬-এর গোলমাল: Rahim-এর ঠিকানা প্রতিটি অর্ডারের সারিতে, বদলালে সব জায়গায় বদলাতে হয়; অর্ডার ছাড়া গ্রাহক রাখা যায় না। আর লক্ষ করুন — বড় টেবিলেও fan-out আছে, শুধু লুকানো: অর্ডারের চার্জ প্রতিটি লাইনে কপি থাকলে যোগ করলেই দ্বিগুণ।
Document ডেটাবেস (যেমন MongoDB, পর্ব ৫৭) একটি মাঝামাঝি পথ নেয়: যে ডেটা সবসময় একসাথে পড়া হয় (অর্ডার আর তার লাইন), তা এক document-এ; যা আলাদা জীবন যাপন করে (গ্রাহক, পণ্য), তা আলাদা। কিন্তু বাস্তবের প্রশ্ন শেষমেশ সম্পর্ক জানতে চায় — তাই MongoDB-ও ৩.২ সংস্করণ (২০১৫) থেকে $lookup নামে একটি left outer join-ধর্মী অপারেশন যোগ করেছে।
আর join-এর খরচ যতটা ভয় পাওয়া হয়, ততটা নয়: FK-তে index থাকলে কয়েকটি টেবিলের join মিলিসেকেন্ডে হয়, কারণ প্রতিটি মিল খোঁজা log n খরচের। যেখানে সত্যিই কয়েক কোটি সারির join বারবার লাগে, সেখানে পর্ব ২৪-এর materialized view বা পর্ব ৩৮-এর পরিকল্পিত denormalization — মাপজোখ করে, কারণ লিখে।
মূল উপলব্ধি: ভাগ করা ডেটা শুদ্ধতা আর নমনীয়তা দেয়, join তার দাম — আর index সেই দাম ছোট রাখে। প্রথমে normalize করুন, join-এর খরচ মেপে দেখুন, তারপর দরকার হলে নির্দিষ্ট জায়গায় denormalize করুন।
প্র ০৪Join-শর্তে NULL কখনো NULL-এর সাথে মেলে না। কিন্তু দুই টেবিলেই “শহর অজানা” থাকলে কি সেগুলো মেলা উচিত নয়? এমন ক্ষেত্রে কী করবেন?
SQL-এর NULL মানে “অজানা”, “খালি” নয়। দুটি অজানা মান সমান কি না — সেটিও অজানা; তাই NULL = NULL-এর ফল UNKNOWN, আর join শুধু TRUE জোড়া রাখে। এই নিয়ম আসে Codd-এর তিন-মানের যুক্তি থেকে, আর সমালোচনাও কম হয়নি — Chris Date-সহ কয়েকজন relational তাত্ত্বিক বহু বছর ধরে যুক্তি দিয়েছেন যে NULL আর তিন-মানের যুক্তি ধাঁধা তৈরি করে। কিন্তু standard আর সব বড় DBMS এই আচরণই মানে।
বেশিরভাগ ক্ষেত্রে আচরণটি সঠিক। Habib-এর dept_id NULL — তাঁকে কোনো বিভাগের সাথে মেলানো উচিত নয়। Imran-এর city NULL — তাঁকে ঢাকার ডেলিভারি চার্জ দেওয়া উচিত নয়, কারণ তিনি ঢাকায় না-ও থাকতে পারেন। দুই অজানাকে “মিলিয়ে” ফেললে মিথ্যা সম্পর্ক তৈরি হতো।
তবু কখনো কখনো NULL-কে একটি মান হিসেবে মেলাতেই হয় — যেমন দুটি ডেটা-কপি তুলনা করতে, যেখানে “দুই জায়গাতেই NULL” মানে “মিলেছে”। এর জন্য standard SQL-এ আছে IS NOT DISTINCT FROM: ON a.city IS NOT DISTINCT FROM b.city — দুটি NULL হলে TRUE. PostgreSQL এটি চালায়; MySQL-এ একই কাজের চিহ্ন <=> (null-safe equal); SQLite-এ IS অপারেটর দুটি মানের মধ্যে একই কাজ করে। পুরোনো বিকল্প COALESCE(a.city, '?') = COALESCE(b.city, '?') চলে, কিন্তু কলামের ওপর ফাংশন বসলে সাধারণ index আর কাজে লাগে না।
সবচেয়ে ভালো সমাধান প্রায়ই ডিজাইনে: যদি “অজানা শহর” একটি আসল ব্যবসায়িক অবস্থা হয় (যেমন আলাদা চার্জ), তবে delivery_zones-এ একটি আসল সারি রাখুন, আর customers-এ সেই সারির key — NULL দিয়ে অর্থ বোঝানোর বদলে।
মূল উপলব্ধি: join-এ NULL না মেলা ভুল নয়, সতর্কতা — অজানাকে অজানার সাথে মেলালে মিথ্যা সম্পর্ক তৈরি হয়। NULL মেলাতে চাইলে সেটি স্পষ্ট লিখুন (IS NOT DISTINCT FROM), আর অর্থবহ অবস্থা হলে NULL-এর বদলে আসল মান রাখুন।
প্র ০৫Fan-out এত সাধারণ ভুল হলে ব্যবসায়িক রিপোর্টিং টুলগুলো এটি কীভাবে সামলায়? আর বড় প্রজেক্টে দল হিসেবে কীভাবে এটি প্রতিরোধ করা যায়?
Business intelligence (BI) টুলের জগতে সমস্যাটির নামই আছে। SAP BusinessObjects-এর মতো পুরোনো টুলে “universe” ডিজাইনের সময় দুটি ফাঁদ শেখানো হয়: fan trap — এক → অনেক join-এর পর “এক” দিকের সংখ্যা যোগ করা (অংশ ৭-এর ডেলিভারি চার্জ), আর chasm trap — একটি টেবিল থেকে দুটি আলাদা “অনেক” দিকে join (অংশ ৭-এর সতর্কবার্তার লাইন আর পেমেন্ট)। এই টুলগুলো সাধারণত প্রতিটি শাখার জন্য আলাদা SQL চালিয়ে পরে ফল জোড়া দিয়ে সমস্যা এড়ায় — ঠিক pre-aggregation-এর মতো।
Looker-এর মতো নতুন টুল একটি চতুর কৌশল ব্যবহার করে, যার নাম “symmetric aggregates”: প্রতিটি সারির primary key জেনে নিয়ে এমনভাবে যোগ করা হয় যাতে একই অর্ডারের বারবার আসা সারি একবারই গোনা হয় — যৌক্তিকভাবে “প্রতিটি আলাদা order_id-এর একটি fee যোগ”। এটি SUM(DISTINCT)-এর মতো মান দেখে না, key দেখে, তাই অংশ ৭-এর “দুটি ৬০ মিলে একটি” ভুল হয় না। কিন্তু টুলকে কোন কলাম primary key আর কোন join কোন দিকে “অনেক”, তা সঠিকভাবে বলে দিতে হয় — ভুল মেটাডেটা মানে আবার ভুল সংখ্যা।
দলের জন্য সবচেয়ে কার্যকর অভ্যাসটি ডেটা ওয়্যারহাউসের পুরোনো শিক্ষা থেকে আসে: প্রতিটি টেবিল আর প্রতিটি কোয়েরির grain লিখে রাখা। Ralph Kimball-এর dimensional modeling পদ্ধতিতে fact table ডিজাইনের প্রথম ধাপই হলো grain ঘোষণা করা — “এক সারি = একটি অর্ডারের একটি লাইন” (পর্ব ৫৫)। তারপর নিয়ম: একটি fact টেবিলে শুধু সেই grain-এর সংখ্যা থাকবে; অর্ডার-স্তরের চার্জ যাবে অর্ডার-স্তরের টেবিলে।
কোড রিভিউতে এর সাথে দুটি যান্ত্রিক পরীক্ষা যোগ করুন: প্রতিটি join-এর আগে-পরে সারির সংখ্যা, আর ফলের key-তে ডুপ্লিকেট আছে কি না (GROUP BY key HAVING COUNT(*) > 1). dbt-এর মতো আধুনিক data টুলে এমন “unique” পরীক্ষা প্রতিটি মডেলে স্বয়ংক্রিয়ভাবে চালানো যায়।
মূল উপলব্ধি: fan-out কোনো SQL-এর বাগ নয়, grain-এর ভুল বোঝাবুঝি। প্রতিটি ফলের “এক সারি = কী” লিখে রাখুন, আর শুধু সেই grain-এর সংখ্যাই যোগ করুন।
অনুশীলন
একটি কোয়েরি লেখা, একটি হাতে হিসাব, একটি বাগ খোঁজা আর একটি সারি-গোনা।
-
কোয়েরি লিখুন: db-uni-তে প্রত্যেক শিক্ষকের নাম, তাঁর বিভাগের নাম, আর তিনি কয়টি কোর্স পড়ান — যিনি একটিও পড়ান না, তিনিও 0 দিয়ে থাকবেন। বেশি কোর্স আগে। (instructors, departments, courses) অংশ ৫-এর এডিটরে চালিয়ে দেখুন।
SELECT i.name, d.name AS dept, COUNT(c.course_id) AS courses FROM instructors i JOIN departments d ON d.dept_id = i.dept_id LEFT JOIN courses c ON c.instructor_id = i.instructor_id GROUP BY i.instructor_id, i.name, d.name ORDER BY courses DESC, i.name;ছয়টি সারি: Tasnim Chowdhury (CSE) 2, তারপর Dr. Anisur Rahman (CSE), Dr. Mahbub Alam (EEE), Kamrul Hasan (BBA), Shahana Parvin (CSE) — প্রত্যেকে 1, আর শেষে Rumana Islam (EEE) 0. departments-এর সাথে INNER JOIN চলে, কারণ প্রত্যেক শিক্ষকের dept_id আছে; courses-এর সাথে LEFT JOIN জরুরি, নইলে Rumana হারাতেন।
COUNT(*)নয়,COUNT(c.course_id)— নইলে Rumana-র NULL-ভরা সারিটি গুনে 1 দেখাত। -
ফল আগে বলুন (trace): দুটি এক-কলামের টেবিল: A(id) = 1, 2, 2, NULL আর B(id) = 2, 2, 3, NULL. শর্ত
A.id = B.id. INNER JOIN, LEFT JOIN (A বাঁয়ে), FULL OUTER JOIN আর CROSS JOIN-এ কয়টি করে সারি আসবে? হাতে হিসাব করুন, তারপরdata-setupছাড়াই এডিটরে CREATE TABLE লিখে মিলিয়ে নিন।INNER = ৪। শুধু ২ মেলে: A-র দুটি ২ × B-র দুটি ২ = ৪টি জোড়া। দুই NULL পরস্পর মেলে না, ১ আর ৩-এর কোনো সঙ্গী নেই।
LEFT = ৬। ৪টি মিল + A-র অমিল সারি ১ আর NULL — প্রত্যেকে একবার, ডানে NULL নিয়ে।
FULL = ৮। LEFT-এর ৬ + B-র অমিল সারি ৩ আর NULL. লক্ষ করুন দুটি NULL আলাদা আলাদা সারি হয়ে আসে — একসাথে নয়।
CROSS = ১৬। ৪ × ৪, কোনো শর্ত নেই। শিক্ষা: ডুপ্লিকেট key-তে join ফলকে গুণ করে, আর NULL কখনো মেলে না।
-
বাগ খুঁজুন: একজন সহকর্মী রিপোর্ট বানিয়েছেন “প্রত্যেক শিক্ষার্থী 2023-Fall term-এ কয়টি কোর্স নিয়েছেন, যিনি নেননি তিনি 0”:
SELECT s.name, COUNT(e.course_id) FROM students s LEFT JOIN enrollments e ON e.student_id = s.student_id WHERE e.term = '2023-Fall' GROUP BY s.student_id, s.name;db-uni-তে এটি মাত্র তিনজনকে দেখায়। কেন, আর সমাধান কী?ডান টেবিলের (enrollments) শর্ত WHERE-এ — অংশ ৪-এর ফাঁদ। যাঁদের 2023-Fall-এর কোনো ভর্তি নেই, তাঁদের সারিতে হয় অন্য term, নয়তো (Habib-এর মতো) NULL; দুটিই WHERE-এ বাদ পড়ে। থাকেন শুধু Arif Hossain 2, Bristy Saha 1, Gulshan Ara 2. সমাধান: শর্তটি ON-এ নিন —
LEFT JOIN enrollments e ON e.student_id = s.student_id AND e.term = '2023-Fall'. তখন আটজনই আসবেন; বাকি পাঁচজন (Chayon, Dilruba, Emon, Fahim, Habib) 0. -
সারি গুনুন: db-shop-এ (ক)
customers JOIN orders JOIN order_items JOIN products(সব শর্ত FK = PK, কোনো WHERE নেই) কয়টি সারি দেবে? (খ)customers LEFT JOIN orders LEFT JOIN order_itemsকয়টি? (গ) এই দ্বিতীয় ফলেCOUNT(DISTINCT c.customer_id)কত? প্রতিটি উত্তরের কারণ বলুন।(ক) ১৬ — order_items-এর লাইনের সংখ্যা। প্রতিটি লাইনের ঠিক একটি অর্ডার, প্রতিটি অর্ডারের ঠিক একজন গ্রাহক, প্রতিটি লাইনের ঠিক একটি পণ্য (সব FK NOT NULL); তাই grain = লাইন, আর কোনো লাইন বাদও পড়ে না।
(খ) ১৭ — ১৬টি লাইন + Tanvir Ahmed-এর একটি NULL-ভরা সারি (তাঁর অর্ডার নেই, তাই order_items-ও NULL). প্রতিটি অর্ডারে অন্তত একটি লাইন আছে বলে আর কোনো NULL সারি নেই।
(গ) ৮ — সব গ্রাহক, Tanvir-সহ। ১৭টি সারিতে ৮ জন ছড়িয়ে আছেন — fan-out-এর আরেকটি রূপ। “কতজন গ্রাহক” গুনতে এখানে
COUNT(*)লিখলে ১৭ আসত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমLEFT JOIN আর GROUP BY আছে এমন একটি SELECT যৌক্তিকভাবে যে ক্রমে ফল তৈরি করে, সেই ধাপগুলো সাজান (▲▼ চেপে সরান)
- দুই টেবিলের প্রতিটি সারির জোড়া কল্পনা করা (Cartesian product)
- ON-শর্ত সত্য এমন জোড়াগুলো রাখা
- মিল না পাওয়া বাঁ-সারি একবার যোগ করা, ডান কলামে NULL
- WHERE দিয়ে সারি ছাঁকা
- GROUP BY দিয়ে দল বানিয়ে COUNT/SUM হিসাব
- SELECT-এর কলাম বেছে ORDER BY দিয়ে সাজানো
customers টেবিলে ৮টি আর orders টেবিলে ১০টি সারি। SELECT COUNT(*) FROM customers, orders; — কোনো WHERE নেই। ফল কত?
- 10
- 18
- 80
- ত্রুটি — শর্ত ছাড়া join চলে না
SELECT c.name, o.order_id FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.status = 'delivered'; — এই কোয়েরির ফল সম্পর্কে কোনটি সত্য?
- সব গ্রাহক থাকবেন; delivered অর্ডার না থাকলে order_id NULL
- ফল INNER JOIN-এর মতো — শুধু যাঁদের অন্তত একটি delivered অর্ডার আছে
- syntax error, কারণ LEFT JOIN-এর পর WHERE লেখা যায় না
- প্রত্যেক গ্রাহকের প্রতিটি অর্ডার দুবার আসবে
customers(customer_id, name) আর orders(order_id, customer_id) থেকে যাঁদের কোনো অর্ডার নেই, তাঁদের খোঁজার সঠিক কোয়েরি কোনটি?
… FROM customers c JOIN orders o ON o.customer_id = c.customer_id WHERE o.order_id IS NULL… FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.order_id = NULL… FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.order_id IS NOT NULL… FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.order_id IS NULL
students(student_id, name, dept_id) আর departments(dept_id, name, building) — SELECT * FROM students NATURAL JOIN departments; কী করবে?
- dept_id আর name দুটিই মেলাবে, তাই সম্ভবত কোনো সারি আসবে না
- শুধু foreign key dept_id মেলাবে, প্রত্যেক শিক্ষার্থী তাঁর বিভাগসহ আসবেন
- Cartesian product দেবে, কারণ কোনো ON নেই
- ত্রুটি দেবে, কারণ দুই টেবিলে name কলাম আছে
orders টেবিলে প্রতিটি অর্ডারে delivery_fee = 60, আর প্রতিটি অর্ডারের order_items-এ ঠিক ৩টি লাইন। একজন গ্রাহকের ২টি অর্ডার। orders আর order_items join করে SUM(o.delivery_fee) চালালে ফল আর সঠিক সমাধান কী?
- 120 — join-এ ফলের কোনো পরিবর্তন হয় না
- 360; সমাধান SUM(DISTINCT o.delivery_fee)
- 360; সমাধান আগে order_items অর্ডার-প্রতি যোগ করে তারপর join
- 360; সমাধান INNER-এর বদলে LEFT JOIN
টেবিল A-র key: 1, 2, 3; টেবিল B-র key: 2, 3, 4, 5 (দুই টেবিলেই key অনন্য)। A FULL OUTER JOIN B ON A.key = B.key-এ কয়টি সারি আসবে?
- 4
- 5
- 7
- 12
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Join = Cartesian product, তারপর ON-শর্তে ছাঁকনি; m × n জোড়া থেকে শর্ত-মানা জোড়াগুলো। আধুনিক
JOIN … ONলিখুন, প্রতিটি কলামে alias দিন; N টেবিলে N − 1টি শর্ত। - FK ধরে “অনেক” দিকে join করলে সারি গুণ হয়, “এক” দিকে গেলে বদলায় না। ফলের grain — “এক সারি = কী” — সবসময় জানুন।
- LEFT JOIN বাঁয়ের প্রতিটি সারি রাখে, অমিলে ডানে NULL;
WHERE ডান.key IS NULL= anti-join (Tanvir, Power Bank). RIGHT = উল্টো LEFT; FULL দুই দিকের সবাই — MySQL-এ LEFT JOIN + UNION ALL + anti-join. - LEFT JOIN-এ ডান টেবিলের শর্ত ON-এ, বাঁ টেবিলের শর্ত WHERE-এ; নইলে LEFT চুপচাপ INNER হয়ে যায়। NATURAL JOIN এড়িয়ে চলুন — শর্ত schema-য় লুকানো।
- অর্ডার-স্তরের সংখ্যা লাইনের সাথে join করে যোগ করলে fan-out; SUM(DISTINCT) সমাধান নয় — আগে যোগ, তারপর join. দ্রুত join-এর জন্য FK কলামে index.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২০ · Self join, cross join, semi join আর anti joinপরবর্তী পর্বjoin-এর বাকি রূপ আর EXISTS.
- পর্ব ১৮ · Aggregation — GROUP BY আর HAVINGআবার দেখুনfan-out বুঝতে COUNT আর SUM-এর আচরণ।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Python · SQLite ডেটাবেসহাতে-কলমেPython থেকে একই join চালানো।
- DSA · Hash tableগভীরে যানhash join যে কাঠামোর ওপর দাঁড়িয়ে।
- DSA · B-treeগভীরে যানFK কলামের index আর index nested loop join.