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

SQL output prediction — কোয়েরির ফল হাতে বের করা

SQL Output Prediction Drills — NULL Logic, GROUP BY, Joins, Subqueries, Set Operations, Window Functions, CTEs, Data Changes and Transactions

Taught in Bangla, this page offers SQL output prediction drills: a method for working out query results by hand, then drills on SELECT, ORDER BY, LIMIT, DISTINCT and CASE, NULL and three-valued logic, GROUP BY, HAVING and aggregates, join row counts and fan-out, subqueries, set operations and CTEs, window functions, and UPDATE, DELETE, constraints, triggers and transactions, each checked with live SQL.

এই পর্বে যা পাবেন

  • যেকোনো SELECT হাতে চালানোর একটি নির্ভরযোগ্য পদ্ধতি — যৌক্তিক ক্রম, প্রতিটি ধাপে সারি গোনা আর NULL-এর হিসাব
  • NULL, NOT IN, COUNT(*) বনাম COUNT(col), AVG আর empty set-এর সবচেয়ে প্রচলিত ফাঁদ চেনা
  • GROUP BY/HAVING, সব রকম JOIN, fan-out, subquery, UNION/EXCEPT আর recursive CTE-এর ফল নির্ভুলভাবে বের করা
  • ROW_NUMBER, RANK, DENSE_RANK, running total আর LAG-এর ফল টাই আর NULL-সহ বলা
  • UPDATE/DELETE-এ কয়টি সারি বদলায়, কোন স্টেটমেন্ট constraint ভাঙে, আর ROLLBACK/SAVEPOINT-এর পরে টেবিলের চূড়ান্ত অবস্থা

১ · মডিউল ১০ শুরু — কোয়েরির ফল হাতে বের করার পদ্ধতি

বিশ্ববিদ্যালয়ের Database Systems পরীক্ষা, চাকরির লিখিত পরীক্ষা আর ডেটা অ্যানালিস্ট বা ব্যাকএন্ড ইন্টারভিউ — সবখানে এক ধরনের প্রশ্ন প্রায়ই আসে: “এই টেবিলে এই কোয়েরি চালালে কী ফল আসবে?” এখানে কম্পিউটার নেই; ফলটি আপনাকে মাথায় আর খাতায় বের করতে হয়। এই ধরনের প্রশ্নকে বলা হয় output prediction. এটি শুধু পরীক্ষার কৌশল নয়। কাজের জায়গায় একটি রিপোর্ট ভুল সংখ্যা দেখালে প্রথম কাজই হলো কোয়েরিটি হাতে চালিয়ে দেখা, কোন ধাপে সারি হারাল বা বেড়ে গেল।

মুদি দোকানের মাসশেষের হিসাব মেলানোর কথা ভাবুন। ক্যাশবাক্সের টাকা খাতার সাথে মিলছে না। অভিজ্ঞ দোকানি পুরো মাস একবারে দেখেন না — প্রতিটি পাতার যোগফল আলাদা করে লেখেন, তারপর কোন পাতায় অমিল, সেখানে থামেন। SQL-এর ফল হাতে বের করাও একই কাজ: প্রতিটি ধাপের পরে কয়টি সারি রইল, তা লিখে রাখা। ভুলটি সবসময় কোনো একটি ধাপে লুকিয়ে থাকে।

কোয়েরি যে ক্রমে লেখা হয় (SELECT আগে), DBMS তাকে সেই ক্রমে বোঝে না। প্রতিটি অংশের অর্থ ঠিক হয় একটি নির্দিষ্ট logical processing order দিয়ে (পর্ব ১৫ আর ১৮)। আসল ইঞ্জিন ভেতরে অন্য ক্রমে চালাতে পারে, কিন্তু ফল সবসময় এই যৌক্তিক ক্রমের ফলের সমান হয়। তাই হাতে হিসাবের সময় এই ক্রমটিই আপনার মানচিত্র।

পদ্ধতির বাক্স

১. ক্রম মেনে চলুন: FROM (আর JOIN) → WHERE → GROUP BY → HAVING → SELECT (window function এখানে) → DISTINCT → ORDER BY → LIMIT/OFFSET. প্রতিটি ধাপ আগের ধাপের ফল-টেবিল নেয়, নতুন একটি টেবিল দেয়।

২. প্রতিটি ধাপে সারি গুনুন: খাতায় একটি ছোট trace table রাখুন — ধাপের নাম আর সারির সংখ্যা। JOIN-এর পরে সংখ্যা বাড়তে পারে (একটি সারি একাধিক সারির সাথে মেলে), WHERE আর HAVING কেবল কমায়, GROUP BY দলের সংখ্যায় নামায়।

৩. NULL আলাদা রঙে লিখুন: প্রতিটি NULL-এর পাশে দাগ দিন। তুলনায় NULL মানে UNKNOWN, আর WHERE/HAVING শুধু TRUE রাখে। aggregate (COUNT(*) ছাড়া) NULL বাদ দিয়ে হিসাব করে। GROUP BY, DISTINCT আর UNION সব NULL-কে এক দলে রাখে।

৪. টাই আর ক্রম যাচাই করুন: ORDER BY না থাকলে সারির ক্রম অনির্দিষ্ট — উত্তরে “ক্রম নিশ্চিত নয়” লিখুন। ORDER BY-তে টাই থাকলে LIMIT কোন সারি নেবে, তা-ও অনির্দিষ্ট।

৫. DBMS জেনে নিন: প্রশ্নে কোন DBMS তা দেখুন। NULL-এর ক্রম, পূর্ণসংখ্যার ভাগ, তারিখ আর GROUP BY-এর নিয়ম DBMS ভেদে আলাদা। না বলা থাকলে standard SQL ধরে উত্তর দিন এবং পার্থক্যটি এক লাইনে লিখে দিন।

এই পাতার বেশিরভাগ প্রশ্ন দুটি ছোট টেবিলের ওপর। emp হলো একটি অফিসের ছয়জন কর্মী — id, নাম, বিভাগ (dept) আর বেতন (salary). দুটি NULL ইচ্ছে করে রাখা হয়েছে: Kamal-এর বিভাগ জানা নেই, Lipi-র বেতন জানা নেই। dept হলো বিভাগের তালিকা — code আর title. খেয়াল করুন, BBA বিভাগে কোনো কর্মী নেই। এই দুটি টেবিল মুখস্থ করে ফেলুন; পরের প্রশ্নগুলোতে বারবার আসবে।

emp                                    dept
 id │ name  │ dept │ salary             code │ title
 1  │ Rina  │ CSE  │ 50000              CSE  │ Computer
 2  │ Sumon │ CSE  │ 40000              EEE  │ Electrical
 3  │ Tania │ EEE  │ 40000              BBA  │ Business
 4  │ Kamal │ NULL │ 30000
 5  │ Lipi  │ EEE  │ NULL
 6  │ Babu  │ CSE  │ 50000

দুটি টেবিল SQL-এ এভাবে বানানো (এডিটরগুলোতে এগুলো আগে থেকেই চলে, “নমুনা ডেটাবেস” বাক্স খুলে দেখতে পারবেন): emp(id INTEGER PRIMARY KEY, name TEXT, dept TEXT, salary INTEGER) আর dept(code TEXT PRIMARY KEY, title TEXT). এবার পদ্ধতিটি একটি কঠিন কোয়েরিতে প্রয়োগ করে দেখুন। কোয়েরিটি আগে নিজে পড়ুন এবং অনুমান করুন — তারপর ডায়াগ্রামের ধাপগুলো চাপুন।

SELECT d.code, COUNT(*) AS n, COUNT(e.id) AS staff, AVG(e.salary) AS avg_sal
FROM dept d LEFT JOIN emp e ON e.dept = d.code
WHERE e.salary < 50000 OR e.salary IS NULL
GROUP BY d.code
HAVING COUNT(e.id) >= 1
ORDER BY n DESC
LIMIT 1;
ধাপ ১ · FROM dept LEFT JOIN emp৩ বিভাগ → ৬ সারি
 code │ e.id │ name  │ salary
 BBA  │ NULL │ NULL  │ NULL     ← মেলেনি, তবু থাকে
 CSE  │ 1    │ Rina  │ 50000
 CSE  │ 2    │ Sumon │ 40000
 CSE  │ 6    │ Babu  │ 50000
 EEE  │ 3    │ Tania │ 40000
 EEE  │ 5    │ Lipi  │ NULL

Kamal (dept NULL) কোনো বিভাগের সাথে মেলেননি —
বাঁ টেবিল dept, তাই তিনি ফলে নেই
ধাপ ২ · WHERE salary < 50000 OR salary IS NULL৬ → ৪ সারি
 code │ e.id │ name  │ salary │ শর্ত
 BBA  │ NULL │ NULL  │ NULL   │ TRUE (IS NULL)
 CSE  │ 1    │ Rina  │ 50000  │ FALSE ✗
 CSE  │ 2    │ Sumon │ 40000  │ TRUE
 CSE  │ 6    │ Babu  │ 50000  │ FALSE ✗
 EEE  │ 3    │ Tania │ 40000  │ TRUE
 EEE  │ 5    │ Lipi  │ NULL   │ TRUE (IS NULL)
ধাপ ৩ · GROUP BY d.code৪ সারি → ৩ দল
 দল BBA : { (NULL, NULL, NULL) }
 দল CSE : { (2, Sumon, 40000) }
 দল EEE : { (3, Tania, 40000), (5, Lipi, NULL) }

BBA দলে একটি সারি আছে — কিন্তু সেটি
“কর্মী নেই” বোঝানো NULL-সারি
ধাপ ৪ · HAVING COUNT(e.id) >= 1৩ → ২ দল
 দল  │ COUNT(*) │ COUNT(e.id) │ রইল?
 BBA │ 1        │ 0           │ ✗
 CSE │ 1        │ 1           │ ✓
 EEE │ 2        │ 2           │ ✓

COUNT(*) সারি গোনে, COUNT(e.id) শুধু
NULL নয় এমন মান — BBA-র পার্থক্য এখানেই
ধাপ ৫ · SELECT — প্রতিটি দলের হিসাব২ সারি
 code │ n │ staff │ avg_sal
 CSE  │ 1 │ 1     │ 40000.0
 EEE  │ 2 │ 2     │ 40000.0

EEE: AVG(40000, NULL) = 40000 ÷ 1,
20000 নয় — AVG NULL বাদ দেয়।
DISTINCT নেই, তাই পরের ধাপ ORDER BY
ধাপ ৬ · ORDER BY n DESCক্রম বদলাল
 code │ n │ staff │ avg_sal
 EEE  │ 2 │ 2     │ 40000.0
 CSE  │ 1 │ 1     │ 40000.0
ধাপ ৭ · LIMIT 1চূড়ান্ত ফল: ১ সারি
 code │ n │ staff │ avg_sal
 EEE  │ 2 │ 2     │ 40000.0
খাতার trace table
ধাপ
সারি / দল
NULL-এর খবর
  1. FROM আর LEFT JOIN আগে। বাঁ টেবিল dept-এর প্রতিটি সারি অন্তত একবার থাকে। CSE তিনজনের সাথে মেলে (৩ সারি), EEE দুজনের সাথে (২ সারি), BBA কারও সাথে মেলে না, তাই ডান দিকের সব কলাম NULL নিয়ে একটি সারি। মোট ৬. Kamal-এর dept NULL, কোনো code-এর সমান নয়, তাই তিনি নেই।
  2. WHERE প্রতিটি সারি আলাদা করে দেখে। Rina আর Babu-র 50000 < 50000 মিথ্যা, তাঁরা বাদ। BBA-র NULL-সারি আর Lipi “IS NULL” অংশে TRUE পায়, তাই থাকে। ৬ সারি থেকে ৪.
  3. GROUP BY তিনটি দল বানায়। BBA দলে একটি সারি — তবে সেটি “কেউ নেই”-এর সারি। CSE-তে শুধু Sumon, EEE-তে Tania আর Lipi.
  4. HAVING দল ছাঁকে। BBA-তে COUNT(*) = 1 কিন্তু COUNT(e.id) = 0, কারণ e.id সেখানে NULL. তাই BBA বাদ। এখানে COUNT(*) লিখলে BBA থেকে যেত — এই পাতার সবচেয়ে বেশি দেখা ফাঁদগুলোর একটি।
  5. এখন SELECT-এর হিসাব। EEE-তে n = 2, staff = 2, কিন্তু avg_sal = 40000.0: AVG শুধু 40000 নিয়েছে, Lipi-র NULL বাদ। SQLite-এ AVG সবসময় দশমিক সংখ্যা দেয়, তাই 40000.0. কোয়েরিতে DISTINCT নেই, তাই এই ধাপ বাদ।
  6. ORDER BY n DESC. EEE (2) আগে, CSE (1) পরে। এখানে টাই নেই, তাই ক্রম নিশ্চিত।
  7. LIMIT 1 সবার শেষে। শুধু প্রথম সারি: EEE │ 2 │ 2 │ 40000.0. খাতায় চূড়ান্ত টেবিলের সাথে trace table-টিও লিখলে পরীক্ষক প্রতিটি ধাপ মিলিয়ে নম্বর দিতে পারেন।

এবার একই কোয়েরি চালিয়ে দেখুন। দ্বিতীয় কোয়েরিটি শুধু ধাপ ১-এর অবস্থা দেখায় — হাতে trace করার সময় মাঝের অবস্থা এভাবে আলাদা করে দেখা খুব কাজের অভ্যাস।

SELECT d.code, COUNT(*) AS n, COUNT(e.id) AS staff,
       AVG(e.salary) AS avg_sal
FROM dept d LEFT JOIN emp e ON e.dept = d.code
WHERE e.salary < 50000 OR e.salary IS NULL
GROUP BY d.code
HAVING COUNT(e.id) >= 1
ORDER BY n DESC
LIMIT 1;

