সরাসরি পর্বে যান
পর্ব ৩৩ · ৬৭-এর মধ্যে · মডিউল ৫

Relational algebra — join আর division

Relational Algebra II — Theta, Natural and Outer Joins, Semi/Anti Joins, Division, Aggregation and Query Trees

Taught in Bangla, this lesson continues relational algebra with joins: theta and equi joins as product plus selection, natural join, left, right and full outer joins, semi and anti joins for 'at least one' and 'none' questions, the division operator for 'all' queries, extended algebra with aggregation γ and generalized projection, query trees and join reordering, and exam-style problems on a university schema.

এই পর্বে যা শিখবেন

  • × আর σ থেকে theta join, equi join আর natural join-এর সংজ্ঞা — আর natural join-এর বিনিময় ও সংযোজন নিয়মের প্রমাণ
  • Outer join, semi join আর anti join কীভাবে মৌলিক অপারেটর দিয়ে লেখা যায়, আর SQL-এর কোন লেখার সাথে মেলে
  • Division-এর অর্থ, আনুষ্ঠানিক সংজ্ঞা, ধাপে ধাপে উৎপত্তি আর প্রমাণ; SQL-এ দুই উপায়ে, আর খালি ভাজকের ফাঁদ
  • γ দিয়ে aggregation, generalized projection, extended অপারেটরের তালিকা আর relational completeness
  • Query tree, equivalence rule, σ ও π নিচে ঠেলা — আর বিশ্ববিদ্যালয়ের schema-য় পরীক্ষার ধাঁচের অনেক সমস্যা

১ · Theta join আর equi join — × তারপর σ

পর্ব ৩২-এ (Relational algebra — select, project আর set অপারেশন) আপনি মৌলিক ছয়টি অপারেটর দেখেছেন: selection σ (সারি ছাঁকা), projection π (কলাম বাছা), rename ρ (নাম বদল), union ∪, set difference − আর Cartesian product ×. প্রতিটির ইনপুট relation, আউটপুটও relation — এই closure property-র জন্যই অপারেটরগুলো একটির পর একটি জোড়া দেওয়া যায়, ঠিক যেমন যোগ আর গুণ মিলিয়ে বড় গাণিতিক রাশি লেখা যায়। এই পর্বে নতুন যা আসবে, তার প্রায় সবই এই ছয়টি দিয়ে লেখা সংক্ষিপ্ত রূপ।

প্রথম সংক্ষিপ্ত রূপটি সবচেয়ে বেশি ব্যবহৃত। R × S দুই relation-এর প্রতিটি tuple-এর সাথে প্রতিটি tuple জোড়ে: |R| · |S| টি tuple. বাস্তবে আমরা প্রায় কখনো সব জোড়া চাই না; চাই শুধু অর্থবহ জোড়া। তাই × এর ঠিক পরে একটি σ বসে। এই জুটির নাম theta join:

R ⋈θ S  =  σθ(R × S)

θ (গ্রিক অক্ষর থিটা) যেকোনো শর্ত — R-এর একটি attribute আর S-এর একটি attribute-এর তুলনা (=, ≠, <, ≤, >, ≥), আর এমন তুলনার ∧ (AND). যখন θ-তে শুধু সমতার তুলনা থাকে, তখন একে বলে equi join — বাস্তবের প্রায় সব join এই জাতের: foreign key = primary key.

ঘটকের খাতা ভাবুন। এক পাতায় পাত্রদের তালিকা, আরেক পাতায় পাত্রীদের। সব সম্ভাব্য জোড়া লিখে ফেলা হলো × — দশজন পাত্র আর দশজন পাত্রী হলে একশোটি জোড়া। তারপর ঘটক শর্ত দিয়ে কেটে দেন: “একই জেলা” (সমতা — equi join), “পাত্রের বয়স পাত্রীর চেয়ে বেশি” (অসমতা — theta join). যা টিকে থাকে, সেটিই join. কম্পিউটার অবশ্য একশোটি জোড়া সত্যিই লেখে না; সংজ্ঞাটি শুধু বলে কী পাওয়া যাবে, কীভাবে নয়।

একটি উদাহরণ db-uni থেকে। এই কোর্সের বিশ্ববিদ্যালয় ডেটাবেসের instructors টেবিলে কলাম পাঁচটি: instructor_id, name, dept_id (বিভাগ), salary (মাসিক বেতন, টাকায়) আর mentor_id. প্রশ্ন: “একই বিভাগের মধ্যে কে কার চেয়ে বেশি বেতন পান?” এখানে θ দুই অংশের: a.dept_id = b.dept_id (সমতা) ∧ a.salary > b.salary (অসমতা)। algebra-য় একই relation দুবার লাগে, তাই আগে ρ দিয়ে দুটি নাম:

ρa(instructors) ⋈a.dept_id = b.dept_id ∧ a.salary > b.salary ρb(instructors)
-- R × S: সব জোড়া
SELECT COUNT(*) AS cross_rows FROM instructors a CROSS JOIN instructors b;

-- σθ(R × S): আগে ×, তারপর WHERE-এ θ
SELECT a.name AS earns_more, b.name AS than_whom, a.dept_id
FROM instructors a CROSS JOIN instructors b
WHERE a.dept_id = b.dept_id AND a.salary > b.salary;

-- R ⋈θ S: একই কাজ JOIN … ON দিয়ে
SELECT a.name AS earns_more, b.name AS than_whom, a.dept_id
FROM instructors a JOIN instructors b
  ON a.dept_id = b.dept_id AND a.salary > b.salary;

প্রথম ফল 36 = ৬ × ৬. দ্বিতীয় আর তৃতীয় কোয়েরি হুবহু একই ৪টি সারি দেয়: Dr. Anisur Rahman | Shahana Parvin | 1, Dr. Anisur Rahman | Tasnim Chowdhury | 1, Shahana Parvin | Tasnim Chowdhury | 1 আর Dr. Mahbub Alam | Rumana Islam | 2. ৩৬টি জোড়া থেকে θ ৪টি রেখেছে। এটাই সংজ্ঞার প্রমাণ হাতে-কলমে: CROSS JOIN … WHERE θ আর JOIN … ON θ একই relation. BBA-তে Kamrul Hasan একা, তাই তাঁর কোনো জোড়া নেই; নিজের সাথে জোড়া (a = b) > শর্তেই বাদ পড়ে।

সংজ্ঞা থেকে দুটি তথ্য সরাসরি আসে। এক, আকার: 0 ≤ |R ⋈θ S| ≤ |R| · |S| — σ কখনো সারি বাড়ায় না। দুই, schema: ফলের attribute হলো R-এর সব attribute আর S-এর সব attribute, তাই equi join-এ join-এর কলামটি দুবার থাকে (a.dept_id আর b.dept_id) — একই মান, দুটি কলাম। পরের অংশের natural join ঠিক এই অপচয় দূর করে।

সংজ্ঞা বনাম বাস্তবায়ন: σθ(R × S) হলো অর্থ, পরিকল্পনা নয়। দুটি দশ লাখ সারির টেবিলের × মানে এক লক্ষ কোটি জোড়া — কোনো DBMS এটি বানায় না। equi join-এ সে hash join দিয়ে মোটামুটি |R| + |S| + |ফল| অনুপাতের কাজে একই ফল বের করে — দুই টেবিল একবার করে পড়া, যদি ছোটটির hash table memory-তে আঁটে (পর্ব ৪২)। algebra-র কাজ হলো প্রমাণ করা যে দুটি পরিকল্পনা একই ফল দেবে; কোনটি সস্তা, তা ঠিক করে optimizer.

২ · Natural join ⋈ — একই নামের attribute মিলিয়ে

Natural join R ⋈ S-এ কোনো শর্ত লিখতে হয় না। নিয়মটি নিজে থেকেই আসে: R আর S-এর একই নামের সব attribute-এর মান সমান হতে হবে, আর ফলে সেই attribute-গুলো একবারই থাকবে। ধরুন R-এর attribute-সেট A, S-এর B, আর সাধারণ অংশ C = A ∩ B = {C1, …, Ck}. তাহলে:

R ⋈ S  =  πA ∪ B( σR.C1 = S.C1 ∧ … ∧ R.Ck = S.Ck( R × S ) )

এখানে একটি সূক্ষ্মতা আছে। R × S-এ দুই দিকে একই নামের attribute থাকলে নামগুলো আলাদা করতে হয় — তাই কঠোরভাবে লিখলে আগে ρ দিয়ে S-এর সাধারণ attribute-গুলোর নাম বদলাতে হয়, তারপর ×, σ, আর শেষে π দিয়ে বদলানো কপিগুলো ফেলে দিতে হয়। দুটি বিশেষ ক্ষেত্র মনে রাখার মতো: C খালি হলে শর্ত কিছুই থাকে না, তাই R ⋈ S = R × S. A = B হলে (দুটির schema হুবহু এক) প্রতিটি attribute মিলতে হয়, তাই R ⋈ S = R ∩ S.

নিচের ডায়াগ্রামে দুটি ছোট relation: st(sid, name, dept_id) — তিনজন শিক্ষার্থী, আর dp(dept_id, dname) — তিনটি বিভাগ। Emon-এর বিভাগ 3 dp-তে নেই, আর English (4)-এ st-এর কেউ নেই। ধাপে ধাপে সংজ্ঞাটি প্রয়োগ করুন:

st ⋈ dpলাল = এই ধাপে যা টিকে থাকে

            
হিসাব
tuple
attribute
অপারেটর
  1. দুটি relation. st-এর attribute {sid, name, dept_id}, dp-র {dept_id, dname}. সাধারণ অংশ শুধু dept_id — natural join এটির ওপরেই মেলাবে।
  2. ρ দিয়ে নাম বদল. × করতে গেলে দুটি dept_id-এর নাম এক হয়ে যায়, তাই dp-র কপিটির নাম d2. এখনো কোনো tuple বদলায়নি।
  3. × — সব জোড়া. ৩ × ৩ = ৯টি tuple, ৫টি attribute. বেশিরভাগ জোড়া অর্থহীন: Arif-এর পাশে EEE বা English.
  4. σ dept_id = d2. শুধু ২টি জোড়ায় দুটি মান সমান: Arif–CSE আর Chayon–EEE. বাকি ৭টি বাদ।
  5. π দিয়ে d2 ফেলে দেওয়া. d2 আর dept_id এখন সব tuple-এ একই মান, তাই একটি রাখলেই চলে: ২টি tuple, ৪টি attribute. Emon আর English কোনো জোড়া পায়নি — এদের বলে dangling tuple; অংশ ৩-এ এদের বাঁচানোর উপায়।

SQL-এ NATURAL JOIN ঠিক এই সংজ্ঞা মানে — নাম মিলিয়ে। আর এখানেই একটি বাস্তব ফাঁদ। db-uni-র students (student_id, name, dept_id, batch, cgpa) আর departments (dept_id, name, building) — দুটিতেই name কলাম আছে! তাই natural join শুধু dept_id নয়, name-ও মেলাতে চাইবে: “শিক্ষার্থীর নাম = বিভাগের নাম”। চালিয়ে দেখুন:

-- equi join: dept_id দুবার আসে
SELECT * FROM students s JOIN departments d ON s.dept_id = d.dept_id
WHERE s.student_id = 2101;

-- USING: join-এর কলাম একবার (natural join-এর মতো)
SELECT * FROM students JOIN departments USING (dept_id)
WHERE student_id = 2101;

-- NATURAL JOIN: dept_id আর name দুটিই মেলাতে চায়
SELECT COUNT(*) AS natural_rows FROM students NATURAL JOIN departments;

-- courses আর instructors-এর সাধারণ কলাম: instructor_id আর dept_id
SELECT course_id, title, name, dept_id FROM courses NATURAL JOIN instructors;

প্রথম ফলে ৮টি কলাম, যার মধ্যে dept_id দুবার: 2101 | Arif Hossain | 1 | 2021 | 3.75 | 1 | CSE | Academic-1. দ্বিতীয়টিতে ৭টি কলাম — USING কলামটি একবার রাখে, ঠিক π-এর মতো। তৃতীয় ফল 0: কোনো শিক্ষার্থীর নাম “CSE” নয়, তাই natural join খালি। কোনো ত্রুটি নেই — শুধু শূন্য। চতুর্থটিতে ৬টি সারি (CSE101 থেকে BBA101), কারণ সাধারণ কলাম দুটি: instructor_id আর dept_id. অর্থাৎ প্রশ্নটি নিঃশব্দে হয়ে গেছে “যে কোর্সের শিক্ষক একই বিভাগের”; এই ডেটায় সবাই একই বিভাগের বলে ফল ঠিক দেখাচ্ছে, কিন্তু কাল EEE-র কোনো শিক্ষক CSE-র কোর্স পড়ালে সেই সারি চুপচাপ হারাবে। ENG101-এর instructor_id NULL, তাই সেটি এমনিতেই বাদ।

শিক্ষা দুটি। তত্ত্বে natural join খুব সুন্দর, কারণ ভালো ডিজাইনে একই নাম মানে একই অর্থ (পর্ব ৩৬-৩৭-এর normalization-এ decomposition আর join এই অনুমানের ওপর দাঁড়িয়ে)। বাস্তব SQL-এ NATURAL JOIN-এর বদলে JOIN … USING (…) বা ON লিখুন — কোন কলামে মেলাচ্ছেন, তা নিজে বলে দিন।

বিনিময় আর সংযোজন নিয়ম — ছোট প্রমাণ

প্রথমে একটি সহায়ক কথা (lemma). attribute-সেট A ∪ B-র ওপর একটি tuple t ধরুন, আর t[A] মানে t-এর শুধু A-অংশ। সংজ্ঞা থেকে:

t ∈ R ⋈ S  ⇔  t[A] ∈ R  এবং  t[B] ∈ S

কেন? (⇒) R × S-এর একটি জোড়া (r, s) σ পেরোলে সাধারণ attribute-গুলোতে r আর s-এর মান সমান, তাই π-এর পরের tuple t-এর A-অংশ ঠিক r, B-অংশ ঠিক s. (⇐) t[A] = r ∈ R আর t[B] = s ∈ S হলে r আর s সাধারণ attribute-এ একই মান নেয় (দুটিই t-এর অংশ), তাই (r, s) σ পেরোয় আর π-এর পর ঠিক t দেয়। ∎

  • বিনিময় (commutativity): lemma-র ডান পাশ “t[A] ∈ R এবং t[B] ∈ S” — R আর S বদলালে একই কথা। তাই R ⋈ S = S ⋈ R. (relational model-এ attribute নাম দিয়ে চেনা হয়, অবস্থান দিয়ে নয়; তাই কলামের ক্রম আলাদা হলেও relation একই।)
  • সংযোজন (associativity): T-এর attribute-সেট D ধরুন। lemma দুবার লাগালে t ∈ (R ⋈ S) ⋈ T ⇔ t[A ∪ B] ∈ R ⋈ S এবং t[D] ∈ T ⇔ t[A] ∈ R, t[B] ∈ S, t[D] ∈ T. একইভাবে t ∈ R ⋈ (S ⋈ T)-এর শর্তও ঠিক এই তিনটি। তাই (R ⋈ S) ⋈ T = R ⋈ (S ⋈ T). ∎

ফলাফল বড়: n টি relation-এর natural join যেকোনো ক্রমে, যেকোনো বন্ধনীতে হিসাব করা যায়। theta join-এর জন্য নিয়মটি একটু শর্তসাপেক্ষ: (R ⋈θ1 S) ⋈θ2 T = R ⋈θ1 (S ⋈θ2 T), যদি θ2-তে শুধু S আর T-এর attribute থাকে — নইলে ডান পাশে θ2 লেখাই যায় না। অংশ ৭-এ optimizer এই স্বাধীনতা কীভাবে কাজে লাগায়, দেখবেন।

৩ · Outer join ⟕ ⟖ ⟗ — মিল না পেলে NULL দিয়ে ভরাট

Natural join-এ Emon আর English হারিয়ে গেছে। “সব শিক্ষার্থী, বিভাগ থাকুক বা না থাকুক” চাইলে দরকার outer join: join-এর ফল, সাথে যে tuple মিল পায়নি (dangling tuple) সেগুলোও — অন্য দিকের attribute-গুলোতে NULL বসিয়ে। তিনটি রূপ:

চিহ্ননামকোন dangling tuple রাখেSQL
R ⟕ Sleft outer joinR-এরLEFT JOIN
R ⟖ Sright outer joinS-এরRIGHT JOIN
R ⟗ Sfull outer joinদুই দিকেরFULL JOIN

এগুলো নতুন ক্ষমতা নয়, মৌলিক অপারেটরের সংক্ষেপ — যদি NULL-কে একটি বিশেষ মান হিসেবে মেনে নিই। B − A হলো শুধু S-এ থাকা attribute, আর {(NULL, …, NULL)} সেই attribute-গুলোর ওপর এক-tuple-এর একটি relation. তাহলে:

R ⟕ S  =  (R ⋈ S)  ∪  ( (R − πA(R ⋈ S)) × {(NULL, …, NULL)} )

পড়ুন এভাবে: join-এর ফল, যোগ R-এর সেই tuple-গুলো যারা join-এ কোনো অংশ পায়নি (R − πA(R ⋈ S)), প্রতিটির পাশে NULL-এর একটি সারি জুড়ে। R ⟖ S একইভাবে S-এর দিক থেকে, আর R ⟗ S = (R ⟕ S) ∪ (R ⟖ S). একই st আর dp-তে দেখুন:

st (sid, name, dept_id) · dp (dept_id, dname)লাল = নতুন যোগ হওয়া tuple

            
গোনা
tuple
NULL যোগ হলো
কে বাঁচল
  1. শুরু natural join দিয়ে. ২টি tuple. dangling: st-এর দিকে Emon (বিভাগ 3 dp-তে নেই), dp-র দিকে English (কোনো শিক্ষার্থী নেই)।
  2. Left outer join. st-এর প্রতিটি tuple অন্তত একবার থাকবে। Emon আসেন dname = NULL নিয়ে। ৩টি tuple.
  3. Right outer join. এবার dp-র প্রতিটি tuple থাকবে। English আসে sid আর name NULL নিয়ে। Emon বাদ। ৩টি tuple.
  4. Full outer join. দুই দিকের dangling tuple-ই: ২ + ১ + ১ = ৪টি। কোনো তথ্য হারায়নি।
  5. সংজ্ঞা যাচাই. R − πA(R ⋈ S) ঠিক Emon, তার পাশে NULL জুড়ে join-এর সাথে ∪ — ৩টি tuple, ধাপ ২-এর সাথে হুবহু। R-এর প্রতিটি tuple হয় join-এ, নয় এই অংশে আছে, তাই |R ⟕ S| ≥ |R|.

একই কাজ SQL-এ। এডিটরটি আগে ডায়াগ্রামের দুটি ছোট টেবিল তৈরি করে নেয় — st(sid, name, dept_id) আর dp(dept_id, dname). SQLite 3.39 (২০২২) থেকে RIGHT আর FULL JOIN চালায়; এই পাতার ইঞ্জিন তার চেয়ে নতুন।

-- st ⟕ dp
SELECT st.sid, st.name, st.dept_id, dp.dname
FROM st LEFT JOIN dp ON dp.dept_id = st.dept_id;

-- st ⟖ dp
SELECT st.sid, st.name, dp.dept_id, dp.dname
FROM st RIGHT JOIN dp ON dp.dept_id = st.dept_id;

-- st ⟗ dp: dept_id যেকোনো দিক থেকে
SELECT st.sid, st.name, COALESCE(st.dept_id, dp.dept_id) AS dept_id, dp.dname
FROM st FULL JOIN dp ON dp.dept_id = st.dept_id
ORDER BY dept_id;

তিনটি ফল ডায়াগ্রামের ধাপ ২, ৩ আর ৪-এর সাথে হুবহু: LEFT-এ 2202 | Emon | 3 | NULL, RIGHT-এ NULL | NULL | 4 | English, FULL-এ ৪টি সারি। FULL JOIN-এ COALESCE লাগল, কারণ English-এর সারিতে st.dept_id NULL আর Emon-এর সারিতে dp.dept_id NULL — natural join-এর “একটি dept_id কলাম” পেতে দুই দিক থেকে যেটি NULL নয়, সেটি নিতে হয়। MySQL এখনো FULL JOIN চালায় না; সেখানে LEFT JOIN আর RIGHT JOIN-এর ফল UNION করে লেখা হয় — ঠিক উপরের ⟗ = ⟕ ∪ ⟖ সংজ্ঞা।

Outer join-এর বীজগণিত দুর্বল: ⟕ বিনিময়যোগ্য নয় (R ⟕ S = S ⟖ R, কিন্তু R ⟕ S ≠ S ⟕ R), আর inner join-এর সাথে মেশালে সাধারণত সংযোজনযোগ্যও নয় — (R ⟕ S) ⋈ T আর R ⟕ (S ⋈ T) আলাদা ফল দিতে পারে, পর্ব ২০-এর English বিভাগের উদাহরণে যেমন দেখেছেন। শুধু ⟗ বিনিময়যোগ্য। তাই optimizer outer join-এর ক্রম বদলায় অনেক সতর্ক নিয়ম মেনে।

৪ · Semi join ⋉ আর anti join ▷ — “অন্তত একটি” আর “একটিও নেই”

পর্ব ২০ আর ২১-এ আপনি EXISTS আর NOT EXISTS লিখেছেন। algebra-য় এদের নাম semi join আর anti join, আর সংজ্ঞা মাত্র এক লাইনের:

R ⋉ S  =  πA(R ⋈ S)      R ▷ S  =  R − (R ⋉ S)

