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

SQL ফাংশন — string, number আর date

SQL Functions — Text, Numbers, Dates and Times, Type Conversion, and Why Functions in WHERE Can Hide an Index

Taught in Bangla, this lesson covers scalar versus aggregate functions, string length and case functions and the care needed with Bangla text, cutting, searching, replacing and joining strings, cleaning messy data with trim and lower, numeric functions such as rounding and type conversion, storing and calculating dates and times in SQLite, and why wrapping a column in a function in WHERE can hide an index.

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

  • scalar আর aggregate ফাংশনের পার্থক্য, আর একটির ভেতরে আরেকটি ফাংশন কোন ক্রমে হিসাব হয়
  • লেখার ফাংশন — LENGTH, UPPER/LOWER, TRIM, SUBSTR, INSTR, REPLACE, ||, CONCAT, printf — বাংলা লেখাসহ, আর অগোছালো ডেটা পরিষ্কার করা
  • সংখ্যার ফাংশন — ROUND, ABS, CEIL/FLOOR, ভাগের নিয়ম, random(), আর VAT হিসাবে floating-point-এর ফাঁদ
  • SQLite-এ তারিখ রাখার তিন উপায়, date/strftime/julianday, modifier, বয়সের হিসাব আর টাইমজোন; PostgreSQL ও MySQL-এর সমতুল্য
  • CAST আর typeof() দিয়ে টাইপ বদল, আর WHERE-এ ফাংশন কেন index-এর পথ বন্ধ করে দিতে পারে

১ · ফাংশন কী — scalar বনাম aggregate

পর্ব ১৫-এ SELECT দিয়ে কলাম বেছেছেন, পর্ব ১৬-এ অপারেটর আর CASE দিয়ে নতুন মান বানিয়েছেন। কিন্তু বাস্তবের প্রশ্নগুলো প্রায়ই এমন: “নামগুলো বড় হাতের অক্ষরে দেখাও”, “ফোন নম্বরের শেষ ছয় অঙ্ক লুকাও”, “VAT-সহ দাম দুই দশমিক ঘরে”, “কোন মাসে কয়টি অর্ডার?” এসব কাজের জন্য SQL-এ তৈরি থাকে অনেক ফাংশন — নাম, তারপর বন্ধনীর ভেতরে এক বা একাধিক মান (argument), আর ফেরত আসে একটি মান।

মুদি দোকানের দুটি কাজ ভাবুন। প্রতিটি পণ্যের দামের সাথে ভ্যাট যোগ করা — প্রতিটি লাইনে আলাদা হিসাব, যত লাইন তত ফল। আর দিনশেষে খাতার সব লাইন যোগ করে “আজ মোট বিক্রি” — অনেক লাইন থেকে একটি সংখ্যা। প্রথমটি scalar ফাংশন, দ্বিতীয়টি aggregate ফাংশন.

এই পর্বের সব ফাংশন scalar: প্রতিটি সারির জন্য একবার চলে, সেই সারির মান নিয়ে একটি ফল দেয়, আর সারির সংখ্যা বদলায় না। Aggregate ফাংশন (COUNT, SUM, AVG, MIN, MAX) সারিগুলোকে একটি দলে গুটিয়ে ফেলে — GROUP BY আর HAVING-সহ সেটি পরের পর্ব ১৮-এর বিষয়। নিচের এডিটরে দুই ধরনের পার্থক্য নিজের চোখে দেখুন। এডিটরটি পাতাতেই আসল SQLite চালায়: প্রতিবার “রান” চাপলে নতুন করে দোকানের নমুনা ডেটাবেস (db-shop) লোড হয়, তারপর আপনার কোড চলে। এখানে লাগবে products টেবিল — product_id, name (পণ্যের নাম), category_id, price (টাকা), stock (মজুত)।

-- scalar: প্রতিটি সারির জন্য একটি করে ফল
SELECT name, upper(name) AS big, length(name) AS letters
FROM products
WHERE product_id <= 103;

-- aggregate: অনেক সারি মিলে একটি ফল (পর্ব ১৮)
SELECT COUNT(*) AS products, MAX(price) AS costliest,
       MIN(length(name)) AS shortest_name
FROM products;

প্রথম ফলে তিনটি সারি — যতগুলো সারি WHERE পার হয়েছে, ঠিক ততগুলো: Mobile Phone | MOBILE PHONE | 12, Headphone | HEADPHONE | 9, Power Bank | POWER BANK | 10. দ্বিতীয় ফলে মাত্র একটি সারি: 10 | 15000 | 5 — দশটি পণ্য, সবচেয়ে দামি 15000 টাকা, সবচেয়ে ছোট নাম ৫ অক্ষরের (Saree). খেয়াল করুন MIN(length(name)): ভেতরের scalar ফাংশন প্রতিটি সারিতে চলে দশটি সংখ্যা বানায়, তারপর বাইরের aggregate সেগুলো থেকে একটি বেছে নেয়। ফাংশনের ভেতরে ফাংশন — এটিই এই পর্বের মূল কৌশল।

ফাংশনগুলোকে চার পরিবারে ভাগ করে মনে রাখা সহজ:

লেখা (string)

দৈর্ঘ্য মাপা, কাটা, খোঁজা, বদলানো, জোড়া, ফরম্যাট করা।

length · substr · replace

সংখ্যা (numeric)

রাউন্ড, পরম মান, ওপরে-নিচে পূর্ণসংখ্যা, ভাগশেষ, দৈব সংখ্যা।

round · abs · ceil

তারিখ-সময় (date)

তারিখ বের করা, দিন যোগ-বিয়োগ, মাস বের করা, দুই তারিখের ব্যবধান।

date · strftime · julianday

টাইপ বদল (conversion)

লেখাকে সংখ্যা, সংখ্যাকে লেখা; মানটি আসলে কোন টাইপের, তা দেখা।

CAST · typeof
একটি সাধারণ নিয়ম

প্রায় সব scalar ফাংশনে কোনো argument NULL হলে ফলও NULL — “অজানা মানের দৈর্ঘ্যও অজানা”। length(NULL) = NULL, upper(NULL) = NULL. ব্যতিক্রম হাতে গোনা: যেমন coalesce() আর SQLite/PostgreSQL-এর concat(), যারা NULL উপেক্ষা করে। তাই কোনো কলামে NULL থাকলে আগে ভাবুন, ফলে NULL এলে চলবে কি না।

২ · লেখার দৈর্ঘ্য আর বড়-ছোট হাতের অক্ষর — বাংলা লেখায় সাবধান

String ফাংশন-এর প্রথম প্রশ্নটিই সবচেয়ে গভীর: একটি লেখা কত “লম্বা”? ইংরেজিতে উত্তর সহজ — 'Dhaka' পাঁচ অক্ষর, কম্পিউটারে পাঁচ বাইট। বাংলায় তিনটি আলাদা উত্তর আছে, আর DBMS-গুলো একেকটি বেছে নেয়।

  • বাইট: ডিস্কে কত জায়গা লাগে। আজকের ডেটাবেস সাধারণত লেখা রাখে UTF-8 encoding-এ, যেখানে ইংরেজি অক্ষর ১ বাইট, কিন্তু প্রতিটি বাংলা অক্ষর ৩ বাইট।
  • Code point: Unicode প্রতিটি অক্ষর-চিহ্নকে একটি নম্বর দেয় — ব = U+09AC, া (আ-কার) = U+09BE. এই নম্বরকে বলে code point. SQLite-এর length() এগুলোই গোনে।
  • চোখে দেখা অক্ষর: পাঠক যাকে একটি অক্ষর ভাবেন — যেমন “ক্ষ” — সেটি আসলে ক + ্ (হসন্ত) + ষ, তিনটি code point. চোখে দেখা এই এককের নাম grapheme cluster; প্রায় কোনো DBMS-এর সাধারণ length এটি গোনে না।
SELECT length('বাংলা')       AS chars,
       octet_length('বাংলা') AS bytes,
       length('ক্ষ')          AS kkha,
       unicode('বাংলা')      AS first_code_point;

SELECT upper('dhaka') AS en, upper('ঢাকা') AS bn, upper('café') AS accent;

SELECT 'Dhaka' = 'dhaka' AS same, lower('Dhaka') = lower('dhaka') AS same_lower;

প্রথম ফল: 5 | 15 | 3 | 2476. “বাংলা”-য় পাঁচটি code point — ব, া, ং, ল, া — তাই length 5; প্রতিটি UTF-8-এ ৩ বাইট, তাই octet_length 15 (এই ফাংশন SQLite 3.43 থেকে আছে)। “ক্ষ” চোখে একটি, কিন্তু length 3. unicode() প্রথম অক্ষরের code point দেয়: 2476 = হেক্সাডেসিমালে 09AC, অর্থাৎ “ব”। দ্বিতীয় ফল: DHAKA | ঢাকা | CAFé — বাংলায় বড়-ছোট হাতের অক্ষর নেই, তাই ঢাকা বদলায়নি; কিন্তু é-ও বদলায়নি, কারণ SQLite-এর upper()/lower() ডিফল্টে শুধু ASCII (ইংরেজি a–z) চেনে। তৃতীয় ফল: 0 | 1 — SQLite-এ = বড়-ছোট হাতের অক্ষর আলাদা ধরে, তাই তুলনার আগে দুই দিকেই lower().

একই নাম, ভিন্ন অর্থ: MySQL-এ LENGTH('বাংলা') ফেরত দেয় 15 — বাইট; অক্ষর গুনতে লাগে CHAR_LENGTH(). PostgreSQL-এ length() অক্ষর গোনে (5), বাইটের জন্য octet_length(). তাই “নাম সর্বোচ্চ ৫০ অক্ষর” ধরনের যাচাই এক DBMS থেকে আরেকটিতে নিলে বাংলা নামের বেলায় ফল বদলে যেতে পারে। আর PostgreSQL-এ upper('café') সাধারণত CAFÉ দেয়, কারণ সেখানে ফল ডেটাবেসের locale/collation মেনে চলে। SQLite-এ পূর্ণ Unicode বড়-ছোট হাতের রূপান্তরের জন্য আলাদা ICU extension লাগে।

বাংলা নাম খুঁজতে বা সাজাতে এই পার্থক্যগুলো বাস্তব প্রভাব ফেলে। “রহিম” আর “রহীম” দুটি আলাদা বানান — কোনো ফাংশন এদের এক করবে না; একই শব্দ দুই ভাবে টাইপ করা (যেমন যুক্তাক্ষর ভিন্ন ক্রমে) হলেও লেখা দুটি বাইটে আলাদা হতে পারে। Unicode আর বাংলা টাইপিংয়ের ভেতরের গল্প পাবেন কম্পিউটার ফান্ডামেন্টালস কোর্সের বাংলা টাইপিং আর Unicode পর্বে।

৩ · লেখা কাটা, খোঁজা, বদলানো আর জোড়া

দৈনন্দিন কাজের বেশিরভাগই এই কয়েকটি ফাংশনে হয়। SQLite-এ লেখার অবস্থান গোনা শুরু হয় ১ থেকে, ০ থেকে নয় — প্রোগ্রামিং ভাষার সাথে এখানেই প্রথম গোলমাল হয়।

