এই পর্বে যা শিখবেন
- Self join দিয়ে শিক্ষক–mentor আর একই বিভাগের শিক্ষার্থী-জোড়া; কেন বহু স্তরের hierarchy-তে self join যথেষ্ট নয়
- CROSS JOIN-এর আসল কাজ — সব সমন্বয় আর ক্যালেন্ডার-ছক — আর ভুল করে Cartesian product হওয়ার বিপদ
- পরিসর মিলিয়ে non-equi join: দামের স্তর, নম্বর থেকে letter grade আর lookup টেবিল দিয়ে GPA
- Semi join আর anti join-এর তিনটি লেখার ধরন, আর NOT IN-এর NULL ফাঁদের প্রমাণ
- একাধিক কলামে join, grouped subquery-র সাথে join, LATERAL/APPLY-এর ধারণা, join-এর ক্রম আর relational algebra-র ⋉ ও ▷
১ · Self join — একই টেবিল, দুটি ভূমিকা
পর্ব ১৯-এ (JOIN — INNER, LEFT, RIGHT আর FULL) আপনি দুটি আলাদা টেবিল জুড়েছেন: orders-এর পাশে customers, enrollments-এর পাশে courses. এই পর্বে প্রশ্নগুলো একটু অন্য রকম। “এই শিক্ষকের mentor কে?” — উত্তরটি আছে একই instructors টেবিলে। “কোন গ্রাহক অন্তত একটি অর্ডার পেয়েছেন?” — এখানে অর্ডারের তালিকা চাই না, শুধু হ্যাঁ বা না। “কোন বিভাগে একজনও শিক্ষার্থী নেই?” — এখানে খুঁজছি যা নেই। প্রতিটি প্রশ্নের জন্য join-এর একটি বিশেষ রূপ আছে।
প্রথমে টেবিলটি মনে করুন। db-uni-র instructors টেবিলে কলাম পাঁচটি: instructor_id (শিক্ষকের নম্বর), name, dept_id (বিভাগ), salary (মাসিক বেতন, টাকায়) আর mentor_id — নতুন শিক্ষককে যে সিনিয়র শিক্ষক দেখেন, তাঁর instructor_id. mentor_id নিজের টেবিলের দিকেই একটি foreign key — পর্ব ১০ আর ১১-এ দেখা recursive relationship. ছয়জন শিক্ষকের তিনজনের mentor আছে: Shahana Parvin-এর mentor 1 (Dr. Anisur Rahman), Rumana Islam-এর 3 (Dr. Mahbub Alam), আর Tasnim Chowdhury-র 2 (Shahana Parvin). বাকি তিনজনের mentor_id NULL.
বিশ্ববিদ্যালয়ের রেজিস্টারের একটি পাতায় শিক্ষকদের তালিকা, আর প্রতিটি লাইনের শেষে লেখা “mentor: ক্রমিক ২”। নাম জানতে আপনি আঙুল রাখেন লাইনে, তারপর একই পাতার ক্রমিক ২-এ আরেকটি আঙুল নিয়ে যান। দুটি আঙুল, একটি পাতা। SQL-এ দুটি আঙুল হলো দুটি alias — একই টেবিলের দুটি নাম।
Self join নতুন কোনো keyword নয়। এটি একটি সাধারণ JOIN, যেখানে দুই পাশে একই টেবিল। DBMS-কে আলাদা করে চেনাতে প্রতিটি কপিকে আলাদা alias দিতে হয় — নইলে instructors.name কোন কপির, বোঝা যায় না। নিচের এডিটরে i হলো “শিক্ষক হিসেবে” কপি, m হলো “mentor হিসেবে” কপি। join-এর শর্ত বলে: m-এর instructor_id সমান i-এর mentor_id.
-- INNER self join: শুধু যাঁদের mentor আছে
SELECT i.name AS instructor, m.name AS mentor
FROM instructors i
JOIN instructors m ON m.instructor_id = i.mentor_id;
-- LEFT self join: সব শিক্ষক, mentor না থাকলে NULL
SELECT i.name AS instructor, m.name AS mentor
FROM instructors i
LEFT JOIN instructors m ON m.instructor_id = i.mentor_id
ORDER BY i.instructor_id;প্রথম কোয়েরিতে ৩টি সারি: Shahana Parvin | Dr. Anisur Rahman, Rumana Islam | Dr. Mahbub Alam, Tasnim Chowdhury | Shahana Parvin. বাকি তিনজন হারিয়ে গেছেন, কারণ তাঁদের mentor_id NULL — আর m.instructor_id = NULL কখনো সত্য নয় (পর্ব ১৬-এর তিন-মানের যুক্তি)। দ্বিতীয় কোয়েরিতে ৬টি সারি: Dr. Anisur Rahman, Dr. Mahbub Alam আর Kamrul Hasan-এর পাশে mentor NULL. “সব শিক্ষকের তালিকা” চাইলে LEFT self join-ই সঠিক। খেয়াল করুন, Shahana Parvin দুই ভূমিকাতেই আছেন: এক সারিতে শিক্ষক (i), আরেক সারিতে Tasnim-এর mentor (m).
শর্তের দিক উল্টে লিখলে (m.mentor_id = i.instructor_id) প্রশ্নটিই বদলে যায়: তখন i-এর পাশে আসে i যাঁদের mentor, অর্থাৎ mentee-রা। self join-এ কোন alias কোন ভূমিকা নিচ্ছে, তা কলামের নামে (AS mentor) স্পষ্ট লিখে রাখুন — পরে পড়তে গিয়ে নিজেই গুলিয়ে ফেলবেন না।
জোড়া খোঁজা — duplicate ছাড়া
দ্বিতীয় প্রচলিত কাজ: একই বিভাগের শিক্ষার্থীদের সব জোড়া — যেমন প্রজেক্টের জন্য দুজনের দল বানাতে। students টেবিলে student_id, name, dept_id, batch আর cgpa. শর্ত “একই বিভাগ” লিখলেই কাজ শেষ নয়; তখন প্রত্যেকে নিজের সাথেও জোড়া পান, আর প্রতিটি জোড়া দুবার আসে (Arif–Bristy আর Bristy–Arif). সমাধান একটি ছোট কৌশল: a.student_id < b.student_id. নিচের ডায়াগ্রামে CSE-র চারজন শিক্ষার্থী দিয়ে ধাপে ধাপে দেখুন:
| সারি (CSE) | |
| নিজের সাথে জোড়া | |
| প্রতিটি জোড়া কতবার | |
| পুরো টেবিলে সারি |
- একই টেবিল, দুটি alias. a আর b দুটিই students. সরলতার জন্য শুধু CSE-র চারজন দেখাচ্ছি: 2101 Arif, 2102 Bristy, 2201 Dilruba, 2301 Gulshan.
- শুধু “একই বিভাগ”। চারজনই CSE, তাই 4 × 4 = ১৬টি সমন্বয়ের সবগুলো টিকে যায় — কর্ণের ৪টি ঘরে প্রত্যেকে নিজের সাথে জোড়া।
- নিজের সাথে জোড়া বাদ।
<>লিখলে কর্ণ বাদ, ১২টি থাকে। কিন্তু Arif–Bristy আর Bristy–Arif দুটোই আছে — একই দল দুবার। পুরো টেবিলে এভাবে ১৪টি সারি। <দিয়ে একটি দিক বাছা। শুধু কর্ণের ওপরের ত্রিভুজ থাকে: ৬টি জোড়া, প্রতিটি একবার। সাধারণভাবে n জনের মধ্যে জোড়া n(n−1)/2 — এখানে 4 × 3 / 2 = ৬।- পুরো টেবিলে। CSE-র ৬টি আর EEE-র ১টি (Chayon–Fahim) মিলে ৭টি সারি। BBA-তে Emon একা। Habib Ullah-র dept_id NULL, তাই তিনি কারও সাথে — এমনকি নিজের সাথেও — মেলেন না।
-- একই বিভাগের শিক্ষার্থী-জোড়া, প্রতিটি একবার
SELECT a.name AS student_1, b.name AS student_2, a.dept_id
FROM students a
JOIN students b ON b.dept_id = a.dept_id
AND a.student_id < b.student_id
ORDER BY a.dept_id, a.student_id, b.student_id;
-- <> দিলে কত সারি হতো?
SELECT COUNT(*) AS pairs_with_not_equal
FROM students a JOIN students b
ON b.dept_id = a.dept_id AND a.student_id <> b.student_id;প্রথম কোয়েরিতে ঠিক ৭টি সারি — ডায়াগ্রামের শেষ ধাপের সাথে মিলিয়ে নিন: CSE-র ছয়টি (Arif Hossain–Bristy Saha থেকে Dilruba Khatun–Gulshan Ara পর্যন্ত) আর Chayon Roy | Fahim Karim | 2. দ্বিতীয় কোয়েরির ফল 14 — ঠিক দ্বিগুণ, কারণ প্রতিটি জোড়া দুই দিক থেকে এসেছে। < কাজ করে কারণ student_id অনন্য: দুজন ভিন্ন মানুষের মধ্যে ঠিক একজনের id ছোট, তাই প্রতিটি অক্রমিক জোড়া ঠিক একবার শর্ত পূরণ করে।
২ · Self join-এর সীমা — বহু স্তরের hierarchy
mentor_id-এর মতো “নিজের টেবিলের দিকে আঙুল” দিয়ে গাছের মতো কাঠামো রাখাকে বলে adjacency list — প্রতিটি সারি শুধু তার ঠিক ওপরের জনকে চেনে। অফিসের boss–কর্মী, পণ্যের শ্রেণি–উপশ্রেণি, ফেসবুক মন্তব্যের উত্তর, জেলা–উপজেলা–ইউনিয়ন — সবই এমন hierarchical data. এক স্তর ওপরে যেতে একটি self join লাগে। দুই স্তর? দুটি:
-- শিক্ষক → mentor → mentor-এর mentor
SELECT i.name AS instructor,
m1.name AS mentor,
m2.name AS mentors_mentor,
m2.mentor_id AS level_3_id
FROM instructors i
LEFT JOIN instructors m1 ON m1.instructor_id = i.mentor_id
LEFT JOIN instructors m2 ON m2.instructor_id = m1.mentor_id
ORDER BY i.instructor_id;৬টি সারি। শুধু একটি সারিতে তিনটি স্তর পূর্ণ: Tasnim Chowdhury | Shahana Parvin | Dr. Anisur Rahman | NULL. Shahana আর Rumana-র mentors_mentor NULL, আর তিন সিনিয়রের দুটি কলামই NULL. শেষ কলাম level_3_id সবখানে NULL — অর্থাৎ এই ডেটায় তিন স্তরের বেশি নেই। কিন্তু সেটি আমরা জানলাম ডেটা দেখে; কোয়েরি নিজে জানত না।
এখানেই self join-এর মৌলিক সীমা। প্রতিটি স্তরের জন্য একটি করে join লিখতে হয়, অর্থাৎ গভীরতা কোয়েরি লেখার সময়েই জানা থাকতে হবে। কিন্তু কাল কেউ Tasnim-এর অধীনে নতুন শিক্ষক যোগ করলে চতুর্থ স্তর তৈরি হবে, আর উপরের কোয়েরি চুপচাপ তাকে কেটে দেবে। “Dr. Anisur Rahman-এর অধীনে মোট কতজন, যত স্তরই হোক?” — এমন প্রশ্ন নির্দিষ্ট সংখ্যক join দিয়ে লেখাই যায় না।
k স্তর ওপরে যেতে k-টি self join লাগে। অজানা গভীরতার জন্য দরকার এমন কোয়েরি, যা নিজের ফলের ওপর বারবার চলে যতক্ষণ না নতুন সারি আসে — recursive query. SQL:1999 মানে এটি এসেছে WITH RECURSIVE নামে, আর SQLite, PostgreSQL, MySQL 8, SQL Server, Oracle সবাই এখন এটি চালায় (SQL Server আর Oracle-এ RECURSIVE শব্দটি বাদ দিয়ে শুধু WITH লেখা হয়)। পুরো কৌশল পর্ব ২৫-এ (CTE আর recursive query).
শুধু এক ঝলক দেখে রাখুন, পর্ব ২৫-এ কোয়েরিটি কেমন দেখাবে। ধারণাটি: প্রথমে শুরুর মানুষ (anchor), তারপর “আগের ধাপের প্রত্যেকের mentee-রা” — ঠিক একটি self join, কিন্তু নিজের আগের ফলের সাথে:
-- পূর্বাভাস: পর্ব ২৫ — Dr. Anisur Rahman-এর অধীনে সবাই, যত স্তরই হোক
WITH RECURSIVE team(instructor_id, name, level) AS (
SELECT instructor_id, name, 0 FROM instructors WHERE instructor_id = 1
UNION ALL
SELECT i.instructor_id, i.name, t.level + 1
FROM instructors i JOIN team t ON i.mentor_id = t.instructor_id
)
SELECT * FROM team;Oracle-এ এর জন্য বহু আগে থেকে নিজস্ব CONNECT BY লেখার ধরনও আছে। আবার কিছু ডিজাইন hierarchy-কে অন্যভাবে রাখে — পুরো পথ একটি কলামে ('/1/2/6/') বা সব পূর্বপুরুষ–উত্তরসূরি জোড়া আলাদা টেবিলে; ভাবনার প্রশ্ন ৩-এ এদের তুলনা।
৩ · CROSS JOIN — সব সম্ভাব্য জোড়া, ইচ্ছে করে
CROSS JOIN-এর কোনো শর্ত নেই: বাঁ টেবিলের প্রতিটি সারির সাথে ডান টেবিলের প্রতিটি সারি। m সারি × n সারি = m·n সারি — পর্ব ৫-এর Cartesian product. সাধারণত এটি একটি দুর্ঘটনা (join-এর শর্ত ভুলে গেলে), কিন্তু কিছু কাজে ঠিক এটিই চাই: যখন উত্তরে প্রতিটি সমন্বয় থাকতে হবে, ডেটায় থাকুক বা না থাকুক।
একজন দর্জির দোকানে পাঞ্জাবি চার সাইজের (S, M, L, XL) আর তিন রঙের (Black, Blue, Red). মালিক স্টক-খাতার জন্য প্রতিটি সম্ভাব্য “সাইজ–রং” লিখতে চান — যেটির একটিও বিক্রি হয়নি, সেটিও। খাতার প্রতিটি সাইজের পাশে তিনটি রং লিখলে ১২টি লাইন হয়। এটাই CROSS JOIN.
দ্বিতীয় কাজটি রিপোর্টে খুব কাজের: ক্যালেন্ডার-ছক। “প্রতিটি শ্রেণির প্রতি মাসের বিক্রি” চাইলে শুধু GROUP BY করলে যে মাসে কোনো বিক্রি নেই, সেই ঘরটি ফল থেকে উধাও হয় — অথচ চার্টে সেখানে ০ দেখানো দরকার। সমাধান: আগে শ্রেণি × মাস দিয়ে সম্পূর্ণ ছক বানান, তারপর বিক্রির হিসাব LEFT JOIN করুন। নিচের এডিটর db-shop-এর সাথে তিনটি ছোট টেবিল আগে তৈরি করে নেয়: sizes (৪টি সাইজ), colours (৩টি রং) আর months ('2025-09', '2025-10'). db-shop-এর 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) আর categories (category_id, name) পর্ব ১১-এ দেখেছেন; পুরো ডেটা এডিটরের “নমুনা ডেটা” বাক্সে।
-- ১) সব সাইজ × সব রং
SELECT COUNT(*) AS variants FROM sizes CROSS JOIN colours;
-- ২) শ্রেণি × মাসের সম্পূর্ণ ছক, তারপর বিক্রি (বাতিল বাদ)
SELECT c.name AS category, m.month, COALESCE(s.revenue, 0) AS revenue
FROM categories c
CROSS JOIN months m
LEFT JOIN (SELECT p.category_id, substr(o.order_date, 1, 7) AS month,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY p.category_id, substr(o.order_date, 1, 7)) s
ON s.category_id = c.category_id AND s.month = m.month
ORDER BY c.category_id, m.month;প্রথম ফল 12 = ৪ × ৩. COUNT(*) মুছে SELECT * লিখলে বারোটি সাইজ–রং জোড়াই দেখবেন। দ্বিতীয় কোয়েরিতে ঠিক ৮টি সারি (৪টি শ্রেণি × ২টি মাস)। বেশিরভাগ ঘরে বিক্রি আছে — যেমন Electronics | 2025-09 | 20900 (অর্ডার 1001-এর 17400 আর pending 1006-এর 3500). কিন্তু দেখুন Clothing | 2025-10 | 0: অক্টোবরে কোনো পোশাক বিক্রি হয়নি। শুধু GROUP BY করলে এই সারিটি থাকতই না; CROSS JOIN ছকটি আগে বানিয়ে রাখায় ঘরটি টিকে আছে, আর COALESCE NULL-কে ০ বানিয়েছে। খেয়াল করুন শেষ ON-এ দুটি শর্ত — শ্রেণি আর মাস দুটোই মিলতে হবে; অংশ ৭-এ এর বিস্তারিত।
দুর্ঘটনার CROSS JOIN: পুরোনো ধাঁচের FROM orders, customers লিখে WHERE-এ join-এর শর্ত ভুলে গেলে আপনি চুপচাপ Cartesian product পান — db-shop-এ ১০ × ৮ = ৮০টি সারি, আর এক লাখ অর্ডার × দশ হাজার গ্রাহকে একশো কোটি সারি। তাই ইচ্ছাকৃত হলে CROSS JOIN শব্দটি স্পষ্ট লিখুন, আর বাকি সব join-এ JOIN … ON. পাঠক তখন বুঝবেন এটি ভুল নয়, পরিকল্পনা।
আরও কিছু বৈধ ব্যবহার: পরীক্ষার হলে প্রতিটি শিক্ষার্থী × প্রতিটি বিষয়ের খালি নম্বরপত্র তৈরি, প্রতিটি শাখা × প্রতিটি দিনের হাজিরা-ছক, বা একটি এক-সারির টেবিল (যেমন আজকের ডলার রেট) প্রতিটি সারির পাশে জুড়ে দেওয়া। শেষটিতে ১ × n = n, তাই কোনো বিস্ফোরণ নেই।
৪ · Non-equi join — সমতা নয়, পরিসর মিলিয়ে
এ পর্যন্ত প্রায় সব join-এর শর্ত ছিল সমতা (=) — একে বলে equi join. কিন্তু ON-এর ভেতরে যেকোনো শর্ত লেখা যায়: <, >=, BETWEEN, এমনকি ফাংশন। সমতা ছাড়া অন্য শর্তে join-কে বলে non-equi join. অংশ ১-এর a.student_id < b.student_id-ও আসলে এর একটি উদাহরণ। সবচেয়ে প্রচলিত ব্যবহার হলো পরিসরের টেবিল: কোন মান কোন স্তরে পড়ে।
বিদ্যুৎ বিলের স্ল্যাব ভাবুন: প্রথম এত ইউনিট এক দরে, পরের এত ইউনিট আরেক দরে। বিলের অফিস প্রতিটি গ্রাহকের ইউনিট নিয়ে স্ল্যাবের তালিকায় খোঁজে “কোন সীমার মধ্যে পড়ল?”। স্ল্যাবের তালিকা একটি টেবিল, আর খোঁজাটি একটি non-equi join. দর বদলালে শুধু তালিকা বদলায়, কোয়েরি নয়।
দোকানের মালিক পণ্যগুলো চারটি দামের স্তরে দেখাতে চান। নিচের এডিটর একটি price_bands টেবিল বানায় — band (স্তরের নাম), min_price আর max_price: Budget 0–500, Mid 500–2000, Premium 2000–10000, Luxury 10000–1000000. লক্ষ করুন সীমাগুলো অর্ধ-খোলা: নিচের সীমা অন্তর্ভুক্ত, ওপরেরটি নয় (min_price <= price < max_price). তাই ঠিক 500 টাকার পণ্য শুধু Mid-এ পড়ে, দুই স্তরে নয়।
-- প্রতিটি পণ্য কোন দামের স্তরে
SELECT p.name, p.price, b.band
FROM products p
JOIN price_bands b
ON p.price >= b.min_price AND p.price < b.max_price
ORDER BY p.price;
-- প্রতিটি স্তরে কয়টি পণ্য (খালি স্তরও দেখাবে)
SELECT b.band, COUNT(p.product_id) AS products
FROM price_bands b
LEFT JOIN products p ON p.price >= b.min_price AND p.price < b.max_price
GROUP BY b.band, b.min_price
ORDER BY b.min_price;প্রথম কোয়েরিতে ১০টি সারি — প্রতিটি পণ্য ঠিক একবার: Mustard Oil 1L (260) আর Rice 5kg (420) Budget; SQL Book (550) থেকে Power Bank (1800) পর্যন্ত চারটি Mid; Panjabi, Smart Watch আর Saree Premium; Mobile Phone (15000) Luxury. দ্বিতীয় কোয়েরি: Budget | 2, Mid | 4, Premium | 3, Luxury | 1 — যোগফল ১০, মোট পণ্যের সংখ্যার সমান। এই মিলিয়ে দেখাটাই non-equi join-এর সবচেয়ে বড় পরীক্ষা: যোগফল বেশি হলে কোথাও পরিসর একে অপরকে ঢেকেছে, কম হলে মাঝে ফাঁক আছে।
নম্বর থেকে letter grade, letter grade থেকে GPA
বিশ্ববিদ্যালয়ের সবচেয়ে পরিচিত পরিসর-টেবিল হলো grade-এর তালিকা। নিচে একটি grade_points টেবিল — letter, point, min_mark আর max_mark — বাংলাদেশের অনেক বিশ্ববিদ্যালয়ে প্রচলিত একটি স্কেল ধরে: 80–100 A+ (4.00), 75–79 A (3.75), 70–74 A- (3.50), 65–69 B+ (3.25) … 40–44 D (2.00), 0–39 F (0.00). (প্রতিষ্ঠানভেদে সীমা আলাদা হতে পারে — আপনারটি দেখে নেবেন।) নম্বর পূর্ণসংখ্যা বলে এখানে দুই দিক-অন্তর্ভুক্ত BETWEEN নিরাপদ। সাথে একটি ছোট lab_marks টেবিল (student_id, marks): চারজন শিক্ষার্থীর ল্যাব-পরীক্ষার নম্বর 82, 67, 74, 38. প্রথমে দেখুন প্রতিটি নম্বর কীভাবে একটি সীমা খুঁজে পায়:
| এখনকার সারি | |
| শর্ত | |
| মিলের সংখ্যা | |
| ফলের সারি |
- পরিসরের টেবিল। দশটি সারি, প্রতিটি একটি বন্ধ পরিসর। join-এর শর্ত
m.marks BETWEEN g.min_mark AND g.max_mark— প্রতিটি নম্বরের জন্য DBMS এমন সারি খোঁজে, যার সীমার ভেতরে নম্বরটি পড়ে। - 2101-এর 82. শুধু A+ সারির 80–100-এ পড়ে। একটি মিল, ফলে একটি সারি: A+, 4.00.
- 2102-এর 67. 65–69 — B+, 3.25. বাকি নয়টি সারিতে শর্ত মিথ্যা।
- 2201-এর 74 — ঠিক সীমায়। BETWEEN দুই প্রান্তই ধরে, তাই 74 পড়ে A- (70–74)-এ। পরের সারি A শুরু 75 থেকে, তাই দুই সারিতে মেলার ভয় নেই।
- 2301-এর 38. F (0–39). চারটি নম্বর, চারটি ফল — প্রতিটি নম্বর ঠিক একটি সীমায়, কারণ পূর্ণসংখ্যার জন্য সীমাগুলো ফাঁকহীন আর পরস্পর-বিচ্ছিন্ন।
- দশমিক নম্বর এলে? কেউ 79.5 পেলে কোনো সারিতেই মেলে না (79 আর 80-এর মাঝে ফাঁক), আর INNER JOIN নিঃশব্দে শিক্ষার্থীকেই ফল থেকে বাদ দেয়। সমাধান অর্ধ-খোলা সীমা:
marks >= min_mark AND marks < next_min.
এবার আসল কাজ: এই একই lookup টেবিল দিয়ে প্রত্যেক শিক্ষার্থীর GPA. পর্ব ১১-এ এই হিসাব করেছিলেন দশ লাইনের একটি CASE দিয়ে; এখন grade-এর point আসে একটি টেবিল থেকে, join করে। স্কেল বদলালে কোয়েরি বদলাতে হবে না, শুধু টেবিলের সারি।
-- ১) non-equi join: নম্বর → letter grade
SELECT m.student_id, m.marks, g.letter, g.point
FROM lab_marks m
JOIN grade_points g ON m.marks BETWEEN g.min_mark AND g.max_mark
ORDER BY m.student_id;
-- ২) equi join: letter → point, তারপর credit দিয়ে ভারিত গড়
SELECT e.student_id,
ROUND(SUM(c.credits * g.point) / SUM(c.credits), 2) AS gpa
FROM enrollments e
JOIN grade_points g ON g.letter = e.grade
JOIN courses c ON c.course_id = e.course_id
GROUP BY e.student_id
ORDER BY e.student_id;প্রথম ফল ডায়াগ্রামের সাথে হুবহু: 2101 | 82 | A+ | 4, 2102 | 67 | B+ | 3.25, 2201 | 74 | A- | 3.5, 2301 | 38 | F | 0. দ্বিতীয় ফলে সাতজন: 2101 → 3.65, 2102 → 3.13, 2103 → 2.5, 2201 → 3.75, 2202 → 2.75, 2203 → 0, 2301 → 3.38 — পর্ব ১১-এর CASE-ভিত্তিক হিসাবের সাথে প্রতিটি মিলে যায়। grade NULL এমন সারি (2201-এর CSE301) নিজে থেকেই বাদ পড়েছে, কারণ g.letter = NULL সত্য নয় — এখানে সেটিই চাই, কারণ ফল না আসা কোর্স GPA-তে ধরা হয় না। কিন্তু সাবধান: enrollments-এ কেউ 'Z'-এর মতো ভুল grade লিখলে সেই সারিও চুপচাপ বাদ যাবে; সারির সংখ্যা মিলিয়ে দেখার অভ্যাস রাখুন।
Non-equi join-এর দাম মনে রাখুন: সমতার join-এ DBMS hash table বানিয়ে প্রতিটি সারির মিল প্রায় O(1)-এ খুঁজতে পারে (পর্ব ৪২), কিন্তু < বা BETWEEN-এ hash কাজ করে না। তখন ভরসা nested loop (m × n তুলনা) বা সাজানো ডেটা আর index-এর ওপর পরিসর-খোঁজ। দশ সারির lookup টেবিলে এটি টেরই পাবেন না; দুটি বড় টেবিলের মধ্যে পরিসর-join-এ পাবেন।
৫ · Semi join — “অন্তত একটি আছে কি?”
প্রশ্ন: “কোন কোন গ্রাহক অন্তত একটি অর্ডার হাতে পেয়েছেন (delivered)?” উত্তরে চাই গ্রাহকের তালিকা — প্রত্যেকে একবার। অর্ডারের কোনো কলাম চাই না; orders টেবিলটি শুধু একটি পরীক্ষা: মিল আছে কি না। এমন join-কে বলে semi join. SQL-এ এর আলাদা keyword নেই; লেখা হয় EXISTS বা IN দিয়ে।
পরীক্ষার হলে পরিদর্শক জানতে চান, “এই সারির কোন কোন শিক্ষার্থী অন্তত একটি অতিরিক্ত খাতা নিয়েছেন?” কেউ তিনটি খাতা নিলেও তালিকায় তাঁর নাম একবারই উঠবে। প্রথম অতিরিক্ত খাতা দেখেই টিক দিয়ে পরের জনের কাছে চলে যান — বাকি খাতা গোনার দরকার নেই।
সাধারণ JOIN দিয়ে লিখলে সমস্যাটি কোথায়, ধাপে ধাপে দেখুন। প্রতিটি গ্রাহকের পাশে তাঁর delivered অর্ডারগুলো লেখা আছে; বাঁ কলামে JOIN-এর ফল, ডানে semi join-এর:
| JOIN-এর সারি | |
| EXISTS-এর সারি | |
| EXISTS কতটি অর্ডার দেখল |
- Rahim Uddin. তাঁর delivered অর্ডার একটি (1001). JOIN একটি সারি দেয়, EXISTS-ও একটি। এখনো কোনো পার্থক্য নেই।
- Karima Begum — দুটি মিল। 1002 আর 1007 দুটিই delivered. JOIN প্রতিটি মিলের জন্য একটি সারি বানায়, তাই Karima দুবার। EXISTS প্রথম মিল (1002) পেয়েই “হ্যাঁ” বলে থামে — Karima একবার।
- Sabbir আর Nusrat. Sabbir-এর একমাত্র অর্ডার বাতিল, তাই দুই পদ্ধতিতেই বাদ। Nusrat-এর একটি delivered (1005) — দুই দিকে একটি করে সারি।
- Tanvir আর Farhana. Tanvir-এর কোনো অর্ডারই নেই, Farhana-র একমাত্রটি pending. কারও কোনো সারি যোগ হয় না।
- Imran আর Mitu. Imran-এর 1010 delivered — একটি করে সারি। Mitu-র 1008 এখনো shipped, তাই বাদ।
- ফল: ৫ বনাম ৪। JOIN-এ Karima দুবার, কারণ JOIN-এর কাজই প্রতিটি মিলকে আলাদা সারি বানানো। semi join-এ প্রত্যেক গ্রাহক সর্বোচ্চ একবার — মিল যতগুলোই হোক।
-- ১) সাধারণ JOIN: যতগুলো মিল, ততবার
SELECT c.customer_id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered';
-- ২) semi join, EXISTS দিয়ে
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
AND o.status = 'delivered');
-- ৩) semi join, IN দিয়ে
SELECT customer_id, name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders
WHERE status = 'delivered');প্রথম কোয়েরিতে ৫টি সারি, যার মধ্যে 2 | Karima Begum দুবার (এখানে ক্রম Rahim, Karima, Nusrat, Karima, Imran; ORDER BY ছাড়া ক্রমের কোনো নিশ্চয়তা নেই). দ্বিতীয় আর তৃতীয় কোয়েরিতে ৪টি করে সারি — Rahim Uddin, Karima Begum, Nusrat Jahan, Imran Hossain — প্রত্যেকে একবার। EXISTS-এর ভেতরের SELECT 1-এ কী লিখলেন তাতে কিছু আসে যায় না; EXISTS শুধু দেখে অন্তত একটি সারি আছে কি না। ভেতরের কোয়েরিটি বাইরের c.customer_id ব্যবহার করছে — একে বলে correlated subquery, পর্ব ২১-এ বিস্তারিত।
“তাহলে JOIN-এর পরে DISTINCT দিলেই তো হয়?” এই ছোট ডেটায় হয়। কিন্তু তিনটি কারণে semi join-ই সঠিক প্রকাশ। এক, DISTINCT পুরো সারি দেখে: দুজন আলাদা গ্রাহকের নাম আর শহর এক হলে, আর আপনি customer_id না দেখালে, তাঁরা এক হয়ে যাবেন। দুই, DISTINCT আগে সব duplicate তৈরি করে, তারপর সাজিয়ে বা hash করে মোছে — একজন গ্রাহকের হাজারটি অর্ডার থাকলে হাজার সারি বানিয়ে ৯৯৯টি ফেলে দেওয়া। তিন, JOIN-এর পর SUM বা COUNT করলে duplicate গোনায় ঢুকে যায় (এক সারির মান অনেকবার যোগ — fan-out), আর DISTINCT সেটি বাঁচাতে পারে না। নিয়মটি সহজ: ডান টেবিলের কোনো কলাম ফলে না লাগলে semi join; লাগলে JOIN.
IN আর EXISTS-এর মধ্যে কোনটি দ্রুত? পুরোনো বইয়ে প্রায়ই পাবেন “বড় সাবকোয়েরিতে EXISTS দ্রুত”। আধুনিক optimizer (PostgreSQL, SQL Server, Oracle, নতুন MySQL) সাধারণত দুটিকেই একই semi join পরিকল্পনায় বদলে নেয়, তাই অনুমান না করে EXPLAIN দেখুন (পর্ব ৪১)। অর্থের দিক থেকে দুটি সমান — যতক্ষণ না NOT লাগে। NOT-এর গল্প পরের অংশে।
৬ · Anti join — “একটিও নেই” আর NOT IN-এর NULL ফাঁদ
Semi join-এর উল্টো প্রশ্ন: “কোন বিভাগে একজনও শিক্ষার্থী নেই?”, “কোন পণ্য কখনো বিক্রি হয়নি?”, “কোন গ্রাহক কখনো অর্ডার দেননি?” — বাঁ টেবিলের সেই সারিগুলো, যাদের ডান টেবিলে কোনো মিল নেই। একে বলে anti join. SQL-এ এটি লেখার তিনটি প্রচলিত ধরন আছে, আর তিনটি সবসময় একই ফল দেয় না।
NOT EXISTS
“এই বিভাগের এমন কোনো শিক্ষার্থী আছে কি? না থাকলে রাখো।” সরাসরি প্রশ্নটিই লেখা। NULL নিয়ে কোনো বিস্ময় নেই।
সবচেয়ে নিরাপদLEFT JOIN … IS NULL
LEFT JOIN করুন, তারপর যেসব সারির ডান পাশ পুরো NULL (মিল নেই), শুধু সেগুলো রাখুন। পর্ব ১৯-এর পরিচিত কৌশল।
নিরাপদ, NOT NULL কলামে পরীক্ষা করলেNOT IN (subquery)
“এই বিভাগের id কি শিক্ষার্থীদের dept_id-এর তালিকায় নেই?” পড়তে সহজ — কিন্তু তালিকায় একটি NULL থাকলে ফল শূন্য।
ফাঁদdb-uni-তে চারটি বিভাগ; শিক্ষার্থীদের dept_id-গুলো 1, 1, 2, 1, 3, 2, 1 আর Habib Ullah-র NULL. অর্থাৎ উত্তর হওয়া উচিত শুধু English (4). তিনটি ধরন একসাথে চালান:
-- ১) NOT EXISTS
SELECT d.name FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM students s WHERE s.dept_id = d.dept_id);
-- ২) LEFT JOIN … IS NULL (primary key কলাম পরীক্ষা)
SELECT d.name FROM departments d
LEFT JOIN students s ON s.dept_id = d.dept_id
WHERE s.student_id IS NULL;
-- ৩) NOT IN — তালিকায় Habib-এর NULL
SELECT d.name FROM departments d
WHERE d.dept_id NOT IN (SELECT s.dept_id FROM students s);
-- ৪) NOT IN, NULL বাদ দিয়ে
SELECT d.name FROM departments d
WHERE d.dept_id NOT IN (SELECT s.dept_id FROM students s
WHERE s.dept_id IS NOT NULL);প্রথম, দ্বিতীয় আর চতুর্থ কোয়েরিতে একটিই সারি: English. তৃতীয়টিতে ০টি সারি — কোনো ত্রুটি নেই, কোনো সতর্কবার্তা নেই, শুধু ফাঁকা ফল। রিপোর্টে লেখা হবে “সব বিভাগে শিক্ষার্থী আছে”, যা মিথ্যা। কারণ একটি NULL: Habib Ullah এখনো বিভাগ বাছেননি। একই ফাঁদ db-uni-র instructors-এও: “কে কারও mentor নন?” প্রশ্নে NOT IN (SELECT mentor_id FROM instructors) ফাঁকা ফল দেবে, কারণ তিনজনের mentor_id NULL.
কেন একটি NULL পুরো ফল মুছে দেয় — প্রমাণ
x NOT IN (a, b, c)-এর সংজ্ঞাই হলো x <> a AND x <> b AND x <> c. English-এর জন্য x = 4, আর তালিকা (1, 1, 2, 1, 3, 2, 1, NULL):
4 <> 1,4 <> 2,4 <> 3— সবই TRUE.4 <> NULL— UNKNOWN, কারণ NULL মানে “অজানা”; অজানা মানটি 4 হতেও পারে।- তিন-মানের যুক্তিতে TRUE AND UNKNOWN = UNKNOWN. একটি UNKNOWN থাকলে AND-এর ফল কখনো TRUE হতে পারে না — বড়জোর UNKNOWN, কোনো FALSE থাকলে FALSE.
- WHERE শুধু TRUE সারি রাখে। তাই কোনো x-এর জন্যই শর্ত TRUE হয় না: সাবকোয়েরির ফলে অন্তত একটি NULL থাকলে
NOT INকখনো কোনো সারি দেয় না। ∎
উল্টো দিকে IN নিরাপদ: x IN (…) মানে OR-এর শিকল, আর TRUE OR UNKNOWN = TRUE — মিল থাকলে সারিটি আসে, না থাকলে UNKNOWN হয়, যা WHERE-এ FALSE-এর মতোই বাদ। তাই semi join-এ IN আর EXISTS সমান, কিন্তু anti join-এ NOT IN আর NOT EXISTS সমান নয়। NOT EXISTS প্রতিটি সারির জন্য শুধু জিজ্ঞেস করে “এমন সারি আছে কি?” — যার উত্তর সবসময় TRUE বা FALSE, কখনো UNKNOWN নয়।
দুটি পরিচিত ভুল: (১) LEFT JOIN … IS NULL-এ এমন কলাম পরীক্ষা করুন যা ডান টেবিলে কখনো NULL নয় — primary key বা join-এর কলাম। WHERE s.cgpa IS NULL লিখলে Fahim Karim-এর মতো মিল থাকা কিন্তু cgpa-হীন সারিও ঢুকে যাবে। (২) ডান টেবিলের শর্ত WHERE-এ নয়, ON-এ লিখুন। “কোন গ্রাহক কখনো delivered অর্ডার পাননি?” — LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status = 'delivered' WHERE o.order_id IS NULL. status-এর শর্ত WHERE-এ গেলে LEFT JOIN আসলে inner join হয়ে যায় (অনুশীলন ৪ দেখুন)।
তাহলে কি NOT IN কখনো লিখবেন না? সাবকোয়েরির কলামটি NOT NULL হলে (যেমন db-shop-এর orders.customer_id) NOT IN সঠিক ফল দেয়: “কখনো অর্ডার দেননি” প্রশ্নে উত্তর Tanvir Ahmed. কিন্তু আজ NOT NULL কলাম কাল কেউ বদলে দিতে পারেন, আর তখন কোয়েরিটি কোনো ত্রুটি ছাড়াই ভুল ফল দেবে। পেশাদার অভ্যাস: anti join মানেই NOT EXISTS. হাতে লেখা মানের তালিকায় (NOT IN ('cancelled', 'pending')) NULL নেই জানলে NOT IN ঠিক আছে।
৭ · একাধিক কলামে join, grouped subquery আর LATERAL
৭.১ · Composite key-তে join — সব কলাম মেলাতে হবে
যখন কোনো সারিকে চিনতে একাধিক কলাম লাগে (composite key), join-এর শর্তেও সবগুলো লিখতে হয়। একটি উদাহরণ: বিশ্ববিদ্যালয় নিশ্চিত হতে চায়, শিক্ষার্থীরা পূর্বশর্ত-কোর্স (prerequisite) পাস করে তারপর উঁচু কোর্স নিয়েছেন কি না। নিচের এডিটর একটি prerequisites টেবিল বানায় — course_id আর prereq_id, দুটি মিলে primary key: CSE201-এর আগে CSE101, CSE301 আর CSE302-এর আগে CSE201. এখন enrollments-কে নিজের সাথে join করতে হবে: “একই শিক্ষার্থী আর পূর্বশর্ত-কোর্সটি”।
SELECT e.student_id, e.course_id, p.prereq_id,
done.term AS prereq_term, done.grade AS prereq_grade
FROM enrollments e
JOIN prerequisites p ON p.course_id = e.course_id
LEFT JOIN enrollments done
ON done.student_id = e.student_id
AND done.course_id = p.prereq_id
AND done.grade IS NOT NULL AND done.grade <> 'F'
ORDER BY e.student_id, e.course_id;
-- student_id মেলাতে ভুলে গেলে?
SELECT COUNT(*) AS rows_if_student_id_forgotten
FROM enrollments e
JOIN prerequisites p ON p.course_id = e.course_id
LEFT JOIN enrollments done ON done.course_id = p.prereq_id;প্রথম কোয়েরিতে ৭টি সারি — পূর্বশর্ত আছে এমন প্রতিটি ভর্তি একবার। পাঁচটিতে পূর্বশর্ত পাস পাওয়া গেছে, যেমন 2101 | CSE301 | CSE201 | 2022-Spring | A. দুটিতে শেষ দুই কলাম NULL: 2101 | CSE201 | CSE101 | NULL | NULL আর 2102 | CSE201 | CSE101 | NULL | NULL — নমুনা ডেটায় Arif আর Bristy-র CSE101-এর কোনো রেকর্ড নেই (হয়তো আগের ব্যবস্থায় ছাড় পেয়েছিলেন; বাস্তবে এগুলো যাচাইয়ের তালিকা)। দ্বিতীয় কোয়েরির ফল 18: student_id ভুলে গেলে প্রতিটি ভর্তি সব শিক্ষার্থীর পূর্বশর্ত-রেকর্ডের সাথে মিলেছে, আর ৭টির জায়গায় ১৮টি সারি — অন্য কারও A দেখে মনে হবে Arif পাস করেছেন। (একটি ছোট সতর্কতা: term-গুলো লেখা হিসেবে তুলনা করবেন না; লেখার ক্রমে '2023-Fall' আসে '2023-Spring'-এর আগে, যদিও Fall বছরের পরের অংশে।)
যখন দুই টেবিলে কলামগুলোর নাম হুবহু এক, তখন সংক্ষেপে JOIN … USING (student_id, course_id) লেখা যায় (SQL Server ছাড়া প্রধান DBMS-গুলোতে)। কিন্তু NATURAL JOIN এড়িয়ে চলুন: এটি একই নামের সব কলামে নিজে থেকে join করে, আর কাল কেউ দুই টেবিলে created_at যোগ করলে কোয়েরির অর্থ নিঃশব্দে বদলে যাবে।
৭.২ · Grouped subquery-র সাথে join — প্রতিজনের সর্বশেষ অর্ডার
“প্রতিটি গ্রাহকের সর্বশেষ অর্ডারটি — order_id আর status সহ।” শুধু GROUP BY customer_id আর MAX(order_date) দিলে তারিখ পাবেন, কিন্তু সেই অর্ডারের id বা status নয় (সেগুলো group-এর ভেতরে অনেকগুলো)। কৌশল: প্রথমে একটি derived table (FROM-এর ভেতরের subquery)-তে প্রতি গ্রাহকের সর্বশেষ তারিখ বের করুন, তারপর সেই ফলকে orders-এর সাথে দুই কলামে join করে আসল সারিটি ফিরিয়ে আনুন:
SELECT c.name, o.order_id, o.order_date, o.status
FROM customers c
JOIN (SELECT customer_id, MAX(order_date) AS last_date
FROM orders
GROUP BY customer_id) lo ON lo.customer_id = c.customer_id
JOIN orders o ON o.customer_id = lo.customer_id
AND o.order_date = lo.last_date
ORDER BY o.order_date DESC;৭টি সারি, সবচেয়ে নতুন আগে: Imran Hossain | 1010 | 2025-10-09 | delivered, তারপর Rahim Uddin-এর 1009 (pending), Mitu Rani-র 1008, Karima Begum-এর 1007, Farhana Akter-এর 1006, Nusrat Jahan-এর 1005 আর Sabbir Hasan-এর 1004 (cancelled). Rahim-এর তিনটি অর্ডারের মধ্যে শুধু 1009 এসেছে। Tanvir Ahmed নেই — তাঁর কোনো অর্ডার নেই, আর দুটি join-ই inner. সতর্কতা: একই গ্রাহকের একই দিনে দুটি অর্ডার থাকলে দুটিই আসবে, কারণ দুটিই “সর্বশেষ তারিখ”-এর সাথে মেলে। নির্দিষ্টভাবে একটি চাইলে পর্ব ২৬-এর ROW_NUMBER() লাগে; এমন “প্রতি দলের সেরা N” সমস্যার সব প্যাটার্ন পর্ব ২৭-এ।
৭.৩ · LATERAL আর APPLY — প্রতিটি সারির জন্য আলাদা subquery
উপরের derived table-টি একবারই হিসাব হয়, আর বাইরের কোনো সারি “দেখতে” পায় না। কিন্তু ধরুন প্রতিটি গ্রাহকের শেষ দুটি অর্ডার চাই। তখন সবচেয়ে স্বাভাবিক চিন্তা: “প্রতিটি গ্রাহকের জন্য, তাঁর অর্ডারগুলো তারিখ অনুযায়ী সাজিয়ে প্রথম দুটি নাও।” এর জন্য FROM-এর subquery-কে বাঁ পাশের সারি দেখতে দিতে হয় — একে বলে lateral join. SQL:1999 মানে এর নাম LATERAL; PostgreSQL (9.3 থেকে), MySQL (8.0.14 থেকে) আর Oracle (12c থেকে) এটি চালায়। SQL Server-এ একই ধারণার নাম CROSS APPLY আর OUTER APPLY. SQLite-এ এটি নেই, তাই নিচের কোড পাতায় চলবে না:
-- প্রতিটি গ্রাহকের শেষ দুটি অর্ডার
SELECT c.name, t.order_id, t.order_date
FROM customers c
CROSS JOIN LATERAL (
SELECT o.order_id, o.order_date
FROM orders o
WHERE o.customer_id = c.customer_id -- বাঁ পাশের সারি দেখছে
ORDER BY o.order_date DESC
LIMIT 2
) AS t;
-- অর্ডারহীন গ্রাহককেও রাখতে: LEFT JOIN LATERAL (...) AS t ON trueSELECT c.name, t.order_id, t.order_date
FROM customers c
CROSS APPLY (
SELECT TOP (2) o.order_id, o.order_date
FROM orders o
WHERE o.customer_id = c.customer_id
ORDER BY o.order_date DESC
) AS t;
-- অর্ডারহীন গ্রাহককেও রাখতে: OUTER APPLYফলটি হতো: Rahim Uddin-এর 1009 আর 1003, Karima Begum-এর 1007 আর 1002, আর বাকি পাঁচজনের একটি করে অর্ডার — মোট ৯টি সারি; Tanvir শুধু LEFT JOIN LATERAL বা OUTER APPLY-তে, NULL-সহ। ধারণাগতভাবে LATERAL হলো “প্রতিটি বাঁ-সারির জন্য একবার চলা একটি correlated subquery, যা একাধিক সারি আর কলাম ফেরত দিতে পারে”। SQLite আর অন্য সব DBMS-এ একই কাজ পর্ব ২৬-এর window function দিয়েও করা যায়।
৮ · Join-এর ক্রম আর relational algebra-র চোখে join
Inner join-এর ক্রম মুক্ত, outer join-এর নয়
তিনটি টেবিল A JOIN B JOIN C — আগে A আর B জুড়ে তারপর C, নাকি আগে B আর C? Inner join-এ উত্তর একই। কারণ inner join আসলে “Cartesian product, তারপর সব শর্ত দিয়ে ছাঁকা”, আর ছাঁকার শর্তগুলোর AND যে ক্রমেই প্রয়োগ করুন, একই সারি টেকে: σp(σq(A × B × C)) = σp ∧ q(A × B × C), আর × নিজেও (কলামের ক্রম বাদে) বিনিময়যোগ্য ও সংযোজনযোগ্য। তাই optimizer inner join-গুলো যে ক্রমে সস্তা, সেই ক্রমে চালাতে পারে (পর্ব ৪৩)। কিন্তু outer join NULL দিয়ে সারি “বাঁচিয়ে রাখে”, আর পরের একটি inner join সেই বাঁচানো সারি আবার ফেলে দিতে পারে। নিচে প্রশ্ন: “প্রতিটি বিভাগ, তার কোর্স আর শিক্ষক — কোর্স বা শিক্ষক না থাকলেও বিভাগটি দেখাও।”
| সারি | |
| English দেখা যায়? | |
| প্রশ্নের উত্তর? |
- তিনটি টেবিল। চারটি বিভাগের প্রত্যেকের অন্তত একটি কোর্স আছে, কিন্তু English-এর ENG101-এর শিক্ষক এখনো ঠিক হয়নি (instructor_id NULL).
- ক, প্রথম ধাপ: departments LEFT JOIN courses. ৭টি সারি, English-ও আছে ENG101-সহ। এখন পর্যন্ত সব ঠিক।
- ক, দ্বিতীয় ধাপ: তারপর সাধারণ JOIN instructors. ENG101-এর
NULL = i.instructor_idকখনো সত্য নয়, তাই সারিটি বাদ — ৬টি সারি, English উধাও। LEFT JOIN-এর প্রতিশ্রুতি পরের inner join ভেঙে দিল। - খ: বন্ধনী দিয়ে ক্রম বদল.
d LEFT JOIN (c JOIN i)-এ আগে কোর্স–শিক্ষক জোড়া (ENG101 বাদ), তারপর বিভাগের সাথে LEFT — English থাকে, কিন্তু কোর্স আর শিক্ষক দুটিই NULL. ৭টি সারি, তবে ENG101 যে আছে তা জানা গেল না। - গ: দুটিই LEFT. English │ ENG101 │ NULL — বিভাগ, কোর্স আর “শিক্ষক নেই” তিনটি তথ্যই ঠিক। ৭টি সারি। প্রশ্নের সঠিক উত্তর এটিই।
- শুধু inner join হলে ক্রম মুক্ত. তিনটিই JOIN হলে যে ক্রমে বা বন্ধনীতেই লিখুন, ফল ৬টি সারি — কিন্তু সেটি “শিক্ষক আছে এমন কোর্স” প্রশ্নের উত্তর, এই প্রশ্নের নয়।
বাস্তব নিয়ম: একবার LEFT JOIN শুরু করলে, সেই টেবিলের ওপর নির্ভরশীল পরের join-গুলোও সাধারণত LEFT হওয়া উচিত — নইলে প্রশ্ন করুন, “আমি কি সত্যিই চাই NULL-যুক্ত সারিগুলো এখানে বাদ পড়ুক?” একই কারণে LEFT JOIN-এর ডান টেবিলের শর্ত WHERE-এ লিখলেও outer join নিঃশব্দে inner হয়ে যায়।
Relational algebra-র ভাষায় — পর্ব ৩৩-এর পূর্বাভাস
পর্ব ৩২–৩৩-এ relational algebra আনুষ্ঠানিকভাবে আসবে; এই পর্বের join-গুলো সেখানে এভাবে লেখা হয় (R বাঁ, S ডান relation, θ শর্ত):
| SQL | Algebra | সংজ্ঞা |
|---|---|---|
CROSS JOIN | R × S | সব জোড়া; |R × S| = |R| · |S| |
JOIN … ON θ (equi বা non-equi) | R ⋈θ S | σθ(R × S) |
| Self join | ρa(R) ⋈θ ρb(R) | ρ (rename) দিয়ে একই relation-কে দুটি নাম |
EXISTS / IN | R ⋉θ S | πattr(R)(R ⋈θ S) — শুধু R-এর কলাম |
NOT EXISTS | R ▷θ S | R − (R ⋉θ S) |
ছোট প্রমাণ — semi join কখনো সারি বাড়ায় না: সংজ্ঞা অনুযায়ী R ⋉ S-এর প্রতিটি tuple হলো R ⋈ S-এর কোনো tuple-এর R-অংশ, অর্থাৎ R-এরই একটি tuple. তাই R ⋉ S ⊆ R, ফলে |R ⋉ S| ≤ |R|. অন্যদিকে R ⋈ S-এ R-এর একটি tuple যতগুলো S-tuple-এর সাথে মেলে, ততবার আসে — Karima-র দুটি delivered অর্ডার মানে দুটি সারি। (SQL-এর bag semantics-এ semi join R-এর প্রতিটি সারিকে ঠিক যতবার R-এ আছে ততবার রাখে, মিলের সংখ্যা যা-ই হোক।) আর R ▷ S = R − (R ⋉ S) থেকে সরাসরি পাওয়া যায় (R ⋉ S) ∪ (R ▷ S) = R, আর দুটি পরস্পর-বিচ্ছিন্ন: প্রতিটি বাঁ-সারি ঠিক একটিতে পড়ে — “অন্তত একটি মিল আছে” অথবা “একটিও নেই”। এই বিভাজন NOT EXISTS-এর জন্য সবসময় সত্য, কিন্তু NULL-যুক্ত NOT IN-এর জন্য নয় — অংশ ৬-এ English দুই দিকের কোনোটিতেই পড়েনি।
আরও গভীরে: PostgreSQL-এ EXPLAIN চালালে EXISTS আর NOT EXISTS-এর পরিকল্পনায় প্রায়ই সরাসরি “Hash Semi Join” বা “Hash Anti Join” লেখা দেখবেন — optimizer ভেতরে সত্যিই এই আলাদা অপারেটর ব্যবহার করে, যা প্রথম মিল পেলেই (semi) বা কোনো মিল না পেলে (anti) সিদ্ধান্ত নেয়। কিন্তু nullable কলামে NOT IN-কে সে সাধারণত anti join-এ বদলাতে পারে না, কারণ ওপরের NULL-নিয়মে দুটির অর্থ আলাদা; তখন পরিকল্পনা হয় একটি filter আর subplan, যা বড় টেবিলে অনেক ধীর হতে পারে। অর্থাৎ NOT EXISTS শুধু সঠিকই নয়, প্রায়ই দ্রুতও।
৯ · AI-এর যুগে advanced join
“প্রতিটি গ্রাহক যিনি কখনো delivered অর্ডার পাননি” — AI কয়েক সেকেন্ডে একটি কোয়েরি দেবে, আর সেটি চলবেও। কিন্তু join-এর ভুলগুলো ত্রুটি দেয় না; দেয় দেখতে বিশ্বাসযোগ্য, অথচ ভুল সংখ্যা। এই পর্বের বিষয়ে AI-এর প্রচলিত ভুল:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
Anti join-এ NOT IN (SELECT nullable_col …) | একটি NULL থাকলেই ফাঁকা ফল | সাবকোয়েরিতে WHERE col IS NULL গুনে দেখুন; NOT EXISTS-এ বদলান |
| “অন্তত একটি” প্রশ্নে JOIN, তারপর COUNT বা SUM | duplicate, ফুলে ওঠা যোগফল | ফলের সারি গুনে বাঁ টেবিলের সারির সাথে মেলান |
| LEFT JOIN-এর ডান টেবিলের শর্ত WHERE-এ | outer join নিঃশব্দে inner | মিলহীন একটি সারি (Tanvir) ফলে আছে কি না দেখুন |
Self join-এ <>, < নয় | প্রতিটি জোড়া দুবার | ফল n(n−1)/2-এর সাথে মেলান |
| Composite key-র একটি কলামে join | সারি বিস্ফোরণ, অন্যের ডেটা মিশে যায় | key-র সব কলাম ON-এ আছে কি না দেখুন |
| পরিসর-join-এ BETWEEN দুই দিকে, সীমা এক | সীমার মান দুই স্তরে | প্রতিটি স্তরের গণনার যোগফল = মোট সারি? |
| অজানা গভীরতার hierarchy-তে ৩–৪টি self join | গভীর স্তর নিঃশব্দে কাটা | শেষ স্তরের mentor_id সব NULL কি না দেখুন; recursive CTE চান |
| SQLite-এ LATERAL বা SQL Server-এ LIMIT | অন্য DBMS-এ চলে না | প্রম্পটে DBMS আর সংস্করণ লিখে দিন |
AI-কে শুধু “কোয়েরি লেখো” নয়, বলুন কোন join-এর ধরন, কেন, আর কোন পরীক্ষায় এটি ধরা পড়বে — তাতে উত্তর যাচাই করা সহজ হয়:
AI-এর লেখা join কোয়েরি হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Self join কী, একটি উদাহরণ দিন। — একই টেবিলকে দুটি alias দিয়ে নিজের সাথে join; যেমন employee–manager বা instructor–mentor.
- ৫ সারি আর ৪ সারির টেবিলের CROSS JOIN-এ কত সারি? — ৫ × ৪ = ২০.
- NOT IN আর NOT EXISTS-এর পার্থক্য? — সাবকোয়েরিতে NULL থাকলে NOT IN কোনো সারি দেয় না (তিন-মানের যুক্তি); NOT EXISTS ঠিক ফল দেয়।
- Semi join আর inner join-এর পার্থক্য? — semi join বাঁ টেবিলের প্রতিটি মিল-থাকা সারি একবার দেয়, শুধু বাঁ-কলাম; inner join প্রতিটি মিলের জন্য একটি সারি দেয়।
- Non-equi join-এর একটি উদাহরণ? — নম্বর BETWEEN min_mark AND max_mark দিয়ে grade বের করা, বা বেতনের স্ল্যাবে কর নির্ধারণ।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এখানে উত্তরের চেয়ে যুক্তিটা বেশি জরুরি।
প্র ০১একটি NULL পুরো NOT IN-এর ফল মুছে দেয় — এটা কি SQL-এর নকশার ভুল? NULL আর তিন-মানের যুক্তি এল কোথা থেকে?
Relational model-এর জনক Edgar F. Codd ১৯৭০ সালের মূল প্রবন্ধে NULL রাখেননি। ১৯৭৯ সালে ACM Transactions on Database Systems-এ প্রকাশিত “Extending the Database Relational Model to Capture More Meaning” প্রবন্ধে তিনি “অজানা মান” প্রকাশের জন্য null আর তার সাথে তিন-মানের যুক্তি (TRUE, FALSE, UNKNOWN) প্রস্তাব করেন। SQL মান সেই ধারণা নিয়েছে: NULL-এর সাথে যেকোনো তুলনা UNKNOWN. এই যুক্তির ভেতরে NOT IN-এর আচরণ কোনো বাগ নয়, বরং সামঞ্জস্যপূর্ণ ফল। Habib Ullah-র বিভাগ যদি সত্যিই “অজানা” হয়, তাহলে তিনি English বিভাগের কি না, তা-ও অজানা; তাই “English-এ কেউ নেই” — এটি নিশ্চিতভাবে বলা যায় না। SQL “নিশ্চিত না হলে বাদ” নীতিতে চলে।
সমস্যা হলো মানুষ NULL-কে প্রায়ই “অজানা” অর্থে নয়, “প্রযোজ্য নয়” বা “এখনো বাছেননি” অর্থে ব্যবহার করেন। Habib-এর ক্ষেত্রে বাস্তব সত্যটি হলো “তিনি কোনো বিভাগে নেই” — তাহলে English-এ কেউ নেই, এটি নিশ্চিত। Codd নিজেও পরে এই পার্থক্য স্বীকার করেন; ১৯৯০ সালের বই The Relational Model for Database Management: Version 2-এ তিনি দুই রকম চিহ্ন (“অজানা” আর “প্রযোজ্য নয়”) আর চার-মানের যুক্তির প্রস্তাব দেন, যা কোনো বাণিজ্যিক DBMS গ্রহণ করেনি। অন্যদিকে C. J. Date আর Hugh Darwen-এর মতো গবেষকরা বহু বছর ধরে যুক্তি দিয়েছেন যে NULL-ই relational model-এ বাড়তি জটিলতা আনে, আর তাঁরা NULL ছাড়া নকশার পক্ষে।
বাস্তবের শিক্ষা তিনটি। এক, প্রতিটি nullable কলামের NULL-এর অর্থ লিখে রাখুন (পর্ব ১১)। দুই, NOT IN-এর বদলে NOT EXISTS লিখুন, যা প্রতিটি সারির জন্য “আছে কি নেই” প্রশ্ন করে এবং কখনো UNKNOWN দেয় না। তিন, সন্দেহ হলে ফলের সারি গুনে দেখুন — ফাঁকা ফলই সবচেয়ে বিপজ্জনক, কারণ সেটি দেখে কেউ সন্দেহ করেন না।
মূল উপলব্ধি: NOT IN-এর ফাঁদ তিন-মানের যুক্তির একটি সৎ ফল — “অজানা” থাকলে SQL কিছু নিশ্চিত বলে না। NULL-এর অর্থ নিয়ে আপনার ধারণা আর SQL-এর ধারণা এক না হলে NOT EXISTS ব্যবহার করুন।
প্র ০২Semi join শুধু একটি সুবিধাজনক লেখার ধরন, নাকি এর পেছনে বড় কোনো প্রকৌশল-গল্প আছে? দুটি শহরে রাখা দুটি টেবিল জুড়তে হলে এটি কীভাবে কাজে লাগে?
Semi join-এর সবচেয়ে বিখ্যাত ব্যবহার এসেছে distributed database-এর গবেষণা থেকে। ১৯৭০-এর দশকের শেষে Computer Corporation of America-র SDD-1 প্রকল্প এমন একটি ডেটাবেস বানাতে চেয়েছিল, যার টেবিলগুলো আলাদা আলাদা কম্পিউটারে, ধীর নেটওয়ার্কে যুক্ত। তখন join-এর আসল খরচ CPU নয়, নেটওয়ার্কে কত বাইট পাঠাতে হয়। Philip Bernstein ও সহকর্মীরা দেখান, semi join দিয়ে এই খরচ অনেক কমানো যায়; ১৯৮১ সালে Bernstein আর Dah-Ming Chiu-র প্রবন্ধ “Using Semi-Joins to Solve Relational Queries” (Journal of the ACM) এর তাত্ত্বিক ভিত্তি দেয়।
কৌশলটি এমন। ধরুন orders ঢাকার সার্ভারে, customers চট্টগ্রামে, আর প্রশ্ন “delivered অর্ডারের গ্রাহকদের বিস্তারিত তথ্য”। সব orders চট্টগ্রামে পাঠানোর বদলে ঢাকা শুধু delivered অর্ডারগুলোর customer_id-এর তালিকা পাঠায় — কয়েকটি সংখ্যা। চট্টগ্রাম সেই তালিকা দিয়ে customers ⋉ তালিকা চালায়, আর শুধু মেলা গ্রাহকদের ফেরত পাঠায়। দুই দিকেই অপ্রয়োজনীয় সারি নেটওয়ার্কে যায়ই না। পরে এই চিন্তা আরও এগোয়: id-এর পুরো তালিকার বদলে একটি Bloom filter (Burton Bloom-এর ১৯৭০ সালের ছোট, সম্ভাব্যতাভিত্তিক set-কাঠামো, যা কখনো “না” ভুল বলে না) পাঠানো, যাতে আরও কম বাইটে বেশিরভাগ অমিল সারি বাদ পড়ে।
আজও এর উত্তরসূরিরা সর্বত্র: বড় data warehouse আর Spark-এর মতো distributed ইঞ্জিনে join-এর আগে “runtime filter” বা “dynamic filter” তৈরি হয়, যা এক পাশের key দিয়ে অন্য পাশের স্ক্যান ছোট করে। একটি কম্পিউটারের ভেতরেও semi join-এর “প্রথম মিলে থামা” আর duplicate না বানানো সময় ও memory বাঁচায়।
মূল উপলব্ধি: semi join একটি সত্যিকারের অপারেটর, যার মূল কাজ “শুধু যা দরকার, তা-ই সরাও”। প্রশ্নটি “আছে কি না” হলে EXISTS লিখলে আপনি DBMS-কে এই কৌশল ব্যবহারের সুযোগ দেন।
প্র ০৩mentor_id-এর মতো adjacency list-এ গভীর hierarchy প্রশ্ন কঠিন। তাহলে ভাঁজে ভাঁজে সাজানো ডেটা (জেলা–উপজেলা–ইউনিয়ন, পণ্যের শ্রেণি, অফিসের চেইন) টেবিলে রাখার আর কী কী উপায় আছে, আর কোনটি কখন?
চারটি প্রধান নকশা আছে, প্রতিটির দাম আলাদা। Adjacency list (parent_id কলাম) সবচেয়ে সরল: একটি সারি সরাতে বা অন্য boss-এর অধীনে নিতে একটিমাত্র UPDATE, আর foreign key দিয়ে শুদ্ধতা পাহারা দেওয়া যায়। দুর্বলতা পড়ায়: “পুরো উপগাছ” পেতে recursive query লাগে। SQL:1999 মানে WITH RECURSIVE আসার পর এটি আর বড় সমস্যা নয়; আর Oracle বহু আগে থেকেই নিজস্ব CONNECT BY দিয়ে এমন প্রশ্নের উত্তর দিত।
Materialized path: প্রতিটি সারিতে পুরো পথ লেখা, যেমন Tasnim-এর '/1/2/6/'. “Anisur-এর অধীনে সবাই” মানে path LIKE '/1/%' — একটি index-যোগ্য খোঁজ। কিন্তু কাউকে অন্য শাখায় সরালে তাঁর সব উত্তরসূরির path বদলাতে হয়, আর ডেটাবেস নিজে path-এর শুদ্ধতা যাচাই করতে পারে না। PostgreSQL-এর ltree extension এই নকশার জন্যই বানানো।
Nested sets: গাছটি একবার গভীরে ঘুরে প্রতিটি সারিতে দুটি সংখ্যা (left, right) বসানো, যাতে উত্তরসূরিদের সংখ্যা পূর্বপুরুষের সীমার ভেতরে পড়ে। উপগাছ পড়া একটি non-equi join — এই পর্বের পরিসর-কৌশল! Joe Celko তাঁর SQL বইগুলোতে এটি জনপ্রিয় করেন। কিন্তু একটি নতুন সারি ঢোকালে গড়ে অর্ধেক টেবিলের সংখ্যা বদলায়, তাই ঘন ঘন বদলানো ডেটায় অচল।
Closure table: আলাদা একটি টেবিলে প্রতিটি (পূর্বপুরুষ, উত্তরসূরি, দূরত্ব) জোড়া। পড়া খুব সহজ — সাধারণ join — আর FK-ও দেওয়া যায়; দাম হলো জায়গা (গভীর গাছে জোড়ার সংখ্যা অনেক) আর প্রতিটি বদলে অনেক সারি হালনাগাদ।
বাস্তবে বিশ্ববিদ্যালয়ের mentor বা অফিসের চেইনের মতো ছোট, প্রায়ই বদলানো hierarchy-তে adjacency list আর recursive CTE যথেষ্ট। ক্যাটালগের মতো বড়, কম বদলানো, খুব বেশি পড়া গাছে path বা closure table লাভজনক।
মূল উপলব্ধি: hierarchy রাখার কোনো একক সেরা উপায় নেই — লেখা সস্তা বনাম পড়া সস্তা, এই trade-off থেকেই নকশা বাছতে হয়।
প্র ০৪Inner join-এর ক্রমে ফল না বদলালে optimizer ক্রম নিয়ে এত মাথা ঘামায় কেন? আর outer join-এর ক্রম সে একেবারেই বদলাতে পারে না?
ফল একই হলেও খরচ আকাশ-পাতাল আলাদা হতে পারে। ধরুন এক কোটি সারির orders, দশ লাখ সারির customers আর ৫টি সারির একটি টেবিল (শুধু ঢাকার শহর)। আগে বড় দুটি জুড়ে পরে ছাঁকলে মাঝপথে কোটি সারির ফল বানাতে হয়; আগে ছোট টেবিলের সাথে customers জুড়ে নিলে মাঝপথের ফল অনেক ছোট থাকে। প্রথম কোন টেবিল, কোন join অ্যালগরিদম, কোন index — এসব বাছাই-ই query optimization.
১৯৭৯ সালে IBM-এর System R প্রকল্পে Patricia Selinger ও সহকর্মীদের প্রবন্ধ “Access Path Selection in a Relational Database Management System” এর ভিত্তি গড়ে। তাঁরা statistics দিয়ে প্রতিটি পরিকল্পনার খরচ অনুমান করেন আর dynamic programming দিয়ে সেরা ক্রম খোঁজেন — খরচ বাঁচাতে মূলত “left-deep” পরিকল্পনাগুলোর মধ্যে। কারণ n টেবিলের join-ক্রম n! রকম (১০টি টেবিলে ৩৬ লাখের বেশি), সব দেখা অসম্ভব। আজকের প্রায় সব বড় DBMS-এর optimizer এই চিন্তার উত্তরসূরি; টেবিল অনেক বেশি হলে PostgreSQL-এর মতো সিস্টেম genetic বা heuristic খোঁজে চলে যায় (পর্ব ৪৩)।
Outer join-এ স্বাধীনতা কম, কিন্তু শূন্য নয়। এই পর্বের stepper-এ দেখেছেন, (d LEFT JOIN c) JOIN i আর d LEFT JOIN (c JOIN i)-এর ফল আলাদা — তাই optimizer এমন বদল করতে পারে না। তবে কিছু বদল নিরাপদ, আর optimizer সেগুলো খোঁজে। যেমন LEFT JOIN-এর পরে WHERE-এ ডান টেবিলের এমন শর্ত থাকলে যা NULL-এ কখনো সত্য নয়, তাহলে outer join আসলে inner join-এর সমান — optimizer সেটিকে inner বানিয়ে আবার ক্রম বদলের স্বাধীনতা পায়। এই “outer join simplification” আর নিরাপদ পুনর্বিন্যাসের নিয়ম নিয়ে ১৯৯০-এর দশকে আনুষ্ঠানিক গবেষণা হয়েছে (যেমন Galindo-Legaria ও Rosenthal-এর কাজ)।
মূল উপলব্ধি: আপনি SQL-এ লেখেন কী চান; inner join-এর ক্রম optimizer-এর হাতে ছেড়ে দিন। কিন্তু outer join-এর ক্রম আর বন্ধনী আপনার প্রশ্নের অর্থ — সেটি আপনাকেই ঠিক লিখতে হবে।
প্র ০৫পুরোনো কোডে প্রায়ই দেখা যায় FROM orders, customers WHERE …. আধুনিক JOIN … ON এল কবে, কেন — আর এক রিপোর্টে কয়েকটি join করে SUM করলে সংখ্যা ফুলে ওঠে কেন?
প্রথম দিকের SQL-এ join লেখার একটিই উপায় ছিল: FROM-এ কমা দিয়ে টেবিলগুলো, আর WHERE-এ join-এর শর্ত আর ছাঁকার শর্ত মিশিয়ে। অর্থাৎ ভাষার দিক থেকে প্রতিটি join একটি CROSS JOIN আর তার পরে ফিল্টার। outer join-এর কোনো মানসম্মত লেখা ছিল না; প্রতিটি vendor নিজের চিহ্ন বানায় — Oracle-এ (+), পুরোনো SQL Server ও Sybase-এ *=. ১৯৯২ সালের SQL-92 মান স্পষ্ট JOIN … ON, LEFT/RIGHT/FULL OUTER JOIN, CROSS JOIN, NATURAL JOIN আর USING আনে। লাভ দুটি: join-এর শর্ত টেবিলের পাশেই থাকে, তাই কোনো টেবিলের শর্ত ভুলে গেলে চোখে পড়ে; আর outer join-এর অর্থ স্পষ্ট হয় — শর্ত ON-এ না WHERE-এ, সেটিই ঠিক করে কোন সারি বাঁচবে। কমা-ধাঁচ আজও বৈধ, কিন্তু অনেক দলের কোড-রিভিউ নিয়মে নিষিদ্ধ।
দ্বিতীয় প্রশ্নটি আরও সূক্ষ্ম, আর ডেটা অ্যানালিস্টের ইন্টারভিউতে প্রিয়। ধরুন প্রতিটি গ্রাহকের মোট অর্ডারের মূল্য আর মোট পেমেন্ট একসাথে চাই, তাই customers-এর সাথে order_items আর payments দুটোই join করে SUM করা হলো। একজন গ্রাহকের ৩টি অর্ডার-লাইন আর ২টি পেমেন্ট থাকলে join-এর পর তাঁর ৩ × ২ = ৬টি সারি — প্রতিটি লাইনের মূল্য দুবার, প্রতিটি পেমেন্ট তিনবার যোগ হয়। দুটি স্বাধীন one-to-many সম্পর্ক একসাথে join করলে তাদের মধ্যে একটি গোপন CROSS JOIN তৈরি হয়; BI-জগতে একে কখনো “chasm trap” বলা হয়। কোনো ত্রুটি আসে না, শুধু সংখ্যা বড় হয়।
সমাধান এই পর্বের অংশ ৭.২-এর কৌশল: প্রতিটি one-to-many দিক আগে আলাদা subquery-তে GROUP BY করে এক সারিতে নামিয়ে আনুন, তারপর join করুন। আর অভ্যাস হিসেবে প্রতিটি join-এর পর সারির সংখ্যা দেখুন।
মূল উপলব্ধি: প্রতিটি join একটি গুণ। কতগুণ সারি বাড়বে, তা আগে থেকে হিসাব করতে পারলেই আপনি দুর্ঘটনার CROSS JOIN আর ফুলে ওঠা SUM দুটোই ধরতে পারবেন।
অনুশীলন
দুটি কোয়েরি লেখা, একটি হাতে ফল বের করা আর একটি AI-এর কোয়েরির বাগ খোঁজা।
-
কোয়েরি লিখুন (anti join): db-shop-এ কোন পণ্য কখনো কোনো অর্ডারে আসেনি? তারপর প্রশ্নটি একটু কঠিন করুন: কোন পণ্য কখনো বাতিল নয় এমন অর্ডারে বিক্রি হয়নি? অংশ ৫-এর এডিটরে চালান।
SELECT p.product_id, p.name FROM products p WHERE NOT EXISTS (SELECT 1 FROM order_items oi JOIN orders o ON o.order_id = oi.order_id WHERE oi.product_id = p.product_id AND o.status <> 'cancelled');প্রথম প্রশ্নে (ভেতরে শুধু order_items, orders-এর join আর status-এর শর্ত ছাড়া) উত্তর একটি:
103 | Power Bank— স্টকও ০। দ্বিতীয় প্রশ্নে দুটি: Power Bank আর107 | Saree, কারণ Saree-র একমাত্র অর্ডার 1004 বাতিল হয়েছে। শর্তটি subquery-র ভেতরে রাখায় এটি সঠিক anti join; বাইরে রাখলে অর্থ বদলে যেত। -
ফল আগে বলুন (trace): টেবিল
t(x)-এ তিনটি সারি: 1, 2, 3. টেবিলu(y)-এ তিনটি সারি: 2, NULL, 2. হাতে বলুন প্রতিটি কোয়েরি কয়টি সারি দেবে, কোনগুলো: (ক)SELECT x FROM t WHERE x IN (SELECT y FROM u)(খ)… WHERE x NOT IN (SELECT y FROM u)(গ)… WHERE NOT EXISTS (SELECT 1 FROM u WHERE u.y = t.x)(ঘ)SELECT t.x FROM t JOIN u ON u.y = t.x(ঙ)SELECT COUNT(*) FROM t CROSS JOIN u.- (ক) ১টি সারি: 2 — semi join, u-তে 2 দুবার থাকলেও একবার।
- (খ) ০টি সারি — u-তে একটি NULL, তাই 1 আর 3-এর জন্যও শর্ত UNKNOWN.
- (গ) ২টি সারি: 1 আর 3 — NOT EXISTS-এ NULL কোনো সমস্যা নয়।
- (ঘ) ২টি সারি: 2, 2 — inner join প্রতিটি মিলের জন্য একটি।
- (ঙ) ৯ = ৩ × ৩; NULL-এর সারিও গোনা হয়, কারণ CROSS JOIN-এ কোনো শর্তই নেই।
খেয়াল করুন (ক) আর (গ) মিলে t-এর তিনটি সারিই ঠিক একবার — অংশ ৮-এর বিভাজন। (ক) আর (খ) মিলে মাত্র একটি।
-
কোয়েরি লিখুন (self join): একই বিভাগের এমন শিক্ষক-জোড়া খুঁজুন, যাঁদের বেতনের পার্থক্য ২৫,০০০ টাকার কম — প্রতিটি জোড়া একবার, সাথে পার্থক্যটি। db-uni-তে কতটি সারি আসবে?
SELECT a.name, b.name, a.salary - b.salary AS diff FROM instructors a JOIN instructors b ON b.dept_id = a.dept_id AND a.instructor_id < b.instructor_id WHERE abs(a.salary - b.salary) < 25000;একটি সারি:
Shahana Parvin | Tasnim Chowdhury | 20000. CSE-র বাকি দুটি জোড়ার পার্থক্য 35000 আর 55000, EEE-র একমাত্র জোড়ার 40000, আর BBA-তে একজনই শিক্ষক।abs()দরকার, কারণ id ছোট মানে বেতন বেশি — এমন কোনো নিয়ম নেই; a-র বেতন কম হলে পার্থক্য ঋণাত্মক হতো। -
বাগ খুঁজুন: “কোন গ্রাহক কখনো delivered অর্ডার পাননি?” — এই প্রশ্নে AI লিখেছে:
SELECT c.customer_id, c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE o.status <> 'delivered';db-shop-এ এটি কী ফল দেবে, কেন ভুল, আর সঠিক কোয়েরি কী?ফল ৫টি সারি: Rahim Uddin (1003-এর জন্য), Sabbir Hasan, Farhana Akter, Mitu Rani আর আবার Rahim Uddin (1009). দুটি ভুল। এক, Rahim delivered অর্ডার (1001) পেয়েছেন, তবু এসেছেন — কোয়েরিটি আসলে প্রশ্ন করছে “কার কোনো একটি অর্ডার delivered নয়?”, আর তিনি দুবার। দুই, Tanvir Ahmed নেই: তাঁর কোনো অর্ডার না থাকায় o.status NULL, আর
NULL <> 'delivered'UNKNOWN — WHERE তাঁকে ফেলে দিল, LEFT JOIN-এর উদ্দেশ্যই নষ্ট।SELECT c.customer_id, c.name FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.status = 'delivered');সঠিক ফল ৪ জন: Sabbir Hasan, Tanvir Ahmed, Farhana Akter, Mitu Rani. যাচাইয়ের সূত্র: এই ৪ জন আর অংশ ৫-এর semi join-এর ৪ জন মিলে মোট ৮ জন গ্রাহক — ঠিক একবার করে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমenrollments আর একটি grade_points lookup টেবিল দিয়ে প্রত্যেক শিক্ষার্থীর GPA বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- grade_points টেবিলে প্রতিটি letter grade-এর point রাখা
- enrollments-কে grade_points (letter = grade) আর courses (course_id)-এর সাথে join করে প্রতিটি ভর্তির point আর credits পাশাপাশি আনা
- student_id দিয়ে GROUP BY করা
- প্রতিটি দলে SUM(credits × point) ÷ SUM(credits) হিসাব করা
- ফল ROUND করে student_id অনুযায়ী সাজানো
একটি students টেবিলে একই বিভাগের ৪ জন শিক্ষার্থী আছেন (student_id অনন্য)। students a JOIN students b ON b.dept_id = a.dept_id AND a.student_id < b.student_id কয়টি সারি দেবে?
- ১৬
- ৬
- ১২
- ৪
sizes টেবিলে ৪টি সারি আর colours টেবিলে ৩টি সারি। SELECT * FROM sizes CROSS JOIN colours কয়টি সারি দেবে?
- ৭
- ৩
- ত্রুটি, কারণ ON শর্ত নেই
- ১২
departments টেবিলে dept_id 1, 2, 3, 4. students টেবিলের dept_id কলামে মানগুলো 1, 2, 3 আর একটি NULL. SELECT * FROM departments WHERE dept_id NOT IN (SELECT dept_id FROM students) কী দেবে?
- শুধু dept_id 4-এর সারি
- একটি ত্রুটি
- কোনো সারি নয়
- চারটি সারিই
customers(customer_id, name) আর orders(order_id, customer_id) টেবিল থেকে “অন্তত একটি অর্ডার দিয়েছেন এমন প্রত্যেক গ্রাহক, একবার করে” চাই। কোনটি এই প্রশ্নের সবচেয়ে সঠিক প্রকাশ?
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)customers c JOIN orders o ON o.customer_id = c.customer_idWHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)customers c CROSS JOIN orders o
কোয়েরি: departments d LEFT JOIN courses c ON c.dept_id = d.dept_id JOIN instructors i ON i.instructor_id = c.instructor_id. যে বিভাগের কোনো কোর্স নেই, তার কী হবে?
- বিভাগটি আসবে, কোর্স আর শিক্ষক NULL
- বিভাগটি ফল থেকে বাদ পড়বে
- কোয়েরিটি ত্রুটি দেবে
- বিভাগটি দুবার আসবে
Relational algebra-য় semi join R ⋉ S কোনটির সমান?
- R × S
- R − (R ⋉ S)
- πattr(R)(R ⋈ S)
- πattr(S)(R ⋈ S)
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Self join = একই টেবিল, দুটি alias, দুটি ভূমিকা; সবাইকে রাখতে LEFT; জোড়ায়
a.id < b.id. অজানা গভীরতার hierarchy-তে self join নয়, recursive CTE (পর্ব ২৫)। - CROSS JOIN ইচ্ছে করে লিখুন — সব সমন্বয় আর ফাঁকা ঘরসহ ছক; আকার m × n. শর্ত ভুলে যাওয়া কমা-join থেকে সাবধান।
- Non-equi join-এ পরিসর ফাঁকহীন আর পরস্পর-বিচ্ছিন্ন রাখুন; দশমিক থাকলে অর্ধ-খোলা সীমা; প্রতিটি স্তরের গণনার যোগফল মিলিয়ে দেখুন।
- Semi join (EXISTS/IN) প্রতিটি বাঁ-সারি একবার দেয়; anti join মানে NOT EXISTS বা LEFT JOIN … IS NULL — nullable কলামে NOT IN কখনো নয়।
- Composite key-তে সব কলামে join; grouped subquery-র সাথে join দিয়ে “প্রতিজনের সর্বশেষ”; inner join-এর ক্রম মুক্ত, outer join-এর ক্রম প্রশ্নের অর্থ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · Subquery — nested, correlated আর EXISTSপরবর্তী পর্বEXISTS আর IN-এর পুরো গল্প।
- পর্ব ১৯ · JOIN — INNER, LEFT, RIGHT আর FULLআবার দেখুনএই পর্বের ভিত্তি — মৌলিক join.
- পর্ব ২৫ · CTE আর recursive queryসামনেযেকোনো গভীরতার hierarchy.
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- DSA · Hash tableগভীরে যানequi join আর semi join দ্রুত হয় যে কাঠামোয়।
- Python · SQLite ডেটাবেসহাতে-কলমেএকই কোয়েরি Python প্রোগ্রাম থেকে চালানো।