R ⋉ S হলো R-এর সেই tuple-গুলো, যাদের S-এ অন্তত একটি মিল আছে — কিন্তু ফলে শুধু R-এর attribute (A). R ▷ S হলো বাকিগুলো: যাদের কোনো মিল নেই। anti join-এর চিহ্ন বইভেদে ▷ বা ⊳ — অর্থ একই। দুটি তথ্য সংজ্ঞা থেকেই আসে: R ⋉ S ⊆ R (তাই কখনো সারি বাড়ায় না), আর (R ⋉ S) ∪ (R ▷ S) = R, দুটি পরস্পর-বিচ্ছিন্ন — প্রতিটি tuple ঠিক একটিতে পড়ে। এটি outer join-এর বর্ণনাতেও লুকিয়ে ছিল: অংশ ৩-এর R − πA(R ⋈ S) আসলে R ▷ S.

উদাহরণ: “কোন শিক্ষক অন্তত একটি কোর্স পড়ান?” = instructors ⋉ courses, আর “কে কোনো কোর্স পড়ান না?” = instructors ▷ courses (শর্ত instructors.instructor_id = courses.instructor_id). db-uni-র courses টেবিলে course_id, title, dept_id, credits আর instructor_id (কে পড়ান):

-- instructors ⋉ courses  (EXISTS)
SELECT i.instructor_id, i.name FROM instructors i
WHERE EXISTS (SELECT 1 FROM courses c WHERE c.instructor_id = i.instructor_id);

-- শুধু ⋈: যতগুলো কোর্স, ততবার
SELECT i.instructor_id, i.name FROM instructors i
JOIN courses c ON c.instructor_id = i.instructor_id;

-- instructors ▷ courses  (NOT EXISTS)
SELECT i.instructor_id, i.name FROM instructors i
WHERE NOT EXISTS (SELECT 1 FROM courses c WHERE c.instructor_id = i.instructor_id);

প্রথম ফলে ৫ জন: 1 Dr. Anisur Rahman, 2 Shahana Parvin, 3 Dr. Mahbub Alam, 5 Kamrul Hasan, 6 Tasnim Chowdhury. দ্বিতীয়টিতে ৬টি সারি — 6 | Tasnim Chowdhury দুবার, কারণ তিনি CSE301 আর CSE302 দুটিই পড়ান। SQL bag (multiset) নিয়ে কাজ করে, algebra set নিয়ে: algebra-র π নিজেই duplicate ফেলে দেয়, তাই πA(R ⋈ S)-এর SQL রূপ হলো SELECT DISTINCT … বা, আরও ভালো, EXISTS. তৃতীয় ফলে একজন: 4 | Rumana Islam. ৫ + ১ = ৬, মোট শিক্ষকের সংখ্যা — বিভাজনটি মিলে গেল।

পর্ব ২০-এর NOT IN ফাঁদ এখানে নতুন আলোয় দেখা যায়: algebra-র ▷ সংজ্ঞায় শুধু ⋈ আর − আছে, যেখানে NULL কখনো কোনো কিছুর সাথে মেলে না, তাই কোনো UNKNOWN তৈরি হয় না। NOT EXISTS এই সংজ্ঞার বিশ্বস্ত অনুবাদ; nullable কলামে NOT IN নয়, কারণ সেটি তিন-মানের যুক্তি-র AND-শিকল।

৫ · Division ÷ — “সবগুলো” প্রশ্নের অপারেটর

কিছু প্রশ্নে “অন্তত একটি” নয়, চাই “প্রতিটি”: “কোন শিক্ষার্থী CSE-র সব core কোর্স নিয়েছেন?”, “কোন সরবরাহকারী (supplier) সব যন্ত্রাংশ সরবরাহ করে?”, “কোন গ্রাহক তালিকার প্রতিটি পণ্য একবার হলেও কিনেছেন?” যুক্তিবিদ্যায় “সবার জন্য” হলো ∀ — universal quantification. join, σ বা π-এর কোনোটিই একা ∀ প্রকাশ করে না; সবগুলো স্বভাবে “অস্তিত্ব” (∃) খোঁজে। এই কাজের জন্য Codd তাঁর মূল algebra-য় একটি আলাদা অপারেটর রেখেছিলেন: division ÷.

পাড়ার মুদি দোকানদারের কাছে রমজানের “প্যাকেজ তালিকা”: চাল, ডাল, তেল, ছোলা। খাতায় লেখা কোন গ্রাহক কী কিনেছেন। প্রশ্ন: “কে পুরো প্যাকেজ কিনেছেন?” দোকানদার প্রত্যেক গ্রাহকের পাতা খুলে তালিকার প্রতিটি জিনিসে টিক দেন; একটিও বাদ থাকলে সেই গ্রাহক বাদ। কেউ বাড়তি চিনি কিনলে তাতে কিছু আসে যায় না। এটাই division: ভাজ্য (dividend) হলো “কে কী কিনেছেন” খাতা, ভাজক (divisor) হলো প্যাকেজ তালিকা।

৫.১ · আনুষ্ঠানিক সংজ্ঞা

ধরুন R-এর attribute X ∪ Y, আর S-এর attribute ঠিক Y (X আর Y পরস্পর-বিচ্ছিন্ন, Y খালি নয়)। উদাহরণে R = takes(sid, cid), S = core(cid); X = {sid}, Y = {cid}. তাহলে:

R ÷ S  =  { t ∈ πX(R)  :  প্রতিটি s ∈ S-এর জন্য (t, s) ∈ R }

সমতুল্য আরেকটি বর্ণনা, যা নামটির ব্যাখ্যা দেয়: R ÷ S হলো πX(R)-এর সবচেয়ে বড় উপসেট T, যার জন্য T × S ⊆ R. ঠিক যেমন পূর্ণসংখ্যায় 13 ÷ 4 = 3, কারণ 3 হলো সবচেয়ে বড় q যার জন্য q × 4 ≤ 13 — আর “ভাগশেষ” পড়ে থাকে। এখানেও (R ÷ S) × S ⊆ R, আর R-এর বাড়তি tuple-গুলো (যেমন Dilruba-র বাড়তি CSE301) ভাগশেষ।

৫.২ · মৌলিক অপারেটর দিয়ে উৎপত্তি

÷ কোনো নতুন ক্ষমতা আনে না; এটি π, × আর − দিয়ে লেখা যায়। যুক্তিটি দোকানদারের উল্টো দিক থেকে: “কে যোগ্য” সরাসরি না খুঁজে আগে খুঁজুন “কে অযোগ্য”, তারপর সবার থেকে তাঁদের বাদ দিন।

  1. T1 = πX(R) — সব প্রার্থী।
  2. T2 = T1 × S — “প্রত্যেক প্রার্থী প্রতিটি core নিলে” যে জোড়াগুলো থাকত।
  3. T3 = T2 − R — যে জোড়াগুলো থাকা উচিত ছিল কিন্তু নেই: কার কোন core বাকি।
  4. T4 = πX(T3) — অন্তত একটি core বাকি এমন প্রার্থী, অর্থাৎ অযোগ্যরা।
  5. R ÷ S = T1 − T4.
R ÷ S  =  πX(R)  −  πX( (πX(R) × S) − R )

প্রমাণ. t ∈ πX(R) নিন। t ∈ T4 ⇔ এমন s ∈ S আছে যার জন্য (t, s) ∈ T2 − R ⇔ এমন s ∈ S আছে যার জন্য (t, s) ∉ R (কারণ t ∈ T1 বলে (t, s) সবসময় T2-তে আছে)। তাই t ∉ T4 ⇔ প্রতিটি s ∈ S-এর জন্য (t, s) ∈ R — ঠিক সংজ্ঞার শর্ত। যুক্তির মূল চাবি: ¬∃s ¬P(s) ≡ ∀s P(s). ∎

নিচের ডায়াগ্রামে ছোট উদাহরণ: takes-এ ৬টি tuple — Arif (CSE101, CSE201), Bristy (CSE101), Dilruba (CSE101, CSE201, CSE301); core = {CSE101, CSE201}.

প্রতিটি মধ্যবর্তী relationলাল = এই ধাপের ফল

            
আকার
এই ধাপের relation
tuple
অপারেটর
  1. ভাজ্য আর ভাজক. R-এ ৬টি tuple, S-এ ২টি। R-এর যে attribute S-এ নেই, সেটি X = {sid} — উত্তর এই attribute-এর ওপর হবে।
  2. T1 = πsid(R). তিনজন প্রার্থী: Arif, Bristy, Dilruba. set বলে Dilruba একবার।
  3. T2 = T1 × S. ৩ × ২ = ৬টি জোড়া — “প্রত্যেকে দুটি core-ই নিলে” takes দেখতে এমন হতো।
  4. T3 = T2 − R. ছয়টির পাঁচটি সত্যিই R-এ আছে। শুধু (Bristy, CSE201) নেই — Bristy-র একটি core বাকি।
  5. T4 = πsid(T3). অযোগ্য প্রার্থী: শুধু Bristy. একটি core বাকি থাকাই বাদ পড়ার জন্য যথেষ্ট।
  6. T1 − T4. ফল {Arif, Dilruba}. Dilruba-র বাড়তি CSE301 ফলকে প্রভাবিত করেনি, কারণ T2-তে শুধু S-এর কোর্স আছে। যাচাই: দুজন × দুটি core = ৪টি জোড়া, চারটিই R-এ।

SQL-এ ÷ অপারেটর নেই, কিন্তু উপরের সূত্রটি প্রায় অক্ষরে অক্ষরে লেখা যায় — × হলো CROSS JOIN, − হলো EXCEPT, π হলো SELECT DISTINCT (EXCEPT নিজেও duplicate ফেলে দেয়)। এডিটরটি ডায়াগ্রামের takes(sid, cid) আর core(cid) টেবিল তৈরি করে নেয়:

-- T3 = (π sid(R) × S) − R : কার কোন core বাকি
SELECT t.sid, c.cid FROM (SELECT DISTINCT sid FROM takes) t CROSS JOIN core c
EXCEPT
SELECT sid, cid FROM takes;

-- R ÷ S = π sid(R) − π sid(T3)
SELECT sid FROM takes
EXCEPT
SELECT sid FROM (SELECT t.sid, c.cid
                 FROM (SELECT DISTINCT sid FROM takes) t CROSS JOIN core c
                 EXCEPT
                 SELECT sid, cid FROM takes);

প্রথম ফল একটি সারি: Bristy | CSE201 — ডায়াগ্রামের T3. দ্বিতীয় ফল দুটি সারি: Arif আর Dilruba — R ÷ S. লক্ষ করুন বাইরের SELECT sid FROM takes-এ Dilruba তিনবার আছে, কিন্তু EXCEPT set-অপারেশন বলে ফলে একবার। এই অনুবাদ সঠিক, কিন্তু পড়তে কষ্ট; বাস্তবে দুটি পরিচিত ধরন বেশি চলে।

৫.৩ · SQL-এ প্রথম ধরন — দুই স্তরের NOT EXISTS

SQL-এ ∀ নেই, কিন্তু ∃ আছে (EXISTS). তাই প্রমাণের চাবিটিই কাজে লাগে: “প্রতিটি core নিয়েছেন” ≡ “এমন কোনো core নেই, যেটি তিনি নেননি”। দুটি “না” — দুটি NOT EXISTS. এবার আসল db-uni-তে: enrollments টেবিলে student_id, course_id, term আর grade (প্রতিটি ভর্তি একটি সারি)। এডিটর একটি cse_core(course_id) টেবিল বানায় — CSE101, CSE201, CSE301:

-- এমন কোনো core নেই, যেটি এই শিক্ষার্থী নেননি
SELECT s.student_id, s.name
FROM students s
WHERE NOT EXISTS (
  SELECT 1 FROM cse_core k
  WHERE NOT EXISTS (
    SELECT 1 FROM enrollments e
    WHERE e.student_id = s.student_id
      AND e.course_id = k.course_id));

একটি সারি: 2201 | Dilruba Khatun. তিনি CSE101 (2022-Fall), CSE201 (2023-Spring) আর CSE301 (2024-Fall, grade এখনো NULL) — তিনটিই নিয়েছেন। Arif Hossain (2101)-এর CSE201 আর CSE301 আছে, কিন্তু CSE101 নেই: ভেতরের কোয়েরি তাঁর জন্য CSE101 খুঁজে পায় “যেটি তিনি নেননি”, তাই বাইরের NOT EXISTS মিথ্যা। কোয়েরিটি পড়ুন ভেতর থেকে বাইরে: সবচেয়ে ভেতরের স্তর জিজ্ঞেস করে “s কি k নিয়েছেন?”, মাঝের স্তর “s-এর বাকি থাকা কোনো k আছে কি?” — এটিই T3, আর বাইরের স্তর “না থাকলে s যোগ্য” — T1 − T4. দুই স্তরেই correlated subquery. “ভর্তি হয়েছেন” মানে এখানে enrollments-এ একটি সারি; “পাস করেছেন” চাইলে ভেতরে AND e.grade IS NOT NULL AND e.grade <> 'F' যোগ করুন।

৫.৪ · দ্বিতীয় ধরন — GROUP BY … HAVING COUNT

অনেকের কাছে সহজ চিন্তা গোনা: “শিক্ষার্থী যতগুলো ভিন্ন core নিয়েছেন, সেই সংখ্যা = মোট core-এর সংখ্যা” হলে তিনি সব নিয়েছেন। দুটি সতর্কতা লাগে: শুধু core কোর্সগুলোই গুনতে হবে (তাই আগে cse_core-এর সাথে join), আর একই কোর্স দুবার নিলে (ফেল করে আবার) যেন দুবার না গোনা হয় (তাই COUNT(DISTINCT …))।

-- প্রত্যেকে কয়টি core নিয়েছেন
SELECT e.student_id, COUNT(DISTINCT e.course_id) AS core_taken
FROM enrollments e JOIN cse_core k ON k.course_id = e.course_id
GROUP BY e.student_id;

-- সংখ্যা = মোট core হলে, সব নিয়েছেন
SELECT e.student_id
FROM enrollments e JOIN cse_core k ON k.course_id = e.course_id
GROUP BY e.student_id
HAVING COUNT(DISTINCT e.course_id) = (SELECT COUNT(*) FROM cse_core);

প্রথম ফলে চারজন: 2101 | 2, 2102 | 2, 2201 | 3, 2301 | 1. দ্বিতীয় ফল: 2201 — NOT EXISTS-এর সাথে একই উত্তর। join-টি বাদ দিলে কী হতো? Arif (2101)-এর CSE বিভাগের ভর্তিও তিনটি (CSE201, CSE301, CSE302), তাই “৩টি CSE কোর্স” গুনলে তিনিও ভুল করে ঢুকে পড়তেন — অনুশীলন ৪-এ এই বাগটিই আছে। COUNT-এর পদ্ধতি তখনই ঠিক, যখন গোনা হয় শুধু ভাজকের সদস্য, একবার করে।

৫.৫ · খালি ভাজক — তিন পদ্ধতি, তিন উত্তর

ভাজক S খালি হলে কী হয়? algebra-র সূত্রে T2 = T1 × ∅ = ∅, তাই T4 = ∅ আর R ÷ ∅ = πX(R) — R-এর সবাই। যুক্তিবিদ্যায় এটি স্বাভাবিক: খালি সেটের ওপর “সবার জন্য” সবসময় সত্য (vacuous truth). কিন্তু SQL-এর দুই পদ্ধতি এখানে একমত নয়। পরীক্ষা: “কোন শিক্ষার্থী Rumana Islam (instructor 4)-এর পড়ানো সব কোর্স নিয়েছেন?” — Rumana কোনো কোর্স পড়ান না (অংশ ৪)।

SELECT COUNT(*) AS not_exists_way FROM students s
WHERE NOT EXISTS (SELECT 1 FROM courses c WHERE c.instructor_id = 4
  AND NOT EXISTS (SELECT 1 FROM enrollments e
      WHERE e.student_id = s.student_id AND e.course_id = c.course_id));

SELECT COUNT(*) AS count_way FROM (SELECT e.student_id
  FROM enrollments e JOIN courses c ON c.course_id = e.course_id
  WHERE c.instructor_id = 4 GROUP BY e.student_id
  HAVING COUNT(DISTINCT e.course_id) =
         (SELECT COUNT(*) FROM courses WHERE instructor_id = 4));

SELECT COUNT(DISTINCT student_id) AS algebra_way FROM enrollments;

তিনটি আলাদা সংখ্যা: 8, 0 আর 7. NOT EXISTS-এর পদ্ধতি students টেবিলের সব ৮ জনকে দেয় — Habib Ullah-কেও, যাঁর একটিও ভর্তি নেই। COUNT-এর পদ্ধতি ০ দেয়, কারণ খালি কোর্স-তালিকার সাথে join-এ কোনো দলই তৈরি হয় না। algebra-র R ÷ ∅ = πstudent_id(enrollments) — ভর্তি আছে এমন ৭ জন (তৃতীয় কোয়েরি এটিই গোনে)। কোনটি “সঠিক”, তা প্রশ্নের ব্যবসায়িক অর্থের ওপর নির্ভর করে। পেশাদার অভ্যাস: division-এর কোয়েরি লেখার আগে ঠিক করুন, ভাজক খালি হলে কী চান — আর সেই ঘটনাটি আলাদা করে পরীক্ষা করুন।

৬ · Extended relational algebra — γ, generalized projection আর completeness

মৌলিক algebra দিয়ে “প্রতিটি বিভাগে কতজন শিক্ষার্থী?” বা “মোট বেতন কত?” লেখা যায় না — যোগ, গড় বা গণনার কোনো অপারেটর নেই। বাস্তব DBMS-এর জন্য তাই algebra-কে বাড়ানো হয়েছে; একে বলে extended relational algebra.

৬.১ · Aggregation আর grouping — γ

Grouping operator-এর লেখা বইভেদে আলাদা: Garcia-Molina–Ullman–Widom-এর বইয়ে γL(R), Silberschatz–Korth–Sudarshan-এর বইয়ের পুরোনো সংস্করণে বাঁকা 𝒢 (নতুন সংস্করণে γ)। সাধারণ রূপ:

G1, …, GnγF1(A1), …, Fm(Am)(E)

বাঁ দিকের নিচে grouping attribute (যাদের মান এক, তারা এক দলে), ডান দিকের নিচে aggregate function — COUNT, SUM, AVG, MIN, MAX. অর্থ: E-কে G-এর মান অনুযায়ী ভাগ করুন, প্রতিটি দল থেকে একটি tuple — G-এর মান আর প্রতিটি F-এর ফল। G খালি হলে পুরো E একটি দল, ফলে একটি tuple. SQL-এ এটিই GROUP BY (পর্ব ১৮), আর HAVING হলো γ-এর ওপরে বসানো একটি σ. উদাহরণ: dept_idγCOUNT(student_id), AVG(cgpa)(students).

-- dept_id γ COUNT(*), COUNT(cgpa), AVG(cgpa) (students)
SELECT dept_id, COUNT(*) AS n, COUNT(cgpa) AS with_cgpa,
       ROUND(AVG(cgpa), 2) AS avg_cgpa
FROM students GROUP BY dept_id;

-- G খালি: পুরো relation একটি দল
SELECT COUNT(*) AS students, MAX(cgpa) AS best FROM students;

-- π d.name, total (departments ⋈ (dept_id γ SUM(salary) (instructors)))
SELECT d.name AS dept, g.total_salary
FROM departments d
JOIN (SELECT dept_id, SUM(salary) AS total_salary
      FROM instructors GROUP BY dept_id) g ON g.dept_id = d.dept_id;

প্রথম ফলে ৪টি দল: NULL | 1 | 1 | 2.8 (Habib Ullah — NULL-ও একটি দল বানায়), 1 | 4 | 4 | 3.65, 2 | 2 | 1 | 2.95, 3 | 1 | 1 | 3.1. EEE (2)-তে দুজন, কিন্তু cgpa আছে একজনের — Fahim Karim-এর NULL; COUNT(cgpa) আর AVG NULL বাদ দেয়, তাই গড় শুধু Chayon-এর 2.95. দ্বিতীয় ফল এক সারি: 8 | 3.9. তৃতীয়টি দেখায় γ-এর ফলও একটি relation, তাই তার সাথে join করা যায়: CSE | 270000, EEE | 180000, BBA | 90000. English-এ কোনো শিক্ষক নেই বলে inner join-এ বাদ।

৬.২ · Generalized projection

মৌলিক π শুধু attribute বাছে। Generalized projection πF1, …, Fn(E)-এ প্রতিটি F একটি রাশি হতে পারে — attribute, ধ্রুবক বা গাণিতিক হিসাব — যেমন πname, salary × 12 → yearly(instructors). SQL-এর SELECT name, salary * 12 AS yearly ঠিক এটি (db-uni-তে Dr. Mahbub Alam-এর yearly 1320000). আর ρ দিয়ে ফলের attribute-এর নাম দেওয়াই SQL-এর AS.

৬.৩ · Extended অপারেটরের সারসংক্ষেপ

অপারেটরচিহ্নমৌলিক দিয়ে লেখা যায়?SQL
Theta / equi joinR ⋈θ Sহ্যাঁ: σθ(R × S)JOIN … ON
Natural joinR ⋈ Sহ্যাঁ: π(σ(R × ρ(S)))NATURAL JOIN, USING
IntersectionR ∩ Sহ্যাঁ: R − (R − S)INTERSECT
Semi / anti joinR ⋉ S, R ▷ Sহ্যাঁ: πA(R ⋈ S), R − (R ⋉ S)EXISTS, NOT EXISTS
DivisionR ÷ Sহ্যাঁ: π, ×, −দুই স্তরের NOT EXISTS, HAVING COUNT
Outer join⟕ ⟖ ⟗NULL-কে মান ধরলে হ্যাঁLEFT/RIGHT/FULL JOIN
Generalized projectionπF…না — হিসাব নতুনSELECT-এর রাশি
Grouping / aggregationγ বা 𝒢নাGROUP BY, aggregate
Duplicate elimination, sorting (bag algebra)δ, τset algebra-য় দরকারই নেইDISTINCT, ORDER BY
AssignmentT ← Eশুধু নাম দেওয়াWITH (CTE), view

