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

UNION, INTERSECT আর EXCEPT

SQL Set Operations — UNION vs UNION ALL, INTERSECT, EXCEPT (MINUS), NULLs, Precedence and Reconciliation

Taught in Bangla, this lesson covers SQL set operations: union, intersection and difference from set theory, UNION versus UNION ALL and the cost of removing duplicates, union-compatibility rules, ORDER BY and LIMIT on the combined result, INTERSECT and EXCEPT (MINUS), how they differ from semi and anti joins, how NULLs compare in set operations, precedence and data reconciliation.

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

  • Set তত্ত্বের union, intersection আর difference — আর কেন এখানে Venn diagram একদম ঠিক ছবি
  • UNION বনাম UNION ALL — ফল, খরচ, আর কোনটি কখন
  • Union-compatibility, কলামের নাম, ORDER BY / LIMIT আর বিভিন্ন DBMS-এর বন্ধনীর নিয়ম
  • INTERSECT, EXCEPT, তাদের ALL রূপ, NULL-এর আচরণ আর semi join / anti join-এর সাথে মিল-অমিল
  • বাস্তব কাজ: লোকের তালিকা জোড়া, CSV আমদানি মিলিয়ে দেখা, দুই snapshot-এর পার্থক্য আর মোট-সারির রিপোর্ট

১ · Set-এর পুরোনো হিসাব — union, intersection আর difference

ধরুন একটি বিয়ের দাওয়াতের তালিকা বানাচ্ছেন। বরের পরিবার একটি তালিকা দিল, কনের পরিবার আরেকটি। এখন তিনটি প্রশ্ন ওঠে। এক, মোট কাকে কাকে দাওয়াত দিতে হবে — দুই তালিকা মিলিয়ে, কিন্তু যিনি দুই তালিকাতেই আছেন তাঁকে একবারই? দুই, কারা দুই পরিবারেরই পরিচিত? তিন, কারা শুধু বরপক্ষের তালিকায়, কনেপক্ষের তালিকায় নেই? এই তিন প্রশ্নই গণিতের তিনটি পুরোনো অপারেশন।

সংজ্ঞা

Union A ∪ B = যা A-তে অথবা B-তে আছে। Intersection A ∩ B = যা A-তে এবং B-তে আছে। Difference A − B = যা A-তে আছে কিন্তু B-তে নেই।

পর্ব ৫-এ দেখেছেন, relational model-এ একটি relation হলো tuple-এর একটি set. তাই দুটি relation-এর ওপর এই তিন অপারেশন সরাসরি চলে, শুধু একটি শর্তে: দুই পাশের সারি একই “আকারের” হতে হবে। SQL-এ এদের নাম UNION, INTERSECT আর EXCEPT. দুটি SELECT-এর মাঝখানে এদের একটি বসালে যে পুরো কোয়েরি হয়, তাকে বলে compound SELECT, আর অপারেটর তিনটিকে একসাথে বলে set operation.

তিনটি set অপারেশন — রং করা অংশটি ফল

A = বাঁয়ের SELECT-এর সারি, B = ডানের SELECT-এর সারি

AB UNIONA ∪ B AB INTERSECTA ∩ B AB EXCEPTA − B
দুই বৃত্তের সাধারণ অংশ মানে “যে সারি হুবহু দুই পাশেই আছে” — পুরো সারি মিলিয়ে।

JOIN-এ Venn ছবি ভুল, এখানে ঠিক। পর্ব ১৯-এ JOIN বোঝাতে দুই বৃত্তের ছবি এড়ানো হয়েছে, কারণ join দুই টেবিলের সারি জোড়া লাগিয়ে নতুন, চওড়া সারি বানায়; একটি সারি অনেকগুলোর সাথে মিলতে পারে। Set অপারেশন কিছু জোড়া লাগায় না। দুই পাশের সারি একই আকারের, আর প্রতিটি সারি হয় “ভেতরে”, নয়তো “বাইরে”। বৃত্তের ছবি ঠিক এই কথাটিই বলে।

দুটি পার্থক্য মনে রাখুন। প্রথমত, ∪ আর ∩ commutative: A ∪ B = B ∪ A. কিন্তু difference নয়: A − B আর B − A সাধারণত আলাদা। দ্বিতীয়ত, SQL-এর টেবিল আসলে খাঁটি set নয়, একটি multiset — একই সারি একাধিকবার থাকতে পারে। তাই SQL-এ প্রতিটি অপারেটরের দুটি রূপ আছে: set রূপ (duplicate বাদ) আর ALL রূপ (গুনে গুনে রাখা)। পরের অংশে এই পার্থক্য দিয়েই শুরু।

এই পর্বের লাইভ SQL চলে দুটি নমুনা ডেটাবেসে। db-shop একটি দোকান: customers (customer_id, name, city, phone, joined_on — ৮ জন, Imran-এর city NULL), products (product_id, name, category_id, price, stock — ১০টি পণ্য), categories, orders (order_id, customer_id, order_date, status — সেপ্টেম্বর ও অক্টোবর ২০২৫-এর ১০টি অর্ডার) আর order_items (order_id, product_id, quantity, unit_price). db-uni একটি বিশ্ববিদ্যালয়: departments, instructors (instructor_id, name, dept_id, salary, mentor_id), students (student_id, name, dept_id, batch, cgpa), courses আর enrollments (student_id, course_id, term, grade). কিছু এডিটর নিজের ছোট টেবিলও বানায়; সেগুলো “নমুনা ডেটা” বাক্সে দেখা যাবে।

২ · UNION আর UNION ALL — duplicate মোছার দাম

দোকানটি একটি কুরিয়ার সার্ভিসের সাথে চুক্তি করতে চায়। কুরিয়ার যেসব শহরে ডেলিভারি দেয়, তার একটি তালিকা এল। এই এডিটরে db-shop-এর সাথে একটি ছোট টেবিল যোগ হয়েছে: courier_cities(city) — Dhaka, Chattogram, Sylhet, Barishal, Rangpur. প্রশ্ন: আমাদের গ্রাহকের শহর আর কুরিয়ারের শহর মিলিয়ে মোট কোন কোন শহরের নাম আসে?

customers.city (৮) আর courier_cities.city (৫)লাল = এই ধাপে যা হচ্ছে

            
হিসাব
যে অপারেটর
ফলে সারি
বাদ পড়ল
  1. দুটি তালিকা। বাঁয়ে ৮ জন গ্রাহকের শহর — Dhaka তিনবার, আর Imran-এর শহর NULL. ডানে কুরিয়ারের ৫টি শহর। দুই পাশেই একটি কলাম (city), টাইপও এক — তাই জোড়া যায়।
  2. UNION ALL: শুধু পরপর বসানো। DBMS প্রথমে বাঁয়ের ৮টি সারি দেয়, তারপর ডানের ৫টি। কোনো তুলনা নেই, কোনো খাতা রাখতে হয় না। ফল ১৩টি সারি, Dhaka পাঁচবার। প্রথম সারিটি সঙ্গে সঙ্গেই পাঠানো যায়।
  3. UNION: প্রতিটি সারি যাচাই। এবার DBMS একটি “দেখা হয়েছে” খাতা রাখে। বাঁয়ের ৮টি সারির মধ্যে দুটি Dhaka বাদ পড়ল, কারণ খাতায় আগেই আছে। খেয়াল করুন, একই পাশের ভেতরের duplicate-ও বাদ যায়। খাতায় এখন ৬টি শহর, NULL-সহ।
  4. ডানের সারিও একই খাতায়। Dhaka, Chattogram আর Sylhet আগেই আছে, তাই বাদ। Barishal আর Rangpur নতুন। খাতায় এখন ৮টি শহর। কোন পাশ থেকে এসেছে, তাতে কিছু যায় আসে না।
  5. ফল আর দাম। UNION দিল ৮টি আলাদা শহর, UNION ALL দিল ১৩টি সারি। SQLite-এ ফল বর্ণানুক্রমে এসেছে (NULL সবার আগে), কারণ সে খাতাটি একটি সাজানো B-tree হিসেবে রাখে। এই ক্রমের ওপর ভরসা করবেন না; ক্রম চাইলে ORDER BY লিখুন (অংশ ৪)।
-- duplicate রেখে: শুধু পরপর বসানো
SELECT city FROM customers
UNION ALL
SELECT city FROM courier_cities;

-- duplicate বাদ দিয়ে: আলাদা আলাদা শহর
SELECT city FROM customers
UNION
SELECT city FROM courier_cities;

প্রথম ফলে ১৩টি সারি: আগে গ্রাহকদের ৮টি শহর তাদের নিজের ক্রমে (Dhaka, Chattogram, Dhaka, Rajshahi, Khulna, Sylhet, NULL, Dhaka), তারপর কুরিয়ারের ৫টি। দ্বিতীয় ফলে ৮টি সারি: NULL, Barishal, Chattogram, Dhaka, Khulna, Rajshahi, Rangpur, Sylhet. Imran-এর NULL শহরটি একটি সারি হিসেবে টিকে আছে। একাধিক NULL থাকলেও UNION সেগুলোকে একটিতে নামিয়ে আনত (অংশ ৭)।

কেন UNION ALL প্রায়ই ঠিক পছন্দ