ফাংশনকাজউদাহরণ → ফল
trim(x), ltrim, rtrimদুই পাশের / বাঁ পাশের / ডান পাশের ফাঁকা জায়গা (বা দেওয়া অক্ষর) ছাঁটাtrim(' Dhaka ') → 'Dhaka'; ltrim('0001234', '0') → '1234'
substr(x, start, n)start থেকে n অক্ষর কেটে নেওয়া; n না দিলে শেষ পর্যন্ত; ঋণাত্মক start মানে শেষ থেকে গোনাsubstr('Mobile Phone', 1, 6) → 'Mobile'; substr('Mobile Phone', -5) → 'Phone'
instr(x, y)x-এর ভেতরে y প্রথম কোথায়; না পেলে 0instr('Mobile Phone', 'Phone') → 8
replace(x, old, new)সব জায়গায় old-কে new দিয়ে বদলানোreplace('01711-000-001', '-', '') → '01711000001'
a || bদুটি লেখা জোড়া (concatenation); কোনো একটি NULL হলে ফল NULL'Order #' || 1001 → 'Order #1001'
concat(…), concat_ws(sep, …)অনেকগুলো জোড়া, NULL বাদ দিয়ে (SQLite 3.44 থেকে)concat_ws('-', 'a', NULL, 'b') → 'a-b'
printf(fmt, …) / formatফরম্যাট-লেখা অনুযায়ী সাজানো: প্রস্থ, দশমিক ঘর, শূন্য দিয়ে ভরাprintf('%08.2f', 3.14159) → '00003.14'; printf('%,d', 1234567) → '1,234,567'

অন্য DBMS-এ নাম একটু আলাদা: substring(x FROM 1 FOR 6) হলো SQL standard-এর রূপ (PostgreSQL, MySQL দুটিতেই চলে; SQLite-এ substring(x, 1, 6) নামটিও চলে), আর instr-এর standard সমতুল্য position('Phone' IN 'Mobile Phone'). বাস্তবে ফাংশন একটির ভেতরে আরেকটি বসিয়ে কাজ হয়। ধরুন পণ্যের নামের প্রথম শব্দটি বড় হাতে চাই। DBMS কীভাবে একটি nested expression হিসাব করে, ভেতর থেকে বাইরে, ধাপে ধাপে দেখুন:

expression আর এই মুহূর্তের মানলাল = এই ধাপে হিসাব হলো

            
হিসাবের অবস্থা
এখনকার মান
টাইপ
দৈর্ঘ্য
  1. আগে পুরো expression পড়ুন। সবচেয়ে বাইরে upper, তার ভেতরে substr, তার ভেতরে trim আর instr. name-এর মানে দুই পাশে দুটি করে ফাঁকা জায়গা — মোট ১৬ অক্ষর।
  2. সবচেয়ে ভেতরের ফাংশন আগে। trim(name) দুই জায়গায় আছে; দুটিই ফাঁকা ছেঁটে 'mobile phone' দেয় — এখন ১২ অক্ষর।
  3. এবার instr. ছাঁটা লেখায় প্রথম ফাঁকা জায়গা কোথায়? গোনা ১ থেকে: m=১ … e=৬, ফাঁকা=৭. তাই 7 — একটি পূর্ণসংখ্যা।
  4. পাটিগণিত। 7 − 1 = 6: প্রথম শব্দে ছয়টি অক্ষর।
  5. substr কাটে। প্রথম ঘর থেকে ছয়টি অক্ষর — 'mobile'.
  6. সবশেষে সবচেয়ে বাইরের ফাংশন। upper → 'MOBILE'. প্রতিটি ধাপের ফল পরের ধাপের argument — ঠিক গণিতের f(g(x)).
  7. প্রান্তিক ক্ষেত্র: এক শব্দের নাম। 'Saree'-তে ফাঁকা জায়গা নেই, instr দেয় 0, তাই substr-এর দৈর্ঘ্য −1 — SQLite ঋণাত্মক দৈর্ঘ্যে শুরুর আগের অক্ষর দেয়, অর্থাৎ কিছুই না। ফল খালি লেখা, ভুল নয় কিন্তু অর্থহীন। সমাধান: CASE WHEN instr(…) = 0 THEN trim(name) ELSE … END.

এবার আসল products টেবিলে। একই সাথে দেখুন পণ্যের নাম থেকে URL-এর অংশ (slug) বানানো আর রসিদের লাইনের মতো সমান প্রস্থে সাজানো:

SELECT name,
       instr(name, ' ')                      AS space_at,
       substr(name, 1, instr(name, ' ') - 1) AS first_word,
       lower(replace(name, ' ', '-'))        AS url_slug,
       printf('%-14s %9.2f', name, price)    AS bill_line
FROM products
WHERE product_id IN (101, 104, 107, 108);

চারটি সারি: Mobile Phone-এর space_at 7, first_word Mobile, slug mobile-phone; DSA Book-এর 4, DSA, dsa-book; Rice 5kg-এর 5, Rice, rice-5kg. আর Saree-র space_at 0, first_word খালি — stepper-এর শেষ ধাপটিই। printf('%-14s %9.2f', …)-এ %-14s মানে “১৪ ঘর জায়গায় লেখা, বাঁয়ে সাঁটা”, আর %9.2f মানে “৯ ঘরে সংখ্যা, দুই দশমিক ঘর, ডানে সাঁটা” — তাই DSA Book 650.00-এর মতো লাইনগুলো সমান কলামে বসে (মনোস্পেস ফন্টে স্পষ্ট দেখা যায়)। খেয়াল রাখুন, printf-এর প্রস্থ code point গোনে, তাই বাংলা নামে কলাম সমান না-ও হতে পারে।

ফোন নম্বর লুকানো — masking

কল সেন্টারের স্ক্রিনে বা রসিদে পুরো ফোন নম্বর দেখানোর দরকার নেই। customers টেবিলে (customer_id, name, city, phone, joined_on) প্রত্যেকের ফোন আছে, শুধু Tanvir Ahmed-এর phone NULL:

SELECT name, phone,
       substr(phone, 1, 5) || '******'                     AS masked,
       substr(phone, 1, 3) || '*****' || substr(phone, -3) AS masked_ends,
       coalesce(substr(phone, 1, 5) || '******', 'নম্বর নেই') AS safe_masked
FROM customers;

আটটি সারি। Rahim Uddin: 01711000001 | 01711****** | 017*****001 | 01711******. বাকিরাও একই ছাঁচে। কিন্তু Tanvir Ahmed-এর সারিতে প্রথম তিনটি কলামই NULL — কারণ NULL || '******' = NULL (অজানার সাথে কিছু জুড়লেও অজানা)। শেষ কলামে coalesce() প্রথম NULL-নয় মানটি নেয়, তাই সেখানে নম্বর নেই. substr(phone, -3)-এর ঋণাত্মক শুরু মানে শেষ তিন অঙ্ক।

Masking নিরাপত্তা নয়, শুধু প্রদর্শন। SELECT-এ নম্বর লুকালেও টেবিলে পুরো নম্বর আছে, আর যিনি কোয়েরি লিখতে পারেন তিনি phone সরাসরি পড়তে পারেন। আসল সুরক্ষা হলো একটি view (পর্ব ২৪) যেখানে শুধু masked কলাম থাকে, আর সেই view-তেই অনুমতি দেওয়া — privilege-এর বিস্তারিত পর্ব ৫১-এ।

জোড়া লাগানো আর শূন্য দিয়ে ভরা — DBMS-ভেদে পার্থক্য

SQL standard-এ জোড়ার অপারেটর ||; SQLite, PostgreSQL আর Oracle এটি মানে। কিন্তু MySQL-এ ডিফল্টে || মানে যৌক্তিক OR, তাই সেখানে CONCAT() লিখতে হয় — আর MySQL-এর CONCAT-এ একটি argument NULL হলেই পুরো ফল NULL, যেখানে PostgreSQL আর SQLite-এর concat NULL বাদ দেয়। অর্ডার নম্বর ৭ থেকে '007' বানানোর জন্য PostgreSQL আর MySQL-এ LPAD আছে, SQLite-এ নেই; সেখানে দুটি বিকল্প: printf('%03d', 7) অথবা substr('000' || 7, -3) (সামনে শূন্য জুড়ে শেষ তিনটি নেওয়া) — দুটিই '007' দেয়।

SELECT lpad('7', 3, '0');                   -- '007'
SELECT format('%s-%s', 'INV', 1001);        -- 'INV-1001'
SELECT position('Phone' IN 'Mobile Phone'); -- 8
SELECT 'a' || NULL, concat('a', NULL, 'b'); -- NULL, 'ab'
SELECT LPAD('7', 3, '0');                       -- '007'
SELECT CONCAT('a', NULL, 'b');                  -- NULL (!)
SELECT CONCAT_WS('-', 'a', NULL, 'b');          -- 'a-b'
SELECT LENGTH('বাংলা'), CHAR_LENGTH('বাংলা');   -- 15, 5 (utf8mb4)

৪ · অগোছালো ডেটা পরিষ্কার — trim আর lower দিয়ে শুরু

অনলাইন ফর্মে শহরের নাম যে যেভাবে খুশি লেখেন: “ dhaka”, “DHAKA ”, “Dhaka”, “Ctg”, “chattogram”. মানুষের চোখে সব ঠিক আছে, কিন্তু DBMS-এর কাছে ' dhaka' আর 'Dhaka' দুটি আলাদা লেখা — GROUP BY করলে দুটি আলাদা দল, WHERE city = 'Dhaka' লিখলে একটি বাদ। এমন ডেটাকে নির্ভরযোগ্য করার কাজকে বলে data cleaning. নিচের সাতজন নতুন গ্রাহকের signup টেবিলে (id, name, city) ধাপে ধাপে পরিষ্কার করা দেখুন:

signup.city-র সাতটি মান[ ] = লেখার সীমানা

            
অবস্থা
প্রয়োগ করা ফাংশন
আলাদা দল (NULL-সহ)
  1. শুরু: সাতটি সারি, সাতটি আলাদা দল। GROUP BY city চালালে প্রতিটি সারি নিজের দলে — অথচ আসলে মাত্র দুটি শহর আর একটি অজানা।
  2. trim — দুই পাশের ফাঁকা জায়গা গেল। ১, ২ আর ৫ নম্বর বদলেছে। কিন্তু dhaka, DHAKA আর Dhaka এখনো তিনটি আলাদা লেখা, তাই দল এখনো ৭।
  3. lower — সব ছোট হাতে। এবার তিনটি dhaka এক হলো, দুটি chattogram এক হলো। দল নেমে এল ৪-এ: dhaka, ctg, chattogram আর NULL. খেয়াল করুন NULL পুরো সময় NULL-ই থাকছে — ফাংশন অজানাকে জানা বানায় না।
  4. বানান-বৈচিত্র্যের মানচিত্র। ctg আর chattogram একই শহর — এটি কোনো ফাংশন জানে না, মানুষকে বলে দিতে হয়: CASE lower(trim(city)) WHEN 'ctg' THEN 'chattogram' ELSE lower(trim(city)) END. এখানে replace() নয়, কারণ replace লেখার ভেতরের 'ctg'-ও বদলাবে (যেমন 'ctg road'). দল এখন ৩।
  5. দেখানোর রূপ। প্রথম অক্ষর upper(substr(city, 1, 1)), বাকিটা substr(city, 2) — দুটি জুড়ে Dhaka, Chattogram. দলের সংখ্যা বদলায়নি, শুধু চেহারা।
  6. যাচাইয়ের পর স্থায়ী করুন। SELECT-এ ফল ঠিক দেখে তবেই UPDATE. এখন GROUP BY দেয় Dhaka ৩, Chattogram ৩, NULL ১. আর ভবিষ্যতে যাতে আবার না হয়, অ্যাপে শহরের তালিকা থেকে বাছাই বা একটি CHECK — পরিষ্কার করার চেয়ে ময়লা ঢুকতে না দেওয়া সস্তা।