Set বনাম bag: এই পর্বের সব সংজ্ঞা set-এর ওপর — কোনো tuple দুবার থাকে না, তাই π নিজেই duplicate মোছে। SQL টেবিল আর কোয়েরির ফল কিন্তু bag: SELECT dept_id FROM students 1 চারবার দেয়। তাই পাঠ্যবইয়ের bag algebra-য় দুটি বাড়তি অপারেটর থাকে: δ (duplicate মোছা, SQL-এর DISTINCT) আর τ (সাজানো, ORDER BY). algebra থেকে SQL-এ অনুবাদ করার সময় প্রতিটি π-এর পাশে ভাবুন — এখানে duplicate কি ফলের অর্থ বদলাবে? গোনা বা যোগের আগে বদলায়; EXISTS-এর ভেতরে বদলায় না।

৬.৪ · Relational completeness

১৯৭২ সালে Codd তাঁর “Relational Completeness of Data Base Sublanguages” প্রবন্ধে একটি মানদণ্ড দেন: কোনো কোয়েরি-ভাষা relationally complete, যদি relational calculus (পর্ব ৩৪) দিয়ে যত কোয়েরি লেখা যায়, সবই সেই ভাষায় লেখা যায়। তিনি দেখান মৌলিক algebra-ও ঠিক এতটা প্রকাশ করতে পারে (safe calculus আর algebra সমান শক্তির)। SQL relationally complete: σ হলো WHERE, π হলো SELECT DISTINCT, × হলো CROSS JOIN, ∪ হলো UNION, − হলো EXCEPT বা NOT EXISTS, ρ হলো AS. আর SQL এর চেয়েও বেশি পারে — aggregation, ORDER BY, আর WITH RECURSIVE (পর্ব ২৫)। শেষেরটি গুরুত্বপূর্ণ: ১৯৭৯ সালে Alfred Aho আর Jeffrey Ullman প্রমাণ করেন, মৌলিক algebra দিয়ে transitive closure (“A-র mentor-এর mentor-এর … যত স্তরই হোক”) প্রকাশ করা যায় না। অর্থাৎ “complete” মানে “সব পারে” নয়, “calculus যা পারে, তা পারে”।

৭ · Query tree আর join-এর ক্রম বদল

একটি algebra রাশিকে গাছ হিসেবে আঁকা যায়: পাতায় (leaf) টেবিল, ভেতরের প্রতিটি গিঁটে (node) একটি অপারেটর, ডেটা নিচ থেকে ওপরে যায়। একে বলে query tree. DBMS আপনার SQL প্রথমে এমন একটি গাছে বদলায়, তারপর equivalence rule দিয়ে গাছটি নতুন করে সাজায় — ফল একই, কাজ কম। মূল নিয়মগুলো (E মানে যেকোনো রাশি):

  1. σ ভাঙা: σθ1 ∧ θ2(E) = σθ1(σθ2(E)), আর σ-গুলোর ক্রম বদলানো যায়।
  2. × থেকে ⋈: σθ(E1 × E2) = E1 ⋈θ E2.
  3. বিনিময় আর সংযোজন: E1 ⋈ E2 = E2 ⋈ E1; (E1 ⋈ E2) ⋈ E3 = E1 ⋈ (E2 ⋈ E3) — অংশ ২-এর প্রমাণ।
  4. σ নিচে ঠেলা (predicate pushdown): θ0-তে শুধু E1-এর attribute থাকলে σθ0(E1 ⋈θ E2) = σθ0(E1) ⋈θ E2.
  5. π নিচে ঠেলা: L1, L2 যথাক্রমে E1, E2-এর দরকারি attribute, আর θ-তে শুধু এদের মধ্যের attribute থাকলে πL1 ∪ L2(E1 ⋈θ E2) = πL1(E1) ⋈θ πL2(E2). θ-তে অন্য attribute থাকলে সেগুলোও নিচের π-তে রাখতে হয়, আর ওপরে একটি π রেখে শেষে ফেলতে হয়।

নিয়ম ৪-এর প্রমাণ: E1 ⋈θ E2-এর প্রতিটি tuple একটি জোড়া (r, s), r ∈ E1. θ0 শুধু r-এর মান দেখে, তাই জোড়াটি σθ0 পেরোয় ⇔ r নিজে σθ0 পেরোয়। অতএব ছাঁকা আগে করা আর পরে করা একই জোড়ার সেট দেয়। ∎ (outer join-এ নিয়মটি শুধু রাখা-দিকের শর্তে খাটে: R ⟕ S-এর ওপরের R-এর শর্ত নিচে ঠেলা যায়, কিন্তু S-এর শর্ত নিচে ঠেললে ফল বদলায় — ওপরের শর্ত NULL-ভরাট সারি বাদ দিত, নিচের শর্তের পর সেগুলো আবার NULL-সহ ফিরে আসে।)

উদাহরণ প্রশ্ন: “CSE বিভাগের যেসব শিক্ষার্থী কোনো কোর্সে 'A' পেয়েছেন, তাঁদের নাম।” প্রথম গাছ সরাসরি সংজ্ঞা থেকে, তারপর নিয়ম দিয়ে সাজানো — প্রতিটি ধাপে সবচেয়ে বড় মধ্যবর্তী ফলের আকার দেখুন (সংখ্যাগুলো db-uni-র আসল ডেটা থেকে):

নিচ থেকে ওপরে পড়ুন · বন্ধনীতে tuple সংখ্যালাল = এই ধাপে বদল

            
খরচের আভাস
সবচেয়ে বড় মধ্যবর্তী
নিয়ম
ফল
  1. সংজ্ঞা থেকে সরাসরি গাছ. তিনটি টেবিলের × — ৮ × ৪ × ১৩ = ৪১৬টি tuple, তারপর একটি বড় σ. ফল সঠিক (দুটি নাম), কিন্তু এটি সবচেয়ে খারাপ পরিকল্পনা।
  2. σ + × → ⋈. join-এর শর্তগুলো σ থেকে × এর সাথে মিশে ⋈ হলো। students ⋈ departments = ৭ (Habib-এর dept_id NULL), তারপর enrollments-এর সাথে ১৩ — সবচেয়ে বড় মধ্যবর্তী ৪১৬ থেকে ১৩।
  3. d.name = 'CSE' নিচে. শর্তটি শুধু departments-এর, তাই নিয়ম ৪ অনুযায়ী সরাসরি তার ওপরে: ১টি tuple. এখন students ⋈ = ৪ জন (CSE-র), আর enrollments-এর সাথে ১০টি।
  4. e.grade = 'A' নিচে. শর্তটি শুধু enrollments-এর: ১৩ থেকে ৪টি সারি। শেষ join-এর ফল ৪ — সবচেয়ে বড় মধ্যবর্তী এখন মাত্র ৪, ফল একই।
  5. π নিচে. প্রতিটি পাতার ঠিক ওপরে শুধু দরকারি কলাম রাখা: join-এর কলাম আর name. সারি একই থাকে, কিন্তু প্রতিটি সারি সরু হয় — ডিস্ক থেকে কম বাইট পড়া, memory-তে কম জায়গা। ফল Arif Hossain আর Dilruba Khatun.

এই ছোট ডেটায় ৪১৬ বনাম ৪ হাস্যকর মনে হতে পারে, কিন্তু অনুপাতটি বড় ডেটায়ও থাকে: দশ হাজার শিক্ষার্থী আর দশ লাখ ভর্তিতে প্রথম গাছটি অসম্ভব, শেষটি এক সেকেন্ডের কম। নিয়ম ৩ (join-এর ক্রম বদল) এর সাথে যুক্ত হলে optimizer বেছে নিতে পারে কোন দুটি টেবিল আগে জুড়লে মধ্যবর্তী ফল সবচেয়ে ছোট — একে বলে join order নির্বাচন, যার পুরো খরচ-হিসাব পর্ব ৪৩-এ।

আরও গভীরে: n টি relation-এর join কত রকম ক্রমে করা যায়? বিনিময় আর সংযোজন ধরলে সব আকারের গাছ মিলিয়ে (2n − 2)! / (n − 1)! টি — n = 5-এ ১,৬৮০, n = 10-এ ১,৭৬৪ কোটির বেশি। তাই ১৯৭৯ সালে IBM System R-এর Patricia Selinger ও সহকর্মীরা শুধু left-deep tree (প্রতিটি join-এর ডান দিকে একটি মূল টেবিল; n! টি ক্রম) আর dynamic programming দিয়ে খোঁজার পদ্ধতি দেন, যা আজও বেশিরভাগ optimizer-এর ভিত্তি। PostgreSQL FROM-এ ১২ বা বেশি টেবিল পেলে (geqo_threshold-এর ডিফল্ট মান) পুরো খোঁজ ছেড়ে genetic algorithm-ভিত্তিক GEQO ব্যবহার করে।

৮ · পরীক্ষার ধাঁচের সমস্যা — বিশ্ববিদ্যালয়ের schema-য়

বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর লিখিত পরীক্ষায় relational algebra-র প্রশ্ন সাধারণত এমন: একটি schema দেওয়া, ইংরেজিতে প্রশ্ন, algebra-য় উত্তর লিখতে হবে। নিচে db-uni-র schema সংক্ষেপে — students(student_id, name, dept_id, batch, cgpa), instructors(instructor_id, name, dept_id, salary, mentor_id), courses(course_id, title, dept_id, credits, instructor_id), enrollments(student_id, course_id, term, grade), departments(dept_id, name, building). প্রতিটির পাশে উত্তর আর এই ডেটায় ফল।

  1. যে শিক্ষার্থী কোনো কোর্সে ভর্তি হননি. πstudent_id(students) − πstudent_id(enrollments), অথবা students ▷ enrollments. ফল: 2302 Habib Ullah.
  2. যে শিক্ষক CSE-র প্রতিটি কোর্স পড়ান. πinstructor_id, course_id(courses) ÷ πcourse_id(σdept_id = 1(courses)). ফল: খালি — CSE-র চারটি কোর্স তিনজন শিক্ষকের মধ্যে ভাগ করা। (এখানে ভাজ্য courses নিজেই, কারণ db-uni-তে কোর্স–শিক্ষক সম্পর্ক courses-এর একটি কলামে; অনেক বইয়ের schema-য় এর জন্য আলাদা teaches টেবিল থাকে।)
  3. যে শিক্ষার্থী Tasnim Chowdhury (6)-এর পড়ানো সব কোর্স নিয়েছেন. πstudent_id, course_id(enrollments) ÷ πcourse_id(σinstructor_id = 6(courses)). ভাজক {CSE301, CSE302}. ফল: 2101 Arif Hossain — Bristy আর Dilruba শুধু CSE301 নিয়েছেন।
  4. একই কোর্সে পড়েছেন এমন শিক্ষার্থী-জোড়া (প্রতিটি জোড়া একবার). πa.student_id, b.student_id(σa.student_id < b.student_id(ρa(enrollments) ⋈a.course_id = b.course_id ρb(enrollments))). ফল ৫টি জোড়া: (2201, 2301), (2101, 2102), (2101, 2201), (2102, 2201), (2103, 2203). π-এর আগে course_id রাখলে ৮টি সারি, কারণ 2101–2102 এর মতো জোড়া দুটি কোর্সে একসাথে।
  5. সর্বোচ্চ বেতন — aggregation ছাড়া. πsalary(instructors) − πa.salary(σa.salary < b.salary(ρa(instructors) × ρb(instructors))). যুক্তি: যে বেতনের চেয়ে বড় কোনো বেতন আছে, সেগুলো বাদ দিলে থাকে শুধু সর্বোচ্চটি। ফল: 120000 (Dr. Anisur Rahman). γ দিয়ে একই কাজ: γMAX(salary)(instructors).
  6. প্রতিটি বিভাগের নাম আর শিক্ষার্থীর সংখ্যা, শূন্যসহ. d.nameγCOUNT(s.student_id)(departments ⟕ students). outer join বলে English আসে ০ নিয়ে; COUNT(*) লিখলে NULL-ভরাট সারিটিও ১ গোনা হতো।
  7. অন্তত দুটি ভিন্ন কোর্স নিয়েছেন এমন শিক্ষার্থী — aggregation ছাড়া. πa.student_id(σa.course_id ≠ b.course_id(ρa(enrollments) ⋈a.student_id = b.student_id ρb(enrollments))). নিজের সাথে জোড়া দিয়ে দেখা হচ্ছে, একই শিক্ষার্থীর দুটি আলাদা কোর্স আছে কি না। ফল ৪ জন: 2101, 2102, 2201, 2301. “অন্তত k টি” লিখতে k টি কপি আর প্রতিটি জোড়ায় ≠ লাগে — তাই বাস্তবে γCOUNT অনেক সহজ।
  8. ঠিক একটি কোর্স নিয়েছেন এমন শিক্ষার্থী. πstudent_id(enrollments) − (আগের সমস্যার ফল). “ঠিক একটি” = “অন্তত একটি” − “অন্তত দুটি”। ফল: 2103, 2202, 2203.

