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

Subquery — nested, correlated আর EXISTS

SQL Subqueries — Scalar, Row & Table Subqueries, IN, ANY/ALL, EXISTS, Correlated Subqueries and Derived Tables

Taught in Bangla, this lesson explains subqueries as questions inside questions: scalar, row and table subqueries, IN and NOT IN, EXISTS and NOT EXISTS, comparisons with ANY and ALL, correlated subqueries evaluated per row, derived tables in FROM, subquery versus JOIN and performance, and using subqueries inside UPDATE and DELETE, all with live SQL.

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

  • Subquery কী, কোথায় কোথায় বসে, আর ভেতর থেকে বাইরে কীভাবে মূল্যায়ন হয়
  • Scalar, row আর table subquery; একটি মানের জায়গায় অনেক সারি এলে কোন DBMS কী করে
  • IN, NOT IN, ANY, ALL, EXISTS আর NOT EXISTS — আর NULL থাকলে কোনটি কী ফল দেয়
  • Correlated subquery আর FROM-এর derived table দিয়ে “আগে হিসাব, পরে ছাঁকা” ধরনের প্রশ্ন
  • কখন subquery আর কখন JOIN, optimizer-এর decorrelation, আর UPDATE/DELETE-এ subquery

১ · Subquery কী — প্রশ্নের ভেতরে প্রশ্ন

মুদি দোকানের মালিক হিসাবরক্ষককে বললেন, “যেসব পণ্যের দাম আমাদের গড় দামের চেয়ে বেশি, সেগুলোর তালিকা দাও।” হিসাবরক্ষক দুই ধাপে কাজটি করবেন। প্রথমে খাতা দেখে গড় দাম বের করবেন, ধরা যাক ৩,০০৮ টাকা। তারপর খাতার প্রতিটি পণ্য সেই সংখ্যার সাথে মেলাবেন। প্রথম ধাপের উত্তরটি দ্বিতীয় ধাপের প্রশ্নের অংশ হয়ে যায়।

SQL-এ এই দুই ধাপ একটি কোয়েরিতেই লেখা যায়। ভেতরের প্রশ্নটি বন্ধনীতে বসে বাইরের প্রশ্নের ভেতরে:

SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);   -- বন্ধনীর ভেতরেরটি subquery

বন্ধনীর ভেতরের SELECT-টিকে বলে subquery (বা nested query, inner query). যে কোয়েরির ভেতরে এটি বসে, সেটি outer query. পর্ব ১৫-এ SELECT আর WHERE, পর্ব ১৮-এ aggregate function আর GROUP BY, আর পর্ব ১৯–২০-এ JOIN দেখেছেন। Subquery এদের সবগুলোকে জুড়ে বড় প্রশ্ন বানানোর উপায়। মূল সত্যটি সহজ: SELECT-এর ফলও একটি টেবিল। তাই যেখানে একটি মান, একটি তালিকা বা একটি টেবিল লেখা যায়, সেখানে প্রায়ই একটি SELECT-ও বসানো যায়।

পরীক্ষার ফল প্রকাশের দিন বিভাগীয় অফিসকে বলা হলো, “যারা নিজের বিভাগের গড়ের চেয়ে ভালো করেছে, তাদের নাম নোটিশ বোর্ডে দিন।” অফিস সহকারী রেজিস্টারের প্রতিটি নামের পাশে গিয়ে আগে দেখেন সে কোন বিভাগের, তারপর সেই বিভাগের গড় হিসাব করেন, তারপর মেলান। প্রতিটি শিক্ষার্থীর জন্য ভেতরের প্রশ্নটি (কোন বিভাগের গড়?) বদলে যাচ্ছে। এটিই correlated subquery, যা অংশ ৬-এ দেখবেন।

Subquery কোথায় বসতে পারে

জায়গাকী ধরনের ফল লাগেউদাহরণ (db-shop)
WHERE / ONএকটি মান (>, =-এর পাশে), বা একটি তালিকা (IN, EXISTS)গড়ের চেয়ে দামি পণ্য
SELECT তালিকাপ্রতিটি সারির জন্য ঠিক একটি মানপ্রতিটি গ্রাহকের পাশে তাঁর অর্ডার-সংখ্যা
FROMপুরো একটি টেবিল (derived table)আগে প্রতি অর্ডারের মোট টাকা, তারপর গ্রাহকভিত্তিক গড়
HAVINGএকটি মান, যার সাথে দলের aggregate মেলানো হয়যে গ্রাহকের গড় অর্ডার সবার গড়ের বেশি
UPDATE / DELETE / INSERTকোন সারি বদলাবে বা কী মান বসবেবাতিল অর্ডারের লাইন মুছে ফেলা

ফলের আকার অনুযায়ী subquery তিন রকম। Scalar subquery ফেরত দেয় একটি সারি আর একটি কলাম, অর্থাৎ একটিমাত্র মান। Row subquery ফেরত দেয় একটি সারি কিন্তু একাধিক কলাম। Table subquery ফেরত দেয় যেকোনো সংখ্যক সারি আর কলাম। কোন জায়গায় কোন আকার চলে, সেটি না মেলালেই ভুল হয়।

এই পর্বের লাইভ SQL চলে আগের পর্বগুলোর দুটি নমুনা ডেটাবেসে। db-shop একটি দোকান: customers (customer_id, name, city, phone, joined_on — ৮ জন), categories (৪টি শ্রেণি: 1 Electronics, 2 Books, 3 Clothing, 4 Grocery), products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার) আর order_items (order_id, product_id, quantity, unit_price). db-uni একটি বিশ্ববিদ্যালয়: departments (dept_id, name, building — CSE, EEE, BBA, English), instructors, students (student_id, name, dept_id, batch, cgpa — ৮ জন; একজনের বিভাগ NULL, একজনের cgpa NULL), courses আর enrollments. প্রতিটি এডিটরের নিচে “নমুনা ডেটা” বাক্সে পুরো টেবিল দেখতে পাবেন। প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়।

২ · Scalar subquery — একটিমাত্র মান

সবচেয়ে সহজ subquery একটি সংখ্যা ফেরত দেয়। সেই সংখ্যাটি তারপর একটি সাধারণ মানের মতো ব্যবহার হয়: তুলনায়, হিসাবে, SELECT তালিকায়। নিচের প্রথম কোয়েরি গড় দাম দেখায়, দ্বিতীয়টি সেই গড় দিয়ে ছাঁকে, আর তৃতীয়টি দুই স্তরের subquery দিয়ে দ্বিতীয় সর্বোচ্চ দাম বের করে — চাকরির ইন্টারভিউর একটি পুরোনো প্রিয় প্রশ্ন।

-- গড় দাম কত?
SELECT AVG(price) AS avg_price FROM products;

-- গড়ের চেয়ে দামি পণ্য
SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- দ্বিতীয় সর্বোচ্চ দাম: সর্বোচ্চের চেয়ে কম দামগুলোর সর্বোচ্চ
SELECT MAX(price) AS second_highest FROM products
WHERE price < (SELECT MAX(price) FROM products);

প্রথম ফল 3008 — দশটি দামের যোগফল 30080, ভাগ ১০. দ্বিতীয় ফলে তিনটি সারি: Mobile Phone | 15000, Saree | 4500, Smart Watch | 3500. Subquery-টি বাইরের কোনো কলামের ওপর নির্ভর করে না, তাই যুক্তির দিক থেকে এটি একবারই চলে, আর ফলটি একটি ধ্রুবকের মতো বসে যায়: WHERE price > 3008. তৃতীয় ফল 4500. ভেতরেরটি দেয় 15000; বাইরেরটি 15000-এর কম দামগুলোর মধ্যে সর্বোচ্চটি নেয়। একই দামে দুটি পণ্য থাকলেও এটি “দ্বিতীয় আলাদা দাম” দেয় — ইন্টারভিউতে এই সূক্ষ্মতা বলতে পারলে নম্বর বাড়ে।

একাধিক স্তরের subquery কোন ক্রমে মূল্যায়ন হয়? নিয়মটি হলো, যে subquery বাইরের কিছুর ওপর নির্ভর করে না, তার মান আগে জানা দরকার, তাই যুক্তির দিক থেকে সবচেয়ে ভেতরেরটি আগে। নিচে দ্বিতীয় সর্বোচ্চ দামের পণ্যটির নাম বের করার তিন স্তরের কোয়েরিটি ধাপে ধাপে দেখুন:

কোয়েরি আর তার মানলাল = এই ধাপে যা মূল্যায়ন হচ্ছে

            
মান
① সবচেয়ে ভেতরের
② মাঝের
③ বাইরের ফল
  1. তিন স্তরের কোয়েরি। ③ বাইরের কোয়েরি নাম চায়, ② মাঝেরটি একটি দাম দেবে, আর ① সবচেয়ে ভেতরেরটিও একটি দাম দেবে। কোনোটিই বাইরের কোনো কলামের ওপর নির্ভর করে না।
  2. ① আগে। ② চলতে হলে ①-এর মান জানা চাই। ① পুরো products টেবিলের ১০টি দাম দেখে সর্বোচ্চটি দেয়: 15000. এটি এখন একটি ধ্রুবক।
  3. তারপর ②। ① বসে যাওয়ার পর ② হয়ে যায় WHERE price < 15000. বাকি ৯টি দামের সর্বোচ্চ 4500.
  4. শেষে ③। এখন বাইরের কোয়েরি সাধারণ একটি ছাঁকনি: WHERE price = 4500. ১০টি সারির প্রতিটি দেখা হয়; শুধু 107 Saree মেলে।
  5. ফল: Saree. প্রতিটি non-correlated subquery যুক্তিতে একবারই চলে। আসল DBMS ভিন্ন কৌশলে চালাতে পারে, কিন্তু ফল এই যুক্তির সাথেই মিলবে — optimizer অর্থ বদলাতে পারে না।

যখন “একটি মান” আসলে অনেকগুলো

Scalar subquery-র চুক্তি হলো “আমি একটির বেশি সারি দেব না”। চুক্তিটি ভাঙলে কী হয়? কোনো সারি না এলে সব DBMS-এ ফল NULL. কিন্তু একাধিক সারি এলে আচরণ আলাদা। নিচের দ্বিতীয় কোয়েরিতে লেখক ভেবেছিলেন Electronics-এর “দাম” একটি, অথচ সেখানে চারটি পণ্য:

-- SELECT তালিকায় scalar subquery: প্রতিটি বইয়ের দাম গড় থেকে কত দূরে
SELECT name, price,
       price - (SELECT AVG(price) FROM products) AS diff_from_avg
FROM products WHERE category_id = 2;

-- ভুল: Electronics-এর দাম একটি নয়, চারটি
SELECT name, price FROM products
WHERE price = (SELECT price FROM products WHERE category_id = 1);

