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

SQL-এর উপভাষা — MySQL, PostgreSQL, SQLite, SQL Server আর Oracle

SQL Dialects — How MySQL, PostgreSQL, SQLite, SQL Server and Oracle Differ, Which Differences Silently Change Results, and How to Write Portable SQL

Taught in Bangla, this lesson compares SQL dialects: why products came before the standard, the five major families and their licences, the same task written five ways, date, string, boolean and division differences, UPSERT, MERGE and RETURNING, silent differences in identifier case, comparison and empty string versus NULL, feature gaps, DDL commit and default isolation, writing portable SQL, migration and choosing a DBMS in Bangladesh.

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

  • উপভাষা কেন তৈরি হলো — standard-এর ইতিহাস, তার বিশাল আকার, ঐচ্ছিক অংশ আর vendor-এর নিজস্ব সংযোজন
  • পাঁচটি DBMS-এ একই কাজের পাশাপাশি তুলনা: স্বয়ংক্রিয় id, নাম উদ্ধৃত করা, জোড়া, প্রথম n সারি, তারিখ, লেখার ফাংশন, boolean, UPSERT, RETURNING
  • যে পার্থক্য ত্রুটি ছাড়াই ফল বদলায়: নামের case folding, collation, '' বনাম NULL, পূর্ণসংখ্যার ভাগ, GROUP BY-এর কড়াকড়ি
  • বড় ক্ষমতার পার্থক্য: recursive CTE, FULL OUTER JOIN, procedural ভাষা, JSON, transactional DDL আর ডিফল্ট isolation
  • লাইসেন্স, MariaDB, cloud সংস্করণ; portable SQL লেখা, MySQL থেকে PostgreSQL-এ যাওয়ার ফাঁদ, আর বাংলাদেশের প্রেক্ষাপটে DBMS বাছাই

১ · উপভাষা কেন — পণ্য আগে এল, standard পরে

পর্ব ১২ থেকে ৩০ পর্যন্ত আপনি যে SQL লিখেছেন, তার প্রায় সবটাই PostgreSQL, MySQL, SQL Server বা Oracle-এ হুবহু চলবে। কিন্তু “প্রায়” শব্দটিই এই পর্বের বিষয়। প্রতিটি DBMS-এর SQL আসলে একটি উপভাষা — মূল ব্যাকরণ এক, কিছু শব্দ আর রীতি আলাদা।

বাংলা ভাষার কথা ভাবুন। ঢাকা, চট্টগ্রাম আর সিলেটের মানুষ নিজেদের আঞ্চলিক ভাষায় কথা বলেন, কিন্তু সবাই প্রমিত বাংলায় লেখা বই পড়তে পারেন। SQL-এর standard হলো সেই প্রমিত রূপ; আর MySQL, Oracle, SQL Server হলো আঞ্চলিক রূপ। পার্থক্য দুই ধরনের: কিছু শব্দ শুনেই বোঝা যায় “এটি অন্য অঞ্চলের” (যেমন TOP 3), আর কিছু শব্দ একই রকম শোনায় কিন্তু মানে আলাদা — বিপদ সেখানেই।

উপভাষার তিনটি মূল কারণ আছে:

পণ্য আগে, standard পরে

IBM-এর System R গবেষণা থেকে SQL-এর জন্ম ১৯৭০-এর দশকে। ১৯৭৯ সালে Oracle (তখন Relational Software, Inc.) প্রথম বাণিজ্যিক SQL DBMS বাজারে আনে, তারপর IBM-এর SQL/DS আর DB2. ANSI standard আসে ১৯৮৬ সালে, ISO ১৯৮৭-তে — ততদিনে প্রত্যেকের নিজের রীতি গ্রাহকের হাজারো প্রোগ্রামে বসে গেছে।

ইতিহাস

Standard বিশাল, অনেক অংশ ঐচ্ছিক

আজকের standard কয়েকটি খণ্ডে ভাগ করা, শুধু মূল খণ্ডই হাজারেরও বেশি পৃষ্ঠা। SQL:1999 থেকে একটি ছোট বাধ্যতামূলক অংশ (Core SQL) আর বাকিটা আলাদা আলাদা ঐচ্ছিক feature. কোনো প্রধান DBMS পুরো standard মেনে চলার দাবি করে না।

conformance

Vendor-এর নিজস্ব সংযোজন

গ্রাহকের প্রয়োজন standard-এর আগে আসে। তাই MySQL দিল ON DUPLICATE KEY UPDATE, PostgreSQL দিল RETURNING, Oracle দিল CONNECT BY — এগুলোকে বলে vendor extension. কিছু পরে standard-এ ঢোকে (যেমন MERGE), কিছু চিরকাল নিজস্ব থাকে।

extension

Standard-এর সংস্করণগুলো জানা থাকলে বুঝবেন কোন feature কত “নতুন”: SQL-86, SQL-89, SQL-92 (JOIN … ON, CASE, ঘোষিত constraint — আজকের মৌলিক SQL), SQL:1999 (recursive CTE, trigger, boolean), SQL:2003 (window function, MERGE, identity column), SQL:2008 (FETCH FIRST), SQL:2011 (temporal টেবিল), SQL:2016 (JSON), আর SQL:2023 (property graph query). একটি feature standard-এ ঢোকার পরও প্রতিটি DBMS-এ আসতে বছরের পর বছর লাগতে পারে — MySQL-এ window function আর CTE এসেছে ৮.০ সংস্করণে (২০১৮), standard-এ ঢোকার দেড়-দুই দশক পরে।

এই পর্বের মূল ভাগ

পার্থক্যগুলোকে তিন ঝুড়িতে রাখুন। (১) সিনট্যাক্সের পার্থক্য — ভুল লিখলে ত্রুটি আসে, তাই ধরা সহজ। (২) অর্থের পার্থক্য — একই লেখা দুই DBMS-এ চলে কিন্তু ভিন্ন ফল দেয়; এগুলো পরীক্ষা ছাড়া ধরা পড়ে না। (৩) ক্ষমতার পার্থক্য — কোনো feature একটিতে আছে, অন্যটিতে নেই। এই পাতার লাইভ এডিটর SQLite চালায়; অন্য চারটির কোড আলাদা লেবেল-দেওয়া ব্লকে, যেগুলো এই পাতায় চালানো যায় না — নিজের DBMS বা অনলাইন fiddle-এ চালিয়ে মিলিয়ে নিতে পারেন।

২ · পাঁচ পরিবার — কে কোথা থেকে এল, কার লাইসেন্স কী

প্রতিটি DBMS-এর ইতিহাস তার উপভাষার চরিত্র ব্যাখ্যা করে। নিচের সারাংশে সংস্করণ নম্বর আর নীতি সময়ের সাথে বদলায় — কাজে লাগানোর আগে অফিসিয়াল নথি দেখে নিন।

DBMSউৎসলাইসেন্সযেখানে বেশি দেখা যায়
MySQLসুইডেনের MySQL AB, ১৯৯৫; ২০০৮-এ Sun, ২০১০-এ Oracle কিনে নেয়Community সংস্করণ GPL v2, পাশাপাশি বাণিজ্যিক লাইসেন্স (dual licensing)ওয়েব, PHP, WordPress, শেয়ার্ড হোস্টিং
MariaDBMySQL-এর মূল নির্মাতা Michael “Monty” Widenius-এর fork, ২০০৯GPL v2অনেক Linux distribution আর হোস্টিংয়ে MySQL-এর জায়গায়
PostgreSQLUC Berkeley-র POSTGRES প্রকল্প (Michael Stonebraker, ১৯৮৬); SQL যোগ হয়ে ১৯৯৬-এ এই নামPostgreSQL License — BSD/MIT ধাঁচের উদার (permissive) লাইসেন্সআধুনিক ওয়েব ব্যাকএন্ড, বিশ্লেষণ, GIS
SQLiteD. Richard Hipp, ২০০০; server ছাড়া একটি লাইব্রেরিPublic domain — কোনো লাইসেন্সই লাগে নামোবাইল অ্যাপ, ব্রাউজার, যন্ত্রের ভেতরে, ছোট টুল
SQL Server১৯৮৯ সালে Sybase-এর সাথে অংশীদারত্বে শুরু; T-SQL সেই উত্তরাধিকারবাণিজ্যিক; শেখা ও ছোট কাজে বিনা মূল্যের Developer আর Express সংস্করণ (সীমাসহ)Microsoft-কেন্দ্রিক প্রতিষ্ঠান, .NET অ্যাপ
Oracle Database১৯৭৯, প্রথম বাণিজ্যিক SQL DBMSবাণিজ্যিক; সীমিত বিনা মূল্যের সংস্করণ আছে (Free / XE)বড় প্রতিষ্ঠান, ব্যাংক, টেলিকম, সরকারি প্রকল্প

লাইসেন্স শুধু দামের প্রশ্ন নয়। GPL সফটওয়্যার নিজের পণ্যের ভেতরে জুড়ে বিতরণ করলে শর্ত মানতে হয় — এ কারণে কিছু কোম্পানি MySQL-এর বাণিজ্যিক লাইসেন্স কেনে; শুধু server-এ চালিয়ে ওয়েব সেবা দিলে সাধারণত সেই শর্ত আসে না। PostgreSQL-এর উদার লাইসেন্সে যে কেউ এটি বদলে নিজের বন্ধ পণ্য বানাতে পারে — তাই অনেক বাণিজ্যিক ও cloud ডেটাবেস ভেতরে PostgreSQL. আর SQLite-এর public domain অবস্থার কারণে কোটি কোটি যন্ত্রে কোনো আইনি ঝামেলা ছাড়াই এটি বসানো যায়। (আইনি সিদ্ধান্তের আগে লাইসেন্সের আসল লেখা পড়ুন বা বিশেষজ্ঞকে জিজ্ঞেস করুন।)

MariaDB বনাম MySQL

Oracle MySQL কিনে নেওয়ার সময় অনেকে এর ভবিষ্যৎ নিয়ে চিন্তিত ছিলেন; Widenius তখন MariaDB শুরু করেন (MySQL নামটি তাঁর মেয়ে My-এর নামে, MariaDB ছোট মেয়ে Maria-র নামে)। শুরুতে MariaDB ছিল MySQL-এর প্রায় হুবহু বদলি, কিন্তু দশ বছরের বেশি সময়ে দুটি আলাদা পথে গেছে: MariaDB-তে RETURNING, sequence আর system-versioned টেবিল আছে; MySQL ৮-এর কিছু feature (যেমন JSON-কে আলাদা binary টাইপে রাখা) MariaDB অন্যভাবে করে — MariaDB-তে JSON আসলে LONGTEXT-এর আরেক নাম। সংস্করণ নম্বরও আর মেলে না। তাই “MySQL-compatible” লেখা দেখলে ধরে নিন মূল অংশ মিলবে, প্রান্ত নয় — হোস্টিং বদলানোর আগে নিজের অ্যাপের পরীক্ষা চালান।

Cloud সংস্করণ — “compatible” মানে হুবহু নয়

Cloud কোম্পানিগুলো এই DBMS-গুলো পরিচালিত সেবা (managed database) হিসেবে চালায়: backup, update আর replication তারা সামলায়। Amazon RDS আর Google Cloud SQL আসল MySQL, PostgreSQL, SQL Server চালায়; Azure SQL Database চলে SQL Server-এর ইঞ্জিনে। আবার Amazon Aurora বা Google AlloyDB নিজেদের storage স্তরের ওপর “MySQL-compatible” বা “PostgreSQL-compatible” — SQL প্রায় একই, কিন্তু কিছু extension, সংস্করণ বা আচরণ আলাদা হতে পারে। একই কথা SQLite-ভিত্তিক নতুন cloud সেবার ক্ষেত্রেও। Cloud ডেটাবেসের বিস্তারিত পর্ব ৬১-এ।

৩ · একই কাজ, পাঁচ রকম লেখা — id, নাম, জোড়া আর প্রথম n সারি

স্বয়ংক্রিয় id