প্রথম তিনটি পাতাতেই চালিয়ে দেখুন। দ্বিতীয়টিতে ভাজ্যের ভূমিকায় courses t — “শিক্ষক i কোর্স c পড়ান” জানতে এটিই আমাদের teaches:

-- ১) students ▷ enrollments
SELECT s.student_id, s.name FROM students s
WHERE NOT EXISTS (SELECT 1 FROM enrollments e WHERE e.student_id = s.student_id);

-- ২) CSE-র প্রতিটি কোর্স পড়ান এমন শিক্ষক
SELECT i.name FROM instructors i
WHERE NOT EXISTS (SELECT 1 FROM courses c WHERE c.dept_id = 1
  AND NOT EXISTS (SELECT 1 FROM courses t
                  WHERE t.course_id = c.course_id AND t.instructor_id = i.instructor_id));

-- ৩) Tasnim (6)-এর সব কোর্স নিয়েছেন এমন শিক্ষার্থী
SELECT s.student_id, s.name FROM students s
WHERE NOT EXISTS (SELECT 1 FROM courses c WHERE c.instructor_id = 6
  AND NOT EXISTS (SELECT 1 FROM enrollments e
                  WHERE e.student_id = s.student_id AND e.course_id = c.course_id));

প্রথম ফল: 2302 | Habib Ullah. দ্বিতীয় ফল খালি (০টি সারি) — উপরের যুক্তি অনুযায়ীই। তৃতীয় ফল: 2101 | Arif Hossain. তিনটি কোয়েরি একই কাঠামোর: বাইরের টেবিল প্রার্থী, মাঝের টেবিল ভাজক, ভেতরের টেবিল ভাজ্য। এই ছাঁচটি মুখস্থ নয়, বুঝে রাখুন — যেকোনো “সবগুলো” প্রশ্নে শুধু তিনটি টেবিলের নাম বদলায়।

৯ · AI-এর যুগে relational algebra

AI-কে “এই প্রশ্নের relational algebra লেখো” বললে উত্তর প্রায়ই দেখতে নিখুঁত — চিহ্নগুলো ঠিক জায়গায়। কিন্তু algebra-র ভুল চোখে পড়ে না, কারণ কেউ এটি “চালায়” না। এই বিষয়ে AI-এর প্রচলিত ভুল:

AI-এর ভুলফলকীভাবে ধরবেন
“সবগুলো” প্রশ্নে ⋈ বা ⋉“অন্তত একটি”-র উত্তরএকজন প্রার্থী যিনি ভাজকের একটি মাত্র নিয়েছেন — তিনি ফলে আছেন?
÷-এর ভাজ্যে বাড়তি attribute (যেমন term, grade)X ভুল, ফল প্রায় খালিভাজ্যের attribute = X ∪ Y কি না, π দিয়ে আগে কাটা হয়েছে কি না
HAVING COUNT-এ DISTINCT নেই বা ভাজকে সীমিত নয়পুনরায় নেওয়া বা বাড়তি কোর্স গোনা হয়একই কোর্স দুবার নেওয়া শিক্ষার্থী দিয়ে পরীক্ষা
Natural join-এ অপ্রত্যাশিত সাধারণ attributeনিঃশব্দে খালি বা কম ফলদুই schema-র সাধারণ নামগুলো হাতে লিখুন
Outer join-এর ক্রম বদলে “সমান” দাবিNULL-সারি হারায়মিলহীন একটি tuple দিয়ে দুই রাশি হাতে হিসাব
Set আর bag গুলিয়ে ফেলাSQL অনুবাদে duplicateπ-এর SQL রূপে DISTINCT বা EXISTS আছে কি না
খালি ভাজকের কথা না ভাবাতিন পদ্ধতিতে তিন উত্তরভাজক খালি করে চালান

AI-কে algebra আর SQL দুটিই চান, আর নিজে যাচাই করার মতো ছোট ডেটাও চান:

আমি relational algebra শিখছি। schema: enrollments(student_id, course_id, term, grade), courses(course_id, title, dept_id, credits, instructor_id). শুধু এই কলামগুলোই আছে, অন্য কোনো কলামের নাম অনুমান করবে না। প্রশ্ন: কোন শিক্ষার্থী instructor_id = 6-এর পড়ানো প্রতিটি কোর্স নিয়েছেন? ১. প্রশ্নটি ⋈, ⋉ না ÷ — এক লাইনে কারণসহ বলো। ২. algebra লেখো: ভাজ্য আর ভাজক আলাদা করে, প্রতিটির attribute-সহ। ৩. π, × আর − দিয়ে ÷ খুলে লেখো, প্রতিটি মধ্যবর্তী relation-এর অর্থ এক লাইনে। ৪. SQLite-এ দুটি SQL দাও: দুই স্তরের NOT EXISTS, আর GROUP BY … HAVING COUNT(DISTINCT …). ৫. ৫–৬ সারির একটি ছোট নমুনা ডেটা দাও, যেখানে একজন সব কোর্স নিয়েছেন, একজন একটি, একজন একই কোর্স দুবার — আর ভাজক খালি হলে দুই SQL-এর ফল কেন আলাদা, ব্যাখ্যা করো।

AI-এর লেখা algebra বা division কোয়েরি হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Theta join-কে মৌলিক অপারেটর দিয়ে লিখুন। — R ⋈θ S = σθ(R × S).
  • Natural join আর equi join-এর পার্থক্য? — natural join একই নামের সব attribute-এ নিজে থেকে সমতা বসায় আর সেই কলাম একবার রাখে; equi join-এ শর্ত লিখতে হয় আর কলাম দুবার থাকে।
  • Division-কে π, ×, − দিয়ে লিখুন। — R ÷ S = πX(R) − πX((πX(R) × S) − R).
  • Division কোন ধরনের প্রশ্নে লাগে? — “সবগুলো”/“প্রতিটি” (∀) প্রশ্নে, যেমন সব core কোর্স নেওয়া শিক্ষার্থী।
  • SQL কি relationally complete? — হ্যাঁ; algebra-র প্রতিটি অপারেটর SQL-এ লেখা যায়, আর SQL তার বাইরেও aggregation ও recursion পারে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এখানে উত্তরের চেয়ে যুক্তিটা বেশি জরুরি।

প্র ০১Division মৌলিক অপারেটর দিয়েই লেখা যায়, তবু Codd এটিকে আলাদা অপারেটর রাখলেন কেন? আর SQL-এ এটি কখনো আসেনি কেন?

Codd-এর ১৯৭০ সালের প্রবন্ধ আর ১৯৭২ সালের “Relational Completeness of Data Base Sublanguages”-এর লক্ষ্য ছিল প্রমাণ করা যে একটি algebra দিয়ে calculus-এর সব কোয়েরি প্রকাশ করা যায়। calculus-এ দুটি quantifier আছে: ∃ (“আছে”) আর ∀ (“সবার জন্য”)। ∃-এর স্বাভাবিক algebra-রূপ হলো π আর ⋈ — join করে কলাম ফেলে দিলেই “এমন কেউ আছে” প্রকাশ পায়। ∀-এর জন্য Codd ÷ রাখেন, যাতে calculus থেকে algebra-য় অনুবাদ সরাসরি হয়: প্রতিটি ∀ একটি ÷. গাণিতিকভাবে ∀x P ≡ ¬∃x ¬P, তাই − দিয়েও কাজ চলে, কিন্তু তখন রাশিটি অংশ ৫-এর মতো দীর্ঘ আর পড়তে কঠিন। অর্থাৎ ÷ একটি সুবিধা, প্রয়োজন নয় — ঠিক যেমন ∩ = R − (R − S).

SQL-এর নকশাকাররা (IBM-এর Donald Chamberlin আর Raymond Boyce, ১৯৭৪ সালের SEQUEL) অন্য পথ নেন। তাঁরা চেয়েছিলেন ইংরেজির কাছাকাছি একটি ভাষা, আর ∀ প্রকাশের দায়িত্ব দেন NOT EXISTS আর subquery-কে। ফলে SQL relationally complete থেকেও কোনো DIVIDE keyword পায়নি। পরে বিভিন্ন লেখক — বিশেষ করে C. J. Date আর Hugh Darwen — দেখান যে division-এর একাধিক রূপ আছে (খালি ভাজকে কী হবে, ভাজ্যের বাইরের প্রার্থীদের কী হবে — অংশ ৫.৫-এর তিন উত্তর), আর কোনো একটিকে মানক করা সহজ নয়। Joe Celko-র মতো SQL লেখকেরা এজন্য “exact division” (ভাগশেষ নেই) আর “division with remainder” আলাদা করে লেখার কৌশল জনপ্রিয় করেন।

বাস্তবে এর ফল এই যে প্রতিটি প্রোগ্রামারকে নিজে ঠিক করতে হয় তিনি কোন division চান। NOT EXISTS-এর রূপ খালি ভাজকে সবাইকে দেয়, COUNT-এর রূপ কাউকে দেয় না। একটি keyword থাকলে এই সিদ্ধান্ত লুকিয়ে থাকত; না থাকায় অন্তত চোখের সামনে থাকে — যদি আপনি জানেন কোথায় দেখতে হবে।