-- উদ্দেশ্য যদি "যেকোনো একটির সমান" হয়: IN
SELECT name, price FROM products
WHERE price IN (SELECT price FROM products WHERE category_id = 1);

প্রথম ফল: DSA Book | 650 | -2358 আর SQL Book | 550 | -2458. দ্বিতীয় কোয়েরি কোনো ত্রুটি দেয় না! ফল একটি সারি: Mobile Phone | 15000. SQLite-এর নিয়ম হলো, scalar subquery একাধিক সারি দিলে প্রথম সারিটি নেওয়া হয়, বাকিগুলো ফেলে দেওয়া হয়। এখানে টেবিল পড়ার ক্রমে প্রথম Electronics পণ্য Mobile Phone. কিন্তু ORDER BY ছাড়া “প্রথম” সারি কোনটি হবে, তার কোনো নিশ্চয়তা নেই — index বা সংস্করণ বদলালে অন্য সারি আসতে পারে। তৃতীয় কোয়েরি সঠিক অর্থ দেয়: চারটি সারি — Mobile Phone, Headphone, Power Bank, Smart Watch.

SQLite-এর নীরবতা একটি ফাঁদ। একই দ্বিতীয় কোয়েরি অন্য DBMS-এ সরাসরি ত্রুটি দেয়, আর সেটিই ভালো — ভুলটি সঙ্গে সঙ্গে ধরা পড়ে। SQLite-এ পরীক্ষা করে ঠিক মনে হওয়া কোয়েরি সার্ভারে গিয়ে ভাঙতে পারে, আবার উল্টোটাও হয়: SQLite-এর অ্যাপে মাসের পর মাস ভুল ফল আসে, কেউ টের পায় না। নিয়ম মানুন: scalar subquery লিখলে নিশ্চিত হন ফল একটি সারি — aggregate (MAX, COUNT…), primary key-তে =, অথবা ORDER BY … LIMIT 1 দিয়ে।

SELECT name, price FROM products
WHERE price = (SELECT price FROM products WHERE category_id = 1);

-- PostgreSQL:  ERROR: more than one row returned by a subquery used as an expression
-- MySQL:       ERROR 1242 (21000): Subquery returns more than 1 row
-- SQL Server:  Msg 512: Subquery returned more than 1 value. ...
-- Oracle:      ORA-01427: single-row subquery returns more than one row

আরেকটি সীমা: scalar subquery-তে একটির বেশি কলাম থাকলে SQLite-ও ত্রুটি দেয় — sub-select returns 2 columns - expected 1. সারির সংখ্যায় SQLite শিথিল, কলামের সংখ্যায় নয়।

৩ · Row আর table subquery — IN আর NOT IN

Table subquery-র সবচেয়ে প্রচলিত ব্যবহার IN-এর সাথে। হাতে লেখা তালিকার IN আপনি চেনেন, যেমন city IN ('Dhaka', 'Khulna') (পর্ব ১৬). এখানে তালিকাটি হাতে না লিখে একটি SELECT থেকে আসে। x IN (subquery) সত্য হয়, যদি subquery-র অন্তত একটি সারির মান x-এর সমান হয়। এক কলামের বদলে কয়েকটি কলাম একসাথে মেলাতে চাইলে row subquery লাগে: (customer_id, order_date) IN (SELECT …). SQLite এটি ৩.১৫ সংস্করণ (২০১৬) থেকে বোঝে, PostgreSQL, MySQL আর Oracle-ও বোঝে, কিন্তু SQL Server এমন বহু-কলামের IN সমর্থন করে না — সেখানে EXISTS লিখতে হয়। অংশ ৬-এ এর একটি চালানো উদাহরণ পাবেন।

-- অন্তত একটি অর্ডার দিয়েছেন এমন গ্রাহক
SELECT customer_id, name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);

-- কখনো অর্ডার দেননি
SELECT customer_id, name FROM customers
WHERE customer_id NOT IN (SELECT customer_id FROM orders);

-- দুই স্তর: ডেলিভারি হওয়া অর্ডারে যেসব পণ্য ছিল
SELECT name FROM products
WHERE product_id IN (SELECT product_id FROM order_items
                     WHERE order_id IN (SELECT order_id FROM orders
                                        WHERE status = 'delivered'));

প্রথম ফলে ৭ জন: 1 Rahim Uddin, 2 Karima Begum, 3 Sabbir Hasan, 4 Nusrat Jahan, 6 Farhana Akter, 7 Imran Hossain, 8 Mitu Rani. Rahim-এর তিনটি অর্ডার থাকলেও তিনি একবারই এসেছেন — IN শুধু “আছে কি না” দেখে, কয়বার আছে তা নয়। এটিই semi join, পর্ব ২০-এ যার নাম শুনেছেন। দ্বিতীয় ফল: 5 | Tanvir Ahmed. তৃতীয় ফলে ৭টি পণ্য: Mobile Phone, Headphone, DSA Book, SQL Book, Panjabi, Rice 5kg, Mustard Oil 1L. ডেলিভারি হওয়া অর্ডার 1001, 1002, 1005, 1007, 1010; তাদের লাইনে DSA Book আর Headphone দুবার করে থাকলেও ফলে একবার। একই প্রশ্ন JOIN দিয়ে লিখলে সারি দ্বিগুণ হতো, যা অংশ ৮-এ দেখবেন।

NOT IN আর NULL — পুরোনো কিন্তু ভয়ংকর ফাঁদ

উপরের NOT IN ঠিক কাজ করেছে, কারণ orders.customer_id-এ NOT NULL আছে। এবার পর্ব ১৬-এর তিন-মানের যুক্তি মনে করুন। x NOT IN (a, b, c) আসলে x <> a AND x <> b AND x <> c. তালিকায় একটি NULL থাকলে x <> NULL হয় UNKNOWN. আর AND-এর মধ্যে একটি UNKNOWN থাকলে পুরো ফল কখনো TRUE হয় না — হয় FALSE, নয়তো UNKNOWN. WHERE শুধু TRUE সারি রাখে। তাই subquery-র ফলে একটিও NULL থাকলে NOT IN কোনো সারিই ফেরত দেয় না। ত্রুটিও দেয় না — শুধু ফাঁকা ফল, যা দেখে মনে হয় “এমন কেউ নেই”। পরের অংশে db-uni-তে এটি নিজের চোখে দেখবেন।

4 NOT IN (1, 2, NULL) = 4<>1 AND 4<>2 AND 4<>NULL = TRUE AND TRUE AND UNKNOWN = UNKNOWN

৪ · EXISTS আর NOT EXISTS — “একটিও আছে কি?”

EXISTS একটি subquery নেয় আর একটিমাত্র প্রশ্নের উত্তর দেয়: এই subquery কি অন্তত একটি সারি ফেরত দেয়? দিলে TRUE, না দিলে FALSE. সারির ভেতরে কী আছে — NULL হোক বা যা-ই হোক — তাতে কিছু যায় আসে না। তাই EXISTS কখনো UNKNOWN হয় না; এটি পুরোপুরি দুই-মানের। এজন্য রীতি হলো ভেতরে SELECT 1 লেখা — SELECT তালিকা এখানে উপেক্ষিত, SELECT * লিখলেও একই ফল।

EXISTS প্রায় সবসময় correlated: ভেতরের WHERE বাইরের সারির কলাম ব্যবহার করে, যেমন WHERE s.dept_id = d.dept_id. বাংলায় পড়ুন: “এই বিভাগের (d) কোনো শিক্ষার্থী (s) আছে কি?” আর DBMS প্রথম মিল পেলেই থামতে পারে — বাকি সারি দেখার দরকার নেই। এবার db-uni-তে প্রশ্ন: কোন বিভাগে কোনো শিক্ষার্থী নেই? মনে রাখুন, Habib Ullah-র dept_id NULL.

-- কোন বিভাগে কোনো শিক্ষার্থী নেই? (NOT IN)
SELECT name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM students);

-- একই প্রশ্ন NOT EXISTS দিয়ে
SELECT d.name FROM departments d
WHERE NOT EXISTS (SELECT 1 FROM students s WHERE s.dept_id = d.dept_id);

-- NOT IN বাঁচানোর উপায়: ভেতর থেকে NULL বাদ দিন
SELECT name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM students WHERE dept_id IS NOT NULL);

-- EXISTS: যে বিভাগে অন্তত একজনের cgpa 3.5 বা বেশি
SELECT d.name FROM departments d
WHERE EXISTS (SELECT 1 FROM students s
              WHERE s.dept_id = d.dept_id AND s.cgpa >= 3.5);

প্রথম কোয়েরির ফল “(কোনো সারি নেই)” — অথচ English বিভাগে একজনও শিক্ষার্থী নেই! দোষী Habib Ullah-র NULL dept_id. দ্বিতীয় কোয়েরি ঠিক উত্তর দেয়: English. তৃতীয়টিও English, কারণ IS NOT NULL দিয়ে তালিকা থেকে NULL সরানো হয়েছে। চতুর্থটির ফল CSE — Arif (3.75), Dilruba (3.90) আর Gulshan (3.55) সবাই CSE-র; EXISTS প্রথম মিল পেলেই থামতে পারে, আর তিনজন মিললেও CSE একবারই আসে।

দুটি পথ কেন আলাদা ফল দিল, ধাপে ধাপে দেখুন:

departments আর students.dept_idলাল = এই ধাপে যা তুলনা হচ্ছে

            
অবস্থা
পদ্ধতি
এই বিভাগের ফল
মোট ফল
  1. ডেটা। চারটি বিভাগ। আটজন শিক্ষার্থীর dept_id-এর তালিকায় 1, 2, 3 আছে, 4 নেই — আর Habib-এর ঘরে NULL (বিভাগ এখনো ঠিক হয়নি)। সঠিক উত্তর হওয়া উচিত English.
  2. NOT IN, CSE. 1 IN (…)-এ প্রথম মানই 1, তাই IN = TRUE, আর NOT IN = FALSE. CSE বাদ। EEE আর BBA-ও একইভাবে বাদ পড়ে।
  3. NOT IN, English — ফাঁদ। 4 কোনো সংখ্যার সমান নয়, সবই FALSE. কিন্তু শেষে 4 = NULL হয় UNKNOWN. FALSE OR UNKNOWN = UNKNOWN, আর NOT UNKNOWN-ও UNKNOWN. WHERE UNKNOWN সারি ফেলে দেয়।
  4. NOT IN-এর ফল ফাঁকা। কোনো বিভাগই TRUE পায়নি। কোনো ত্রুটি নেই, শুধু ভুল উত্তর: “সব বিভাগেই শিক্ষার্থী আছে”।
  5. NOT EXISTS, CSE. প্রশ্নটি এখন সারি খোঁজা: dept_id = 1 এমন শিক্ষার্থী আছে কি? প্রথম সারি Arif-এই মিলে যায়, খোঁজা থামে। EXISTS = TRUE, তাই CSE বাদ।
  6. NOT EXISTS, English. আটজনের কারও dept_id 4 নয়। Habib-এর NULL = 4 UNKNOWN, তাই ভেতরের WHERE সারিটি রাখে না। ভেতরের ফল ০টি সারি, তাই EXISTS = FALSE আর NOT EXISTS = TRUE. NULL এখানে কোনো ক্ষতি করতে পারেনি।
  7. ফল: English. EXISTS শুধু সারি গোনে (০ না ১+), তাই উত্তর TRUE বা FALSE, কখনো UNKNOWN নয়। “কোনটি নেই” ধরনের প্রশ্নে তাই NOT EXISTS নিরাপদ। NOT IN লিখতেই হলে ভেতরে WHERE dept_id IS NOT NULL দিন।