Duplicate মুছতে DBMS-কে প্রতিটি সারি আগের সব সারির সাথে মেলাতে হয়। এর দুটি প্রচলিত উপায় আছে। প্রথমটি sort: সব সারি সাজালে একই সারিগুলো পাশাপাশি আসে, তখন এক নজরে বাদ দেওয়া যায় — খরচ O(N log N), আর মেমরিতে না ধরলে ডিস্কে ভাগ করে সাজাতে হয়। দ্বিতীয়টি hash: প্রতিটি সারির hash দিয়ে একটি hash table-এ খোঁজা — গড়ে O(N), কিন্তু যতগুলো আলাদা সারি, ততটা মেমরি লাগে। UNION ALL-এর খরচ শুধু দুই পাশ পড়া, O(N), আর বাড়তি মেমরি প্রায় শূন্য। সে প্রথম সারিটিও সাথে সাথে পাঠাতে পারে, অথচ UNION-কে অনেক সময় সব পড়া শেষ হওয়া পর্যন্ত অপেক্ষা করতে হয়।

UNION ALL লিখুন যখন

দুই পাশে duplicate হওয়া অসম্ভব (যেমন একটি পাশে শুধু 2024 সালের, আরেক পাশে শুধু 2025 সালের অর্ডার), অথবা duplicate-এর গোনা দরকার — মোট বিক্রি, মোট লেনদেন।

দ্রুত · সঠিক গোনা

UNION লিখুন যখন

প্রশ্নটিই “আলাদা আলাদা কী কী?” — কোন কোন শহর, কোন কোন ফোন নম্বর। তখন duplicate মোছা উত্তরের অংশ, বাড়তি খরচ নয়।

আলাদা মানের তালিকা

UNION চুপচাপ টাকা কমিয়ে দিতে পারে। দুই শাখার বিক্রি জোড়া লাগাচ্ছেন: SELECT amount FROM sales_dhaka UNION SELECT amount FROM sales_ctg. দুই শাখায় দুটি আলাদা ৫০০ টাকার বিক্রি থাকলে UNION সেগুলোকে একটি বানিয়ে দেবে, আর মোট বিক্রি কমে যাবে। কোনো ত্রুটি আসবে না। গোনা বা যোগের আগে সবসময় UNION ALL লিখুন।

৩ · Union-compatibility — কোন দুটি SELECT জোড়া যায়

দুটি তালিকা মেলাতে হলে দুই পাশের সারির আকার এক হতে হবে। গণিতে একে বলে union-compatible. SQL-এ নিয়মগুলো এরকম:

  • কলামের সংখ্যা সমান। বাঁয়ে ২টি কলাম হলে ডানেও ঠিক ২টি।
  • অবস্থান ধরে মেলে, নাম ধরে নয়। বাঁয়ের প্রথম কলাম ডানের প্রথম কলামের সাথে, দ্বিতীয়টি দ্বিতীয়টির সাথে। নাম আলাদা হলে কোনো সমস্যা নেই, ক্রম উল্টে গেলে বড় সমস্যা।
  • প্রতিটি জোড়ার টাইপ মানানসই। INTEGER আর NUMERIC মেলে, TEXT আর DATE সাধারণত মেলে না। PostgreSQL আর SQL Server-এর মতো কঠোর DBMS এখানে ত্রুটি দেয়।
  • ফলের কলামের নাম আসে প্রথম SELECT থেকে। তাই alias (AS) প্রথম SELECT-এ লিখুন।
-- নাম আসে প্রথম SELECT থেকে; SQLite টাইপও মিলিয়ে দেখে না
SELECT name AS item, price FROM products WHERE price > 4000
UNION ALL
SELECT name, city FROM customers WHERE city = 'Sylhet';

-- বাঁয়ে ২টি কলাম, ডানে ১টি: ত্রুটি
SELECT name, city FROM customers
UNION
SELECT name FROM products;

প্রথম ফলের কলামের নাম item আর price — দ্বিতীয় SELECT-এর name, city নাম কোথাও নেই। সারি তিনটি: Mobile Phone | 15000, Saree | 4500 আর Farhana Akter | Sylhet. অর্থাৎ “price” কলামে একটি শহরের নাম ঢুকে গেছে! SQLite প্রতিটি ঘরের টাইপ আলাদাভাবে রাখে (পর্ব ১৩-এর type affinity), তাই আপত্তি করেনি। এটি SQLite-এর নিজস্ব শিথিলতা। দ্বিতীয় কোয়েরিতে ত্রুটি: SELECTs to the left and right of UNION do not have the same number of result columns. কলামের সংখ্যার নিয়ম সব DBMS-এ বাধ্যতামূলক।

একই প্রথম কোয়েরি PostgreSQL-এ চালালে কী হতো? সেখানে প্রতিটি কলামের একটিই টাইপ, তাই সংখ্যা আর লেখা এক কলামে বসতে পারে না। (নিচের ব্লক চালানো যায় না, শুধু তুলনার জন্য।)

SELECT name AS item, price FROM products WHERE price > 4000
UNION ALL
SELECT name, city FROM customers WHERE city = 'Sylhet';
-- ERROR:  UNION types numeric and text cannot be matched

-- সমাধান: ইচ্ছে করে একই টাইপে আনুন
SELECT name AS item, price::text AS info FROM products WHERE price > 4000
UNION ALL
SELECT name, city FROM customers WHERE city = 'Sylhet';

দুটি ব্যবহারিক কৌশল: কোনো পাশে একটি কলাম না থাকলে তার জায়গায় NULL বা একটি ধ্রুব মান বসান, যেমন SELECT name, NULL AS phone FROM instructors. আর কোনো পাশে সংখ্যা ও লেখা মেশাতে হলে CAST দিয়ে নিজেই রূপান্তর করুন, যাতে আচরণ সব DBMS-এ এক থাকে।

৪ · ORDER BY আর LIMIT — পুরো ফলের ওপর

Compound SELECT-এর শেষে লেখা ORDER BY আর LIMIT শেষ SELECT-এর নয়, পুরো ফলের। তাই ORDER BY-তে ফলের কলামের নাম (প্রথম SELECT-এর নাম) বা অবস্থান (ORDER BY 2) লিখতে হয়। SQLite-এ ফলে নেই এমন কলাম দিয়ে সাজাতে চাইলে ত্রুটি আসে: 1st ORDER BY term does not match any column in the result set. আবার মাঝের কোনো SELECT-এ ORDER BY লিখলে আসে ORDER BY clause should come after UNION not before.

এটি বোঝার জন্য একটি বাস্তব কাজ নিন: বিশ্ববিদ্যালয়ের একটি “লোকের তালিকা” (people directory), যেখানে শিক্ষার্থী আর শিক্ষক একসাথে, আর প্রতিটি সারিতে একটি লেবেল থাকবে, সে কোন তালিকা থেকে এসেছে। লেবেলটি একটি ধ্রুব মান, 'student' বা 'instructor'. এই কৌশলের নাম source label.

-- লোকের তালিকা: কে কোন তালিকা থেকে এসেছেন, লেবেলসহ
SELECT name, 'student' AS role, dept_id FROM students
UNION ALL
SELECT name, 'instructor', dept_id FROM instructors
ORDER BY dept_id, role, name;

-- প্রতিটি অংশের নিজের সীমা চাইলে: subquery-র ভেতরে
SELECT * FROM (SELECT name, 'student' AS role FROM students
               ORDER BY cgpa DESC LIMIT 1)
UNION ALL
SELECT * FROM (SELECT name, 'instructor' FROM instructors
               ORDER BY salary DESC LIMIT 1);

প্রথম ফলে ১৪টি সারি (৮ জন শিক্ষার্থী + ৬ জন শিক্ষক)। সবার আগে Habib Ullah | student | NULL — SQLite ছোট থেকে বড় সাজানোর সময় NULL-কে সবার আগে রাখে। তারপর dept 1: আগে তিনজন instructor (Dr. Anisur Rahman, Shahana Parvin, Tasnim Chowdhury), তারপর চারজন student (Arif, Bristy, Dilruba, Gulshan), কারণ বর্ণক্রমে 'instructor' আগে আসে। ORDER BY-র role নামটি শুধু প্রথম SELECT-এ দেওয়া হয়েছে, তবু পুরো ফলে কাজ করছে। দ্বিতীয় ফলে দুটি সারি: Dilruba Khatun | student (cgpa 3.90) আর Dr. Anisur Rahman | instructor (বেতন 120000). এখানে প্রতিটি অংশের ORDER BY আর LIMIT subquery-র ভেতরে বন্দি, তাই অংশ দুটি আলাদাভাবে সাজানো হয়েছে। cgpa বা salary ফলে না থাকলেও সাজানো গেছে, কারণ সেটি subquery-র নিজের ORDER BY.

প্রতিটি অংশে আলাদা ORDER BY / LIMIT লেখার নিয়ম DBMS ভেদে আলাদা। SQLite-এ একটি SELECT-কে সরাসরি বন্ধনীতে রাখা যায় না: (SELECT …) UNION (SELECT …) লিখলে near "(": syntax error আসে, তাই উপরের মতো SELECT * FROM (…) লিখতে হয়। PostgreSQL আর MySQL বন্ধনী মানে:

(SELECT name, 'student' AS role FROM students ORDER BY cgpa DESC LIMIT 1)
UNION ALL
(SELECT name, 'instructor' FROM instructors ORDER BY salary DESC LIMIT 1)
ORDER BY name;          -- এটি আবার পুরো ফলের ওপর

SQL Server আর Oracle-এ ORDER BY শুধু একেবারে শেষে বসে; সেখানে প্রতিটি অংশের সীমার জন্য SQLite-এর মতোই derived table (FROM-এর ভেতরের subquery) লাগে, সাথে SQL Server-এ TOP আর Oracle-এ FETCH FIRST.

৫ · INTERSECT আর EXCEPT — “দুটিতেই” আর “শুধু প্রথমটিতে”

INTERSECT সেই সারিগুলো দেয়, যা দুই পাশেই আছে। EXCEPT দেয় সেই সারিগুলো, যা বাঁয়ে আছে কিন্তু ডানে নেই। Oracle এর নাম রেখেছে MINUS; Oracle 21c থেকে EXCEPT শব্দটিও চলে। দুটিই default-এ duplicate বাদ দেয়, ঠিক UNION-এর মতো। আগের কুরিয়ারের শহর দিয়েই দেখা যাক:

-- যেসব শহরে গ্রাহক আছেন আর কুরিয়ারও যায়
SELECT city FROM customers
INTERSECT
SELECT city FROM courier_cities;

-- গ্রাহক আছেন, কিন্তু কুরিয়ার যায় না: অন্য ব্যবস্থা লাগবে
SELECT city FROM customers
EXCEPT
SELECT city FROM courier_cities;

INTERSECT-এর ফল তিনটি শহর: Chattogram, Dhaka, Sylhet. Dhaka গ্রাহকদের মধ্যে তিনবার থাকলেও একবারই এসেছে। EXCEPT-এর ফলও তিনটি: NULL, Khulna, Rajshahi. NULL-টি Imran-এর। কুরিয়ারের তালিকায় কোনো NULL নেই, তাই EXCEPT একে “কুরিয়ার যায় না” দলে ফেলেছে। ব্যবসার দিক থেকে এটি আসলে একটি সতর্কবার্তা: এই গ্রাহকের শহরই জানা নেই। আর উল্টো দিকে লিখলে (courier_cities EXCEPT customers) আসত Barishal আর Rangpur — যে শহরে কুরিয়ার যায় কিন্তু এখনো গ্রাহক নেই। EXCEPT-এ ক্রম বদলালে প্রশ্নও বদলায়।

এবার একটি ব্যবসায়িক প্রশ্ন: কোন পণ্য সেপ্টেম্বরে বিক্রি হয়েছিল, কিন্তু অক্টোবরে একটিও না? দুই মাসের পণ্যের তালিকা দুটি SELECT, মাঝে EXCEPT. নিচের ডায়াগ্রামে DBMS কীভাবে সারি ধরে ধরে মেলায়, দেখুন:

order_items-এর product_id, মাস অনুযায়ীলাল = এই ধাপে যা দেখা হচ্ছে

            
অবস্থা
বাঁয়ে বাকি
ফলে এ পর্যন্ত
  1. দুটি তালিকা। সেপ্টেম্বরের ৬টি অর্ডারে ১০টি লাইন, অক্টোবরের ৪টি অর্ডারে ৬টি লাইন। প্রতিটি লাইন থেকে শুধু product_id নেওয়া হয়েছে। 104 (DSA Book) সেপ্টেম্বরে দুবার এসেছে।
  2. বাঁ পাশের duplicate বাদ। EXCEPT set রূপে চলে, তাই আগে বাঁয়ের তালিকা আলাদা আলাদা মানে নামে: দ্বিতীয় 104 বাদ, থাকে ৯টি। ফলেও তাই কোনো সারি দুবার আসবে না।
  3. ডান পাশ থেকে খোঁজার তালিকা। অক্টোবরের ৬টি product_id দিয়ে DBMS একটি hash table বা সাজানো তালিকা বানায়, যাতে “এটি আছে কি?” প্রশ্নের উত্তর দ্রুত মেলে।
  4. প্রথম পাঁচটি খোঁজা। 101, 102 আর 105 অক্টোবরেও বিক্রি হয়েছে, তাই বাদ। 104 আর 106 অক্টোবরে নেই, তাই ফলে যায়। পুরো সারি মেলানো হচ্ছে; এখানে সারিতে একটিই কলাম।
  5. বাকি চারটি। 107 (Saree) অক্টোবরে নেই, তাই থাকে। 108, 109 আর 110 অক্টোবরে আছে, তাই বাদ। বাঁয়ের ৯টি মানের প্রতিটি ঠিক একবার খোঁজা হলো।
  6. ফল, আর উল্টো দিক। ফল তিনটি পণ্য: DSA Book, Panjabi, Saree. উল্টো দিকে, অর্থাৎ অক্টোবর থেকে সেপ্টেম্বর বাদ দিলে, কিছুই থাকে না: অক্টোবরের প্রতিটি পণ্য সেপ্টেম্বরেও বিক্রি হয়েছিল। তাই EXCEPT-এ কোন তালিকা বাঁয়ে, সেটিই প্রশ্ন ঠিক করে।
-- সেপ্টেম্বরে বিক্রি হয়েছে, অক্টোবরে হয়নি: কোন পণ্য?
SELECT p.product_id, p.name FROM products p
WHERE p.product_id IN (
  SELECT oi.product_id FROM order_items oi JOIN orders o USING (order_id)
  WHERE o.order_date LIKE '2025-09-%'
  EXCEPT
  SELECT oi.product_id FROM order_items oi JOIN orders o USING (order_id)
  WHERE o.order_date LIKE '2025-10-%');

ফল তিনটি সারি: 104 | DSA Book, 106 | Panjabi, 107 | Saree — ঠিক ডায়াগ্রামের মতো। EXCEPT-টি একটি subquery-র ভেতরে বসেছে (পর্ব ২১), কারণ EXCEPT শুধু product_id-তে চালাতে চাই: দুই পাশে নামও রাখলে পুরো সারি তুলনা হতো। ফল একটি id-র তালিকা, আর বাইরের SELECT সেই id দিয়ে পণ্যের নাম আনে। একটি সতর্কতা: Saree-র একমাত্র অর্ডার 1004 বাতিল (cancelled) হয়েছিল। “বিক্রি” মানে যদি সফল বিক্রি হয়, তবে দুই পাশেই o.status <> 'cancelled' যোগ করতে হবে। Set অপারেশন প্রশ্নের এই খুঁটিনাটি নিজে বোঝে না।

ALL রূপ — INTERSECT ALL আর EXCEPT ALL

Duplicate রেখে চলতে চাইলে standard SQL তিনটিরই ALL রূপ দেয়। ধরুন একটি সারি বাঁয়ে m বার আর ডানে n বার আছে। তাহলে ফলে সেটি কতবার আসবে:

অপারেটরফলে কতবারm = 3, n = 1 হলে
UNIONm + n > 0 হলে ১ বার১
UNION ALLm + n৪
INTERSECTm > 0 আর n > 0 হলে ১ বার১
INTERSECT ALLmin(m, n)১
EXCEPTm > 0 আর n = 0 হলে ১ বার০
EXCEPT ALLmax(m − n, 0)২

EXCEPT ALL একটি খাতা মেলানোর মতো: বাঁয়ে তিনটি ৫০০ টাকার এন্ট্রি, ডানে একটি — দুটি এন্ট্রি মেলেনি। সাধারণ EXCEPT এখানে বলত “সব মিলেছে”, কারণ সে শুধু দেখে মানটি ডানে আছে কি না। PostgreSQL, MySQL 8.0.31+ আর Oracle 21c+ ALL রূপগুলো মানে। কিন্তু SQLite আর SQL Server-এ INTERSECT ALL / EXCEPT ALL নেই; SQLite-এ লিখলে near "ALL": syntax error আসে। সেখানে প্রতিটি মানের সংখ্যা GROUP BY দিয়ে গুনে তুলনা করতে হয় (অনুশীলন ৪)।

-- বাঁয়ে (500, 500, 500, 200), ডানে (500, 200)
SELECT amount FROM bank_statement
EXCEPT ALL
SELECT amount FROM our_ledger;
-- ফল: 500, 500   (তিনটি 500-এর একটি মিলেছে, দুটি মেলেনি)

ইতিহাস: ১৮৮০ সালে ব্রিটিশ যুক্তিবিদ John Venn যুক্তির বাক্যকে বৃত্ত দিয়ে আঁকার যে পদ্ধতি প্রকাশ করেন, সেটিই আজকের Venn diagram. ১৯৭০ সালে E. F. Codd relational model প্রস্তাব করেন, আর ১৯৭২ সালে তাঁর “Relational Completeness” প্রবন্ধে relational algebra-র মূল অপারেশনের মধ্যে union, intersection আর difference-ও ছিল (পর্ব ৩২)। SQL-এর প্রথম standard (SQL-86) শুধু UNION রাখে; INTERSECT আর EXCEPT আসে SQL-92-এ। Oracle অনেক আগে থেকেই একই কাজ MINUS নামে দিত, তাই নামটি আজও টিকে আছে। MySQL দীর্ঘদিন শুধু UNION দিত; INTERSECT আর EXCEPT যোগ হয় MySQL 8.0.31-এ, ২০২২ সালে। তার আগে MySQL ব্যবহারকারীরা একই কাজ JOIN, IN বা NOT EXISTS দিয়ে করতেন — যার ফাঁদগুলো পরের অংশে দেখবেন।

৬ · INTERSECT ≈ semi join, EXCEPT ≈ anti join — কোথায় আলাদা

পর্ব ২০-এ দেখেছেন semi join (“বাঁয়ের যেসব সারির ডানে অন্তত একটি মিল আছে”) আর anti join (“বাঁয়ের যেসব সারির ডানে কোনো মিল নেই”)। পর্ব ২১-এ দেখেছেন, এদের লেখার স্বাভাবিক উপায় EXISTS আর NOT EXISTS. কথাগুলো INTERSECT আর EXCEPT-এর খুব কাছাকাছি:

A INTERSECT B ≈ A-র সারি, WHERE EXISTS (B-তে মিল)
A EXCEPT B ≈ A-র সারি, WHERE NOT EXISTS (B-তে মিল)