-- 1) আগে: একই শহর কত রূপে?
SELECT city, COUNT(*) AS n FROM signup GROUP BY city;

-- 2) পরিষ্কার করে দেখি, এখনো কিছু বদলাইনি
SELECT id, '[' || city || ']' AS raw,
       CASE lower(trim(city)) WHEN 'ctg' THEN 'chattogram'
            ELSE lower(trim(city)) END AS clean
FROM signup;

-- 3) নিশ্চিত হলে স্থায়ীভাবে বদলান, তারপর আবার গুনুন
UPDATE signup SET city = CASE lower(trim(city)) WHEN 'ctg' THEN 'chattogram'
                         ELSE lower(trim(city)) END;
UPDATE signup SET city = upper(substr(city, 1, 1)) || substr(city, 2);
SELECT city, COUNT(*) AS n FROM signup GROUP BY city ORDER BY n DESC, city;

প্রথম ফলে সাতটি দল, প্রতিটিতে ১ — NULL, dhaka, Chattogram, Ctg, DHAKA , Dhaka, chattogram (ক্রমটি বাইটের মান অনুযায়ী: ফাঁকা জায়গা আগে, বড় হাতের অক্ষর ছোট হাতের আগে)। দ্বিতীয় ফলে raw কলামে বন্ধনী ফাঁকা জায়গাগুলো দেখিয়ে দেয়, আর clean কলামে dhaka, dhaka, dhaka, chattogram, chattogram, chattogram — ৭ নম্বরে raw আর clean দুটিই NULL, কারণ '[' || NULL নিজেও NULL. দুটি UPDATE প্রতিটিতে ৭টি সারি বদলায় (NULL সারিও “বদলায়”, NULL থেকে NULL-এ)। শেষ ফল: Chattogram | 3, Dhaka | 3, NULL | 1 — সমান সংখ্যা বলে নাম অনুযায়ী সাজানো।

trim() সব “ফাঁকা” চেনে না। SQLite-এর trim ডিফল্টে শুধু সাধারণ space অক্ষর (U+0020) ছাঁটে। Excel বা ওয়েবপেজ থেকে কপি করা ডেটায় প্রায়ই থাকে tab (char(9)), নতুন লাইন, বা non-breaking space (char(160)) — এগুলো থেকে যায়। তখন দ্বিতীয় argument দিন: trim(city, ' ' || char(9) || char(160)). সন্দেহ হলে length(city) আর hex(city) দিয়ে দেখুন লেখার ভেতরে আসলে কী আছে।

৫ · সংখ্যার ফাংশন — রাউন্ড, ভাগ আর টাইপ বদল

ROUND আর VAT-এর হিসাব

round(x, n) x-কে n দশমিক ঘরে রাউন্ড করে; n না দিলে পূর্ণসংখ্যায়। দোকানের দামের সাথে ১৫% VAT যোগ করে দেখা যাক। ১৫% এখানে শুধু উদাহরণের হার — বাস্তবে VAT-এর হার পণ্য, সেবা আর সময়ের আইন অনুযায়ী আলাদা হতে পারে, তাই আসল সিস্টেমে হারটি একটি টেবিলে রাখুন, কোয়েরিতে লিখে দেবেন না।

-- 15% একটি উদাহরণ হার
SELECT name, price,
       price * 1.15           AS raw_total,
       round(price * 1.15, 2) AS with_vat,
       round(price * 1.15)    AS whole_taka
FROM products
WHERE product_id IN (101, 104, 105, 109);

SELECT 650 * 1.15 = 747.5 AS exact_equal,
       round(2.5) AS r1, round(-2.5) AS r2, round(2.675, 2) AS r3;

Mobile Phone 17250, Mustard Oil 299 — সহজ। কিন্তু DSA Book-এর raw_total 747.4999999999999, আর SQL Book-এর 632.5. ফলে whole_taka-য় DSA Book 747 আর SQL Book 633 — দুটিরই আসল মান ঠিক .৫-এ শেষ, অথচ একটি নিচে, একটি ওপরে গেল! কারণ floating-point: 1.15 binary-তে হুবহু রাখা যায় না, তাই 650 × 1.15 সামান্য কম হয়ে যায়। দ্বিতীয় ফল সেটিই প্রমাণ করে: exact_equal = 0. তারপর 2.5 → 3 আর -2.5 → -3 — SQLite .৫-কে শূন্য থেকে দূরে নেয়; আর round(2.675, 2) = 2.67, কারণ 2.675-ও binary-তে আসলে 2.67499…

সমাধান পর্ব ১১-এর নিয়মটিই: টাকা রাখুন পয়সায়, পূর্ণসংখ্যায়। 650 টাকা = 65000 পয়সা; VAT-সহ = (65000 × 115 + 50) / 100 = 74750 পয়সা — পূর্ণসংখ্যার যোগ-গুণ-ভাগ সবসময় নিখুঁত, আর “+ 50 তারপর পূর্ণসংখ্যার ভাগ” স্পষ্টভাবে “অর্ধেক হলে ওপরে” রাউন্ড করে। কোন রাউন্ডিং নিয়ম মানবেন, তা ব্যবসা বা কর-কর্তৃপক্ষের নিয়ম ঠিক করে — DBMS-এর ডিফল্টের ওপর ছেড়ে দেবেন না, কারণ ডিফল্টগুলো আলাদা:

সিস্টেমround(2.5)নিয়ম
SQLite3শূন্য থেকে দূরে (half away from zero); কিন্তু ইনপুট নিজেই floating-point
PostgreSQL numeric3শূন্য থেকে দূরে
PostgreSQL double precisionসাধারণত 2প্ল্যাটফর্মনির্ভর; বেশিরভাগ ক্ষেত্রে জোড় সংখ্যার দিকে (banker’s rounding)
MySQL — DECIMAL / FLOAT3 / প্রায়ই 2হুবহু দশমিকে শূন্য থেকে দূরে; approximate (FLOAT/DOUBLE) মানে C লাইব্রেরি-নির্ভর, প্রায়ই জোড়ের দিকে
Python 3 round()2জোড়ের দিকে — অ্যাপ আর ডেটাবেস ভিন্ন ফল দিতে পারে

ABS, CEIL/FLOOR আর ভাগের নিয়ম

abs(x) পরম মান দেয় (abs(-15) = 15) — যেমন হিসাবের অমিল কত টাকা, চিহ্ন বাদে। ceil(x) ওপরের পূর্ণসংখ্যা, floor(x) নিচের: ceil(2.1) = 3, floor(−2.1) = −3. এর চেয়েও বেশি ভুল হয় ভাগে: SQLite, PostgreSQL আর SQL Server-এ দুটি পূর্ণসংখ্যার ভাগ পূর্ণসংখ্যা — ভগ্নাংশ ফেলে দেওয়া হয়, একে বলে integer division. MySQL আর Oracle-এ 7 / 2 = 3.5. গুদামে একটি বাক্সে ১২টি পণ্য ধরে — কয়টি বাক্স লাগবে?

SELECT 7 / 2 AS int_div, 7 / 2.0 AS real_div, 7 % 2 AS remainder,
       typeof(7 / 2) AS t1, typeof(7 / 2.0) AS t2, 7 / 0 AS div_zero;

SELECT name, stock,
       stock / 12        AS full_boxes,
       stock % 12        AS loose,
       ceil(stock / 12.0) AS boxes_needed
FROM products WHERE product_id IN (101, 102, 108);

SELECT CAST('15000' AS INTEGER) + 1 AS a, CAST('12abc' AS INTEGER) AS b,
       CAST(3.99 AS INTEGER) AS c, '10' + 5 AS d, typeof('10' + 5) AS e;

SELECT abs(random()) % 6 + 1 AS dice;

প্রথম ফল: 3 | 3.5 | 1 | integer | real | NULL. একটি দিক দশমিক হলেই (2.0) ফল real. শূন্য দিয়ে ভাগে SQLite ত্রুটি না দিয়ে NULL দেয়; PostgreSQL দেয় “division by zero” ত্রুটি। দ্বিতীয় ফল: Mobile Phone 12টি → ১টি পূর্ণ বাক্স, ০টি বাকি, ১টি বাক্স লাগবে; Headphone 40 → 3, 4, 4; Rice 5kg 100 → 8, 4, 9. ceil(stock / 12) লিখলে ভাগটি আগেই পূর্ণসংখ্যা হয়ে যেত, ceil কিছুই করত না — তাই 12.0. তৃতীয় ফল: 15001 | 12 | 3 | 15 | integer — CAST লেখার শুরুর সংখ্যাটুকু নেয় ('12abc' → 12), দশমিক কেটে ফেলে (৩.৯৯ → ৩, রাউন্ড নয়), আর '10' + 5-এ SQLite নিজেই লেখাকে সংখ্যা বানায়। শেষ সারিতে ১ থেকে ৬-এর একটি দৈব সংখ্যা — প্রতিবার রান করলে বদলাবে।

ceil/floor সব SQLite-এ নেই — আগে পরীক্ষা করুন। SQLite 3.35 (২০২১) থেকে ceil, floor, pow, ln, mod, trunc-এর মতো math function যোগ হয়েছে, কিন্তু শুধু তখনই, যখন SQLite কম্পাইলের সময় সেগুলো চালু করা হয়। এই পাতার sql.js build-এ পরীক্ষা করে দেখা গেছে: ceil, floor, power, sqrt চলে, কিন্তু pow, ln, mod, trunc নেই (“no such function”) — কারণ এই build-এ SQLite-এর নিজস্ব math function চালু নেই, এগুলো আসে sql.js-এর সাথে জোড়া একটি পুরোনো extension (extension-functions.c) থেকে; আর সেখানে log(100) দেয় 4.605 (natural log), যেখানে SQLite-এর নিজস্ব log() base-10. অন্য কোথাও চালাবেন এমন কোয়েরিতে নিরাপদ বিকল্প: ধনাত্মক x-এর জন্য floor = CAST(x AS INTEGER), আর ceil = CAST(x AS INTEGER) + (x > CAST(x AS INTEGER)).

random() একটি ৬৪-বিট দৈব পূর্ণসংখ্যা দেয় (ধনাত্মক বা ঋণাত্মক), তাই abs(random()) % 6 + 1 দিয়ে ১–৬. লটারির ড্র বা পাসওয়ার্ডের মতো নিরাপত্তার কাজে ডেটাবেসের random() নয়, অ্যাপের cryptographic generator ব্যবহার করুন। আর ORDER BY random() LIMIT 1 দিয়ে “একটি দৈব পণ্য” সহজ, কিন্তু বড় টেবিলে প্রতিটি সারিতে দৈব সংখ্যা বানিয়ে সব সাজাতে হয় — লাখো সারিতে ধীর।