উল্টো দিকে কোনো পার্থক্য নেই। ধনাত্মক IN আর EXISTS WHERE-এ সবসময় একই সারি দেয়: যেখানে মিল আছে সেখানে দুটিই TRUE. IN-এর UNKNOWN শুধু তখন আসে যখন মিল নেই আর তালিকায় NULL আছে — আর তখন FALSE আর UNKNOWN দুটিই সারি বাদ দেয়। বিপদ শুরু হয় NOT লাগালে, কারণ NOT FALSE = TRUE, কিন্তু NOT UNKNOWN = UNKNOWN.

৫ · ANY আর ALL — তালিকার সাথে তুলনা

IN শুধু সমতা দেখে। “সব বইয়ের চেয়ে দামি” বা “অন্তত একটি Electronics পণ্যের চেয়ে দামি” ধরনের প্রশ্নে লাগে ANY আর ALL. x > ALL (subquery) সত্য, যদি x প্রতিটি মানের চেয়ে বড় হয়। x > ANY (subquery) সত্য, যদি অন্তত একটির চেয়ে বড় হয় (SOME হলো ANY-র আরেক নাম)। দুটি সমতুল্যতা মনে রাখুন: = ANY মানে IN, আর <> ALL মানে NOT IN — NULL-এর ফাঁদসহ।

-- সব বইয়ের (category 2) চেয়ে দামি পণ্য
SELECT name, price FROM products
WHERE price > ALL (SELECT price FROM products WHERE category_id = 2);

-- অন্তত একটি Electronics পণ্যের (category 1) চেয়ে দামি
SELECT name, price FROM products
WHERE price > ANY (SELECT price FROM products WHERE category_id = 1);

SQLite-এ ANY, ALL বা SOME নেই — উপরের কোয়েরি চালালে near "ALL": syntax error আসে। কিন্তু প্রায় সব ক্ষেত্রে MAX আর MIN দিয়ে একই প্রশ্ন লেখা যায়: > ALL মানে “সর্বোচ্চটির চেয়েও বড়”, অর্থাৎ > MAX; আর > ANY মানে “সর্বনিম্নটির চেয়ে বড়”, অর্থাৎ > MIN. একইভাবে < ALL = < MIN আর < ANY = < MAX.

-- > ALL (সব বইয়ের চেয়ে দামি) = > MAX
SELECT name, price FROM products
WHERE price > (SELECT MAX(price) FROM products WHERE category_id = 2);

-- > ANY (অন্তত একটি Electronics-এর চেয়ে দামি) = > MIN
SELECT name, price FROM products
WHERE price > (SELECT MIN(price) FROM products WHERE category_id = 1);

-- ফাঁকা সেট: category 9 নেই, তাই MAX হয় NULL
SELECT COUNT(*) AS n FROM products
WHERE price > (SELECT MAX(price) FROM products WHERE category_id = 9);

প্রথম ফলে ৬টি পণ্য: Mobile Phone 15000, Headphone 1200, Power Bank 1800, Panjabi 2200, Saree 4500, Smart Watch 3500 — বইয়ের সর্বোচ্চ দাম 650 (DSA Book). দ্বিতীয় ফলে ৫টি: Mobile Phone, Power Bank, Panjabi, Saree, Smart Watch — Electronics-এর সর্বনিম্ন 1200 (Headphone), আর Headphone নিজে 1200-এর চেয়ে বড় নয়। তৃতীয়টির ফল 0. এখানেই MAX-রূপান্তর আর আসল ALL আলাদা হয়ে যায়: খালি সেটের ওপর > ALL সংজ্ঞা অনুযায়ী TRUE (খালি সেটের “প্রতিটি” সদস্যের চেয়ে বড় হওয়া তুচ্ছভাবে সত্য), তাই PostgreSQL-এ ১০টি সারিই আসত। আর MAX খালি সেটে NULL দেয়, তুলনা UNKNOWN, ফল ০.

রূপান্তরের দুটি কিনারা। (১) খালি সেট: > ALL TRUE, কিন্তু > MAX UNKNOWN. (২) সেটে NULL: 15000 > ALL (650, NULL) হয় UNKNOWN, কারণ NULL-এর সাথে তুলনা অজানা; কিন্তু MAX NULL উপেক্ষা করে 650 দেয়, তাই 15000 > 650 TRUE. বেশিরভাগ ব্যবসায়িক প্রশ্নে MAX/MIN-এর আচরণই কাঙ্ক্ষিত, কিন্তু রূপান্তর করার সময় এই দুই ক্ষেত্র ভেবে নিন — পরীক্ষায় এটি প্রায়ই জিজ্ঞেস করা হয়।

৬ · Correlated subquery — প্রতিটি সারির জন্য নতুন প্রশ্ন

এ পর্যন্ত বেশিরভাগ subquery নিজে নিজে চলতে পারত: “গড় দাম কত?” প্রশ্নের উত্তর বাইরের কোনো সারির ওপর নির্ভর করে না। কিন্তু “নিজের বিভাগের গড়ের চেয়ে বেশি cgpa কার?” প্রশ্নে ভেতরের গড়টি নির্ভর করে বাইরের শিক্ষার্থীটি কোন বিভাগের। যে subquery বাইরের কোয়েরির কোনো কলাম ব্যবহার করে, তাকে বলে correlated subquery. যুক্তির দিক থেকে এটি বাইরের প্রতিটি সারির জন্য আলাদাভাবে, সেই সারির মান বসিয়ে, নতুন করে চলে।

SELECT s.name, s.cgpa
FROM students s                                  -- বাইরের সারি: s
WHERE s.cgpa > (SELECT AVG(s2.cgpa)
                FROM students s2                 -- ভেতরের সারি: s2
                WHERE s2.dept_id = s.dept_id);   -- s.dept_id = বাইরের রেফারেন্স

একই টেবিল দুবার ব্যবহার হচ্ছে, তাই দুটি আলাদা alias জরুরি: s বাইরের, s2 ভেতরের। alias না দিলে dept_id = dept_id লেখা হতো, আর SQL নামটি সবচেয়ে কাছের (ভেতরের) টেবিলে খুঁজত — শর্তটি হয়ে যেত “নিজের সাথে নিজের তুলনা”, যা প্রায় সব সারিতে TRUE. এবার আটজন শিক্ষার্থীর জন্য ভেতরের প্রশ্নটি কীভাবে বদলায়, দেখুন:

বাইরের সারি → ভেতরের প্রশ্নলাল = এই সারির মান

            
হিসাব
s.dept_id (বাইরের মান)
ভেতরের গড়
subquery কতবার চলল
এ পর্যন্ত ফলে
  1. বাইরের টেবিল। students-এর আটটি সারি একে একে পড়া হবে। প্রতিটি সারির dept_id ভেতরের কোয়েরিতে s.dept_id-এর জায়গায় বসবে।
  2. Arif (CSE). ভেতরের প্রশ্ন হলো CSE-র গড়: (3.75 + 3.40 + 3.90 + 3.55) / ৪ = 3.65. Arif-এর 3.75 বড় — রাখা হলো।
  3. Bristy (CSE). একই বিভাগ, একই গড় 3.65. যুক্তির দিক থেকে subquery আবার চলে। 3.40 ছোট — বাদ।
  4. Chayon (EEE). এবার ভেতরের প্রশ্ন বদলেছে: EEE-র গড়। Fahim-এর cgpa NULL, AVG সেটি বাদ দেয়, তাই গড় 2.95. Chayon-এর 2.95 সমান, বড় নয় — বাদ।
  5. Dilruba (CSE). আবার CSE-র গড় 3.65, আবার হিসাব। 3.90 বড় — রাখা হলো।
  6. Emon (BBA). BBA-তে তিনি একাই, গড় তাঁর নিজের 3.10. নিজের গড়ের চেয়ে নিজে বড় হতে পারেন না — বাদ।
  7. Fahim (EEE, cgpa NULL). গড় 2.95, কিন্তু NULL > 2.95 UNKNOWN — বাদ।
  8. Gulshan (CSE). 3.55, গড় 3.65 — বাদ।
  9. Habib (বিভাগ NULL). ভেতরের শর্ত s2.dept_id = NULL কোনো সারিতে TRUE নয়, তাই AVG ফাঁকা সেটের ওপর — NULL. তুলনা UNKNOWN — বাদ।
  10. ফল: Arif আর Dilruba. ভেতরের কোয়েরি যুক্তিতে ৮ বার চলেছে, অথচ আলাদা বিভাগ মাত্র ৪টি (CSE, EEE, BBA, NULL). বাস্তব DBMS এই অপচয় এড়াতে ফল মনে রাখে বা পুরো কোয়েরিকে join-এ বদলায় — অংশ ৮-এ দেখবেন।

এবার নিজে চালান। SELECT তালিকাতেও একটি correlated scalar subquery রাখা হয়েছে, যাতে প্রতিটি সারির পাশে তার বিভাগের গড় দেখা যায়। শেষে EXPLAIN QUERY PLAN দেখায় SQLite নিজে কোয়েরিটি কীভাবে চালানোর পরিকল্পনা করছে:

-- নিজের বিভাগের গড়ের চেয়ে বেশি cgpa
SELECT s.name, s.dept_id, s.cgpa,
       (SELECT ROUND(AVG(s2.cgpa), 2) FROM students s2
        WHERE s2.dept_id = s.dept_id) AS dept_avg
FROM students s
WHERE s.cgpa > (SELECT AVG(s2.cgpa) FROM students s2
                WHERE s2.dept_id = s.dept_id);

-- SQLite কীভাবে চালাবে?
EXPLAIN QUERY PLAN
SELECT s.name FROM students s
WHERE s.cgpa > (SELECT AVG(s2.cgpa) FROM students s2
                WHERE s2.dept_id = s.dept_id);