“≈” চিহ্নটি ইচ্ছে করে দেওয়া। দুটি জায়গায় এরা আলাদা। এক, duplicate: INTERSECT আর EXCEPT ফল থেকে duplicate বাদ দেয়, semi join বাঁয়ের প্রতিটি মেলা সারি যতবার আছে ততবার রাখে। দুই, NULL: EXISTS-এর ভেতরে সাধারণত = দিয়ে মেলানো হয়, যা NULL-কে কখনো মেলায় না; set অপারেশন দুটি NULL-কে মেলায় (পরের অংশ)। তাই সঠিক সমতুল্য রূপ এরকম:

-- A(x, y) INTERSECT B(x, y)-এর হুবহু সমতুল্য
SELECT DISTINCT a.x, a.y FROM A a
WHERE EXISTS (SELECT 1 FROM B b
              WHERE b.x IS NOT DISTINCT FROM a.x
                AND b.y IS NOT DISTINCT FROM a.y);
-- EXCEPT হলে: EXISTS-এর জায়গায় NOT EXISTS

কেন এটি ঠিক — ছোট একটি যুক্তি। ধরা যাক t একটি সারি। t ফলে আসবে ⇔ t বাঁয়ে আছে আর ডানে এমন একটি সারি আছে, যা প্রতিটি কলামে t থেকে “not distinct”. INTERSECT-এর সংজ্ঞা ঠিক এটিই, কারণ standard দুটি সারিকে duplicate ধরে যখন প্রতিটি কলামে তারা not distinct. DISTINCT ফলের প্রতিটি সারিকে একবার রাখে, যা set রূপের সাথে মেলে। EXCEPT-এর জন্য শর্তটি উল্টো (“এমন সারি নেই”), তাই NOT EXISTS. ∎ IS NOT DISTINCT FROM (standard; SQLite 3.39+, PostgreSQL) মানে “দুটি সমান, অথবা দুটিই NULL”. SQLite-এ এর ছোট রূপ IS, MySQL-এ <=>.

Duplicate-এর পার্থক্যটি নিজে দেখুন। db-uni-তে Arif (2101) আর Dilruba (2201) দুজনেই কোন কোন কোর্স নিয়েছেন? তারপর প্রশ্নটি একটু বড় করুন: Arif বা Bristy (2102)-র নেওয়া কোন কোর্স Dilruba-ও নিয়েছেন?

-- দুজনেই নিয়েছেন এমন কোর্স
SELECT course_id FROM enrollments WHERE student_id = 2101
INTERSECT
SELECT course_id FROM enrollments WHERE student_id = 2201;

-- একই প্রশ্ন বড় করে: 2101 বা 2102-র কোর্স, যা 2201-ও নিয়েছেন
SELECT course_id FROM enrollments WHERE student_id IN (2101, 2102)
INTERSECT
SELECT course_id FROM enrollments WHERE student_id = 2201;

SELECT e.course_id FROM enrollments e
WHERE e.student_id IN (2101, 2102)
  AND EXISTS (SELECT 1 FROM enrollments x
              WHERE x.student_id = 2201 AND x.course_id = e.course_id);

প্রথম ফল: CSE201 আর CSE301. Arif নিয়েছেন তিনটি (CSE201, CSE301, CSE302), Dilruba তিনটি (CSE101, CSE201, CSE301); মিল দুটিতে। দ্বিতীয় ফলও দুটি সারি: CSE201, CSE301. কিন্তু তৃতীয়টি, অর্থাৎ EXISTS-ওয়ালা semi join, দিল চারটি সারি: CSE201, CSE301, CSE201, CSE301. Arif আর Bristy দুজনেই দুটি কোর্স নিয়েছেন, আর semi join বাঁয়ের প্রতিটি মেলা সারি রেখে দেয়। SELECT DISTINCT e.course_id লিখলে আবার দুটি সারি হতো। কোনটি “ঠিক”, তা প্রশ্নের ওপর নির্ভর করে: “কোন কোর্স?” হলে INTERSECT, “কতজন মিলছে?” গোনার জন্য semi join.

তাহলে কোনটি লিখবেন? INTERSECT/EXCEPT পুরো সারি তুলনা করে, তাই অনেক কলাম মেলাতে খুব সংক্ষিপ্ত, আর NULL নিয়ে নিজে থেকেই ঠিক থাকে। কিন্তু ফলে শুধু তুলনার কলামগুলোই থাকে। বাঁয়ের অন্য কলাম (যেমন নাম, তারিখ) চাইলে EXISTS ভালো, কারণ সেখানে মেলানোর কলাম আর দেখানোর কলাম আলাদা হতে পারে। বড় টেবিলে বেশিরভাগ query optimizer NOT EXISTS-কে সরাসরি hash anti join-এ বদলে নিতে পারে; EXCEPT-এর সাথে duplicate মোছার বাড়তি কাজও থাকে। পর্ব ৪২-এ এই অ্যালগরিদমগুলোর খরচ দেখবেন।

৭ · NULL — set অপারেশনে দুটি NULL “সমান”

পর্ব ১৬-এ দেখেছেন, NULL = NULL-এর ফল TRUE নয়, UNKNOWN; আর WHERE শুধু TRUE সারি রাখে (three-valued logic). কিন্তু duplicate খোঁজার সময় SQL অন্য একটি প্রশ্ন করে: “এই দুটি মান কি আলাদা?” Standard এখানে বলে, দুটি NULL পরস্পর থেকে আলাদা নয় (not distinct). এই নিয়ম মানে DISTINCT, GROUP BY আর সব set অপারেশন। তাই একই NULL দুই দিকে দুই রকম আচরণ পায়।

a(x) আর b(x)লাল = এই ধাপে যা ঘটছে

            
ফল
প্রশ্ন
ফলের সারি
NULL-কে কী ধরা হলো
  1. দুটি ছোট তালিকা। a-তে 1, 2 আর একটি NULL; b-তে 2, একটি NULL আর 3. দুই দিকেই একটি NULL আছে। প্রশ্ন: কোন মান দুই দিকেই আছে?
  2. = দিয়ে মেলানো। JOIN বা EXISTS-এর ভেতরে a.x = b.x প্রতিটি জোড়ায় যাচাই হয়। NULL-ওয়ালা প্রতিটি তুলনা UNKNOWN, এমনকি NULL = NULL-ও। শুধু 2 = 2 সত্য, তাই ফল শুধু 2.
  3. INTERSECT অন্য প্রশ্ন করে। সে জানতে চায় সারিটি ডানের কোনো সারি থেকে “আলাদা নয়” কি না। দুটি NULL পরস্পর থেকে আলাদা নয়, তাই NULL-ও মেলে। ফল 2 আর NULL.
  4. EXCEPT-ও একই নিয়মে। 2 আর NULL দুটিই ডানে “আছে”, তাই বাদ। ফল শুধু 1. = দিয়ে হাতে লেখা anti join (NOT EXISTS … b.x = a.x) এখানে 1 আর NULL দুটিই দিত।
  5. NOT IN-এর ফাঁদ। x NOT IN (2, NULL, 3) মানে x <> 2 AND x <> NULL AND x <> 3. মাঝের তুলনাটি সবসময় UNKNOWN, তাই পুরো শর্ত কখনো TRUE হয় না। ডানে একটি NULL থাকলেই NOT IN কোনো সারি দেয় না। EXCEPT আর NOT EXISTS এই ফাঁদে পড়ে না।
SELECT x FROM a INTERSECT SELECT x FROM b;              -- set অপারেশন
SELECT a.x FROM a JOIN b ON a.x = b.x;                  -- = দিয়ে join
SELECT x FROM a WHERE EXISTS
  (SELECT 1 FROM b WHERE b.x = a.x);                    -- = দিয়ে semi join
SELECT x FROM a WHERE EXISTS
  (SELECT 1 FROM b WHERE b.x IS a.x);                   -- NULL-নিরাপদ তুলনা
SELECT x FROM a EXCEPT SELECT x FROM b;
SELECT x FROM a WHERE x NOT IN (SELECT x FROM b);

ছয়টি ফল, ক্রমানুসারে: INTERSECT দিল দুটি সারি, NULL আর 2. JOIN দিল শুধু 2. = দিয়ে EXISTS-ও শুধু 2. IS দিয়ে EXISTS দিল 2 আর NULL — এবার INTERSECT-এর সমান। EXCEPT দিল শুধু 1. আর NOT IN দিল “(কোনো সারি নেই)”, কারণ b-তে একটি NULL আছে। শিক্ষা: পুরো সারি মেলাতে চাইলে INTERSECT/EXCEPT নিজেই NULL ঠিকভাবে সামলায়; হাতে লেখা তুলনায় IS NOT DISTINCT FROM (SQLite-এ IS) লাগে, আর NULL থাকতে পারে এমন কলামে NOT IN কখনো নয়।

একটি উল্টো উদাহরণ মনে রাখুন: UNIQUE constraint-এ বেশিরভাগ DBMS দুটি NULL-কে আলাদা ধরে, তাই UNIQUE কলামে একাধিক NULL চলে (পর্ব ৬)। অর্থাৎ “NULL কি NULL-এর সমান?” প্রশ্নের উত্তর SQL-এ জায়গাভেদে আলাদা: WHERE-এ অজানা, set অপারেশন আর GROUP BY-তে সমান, UNIQUE-এ সাধারণত আলাদা।

৮ · একাধিক set অপারেশন একসাথে — কে আগে?

A UNION B INTERSECT C — এর মানে কী? গণিতের যোগ-গুণের মতো এখানেও একটি precedence (আগে-পরের নিয়ম) লাগে। SQL standard বলে, INTERSECT আগে বাঁধে (গুণের মতো), UNION আর EXCEPT সমান স্তরে, বাঁ থেকে ডানে। তাই standard-এ এর মানে A UNION (B INTERSECT C). PostgreSQL, SQL Server আর MySQL 8.0.31+ এই নিয়ম মানে। কিন্তু SQLite আর Oracle সব set অপারেটরকে সমান ধরে, বাঁ থেকে ডানে চালায়: (A UNION B) INTERSECT C. একটি ছোট উদাহরণে দেখা যাক: A = {1, 2}, B = {2, 3}, C = {3, 4}.