typeof() SQLite-এর নিজস্ব ফাংশন: একটি মান আসলে কোন storage class-এ আছে — null, integer, real, text, blob. SQLite-এ একটি কলামে বিভিন্ন সারিতে বিভিন্ন টাইপ থাকতে পারে (type affinity, পর্ব ১৩), তাই সন্দেহজনক ডেটায় SELECT typeof(price), COUNT(*) FROM products GROUP BY 1 চালিয়ে দেখা ভালো অভ্যাস। PostgreSQL বা MySQL-এ কলামের টাইপ কঠোর, সেখানে typeof() নেই (PostgreSQL-এ কাছাকাছি কাজ করে pg_typeof())। CAST-এর আচরণও আলাদা: PostgreSQL-এ CAST('12abc' AS INTEGER) ত্রুটি দেয়, চুপচাপ 12 বানায় না।

৬ · SQLite-এ তারিখ আর সময় — রাখা, পড়া আর যোগ-বিয়োগ

SQLite-এ আলাদা DATE টাইপ নেই। তারিখ রাখার তিনটি স্বীকৃত উপায়, আর SQLite-এর সব date ফাংশন তিনটিই বোঝে:

ISO-8601 লেখা (TEXT)

'2025-09-20', '2025-09-20 14:30:00'. পড়া যায়, আর লেখা হিসেবে সাজালেই সময়ের ক্রম ঠিক থাকে। db-shop এটিই ব্যবহার করে।

iso-8601

Unix epoch (INTEGER)

১৯৭০-০১-০১ 00:00:00 UTC থেকে কত সেকেন্ড: 1756684800 = 2025-09-01. ছোট, দ্রুত, কিন্তু চোখে পড়া যায় না।

unixepoch

Julian day (REAL)

খ্রিস্টপূর্ব ৪৭১৪ সালের ২৪ নভেম্বর গ্রিনিচের দুপুর থেকে কত দিন: 2460919.5 = 2025-09-01. দিনের ব্যবধানের হিসাবে সুবিধা।

julianday

ISO-8601 ফরম্যাটে বছর আগে, তারপর মাস, তারপর দিন, সবসময় দুই অঙ্কে (০৯, ০১)। এর ফলে সাধারণ লেখার তুলনাই সময়ের তুলনা: '2025-10-09' > '2025-09-30' সত্য। কিন্তু বাংলাদেশে প্রচলিত '09/10/2025' রাখলে '9/10/2025' > '10/9/2025'-ও “সত্য” — প্রথম অক্ষর '9' বড় বলে, তারিখ হিসেবে নয়। SQLite-এর date ফাংশন '01/09/2025' বা শূন্য ছাড়া '2025-9-1' বোঝেই না, NULL দেয়। মূল ফাংশনগুলো:

  • date(x) → 'YYYY-MM-DD', time(x) → 'HH:MM:SS', datetime(x) → 'YYYY-MM-DD HH:MM:SS'; x হতে পারে 'now' — এখনকার UTC সময়।
  • strftime(format, x) → যেকোনো রূপে: %Y বছর, %m মাস, %d দিন, %H:%M ঘণ্টা-মিনিট, %w সপ্তাহের দিন (০ = রবিবার), %j বছরের কততম দিন, %s Unix সেকেন্ড।
  • julianday(x) → Julian day সংখ্যা; unixepoch(x) → Unix epoch সেকেন্ড (SQLite 3.38 থেকে)।
  • যেকোনোটির পরে এক বা একাধিক modifier: '+7 days', '-1 month', '+6 hours', 'start of month', 'start of year', 'weekday 5' (পরের শুক্রবার; দিনটি নিজেই শুক্রবার হলে সেদিনই) — বাঁ থেকে ডানে একটির পর একটি প্রয়োগ হয়।

Modifier-এর ক্রম আর মাসের দৈর্ঘ্যই সবচেয়ে বেশি অবাক করে। ধরুন ৩১ জানুয়ারি একটি কিস্তি, পরের কিস্তি “এক মাস পরে”:

SQLite-এর হিসাবলাল = এই ধাপের ফল

            
মান
বর্তমান ফল
julianday
ঠিক কি?
  1. শুরুর তারিখ। d = '2025-01-31', একটি সাধারণ TEXT. ভেতরে SQLite প্রথমেই এটিকে Julian day-তে বদলায়: 2460706.5. শেষে .৫ কেন? Julian day গোনা শুরু হয় দুপুর ১২টায়, তাই মধ্যরাত মানে অর্ধেক দিন।
  2. '+1 month' মাসের ঘরে ১ যোগ করে। দিনের ঘর ৩১ যেমন ছিল তেমন থাকে, ফলে মাঝখানে একটি অস্তিত্বহীন তারিখ: ৩১ ফেব্রুয়ারি।
  3. SQLite উপচে পড়া দিন পরের মাসে ঠেলে দেয়। ২০২৫-এর ফেব্রুয়ারিতে ২৮ দিন, তাই বাড়তি ৩ দিন মার্চে: '2025-03-03'. ফেব্রুয়ারির কিস্তি পুরো বাদ পড়ল! (২০২৪ অধিবর্ষে একই হিসাব দিত 2024-03-02.) PostgreSQL আর MySQL এখানে মাসের শেষ দিনে থামে (2025-02-28) — DBMS-ভেদে আলাদা।
  4. নিরাপদ পথ: আগে মাসের শুরুতে যান। 'start of month' দিনের ঘরকে ১ বানায়: '2025-01-01'.
  5. তারপর মাস যোগ। ১ তারিখ সব মাসে আছে, তাই '+2 months' নিরাপদে '2025-03-01'-এ নেয় — কোনো উপচানো নেই।
  6. এক দিন পিছিয়ে। '-1 day' দেয় '2025-02-28' — ফেব্রুয়ারির শেষ দিন, অধিবর্ষে আপনাআপনি ২৯। “পরের মাসের শেষ দিন” বের করার প্রচলিত কৌশল এটিই: start of month → +2 months → −1 day.
  7. দুই তারিখের ব্যবধান। দুটি julianday বিয়োগ করলে দিনের সংখ্যা, দশমিকসহ (REAL): 28.0. পূর্ণ দিন চাইলে CAST(… AS INTEGER).

এবার অর্ডারের তারিখ থেকে মাস বের করে মাসভিত্তিক অর্ডার গোনা। orders টেবিলে আছে order_id, customer_id, order_date (ISO তারিখ) আর status. GROUP BY-এর পুরো নিয়ম পর্ব ১৮-এ; এখানে শুধু এটুকু জানলেই চলবে যে একই month-এর সারিগুলো এক দলে গোনা হয়:

SELECT order_id, order_date,
       strftime('%Y-%m', order_date)    AS month,
       strftime('%d/%m/%Y', order_date) AS bd_style,
       strftime('%w', order_date)       AS weekday   -- 0 = রবিবার
FROM orders ORDER BY order_id LIMIT 4;

-- মাসভিত্তিক অর্ডার (GROUP BY-এর পূর্বরূপ, পর্ব ১৮)
SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders
FROM orders GROUP BY month ORDER BY month;

-- ঢাকায় ১ অক্টোবর রাত ২:৩০ = UTC-তে ৩০ সেপ্টেম্বর 20:30
SELECT strftime('%Y-%m', '2025-09-30 20:30:00')             AS utc_month,
       strftime('%Y-%m', '2025-09-30 20:30:00', '+6 hours') AS dhaka_month;

প্রথম ফল: 1001 → 2025-09 | 01/09/2025 | 1 (সোমবার), 1002 → 03/09/2025 | 3 (বুধবার), 1003 → 10/09/2025 | 3, 1004 → 12/09/2025 | 5 (শুক্রবার)। দেখানোর জন্য বাংলাদেশি রূপ %d/%m/%Y ঠিক আছে — কিন্তু রাখবেন ISO-তে। দ্বিতীয় ফল: 2025-09 | 6 আর 2025-10 | 4. তৃতীয় ফল সবচেয়ে গুরুত্বপূর্ণ: 2025-09 | 2025-10 — একই মুহূর্ত, UTC ধরে গুনলে সেপ্টেম্বরের বিক্রি, ঢাকার সময় ধরে অক্টোবরের। মাসিক রিপোর্টে কোন সময় ধরে গুনছেন, তা না ঠিক করলে মাসের শেষ রাতের অর্ডার ভুল মাসে চলে যায়।

৭ · দুই তারিখের ব্যবধান, বয়সের ফাঁদ আর টাইমজোন

“গ্রাহক কত দিন ধরে আমাদের সাথে?”, “কোন pending অর্ডার কবে পৌঁছানোর কথা?” — দুটিই তারিখের বিয়োগ আর যোগ। 'now' লিখলে প্রতিদিন ফল বদলাবে, তাই এডিটরে একটি নির্দিষ্ট “আজ” — 2025-10-25 — ধরা হয়েছে, যাতে সবার ফল এক হয়:

SELECT name, joined_on,
       julianday('2025-10-25') - julianday(joined_on)                  AS days_raw,
       CAST(julianday('2025-10-25') - julianday(joined_on) AS INTEGER) AS days
FROM customers ORDER BY days DESC LIMIT 4;

SELECT order_id, order_date, date(order_date, '+7 days') AS due_by
FROM orders WHERE status = 'pending';

-- বয়স: 365.25 দিয়ে ভাগ বনাম ক্যালেন্ডারের নিয়ম (জন্ম 2021-01-01, আজ 2024-01-01)
SELECT CAST((julianday('2024-01-01') - julianday('2021-01-01')) / 365.25 AS INTEGER) AS naive,
       (strftime('%Y', '2024-01-01') - strftime('%Y', '2021-01-01'))
       - (strftime('%m-%d', '2024-01-01') < strftime('%m-%d', '2021-01-01')) AS correct;

প্রথম ফল: Rahim Uddin 283 দিন, Karima Begum 264, Sabbir Hasan 218, Nusrat Jahan 198. days_raw-ও এখানে পূর্ণ দেখায়, কারণ দুটি তারিখই মধ্যরাতের; সময়সহ মানে ভগ্নাংশ আসত, তাই CAST. দ্বিতীয় ফল: 1006 → 2025-09-27, 1009 → 2025-10-15. তৃতীয় ফল: 2 | 3 — ঠিক তিন বছর পূর্ণ হওয়ার দিনেও “৩৬৫.২৫ দিয়ে ভাগ” বলে ২! মাঝের তিন বছরে অধিবর্ষ একটিও পড়েনি (২০২৪-এর ২৯ ফেব্রুয়ারি তখনো আসেনি), তাই দিন ১০৯৫, আর 1095 / 365.25 = 2.998.

বয়স হিসাবের সঠিক নিয়ম