প্রথম ফল ঠিক ডায়াগ্রামের মতো: Arif Hossain | 1 | 3.75 | 3.65 আর Dilruba Khatun | 1 | 3.9 | 3.65. দ্বিতীয় ফলের detail কলামে তিনটি লাইন: SCAN s, CORRELATED SCALAR SUBQUERY 1, আর তার নিচে SCAN s2. অর্থাৎ SQLite সত্যিই বাইরের প্রতিটি সারির জন্য ভেতরের টেবিল পুরোটা পড়ার পরিকল্পনা করছে। ৮টি সারিতে কিছু যায় আসে না, কিন্তু ১০ লক্ষ শিক্ষার্থীতে সেটি ১০ লক্ষ × ১০ লক্ষ সারি-পড়া। s2.dept_id-এ একটি index থাকলে প্রতিবার শুধু নিজের বিভাগের সারিগুলো পড়া হতো। EXPLAIN-এর পুরো ব্যাখ্যা পর্ব ৪১ আর ৪৩-এ।

প্রতিটি গ্রাহকের সর্বশেষ অর্ডার

Correlated subquery-র আরেকটি ক্লাসিক কাজ “প্রতিটি দলের সেরা সারি” (greatest-per-group). প্রশ্ন: প্রতিটি গ্রাহকের সবচেয়ে নতুন অর্ডার কোনটি? শুধু GROUP BY customer_id আর MAX(order_date) দিলে তারিখটি পাওয়া যায়, কিন্তু order_id নয়। নিচে দুই পথ: correlated subquery, আর অংশ ৩-এর row subquery.

-- correlated: এই গ্রাহকের সর্বশেষ তারিখের অর্ডারটিই রাখো
SELECT o.customer_id, o.order_id, o.order_date
FROM orders o
WHERE o.order_date = (SELECT MAX(o2.order_date) FROM orders o2
                      WHERE o2.customer_id = o.customer_id)
ORDER BY o.customer_id;

-- row subquery: (customer_id, order_date) জোড়া মেলানো
SELECT customer_id, order_id, order_date FROM orders
WHERE (customer_id, order_date) IN
      (SELECT customer_id, MAX(order_date) FROM orders GROUP BY customer_id)
ORDER BY customer_id;

দুটি কোয়েরিরই ফল একই ৭টি সারি: 1 | 1009 | 2025-10-08, 2 | 1007 | 2025-10-01, 3 | 1004 | 2025-09-12, 4 | 1005 | 2025-09-15, 6 | 1006 | 2025-09-20, 7 | 1010 | 2025-10-09, 8 | 1008 | 2025-10-05. Tanvir (5)-এর কোনো অর্ডার নেই, তাই তিনি নেই। Rahim-এর তিনটি অর্ডারের মধ্যে শুধু 1009 টিকেছে। একটি সতর্কতা: একই গ্রাহকের একই দিনে দুটি অর্ডার থাকলে দুটিই আসবে, কারণ দুটির তারিখই MAX. একটিই চাইলে পর্ব ২৬-এর ROW_NUMBER() বা আরও সূক্ষ্ম শর্ত লাগবে। দ্বিতীয় কোয়েরি correlated নয় — ভেতরেরটি একবার চলে ৭টি জোড়া দেয়।

৭ · FROM-এ subquery — derived table

কিছু প্রশ্নে দুবার aggregate করতে হয়: “প্রতিটি গ্রাহকের গড় অর্ডারের মূল্য কত?” প্রথমে প্রতিটি অর্ডারের মোট টাকা (order_items-এ SUM), তারপর গ্রাহকভিত্তিক সেই মোটগুলোর গড় (AVG). AVG(SUM(…)) লেখা যায় না — একই স্তরে দুটি aggregate চলে না। সমাধান: প্রথম ধাপটি FROM-এ একটি subquery বানান। এর ফল একটি অস্থায়ী টেবিলের মতো আচরণ করে, যাকে বলে derived table (SQL Server-এ “derived table”, Oracle-এ “inline view”).

-- ভেতরে: প্রতিটি অর্ডারের মোট টাকা; বাইরে: গ্রাহকভিত্তিক গড়
SELECT t.customer_id, COUNT(*) AS n_orders, ROUND(AVG(t.total), 2) AS avg_value
FROM (SELECT o.order_id, o.customer_id,
             SUM(i.quantity * i.unit_price) AS total
      FROM orders o JOIN order_items i ON i.order_id = o.order_id
      GROUP BY o.order_id, o.customer_id) AS t
GROUP BY t.customer_id
-- HAVING-এ subquery: সব অর্ডারের গড়ের চেয়ে বেশি
HAVING AVG(t.total) > (SELECT AVG(total)
                       FROM (SELECT SUM(quantity * unit_price) AS total
                             FROM order_items GROUP BY order_id))
ORDER BY t.customer_id;

ফল একটি সারি: 1 | 3 | 12393.33. ভেতরের derived table t-তে ১০টি সারি — প্রতিটি অর্ডারের মোট, যেমন 1001 = 15000 + 2 × 1200 = 17400, 1009 = 14500 + 3500 = 18000. HAVING-এর subquery সব অর্ডারের গড় বের করে: ১০টি মোটের যোগফল 56820, গড় 5682. গ্রাহকদের গড় অর্ডার: Rahim 12393.33, Karima 2500, Sabbir 4500, Nusrat 5050, Farhana 3500, Imran 1040, Mitu 550 — শুধু Rahim 5682-এর ওপরে। HAVING লাইনটি মুছে চালালে সাতজনের সবার গড় দেখবেন। খেয়াল করুন, Sabbir-এর একমাত্র অর্ডার 1004 বাতিল হয়েছিল, তবু হিসাবে আছে; বাস্তব রিপোর্টে WHERE o.status <> 'cancelled' যোগ করা উচিত।

Alias-এর নিয়ম DBMS ভেদে আলাদা। SQLite-এ derived table-এর নাম (AS t) ঐচ্ছিক — HAVING-এর ভেতরের subquery-টি নাম ছাড়াই চলেছে। MySQL-এ নাম বাধ্যতামূলক (Every derived table must have its own alias), PostgreSQL-এও ১৫ সংস্করণ পর্যন্ত বাধ্যতামূলক ছিল; ১৬ থেকে ঐচ্ছিক। সব জায়গায় চলবে এমন SQL লিখতে প্রতিটি derived table-এর নাম দিন।

যে অর্ডার গ্রাহকের নিজের গড়ের চেয়ে বড়

এবার derived table আর correlated subquery একসাথে: প্রতিটি অর্ডার তার নিজের গ্রাহকের গড় অর্ডারের সাথে মেলানো হবে। লক্ষ করুন, “অর্ডারের মোট” derived table-টি দুবার লিখতে হচ্ছে — একবার বাইরে, একবার ভেতরে:

SELECT o.order_id, o.customer_id, t.total
FROM orders o
JOIN (SELECT order_id, SUM(quantity * unit_price) AS total
      FROM order_items GROUP BY order_id) AS t ON t.order_id = o.order_id
WHERE t.total > (SELECT AVG(t2.total)
                 FROM orders o2
                 JOIN (SELECT order_id, SUM(quantity * unit_price) AS total
                       FROM order_items GROUP BY order_id) AS t2
                   ON t2.order_id = o2.order_id
                 WHERE o2.customer_id = o.customer_id)
ORDER BY o.order_id;

ফল তিনটি সারি: 1001 | 1 | 17400, 1007 | 2 | 3250, 1009 | 1 | 18000. Rahim-এর গড় 12393.33 — 1001 আর 1009 বড়, 1003 (1780) ছোট। Karima-র গড় 2500 — 1007 (3250) বড়, 1002 (1750) ছোট। যাঁদের একটিই অর্ডার, তাঁদের অর্ডার গড়ের সমান, বড় নয়, তাই কেউ আসেননি। কোয়েরিটি ঠিক, কিন্তু পড়তে কষ্ট — একই টুকরো দুবার, চার স্তরের বন্ধনী।

ঠিক এই সমস্যার সমাধান CTE (common table expression). WITH দিয়ে derived table-টিকে একবার নাম দিয়ে উপরে লেখা যায়, তারপর যতবার খুশি ব্যবহার করা যায়। পর্ব ২৫-এ বিস্তারিত দেখবেন; এখানে শুধু চেহারাটি দেখুন:

WITH order_totals AS (
  SELECT o.order_id, o.customer_id, SUM(i.quantity * i.unit_price) AS total
  FROM orders o JOIN order_items i ON i.order_id = o.order_id
  GROUP BY o.order_id, o.customer_id
)
SELECT order_id, customer_id, total
FROM order_totals ot
WHERE total > (SELECT AVG(total) FROM order_totals
               WHERE customer_id = ot.customer_id)
ORDER BY order_id;

৮ · Subquery বনাম JOIN — আর পারফরম্যান্স

অনেক subquery JOIN দিয়েও লেখা যায়, আর উল্টোটাও। কিন্তু দুটি সবসময় হুবহু এক অর্থ দেয় না। সবচেয়ে বড় পার্থক্য duplicate: IN আর EXISTS বাইরের প্রতিটি সারি সর্বোচ্চ একবার রাখে (semi join), কিন্তু JOIN প্রতিটি মিলের জন্য একটি করে সারি বানায়।

-- subquery: ৭টি সারি, প্রতিজন একবার
SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);

-- JOIN: ১০টি সারি — Rahim ৩ বার, Karima ২ বার
SELECT c.name FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;

-- JOIN-কে সমান করতে DISTINCT লাগে (৭টি সারি)
SELECT DISTINCT c.customer_id, c.name FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;

প্রশ্ন যদি হয় “কারা অর্ডার দিয়েছেন”, subquery সরাসরি সেটিই বলে। JOIN + DISTINCT একই ফল দেয়, কিন্তু আগে ১০টি সারি বানিয়ে তারপর duplicate মোছে। আর দুজন গ্রাহকের নাম একই হলে DISTINCT c.name তাঁদের এক করে ফেলবে — তাই key-ও রাখতে হয়।

-- NOT EXISTS
SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);

-- LEFT JOIN … IS NULL (পর্ব ১৯–২০)
SELECT c.name FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;

দুটিই anti join, দুটিই ফল দেয় Tanvir Ahmed. NOT EXISTS প্রশ্নটি সরাসরি পড়ায় (“যাঁর কোনো অর্ডার নেই”), আর NULL-এ নিরাপদ। LEFT JOIN সংস্করণে IS NULL পরীক্ষাটি এমন কলামে করতে হয় যা আসল সারিতে কখনো NULL নয় (এখানে primary key order_id).