মূল উপলব্ধি: division হলো ∀-এর algebra-রূপ। এটি আলাদা অপারেটর সুবিধার জন্য, ক্ষমতার জন্য নয়; আর SQL-এ তার অনুপস্থিতি মানে প্রতিটি “সবগুলো” প্রশ্নে খালি ভাজকের অর্থ আপনাকেই ঠিক করতে হবে।

প্র ০২তত্ত্বে natural join সবচেয়ে মৌলিক join, অথচ অনেক দল SQL-এ NATURAL JOIN নিষিদ্ধ করে। এই দ্বন্দ্বের উৎস কী?

তত্ত্বে relation-এর attribute নাম শুধু লেবেল নয়, অর্থের বাহক। ভালো ডিজাইনে “dept_id” নামটি যেখানেই থাকুক, তার অর্থ একই: একটি বিভাগের পরিচয়। এই অনুমানের ওপর normalization-এর অনেক ফল দাঁড়িয়ে — পর্ব ৩৭-এ দেখবেন, একটি relation-কে ভেঙে টুকরোগুলো natural join করলে আবার মূল relation ফিরে আসে কি না (lossless join), সেই প্রশ্নই decomposition-এর মূল পরীক্ষা। তত্ত্বের লেখায় R ⋈ S ছোট, সুন্দর, আর এর বিনিময় ও সংযোজন নিয়ম প্রমাণে কাজে লাগে। কিছু তাত্ত্বিক, যেমন Date আর Darwen, “একই নাম = একই অর্থ” নীতিকে ডিজাইনের মূল শৃঙ্খলা হিসেবেই দেখেন।

বাস্তব schema এই শৃঙ্খলা মানে না। db-uni-তেই students.name (মানুষের নাম) আর departments.name (বিভাগের নাম) — একই নাম, ভিন্ন অর্থ; অংশ ২-এ natural join শূন্য দিয়েছে। আরও সাধারণ উদাহরণ: প্রায় প্রতিটি টেবিলে created_at, updated_at, status বা id নামের কলাম। এগুলো যোগ হয় পরে, অন্য কেউ যোগ করেন, আর NATURAL JOIN-যুক্ত পুরোনো কোয়েরির অর্থ কোনো ত্রুটি ছাড়াই বদলে যায়। কোড-রিভিউতে কেউ এটি ধরতে পারেন না, কারণ কোয়েরির লেখা বদলায়নি — বদলেছে টেবিল।

SQL-92 NATURAL JOIN আর USING দুটিই এনেছিল। USING মাঝামাঝি সমাধান: কলামের নাম আপনি নিজে বলেন, আর ফলে কলামটি একবার থাকে — natural join-এর সুবিধা, ঝুঁকি ছাড়া। SQL Server এখনো NATURAL JOIN বা USING কোনোটিই চালায় না; সেখানে শুধু ON. PostgreSQL, MySQL, Oracle আর SQLite — চারটিই NATURAL, USING আর ON তিন ধরনই চালায়।

মূল উপলব্ধি: natural join-এর অর্থ নির্ভর করে schema-র নামকরণের শৃঙ্খলার ওপর। তত্ত্বে সেই শৃঙ্খলা ধরে নেওয়া হয়, বাস্তবে নয় — তাই প্রমাণে ⋈ লিখুন, প্রোডাকশন কোডে USING বা ON.

প্র ০৩Outer join কেন মৌলিক algebra-র অংশ নয়, আর কেন DBMS-গুলো এত দেরিতে FULL JOIN পেয়েছে?

মৌলিক algebra-র প্রতিটি অপারেটর একটি “পরিষ্কার” set-অপারেশন: ফলে যা আছে, তা সব ইনপুট থেকে আসা সত্য তথ্য। outer join একটি নতুন জিনিস বানায় — NULL-ভরাট tuple, যা কোনো ইনপুটে ছিল না। একে algebra-য় আনতে NULL-কে একটি মান হিসেবে মানতে হয়, আর তখনই তিন-মানের যুক্তি, “NULL = NULL কি সত্য?” এমন প্রশ্ন ঢুকে পড়ে। Codd ১৯৭৯ সালের “Extending the Database Relational Model to Capture More Meaning” প্রবন্ধে null আর outer join দুটিই প্রস্তাব করেন, কিন্তু এগুলো মূল algebra-র বাইরে extended অংশ হিসেবেই থেকে যায়।

বীজগণিতের দিক থেকেও outer join অস্বস্তিকর। ⟕ বিনিময়যোগ্য নয়; inner join আর outer join মেশালে সংযোজন নিয়ম সাধারণত ভাঙে; আর σ নিচে ঠেলা শুধু রাখা-দিকেই চলে। optimizer গবেষণায় এ নিয়ে আলাদা কাজ হয়েছে — যেমন César Galindo-Legaria আর Arnon Rosenthal-এর ১৯৯০-এর দশকের outer join সরলীকরণ ও ক্রম বদলের নিয়ম। একটি কাজের কৌশল: যদি ওপরের কোনো শর্ত NULL-ভরাট সারিকে সবসময় বাদ দেয় (যেমন WHERE c.title = 'X'), তাহলে outer join আসলে inner join-এর সমান — optimizer সেটি বদলে দিয়ে বেশি স্বাধীনতা পায়।

SQL-এ মানসম্মত outer join আসে SQL-92-এ। তার আগে vendor-রা নিজস্ব চিহ্ন বানায়: Oracle-এ (+), পুরোনো SQL Server ও Sybase-এ *=. FULL JOIN পেতে অনেক DBMS-এর আরও সময় লাগে: SQLite RIGHT আর FULL JOIN যোগ করে ৩.৩৯.০ সংস্করণে (২০২২), আর MySQL এখনো FULL JOIN চালায় না — সেখানে LEFT আর RIGHT-এর UNION লিখতে হয়, যা অংশ ৩-এর ⟗ = ⟕ ∪ ⟖ সংজ্ঞারই অনুবাদ।

মূল উপলব্ধি: outer join তথ্য-হারানো ঠেকায়, কিন্তু NULL-এর দাম দেয় — বীজগণিতের সুন্দর নিয়মগুলো আংশিক ভাঙে। তাই outer join-যুক্ত কোয়েরির ক্রম নিজে বদলানোর আগে প্রতিবার মিলহীন একটি সারি দিয়ে পরীক্ষা করুন।

প্র ০৪Join-এর ক্রম বদলানো ফলের দিক থেকে নিরাপদ, কিন্তু খরচের দিক থেকে কত বড় ব্যাপার? optimizer কীভাবে সিদ্ধান্ত নেয়?

অংশ ২-এর প্রমাণ বলে inner join-এর যেকোনো ক্রম একই ফল দেয়। কিন্তু খরচ নির্ভর করে মধ্যবর্তী ফলের আকারের ওপর। ধরুন orders (দশ লাখ), customers (এক লাখ) আর ছোট একটি cities টেবিল (দশটি শহর, যার একটি শর্তে বাছা)। আগে orders ⋈ customers করলে দশ লাখ সারির মধ্যবর্তী ফল; আগে σ(cities) ⋈ customers করলে হয়তো দশ হাজার গ্রাহক, তারপর তাঁদের অর্ডার। একই উত্তর, কাজের পার্থক্য শতগুণ। অংশ ৭-এর ছোট উদাহরণেও ৪১৬ থেকে ৪-এ নামা গেছে।

সমস্যাটি হলো ক্রমের সংখ্যা। n টি টেবিলের বাঁ-গভীর (left-deep) ক্রম n! টি, আর সব আকারের গাছ (2n − 2)! / (n − 1)! টি। ১৯৭৯ সালে Patricia Selinger ও সহকর্মীদের “Access Path Selection in a Relational Database Management System” প্রবন্ধ (IBM System R) এর একটি কার্যকর সমাধান দেয়: dynamic programming — প্রথমে প্রতিটি একক টেবিলের সেরা পড়ার উপায়, তারপর প্রতিটি দুই-টেবিলের সেরা join, তারপর তিন… প্রতিটি উপসেটের জন্য শুধু সেরা পরিকল্পনাটি মনে রাখা। খরচ অনুমানের জন্য লাগে statistics: প্রতিটি টেবিলে কত সারি, প্রতিটি কলামে কত ভিন্ন মান, আর একটি শর্ত কত ভাগ সারি রাখবে (selectivity). এই প্রবন্ধের “interesting orders” ধারণা (কোন ক্রমে সাজানো ফল পরে কাজে লাগবে) আজও প্রায় সব বাণিজ্যিক optimizer-এ আছে।

বাস্তব DBMS-এ সীমাও আছে। PostgreSQL ১২ বা বেশি FROM-আইটেমে পুরো dynamic programming ছেড়ে GEQO নামের genetic algorithm ব্যবহার করে (geqo_threshold), আর join_collapse_limit (ডিফল্ট ৮) ঠিক করে কতগুলো স্পষ্ট JOIN-কে সে নতুন করে সাজাবে। অনুমান ভুল হলে — যেমন পুরোনো statistics — optimizer ভুল ক্রম বেছে নেয়, আর কোয়েরি হঠাৎ ধীর হয়ে যায়; পর্ব ৪১ আর ৪৩-এ EXPLAIN দিয়ে এটি ধরা শিখবেন।

মূল উপলব্ধি: algebra-র equivalence rule বলে কোন পরিকল্পনাগুলো বৈধ; statistics আর cost model বলে কোনটি সস্তা। প্রথমটি গণিত, দ্বিতীয়টি অনুমান — আর অনুমান ভুল হলে সঠিক কোয়েরিও ধীর হয়।

প্র ০৫SQL relationally complete, তবু transitive closure-এর জন্য WITH RECURSIVE লাগে কেন? “Complete” শব্দটি কি বিভ্রান্তিকর?

Codd-এর “relational completeness” একটি আপেক্ষিক মানদণ্ড: relational calculus যা পারে, ভাষাটি তা পারে কি না। calculus নিজে first-order logic-এর ওপর দাঁড়ানো — ∃, ∀, ∧, ∨, ¬ — আর প্রতিটি সূত্রের দৈর্ঘ্য নির্দিষ্ট। এমন একটি সূত্র “mentor-এর mentor” বলতে পারে, “mentor-এর mentor-এর mentor”-ও পারে, কিন্তু “যত স্তরই হোক” বলতে পারে না, কারণ স্তরের সংখ্যা ডেটার ওপর নির্ভর করে, সূত্রের ওপর নয়। ১৯৭৯ সালে Alfred Aho আর Jeffrey Ullman “Universality of Data Retrieval Languages” প্রবন্ধে আনুষ্ঠানিকভাবে প্রমাণ করেন, relational algebra দিয়ে একটি relation-এর transitive closure প্রকাশ করা যায় না, আর এর জন্য ভাষায় পুনরাবৃত্তি (fixpoint) যোগ করার প্রস্তাব দেন।