বয়স মানে দিন নয়, ক্যালেন্ডারের জন্মদিন পার হয়েছে কি না। নিয়মটি দুই লাইনে: আজকের বছর − জন্মের বছর; তারপর এ বছরের জন্মদিন (%m-%d) এখনো না এলে ১ বিয়োগ। উপরের correct কলামটি ঠিক এটিই করে — তুলনার ফল সত্য হলে 1, মিথ্যা হলে 0. শুধু একটি ক্ষেত্র নিয়মের বাইরে: ২৯ ফেব্রুয়ারির জন্ম। সাধারণ বছরে তাঁর জন্মদিন ২৮ ফেব্রুয়ারি না ১ মার্চ, তা আইন আর প্রতিষ্ঠানভেদে আলাদা হতে পারে — চাকরির বয়সসীমার মতো গুরুত্বপূর্ণ হিসাবে নিয়মটি লিখিতভাবে নিশ্চিত করুন। আর বয়স কখনো কলামে রাখবেন না; জন্মতারিখ রাখুন, বয়স হিসাব করুন — বয়স প্রতিদিন বদলায়, জন্মতারিখ বদলায় না।

টাইমজোন — রাখুন UTC-তে, দেখান ঢাকার সময়ে

বাংলাদেশের সময় UTC + ৬ ঘণ্টা, লেখা হয় +06:00; আন্তর্জাতিক টাইমজোন ডেটাবেসে এর নাম Asia/Dhaka. SQLite-এর 'now' সবসময় UTC দেয়। তাই নিয়ম: একটি ঘটনার মুহূর্ত (অর্ডার দেওয়ার সময়, পেমেন্টের সময়) রাখুন UTC-তে — যেমন '2025-09-30T20:30:00Z' (Z মানে UTC) — আর দেখানোর সময় টাইমজোন অনুযায়ী বদলান: datetime(t, '+6 hours'). ঢাকার ১ অক্টোবর রাত ১২টা আসলে UTC-তে ৩০ সেপ্টেম্বর সন্ধ্যা ৬টা — তাই “১ অক্টোবরের বিক্রি” মানে UTC-তে '2025-09-30 18:00:00' থেকে '2025-10-01 18:00:00'-এর আগ পর্যন্ত। SQLite-এর 'localtime' modifier-ও আছে, কিন্তু সেটি যে কম্পিউটারে কোয়েরি চলছে তার ঘড়ি মানে — সার্ভার বিদেশে হলে ফল বদলে যায়, আর এই পাতায় পাঠকের নিজের ব্রাউজারের টাইমজোন। তাই এডিটরে আমরা স্পষ্ট '+6 hours' লিখেছি।

যা মুহূর্ত নয়, তাকে টাইমজোনে বদলাবেন না। জন্মতারিখ, joined_on, পরীক্ষার তারিখ — এগুলো ক্যালেন্ডারের দিন, কোনো ঘড়ির মুহূর্ত নয়। ৬ ঘণ্টা যোগ-বিয়োগ করলে এগুলো এক দিন সরে যেতে পারে। আর SQLite-এর তারিখ ফাংশন অবৈধ দিনও চুপচাপ “ঠিক” করে নেয়: date('2025-02-30') দেয় 2025-03-02, ত্রুটি নয় — ইনপুট যাচাই অ্যাপে বা CHECK-এ করুন।

৮ · PostgreSQL আর MySQL-এ একই কাজ — তুলনা

তারিখ-সময়ের ফাংশনে DBMS-গুলোর পার্থক্য সবচেয়ে বেশি, কারণ SQL standard এখানে দেরিতে এসেছে আর প্রতিটি পণ্য আগেই নিজের পথ বানিয়ে ফেলেছিল। PostgreSQL আর MySQL-এ আসল DATE, TIMESTAMP টাইপ আছে, আর আছে INTERVAL — “৭ দিন” বা “১ মাস”-এর মতো সময়ের দৈর্ঘ্য, যা একটি মান হিসেবে যোগ-বিয়োগ করা যায়। নিচের ব্লকগুলো এই পাতায় চলে না (পাতার ইঞ্জিন SQLite); নিজের PostgreSQL বা MySQL-এ চালিয়ে দেখতে পারেন:

SELECT now(), current_date;              -- timestamptz, date
SELECT date '2025-09-01' + interval '7 days';     -- 2025-09-08 00:00:00
SELECT date '2025-10-09' - date '2025-09-01';     -- 38 (পূর্ণসংখ্যা দিন)
SELECT date_trunc('month', timestamp '2025-09-20 14:30');  -- 2025-09-01 00:00:00
SELECT extract(year FROM date '2025-09-20');      -- 2025
SELECT to_char(date '2025-09-20', 'YYYY-MM');     -- '2025-09'
SELECT timestamptz '2025-09-30 20:30:00+00' AT TIME ZONE 'Asia/Dhaka';
                                                  -- 2025-10-01 02:30:00
SELECT 7 / 2, 7 / 2.0;                            -- 3, 3.5000000000000000
SELECT NOW(), CURDATE();
SELECT DATE_ADD('2025-09-01', INTERVAL 7 DAY);    -- 2025-09-08
SELECT DATEDIFF('2025-10-09', '2025-09-01');      -- 38
SELECT DATE_FORMAT('2025-09-20', '%Y-%m');        -- '2025-09'
SELECT EXTRACT(YEAR FROM '2025-09-20');           -- 2025
SELECT LAST_DAY('2025-02-10');                    -- 2025-02-28
SELECT TIMESTAMPDIFF(YEAR, '2000-10-10', '2025-10-09');  -- 24 (বয়স)
SELECT 7 / 2, 7 DIV 2;                            -- 3.5000, 3
কাজSQLitePostgreSQLMySQL
অক্ষর গোনাlength(x)length(x)CHAR_LENGTH(x)
বাইট গোনাoctet_length(x)octet_length(x)LENGTH(x)
জোড়াa || b, concata || b, concatCONCAT(a, b) (NULL হলে NULL)
বাঁয়ে শূন্য ভরাprintf('%03d', x)lpad(x, 3, '0')LPAD(x, 3, '0')
আজকের তারিখdate('now') (UTC)current_dateCURDATE()
এখনকার মুহূর্তdatetime('now')now()NOW()
৭ দিন যোগdate(d, '+7 days')d + interval '7 days'DATE_ADD(d, INTERVAL 7 DAY)
দিনের ব্যবধানjulianday(a) - julianday(b)a - b (date)DATEDIFF(a, b)
মাসের শুরুdate(d, 'start of month')date_trunc('month', d)DATE_FORMAT(d, '%Y-%m-01')
বছর বের করাstrftime('%Y', d) (TEXT)extract(year FROM d)YEAR(d) / EXTRACT
7 / 2333.5000

দুটি সূক্ষ্ম পার্থক্য মনে রাখুন। PostgreSQL-এর now() transaction শুরুর মুহূর্ত দেয় — একই transaction-এর সব সারিতে একই সময়, যা সাধারণত সুবিধাজনক; প্রকৃত ঘড়ির জন্য clock_timestamp(). আর SQL Server-এ একই কাজের নাম আরও আলাদা — GETDATE(), DATEADD(day, 7, d), DATEDIFF(day, b, a), LEN(x) (শেষের ফাঁকা জায়গা বাদ দিয়ে গোনে)। উপভাষাগুলোর পুরো তুলনা পর্ব ৩১-এ।

৯ · WHERE-এ ফাংশন — index কেন লুকিয়ে যায়

“সেপ্টেম্বর ২০২৫-এর অর্ডার” — দুইভাবে লেখা যায়, দুটিই একই ৬টি সারি দেয়। কিন্তু ১ কোটি সারির টেবিলে একটি মিলিসেকেন্ডে শেষ হতে পারে, অন্যটি পুরো টেবিল পড়ে। কারণ index. order_date-এর ওপর index মানে সব order_date সাজানো একটি তালিকা (B+ tree, পর্ব ৪০) — অনেকটা অভিধানের মতো, যেখানে শব্দ বর্ণানুক্রমে সাজানো বলে “ক” দিয়ে শুরু শব্দগুলো সরাসরি খুঁজে পাওয়া যায়। কিন্তু প্রশ্ন যদি হয় “যে শব্দগুলোর শেষ অক্ষর ক”, অভিধানের ক্রম কোনো কাজে আসে না — পুরো বই পড়তে হয়।

WHERE-এ কলামকে ফাংশনে মুড়লে ঠিক এটিই ঘটে: index সাজানো আছে order_date অনুযায়ী, strftime('%Y-%m', order_date) অনুযায়ী নয়। DBMS-কে প্রতিটি সারিতে ফাংশন চালিয়ে তুলনা করতে হয় — full table scan. যে শর্ত index দিয়ে সরাসরি খোঁজা যায়, তাকে বলে sargable. SQLite-এর EXPLAIN QUERY PLAN দিয়ে নিজেই দেখুন DBMS কোন পথ বেছে নেয়:

CREATE INDEX idx_orders_date ON orders(order_date);

-- কলাম ফাংশনের ভেতরে: index কাজে লাগে না
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE strftime('%Y-%m', order_date) = '2025-09';

-- একই প্রশ্ন, কলাম খালি রেখে পরিসর দিয়ে
EXPLAIN QUERY PLAN
SELECT * FROM orders
WHERE order_date >= '2025-09-01' AND order_date < '2025-10-01';

SELECT COUNT(*) AS sept_orders FROM orders
WHERE order_date >= '2025-09-01' AND order_date < '2025-10-01';

প্রথম plan-এর detail: SCAN orders — প্রতিটি সারি পড়া। দ্বিতীয়টির: SEARCH orders USING INDEX idx_orders_date (order_date>? AND order_date<?) — index-এ সরাসরি ১ সেপ্টেম্বরে গিয়ে ১ অক্টোবরের আগে থামা। শেষ কোয়েরি দেয় 6, strftime-এর সংস্করণের সমান। পরিসরের শেষটি < '2025-10-01', <= '2025-09-30' নয় — কারণ সময়সহ মান ('2025-09-30 14:00') লেখা হিসেবে '2025-09-30'-এর চেয়ে বড়, তাই বাদ পড়ত। n সারির টেবিলে scan-এর খরচ n-এর সমানুপাতিক; B+ tree-তে খোঁজা মোটামুটি log n পাতা পড়া, তারপর মেলা k টি সারি।

একই ছাঁচের আরও উদাহরণ: WHERE price * 1.15 > 1000-এর বদলে WHERE price > 1000 / 1.15 (হিসাবটি ধ্রুবকের দিকে নিন); WHERE substr(phone, 1, 3) = '017'-এর বদলে WHERE phone LIKE '017%' (শুরুর অংশের LIKE অনেক DBMS-এ index ব্যবহার করতে পারে, তবে SQLite-এ collation-এর শর্ত মানলে তবেই)। আর যখন ফাংশনটি এড়ানো যায় না — যেমন ইমেইল বড়-ছোট হাতের অক্ষর নির্বিশেষে খোঁজা — তখন ফাংশনের ফলের ওপরই index বানান: CREATE INDEX idx_email ON users(lower(email)). একে বলে expression index; SQLite 3.9 (২০১৫) থেকে, PostgreSQL-এ বহু আগে থেকে, আর MySQL 8.0.13 থেকে আছে। শর্ত একটিই: ফাংশনটিকে deterministic হতে হবে — একই ইনপুটে সবসময় একই ফল। তাই julianday('now') দিয়ে index বানাতে গেলে SQLite ত্রুটি দেয়: “non-deterministic use of julianday() in an index”. index বাছাই আর EXPLAIN-এর পুরো আলোচনা পর্ব ৪১-এ।