-- SELECT তালিকায় correlated subquery
SELECT name,
       (SELECT COUNT(*) FROM orders o
        WHERE o.customer_id = c.customer_id) AS n_orders
FROM customers c;

-- LEFT JOIN + GROUP BY
SELECT c.name, COUNT(o.order_id) AS n_orders
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name;

দুটিই ৮ জনকে দেখায়, Tanvir-এর পাশে 0. JOIN সংস্করণে COUNT(*) লিখলে ভুল হতো — LEFT JOIN Tanvir-এর জন্য NULL ভরা একটি সারি বানায়, আর COUNT(*) সেটিকে ১ গুনত। পাশে একটি-দুটি মান বসাতে subquery পরিষ্কার; অনেক কলাম লাগলে JOIN.

কোনটি দ্রুত?

“Subquery ধীর, সবসময় JOIN লিখুন” — এই পরামর্শ এখনো শোনা যায়। এর উৎস মূলত পুরোনো MySQL. MySQL ৪.১ সংস্করণে (২০০৪-এর কাছাকাছি) subquery সমর্থন আসে, কিন্তু ৫.৬ (২০১৩)-এর আগে IN (subquery) প্রায়ই বাইরের প্রতিটি সারির জন্য নতুন করে চলত (EXPLAIN-এ “DEPENDENT SUBQUERY”). বড় টেবিলে তা ভয়ংকর ধীর। আধুনিক optimizer-গুলো এখন কোয়েরির অর্থ না বদলে তার গঠন বদলায়:

  • Unnesting / decorrelation: IN আর EXISTS-কে semi join-এ, NOT EXISTS-কে anti join-এ বদলানো, যাতে hash join বা merge join (পর্ব ৪২) ব্যবহার করা যায়। PostgreSQL, SQL Server, Oracle আর আধুনিক MySQL (৮.০) এগুলো করে।
  • একবার হিসাব: non-correlated subquery একবার চালিয়ে ফল রেখে দেওয়া। PostgreSQL-এর EXPLAIN-এ এটি InitPlan, আর প্রতি সারিতে চলা correlated subquery SubPlan.
  • FROM-এর subquery মিশিয়ে ফেলা: সহজ derived table-কে বাইরের কোয়েরির সাথে এক করে ফেলা (SQLite-এর ভাষায় “query flattening”).

কিন্তু সব রূপান্তর সবসময় সম্ভব নয়। NOT IN-কে anti join-এ বদলানো কঠিন, কারণ NULL থাকলে অর্থ আলাদা — উপরের EXPLAIN-এর মতো চালালে SQLite NOT IN-এর জন্য একটি “LIST SUBQUERY” বানায়, PostgreSQL একটি hashed SubPlan, আর সেটি মেমরিতে না আঁটলে প্রতি সারিতে পুরো তালিকা খোঁজে। আর অংশ ৬-এর মতো aggregate-ওয়ালা correlated subquery অনেক DBMS এখনো প্রতি সারিতে চালায়। তাই নিয়ম: যেটি পড়তে পরিষ্কার, সেটি লিখুন; তারপর বড় ডেটায় query plan দেখে নিন। EXPLAIN পড়া পর্ব ৪১ আর ৪৩-এর বিষয়।

প্রশ্নের ধরনস্বাভাবিক পছন্দকারণ
“অন্তত একটি আছে” — শুধু বাইরের কলাম লাগবেEXISTS / INduplicate নেই, অর্থ সরাসরি
“একটিও নেই”NOT EXISTSNULL-এ নিরাপদ, anti join হয়
দুই টেবিলের কলাম একসাথে দেখাতে হবেJOINsubquery শুধু একটি মান দিতে পারে
দুই স্তরের aggregatederived table / CTEআগে হিসাব, পরে ছাঁকা
গড় বা সর্বোচ্চের সাথে তুলনাscalar subqueryএকটি মান, একবার হিসাব

আরও গভীরে: Subquery-কে join-এ বদলানোর গবেষণা চার দশকের। ১৯৮২ সালে Won Kim ACM TODS-এ nested query-কে ধরন অনুযায়ী ভাগ করে (type A, N, J, JA) join-এ রূপান্তরের নিয়ম দেন। ১৯৮৭ সালে Richard Ganski আর Harry Wong দেখান, aggregate-ওয়ালা correlated subquery-তে (বিশেষত COUNT) সেই রূপান্তর কিছু ক্ষেত্রে ভুল ফল দেয় — বিখ্যাত “COUNT bug”, ভাবনার প্রশ্ন ৩-এ db-shop দিয়ে দেখবেন। ২০১৫ সালে Thomas Neumann আর Alfons Kemper “Unnesting Arbitrary Queries” প্রবন্ধে যেকোনো correlated subquery-কে নিয়মিতভাবে decorrelate করার একটি সাধারণ পদ্ধতি দেন; HyPer আর DuckDB-র মতো আধুনিক engine এই ধারায় কাজ করে।

১৯৭৪ সালে IBM-এর Donald Chamberlin আর Raymond Boyce SEQUEL ভাষার প্রস্তাব দেন, যা পরে SQL হয়। সেই প্রথম প্রস্তাবেই এক query block-এর ভেতরে আরেকটি block বসানো — অর্থাৎ nesting — ছিল ভাষার একটি কেন্দ্রীয় বৈশিষ্ট্য। Codd-এর relational algebra-র join-এর চিহ্ন সাধারণ ব্যবহারকারীর কাছে কঠিন মনে হতো; ভেতরের প্রশ্নের উত্তর দিয়ে বাইরের প্রশ্ন ছাঁকা তুলনায় স্বাভাবিক ভাষার কাছাকাছি। ফলে প্রথম দিকের SQL-এ IN আর nested SELECT ছিল প্রধান হাতিয়ার, আর পরে FROM-এ subquery (SQL-92) এবং WITH (SQL:1999) যোগ হয়।

৯ · UPDATE আর DELETE-এ subquery

পর্ব ১৪-এর UPDATE আর DELETE-এর WHERE-এও subquery বসে। এতে অন্য টেবিলের তথ্য দেখে কোন সারি বদলাবে, তা ঠিক করা যায়। নিচে দুটি কাজ: যে পণ্য কখনো কোনো অর্ডারে আসেনি, তার দাম ১০% বাড়ানো (ধরুন সরবরাহকারী নতুন দাম দিয়েছে, আর পুরোনো দামে কোনো বিক্রির হিসাব নেই); আর বাতিল অর্ডারের লাইনগুলো মুছে ফেলা।

-- কখনো বিক্রি হয়নি এমন পণ্যের দাম ১০% বাড়ান
UPDATE products SET price = ROUND(price * 1.10)
WHERE product_id NOT IN (SELECT product_id FROM order_items);

SELECT product_id, name, price FROM products WHERE product_id = 103;

-- বাতিল (cancelled) অর্ডারের লাইনগুলো মুছুন
DELETE FROM order_items
WHERE order_id IN (SELECT order_id FROM orders WHERE status = 'cancelled');

SELECT COUNT(*) AS items_left FROM order_items;

UPDATE-এ “১টি সারি বদলেছে” — শুধু Power Bank (103) কোনো অর্ডারে নেই। SELECT দেখায় 103 | Power Bank | 1980 (1800 × 1.10). ROUND না দিলে দেখতেন 1980.0000000000002 — দশমিক সংখ্যার floating-point ত্রুটি; টাকার হিসাবে তাই সাধারণত পয়সায় পূর্ণসংখ্যা বা DECIMAL টাইপ রাখা হয়। এখানে NOT IN নিরাপদ, কারণ order_items.product_id-এ NOT NULL আছে। DELETE-এ “১টি সারি বদলেছে” — বাতিল অর্ডার একটিই (1004), তাতে একটি লাইন (Saree). শেষে ১৬-র জায়গায় ১৫টি লাইন। অর্ডারটিও মুছতে চাইলে পরে DELETE FROM orders WHERE status = 'cancelled' চালাতে হবে — আগে child, তারপর parent, নইলে foreign key আটকাবে (পর্ব ৬)।

বদলানো টেবিলকেই subquery-তে পড়া। UPDATE products SET price = price * 0.9 WHERE price > (SELECT AVG(price) FROM products) — SQLite আর PostgreSQL-এ এটি চলে, আর গড় হিসাব হয় পরিবর্তনের আগের অবস্থা থেকে (standard SQL-এর নিয়ম)। কিন্তু MySQL এটি সরাসরি ফিরিয়ে দেয়: ERROR 1093: You can't specify target table 'products' for update in FROM clause. সমাধান হলো subquery-টিকে আরেকটি derived table-এ মুড়ে দেওয়া, যাতে MySQL ফলটি আগে আলাদা করে তৈরি করে নেয়। বড় UPDATE/DELETE চালানোর আগে একই WHERE দিয়ে একটি SELECT চালিয়ে সারির সংখ্যা মিলিয়ে নিন, আর transaction-এর ভেতরে চালান (পর্ব ৪৪)।

১০ · AI-এর যুগে subquery

AI-কে ইংরেজি বা বাংলায় প্রশ্ন বললে সে প্রায়ই nested subquery দিয়ে উত্তর লেখে, আর কোয়েরিটি চলেও। সমস্যা হলো, subquery-র ভুল চোখে পড়ে কম: কোনো ত্রুটি আসে না, শুধু কয়েকটি সারি কম বা বেশি আসে। নিচের ভুলগুলো বারবার দেখা যায়:

AI-এর ভুলফলকীভাবে ধরবেন
nullable কলামে NOT IN (SELECT …)ফাঁকা ফল, কোনো ত্রুটি নেইভেতরের কলামে NULL আছে কি না দেখুন; NOT EXISTS দিয়ে মিলিয়ে নিন
=-এর পাশে এমন subquery যা অনেক সারি দেয়SQLite-এ চুপচাপ একটি সারি; সার্ভারে ত্রুটিভেতরের কোয়েরিটি আলাদা চালিয়ে সারি গুনুন
correlated subquery-তে alias ভুল বা নেইশর্ত নিজের সাথে নিজের তুলনা, প্রায় সব সারি আসেপ্রতিটি কলামের আগে alias লিখুন; হাতে ২–৩টি সারি trace করুন
IN-এর বদলে JOIN, DISTINCT ছাড়াduplicate সারি, SUM দ্বিগুণদুই সংস্করণের সারি-সংখ্যা মেলান
ANY/ALL লিখেছে SQLite-এর জন্যsyntax errorMAX/MIN রূপান্তর, খালি সেট আর NULL-এর কিনারা ভেবে
নেই এমন কলাম (orders.total)ত্রুটি, বা ভুল টেবিলের কলামschema পাশে রেখে প্রতিটি নাম মিলিয়ে নিন

AI-কে কোয়েরি চাইলে schema আর NULL-এর তথ্য দিন, আর যাচাইয়ের উপায়ও চেয়ে নিন:

আমার SQLite ডেটাবেসে এই টেবিলগুলো আছে: departments(dept_id, name), students(student_id, name, dept_id NULL হতে পারে, cgpa NULL হতে পারে). প্রশ্ন: কোন বিভাগে কোনো শিক্ষার্থী নেই, আর প্রতিটি বিভাগে কারা নিজের বিভাগের গড়ের চেয়ে বেশি cgpa পেয়েছে? ১. দুটি কোয়েরি লেখো; প্রথমটি NOT EXISTS দিয়ে, আর বলো NOT IN কেন এখানে বিপজ্জনক। ২. প্রতিটি subquery correlated কি না বলো, আর কোন কলামটি বাইরের রেফারেন্স। ৩. শুধু আমার দেওয়া কলামের নাম ব্যবহার করো; অন্য কোনো কলাম ধরে নিও না। ৪. একই ফল JOIN দিয়েও লেখো, আর বলো কোন ক্ষেত্রে দুই সংস্করণের সারি-সংখ্যা আলাদা হতে পারে। ৫. NULL বিভাগ আর NULL cgpa-ওয়ালা শিক্ষার্থী নিয়ে ৩টি পরীক্ষার ডেটা দাও, প্রত্যাশিত ফলসহ।

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

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

  • Correlated আর non-correlated subquery-র পার্থক্য? — Correlated subquery বাইরের কোয়েরির কলাম ব্যবহার করে, তাই যুক্তিতে প্রতিটি বাইরের সারির জন্য চলে; non-correlated একবার চলে।
  • দ্বিতীয় সর্বোচ্চ বেতন বের করুন। — SELECT MAX(salary) FROM t WHERE salary < (SELECT MAX(salary) FROM t).
  • NOT IN আর NOT EXISTS কি সবসময় একই ফল দেয়? — না; subquery-র ফলে NULL থাকলে NOT IN কোনো সারি দেয় না, NOT EXISTS ঠিক ফল দেয়।
  • EXISTS-এর ভেতরে SELECT * না SELECT 1? — ফল একই; EXISTS শুধু সারি আছে কি না দেখে, কলাম নয়।
  • FROM-এর subquery-কে কী বলে, কখন লাগে? — Derived table (inline view); দুই স্তরের aggregate বা আগে হিসাব করে পরে ছাঁকতে।

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

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

প্র ০১PostgreSQL-এর অফিসিয়াল উইকির “Don't Do This” পাতায় বলা আছে NOT IN ব্যবহার না করতে। অথচ NOT IN SQL standard-এর অংশ, আর পড়তেও সহজ। এই পরামর্শের পেছনে কী কী কারণ?

দুটি কারণ: একটি অর্থের, একটি গতির। অর্থের কারণটি এই পর্বে দেখেছেন। x NOT IN (subquery) আসলে অনেকগুলো x <> v-এর AND. subquery-র ফলে একটিও NULL থাকলে প্রতিটি সারির ফল FALSE বা UNKNOWN হয়, কখনো TRUE নয়। তাই পুরো কোয়েরি ফাঁকা ফল দেয়, কোনো ত্রুটি ছাড়া। বিপদটা আরও বড়, কারণ আজ কলামে NULL না থাকলেও কাল একটি নতুন সারি এসে নীরবে রিপোর্ট ফাঁকা করে দিতে পারে। db-uni-তে Habib-এর একটি NULL বিভাগই “শিক্ষার্থীহীন বিভাগ” রিপোর্টকে শূন্য করে দিয়েছিল।

গতির কারণটি এর থেকেই আসে। NOT EXISTS-কে optimizer নিশ্চিন্তে anti join-এ বদলাতে পারে: বাইরের প্রতিটি সারির জন্য “মিল আছে কি?” — hash join বা merge join দিয়ে একবারে। কিন্তু NOT IN-এর অর্থ NULL-এর ওপর নির্ভর করে, তাই সাধারণ anti join এর সমতুল্য নয়। PostgreSQL তাই NOT IN-এর জন্য subquery-র ফল একটি hash table-এ রাখে (hashed SubPlan); ফল মেমরিতে (work_mem) না আঁটলে প্রতিটি বাইরের সারির জন্য পুরো তালিকা খোঁজে — বড় টেবিলে যা প্রায় দ্বিঘাত সময় নেয়। তিন-মানের যুক্তির খরচ এখানে পারফরম্যান্সেও পড়ছে।

তবে NOT IN নিষিদ্ধ নয়। হাতে লেখা ছোট তালিকায় (status NOT IN ('cancelled', 'pending')) এটি নিরাপদ আর পরিষ্কার, কারণ তালিকায় NULL নেই। subquery-র কলামে NOT NULL constraint থাকলেও অর্থের দিক থেকে নিরাপদ, আর কিছু optimizer তখন anti join-ও করতে পারে। সমস্যা শুধু nullable কলামের subquery-তে, আর সেখানে NOT EXISTS প্রশ্নটি আরও সরাসরি বলে: “এমন কোনো সারি নেই যেখানে…”।

এই বিতর্কের মূলে আছে SQL-এর NULL নিয়ে দীর্ঘ মতভেদ। Codd নিজে পরে দুই রকম NULL (অজানা আর প্রযোজ্য নয়) প্রস্তাব করেছিলেন। আর C. J. Date-এর মতো সমালোচকেরা বহু বছর ধরে লিখেছেন, তিন-মানের যুক্তি এমন ফল দেয় যা মানুষের স্বাভাবিক যুক্তির সাথে মেলে না। NOT IN-এর ফাঁদ এর সবচেয়ে পরিচিত উদাহরণ।

মূল উপলব্ধি: “কোনটি নেই” প্রশ্নে NOT EXISTS লিখুন। এটি NULL-এ নিরাপদ, আর optimizer-এর হাতেও বেশি স্বাধীনতা দেয়।

প্র ০২একজন সিনিয়র ডেভেলপার বলছেন, “subquery সবসময় ধীর, সবসময় JOIN লিখবে।” আরেকজন বলছেন, “এখন optimizer সব সামলায়, যা খুশি লেখো।” কার কথা কতটা ঠিক?

দুজনের কথাতেই সত্য আছে, কিন্তু কোনোটিই পুরো সত্য নয়। প্রথম মতের উৎস ইতিহাস। MySQL-এ subquery আসে ৪.১ সংস্করণে, আর ৫.৬ (২০১৩)-এর আগে optimizer WHERE id IN (SELECT …)-কে প্রায়ই correlated EXISTS-এ বদলে বাইরের প্রতিটি সারির জন্য চালাত — EXPLAIN-এ “DEPENDENT SUBQUERY”. দশ লাখ সারির টেবিলে কোয়েরি মিনিটের পর মিনিট চলত, অথচ একই প্রশ্ন JOIN দিয়ে লিখলে সেকেন্ডে শেষ হতো। সেই অভিজ্ঞতা থেকে একটি প্রজন্মের ডেভেলপার নিয়ম বানিয়ে নেন: subquery মানেই ধীর।

আজকের ছবি আলাদা। MySQL ৫.৬ থেকে semi join আর derived table materialization, ৮.০-এ anti join সহ আরও রূপান্তর এসেছে। PostgreSQL IN/EXISTS-কে semi join, NOT EXISTS-কে anti join বানায়, আর সহজ FROM-subquery মিশিয়ে দেয়। SQL Server আর Oracle-এর unnesting আরও বিস্তৃত। ফলে সহজ IN/EXISTS আর সমতুল্য JOIN প্রায়ই হুবহু একই plan পায়।

কিন্তু “optimizer সব সামলায়” কথাটিও বাড়াবাড়ি। প্রথমত, কিছু রূপান্তর অর্থের কারণেই অসম্ভব — nullable কলামে NOT IN, বা ভাবনার প্রশ্ন ৩-এর COUNT-ওয়ালা correlated subquery. দ্বিতীয়ত, aggregate-ওয়ালা correlated scalar subquery অনেক DBMS এখনো প্রতি সারিতে চালায়; correlated কলামে index না থাকলে খরচ বাইরের সারি × ভেতরের সারি। তৃতীয়ত, optimizer-এর সিদ্ধান্ত নির্ভর করে statistics-এর ওপর (পর্ব ৪৩); পুরোনো statistics-এ ভালো রূপান্তরও খারাপ plan দিতে পারে।

আরেকটি কথা: JOIN আর subquery সবসময় সমতুল্য নয়। IN-কে JOIN-এ বদলালে duplicate আসে, আর তার ওপর SUM করলে টাকার হিসাব দ্বিগুণ হয়ে যায়। “দ্রুত” কিন্তু ভুল কোয়েরির কোনো মূল্য নেই।

পেশাদার পথ তাই তিন ধাপের: প্রশ্নের অর্থ যেভাবে সবচেয়ে পরিষ্কার দেখায়, সেভাবে লিখুন (অস্তিত্বের প্রশ্নে EXISTS, কলাম জোড়ায় JOIN); বাস্তব আকারের ডেটায় EXPLAIN দেখুন; আর ধীর হলে তবেই বিকল্প রূপে লিখে তুলনা করুন।

মূল উপলব্ধি: SQL একটি declarative ভাষা — আপনি বলেন “কী চাই”, DBMS ঠিক করে “কীভাবে”। গতির প্রশ্নের উত্তর পাবেন plan-এ, লোককথায় নয়।

প্র ০৩“COUNT bug” কী? db-shop-এ “যে পণ্যের stock ঠিক ততটি, যতবার সেটি অর্ডারের লাইনে এসেছে” — এই প্রশ্ন দিয়ে দেখান, correlated subquery-কে সরল JOIN + GROUP BY-তে বদলালে কীভাবে ফল বদলে যায়।

Correlated রূপে প্রশ্নটি হলো:

SELECT p.name, p.stock FROM products p
WHERE p.stock = (SELECT COUNT(*) FROM order_items i
                 WHERE i.product_id = p.product_id);

ফল: Power Bank | 0. Power Bank-এর stock 0, আর সে কোনো অর্ডারে আসেনি। ভেতরের COUNT(*) একটি ফাঁকা সেটের ওপর চলে, আর ফাঁকা সেটের COUNT হলো 0, NULL নয়। 0 = 0, তাই সারিটি থাকে।

এবার Won Kim-এর ১৯৮২ সালের ধারায় “সরল” রূপান্তর: আগে order_items থেকে প্রতিটি পণ্যের COUNT একটি derived table-এ, তারপর JOIN:

SELECT p.name, p.stock FROM products p
JOIN (SELECT product_id, COUNT(*) AS n FROM order_items
      GROUP BY product_id) t ON t.product_id = p.product_id
WHERE p.stock = t.n;