-- SQLite: বাঁ থেকে ডানে, (a UNION b) INTERSECT c
SELECT n FROM a
UNION
SELECT n FROM b
INTERSECT
SELECT n FROM c;

-- standard-এর অর্থ চাইলে: subquery দিয়ে বন্ধনী
SELECT n FROM a
UNION
SELECT * FROM (SELECT n FROM b INTERSECT SELECT n FROM c);

প্রথম ফল একটিমাত্র সারি: 3. SQLite আগে a UNION b = {1, 2, 3} বানিয়েছে, তারপর c-এর সাথে INTERSECT করে পেয়েছে {3}. দ্বিতীয় ফল তিনটি সারি: 1, 2, 3. এখানে আগে b INTERSECT c = {3}, তারপর a-এর সাথে UNION = {1, 2, 3}. ঠিক এই দ্বিতীয় অর্থটিই PostgreSQL প্রথম কোয়েরি থেকেই দিত। অর্থাৎ একই লেখা SQL দুই DBMS-এ দুই রকম ফল দেয়, আর কোনো ত্রুটি আসে না।

নিরাপদ নিয়ম একটাই: একাধিক ধরনের set অপারেটর মেশালে সবসময় বন্ধনী দিন। PostgreSQL আর MySQL-এ সরাসরি (SELECT …), SQLite-এ SELECT * FROM (…) বা একটি CTE (পর্ব ২৫)। যে কোয়েরি এক DBMS থেকে আরেকটিতে যেতে পারে, সেখানে এটি ঐচ্ছিক নয়।

SELECT n FROM a UNION SELECT n FROM b INTERSECT SELECT n FROM c;
-- ফল: 1, 2, 3   (INTERSECT আগে)
(SELECT n FROM a UNION SELECT n FROM b) INTERSECT SELECT n FROM c;
-- ফল: 3         (বন্ধনী দিয়ে SQLite-এর অর্থ)

৯ · কাজের জগতে set অপারেশন — মেলানো, পার্থক্য আর মোট

CSV আমদানি মিলিয়ে দেখা (reconciliation)

অফিসের হিসাবরক্ষক একটি Excel ফাইল পাঠালেন: “গ্রাহকদের হালনাগাদ তালিকা”। এটি csv_import টেবিলে আমদানি করা হলো, কলাম customers-এর মতোই: customer_id, name, city, phone. মূল (master) টেবিলের সাথে কোথায় অমিল? দুই তালিকা মিলিয়ে অমিল খোঁজাকে বলে reconciliation — ব্যাংক স্টেটমেন্টের সাথে দোকানের খাতা মেলানোর মতো। কৌশলটি হলো EXCEPT দুই দিকে চালানো, প্রতিটির সাথে একটি source label, আর মাঝখানে UNION ALL.

-- CSV-তে আছে কিন্তু master-এ হুবহু নেই, আর উল্টোটা
SELECT 'only_in_csv' AS side, * FROM (
  SELECT * FROM csv_import
  EXCEPT
  SELECT customer_id, name, city, phone FROM customers)
UNION ALL
SELECT 'only_in_master', * FROM (
  SELECT customer_id, name, city, phone FROM customers
  EXCEPT
  SELECT * FROM csv_import)
ORDER BY customer_id, side;

ফল চারটি সারি। only_in_csv | 3 | Sabbir Hasan | Cumilla | … আর only_in_master | 3 | Sabbir Hasan | Dhaka | … — একই গ্রাহক দুই দিকে এসেছেন, অর্থাৎ তাঁর শহর বদলেছে (Dhaka থেকে Cumilla). only_in_master | 7 | Imran Hossain — CSV-তে তিনি নেই; হয় বাদ পড়েছেন, নয়তো ফাইলটি অসম্পূর্ণ। only_in_csv | 9 | Sadia Islam | Barishal — একদম নতুন গ্রাহক। আর Tanvir Ahmed (5)-এর phone দুই দিকেই NULL, তবু তিনি ফলে নেই, কারণ EXCEPT দুটি NULL-কে সমান ধরে। প্রতিটি কলাম = দিয়ে হাতে মেলালে Tanvir ভুল করে “বদলেছে” দলে চলে আসতেন। দুটি EXCEPT-কে subquery-তে রাখা হয়েছে, কারণ SQLite বাঁ থেকে ডানে চলে (অংশ ৮)।

এই একই কৌশল দুটি snapshot-এর পার্থক্য বের করে — যেমন গতকাল রাতের দামের তালিকা আর আজকের, বা একটি migration-এর আগের ও পরের টেবিল। নিয়মটি সরল: A EXCEPT B আর B EXCEPT A দুটিই খালি হলে, দুই টেবিল set হিসেবে হুবহু এক। যদি সারির সংখ্যাও সমান হয় আর দুই পাশে duplicate না থাকে, তাহলে multiset হিসেবেও এক। Migration-এর পর যাচাইয়ের এটি একটি প্রচলিত পরীক্ষা।

রিপোর্টে মোট-সারি (totals row)

শ্রেণি অনুযায়ী বিক্রির রিপোর্টের নিচে একটি “TOTAL” সারি চাই। GROUP BY দিয়ে শ্রেণির সারিগুলো, আরেকটি SELECT দিয়ে মোট, মাঝে UNION ALL. মোট-সারিটি যেন সবসময় শেষে থাকে, সেজন্য একটি সাজানোর কলাম k যোগ করা হয়েছে, যা বাইরের SELECT দেখায় না:

SELECT category, revenue FROM (
  SELECT c.name AS category, SUM(oi.quantity * oi.unit_price) AS revenue, 1 AS k
  FROM order_items oi
  JOIN products p   ON p.product_id = oi.product_id
  JOIN categories c ON c.category_id = p.category_id
  GROUP BY c.name
  UNION ALL
  SELECT 'TOTAL', SUM(quantity * unit_price), 2 FROM order_items
)
ORDER BY k, revenue DESC;

ফল পাঁচটি সারি: Electronics 40050, Clothing 8900, Grocery 4920, Books 2950, আর শেষে TOTAL 56820. যোগ করে মিলিয়ে নিন: 40050 + 8900 + 4920 + 2950 = 56820. k না থাকলে ORDER BY revenue DESC TOTAL-কে সবার ওপরে তুলে দিত। এখানে UNION লিখলে বিপদ ছিল না, কারণ প্রতিটি সারি আলাদা; তবু UNION ALL লেখা হয়েছে, যাতে অকারণ duplicate-যাচাই না হয়। (হিসাবে বাতিল অর্ডার 1004-ও আছে।) PostgreSQL, MySQL, SQL Server আর Oracle-এ একই কাজ GROUP BY ROLLUP (MySQL-এ WITH ROLLUP) দিয়েও হয়; SQLite-এ ROLLUP নেই, তাই UNION ALL-ই সহজ পথ।

আরও গভীরে: PostgreSQL-এর EXPLAIN-এ UNION সাধারণত দেখায় Append-এর ওপর একটি HashAggregate (বা Sort + Unique); UNION ALL শুধু Append. INTERSECT আর EXCEPT চলে HashSetOp বা সাজানো ইনপুটের ওপর SetOp দিয়ে: প্রতিটি আলাদা সারির জন্য দুটি গণনা রাখা হয় — কতবার বাঁ থেকে এল (m) আর কতবার ডান থেকে (n) — আর শেষে টেবিলের নিয়মে (min(m, n), max(m − n, 0) …) কতবার দেখাবে ঠিক হয়। SQLite duplicate বাদ দেয় একটি অস্থায়ী (ephemeral) B-tree index দিয়ে, যার প্রতিটি ঢোকানো আর খোঁজা O(log N); তাই N সারিতে মোট O(N log N). Optimizer অনেক সময় UNION-এর এক পাশে PRIMARY KEY বা UNIQUE দেখেও duplicate-যাচাই বাদ দিতে পারে না, কারণ দুই পাশ মিলে duplicate হতে পারে — তাই এই সিদ্ধান্ত লেখকেরই নেওয়া উচিত।

১০ · AI-এর যুগে set অপারেশন

“দুই টেবিল মিলিয়ে দেখাও কোন গ্রাহক নতুন” — এমন অনুরোধে AI সাধারণত এমন কোয়েরি দেয় যা চলে, ফলও দেয়। ভুলগুলো ধরা পড়ে না, কারণ ত্রুটি আসে না। এই বিষয়ে AI-এর পরিচিত ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
যোগ বা গোনার আগে UNIONসমান মানের সারি হারায়, মোট কমেদুই পাশের COUNT(*) যোগ করে ফলের সাথে মেলান
EXCEPT-এর বদলে NOT INডানে একটি NULL থাকলেই ফল খালিডানের কলামে NULL আছে কি না দেখুন
হাতে a.col = b.col দিয়ে diffNULL-ওয়ালা অপরিবর্তিত সারি “বদলেছে” দেখায়NULL-সহ একটি সারি দিয়ে পরীক্ষা করুন
দুই পাশে কলামের ক্রম আলাদানাম এক কলামে, শহর আরেকটিতে মিশে যায়প্রথম কয়েকটি সারি চোখে দেখুন
বন্ধনী ছাড়া UNION + INTERSECTDBMS বদলালে ফল বদলায়বন্ধনী দিন, দুই রকম অর্থের ফল তুলনা করুন
SQLite-এ EXCEPT ALL বা (SELECT …)syntax errorকোন DBMS, কোন সংস্করণ — প্রম্পটে বলে দিন

AI-কে set অপারেশনের কোয়েরি চাইলে শুধু “লিখে দাও” না বলে প্রশ্নের খুঁটিনাটি আর যাচাইয়ের উপায়ও চেয়ে নিন:

আমি SQLite 3.4x ব্যবহার করছি (পরে PostgreSQL-এ যাবে)। দুটি টেবিল: customers(customer_id, name, city, phone) আর csv_import (একই কলাম)। phone আর city-তে NULL থাকতে পারে। ১. CSV-তে নতুন, master-এ বাদ পড়া আর বদলে যাওয়া সারি আলাদা লেবেলসহ এক কোয়েরিতে দাও। ২. কেন UNION না UNION ALL, আর কেন NOT IN নয় — এক লাইনে করে বলো। ৩. NULL-ওয়ালা একটি অপরিবর্তিত সারি যেন “বদলেছে” না দেখায়, তা কীভাবে নিশ্চিত করছ বলো। ৪. একাধিক set অপারেটর থাকলে SQLite আর PostgreSQL দুটিতেই এক অর্থ পেতে বন্ধনী দাও। ৫. শেষে ৪–৫টি সারির ছোট নমুনা ডেটা আর প্রত্যাশিত ফল দাও, যাতে আমি নিজে চালিয়ে মেলাতে পারি।

AI-এর দেওয়া কোয়েরি হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

  • UNION আর UNION ALL-এর পার্থক্য? কোনটি দ্রুত? — UNION duplicate বাদ দেয় (sort বা hash লাগে), UNION ALL সব রাখে আর সাধারণত দ্রুত।
  • UNION আর JOIN-এর পার্থক্য? — UNION সারি নিচে নিচে যোগ করে (কলাম একই), JOIN কলাম পাশাপাশি জোড়ে (সারি মিলিয়ে)।
  • Union-compatible মানে কী? — দুই পাশে কলামের সংখ্যা সমান আর অবস্থান অনুযায়ী টাইপ মানানসই।
  • MySQL-এ EXCEPT না থাকলে (8.0.31-এর আগে) কী লিখতেন? — NOT EXISTS (বা LEFT JOIN … IS NULL) দিয়ে anti join, আর NULL-নিরাপদ তুলনার জন্য <=>.
  • A = {1, 1, 2}, B = {1}: A EXCEPT B আর A EXCEPT ALL B? — EXCEPT দেয় {2}, EXCEPT ALL দেয় {1, 2}.

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

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

প্র ০১SELECT নিজে থেকে duplicate রাখে (DISTINCT লিখতে হয়), অথচ UNION নিজে থেকে duplicate মোছে (ALL লিখতে হয়)। SQL-এর এই উল্টো ডিফল্ট কেন?

Relational model-এ উত্তরটি সরল: relation একটি set, তাই কোনো অপারেশনের ফলেই duplicate থাকার কথা নয়। Codd-এর relational algebra-য় projection (কিছু কলাম বেছে নেওয়া) আর union দুটিই duplicate মুছে দেয়। প্রশ্ন হলো, SQL কেন projection-এ এই নিয়ম ভাঙল, কিন্তু union-এ রাখল?

১৯৭০-এর দশকে IBM-এর System R প্রকল্পে SQL (তখন SEQUEL) তৈরি হয়। প্রচলিত ব্যাখ্যা হলো, সেখানে একটি ব্যবহারিক হিসাব কাজ করেছিল। SELECT-এর প্রতিটি ফলে duplicate মুছতে প্রতিবার sort লাগত — তখনকার যন্ত্রে খুব দামি কাজ। আর অনেক প্রশ্নে duplicate আসলে দরকারি: গড় বেতন বের করতে সব বেতন লাগে, একই বেতনের দুজনকে একজন বানালে গড় ভুল হয়। তাই SELECT-এ ডিফল্ট হলো “সব রাখো”, আর যে চায় সে DISTINCT লিখবে।

UNION-এর বেলায় যুক্তিটি উল্টো দিকে গেল। দুটি তালিকা জোড়ার সবচেয়ে স্বাভাবিক প্রশ্ন “মিলিয়ে কোন কোন জিনিস?”, আর এটি গণিতের union-এর সাথে মেলে। ফলে SQL-86-এ UNION এল set অর্থে, আর duplicate রাখার জন্য UNION ALL এল বিকল্প হিসেবে। Relational তত্ত্বের লেখক C. J. Date-সহ অনেকে এই অসংগতির সমালোচনা করেছেন। তাঁদের যুক্তি: duplicate-এর কোনো যৌক্তিক অর্থ নেই, আর ডিফল্ট আচরণ জায়গাভেদে বদলালে প্রোগ্রামার ভুল করবেন।

বাস্তবে ভুলটি এক দিকেই বেশি ঘটে: মানুষ UNION লেখেন, কারণ শব্দটি ছোট আর পরিচিত, অথচ তাঁদের দরকার ছিল UNION ALL. ফল ধীর কোয়েরি, আর কখনো কখনো ভুল যোগফল। অনেক কোড-রিভিউ গাইডে তাই একটি নিয়ম দেখা যায়: “UNION দেখলে জিজ্ঞেস করো, duplicate কি সত্যিই মুছতে হবে?”

মূল উপলব্ধি: SQL খাঁটি তত্ত্ব আর ব্যবহারিক খরচের মাঝখানের একটি সমঝোতা। ডিফল্টগুলো ইতিহাসের ফল, যুক্তির নয় — তাই প্রতিবার নিজে বেছে নিন, ডিফল্টের ওপর ভরসা করে নয়।

প্র ০২WHERE-এ NULL = NULL সত্য নয়, কিন্তু INTERSECT, EXCEPT, DISTINCT আর GROUP BY দুটি NULL-কে এক ধরে। এটি কি SQL-এর অসংগতি, নাকি এর পেছনে যুক্তি আছে?

এটি দুটি আলাদা প্রশ্নের দুটি আলাদা উত্তর। WHERE-এ প্রশ্নটি হলো: “এই দুটি মান কি সমান?” NULL মানে অজানা, আর দুটি অজানা মান সমান কি না বলা যায় না — তাই উত্তর UNKNOWN. এটি three-valued logic-এর সৎ উত্তর। একজন গ্রাহকের শহর অজানা, আরেকজনেরও অজানা; তাঁরা একই শহরের, এমন সিদ্ধান্ত নেওয়া যায় না।

Duplicate খোঁজার সময় প্রশ্নটি অন্য: “এই দুটি সারিকে কি আলাদা করে দেখানোর কোনো কারণ আছে?” এখানে UNKNOWN উত্তর চলে না, কারণ DBMS-কে সারিটি হয় একবার, নয়তো দুবার দেখাতে হবে — তৃতীয় কোনো বিকল্প নেই। GROUP BY city-তে প্রতিটি NULL যদি আলাদা দল হতো, তাহলে NULL শহরের হাজার গ্রাহকের জন্য হাজারটি দল আসত, যা কারও কাজে লাগে না। তাই SQL standard একটি আলাদা ধারণা আনল: “distinct”. দুটি মান not distinct হয় যখন তারা সমান, অথবা দুটিই NULL.

পরে এই ধারণাটি সরাসরি লেখার উপায়ও এল: IS [NOT] DISTINCT FROM (SQL:1999). PostgreSQL এটি অনেক দিন ধরে মানে, SQL Server 2022-এ যোগ হয়েছে, আর SQLite 3.39 (২০২২) থেকে মানে, যদিও SQLite-এর নিজের IS আগে থেকেই একই কাজ করত। MySQL-এর <=> অপারেটরও একই অর্থ বহন করে।

অসংগতি তাহলে কোথায়? UNIQUE constraint-এ। যুক্তি অনুযায়ী সেখানেও “distinct” ধারণাই খাটার কথা, অথচ বেশিরভাগ DBMS দুটি NULL-কে আলাদা ধরে একাধিক NULL চলতে দেয়। এ নিয়ে DBMS-গুলোর মধ্যে আজও মতভেদ আছে (SQL Server একটির বেশি NULL মানে না), আর PostgreSQL 15 NULLS NOT DISTINCT বিকল্প যোগ করেছে।

মূল উপলব্ধি: “সমান” আর “আলাদা নয়” — SQL-এ দুটি আলাদা প্রশ্ন। WHERE আর JOIN প্রথম প্রশ্ন করে, set অপারেশন আর GROUP BY দ্বিতীয়টি। কোন অংশ কোন প্রশ্ন করছে জানলে NULL-এর আচরণ আর অবাক করে না।

প্র ০৩MySQL-এ INTERSECT আর EXCEPT এল ২০২২ সালে, অথচ standard-এ এগুলো ১৯৯২ থেকে। এতগুলো বছর মানুষ কীভাবে কাজ চালাল, আর সেই “বিকল্প” কোয়েরিগুলোর ঝুঁকি কোথায় ছিল?

MySQL-এর যাত্রা শুরু হয় দ্রুত, সহজ একটি ওয়েব-ডেটাবেস হিসেবে। প্রথম দিকে subquery, view, trigger-এর মতো অনেক বৈশিষ্ট্যই ছিল না, যা পরে ধাপে ধাপে এসেছে। INTERSECT আর EXCEPT এসেছে আরও অনেক পরে, MySQL 8.0.31-এ (২০২২)। এর মধ্যে MySQL থেকে আলাদা হওয়া MariaDB এগুলো আগেই যোগ করেছিল। কেন MySQL-এ এত দেরি, তার সরকারি ব্যাখ্যা জানা নেই। তবে একটি স্পষ্ট কারণ হলো, একই কাজ অন্য উপায়েও লেখা যেত, তাই চাপ কম ছিল।