প্রায় প্রতিটি টেবিলে একটি ক্রমিক নম্বরের primary key লাগে (পর্ব ১৩)। Standard-এর রূপ identity column — GENERATED … AS IDENTITY; কিন্তু প্রতিটি DBMS standard-এর আগেই নিজের পথ বানিয়েছিল। একই customers টেবিল পাঁচ রকমে:

CREATE TABLE customers (
  customer_id INT AUTO_INCREMENT PRIMARY KEY,
  name        VARCHAR(100) NOT NULL
);
INSERT INTO customers (name) VALUES ('Rahim Uddin');
SELECT LAST_INSERT_ID();          -- এই connection-এর শেষ নতুন id
CREATE TABLE customers (
  customer_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name        VARCHAR(100) NOT NULL
);
-- পুরোনো রীতি: customer_id SERIAL PRIMARY KEY (পেছনে একটি sequence)
INSERT INTO customers (name) VALUES ('Rahim Uddin')
RETURNING customer_id;
CREATE TABLE customers (
  customer_id INTEGER PRIMARY KEY,  -- rowid-এর আরেক নাম, নিজেই বাড়ে
  name        TEXT NOT NULL
);
-- INTEGER PRIMARY KEY AUTOINCREMENT: মোছা id আর কখনো ফেরত দেয় না
INSERT INTO customers (name) VALUES ('Rahim Uddin');
SELECT last_insert_rowid();
CREATE TABLE customers (
  customer_id INT IDENTITY(1, 1) PRIMARY KEY,   -- শুরু 1, ধাপ 1
  name        NVARCHAR(100) NOT NULL
);
INSERT INTO customers (name)
OUTPUT inserted.customer_id
VALUES (N'Rahim Uddin');
-- অথবা: SELECT SCOPE_IDENTITY();
CREATE TABLE customers (
  customer_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name        VARCHAR2(100) NOT NULL
);
-- 12c-এর আগে: CREATE SEQUENCE + একটি BEFORE INSERT trigger
INSERT INTO customers (name) VALUES ('Rahim Uddin');

খেয়াল করুন টাইপের নামও বদলেছে: VARCHAR2 Oracle-এর, NVARCHAR (Unicode) SQL Server-এর, NUMBER Oracle-এর। Auto-increment-এর নম্বরে ফাঁক পড়া server DBMS-গুলোতে স্বাভাবিক — ROLLBACK হওয়া INSERT-এর নম্বর সাধারণত ফেরত আসে না, কারণ sequence পেছনে ফেরে না। তাই id-কে “কতজন গ্রাহক” গোনার উপায় ভাববেন না।

নাম উদ্ধৃত করা আর লেখা জোড়া

কাজMySQLPostgreSQLSQLiteSQL ServerOracle
নাম উদ্ধৃত করা (quoted identifier)`order`; ANSI_QUOTES mode-এ "order""order""order" (সামঞ্জস্যের জন্য `…`, […]-ও চলে)[order], "order""ORDER"
লেখা জোড়া (concatenation)CONCAT(a, b); ডিফল্টে || মানে ORa || b, concat()a || b, concat() (৩.৪৪ থেকে)a + b, CONCAT() (২০১২ থেকে)a || b, CONCAT(a, b) — শুধু দুটি argument
NULL জুড়লেCONCAT-এ পুরো ফল NULL||-এ NULL; concat NULL বাদ দেয়PostgreSQL-এর মতো+-এ সাধারণত NULL; CONCAT NULL-কে খালি ধরেNULL-কে খালি লেখা ধরে: 'a' || NULL = 'a'

MySQL-এর || সবচেয়ে বড় ফাঁদ: ডিফল্টে এটি যৌক্তিক OR, তাই SELECT 'a' || 'b' ত্রুটি দেয় না, দেয় 0 — দুটি লেখাকে সংখ্যায় বদলে (দুটিই 0) OR করে। PIPES_AS_CONCAT SQL mode চালু করলে তবেই এটি জোড়া; MySQL ৮.০.১৭ থেকে OR অর্থে || deprecated. SQL Server-এ Microsoft সম্প্রতি (Azure SQL আর SQL Server 2025-এ) || যোগ করেছে বলে জানিয়েছে, তবে পুরোনো সংস্করণে এটি চলে না। SQLite-এ নিচে দুই রূপ আর NULL-এর আচরণ দেখুন। customers টেবিলে (customer_id, name, city, phone, joined_on) Tanvir Ahmed-এর phone আর Imran Hossain-এর city NULL:

SELECT customer_id,
       name || ' (' || city || ')'         AS with_pipes,
       concat(name, ' (', city, ')')       AS with_concat,
       concat_ws(' — ', name, city, phone) AS with_ws
FROM customers
WHERE customer_id IN (1, 5, 7);

তিনটি সারি। Rahim Uddin-এর সারিতে প্রথম দুটি কলাম একই — Rahim Uddin (Dhaka), আর তৃতীয়টি Rahim Uddin — Dhaka — 01711000001. Tanvir Ahmed-এর phone NULL, তাই শুধু with_ws বদলায়: Tanvir Ahmed — Khulna — concat_ws NULL বাদ দিয়ে বাকিগুলো জোড়ে। আসল পার্থক্য Imran Hossain-এ (city NULL): with_pipes = NULL, কারণ || NULL পেলেই পুরো ফল NULL; with_concat = Imran Hossain (), কারণ SQLite আর PostgreSQL-এর concat() NULL-কে খালি লেখা ধরে। MySQL-এর CONCAT এখানে NULL দিত — একই নামের ফাংশন, ভিন্ন আচরণ।

প্রথম n সারি — LIMIT, TOP আর FETCH FIRST

“সবচেয়ে দামি ৩টি পণ্য” — পর্ব ১৫-এ LIMIT শিখেছেন। কিন্তু এটি standard নয়: SQL:2008-এর রূপ FETCH FIRST. নিচের ডায়াগ্রামে একটি MySQL কোয়েরিকে ধাপে ধাপে অন্য চারটি DBMS-এ নিয়ে যাওয়া দেখুন। কাজ: মজুত আছে এমন সবচেয়ে দামি ৩টি পণ্য, নামের পাশে শ্রেণির নাম।

কোয়েরিলাল = এই ধাপে বদলানো অংশ

            
অবস্থা
DBMS
প্রথম ৩ সারি
কী বদলাল
  1. মূল কোয়েরি MySQL-এ। নামগুলো backtick-এ মোড়া (কিছু টুল এভাবেই কোড বানায়), জোড়া CONCAT দিয়ে, শেষে LIMIT 3. table alias-এ AS লেখা হয়নি — পরে দেখবেন কেন এটি বুদ্ধিমানের কাজ।
  2. PostgreSQL-এ একটিই বদল। backtick PostgreSQL চেনে না, তাই সরিয়ে দিলাম (উদ্ধৃতি লাগলে লিখতাম double quote). CONCAT PostgreSQL ৯.১ থেকে আছে, আর LIMIT-ও চলে — বাকি সব হুবহু।
  3. SQL Server-এ LIMIT নেই। পুরোনো রীতি হলো SELECT-এর ঠিক পরে TOP (3). ORDER BY না দিলে TOP “যেকোনো” ৩টি দেয় — কোন ৩টি, তার নিশ্চয়তা নেই।
  4. SQL Server 2012 থেকে standard-ঘেঁষা রূপ। ORDER BY-এর পরে OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY. এখানে ORDER BY বাধ্যতামূলক, আর OFFSET অংশটিও লিখতে হয় — pagination-এ এটিই সুবিধাজনক।
  5. Oracle-এ দুটি বদল। Oracle-এর CONCAT ঠিক দুটি argument নেয়, তাই তিনটি টুকরো জুড়তে একটির ভেতরে আরেকটি — অথবা || ব্যবহার করুন, যা Oracle-এ স্বাভাবিক। আর ১২c থেকে FETCH FIRST 3 ROWS ONLY; তার আগে লাগত ROWNUM-এর কৌশল। Oracle table alias-এ AS মানে না — products AS p লিখলে ত্রুটি, তাই প্রথম থেকেই products p.
  6. SQLite-এ MySQL-এর মতোই LIMIT. CONCAT এখানে ৩.৪৪ থেকে আছে; পুরোনো SQLite-এ ||. ফল তিনটি সারি — Mobile Phone, Saree, Smart Watch (Power Bank-এর মজুত 0, তাই বাদ)। মূল যুক্তি — JOIN, WHERE, ORDER BY — একবারও বদলায়নি; বদলেছে শুধু প্রান্তগুলো।

একটিই রূপ কি সব জায়গায় চলে? না। FETCH FIRST চলে PostgreSQL, Oracle ১২c+ আর (OFFSET-সহ) SQL Server-এ; MySQL আর SQLite-এ চলে না। LIMIT চলে MySQL, PostgreSQL আর SQLite-এ। এখন একই কোয়েরি SQLite-এ, আর পরের “পাতা” (OFFSET) সহ — products টেবিল (product_id, name, category_id, price, stock) আর categories (category_id, name):

SELECT p.name || ' - ' || c.name AS label, p.price
FROM products p
JOIN categories c ON c.category_id = p.category_id
WHERE p.stock > 0
ORDER BY p.price DESC
LIMIT 3;

-- পরের পাতা: প্রথম ৩টি বাদ দিয়ে পরের ৩টি
SELECT p.name || ' - ' || c.name AS label, p.price
FROM products p JOIN categories c ON c.category_id = p.category_id
WHERE p.stock > 0
ORDER BY p.price DESC
LIMIT 3 OFFSET 3;

প্রথম ফল stepper-এর শেষ ধাপের মতো: Mobile Phone - Electronics | 15000, Saree - Clothing | 4500, Smart Watch - Electronics | 3500. দ্বিতীয় ফল: Panjabi - Clothing | 2200, Headphone - Electronics | 1200, DSA Book - Books | 650 — Power Bank (1800) মজুত না থাকায় বাদ। MySQL-এ একই কাজ LIMIT 3, 3 লিখেও হয় (প্রথম সংখ্যা offset) — এই ক্রমটি উল্টো বলে অনেক ভুল হয়; LIMIT 3 OFFSET 3 রূপটি তিনটি DBMS-এই চলে।

অন্য উপভাষার শব্দ SQLite-এ লিখলে কী হয়? DBMS সাধারণত স্পষ্ট ত্রুটি দেয় — প্রথম ঝুড়ির পার্থক্য ধরা এ কারণেই সহজ। এডিটর প্রথম ত্রুটিতে থেমে যায়, তাই ভুল স্টেটমেন্টটি শেষে রাখা হয়েছে:

-- SQLite-এর রূপ: চলে
SELECT name FROM products ORDER BY price DESC LIMIT 1;

-- SQL Server-এর রূপ: SQLite চেনে না
SELECT TOP 3 name FROM products ORDER BY price DESC;

প্রথম স্টেটমেন্ট একটি সারি দেয়: Mobile Phone. দ্বিতীয়টিতে ত্রুটি: near "3": syntax error. SQLite TOP-কে keyword নয়, একটি কলামের নাম ভেবেছে (SELECT TOP = “TOP নামের কলাম”), তারপর একটি সংখ্যা পেয়ে বিভ্রান্ত। ত্রুটির বার্তা তাই “TOP চিনি না” বলে না — উপভাষার ভুলে বার্তা প্রায়ই এমন পরোক্ষ হয়। একইভাবে FETCH FIRST 3 ROWS ONLY লিখলে আসে near "FETCH": syntax error.

৪ · ফাংশন আর টাইপ — তারিখ, লেখা, boolean আর ভাগ

পর্ব ১৭-এ দেখেছেন তারিখের ফাংশনে DBMS-গুলো সবচেয়ে বেশি আলাদা। এখানে পাঁচটি পাশাপাশি — নামগুলো মুখস্থ করার দরকার নেই, “একই কাজের নাম আলাদা হতে পারে” এই সচেতনতাই আসল।

কাজMySQLPostgreSQLSQLiteSQL ServerOracle
আজকের তারিখCURDATE()CURRENT_DATEdate('now') (UTC)CAST(GETDATE() AS date)TRUNC(SYSDATE)
এখনকার মুহূর্তNOW()now()datetime('now')SYSDATETIME()SYSTIMESTAMP
৭ দিন যোগd + INTERVAL 7 DAYd + INTERVAL '7 days'date(d, '+7 days')DATEADD(day, 7, d)d + 7
দুই তারিখের ব্যবধান (দিন)DATEDIFF(a, b)a - bjulianday(a) - julianday(b)DATEDIFF(day, b, a)a - b
অংশ কাটাSUBSTRING / SUBSTRsubstring / substrsubstr / substring (৩.৩৪+)SUBSTRING শুধুSUBSTR শুধু
দৈর্ঘ্যCHAR_LENGTH (অক্ষর), LENGTH (বাইট)length (অক্ষর)length (অক্ষর)LEN (শেষের space বাদ), DATALENGTH (বাইট)LENGTH (অক্ষর), LENGTHB (বাইট)
NULL হলে বিকল্পIFNULL, COALESCECOALESCEifnull, coalesceISNULL, COALESCENVL, COALESCE
BooleanBOOLEAN = TINYINT(1), TRUE = 1আসল boolean টাইপটাইপ নেই; TRUE/FALSE = 1/0টাইপ নেই; BIT (0/1)২৩ai থেকে BOOLEAN; আগে শুধু PL/SQL-এ
7 / 23.5000 (7 DIV 2 = 3)3333.5
FROM ছাড়া SELECTSELECT 1;SELECT 1;SELECT 1;SELECT 1;SELECT 1 FROM dual; (২৩ai থেকে FROM ছাড়াও)

দুটি সারি বিশেষভাবে মনে রাখুন। COALESCE পাঁচটিতেই আছে — IFNULL, ISNULL, NVL-এর বদলে এটি লিখলে কোড portable থাকে। আর integer division: PostgreSQL, SQLite আর SQL Server-এ দুটি পূর্ণসংখ্যার ভাগ পূর্ণসংখ্যা, MySQL আর Oracle-এ দশমিক। গড় দাম বা শতাংশের রিপোর্ট MySQL থেকে PostgreSQL-এ নিলে ত্রুটি আসবে না — শুধু সংখ্যাগুলো চুপচাপ ছোট হয়ে যাবে। নিরাপদ রূপ: CAST(a AS DECIMAL(12,2)) / b বা a * 1.0 / b. Oracle-এর dual হলো এক সারির একটি বিশেষ টেবিল (DUAL), কারণ পুরোনো Oracle-এ প্রতিটি SELECT-এ FROM বাধ্যতামূলক ছিল।

এবার তারিখের হিসাব SQLite-এর ভাষায়, orders টেবিলে (order_id, customer_id, order_date, status) — উপরের টেবিলের “৭ দিন যোগ”, “ব্যবধান” আর “মাসের শুরু” একসাথে:

SELECT order_id, order_date,
       date(order_date, '+7 days')                     AS due_date,
       date(order_date, 'start of month')              AS month_start,
       julianday('2025-10-09') - julianday(order_date) AS days_before,
       strftime('%Y-%m', order_date)                   AS ym
FROM orders
WHERE order_id IN (1001, 1006, 1010);

তিনটি সারি: 1001 | 2025-09-01 | 2025-09-08 | 2025-09-01 | 38 | 2025-09, 1006 | 2025-09-20 | 2025-09-27 | 2025-09-01 | 19 | 2025-09, 1010 | 2025-10-09 | 2025-10-16 | 2025-10-01 | 0 | 2025-10. SQLite-এ ব্যবধানটি আসলে REAL (38.0), কারণ julianday দশমিক সংখ্যা দেয় — এডিটর শুধু 38 দেখায়, আর ঘণ্টা-মিনিটসহ সময় হলে ভগ্নাংশও আসবে; PostgreSQL-এ দুটি date বিয়োগ করলে পূর্ণসংখ্যা 38, MySQL-এর DATEDIFF('2025-10-09', '2025-09-01')-ও 38. আর SQL Server-এর DATEDIFF(day, b, a)-এ argument-এর ক্রম MySQL-এর উল্টো — “শুরু, তারপর শেষ” — port করার সময় চিহ্ন উল্টে যাওয়ার প্রচলিত কারণ এটিই।

৫ · ডেটা লেখার পার্থক্য — UPSERT, MERGE আর RETURNING

“থাকলে হালনাগাদ করো, না থাকলে নতুন ঢোকাও” — একে বলে UPSERT (পর্ব ১৪)। ধরুন গুদামে নতুন চালান এল: পুরোনো পণ্যের মজুত বাড়বে, নতুন পণ্য যোগ হবে। Standard-এর উত্তর MERGE (SQL:2003), কিন্তু MySQL, PostgreSQL আর SQLite আগে নিজের ছোট রূপ বানিয়েছিল:

INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 10) AS new
ON DUPLICATE KEY UPDATE stock = stock + new.stock;
-- পুরোনো রূপ: ... UPDATE stock = stock + VALUES(stock)  (৮.০.২০ থেকে deprecated)
-- RETURNING নেই; নতুন id: LAST_INSERT_ID()
INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 10)
ON CONFLICT (product_id) DO UPDATE
  SET stock = products.stock + EXCLUDED.stock
RETURNING product_id, stock;
-- PostgreSQL 15 থেকে standard MERGE-ও আছে
INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 10)
ON CONFLICT (product_id) DO UPDATE
  SET stock = stock + excluded.stock
RETURNING product_id, stock;
-- সাবধান: INSERT OR REPLACE পুরো সারি মুছে নতুন বসায়
MERGE INTO products AS t
USING (VALUES (102, 'Headphone', 1, 1200, 10))
      AS s (product_id, name, category_id, price, stock)
ON t.product_id = s.product_id
WHEN MATCHED THEN UPDATE SET t.stock = t.stock + s.stock
WHEN NOT MATCHED THEN INSERT (product_id, name, category_id, price, stock)
     VALUES (s.product_id, s.name, s.category_id, s.price, s.stock)
OUTPUT $action, inserted.product_id, inserted.stock;
MERGE INTO products t
USING (SELECT 102 AS product_id, 'Headphone' AS name, 1 AS category_id,
              1200 AS price, 10 AS stock FROM dual) s
ON (t.product_id = s.product_id)
WHEN MATCHED THEN UPDATE SET t.stock = t.stock + s.stock
WHEN NOT MATCHED THEN INSERT (product_id, name, category_id, price, stock)
     VALUES (s.product_id, s.name, s.category_id, s.price, s.stock);

লক্ষ করুন MERGE-এর ON-এ যে শর্ত, ON CONFLICT-এ তার জায়গায় একটি UNIQUE বা PRIMARY KEY লাগে — DBMS সেই index দিয়েই সংঘাত ধরে। আরেকটি বাস্তব সতর্কতা: অনেক সারি একসাথে চললে MERGE নিজে থেকে race condition থেকে মুক্ত নয়; SQL Server-এ অভিজ্ঞরা প্রায়ই উপযুক্ত lock hint যোগ করতে বলেন। এখন SQLite-এ চালিয়ে দেখুন — Headphone (102)-এর মজুত 40, আর 111 নম্বরে কোনো পণ্য নেই:

INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 10),
       (111, 'USB Cable', 1,  250, 50)
ON CONFLICT (product_id) DO UPDATE
   SET stock = stock + excluded.stock,
       price = excluded.price;

SELECT product_id, name, price, stock
FROM products WHERE product_id IN (102, 111);

প্রথম স্টেটমেন্ট বলে ২টি সারি বদলেছে — একটি UPDATE, একটি INSERT. ফল: 102 | Headphone | 1200 | 50 (40 + 10) আর 111 | USB Cable | 250 | 50 (নতুন সারি)। excluded হলো “যে সারিটি ঢুকতে চেয়েছিল” — PostgreSQL-এও একই নাম। SET-এর ভেতরে খালি stock মানে টেবিলের বর্তমান মান। MySQL-এ এর সমতুল্য new.stock (বা পুরোনো VALUES(stock)).

বদলানো সারি ফেরত পাওয়া — RETURNING আর OUTPUT

নতুন অর্ডার ঢোকানোর পর অ্যাপের জানা দরকার এর order_id কত। আলাদা SELECT না চালিয়ে একই স্টেটমেন্টে ফেরত পাওয়ার উপায় হলো RETURNING — PostgreSQL (৮.২ থেকে), SQLite (৩.৩৫ থেকে) আর MariaDB-তে (DELETE-এ, আর ১০.৫ থেকে INSERT-এ)। SQL Server-এ একই কাজ করে OUTPUT inserted.* / deleted.*; Oracle-এ PL/SQL-এর ভেতরে RETURNING … INTO চলক; MySQL-এ নেই — সেখানে LAST_INSERT_ID().

-- নতুন অর্ডার: id DBMS নিজে দেবে, ফেরত চাই
INSERT INTO orders (customer_id, order_date, status)
VALUES (3, '2025-10-10', 'pending')
RETURNING order_id, customer_id, status;

-- বই-শ্রেণিতে ১০% ছাড়: কোন সারি কত হলো?
UPDATE products SET price = price * 0.9
WHERE category_id = 2
RETURNING product_id, name, price;

প্রথম ফলে একটি সারি: 1011 | 3 | pending — সবচেয়ে বড় order_id ছিল 1010, তাই SQLite পরের নম্বর 1011 দিয়েছে, আর RETURNING সেটি সাধারণ SELECT-এর মতো ফলের টেবিলে দেখিয়েছে। দ্বিতীয় ফলে দুটি সারি: 104 | DSA Book | 585 আর 105 | SQL Book | 495 — বদলের পরের মান। SQL Server-এর OUTPUT-এ deleted.price লিখে আগের মানও একসাথে পাওয়া যায়; PostgreSQL ১৮-এ RETURNING-এ OLD/NEW যোগ হয়েছে বলে জানা যায় — সংস্করণ দেখে ব্যবহার করুন।

৬ · নীরব পার্থক্য — নামের বড়-ছোট অক্ষর, তুলনা আর '' বনাম NULL

এবার দ্বিতীয় ঝুড়ি: যে পার্থক্যে কোনো ত্রুটি আসে না। প্রথমটি নামের বড়-ছোট অক্ষর। Standard বলে উদ্ধৃতি ছাড়া লেখা identifier বড় হাতের অক্ষরে বদলে যাবে — একে বলে case folding; Oracle তাই করে। PostgreSQL করে উল্টো — ছোট হাতে, আর এর অফিসিয়াল নথিই বলে এটি standard-এর সাথে মেলে না। উদ্ধৃতির ভেতরের নাম দুই জায়গাতেই হুবহু থাকে:

কমান্ড → DBMS-এর catalog-এ যা থাকেলাল = এই ধাপের ফল

            
অবস্থা
DBMS
উদ্ধৃতি?
ফল
  1. উদ্ধৃতি ছাড়া নাম দিয়ে টেবিল বানানো। আপনি লিখেছেন Customers, CustomerID — কিন্তু PostgreSQL catalog-এ রাখে customers, customerid, fullname. বড় হাতের অক্ষরগুলো কোথাও জমা হয়নি।
  2. পড়ার সময় যেভাবে খুশি লিখুন। CustomerID, FULLNAME, CUSTOMERS — সব আবার ছোট হাতে বদলে catalog-এর সাথে মেলে। তাই উদ্ধৃতি না দিলে বড়-ছোট অক্ষর কোনো ব্যাপার নয়।
  3. উদ্ধৃতি দিলেই নিয়ম বদলায়। "CustomerID" মানে “ঠিক এই বানানে খোঁজো” — কিন্তু জমা আছে customerid. ফল ত্রুটি। অন্য DBMS থেকে আনা কোয়েরিতে উদ্ধৃতি থাকলে এটিই ঘটে।
  4. উল্টো দিক: বানানোর সময় উদ্ধৃতি। কিছু GUI টুল, migration স্ক্রিপ্ট বা ORM নাম উদ্ধৃত করে টেবিল বানায়। তখন OrderItems আর OrderID হুবহু, বড় হাতসহ জমা হয়।
  5. এখন উদ্ধৃতি ছাড়া পড়লে পাওয়া যায় না। OrderItems ছোট হাতে বদলে হয় orderitems, যা catalog-এ নেই। এই টেবিলের প্রতিটি কোয়েরিতে চিরকাল double quote লাগবে — PostgreSQL-এ নতুনদের খুব প্রচলিত বিভ্রান্তি।
  6. Oracle standard মেনে বড় হাতে বদলায়। একই CREATE TABLE-এ জমা হয় CUSTOMERS, CUSTOMERID; তাই "Customers" দিয়ে খুঁজলে ত্রুটি। MySQL-এ কলামের নাম বড়-ছোট নির্বিশেষে মেলে, কিন্তু টেবিলের নাম ফাইল হিসেবে থাকে বলে Linux-এ সাধারণত আলাদা ধরা হয়, Windows-এ নয় (lower_case_table_names সেটিং)। SQL Server-এ নির্ভর করে ডেটাবেসের collation-এর ওপর। নিয়ম একটাই: সব নাম ছোট হাতে snake_case-এ, আর কখনো উদ্ধৃতি নয়।

লেখার তুলনা — collation

নামের নিয়মের চেয়েও বেশি প্রভাব ফেলে ডেটার তুলনা। WHERE city = 'dhaka' কি 'Dhaka'-কে খুঁজে পাবে? উত্তর ঠিক করে collation. MySQL ৮-এর ডিফল্ট collation (utf8mb4_0900_ai_ci) বড়-ছোট অক্ষর আর accent দুটিই উপেক্ষা করে — তাই পাবে। SQL Server-এর প্রচলিত ডিফল্ট collation-ও case-insensitive. PostgreSQL, Oracle আর SQLite ডিফল্টে case-sensitive — পাবে না। MySQL থেকে PostgreSQL-এ যাওয়া একটি লগইন পাতায় হঠাৎ “Rahim@Mail.com” দিয়ে ঢোকা বন্ধ হয়ে যেতে পারে, কোনো ত্রুটি ছাড়াই। সমাধান: তুলনার আগে দুই দিকে lower() (আর তার ওপর expression index), বা PostgreSQL-এ citext টাইপ / ILIKE.

Oracle-এ খালি লেখা মানেই NULL

সবচেয়ে বিখ্যাত নীরব পার্থক্য: Oracle-এ VARCHAR2 কলামে '' (খালি লেখা) রাখলে সেটি NULL হয়ে যায়। পর্ব ১৬-এ শিখেছেন NULL মানে “অজানা”, আর খালি লেখা মানে “জানা, কিন্তু কিছু নেই” — standard আর অন্য চার DBMS এই দুটিকে আলাদা রাখে। একটি contacts টেবিলে (id, phone) তিনটি সারি দিয়ে ধাপে ধাপে দেখুন:

Oracle │ PostgreSQLলাল = যেখানে দুটি আলাদা

            
ফলের সংখ্যা
Oracle
PostgreSQL
  1. একই তিনটি INSERT. একজনের ফোন নম্বর জানা, একজন ফর্মে ঘর খালি রেখেছেন (''), একজনের তথ্য আসেইনি (NULL). অর্থের দিক থেকে তিনটি আলাদা অবস্থা।
  2. Oracle দুটি অবস্থা এক করে ফেলে। PostgreSQL তিনটিকেই যেমন দেওয়া হয়েছে তেমন রাখে; Oracle সারি ২-এর ''-কে NULL বানায়। “খালি রেখেছেন” আর “তথ্য নেই” এখন আর আলাদা করা যায় না।
  3. = '' দিয়ে খোঁজা। PostgreSQL সারি ২ পায়। Oracle-এ শর্তটি আসলে phone = NULL, যা তিন-মানের যুক্তিতে সবসময় UNKNOWN — তাই কোনো সারিই নয়। কোনো ত্রুটি আসে না।
  4. IS NULL দিয়ে খোঁজা। Oracle পায় ২ আর ৩, PostgreSQL শুধু ৩। “কতজনের ফোন নেই” রিপোর্ট দুই DBMS-এ ভিন্ন সংখ্যা দেবে।
  5. ফাংশনেও ছড়ায়। Oracle-এ LENGTH('') NULL, PostgreSQL-এ 0. COUNT(phone) NULL বাদ দেয়, তাই Oracle-এ 1, PostgreSQL-এ 2. আর উল্টো সুবিধাও আছে: Oracle-এ 'a' || NULL = 'a', কারণ জোড়ার সময় NULL-কে খালি লেখা ধরা হয়।
  6. NOT NULL কলামে খালি লেখা। Oracle-এ '' মানেই NULL, তাই NOT NULL কলামে ঢুকতে দেয় না (ORA-01400). PostgreSQL থেকে Oracle-এ, বা উল্টো দিকে ডেটা সরালে এখানেই হাজার হাজার সারি আটকে যেতে পারে। Oracle-এর নথি নিজেই সতর্ক করে, ভবিষ্যতে এই আচরণ না-ও থাকতে পারে — তাই খালি লেখাকে NULL-এর বদলি হিসেবে ব্যবহারে ভরসা করবেন না।

এবার দেখুন এই নীরব পার্থক্যগুলোতে SQLite কোন পথ নিয়েছে — নিজের চোখে, লাইভ এডিটরে:

SELECT '' IS NULL   AS empty_is_null,
       length('')   AS len_empty,
       7 / 2        AS int_div,
       TRUE + TRUE  AS true_sum,
       typeof(TRUE) AS bool_type;

SELECT 'Dhaka' = 'dhaka'                AS binary_cmp,
       'Dhaka' = 'dhaka' COLLATE NOCASE AS nocase_cmp;

SELECT COUNT(*) AS no_phone    FROM customers WHERE phone IS NULL;
SELECT COUNT(*) AS empty_phone FROM customers WHERE phone = '';

প্রথম ফল: 0 | 0 | 3 | 2 | integer — SQLite-এ খালি লেখা NULL নয় আর তার দৈর্ঘ্য 0 (PostgreSQL-এর মতো, Oracle-এর উল্টো); ভাগ পূর্ণসংখ্যায়; আর TRUE আসলে পূর্ণসংখ্যা 1, তাই TRUE + TRUE = 2 — boolean টাইপ নেই। দ্বিতীয় ফল: 0 | 1 — ডিফল্ট BINARY collation বড়-ছোট অক্ষর আলাদা ধরে; NOCASE দিলে মেলে (তবে শুধু ইংরেজি A–Z-এ)। শেষ দুটি: 1 আর 0 — Tanvir Ahmed-এর phone NULL, খালি লেখা কারও নেই। একই ডেটা Oracle-এ আনা হলে আর কেউ খালি লেখা দিয়ে থাকলে প্রথম সংখ্যাটি বেড়ে যেত।

৭ · ক্ষমতার পার্থক্য — GROUP BY, recursive CTE, FULL JOIN, procedural ভাষা আর JSON

GROUP BY কতটা কড়া

পর্ব ১৮-এ দেখেছেন, GROUP BY-এ নেই আর aggregate-এর ভেতরেও নেই এমন কলাম SELECT-এ লিখলে কী হয়। Standard বলে — নিষেধ, যদি না কলামটি group-এর কলামের ওপর functionally নির্ভরশীল হয়। SQL Server আর Oracle কড়া: সরাসরি ত্রুটি। PostgreSQL (৯.১ থেকে) ছাড় দেয় শুধু যখন GROUP BY-তে টেবিলের primary key আছে। MySQL ৫.৭.৫ থেকে ONLY_FULL_GROUP_BY ডিফল্টে চালু; তার আগে যেকোনো কলাম চলত, মান আসত দলের যেকোনো সারি থেকে। আর SQLite এখনো ঢিলা — bare column মেনে নেয়:

-- প্রতিটি শ্রেণির সবচেয়ে দামি পণ্য — SQLite-এর বিশেষ সুবিধায়
SELECT category_id, name, MAX(price) AS top_price
FROM products
GROUP BY category_id;

চারটি সারি: 1 | Mobile Phone | 15000, 2 | DSA Book | 650, 3 | Saree | 4500, 4 | Rice 5kg | 420. name GROUP BY-তে নেই, তবু SQLite চালিয়েছে — আর এর নথি নিশ্চয়তা দেয় যে একটিমাত্র MIN() বা MAX() থাকলে bare column-এর মান আসে সেই সারি থেকে, যেখানে সর্বোচ্চ বা সর্বনিম্ন মানটি পাওয়া গেছে। এই কোয়েরি PostgreSQL, SQL Server, Oracle আর ডিফল্ট সেটিংয়ের MySQL-এ ত্রুটি দেবে। Portable সমাধান হলো window function (পর্ব ২৬) বা subquery দিয়ে “প্রতি দলের সেরা সারি” — পর্ব ২৭-এর top-N প্যাটার্ন।

Recursive CTE — RECURSIVE শব্দটি লাগবে কি না

পর্ব ২৫-এর recursive query standard-এ লেখা হয় WITH RECURSIVE দিয়ে; PostgreSQL, MySQL ৮, MariaDB আর SQLite তাই মানে। কিন্তু SQL Server (২০০৫ থেকে) আর Oracle (১১g Release 2 থেকে) recursive CTE লেখে শুধু WITH দিয়ে — SQL Server-এ RECURSIVE লিখলে উল্টো ত্রুটি। Oracle-এর এর চেয়ে পুরোনো নিজস্ব পথও আছে: CONNECT BY PRIOR. SQLite-এ একটি প্রচলিত কাজ — রিপোর্টে যে দিনে অর্ডার নেই, সেই দিনও ০ দিয়ে দেখানো:

WITH RECURSIVE days(d) AS (
  SELECT '2025-09-01'
  UNION ALL
  SELECT date(d, '+1 day') FROM days WHERE d < '2025-09-07'
)
SELECT d AS day, COUNT(o.order_id) AS orders
FROM days LEFT JOIN orders o ON o.order_date = d
GROUP BY d
ORDER BY d;

সাতটি সারি, ১ থেকে ৭ সেপ্টেম্বর: 2025-09-01 | 1, 2025-09-02 | 0, 2025-09-03 | 1, তারপর ৪–৭ তারিখে সব 0. CTE প্রথমে একটি দিন বানায়, তারপর প্রতিবার আগের দিনের সাথে এক দিন যোগ করে যতক্ষণ না ৭ তারিখে পৌঁছায়। SQL Server-এ একই কাজে RECURSIVE শব্দটি বাদ যাবে আর তারিখ যোগ হবে DATEADD(day, 1, d) দিয়ে; PostgreSQL-এ এমন কাজ আরও সহজে করে generate_series() — আরেকটি vendor extension.

WITH days (d) AS (                       -- RECURSIVE লেখা যাবে না
  SELECT CAST('2025-09-01' AS date)
  UNION ALL
  SELECT DATEADD(day, 1, d) FROM days WHERE d < '2025-09-07'
)
SELECT d FROM days
OPTION (MAXRECURSION 100);               -- ডিফল্ট সীমা 100 ধাপ

FULL OUTER JOIN

পর্ব ১৯-এর FULL OUTER JOIN PostgreSQL, SQL Server আর Oracle-এ বহু আগে থেকে আছে; SQLite-এ এসেছে ৩.৩৯ (২০২২) সংস্করণে, RIGHT JOIN-সহ। MySQL আর MariaDB-তে এখনো নেই — সেখানে LEFT JOIN আর একটি anti join-এর UNION ALL দিয়ে বানাতে হয়:

SELECT c.name, o.order_id
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
UNION ALL
SELECT NULL, o.order_id
FROM orders o LEFT JOIN customers c ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;     -- শুধু ডান দিকের মিলহীন সারি

Procedural ভাষা আর JSON

SQL নিজে declarative — “কী চাই” বলে। কিন্তু loop, চলক আর শর্তসহ server-এ চলা প্রোগ্রাম (stored procedure, পর্ব ২৮) লিখতে প্রতিটি DBMS নিজের ভাষা বানিয়েছে, আর এখানেই উপভাষা সবচেয়ে বেশি আলাদা — একটির procedure আরেকটিতে প্রায় নতুন করে লিখতে হয়:

DBMSProcedural ভাষাJSON
MySQLStored procedure, function আর trigger — standard-এর SQL/PSM ধাঁচের সিনট্যাক্সJSON টাইপ (৫.৭.৮ থেকে), ->, ->>, JSON_EXTRACT
PostgreSQLPL/pgSQL; extension হিসেবে PL/Python, PL/Perl ইত্যাদিjson আর binary jsonb (৯.৪ থেকে), @> অপারেটর, GIN index
SQLiteনেই; trigger আছে, আর অ্যাপের ভাষায় (Python, C) নিজের ফাংশন যোগ করা যায়JSON ফাংশন ৩.৩৮ থেকে ডিফল্টে, ->, ->>; ৩.৪৫ থেকে JSONB
SQL ServerT-SQL — ভাষাটিই procedural; আলাদা ব্লক লাগে না২০১৬ থেকে JSON ফাংশন (OPENJSON, JSON_VALUE), লেখা হিসেবে রাখা; নতুন সংস্করণে আলাদা json টাইপ ঘোষিত
OraclePL/SQL — Ada ভাষার আদলে, package-সহ১২c থেকে JSON ফাংশন, ২১c থেকে আলাদা JSON টাইপ

JSON-এর standard রূপ (SQL:2016-এর JSON_VALUE, JSON_QUERY ইত্যাদি) এসেছে অনেক DBMS নিজের অপারেটর বানিয়ে ফেলার পরে, তাই ->>-এর মতো সংক্ষিপ্ত রূপ সুবিধাজনক হলেও portable নয়। বিস্তারিত পর্ব ৩০-এ।

৮ · Transaction-এর পার্থক্য — DDL-এর commit আর ডিফল্ট isolation

পর্ব ৪৪-এ transaction বিস্তারিত শিখবেন; এখানে শুধু দুটি পার্থক্য, যা migration স্ক্রিপ্ট লেখার সময়ই কামড়ায়। প্রথমটি: CREATE TABLE বা ALTER TABLE কি ROLLBACK করা যায়? PostgreSQL আর SQLite-এ যায় — একে বলে transactional DDL. তাই দশটি schema বদলের একটি migration-এর মাঝপথে ত্রুটি হলে সব ফিরে যায়। SQL Server-এও বেশিরভাগ DDL transaction-এর ভেতরে ফেরানো যায়। কিন্তু MySQL আর Oracle-এ DDL চালানোর আগে আর পরে নিজে থেকে commit হয় (implicit commit): মাঝপথে থামলে অর্ধেক বদল থেকে যায়। SQLite-এ পরীক্ষা করুন:

BEGIN;
CREATE TABLE coupons (code TEXT PRIMARY KEY, pct INTEGER);
INSERT INTO coupons VALUES ('EID10', 10);
SELECT COUNT(*) AS inside_tx FROM coupons;
ROLLBACK;

-- টেবিলটি কি আছে? SQLite-এর catalog দেখি
SELECT COUNT(*) AS coupons_table_exists
FROM sqlite_master WHERE name = 'coupons';

transaction-এর ভেতরে inside_tx = 1 — টেবিল তৈরি হয়েছে, একটি সারিও আছে। ROLLBACK-এর পর coupons_table_exists = 0: শুধু সারি নয়, টেবিলটিই নেই। একই স্ক্রিপ্ট MySQL-এ চালালে CREATE TABLE নিজেই আগের সবকিছু commit করে দিত, আর ROLLBACK-এর পরও টেবিলটি থেকে যেত — এমনকি INSERT-ও থেকে যেত, কারণ CREATE TABLE চলমান transaction শেষ করে দেয়, আর autocommit চালু থাকলে পরের INSERT নিজেই commit হয়ে যায়। MySQL ৮-এর “atomic DDL” মানে একটি DDL স্টেটমেন্ট পুরো হয় বা একেবারেই হয় না — কয়েকটিকে একসাথে ফেরানো নয়।

দ্বিতীয়টি: একসাথে অনেক transaction চললে একটি আরেকটির বদল কতটা দেখবে, তা ঠিক করে isolation level (পর্ব ৪৭)। ডিফল্টগুলো আলাদা, আর একই অ্যাপ এক DBMS থেকে আরেকটিতে নিলে concurrency-র আচরণ চুপচাপ বদলে যায়:

DBMSDDL ROLLBACK করা যায়?ডিফল্ট isolationমন্তব্য
PostgreSQLহ্যাঁ (কিছু ব্যতিক্রম, যেমন CREATE DATABASE)READ COMMITTEDMVCC; SERIALIZABLE বাস্তবায়িত SSI দিয়ে
MySQL (InnoDB)না — implicit commitREPEATABLE READসাধারণ SELECT একটি snapshot পড়ে; locking read-এ gap lock
SQL Serverবেশিরভাগ ক্ষেত্রে হ্যাঁREAD COMMITTEDডিফল্টে lock-ভিত্তিক; READ_COMMITTED_SNAPSHOT চালু করলে MVCC-ধাঁচ (Azure SQL Database-এ ডিফল্টে চালু)
Oracleনা — implicit commitREAD COMMITTEDবহু দশক ধরে multiversion read consistency; পাঠক লেখককে আটকায় না
SQLiteহ্যাঁSERIALIZABLEএকসাথে একজনই লেখক, তাই স্বাভাবিকভাবে serializable

একই নামের level-ও সব DBMS-এ একই আচরণ করে না: PostgreSQL-এর REPEATABLE READ আসলে snapshot isolation, যা MySQL-এর REPEATABLE READ-এর চেয়ে কিছু ক্ষেত্রে কড়া; আর Oracle-এর SERIALIZABLE-ও snapshot-ভিত্তিক, পূর্ণ serializable নয়। কোন anomaly কোথায় সম্ভব, তা পর্ব ৪৭-এর বিষয়; এখানে শুধু মনে রাখুন — “ডিফল্টে চলছে” মানে সব DBMS-এ এক নয়।

আরও গভীরে: SQLite-এ SELECT "abc" লিখলে SQLite ত্রুটি দেয় না, abc লেখাটি ফেরত দেয় — কারণ double quote-এর নাম কোনো কলামের সাথে না মিললে SQLite পুরোনো MySQL-এর সাথে মানিয়ে নিতে সেটিকে string literal ধরে। SQLite-এর নথি নিজেই একে “misfeature” বলে, আর SQLITE_DQS compile option দিয়ে বন্ধ করার পরামর্শ দেয়। ফল: কলামের নামে বানান ভুল করলে (WHERE "stauts" = 'pending') ত্রুটি না এসে শর্তটি নীরবে দুটি লেখার তুলনা হয়ে যায়। Standard-এ double quote শুধু নামের জন্য, single quote শুধু লেখার জন্য — এই নিয়ম মানলে পাঁচ DBMS-এই নিরাপদ।

৯ · Portable SQL, migration আর বাংলাদেশে DBMS বাছাই

সব পার্থক্য জানার পর প্রশ্ন: এমন SQL কীভাবে লিখবেন যা যতটা সম্ভব সব জায়গায় চলে? একে বলে portable SQL. কয়েকটি অভ্যাস প্রায় বিনা খরচে বেশিরভাগ ঝামেলা দূর করে:

ORM (পর্ব ৫০) — যেমন Django ORM, SQLAlchemy, Hibernate বা Laravel-এর Eloquent — প্রতিটি DBMS-এর জন্য একটি “dialect” স্তর রাখে, যা LIMIT/TOP, quoting, id ফেরত পাওয়া আর টাইপের পার্থক্য নিজে সামলায়। কিন্তু তিনটি জিনিস ORM ঢাকে না: collation আর বড়-ছোট অক্ষরের তুলনা, Oracle-এর '', আর transaction/isolation-এর আচরণ। আর হাতে লেখা raw SQL তো ORM-এর বাইরেই থাকে। একটি প্রচলিত ভুল হলো দ্রুততার জন্য test চালানো SQLite-এ, অথচ production PostgreSQL — test সবুজ, কিন্তু production-এ GROUP BY বা collation-এর ত্রুটি।

MySQL থেকে PostgreSQL — migration-এর চেনা ফাঁদ

বাংলাদেশে অনেক প্রজেক্ট শেয়ার্ড হোস্টিংয়ে MySQL দিয়ে শুরু হয়, পরে VPS বা cloud-এ গিয়ে PostgreSQL-এ সরতে চায়। এই পথের প্রচলিত গর্তগুলো:

MySQL-এPostgreSQL-একী ঘটে
`backtick`, AUTO_INCREMENT"…" বা উদ্ধৃতিহীন, GENERATED … AS IDENTITYসিনট্যাক্স ত্রুটি — সহজে ধরা পড়ে
TINYINT(1) দিয়ে boolean, UNSIGNEDboolean; unsigned নেই (CHECK বা বড় টাইপ)WHERE active = 1 ধরনের কোড বদলাতে হয়
'0000-00-00' তারিখ (পুরোনো ডেটায়)অবৈধ তারিখডেটা লোড ব্যর্থ — আগে NULL-এ বদলাতে হয়
ডিফল্ট collation বড়-ছোট অক্ষর উপেক্ষা করেcase-sensitiveলগইন, খোঁজা আর UNIQUE নীরবে বদলায়
5 / 2 = 2.50005 / 2 = 2রিপোর্টের সংখ্যা নীরবে ভুল
ঢিলা GROUP BY (পুরোনো সেটিং)কড়া GROUP BYকোয়েরি ত্রুটি দেয় — লুকানো ভুল প্রকাশ পায়
ON DUPLICATE KEY UPDATE, LIMIT 10, 20, IFNULLON CONFLICT, LIMIT 20 OFFSET 10, COALESCEকোয়েরি নতুন করে লিখতে হয়
ডেটা কপির পর id চালু থাকেidentity/sequence-এর পরের মান পুরোনো জায়গায় থাকেপ্রথম নতুন INSERT-এ duplicate key ত্রুটি — setval() দিয়ে sequence এগিয়ে দিতে হয়

লক্ষ করুন, কিছু ফাঁদ ত্রুটি দেয় (ভালো — আপনি জানতে পারেন), কিছু চুপচাপ ফল বদলায় (বিপজ্জনক)। তাই migration-এর আসল পরীক্ষা হলো একই প্রশ্ন দুই DBMS-এ চালিয়ে ফল মেলানো — সারির সংখ্যা, যোগফল, কয়েকটি নির্দিষ্ট সারি। আর একটি পুরোনো সতর্কতা: MySQL ৮.০.১৬-এর আগে CHECK constraint পড়া হতো কিন্তু প্রয়োগ হতো না, তাই পুরোনো MySQL-এর ডেটায় এমন সারি থাকতে পারে যা PostgreSQL-এর CHECK আটকে দেবে। Schema বদলের নিয়ন্ত্রিত পদ্ধতি পর্ব ৫০-এর migration অংশে।

বাংলাদেশে প্রজেক্টের জন্য DBMS বাছাই

“সবচেয়ে ভালো DBMS কোনটি?” প্রশ্নের উত্তর নেই; আছে “এই প্রজেক্টের জন্য কোনটি?” নিচের চারটি প্রশ্ন ক্রমে জিজ্ঞেস করুন:

  1. অ্যাপ কোথায় চলবে? মোবাইল বা ডেস্কটপের ভেতরে, ইন্টারনেট ছাড়া — SQLite প্রায় স্বাভাবিক উত্তর। দেশের প্রচলিত cPanel শেয়ার্ড হোস্টিংয়ে সাধারণত MySQL বা MariaDB-ই দেওয়া থাকে; PostgreSQL কিছু হোস্টিংয়ে পাওয়া যায়, তবে VPS বা cloud-এ নিজে চালানোই বেশি প্রচলিত। প্যাকেজ কেনার আগে হোস্টিং কোম্পানির কাছে নিশ্চিত হয়ে নিন।
  2. দলের দক্ষতা কী? দলে যে DBMS ভালো জানা আছে, তাতে backup, index আর সমস্যা সমাধান দ্রুত হয়। PHP/Laravel আর WordPress-এর জগতে MySQL-এর অভিজ্ঞতা সহজলভ্য; Django, Node আর Spring-এর জগতে PostgreSQL ক্রমশ বেশি দেখা যায়।
  3. প্রতিষ্ঠানে আগে থেকে কী আছে? ব্যাংক, টেলিকম আর বড় প্রতিষ্ঠানে Oracle বা SQL Server প্রায়ই চালু থাকে — নতুন মডিউল সেই সিস্টেমের সাথে জুড়তে হলে, DBA আর লাইসেন্স আগেই থাকলে সেটিই বাস্তব পছন্দ।
  4. খরচ আর লাইসেন্স। ছোট ব্যবসা বা স্টার্টআপের জন্য open-source DBMS লাইসেন্সের খরচ বাঁচায়; বাণিজ্যিক DBMS-এ লাইসেন্স সাধারণত CPU core বা ব্যবহারকারী অনুযায়ী, আর বিনা মূল্যের সংস্করণে ডেটার আকার বা CPU-র সীমা থাকে।

চাকরির বাজার নিয়ে একটি সৎ কথা: দেশের চাকরির বিজ্ঞপ্তিতে MySQL, PostgreSQL, Oracle আর SQL Server — চারটির নামই নিয়মিত দেখা যায়, কোনটি কত শতাংশ তার নির্ভরযোগ্য পরিসংখ্যান আমাদের হাতে নেই, তাই কোনো সংখ্যা দিচ্ছি না। ব্যাংক আইটি-র লিখিত পরীক্ষা বা ল্যাবে কোন DBMS ব্যবহার হবে, তা প্রতিষ্ঠানভেদে আলাদা। সবচেয়ে নিরাপদ কৌশল: standard SQL গভীরভাবে শিখুন (এই কোর্সের মূল লক্ষ্য), একটি DBMS-এ হাত পাকান, আর বাকিগুলোর পার্থক্যের এই মানচিত্র মাথায় রাখুন — নতুন উপভাষা তখন কয়েক দিনের অভ্যাস।

প্রজেক্টযুক্তিসঙ্গত শুরুকেন
অফলাইনে চলা দোকানের হিসাবের অ্যাপSQLiteserver নেই, একটি ফাইল, ফোনে বা ল্যাপটপে চলে
cPanel হোস্টিংয়ে WordPress বা Laravel সাইটMySQL / MariaDBহোস্টিংয়ে দেওয়া, ফ্রেমওয়ার্কের ডিফল্ট
নতুন SaaS বা API, VPS/cloud-এPostgreSQLকড়া standard, JSON, transactional DDL, উদার লাইসেন্স
ব্যাংকের বিদ্যমান সিস্টেমের সাথে নতুন মডিউলপ্রতিষ্ঠানের Oracle বা SQL Serverসংযোগ, DBA, নিরাপত্তা নীতি আর লাইসেন্স আগে থেকেই আছে

১০ · AI-এর যুগে উপভাষা

AI টুল SQL লেখে দ্রুত, কিন্তু উপভাষার ক্ষেত্রে তার একটি নির্দিষ্ট দুর্বলতা আছে: আপনি DBMS আর সংস্করণ না বললে সে প্রায়ই কয়েকটি উপভাষা মিশিয়ে ফেলে — PostgreSQL-এর কোয়েরিতে MySQL-এর IFNULL, SQL Server-এর জন্য LIMIT, বা SQLite-এ নেই এমন ফাংশন। এর চেয়েও সূক্ষ্ম, কোয়েরি চলে কিন্তু নীরব পার্থক্যে (collation, '', integer division) ফল ভুল। নিচের ভুলগুলো সবচেয়ে বেশি দেখা যায়:

AI-এর লেখাসমস্যাকীভাবে ধরবেন
DBMS না বলায় মিশ্র সিনট্যাক্সত্রুটি, বা ভুল ফাংশনের ভুল অর্থপ্রতিটি ফাংশনের নাম লক্ষ্য DBMS-এর নথিতে খুঁজুন
MySQL-এ 'a' || 'b'ত্রুটি নয়, ফল 0একটি ছোট SELECT-এ নিজে চালান
PostgreSQL-এ WHERE email = ? লগইনেবড়-ছোট অক্ষর আলাদা — ব্যবহারকারী আটকে যানবড় হাতের অক্ষরসহ একটি পরীক্ষার সারি রাখুন
Oracle-এ WHERE phone = ''কখনো কোনো সারি দেয় নাIS NULL-এর সাথে সংখ্যা মিলিয়ে দেখুন
“migration transaction-এ মোড়ানো, নিরাপদ”MySQL/Oracle-এ DDL নিজে commit করেপরীক্ষার ডেটাবেসে মাঝপথে ইচ্ছা করে ত্রুটি ঘটিয়ে দেখুন
পুরোনো সংস্করণে নতুন feature (RETURNING, MERGE, FULL JOIN)production-এর সংস্করণে চলে নাSELECT version(); বা sqlite_version() দিয়ে সংস্করণ নিশ্চিত করুন

সবচেয়ে কাজের কৌশল: প্রথম লাইনেই DBMS আর সংস্করণ বলুন, আর AI-কে দিয়ে portable অংশ আর DBMS-নির্ভর অংশ আলাদা করিয়ে নিন:

আমার production ডেটাবেস PostgreSQL 16, আর লোকাল পরীক্ষা SQLite 3.4x-এ। টেবিল: products(product_id, name, category_id, price, stock), categories(category_id, name). ১. প্রতিটি শ্রেণির সবচেয়ে দামি পণ্য বের করার কোয়েরি দাও, যা দুই DBMS-এই একই ফল দেবে। SQLite-এর bare column সুবিধা ব্যবহার করবে না। ২. কোয়েরিতে যে অংশ PostgreSQL-নির্ভর বা SQLite-নির্ভর, প্রতিটির পাশে মন্তব্যে লিখে দাও। ৩. NULL, সমান দাম (tie), বড়-ছোট অক্ষর আর integer division — কোনোটিতে দুই DBMS ভিন্ন ফল দিতে পারে কি না, এক লাইনে করে বলো। ৪. যাচাইয়ের জন্য ৩টি পরীক্ষার INSERT দাও (একটি tie, একটি NULL category_id) আর প্রত্যাশিত ফল আগে লিখে দাও। কোনো কলামের নাম বা ফাংশন অনুমান করবে না; নিশ্চিত না হলে বলবে।

AI-এর দেওয়া SQL অন্য DBMS-এ নেওয়ার আগে — চেকলিস্ট

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

  • MySQL, SQL Server আর Oracle-এ প্রথম ১০টি সারি কীভাবে নেবেন? — MySQL-এ LIMIT 10; SQL Server-এ TOP (10) বা OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY; Oracle ১২c+-এ FETCH FIRST 10 ROWS ONLY (পুরোনোতে ROWNUM).
  • Oracle-এ '' আর NULL-এর সম্পর্ক কী? — VARCHAR2-এ খালি লেখা NULL হিসেবে রাখা হয়, তাই = '' কোনো সারি দেয় না, IS NULL দেয়।
  • Transactional DDL কী, কোন DBMS-এ আছে? — CREATE/ALTER-ও transaction-এর অংশ হয়ে ROLLBACK করা যায়; PostgreSQL আর SQLite-এ আছে, MySQL আর Oracle-এ DDL implicit commit করে।
  • MySQL InnoDB আর PostgreSQL-এর ডিফল্ট isolation level? — InnoDB-তে REPEATABLE READ, PostgreSQL-এ READ COMMITTED.
  • MySQL আর MariaDB কি একই? — না; MariaDB ২০০৯-এর fork, মূল SQL মেলে, কিন্তু JSON, RETURNING, sequence আর optimizer-এ দুটি আলাদা পথে গেছে।

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

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

প্র ০১Standard যখন আছেই, তখন DBMS কোম্পানিগুলো সেটি পুরোপুরি মেনে চলে না কেন? এতে কার লাভ, কার ক্ষতি?

প্রথম কারণ ইতিহাস। IBM-এর San Jose গবেষণাগারে ১৯৭০-এর দশকে System R প্রকল্পে Donald Chamberlin আর Raymond Boyce SEQUEL নামে ভাষাটি নকশা করেন। IBM গবেষণা প্রকাশ করছিল, আর সেই প্রকাশিত কাজ থেকে অনুপ্রাণিত হয়ে Larry Ellison-এর দল ১৯৭৯ সালে Oracle V2 বাজারে আনে — IBM-এর নিজের বাণিজ্যিক পণ্যেরও আগে। ১৯৮৬ সালে ANSI যখন প্রথম standard প্রকাশ করে, তখন সেটি মূলত তখনকার IBM-ধাঁচের SQL-এর একটি সাধারণ ভাগ লিখে রাখে। ততদিনে প্রতিটি vendor-এর গ্রাহকরা হাজারো প্রোগ্রাম লিখে ফেলেছেন। Standard-এর সাথে মেলাতে কোনো আচরণ বদলালে সেই প্রোগ্রামগুলো ভাঙবে — আর কোনো কোম্পানি নিজের গ্রাহকের অ্যাপ ভাঙতে চায় না। Backward compatibility তাই প্রায় সবসময় standard-এর চেয়ে বড় অগ্রাধিকার পায়।

দ্বিতীয় কারণ বাজার। একসময় যুক্তরাষ্ট্রের সরকারি সংস্থা NIST সরকারি কেনাকাটার জন্য SQL conformance পরীক্ষা চালাত, যা vendor-দের standard মানতে চাপ দিত; ১৯৯০-এর দশকের মাঝামাঝি সেই পরীক্ষা বন্ধ হওয়ার পর চাপও কমে যায়। আর vendor extension একটি প্রতিযোগিতার অস্ত্র: গ্রাহক যত বেশি PL/SQL বা T-SQL লেখেন, অন্য DBMS-এ যাওয়া তত কঠিন — একে বলে vendor lock-in. লাভ vendor-এর আর স্বল্পমেয়াদে গ্রাহকেরও (শক্তিশালী feature দ্রুত পান); ক্ষতি দীর্ঘমেয়াদে গ্রাহকের — দাম বাড়লে বা পণ্য দুর্বল হলে সরে যাওয়ার খরচ বিশাল।

তৃতীয় কারণ standard নিজেই: এটি বিশাল, দামি (ISO-র নথি কিনে পড়তে হয়), আর অনেক অংশ ঐচ্ছিক। ফলে “standard মানি” কথাটির অর্থ দাঁড়ায় “Core অংশ আর কিছু ঐচ্ছিক feature মানি।” PostgreSQL-এর মতো প্রকল্প standard-ঘেঁষা থাকাকে নিজের নীতি বানিয়েছে, তবু সেখানেও উদ্ধৃতিহীন নাম ছোট হাতে বদলানোর মতো ঐতিহাসিক ব্যতিক্রম রয়ে গেছে।

মূল উপলব্ধি: উপভাষা দুর্ঘটনা নয়, ইতিহাস, পুরোনো অ্যাপের সামঞ্জস্য আর বাজারের প্রণোদনার ফল। তাই এটি দূর হবে না — একে মেনে নিয়ে পরিকল্পনা করাই পেশাদারি।

প্র ০২Oracle-এ '' মানে NULL — এটি standard-বিরোধী জেনেও Oracle এতদিন বদলায়নি কেন? আপনার অ্যাপ Oracle আর PostgreSQL দুটিতেই চালাতে হলে কী নীতি নেবেন?

এই আচরণ Oracle-এর শুরুর দিনগুলো থেকে, standard-এর আগে থেকে। তখন অনেক সিস্টেমে “খালি ঘর” আর “কোনো মান নেই”-এর পার্থক্য স্পষ্ট ছিল না, আর Oracle দুটিকে এক ধরল। এরপর চার দশকে লক্ষ লক্ষ PL/SQL প্রোগ্রাম, রিপোর্ট আর ফর্ম এই ধারণার ওপর লেখা হয়েছে: IF v IS NULL দিয়ে “ফাঁকা কি না” দেখা, বা 'a' || NULL = 'a' ধরে লেখা জোড়া। আচরণটি হঠাৎ বদলালে এর অনেকগুলো নীরবে ভুল ফল দেবে — ত্রুটি দিলে তো ঠিক করা যেত, কিন্তু নীরব ভুল খুঁজে বের করা প্রায় অসম্ভব। তাই Oracle-এর নথি বহু বছর ধরে শুধু সতর্ক করে আসছে যে ভবিষ্যতে এটি বদলাতে পারে, এবং খালি লেখাকে NULL হিসেবে ব্যবহার না করার পরামর্শ দেয়; কিন্তু ডিফল্ট আচরণ বদলায়নি।

দুই DBMS-এ চলা অ্যাপের জন্য সবচেয়ে নিরাপদ নীতি হলো “খালি লেখা কখনো জমা হবে না।” অ্যাপের স্তরে ইনপুট নেওয়ার সময়ই trim করে খালি হলে NULL বানান, যাতে PostgreSQL-এও একই অবস্থা হয়। PostgreSQL-এ একটি CHECK দিয়ে পাহারা দিন: CHECK (phone <> ''). কোয়েরিতে সবসময় IS NULL ব্যবহার করুন, = '' নয়। আর যদি “ব্যবহারকারী ইচ্ছা করে খালি রেখেছেন” তথ্যটি সত্যিই দরকার হয়, সেটিকে আলাদা কলামে রাখুন — যেমন phone_declined BOOLEAN (Oracle-এর পুরোনো সংস্করণে NUMBER(1)) — একটি মানের দুই অর্থ না বানিয়ে।

এই উদাহরণ একটি বড় নীতিও শেখায়: portable হতে গেলে “সব DBMS-এর আচরণের ছেদ” বেছে নিতে হয় — যে আচরণ সব জায়গায় একই, শুধু তার ওপর নির্ভর করা।

মূল উপলব্ধি: নীরব পার্থক্য এড়ানোর সেরা উপায় হলো এমন ডেটা কখনো তৈরি না হতে দেওয়া, যেখানে পার্থক্যটি প্রকাশ পায়। খালি লেখার বদলে NULL, আর অর্থ আলাদা হলে আলাদা কলাম।

প্র ০৩PostgreSQL উদ্ধৃতিহীন নাম ছোট হাতে বদলায়, standard বলে বড় হাতে। এই ছোট পার্থক্য বাস্তবে কতটা সমস্যা করে, আর ORM বা GUI টুল কীভাবে এটিকে বাড়িয়ে তোলে?

PostgreSQL-এর জন্ম Berkeley-র POSTGRES প্রকল্পে, যার নিজের ভাষা ছিল POSTQUEL; SQL যোগ হয় পরে, Postgres95-এ। Unix আর C-এর জগতে ছোট হাতের নামই রীতি ছিল, আর ছোট হাতে বদলানোর সিদ্ধান্ত সেই ঐতিহ্য থেকে বলে ধারণা করা হয়। PostgreSQL-এর নথি খোলাখুলি স্বীকার করে যে এটি standard-এর সাথে মেলে না, আর portable হতে চাইলে একটি নাম হয় সবসময় উদ্ধৃত করতে, নয়তো কখনোই না করতে বলে।

যতক্ষণ আপনি নিজে উদ্ধৃতিহীন ছোট হাতের নাম লেখেন, কোনো সমস্যা নেই — standard আর PostgreSQL দুটির নিয়মেই customers নামটি সবসময় মেলে, শুধু catalog-এ বড় না ছোট হাতে জমা, সেটি আলাদা। সমস্যা শুরু হয় যখন অন্য কেউ নাম উদ্ধৃত করে। অনেক টুল নিরাপত্তার জন্য প্রতিটি নাম উদ্ধৃত করে টেবিল বানায় — কিছু GUI-তে “CustomerOrders” টাইপ করলে, বা কিছু ORM-এ class-এর নাম থেকে টেবিল বানালে। তারপর একজন analyst হাতে SELECT * FROM CustomerOrders লেখেন, আর পান “relation does not exist” — টেবিল চোখের সামনে থাকা সত্ত্বেও। অন্য DBMS থেকে আনা schema-ও এই সমস্যা নিয়ে আসে: Oracle-এর export-এ সব নাম বড় হাতে উদ্ধৃত থাকলে PostgreSQL-এ প্রতিটি কোয়েরিতে "CUSTOMERS" লিখতে হবে।

উল্টো দিকে MySQL-এর টেবিল নাম ফাইলের নাম হিসেবে থাকে বলে Windows-এ বানানো অ্যাপ Linux server-এ গিয়ে Orders আর orders-কে আলাদা দেখে — ডেভেলপারের ল্যাপটপে সব ঠিক, production-এ “table doesn't exist”. বাংলাদেশে অনেকেই Windows-এ কাজ করে Linux হোস্টিংয়ে তোলেন, তাই এটি খুব চেনা অভিজ্ঞতা।

মূল উপলব্ধি: নামকরণের একটি কঠোর নিয়ম — ছোট হাতে snake_case, কোনো উদ্ধৃতি নয়, কোনো keyword নয় — পাঁচ DBMS আর সব OS-এ এক আচরণ নিশ্চিত করে। টুলকে নাম উদ্ধৃত করতে দেবেন না।

প্র ০৪MySQL Oracle-এর মালিকানায়, MariaDB তার fork, PostgreSQL কোনো এক কোম্পানির নয়। একটি DBMS-এর মালিকানা আর লাইসেন্স কেন একজন সাধারণ ডেভেলপারের সিদ্ধান্তে আসা উচিত?

কারণ DBMS একটি দীর্ঘমেয়াদি সিদ্ধান্ত — একটি অ্যাপের ডেটা দশ-বিশ বছর বাঁচতে পারে, আর DBMS বদলানো অ্যাপের ভাষা বদলানোর চেয়েও কঠিন হতে পারে। MySQL-এর গল্পটি দেখুন। সুইডেনের MySQL AB ১৯৯৫ সালে এটি শুরু করে, GPL আর বাণিজ্যিক লাইসেন্স দুটিতে দিত। ২০০৮ সালে Sun Microsystems কোম্পানিটি কিনে নেয়, আর ২০১০ সালে Oracle কেনে Sun-কে — অর্থাৎ সবচেয়ে জনপ্রিয় open-source DBMS-টি চলে যায় সবচেয়ে বড় বাণিজ্যিক DBMS কোম্পানির হাতে। সম্প্রদায়ের অনেকে উদ্বিগ্ন ছিলেন, আর MySQL-এর স্রষ্টা Monty Widenius ২০০৯ সালেই MariaDB শুরু করেন। GPL এখানে নিরাপত্তা দিয়েছিল: কোড খোলা ছিল বলে fork করা আইনত সম্ভব ছিল। এরপর কয়েকটি বড় Linux distribution আর প্রতিষ্ঠান ডিফল্ট হিসেবে MariaDB-তে চলে যায়; MySQL নিজেও Oracle-এর অধীনে উন্নত হতে থাকে — দুটিই আজ সক্রিয়, কিন্তু আলাদা।

PostgreSQL-এর মডেল আলাদা: কোনো একক কোম্পানি এর মালিক নয়; একটি বৈশ্বিক ডেভেলপার সম্প্রদায় আর অনেক কোম্পানি মিলে এটি চালায়, আর উদার লাইসেন্সের কারণে কেউ এটি “কিনে নিয়ে বন্ধ” করতে পারে না। একই উদার লাইসেন্সের কারণে অনেক cloud কোম্পানি এর ওপর নিজের সেবা বানায়। SQLite-এর লেখকেরা কোডটি public domain-এ দিয়েছেন, আর দীর্ঘমেয়াদি সমর্থনের অঙ্গীকার করেছেন।

ডেভেলপারের জন্য বাস্তব প্রশ্নগুলো তাই: লাইসেন্সের শর্ত আমার পণ্যের বিতরণ-পদ্ধতির সাথে মেলে কি? বাণিজ্যিক DBMS-এ লাইসেন্সের খরচ ব্যবসা বড় হলে কতটা বাড়বে? মালিকানা বা নীতি বদলালে বিকল্প আছে কি? আর উপভাষার নিজস্ব feature-এ যত বেশি নির্ভর করবেন, বিকল্পে যাওয়া তত কঠিন হবে।

মূল উপলব্ধি: প্রযুক্তিগত ক্ষমতার পাশাপাশি DBMS-এর মালিকানা, লাইসেন্স আর সম্প্রদায় তার ভবিষ্যৎ ঠিক করে। ডেটা যত দীর্ঘ বাঁচবে, এই প্রশ্নগুলো তত গুরুত্বপূর্ণ।

প্র ০৫“সব DBMS-এ চলে এমন SQL লিখুন” — এই নীতি কি সবসময় ভালো? কখনো কি একটি DBMS-এর নিজস্ব feature পুরোদমে ব্যবহার করাই বুদ্ধিমানের কাজ?

পুরোপুরি portable SQL-এর একটি দাম আছে: আপনি সব DBMS-এর “সর্বনিম্ন সাধারণ ভাগ”-এ আটকে যান। MySQL-এ FULL JOIN নেই বলে কোথাও FULL JOIN লিখবেন না? SQLite-এ stored procedure নেই বলে PostgreSQL-এও সব যুক্তি অ্যাপে রাখবেন? PostgreSQL-এর jsonb আর GIN index, Oracle-এর partitioning, SQL Server-এর columnstore — এগুলো বছরের পর বছর প্রকৌশলের ফল, আর ঠিক জায়গায় ব্যবহার করলে কোডকে সরল আর দ্রুত করে। শুধু “কোনোদিন হয়তো DBMS বদলাব” ভেবে এগুলো এড়ানো প্রায়ই এমন একটি বীমা কেনা, যার দাবি কখনো করা হয় না — বাস্তবে বেশিরভাগ প্রজেক্ট সারা জীবন এক DBMS-এই থাকে।

উল্টো দিকে কিছু পরিস্থিতিতে portability সত্যিই জরুরি: আপনি যদি এমন সফটওয়্যার বিক্রি করেন যা গ্রাহকের নিজের DBMS-এ চলবে (অনেক ERP বা ব্যাংকিং পণ্য একসাথে Oracle আর SQL Server সমর্থন করে), বা open-source প্রকল্প যার ব্যবহারকারীরা বিভিন্ন DBMS চালান। এ কারণেই Hibernate, Django বা SQLAlchemy-এর মতো ORM প্রতিটি DBMS-এর জন্য আলাদা dialect ক্লাস রাখে — উপভাষার পার্থক্য তারা একটি স্তরে আটকে রাখে।

বাস্তব মধ্যপথটি তাই: DBMS সচেতনভাবে একবার বাছুন; মূল ডেটা-মডেল আর সাধারণ কোয়েরি standard SQL-এ লিখুন; নিজস্ব feature ব্যবহার করুন যেখানে সত্যিকারের লাভ আছে, কিন্তু সেগুলো কোডের নির্দিষ্ট জায়গায় রাখুন, নথিভুক্ত করুন; আর test চালান সেই DBMS-এই। তাহলে বদলানোর দিন এলে জানবেন ঠিক কোন জায়গাগুলো নতুন করে লিখতে হবে।

মূল উপলব্ধি: Portability একটি লক্ষ্য নয়, একটি বাণিজ্যিক সিদ্ধান্ত। ডিফল্ট হোক standard SQL, ব্যতিক্রম হোক সচেতন, সীমাবদ্ধ আর নথিভুক্ত।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে ফল বলা, একটি port করা আর একটি বাছাইয়ের সিদ্ধান্ত।

  1. কোয়েরি লিখুন: দোকানের ওয়েবসাইটে পণ্যের তালিকা সস্তা থেকে দামি, প্রতি পাতায় ৪টি। db-shop-এর products টেবিলে (product_id, name, price) দ্বিতীয় পাতার কোয়েরি SQLite-এ লিখে যেকোনো এডিটরে চালান (সমান দামে product_id দিয়ে ক্রম ঠিক রাখুন)। তারপর একই কোয়েরি SQL Server ২০১২+ আর Oracle ১২c+-এর জন্য লিখুন।
    -- SQLite / PostgreSQL / MySQL
    SELECT name, price FROM products
    ORDER BY price, product_id
    LIMIT 4 OFFSET 4;
    
    -- SQL Server 2012+  এবং  Oracle 12c+ (দুটিতেই চলে)
    SELECT name, price FROM products
    ORDER BY price, product_id
    OFFSET 4 ROWS FETCH NEXT 4 ROWS ONLY;

    ফল চারটি সারি: Headphone | 1200, Power Bank | 1800, Panjabi | 2200, Smart Watch | 3500. প্রথম পাতায় ছিল Mustard Oil (260), Rice 5kg (420), SQL Book (550), DSA Book (650). দ্বিতীয় রূপটি PostgreSQL-ও মানে, তাই চারটি DBMS-এ একটিই লেখা চলে — শুধু MySQL আর SQLite বাদ। বড় টেবিলে OFFSET ধীর হয়; keyset pagination (WHERE price > শেষ_দেখা_দাম …) পর্ব ১৫-এ দেখেছেন।

  2. ফল আগে বলুন (trace): প্রতিটি ক্ষেত্রে দুই DBMS-এর ফল হাতে লিখুন: (ক) SELECT 7 / 2 — PostgreSQL আর MySQL (খ) '' IS NULL — Oracle আর PostgreSQL (গ) 'Dhaka' = 'dhaka' — ডিফল্ট collation-এর MySQL ৮ আর PostgreSQL (ঘ) SELECT 'a' || 'b' — ডিফল্ট sql_mode-এর MySQL আর SQLite (ঙ) LENGTH('বাংলা') — MySQL (utf8mb4) আর PostgreSQL. শেষে SQLite-এর ফল একটি এডিটরে মিলিয়ে নিন।

    (ক) PostgreSQL 3, MySQL 3.5000. (খ) Oracle-এ সত্য (খালি লেখা NULL; Oracle-এ সরাসরি SELECT-এ boolean দেখাতে হলে CASE লাগে), PostgreSQL-এ false. (গ) MySQL 1 (case-insensitive collation), PostgreSQL false. (ঘ) MySQL 0 (যৌক্তিক OR, সঙ্গে deprecation সতর্কবার্তা), SQLite 'ab'. (ঙ) MySQL 15 (বাইট), PostgreSQL 5 (অক্ষর)। SQLite-এ SELECT 7 / 2, '' IS NULL, 'Dhaka' = 'dhaka', 'a' || 'b', length('বাংলা'); দেয় 3 | 0 | 0 | ab | 5 — বেশিরভাগ ক্ষেত্রে PostgreSQL-এর মতো।

  3. Port করুন: নিচের MySQL কোডটি PostgreSQL-এর জন্য নতুন করে লিখুন, আর প্রতিটি বদলের কারণ এক লাইনে বলুন:
    CREATE TABLE `members` (
      `id` INT AUTO_INCREMENT PRIMARY KEY,
      `email` VARCHAR(120) UNIQUE,
      `active` TINYINT(1) DEFAULT 1
    );
    INSERT INTO `members` (`email`) VALUES ('a@x.com')
      ON DUPLICATE KEY UPDATE `active` = 1;
    SELECT IFNULL(`email`, '-') FROM `members` WHERE `active` = 1 LIMIT 5, 10;
    CREATE TABLE members (
      id     INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
      email  VARCHAR(120) UNIQUE,
      active BOOLEAN DEFAULT TRUE
    );
    INSERT INTO members (email) VALUES ('a@x.com')
      ON CONFLICT (email) DO UPDATE SET active = TRUE;
    SELECT COALESCE(email, '-') FROM members WHERE active LIMIT 10 OFFSET 5;

    backtick বাদ (PostgreSQL চেনে না; নামগুলো এমনিতেই ছোট হাতে, তাই উদ্ধৃতি লাগে না)। AUTO_INCREMENT → identity (BY DEFAULT রাখলাম, যাতে পুরোনো id-সহ ডেটা কপি করা যায়)। TINYINT(1) → আসল BOOLEAN. ON DUPLICATE KEY → ON CONFLICT, যেখানে কোন UNIQUE কলামে সংঘাত, তা বলে দিতে হয়। IFNULL → COALESCE (standard). LIMIT 5, 10-এর অর্থ “৫টি বাদ, ১০টি নাও” — তাই LIMIT 10 OFFSET 5, সংখ্যা দুটি উল্টো ক্রমে। আরও একটি সতর্কতা: MySQL-এ email তুলনা বড়-ছোট অক্ষর উপেক্ষা করত, PostgreSQL-এ করে না — 'A@x.com' আর 'a@x.com' এখন দুটি আলাদা UNIQUE মান।

  4. বাছাই করুন: তিনটি প্রজেক্টের প্রতিটির জন্য একটি DBMS বেছে দুই-তিন বাক্যে যুক্তি দিন: (ক) একটি কোচিং সেন্টারের Android হাজিরা অ্যাপ, যা ক্লাসরুমে ইন্টারনেট ছাড়া চলবে, দিনশেষে server-এ পাঠাবে; (খ) একটি জেলা-শহরের দোকানের Laravel ই-কমার্স সাইট, বাজেট কম, cPanel হোস্টিং; (গ) একটি ব্যাংকের নতুন ঋণ-আবেদন মডিউল, যার core banking সিস্টেম আগে থেকেই Oracle-এ।

    (ক) ফোনে SQLite — Android-এ আগে থেকেই আছে, server লাগে না, অফলাইনে চলে; server-এর দিকে যেকোনো DBMS (যেমন PostgreSQL), আর sync-এর সময় দুই উপভাষার পার্থক্য (তারিখ, boolean) মাথায় রাখতে হবে। (খ) MySQL বা MariaDB — cPanel হোস্টিংয়ে সাধারণত এটিই দেওয়া, Laravel-এর সাথে প্রচলিত, অতিরিক্ত খরচ নেই; ভবিষ্যতে সরতে হতে পারে ভেবে নাম আর কোয়েরি portable রাখুন। (গ) বিদ্যমান Oracle — core-এর ডেটার সাথে join, প্রতিষ্ঠানের DBA, backup আর নিরাপত্তা নীতি আগে থেকেই আছে; আলাদা DBMS আনলে দুই সিস্টেমের মাঝে ডেটা মেলানো আর নিরীক্ষার ঝামেলা বাড়ে। তবে চূড়ান্ত সিদ্ধান্ত প্রতিষ্ঠানের নীতি আর লাইসেন্সের শর্ত দেখে।

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

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

ক্রমMySQL থেকে PostgreSQL-এ একটি চালু অ্যাপের ডেটাবেস সরানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কোথায় কোথায় MySQL-নির্ভর SQL, টাইপ আর আচরণ আছে, তার তালিকা করা
  2. schema রূপান্তর — টাইপ, AUTO_INCREMENT থেকে identity, উদ্ধৃতি আর constraint
  3. পরীক্ষার server-এ ডেটা লোড করে sequence-এর পরের মান ঠিক করা
  4. অ্যাপের কোয়েরি বদলে পুরো test PostgreSQL-এর ওপর চালানো
  5. দুই DBMS-এ সারির সংখ্যা, যোগফল আর নির্বাচিত সারি মিলিয়ে দেখা
  6. ফেরার পরিকল্পনা হাতে রেখে production-এ বদল (cutover)

Oracle-এ contacts টেবিলের VARCHAR2 কলাম phone-এ তিনটি সারি: '01711000001', '' (খালি লেখা) আর NULL. SELECT COUNT(*) FROM contacts WHERE phone IS NULL; কত দেবে?

  • 1
  • 3
  • 2
  • ত্রুটি দেবে

PostgreSQL-এ CREATE TABLE Orders (OrderID int); চালানোর পর কোন কোয়েরিটি ত্রুটি দেবে?

  • SELECT * FROM ORDERS;
  • SELECT * FROM "Orders";
  • SELECT * FROM "orders";
  • SELECT orderid FROM orders;

ডিফল্ট সেটিংয়ের MySQL-এ SELECT 'a' || 'b'; চালালে কী পাওয়া যায়?

  • 'ab'
  • সিনট্যাক্স ত্রুটি
  • NULL
  • 0

একটি স্ক্রিপ্ট: BEGIN; CREATE TABLE coupons (code TEXT); ROLLBACK; — কোন DBMS-এ ROLLBACK-এর পর coupons টেবিলটি আর থাকে না?

  • PostgreSQL
  • MySQL (InnoDB)
  • Oracle
  • কোনোটিতেই না — DDL কখনো ROLLBACK হয় না

InnoDB storage engine-এ MySQL-এর ডিফল্ট transaction isolation level কোনটি?

  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE
  • READ UNCOMMITTED

SQL Server-এ একটি recursive CTE লেখার সঠিক শুরু কোনটি?

  • WITH RECURSIVE t AS (…)
  • SELECT … CONNECT BY PRIOR …
  • WITH t AS (… UNION ALL … FROM t …)
  • CREATE RECURSIVE CTE t AS (…)

শব্দকোষ

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

মনে রাখুন

  1. উপভাষা এসেছে কারণ পণ্য standard-এর আগে এসেছিল, standard বিশাল ও ঐচ্ছিক অংশে ভরা, আর vendor-রা নিজস্ব সংযোজন করে; মূল SQL এক, প্রান্ত আলাদা।
  2. সিনট্যাক্সের পার্থক্য ত্রুটি দেয়: AUTO_INCREMENT/IDENTITY, LIMIT/TOP/FETCH FIRST, ||/CONCAT/+, backtick/[ ]/double quote, ON CONFLICT/ON DUPLICATE KEY/MERGE, RETURNING/OUTPUT.
  3. নীরব পার্থক্য ফল বদলায়: PostgreSQL নাম ছোট হাতে আর Oracle বড় হাতে বদলায়; MySQL ও SQL Server-এর ডিফল্ট তুলনা case-insensitive; Oracle-এ '' = NULL; 7 / 2 কোথাও 3, কোথাও 3.5.
  4. ক্ষমতা আর transaction: MySQL-এ FULL JOIN নেই; SQL Server/Oracle-এ recursive CTE শুধু WITH; procedural ভাষা প্রত্যেকের নিজের; PostgreSQL ও SQLite-এ DDL ROLLBACK হয়, MySQL ও Oracle-এ implicit commit; ডিফল্ট isolation InnoDB-তে REPEATABLE READ, বাকিগুলোতে READ COMMITTED, SQLite-এ SERIALIZABLE.
  5. Portable SQL: standard রূপ, ছোট হাতের উদ্ধৃতিহীন নাম, COALESCE, স্পষ্ট টাইপ, DBMS-নির্ভর কোড এক জায়গায়, আর test আসল লক্ষ্য DBMS-এ; বাছাই করুন hosting, দক্ষতা, বিদ্যমান সিস্টেম আর লাইসেন্স দেখে।

পরের পর্বে: উপভাষার নিচে সব SQL একটি গাণিতিক ভিত্তির ওপর দাঁড়িয়ে — relational algebra. পর্ব ৩২-এ select, project আর set অপারেশন দিয়ে দেখবেন, প্রতিটি SQL কোয়েরি আসলে কয়েকটি সহজ অপারেশনের গঠন, যা DBMS-এর optimizer প্রতিদিন ব্যবহার করে।

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

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