এর পরের দশকে ডেটাবেস গবেষণায় Datalog এই ধারণাকে কেন্দ্রে আনে — নিয়মভিত্তিক, recursive কোয়েরি-ভাষা। SQL:1999 মানে অবশেষে WITH RECURSIVE আসে (পর্ব ২৫), যা একটি fixpoint: প্রতিটি ধাপে নতুন সারি যোগ, যতক্ষণ না নতুন কিছু আসে। একইভাবে aggregation (COUNT, SUM) মৌলিক algebra-য় নেই; এটিও extended অংশ। তাই আজকের SQL relational algebra-র চেয়ে স্পষ্টভাবে বেশি শক্তিশালী — recursive CTE আর window function মিলিয়ে কিছু অর্থে প্রায় একটি সাধারণ প্রোগ্রামিং ভাষার কাছাকাছি।

তাহলে “complete” কেন? কারণ শব্দটি এসেছে তুলনা থেকে: ১৯৭০-এর দশকে অনেক navigational ভাষা (পর্ব ৪-এর CODASYL) declarative কোয়েরির একটি নির্দিষ্ট মানও পূরণ করত না। Codd একটি মাপকাঠি দিলেন যাতে বলা যায় “এই ভাষা অন্তত এতটা পারে”। সমস্যা শুধু নামে — পরীক্ষায় “SQL relationally complete” লিখলে ঠিক, “SQL শুধু relationally complete” লিখলে ভুল।

মূল উপলব্ধি: relational completeness একটি ন্যূনতম মান, সর্বোচ্চ সীমা নয়। মৌলিক algebra নির্দিষ্ট দৈর্ঘ্যের প্রশ্ন পারে; “যত স্তরই হোক” প্রশ্নে লাগে recursion — SQL-এ যা WITH RECURSIVE.

অনুশীলন

একটি হাতে division, একটি algebra থেকে SQL, একটি “সবার জন্য” কোয়েরি লেখা আর একটি AI-এর কোয়েরির বাগ খোঁজা।

  1. হাতে হিসাব (trace): R(A, B) = {(1, x), (1, y), (2, x), (3, x), (3, y), (3, z)} আর S(B) = {x, y}. ধাপে ধাপে T1 = πA(R), T2 = T1 × S, T3 = T2 − R, T4 = πA(T3) আর R ÷ S বের করুন। S = {x} হলে, আর S = ∅ হলে ফল কী?
    • T1 = {1, 2, 3}
    • T2 = {(1, x), (1, y), (2, x), (2, y), (3, x), (3, y)} — ৬টি
    • T3 = {(2, y)} — 2-এর y বাকি
    • T4 = {2}; R ÷ S = {1, 3}. 3-এর z ভাগশেষ।
    • S = {x} হলে T3 = ∅, তাই ফল {1, 2, 3} — সবাই x নিয়েছে। S = ∅ হলেও ফল {1, 2, 3} = πA(R) (vacuous truth).
  2. Algebra থেকে SQL: এই রাশিটি কী প্রশ্নের উত্তর দেয়, আর SQL-এ কী হবে? πname(students ⋈ (πstudent_id(enrollments) − πstudent_id(σgrade = 'F'(enrollments)))). db-uni-তে কয়জন আসবেন?

    প্রশ্ন: “যে শিক্ষার্থীরা অন্তত একটি কোর্সে ভর্তি, কিন্তু কোনো কোর্সে F পাননি — তাঁদের নাম।” ভেতরের − একটি anti join-এর কাজ করে।

    SELECT s.name FROM students s
    WHERE EXISTS (SELECT 1 FROM enrollments e WHERE e.student_id = s.student_id)
      AND NOT EXISTS (SELECT 1 FROM enrollments e
                      WHERE e.student_id = s.student_id AND e.grade = 'F');

    ভর্তি আছে ৭ জনের; F পেয়েছেন শুধু Fahim Karim (2203, EEE101). তাই ৬ জন: Arif Hossain, Bristy Saha, Chayon Roy, Dilruba Khatun, Emon Mia, Gulshan Ara. grade NULL (Dilruba-র CSE301) F নয়, তাই তিনি থাকেন — algebra-য় σgrade = 'F' NULL-কে বাছে না।

  3. কোয়েরি লিখুন (“সবার জন্য”): db-uni-তে সেই বিভাগগুলো খুঁজুন, যার প্রতিটি শিক্ষার্থীর cgpa 3.0 বা বেশি। cgpa NULL হলে সেই শিক্ষার্থী শর্ত পূরণ করেননি ধরুন। কোন বিভাগগুলো আসবে — আর একটি বিভাগ আসা অবাক করবে কেন?
    SELECT d.name FROM departments d
    WHERE NOT EXISTS (SELECT 1 FROM students s
                      WHERE s.dept_id = d.dept_id
                        AND (s.cgpa < 3.0 OR s.cgpa IS NULL))
    ORDER BY d.dept_id;

    ফল: CSE, BBA, English. EEE বাদ, কারণ Chayon Roy-র 2.95 (আর Fahim Karim-এর NULL). English-এ একজনও শিক্ষার্থী নেই — “এমন কোনো শিক্ষার্থী নেই যিনি শর্ত ভাঙেন” তাই সত্য। এটি অংশ ৫.৫-এর খালি সেটের ব্যাপার; English বাদ দিতে চাইলে AND EXISTS (SELECT 1 FROM students s WHERE s.dept_id = d.dept_id) যোগ করুন। OR s.cgpa IS NULL না লিখলে Fahim-এর NULL তুলনায় UNKNOWN হয়ে শর্ত ভাঙা হিসেবে ধরা পড়ত না — তবে EEE তবু Chayon-এর জন্য বাদ পড়ত।

  4. বাগ খুঁজুন: “কোন শিক্ষার্থী CSE101, CSE201 আর CSE301 — তিনটিই নিয়েছেন?” AI লিখেছে: SELECT e.student_id FROM enrollments e JOIN courses c ON c.course_id = e.course_id WHERE c.dept_id = 1 GROUP BY e.student_id HAVING COUNT(*) = 3; db-uni-তে এটি কী দেবে, কেন ভুল, আর দুটি সম্ভাব্য বাগ কী?

    ফল দুটি সারি: 2101 আর 2201. 2201 (Dilruba) সঠিক, কিন্তু 2101 (Arif) ভুল — তাঁর CSE বিভাগের তিনটি ভর্তি CSE201, CSE301 আর CSE302; CSE101 নেই। বাগ এক: ভাজক “তিনটি নির্দিষ্ট কোর্স” নয়, “CSE বিভাগের যেকোনো কোর্স” ধরা হয়েছে। বাগ দুই: COUNT(*), DISTINCT নয় — কেউ CSE201 ফেল করে আবার নিলে সেই কোর্স দুবার গোনা হতো। সঠিক রূপ অংশ ৫.৪-এর: ভাজকের টেবিলের (বা e.course_id IN ('CSE101', 'CSE201', 'CSE301')) সাথে মিলিয়ে HAVING COUNT(DISTINCT e.course_id) = 3, অথবা দুই স্তরের NOT EXISTS. যাচাইয়ের সূত্র: প্রতিটি ফলের শিক্ষার্থীর ভর্তি তালিকা হাতে খুলে তিনটি কোর্সে টিক দিন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমR(X, Y) ÷ S(Y) মৌলিক অপারেটর দিয়ে বের করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. πX(R) দিয়ে সব প্রার্থী বের করা
  2. প্রার্থীদের সাথে S-এর Cartesian product নেওয়া
  3. সেই product থেকে R বিয়োগ করে “বাকি থাকা” জোড়া পাওয়া
  4. বাকি থাকা জোড়াগুলোর X-অংশ নিয়ে অযোগ্য প্রার্থী পাওয়া
  5. সব প্রার্থী থেকে অযোগ্যদের বিয়োগ করা

Relational algebra-য় theta join R ⋈θ S কোনটির সমান?

  • πθ(R × S)
  • σθ(R ∪ S)
  • σθ(R × S)
  • σθ(R − S)

R(A, B, C)-তে ৪টি tuple আর S(D, E)-তে ৫টি tuple; দুটির কোনো attribute-এর নাম এক নয়। R ⋈ S (natural join)-এ কয়টি tuple?

  • ০
  • ২০
  • ৯
  • সংজ্ঞায়িত নয়, ত্রুটি

takes(sid, cid) = {(1, a), (1, b), (2, a), (3, a), (3, b), (3, c)} আর core(cid) = {a, b}. takes ÷ core কী?

  • {1, 2, 3}
  • {1}
  • {3}
  • {1, 3}

R(sid, name)-এ ৫টি tuple, যাদের ২টির S-এ কোনো মিল নেই। R ⟕ S-এ কমপক্ষে কয়টি tuple থাকবে?

  • ৫
  • ৩
  • ২
  • ১০

“কোন শিক্ষার্থী তালিকার প্রতিটি কোর্স নিয়েছেন” — SQL-এ এর সবচেয়ে প্রচলিত লেখা কোনটি?

  • একটি EXISTS subquery
  • কোর্স তালিকার সাথে INNER JOIN
  • দুই স্তরের NOT EXISTS
  • LEFT JOIN … WHERE ডান.key IS NULL

θ0 শর্তে শুধু E1-এর attribute আছে। কোন রূপান্তরটি সবসময় সঠিক?

  • σθ0(E1 ⋈ E2) = E1 ⋈ σθ0(E2)
  • σθ0(E1 ⋈ E2) = σθ0(E1) ⋈ E2
  • σθ0(E1 ⋈ E2) = σθ0(E1) ∪ E2
  • একই নিয়ম E1 ⟖ E2-এও সবসময় সঠিক

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. R ⋈θ S = σθ(R × S); শুধু সমতা হলে equi join; natural join = একই নামের সব attribute-এ সমতা, তারপর π দিয়ে duplicate কলাম বাদ। কোনো সাধারণ attribute না থাকলে ⋈ = ×.
  2. Natural join বিনিময়যোগ্য ও সংযোজনযোগ্য (t ∈ R ⋈ S ⇔ t[A] ∈ R আর t[B] ∈ S); outer join নয় — তার ক্রম বদলালে NULL-সারি হারাতে পারে।
  3. ⟕ = ⋈ ∪ (R ▷ S × NULL); ⋉ = πR(R ⋈ S) = EXISTS; ▷ = R − (R ⋉ S) = NOT EXISTS.
  4. R ÷ S = πX(R) − πX((πX(R) × S) − R) — “সবগুলো”; SQL-এ দুই স্তরের NOT EXISTS বা HAVING COUNT(DISTINCT) = মোট; খালি ভাজকে উত্তর আলাদা।
  5. Extended algebra: γ (GROUP BY), generalized projection, outer join; SQL relationally complete আর তার চেয়েও বেশি। query tree-তে σ আর π নিচে ঠেলুন, join-এর ক্রম বাছে optimizer.

পরের পর্বে: algebra বলে “কীভাবে” হিসাব করতে হবে — অপারেটরের একটি ক্রম। পর্ব ৩৪-এ দেখবেন আরেকটি ভাষা, relational calculus, যা শুধু বলে “কী” চাই — tuple আর domain calculus, ∃ আর ∀ সরাসরি, আর কেন দুটি ভাষা একই শক্তির।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান