অধ্যায় 2 · ডেটা পড়া
BETWEEN, IN, LIKE ও NULL
- পৃষ্ঠা 8 / 22
- 12 মিনিট পড়া
আগের পাতায় তুলনা আর AND/OR দিয়ে WHERE লেখা শিখেছেন। এই পাতায় আসছে আরও চারটা হাতিয়ার, যেগুলো রোজকার ফিল্টারকে ছোট আর পরিষ্কার করে: রেঞ্জের জন্য BETWEEN, লিস্টের জন্য IN, টেক্সটের প্যাটার্নের জন্য LIKE, আর অনুপস্থিত মানের জন্য IS NULL।
শেষেরটাই সবচেয়ে জরুরি। আসল ডেটা ফাঁকে ভরা: কোনো কাস্টমার city ঘর খালি রেখেছেন, কোনো প্রোডাক্টের স্টকের হিসাব নেই, কোনো ট্রেনিং উদাহরণ কেউ এখনো লেবেল করেনি। SQL অনুপস্থিত মান সামলায় NULL আর তিন-মানের একটা লজিক দিয়ে, যা প্রায় সবাইকে অন্তত একবার চমকে দেয়। এখানে শিখে নিলে একগাদা নীরব বাগ এড়াতে পারবেন, যেগুলো কিছু না জানিয়ে রিপোর্ট বা ট্রেনিং সেট থেকে সারি বাদ দিয়ে দেয়।
যা শিখবেন
BETWEENদিয়ে রেঞ্জ (দুই প্রান্তসহ), আর সময়ের রেঞ্জ কেন>= start AND < endহিসেবে লেখা নিরাপদ।INআরNOT INদিয়ে লিস্ট।LIKE,%আর_দিয়ে টেক্সটের প্যাটার্ন, আর বড়-ছোট হাতের অক্ষরের নিয়ম ডেটাবেসভেদে কীভাবে আলাদা (ILIKE,LOWER)।NULL-এর মানে, তিন-মানের লজিক, আরIS NULL/IS NOT NULL।NOT IN-এর ফাঁদ: লিস্টে একটা NULL থাকলেই কিছু ফেরত আসে না।
BETWEEN: রেঞ্জ, দুই প্রান্তই ধরা
SELECT name, price
FROM products
WHERE price BETWEEN 1000 AND 2500
ORDER BY price;+---------------------------+-------+
| name | price |
+---------------------------+-------+
| Gift Card | 1000 |
| Python Crash Course | 1200 |
| Laptop Stand | 1500 |
| USB-C Hub | 2200 |
| Hands-On Machine Learning | 2500 |
+---------------------------+-------+Gift Card (ঠিক ১০০০) আর Hands-On Machine Learning (ঠিক ২৫০০) দুটোই এসেছে: BETWEEN a AND b মানে >= a AND <= b। দুটো খুঁটিনাটি: ছোট মানটা আগে লিখতে হয় (BETWEEN 2500 AND 1000 কিছুর সাথেই মেলে না), আর NOT BETWEEN রেঞ্জের বাইরের সবকিছু রাখে।
তারিখের রেঞ্জ
শুধু তারিখ হলে BETWEEN ভালোই কাজ করে। মার্চ ২০২৬-এ দেওয়া অর্ডার:
SELECT order_id, order_date
FROM orders
WHERE order_date BETWEEN '2026-03-01' AND '2026-03-31'
ORDER BY order_date;+----------+------------+
| order_id | order_date |
+----------+------------+
| 6 | 2026-03-08 |
| 7 | 2026-03-15 |
| 8 | 2026-03-28 |
+----------+------------+এবার ভাবুন, কলামে তারিখের সাথে সময়ও আছে, যেমন বেশিরভাগ ইভেন্ট আর লগ টেবিলে থাকে। ৩১ মার্চ দুপুর ২টার একটা অর্ডার রাখা হয় '2026-03-31 14:00' হিসেবে, আর সেটা '2026-03-31'-এর চেয়ে বড়:
SELECT '2026-03-31 14:00' BETWEEN '2026-03-01' AND '2026-03-31' AS with_time,
'2026-03-31' BETWEEN '2026-03-01' AND '2026-03-31' AS date_only;+-----------+-----------+
| with_time | date_only |
+-----------+-----------+
| 0 | 1 |
+-----------+-----------+ফলে ৩১ তারিখ রাত ১২টার পরের সব ইভেন্ট বাদ পড়ে যায়। যে অভ্যাস সবসময় ঠিক, তা হলো অর্ধ-খোলা রেঞ্জ (half-open range): শুরুটা ধরুন, পরের সময়কালের শুরুটা বাদ দিন।
SELECT order_id, order_date
FROM orders
WHERE order_date >= '2026-03-01'
AND order_date < '2026-04-01'
ORDER BY order_date;+----------+------------+
| order_id | order_date |
+----------+------------+
| 6 | 2026-03-08 |
| 7 | 2026-03-15 |
| 8 | 2026-03-28 |
+----------+------------+এখানে একই সারি আসে, তারিখ আর টাইমস্ট্যাম্প দুটোতেই কাজ করে, আর "এই মাসে কত দিন?" ভাবতেও হয় না। ML-এর কাজে সময়ভিত্তিক train/test ভাগ এভাবেই করা হয়: ট্রেনিং < '2026-05-01'-এ, টেস্ট >= '2026-05-01'-এ। সীমানাটা ঠিক একদিকেই পড়ে, তাই কোনো সারি দুই দিকে ঢুকে পড়ে না।
IN আর NOT IN: মানের লিস্ট
OR দিয়ে জোড়া কয়েকটা = সংক্ষেপে লেখার উপায় হলো IN:
SELECT order_id, customer_id, status
FROM orders
WHERE status IN ('shipped', 'pending', 'cancelled')
ORDER BY order_id;+----------+-------------+-----------+
| order_id | customer_id | status |
+----------+-------------+-----------+
| 5 | 4 | cancelled |
| 12 | 2 | shipped |
| 13 | 6 | pending |
+----------+-------------+-----------+এটা status = 'shipped' OR status = 'pending' OR status = 'cancelled'-এর সমান, কিন্তু ছোট, আর পরে আরও শর্ত যোগ করলেও AND/OR অগ্রাধিকারের বাগ এতে ঢোকে না। NOT IN সেই সারিগুলো রাখে যাদের মান লিস্টে নেই:
SELECT name, city
FROM customers
WHERE city NOT IN ('Dhaka', 'Chattogram')
ORDER BY customer_id;+---------------+----------+
| name | city |
+---------------+----------+
| Rafiq Islam | Sylhet |
| Imran Hossain | Khulna |
| Karim Uddin | Rajshahi |
+---------------+----------+সাদিয়া, যাঁর city NULL, আবারও নেই। কথাটা মনে রাখুন; NULL অংশে এর ব্যাখ্যা আছে।
LIKE: টেক্সটের প্যাটার্ন মেলানো
LIKE টেক্সটকে এমন একটা প্যাটার্নের সাথে মেলায়, যাতে দুটো ওয়াইল্ডকার্ড থাকে:
| ওয়াইল্ডকার্ড | যার সাথে মেলে | উদাহরণ | মেলে |
|---|---|---|---|
% | যেকোনো সংখ্যক অক্ষর, একটাও না থাকলেও | '%Learning%' | "Hands-On Machine Learning" |
_ | ঠিক একটা অক্ষর | '____@%' | ৪ অক্ষরের নামওয়ালা ইমেইল |
SELECT name
FROM products
WHERE name LIKE '%-%' -- নামে কোথাও হাইফেন আছে
ORDER BY product_id;+-----------------------------+
| name |
+-----------------------------+
| Hands-On Machine Learning |
| 27-inch Monitor |
| USB-C Hub |
| Noise-Cancelling Headphones |
+-----------------------------+SELECT email
FROM customers
WHERE email LIKE '____@%' -- চারটা অক্ষর, তারপর @
ORDER BY customer_id;+------------------+
| email |
+------------------+
| mitu@example.com |
+------------------+ওয়াইল্ডকার্ড ছাড়া প্যাটার্ন স্রেফ সমতার পরীক্ষা: LIKE 'Dhaka' আচরণ করে = 'Dhaka'-এর মতো (বড়-ছোট হাতের অক্ষরের ব্যাপারটা বাদে, নিচে দেখুন)। সত্যিকারের % বা _ মেলাতে একটা এস্কেপ অক্ষর বেছে নিয়ে সেটা আগে বসান: LIKE '%10!%%' ESCAPE '!' খুঁজে দেয় এমন টেক্সট যাতে "10%" আছে। যেকোনো অক্ষর চলে; ব্যাকস্ল্যাশের চেয়ে ! নিরাপদ, কারণ MySQL স্ট্রিংয়ের ভেতরে ব্যাকস্ল্যাশকে বিশেষভাবে ধরে।
বড় হাতের আর ছোট হাতের অক্ষর
এখানে ডেটাবেসগুলো আলাদা, আর কোয়েরি এক জায়গা থেকে আরেক জায়গায় নিলে এটাই মানুষকে বিপদে ফেলে:
| ডেটাবেস | 'Keyboard' LIKE '%keyboard%' | case-insensitive উপায় |
|---|---|---|
| SQLite | মেলে (LIKE শুধু A–Z-এর বেলায় বড়-ছোট হাতের পার্থক্য উপেক্ষা করে) | এটাই ডিফল্ট |
| MySQL | মেলে (ডিফল্ট কোলেশন বড়-ছোট হাতের পার্থক্য উপেক্ষা করে) | এটাই ডিফল্ট |
| PostgreSQL | মেলে না (LIKE case-sensitive) | ILIKE, অথবা LOWER(col) LIKE … |
-- PostgreSQL
SELECT name FROM products WHERE name LIKE '%keyboard%';
SELECT name FROM products WHERE name ILIKE '%keyboard%';+------+
| name |
+------+
+------+
+---------------------+
| name |
+---------------------+
| Mechanical Keyboard |
+---------------------+প্রথম কোয়েরি কিছুই পায় না; ILIKE (শুধু PostgreSQL-এ আছে) কিবোর্ডটা খুঁজে পায়। সব জায়গায় একই উত্তর দেয় এমন উপায় হলো দুই দিককেই ছোট হাতের অক্ষরে নিয়ে আসা:
SELECT name
FROM products
WHERE LOWER(name) LIKE '%keyboard%'
ORDER BY product_id;+---------------------+
| name |
+---------------------+
| Mechanical Keyboard |
+---------------------+
LIKE '%word%'-কে প্রতিটা সারি পড়তে হয়, কারণ%দিয়ে শুরু হওয়া প্যাটার্নে ইনডেক্স কোনো সাহায্য করতে পারে না। কয়েক হাজার সারিতে এতে সমস্যা নেই। লাখ লাখ ডকুমেন্ট বা প্রম্পটে খুঁজতে হলে ফুল-টেক্সট সার্চ (SQLite FTS5, PostgreSQLtsvector) বা ভেক্টর সার্চ ব্যবহার করুন; দুটোই অ্যাডভান্সড টিউটোরিয়ালে আছে।
NULL: যে মান নেই
NULL মানে "কোনো মান নেই": অজানা, অনুপস্থিত, বা প্রযোজ্য নয়। এটা 0 নয়, খালি স্ট্রিং ''-ও নয়। স্টক 0 (USB-C Hub) মানে "আমাদের কাছে একটাও নেই"; স্টক NULL (কোর্সগুলো) মানে "স্টকের কোনো হিসাবই নেই", যা একটা কোর্সের বেলায় যুক্তিসংগত।
NULL অজানা বলে তার সাথে যেকোনো তুলনার ফলও অজানা, যাকেও NULL লেখা হয়। এমনকি NULL = NULL-ও অজানা: দুটো অজানা শহর একই শহর হতেও পারে, না-ও পারে।
SELECT NULL = NULL AS equal,
NULL <> 'Dhaka' AS not_equal,
NULL + 1 AS plus_one,
NULL IS NULL AS is_null;+-------+-----------+----------+---------+
| equal | not_equal | plus_one | is_null |
+-------+-----------+----------+---------+
| NULL | NULL | NULL | 1 |
+-------+-----------+----------+---------+তাই SQL-এর লজিকে মান তিনটা: সত্য, মিথ্যা আর অজানা। শর্ত সত্য হলেই কেবল WHERE সারি রাখে; মিথ্যা আর অজানা দুটোই বাদ। এ কারণেই city <> 'Dhaka' আর city NOT IN ('Dhaka', 'Chattogram') সাদিয়াকে হারিয়েছে: তাঁর সারির জন্য উত্তরটা ছিল অজানা।
| A | B | A AND B | A OR B | NOT A |
|---|---|---|---|---|
| সত্য | অজানা | অজানা | সত্য | মিথ্যা |
| মিথ্যা | অজানা | মিথ্যা | অজানা | সত্য |
| অজানা | অজানা | অজানা | অজানা | অজানা |
এভাবে পড়লে সহজ হয়: অজানাটা শেষমেশ যা-ই হোক, "অজানা AND মিথ্যা" মিথ্যাই; একই কারণে "অজানা OR সত্য" সত্য। বাকি সব অজানাই থাকে।
IS NULL আর IS NOT NULL
NULL পরীক্ষা করতে কখনো = NULL লিখবেন না (সবসময় অজানা, তাই কিছুর সাথেই মেলে না)। লিখুন IS NULL বা IS NOT NULL:
SELECT name, category_id, stock
FROM products
WHERE category_id IS NULL OR stock IS NULL
ORDER BY product_id;+-------------------------+-------------+-------+
| name | category_id | stock |
+-------------------------+-------------+-------+
| SQL Masterclass | 3 | NULL |
| AI Engineering Bootcamp | 3 | NULL |
| Gift Card | NULL | NULL |
+-------------------------+-------------+-------+আর "কে কে ঢাকায় থাকেন না?" প্রশ্নের পুরো উত্তর, অজানা শহরকে "ঢাকা নয়" ধরে:
SELECT name, city
FROM customers
WHERE city <> 'Dhaka' OR city IS NULL
ORDER BY customer_id;+-----------------+------------+
| name | city |
+-----------------+------------+
| Tanvir Ahmed | Chattogram |
| Rafiq Islam | Sylhet |
| Sadia Chowdhury | NULL |
| Imran Hossain | Khulna |
| Karim Uddin | Rajshahi |
+-----------------+------------+SQLite (3.39+) আর PostgreSQL-এ city IS DISTINCT FROM 'Dhaka'-ও আছে, এমন একটা তুলনা যা NULL-কে সাধারণ মানের মতো ধরে আর কখনো অজানা ফেরত দেয় না। MySQL-এ এর রূপ হলো null-safe সমান <=>:
-- MySQL
SELECT name, city
FROM customers
WHERE NOT (city <=> 'Dhaka')
ORDER BY customer_id;+-----------------+------------+
| name | city |
+-----------------+------------+
| Tanvir Ahmed | Chattogram |
| Rafiq Islam | Sylhet |
| Sadia Chowdhury | NULL |
| Imran Hossain | Khulna |
| Karim Uddin | Rajshahi |
+-----------------+------------+pandas অন্যভাবে করে
একই ডেটা pandas-এ ছাঁকলে অনুপস্থিত city উল্টো আচরণ করে:
import pandas as pd
from sqlhelp import con
df = pd.read_sql("SELECT customer_id, name, city FROM customers ORDER BY customer_id", con)
print(df[df["city"] != "Dhaka"]["name"].tolist())
print(df[df["city"].isna()]["name"].tolist())['Tanvir Ahmed', 'Rafiq Islam', 'Sadia Chowdhury', 'Imran Hossain', 'Karim Uddin']
['Sadia Chowdhury']pandas-এ অনুপস্থিত মান (NaN) "Dhaka"-এর "সমান নয়", তাই সাদিয়া থেকে যান। SQL-এ একই ফিল্টার তাঁকে বাদ দেয়। কোনো ফিল্টার SQL থেকে pandas-এ (বা উল্টো দিকে) নিলে আপনার সারির সংখ্যা বদলে যেতে পারে। অনুপস্থিত মান নিয়ে কী হবে তা ঠিক করুন, আর দুই জায়গাতেই স্পষ্ট করে লিখুন: SQL-এ IS NULL, pandas-এ .isna()।
NOT IN-এর ফাঁদ
কোন ক্যাটাগরিতে কোনো প্রোডাক্ট নেই? স্বাভাবিক প্রথম চেষ্টায় লাগে একটা সাবকোয়েরি, অর্থাৎ বন্ধনীর ভেতরে একটা কোয়েরি, যার ফলাফলই হয়ে যায় লিস্ট (সাবকোয়েরি নিয়ে পরে আলাদা পাতা আছে):
SELECT name
FROM categories
WHERE category_id NOT IN (SELECT category_id FROM products)
ORDER BY category_id;+------+
| name |
+------+
+------+কিছুই আসেনি, অথচ Furniture-এ (ক্যাটাগরি ৫) কোনো প্রোডাক্ট নেই। ভেতরের কোয়েরি ফেরত দেয় 1, 1, 2, 2, 2, 3, 3, 4, 4, 2, NULL: Gift Card-এর কোনো ক্যাটাগরি নেই। Furniture-এর জন্য 5 NOT IN (1, 2, 3, 4, NULL) মানে 5 <> 1 AND … AND 5 <> NULL। শেষ অংশটা অজানা, তাই পুরো শর্তই অজানা, আর সারিটা বাদ। লিস্টে একটা NULL পুরো ফলাফল খালি করে দেয়। লিস্ট থেকে NULL সরিয়ে দিন:
SELECT name
FROM categories
WHERE category_id NOT IN (SELECT category_id FROM products
WHERE category_id IS NOT NULL)
ORDER BY category_id;+-----------+
| name |
+-----------+
| Furniture |
+-----------+IN-এও একই জিনিসের একটা হালকা রূপ আছে: stock IN (0, NULL) USB-C Hub খুঁজে পায়, কিন্তু কোর্সগুলো কখনো নয়, কারণ কিছুই কখনো NULL-এর "সমান" নয়। "যে সারির কোনো মিল নেই" খোঁজার সবচেয়ে নিরাপদ উপায় হলো NOT EXISTS, যা দেখানো হয়েছে সাবকোয়েরি, EXISTS আর কোরিলেটেড কোয়েরি পাতায়।
কোথায় কাজে লাগবে
- ট্রেনিং আর মূল্যায়নের সময়সীমা: ট্রেনিংয়ের জন্য
created_at >= '2026-01-01' AND created_at < '2026-05-01', টেস্টের জন্য তার পরের সময়কাল। - লেবেলের লিস্ট: ট্রেনিংয়ের আগে অপ্রত্যাশিত লেবেলের সারি বাদ দিতে
WHERE label IN ('spam', 'ham')। - বাকি কাজ খুঁজে বের করা:
WHERE embedding IS NULLখুঁজে দেয় কোন ডকুমেন্টের এখনো এমবেডিং লাগবে;WHERE label IS NULLখুঁজে দেয় কোন উদাহরণ অ্যানোটেটরের অপেক্ষায়। - লগে দ্রুত টেক্সট খোঁজা: আরও জটিল কিছু বানানোর আগে
WHERE LOWER(prompt) LIKE '%refund%'রিফান্ড নিয়ে কথোপকথনগুলো খুঁজে দেয়।
সাধারণ ভুল
IS NULL-এর বদলে= NULL।WHERE city = NULLকোনো এরর ছাড়াই কিছুই ফেরত দেয় না। লিখুনWHERE city IS NULL।- এমন লিস্টে NOT IN, যাতে NULL থাকতে পারে। ফলাফল খালি। সাবকোয়েরির ভেতরে
WHERE col IS NOT NULLযোগ করুন, অথবাNOT EXISTSব্যবহার করুন। - টাইমস্ট্যাম্পে BETWEEN।
BETWEEN '2026-03-01' AND '2026-03-31'৩১ তারিখ মধ্যরাতের পরের সবকিছু হারায়। লিখুন>= '2026-03-01' AND < '2026-04-01'। - LIKE-এ বড়-ছোট হাতের অক্ষরের নিয়ম আন্দাজে ধরে নেওয়া। SQLite বা MySQL-এ যে খোঁজ কাজ করে, PostgreSQL-এ সেটা কিছুই পায় না। বড়-ছোট হাতের পার্থক্য যদি গুরুত্বপূর্ণ না হয়, লিখুন
LOWER(col) LIKE '%word%'। - ভুলে যাওয়া যে
_-ও ওয়াইল্ডকার্ড।LIKE 'user_1''userX1'-এর সাথেও মেলে। এস্কেপ করুন:LIKE 'user!_1' ESCAPE '!'।
নিজে চেষ্টা করুন
- সহজ: ৩,০০০ থেকে ৫,০০০ টাকার (দুটোসহ) পেমেন্টগুলো বের করুন, ছোটটা আগে।
- মাঝারি: ফেব্রুয়ারি বা মার্চ ২০২৬-এ দেওয়া (অর্ধ-খোলা রেঞ্জ দিয়ে) যে অর্ডারগুলোর স্ট্যাটাস
deliveredবাcancelled(INদিয়ে), সেগুলো বের করুন। - মাঝারি: যে প্রোডাক্টগুলোর নামে "learning" বা "course" আছে (বড় বা ছোট যে হাতের অক্ষরেই হোক), সেগুলো খুঁজুন, এমনভাবে যাতে তিন ডেটাবেসেই একই উত্তর আসে।
- কঠিন: যে প্রোডাক্টগুলো স্টক-আউট নয়, অর্থাৎ স্টক ঠিক ০ ছাড়া যেকোনো কিছু, সেগুলো বের করুন, আর যেগুলোর স্টকের হিসাব নেই সেগুলোও রাখুন। তারপর ব্যাখ্যা করুন
WHERE stock NOT IN (0)কেন আলাদা উত্তর দেয়।
উত্তর
-- 1
SELECT payment_id, amount
FROM payments
WHERE amount BETWEEN 3000 AND 5000
ORDER BY amount, payment_id;
-- 2
SELECT order_id, order_date, status
FROM orders
WHERE order_date >= '2026-02-01'
AND order_date < '2026-04-01'
AND status IN ('delivered', 'cancelled')
ORDER BY order_date, order_id;
-- 3: কলামে LOWER() দিলে সব ডেটাবেসে একই উত্তর আসে
SELECT name
FROM products
WHERE LOWER(name) LIKE '%learning%'
OR LOWER(name) LIKE '%course%'
ORDER BY product_id;
-- 4
SELECT name, stock
FROM products
WHERE stock <> 0 OR stock IS NULL
ORDER BY product_id;৪-এর জন্য: stock NOT IN (0) আর stock <> 0 একই কথা, যা NULL স্টকের তিনটা প্রোডাক্টের জন্য অজানা, তাই সেগুলো হারিয়ে যায়। ফলে ১০টার বদলে আসে ৭টা সারি।
সারসংক্ষেপ
BETWEEN a AND bদুই প্রান্তই ধরে। সময়ের রেঞ্জে>= start AND < next_startলেখাই ভালো।IN (…)OR-এর লম্বা শিকলের জায়গা নেয়; লিস্টে NULL থাকলেNOT IN (…)কিছুই ফেরত দেয় না।LIKEব্যবহার করে%(যেকোনো সংখ্যক অক্ষর) আর_(একটা অক্ষর)। বড়-ছোট হাতের অক্ষরের নিয়ম আলাদা: SQLite আর MySQL ডিফল্টে পার্থক্য করে না, PostgreSQL করে (ILIKE,LOWER)।NULLমানে অজানা। এর সাথে তুলনাও অজানা, আরWHEREরাখে শুধু সত্য। পরীক্ষা করুনIS NULL/IS NOT NULLদিয়ে।- pandas ফিল্টারে অনুপস্থিত মানকে অন্যভাবে ধরে; দুই জায়গাতেই NULL নিয়ে স্পষ্ট থাকুন।
এরপর: এরর পড়া আর কোয়েরি ডিবাগ করা। এখন আপনি আসল ফিল্টার লিখতে পারেন, তাই আসল ভুলও করবেন। পরের পাতা দেখাবে ডেটাবেস যা বলে তা কীভাবে পড়তে হয়, আর যে বাগের কথা সে বলে না, সেগুলো খোঁজার একটা নিয়মিত পদ্ধতি।