এই পর্বে যা শিখবেন
- গণিতের, লেখা জোড়ার আর তুলনার অপারেটর — আর বিভিন্ন DBMS-এ তাদের আচরণের পার্থক্য
- BETWEEN, IN, LIKE, GLOB, ILIKE দিয়ে সারি ছাঁকা, তারিখসহ
- NULL আর তিন-মানের যুক্তি — truth table, প্রমাণ, আর WHERE কেন UNKNOWN সারি ফেলে দেয়
- NULL-এর তিনটি বিখ্যাত ফাঁদ: হিসাবে NULL ছড়ানো, NOT IN আর COUNT
- COALESCE, NULLIF, CASE আর CAST — রিপোর্টে লেবেল, নিজের ক্রম, শর্তসাপেক্ষ গণনা আর নিরাপদ ভাগ
১ · Expression — গণিতের অপারেটর, integer division আর লেখা জোড়া
পর্ব ১৫-এ আপনি SELECT দিয়ে কলাম বেছেছেন আর WHERE দিয়ে সারি ছেঁকেছেন। সেখানে price > 1000 লেখার সময় আপনি আসলে একটি expression লিখেছিলেন — মান, কলাম আর operator মিলিয়ে এমন একটি টুকরো, যা হিসাব করলে একটি মান পাওয়া যায়। price * stock একটি expression, name || ' (' || city || ')' একটি expression, city = 'Dhaka'-ও একটি expression, যার মান সত্য বা মিথ্যা। এই পর্বের পুরোটাই expression নিয়ে: কোন অপারেটর কী করে, আর কোথায় চুপচাপ ভুল উত্তর দেয়।
মুদি দোকানের ক্যালকুলেটরের কথা ভাবুন। দোকানদার “দাম × পরিমাণ” টিপে বিল বানান, “মোট − ছাড়” টিপে পরিশোধের অঙ্ক বের করেন। SQL-এর expression ঠিক এই ক্যালকুলেটর, শুধু পার্থক্য একটি: এটি একবারে একটি সারির জন্য হিসাব করে না, টেবিলের প্রতিটি সারির জন্য আলাদা করে করে। ১০টি পণ্যে price * stock লিখলে ১০টি আলাদা গুণফল আসে।
গণিতের arithmetic operator পাঁচটি: + (যোগ), - (বিয়োগ), * (গুণ), / (ভাগ) আর % (ভাগশেষ, যাকে বলে modulo). প্রথম তিনটি নিয়ে বলার কিছু নেই। গোলমাল বাধে ভাগে। দুটি পূর্ণসংখ্যা (integer) ভাগ করলে SQLite, PostgreSQL আর SQL Server উত্তরও দেয় পূর্ণসংখ্যায় — দশমিকের পরের অংশ কেটে ফেলে। 7 / 2 হয় 3, 3.5 নয়। একে বলে integer division. স্কুলের ভাগের “ভাগফল ৩, ভাগশেষ ১”-এর মতো: / দেয় ভাগফল, % দেয় ভাগশেষ।
এই পর্বের লাইভ SQL চলে দুটি পরিচিত ডেটাবেসে। প্রথমটি দোকানের db-shop: customers (customer_id, name, city, phone, joined_on — ৮ জন গ্রাহক; Imran Hossain-এর city আর Tanvir Ahmed-এর phone NULL), categories, products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার) আর order_items. দ্বিতীয়টি বিশ্ববিদ্যালয়ের db-uni, যা অংশ ৬-এ লাগবে। প্রতিটি এডিটরের নিচে “নমুনা ডেটা” বাক্সে পুরো টেবিল দেখা যায়, আর প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়।
-- ভাগ, ভাগশেষ আর টাইপ
SELECT 7 / 2 AS int_div, 7 / 2.0 AS real_div,
CAST(7 AS REAL) / 2 AS cast_div,
7 % 2 AS mod_2, -7 % 3 AS neg_mod, 5 / 0 AS div_zero;
-- বইয়ের স্টক: মোট মূল্য, কত সপ্তাহ চলবে (দিনে ১টি বিক্রি ধরে)
SELECT name, price, stock, price * stock AS stock_value,
stock / 7 AS weeks, stock % 7 AS days_left
FROM products WHERE category_id = 2;প্রথম ফলে একটি সারি: 3 | 3.5 | 3.5 | 1 | -1 | NULL. 7 / 2-এ দুই দিকই পূর্ণসংখ্যা, তাই ফল 3. একটি দিক দশমিক হলেই (2.0 লিখে বা CAST দিয়ে) ফল 3.5. -7 % 3 হয় -1, কারণ SQLite (C ভাষার মতো) ভাগফল শূন্যের দিকে কাটে, ফলে ভাগশেষের চিহ্ন ভাজ্যের মতো হয়; PostgreSQL আর MySQL-এও -1. আর 5 / 0 SQLite-এ ত্রুটি দেয় না, চুপচাপ NULL দেয় — এটি SQLite-এর নিজস্ব আচরণ, সব DBMS-এর নয় (নিচের টেবিল দেখুন)।
দ্বিতীয় ফলে দুটি বই: DSA Book-এর stock 25, তাই stock / 7 = 3 সপ্তাহ, stock % 7 = 4 দিন বাকি; SQL Book-এর 30 থেকে 4 সপ্তাহ আর 2 দিন। এখানে integer division-ই আমাদের দরকার ছিল — “পুরো সপ্তাহ কয়টি” প্রশ্নের উত্তর পূর্ণসংখ্যাই। price REAL কলাম, তাই price * stock দশমিক টাইপে আসে (16250 আর 16500).
| DBMS | 7 / 2 | পূর্ণসংখ্যার ভাগ আলাদা করে | 5 / 0 |
|---|---|---|---|
| SQLite | 3 | / দুই integer-এ | NULL |
| PostgreSQL | 3 | / দুই integer-এ, বা div(7, 2) | ত্রুটি: division by zero |
| MySQL | 3.5000 | 7 DIV 2 = 3 | NULL ও warning (strict mode-এ INSERT/UPDATE-এ ত্রুটি হতে পারে) |
| SQL Server | 3 | / দুই int-এ | ত্রুটি: Divide by zero error encountered |
| Oracle | 3.5 | TRUNC(7 / 2) | ত্রুটি: ORA-01476 |
অনুপাত আর গড়ের রিপোর্টে সবচেয়ে বেশি ধরা খাওয়ার জায়গা এটি। “কতগুলো অর্ডার ডেলিভারি হয়েছে, মোটের কত ভাগ” হিসাব করতে delivered / total লিখলে SQLite বা PostgreSQL-এ 5 / 10 = 0 আসে — সব দোকানের সফলতার হার শূন্য! সমাধান: delivered * 1.0 / total, CAST(delivered AS REAL) / total, বা 100.0 * delivered / total. MySQL-এ লেখা কোয়েরি PostgreSQL-এ সরালে ঠিক এই কারণে রিপোর্টের সংখ্যা বদলে যায়, কোনো ত্রুটি ছাড়াই।
অপারেটরের অগ্রাধিকারও (operator precedence) স্কুলের অঙ্কের মতো: *, /, % আগে, +, - পরে। 100 + 50 * 2 = 200, 300 নয়। সন্দেহ হলে বন্ধনী দিন — বন্ধনী কখনো ভুল করে না, আর পরে যিনি কোড পড়বেন তাঁর সময়ও বাঁচায়। যুক্তির অপারেটরেও অগ্রাধিকার আছে, যা অংশ ৫-এ আসবে।
লেখা জোড়া — ||, CONCAT আর +
রিপোর্টে প্রায়ই দুটি কলাম জুড়ে একটি লেবেল বানাতে হয়: “Rahim Uddin (Dhaka)”. একে বলে string concatenation. SQL standard-এর অপারেটর হলো দুটি খাড়া দাগ ||, যা SQLite, PostgreSQL আর Oracle মানে। কিন্তু এখানে উপভাষার পার্থক্য সবচেয়ে বেশি চোখে পড়ে:
| DBMS | লেখা জোড়ার উপায় | একটি অংশ NULL হলে |
|---|---|---|
| SQLite | a || b (৩.৪৪ থেকে concat()-ও) | ||-এ পুরো ফল NULL |
| PostgreSQL | a || b, CONCAT(a, b) | ||-এ NULL; CONCAT NULL-কে ফাঁকা ধরে |
| MySQL | CONCAT(a, b) | যেকোনো অংশ NULL হলে NULL; CONCAT_WS NULL বাদ দেয় |
| SQL Server | a + b, CONCAT(a, b) | +-এ NULL; CONCAT NULL-কে ফাঁকা ধরে |
| Oracle | a || b | NULL-কে ফাঁকা লেখা ধরে (Oracle-এ '' নিজেই NULL) |
MySQL-এ || লেখা জোড়ে না। ডিফল্ট সেটিংয়ে এটি যুক্তির OR. SELECT 'Dhaka' || 'Sylhet' MySQL-এ দুটি লেখাকে সংখ্যায় বদলে OR করে, উত্তর আসে 0 — ত্রুটি নয়, শুধু ভুল। PIPES_AS_CONCAT নামের sql_mode চালু করলে তবেই standard আচরণ পাওয়া যায়। SQLite-এ লেখা কোয়েরি MySQL-এ নেওয়ার সময় তাই || খুঁজে খুঁজে CONCAT বসান।
SELECT CONCAT(name, ' (', city, ')') AS label FROM customers; -- city NULL হলে পুরোটা NULL
SELECT CONCAT_WS(' - ', name, city, phone) AS label FROM customers; -- NULL অংশ বাদ যায়এবার SQLite-এ দেখুন NULL কীভাবে পুরো লেবেল খেয়ে ফেলে। Imran Hossain (৭)-এর city NULL, Tanvir Ahmed (৫)-এর phone NULL:
SELECT customer_id,
name || ' (' || city || ')' AS label,
name || ' (' || COALESCE(city, 'Unknown') || ')' AS safe_label,
COALESCE(phone, 'no phone') AS phone
FROM customers
WHERE customer_id IN (1, 5, 7);তিনটি সারি। Rahim (১): Rahim Uddin (Dhaka) দুই কলামেই। Tanvir (৫): লেবেল ঠিক আছে, কারণ city আছে; phone কলামে এসেছে no phone. Imran (৭): label কলামে পুরোটাই NULL, যদিও নামটি জানা — একটি NULL অংশ পুরো জোড়াকে NULL বানিয়ে দেয়। একে বলে NULL propagation. safe_label-এ COALESCE(city, 'Unknown') NULL-এর জায়গায় 'Unknown' বসিয়েছে, তাই এসেছে Imran Hossain (Unknown). COALESCE-এর পুরো গল্প অংশ ৭-এ; আপাতত মনে রাখুন: “প্রথম যে মানটি NULL নয়, সেটি দাও”।
২ · তুলনা, BETWEEN আর IN
তুলনার comparison operator ছয়টি: =, <> (সমান নয়; বেশিরভাগ DBMS !=-ও মানে), <, >, <=, >=. সংখ্যায় এরা স্বাভাবিক। লেখায় তুলনা হয় অক্ষরের ক্রমে — অভিধানের মতো, তবে কোন অক্ষর কোনটির আগে, বড় হাতের আর ছোট হাতের অক্ষর এক কি না, তা ঠিক করে collation. তারিখ যদি 'YYYY-MM-DD' আকারে লেখা থাকে (আমাদের orders.order_date-এর মতো), তবে লেখার ক্রম আর ক্যালেন্ডারের ক্রম একই হয়: '2025-09-30' < '2025-10-01'. ঠিক এই কারণেই ISO আকারে তারিখ রাখা হয়।
BETWEEN হলো দুটি তুলনার সংক্ষিপ্ত রূপ: x BETWEEN a AND b মানে x >= a AND x <= b. দুই প্রান্তই অন্তর্ভুক্ত। আর IN হলো অনেকগুলো সমতার সংক্ষেপ: status IN ('pending', 'shipped') মানে status = 'pending' OR status = 'shipped'. এদের উল্টো রূপ NOT BETWEEN আর NOT IN.
-- সেপ্টেম্বর ২০২৫-এর অর্ডার (দুই প্রান্তসহ)
SELECT order_id, order_date, status FROM orders
WHERE order_date BETWEEN '2025-09-01' AND '2025-09-30';
-- ৫৫০ থেকে ১৮০০ টাকার পণ্য
SELECT name, price FROM products WHERE price BETWEEN 550 AND 1800;
-- যে অর্ডার এখনো পৌঁছায়নি
SELECT order_id, status FROM orders WHERE status IN ('pending', 'shipped');
SELECT name, city FROM customers WHERE city NOT IN ('Dhaka', 'Chattogram');প্রথম ফলে ৬টি অর্ডার, 1001 (2025-09-01) থেকে 1006 (2025-09-20). 09-01 নিজেও এসেছে, কারণ BETWEEN প্রান্তসহ। দ্বিতীয় ফলে ৪টি পণ্য: Headphone (1200), Power Bank (1800), DSA Book (650), SQL Book (550) — 550 আর 1800 দুটিই প্রান্তের মান, দুটিই এসেছে। তৃতীয়টিতে ৪টি: 1003 ও 1008 (shipped), 1006 ও 1009 (pending). চতুর্থটিতে ৩ জন: Nusrat (Rajshahi), Tanvir (Khulna), Farhana (Sylhet). খেয়াল করুন, Imran Hossain নেই — তাঁর city NULL, আর NULL “Dhaka বা Chattogram নয়” কি না, তা SQL জানে না। এই রহস্যের সমাধান অংশ ৫-এ।
BETWEEN-এর দুটি সূক্ষ্ম কথা। এক, ক্রম জরুরি: price BETWEEN 1800 AND 550 কোনো সারি দেয় না, কারণ এর মানে price >= 1800 AND price <= 550. (SQL standard আর PostgreSQL-এ BETWEEN SYMMETRIC লিখলে দুই প্রান্ত নিজে থেকে সাজিয়ে নেয়।) দুই, তারিখের সাথে সময় থাকলে সাবধান। কলামে যদি '2025-09-30 18:45' থাকে, তবে BETWEEN '2025-09-01' AND '2025-09-30' সেটিকে বাদ দেবে, কারণ লেখার ক্রমে '2025-09-30 18:45' > '2025-09-30'. নিরাপদ অভ্যাস হলো half-open interval: order_date >= '2025-09-01' AND order_date < '2025-10-01' — শুরু অন্তর্ভুক্ত, শেষ নয়। মাসের দিন কয়টি, সময় আছে কি না, কিছুই ভাবতে হয় না।
৩ · প্যাটার্ন দিয়ে খোঁজা — LIKE, GLOB, ILIKE আর REGEXP
লাইব্রেরির ক্যাটালগে “নামে Book আছে এমন সব বই” বা “S দিয়ে শুরু হওয়া সব গ্রাহক” খুঁজতে পুরো মান জানা থাকে না, জানা থাকে একটি নকশা। LIKE অপারেটর এই নকশা মেলায়, দুটি বিশেষ চিহ্ন বা wildcard দিয়ে:
%— যেকোনো সংখ্যক অক্ষর, শূন্যটিও।'%Book%'মানে “কোথাও Book আছে”,'S%'মানে “S দিয়ে শুরু”।_— ঠিক একটি অক্ষর।'_a%'মানে “দ্বিতীয় অক্ষরটি a”.'017________'(আটটি_) মানে “017 দিয়ে শুরু, মোট ১১ অক্ষর”।
কিন্তু যদি খুঁজতে চান আসল % চিহ্নটিই — যেমন “50% off” লেখা অফারগুলো? তখন escape character লাগে: LIKE '%!%%' ESCAPE '!' মানে “!-এর পরের %-টি আসল % চিহ্ন, wildcard নয়”। PostgreSQL আর MySQL-এ ডিফল্ট escape হলো backslash (\), SQLite-এ কোনো ডিফল্ট নেই, তাই ESCAPE লিখে দেওয়াই সবখানে নিরাপদ।
SELECT name FROM products WHERE name LIKE '%Book%';
SELECT name FROM customers WHERE name LIKE 'S%';
SELECT name FROM customers WHERE name LIKE '_a%';
-- বড়-ছোট হাতের অক্ষর: LIKE বনাম GLOB (SQLite)
SELECT name FROM customers WHERE name LIKE 's%';
SELECT name FROM customers WHERE name GLOB 's*';
-- আসল % চিহ্ন খোঁজা
SELECT '50% off' LIKE '%!%%' ESCAPE '!' AS has_pct,
'Sale 50' LIKE '%!%%' ESCAPE '!' AS no_pct;ছয়টি ফল। ১) DSA Book আর SQL Book. ২) শুধু Sabbir Hasan. ৩) পাঁচজন — Rahim Uddin, Karima Begum, Sabbir Hasan, Tanvir Ahmed, Farhana Akter; প্রত্যেকের নামের দ্বিতীয় অক্ষর a. ৪) ছোট হাতের 's%' দিয়েও Sabbir Hasan এসেছেন! SQLite-এর LIKE ইংরেজি (ASCII) অক্ষরে বড়-ছোট হাতের পার্থক্য করে না। ৫) GLOB-এ কোনো সারি নেই — GLOB Unix-এর ফাইল-নামের নকশার মতো, * আর ? ব্যবহার করে, আর বড়-ছোট হাতের পার্থক্য করে। ৬) 1 | 0: প্রথম লেখায় আসল % আছে, দ্বিতীয়টিতে নেই। SQLite-এ সত্য-মিথ্যা আসে 1 আর 0 হয়ে — অংশ ৯-এ এর ব্যাখ্যা।
| DBMS | LIKE বড়-ছোট হাতের পার্থক্য করে? | অন্য উপায় |
|---|---|---|
| SQLite | না (শুধু ASCII অক্ষরে; বাংলা বা অন্য Unicode-এ সাধারণত করে) | GLOB (পার্থক্য করে); REGEXP শুধু বাড়তি ফাংশন যোগ করলে |
| PostgreSQL | হ্যাঁ | ILIKE (পার্থক্য করে না), ~ আর ~* regex, SIMILAR TO |
| MySQL | collation অনুযায়ী; ডিফল্ট collation-এ সাধারণত না | REGEXP / RLIKE, REGEXP_LIKE() |
| SQL Server | collation অনুযায়ী; প্রচলিত ডিফল্টে না | LIKE-এ [a-c] ধরনের অক্ষর-সেট |
| Oracle | হ্যাঁ | REGEXP_LIKE() |
Regular expression (regex) LIKE-এর চেয়ে অনেক শক্তিশালী নকশা-ভাষা — “শুধু অঙ্ক, ঠিক ১১টি, শুরুতে 01” লেখা যায় '^01[0-9]{9}$' দিয়ে। SQLite-এ REGEXP শব্দটি ব্যাকরণে আছে, কিন্তু কাজ করে তখনই, যখন অ্যাপ বা কোনো extension regexp() নামের ফাংশন যোগ করে; এই পাতার ইঞ্জিনে সেটি নেই।
SELECT name FROM customers WHERE name ILIKE 's%'; -- Sabbir Hasan
SELECT name FROM customers WHERE phone ~ '^01[3-9][0-9]{8}$'; -- regex দিয়ে ফোন নম্বর যাচাইগতির কথা: LIKE 'S%' (শুরু জানা) index ব্যবহার করতে পারে — লাইব্রেরির বর্ণানুক্রমিক তাকে S-এর জায়গায় সরাসরি যাওয়ার মতো, যদিও কোন collation-এ index ব্যবহার হবে তা DBMS-ভেদে আলাদা। কিন্তু LIKE '%Book%' (শুরুতেই %) সাধারণ index দিয়ে খোঁজা যায় না, প্রতিটি সারি পড়তে হয়। লক্ষ লক্ষ সারিতে এমন খোঁজের জন্য আছে full-text search (পর্ব ৩০) আর বিশেষ index (পর্ব ৪১)।
৪ · NULL — “জানা নেই” মানটি
পর্ব ৫ আর ৬-এ আপনি NULL-এর সাথে পরিচিত হয়েছেন: কোনো ঘরের মান জানা নেই, বা প্রযোজ্য নয়। NULL শূন্য নয়, ফাঁকা লেখা ''-ও নয়। Tanvir Ahmed-এর phone NULL মানে “তাঁর ফোন নম্বর আমরা জানি না”, “তাঁর ফোন নম্বর শূন্য” নয়। বিশ্ববিদ্যালয়ের ফলাফল রেজিস্টারে যে শিক্ষার্থীর পরীক্ষার খাতা এখনো দেখা হয়নি, তাঁর ঘরে শূন্য লিখলে তিনি ফেল করে যাবেন; ঘরটি ফাঁকা রাখাই সৎ — NULL হলো সেই ফাঁকা ঘর।
এখান থেকেই SQL-এর সবচেয়ে গুরুত্বপূর্ণ নিয়মটি আসে: NULL-এর সাথে যেকোনো তুলনার ফল সত্যও নয়, মিথ্যাও নয় — অজানা (UNKNOWN). “Tanvir-এর ফোন নম্বর কি 01711000001?” — জানি না। “Tanvir-এর ফোন নম্বর কি Imran-এর অজানা শহরের সমান?” — তা-ও জানি না। এমনকি NULL = NULL-ও UNKNOWN: দুজন অচেনা মানুষের বয়স জানা নেই বলে তাঁদের বয়স সমান — এমন বলা যায় না।
তাহলে “ফোন নম্বর নেই এমন গ্রাহক” খুঁজবেন কীভাবে? phone = NULL দিয়ে নয় — সেটি প্রতিটি সারিতে UNKNOWN দেয়, আর WHERE কোনো সারিই রাখে না। এর জন্য আলাদা অপারেটর IS NULL আর IS NOT NULL, যারা কখনো UNKNOWN দেয় না, শুধু TRUE বা FALSE.
-- ভুল: = NULL কোনো সারি দেয় না
SELECT name FROM customers WHERE phone = NULL;
-- ঠিক: ফোন নম্বর নেই এমন গ্রাহক
SELECT name, phone FROM customers WHERE phone IS NULL;
SELECT COUNT(*) AS with_phone FROM customers WHERE phone IS NOT NULL;
-- NULL-কে একটি সাধারণ মানের মতো তুলনা: IS DISTINCT FROM
SELECT name, city FROM customers WHERE city IS DISTINCT FROM 'Dhaka';
SELECT NULL = NULL AS eq, NULL IS NULL AS is_null,
NULL IS NOT DISTINCT FROM NULL AS same, 'Dhaka' IS NOT 'Dhaka' AS diff;প্রথমটিতে “(কোনো সারি নেই)” — ত্রুটিও নেই, তাই এই ভুল চোখে পড়ে না। দ্বিতীয়টিতে একটি সারি: Tanvir Ahmed | NULL. তৃতীয়টিতে 7. চতুর্থটিতে ৫ জন: Karima (Chattogram), Nusrat (Rajshahi), Tanvir (Khulna), Farhana (Sylhet) আর Imran Hossain (NULL). IS DISTINCT FROM NULL-কে একটি সাধারণ মানের মতো দেখে: NULL আর 'Dhaka' আলাদা — TRUE; NULL আর NULL একই — FALSE. তাই অংশ ২-এর NOT IN যাঁকে হারিয়েছিল, এখানে তিনি এসেছেন। শেষ সারি: NULL | 1 | 1 | 0 — NULL = NULL অজানা, কিন্তু IS NULL আর IS NOT DISTINCT FROM নিশ্চিত উত্তর দেয়।
IS DISTINCT FROM এসেছে SQL:1999 standard-এ। PostgreSQL এটি অনেক আগে থেকে মানে, SQLite ৩.৩৯ (২০২২) থেকে, SQL Server 2022 থেকে। SQLite-এ এর পুরোনো আর ছোট রূপ হলো IS আর IS NOT, যা যেকোনো দুটি মানে চলে — city IS 'Dhaka'. MySQL-এ একই কাজ করে <=> (null-safe equality): NULL <=> NULL = 1.
NULL-এর ধারণা relational model-এর জনক Edgar F. Codd-এর। ১৯৭০-এর দশকে তিনি অজানা মানের জন্য একটি চিহ্ন আর তার সাথে তিন-মানের যুক্তি প্রস্তাব করেন; ১৯৭৯ সালের RM/T প্রবন্ধে এটি বিস্তারিত আসে। SQL-এর প্রথম standard (SQL-86) থেকেই NULL আর তিন-মানের যুক্তি ভাষার অংশ। কিন্তু বিতর্ক থামেনি। Codd নিজেই ১৯৯০ সালের বই The Relational Model for Database Management: Version 2-এ দুই রকম চিহ্ন প্রস্তাব করেন — “মান আছে কিন্তু জানা নেই” আর “মান প্রযোজ্যই নয়” — আর তার সাথে চার-মানের যুক্তি। অন্যদিকে Chris Date আর Hugh Darwen-এর মতো গবেষকেরা বহু বছর ধরে যুক্তি দিয়েছেন, NULL পুরোপুরি বাদ দেওয়া উচিত, কারণ এটি যুক্তির নিয়ম ভাঙে। SQL দুই পথের কোনোটিতেই যায়নি — একটিমাত্র NULL আর তিনটি সত্যমান নিয়েই আজও চলছে।
৫ · তিন-মানের যুক্তি — TRUE, FALSE আর UNKNOWN
সাধারণ যুক্তিতে প্রতিটি বাক্য সত্য বা মিথ্যা। SQL-এ তৃতীয় একটি মান আছে: UNKNOWN — এজন্যই একে বলে তিন-মানের যুক্তি। এখন প্রশ্ন, AND, OR আর NOT এই তৃতীয় মানের সাথে কী করে? উত্তরটি একটি সহজ বুদ্ধি থেকে আসে: অজানা মানটি সত্য হলে আর মিথ্যা হলে — দুই ক্ষেত্রেই যদি ফল একই হয়, তবে সেটিই ফল; না হলে ফলও অজানা। যেমন “UNKNOWN AND FALSE”: অজানাটি সত্য হোক বা মিথ্যা, FALSE-এর সাথে AND করলে মিথ্যাই হবে — তাই FALSE. কিন্তু “UNKNOWN AND TRUE”: অজানাটির ওপর নির্ভর করে, তাই UNKNOWN. এই নিয়মে তৈরি truth table ধাপে ধাপে দেখুন:
| দেখছি | |
| মূল নিয়ম |
- তিনটি সত্যমান। দুটি জানা মানের তুলনা TRUE বা FALSE দেয়। কিন্তু যেকোনো একটি দিক NULL হলেই ফল UNKNOWN —
=,<>,>সব অপারেটরেই।NULL = NULL-ও UNKNOWN. - NOT. TRUE-এর উল্টো FALSE, FALSE-এর উল্টো TRUE. আর UNKNOWN-এর উল্টো? “জানি না”-র উল্টো “এখনো জানি না” — UNKNOWN. এজন্যই
NOT (city = 'Dhaka')Imran-এর সারিকে TRUE বানাতে পারে না। - AND. লাল সারিটি দেখুন: একটি দিক FALSE হলে অন্য দিক যা-ই হোক, ফল FALSE. UNKNOWN AND FALSE = FALSE, কারণ অজানাটি সত্য হলেও লাভ নেই। কিন্তু UNKNOWN AND TRUE = UNKNOWN — ফল এখন অজানাটির ওপর নির্ভর করে।
- OR. উল্টো ছবি: একটি দিক TRUE হলেই ফল TRUE, অন্য দিক UNKNOWN হলেও। UNKNOWN OR FALSE = UNKNOWN. দুই টেবিলের মিল খেয়াল করুন — AND-এ FALSE “জেতে”, OR-এ TRUE.
- সংখ্যার মডেল। FALSE = 0, UNKNOWN = ½, TRUE = 1 বসালে AND হয় ছোটটি (min), OR হয় বড়টি (max), NOT হয় 1 থেকে বিয়োগ। ৯টি ঘর মুখস্থ না করে এই তিন সূত্রেই পুরো টেবিল বেরিয়ে আসে। এই যুক্তিব্যবস্থার নাম Kleene-এর শক্তিশালী তিন-মানের যুক্তি।
- WHERE কী রাখে। WHERE প্রতিটি সারির জন্য শর্তটি হিসাব করে, আর রাখে শুধু যেগুলোর ফল TRUE. FALSE আর UNKNOWN — দুটিই বাদ। ছোট কিন্তু জরুরি ব্যতিক্রম: CHECK constraint আটকায় শুধু FALSE হলে, তাই UNKNOWN সেখানে পার পেয়ে যায়।
একটি ছোট প্রমাণ — কোন নিয়ম টেকে, কোনটি ভাঙে
সংখ্যার মডেলটি শুধু মনে রাখার কৌশল নয়, প্রমাণের হাতিয়ারও। মান তিনটি {0, ½, 1} আর AND = min, OR = max, NOT x = 1 − x.
দাবি ১ — De Morgan-এর নিয়ম টেকে: NOT (p AND q) = (NOT p) OR (NOT q). প্রমাণ: 1 − min(p, q) = max(1 − p, 1 − q), কারণ যে সংখ্যাটি ছোট, 1 থেকে বিয়োগ করলে সেটিই বড় হয়। এটি {0, ½, 1}-এর যেকোনো p, q-তে সত্য। একইভাবে NOT (p OR q) = (NOT p) AND (NOT q). ∎
দাবি ২ — “p OR NOT p সবসময় সত্য” ভাঙে: p = ½ (UNKNOWN) হলে max(½, 1 − ½) = ½, অর্থাৎ UNKNOWN. ∎ সাধারণ যুক্তির এই নিয়মের (law of excluded middle) ব্যর্থতার সরাসরি ফল: WHERE city = 'Dhaka' OR city <> 'Dhaka' টেবিলের সব সারি দেয় না — city NULL এমন সারি বাদ পড়ে।
দাবি ৩ — WHERE p আর WHERE NOT p মিলে পুরো টেবিল হয় না: একটি সারি প্রথমটিতে আসে যদি p = 1, দ্বিতীয়টিতে যদি 1 − p = 1, অর্থাৎ p = 0. p = ½ এমন সারি কোনোটিতেই আসে না। তাই |WHERE p| + |WHERE NOT p| + |p UNKNOWN এমন সারি| = মোট সারি। ∎
দাবি ৩-কে এখন বাস্তব ডেটায় দেখি। নিচের ডায়াগ্রামে customers থেকে চারজনের ওপর একটি দুই-অংশের শর্ত হিসাব করা হচ্ছে: “ঢাকার বাইরের গ্রাহক, যাঁদের ফোন নম্বর 01 দিয়ে শুরু”।
| সারি | |
| city <> 'Dhaka' | |
| phone LIKE '01%' | |
| AND-এর ফল | |
| ফলে থাকে? |
- চারজন, একটি শর্ত। দুজনের সব তথ্য আছে (Rahim, Karima), একজনের phone NULL (Tanvir), একজনের city NULL (Imran). শর্তের দুটি অংশ AND দিয়ে জোড়া। DBMS প্রতিটি সারির জন্য আলাদা করে দুটি অংশ হিসাব করবে, তারপর AND-এর টেবিল মিলিয়ে ফল নেবে।
- Rahim — FALSE. তাঁর city Dhaka, তাই প্রথম অংশ FALSE. ফোন নম্বর 01 দিয়ে শুরু — TRUE. FALSE AND TRUE = FALSE. সারিটি বাদ। এখানে NULL-এর কোনো ভূমিকা নেই।
- Karima — TRUE. Chattogram ≠ Dhaka — TRUE; ফোন 018… — TRUE. TRUE AND TRUE = TRUE. চারজনের মধ্যে একমাত্র তিনিই নিশ্চিতভাবে শর্ত পূরণ করেন।
- Tanvir — UNKNOWN. Khulna ≠ Dhaka — TRUE. কিন্তু ফোন NULL, তাই
NULL LIKE '01%'= UNKNOWN. TRUE AND UNKNOWN = UNKNOWN. তাঁর নম্বর জানা গেলে হয়তো শর্ত পূরণ হতো — কিন্তু SQL অনুমান করে না। - Imran — UNKNOWN. এবার উল্টো দিক: ফোন 013… — TRUE, কিন্তু city NULL, তাই
NULL <> 'Dhaka'= UNKNOWN. UNKNOWN AND TRUE = UNKNOWN. লক্ষ করুন, “ঢাকার বাইরে” শর্তে তিনি আসেননি, আবার “ঢাকায়” শর্তেও আসতেন না। - ফল: শুধু Karima. WHERE শুধু TRUE রাখে। দুটি UNKNOWN সারি নিঃশব্দে বাদ পড়েছে — কোনো warning নেই। রিপোর্টে “ঢাকার বাইরের গ্রাহক” গুনলে এই দুজন হারিয়ে যাবেন, যদি না আপনি NULL-এর জন্য আলাদা করে ভাবেন।
এবার নিজে চালান। প্রথম SELECT-এ truth table-এর কয়েকটি ঘর SQLite-কে দিয়েই হিসাব করানো হয়েছে (SQLite-এ FALSE = 0, TRUE = 1, UNKNOWN = NULL). তারপর ডায়াগ্রামের শর্তটি পুরো customers টেবিলে, আর দাবি ৩-এর গণনা:
SELECT NULL AND 0 AS u_and_f, NULL AND 1 AS u_and_t,
NULL OR 1 AS u_or_t, NULL OR 0 AS u_or_f,
NOT NULL AS not_u, 5 > 3 AS gt;
SELECT name, city, phone FROM customers
WHERE city <> 'Dhaka' AND phone LIKE '01%';
SELECT COUNT(*) AS dhaka FROM customers WHERE city = 'Dhaka';
SELECT COUNT(*) AS not_dhaka FROM customers WHERE NOT (city = 'Dhaka');প্রথম সারি: 0 | NULL | 1 | NULL | NULL | 1 — truth table-এর সাথে হুবহু মেলে: UNKNOWN AND FALSE = FALSE (0), UNKNOWN AND TRUE = UNKNOWN (NULL), UNKNOWN OR TRUE = TRUE (1), UNKNOWN OR FALSE = UNKNOWN, NOT UNKNOWN = UNKNOWN. দ্বিতীয় ফলে ৩ জন: Karima, Nusrat আর Farhana — পুরো টেবিলে Tanvir (phone NULL) আর Imran (city NULL) বাদ পড়েছেন, ডায়াগ্রামের মতোই। শেষ দুটি গণনা 3 আর 4: মোট ৮ জন, অথচ 3 + 4 = 7. হারানো একজন Imran, যাঁর জন্য শর্তটি UNKNOWN — দাবি ৩ হাতেনাতে।
AND আর OR-এর অগ্রাধিকার। NOT সবার আগে, তারপর AND, তারপর OR. তাই WHERE status = 'pending' OR status = 'shipped' AND customer_id = 1 আসলে status = 'pending' OR (status = 'shipped' AND customer_id = 1) — সব গ্রাহকের pending অর্ডার চলে আসবে। আপনি যদি “গ্রাহক ১-এর pending বা shipped অর্ডার” চান, বন্ধনী দিন: (status = 'pending' OR status = 'shipped') AND customer_id = 1, বা আরও পরিষ্কার: status IN ('pending', 'shipped') AND customer_id = 1.
৬ · NULL-এর তিন ফাঁদ — হিসাব, NOT IN আর COUNT
ফাঁদ ১ — NULL হিসাবে ছড়িয়ে পড়ে
অংশ ১-এ দেখেছেন, লেখা জোড়ায় একটি NULL পুরো ফলকে NULL করে। গণিতেও একই: NULL + 5, NULL * 0, price - NULL — সবই NULL. যুক্তিটি একই: অজানা সংখ্যার সাথে ৫ যোগ করলে ফলও অজানা। এমনকি NULL * 0-ও NULL, যদিও গণিতে যেকোনো সংখ্যা × ০ = ০ — SQL এখানে সূক্ষ্ম যুক্তিতে যায় না, নিয়ম সরল রাখে: যেকোনো অংশ NULL, তো ফল NULL. বেতনের হিসাবে salary + bonus লিখলে যাঁর bonus NULL, তাঁর মোট বেতনই NULL হয়ে যাবে; লিখতে হয় salary + COALESCE(bonus, 0).
ফাঁদ ২ — NOT IN আর একটি NULL
এটি SQL-এর সবচেয়ে বিখ্যাত ফাঁদ, আর ইন্টারভিউর প্রিয় প্রশ্ন। এখানে প্রথমবার db-uni লাগবে: departments (dept_id, name, building — CSE, EEE, BBA আর English), আর students (student_id, name, dept_id, batch, cgpa — ৮ জন শিক্ষার্থী; Habib Ullah-এর dept_id এখনো NULL, Fahim Karim-এর cgpa NULL). প্রশ্ন: “কোন বিভাগে একজনও শিক্ষার্থী নেই?” স্বাভাবিক উত্তর English. স্বাভাবিক কোয়েরি:
SELECT name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM students);বন্ধনীর ভেতরের SELECT-টি একটি subquery — একটি কোয়েরির ভেতরে আরেকটি কোয়েরি, যার ফল এখানে একটি মানের তালিকা। Subquery-র পুরো আলোচনা পর্ব ২১-এ; এখানে শুধু দেখুন NULL কী ঘটায়:
| যে বিভাগ দেখছি | |
| শর্তের ফল | |
| ফলে সারি |
- দুটি তালিকা। বিভাগ চারটি। Subquery শিক্ষার্থীদের dept_id তুলে আনে: 1, 1, 2, 1, 3, 2, 1 আর একটি NULL — Habib Ullah এখনো কোনো বিভাগে যুক্ত হননি। English (4) তালিকায় নেই, তাই আমরা আশা করছি ফলে English আসবে।
- NOT IN আসলে কী।
x IN (a, b, c)মানেx = a OR x = b OR x = c. তার NOT নিলে De Morgan-এর নিয়মে (অংশ ৫-এর দাবি ১) হয়x <> a AND x <> b AND x <> c. তালিকায় NULL থাকায় শেষে একটি অংশ যোগ হয়েছে:x <> NULL— যা সবসময় UNKNOWN. - CSE — FALSE. 1 <> 1 মিথ্যা। AND-এ একটি FALSE থাকলেই পুরোটা FALSE, শেষের UNKNOWN কিছু বদলাতে পারে না। CSE বাদ — এটি ঠিকই হয়েছে, কারণ CSE-তে শিক্ষার্থী আছে।
- EEE আর BBA — FALSE. একইভাবে নিজের মানের সাথে তুলনায় একটি FALSE আসে, তাই দুটিই বাদ। এ পর্যন্ত কোয়েরিটি ঠিকঠাক আচরণ করছে।
- English — UNKNOWN. 4 তালিকার কোনো সংখ্যার সমান নয়, তিনটি তুলনাই TRUE. কিন্তু শেষ অংশ
4 <> NULLUNKNOWN, আর TRUE AND UNKNOWN = UNKNOWN. WHERE শুধু TRUE রাখে — তাই English-ও বাদ! SQL-এর যুক্তি: “Habib-এর বিভাগ যদি English হয়?” — জানা নেই, তাই নিশ্চিত করে বলা যায় না। - ফল শূন্য, আর সমাধান। একটি NULL পুরো উত্তর মুছে দিয়েছে, কোনো ত্রুটি ছাড়াই। সাধারণভাবে: তালিকায় NULL থাকলে
x NOT IN (…)কখনো TRUE হতে পারে না — হয় FALSE, নয় UNKNOWN. সমাধান: subquery-তেWHERE dept_id IS NOT NULL, অথবা NOT EXISTS ব্যবহার, যা NULL-এ এই আচরণ করে না।
-- ফাঁদ: একটি NULL dept_id সব উত্তর মুছে দেয়
SELECT name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM students);
-- সমাধান: subquery থেকে NULL বাদ
SELECT name FROM departments
WHERE dept_id NOT IN (SELECT dept_id FROM students
WHERE dept_id IS NOT NULL);
-- তুলনা: IN-এ NULL কোনো ক্ষতি করে না
SELECT name FROM departments
WHERE dept_id IN (SELECT dept_id FROM students);প্রথমটিতে “(কোনো সারি নেই)”, দ্বিতীয়টিতে একটি সারি: English, তৃতীয়টিতে তিনটি: CSE, EEE, BBA. শেষটি খেয়াল করুন: IN হলো OR-এর শৃঙ্খল, আর OR-এ একটি TRUE থাকলেই TRUE — তাই তালিকায় NULL থাকলেও যে মান মেলে সেটি ঠিকই আসে। সমস্যা শুধু NOT IN-এ, কারণ NOT সেটিকে AND-এর শৃঙ্খলে বদলে দেয়। বাস্তবে এই বাগ সবচেয়ে বিপজ্জনক, কারণ আজ কোয়েরিটি ঠিক উত্তর দেয় — যেদিন প্রথম NULL ঢোকে, সেদিন থেকে রিপোর্ট চুপচাপ খালি হয়ে যায়।
আরও গভীরে: NOT IN আর NOT EXISTS শুধু NULL-এ নয়, গতিতেও আলাদা হতে পারে। NOT EXISTS-এর অর্থ পরিষ্কার “মেলে এমন সারি নেই”, তাই PostgreSQL সেটিকে সাধারণত একটি anti join-এ (পর্ব ২০) রূপান্তর করে hash বা merge পদ্ধতিতে চালায়। কিন্তু NOT IN-এর NULL-নিয়মের কারণে সেই রূপান্তর সাধারণভাবে বৈধ নয়, তাই PostgreSQL তাকে আলাদা subplan হিসেবে চালায়; subquery-র ফল মেমরিতে (work_mem) না ধরলে প্রতিটি বাইরের সারির জন্য তালিকা ঘুরে দেখতে হয়, যা বড় টেবিলে খুব ধীর। এজন্যই PostgreSQL-এর wiki-র “Don't Do This” পাতায় subquery-সহ NOT IN এড়াতে বলা হয়েছে।
ফাঁদ ৩ — COUNT(*) আর COUNT(কলাম)
COUNT(*) সারি গোনে — সারিতে যা-ই থাকুক। COUNT(phone) গোনে শুধু সেই সারিগুলো, যেখানে phone NULL নয়। অন্য aggregate function-ও (SUM, AVG, MIN, MAX) NULL বাদ দিয়ে হিসাব করে। প্রায়ই এটাই চাই, কিন্তু গড় হিসাবে এটি একটি সিদ্ধান্ত, যা আপনাকে সচেতনভাবে নিতে হবে। (GROUP BY-সহ পুরো আলোচনা পর্ব ১৮-এ।)
SELECT COUNT(*) AS all_rows, COUNT(phone) AS with_phone,
COUNT(city) AS with_city, COUNT(DISTINCT city) AS cities
FROM customers;
-- Fahim Karim-এর cgpa NULL: গড় কতজনের?
SELECT COUNT(*) AS students, COUNT(cgpa) AS with_cgpa,
ROUND(AVG(cgpa), 3) AS avg_cgpa,
ROUND(SUM(cgpa) / COUNT(*), 3) AS avg_over_all
FROM students;প্রথম সারি: 8 | 7 | 7 | 5 — ৮ জন গ্রাহক, ফোন আছে ৭ জনের (Tanvir বাদ), শহর জানা ৭ জনের (Imran বাদ), আর আলাদা শহর ৫টি (Dhaka, Chattogram, Rajshahi, Khulna, Sylhet; NULL শহর হিসেবে গোনা হয়নি)। দ্বিতীয় সারি: 8 | 7 | 3.35 | 2.931. AVG ৭ জনের cgpa যোগ করে (23.45) ৭ দিয়ে ভাগ করেছে = 3.35. আর SUM / COUNT(*) একই যোগফলকে ৮ দিয়ে ভাগ করেছে = 2.931, যেন Fahim-এর cgpa শূন্য। কোনটি ঠিক? Fahim-এর ফল এখনো আসেনি, তাই তাঁকে শূন্য ধরা অন্যায় — এখানে AVG-ই সঠিক। কিন্তু “বিক্রি নেই মানে ০ টাকা” এমন ক্ষেত্রে NULL-কে শূন্য ধরাই সঠিক, তখন লিখতে হয় AVG(COALESCE(x, 0)).
৭ · COALESCE আর NULLIF — NULL সামলানোর দুই হাতিয়ার
COALESCE(a, b, c, …) বাঁ থেকে ডানে দেখে, প্রথম যে মানটি NULL নয় সেটি ফেরত দেয়; সবগুলো NULL হলে NULL. COALESCE(phone, 'no phone'), COALESCE(bonus, 0), COALESCE(mobile, office_phone, 'যোগাযোগ নেই') — একটির পর একটি বিকল্প। এটি SQL standard, সব বড় DBMS-এ চলে। কাজটি আসলে একটি CASE-এর সংক্ষেপ: CASE WHEN a IS NOT NULL THEN a ELSE b END.
NULLIF(a, b) ঠিক উল্টো কাজ করে: a আর b সমান হলে NULL দেয়, নইলে a. প্রথমে অদ্ভুত লাগে — কেউ ইচ্ছা করে NULL বানাবে কেন? সবচেয়ে বড় ব্যবহার: শূন্য দিয়ে ভাগ ঠেকানো। x / NULLIF(y, 0): y শূন্য হলে হর NULL হয়ে যায়, আর ভাগফল NULL — PostgreSQL বা SQL Server-এ যেখানে পুরো কোয়েরি ত্রুটিতে থেমে যেত, সেখানে শুধু সেই সারির ফল ফাঁকা থাকে। আরেকটি ব্যবহার: পুরোনো ডেটায় ফাঁকা লেখাকে NULL বানানো — NULLIF(phone, '').
একটি চায়ের দোকান প্রতিদিন হিসাব রাখে: কত টাকা বিক্রি, কয়টি অর্ডার। “গড় অর্ডারের মূল্য” = বিক্রি ÷ অর্ডার। হরতালের দিন দোকান বন্ধ — বিক্রি ০, অর্ডার ০. ০ ÷ ০-এর কোনো অর্থ নেই; খাতায় সেদিনের ঘরে একটি ড্যাশ দেওয়াই ঠিক। NULLIF ঠিক সেই ড্যাশ বসায়, আর COALESCE চাইলে রিপোর্টে ড্যাশের জায়গায় ০ দেখায়।
নিচের এডিটরে একটি ছোট নতুন টেবিল daily_sales তৈরি হয় (day, revenue = বিক্রি টাকায়, orders_count = অর্ডারের সংখ্যা), চার দিনের হিসাব, যার একটি হরতালের দিন। একই কলামে integer division-এর ফাঁদটিও আবার দেখবেন:
SELECT day, revenue, orders_count,
revenue / orders_count AS naive,
revenue * 1.0 / NULLIF(orders_count, 0) AS avg_order,
COALESCE(ROUND(revenue * 1.0
/ NULLIF(orders_count, 0), 2), 0) AS report
FROM daily_sales;চারটি সারি। ১ অক্টোবর: naive = 416 (integer division ৪১৬.৬৭-কে কেটে ফেলেছে), avg_order = 416.666…, report = 416.67. ২ অক্টোবর (হরতাল): naive NULL, avg_order NULL, report 0. ৩ আর ৪ অক্টোবর: 500 আর 450 — এখানে ভাগ নিঃশেষে মেলে, তাই integer division-এর ভুল ধরা পড়ে না; ঠিক এ কারণেই বাগটি পরীক্ষায় লুকিয়ে থাকে। একটি সতর্কতা: SQLite-এ naive কলামটিও ২ অক্টোবরে শুধু NULL দিয়েছে, কারণ SQLite শূন্য দিয়ে ভাগে NULL দেয়। একই কোয়েরি PostgreSQL-এ চালালে naive কলামের জন্য পুরো কোয়েরি division by zero ত্রুটিতে থেমে যেত; NULLIF-ওয়ালা কলামগুলো সেখানেও নিরাপদ। পোর্টেবল কোড তাই সবসময় NULLIF দিয়ে লেখা হয়।
একই কাজ, ভিন্ন নাম
| ফাংশন | কোথায় | কী করে |
|---|---|---|
COALESCE(a, b, …) | সবখানে (standard) | প্রথম non-NULL মান; যত খুশি বিকল্প |
NULLIF(a, b) | সবখানে (standard) | a = b হলে NULL, নইলে a |
IFNULL(a, b) | SQLite, MySQL | দুই-বিকল্পের COALESCE |
ISNULL(a, b) | SQL Server | দুই-বিকল্পের COALESCE (ফলের টাইপ a-র মতো) |
ISNULL(a) | MySQL | ভিন্ন জিনিস: a NULL হলে 1, নইলে 0 — একটি পরীক্ষা |
NVL(a, b), NVL2(a, b, c) | Oracle | NVL = দুই-বিকল্পের COALESCE; NVL2: a NULL নয় হলে b, NULL হলে c |
নিয়মটি সহজ: নতুন কোডে COALESCE লিখুন — সব DBMS-এ চলে, একাধিক বিকল্প নেয়, আর অন্য কেউ পড়লে বোঝেন। পুরোনো কোড এক DBMS থেকে অন্যটিতে সরালে IFNULL, ISNULL, NVL খুঁজে বদলাতে হয়, বিশেষ করে MySQL-এর এক-আর্গুমেন্টের ISNULL, যা নামে এক হলেও SQL Server-এরটির মতো কাজ করে না।
৮ · CASE — SQL-এর “যদি… তবে… নইলে”
বেশিরভাগ প্রোগ্রামিং ভাষায় if-else আছে। SQL-এ তার জায়গায় আছে CASE expression — একটি expression, অর্থাৎ এটি একটি মান তৈরি করে, আর যেখানে মান লেখা যায় (SELECT, WHERE, ORDER BY, এমনকি aggregate-এর ভেতরে) সেখানেই বসানো যায়। এর দুটি রূপ:
Simple CASE
একটি মানকে কয়েকটি মানের সাথে সমতায় মেলায়: CASE status WHEN 'pending' THEN 1 WHEN 'shipped' THEN 2 ELSE 3 END. সুইচবোর্ডের মতো — শুধু = দিয়ে তুলনা।
Searched CASE
প্রতিটি WHEN-এ একটি পূর্ণ শর্ত: CASE WHEN price >= 5000 THEN 'premium' WHEN price >= 1000 THEN 'mid' ELSE 'budget' END. যেকোনো তুলনা, IS NULL, LIKE — সব চলে।
দুই রূপেরই নিয়ম এক: WHEN-গুলো ওপর থেকে নিচে ক্রমে দেখা হয়; প্রথম যেটি TRUE, তার THEN-এর মান ফেরত, বাকিগুলো আর দেখা হয় না। কোনোটি TRUE না হলে ELSE-এর মান; ELSE না থাকলে NULL. আর এখানেও তিন-মানের যুক্তি: UNKNOWN শর্ত TRUE নয়, তাই সেটি পার হয় না। Simple CASE-এ CASE x WHEN NULL THEN … কখনো মেলে না, কারণ ভেতরে x = NULL হিসাব হয়।
| পণ্য | |
| যা দেখা হলো | |
| ফল |
- প্রথম WHEN-এই মিল। Mobile Phone-এর দাম 15000. প্রথম শর্ত
price >= 5000TRUE, তাই ফল 'premium', আর বাকি দুটি শাখা দেখাই হয় না। 15000 যে 1000-এর চেয়েও বড়, তাতে কিছু আসে যায় না — প্রথম মিলই জেতে। - দ্বিতীয় WHEN. Saree-র দাম 4500. প্রথম শর্ত FALSE, দ্বিতীয়
price >= 1000TRUE — ফল 'mid'. খেয়াল করুন, দ্বিতীয় শর্তে “এবং 5000-এর কম” লিখতে হয়নি; আগের শাখা ব্যর্থ হয়েছে বলেই এখানে আসা, তাই সেটি আপনিই সত্য। - ELSE. Rice 5kg-এর দাম 420 — দুটি শর্তই FALSE. কোনো WHEN TRUE না হলে ELSE-এর মান: 'budget'.
- ক্রম ভুল হলে। একই শর্ত দুটি উল্টো ক্রমে লিখলে Mobile Phone প্রথম শাখা
price >= 1000-এই আটকে যায় — ফল 'mid'. 'premium' শাখায় কোনো পণ্য কখনো পৌঁছাবে না। কোনো ত্রুটি নেই, শুধু ভুল লেবেল। নিয়ম: সবচেয়ে কড়া শর্ত আগে। - NULL হলে। ধরুন কোনো পণ্যের দাম অজানা।
NULL >= 5000UNKNOWN,NULL >= 1000UNKNOWN — কোনোটিই TRUE নয়, তাই ELSE: 'budget'. অজানা দাম রিপোর্টে “সস্তা” দেখাচ্ছে! ভালো অভ্যাস: প্রথম শাখায়WHEN price IS NULL THEN 'unknown'. - ELSE না থাকলে। ELSE বাদ দিলে কোনো শাখা না মিললে ফল NULL — যেন
ELSE NULLলেখা আছে। ইচ্ছা করে এটিই কখনো কখনো চাই (পরের এডিটরের COUNT দেখুন), কিন্তু ভুলে বাদ পড়লে রিপোর্টে ফাঁকা ঘর আসে।
এবার CASE-এর তিনটি প্রচলিত ব্যবহার একসাথে: SELECT-এ লেবেল (price band), ORDER BY-তে নিজের পছন্দের ক্রম (প্রথমে pending, তারপর shipped…), আর aggregate-এর ভেতরে শর্তসাপেক্ষ গণনা (conditional aggregation).
SELECT name, price,
CASE WHEN price >= 5000 THEN 'premium'
WHEN price >= 1000 THEN 'mid'
ELSE 'budget' END AS band
FROM products ORDER BY price DESC;
SELECT order_id, status FROM orders
ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'shipped' THEN 2
WHEN 'delivered' THEN 3 ELSE 4 END, order_id;
SELECT SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) AS delivered,
COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending,
COUNT(*) AS total
FROM orders;প্রথম ফলে ১০টি পণ্য: শুধু Mobile Phone 'premium'; Saree (4500), Smart Watch (3500), Panjabi (2200), Power Bank (1800) আর Headphone (1200) 'mid'; DSA Book, SQL Book, Rice 5kg, Mustard Oil 1L 'budget'. দ্বিতীয় ফলে ১০টি অর্ডার এই ক্রমে: 1006, 1009 (pending), 1003, 1008 (shipped), 1001, 1002, 1005, 1007, 1010 (delivered), শেষে 1004 (cancelled). বর্ণানুক্রমে সাজালে আসত cancelled, delivered, pending, shipped — যা দোকানের কাজের ক্রমের সাথে মেলে না; CASE প্রতিটি status-কে একটি ক্রম-নম্বর দিয়েছে। তৃতীয় ফল: 5 | 2 | 10. দুটি কৌশল খেয়াল করুন: SUM-এ মিললে 1, না মিললে 0 যোগ; আর COUNT-এ ELSE নেই, তাই না মিললে NULL — আর COUNT NULL গোনে না (অংশ ৬)। এক কোয়েরিতে অনেক রকম গণনার এই কৌশল পর্ব ১৮ আর ২৭-এর pivot-এ কাজে লাগবে।
৯ · CAST, টাইপ বদল আর 0/1 দিয়ে সত্য-মিথ্যা
CAST(x AS type) একটি মানকে অন্য data type-এ বদলায়: লেখা থেকে সংখ্যা, পূর্ণসংখ্যা থেকে দশমিক, দশমিক থেকে লেখা। অংশ ১-এ CAST(7 AS REAL) / 2 integer division এড়াতে এটিই করেছিল। কেউ না বললেও DBMS অনেক সময় নিজেই টাইপ বদলায় — একে বলে implicit conversion. এখানে DBMS-গুলোর মেজাজ সবচেয়ে আলাদা:
| Expression | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
CAST('42' AS INTEGER) + 1 | 43 | 43 | 43 (AS SIGNED লিখতে হয়) |
CAST(3.99 AS INTEGER) | 3 (কাটে) | 4 (গোল করে) | 4 (AS SIGNED, গোল করে) |
CAST('12abc' AS INTEGER) | 12 (শুরুর অঙ্ক নেয়) | ত্রুটি: invalid input syntax | 12, সাথে warning |
'10' + 5 | 15 | 15 (উদ্ধৃত literal-এর টাইপ অনুমান করে; TEXT কলাম হলে ত্রুটি) | 15 |
5 > 3 | 1 | true (আসল boolean) | 1 |
SQLite-এ কোনো আলাদা boolean টাইপ নেই। তুলনার ফল সংখ্যা: TRUE = 1, FALSE = 0, UNKNOWN = NULL; TRUE আর FALSE শব্দ দুটি ৩.২৩ (২০১৮) থেকে 1 আর 0-এর অন্য নাম। এজন্যই অংশ ৩-এ 1 | 0 আর অংশ ৫-এ 0 | NULL | 1 দেখেছেন। এর একটি সুবিধাও আছে: SUM(status = 'delivered') লিখলেই ডেলিভারি গোনা হয়ে যায়, CASE ছাড়াই। কিন্তু এটি SQLite আর MySQL-এর কৌশল; PostgreSQL-এ boolean যোগ করা যায় না (লিখতে হয় COUNT(*) FILTER (WHERE …) বা CASE), আর SQL Server-এ SELECT 5 > 3 লেখাই যায় না। সব জায়গায় চলে এমন কোড লিখতে চাইলে CASE-ই নিরাপদ।
SELECT CAST('2025-09-01' AS DATE) + 30; -- 2025-10-01 (তারিখ + দিন)
SELECT '42'::integer, 7::numeric / 2; -- 42 | 3.5000000000000000
SELECT COUNT(*) FILTER (WHERE status = 'delivered') AS delivered FROM orders;SQLite-এর টাইপ নমনীয় — সেটিকে সার্বজনীন ভাববেন না। SQLite প্রতিটি কলামকে একটি type affinity দেয়, কিন্তু চাইলে INTEGER কলামেও লেখা জমা রাখতে দেয় (STRICT টেবিল ছাড়া)। PostgreSQL, SQL Server, Oracle কড়া টাইপের — ভুল টাইপ ঢোকালে ত্রুটি দেয়। SQLite-এ যে কোয়েরি “চলে”, অন্য DBMS-এ সেটি টাইপের কারণে থেমে যেতে পারে, আবার উল্টোও হতে পারে। তাই টাইপ মেলানো দরকার হলে CAST লিখে দিন — অনুমানের ওপর ছাড়বেন না।
১০ · AI-এর যুগে অপারেটর আর NULL
AI-কে “ঢাকার বাইরের গ্রাহক কতজন” বা “প্রতিটি দোকানের সফলতার হার” জিজ্ঞেস করলে চমৎকার দেখতে SQL আসে, আর সেটি চলেও। সমস্যা হলো, এই পর্বের ভুলগুলো কোনো ত্রুটি দেয় না — শুধু সংখ্যা ভুল আসে। AI প্রায়ই যা করে:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
WHERE phone = NULL | সবসময় শূন্য সারি | প্রতিটি NULL তুলনায় IS NULL আছে কি না দেখুন |
NULL থাকতে পারে এমন subquery-তে NOT IN | যেদিন প্রথম NULL ঢোকে, সেদিন থেকে ফল খালি | subquery-র কলামে NULL ঢুকিয়ে আবার চালান; NOT EXISTS চান |
city <> 'Dhaka' দিয়ে “ঢাকার বাইরে” | শহর অজানা এমন সারি গায়েব | WHERE p আর WHERE NOT p-এর গণনা যোগ করে মোট সারির সাথে মেলান |
অনুপাতে a / b (দুটিই integer) | SQLite/PostgreSQL-এ 0 বা কাটা ফল | এমন ডেটায় পরীক্ষা করুন যেখানে ভাগ নিঃশেষে মেলে না |
| শূন্য হতে পারে এমন হর | PostgreSQL-এ পুরো রিপোর্ট ত্রুটিতে থামে | হর-এ NULLIF(…, 0) আছে কি না দেখুন |
MySQL-এর জন্য || দিয়ে জোড়া | লেবেলের জায়গায় 0 | লক্ষ্য DBMS বলে দিন; CONCAT চান |
| CASE-এ ঢিলা শর্ত আগে | কিছু শাখায় কেউ পৌঁছায় না | প্রতিটি শাখার জন্য একটি করে নমুনা সারি বানিয়ে দেখুন |
| AND/OR মিশিয়ে বন্ধনী ছাড়া | অন্য গ্রাহকের সারি ঢুকে পড়ে | বন্ধনী দিয়ে নতুন করে লিখে ফল মেলান |
সব জায়গায় COALESCE(x, 0) | “জানা নেই” আর “শূন্য” মিশে গড় ভুল | প্রতিটি COALESCE-এর পক্ষে এক লাইনের ব্যবসায়িক যুক্তি চান |
AI-কে SQL চাওয়ার সময় NULL আর টাইপের তথ্য আগেই দিয়ে দিন, আর পরীক্ষার ডেটাও চেয়ে নিন:
AI-এর দেওয়া কোয়েরি চালানোর আগে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
NULL = NULL-এর ফল কী? — TRUE নয়, UNKNOWN; তাই NULL খুঁজতে IS NULL লাগে।x NOT IN (1, 2, NULL)কেন কখনো সারি দেয় না? — এটিx <> 1 AND x <> 2 AND x <> NULL; শেষ অংশ সবসময় UNKNOWN, তাই ফল কখনো TRUE হয় না।- COUNT(*) আর COUNT(col)-এর পার্থক্য? — প্রথমটি সব সারি গোনে, দ্বিতীয়টি শুধু যেখানে col NULL নয়।
- WHERE আর CHECK UNKNOWN-কে কীভাবে দেখে? — WHERE UNKNOWN সারি ফেলে দেয়, CHECK UNKNOWN সারি ঢুকতে দেয় (আটকায় শুধু FALSE).
- COALESCE আর NULLIF কী করে? — COALESCE প্রথম non-NULL মান দেয়; NULLIF(a, b) সমান হলে NULL দেয়, যা দিয়ে শূন্য দিয়ে ভাগ ঠেকানো হয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোয়েরি-লেখক থেকে ডেটাবেস-ডিজাইনার বানাবে।
প্র ০১একটি NULL দিয়ে “জানা নেই” আর “প্রযোজ্যই নয়” — দুটোই বোঝানো কি ঠিক? Codd আর তাঁর সমালোচকেরা এ নিয়ে কী বলেছিলেন?
দুটি উদাহরণ ভাবুন। Tanvir Ahmed-এর phone NULL — তাঁর ফোন নিশ্চয়ই আছে, শুধু আমরা জানি না। আর একজন শিক্ষার্থীর spouse_name NULL — হয়তো তিনি অবিবাহিত, অর্থাৎ মানটিই প্রযোজ্য নয়। SQL দুটিকে একই NULL দিয়ে লেখে, আর একই তিন-মানের যুক্তি চালায়। দ্বিতীয় ক্ষেত্রে “অজানা” বলাটা আসলে ভুল: আমরা নিশ্চিতভাবেই জানি যে কোনো স্বামী বা স্ত্রী নেই।
Codd এই সমস্যা দেখেছিলেন। ১৯৯০ সালে প্রকাশিত RM/V2 বইয়ে তিনি দুই ধরনের চিহ্ন প্রস্তাব করেন — “A-mark” (applicable কিন্তু অজানা) আর “I-mark” (inapplicable) — আর তার জন্য চার-মানের যুক্তি। সমালোচনা এল দুই দিক থেকে। একদিকে, চার-মানের truth table মানুষের পক্ষে মনে রাখা আরও কঠিন; তিন মানেই যেখানে পেশাদারেরা ভুল করেন, সেখানে চার মান বাস্তবে কতটা টিকবে? অন্যদিকে Chris Date আর Hugh Darwen বহু প্রবন্ধে আর তাঁদের “The Third Manifesto”-তে যুক্তি দেন, NULL নিজেই relational model-এর সাথে বেমানান — একটি relation-এর ঘরে মানই থাকার কথা, “মানের অনুপস্থিতি” নয়। তাঁদের প্রস্তাব: অনুপস্থিত তথ্য আলাদা টেবিলে রাখুন। যাঁদের ফোন জানা, শুধু তাঁরা customer_phones টেবিলে থাকবেন।
SQL কোনো পথেই যায়নি, কিন্তু ডিজাইনে দুটি শিক্ষাই কাজে লাগে। প্রথমত, যেখানে “প্রযোজ্য নয়” আর “জানা নেই” আলাদা অর্থ বহন করে, সেখানে একটি আলাদা কলাম রাখুন — যেমন marital_status — যাতে NULL-এর অর্থ দ্ব্যর্থহীন হয়। দ্বিতীয়ত, ঐচ্ছিক তথ্য যদি অনেক কলামে ছড়িয়ে থাকে (EER-এর subclass-এর মতো, পর্ব ৯), তবে আলাদা টেবিল প্রায়ই পরিষ্কার নকশা দেয়। আর প্রতিটি NULL-যোগ্য কলামের ডকুমেন্টেশনে এক লাইনে লিখে রাখুন: “NULL মানে কী”।
মূল উপলব্ধি: NULL একটি মান নয়, একটি সংকেত — আর সংকেতের অর্থ ডেটাবেস জানে না, জানেন ডিজাইনার। তাই অর্থটি schema-তে বা ডকুমেন্টেশনে স্পষ্ট করে দেওয়া আপনার দায়িত্ব।
প্র ০২Oracle-এ ফাঁকা লেখা '' আর NULL একই জিনিস। একটি অ্যাপ Oracle থেকে PostgreSQL-এ সরানোর সময় এ থেকে কী কী বাগ হতে পারে?
Oracle বহু দশক ধরে দৈর্ঘ্য শূন্য এমন লেখাকে NULL হিসেবে দেখে। ফলে Oracle-এ INSERT … VALUES ('') আসলে NULL জমা রাখে, WHERE middle_name = '' কখনো কোনো সারি দেয় না (কারণ এটি = NULL), আর 'Rahim' || NULL হয় 'Rahim' — Oracle-এর || NULL-কে ফাঁকা লেখা ধরে। Oracle-এর নিজের ডকুমেন্টেশনেও বলা আছে, এই আচরণ ভবিষ্যতে বদলাতে পারে, তাই এর ওপর নির্ভর না করতে। কিন্তু পুরোনো কোড স্বাভাবিকভাবেই নির্ভর করে।
PostgreSQL (আর SQL standard) '' আর NULL-কে আলাদা দেখে: ফাঁকা লেখা একটি জানা মান — “মধ্যনাম নেই, আমরা নিশ্চিত”। এখন সরানোর পর কী ভাঙে, তালিকা করি। এক, যে রিপোর্ট WHERE middle_name IS NULL দিয়ে “মধ্যনাম নেই” গুনত, সে এখন নতুন অ্যাপের ঢোকানো ''-গুলো হারাবে। দুই, Oracle-এ যে লেবেল first || ' ' || middle || ' ' || last ঠিকঠাক কাজ করত, PostgreSQL-এ middle NULL হলেই পুরো নাম NULL হবে — অংশ ১-এর NULL propagation. তিন, UNIQUE কলামে একাধিক NULL চলে কিন্তু একাধিক '' চলে না, তাই আগে যে ডেটা ঢুকত, এখন duplicate ত্রুটি দিতে পারে। চার, NOT NULL কলামে Oracle '' ঢুকতে দিত না (কারণ সেটি NULL), PostgreSQL দেবে — ফলে “বাধ্যতামূলক” ঘর ফাঁকা লেখায় ভরে যেতে পারে।
পেশাদার মাইগ্রেশনে তাই একটি সিদ্ধান্ত আগে নেওয়া হয়: প্রতিটি লেখার কলামে “ফাঁকা” কে কোনটি দিয়ে বোঝানো হবে। প্রচলিত পথ হলো ডেটা আনার সময় NULLIF(trim(col), '') দিয়ে সব ফাঁকা লেখাকে NULL বানানো, বা উল্টোভাবে COALESCE দিয়ে NULL-কে ''; তারপর CHECK (col <> '') দিয়ে নিয়মটি পাকা করা। আর লেবেল বানাতে ||-এর বদলে CONCAT বা concat_ws, যা NULL-কে সহ্য করে।
মূল উপলব্ধি: “ফাঁকা” আর “অজানা” দুটি আলাদা তথ্য, আর প্রতিটি DBMS এদের আলাদাভাবে দেখে। এক সিস্টেম থেকে আরেকটিতে যাওয়ার আগে কলামপ্রতি এই অর্থ ঠিক না করলে বাগ আসে কোনো ত্রুটি ছাড়াই।
প্র ০৩Tony Hoare null reference-কে বলেছিলেন তাঁর “billion-dollar mistake”. প্রোগ্রামিং ভাষার null আর SQL-এর NULL কি একই জিনিস? অ্যাপ আর ডেটাবেসের সীমানায় কী গোলমাল হয়?
১৯৬৫ সালে ALGOL W ভাষার নকশা করতে গিয়ে Tony Hoare null reference যোগ করেন — “এটি কোনো কিছুর দিকে নির্দেশ করে না”। ২০০৯ সালে লন্ডনের একটি সম্মেলনে তিনি একে বলেন তাঁর “billion-dollar mistake”, কারণ এরপর অগণিত প্রোগ্রাম null-এ হাত দিয়ে ভেঙেছে — Java-র NullPointerException, C-র segmentation fault. Kotlin, Swift, Rust-এর মতো নতুন ভাষা তাই null-কে টাইপের অংশ বানিয়েছে, যাতে compiler আগেই সতর্ক করে।
SQL-এর NULL দেখতে একই রকম, কিন্তু আচরণে আলাদা। প্রোগ্রামিং ভাষায় null-এ হাত দিলে প্রোগ্রাম জোরে ভাঙে — বাগ সাথে সাথে চোখে পড়ে। SQL-এ NULL চুপচাপ ছড়ায়: NULL + 5 NULL, তুলনা UNKNOWN, WHERE সারি ফেলে দেয় — কোনো ত্রুটি নেই। আর সমতার নিয়মও আলাদা: Python-এ None == None হলো True, JavaScript-এ null === null true, কিন্তু SQL-এ NULL = NULL UNKNOWN.
গোলমাল হয় ঠিক সীমানায়। অ্যাপের প্রোগ্রামার কোডে লেখেন "SELECT * FROM customers WHERE phone = ?" আর প্যারামিটারে পাঠান None. Driver সেটিকে SQL NULL বানায়, শর্তটি হয় phone = NULL — কোনো সারি আসে না, যদিও প্রোগ্রামারের মাথায় ছিল Python-এর সমতা। ভালো ORM এটি জানে: SQLAlchemy-তে Customer.phone == None লিখলে সে নিজেই phone IS NULL বানায়, Django-তে filter(phone=None)-ও IS NULL হয়। কিন্তু হাতে লেখা SQL-এ বা ORM-এর কাঁচা কোয়েরিতে এই সুরক্ষা নেই। নিরাপদ উপায় হলো phone IS NOT DISTINCT FROM ? (বা SQLite-এ phone IS ?), যা NULL-সহ সব মানে সঠিক তুলনা করে।
উল্টো দিকেও সমস্যা আছে: ডেটাবেস থেকে NULL এসে অ্যাপে None হয়, আর অ্যাপ সেটিকে সংখ্যা ভেবে যোগ করতে গেলে TypeError. তাই schema-তে যত বেশি সম্ভব NOT NULL দিন — যে কলাম NULL হতেই পারে না, তার জন্য অ্যাপে কোনো সাবধানতা লাগে না।
মূল উপলব্ধি: নাম এক হলেও দুই জগতের null-এর নিয়ম আলাদা: একটি জোরে ভাঙে, অন্যটি চুপচাপ ছড়ায়। অ্যাপ আর ডেটাবেসের সংযোগে প্রতিটি সমতা-তুলনায় ভাবুন — এখানে কোন জগতের নিয়ম চলছে?
প্র ০৪SQL standard বলে, দুটি পূর্ণসংখ্যা ভাগের ফলের দশমিক অংশ কতটা থাকবে, তা “implementation-defined”. এর ফলে MySQL 3.5 আর PostgreSQL 3 দেয়। কোনটি সঠিক, আর এমন অস্পষ্টতা নিয়ে কীভাবে কাজ করবেন?
দুটিই standard মেনে চলে। SQL standard বলে, দুটি exact numeric (পূর্ণসংখ্যা বা নির্দিষ্ট দশমিকের সংখ্যা) ভাগ করলে ফলও exact numeric হবে, তবে তার precision আর scale (মোট অঙ্ক আর দশমিকের পরের অঙ্ক) ঠিক করবে প্রতিটি DBMS নিজে। PostgreSQL, SQL Server আর SQLite ঠিক করেছে: দুই integer-এর ভাগ integer — C, Java-র মতো প্রোগ্রামিং ভাষার পরিচিত নিয়ম। MySQL ঠিক করেছে / সবসময় দশমিক দেবে (ডিফল্টে দশমিকের পরে চার অঙ্ক, তাই 3.5000), আর পূর্ণসংখ্যার ভাগের জন্য আলাদা অপারেটর DIV. Oracle-এর NUMBER টাইপে integer আর দশমিকের আলাদা টাইপই নেই, তাই 3.5.
এই অস্পষ্টতা বাস্তবে দুই রকম বাগ আনে। প্রথমটি অংশ ১-এর সতর্কতা: অনুপাত শূন্য হয়ে যাওয়া। দ্বিতীয়টি আরও সূক্ষ্ম — দশমিকের নির্ভুলতা। 1 / 3.0-এর ফলে দশমিকের পরে কয়টি অঙ্ক থাকবে, তা-ও DBMS-ভেদে আলাদা — কোথাও চার-পাঁচটি, কোথাও পনেরো-বিশটি। টাকার হিসাবে এক পয়সার পার্থক্যও অডিটে ধরা পড়ে। পর্ব ১১-এ দেখেছেন, টাকা REAL-এ (floating-point) রাখলে ০.১ নিখুঁতভাবে জমা হয় না; ভাগে সেই সমস্যা আরও বাড়ে।
পেশাদার নিয়ম তিনটি। এক, ভাগের আগে টাইপ স্পষ্ট করুন: CAST(a AS NUMERIC(12, 4)) / b বা অন্তত a * 1.0 / b. দুই, কোথায় গোল করবেন, তা ঠিক করুন — সাধারণত শেষ ধাপে, একবার, ROUND(…, 2) দিয়ে; মাঝপথে বারবার গোল করলে ভুল জমে। তিন, পরীক্ষার ডেটায় এমন সংখ্যা রাখুন যা নিঃশেষে ভাগ হয় না (১০ ÷ ৩), কারণ ১০ ÷ ২-এ integer division-এর বাগ লুকিয়ে থাকে। একটি কোয়েরি একাধিক DBMS-এ চালাতে হলে এই তিনটিই বাধ্যতামূলক।
স্প্রেডশিট থেকে আসা বিশ্লেষকেরা এখানে সবচেয়ে বেশি অবাক হন, কারণ Excel-এ =7/2 সবসময় 3.5. SQL-এ টাইপ মানের অংশ — একই অঙ্ক দুটি ভিন্ন টাইপে ভিন্ন আচরণ করে।
মূল উপলব্ধি: SQL-এ একটি অপারেটরের ফল শুধু মান দিয়ে নয়, টাইপ দিয়েও ঠিক হয়। যেখানে standard সিদ্ধান্ত DBMS-এর হাতে ছেড়েছে, সেখানে আপনার কোয়েরিতে সিদ্ধান্তটি নিজে লিখে দিন।
প্র ০৫WHERE UNKNOWN সারি ফেলে দেয়, কিন্তু CHECK constraint UNKNOWN সারি ঢুকতে দেয়। SQL কেন এই দুই জায়গায় উল্টো আচরণ বেছে নিয়েছে, আর এতে কী ঝুঁকি?
SQL standard অনুযায়ী একটি CHECK constraint ভাঙে কেবল তখন, যখন শর্তটি কোনো সারির জন্য FALSE হয়। UNKNOWN হলে সারিটি গৃহীত। db-uni-এর students টেবিলে CHECK (cgpa BETWEEN 0 AND 4) আছে, অথচ Fahim Karim-এর cgpa NULL দিব্যি জমা আছে — NULL BETWEEN 0 AND 4 UNKNOWN, তাই আটকায়নি। অন্যদিকে SELECT … WHERE cgpa BETWEEN 0 AND 4 লিখলে Fahim আসবেন না।
যুক্তিটি “সন্দেহের সুবিধা”-র। WHERE একটি প্রশ্নের উত্তর দেয় — “কারা নিশ্চিতভাবে শর্ত পূরণ করে?” — তাই অজানাকে বাদ দেয়। CHECK একটি নিষেধ পাহারা দেয় — “কারা নিশ্চিতভাবে নিয়ম ভাঙে?” — তাই অজানাকে আটকায় না। আদালতের মতো: প্রমাণ না থাকলে দোষী সাব্যস্ত করা যায় না, আবার নিরপরাধও প্রমাণ হয় না। এই নকশার ব্যবহারিক কারণও আছে: NULL-যোগ্য কলামে CHECK যদি UNKNOWN-কে আটকাত, তবে প্রতিটি ঐচ্ছিক কলামে CHECK লিখলেই সেটি কার্যত NOT NULL হয়ে যেত। “ঐচ্ছিক, কিন্তু থাকলে ০ থেকে ৪” — এই খুব সাধারণ নিয়মটি লেখা যেত না।
ঝুঁকি হলো, অনেকে CHECK লিখে ভাবেন কলামটি সুরক্ষিত। CHECK (price > 0) থাকলেও NOT NULL না দিলে দামহীন পণ্য ঢুকবে। db-shop-এর products-এ তাই দুটিই আছে: price REAL NOT NULL CHECK (price > 0). একই যুক্তি একাধিক কলামের CHECK-এ আরও সূক্ষ্ম: CHECK (end_date > start_date) — যেকোনো একটি NULL হলেই নিয়মটি নিষ্ক্রিয়। আর UNIQUE-এর বেলায়ও একই দর্শন: দুটি NULL “সমান কি না জানা নেই”, তাই বেশিরভাগ DBMS এদের duplicate ধরে না (পর্ব ৬)।
যাচাইয়ের অভ্যাস: প্রতিটি CHECK-এর জন্য নিজেকে জিজ্ঞেস করুন — “এখানে NULL এলে কী হবে, আর সেটি কি আমি চাই?” না চাইলে NOT NULL যোগ করুন, অথবা শর্তে স্পষ্ট লিখুন: CHECK (cgpa IS NOT NULL AND cgpa BETWEEN 0 AND 4).
মূল উপলব্ধি: একই তিন-মানের যুক্তি দুই জায়গায় দুই প্রশ্নের উত্তর দেয় — WHERE খোঁজে “নিশ্চিত হ্যাঁ”, CHECK খোঁজে “নিশ্চিত না”। NULL-কে আটকাতে চাইলে তা আলাদা করে বলতে হয়: NOT NULL.
অনুশীলন
খাতা-কলম নিন। দুটি হাতে হিসাব, দুটি কোয়েরি লেখা — উপরের যেকোনো db-shop এডিটরে চালিয়ে মিলিয়ে নিতে পারবেন।
-
ফল আগে বলুন (হাতে): SQLite-এ এই expression-গুলোর মান কী? (ক)
10 / 4(খ)10 % 4(গ)10 / 4.0(ঘ)NULL + 1(ঙ)'a' || NULL(চ)COALESCE(NULL, NULL, 'x')(ছ)NULLIF(5, 5)(জ)1 IN (1, NULL)(ঝ)2 IN (1, NULL)(ঞ)NULL OR 1(ক) 2 — integer division. (খ) 2 — ভাগশেষ। (গ) 2.5. (ঘ) NULL. (ঙ) NULL — NULL propagation. (চ) 'x' — প্রথম non-NULL. (ছ) NULL — দুটি সমান। (জ) 1 —
1 = 1 OR 1 = NULL= TRUE OR UNKNOWN = TRUE. (ঝ) NULL —2 = 1 OR 2 = NULL= FALSE OR UNKNOWN = UNKNOWN; WHERE-এ থাকলে সারিটি বাদ যেত। (ঞ) 1 — OR-এ একটি TRUE-ই যথেষ্ট।(জ) আর (ঝ) পাশাপাশি রাখলেই NOT IN-এর ফাঁদের মূল কথা: তালিকায় NULL থাকলে “নেই” কখনো নিশ্চিত হয় না।
-
Trace করুন: একটি টেবিল
t(a)-তে পাঁচটি সারি: 1, 2, 3, NULL, NULL. প্রতিটি কোয়েরি কয়টি সারি দেবে? (ক)WHERE a > 1(খ)WHERE NOT (a > 1)(গ)WHERE a > 1 OR a IS NULL(ঘ)WHERE a NOT IN (2, NULL)(ঙ)SELECT COUNT(*), COUNT(a), AVG(a) FROM t(ক) ২টি (2, 3). (খ) ১টি (1) — দুটি NULL সারির জন্য
NOT (NULL > 1)= NOT UNKNOWN = UNKNOWN, তাই বাদ; ২ + ১ = ৩, মোট ৫ নয় — অংশ ৫-এর দাবি ৩। (গ) ৪টি (2, 3, NULL, NULL). (ঘ) ০টি — যেকোনো a-র জন্যa <> NULLUNKNOWN, তাই ফল FALSE বা UNKNOWN, কখনো TRUE নয়। (ঙ)5 | 3 | 2.0— AVG শুধু 1, 2, 3-এর গড়। -
কোয়েরি লিখুন: db-shop-এর products থেকে প্রতিটি পণ্যের নাম, stock আর একটি stock_status কলাম দেখান: stock 0 হলে 'out of stock', 10-এর কম হলে 'low', নইলে 'ok'. ফল এমনভাবে সাজান যাতে 'out of stock' সবার আগে, তারপর 'low', তারপর 'ok'; একই দলে নামের বর্ণানুক্রমে।
SELECT name, stock, CASE WHEN stock = 0 THEN 'out of stock' WHEN stock < 10 THEN 'low' ELSE 'ok' END AS stock_status FROM products ORDER BY CASE WHEN stock = 0 THEN 1 WHEN stock < 10 THEN 2 ELSE 3 END, name;ফল: প্রথমে Power Bank (0, out of stock); তারপর Saree (8) আর Smart Watch (5) — low; তারপর বাকি সাতটি ok: DSA Book, Headphone, Mobile Phone, Mustard Oil 1L, Panjabi, Rice 5kg, SQL Book. খেয়াল করুন,
stock = 0শাখাstock < 10-এর আগে — উল্টো হলে Power Bank-ও 'low' হয়ে যেত (CASE-এর প্রথম মিলই জেতে)। -
বাগ ঠিক করুন: একজন সহকর্মী (বা AI) “ঢাকার বাইরের সব গ্রাহক” রিপোর্টের জন্য লিখেছেন
SELECT name FROM customers WHERE city != 'Dhaka';ম্যানেজার বলছেন, শহর অজানা গ্রাহকদেরও তালিকায় চাই, যাতে কল করে শহর জেনে নেওয়া যায়। কোয়েরিটি ঠিক করুন — দুটি ভিন্ন উপায়ে। db-shop-এ কতজন আসবেন?SELECT name FROM customers WHERE city <> 'Dhaka' OR city IS NULL; SELECT name FROM customers WHERE city IS DISTINCT FROM 'Dhaka';দুটিই ৫ জন দেয়: Karima Begum, Nusrat Jahan, Tanvir Ahmed, Farhana Akter আর Imran Hossain. মূল কোয়েরি দিত ৪ জন — Imran-এর জন্য
NULL != 'Dhaka'UNKNOWN. যাচাই:city = 'Dhaka'-এর ৩ জন + এই ৫ জন = মোট ৮, কোনো গরমিল নেই। তৃতীয় উপায় SQLite-এ:city IS NOT 'Dhaka'; আর সবখানে চলেCOALESCE(city, '') <> 'Dhaka', যদিও সেটি city-র index ব্যবহার করতে পারে না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি searched CASE expression হিসাব করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রথম WHEN-এর শর্তটি হিসাব করা
- শর্তটি TRUE হলে তার THEN-এর মান নিয়ে থেমে যাওয়া
- FALSE বা UNKNOWN হলে পরের WHEN-এ যাওয়া
- কোনো WHEN TRUE না হলে ELSE-এর মান নেওয়া
- ELSE না থাকলে ফল হিসেবে NULL দেওয়া
PostgreSQL-এ SELECT 7 / 2; চালালে ফল কী আসে?
- 3
- 3.5
- 4
- ত্রুটি
SQL-এর তিন-মানের যুক্তিতে UNKNOWN AND FALSE-এর ফল কী?
- UNKNOWN
- FALSE
- 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)
- ৩টি (1, 2, 3)
- ৪টি
- ০টি
একটি customers টেবিলে ১০টি সারি; এর মধ্যে ৩ জনের phone NULL. SELECT COUNT(*), COUNT(phone) FROM customers;-এর ফল কী?
- 10 আর 10
- 7 আর 7
- 10 আর 7
- 10 আর NULL
PostgreSQL-এ revenue আর orders_count দুটি integer কলাম; কিছু সারিতে orders_count = 0. কোন expression শূন্য দিয়ে ভাগের ত্রুটি এড়িয়ে দশমিকসহ গড় দেয়?
- revenue / orders_count
- revenue * 1.0 / NULLIF(orders_count, 0)
- revenue * 1.0 / COALESCE(orders_count, 0)
- NULLIF(revenue, 0) / orders_count
score = 45 হলে CASE WHEN score >= 80 THEN 'A' WHEN score >= 60 THEN 'B' END-এর মান কী?
- 'A'
- 'B'
- ত্রুটি, কারণ ELSE নেই
- NULL
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- দুই integer-এর ভাগ SQLite/PostgreSQL/SQL Server-এ পূর্ণসংখ্যা দেয়; অনুপাতে
* 1.0বা CAST, হরেNULLIF(…, 0). - BETWEEN দুই প্রান্তসহ; সময়সহ তারিখে
>= শুরু AND < পরের দিন. LIKE-এ%যেকোনো কয়টি,_ঠিক একটি অক্ষর। - NULL-এর সাথে যেকোনো তুলনা UNKNOWN; NULL খুঁজতে IS NULL, NULL-সহ সমতায় IS DISTINCT FROM.
- WHERE শুধু TRUE রাখে, CHECK শুধু FALSE আটকায়; তালিকায় NULL থাকলে NOT IN কখনো TRUE নয়.
- CASE-এ প্রথম TRUE শাখা জেতে, ELSE না থাকলে NULL; COALESCE প্রথম non-NULL দেয়; COUNT(*) সারি গোনে, COUNT(col) NULL বাদ দেয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৭ · SQL ফাংশন — string, number আর dateপরবর্তী পর্বলেখা, সংখ্যা আর তারিখ নিয়ে কাজের ফাংশন।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- কম্পিউটার ফান্ডামেন্টালস · Boolean algebraভিত্তিদুই-মানের যুক্তি আর De Morgan-এর নিয়ম — তিন-মানের যুক্তির আগের ধাপ।
- JavaScript · মান, টাইপ আর coercionপাশের ট্র্যাকআরেকটি ভাষায় implicit conversion আর null-এর আচরণ — তুলনা করুন।
- Python · SQLite ডেটাবেসহাতে-কলমেPython-এর None কীভাবে SQL-এর NULL হয়, আর প্যারামিটারে NULL পাঠানোর সাবধানতা।