ফল: কোনো সারি নেই! GROUP BY শুধু সেই পণ্যগুলোর দল বানায় যাদের অন্তত একটি লাইন আছে। Power Bank-এর কোনো লাইন নেই, তাই derived table-এ তার কোনো সারিই নেই — “0” মানটি তৈরিই হয়নি। Inner JOIN তাকে বাদ দেয়। SUM, MAX বা AVG হলে সমস্যা হতো না, কারণ ফাঁকা সেটে সেগুলো NULL দেয়, আর NULL-এর সাথে তুলনা এমনিতেই সারি বাদ দেয়। COUNT-ই একমাত্র aggregate যা ফাঁকা সেটে একটি আসল মান (0) দেয়।

১৯৮৭ সালে Richard Ganski আর Harry Wong SIGMOD-এ এই ত্রুটিটি দেখান, আর নাম হয়ে যায় “COUNT bug”. সমাধান: inner JOIN-এর বদলে LEFT JOIN, আর NULL-কে 0 বানানো — COALESCE(t.n, 0). পরে বহু গবেষণায় দেখা যায়, decorrelation-এর প্রতিটি নিয়মকে এমন কিনারায় প্রমাণ করতে হয়। ২০১৫ সালে Neumann ও Kemper-এর সাধারণ পদ্ধতিও এই সমস্যা মাথায় রেখেই তৈরি।

আপনার জন্য শিক্ষাটি ব্যবহারিক। AI বা সহকর্মী “এই subquery ধীর, JOIN-এ বদলে দিলাম” বললে বদলানো সংস্করণটি অবশ্যই এমন সারি দিয়ে পরীক্ষা করুন, যার কোনো মিল নেই।

মূল উপলব্ধি: কোয়েরি রূপান্তরের আসল পরীক্ষা হয় ফাঁকা দলে। COUNT সেখানে 0 দেয়, কিন্তু JOIN সেই দলকে অস্তিত্বই দেয় না।

প্র ০৪একই ভুল কোয়েরিতে SQLite চুপচাপ প্রথম সারি নেয়, PostgreSQL ত্রুটি দেয়। SQLite এমন “উদার” কেন, আর নিজের অ্যাপে কীভাবে এর থেকে নিজেকে বাঁচাবেন?

SQLite-এর ডিজাইন-দর্শনই শিথিলতার দিকে। এটি একটি embedded library: মোবাইল ফোন, ব্রাউজার, ছোট ডিভাইসের ভেতরে চলে, আর সেখানে “কোনোভাবে উত্তর দেওয়া, না থামা” অনেক সময় গুরুত্বপূর্ণ মনে করা হয়েছে। এর আরও উদাহরণ আছে: INTEGER কলামে লেখা রাখতে দেওয়া (type affinity, পর্ব ১৩), GROUP BY ছাড়া কলাম SELECT-এ রাখতে দেওয়া, আর scalar subquery-র প্রথম সারি নেওয়া। SQLite-এর ডকুমেন্টেশন নিজেই স্বীকার করে, এর কিছু আচরণ পুরোনো দিনের সিদ্ধান্ত, যা এখন বদলালে লাখো অ্যাপ ভেঙে যাবে। তাই সেগুলো রয়ে গেছে।

PostgreSQL উল্টো দর্শনে চলে: অস্পষ্ট বা সম্ভবত ভুল অনুরোধে ত্রুটি দেওয়া, যাতে ভুল ডেভেলপারের টেবিলেই ধরা পড়ে, গ্রাহকের রিপোর্টে নয়। Standard SQL-ও এই পক্ষে: scalar subquery-র ফলে একাধিক সারি হলে সেটি একটি cardinality violation ত্রুটি। MySQL, SQL Server আর Oracle সবাই ত্রুটি দেয়।

বাস্তব বিপদটা ভাবুন। একটি দোকানের অ্যাপে লেখা আছে WHERE price = (SELECT price FROM products WHERE name = 'Panjabi'). যতদিন একটি Panjabi আছে, সব ঠিক। একদিন “Panjabi” নামে দ্বিতীয় পণ্য (অন্য রঙ) যোগ হলো। PostgreSQL-এ অ্যাপ সেদিনই ত্রুটি দেখাবে, ডেভেলপার ঠিক করবেন। SQLite-এ অ্যাপ চলতেই থাকবে, কিন্তু কোন Panjabi-র দাম নেওয়া হচ্ছে তা নির্ভর করবে টেবিল পড়ার ক্রমের ওপর — index বদলালে উত্তরও বদলাবে।

নিজেকে বাঁচানোর উপায়: (১) scalar subquery-কে গঠনগতভাবে এক সারির বানান — primary key বা UNIQUE কলামে =, অথবা aggregate (MAX, MIN, COUNT). (২) সত্যিই “যেকোনো একটি” চাইলে সেটি স্পষ্ট লিখুন: ORDER BY … LIMIT 1. (৩) “অনেকের যেকোনোটি” বোঝালে IN বা EXISTS ব্যবহার করুন। (৪) অ্যাপ পরে PostgreSQL বা MySQL-এ যাবে জানলে সেখানেও একবার test চালান। SQLite ৩.৩৭ (২০২১) থেকে STRICT টেবিল টাইপের শিথিলতা কমায়, কিন্তু subquery-র এই আচরণ বদলায় না।

মূল উপলব্ধি: যে DBMS ত্রুটি দেয়, সে আসলে আপনাকে সাহায্য করছে। নীরব DBMS-এ সেই পাহারা আপনাকেই দিতে হবে — চুক্তিটি (“একটিই সারি”) কোয়েরির গঠনেই লিখে।

প্র ০৫“যে শিক্ষার্থী CSE-র সব 300-স্তরের কোর্স নিয়েছেন” — এমন “সব” প্রশ্ন SQL-এ সরাসরি লেখার উপায় নেই (FOR ALL নেই)। দুই স্তরের NOT EXISTS কীভাবে এই কাজটি করে, আর তত্ত্বে এর ভিত্তি কী?

SQL-এ EXISTS আছে (∃, “অন্তত একটি”), কিন্তু “প্রতিটির জন্য” (∀) নেই। যুক্তিবিদ্যার একটি পুরোনো সমতুল্যতা কাজে আসে: “প্রতিটি কোর্স সে নিয়েছে” মানে “এমন কোনো কোর্স নেই, যা সে নেয়নি”। প্রতীকে ∀c P(c) ≡ ¬∃c ¬P(c). দুটি “না” পাশাপাশি — তাই দুই স্তরের NOT EXISTS:

SELECT s.name FROM students s
WHERE NOT EXISTS (                       -- এমন কোনো কোর্স নেই…
  SELECT 1 FROM courses c
  WHERE c.course_id LIKE 'CSE3%'
    AND NOT EXISTS (                     -- …যা s নেয়নি
      SELECT 1 FROM enrollments e
      WHERE e.student_id = s.student_id
        AND e.course_id = c.course_id));

db-uni-তে CSE-র 300-স্তরের কোর্স দুটি: CSE301 আর CSE302. ফল: Arif Hossain — শুধু তিনিই দুটিই নিয়েছেন। Bristy আর Dilruba CSE301 নিয়েছেন, CSE302 নয়; তাঁদের জন্য ভেতরে CSE302 “না-নেওয়া কোর্স” হিসেবে পাওয়া যায়, তাই বাইরের NOT EXISTS FALSE. শর্ত বদলে “CSE বিভাগের সব কোর্স” (dept_id = 1, চারটি কোর্স) করলে ফল ফাঁকা হয় — কেউই চারটিই নেননি। Arif-এর CSE101 নেই, Dilruba-র CSE302 নেই।

তত্ত্বে এটি relational algebra-র division (÷) অপারেশন, যা পর্ব ৩৩-এ দেখবেন: enrollments(student, course) ÷ cse3_courses(course) = যাঁরা ভাজকের সব course-এর সাথে জোড়া বেঁধেছেন। আর SQL-এর এই লেখার ধরনটি সরাসরি এসেছে tuple relational calculus থেকে (পর্ব ৩৪), যেখানে ∀ আর ∃ দুটিই আছে। Codd ১৯৭২ সালে দেখান, algebra আর calculus সমান শক্তিশালী — Codd-এর উপপাদ্য; SQL তার calculus-ঘেঁষা রূপ।

একই প্রশ্নের আরেকটি সমাধান গোনা দিয়ে: প্রতিটি শিক্ষার্থীর নেওয়া CSE3 কোর্স গুনে মোট CSE3 কোর্সের সংখ্যার সাথে মেলানো (GROUP BY … HAVING COUNT(DISTINCT course_id) = (SELECT COUNT(*) …)). এটি অনেকের কাছে পড়তে সহজ, তবে DISTINCT ভুলে গেলে আর একই কোর্স দুবার নেওয়া থাকলে ভুল হয়। আর ভাজক ফাঁকা হলে দুই পদ্ধতির ফল আলাদা হতে পারে।

মূল উপলব্ধি: “সব” = “একটিও বাদ নেই”। দুই স্তরের NOT EXISTS হলো SQL-এ ∀ লেখার উপায়, আর এটিই relational division-এর সবচেয়ে প্রচলিত SQL রূপ।

অনুশীলন