বিকল্পগুলো ছিল তিনটি। INTERSECT-এর জন্য SELECT DISTINCT a.x FROM a JOIN b ON a.x = b.x বা WHERE x IN (SELECT x FROM b). EXCEPT-এর জন্য NOT IN, NOT EXISTS বা LEFT JOIN … WHERE b.x IS NULL. প্রতিটির নিজের ফাঁদ ছিল:

  • NULL: = দিয়ে মেলালে NULL-ওয়ালা সারি কখনো মেলে না। INTERSECT-এর বিকল্প NULL হারায়, EXCEPT-এর বিকল্প NULL-কে “নেই” দলে ফেলে। আর NOT IN-এর ডানে একটি NULL থাকলেই পুরো ফল খালি হয়ে যায়।
  • Duplicate: JOIN বা semi join বাঁয়ের duplicate রেখে দেয়। DISTINCT ভুলে গেলে ফল বড় হয়, আর ডানে একই মান কয়েকবার থাকলে JOIN আরও বাড়িয়ে দেয়।
  • অনেক কলাম: পাঁচ কলামের সারি মেলাতে পাঁচটি শর্ত লিখতে হয়। একটি ভুলে গেলে বা একটিতে NULL-নিরাপদ <=>-এর বদলে = লিখলে ভুলটি চোখে পড়ে না।

EXCEPT-এর সুবিধা তাই শুধু ছোট লেখা নয়। এটি একটি সংজ্ঞা: “পুরো সারি, NULL-সহ, ডানে নেই।” হাতে লেখা বিকল্পে এই সংজ্ঞাটি প্রতিবার নতুন করে, নির্ভুলভাবে লিখতে হয়। অন্যদিকে NOT EXISTS-এর সুবিধাও আছে: বাঁয়ের বাকি কলামগুলো দেখানো যায়, আর optimizer এটিকে ভালো anti join-এ বদলাতে পারে।

মূল উপলব্ধি: যেকোনো set অপারেশন join দিয়ে লেখা যায়, কিন্তু NULL আর duplicate-এর নিয়মসহ নির্ভুলভাবে লেখা কঠিন। পুরোনো কোড পড়ার সময় এই তিন ফাঁদ খুঁজুন।

প্র ০৪একটি ব্যাংক প্রতি রাতে কয়েক কোটি অ্যাকাউন্টের ব্যালান্সের snapshot রাখে। দুই রাতের পার্থক্য বের করতে দুই দিকে EXCEPT চালানো কি ভালো পদ্ধতি? বিকল্প কী?

শুদ্ধতার দিক থেকে দুই দিকে EXCEPT চমৎকার। প্রতিটি কলাম তুলনা হয়, NULL ঠিকভাবে সামলানো হয়, আর কোয়েরিটি ছোট, তাই ভুলের সুযোগ কম। কয়েক লাখ সারিতে এটিই সবচেয়ে নিরাপদ শুরু। কিন্তু কোটি সারিতে খরচটি হিসাব করতে হয়।

প্রতিটি EXCEPT-এ দুই snapshot-এর সব সারি পড়তে হয়, আর duplicate বাদ দিয়ে মেলাতে sort (O(N log N)) বা hash (গড়ে O(N), কিন্তু মেমরি লাগে) লাগে। দুই দিকে চালালে কাজ দ্বিগুণ। কোটি সারির hash table মেমরিতে না ধরলে ডিস্কে ভাগ করে কাজ করতে হয়, আর রাতের ব্যাচের সময় সীমিত। আবার পরিবর্তন হয়তো মাত্র কয়েক লাখ সারিতে — অর্থাৎ বেশিরভাগ কাজই হচ্ছে “যা বদলায়নি” তা মিলিয়ে দেখতে।

তাই বড় সিস্টেমে কয়েকটি বিকল্প ব্যবহার হয়:

  • Key দিয়ে FULL OUTER JOIN: account_id দিয়ে join করে কলামগুলো IS DISTINCT FROM দিয়ে তুলনা। এতে একই অ্যাকাউন্টের পুরোনো ও নতুন মান এক সারিতে পাশাপাশি আসে, যা রিপোর্টের জন্য সুবিধাজনক।
  • Row hash: প্রতিটি সারির সব কলামের একটি hash আগেই হিসাব করে রাখা হয়; তুলনা হয় শুধু (key, hash) জোড়ায়। এতে কম ডেটা নাড়াচাড়া করতে হয়।
  • updated_at বা version কলাম: শুধু শেষ snapshot-এর পরে বদলানো সারি দেখা। তবে এটি নির্ভর করে প্রতিটি লেখার পথ কলামটি ঠিকভাবে বসাচ্ছে কি না তার ওপর।
  • Change data capture (CDC): DBMS-এর log থেকে সরাসরি প্রতিটি পরিবর্তন পড়া (পর্ব ৪৮-এর WAL). এতে snapshot তুলনার দরকারই পড়ে না।

একটি জিনিস অবশ্য সবখানেই খাটে: যে পদ্ধতিই নিন, তার ফল মাঝে মাঝে একটি পূর্ণ EXCEPT দিয়ে যাচাই করুন। অডিটে “আমাদের CDC কি সব পরিবর্তন ধরছে?” প্রশ্নের সবচেয়ে বিশ্বাসযোগ্য উত্তর হলো দুই দিকের EXCEPT খালি আসা।

মূল উপলব্ধি: EXCEPT হলো পার্থক্যের সবচেয়ে নির্ভুল সংজ্ঞা, কিন্তু সবচেয়ে সস্তা পদ্ধতি নয়। বড় ডেটায় দ্রুত পদ্ধতি দিয়ে কাজ চালান, আর EXCEPT দিয়ে মাঝে মাঝে সেটি যাচাই করুন।

প্র ০৫SQL standard INTERSECT-কে UNION-এর চেয়ে বেশি precedence দেয়, কিন্তু SQLite আর Oracle দেয় না। এই পার্থক্যের উৎস কী, আর এটি থেকে কোড লেখার কী শিক্ষা নেওয়া উচিত?

Standard-এর পছন্দের পেছনে গণিতের একটি সাদৃশ্য আছে। Boolean algebra-য় intersection হলো AND, union হলো OR. আর প্রায় সব প্রোগ্রামিং ভাষায় AND আগে বাঁধে, OR পরে, ঠিক যেমন গুণ যোগের আগে। Set-এর ভাষায়ও ∩ আর ∪-এর সম্পর্ক তাই: A ∪ B ∩ C লিখলে সাধারণত A ∪ (B ∩ C) বোঝানো হয়। SQL-92 এই প্রথা মেনে INTERSECT-কে বেশি precedence দিয়েছে।

Oracle-এর MINUS আর UNION অনেক আগে থেকে ছিল, standard-এর এই নিয়ম আসার আগেই। তখন থেকে তাদের নিয়ম সরল: সব set অপারেটর সমান, বাঁ থেকে ডানে। লাখো পুরোনো কোয়েরি এই নিয়মের ওপর নির্ভর করে। নিয়ম বদলালে সেগুলো কোনো ত্রুটি ছাড়াই ভিন্ন ফল দিতে শুরু করবে, যা সবচেয়ে বিপজ্জনক ধরনের ভাঙন। তাই Oracle-এর ডকুমেন্টেশন নিজেই পরামর্শ দেয়, standard মানতে চাইলে বন্ধনী দিন। SQLite-ও একই সরল নিয়ম রেখেছে, আর তার ডকুমেন্টেশন স্পষ্ট বলে, compound SELECT বাঁ থেকে ডানে জোড়া লাগে। ছোট, স্থির SQLite-এর নীতিই হলো পুরোনো আচরণ না ভাঙা।

এখান থেকে তিনটি শিক্ষা পাওয়া যায়। প্রথমত, “চলছে” মানে “ঠিক” নয় — দুই অর্থেই কোয়েরি চলে, শুধু ফল আলাদা। দ্বিতীয়ত, যে কোড এক DBMS থেকে আরেকটিতে যেতে পারে (SQLite-এ টেস্ট, PostgreSQL-এ production — খুব প্রচলিত ব্যবস্থা), সেখানে precedence-এর ওপর নির্ভর করা চলে না; বন্ধনী বা CTE দিয়ে অর্থ স্পষ্ট করতে হয়। তৃতীয়ত, এমন সূক্ষ্ম পার্থক্য ধরার সেরা উপায় হলো ছোট, বুদ্ধি খাটিয়ে বানানো নমুনা ডেটা — যেমন অংশ ৮-এর {1, 2}, {2, 3}, {3, 4}, যেখানে দুই অর্থের ফল আলাদা হয়। যে নমুনায় দুই অর্থ একই ফল দেয়, সেটি কিছুই প্রমাণ করে না।

মূল উপলব্ধি: যোগ-গুণের মতো precedence মনে রাখার জিনিস, কিন্তু SQL-এ এটি DBMS-ভেদে আলাদা। পাঠকের মাথায় থাকা নিয়মের ওপর নির্ভর না করে বন্ধনী দিয়ে অর্থটি লিখে দিন।

অনুশীলন