আরও গভীরে: SQLite-এর তারিখ কোড (date.c) প্রতিটি ইনপুটকে প্রথমে একটি ৬৪-বিট পূর্ণসংখ্যায় বদলায় — Julian day × ৮৬,৪০০,০০০, অর্থাৎ মিলিসেকেন্ডে — তারপর modifier প্রয়োগ করে আবার লেখায় ফেরে; তাই ফরম্যাট যা-ই হোক, হিসাব একটিই পথে। এর বৈধ সীমা ০০০০ থেকে ৯৯৯৯ সাল। আর একই SQL statement-এর ভেতরে প্রতিটি 'now' একই মান দেয় (SQLite একটি statement-এর এক ধাপে সময় একবারই পড়ে), তাই একটি UPDATE-এর সব সারিতে সময় সমান থাকে।

১০ · AI-এর যুগে SQL ফাংশন

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

AI-এর ভুলফলকীভাবে ধরবেন
অন্য DBMS-এর ফাংশন: SQLite-এ DATE_FORMAT, NOW(), LPAD“no such function” — বা আরও খারাপ, MySQL-এ LENGTH দিয়ে অক্ষর গোনাপ্রম্পটে DBMS আর সংস্করণ লিখুন; বাংলা লেখায় length চালিয়ে দেখুন
YEAR(order_date) = 2025 ধরনের WHEREসঠিক ফল, কিন্তু index বাদ, বড় টেবিলে ধীরEXPLAIN QUERY PLAN-এ SCAN খুঁজুন
বয়স = দিন ÷ 365 বা 365.25জন্মদিনের আশেপাশে ১ বছর ভুলজন্মদিনের আগের দিন, সেদিন আর পরের দিন — তিনটি তারিখে পরীক্ষা
টাকায় ROUND(price * 1.15, 2).৫-এর কাছে পয়সার ভুলযে দামে ×1.15 ঠিক .৫-এ শেষ হয়, সেটি দিয়ে চালান
SUM(a) / COUNT(*) দুটি পূর্ণসংখ্যায়গড়ের দশমিক উধাওহাতে একটি ছোট উদাহরণ হিসাব করে মেলান
সময় টাইমজোন ছাড়া, বা 'localtime'মাস-দিনের সীমানায় ভুল দিনে বিক্রিরাত ১২টা–৬টার একটি অর্ডার দিয়ে রিপোর্ট চালান
|| দিয়ে NULL কলাম জোড়াপুরো লেখা NULL, সারি “ফাঁকা”NULL আছে এমন সারিতে আলাদা করে দেখুন
নেই এমন কলাম কল্পনা (birth_date, created_at)ত্রুটি, বা ভুল কলামে হিসাবschema পাশে রেখে প্রতিটি নাম মেলান

সবচেয়ে কাজের কৌশল: AI-কে শুধু কোয়েরি নয়, প্রান্তিক পরীক্ষার ডেটাও বানাতে বলুন, আর DBMS, টাইমজোন আর রাউন্ডিংয়ের নিয়ম আগে থেকে বলে দিন:

আমার ডেটাবেস SQLite 3.4x (ব্রাউজারে sql.js). টেবিল: orders(order_id, customer_id, order_date TEXT 'YYYY-MM-DD', status), customers(customer_id, name, city, phone, joined_on TEXT). সময় UTC-তে রাখা, রিপোর্ট দেখাতে হবে Asia/Dhaka (+06:00) ধরে। ১. প্রতি মাসের delivered অর্ডারের সংখ্যা বের করার কোয়েরি দাও। শুধু SQLite-এ আছে এমন ফাংশন ব্যবহার করবে; MySQL/PostgreSQL-এর ফাংশন (DATE_FORMAT, NOW, LPAD) নয়। ২. WHERE-এ কলামকে কোনো ফাংশনে মুড়বে না — পরিসর (>= আর <) দিয়ে লিখবে, যাতে order_date-এর index কাজে লাগে। ৩. প্রতিটি ফাংশন কেন বেছে নিলে, এক লাইনে বলো। ৪. কোয়েরি যাচাইয়ের জন্য ৫টি প্রান্তিক INSERT দাও: মাসের শেষ দিন, অধিবর্ষের ২৯ ফেব্রুয়ারি, NULL city, আর প্রত্যাশিত ফল কী আসবে তা আগে লিখে দাও। কোনো কলামের নাম অনুমান করবে না; আমার দেওয়া নামগুলোই ব্যবহার করবে।

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

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

  • Scalar আর aggregate ফাংশনের পার্থক্য কী? — scalar প্রতিটি সারিতে একটি ফল দেয় (UPPER, ROUND); aggregate অনেক সারি থেকে একটি ফল (SUM, COUNT).
  • MySQL-এ LENGTH আর CHAR_LENGTH-এর পার্থক্য? — LENGTH বাইট গোনে, CHAR_LENGTH অক্ষর; UTF-8-এ বাংলা অক্ষর ৩ বাইট, তাই দুটি আলাদা ফল দেয়।
  • দুই তারিখের মাঝে কত দিন — SQLite-এ কীভাবে? — julianday(a) - julianday(b); MySQL-এ DATEDIFF, PostgreSQL-এ দুটি date বিয়োগ।
  • WHERE YEAR(order_date) = 2025 কেন ধীর হতে পারে, সমাধান কী? — কলাম ফাংশনে মোড়া বলে index ব্যবহার হয় না (non-sargable); পরিসর লিখুন বা expression index বানান।
  • ডেটাবেসে সময় কোন টাইমজোনে রাখা উচিত? — UTC-তে; দেখানোর সময় ব্যবহারকারীর টাইমজোনে (যেমন Asia/Dhaka) রূপান্তর।

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

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

প্র ০১“ক্ষ” চোখে একটি অক্ষর, অথচ SQLite বলে length 3. ডেটাবেস কেন “অক্ষর” গুনতে এত দ্বিধায়, আর বাংলা নামের কলামের দৈর্ঘ্যসীমা ঠিক করতে কী ভাববেন?

কারণ “অক্ষর” শব্দটির অন্তত তিনটি অর্থ, আর কম্পিউটারের ইতিহাস তিনটিকে আলাদা সময়ে আলাদাভাবে সামলেছে। শুরুর দিকে ASCII-তে এক অক্ষর মানে এক বাইট — প্রশ্নই ছিল না। বিভিন্ন ভাষার জন্য আলাদা আলাদা encoding-এর বিশৃঙ্খলা দূর করতে ১৯৯১ সালে Unicode-এর প্রথম সংস্করণ আসে, যা বিশ্বের প্রতিটি লিপির প্রতিটি চিহ্নকে একটি নম্বর (code point) দেয়; বাংলার জায়গা U+0980 থেকে U+09FF. ১৯৯২ সালে Ken Thompson আর Rob Pike UTF-8 নকশা করেন — ASCII-র সাথে সামঞ্জস্য রেখে প্রতিটি code point-কে ১ থেকে ৪ বাইটে লেখার একটি উপায়, যেখানে বাংলার প্রতিটি code point ৩ বাইট।

কিন্তু বাংলা লিপিতে পাঠক যাকে একটি অক্ষর দেখেন, তা প্রায়ই কয়েকটি code point-এর জোড়: “ক্ষ” = ক + হসন্ত + ষ; “কি” = ক + ই-কার। Unicode-এর UAX #29 মানদণ্ড এই চোখে-দেখা এককের নাম দিয়েছে grapheme cluster, আর তা গুনতে জটিল নিয়ম লাগে — তাই প্রায় সব DBMS সস্তা পথ নেয়: SQLite আর PostgreSQL code point গোনে, MySQL-এর LENGTH বাইট গোনে। কলামের সীমাতেও একই বিভাজন: PostgreSQL-এর VARCHAR(n) আর MySQL-এর VARCHAR(n)-এ n হলো অক্ষর (code point); Oracle-এ VARCHAR2(n)-এর ডিফল্ট অর্থ সাধারণত বাইট; SQL Server-এর NVARCHAR(n)-এ UTF-16 এককের সংখ্যা। আর MySQL-এর পুরোনো “utf8” আসলে সর্বোচ্চ ৩ বাইটের utf8mb3 — বাংলা ধরে, কিন্তু emoji ধরে না; তাই আধুনিক MySQL-এ utf8mb4 ব্যবহার করুন।

বাংলা নামের সীমা ঠিক করতে তাই তিনটি প্রশ্ন: আপনার DBMS n দিয়ে কী গোনে? ইংরেজিতে ২০ অক্ষরের একটি নাম বাংলায় কত code point হয় (যুক্তাক্ষর আর কার-চিহ্ন বেশি জায়গা নেয়)? আর ফর্মের যাচাই (অ্যাপের ভাষা হয়তো UTF-16 গোনে) কি ডেটাবেসের সাথে একই হিসাব করে?

মূল উপলব্ধি: “দৈর্ঘ্য” তিন রকম — বাইট, code point, চোখে-দেখা অক্ষর। বাংলা ডেটায় কোনটি গুনছেন তা স্পষ্ট না জানলে সীমা, কাটা (substr) আর সাজানো — তিনটিই অপ্রত্যাশিত ফল দেবে।

প্র ০২“Banker’s rounding”-এ 2.5 রাউন্ড হয়ে 2 হয় — শুনতে ভুল মনে হয়। তাহলে এই নিয়ম কেন তৈরি হলো, আর কেন অনেক সিস্টেমের ডিফল্ট এটিই?

কারণ একটি সংখ্যার জন্য যা “স্বাভাবিক”, হাজারো সংখ্যার যোগফলে সেটি পক্ষপাত তৈরি করে। স্কুলে শেখা নিয়ম — .৫ হলে ওপরে — প্রতিবার ঠিক মাঝের মানগুলো একই দিকে ঠেলে দেয়। চারটি সংখ্যা ধরুন: 0.5, 1.5, 2.5, 3.5. আসল যোগফল 8. স্কুলের নিয়মে রাউন্ড করলে 1 + 2 + 3 + 4 = 10, অর্থাৎ ২ বেশি। “জোড়ের দিকে” নিয়মে 0 + 2 + 2 + 4 = 8 — হুবহু। মাঝের মানগুলোর অর্ধেক ওপরে, অর্ধেক নিচে যায়, তাই দীর্ঘ হিসাবে ভুল কাটাকাটি হয়ে যায়। নামটি ব্যাংকের সাথে জুড়েছে লোকমুখে, কিন্তু নিয়মটি পরিসংখ্যান আর বিজ্ঞানের হিসাবে বহু পুরোনো।

কম্পিউটারে এটিকে মানদণ্ড বানায় ১৯৮৫ সালের IEEE 754 — floating-point হিসাবের আন্তর্জাতিক মানদণ্ড — যার ডিফল্ট রাউন্ডিং মোড “round half to even”. প্রতিটি যোগ-গুণের ফল যখন সবচেয়ে কাছের প্রকাশযোগ্য সংখ্যায় রাউন্ড হয়, তখন এই নিয়ম দীর্ঘ হিসাবে ভুল জমতে দেয় না। ফলে C লাইব্রেরির rint(), Python 3-এর round() আর অনেক DBMS-এর floating-point রাউন্ড এই পথে চলে। অথচ DECIMAL/NUMERIC-এ বেশিরভাগ DBMS স্কুলের মতো “শূন্য থেকে দূরে” নেয়; SQLite-এর round()-ও (REAL মানেও) এই নিয়ম মানে। একই অ্যাপে Python আর ডেটাবেস তাই একই সংখ্যাকে ভিন্নভাবে রাউন্ড করতে পারে।

টাকার বেলায় সিদ্ধান্ত প্রযুক্তির নয়, নিয়মের: ভ্যাট, সুদ বা বিলের রাউন্ডিং কোন নিয়মে হবে তা ব্যবসা, ব্যাংক বা কর-কর্তৃপক্ষের নির্দেশনা ঠিক করে। প্রকৌশলীর কাজ সেই নিয়মটি স্পষ্ট লেখা — পয়সায় পূর্ণসংখ্যা, আর নিজের লেখা রাউন্ডিং — যাতে DBMS বা ভাষা বদলালেও ফল না বদলায়; আর রাউন্ড কখন হবে (প্রতিটি লাইনে, নাকি মোটে) সেটিও লিখে রাখা, কারণ দুটি আলাদা ফল দেয়।

মূল উপলব্ধি: রাউন্ডিং কোনো একটি “সঠিক” নিয়ম নয়, একটি বাছাই। ডিফল্টের ওপর ভরসা না করে নিয়মটি নিজে বেছে নিন, লিখে রাখুন, আর .৫-এর ক্ষেত্র দিয়ে পরীক্ষা করুন।

প্র ০৩SQLite-এ DATE টাইপই নেই, তারিখ রাখা হয় সাধারণ লেখা বা সংখ্যায়। এটি কি দুর্বলতা? ISO লেখা, Unix epoch আর Julian day-এর মধ্যে কখন কোনটি বাছবেন?

এটি দুর্বলতা নয়, একটি সচেতন নকশা। Richard Hipp ২০০০ সালে SQLite লেখা শুরু করেন একটি ছোট, নির্ভরযোগ্য, একটি ফাইলেই চলা ডেটাবেস হিসেবে। এর মাত্র পাঁচটি storage class — NULL, INTEGER, REAL, TEXT, BLOB — আর বাকি সব (তারিখ, boolean, টাকা) এগুলোর ওপর নিয়ম দিয়ে তৈরি। লাভ: ইঞ্জিন ছোট থাকে, আর তারিখের সব যুক্তি এক জায়গায় — date ফাংশনগুলোতে। দাম: কলামে '05/10/2025' বা 'kal' ঢুকলেও DBMS আটকায় না, যদি না আপনি CHECK লেখেন — যেমন CHECK (date(joined_on) = joined_on), যা শুধু সঠিক ISO তারিখ ঢুকতে দেয়।

তিনটি রূপের পেছনে তিনটি ইতিহাস। ISO 8601 প্রথম প্রকাশিত হয় ১৯৮৮ সালে, দেশভেদে তারিখ লেখার গোলমাল (০৫/১০ মানে ৫ অক্টোবর না ১০ মে?) দূর করতে; বড় একক আগে বলে লেখা হিসেবে সাজালেই সময়ের ক্রম। Unix epoch এসেছে ১৯৭০-এর দশকের Unix থেকে — ১৯৭০-০১-০১ UTC থেকে সেকেন্ড; সমস্যা হলো ৩২-বিট signed সংখ্যায় এটি ২০৩৮ সালের ১৯ জানুয়ারি ফুরিয়ে যায় (“Year 2038 problem”), তাই ৬৪-বিট দরকার, SQLite-এর INTEGER অবশ্য ৬৪-বিট পর্যন্ত। Julian day আরও পুরোনো: ১৫৮৩ সালে পণ্ডিত Joseph Scaliger জ্যোতির্বিদ্যা আর ইতিহাসের তারিখ মেলাতে “Julian Period” প্রস্তাব করেন, যা থেকে জ্যোতির্বিদরা ক্যালেন্ডার-নিরপেক্ষ দিন-গণনা চালু করেন।

বাছাইয়ের সহজ নিয়ম: সাধারণ অ্যাপে ISO TEXT — পড়া যায়, ডিবাগ সহজ, ক্রম ঠিক, আর অন্য DBMS-এ সরানো সহজ। খুব বেশি সারি আর জায়গা-বাঁচানো জরুরি হলে, বা অ্যাপ নিজেই epoch-এ চলে, তাহলে INTEGER epoch (মনে রাখুন এটি সবসময় UTC). দিনের ভগ্নাংশসহ অনেক ব্যবধান-হিসাব হলে Julian day. যেটিই বাছুন, এক কলামে একটিই রূপ রাখুন — মিশ্রণই আসল বিপদ।

মূল উপলব্ধি: টাইপ না থাকলে শৃঙ্খলা আপনার দায়িত্ব — একটি রূপ বাছুন, CHECK দিয়ে পাহারা দিন, আর ফাংশনগুলোকে হিসাবের কাজ করতে দিন।

প্র ০৪টাইমজোনের তথ্য রাখতে “+06:00” লিখে রাখলেই কি যথেষ্ট, নাকি “Asia/Dhaka” নাম রাখা দরকার? দুটির পার্থক্য বাস্তবে কোথায় ধরা পড়ে?

দুটি আলাদা জিনিস। “+06:00” একটি offset — একটি নির্দিষ্ট মুহূর্তে UTC থেকে কত এগিয়ে। “Asia/Dhaka” একটি zone — একটি এলাকার ঘড়ির নিয়মের পুরো ইতিহাস আর ভবিষ্যৎ: কবে offset কত ছিল, কবে বদলাবে। অতীতের একটি মুহূর্ত দেখাতে offset-ই যথেষ্ট; কিন্তু ভবিষ্যতের কোনো ঘটনা — “প্রতি মাসের ৫ তারিখ সকাল ১০টায় কিস্তি” — নির্ধারণ করতে zone লাগে, কারণ নিয়ম বদলাতে পারে।

বাংলাদেশেই উদাহরণ আছে। ২০০৯ সালে বিদ্যুৎ সাশ্রয়ের জন্য জুন মাস থেকে ঘড়ি এক ঘণ্টা এগিয়ে +07:00 করা হয়, বছরের শেষে আবার +06:00-এ ফেরানো হয়। কেউ যদি ২০০৯-এর শুরুতে “ডিসেম্বর পর্যন্ত প্রতিদিন সকাল ৯টা” সময়সূচি +06:00 ধরে UTC-তে বদলে রাখতেন, জুনের পর সবকিছু এক ঘণ্টা সরে যেত। বিশ্বের এমন সব নিয়ম-বদল একটি উন্মুক্ত ডেটাবেসে লেখা থাকে — IANA time zone database (tz বা zoneinfo নামেও পরিচিত), যা Arthur David Olson ১৯৮০-এর দশকে শুরু করেন আর ২০১১ সাল থেকে IANA রক্ষণাবেক্ষণ করে। দেশগুলো নিয়ম বদলালে এর নতুন সংস্করণ আসে, আর অপারেটিং সিস্টেম, PostgreSQL, Java — সবাই সেটি হালনাগাদ করে।

তাই পেশাদার নিয়ম তিন স্তরের: ঘটে যাওয়া মুহূর্ত রাখুন UTC-তে (PostgreSQL-এ timestamptz, SQLite-এ ISO লেখার শেষে Z); ব্যবহারকারী বা শাখার zone নাম রাখুন আলাদা কলামে ('Asia/Dhaka'); আর দেখানো বা “আজকের বিক্রি”-র সীমানা হিসাব করুন সেই zone ধরে — PostgreSQL-এ AT TIME ZONE 'Asia/Dhaka'. MySQL-এ zone নাম ব্যবহারের আগে সার্ভারে টাইমজোন টেবিল লোড করা লাগতে পারে; SQLite-এ zone ধারণাই নেই, তাই রূপান্তর অ্যাপের দায়িত্ব।

মূল উপলব্ধি: offset একটি সংখ্যা, zone একটি নিয়মের বই। অতীত মুহূর্তের জন্য UTC যথেষ্ট; ভবিষ্যতের পরিকল্পনা আর স্থানীয় দিনের সীমানার জন্য zone নাম রাখুন।

প্র ০৫“Sargable” শব্দটি কোথা থেকে এল? আর DBMS কি নিজে বুঝে strftime('%Y', d) = '2025'-কে একটি পরিসরে বদলে নিতে পারে না?

শব্দটি এসেছে “Search ARGument ABLE” থেকে। ১৯৭৯ সালে IBM-এর System R প্রকল্পের Patricia Selinger ও সহকর্মীদের বিখ্যাত প্রবন্ধ “Access Path Selection in a Relational Database Management System” — যা আধুনিক cost-based query optimizer-এর ভিত্তি — WHERE-এর যে শর্তগুলো সরাসরি index বা storage স্তরে পরীক্ষা করা যায়, সেগুলোকে বলেছিল “search arguments” বা SARGs. সেখান থেকে চলতি ভাষায় “sargable”: যে শর্তে কলাম অপারেটর ধ্রুবক রূপটি অক্ষত, তাই index-এর সাজানো ক্রম কাজে লাগে।

Optimizer কেন নিজে বদলে নেয় না? কারণ একটি ফাংশনকে উল্টে পরিসরে আনা মানে ফাংশনটির গাণিতিক ধর্ম জানা — বিশেষত সেটি ক্রম রক্ষা করে কি না (monotonic). strftime('%Y', d) ISO তারিখে ক্রম রক্ষা করে, কিন্তু optimizer-কে সেটি জানতে হলে প্রতিটি ফাংশন আর প্রতিটি ফরম্যাট-লেখার জন্য আলাদা নিয়ম লিখতে হয়; lower(email) ক্রম রক্ষাই করে না (ছোট-বড় হাতের অক্ষর মিশে যায়)। কিছু DBMS নির্দিষ্ট ক্ষেত্রে এমন রূপান্তর করে — যেমন শুরুর অংশের LIKE '017%'-কে পরিসরে বদলানো, শর্তসাপেক্ষে SQLite-ও করে — কিন্তু সাধারণ ফাংশনে ভরসা করা যায় না। তাই সমাধান দুটি: শর্তটি নিজে পরিসরে লিখুন; অথবা ফাংশনের ফলের ওপর expression index বা generated column-এ index বানান, যাতে সাজানো ক্রমটি ফাংশনের ফলেরই হয়।

দাম নগণ্য নয়: n সারিতে scan মানে n সারি পড়া, আর B+ tree index মানে মোটামুটি log n উচ্চতার গাছ বেয়ে নামা আর মেলা k সারি পড়া। ১ কোটি সারিতে পার্থক্য লাখো গুণ হতে পারে। আবার ছোট টেবিলে, বা যখন বেশিরভাগ সারিই মেলে, scan-ই সস্তা — optimizer তখন index থাকলেও scan বাছতে পারে। তাই অনুমান নয়, EXPLAIN দেখে সিদ্ধান্ত নিন (পর্ব ৪১, ৪৩)।

মূল উপলব্ধি: index সাজানো থাকে কলামের মান অনুযায়ী; কলামকে ফাংশনে মুড়লে সেই ক্রম হারায়। কলামকে খালি রাখুন, হিসাব ধ্রুবকের দিকে নিন — বা ফাংশনের ফলের ওপরই index বানান।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে ফল বলা, একটি বয়সের হিসাব আর একটি index-বান্ধব রূপান্তর।

  1. কোয়েরি লিখুন: দোকানের গুদামের জন্য প্রতিটি পণ্যের একটি SKU কোড চাই: শ্রেণির নম্বর, তারপর '-', তারপর নামের প্রথম তিন অক্ষর বড় হাতে, তারপর '-', তারপর product_id — যেমন Mobile Phone-এর জন্য 1-MOB-101. db-shop-এর products টেবিলে (product_id, name, category_id) কোয়েরিটি লিখে যেকোনো এডিটরে চালান। Mustard Oil 1L-এর কোড কী?
    SELECT product_id, name,
           category_id || '-' || upper(substr(name, 1, 3)) || '-' || product_id AS sku
    FROM products
    ORDER BY product_id;

    দশটি সারি: 1-MOB-101, 1-HEA-102, 1-POW-103, 2-DSA-104, 2-SQL-105, 3-PAN-106, 3-SAR-107, 4-RIC-108, 4-MUS-109, 1-SMA-110. Mustard Oil 1L → 4-MUS-109. || সংখ্যাকেও লেখা বানিয়ে জোড়ে। category_id NULL হলে পুরো SKU NULL হতো — তখন coalesce(category_id, 0).

  2. ফল আগে বলুন (trace): SQLite-এ নিচের প্রতিটির ফল হাতে লিখুন, তারপর একটি এডিটরে SELECT …; চালিয়ে মিলিয়ে নিন: (ক) length(trim(' ঢাকা ')) (খ) substr('01711000001', -6) (গ) round(-2.5) (ঘ) 7 / 2 * 2 (ঙ) date('2025-01-31', '+1 month') (চ) 'a' || NULL (ছ) upper('ঢাকা dhaka').

    (ক) 4 — trim ফাঁকা ছাঁটে, “ঢাকা” = ঢ, া, ক, া. (খ) '000001' — শেষ ছয় অক্ষর। (গ) -3 — SQLite শূন্য থেকে দূরে নেয়। (ঘ) 6 — বাঁ থেকে ডানে: 7 / 2 = 3 (integer division), তারপর 3 × 2 = 6, 7 নয়! (ঙ) '2025-03-03' — ৩১ ফেব্রুয়ারি উপচে মার্চে। (চ) NULL. (ছ) 'ঢাকা DHAKA' — বাংলা অংশ অপরিবর্তিত।

  3. বয়স হিসাব করুন: একজন চাকরিপ্রার্থীর জন্মতারিখ '2004-02-29'. SQLite-এ ক্যালেন্ডারের নিয়মে (বছরের বিয়োগ, জন্মদিন না এলে ১ কম) তাঁর বয়স বের করার expression লিখুন, আর হিসাব করুন '2025-02-28' ও '2025-03-01' তারিখে কত আসে। “৩৬৫.২৫ দিয়ে ভাগ” পদ্ধতি '2025-02-28'-এ কী দেয়?
    SELECT (strftime('%Y', '2025-02-28') - strftime('%Y', '2004-02-29'))
         - (strftime('%m-%d', '2025-02-28') < strftime('%m-%d', '2004-02-29')) AS age_feb28,
           (strftime('%Y', '2025-03-01') - strftime('%Y', '2004-02-29'))
         - (strftime('%m-%d', '2025-03-01') < strftime('%m-%d', '2004-02-29')) AS age_mar1;

    ফল: 20 | 21. ২৮ ফেব্রুয়ারিতে '02-28' < '02-29' সত্য, তাই ২১ − ১ = ২০; ১ মার্চে '03-01' < '02-29' মিথ্যা, তাই ২১. “÷ ৩৬৫.২৫” এখানে ২০ দেয় — এবার মিলেছে, কিন্তু অংশ ৭-এর এডিটরের মতো অন্য তারিখে ভুল করে, তাই সেটি ভরসাযোগ্য নয়। আর মনে রাখুন, সাধারণ বছরে লিপ-ডে জন্মদিন ২৮ ফেব্রুয়ারি না ১ মার্চ ধরা হবে, তা প্রতিষ্ঠানের নিয়মে লিখিত থাকা দরকার; এই expression ১ মার্চ ধরে।

  4. index-বান্ধব করে লিখুন: orders(order_date)-এ আর products(price)-এ index আছে, আর customers-এ phone কলামে। নিচের তিনটি WHERE-কে এমনভাবে লিখুন যাতে একই সারি আসে কিন্তু কলামটি ফাংশনে মোড়া না থাকে: (ক) WHERE strftime('%Y', order_date) = '2025' (খ) WHERE price * 1.15 > 1000 (গ) WHERE substr(phone, 1, 3) = '017'.
    • (ক) WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01' — db-shop-এ দুটিই ১০টি অর্ডার দেয়।
    • (খ) WHERE price > 1000 / 1.15 — হিসাবটি ধ্রুবকের দিকে (≈ 869.57); দুটিই একই ছয়টি পণ্য দেয় (Mobile Phone, Headphone, Power Bank, Panjabi, Saree, Smart Watch). তবে ঠিক সীমানার কাছের দামে floating-point-এর কারণে দুই রূপ ভিন্ন ফল দিতে পারে — টাকা পয়সায় পূর্ণসংখ্যায় রাখলে এই ঝুঁকিও কমে।
    • (গ) WHERE phone LIKE '017%' — শুরুর অংশের LIKE; SQLite-এ এটি index ব্যবহার করবে কি না তা collation-এর শর্তের ওপর নির্ভর করে, তাই EXPLAIN QUERY PLAN দিয়ে যাচাই করুন। নিশ্চিত বিকল্প: WHERE phone >= '017' AND phone < '018'.

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

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

ক্রমএকটি অগোছালো city কলাম পরিষ্কারের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. GROUP BY city চালিয়ে দেখা কত রকম রূপ আছে
  2. trim() দিয়ে দুই পাশের ফাঁকা জায়গা ছাঁটা
  3. lower() দিয়ে বড়-ছোট হাতের অক্ষর এক করা
  4. CASE দিয়ে বানান-বৈচিত্র্য (Ctg → chattogram) এক নামে আনা
  5. SELECT-এ ফল যাচাই করে তারপর UPDATE দিয়ে স্থায়ী করা
  6. অ্যাপের ইনপুট-নিয়ম বা CHECK দিয়ে ভবিষ্যতের ময়লা আটকানো

SQLite-এ SELECT length('বাংলা'); চালালে কী ফল আসে?

  • 5
  • 15
  • 3
  • ত্রুটি — length() বাংলা লেখায় চলে না

SQLite বা PostgreSQL-এ SELECT 7 / 2; চালালে ফল কী, আর কেন?

  • 3.5 — সাধারণ ভাগ
  • 3 — দুটি পূর্ণসংখ্যার ভাগ পূর্ণসংখ্যা
  • 4 — রাউন্ড করে
  • ত্রুটি — টাইপ মেলে না

customers টেবিলে একজনের phone NULL. SQLite-এ substr(phone, 1, 5) || '******' সেই সারিতে কী দেয়?

  • '******'
  • '' (খালি লেখা)
  • NULL
  • ত্রুটি

orders টেবিলের order_date কলামে ('YYYY-MM-DD' লেখা) একটি index আছে। সেপ্টেম্বর ২০২৫-এর অর্ডার খুঁজতে কোন শর্তটি index সবচেয়ে ভালোভাবে ব্যবহার করতে পারে?

  • WHERE strftime('%Y-%m', order_date) = '2025-09'
  • WHERE substr(order_date, 1, 7) = '2025-09'
  • WHERE date(order_date) BETWEEN '2025-09-01' AND '2025-09-30'
  • WHERE order_date >= '2025-09-01' AND order_date < '2025-10-01'

SQLite-এ SELECT date('2025-01-31', '+1 month'); কী ফেরত দেয়?

  • '2025-03-03'
  • '2025-02-28'
  • '2025-02-31'
  • NULL

একটি বাংলাদেশি ই-কমার্স অ্যাপের সার্ভার বিদেশে, ডিফল্টে UTC-তে চলে। অর্ডারের সময় ডেটাবেসে কীভাবে রাখা সবচেয়ে নিরাপদ?

  • ঢাকার স্থানীয় সময়ে, টাইমজোনের উল্লেখ ছাড়া
  • UTC-তে রাখা, আর দেখানোর সময় Asia/Dhaka (+06:00)-এ রূপান্তর
  • SQLite-এর 'localtime' modifier দিয়ে সার্ভারের স্থানীয় সময়ে
  • দিন/মাস/বছর রূপে লেখা, যেমন '09/10/2025 8:30 PM'

SQLite-এ একটি পণ্যের দাম REAL কলামে 650. round(price * 1.15) চালালে 748-এর বদলে 747 আসে। মূল কারণ কোনটি?

  • SQLite সবসময় নিচের দিকে রাউন্ড করে
  • SQLite banker's rounding ব্যবহার করে
  • floating-point-এ 650 × 1.15 আসলে 747.4999… হয়
  • round()-এ দশমিক ঘরের সংখ্যা দেওয়া হয়নি

শব্দকোষ

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

মনে রাখুন

  1. Scalar ফাংশন প্রতি সারিতে একটি ফল, aggregate অনেক সারিতে একটি; nested ফাংশন হিসাব হয় ভেতর থেকে বাইরে, আর NULL ইনপুট সাধারণত NULL ফল।
  2. লেখার দৈর্ঘ্য তিন রকম — বাইট, code point, চোখে-দেখা অক্ষর; SQLite-এর length() code point গোনে, MySQL-এর LENGTH() বাইট; SQLite-এর upper()/lower() শুধু ASCII.
  3. অগোছালো লেখা: আগে GROUP BY দিয়ে দেখুন, তারপর trim → lower → CASE-মানচিত্র, SELECT-এ যাচাই করে তবে UPDATE.
  4. সংখ্যায় পূর্ণসংখ্যার ভাগ আর floating-point-এর ফাঁদ; টাকা পয়সায় INTEGER, রাউন্ডিং নিয়ম নিজে বেছে লিখে রাখুন; ceil/floor-এর মতো math function আছে কি না আগে পরীক্ষা করুন।
  5. তারিখ ISO-8601-এ; modifier বাঁ থেকে ডানে; মাসের শেষ দিন = start of month → +2 months → −1 day; বয়স ক্যালেন্ডারে; মুহূর্ত UTC-তে; আর WHERE-এ কলাম ফাংশনে না মুড়ে পরিসর লিখুন।

পরের পর্বে: এই পর্বের ফাংশনগুলো প্রতিটি সারিতে আলাদা চলেছে। পর্ব ১৮-এ সারিগুলোকে দলে বাঁধবেন — COUNT, SUM, AVG, GROUP BY আর HAVING দিয়ে “কোন শহরের গ্রাহক সবচেয়ে বেশি কেনেন?” ধরনের প্রশ্নের উত্তর।

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

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