-- মাঝের অবস্থা: শুধু ধাপ ১ (FROM + LEFT JOIN)
SELECT d.code, e.id, e.name, e.salary
FROM dept d LEFT JOIN emp e ON e.dept = d.code
ORDER BY d.code, e.id;

প্রথম ফল একটি সারি: EEE | 2 | 2 | 40000 — এডিটর দশমিক মানের শেষের “.0” দেখায় না; SQLite-এর নিজের ফল 40000.0 (REAL). দ্বিতীয় ফল ছয়টি সারি: BBA | NULL | NULL | NULL, তারপর CSE-র Rina (1), Sumon (2), Babu (6), তারপর EEE-র Tania (3, 40000) আর Lipi (5, NULL) — ঠিক ডায়াগ্রামের ধাপ ১. নিজে বদলে দেখুন: HAVING-এ COUNT(*) >= 1 লিখে LIMIT মুছে দিন — তখন BBA-ও আসে (BBA | 1 | 0 | NULL), কারণ COUNT(*) NULL-সারিটিও গোনে।

কীভাবে উত্তর মেলাবেন: শুধু শেষ সংখ্যা নয় — সারির সংখ্যা, কলামের নাম আর প্রতিটি NULL মেলান। “প্রায় ঠিক” উত্তরও পরীক্ষায় ভুল, কারণ একটি NULL বা একটি বাড়তি সারি সাধারণত একটি নির্দিষ্ট ধারণার ভুল দেখায়।

২ · ড্রিল ১ — SELECT, ORDER BY, LIMIT, DISTINCT, CASE আর ফাংশন

প্রথম ড্রিল সবচেয়ে মৌলিক অংশ নিয়ে — কিন্তু সহজ প্রশ্নেও NULL, টাই আর ডেটা টাইপের ফাঁদ থাকে। সব প্রশ্ন অংশ ১-এর emp টেবিলের ওপর, যদি না অন্য টেবিল বলা থাকে।

ফ-০১ (সহজ) — কয়টি নাম আসবে, কোন ক্রমে?

SELECT name FROM emp
WHERE salary > 40000 OR dept = 'EEE'
ORDER BY id;
ফ-০১উত্তর ও কারণ
name
Rina
Tania
Lipi
Babu        (৪টি সারি)

Rina আর Babu বেতনের শর্তে আসেন, Tania বিভাগের শর্তে। মূল প্রশ্ন Lipi: NULL > 40000 হলো UNKNOWN, কিন্তু UNKNOWN OR TRUE = TRUE, তাই তিনি আসেন। Kamal-এর বেলায় FALSE OR UNKNOWN = UNKNOWN — বাদ।

ফ-০২ (মাঝারি) — দুটি কোয়েরি। SQLite-এ কী আসবে, আর PostgreSQL-এ কী বদলাবে?

SELECT name, salary FROM emp ORDER BY salary DESC, name LIMIT 3;
SELECT name, salary FROM emp ORDER BY salary, id LIMIT 2 OFFSET 2;
ফ-০২উত্তর ও কারণ
(১)  name  │ salary          (২)  name  │ salary
     Babu  │ 50000                Sumon │ 40000
     Rina  │ 50000                Tania │ 40000
     Sumon │ 40000

(১)-এ 50000-এর টাই নাম দিয়ে ভাঙে — Babu, Rina. 40000-এ Sumon আর Tania, নামের ক্রমে Sumon আগে, আর LIMIT 3 সেখানেই থামে। (২)-তে SQLite NULL-কে সবচেয়ে ছোট ধরে, তাই ছোট থেকে বড় ক্রম: Lipi (NULL), Kamal, Sumon, Tania, Rina, Babu. প্রথম দুটি বাদ দিয়ে পরের দুটি: Sumon, Tania.

PostgreSQL-এ: NULL-কে সবচেয়ে বড় ধরা হয়। (১)-এ Lipi প্রথমে চলে আসেন: Lipi, Babu, Rina. (২)-এর ক্রম Kamal, Sumon, Tania, Rina, Babu, Lipi — ফল Tania, Rina. MySQL আর SQL Server SQLite-এর মতো (NULL ছোট)। সব জায়গায় এক ফল চাইলে NULLS LAST লিখুন (PostgreSQL, Oracle, SQLite 3.30+).

SELECT name, salary FROM emp ORDER BY salary DESC, name LIMIT 3;
SELECT name, salary FROM emp ORDER BY salary, id LIMIT 2 OFFSET 2;

-- PostgreSQL-এর মতো ক্রম চাইলে:
SELECT name, salary FROM emp ORDER BY salary DESC NULLS FIRST, name LIMIT 3;

প্রথম দুটি ফল ঠিক ওপরের মতো: Babu | 50000, Rina | 50000, Sumon | 40000 আর Sumon | 40000, Tania | 40000. তৃতীয় কোয়েরি Lipi | NULL, Babu | 50000, Rina | 50000 দেয় — PostgreSQL-এর ডিফল্ট আচরণ, SQLite-এ নিজে লিখে আনা।

ফ-০৩ (সহজ) — তিনটি কোয়েরি; প্রতিটিতে কয়টি সারি, আর দ্বিতীয়টির দুটি মান কত?

SELECT DISTINCT dept FROM emp;
SELECT COUNT(DISTINCT dept), COUNT(dept) FROM emp;
SELECT DISTINCT dept, salary FROM emp;
ফ-০৩উত্তর ও কারণ

(১) ৩টি সারি: CSE, EEE আর NULL. DISTINCT সব NULL-কে একে অপরের “সমান” ধরে একটি সারি রাখে। (২) 2 | 5: COUNT(DISTINCT dept) NULL গোনে না, তাই 2; COUNT(dept) পাঁচটি অ-NULL মান গোনে। অর্থাৎ (১)-এর সারির সংখ্যা আর (২)-এর প্রথম মান আলাদা — NULL-এর জন্য। (৩) ৫টি সারি: (CSE, 50000) জোড়া দুবার আছে (Rina, Babu), বাকি চারটি জোড়া আলাদা। DISTINCT পুরো সারি তুলনা করে, প্রথম কলাম নয়।

ফ-০৪ (মাঝারি) — Kamal (id 4) আর Lipi (id 5)-এর জন্য তিনটি CASE কী দেবে?

SELECT name,
  CASE WHEN salary >= 50000 THEN 'high'
       WHEN salary >= 40000 THEN 'mid' ELSE 'low' END AS band,
  CASE dept WHEN NULL THEN 'none' ELSE dept END AS d1,
  CASE WHEN dept IS NULL THEN 'none' ELSE dept END AS d2
FROM emp WHERE id IN (4, 5) ORDER BY id;
ফ-০৪উত্তর ও কারণ
name  │ band │ d1   │ d2
Kamal │ low  │ NULL │ none
Lipi  │ low  │ EEE  │ EEE

Lipi-র বেতন NULL — দুটি WHEN-ই UNKNOWN, আর CASE শুধু TRUE-তে থামে, তাই ELSE-এ গিয়ে 'low'. “বেতন জানা নেই” আর “কম বেতন” এক হয়ে গেল — রিপোর্টে এটি বড় ভুল। Kamal-এর d1: simple CASE ভেতরে dept = NULL তুলনা করে, যা কখনো TRUE নয়; তাই ELSE dept, অর্থাৎ NULL. NULL ধরতে d2-র মতো WHEN dept IS NULL লিখতে হয় — সব DBMS-এ একই।

ফ-০৫ (মাঝারি) — একটি টেবিল m(v INTEGER)-এ তিনটি সারি: 1, 2, 2. SQLite-এ দুটি কোয়েরির প্রতিটি কলামের মান কত?

SELECT 7/2, 7/2.0, 7%3, -7/2, CAST(7 AS REAL)/2;
SELECT AVG(v), SUM(v)/COUNT(v), SUM(v)*1.0/COUNT(v), ROUND(AVG(v), 2) FROM m;
ফ-০৫উত্তর ও কারণ
(১)  3 │ 3.5 │ 1 │ -3 │ 3.5
(২)  1.6666666666666667 │ 1 │ 1.6666666666666667 │ 1.67

দুটি পূর্ণসংখ্যার ভাগ পূর্ণসংখ্যা — integer division, ভগ্নাংশ কেটে ফেলা হয়। ঋণাত্মক সংখ্যায় কাটা হয় শূন্যের দিকে, তাই −7/2 = −3 (Python-এর // দিত −4). একটি দিকও দশমিক হলে ফল দশমিক। (২)-এ AVG সবসময় দশমিক দেয়, কিন্তু নিজে SUM/COUNT লিখলে 5/3 = 1 — গড় হিসাবের খুব প্রচলিত বাগ। * 1.0 দিয়ে আগে দশমিকে নিলে ঠিক হয়।

অন্য DBMS-এ: PostgreSQL আর SQL Server-এ 7/2 = 3 (একই)। MySQL-এ / সবসময় দশমিক ভাগ — 7/2 = 3.5000; পূর্ণসংখ্যার ভাগের জন্য 7 DIV 2. Oracle-এ 7/2 = 3.5. তাই প্রশ্নে DBMS-এর নাম না থাকলে উত্তরে লিখুন, কোনটি ধরেছেন।

ফ-০৬ (মাঝারি) — SQLite-এ সাতটি কলামের মান কত?

SELECT UPPER('dhaka'), LENGTH('ঢাকা'), SUBSTR('Database', 1, 4),
       SUBSTR('Database', -4), 'abc' || NULL, TRIM('  hi  '),
       REPLACE('a-b-c', '-', '');
ফ-০৬উত্তর ও কারণ
DHAKA │ 4 │ Data │ base │ NULL │ hi │ abc

SQLite-এর LENGTH লেখার অক্ষর (Unicode character) গোনে: “ঢাকা” = ঢ, া, ক, া — ৪টি। SUBSTR-এ অবস্থান 1 থেকে শুরু; ঋণাত্মক শুরু মানে শেষ থেকে গোনা। 'abc' || NULL হলো NULL — NULL propagation: NULL-এর সাথে যেকোনো হিসাব NULL.

অন্য DBMS-এ: MySQL-এ LENGTH বাইট গোনে — UTF-8-এ প্রতিটি বাংলা অক্ষর ৩ বাইট, তাই 12; অক্ষর গুনতে CHAR_LENGTH. MySQL-এ ডিফল্টভাবে || হলো যৌক্তিক OR (জোড়া লাগাতে CONCAT). Oracle-এ 'abc' || NULL হয় 'abc', কারণ Oracle ফাঁকা string আর NULL-কে একই ধরে।

ফ-০৭ (কঠিন) — SQLite-এর তারিখ ফাংশন। পাঁচটি মান কত?

SELECT date('2025-01-31', '+1 month'),
       julianday('2025-03-01') - julianday('2025-02-01'),
       strftime('%m', '2025-09-15'),
       typeof(strftime('%m', '2025-09-15')),
       date('2025-09-15', 'start of month', '-1 day');
ফ-০৭উত্তর ও কারণ
2025-03-03 │ 28.0 │ 09 │ text │ 2025-08-31

SQLite '+1 month' দিলে মাসের ঘর এক বাড়িয়ে 2025-02-31 বানায়, তারপর সেটিকে স্বাভাবিক করে — ফেব্রুয়ারির ২৮ দিনের পরে আরও ৩ দিন, অর্থাৎ ৩ মার্চ। ২০২৫ অধিবর্ষ নয়, তাই ফেব্রুয়ারি ২৮ দিনের, পার্থক্য 28.0. strftime লেখা (text) দেয় — '09', সংখ্যা 9 নয়; তাই strftime('%m', d) = 9 লিখলে কোনো সারি মিলবে না। শেষটি: মাসের শুরু (০১ সেপ্টেম্বর) থেকে এক দিন পেছনে — আগের মাসের শেষ দিন।

অন্য DBMS-এ: PostgreSQL-এ DATE '2025-01-31' + INTERVAL '1 month' আর MySQL-এ DATE_ADD('2025-01-31', INTERVAL 1 MONTH) দুটিই মাসের শেষ দিনে আটকে 2025-02-28 দেয়। বিল বা কিস্তির তারিখ হিসাবে এই পার্থক্য সরাসরি টাকার হিসাব বদলে দিতে পারে।

ফ-০৮ (মাঝারি) — codes(c TEXT) টেবিলে তিনটি মান: '9', '10', '100'. SQLite-এ চারটি কোয়েরির ফল কী?

SELECT COUNT(*) FROM emp WHERE name LIKE 'r%';
SELECT '10' < '9', 10 < 9;
SELECT c FROM codes ORDER BY c;
SELECT c FROM codes ORDER BY CAST(c AS INTEGER);
ফ-০৮উত্তর ও কারণ

(১) 1 — SQLite-এর LIKE ইংরেজি (ASCII) অক্ষরে বড়-ছোট হাতের পার্থক্য করে না, তাই 'r%' মেলে Rina-র সাথে। PostgreSQL-এর LIKE পার্থক্য করে — সেখানে 0 (ILIKE দিলে 1). (২) 1 | 0 — দুটি text তুলনা হয় অভিধানের মতো অক্ষর ধরে: '1' < '9', তাই '10' < '9' সত্য (1); সংখ্যা হিসেবে 10 < 9 মিথ্যা (0). (৩) 10, 100, 9 — text হিসেবে সাজানো। (৪) 9, 10, 100 — সংখ্যায় রূপান্তর করে সাজানো। রোল নম্বর বা কোড TEXT কলামে রাখলে এই ফাঁদ প্রায়ই দেখা যায়।

-- ফ-০৫ থেকে ফ-০৮: ভাগ, string, তারিখ আর text-তুলনা
SELECT 7/2, 7/2.0, -7/2, AVG(v), SUM(v)/COUNT(v) FROM m;
SELECT LENGTH('ঢাকা') AS len, 'abc' || NULL AS joined,
       date('2025-01-31', '+1 month') AS next_month,
       typeof(strftime('%m', '2025-09-15')) AS t;
SELECT '10' < '9' AS text_cmp, 10 < 9 AS num_cmp;
SELECT c FROM codes ORDER BY c;

ফল: 3 | 3.5 | -3 | 1.6666666666666667 | 1; তারপর 4 | NULL | 2025-03-03 | text; তারপর 1 | 0; শেষে তিনটি সারি 10, 100, 9. প্রতিটি মান ফ-০৫ থেকে ফ-০৮-এর উত্তরের সাথে মিলিয়ে নিন, তারপর নিজের প্রশ্ন বানান — যেমন date('2024-01-31', '+1 month'); ২০২৪ অধিবর্ষ, তাই এবার কোন তারিখ আসবে?

৩ · ড্রিল ২ — NULL আর তিন-মানের যুক্তি

SQL-এর ফল-প্রশ্নে সবচেয়ে বেশি নম্বর হারায় NULL-এ। নিয়মটি ছোট: NULL মানে “জানা নেই”, তাই NULL-এর সাথে যেকোনো তুলনার ফল TRUE বা FALSE নয়, তৃতীয় একটি মান — UNKNOWN. একে বলে three-valued logic. WHERE, HAVING আর JOIN-এর ON শুধু TRUE সারি রাখে; CHECK constraint শুধু FALSE সারি আটকায়। এই অসমতা থেকেই বেশিরভাগ ফাঁদ।

ফ-০৯ (সহজ) — তিনটি কোয়েরির ফল কী?

SELECT COUNT(*) FROM emp WHERE dept = NULL;
SELECT name FROM emp WHERE dept IS NULL;
SELECT name FROM emp WHERE dept <> 'CSE' ORDER BY id;
ফ-০৯উত্তর ও কারণ

(১) 0 — dept = NULL প্রতিটি সারিতে UNKNOWN, Kamal-এর সারিতেও। কোনো ত্রুটি হয় না, তাই ভুলটি চোখে পড়ে না। (২) Kamal — NULL খোঁজার একমাত্র ঠিক পথ IS NULL. (৩) Tania, Lipi — Kamal আসেন না, কারণ NULL <> 'CSE'-ও UNKNOWN. “CSE ছাড়া সবাই” বললে মানুষ Kamal-কে ধরেন, SQL ধরে না; চাইলে OR dept IS NULL যোগ করতে হয়।

ফ-১০ (মাঝারি) — প্রতিটি কলামের মান কত?

SELECT COUNT(*), COUNT(dept), COUNT(salary), COUNT(DISTINCT salary) FROM emp;
SELECT AVG(salary), SUM(salary)/COUNT(*), AVG(COALESCE(salary, 0)) FROM emp;
ফ-১০উত্তর ও কারণ
(১)  6 │ 5 │ 5 │ 3
(২)  42000.0 │ 35000 │ 35000.0

COUNT(*) সারি গোনে (৬); COUNT(কলাম) শুধু অ-NULL মান (dept-এ Kamal বাদ, salary-তে Lipi বাদ — দুটিই ৫); COUNT(DISTINCT salary) আলাদা মান গোনে: 50000, 40000, 30000 — ৩। (২)-এ AVG NULL বাদ দিয়ে 210000 ÷ 5 = 42000. কিন্তু SUM ÷ COUNT(*) মানে 210000 ÷ 6 = 35000 — Lipi-কে শূন্য বেতনের কর্মী ধরা হলো। COALESCE(salary, 0) একই কাজ খোলাখুলি করে। কোনটি ঠিক, তা ব্যবসার প্রশ্ন: “জানা বেতনের গড়” নাকি “মাথাপিছু খরচ”। পরীক্ষায় দুটির পার্থক্য লিখলেই পূর্ণ নম্বর।

SELECT COUNT(*), COUNT(dept), COUNT(salary),
       COUNT(DISTINCT salary)
FROM emp;

SELECT AVG(salary), SUM(salary)/COUNT(*),
       AVG(COALESCE(salary, 0))
FROM emp;

ফল ঠিক ওপরের মতো: 6 | 5 | 5 | 3 আর 42000 | 35000 | 35000 (এডিটর REAL মানের “.0” দেখায় না — প্রথম আর তৃতীয়টি আসলে 42000.0 আর 35000.0, দ্বিতীয়টি পূর্ণসংখ্যা)। এবার Lipi-র বেতন বসিয়ে দেখুন — শুরুতে UPDATE emp SET salary = 40000 WHERE id = 5; যোগ করুন। তখন তিনটি গড়ই 41666 ঘরের কাছাকাছি সমান হয়ে যায় — পার্থক্যের পুরোটাই ছিল একটি NULL-এর জন্য।

ফ-১১ (মাঝারি) — SQLite-এ TRUE লেখা হয় 1, FALSE 0, UNKNOWN দেখায় NULL. এগারোটি মান কত?

SELECT NULL = NULL, NULL IS NULL, NULL AND 0, NULL AND 1,
       NULL OR 1, NULL OR 0, NOT NULL, NULL + 5,
       COALESCE(NULL, NULL, 3), NULLIF(5, 5), NULLIF(5, 6);
ফ-১১উত্তর ও কারণ
NULL │ 1 │ 0 │ NULL │ 1 │ NULL │ NULL │ NULL │ 3 │ NULL │ 5

মনে রাখার কৌশল: UNKNOWN-কে “হয়তো সত্য, হয়তো মিথ্যা” ভাবুন। NULL AND 0 — একটি দিক নিশ্চিত মিথ্যা, তাই পুরোটাই মিথ্যা (0). NULL OR 1 — একটি দিক নিশ্চিত সত্য, তাই সত্য (1). বাকি ক্ষেত্রে উত্তর অজানাই থাকে। NOT UNKNOWN-ও UNKNOWN. NULLIF(a, b) সমান হলে NULL দেয়, না হলে a — ভাগে শূন্য এড়াতে কাজে লাগে: x / NULLIF(y, 0).

ফ-১২ (কঠিন) — “কোন বিভাগে কোনো কর্মী নেই?” প্রশ্নের তিনটি সমাধান। কোনটি কী দেবে? (উত্তর তো BBA হওয়ার কথা।)

SELECT code FROM dept WHERE code NOT IN (SELECT dept FROM emp);
SELECT code FROM dept d
WHERE NOT EXISTS (SELECT 1 FROM emp e WHERE e.dept = d.code);
SELECT code FROM dept WHERE code IN (SELECT dept FROM emp);
ফ-১২উত্তর ও কারণ

(১) কোনো সারি নেই। (২) BBA. (৩) CSE, EEE.

(১)-এর subquery-র তালিকা: CSE, CSE, EEE, NULL, EEE, CSE. BBA-র জন্য 'BBA' NOT IN (…, NULL) মানে 'BBA' <> 'CSE' AND … AND 'BBA' <> NULL. শেষ অংশটি UNKNOWN, আর TRUE AND UNKNOWN = UNKNOWN — তাই BBA বাদ। তালিকায় একটি NULL থাকলেই NOT IN কখনো TRUE হয় না। (২) anti-join-এর নিরাপদ রূপ: NOT EXISTS শুধু দেখে “মেলে এমন সারি আছে কি না”, আর NULL কারও সাথে মেলে না। (৩) IN-এ সমস্যা নেই: সমান একটি মান পেলেই TRUE.

-- ১. ফাঁদ: subquery-তে NULL
SELECT code FROM dept WHERE code NOT IN (SELECT dept FROM emp);

-- ২. নিরাপদ: NOT EXISTS
SELECT code FROM dept d
WHERE NOT EXISTS (SELECT 1 FROM emp e WHERE e.dept = d.code);

-- ৩. NOT IN রাখতে চাইলে NULL ছেঁকে নিন
SELECT code FROM dept
WHERE code NOT IN (SELECT dept FROM emp WHERE dept IS NOT NULL);

প্রথম ফল “(কোনো সারি নেই)”, দ্বিতীয় আর তৃতীয় দুটিতেই একটি সারি: BBA. এবার Kamal-এর বিভাগ বসিয়ে দেখুন (শুরুতে UPDATE emp SET dept = 'CSE' WHERE id = 4;) — তখন প্রথম কোয়েরিও BBA দেয়। অর্থাৎ ডেটায় একটি NULL ঢুকলেই NOT IN-ওয়ালা রিপোর্ট নিঃশব্দে ফাঁকা হয়ে যায়।

ফ-১৩ (মাঝারি) — BBA বিভাগে কোনো কর্মী নেই। এই কোয়েরি কয়টি সারি দেবে, প্রতিটি মান কত? আর SQLite-এর TOTAL(salary) দিলে?

SELECT SUM(salary), COUNT(*), MAX(salary), AVG(salary), COUNT(salary)
FROM emp WHERE dept = 'BBA';
ফ-১৩উত্তর ও কারণ
NULL │ 0 │ NULL │ NULL │ 0          (১টি সারি)

GROUP BY ছাড়া aggregate কোয়েরি সবসময় ঠিক একটি সারি দেয়, WHERE কোনো সারি না রাখলেও। খালি সেটে COUNT 0, কিন্তু SUM, MAX, AVG NULL — “শূন্য টাকা” নয়, “যোগ করার কিছু নেই”। রিপোর্টে 0 দেখাতে চাইলে COALESCE(SUM(salary), 0). SQLite-এর নিজস্ব TOTAL(salary) খালি সেটে 0.0 দেয় — এটি standard নয়, অন্য DBMS-এ নেই।

ফ-১৪ (মাঝারি) — তিনটি গণনা। প্রথম দুটির যোগফল কি ৬ (মোট কর্মী)?

SELECT COUNT(*) FROM emp WHERE salary > 40000;
SELECT COUNT(*) FROM emp WHERE NOT (salary > 40000);
SELECT COUNT(*) FROM emp WHERE (salary > 40000) IS NOT TRUE;
ফ-১৪উত্তর ও কারণ

2, 3, 4. প্রথম দুটির যোগফল ৫, ৬ নয়। Lipi-র জন্য শর্তটি UNKNOWN, আর NOT UNKNOWN-ও UNKNOWN — তাই তিনি কোনো দলেই নেই। একটি শর্ত আর তার NOT মিলে টেবিলকে দুই ভাগ করে না, তিন ভাগ করে: TRUE, FALSE আর UNKNOWN. তৃতীয় কোয়েরি IS NOT TRUE দিয়ে FALSE আর UNKNOWN দুটিই ধরে — 4 (Sumon, Tania, Kamal, Lipi). IS TRUE/IS NOT TRUE SQLite (3.23+) আর PostgreSQL-এ চলে; SQL Server-এ নেই।

৪ · ড্রিল ৩ — GROUP BY, HAVING আর aggregate

GROUP BY-এর প্রশ্নে দুটি জিনিস আগে ঠিক করুন: কয়টি দল হবে (NULL-ও একটি দল), আর প্রতিটি শর্ত সারির ওপর (WHERE) নাকি দলের ওপর (HAVING). দল গোনা ঠিক হলে বাকি হিসাব প্রায় যান্ত্রিক।

ফ-১৫ (সহজ) — কয়টি সারি, কী মান?

SELECT dept, COUNT(*) AS n, SUM(salary) AS total
FROM emp GROUP BY dept ORDER BY dept;
ফ-১৫উত্তর ও কারণ
dept │ n │ total
NULL │ 1 │ 30000
CSE  │ 3 │ 140000
EEE  │ 2 │ 40000

তিনটি দল — GROUP BY সব NULL-কে একটি দলে রাখে, আর SQLite ছোট থেকে বড় সাজালে NULL আগে আসে (PostgreSQL-এ শেষে)। EEE-র total 40000: SUM Lipi-র NULL বাদ দিয়েছে, কিন্তু n = 2, কারণ COUNT(*) সারি গোনে। BBA নেই — emp-এ তার কোনো সারিই নেই; BBA দেখাতে dept থেকে LEFT JOIN লাগত।

ফ-১৬ (মাঝারি) — তিনটি কোয়েরি, শুধু শর্তের জায়গা আর ধরন আলাদা। কোনটিতে কোন দল আসবে?

SELECT dept, COUNT(*) FROM emp GROUP BY dept HAVING COUNT(*) >= 2;
SELECT dept, COUNT(salary) FROM emp GROUP BY dept HAVING COUNT(salary) >= 2;
SELECT dept, COUNT(*) FROM emp WHERE salary >= 40000 GROUP BY dept;
ফ-১৬উত্তর ও কারণ

(১) CSE 3, EEE 2. (২) শুধু CSE 3 — EEE-তে অ-NULL বেতন একটিই (Tania). (৩) CSE 3, EEE 1 — WHERE আগে চলে সারি ছাঁকে: Kamal (30000) আর Lipi (NULL) বাদ, তারপর দল। NULL দল নেই, কারণ তার একমাত্র সারি (Kamal) আগেই বাদ। নিয়ম: “কোন সারি গোনা হবে” → WHERE; “কোন দল দেখানো হবে” → HAVING.

ফ-১৭ (কঠিন) — নিচের কোয়েরি SQLite-এ কী দেবে? PostgreSQL আর MySQL-এ কী হবে?

SELECT dept, name, MIN(salary) FROM emp GROUP BY dept ORDER BY dept;
ফ-১৭উত্তর ও কারণ
dept │ name  │ MIN(salary)
NULL │ Kamal │ 30000
CSE  │ Sumon │ 40000
EEE  │ Tania │ 40000

name GROUP BY-তে নেই, aggregate-এর ভেতরেও নেই — একে বলে bare column. SQLite এটি চালায়, আর একটি বিশেষ নিয়ম মানে: কোয়েরিতে একটিমাত্র MIN বা MAX থাকলে bare column আসে সেই সারি থেকে, যেখানে MIN/MAX পাওয়া গেছে। তাই CSE-তে Sumon (40000). MIN NULL বাদ দেয়, তাই EEE-তে Tania. MIN/MAX না থাকলে (যেমন COUNT) কোন সারির নাম আসবে, তা অনির্দিষ্ট।

অন্য DBMS-এ: PostgreSQL, SQL Server আর Oracle ত্রুটি দেয় — PostgreSQL-এর বার্তা: name কলামটি GROUP BY-তে থাকতে হবে বা aggregate-এ ব্যবহার করতে হবে। MySQL 5.7.5 থেকে ডিফল্ট ONLY_FULL_GROUP_BY মোডে একই রকম ত্রুটি দেয়। পরীক্ষায় “কী আসবে” জিজ্ঞেস করলে standard SQL-এর উত্তর: ত্রুটি।

-- SQLite-এর bare column: MIN-এর সারি থেকে name
SELECT dept, name, MIN(salary) FROM emp GROUP BY dept ORDER BY dept;

-- বহনযোগ্য পথ: window function দিয়ে প্রতি বিভাগে সর্বনিম্ন বেতনের সবাই
WITH r AS (
  SELECT dept, name, salary,
         RANK() OVER (PARTITION BY dept ORDER BY salary) AS rk
  FROM emp WHERE salary IS NOT NULL)
SELECT dept, name, salary FROM r WHERE rk = 1 ORDER BY dept;

প্রথম ফল ওপরের তিনটি সারি। দ্বিতীয় ফলও একই তিনটি সারি — NULL | Kamal | 30000, CSE | Sumon | 40000, EEE | Tania | 40000 — কিন্তু এটি PostgreSQL, MySQL 8 আর SQL Server-এও একই ফল দেয়, আর টাই থাকলে সবাইকে দেখায়। MIN বদলে MAX করে দেখুন: CSE-তে 50000-এ Rina আর Babu টাই; SQLite একজনকে (এখানে Rina) দেখায় — কোনজন, তার নিশ্চয়তা নেই।

ফ-১৮ (কঠিন) — তিনটি কোয়েরির প্রতিটিতে কয়টি সারি?

SELECT COUNT(*), SUM(salary) FROM emp WHERE 1 = 0;
SELECT dept, COUNT(*) FROM emp WHERE 1 = 0 GROUP BY dept;
SELECT COUNT(*) FROM emp HAVING COUNT(*) > 10;
ফ-১৮উত্তর ও কারণ

(১) ১টি সারি: 0 | NULL. GROUP BY নেই, তাই পুরো (খালি) টেবিল একটি দল। (২) ০টি সারি। GROUP BY থাকলে দল তৈরি হয় সারি থেকে — সারি নেই, দলও নেই, তাই “COUNT = 0” লেখা কোনো সারিও নেই। (৩) ০টি সারি। পুরো টেবিল একটি দল (COUNT 6), HAVING সেটি বাদ দেয়। SQLite-এ GROUP BY ছাড়া HAVING চলে 3.39.0 থেকে; পুরোনো সংস্করণে ত্রুটি দিত। PostgreSQL-এ চলে।

ফ-১৯ (সহজ) — db-shop-এর orders টেবিলের দশটি অর্ডার (এই পাতার পরের ড্রিলেও লাগবে):

order_id │ customer_id │ status          order_id │ customer_id │ status
1001     │ 1           │ delivered       1006     │ 6           │ pending
1002     │ 2           │ delivered       1007     │ 2           │ delivered
1003     │ 1           │ shipped         1008     │ 8           │ shipped
1004     │ 3           │ cancelled       1009     │ 1           │ pending
1005     │ 4           │ delivered       1010     │ 7           │ delivered

customers টেবিলে ৮ জন গ্রাহক (customer_id 1–8: Rahim, Karima, Sabbir, Nusrat, Tanvir, Farhana, Imran, Mitu). এই কোয়েরির ফল কী?

SELECT status, COUNT(*) AS n FROM orders
GROUP BY status HAVING COUNT(*) > 1
ORDER BY n DESC, status;
ফ-১৯উত্তর ও কারণ
status    │ n
delivered │ 5
pending   │ 2
shipped   │ 2

চারটি দল: delivered ৫, shipped ২, pending ২, cancelled ১. HAVING cancelled বাদ দেয়। pending আর shipped-এর টাই status দিয়ে ভাঙে — বর্ণানুক্রমে pending আগে। ORDER BY-তে n — SELECT-এর alias — ব্যবহার করা যায়, কারণ ORDER BY চলে SELECT-এর পরে।

৫ · ড্রিল ৪ — JOIN: সারি গোনা, fan-out আর ফাঁদ

JOIN-এর প্রশ্নে প্রথম কাজ: ফলের সারির সংখ্যা। একটি কৌশল প্রায় সবসময় কাজ করে — বাঁ টেবিলের প্রতিটি সারির জন্য লিখুন, ডান টেবিলের কয়টি সারির সাথে মেলে। INNER JOIN-এ সেই সংখ্যাগুলোর যোগফল; LEFT JOIN-এ প্রতিটি 0-কে 1 ধরে যোগফল। একটি সারি একাধিক সারির সাথে মিললে সারি বেড়ে যায় — একে বলে fan-out.

এই অংশের জন্য দুটি এক-কলামের টেবিল, ইচ্ছে করে ডুপ্লিকেট আর NULL-সহ:

a.x : 1, 2, 2, NULL          b.x : 2, 2, 3, NULL

ফ-২০ (মাঝারি) — প্রতিটি JOIN-এ কয়টি সারি? ON a.x = b.x সবগুলোতে (CROSS ছাড়া)।

(১) a INNER JOIN b    (২) a LEFT JOIN b    (৩) a RIGHT JOIN b
(৪) a FULL JOIN b     (৫) a CROSS JOIN b
ফ-২০উত্তর ও কারণ

4, 6, 6, 8, 16. a-র দুটি 2 প্রতিটি b-র দুটি 2-এর সাথে মেলে: 2 × 2 = 4 — এটিই INNER. NULL = NULL TRUE নয়, তাই দুই দিকের NULL কারও সাথে মেলে না। LEFT: 4 + (a-র 1) + (a-র NULL) = 6. RIGHT: 4 + (b-র 3) + (b-র NULL) = 6. FULL: 4 + দুই দিকের অমিল ২ + ২ = 8. CROSS: 4 × 4 = 16, শর্ত নেই। সূত্র: FULL = INNER + বাঁয়ের অমিল + ডানের অমিল।

SELECT 'INNER' AS j, COUNT(*) AS n FROM a JOIN b ON a.x = b.x
UNION ALL SELECT 'LEFT',  COUNT(*) FROM a LEFT  JOIN b ON a.x = b.x
UNION ALL SELECT 'RIGHT', COUNT(*) FROM a RIGHT JOIN b ON a.x = b.x
UNION ALL SELECT 'FULL',  COUNT(*) FROM a FULL  JOIN b ON a.x = b.x
UNION ALL SELECT 'CROSS', COUNT(*) FROM a CROSS JOIN b;

-- FULL JOIN-এর ৮টি সারি নিজের চোখে
SELECT a.x AS ax, b.x AS bx FROM a FULL JOIN b ON a.x = b.x;

প্রথম ফল: INNER | 4, LEFT | 6, RIGHT | 6, FULL | 8, CROSS | 16. দ্বিতীয় ফলের ৮টি সারি: 1 | NULL, চারবার 2 | 2, NULL | NULL (a-র NULL), NULL | 3, NULL | NULL (b-র NULL). দুটি NULL | NULL সারি দেখতে এক, কিন্তু উৎস আলাদা — হাতে trace করার সময় এটি গুলিয়ে ফেলা খুব সহজ। RIGHT আর FULL JOIN SQLite-এ এসেছে 3.39.0 (২০২২) থেকে; MySQL-এ FULL JOIN এখনো নেই (LEFT আর RIGHT-এর UNION দিয়ে বানাতে হয়)।

ফ-২১ (কঠিন) — db-shop (ফ-১৯-এর টেবিল)। শর্ত ON-এ, নাকি WHERE-এ — দুটি কোয়েরিতে কয়টি সারি?

SELECT COUNT(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status = 'delivered';

SELECT COUNT(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered';
ফ-২১উত্তর ও কারণ

9 আর 5. delivered অর্ডার: Rahim ১টি, Karima ২টি, Nusrat ১টি, Imran ১টি। প্রথম কোয়েরিতে শর্তটি “কোন অর্ডার জুড়ব” ঠিক করে, কোনো গ্রাহক বাদ দেয় না: ১ + ২ + ১ + ১ = ৫ সারি, আর বাকি চারজন (Sabbir, Tanvir, Farhana, Mitu) একটি করে NULL-সারি — মোট ৯. দ্বিতীয়টিতে WHERE চলে JOIN-এর পরে; NULL-সারিতে o.status = 'delivered' UNKNOWN, তাই সেগুলো বাদ — LEFT JOIN কার্যত INNER JOIN হয়ে যায়: ৫.

-- শর্ত ON-এ: সব গ্রাহক থাকেন
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
                  AND o.status = 'delivered'
ORDER BY c.customer_id, o.order_id;

-- শর্ত WHERE-এ: NULL-সারি বাদ
SELECT COUNT(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.status = 'delivered';

প্রথম ফল ৯টি সারি: Rahim Uddin | 1001, Karima Begum | 1002, Karima Begum | 1007, Sabbir Hasan | NULL, Nusrat Jahan | 1005, Tanvir Ahmed | NULL, Farhana Akter | NULL, Imran Hossain | 1010, Mitu Rani | NULL. দ্বিতীয় ফল 5. Rahim-এর shipped আর pending অর্ডার (1003, 1009) প্রথম ফলে নেই — শর্তটি ON-এ বলে সেগুলো জোড়া হয়নি, কিন্তু Rahim নিজে আছেন।

ফ-২২ (মাঝারি) — প্রতিটি গ্রাহকের অর্ডার-সংখ্যা। Tanvir (customer 5, কোনো অর্ডার নেই) আর Rahim (customer 1)-এর সারিতে a আর b কত?

SELECT c.name, COUNT(*) AS a, COUNT(o.order_id) AS b
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id;
ফ-২২উত্তর ও কারণ

Tanvir: a = 1, b = 0. Rahim: a = 3, b = 3. LEFT JOIN Tanvir-এর জন্য একটি NULL-সারি বানায়; COUNT(*) সেটিকে গোনে, COUNT(o.order_id) গোনে না। “প্রতিটি গ্রাহকের অর্ডার কয়টি” প্রশ্নে COUNT(*) লিখলে অর্ডারহীন সবার পাশে 1 আসে — ভুল, অথচ দেখতে বিশ্বাসযোগ্য। মোট ৮টি সারি: Karima 2 | 2, বাকি পাঁচজন 1 | 1.

ফ-২৩ (কঠিন) — শিক্ষার্থী s(id, name): (1, Arif), (2, Bristy). ফোন phone(sid, num): Arif-এর দুটি — (1, '017'), (1, '018'). ফি জমা fee(sid, amount): Arif 1000, 2000, 3000; Bristy 500. Arif আসলে মোট কত টাকা দিয়েছেন, আর কোয়েরি কী বলবে?

SELECT s.name, COUNT(*) AS rows_, SUM(f.amount) AS paid
FROM s JOIN phone p ON p.sid = s.id
       JOIN fee f   ON f.sid = s.id
GROUP BY s.id;
ফ-২৩উত্তর ও কারণ
name │ rows_ │ paid
Arif │ 6     │ 12000        (১টি সারি — Bristy নেই)

আসল জমা 6000, কিন্তু কোয়েরি বলে 12000. Arif-এর ২টি ফোন আর ৩টি ফি একে অপরের সাথে কোনো সম্পর্ক ছাড়াই জোড়া লাগে: ২ × ৩ = ৬ সারি, প্রতিটি ফি দুবার যোগ হয়। দুটি স্বাধীন one-to-many সম্পর্ক একসাথে JOIN করলেই এই দ্বিগুণ-গোনা — fan-out-এর সবচেয়ে ক্ষতিকর রূপ। Bristy-র ফোন নেই, তাই INNER JOIN-এ তিনি পুরোই হারিয়ে গেছেন। সমাধান: আগে প্রতিটি সম্পর্ক আলাদা করে aggregate করুন (subquery/CTE), তারপর জুড়ুন।

-- ফাঁদ: দুই one-to-many একসাথে
SELECT s.name, COUNT(*) AS rows_, SUM(f.amount) AS paid
FROM s JOIN phone p ON p.sid = s.id
       JOIN fee f   ON f.sid = s.id
GROUP BY s.id;

-- ঠিক: ফি আলাদা করে যোগ, ফোন আলাদা করে গোনা
SELECT s.name,
       (SELECT SUM(amount) FROM fee   WHERE sid = s.id) AS paid,
       (SELECT COUNT(*)    FROM phone WHERE sid = s.id) AS phones
FROM s ORDER BY s.id;

প্রথম ফল Arif | 6 | 12000. দ্বিতীয় ফল Arif | 6000 | 2 আর Bristy | 500 | 0 — দুজনই আছেন, সংখ্যা ঠিক। নিজে যাচাইয়ের অভ্যাস: JOIN-এর পরে COUNT(*) দেখুন; প্রত্যাশিত সারির চেয়ে বেশি হলে fan-out হয়েছে। প্রথম কোয়েরিতে JOIN phone-কে LEFT JOIN phone করলে Bristy ফিরে আসেন (Bristy | 1 | 500), কিন্তু Arif-এর 12000 থেকেই যায়।

ফ-২৪ (সহজ) — db-uni-র instructors টেবিলে ছয়জন শিক্ষক; mentor_id একই টেবিলের instructor_id-কে নির্দেশ করে। Shahana (2)-এর mentor 1 (Dr. Anisur Rahman), Rumana (4)-এর mentor 3 (Dr. Mahbub Alam), Tasnim (6)-এর mentor 2 (Shahana); বাকি তিনজনের mentor_id NULL. দুটি self join-এ কয়টি সারি?

SELECT COUNT(*) FROM instructors i
JOIN instructors m ON i.mentor_id = m.instructor_id;
SELECT COUNT(*) FROM instructors i
LEFT JOIN instructors m ON i.mentor_id = m.instructor_id;
ফ-২৪উত্তর ও কারণ

3 আর 6. INNER-এ শুধু যাঁদের mentor আছে: Shahana → Anisur, Rumana → Mahbub, Tasnim → Shahana. LEFT-এ ছয়জনই, বাকি তিনজনের পাশে mentor-এর নাম NULL. একই টেবিলকে দুটি নাম (i আর m) দিলে DBMS দুটিকে আলাদা টেবিলের মতোই দেখে।

ফ-২৫ (কঠিন) — db-uni: students(student_id, name, dept_id, batch, cgpa) আর departments(dept_id, name, building). আটজন শিক্ষার্থী, একজনের (Habib) dept_id NULL. দুটি কোয়েরিতে কয়টি সারি?

SELECT COUNT(*) FROM students NATURAL JOIN departments;
SELECT COUNT(*) FROM students JOIN departments USING (dept_id);
ফ-২৫উত্তর ও কারণ

0 আর 7. NATURAL JOIN একই নামের সব কলামে মেলায় — এখানে dept_id আর name দুটিই। শর্তটি হয়ে যায় “শিক্ষার্থীর নাম = বিভাগের নাম”, যা কারও বেলায় সত্য নয়। USING (dept_id) শুধু একটি কলামে মেলায়: Habib-এর NULL কোনো বিভাগের সাথে মেলে না, বাকি ৭ জন মেলেন। শিক্ষা: পরে কেউ কোনো টেবিলে একই নামের কলাম যোগ করলে NATURAL JOIN-এর অর্থ নিঃশব্দে বদলে যায় — তাই বাস্তবে এটি এড়ানো হয়।

ফ-২৬ (মাঝারি) — db-shop. “কতজন গ্রাহক অন্তত একটি অর্ডার করেছেন?” তিনটি উত্তর:

SELECT COUNT(*) FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
SELECT COUNT(*) FROM customers c JOIN orders o ON o.customer_id = c.customer_id;
SELECT COUNT(DISTINCT c.customer_id)
FROM customers c JOIN orders o ON o.customer_id = c.customer_id;
ফ-২৬উত্তর ও কারণ

7, 10, 7. IN হলো semi join: প্রতিটি গ্রাহক একবারই আসেন, যতগুলো অর্ডারই থাকুক। JOIN অর্ডার ধরে সারি বানায় — Rahim ৩ বার, Karima ২ বার — তাই ১০, যা আসলে অর্ডারের সংখ্যা। COUNT(DISTINCT …) দিয়ে আবার ৭-এ ফেরা যায়, কিন্তু এর মানে আগে অপ্রয়োজনে সারি বাড়ানো হয়েছিল। “আছে কি না” প্রশ্নে IN বা EXISTS স্বাভাবিক পছন্দ।

৬ · ড্রিল ৫ — subquery, set operation আর CTE

Subquery-র প্রশ্নে আগে ঠিক করুন সেটি কোন ধরনের: একটি মান দেয় (scalar), একটি তালিকা দেয় (IN-এর জন্য), নাকি বাইরের সারির ওপর নির্ভর করে (correlated). Set operation-এ মনে রাখুন: UNION, INTERSECT, EXCEPT ডুপ্লিকেট সরায় আর দুটি NULL-কে সমান ধরে — WHERE-এর তুলনা থেকে উল্টো নিয়ম।

ফ-২৭ (সহজ) — কার কার নাম আসবে?

SELECT name FROM emp
WHERE salary > (SELECT AVG(salary) FROM emp) ORDER BY id;
ফ-২৭উত্তর ও কারণ

Rina, Babu. ভেতরের scalar subquery একবার চলে: AVG = 42000 (NULL বাদ দিয়ে, ফ-১০)। 42000-এর বেশি শুধু দুজন 50000. AVG-কে 35000 ভাবলে Sumon আর Tania-ও আসতেন — তাই গড়টি আগে ঠিক করুন।

ফ-২৮ (মাঝারি) — নিজের বিভাগের গড়ের চেয়ে বেশি বেতন কার? Kamal (বিভাগ NULL) কি আসবেন?

SELECT name FROM emp e
WHERE salary > (SELECT AVG(salary) FROM emp e2 WHERE e2.dept = e.dept)
ORDER BY id;
ফ-২৮উত্তর ও কারণ

Rina, Babu. এটি correlated subquery — বাইরের প্রতিটি সারির জন্য আবার চলে। CSE-র গড় 140000 ÷ 3 ≈ 46666.67: Rina আর Babu বেশি, Sumon কম। EEE-র গড় 40000 (Lipi-র NULL বাদ): Tania-র 40000 সমান, বেশি নয়; Lipi-র NULL তুলনা UNKNOWN. Kamal-এর বেলায় ভেতরে e2.dept = NULL — কোনো সারি মেলে না, AVG খালি সেটে NULL, আর 30000 > NULL UNKNOWN. অথচ GROUP BY দিয়ে দেখলে NULL দলের গড় 30000! GROUP BY NULL-কে দল বানায়, কিন্তু = দিয়ে মেলানো subquery NULL-কে মেলায় না।

-- প্রতিটি কর্মীর পাশে তাঁর বিভাগের গড় (correlated)
SELECT name, dept, salary,
       (SELECT ROUND(AVG(salary), 2) FROM emp e2
        WHERE e2.dept = e.dept) AS dept_avg
FROM emp e ORDER BY id;

-- GROUP BY-তে NULL নিজেই একটি দল
SELECT dept, ROUND(AVG(salary), 2) AS avg_sal
FROM emp GROUP BY dept;

প্রথম ফলে dept_avg: Rina, Sumon, Babu-র পাশে 46666.67; Tania আর Lipi-র পাশে 40000; আর Kamal-এর পাশে NULL. দ্বিতীয় ফলে তিনটি দল: NULL | 30000, CSE | 46666.67, EEE | 40000. Kamal-কে নিজের “দলের” সাথে তুলনা করতে চাইলে ভেতরের শর্ত e2.dept IS e.dept লিখুন — SQLite-এর IS দুটি NULL-কে সমান ধরে (standard SQL-এ IS NOT DISTINCT FROM).

ফ-২৯ (মাঝারি) — scalar subquery-র জায়গায় একাধিক সারি, বা একটিও না। SQLite-এ কী আসবে? PostgreSQL-এ?

SELECT (SELECT name FROM emp WHERE dept = 'CSE' ORDER BY id) AS who;
SELECT (SELECT name FROM emp WHERE dept = 'BBA') AS who;
ফ-২৯উত্তর ও কারণ

SQLite: Rina, তারপর NULL. SQLite scalar subquery-র প্রথম সারিটি নেয়, বাকিগুলো নিঃশব্দে ফেলে দেয়; কোনো সারি না পেলে NULL. PostgreSQL: প্রথমটিতে ত্রুটি — expression হিসেবে ব্যবহৃত subquery একাধিক সারি দিয়েছে। MySQL, SQL Server আর Oracle-ও ত্রুটি দেয়। দ্বিতীয়টি সবখানে NULL. SQLite-এ পরীক্ষা করা কোয়েরি অন্য DBMS-এ ভেঙে যাওয়ার একটি পরিচিত কারণ এটি।

ফ-৩০ (সহজ) — emp.dept-এ ছয়টি মান (CSE, CSE, EEE, NULL, EEE, CSE), dept.code-এ তিনটি (CSE, EEE, BBA). তিনটি কোয়েরিতে কয়টি সারি?

SELECT dept FROM emp UNION SELECT code FROM dept;
SELECT dept FROM emp UNION ALL SELECT code FROM dept;
SELECT dept FROM emp INTERSECT SELECT code FROM dept;
ফ-৩০উত্তর ও কারণ

4, 9, 2. UNION সব ডুপ্লিকেট সরায়: NULL, BBA, CSE, EEE — ৪টি (NULL একবার)। UNION ALL কিছুই সরায় না: ৬ + ৩ = ৯. INTERSECT: দুই দিকেই আছে CSE আর EEE. UNION-এর ফলের ক্রম নিশ্চিত নয় (SQLite ডুপ্লিকেট সরাতে সাজায় বলে প্রায়ই সাজানো দেখায়) — ক্রম চাইলে শেষে ORDER BY. দ্রুততার দিক থেকেও পার্থক্য আছে: UNION-কে ডুপ্লিকেট খুঁজতে সাজাতে বা hash করতে হয়, UNION ALL শুধু জোড়া লাগায়।

ফ-৩১ (মাঝারি) — ফ-১২-তে NOT IN কোনো সারি দেয়নি। এবার EXCEPT; দুই দিক থেকে কী আসবে?

SELECT code FROM dept EXCEPT SELECT dept FROM emp;
SELECT dept FROM emp EXCEPT SELECT code FROM dept;
ফ-৩১উত্তর ও কারণ

(১) BBA. (২) একটি সারি: NULL. EXCEPT তুলনা করে “আলাদা কি না” দিয়ে, যেখানে দুটি NULL আলাদা নয় — তাই emp-এর NULL বাঁ দিকের BBA-কে আটকায় না, আর (১) ঠিক উত্তর দেয়। (২)-এ Kamal-এর NULL কোনো বিভাগের code নয়, তাই সেটিই একমাত্র “বাড়তি” মান। একই প্রশ্ন, তিন রকম লেখা, তিন রকম NULL-নিয়ম: NOT IN ফাঁদে পড়ে, NOT EXISTS আর EXCEPT পড়ে না।

ফ-৩২ (মাঝারি) — recursive CTE. কয়টি সারি, শেষ মান কত? দ্বিতীয়টিতে?

WITH RECURSIVE n(i) AS (
  SELECT 1
  UNION ALL
  SELECT i * 2 FROM n WHERE i < 50)
SELECT i FROM n;

-- শর্ত বদলে: WHERE i * 2 < 50
ফ-৩২উত্তর ও কারণ

প্রথমটি ৭টি সারি: 1, 2, 4, 8, 16, 32, 64. শর্ত দেখা হয় আগের সারির ওপর: i = 32 < 50 সত্য, তাই 64 তৈরি হয় — ৫০ পেরিয়েও। তারপর 64 < 50 মিথ্যা, নতুন সারি নেই, থামা। দ্বিতীয়টি ৬টি সারি, শেষ 32 — শর্ত নতুন মানের ওপর, তাই 64 তৈরিই হয় না। এই এক-ঘর-বেশি ভুল (off-by-one) recursive প্রশ্নে খুব প্রচলিত।

ফ-৩৩ (মাঝারি) — db-uni-র mentor শৃঙ্খল (ফ-২৪-এর ডেটা)। Tasnim (6) থেকে ওপরে উঠলে কী আসবে?

WITH RECURSIVE chain(id, name, mentor_id, lvl) AS (
  SELECT instructor_id, name, mentor_id, 0
  FROM instructors WHERE instructor_id = 6
  UNION ALL
  SELECT i.instructor_id, i.name, i.mentor_id, c.lvl + 1
  FROM instructors i JOIN chain c ON i.instructor_id = c.mentor_id)
SELECT name, lvl FROM chain;
ফ-৩৩উত্তর ও কারণ
name              │ lvl
Tasnim Chowdhury  │ 0
Shahana Parvin    │ 1
Dr. Anisur Rahman │ 2

শুরুর সারি Tasnim (mentor 2). প্রথম ধাপে instructor 2 = Shahana (mentor 1), দ্বিতীয় ধাপে instructor 1 = Anisur (mentor NULL). তৃতীয় ধাপে i.instructor_id = NULL কারও সাথে মেলে না — নতুন সারি নেই, তাই থামে। NULL-ই এখানে থামার শর্ত। ডেটায় চক্র থাকলে (A-র mentor B, B-র mentor A) এই কোয়েরি থামত না — তখন recursive অংশে WHERE c.lvl < 10-এর মতো সীমা দিতে হয়। (এখানে UNION দিয়েও বাঁচা যেত না, কারণ lvl প্রতিবার বাড়ে বলে প্রতিটি সারিই নতুন।)

-- ফ-৩২: দুই রকম থামার শর্ত
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i * 2 FROM n WHERE i < 50)
SELECT COUNT(*) AS rows_, MAX(i) AS last FROM n;
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i * 2 FROM n WHERE i * 2 < 50)
SELECT COUNT(*) AS rows_, MAX(i) AS last FROM n;

-- ফ-৩৩: Tasnim থেকে ওপরের দিকে
WITH RECURSIVE chain(id, name, mentor_id, lvl) AS (
  SELECT instructor_id, name, mentor_id, 0
  FROM instructors WHERE instructor_id = 6
  UNION ALL
  SELECT i.instructor_id, i.name, i.mentor_id, c.lvl + 1
  FROM instructors i JOIN chain c ON i.instructor_id = c.mentor_id)
SELECT name, lvl FROM chain;

ফল: 7 | 64, তারপর 6 | 32, তারপর তিনটি সারি Tasnim Chowdhury | 0, Shahana Parvin | 1, Dr. Anisur Rahman | 2. শুরুর instructor_id = 6 বদলে 4 করে দেখুন: Rumana (0) আর Dr. Mahbub Alam (1) — দুটি সারি।

ফ-৩৪ (কঠিন) — relational division: “CSE বিভাগের (dept_id = 1) সব ৩-ক্রেডিটের কোর্সে ভর্তি হয়েছেন কে?” db-uni-তে এমন কোর্স তিনটি: CSE101, CSE201, CSE301 (CSE302 ১.৫ ক্রেডিট)। CSE কোর্সে ভর্তি: Arif — CSE201, CSE301, CSE302; Bristy — CSE201, CSE301; Dilruba — CSE101, CSE201, CSE301 (CSE301-এর grade এখনো NULL); Gulshan — CSE101. দুই স্তরের NOT EXISTS কী দেবে? আর কোর্সের শর্ত dept_id = 99 (এমন কোর্স নেই) হলে?

SELECT s.name FROM students s
WHERE NOT EXISTS (
  SELECT 1 FROM courses c WHERE c.dept_id = 1 AND c.credits = 3
  AND NOT EXISTS (SELECT 1 FROM enrollments e
                  WHERE e.student_id = s.student_id
                    AND e.course_id = c.course_id));
ফ-৩৪উত্তর ও কারণ

শুধু Dilruba Khatun. কোয়েরিটি পড়ুন এভাবে: “এমন কোনো কোর্স নেই, যেটিতে শিক্ষার্থীটি ভর্তি হননি।” Arif-এর CSE101 নেই, Bristy-র CSE101 নেই, Gulshan-এর CSE201 নেই। Dilruba-র তিনটিই আছে — grade NULL হলেও ভর্তির সারিটি আছে, আর কোয়েরি grade দেখে না। “পাস করেছেন” চাইলে ভেতরে AND e.grade IS NOT NULL AND e.grade <> 'F' লাগত, তখন Dilruba-ও বাদ।

dept_id = 99 হলে ৮ জনই আসেন। কোর্সের সেট খালি হলে “এমন কোনো কোর্স নেই যেটি তিনি নেননি” সবার জন্য সত্য — গণিতের ভাষায় vacuous truth. Division-এর এই আচরণ বীজগণিতের সংজ্ঞার সাথেও মেলে, কিন্তু রিপোর্টে প্রায়ই অপ্রত্যাশিত।

৭ · ড্রিল ৬ — window function

Window function সারির সংখ্যা বদলায় না — প্রতিটি সারির পাশে একটি নতুন মান বসায়। তাই প্রশ্নের মূল কাজ তিনটি: partition কোনটি, ভেতরের ক্রম কী (টাই আর NULL কোথায়), আর frame কতটুকু। মনে রাখুন, window চলে WHERE আর GROUP BY-এর পরে, কিন্তু ORDER BY আর LIMIT-এর আগে।

ফ-৩৫ (সহজ) — SQLite-এ rn, rk আর dr কলামে কী আসবে?

SELECT name, salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC, id) AS rn,
       RANK()       OVER (ORDER BY salary DESC)     AS rk,
       DENSE_RANK() OVER (ORDER BY salary DESC)     AS dr
FROM emp ORDER BY rn;
ফ-৩৫উত্তর ও কারণ
name  │ salary │ rn │ rk │ dr
Rina  │ 50000  │ 1  │ 1  │ 1
Babu  │ 50000  │ 2  │ 1  │ 1
Sumon │ 40000  │ 3  │ 3  │ 2
Tania │ 40000  │ 4  │ 3  │ 2
Kamal │ 30000  │ 5  │ 5  │ 3
Lipi  │ NULL   │ 6  │ 6  │ 4

ROW_NUMBER সবসময় আলাদা নম্বর (টাই id দিয়ে ভাঙা)। RANK = আমার চেয়ে আগের সারির সংখ্যা + 1, তাই টাই-এর পরে ফাঁক (1, 1, 3). DENSE_RANK = আগের আলাদা মানের সংখ্যা + 1, ফাঁক নেই। Lipi-র NULL একটি আলাদা মান হিসেবেই র‍্যাংক পায় — SQLite-এ DESC ক্রমে শেষে। PostgreSQL-এ DESC-এ NULL প্রথমে, সেখানে Lipi হতেন rn 1, rk 1, dr 1, আর বাকি সবার নম্বর এক ঘর করে সরে যেত।

ফ-৩৬ (কঠিন) — “প্রতি বিভাগে দ্বিতীয় সর্বোচ্চ বেতন” — কার নাম আসবে? SQLite-এ আর PostgreSQL-এ।

WITH r AS (
  SELECT name, dept, salary,
         DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr
  FROM emp)
SELECT name, dept, salary FROM r WHERE dr = 2 ORDER BY dept;
ফ-৩৬উত্তর ও কারণ
name  │ dept │ salary
Sumon │ CSE  │ 40000
Lipi  │ EEE  │ NULL

CSE: 50000 (Rina, Babu) dr 1, 40000 (Sumon) dr 2 — ঠিক। NULL দলে শুধু Kamal, dr 2 কেউ নেই। চমকটি EEE-তে: Tania 40000 dr 1, আর Lipi-র NULL একটি আলাদা মান হিসেবে dr 2 পায়! “বেতন জানা নেই” এমন কাউকে “দ্বিতীয় সর্বোচ্চ বেতন” বলা অর্থহীন। PostgreSQL-এ DESC-এ NULL প্রথমে: Lipi dr 1, Tania dr 2 — ফল Sumon আর Tania. দুই DBMS-এ দুই রকম, দুটিই প্রশ্নের দিক থেকে ভুলের ঝুঁকিতে। ঠিক পথ: CTE-র ভেতরে WHERE salary IS NOT NULL.

-- ফ-৩৫: তিন রকম র‍্যাংক
SELECT name, salary,
       ROW_NUMBER() OVER (ORDER BY salary DESC, id) AS rn,
       RANK()       OVER (ORDER BY salary DESC)     AS rk,
       DENSE_RANK() OVER (ORDER BY salary DESC)     AS dr
FROM emp ORDER BY rn;

-- ফ-৩৬: NULL বাদ দিয়ে দ্বিতীয় সর্বোচ্চ
WITH r AS (
  SELECT name, dept, salary,
         DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr
  FROM emp WHERE salary IS NOT NULL)
SELECT name, dept, salary FROM r WHERE dr = 2 ORDER BY dept;

প্রথম ফল ফ-৩৫-এর টেবিলের ছয়টি সারি। দ্বিতীয় ফল এখন একটিই সারি: Sumon | CSE | 40000 — Lipi বাদ, আর EEE-তে বেতন-জানা একজনই (Tania), তাই সেখানে দ্বিতীয় কেউ নেই। WHERE-এর শর্তটি মুছে দিলে Lipi | EEE | NULL ফিরে আসে।

ফ-৩৭ (কঠিন) — দুটি running total; পার্থক্য শুধু frame-এ। ছয়টি সারিতে s_range আর s_rows কত?

SELECT name, salary,
       SUM(salary) OVER (ORDER BY salary) AS s_range,
       SUM(salary) OVER (ORDER BY salary, id ROWS UNBOUNDED PRECEDING) AS s_rows
FROM emp ORDER BY salary, id;
ফ-৩৭উত্তর ও কারণ
name  │ salary │ s_range │ s_rows
Lipi  │ NULL   │ NULL    │ NULL
Kamal │ 30000  │ 30000   │ 30000
Sumon │ 40000  │ 110000  │ 70000
Tania │ 40000  │ 110000  │ 110000
Rina  │ 50000  │ 210000  │ 160000
Babu  │ 50000  │ 210000  │ 210000

ORDER BY দিলে default frame হলো RANGE … CURRENT ROW — বর্তমান সারির সব সমান-মানের সঙ্গী (peer)-সহ। তাই Sumon-এর সারিতেই Tania-র 40000 ঢুকে 110000. ROWS এক সারি করে এগোয়: 30000, 70000, 110000… Lipi প্রথম সারি (SQLite-এ ছোট থেকে বড়-তে NULL আগে), frame-এ শুধু NULL — SUM NULL. PostgreSQL-এ Lipi শেষে আসতেন, আর তাঁর s_range হতো 210000 (frame-এ সবাই, SUM NULL বাদ দেয়)।

ফ-৩৮ (মাঝারি) — LAG-এর তৃতীয় argument হলো default মান। id 1, 5 আর 6-এর সারিতে prev আর diff কত?

SELECT id, salary,
       LAG(salary) OVER (ORDER BY id) AS prev,
       salary - LAG(salary, 1, 0) OVER (ORDER BY id) AS diff
FROM emp ORDER BY id;
ফ-৩৮উত্তর ও কারণ
id │ salary │ prev  │ diff
1  │ 50000  │ NULL  │ 50000
2  │ 40000  │ 50000 │ -10000
3  │ 40000  │ 40000 │ 0
4  │ 30000  │ 40000 │ -10000
5  │ NULL   │ 30000 │ NULL
6  │ 50000  │ NULL  │ NULL

id 1-এর আগে কোনো সারি নেই — prev NULL, কিন্তু LAG(…, 1, 0) default 0 দেয়, diff = 50000. id 5: নিজের বেতন NULL, তাই NULL − 30000 = NULL. id 6: আগের সারি আছে (Lipi), তার মান NULL — default শুধু “সারি নেই” হলে কাজ করে, “মান NULL” হলে নয়। তাই diff NULL. এই পার্থক্যটি পরীক্ষায় প্রায়ই ধরা হয়।

-- ফ-৩৭: RANGE বনাম ROWS
SELECT name, salary,
       SUM(salary) OVER (ORDER BY salary) AS s_range,
       SUM(salary) OVER (ORDER BY salary, id
                         ROWS UNBOUNDED PRECEDING) AS s_rows
FROM emp ORDER BY salary, id;

-- ফ-৩৮: LAG-এর default আর NULL
SELECT id, salary,
       LAG(salary) OVER (ORDER BY id) AS prev,
       salary - LAG(salary, 1, 0) OVER (ORDER BY id) AS diff
FROM emp ORDER BY id;

দুটি ফলই ওপরের দুই টেবিলের সাথে হুবহু মেলে: s_range কলামে NULL, 30000, 110000, 110000, 210000, 210000, s_rows-এ NULL, 30000, 70000, 110000, 160000, 210000; আর diff কলামে 50000, -10000, 0, -10000, NULL, NULL. নিজে চেষ্টা করুন: LAG(COALESCE(salary, 0), 1, 0) লিখলে id 6-এর diff কত হবে? (উত্তর: 50000.)

ফ-৩৯ (মাঝারি) — তিনটি কোয়েরি। প্রথমটির total কত? দ্বিতীয় আর তৃতীয়টিতে কয়টি সারি?

SELECT name, COUNT(*) OVER () AS total
FROM emp WHERE dept = 'CSE' ORDER BY id LIMIT 1;
SELECT dept, AVG(salary) OVER (PARTITION BY dept) FROM emp;
SELECT DISTINCT dept, AVG(salary) OVER (PARTITION BY dept) FROM emp;
ফ-৩৯উত্তর ও কারণ

(১) Rina | 3. WHERE আগে চলে (৩ জন CSE), window সেই ৩ জনের ওপর হিসাব করে, LIMIT সবার শেষে একটি সারি রাখে — কিন্তু total-এ 3 থেকেই যায়, 1 নয়। (২) ৬টি সারি — window সারি গোটায় না; CSE-র তিনটি সারিতেই 46666.67… (৩) ৩টি সারি — DISTINCT চলে window-এর পরে, একই (dept, গড়) জোড়াগুলো এক করে। ফল GROUP BY-এর মতো দেখালেও কাজটি অনেক বেশি; সরাসরি GROUP BY লেখাই ভালো।

ফ-৪০ (মাঝারি) — ছয়টি সারিকে NTILE(4) চার ভাগে ভাগ করে। কোন id কোন ভাগে?

SELECT id, NTILE(4) OVER (ORDER BY id) AS q FROM emp;
ফ-৪০উত্তর ও কারণ

id 1, 2 → 1; id 3, 4 → 2; id 5 → 3; id 6 → 4. ৬ ÷ ৪ = ১, ভাগশেষ ২. প্রতিটি ভাগে অন্তত ১টি সারি, আর প্রথম ২টি (ভাগশেষের সমান) ভাগে একটি করে বেশি: আকার ২, ২, ১, ১. NTILE মান দেখে না, শুধু সারি গোনে — তাই সমান বেতনের দুজন আলাদা ভাগে যেতে পারেন।

৮ · ড্রিল ৭ — UPDATE, DELETE, constraint, trigger আর transaction

ডেটা বদলানোর প্রশ্নে “ফল” মানে তিনটি জিনিস: কয়টি সারি বদলাল (rows affected), কোন স্টেটমেন্ট ব্যর্থ হলো আর কেন, আর সব শেষে টেবিলের অবস্থা কী। দুটি নিয়ম প্রায় সব প্রশ্নের চাবি। এক, একটি স্টেটমেন্ট হয় পুরোটা সফল, নয়তো কিছুই না — statement atomicity. দুই, ভেতরের subquery বদলের আগের টেবিল দেখে।

ফ-৪১ (সহজ) — প্রতিটি UPDATE-এ কয়টি সারি বদলাবে? Lipi-র বেতন শেষে কত?

UPDATE emp SET salary = salary + 5000 WHERE dept = 'EEE';
UPDATE emp SET dept = 'BBA' WHERE dept = NULL;
ফ-৪১উত্তর ও কারণ

প্রথমটিতে ২টি সারি, দ্বিতীয়টিতে ০টি। WHERE Tania আর Lipi দুজনকেই ধরে, তাই SQLite (আর বেশিরভাগ DBMS) বলে ২টি সারি বদলেছে — যদিও Lipi-র মান NULL + 5000 = NULL-ই থেকে গেছে। Tania এখন 45000. দ্বিতীয়টিতে = NULL কোনো সারি ধরে না (ফ-০৯), কোনো ত্রুটিও দেয় না। একটি পার্থক্য: MySQL ডিফল্টভাবে শুধু যে সারির মান সত্যিই বদলেছে সেগুলো “affected” বলে, তাই প্রথমটিতেও সেখানে ১ দেখাত।

ফ-৪২ (মাঝারি) — p(id, a, b) টেবিলে একটি সারি: (1, 10, 20). UPDATE-এর পরে a আর b কত?

UPDATE p SET a = b, b = a;
ফ-৪২উত্তর ও কারণ

SQLite, PostgreSQL, SQL Server, Oracle: a = 20, b = 10 — অদলবদল হয়। Standard SQL-এ SET-এর ডান দিকের সব expression হিসাব হয় সারির পুরোনো মান দিয়ে, যেন সব একসাথে বসানো হলো। MySQL-এ a = 20, b = 20. MySQL একক টেবিলের UPDATE-এ বাঁ থেকে ডানে একটি একটি করে বসায়, তাই b = a দেখে নতুন a (20). MySQL-এর documentation নিজেই এই আচরণ উল্লেখ করে — মান অদলবদলে সেখানে একটি অস্থায়ী মান লাগে।

ফ-৪৩ (মাঝারি) — কয়টি সারি মুছবে, কে কে থাকবেন? গড় কি প্রতিটি মোছার পরে নতুন করে হিসাব হয়?

DELETE FROM emp WHERE salary < (SELECT AVG(salary) FROM emp);
ফ-৪৩উত্তর ও কারণ

৩টি সারি মোছে (Sumon, Tania, Kamal); থাকেন Rina, Lipi, Babu. গড় একবারই হিসাব হয় — মোছার আগের টেবিলে, 42000. প্রতিটি মোছার পরে নতুন গড় হলে ফল সারির ক্রমের ওপর নির্ভর করত, যা SQL-এর অর্থের সাথে মেলে না। Lipi টিকে যান: NULL < 42000 UNKNOWN, আর DELETE-ও শুধু TRUE সারি মোছে। (MySQL-এ একই টেবিল subquery-তে পড়ে DELETE করা যায় না — ত্রুটি 1093; subquery-কে আরেকটি derived table-এ মুড়ে লিখতে হয়।)

-- ফ-৪১
UPDATE emp SET salary = salary + 5000 WHERE dept = 'EEE';
UPDATE emp SET dept = 'BBA' WHERE dept = NULL;

-- ফ-৪২
UPDATE p SET a = b, b = a;
SELECT * FROM p;

-- ফ-৪৩ (খেয়াল করুন: ফ-৪১-এর পরে Tania এখন 45000)
DELETE FROM emp WHERE salary < (SELECT AVG(salary) FROM emp);
SELECT name, salary FROM emp ORDER BY id;

ফল: “✓ UPDATE — ২টি সারি বদলেছে”, “✓ UPDATE — ০টি সারি বদলেছে”, অদলবদলের UPDATE-এ “১টি সারি বদলেছে”, তারপর p-তে 1 | 20 | 10. এরপর DELETE বলে “২টি সারি বদলেছে”, আর বাকি থাকেন চারজন: Rina | 50000, Tania | 45000, Lipi | NULL, Babu | 50000. কেন ৩ নয়? ফ-৪১-এর UPDATE আগে চলেছে বলে গড় এখন 215000 ÷ 5 = 43000, আর Tania-র 45000 তার ওপরে। একই এডিটরের আগের স্টেটমেন্ট পরের প্রশ্নের ডেটা বদলে দেয় — trace-এর সময় এটিও মনে রাখতে হয়। ওপরের দুটি UPDATE মুছে চালালে DELETE ঠিক ৩টি সারি মোছে।

ফ-৪৪ (মাঝারি) — t(id INTEGER PRIMARY KEY, v TEXT NOT NULL)-এ আগে থেকে (1, 'a'). নিচের এক স্টেটমেন্টে তিনটি সারি; দ্বিতীয়টির id ডুপ্লিকেট। চালানোর পরে t-তে কয়টি সারি? আর INSERT OR IGNORE লিখলে?

INSERT INTO t VALUES (2, 'b'), (1, 'c'), (3, 'd');
ফ-৪৪উত্তর ও কারণ

ত্রুটি (UNIQUE constraint failed: t.id), আর t-তে ১টি সারিই থাকে। (2, 'b') আগে ঢুকেছিল, কিন্তু স্টেটমেন্ট ব্যর্থ হওয়ায় সেটিও ফিরিয়ে নেওয়া হয় — statement atomicity. “প্রথম সারি ঢুকে গেছে” ভাবা সাধারণ ভুল। INSERT OR IGNORE (SQLite; MySQL-এ INSERT IGNORE, PostgreSQL-এ ON CONFLICT DO NOTHING) সমস্যার সারিটি বাদ দিয়ে বাকিগুলো রাখে: (1, 'a'), (2, 'b'), (3, 'd') — ৩টি সারি, আর বার্তা “২টি সারি বদলেছে”।

ফ-৪৫ (কঠিন) — db-uni-র students.cgpa-এ CHECK (cgpa BETWEEN 0 AND 4); db-shop-এর customers.phone-এ UNIQUE, আর Tanvir-এর phone আগে থেকেই NULL. কোন INSERT সফল হবে?

INSERT INTO students VALUES (2401, 'Jui Akter', 1, 2024, NULL);
INSERT INTO customers VALUES (9, 'Nayeem Islam', 'Barishal', NULL, '2025-09-30');
INSERT INTO students VALUES (2402, 'Kawsar Ali', 1, 2024, 4.5);
ফ-৪৫উত্তর ও কারণ

প্রথম দুটি সফল, তৃতীয়টি ব্যর্থ (CHECK constraint failed). CHECK শুধু FALSE হলে আটকায়; NULL BETWEEN 0 AND 4 UNKNOWN, তাই NULL cgpa ঢুকে যায়। NULL আটকাতে আলাদা NOT NULL লাগে। UNIQUE-এর চোখে দুটি NULL “সমান” নয়, তাই দ্বিতীয় NULL phone-ও বৈধ — SQLite, PostgreSQL, MySQL আর Oracle-এ। SQL Server এখানে ব্যতিক্রম: UNIQUE constraint সেখানে একটির বেশি NULL মানে না। (PostgreSQL 15 থেকে চাইলে UNIQUE NULLS NOT DISTINCT লিখে SQL Server-এর মতো আচরণ পাওয়া যায়।)

ফ-৪৬ (সহজ) — db-shop. orders.customer_id customers-কে foreign key দিয়ে নির্দেশ করে, কোনো ON DELETE action নেই। Tanvir (5)-এর কোনো অর্ডার নেই, Rahim (1)-এর তিনটি। কোন DELETE সফল?

DELETE FROM customers WHERE customer_id = 5;
DELETE FROM customers WHERE customer_id = 1;
ফ-৪৬উত্তর ও কারণ

প্রথমটি সফল (১টি সারি), দ্বিতীয়টি ব্যর্থ — FOREIGN KEY constraint failed. Rahim-কে মুছলে তিনটি অর্ডার এমন গ্রাহককে নির্দেশ করত, যিনি নেই। ON DELETE না লিখলে ডিফল্ট NO ACTION — মোছা আটকায়। মনে রাখুন, SQLite-এ foreign key তখনই কাজ করে, যখন PRAGMA foreign_keys = ON (এই পাতার এডিটরে চালু); ডিফল্টভাবে বন্ধ থাকে, তখন দ্বিতীয় DELETE-ও নিঃশব্দে সফল হতো।

-- ফ-৪৫: CHECK আর UNIQUE-এ NULL
INSERT INTO students VALUES (2401, 'Jui Akter', 1, 2024, NULL);
INSERT INTO customers VALUES (9, 'Nayeem Islam', 'Barishal', NULL, '2025-09-30');
SELECT COUNT(*) AS null_phones FROM customers WHERE phone IS NULL;

-- ফ-৪৬: FOREIGN KEY
DELETE FROM customers WHERE customer_id = 5;
DELETE FROM customers WHERE customer_id = 1;   -- এখানে ত্রুটি

ফল: দুটি “✓ INSERT — ১টি সারি বদলেছে”, তারপর null_phones | 2, তারপর “✓ DELETE — ১টি সারি বদলেছে”, আর শেষে “ত্রুটি (স্টেটমেন্ট ৫): FOREIGN KEY constraint failed”. এডিটর প্রথম ত্রুটিতে থামে, তাই ত্রুটির স্টেটমেন্টটি শেষে রাখা হয়েছে। নিজে চেষ্টা করুন: শেষ লাইনটির জায়গায় INSERT INTO students VALUES (2402, 'Kawsar Ali', 1, 2024, 4.5); লিখুন — বার্তা আসবে CHECK constraint failed.

ফ-৪৭ (কঠিন) — parent(id)-এ 1 আর 2; child(id, pid REFERENCES parent(id) ON DELETE CASCADE)-এ চারটি সারি — তিনটির pid 1, একটির 2. DELETE-এর পরে SQLite কয়টি “সারি বদলেছে” বলবে, আর child-এ কয়টি সারি থাকবে?

DELETE FROM parent WHERE id = 1;
SELECT changes();
SELECT COUNT(*) FROM child;
ফ-৪৭উত্তর ও কারণ

changes() = 1, child-এ ১টি সারি। CASCADE তিনটি child সারিও মুছেছে, কিন্তু SQLite-এর changes() শুধু স্টেটমেন্টটি সরাসরি যে টেবিলে কাজ করেছে, সেখানকার সারি গোনে — foreign key action বা trigger-এ বদলানো সারি নয়। তাই “কয়টি সারি গেল” জানতে child টেবিলেই গুনুন। অ্যাপের লগে “১টি সারি মুছেছে” দেখে ভরসা করা বিপজ্জনক হতে পারে।

ফ-৪৮ (মাঝারি) — db-shop. products-এ CHECK (stock >= 0); Headphone (102)-এর stock 40, Power Bank (103)-এর 0. নিচের AFTER trigger-এর পরে দুটি INSERT. প্রতিটির ফল কী, শেষে দুটি পণ্যের stock কত?

CREATE TRIGGER sold AFTER INSERT ON order_items
BEGIN
  UPDATE products SET stock = stock - NEW.quantity
  WHERE product_id = NEW.product_id;
END;
INSERT INTO order_items VALUES (1006, 102, 3, 1200);
INSERT INTO order_items VALUES (1006, 103, 1, 1800);
ফ-৪৮উত্তর ও কারণ

প্রথম INSERT সফল, Headphone-এর stock 37. দ্বিতীয়টি ব্যর্থ — CHECK constraint failed: stock >= 0; Power Bank-এর stock 0-ই থাকে, আর order_items-এ নতুন সারিটিও নেই। Trigger-এর কাজ স্টেটমেন্টেরই অংশ: trigger-এর ভেতরের UPDATE ব্যর্থ হলে পুরো INSERT ফিরিয়ে নেওয়া হয়। এভাবেই “স্টকের বেশি বিক্রি” ডেটাবেস নিজেই আটকায়।

ফ-৪৯ (মাঝারি) — acc(id, bal)-এ (1, 1000) আর (2, 500). SAVEPOINT-সহ একটি transaction-এর পরে দুটি ব্যালান্স কত?

BEGIN;
UPDATE acc SET bal = bal - 200 WHERE id = 1;
SAVEPOINT s1;
UPDATE acc SET bal = bal + 200 WHERE id = 2;
UPDATE acc SET bal = 0 WHERE id = 1;
ROLLBACK TO s1;
UPDATE acc SET bal = bal + 300 WHERE id = 2;
COMMIT;
ফ-৪৯উত্তর ও কারণ
id │ bal
1  │ 800
2  │ 800

SAVEPOINT-এর আগের কাজ (id 1 থেকে 200 কমানো) টিকে থাকে। ROLLBACK TO s1 শুধু s1-এর পরের দুটি UPDATE বাতিল করে — id 2-এর +200 আর id 1-এর 0. Transaction তখনও খোলা; তারপর id 2-এ +300, আর COMMIT সব স্থায়ী করে: 1000 − 200 = 800, 500 + 300 = 800. সাধারণ ভুল: ROLLBACK TO-কে পুরো transaction-এর ROLLBACK ভাবা (তাহলে 1000 আর 800 হতো না), অথবা ভাবা যে এর পরে transaction শেষ।

BEGIN;
UPDATE acc SET bal = bal - 200 WHERE id = 1;
SAVEPOINT s1;
UPDATE acc SET bal = bal + 200 WHERE id = 2;
UPDATE acc SET bal = 0 WHERE id = 1;
ROLLBACK TO s1;
UPDATE acc SET bal = bal + 300 WHERE id = 2;
COMMIT;
SELECT * FROM acc;

-- আরেকটি: RELEASE কি COMMIT?
BEGIN;
DELETE FROM acc WHERE id = 2;
SAVEPOINT a;
UPDATE acc SET bal = 1 WHERE id = 1;
RELEASE a;
ROLLBACK;
SELECT * FROM acc;

প্রথম SELECT দেয় 1 | 800 আর 2 | 800. দ্বিতীয় transaction-এর পরেও ঠিক একই দুটি সারি: RELEASE a শুধু savepoint-টি সরিয়ে তার কাজগুলো বাইরের transaction-এ মিশিয়ে দেয়, কিছু স্থায়ী করে না। তাই শেষের ROLLBACK DELETE আর UPDATE দুটিই বাতিল করে। আর বাইরে কোনো BEGIN না থাকলে? SQLite-এ তখন SAVEPOINT নিজেই একটি transaction শুরু করে, আর সেটির RELEASE সবকিছু COMMIT করে — কিন্তু এখানে BEGIN আগেই ছিল।

ফ-৫০ (কঠিন) — t-তে আগে থেকে (1, 'a'). একটি transaction-এর মাঝখানে একটি স্টেটমেন্ট ব্যর্থ হয়। SQLite-এ শেষে t-তে কী থাকবে? PostgreSQL-এ?

BEGIN;
INSERT INTO t VALUES (2, 'b');
INSERT INTO t VALUES (1, 'x');      -- PRIMARY KEY ডুপ্লিকেট: ত্রুটি
INSERT INTO t VALUES (3, 'c');
COMMIT;
ফ-৫০উত্তর ও কারণ

SQLite: (1, 'a'), (2, 'b'), (3, 'c'). ব্যর্থ স্টেটমেন্টটি একাই ফিরিয়ে নেওয়া হয়; transaction খোলা থাকে, পরের INSERT চলে, COMMIT বাকিগুলো স্থায়ী করে। MySQL (InnoDB) আর SQL Server-এর ডিফল্ট আচরণও এ রকম — অ্যাপকেই ত্রুটি দেখে ROLLBACK করতে হয়। PostgreSQL: শুধু (1, 'a'). সেখানে একটি ত্রুটির পরে পুরো transaction “aborted” অবস্থায় যায়: পরের INSERT বাতিল হয় (current transaction is aborted…), আর COMMIT আসলে ROLLBACK করে। একই কোড, দুই DBMS-এ দুই ফল — তাই অ্যাপে ত্রুটি ধরলেই নিজে ROLLBACK করাই নিরাপদ অভ্যাস (পর্ব ৪৪, ৪৯)। (এই প্রশ্নের জন্য লাইভ এডিটর নেই, কারণ এডিটর প্রথম ত্রুটিতেই থেমে যায়; উত্তরটি Python-এর sqlite3 দিয়ে চালিয়ে যাচাই করা।)

৯ · খাতায় উত্তর লেখা আর SQLite বনাম অন্য DBMS — এক টেবিলে

লিখিত পরীক্ষায় শুধু ফল-টেবিল লিখলে ভুল হলে কোনো নম্বর নেই; যুক্তির ধাপ লিখলে আংশিক নম্বরের সুযোগ থাকে। নিচের ছকটি প্রায় সব output প্রশ্নে খাটে।

খাতায় উত্তরের ছক

১. ধরে নেওয়া লিখুন: “standard SQL / SQLite ধরে উত্তর; ORDER BY না থাকায় সারির ক্রম নিশ্চিত নয়।” এক লাইনেই পরীক্ষক বোঝেন, আপনি ফাঁদগুলো জানেন।

২. ধাপের trace table: FROM/JOIN-এর পরে কয়টি সারি → WHERE-এর পরে → দল → HAVING-এর পরে। প্রতিটি সংখ্যার পাশে এক শব্দের কারণ (“NULL বাদ”, “fan-out ×২”)।

৩. চূড়ান্ত টেবিল: কলামের নাম (alias থাকলে alias), প্রতিটি সারি, আর NULL স্পষ্ট করে “NULL” লেখা — ফাঁকা ঘর নয়।

৪. এক লাইনের “কেন”: উত্তরের মূল ফাঁদটি নাম ধরে বলুন: “NOT IN-এর তালিকায় NULL থাকায় কোনো সারি নেই”, “AVG NULL বাদ দেয়”।

৫. DBMS-এর পার্থক্য (প্রাসঙ্গিক হলে): “PostgreSQL-এ NULL শেষে আসত, তাই …” — এটি বাড়তি নম্বর আনে, আর ইন্টারভিউতে অভিজ্ঞতার পরিচয় দেয়।

এই পাতায় যেসব জায়গায় DBMS ভেদে ফল বদলেছে, সব এক টেবিলে:

বিষয় (প্রশ্ন)SQLitePostgreSQLMySQLঅন্যরা
ক্রমে NULL-এর জায়গা (ফ-০২, ৩৫–৩৭)সবচেয়ে ছোট — ASC-এ আগেসবচেয়ে বড় — ASC-এ শেষে, DESC-এ আগেSQLite-এর মতোSQL Server: ছোট; Oracle: বড়
7/2 (ফ-০৫)333.5000 (DIV দিলে 3)SQL Server 3; Oracle 3.5
'abc' || NULL (ফ-০৬)NULLNULL|| মানে OR; CONCAT দিলে NULLOracle: 'abc'
LENGTH('ঢাকা') (ফ-০৬)4 (অক্ষর)412 (বাইট); CHAR_LENGTH 4SQL Server LEN: 4
Jan 31 + ১ মাস (ফ-০৭)2025-03-032025-02-282025-02-28—
LIKE-এ বড়-ছোট হাত (ফ-০৮)ASCII-তে পার্থক্য নেইপার্থক্য আছে (না চাইলে ILIKE)collation অনুযায়ী (ডিফল্টে নেই)SQL Server: collation অনুযায়ী
Bare column (ফ-১৭)চলে; MIN/MAX-এর সারি থেকেত্রুটিত্রুটি (ONLY_FULL_GROUP_BY)ত্রুটি
Scalar subquery-তে বহু সারি (ফ-২৯)প্রথম সারিত্রুটিত্রুটিত্রুটি
FULL JOIN (ফ-২০)আছে (3.39+)আছেনেইআছে
SET a = b, b = a (ফ-৪২)অদলবদলঅদলবদলদুটিই 20অদলবদল
UNIQUE-এ একাধিক NULL (ফ-৪৫)চলেচলে (চাইলে NULLS NOT DISTINCT)চলেSQL Server: একটি
Transaction-এর ভেতরে ত্রুটি (ফ-৫০)শুধু সেই স্টেটমেন্ট বাতিলপুরো transaction বাতিলশুধু সেই স্টেটমেন্টSQL Server: ডিফল্টে স্টেটমেন্ট

টেবিলের বাইরেও একটি বড় পার্থক্য আছে: SQLite-এর type affinity — SQLite কলামের ঘোষিত টাইপ নমনীয়ভাবে মানে (একটি INTEGER কলামেও লেখা রাখা যায়), কিন্তু বেশিরভাগ DBMS কঠোরভাবে মানে; ফ-০৮-এর মতো text আর সংখ্যার তুলনায় এর প্রভাব পড়ে; বিস্তারিত পর্ব ১৩ আর ৩১-এ।

১০ · AI-এর যুগে — AI-এর বলা “ফল” যাচাই করা

AI-কে “এই কোয়েরি কী ফল দেবে?” জিজ্ঞেস করলে প্রায়ই আত্মবিশ্বাসী, সুন্দর করে সাজানো একটি টেবিল পাওয়া যায় — আর ঠিক এই পাতার ফাঁদগুলোতেই সেটি ভুল হয়: NOT IN-এর NULL, COUNT(*) বনাম COUNT(col), LEFT JOIN-এর পরে WHERE, RANGE frame-এর টাই, PostgreSQL আর SQLite-এর NULL-ক্রম। কারণ ভাষা-মডেল কোয়েরি চালায় না; সে “সাধারণত কী আসে” তার ভিত্তিতে লেখে। AI-কে কাজে লাগানোর সবচেয়ে ভালো উপায় তাকে উত্তরদাতা নয়, প্রশ্ন-বানানো সহকারী হিসেবে ব্যবহার করা — আর উত্তর নিজে চালিয়ে যাচাই করা।

নিচের prompt দিয়ে AI-কে নতুন অনুশীলনী প্রশ্ন বানাতে বলুন। তারপর প্রতিটির উত্তর আগে নিজে বের করুন, তারপর এই পাতার যেকোনো এডিটরে চালিয়ে মেলান — AI-এর দেওয়া উত্তরের সাথেও।

তুমি একজন SQL পরীক্ষক। নিচের দুটি টেবিল (CREATE TABLE আর INSERT দিলাম) ব্যবহার করে ৫টি "এই কোয়েরির ফল কী?" প্রশ্ন বানাও। নিয়ম: ১. প্রতিটি প্রশ্ন একটি নির্দিষ্ট ফাঁদ পরীক্ষা করবে: NULL তুলনা, NOT IN-এ NULL, LEFT JOIN-এর পরে WHERE, GROUP BY/HAVING, আর একটি window function-এ টাই। ২. শুধু এই টেবিলের কলাম ব্যবহার করবে; নতুন কলাম বা টেবিল বানাবে না। ৩. প্রতিটি কোয়েরিতে ORDER BY দেবে, যাতে ফলের ক্রম নিশ্চিত হয়। ৪. উত্তর এখনই দেবে না। আমি নিজের উত্তর লিখে পাঠালে তখন মেলাবে। ৫. মেলানোর সময় বলবে কোন DBMS ধরে উত্তর দিচ্ছ (SQLite), আর PostgreSQL-এ ফল আলাদা হলে কেন। ৬. তোমার উত্তর SQLite-এ চালিয়ে যাচাই করা নয় — সেটি স্পষ্ট করে লিখবে, আমি নিজে চালিয়ে মেলাব। [এখানে CREATE TABLE আর INSERT]

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

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

সব প্রশ্নই “ফল কী আসবে” ধরনের — SQLite ধরে উত্তর দিন। ভুল হলে কারণসহ ব্যাখ্যা পাবেন।

ক্রমএকটি SELECT কোয়েরির অংশগুলো যে যৌক্তিক ক্রমে মূল্যায়ন হয়, সেভাবে সাজান (▲▼ চেপে সরান)

  1. FROM আর JOIN — কোন সারিগুলো নিয়ে কাজ
  2. WHERE — প্রতিটি সারি ছাঁকা
  3. GROUP BY আর HAVING — দলে ভাগ, দল ছাঁকা
  4. SELECT — কলাম, expression আর window function
  5. DISTINCT — একই সারি একবার
  6. ORDER BY আর LIMIT — সাজানো, তারপর কেটে রাখা

(মাঝারি) t(x) টেবিলে তিনটি সারি: 1, 2, NULL. SELECT COUNT(*) FROM t WHERE x NOT IN (1, NULL); কী দেবে?

  • 1
  • 0
  • 2
  • ত্রুটি — তালিকায় NULL রাখা যায় না

(সহজ) SQLite বা PostgreSQL-এ SELECT 5 / 2 * 2; কী দেবে?

  • 5
  • 5.0
  • 4
  • 1

(মাঝারি) t(v)-তে চারটি মান: 10, 20, 20, 30. ORDER BY v (ছোট থেকে বড়) দিয়ে 30-এর সারিতে RANK() আর DENSE_RANK() কত?

  • RANK 4, DENSE_RANK 4
  • RANK 3, DENSE_RANK 3
  • RANK 3, DENSE_RANK 4
  • RANK 4, DENSE_RANK 3

(মাঝারি) a-তে ৩টি সারি, b-তে ৪টি। SELECT COUNT(*) FROM a LEFT JOIN b ON 1 = 0; কত?

  • 3
  • 0
  • 12
  • 4

(কঠিন) t(g, v) টেবিল সম্পূর্ণ খালি। SELECT COUNT(*) FROM t; আর SELECT g, COUNT(*) FROM t GROUP BY g; — দুটি কোয়েরি কয়টি করে সারি দেবে?

  • দুটিই ১টি সারি (মান 0)
  • প্রথমটি ১টি সারি (0), দ্বিতীয়টি ০টি সারি
  • দুটিই ০টি সারি
  • প্রথমটি ১টি সারি (NULL), দ্বিতীয়টি ১টি সারি (NULL, 0)

(মাঝারি) WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM c WHERE n < 5) SELECT SUM(n) FROM c; কী দেবে?

  • 15
  • 10
  • 21
  • ত্রুটি — recursion থামে না

(মাঝারি) খালি টেবিল t(v)-এ চালানো হলো: BEGIN; INSERT 1; SAVEPOINT a; INSERT 2; ROLLBACK TO a; INSERT 3; COMMIT; (প্রতিটি INSERT মানে INSERT INTO t VALUES (…))। শেষে t-তে কী আছে?

  • কিছুই নেই
  • শুধু 1
  • 1, 2, 3
  • 1 আর 3

(সহজ) p(v)-তে 1, 1, 2; q(v)-তে 2, 3. SELECT v FROM p UNION SELECT v FROM q আর একই কোয়েরি UNION ALL দিয়ে — কয়টি করে সারি?

  • দুটিই ৩টি
  • UNION ৪টি, UNION ALL ৫টি
  • UNION ৩টি, UNION ALL ৫টি
  • UNION ৫টি, UNION ALL ৩টি

শব্দকোষ

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

মনে রাখুন

  1. যৌক্তিক ক্রম FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT; প্রতিটি ধাপের পরে সারি গুনে trace table লিখুন।
  2. NULL তুলনায় UNKNOWN — WHERE/ON/HAVING শুধু TRUE রাখে, CHECK শুধু FALSE আটকায়; NOT IN-এর তালিকায় NULL থাকলে কোনো সারি আসে না, NOT EXISTS আর EXCEPT নিরাপদ।
  3. COUNT(*) সারি গোনে, COUNT(col) আর AVG/SUM NULL বাদ দেয়; খালি সেটে COUNT 0, বাকি aggregate NULL; GROUP BY থাকলে খালি টেবিলে ০টি সারি।
  4. JOIN-এ সারি = প্রতিটি বাঁ-সারির মিলের যোগফল; দুই one-to-many একসাথে জুড়লে fan-out-এ যোগফল বেড়ে যায়; LEFT JOIN-এর পরে WHERE-এ ডান টেবিলের শর্ত তাকে INNER বানায়।
  5. স্টেটমেন্ট হয় পুরো সফল, নয়তো কিছুই না; ROLLBACK TO শুধু savepoint-এর পরের কাজ বাতিল করে; NULL-ক্রম, ভাগ, তারিখ, bare column আর transaction-এর ত্রুটি DBMS ভেদে আলাদা — উত্তরে DBMS লিখুন।

পরের পর্বে: পর্ব ৬৪-এ পুরো কোর্সের MCQ প্রশ্নব্যাংক — ER মডেল, relational algebra, normalization, index, transaction, NoSQL — মডিউল ধরে সাজানো, প্রতিটি উত্তরের কারণসহ।

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

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