খাতা-কলম নিন। একটি হাতে হিসাব, একটি কোয়েরি লেখা, একটি ফল আগে বলা আর একটি ডিজাইন।

  1. হাতে হিসাব করুন: A-র একটি কলামে মানগুলো (1, 1, 1, 2, 3, NULL), B-তে (1, 3, 3, NULL, NULL, 4). প্রতিটির ফল লিখুন: A UNION B, A UNION ALL B (সারির সংখ্যা), A INTERSECT B, A INTERSECT ALL B, A EXCEPT B, A EXCEPT ALL B.

    প্রতিটি মান কতবার আছে: A-তে 1 তিনবার, 2 একবার, 3 একবার, NULL একবার; B-তে 1 একবার, 3 দুবার, NULL দুবার, 4 একবার। মনে রাখুন, NULL এখানে NULL-এর সমান।

    • UNION: {NULL, 1, 2, 3, 4} — ৫টি সারি।
    • UNION ALL: ৬ + ৬ = ১২টি সারি।
    • INTERSECT: {NULL, 1, 3} — তিনটিই দুই পাশে আছে।
    • INTERSECT ALL: min দিয়ে: 1 → min(3, 1) = ১, 3 → min(1, 2) = ১, NULL → min(1, 2) = ১। ফল (1, 3, NULL).
    • EXCEPT: {2} — 1, 3, NULL ডানে আছে।
    • EXCEPT ALL: max(m − n, 0) দিয়ে: 1 → ৩ − ১ = ২, 2 → ১, 3 → max(১ − ২, ০) = ০, NULL → ০। ফল (1, 1, 2).
  2. কোয়েরি লিখুন: db-uni-তে কোন বিভাগে (departments) একজনও শিক্ষক (instructors) নেই? বিভাগের id আর নাম দেখান, EXCEPT ব্যবহার করে। তারপর ভাবুন: departments-এর বদলে students.dept_id থেকে instructors.dept_id বাদ দিলে কী আসত, আর কেন?
    SELECT d.dept_id, d.name FROM departments d
    WHERE d.dept_id IN (SELECT dept_id FROM departments
                        EXCEPT
                        SELECT dept_id FROM instructors);

    ফল: 4 | English. বিভাগ 1, 2, 3-এ শিক্ষক আছেন, English-এ নেই। দ্বিতীয় প্রশ্ন: SELECT dept_id FROM students EXCEPT SELECT dept_id FROM instructors দিত একটিমাত্র সারি, NULL. Habib Ullah-র dept_id NULL, আর instructors-এ কোনো NULL নেই, তাই EXCEPT এটিকে রেখে দেয়। আর English বিভাগে কোনো শিক্ষার্থীও নেই, তাই 4 আসেনি। প্রশ্নটি “বিভাগ” নিয়ে হলে বাঁ পাশে সব বিভাগের তালিকা (departments) রাখতে হয়। এই কোয়েরিটি উপরের যেকোনো db-uni এডিটরে চালিয়ে মিলিয়ে নিন।

  3. ফল আগে বলুন (trace): তিনটি এক-কলামের টেবিল: p = (5, 6, 7), q = (6, 8), r = (5, 8). কোয়েরি: SELECT v FROM p EXCEPT SELECT v FROM q UNION SELECT v FROM r; SQLite-এ ফল কী? PostgreSQL-এ? আর SELECT v FROM p UNION SELECT v FROM q INTERSECT SELECT v FROM r;?

    প্রথম কোয়েরি: EXCEPT আর UNION সমান স্তরের, তাই standard-এও বাঁ থেকে ডানে। দুই DBMS-এই (p EXCEPT q) UNION r = {5, 7} ∪ {5, 8} = {5, 7, 8}.

    দ্বিতীয় কোয়েরি: SQLite বাঁ থেকে ডানে: (p UNION q) INTERSECT r = {5, 6, 7, 8} ∩ {5, 8} = {5, 8}. PostgreSQL-এ INTERSECT আগে: p UNION (q INTERSECT r) = {5, 6, 7} ∪ {8} = {5, 6, 7, 8}. শিক্ষা: শুধু INTERSECT থাকলেই precedence-এর পার্থক্য দেখা দেয়।

  4. ডিজাইন করুন: একটি মোবাইল ওয়ালেটের মার্চেন্ট প্রতি মাসে ব্যাংকের settlement ফাইল (তারিখ, অঙ্ক, TrxID) নিজের বিক্রির খাতার সাথে মেলান। SQLite-এ কাজ হয়, যেখানে EXCEPT ALL নেই। কোন কলাম দিয়ে মেলাবেন, আর একই দিনে দুটি ৫০০ টাকার আলাদা লেনদেন থাকলে কীভাবে নিশ্চিত হবেন যে একটি হারিয়ে যায়নি?
    • সবচেয়ে ভালো: TrxID-সহ মেলান — (trx_id, date, amount) দুই দিকে EXCEPT. TrxID প্রতিটি লেনদেনে আলাদা, তাই দুটি ৫০০ টাকার লেনদেন দুটি আলাদা সারি, set রূপের EXCEPT-ই যথেষ্ট। এক দিকে থাকা সারি = অমিল; একই TrxID দুই দিকে ভিন্ন অঙ্কে = অঙ্কের গরমিল।
    • TrxID না থাকলে শুধু (date, amount) দিয়ে EXCEPT বিপজ্জনক: খাতায় দুটি ৫০০, ফাইলে একটি — EXCEPT বলবে “মিলেছে”। এখানে EXCEPT ALL লাগত। SQLite-এ বিকল্প: দুই দিকে GROUP BY date, amount করে COUNT(*) AS n বের করুন, তারপর (date, amount, n) দিয়ে দুই দিকে EXCEPT. সংখ্যা না মিললে সেই দলটি ফলে আসবে।
    • যোগফলও মেলান: মাসের মোট অঙ্ক আর মোট সারির সংখ্যা দুই দিকে সমান কি না — UNION ALL দিয়ে দুই দিকের মোট পাশাপাশি একটি রিপোর্টে।

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

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

ক্রমSELECT … UNION SELECT … ORDER BY … LIMIT … কোয়েরির কাজগুলো যৌক্তিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. দুই SELECT-এর কলামের সংখ্যা মেলে কি না যাচাই
  2. প্রতিটি SELECT নিজের FROM, WHERE আর GROUP BY চালিয়ে সারি বানায়
  3. দুই পাশের সারি একসাথে জোড়া হয়
  4. Duplicate সারি বাদ পড়ে
  5. পুরো ফল ORDER BY অনুযায়ী সাজানো হয়
  6. LIMIT অনুযায়ী প্রথম কয়েকটি সারি রাখা হয়

customers টেবিলে ৮টি সারি, যার city কলামে Dhaka তিনবার আর বাকি ৫টি আলাদা মান। courier_cities টেবিলে ৫টি আলাদা শহর, যার ৩টি customers-এও আছে। SELECT city FROM customers UNION ALL SELECT city FROM courier_cities কয়টি সারি দেবে?

  • ৮টি
  • ১৩টি
  • ৫টি
  • ৩টি

a(x)-এ মান 1, 2, NULL আর b(x)-এ মান 2, NULL, 3. SELECT x FROM a INTERSECT SELECT x FROM b-এর ফল কী?

  • শুধু 2
  • শুধু 1
  • ত্রুটি, কারণ NULL তুলনা করা যায় না
  • 2 আর NULL

a = {1, 2}, b = {2, 3}, c = {3, 4} তিনটি এক-কলামের টেবিল। SELECT n FROM a UNION SELECT n FROM b INTERSECT SELECT n FROM c SQLite-এ কী দেবে?

  • শুধু 3
  • 1, 2, 3
  • 1, 2, 3, 4
  • ত্রুটি

একটি UNION কোয়েরির প্রথম SELECT-এ কলামের নাম item, দ্বিতীয়টিতে একই অবস্থানের কলামের নাম product_name. ফলের কলামের নাম কী হবে, আর পুরো ফল সাজাতে কোথায় ORDER BY লিখবেন?

  • product_name; প্রতিটি SELECT-এর ভেতরে ORDER BY
  • ত্রুটি, কারণ নাম মেলেনি
  • item; একেবারে শেষে, শেষ SELECT-এর পরে
  • item; প্রথম SELECT-এর ভেতরে

orders(order_id, …) আর archived_orders(order_id, …) দুটি টেবিলে কোনো order_id দুবার নেই, আর দুই টেবিলে একই order_id কখনো থাকে না। দুটির সব অর্ডারের মোট অঙ্ক বের করতে প্রথমে কোনটি দিয়ে জোড়া উচিত?

  • UNION — duplicate থাকার ঝুঁকি নেই
  • UNION ALL
  • INTERSECT
  • EXCEPT

A-তে মান 7 তিনবার আর B-তে 7 একবার আছে। A EXCEPT ALL B-এর ফলে 7 কতবার আসবে?

  • ০ বার
  • ১ বার
  • ২ বার
  • ৪ বার

শব্দকোষ

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

মনে রাখুন

  1. UNION = অথবা, INTERSECT = এবং, EXCEPT (MINUS) = আছে কিন্তু ওখানে নেই; পুরো সারি তুলনা হয়, আর এখানে Venn ছবি সঠিক।
  2. Set রূপ duplicate মোছে (sort বা hash লাগে); গোনা বা যোগের কাজে UNION ALL. ALL রূপের হিসাব: m + n, min(m, n), max(m − n, 0).
  3. কলামের সংখ্যা সমান, টাইপ মানানসই, অবস্থান ধরে মেলে; নাম প্রথম SELECT থেকে; ORDER BY / LIMIT শেষে, পুরো ফলের ওপর।
  4. Set অপারেশনে দুটি NULL সমান (not distinct); = দিয়ে লেখা semi/anti join আর NOT IN এখানে আলাদা ফল দেয়।
  5. INTERSECT standard-এ আগে বাঁধে, SQLite/Oracle-এ বাঁ থেকে ডানে — মেশালে বন্ধনী দিন।

পরের পর্বে: মডিউল ৩-এর শেষ পর্ব। এতদিন বানানো টেবিল থেকে ডেটা পড়েছেন আর মিলিয়েছেন। কিন্তু চালু টেবিলে নতুন কলাম বা নতুন নিয়ম যোগ করবেন কীভাবে? পর্ব ২৩-এ ALTER TABLE, constraint আর referential action.

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

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