উপরের যেকোনো db-uni এডিটরে কোড বদলে নিজের উত্তর চালিয়ে দেখতে পারেন।

  1. কোয়েরি লিখুন: db-uni-তে কোন শিক্ষকের (instructors) কোনো কোর্স নেই? courses টেবিলের instructor_id কলামে কিন্তু একটি NULL আছে (ENG101-এর শিক্ষক ঠিক হয়নি)। আগে NOT IN দিয়ে লিখে দেখুন কী আসে, তারপর সঠিক কোয়েরি লিখুন।
    SELECT i.name FROM instructors i
    WHERE NOT EXISTS (SELECT 1 FROM courses c
                      WHERE c.instructor_id = i.instructor_id);

    ফল: Rumana Islam (instructor 4). courses-এ শিক্ষক 2, 1, 6, 6, 3, 5 আর NULL — 4 নেই। WHERE instructor_id NOT IN (SELECT instructor_id FROM courses) লিখলে ফল “(কোনো সারি নেই)”, কারণ ENG101-এর NULL প্রতিটি তুলনাকে UNKNOWN করে দেয়। NOT IN রাখতে চাইলে ভেতরে WHERE instructor_id IS NOT NULL লাগবে।

  2. ফল আগে বলুন (trace): টেবিল t(x)-এ তিনটি সারি: 5, 10, NULL. টেবিল u(y)-এ দুটি সারি: 10, NULL. প্রতিটির ফলে কয়টি সারি? (ক) SELECT x FROM t WHERE x IN (SELECT y FROM u) (খ) SELECT x FROM t WHERE x NOT IN (SELECT y FROM u) (গ) SELECT x FROM t WHERE NOT EXISTS (SELECT 1 FROM u WHERE u.y = t.x) (ঘ) SELECT x FROM t WHERE x > (SELECT MAX(y) FROM u WHERE y > 100)

    (ক) ১টি সারি: 10. 5-এর জন্য: 5 = 10 FALSE, 5 = NULL UNKNOWN → UNKNOWN, বাদ। 10 মেলে → TRUE. NULL-এর জন্য সব তুলনা UNKNOWN, বাদ।

    (খ) ০টি সারি। 5: NOT (UNKNOWN) = UNKNOWN. 10: NOT TRUE = FALSE. NULL: UNKNOWN. u-তে NULL থাকায় কেউ TRUE পায় না।

    (গ) ২টি সারি: 5 আর NULL. 5-এর জন্য ভেতরে u.y = 5 কোনো সারিতে TRUE নয় → NOT EXISTS TRUE. 10-এর মিল আছে → বাদ। t-এর NULL-এর জন্য u.y = NULL কোথাও TRUE নয় → NOT EXISTS TRUE, তাই NULL সারিটিও আসে! NOT EXISTS আর NOT IN কিনারায় দুই দিকেই আলাদা।

    (ঘ) ০টি সারি। y > 100 এমন সারি নেই, MAX ফাঁকা সেটে NULL, তাই প্রতিটি তুলনা UNKNOWN. PostgreSQL-এ আসল x > ALL (…) লিখলে ফাঁকা সেটে কোনো তুলনাই হয় না, ফল সরাসরি TRUE — তখন NULL-সহ ৩টি সারিই আসত।

  3. রূপান্তর করুন: “নিজের বিভাগের গড়ের চেয়ে বেশি cgpa” কোয়েরিটি correlated subquery ছাড়া লিখুন — বিভাগভিত্তিক গড়ের একটি derived table বানিয়ে JOIN করে। দুই সংস্করণের ফল কি সবসময় এক হবে? Habib (বিভাগ NULL)-এর কী হবে?
    SELECT s.name, s.cgpa, d.avg_cgpa
    FROM students s
    JOIN (SELECT dept_id, AVG(cgpa) AS avg_cgpa
          FROM students GROUP BY dept_id) AS d ON d.dept_id = s.dept_id
    WHERE s.cgpa > d.avg_cgpa;

    ফল একই: Arif (3.75, 3.65) আর Dilruba (3.9, 3.65). এখানে প্রতিটি বিভাগের গড় একবারই হিসাব হয় — ৮ বারের বদলে ৪টি দল, তাই বড় ডেটায় সাধারণত দ্রুত। Habib: GROUP BY NULL dept_id-এর জন্যও একটি দল বানায় (গড় 2.80), কিন্তু JOIN-এর NULL = NULL UNKNOWN, তাই তিনি বাদ পড়েন — correlated সংস্করণের মতোই। এখানে COUNT নেই, তাই প্রশ্ন ৩-এর COUNT bug-এর ঝুঁকিও নেই; AVG ফাঁকা দলে NULL দিত, যা এমনিতেই বাদ পড়ে।

  4. ইন্টারভিউ সমস্যা: db-uni-র instructors টেবিলে (instructor_id, name, salary) দ্বিতীয় সর্বোচ্চ বেতনের শিক্ষকের নাম আর বেতন বের করুন। তারপর এমন একটি correlated সংস্করণ লিখুন, যা 2-এর জায়গায় যেকোনো N বসালে N-তম সর্বোচ্চ দেয়।
    -- দ্বিতীয় সর্বোচ্চ
    SELECT name, salary FROM instructors
    WHERE salary = (SELECT MAX(salary) FROM instructors
                    WHERE salary < (SELECT MAX(salary) FROM instructors));
    
    -- N-তম সর্বোচ্চ (এখানে N = 2): নিজের সমান বা বড় আলাদা বেতন ঠিক N টি
    SELECT name, salary FROM instructors i
    WHERE 2 = (SELECT COUNT(DISTINCT salary) FROM instructors i2
               WHERE i2.salary >= i.salary);

    দুটিরই ফল: Dr. Mahbub Alam | 110000. বেতনগুলো 120000, 110000, 90000, 85000, 70000, 65000. দ্বিতীয় কোয়েরিতে Mahbub-এর জন্য ≥ 110000 আলাদা বেতন দুটি (120000, 110000), তাই শর্ত মেলে। DISTINCT থাকায় সমান বেতন একবারই গোনা হয় (DENSE_RANK-এর মতো), আর একই বেতনের দুজন থাকলে দুজনই আসবেন। এটি correlated, তাই যুক্তিতে O(n²); বড় টেবিলে পর্ব ২৬-এর DENSE_RANK() ভালো বিকল্প।

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

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

ক্রমএকটি correlated subquery (WHERE-এ) যুক্তির দিক থেকে কোন ক্রমে মূল্যায়ন হয়, সাজান (▲▼ চেপে সরান)

  1. বাইরের কোয়েরির FROM থেকে একটি সারি নেওয়া
  2. সেই সারির কলামের মান ভেতরের কোয়েরির বাইরের রেফারেন্সে বসানো
  3. ভেতরের কোয়েরি চালিয়ে তার ফল পাওয়া
  4. সেই ফল দিয়ে বাইরের WHERE-এর শর্ত TRUE কি না দেখা
  5. TRUE হলে সারিটি রাখা, তারপর পরের বাইরের সারিতে যাওয়া

departments টেবিলে dept_id 1, 2, 3, 4. students টেবিলের dept_id কলামে মান 1, 2, 3 আর একটি NULL. SELECT dept_id FROM departments WHERE dept_id NOT IN (SELECT dept_id FROM students); কয়টি সারি দেবে?

  • ১টি (4)
  • ৪টি
  • ০টি
  • ত্রুটি দেবে

SQLite-এ SELECT name FROM products WHERE price = (SELECT price FROM products WHERE category_id = 1); চালানো হলো, আর category 1-এ চারটি পণ্য আছে। কী হবে?

  • “more than one row returned by a subquery” ত্রুটি
  • ত্রুটি হবে না; subquery-র প্রথম সারির দাম দিয়ে তুলনা হবে
  • চারটি দামের যেকোনোটির সমান পণ্য আসবে, IN-এর মতো
  • subquery-র মান NULL ধরে কোনো সারি আসবে না

কোনটি correlated subquery-র সংজ্ঞা?

  • FROM অংশে বসানো যেকোনো subquery
  • যে subquery একাধিক সারি ফেরত দেয়
  • যে subquery-তে aggregate function আছে
  • যে subquery বাইরের কোয়েরির কোনো কলাম ব্যবহার করে

SQLite-এ ANY/ALL নেই। price > ALL (SELECT price FROM products WHERE category_id = 2)-এর সবচেয়ে কাছের রূপান্তর কোনটি?

  • price > (SELECT MAX(price) FROM products WHERE category_id = 2)
  • price > (SELECT MIN(price) FROM products WHERE category_id = 2)
  • price IN (SELECT price FROM products WHERE category_id = 2)
  • price > (SELECT AVG(price) FROM products WHERE category_id = 2)

customers আর orders টেবিলে একজন গ্রাহকের তিনটি অর্ডার আছে। “যাঁরা অন্তত একটি অর্ডার দিয়েছেন” — এই প্রশ্নে WHERE customer_id IN (SELECT customer_id FROM orders) আর DISTINCT ছাড়া customers JOIN orders-এর মধ্যে মূল পার্থক্য কী?

  • IN সংস্করণ NULL-এর কারণে কিছু গ্রাহক বাদ দেয়
  • দুটির ফল সবসময় হুবহু এক
  • JOIN সংস্করণে সেই গ্রাহক তিনবার আসেন, IN সংস্করণে একবার
  • IN সংস্করণ শুধু PostgreSQL-এ চলে

WHERE EXISTS (SELECT 1 FROM students s WHERE s.dept_id = d.dept_id)-এ SELECT 1-এর বদলে SELECT * বা SELECT NULL লিখলে কী হয়?

  • SELECT NULL দিলে EXISTS সবসময় FALSE হয়
  • ফল একই থাকে, কারণ EXISTS শুধু সারি আছে কি না দেখে
  • SELECT * দিলে syntax error হয়
  • SELECT NULL দিলে EXISTS UNKNOWN হয়

“প্রতিটি গ্রাহকের গড় অর্ডারের মূল্য” বের করতে আগে প্রতি অর্ডারের মোট (SUM) আর তারপর গ্রাহকভিত্তিক গড় (AVG) লাগে। কোন পদ্ধতি সঠিক?

  • SELECT customer_id, AVG(SUM(quantity * unit_price)) একটি GROUP BY-তে
  • WHERE-এ SUM(...) > AVG(...) লেখা
  • order_items-এ JOIN করে লাইনগুলোর সরাসরি AVG নেওয়া
  • FROM-এ একটি derived table-এ প্রতি অর্ডারের SUM, তারপর বাইরে গ্রাহকভিত্তিক AVG

শব্দকোষ

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

মনে রাখুন

  1. Subquery হলো বন্ধনীর ভেতরের SELECT; বসে WHERE, SELECT তালিকা, FROM (derived table), HAVING আর UPDATE/DELETE-এ।
  2. Scalar subquery একটি মান দেয়; একাধিক সারি এলে PostgreSQL/MySQL ত্রুটি দেয়, SQLite চুপচাপ প্রথম সারি নেয় — aggregate, key বা LIMIT 1 দিয়ে এক সারি নিশ্চিত করুন।
  3. ভেতরের ফলে একটি NULL থাকলেই NOT IN ফাঁকা ফল দেয়; “কোনটি নেই” প্রশ্নে NOT EXISTS লিখুন।
  4. Correlated subquery বাইরের কলাম ব্যবহার করে আর যুক্তিতে প্রতি সারিতে চলে; SQLite-এ ANY/ALL-এর বদলে MAX/MIN, কিন্তু খালি সেট আর NULL-এর কিনারা মনে রাখুন।
  5. IN/EXISTS-এ duplicate হয় না, JOIN-এ হয়; যেটি পরিষ্কার সেটি লিখুন, আর EXPLAIN দিয়ে গতি যাচাই করুন। বারবার একই derived table লাগলে CTE.

পরের পর্বে: Subquery একটি কোয়েরির ভেতরে আরেকটি বসায়। কিন্তু দুটি কোয়েরির ফল যদি পাশাপাশি জুড়তে, মিলিয়ে নিতে বা বাদ দিতে হয়? পর্ব ২২-এ UNION, INTERSECT আর EXCEPT — আর দেখবেন EXCEPT কীভাবে NOT EXISTS-এর আরেকটি রূপ।

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

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