এই পর্বে যা শিখবেন
- ফাইল কোথায় থামে আর ডেটাবেস কেন লাগে; SQLite কী, আর কোথায় কোথায় চলে
- টেবিল, সারি, কলাম, primary ও foreign key, constraint — একটি দোকানের schema নিজে নকশা করা
- দরকারি SQL: CREATE, INSERT, SELECT, JOIN, GROUP BY, UPDATE, DELETE — আর SELECT আসলে কোন ক্রমে চলে
sqlite3-এর connect, cursor, execute, executemany, fetch,sqlite3.Row, commit/rollback আর connection ঠিকভাবে বন্ধ করা- parameterized query, transaction ও ACID, index, তারিখ ও বাংলা লেখা, SQLite-এর ধরন-নমনীয়তা, migration — আর কখন PostgreSQL বা ORM
১ · খাতা থেকে ডেটাবেস — ফাইল কোথায় থামে
পর্ব ২৬-এ ফাইলে লিখতে শিখেছেন, পর্ব ২৭-এ CSV আর JSON. ছোট কাজের জন্য এগুলো যথেষ্ট — একটি মার্কশিট, একটি অ্যাপের সেটিংস। এবার ভাবুন একটি অনলাইন দোকানের কথা: পাঁচ বছরে দশ লাখ অর্ডার, হাজার পণ্য, লাখো ক্রেতা। একজন ক্রেতা ফোন নম্বর দিয়ে নিজের অর্ডার খুঁজছেন, একই মুহূর্তে আরও বিশজন অর্ডার দিচ্ছেন, আর ম্যানেজার জানতে চান “গত মাসে কোন জেলায় কত বিক্রি?”. ডেটা যদি একটি JSON ফাইলে থাকে, তাহলে কী হবে?
🐢 খোঁজা ধীর
একজন ক্রেতার অর্ডার খুঁজতে প্রতিবার পুরো ফাইল পড়ে মেমরিতে আনতে হয়। দশ লাখ রেকর্ডে প্রতিটি প্রশ্নে কয়েক সেকেন্ড — আর মেমরিও ফুরিয়ে যেতে পারে।
👥 একসাথে লেখা বিপজ্জনক
দুজন একসাথে ফাইল পড়ে, নিজের অর্ডার যোগ করে, আবার পুরো ফাইল লিখলে একজনের অর্ডার চুপচাপ মুছে যায় — যাকে বলে lost update.
⚡ মাঝপথে বিদ্যুৎ গেলে
লোডশেডিংয়ে ফাইল লেখার মাঝখানে কম্পিউটার বন্ধ হলে অর্ধেক লেখা ফাইল থাকে — JSON ভেঙে যায়, পুরো ডেটা আর পড়াই যায় না।
❓ প্রশ্ন করা কঠিন
“কোন জেলায় কত বিক্রি” জানতে প্রতিবার নতুন লুপ, নতুন dict, নতুন কোড লিখতে হয়। প্রতিটি নতুন প্রশ্ন মানে নতুন প্রোগ্রাম।
একটি database এই চারটি সমস্যাই সমাধান করে: পুরোটা না পড়েই নির্দিষ্ট রেকর্ড খুঁজে পায়, একসাথে কয়েকজনের লেখা সামলায়, আর মাঝপথে crash হলেও ডেটা থাকে হয় আগের, নয়তো নতুন অবস্থায় — অর্ধেক অবস্থায় কখনো নয়। আর প্রশ্ন করার জন্য আছে একটি আলাদা ভাষা, SQL: “কী চাই” বললেই হয়, “কীভাবে খুঁজবে” ডেটাবেস নিজেই ঠিক করে।
পাড়ার মুদি দোকানের মালিক বাকির হিসাব রাখেন একটি খাতায়। বিশজন ক্রেতা হলে চমৎকার চলে। কিন্তু পঞ্চাশ শাখার একটি সুপারশপ চেইনে খাতা অচল — দরকার এমন ব্যবস্থা, যেখানে পণ্য, ক্রেতা আর বিক্রির তালিকা আলাদা, আর কম্পিউটার মুহূর্তে বলে দেয় “মিরপুর শাখায় গতকাল কত কেজি চাল বিক্রি হয়েছে”. CSV আর JSON হলো খাতা; ডেটাবেস হলো সেই ব্যবস্থা।
SQLite — একটি ফাইলে পুরো ডেটাবেস
বেশিরভাগ বড় ডেটাবেস (যেমন PostgreSQL, MySQL) আলাদা একটি সার্ভার প্রোগ্রাম হিসেবে চলে; আপনার অ্যাপ নেটওয়ার্কে তার সাথে সংযোগ করে — একে বলে client-server database. SQLite অন্যরকম: এটি একটি embedded database. কোনো সার্ভার নেই, কোনো পাসওয়ার্ড বা সেটআপ নেই — ডেটাবেসটি আপনার প্রোগ্রামের ভেতরেই একটি লাইব্রেরি হিসেবে চলে, আর সব ডেটা (সব টেবিল, index, সবকিছু) থাকে একটি মাত্র ফাইলে, যেমন shop.db. সেই ফাইল কপি করলেই পুরো ডেটাবেস কপি হয়ে যায়।
সবচেয়ে বড় সুখবর: Python-এর standard library-তে sqlite3 module আছে, আর SQLite নিজে Python-এর সাথেই আসে। import sqlite3 লিখলেই একটি পূর্ণাঙ্গ SQL ডেটাবেস হাতে পেয়ে যান।
২০০০ সালে মার্কিন প্রোগ্রামার D. Richard Hipp SQLite লেখা শুরু করেন — ছোট, নির্ভরযোগ্য আর সার্ভারহীন একটি SQL ডেটাবেস। এর কোড public domain — যে কেউ যেকোনো কাজে ব্যবহার করতে পারে। SQLite-এর নিজের ওয়েবসাইট সুন্দর করে বলে: SQLite client-server ডেটাবেসের সাথে প্রতিযোগিতা করে না, প্রতিযোগিতা করে fopen()-এর সাথে — অর্থাৎ এটি সাধারণ ফাইলের বদলি। Python-এ sqlite3 module যোগ হয় Python 2.5-এ (২০০৬), Gerhard Häring-এর pysqlite প্রকল্প থেকে।
ফাইল জানে শুধু “লেখা আর পড়া”. ডেটাবেস জানে ডেটার গঠন, নিয়ম আর প্রশ্ন. SQLite সেই ক্ষমতা দেয় একটি ফাইলের সরলতায়।
২ · relational ভাবনা — টেবিল, সারি, কলাম আর key
relational ডেটাবেসে সব ডেটা থাকে টেবিলে, আর প্রতিটি টেবিল একই ধরনের জিনিসের তালিকা (ক্রেতা, পণ্য, অর্ডার)। টেবিলের প্রতিটি সারি (row) একটি রেকর্ড — একজন ক্রেতা; প্রতিটি কলাম (column) একটি নির্দিষ্ট তথ্য — নাম, ফোন, জেলা। দেখতে Excel-এর শিটের মতো, কিন্তু পার্থক্য বড়: প্রতিটি কলামের একটি নির্দিষ্ট ধরন আর নিয়ম আছে, আর টেবিলগুলো একে অপরের সাথে যুক্ত।
| id | name | phone | district |
|---|---|---|---|
| 1 | রিয়া | 01711000001 | ঢাকা |
| 2 | জয় | 01811000002 | সিলেট |
| 3 | মিতু | 01911000003 | ঢাকা |
প্রতিটি সারিকে আলাদাভাবে চেনার জন্য একটি কলাম লাগে, যার মান কখনো দুটি সারিতে এক হবে না — একে বলে primary key. এখানে id. নাম primary key হতে পারে না (দুজন “রিয়া” থাকতেই পারে); ফোন নম্বরও ঝুঁকিপূর্ণ (মানুষ নম্বর বদলায়)। তাই সাধারণত একটি অর্থহীন ক্রমিক নম্বর নেওয়া হয় — যেমন বোর্ড পরীক্ষার রোল নম্বর, যা নামের ওপর নির্ভর করে না।
কেন একটি টেবিল নয়, তিনটি?
প্রথম চিন্তা আসতে পারে: সব একটি বড় টেবিলে রাখি — প্রতিটি অর্ডারের সারিতে ক্রেতার নাম, ফোন, জেলা, পণ্যের নাম, দাম। সমস্যা হলো, রিয়া একশটি অর্ডার দিলে তাঁর ফোন নম্বর একশবার লেখা থাকবে — নম্বর বদলালে একশ জায়গায় বদলাতে হবে, একটি ভুলে গেলেই ডেটা অসঙ্গত। সমাধান: প্রতিটি তথ্য একবার রাখা, আর অন্য টেবিল থেকে শুধু তার key দিয়ে নির্দেশ করা। এই নকশা-নীতির নাম normalization.
👤 customers
id (primary key), name, phone (অনন্য), district. প্রতিজন ক্রেতা একবার।
📦 products
id, sku (অনন্য কোড), name, price, stock. প্রতিটি পণ্য একবার।
🧾 orders
id, customer_id → customers, product_id → products, qty, amount, created_at.
orders.customer_id কলামে ক্রেতার নাম নেই, আছে শুধু তাঁর id — এমন কলামকে বলে foreign key: এক টেবিলের কলাম, যা অন্য টেবিলের primary key-র দিকে নির্দেশ করে। ডেটাবেস চাইলে নিশ্চিত করতে পারে যে অচেনা ক্রেতার কোনো অর্ডার থাকবে না। টেবিল, কলাম, ধরন, key আর নিয়মের পুরো নকশাকে বলে schema — কোড লেখার আগে কাগজে আঁকুন (পর্ব ০২-এর “কোডের আগে ভাবা”)।
নিয়ম বসানো — constraint
schema-র সবচেয়ে শক্তিশালী অংশ হলো constraint — কলামে বসানো নিয়ম, যা ভাঙলে ডেটাবেস লেখাটি নেয়ই না। Python কোডে যাচাই ভুলে গেলেও ডেটাবেস শেষ পাহারাদার হিসেবে দাঁড়িয়ে থাকে:
CREATE TABLE customers (
id INTEGER PRIMARY KEY, -- নিজে থেকে 1, 2, 3 … পায়
name TEXT NOT NULL, -- ফাঁকা রাখা যাবে না
phone TEXT NOT NULL UNIQUE, -- দুজনের একই নম্বর নয়
district TEXT NOT NULL
);
CREATE TABLE products (
id INTEGER PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
price INTEGER NOT NULL CHECK (price > 0), -- টাকা, পয়সা নয়
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0)
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
product_id INTEGER NOT NULL REFERENCES products(id),
qty INTEGER NOT NULL CHECK (qty > 0),
amount INTEGER NOT NULL,
created_at TEXT NOT NULL -- '2026-10-18 14:30:00'
);NOT NULL মানে ঘরটি ফাঁকা রাখা যাবে না। SQL-এ “মান নেই” বোঝাতে আছে বিশেষ চিহ্ন NULL, যা Python-এ None হয়ে আসে। UNIQUE মানে একই মান দুবার নয়। CHECK যেকোনো শর্ত বসায় — দাম শূন্যের বেশি, স্টক ঋণাত্মক নয়। নিয়ম ভাঙলে Python-এ আসে sqlite3.IntegrityError, যা পর্ব ২৫-এর try/except দিয়ে ধরতে পারেন। টাকা INTEGER-এ রাখা হয়েছে — পর্ব ০৭-এ দেখেছেন float-এ টাকা রাখলে কী বিপদ হয়।
SQLite-এ foreign key যাচাই ডিফল্টভাবে বন্ধ। পুরোনো ডেটাবেসের সাথে সামঞ্জস্য রাখতে SQLite REFERENCES লিখলেও নিজে থেকে যাচাই করে না। প্রতিটি connection খোলার ঠিক পরেই con.execute("PRAGMA foreign_keys = ON") চালান। আরেকটি ফাঁদ: কোনো transaction খোলা থাকা অবস্থায় এই PRAGMA চুপচাপ কিছুই করে না — তাই এটি দিন সবার আগে, কোনো INSERT-এর আগে।
৩ · SQL-এর মূল বাক্য — আর SELECT আসলে কোন ক্রমে চলে
SQL একটি ঘোষণামূলক (declarative) ভাষা। Python-এ আপনি বলেন “কীভাবে”: লুপ চালাও, if দিয়ে মেলাও, list-এ যোগ করো। SQL-এ বলেন শুধু “কী চাই”: “ঢাকার ক্রেতাদের নাম, নাম অনুযায়ী সাজিয়ে”. কোন পথে খুঁজবে — পুরো টেবিল পড়বে নাকি index ধরে লাফাবে — ঠিক করে ডেটাবেস নিজেই। ডেটা নিয়ে চারটি মৌলিক কাজকে (CRUD) SQL-এ এভাবে লেখা হয়:
-- Create: নতুন সারি
INSERT INTO customers (name, phone, district) VALUES ('রিয়া', '01711000001', 'ঢাকা');
-- Read: শর্ত, ক্রম আর সংখ্যা-সীমা
SELECT name, phone FROM customers
WHERE district = 'ঢাকা'
ORDER BY name
LIMIT 10;
-- Update: WHERE ছাড়া দিলে সব সারি বদলে যাবে!
UPDATE products SET stock = stock - 2 WHERE sku = 'rice-5kg';
-- Delete: এখানেও WHERE ভুলবেন না
DELETE FROM orders WHERE id = 42;লক্ষ করুন, SQL-এ string থাকে একক উদ্ধৃতিতে ('ঢাকা'), আর সমান-তুলনা একটি = দিয়ে, Python-এর মতো == নয়। ORDER BY name DESC উল্টো ক্রমে সাজায়। আর সবচেয়ে বড় সতর্কতা: UPDATE বা DELETE-এ WHERE ভুলে গেলে বদল লাগে সব সারিতে। তাই আগে একই WHERE দিয়ে একটি SELECT চালিয়ে দেখে নিন কোন সারিগুলো বদলাবে।
লেখার ক্রম বনাম চলার ক্রম
একটি SELECT-এ অনেকগুলো অংশ থাকতে পারে: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. লেখা হয় এই ক্রমে, কিন্তু যুক্তির দিক থেকে ডেটাবেস এগুলো ভিন্ন ক্রমে প্রয়োগ করে। এই ক্রম জানলে SQL-এর অনেক “অদ্ভুত” error-এর মানে পরিষ্কার হয়ে যায়। নিচের প্রশ্নটি ধাপে ধাপে চালিয়ে দেখুন — “সক্রিয় ক্রেতা অন্তত দুজন, এমন জেলাগুলোর মধ্যে সবচেয়ে বেশি ক্রেতার দুটি জেলা”:
- FROM
customersকোন টেবিল — ১০টি সারি - WHERE
active = 1সারি ছাঁকা — ৮টি থাকে - GROUP BY
districtদল বানানো — ৪টি দল - HAVING
COUNT(*) >= 2দল ছাঁকা — ৩টি থাকে - SELECT
district, COUNT(*) AS nকলাম বানানো, নাম n জন্মায় - ORDER BY
n DESC, districtসাজানো - LIMIT
2প্রথম দুটি সারি
রিয়া ঢাকা 1 জয় সিলেট 1 মিতু ঢাকা 1 সাকিব চট্টগ্রাম 1 তানিয়া চট্টগ্রাম 0 রাফি ঢাকা 0 নুসরাত চট্টগ্রাম 1 অনিক সিলেট 1 শাওন রাজশাহী 1 সুমি ঢাকা 1 # কলাম: name, district, active (1 = সক্রিয়)
রিয়া ঢাকা মিতু ঢাকা সুমি ঢাকা
জয় সিলেট অনিক সিলেট
সাকিব চট্টগ্রাম নুসরাত চট্টগ্রাম
শাওন রাজশাহী
# তানিয়া আর রাফি বাদ — ৮টি সারি থাকল
ঢাকা → [রিয়া, মিতু, সুমি] COUNT = 3 সিলেট → [জয়, অনিক] COUNT = 2 চট্টগ্রাম → [সাকিব, নুসরাত] COUNT = 2 রাজশাহী → [শাওন] COUNT = 1
ঢাকা 3 সিলেট 2 চট্টগ্রাম 2 রাজশাহী 1 → বাদ # WHERE-এ COUNT(*) লেখা যায় না: তখনো দলই বানানো হয়নি # SQLite বলে: misuse of aggregate: COUNT()
district n
ঢাকা 3
সিলেট 2
চট্টগ্রাম 2
# n নামটি এই ধাপে জন্মায়
ঢাকা 3 চট্টগ্রাম 2 সিলেট 2 # n এখানে ব্যবহার করা যায় — SELECT আগেই হয়ে গেছে
[('ঢাকা', 3), ('চট্টগ্রাম', 2)]
- FROM — আগে টেবিল। লেখায় SELECT প্রথমে থাকলেও ডেটাবেস প্রথমে জানতে চায় কোথা থেকে ডেটা আসবে। JOIN থাকলে টেবিল জোড়া লাগানোও এই ধাপে হয় (পরের অংশে দেখবেন)।
- WHERE — সারি ধরে ছাঁকা। প্রতিটি সারি একা একা শর্তের সামনে দাঁড়ায়:
active = 1কি না। অসক্রিয় দুজন বাদ। এই ধাপে এখনো কোনো দল নেই, তাই “কত জন” ধরনের প্রশ্ন এখানে করা যায় না। - GROUP BY — দল বানানো। একই জেলার সারিগুলো একটি দলে জড়ো হয়। এখন থেকে প্রতিটি দলের জন্য একটি করে সারি ফল আসবে, আর aggregate function (
COUNT,SUM,AVG,MIN,MAX) প্রতিটি দলের ওপর আলাদাভাবে হিসাব করে। - HAVING — দল ধরে ছাঁকা। WHERE-এর মতোই শর্ত, কিন্তু দলের ওপর। রাজশাহীর দলে মাত্র একজন, তাই বাদ। নিয়মটি মনে রাখুন: সারির শর্ত WHERE-এ, দলের শর্ত HAVING-এ. WHERE-এ COUNT লিখলে SQLite “misuse of aggregate” error দেয়।
- SELECT — এবার কলাম বানানো। প্রতিটি দল থেকে জেলার নাম আর সংখ্যা নিয়ে ফলাফলের সারি তৈরি হয়।
AS nদিয়ে দেওয়া নামটি এখানেই জন্মায়। তাই স্ট্যান্ডার্ড SQL-এ WHERE-এ এই নাম ব্যবহার করা যায় না (SQLite নিজে কিছুটা ছাড় দেয়, কিন্তু অন্য ডেটাবেসে চলবে না — এর ওপর ভরসা করবেন না)। - ORDER BY — সাজানো। SELECT-এর পরে আসে বলে এখানে
nনামটি ব্যবহার করা যায়। চট্টগ্রাম আর সিলেট দুটিতেই ২ জন — দ্বিতীয় চাবিdistrictসমতা ভাঙে। ORDER BY না দিলে ডেটাবেস কোনো নির্দিষ্ট ক্রমের প্রতিশ্রুতি দেয় না। - LIMIT — শেষে কাটা। সাজানো ফলের প্রথম দুটি সারি। LIMIT সবার শেষে আসে বলেই “সেরা দুটি” পেতে ORDER BY-এর সাথে জোড়া লাগে; ORDER BY ছাড়া LIMIT মানে “যেকোনো দুটি”.
চলে: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
৪ · JOIN আর GROUP BY — দুই টেবিল মেলানো, জেলাভিত্তিক বিক্রির রিপোর্ট
normalization-এর ফলে অর্ডারের সারিতে ক্রেতার নাম বা জেলা নেই, আছে শুধু customer_id. তাহলে “জেলাভিত্তিক বিক্রি” বের করব কীভাবে? উত্তর JOIN: দুটি টেবিলের সারিগুলোকে একটি শর্ত অনুযায়ী মিলিয়ে এক সারিতে আনা। প্রশ্নটি দেখতে এমন:
SELECT c.district, COUNT(*) AS n, SUM(o.amount) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
GROUP BY c.district
ORDER BY total DESC;orders o মানে টেবিলটিকে ছোট নাম o দেওয়া, যাতে o.amount লেখা যায়। ON c.id = o.customer_id হলো মেলানোর শর্ত। ডেটাবেস ভেতরে কীভাবে মেলায়, ধাপে ধাপে দেখুন:
orders o customers c id customer_id amount id name district 1 1 500 1 রিয়া ঢাকা 2 2 1200 2 জয় সিলেট 3 1 300 3 মিতু ঢাকা 4 3 700 4 সাকিব চট্টগ্রাম
o: 1 1 500 ⟶ c: 1 রিয়া ঢাকা # মিলে গেছে — দুই সারি জুড়ে এক সারি: (o.id=1, amount=500, name=রিয়া, district=ঢাকা)
o: 2 2 1200 ⟶ c: 2 জয় সিলেট (o.id=2, amount=1200, name=জয়, district=সিলেট)
o: 3 1 300 ⟶ c: 1 রিয়া ঢাকা # রিয়া দ্বিতীয়বার o: 4 3 700 ⟶ c: 3 মিতু ঢাকা
ঢাকা 500, 300, 700 → n = 3, total = 1500 সিলেট 1200 → n = 1, total = 1200 সাকিব (চট্টগ্রাম) — কোনো অর্ডার নেই, ফলে নেই # LEFT JOIN customers → orders দিলে সাকিবও আসতেন, amount = NULL
- দুটি টেবিল, একটি শর্ত। orders-এ শুধু ক্রেতার নম্বর, customers-এ নাম আর জেলা।
ONশর্ত বলে দেয় কোন সারি কোন সারির জোড়া: অর্ডারের customer_id = ক্রেতার id. - প্রথম অর্ডার। customer_id 1 — customers-এ id 1 খোঁজা হয়, পাওয়া যায় রিয়া। দুই সারির কলামগুলো পাশাপাশি বসে একটি চওড়া সারি হয়। primary key-তে খোঁজা দ্রুত, কারণ primary key-র ওপর ডেটাবেস নিজেই সাজানো কাঠামো রাখে।
- দ্বিতীয় অর্ডার। একই প্রক্রিয়া — জয়, সিলেট।
- একজন ক্রেতা, অনেক অর্ডার। রিয়ার দুটি অর্ডার, তাই তাঁর নাম ফলাফলে দুবার আসে। টেবিলে নাম একবারই লেখা আছে; JOIN প্রয়োজন মতো জুড়ে দেখায়। এটাই normalization-এর সৌন্দর্য।
- ফল আর একটি সূক্ষ্মতা। এই সাধারণ (INNER) JOIN শুধু মিলে যাওয়া জোড়া রাখে। সাকিবের কোনো অর্ডার নেই, তাই তিনি ফলাফলে নেই। “কোনো অর্ডার দেননি এমন ক্রেতাও দেখাও” চাইলে
LEFT JOIN, যেখানে মিল না পেলে অর্ডারের কলামগুলো NULL হয়। এরপর GROUP BY জেলা ধরে দল বানায়।
এবার পুরো ব্যাপারটি Python থেকে চালান। কোডটি RAM-এ একটি অস্থায়ী ডেটাবেস (":memory:") বানায়, দুটি টেবিল তৈরি করে, কয়েকটি সারি ঢোকায়, জেলাভিত্তিক রিপোর্ট ছাপে, আর ফোন নম্বর দিয়ে একজন ক্রেতাকে নিরাপদে খোঁজে:
import sqlite3
con = sqlite3.connect(":memory:")
con.row_factory = sqlite3.Row
con.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL,
phone TEXT UNIQUE, district TEXT NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, amount INTEGER CHECK (amount > 0),
customer_id INTEGER NOT NULL REFERENCES customers(id));""")
con.executemany("INSERT INTO customers VALUES (?, ?, ?, ?)", [
(1, "রিয়া", "01711000001", "ঢাকা"), (2, "জয়", "01811000002", "সিলেট"),
(3, "মিতু", "01911000003", "ঢাকা")])
con.executemany("INSERT INTO orders (customer_id, amount) VALUES (?, ?)",
[(1, 500), (2, 1200), (1, 300), (3, 700)])
con.commit()
for r in con.execute("""SELECT c.district, COUNT(*) AS n, SUM(o.amount) AS total
FROM orders o JOIN customers c ON c.id = o.customer_id
GROUP BY c.district ORDER BY total DESC"""):
print(r["district"], r["n"], r["total"])
phone = "01811000002"
row = con.execute("SELECT name, district FROM customers WHERE phone = ?", (phone,)).fetchone()
print(dict(row))
con.close()আউটপুটের প্রথম দুই লাইন ঢাকা 3 1500 আর সিলেট 1 1200 — ঢাকার তিনটি অর্ডার (রিয়ার দুটি, মিতুর একটি) মিলে ১৫০০ টাকা। ORDER BY total DESC থাকায় ক্রম নিশ্চিত। শেষ লাইন {'name': 'জয়', 'district': 'সিলেট'}: ফোন নম্বর ? চিহ্নের জায়গায় আলাদা করে পাঠানো হয়েছে, আর row_factory = sqlite3.Row থাকায় সারিটি কলামের নাম দিয়ে পড়া যায়, dict()-এ বদলানোও যায়। executescript একসাথে কয়েকটি SQL বাক্য চালায় — schema বানাতে সুবিধাজনক, কিন্তু এতে ? চলে না, তাই ব্যবহারকারীর মান কখনো এতে নয়। বাংলা নাম আর জেলা কোনো ঝামেলা ছাড়াই গেল আর এল: SQLite লেখা রাখে UTF-8-এ।
৫ · Python-এর sqlite3 — connect, cursor, fetch আর সংযোগ বন্ধ করা
উপরের কোডের অংশগুলো এবার একে একে বুঝে নিন। Python-এর ডেটাবেস লাইব্রেরিগুলো মোটামুটি একই নকশা মানে — DB-API 2.0 (PEP 249) — তাই sqlite3 শিখলে PostgreSQL-এর লাইব্রেরিও চেনা লাগবে।
| অংশ | কী করে | মনে রাখার কথা |
|---|---|---|
sqlite3.connect("shop.db") | ফাইলটি খোলে (না থাকলে বানায়), একটি connection দেয় | ":memory:" দিলে RAM-এ অস্থায়ী in-memory database — টেস্টের জন্য দারুণ |
con.execute(sql, params) | একটি SQL বাক্য চালায়, একটি cursor ফেরত দেয় | আসলে ভেতরে নতুন cursor বানিয়ে তার execute ডাকে — শর্টকাট |
con.executemany(sql, rows) | একই বাক্য অনেক সারির জন্য | executemany লুপের চেয়ে সহজ ও দ্রুত |
cur.fetchone() | পরের একটি সারি, না থাকলে None | ফোন দিয়ে খোঁজার মতো “একটি বা কোনোটিই না” প্রশ্নে |
cur.fetchall() | বাকি সব সারি একটি list-এ | লাখো সারিতে মেমরি ভরে যায় — তখন for row in cur |
con.row_factory = sqlite3.Row | সারি আসে নাম দিয়ে পড়ার মতো object হয়ে | row_factory ডিফল্টে সারি tuple |
con.commit() / con.rollback() | বদল স্থায়ী করা / বাতিল করা | commit ছাড়া বন্ধ করলে বদল হারায় |
con.close() | সংযোগ বন্ধ | commit করে না! |
cursor-কে ভাবুন একটি আঙুলের মতো, যা ফলাফলের সারিগুলোর ওপর দিয়ে চলে। fetchone() আঙুল এক সারি এগোয়; একবার পড়া সারি আর ফেরে না — পর্ব ৩৬-এর iterator-এর মতোই। for row in con.execute(...) সবচেয়ে Pythonic: সারি আসে একটি একটি করে।
commit — আর Python-এর লুকানো transaction
নতুনদের সবচেয়ে প্রচলিত বাগ: প্রোগ্রাম চলার সময় ডেটা দেখা যায়, কিন্তু পরের বার খুললে নেই। কারণ commit() ডাকা হয়নি। Python-এর sqlite3 ডিফল্টভাবে প্রথম INSERT, UPDATE বা DELETE-এর আগে নিজে থেকে একটি transaction খোলে, আর আপনি commit() না ডাকা পর্যন্ত বদলগুলো অস্থায়ী থাকে। close() নিজে commit করে না — না-commit করা বদল ফেলে দেয়। (Python 3.12 থেকে autocommit নামে একটি নতুন সেটিং আছে, যা দিয়ে এই আচরণ স্পষ্টভাবে ঠিক করা যায়; ডিফল্ট আগের মতোই।)
with con: — commit করে, কিন্তু বন্ধ করে না
পর্ব ২৬-এ with open(...) শিখেছেন — ব্লক শেষে ফাইল বন্ধ হয়। তাই স্বাভাবিকভাবেই মনে হয় with sqlite3.connect(...) as con: ব্লক শেষে connection বন্ধ করবে। করে না। connection-এর context manager ভিন্ন কাজ করে: ব্লক সফলভাবে শেষ হলে commit(), exception হলে rollback() — আর connection খোলাই থাকে। এটি একটি transaction-এর সীমানা, সংযোগের আয়ু নয়।
import sqlite3
from contextlib import closing
# with con: → commit/rollback; closing(...) → শেষে close()
with closing(sqlite3.connect("shop.db")) as con:
con.execute("PRAGMA foreign_keys = ON")
with con: # একটি transaction
con.execute("INSERT INTO customers (name, phone, district) VALUES (?, ?, ?)",
("সাকিব", "01611000004", "চট্টগ্রাম"))
# এখানে commit হয়ে গেছে, connection এখনো খোলা
# এখানে connection বন্ধcontextlib.closing ব্লক শেষে object-এর close() ডাকে। বাইরের with সংযোগের আয়ু, ভেতরেরটি transaction. শেষে শুধু con.close() লিখলে মাঝপথের exception-এ সেই লাইনে পৌঁছানো যায় না — তাই closing নিরাপদ। পর্ব ৩৯-এ context manager-এর ভেতরের কথা বিস্তারিত দেখবেন।
sqlite3.Row-এর কয়েকটি সুবিধা: row["name"] আর row[0] দুটিই চলে; নামের বড়-ছোট হাতের অক্ষর মানে না (row["NAME"]-ও চলে); row.keys() কলামের নামের list দেয়; আর dict(row) দিয়ে সরাসরি dict — পর্ব ২৭-এর json.dumps-এ পাঠানোর জন্য তৈরি।
৬ · ? চিহ্নই নিরাপত্তা — parameterized query বনাম f-string SQL
পর্ব ১০-এ f-string শিখেছেন, আর সেটি এত সুবিধাজনক যে SQL লিখতেও হাত চলে যায়: f"... WHERE name = '{name}'". এটি ডেটাবেস প্রোগ্রামিংয়ের সবচেয়ে বিপজ্জনক অভ্যাস। কেন, একটি নিরীহ উদাহরণে দেখুন। আমাদের দেশে D'Costa, D'Rozario-র মতো পদবি আছে, যার ভেতরে একটি apostrophe ('):
name = "D'Costa"
sql = f"SELECT phone FROM customers WHERE name = '{name}'"
print(sql)
# SELECT phone FROM customers WHERE name = 'D'Costa'
# con.execute(sql) → sqlite3.OperationalError: near "Costa": syntax errorকী ঘটল? নামের ভেতরের ' চিহ্নটি SQL-এর string-কে আগেই শেষ করে দিয়েছে, আর বাকি Costa' অংশটিকে ডেটাবেস SQL-এর নির্দেশ হিসেবে পড়তে গেছে। অর্থাৎ ব্যবহারকারীর দেওয়া ডেটা SQL-এর কোডের অংশ হয়ে গেছে। এখানে ফল শুধু একটি syntax error. কিন্তু কেউ জেনে-বুঝে এমন লেখা দিলে, যা query-র গঠনই বদলে দেয় — শর্তটিকে সবসময় সত্য করে, বা অন্য টেবিলের ডেটা টেনে আনে — সেটিই SQL injection: পর্ব ১০-এ দেখা injection-এর সবচেয়ে বিখ্যাত রূপ। লগইন ফর্ম, সার্চ বক্স, URL-এর প্যারামিটার — যেখানেই ইনপুট SQL-এ জোড়া লাগে, সেখানেই এই ঝুঁকি।
সমাধান — মান আলাদা পথে পাঠানো
সমাধান সহজ ও সম্পূর্ণ: SQL লেখায় মানের জায়গায় শুধু একটি চিহ্ন রাখুন, আর মানগুলো আলাদা করে দিন। ডেটাবেস প্রথমে SQL-এর গঠন বুঝে নেয়, তারপর মানগুলো বসায় — শুধুই মান হিসেবে, কোড হিসেবে কখনো নয়। মানের ভেতরে যত উদ্ধৃতি চিহ্নই থাকুক, তা নামেরই অংশ থাকে।
# ১) প্রশ্নবোধক চিহ্ন — মান যায় একটি tuple-এ (একটি মান হলেও কমা দিন)
con.execute("SELECT phone FROM customers WHERE name = ?", (name,))
# ২) নামযুক্ত চিহ্ন — মান যায় একটি dict-এ; অনেক মান হলে পড়তে সহজ
con.execute("SELECT * FROM orders WHERE customer_id = :cid AND amount >= :min",
{"cid": 1, "min": 500})দুটি ছোট খুঁটিনাটি। প্রথমত, (name,)-এর কমা জরুরি — (name) শুধু বন্ধনীতে মোড়া একটি string, tuple নয় (পর্ব ১৫)। string দিলে sqlite3 প্রতিটি অক্ষরকে আলাদা মান ভাবে আর “Incorrect number of bindings” error দেয়। দ্বিতীয়ত, চিহ্ন বসে শুধু মানের জায়গায়। টেবিল বা কলামের নাম (ORDER BY ?) চিহ্ন দিয়ে বদলানো যায় না। ব্যবহারকারী যদি সাজানোর কলাম বাছেন, তাহলে আগে থেকে ঠিক করা একটি তালিকা থেকে মিলিয়ে নিন: if col not in {"name", "district"}: raise ValueError — একে বলে allow-list.
“আমি নিজে escape করে নেব” — করবেন না। name.replace("'", "''") ধাঁচের হাতে বানানো সুরক্ষা একটি না একটি কোণে ফসকে যায়: অন্য encoding, অন্য চিহ্ন, সংখ্যার ঘর যেখানে উদ্ধৃতিই নেই। parameterized query-ই একমাত্র নির্ভরযোগ্য পথ, আর সেটি কোডকে পরিষ্কারও রাখে। সাথে least privilege: অ্যাপের ডেটাবেস ব্যবহারকারীকে শুধু দরকারি অনুমতি দিন, যাতে কোথাও ভুল হলেও ক্ষতি ছোট থাকে। নিরাপত্তার বাকি দিক পর্ব ৫৪-এ।
৭ · transaction আর ACID — ওয়ালেট থেকে ওয়ালেটে টাকা পাঠানো
বিকাশ বা নগদে রিয়া জয়কে ৫০০ টাকা পাঠালেন। ডেটাবেসে এটি দুটি কাজ: রিয়ার ব্যালান্স থেকে ৫০০ কাটা, জয়ের ব্যালান্সে ৫০০ যোগ। এখন ভাবুন, প্রথম কাজের পর আর দ্বিতীয়টির আগে সার্ভারের বিদ্যুৎ চলে গেল। রিয়ার টাকা কাটা হয়েছে, জয় পাননি — ৫০০ টাকা হাওয়া। দরকার এমন নিশ্চয়তা: দুটি কাজ হয় একসাথে ঘটবে, নয়তো একটিও না. এটাই transaction.
ডেটাবেসের transaction চারটি প্রতিশ্রুতি দেয়, যাদের আদ্যক্ষর মিলে ACID:
| অক্ষর | মানে | ওয়ালেটের উদাহরণে |
|---|---|---|
| A — Atomicity | সব, নয়তো কিছুই না | কাটা আর যোগ — দুটিই হবে, নয়তো কোনোটিই না |
| C — Consistency | প্রতিটি নিয়ম অটুট থাকে | CHECK (balance >= 0) কখনো ভাঙবে না |
| I — Isolation | একে অপরের অর্ধেক কাজ দেখে না | মাঝপথে অন্য কেউ “কাটা হয়েছে, যোগ হয়নি” অবস্থা দেখবে না |
| D — Durability | commit মানে স্থায়ী | “সফল” দেখানোর পর বিদ্যুৎ গেলেও লেনদেন টিকে থাকে |
SQLite কীভাবে durability দেয়? লেখার আগে সে বদলের একটি হিসাব আলাদা ফাইলে (journal) রাখে। মাঝপথে crash হলে পরের বার খোলার সময় সেই হিসাব দেখে অসম্পূর্ণ কাজ মুছে আগের অবস্থায় ফেরে। আধুনিক মোড WAL (PRAGMA journal_mode=WAL) এর আরেক রূপ — এতে একজন লিখতে থাকলেও অন্যরা পড়তে পারে। এবার ওয়ালেটের হিসাব Python-এ। ইচ্ছা করেই আগে যোগ, পরে কাটা — দ্বিতীয় ধাপ ব্যর্থ হলে প্রথমটিও ফেরত যায় কি না দেখতে:
1with con: # জয় → রিয়া, 900 টাকা2 con.execute("UPDATE wallets SET balance = balance + 900 WHERE name = 'রিয়া'")3 con.execute("UPDATE wallets SET balance = balance - 900 WHERE name = 'জয়'")4# with শেষ: exception থাকলে rollback, না থাকলে commit5print(con.execute("SELECT name, balance FROM wallets ORDER BY name").fetchall())
| রিয়া | |
| জয় | |
| অবস্থা |
- শুরুর অবস্থা। আগের একটি সফল লেনদেনের পর রিয়ার ৭০০, জয়ের ৫০০ টাকা — commit করা, স্থায়ী। এখন জয় রিয়াকে ৯০০ পাঠাতে চান, যা তাঁর কাছে নেই।
with con:ব্লকে ঢোকা হলো। - প্রথম UPDATE সফল। sqlite3 নিজে থেকে একটি transaction খোলে, রিয়ার ব্যালান্স হয় ১৬০০. কিন্তু এটি এখনো অস্থায়ী — commit হয়নি, অন্য কোনো connection এই ১৬০০ দেখছে না (Isolation).
- দ্বিতীয় UPDATE ব্যর্থ। ৫০০ − ৯০০ = −৪০০, যা
CHECK (balance >= 0)ভাঙে। ডেটাবেস এই বাক্যটি নাকচ করে, জয়ের সারি বদলায় না, আর Python-এsqlite3.IntegrityErrorওঠে। সমস্যা: রিয়ার অস্থায়ী ১৬০০ এখনো ঝুলে আছে। - rollback — সব ফেরত। exception নিয়ে
withব্লক থেকে বেরোনোর সময় connection-এর context managerrollback()ডাকে। transaction-এর সব অস্থায়ী বদল মুছে যায় — রিয়া আবার ৭০০. exception নিজে কিন্তু থামে না, বাইরে যায়; তাকে ধরতে হবে try/except দিয়ে। - দুটি সারিই অক্ষত। SELECT দেখায় ঠিক শুরুর অবস্থা। টাকা তৈরি হয়নি, হারায়নি — Atomicity আর Consistency একসাথে কাজ করেছে। CHECK constraint না থাকলে জয়ের ব্যালান্স −৪০০ হয়ে যেত, আর এই সুরক্ষা ঘটতই না; তাই নিয়ম Python-এ যাচাই করার পাশাপাশি schema-তেও বসান।
import sqlite3
from contextlib import closing
def transfer(con, src, dst, amount):
try:
with con: # সফল হলে commit, exception হলে rollback
con.execute("UPDATE wallets SET balance = balance + ? WHERE name = ?", (amount, dst))
con.execute("UPDATE wallets SET balance = balance - ? WHERE name = ?", (amount, src))
print("সফল:", src, "→", dst, amount)
except sqlite3.IntegrityError as e:
print("বাতিল:", e)
with closing(sqlite3.connect(":memory:")) as con:
con.execute("CREATE TABLE wallets (name TEXT PRIMARY KEY, "
"balance INTEGER NOT NULL CHECK (balance >= 0)) STRICT")
con.executemany("INSERT INTO wallets VALUES (?, ?)", [("রিয়া", 1000), ("জয়", 200)])
con.commit()
transfer(con, "রিয়া", "জয়", 300)
transfer(con, "জয়", "রিয়া", 900)
print(con.execute("SELECT name, balance FROM wallets ORDER BY name").fetchall())আউটপুট তিন লাইন। সফল: রিয়া → জয় 300 — প্রথম লেনদেনে দুটি UPDATE-ই সফল, with ব্লক commit করেছে; এখন রিয়া ৭০০, জয় ৫০০. বাতিল: CHECK constraint failed: balance >= 0 — দ্বিতীয় লেনদেনে রিয়ার যোগ হয়ে গিয়েছিল, কিন্তু জয়ের কাটা নিয়ম ভাঙায় পুরো transaction rollback. শেষ লাইন [('জয়', 500), ('রিয়া', 700)] প্রমাণ করে রিয়ার অস্থায়ী ৯০০ টাকাও ফেরত গেছে। ORDER BY name-এ বাংলা নাম Unicode code point অনুযায়ী সাজে — “জ” আগে, “র” পরে। আর closing শেষে connection বন্ধ করেছে; with con: একা তা করত না। টেবিলের শেষে STRICT কী করে, পরের অংশে দেখবেন।
বাস্তব ব্যবস্থায় আরও একটি স্তর থাকে: দুজন একই মুহূর্তে একই ওয়ালেট থেকে টাকা পাঠালে কী হবে? SQLite একসময়ে একজন লেখককেই অনুমতি দেয় — অন্যজন অপেক্ষা করে (ডিফল্টে ৫ সেকেন্ড, connect(..., timeout=...) দিয়ে বদলানো যায়), সময় পেরোলে database is locked error. পর্ব ৪৪-এর race condition-এর ডেটাবেস-সমাধান: “পড়ো, হিসাব করো, লেখো” Python-এ না করে balance = balance - ? দিয়ে ডেটাবেসের ভেতরে এক বাক্যে, একটি transaction-এ।
৮ · index, তারিখ, বাংলা আর SQLite-এর ধরন-নমনীয়তা
index — বইয়ের শেষের নির্ঘণ্ট
দশ লাখ ক্রেতার টেবিলে WHERE phone = ? চালালে index না থাকলে ডেটাবেসকে প্রতিটি সারি একে একে দেখতে হয় — একে বলে full table scan. একটি index হলো বইয়ের শেষের নির্ঘণ্টের মতো: বিষয়গুলো বর্ণানুক্রমে সাজানো, পাশে পৃষ্ঠা নম্বর। ডেটাবেস নিজের ভেতরের query plan দেখাতে পারে — EXPLAIN QUERY PLAN দিয়ে:
SCAN customers সারি 1 … মেলে না সারি 2 … মেলে না … সারি 1000000 … # প্রতিটি সারি পড়তে হয় — O(n)
CREATE INDEX idx_customers_phone ON customers(phone); # ফোন নম্বরগুলো সাজানো অবস্থায় আলাদা একটি B-tree-তে, # প্রতিটির পাশে সেই সারির rowid (ঠিকানা)
মূল নোড: … 0171 … 0181 … 0191 … → 0181-এর শাখা মাঝের নোড: … 018110 … 018120 … → 018110-এর শাখা পাতা: 01811000002 → rowid 2 # দশ লাখ সারিতেও মাত্র কয়েক ধাপ — O(log n)
SEARCH customers USING INDEX idx_customers_phone (phone=?) # rowid 2 ধরে সরাসরি সেই সারিতে লাফ
+ বাড়তি জায়গা (প্রতিটি index আলাদা একটি গাছ) + প্রতিটি INSERT/UPDATE/DELETE-এ index-ও বদলাতে হয় # নিয়ম: যে কলাম দিয়ে বারবার খোঁজেন, JOIN করেন বা সাজান — # শুধু সেগুলোতেই index
- index ছাড়া — SCAN. ডেটাবেস জানে না কোন সারিতে নম্বরটি আছে, তাই প্রথম থেকে শেষ পর্যন্ত সব পড়ে। এক হাজার সারিতে টের পাবেন না, দশ লাখে প্রতিটি খোঁজা ধীর হবে, আর সার্ভারে একসাথে অনেক খোঁজা হলে সব আটকে যাবে।
- index বানানো।
CREATE INDEXফোন নম্বরগুলোকে সাজিয়ে একটি আলাদা B-tree-তে রাখে, প্রতিটির পাশে আসল সারির ঠিকানা।UNIQUEconstraint দিলে SQLite নিজেই এমন একটি index বানিয়ে নেয়। - গাছ বেয়ে নামা। সাজানো তালিকায় খোঁজা পর্ব ০২-এর binary search-এর আত্মীয়। B-tree-র প্রতিটি নোডে অনেক চাবি থাকে, তাই গাছটি খুব চওড়া আর অগভীর — দশ লাখ সারিতেও মাত্র কয়েকটি নোড পড়লেই চলে।
- প্ল্যান বদলাল। একই প্রশ্নে এবার
SEARCH … USING INDEX. SQL একটুও বদলাতে হয়নি — ডেটাবেস নিজে index দেখে পথ বদলেছে। এটাই declarative ভাষার সুবিধা। - প্রতিটি index-এর দাম আছে। জায়গা লাগে, আর প্রতিটি লেখায় index-ও হালনাগাদ করতে হয় বলে লেখা ধীর হয়। তাই “সব কলামে index” ভুল। যেগুলো দিয়ে WHERE, JOIN বা ORDER BY করেন, সেগুলো বেছে নিন, আর EXPLAIN QUERY PLAN দিয়ে মেপে দেখুন।
Python থেকে con.execute("EXPLAIN QUERY PLAN SELECT ...", params).fetchall() একটি tuple-এর list দেয়, যার শেষ ঘরে 'SCAN customers' বা 'SEARCH customers USING INDEX ...' লেখা থাকে। হুবহু লেখা SQLite-এর version ভেদে একটু আলাদা হতে পারে (Python 3.14-এর Windows build-এ SQLite 3.50 থাকে)। পর্ব ৪৬-এর মন্ত্র — “আগে মাপো, পরে দ্রুত করো” — ডেটাবেসে তার হাতিয়ার এটিই।
তারিখ — ISO লেখা হিসেবে
SQLite-এ আলাদা কোনো তারিখের ধরন নেই। প্রচলিত পথ হলো তারিখকে ISO 8601 লেখা হিসেবে রাখা: '2026-10-18' বা '2026-10-18 14:30:00'. এই ফরম্যাটের জাদু হলো, লেখা হিসেবে সাজালেই তারিখের ক্রমে সাজে — তাই WHERE created_at >= '2026-10-01' আর ORDER BY created_at ঠিকঠাক চলে। “18/10/2026” লিখলে এই সুবিধা হারাবেন। SQLite-এর date() function-ও এই ফরম্যাট বোঝে: SELECT date('2026-10-18', '+30 days') দেয় '2026-11-17'. Python দিকে .isoformat() আর fromisoformat() — পরের পর্বে (পর্ব ২৯) datetime বিস্তারিত দেখবেন।
বাংলা লেখা
SQLite লেখা রাখে UTF-8-এ, তাই বাংলা রাখা-পড়ায় কোনো বিশেষ সেটিং লাগে না — উপরের দুটি কোডেই দেখেছেন। দুটি সীমা: ORDER BY বাংলা সাজায় Unicode code point অনুযায়ী, যা অভিধানের ক্রমের সাথে সবসময় মেলে না; আর বিল্ট-ইন LIKE ও lower()-এর বড়-ছোট হাতের নিয়ম শুধু ইংরেজি (ASCII) অক্ষরে খাটে।
ধরন-নমনীয়তা — আর STRICT টেবিল
এবার SQLite-এর সবচেয়ে বড় চমক। PostgreSQL-এ INTEGER কলামে 'abc' ঢোকাতে চাইলে error. SQLite-এ সাধারণ টেবিলে ঢুকে যায়. কারণ SQLite-এ কলামের ঘোষিত ধরন একটি “পছন্দ” মাত্র — একে বলে type affinity: সম্ভব হলে মানকে সেই ধরনে বদলায় ('42' হয়ে যায় সংখ্যা 42), না পারলে যা পেয়েছে তা-ই রাখে। পর্ব ০৬-এর dynamic typing-এর মতো — ধরন থাকে মানের সাথে, কলামের সাথে নয়।
-- সাধারণ টেবিল: CREATE TABLE f (n INTEGER)
INSERT INTO f VALUES ('abc'), ('42'), (3.0), ('৫');
SELECT n, typeof(n) FROM f;
-- ('abc', 'text'), (42, 'integer'), (3, 'integer'), ('৫', 'text')
-- STRICT টেবিল: CREATE TABLE s (id INTEGER PRIMARY KEY, n INTEGER) STRICT
INSERT INTO s VALUES (1, 'abc');
-- sqlite3.IntegrityError: cannot store TEXT value in INTEGER column s.n
লক্ষ করুন, বাংলা অঙ্ক '৫' সংখ্যা হয়নি — লেখাই থেকে গেছে; ফর্ম থেকে আসা বাংলা অঙ্ক Python-এ আগে রূপান্তর করে নিন। SQLite 3.37 (২০২১) থেকে টেবিলের শেষে STRICT লিখলে ধরন কড়াভাবে মানা হয় — একে বলে STRICT table. তখন শুধু INT/INTEGER, REAL, TEXT, BLOB আর ANY ধরন লেখা যায়, আর ভুল ধরনের মান ঢুকতে পারে না ('42'-এর মতো নিখুঁতভাবে সংখ্যায় বদলানো যায় এমন লেখা অবশ্য বদলে নেওয়া হয়)। নতুন প্রজেক্টে STRICT ব্যবহার করুন — ভুল ডেটা ঢোকার মুহূর্তেই ধরা পড়বে, মাস পরে রিপোর্টে নয়।
schema বদলানো — migration আর user_version
অ্যাপ চালু হওয়ার তিন মাস পরে দরকার হলো ক্রেতার ইমেইল রাখা। ডেটাবেসে ইতিমধ্যে হাজার ক্রেতা — টেবিল মুছে নতুন করে বানানো চলবে না। চলমান ডেটা না হারিয়ে schema ধাপে ধাপে বদলানোকে বলে schema migration. SQLite ফাইলের ভেতরে একটি ছোট সংখ্যা রাখার জায়গা দেয়, PRAGMA user_version (শুরুতে 0). কৌশল: প্রতিটি বদলের একটি ক্রমিক নম্বর রাখুন, প্রোগ্রাম চালুর সময় দেখুন ফাইলটি কোন সংস্করণে আছে, আর বাকি ধাপগুলো চালান:
MIGRATIONS = [
"CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL) STRICT", # 1
"ALTER TABLE customers ADD COLUMN email TEXT", # 2
]
def migrate(con):
(version,) = con.execute("PRAGMA user_version").fetchone()
for number, sql in enumerate(MIGRATIONS[version:], start=version + 1):
with con:
con.execute(sql)
con.execute(f"PRAGMA user_version = {number}") # number আমাদের নিজের intশেষ লাইনে f-string? PRAGMA-তে ? চলে না, আর number ব্যবহারকারীর ইনপুট নয় — আমাদের নিজের লুপের int. নিয়মটা তাই: বাইরে থেকে আসা মান কখনো SQL লেখায় নয়. বড় প্রজেক্টে এই কাজের জন্য তৈরি টুল আছে, যেমন SQLAlchemy-র সঙ্গী Alembic.
৯ · বাস্তবে — মাঝপথের প্রজেক্ট ডেটাবেসে, কখন SQLite ছাড়বেন, আর ORM কোথায় বসে
পর্ব ২১-এর ফলাফল-টুলকে ডেটাবেসে আনা
পর্ব ২১-এর মাঝপথের প্রজেক্টে ছাত্রদের তথ্য থাকত একটি dict-এ: নাম → {"name": ..., "marks": [...]}. প্রথম কাজ schema ভাবা। marks একটি list — কিন্তু relational টেবিলের একটি ঘরে একটিই মান। তাই list ভেঙে আলাদা টেবিল:
CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
) STRICT;
CREATE TABLE marks (
student_id INTEGER NOT NULL REFERENCES students(id),
subject TEXT NOT NULL,
mark INTEGER NOT NULL CHECK (mark BETWEEN 0 AND 100),
PRIMARY KEY (student_id, subject) -- এক ছাত্রের এক বিষয়ে একটিই নম্বর
) STRICT;এবার আগের টুলের কমান্ডগুলো একেকটি SQL হয়ে যায়। “রিয়ার নম্বর যোগ করো” — একটি INSERT. “সবার গড়, বড় থেকে ছোট” — SELECT s.name, AVG(m.mark) AS avg FROM students s JOIN marks m ON m.student_id = s.id GROUP BY s.id ORDER BY avg DESC. “কে কে ফেল করেছে” — HAVING MIN(m.mark) < 33. আগে প্রতিটি প্রশ্নে লুপ আর dict; এখন একটি বাক্য। আর CHECK নিশ্চিত করে কেউ ভুল করে ১৫০ নম্বর তুলতে পারবে না।
N+1 — লুপের ভেতরে query
Python থেকে ডেটাবেস ব্যবহারের সময় একটি ভুল খুব প্রচলিত: আগে সব ছাত্রের তালিকা আনা, তারপর লুপে প্রতিটি ছাত্রের জন্য আলাদা করে SELECT ... WHERE student_id = ?. একশ ছাত্র মানে ১টি + ১০০টি query — একে বলে N+1 সমস্যা. SQLite-এ খরচ কম, কিন্তু নেটওয়ার্কের ওপারের PostgreSQL-এ প্রতিটি query-তে যাওয়া-আসার সময় যোগ হয়। সমাধান: একটি JOIN দিয়ে একবারে সব আনা, বা WHERE student_id IN (...).
কখন SQLite ছেড়ে PostgreSQL বা MySQL
| পরিস্থিতি | SQLite | PostgreSQL / MySQL |
|---|---|---|
| ডেস্কটপ/মোবাইল অ্যাপ, CLI টুল, ছোট ওয়েবসাইট | আদর্শ | অতিরিক্ত ঝামেলা |
| টেস্ট, প্রোটোটাইপ, ডেটা বিশ্লেষণ | আদর্শ (:memory:) | সম্ভব, তবে সেটআপ লাগে |
| একসাথে অনেক লেখক | একসময়ে একজন লেখক | অনেকে একসাথে, সারি ধরে lock |
| কয়েকটি সার্ভার একই ডেটা ব্যবহার করবে | ফাইল এক কম্পিউটারে — উপযুক্ত নয় | নেটওয়ার্কে সংযোগ — তৈরিই এর জন্য |
| ব্যবহারকারী, অনুমতি, replication, কড়া ধরন | সীমিত | পূর্ণ সুবিধা |
মোটা দাগে: একটি প্রোগ্রাম, একটি কম্পিউটার — SQLite; অনেক লেখক, কয়েকটি সার্ভার, আলাদা অনুমতি — PostgreSQL বা MySQL. SQL প্রায় একই থাকে, আর লাইব্রেরিগুলো DB-API মানে। একটি পার্থক্য: অনেক লাইব্রেরিতে চিহ্ন ?-এর বদলে %s — দেখতে % formatting-এর মতো হলেও এটিও parameterized query.
ORM — SQL-এর ওপরে একটি স্তর
বড় অ্যাপে প্রতিটি টেবিলের জন্য হাতে SQL লেখা আর সারিকে Python object-এ বদলানো একঘেয়ে হয়ে যায়। ORM (Object-Relational Mapping) এই কাজটি করে: টেবিল হয় একটি Python class, সারি হয় object, আর session.add(customer) লিখলে ORM নিজে INSERT বানায় — অবশ্যই parameterized. পর্ব ৫২-এ SQLAlchemy দিয়ে এটি শিখবেন, পর্ব ৫১-এর FastAPI-র সাথে মিলিয়ে। কিন্তু ORM SQL-কে মুছে দেয় না, শুধু ঢেকে রাখে — ORM-এর কোড ধীর হলে বা N+1 তৈরি করলে বুঝতে হবে নিচে কোন SQL চলছে।
SQLite নিজেই C-তে লেখা; Python-এর sqlite3 module সেই C লাইব্রেরির ওপর একটি পাতলা মোড়ক। C-তে একই কাজ হয় sqlite3_open, sqlite3_prepare_v2, sqlite3_bind_text, sqlite3_step আর শেষে sqlite3_finalize/sqlite3_close দিয়ে — প্রতিটি ধাপে return code যাচাই আর হাতে resource মুক্ত করা। Python-এর ? চিহ্ন ভেতরে সেই bind-ই। ফাইল খোলার নিচের স্তর দেখুন C কোর্সের ফাইল I/O পর্বে; Java থেকে একই ধারণা (PreparedStatement, transaction) দেখতে চাইলে Java কোর্সের JDBC পর্ব।
- primary key আর foreign key-র পার্থক্য? — primary key নিজের টেবিলের প্রতিটি সারিকে অনন্যভাবে চেনায়; foreign key অন্য টেবিলের primary key-র দিকে নির্দেশ করে দুই টেবিল যুক্ত করে।
- WHERE আর HAVING-এর পার্থক্য? — WHERE দল বানানোর আগে প্রতিটি সারি ছাঁকে; HAVING GROUP BY-এর পরে দল ছাঁকে, তাই COUNT/SUM-এর শর্ত HAVING-এ।
- ACID কী? — Atomicity, Consistency, Isolation, Durability — transaction সব-নয়তো-কিছুই-না, নিয়ম অটুট, একে অপরের অর্ধেক কাজ দেখে না, commit-এর পর স্থায়ী।
- SQL injection কীভাবে ঠেকাবেন? — মান কখনো string জোড়া দিয়ে নয়; parameterized query (
?বা:name), নাম-জাতীয় অংশে allow-list, আর least privilege. with sqlite3.connect(...) as con:কি connection বন্ধ করে? — না; সফল হলে commit, exception হলে rollback করে মাত্র। বন্ধ করতেclose()বাcontextlib.closing.
১০ · AI-এর যুগে — AI-এর লেখা ডেটাবেস কোড যাচাই
“একটি SQLite ডেটাবেসে ক্রেতা সেভ করো আর ফোন দিয়ে খোঁজো” — AI এমন কোড সেকেন্ডে দেয়, আর নমুনা ডেটায় চলেও। সমস্যা ধরা পড়ে পরে: আসল ব্যবহারকারীর ইনপুটে, বড় ডেটায়, crash-এর পর। নিচের ভুলগুলো বারবার দেখা যায়:
| AI প্রায়ই যা লেখে | সমস্যা | ভালো রূপ |
|---|---|---|
f"... WHERE phone = '{phone}'" | ইনপুট SQL-এর অংশ হয়ে যায় — injection; apostrophe-তেই ভাঙে | WHERE phone = ?, মান আলাদা tuple-এ |
INSERT-এর পর commit() নেই | প্রোগ্রাম বন্ধ হলে ডেটা হারায়, কেউ টের পায় না | with con: ব্লকে লেখা |
con = sqlite3.connect(...), কোথাও close নেই | সংযোগ খোলা থাকে, ফাইল lock থাকতে পারে | with closing(sqlite3.connect(...)) as con: |
“with connection বন্ধ করে” ধরে নেওয়া | বন্ধ হয় না — শুধু commit/rollback | closing বা স্পষ্ট close() |
SELECT * সবখানে | অদরকারি কলাম আসে; কলাম যোগ হলে index-ধরে-পড়া কোড ভাঙে | দরকারি কলামের নাম লেখা, sqlite3.Row |
| WHERE-এর কলামে কোনো index নেই | ডেটা বাড়লে প্রতিটি খোঁজা full scan | CREATE INDEX, EXPLAIN QUERY PLAN দিয়ে যাচাই |
| লুপের ভেতরে প্রতিটি উপাদানের জন্য query | N+1 — অসংখ্য query | একটি JOIN বা IN (...) |
| দুটি সম্পর্কিত লেখা আলাদা আলাদা commit | মাঝপথে error হলে অর্ধেক কাজ থেকে যায় | দুটি একটি transaction-এ |
REFERENCES লিখে নিশ্চিন্ত | SQLite-এ foreign key যাচাই ডিফল্টে বন্ধ | connect-এর পরেই PRAGMA foreign_keys = ON |
AI-কে ডেটাবেস কোড লিখতে বলার সময় শর্তগুলো আগেই বলে দিন:
AI-এর লেখা ডেটাবেস কোড পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১১৯৭০ সালে IBM-এর গবেষক Edgar F. Codd “টেবিল আর গণিত” দিয়ে ডেটাবেস ভাবার প্রস্তাব দেন। এর আগে ডেটাবেস কেমন ছিল, আর Codd-এর ধারণা কেন বিপ্লবী ছিল?
১৯৬০-এর দশকের ডেটাবেসগুলো ছিল মূলত দুই ধাঁচের — hierarchical (যেমন IBM-এর IMS, যা Apollo মহাকাশ কর্মসূচির যন্ত্রাংশের হিসাব রাখতেও ব্যবহৃত হয়েছিল) আর network মডেল (CODASYL). দুটিতেই ডেটা থাকত পয়েন্টার দিয়ে জোড়া রেকর্ডের গাছ বা জালে। কোনো তথ্য পেতে প্রোগ্রামারকে লিখতে হতো ঠিক কোন পথে হাঁটতে হবে: “এই গ্রাহকের রেকর্ড থেকে প্রথম অর্ডারে যাও, তারপর পরের অর্ডারে…”। ফলে ডিস্কের সাজানো বদলালে প্রোগ্রামগুলোও নতুন করে লিখতে হতো।
১৯৭০ সালের জুনে Communications of the ACM পত্রিকায় Codd-এর প্রবন্ধ “A Relational Model of Data for Large Shared Data Banks” প্রকাশিত হয়। তাঁর মূল কথা: ডেটাকে দেখুন গাণিতিক relation হিসেবে — সহজ ভাষায় টেবিল, যার প্রতিটি সারি একটি তথ্যের সেট। টেবিলগুলোর মধ্যে সম্পর্ক পয়েন্টারে নয়, মানে — একটি টেবিলের key আরেকটির কলামে। আর প্রশ্ন করা হবে সেট-গণিতের ভাষায় (relational algebra ও calculus): কী চাই বলুন, কীভাবে খুঁজবে তা ব্যবস্থা ঠিক করবে।
এর সবচেয়ে বড় ফল data independence: ডেটা ডিস্কে কীভাবে রাখা আছে (কোন index, কোন ক্রম) আর প্রোগ্রাম কী দেখে — এই দুটি আলাদা হয়ে গেল। আজ যখন আপনি একটি index যোগ করেন আর SQL একটুও না বদলে প্রশ্ন দ্রুত হয়ে যায়, সেটি সরাসরি Codd-এর ধারণার ফল। শুরুতে অনেকে সন্দেহ করেছিলেন এত উঁচু স্তরের ভাষা যথেষ্ট দ্রুত চলবে কি না (IBM নিজেও দ্বিধায় ছিল, কারণ IMS ছিল তাদের লাভজনক পণ্য); query optimizer-এর উন্নতি সেই সন্দেহ ভুল প্রমাণ করে। ১৯৮১ সালে Codd কম্পিউটার বিজ্ঞানের সর্বোচ্চ সম্মান Turing Award পান।
পর্ব ১৭-এর set-এর union, intersection-এর গণিতই relational algebra-র ভিত্তি: SQL-এর UNION, INTERSECT, EXCEPT সরাসরি সেট-অপারেশন, আর JOIN হলো দুই সেটের গুণফল থেকে শর্ত মেনে বাছাই।
মূল উপলব্ধি: ভালো abstraction “কী” আর “কীভাবে”-কে আলাদা করে। Codd ডেটার অর্থকে তার সংরক্ষণের কৌশল থেকে মুক্ত করেছিলেন — তাই পঞ্চাশ বছর পরেও আমরা একই ধারণায় কাজ করছি, যদিও ভেতরের প্রযুক্তি বহুবার বদলে গেছে।
প্র ০২SQL-কে কেউ “এস-কিউ-এল” বলেন, কেউ “সিকুয়েল”. এই দুই নামের পেছনে কোন ইতিহাস লুকিয়ে আছে, আর একটি ১৯৭০-এর দশকের ভাষা আজও কেন ডেটার প্রধান ভাষা?
Codd-এর তত্ত্ব ছিল গাণিতিক; সাধারণ প্রোগ্রামারের জন্য একটি ব্যবহারযোগ্য ভাষা দরকার ছিল। ১৯৭০-এর দশকের শুরুতে IBM-এর San Jose গবেষণাগারে Donald Chamberlin ও Raymond Boyce এমন একটি ভাষা নকশা করেন, যা ইংরেজি বাক্যের মতো পড়া যায়: SELECT name FROM customers WHERE district = 'ঢাকা'. ১৯৭৪ সালে প্রকাশিত এই ভাষার নাম ছিল SEQUEL — Structured English Query Language; একটি ব্রিটিশ কোম্পানির ট্রেডমার্কের সাথে সংঘাত এড়াতে পরে নাম হয় SQL. তাই “সিকুয়েল” পুরোনো নামের স্মৃতি; দুটি উচ্চারণই প্রচলিত।
একই গবেষণাগারের System R প্রকল্প দেখায় যে relational ডেটাবেস বাস্তবে যথেষ্ট দ্রুত বানানো সম্ভব; এর cost-based query optimizer (বিভিন্ন পথের খরচ অনুমান করে সবচেয়ে সস্তাটি বাছা) আজকের প্ল্যানারগুলোর পূর্বসূরি। মজার ব্যাপার, প্রথম বাণিজ্যিক SQL ডেটাবেস IBM-এর নয়: Larry Ellison-এর কোম্পানি (পরে Oracle) প্রকাশিত গবেষণাপত্র পড়ে ১৯৭৯ সালে নিজেদের পণ্য আনে। Berkeley-র Ingres প্রকল্পের উত্তরসূরি Postgres থেকেই আজকের PostgreSQL. ১৯৮৬ সালে ANSI আর ১৯৮৭ সালে ISO SQL-কে মানদণ্ড হিসেবে গ্রহণ করে।
SQL কেন টিকে আছে? প্রথমত, declarative নকশা: আপনি ফল বর্ণনা করেন, তাই ডেটাবেস ভেতরের কৌশল বদলালেও (নতুন index, নতুন হার্ডওয়্যার, সমান্তরাল প্রক্রিয়া) পুরোনো প্রশ্ন আরও দ্রুত চলে। দ্বিতীয়ত, শিক্ষার বিনিয়োগ: কোটি মানুষ, অসংখ্য টুল আর রিপোর্ট SQL জানে। তৃতীয়ত, ২০১০-এর দশকে “NoSQL” আন্দোলন SQL-কে চ্যালেঞ্জ করেছিল; কিন্তু পরে অনেক NoSQL ও big-data ব্যবস্থাই SQL-ধাঁচের প্রশ্নের ভাষা যোগ করে — কারণ ব্যবহারকারীরা সেটিই চাইছিলেন।
মূল উপলব্ধি: একটি ভাষা টিকে থাকে যখন সে মানুষের উদ্দেশ্য প্রকাশ করে, যন্ত্রের কৌশল নয়। SQL পঞ্চাশ বছর ধরে “কী চাই” বলার ভাষা — তাই নিচের প্রযুক্তি যত বদলায়, SQL তত বেশি মূল্যবান থাকে।
প্র ০৩একজন প্রোগ্রামারের লেখা, কোনো কোম্পানির পণ্য নয় — তবু SQLite-কে বলা হয় পৃথিবীর সবচেয়ে বেশি ব্যবহৃত ডেটাবেস। এটি কীভাবে সম্ভব হলো, আর এর পেছনের প্রকৌশল-সিদ্ধান্তগুলো কী শেখায়?
D. Richard Hipp ২০০০ সালে SQLite শুরু করেন — তিনি তখন একটি যুদ্ধজাহাজের ক্ষতি-নিয়ন্ত্রণ সফটওয়্যারে কাজ করছিলেন বলে জানা যায়, যেখানে আলাদা ডেটাবেস সার্ভার চালানো ঝামেলার ছিল। তাঁর প্রশ্ন: SQL-এর সব সুবিধা যদি একটি লাইব্রেরিতে, একটি ফাইলে পাওয়া যায়? আজও ছোট একটি দল এটি রক্ষণাবেক্ষণ করে।
“সবচেয়ে বেশি ব্যবহৃত” কথাটি SQLite-এর নিজস্ব দাবি, এবং সেটি যুক্তিসংগত মনে হয়: প্রতিটি Android ও iPhone, প্রধান ব্রাউজারগুলো (Chrome, Firefox, Safari), Windows ও macOS-এর নানা অংশ, অসংখ্য অ্যাপ আর Python, PHP-র মতো ভাষা SQLite বহন করে — একটি স্মার্টফোনেই কয়েকশ SQLite ফাইল থাকতে পারে।
এই বিস্তারের পেছনে কয়েকটি সচেতন সিদ্ধান্ত। এক, শূন্য সেটআপ: কোনো সার্ভার, ব্যবহারকারী বা কনফিগারেশন নেই — অ্যাপের সাথে লাইব্রেরি জুড়ে দিলেই হলো। দুই, লাইসেন্সের বাধা নেই: public domain হওয়ায় যেকোনো কোম্পানি আইনি দুশ্চিন্তা ছাড়াই ব্যবহার করতে পারে। তিন, স্থিতিশীল ফাইল ফরম্যাট: SQLite 3-এর ফাইল ফরম্যাট ২০০৪ থেকে পেছনের দিকে সামঞ্জস্যপূর্ণ, আর ডেভেলপাররা ২০৫০ সাল পর্যন্ত সমর্থনের অঙ্গীকার করেছেন; US Library of Congress ডিজিটাল সংরক্ষণের জন্য SQLite ফরম্যাটকে সুপারিশকৃত ফরম্যাটের তালিকায় রেখেছে। চার, কঠোর টেস্টিং: SQLite-এর টেস্ট কোড মূল কোডের চেয়ে বহু গুণ বড়, আর প্রতিটি শাখা পরীক্ষার আওতায় — বিমান-সফটওয়্যারের মানদণ্ড থেকে নেওয়া পদ্ধতিতে।
সীমাও স্পষ্ট করে বলা: একসময়ে একজন লেখক, ব্যবহারকারী-অনুমতির ব্যবস্থা নেই — SQLite নিজের ওয়েবসাইটেই বলে কখন অন্য ডেটাবেস নেবেন। এই সংকীর্ণ লক্ষ্যই (“fopen-এর বদলি হও, Oracle নয়”) তাকে নিজের কাজে অসাধারণ করেছে।
মূল উপলব্ধি: সবচেয়ে বেশি ব্যবহৃত সফটওয়্যার প্রায়ই সবচেয়ে বড় বা জাঁকালো নয়, বরং যেটি সবচেয়ে কম বাধা দেয়, সবচেয়ে বেশি নির্ভরযোগ্য, আর নিজের সীমা জানে। আপনার নিজের লাইব্রেরি বা টুল নকশার সময়ও এই প্রশ্নগুলো করুন।
প্র ০৪ACID শব্দটি ১৯৮৩ সালে দুই জার্মান গবেষক জনপ্রিয় করেন। কিন্তু transaction-এর ধারণা কোথা থেকে এল, আর বড় ওয়েব কোম্পানিগুলো কেন একসময় ACID থেকে সরে গিয়ে আবার ফিরে আসে?
transaction-এর ধারণা ব্যবসার পুরোনো অভিজ্ঞতা থেকে — ব্যাংকের হিসাব, বিমানের টিকিট। ১৯৭০-এর দশকে IBM-এর System R-এ কাজ করা Jim Gray একে প্রকৌশলের ভাষা দেন (locking, log, crash-এর পর পুনরুদ্ধার); তাঁর ১৯৮১ সালের প্রবন্ধ “The Transaction Concept: Virtues and Limitations” মূল গুণগুলো বর্ণনা করে। ১৯৯৮ সালে তিনি Turing Award পান।
১৯৮৩ সালে Theo Härder ও Andreas Reuter ACM Computing Surveys-এ “Principles of Transaction-Oriented Database Recovery” প্রবন্ধে চারটি গুণের আদ্যক্ষর দিয়ে স্মরণীয় শব্দটি বানান: ACID. লক্ষ করার বিষয়, চারটি অক্ষর সমান নয়: “C” অনেকটাই অ্যাপের দায়িত্ব — ডেটাবেস শুধু আপনার দেওয়া নিয়ম (CHECK, foreign key) রক্ষা করে; নিয়ম না দিলে রক্ষা করার কিছু নেই। আর “I”-এর অনেক স্তর আছে (isolation level) — পূর্ণ isolation ধীর, তাই অনেক ডেটাবেস ডিফল্টে একটু শিথিল স্তর দেয়।
২০০০-এর দশকে Google, Amazon-এর মতো কোম্পানির ডেটা এক মেশিনে আঁটছিল না, আর হাজার মেশিনে ছড়ানো ডেটায় ACID বজায় রাখা কঠিন — Eric Brewer-এর CAP ধারণা দেখায়, নেটওয়ার্ক বিচ্ছিন্ন হলে consistency আর availability দুটি একসাথে পুরোপুরি পাওয়া যায় না। ফলে অনেক “NoSQL” ব্যবস্থা ইচ্ছা করে দুর্বল নিশ্চয়তা বেছে নেয় — “শেষ পর্যন্ত সব মিলে যাবে” (eventual consistency). কিন্তু অভিজ্ঞতা দেখাল, দুর্বল নিশ্চয়তা মানে অ্যাপের প্রোগ্রামারদের ঘাড়ে অসংখ্য সূক্ষ্ম বাগ। পরে Google-এর Spanner-এর মতো ব্যবস্থা বিশ্বজুড়ে ছড়ানো ডেটাতেও transaction ফিরিয়ে আনে, আর অনেক NoSQL ডেটাবেসও transaction যোগ করে।
মূল উপলব্ধি: নিশ্চয়তা দুর্বল করলে গতি বাড়ে, কিন্তু জটিলতা হারিয়ে যায় না — ডেটাবেস থেকে সরে আপনার কোডে চলে আসে। তাই যেখানে টাকা বা গুরুত্বপূর্ণ হিসাব, সেখানে আগে transaction, পরে গতির চিন্তা।
প্র ০৫parameterized query-র সমাধান কয়েক দশক ধরে জানা, তবু injection-জাতীয় দুর্বলতা OWASP-এর ঝুঁকি-তালিকার ওপরের দিকেই থেকে যায়। কেন এই সহজ-সমাধানের সমস্যা এত টেকসই?
OWASP ২০০৩ সাল থেকে ওয়েব অ্যাপের দশটি প্রধান ঝুঁকির তালিকা প্রকাশ করছে। injection সেই তালিকায় শুরু থেকেই আছে, আর ২০১০, ২০১৩ ও ২০১৭ সালের তালিকায় ছিল এক নম্বরে। ২০২১-এর তালিকায় এটি তিন নম্বরে নামে (তখন cross-site scripting-কেও এর সাথে মেলানো হয়), কিন্তু পরের তালিকাগুলো থেকেও সরেনি। অথচ সমাধান — ডেটা আর কোড আলাদা পথে পাঠানো — প্রায় সব ডেটাবেস লাইব্রেরিতে বহু বছর ধরে আছে। তাহলে সমস্যা টিকে আছে কেন?
প্রথম কারণ সুবিধার টান: string জোড়া লাগানো সবচেয়ে স্বাভাবিক মনে হয়। f-string-এর মতো সুন্দর সুবিধা SQL লিখতেও প্রলুব্ধ করে, আর পরীক্ষার নিরীহ ডেটায় কোড চলেও যায় — বিপদ দেখা দেয় শুধু অস্বাভাবিক ইনপুটে। দ্বিতীয়, পুরোনো কোড, যার প্রতিটি query আবার লেখা ব্যয়বহুল। তৃতীয়, সমাধানের ফাঁক: টেবিল বা কলামের নাম, ORDER BY-এর দিক চিহ্ন দিয়ে পাঠানো যায় না, তাই সেখানে মানুষ আবার string জোড়ে; ORM-এও “raw SQL”-এর রাস্তা খোলা। চতুর্থ, শিক্ষা: অনেক টিউটোরিয়াল আর পুরোনো উত্তর অনিরাপদ উদাহরণ দিয়ে শেখায় — আর সেই উদাহরণ দেখে শেখা AI-ও একই ধাঁচ পুনরাবৃত্তি করতে পারে।
injection শুধু SQL-এ নয় — একই মূল ভুল OS command (পর্ব ৫৩-এর subprocess), HTML (cross-site scripting), এমনকি AI-এর prompt-এও দেখা যায়: অবিশ্বস্ত ডেটা যখন নির্দেশের সাথে এক স্রোতে মেশে। ২০০৭ সালের xkcd কমিক “Exploits of a Mom” (“Bobby Tables”) এই শিক্ষার প্রতীক।
প্রতিরক্ষার স্তরগুলো তাই একসাথে দরকার: সবখানে parameterized query, নাম-জাতীয় অংশে allow-list, ন্যূনতম অনুমতি, ইনপুট যাচাই, আর code review ও স্বয়ংক্রিয় টুল (যেমন পর্ব ৫৪-এর bandit), যা f-string-এ SQL দেখলে সতর্ক করে।
মূল উপলব্ধি: নিরাপত্তার সমস্যা প্রায়ই জ্ঞানের অভাব নয়, অভ্যাস আর সুবিধার সমস্যা। তাই নিরাপদ পথকেই সবচেয়ে সহজ পথ বানাতে হয় — দলে নিয়ম, টুলে স্বয়ংক্রিয় পরীক্ষা, আর নিজের হাতে “মান সবসময় আলাদা পথে” অভ্যাস।
অনুশীলন
এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — অনুমান করা, হাতে চালানো আর মিলিয়ে নেওয়াই আসল কাজ।
-
schema নকশা করুন: একটি লাইব্রেরির জন্য টেবিলগুলো কাগজে আঁকুন — বই (একই বইয়ের কয়েকটি কপি থাকতে পারে), সদস্য, আর কে কোন কপি কবে নিলেন ও ফেরত দিলেন। প্রতিটি টেবিলের primary key, foreign key আর অন্তত দুটি constraint লিখুন।
একটি ভালো নকশা:
books(id PK, title NOT NULL, author, isbn UNIQUE);copies(id PK, book_id → books, shelf)— কারণ একই বইয়ের কয়েকটি কপি, আর ধার দেওয়া হয় কপি, বই নয়;members(id PK, name NOT NULL, phone UNIQUE);loans(id PK, copy_id → copies, member_id → members, borrowed_on TEXT NOT NULL, returned_on TEXT).returned_onNULL মানে বই এখনো ফেরত আসেনি। constraint:CHECK (returned_on IS NULL OR returned_on >= borrowed_on)— ISO তারিখ বলে লেখা হিসেবে তুলনা ঠিক চলে। একটি কপি একসাথে দুজনের কাছে থাকতে পারবে না — এটি একটি সাধারণ CHECK-এ বলা যায় না; অ্যাপে যাচাই করবেন, বা SQLite-এর “partial index” দিয়ে (CREATE UNIQUE INDEX ... ON loans(copy_id) WHERE returned_on IS NULL). -
ক্রম অনুমান করুন: নিচের প্রতিটি প্রশ্নে কী সমস্যা, আর কীভাবে ঠিক করবেন? (ক)
SELECT district, COUNT(*) FROM customers WHERE COUNT(*) > 5 GROUP BY district; (খ)SELECT name FROM customers LIMIT 3— “তিনজন সবচেয়ে পুরোনো ক্রেতা” পেতে; (গ)UPDATE products SET price = price * 1.1— শুধু চালের দাম বাড়াতে।(ক) WHERE চলে GROUP BY-এর আগে, তখনো দল বা COUNT নেই — “misuse of aggregate” error. ঠিক রূপ:
... GROUP BY district HAVING COUNT(*) > 5. (খ) ORDER BY নেই, তাই “যেকোনো তিনজন” — কোনো ক্রমের প্রতিশ্রুতি নেই। ঠিক রূপ:ORDER BY id LIMIT 3(বা যোগদানের তারিখের কলাম থাকলে সেটি দিয়ে)। (গ) WHERE নেই — সব পণ্যের দাম বাড়বে! তার ওপর INTEGER দামে 1.1 গুণ করলে ভগ্নাংশ আসে, আর সাধারণ টেবিলে কলামে float জমা হবে (STRICT টেবিলে error). ঠিক রূপ:WHERE sku = ?দিয়ে, আর নতুন দাম Python-এ পূর্ণসংখ্যায় হিসাব করে পাঠান। আগে একই WHERE দিয়ে SELECT চালিয়ে দেখে নিন। -
বাগ খুঁজুন: একটি AI এই কোড দিয়েছে:
con = sqlite3.connect("shop.db"), তারপর একটি function-এcur.execute(f"INSERT INTO customers (name, phone) VALUES ('{name}', '{phone}')"), আর অন্য function-এ লুপে প্রতিটি ক্রেতার জন্যSELECT * FROM orders WHERE customer_id = {c[0]}. প্রোগ্রাম শেষে কিছু নেই। অন্তত পাঁচটি সমস্যা খুঁজুন।(১) f-string দিয়ে মান জোড়া — D'Costa নামেই ভাঙবে, আর injection-এর দরজা খোলা;
VALUES (?, ?)দিয়ে(name, phone)পাঠান। (২) commit নেই — প্রোগ্রাম শেষে সব নতুন ক্রেতা হারাবে;with con:ব্লকে লিখুন। (৩) connection কখনো বন্ধ হয় না —closingদিয়ে খুলুন। (৪) লুপের ভেতরে প্রতিটি ক্রেতার জন্য আলাদা query — N+1; একটি JOIN দিয়ে একবারে আনুন। (৫)SELECT *আরc[0]— কলামের অবস্থানের ওপর নির্ভর; কলামের নাম আরsqlite3.Rowব্যবহার করুন। (৬)orders.customer_id-তে index না থাকলে প্রতিটি খোঁজা full scan. (৭) schema দেখা না গেলেও জিজ্ঞেস করুন: phone কি UNIQUE, foreign_keys কি চালু? -
ছোট কোড লিখুন: একটি খরচের হিসাবের জন্য
expenses(id, spent_on, category, amount)টেবিল বানান (STRICT, amount > 0), কয়েকটি খরচexecutemanyদিয়ে ঢোকান, আর একটি functionmonthly_report(con, month)লিখুন, যা'2026-10'ধাঁচের মাস নিয়ে খাতভিত্তিক মোট খরচ বড় থেকে ছোট ক্রমে ফেরত দেয়।import sqlite3 from contextlib import closing def monthly_report(con, month): return con.execute( """SELECT category, SUM(amount) AS total FROM expenses WHERE spent_on LIKE ? GROUP BY category ORDER BY total DESC, category""", (month + "-%",)).fetchall() with closing(sqlite3.connect(":memory:")) as con: con.execute("""CREATE TABLE expenses (id INTEGER PRIMARY KEY, spent_on TEXT NOT NULL, category TEXT NOT NULL, amount INTEGER NOT NULL CHECK (amount > 0)) STRICT""") with con: con.executemany("INSERT INTO expenses (spent_on, category, amount) VALUES (?, ?, ?)", [("2026-10-01", "বাজার", 1200), ("2026-10-03", "যাতায়াত", 300), ("2026-10-09", "বাজার", 800), ("2026-11-02", "বাজার", 500)]) print(monthly_report(con, "2026-10")) # [('বাজার', 2000), ('যাতায়াত', 300)]মাসটিও
?দিয়ে গেছে —%wildcard যোগ করা হয়েছে Python-এ, মানের ভেতরে, SQL লেখায় নয়। ISO তারিখ বলে'2026-10-%'শুধু অক্টোবরের সারি মেলায়। বিকল্প:WHERE spent_on >= ? AND spent_on < ?দিয়ে('2026-10-01', '2026-11-01')— এটিspent_on-এর index ব্যবহার করতে পারে, বড় ডেটায় দ্রুত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি SQL SELECT-এর অংশগুলো ডেটাবেস যুক্তির দিক থেকে যে ক্রমে প্রয়োগ করে, সেই ক্রমে সাজান (▲▼ চেপে সরান)
- 📚
FROM/JOIN— কোন টেবিল, কীভাবে জোড়া - 🚫
WHERE— প্রতিটি সারি ছাঁকা - 🗂️
GROUP BY— দল বানানো - 🧹
HAVING— দল ছাঁকা - 🧮
SELECT— ফলাফলের কলাম বানানো - ↕️
ORDER BYতারপরLIMIT— সাজিয়ে কাটা
Python-এ with sqlite3.connect("shop.db") as con: ব্লক কোনো exception ছাড়া শেষ হলে কী ঘটে?
- বদল commit হয় আর connection বন্ধ হয়
- connection বন্ধ হয়, কিন্তু commit হয় না
- বদল commit হয়, কিন্তু connection খোলা থাকে
- সব বদল rollback হয়
SQL-এ “অন্তত ৫ জন ক্রেতা আছে এমন জেলা” বের করতে COUNT(*) >= 5 শর্তটি কোথায় বসাতে হয়?
WHERE-এGROUP BY-এর পরেHAVING-এORDER BY-এFROM-এ
Python-এর sqlite3-এ ব্যবহারকারীর দেওয়া phone দিয়ে নিরাপদে খোঁজার সঠিক রূপ কোনটি?
con.execute(f"SELECT name FROM customers WHERE phone = '{phone}'")con.execute("SELECT name FROM customers WHERE phone = '%s'" % phone)con.execute("SELECT name FROM customers WHERE phone = ?", (phone))con.execute("SELECT name FROM customers WHERE phone = ?", (phone,))
একটি ব্যাংক ট্রান্সফারে “টাকা কাটা হলো কিন্তু অন্য হিসাবে জমা হলো না” — এমন অর্ধেক অবস্থা কখনো ঘটবে না, এই নিশ্চয়তা ACID-এর কোন অক্ষর দেয়?
- A — Atomicity
- C — Consistency
- I — Isolation
- D — Durability
SQLite-এ REFERENCES customers(id) দিয়ে foreign key লেখার পরেও অচেনা customer_id-সহ অর্ডার ঢুকে যাচ্ছে। সবচেয়ে সম্ভাব্য কারণ কী?
- SQLite foreign key একেবারেই সমর্থন করে না
- commit ডাকা হয়নি
PRAGMA foreign_keys = ONচালানো হয়নি- টেবিলটি STRICT নয়
দশ লাখ সারির একটি টেবিলে WHERE phone = ? দিয়ে খোঁজা ধীর। CREATE INDEX দেওয়ার পর EXPLAIN QUERY PLAN-এ কোন পরিবর্তন আশা করবেন?
- SEARCH থেকে SCAN
- SCAN থেকে SEARCH … USING INDEX
- কোনো পরিবর্তন হবে না, SQL বদলাতে হবে
- টেবিলটি কয়েকটি ছোট টেবিলে ভাগ হয়ে যাবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- বড়, প্রশ্ন-ভারী বা একসাথে লেখা ডেটায় ডেটাবেস. SQLite
sqlite3হিসেবে Python-এর সাথেই আসে — একটি ফাইলে পুরো relational ডেটাবেস; টেস্টে":memory:". - প্রতিটি তথ্য একবার, টেবিলগুলো key দিয়ে যুক্ত; schema-তে NOT NULL, UNIQUE, CHECK, foreign key — আর SQLite-এ
PRAGMA foreign_keys = ON, নতুন টেবিলেSTRICT. - SELECT চলে FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT; সারির শর্ত WHERE-এ, দলের শর্ত HAVING-এ; একাধিক সারি ছাপলে সবসময় ORDER BY.
- মান সবসময়
?বা:nameদিয়ে — কখনো f-string দিয়ে নয়; নাম-জাতীয় অংশে allow-list. with con:= একটি transaction (commit/rollback), কিন্তু বন্ধ করে না —closingদিন; সম্পর্কিত লেখা এক transaction-এ; বারবার খোঁজা কলামে index, EXPLAIN QUERY PLAN দিয়ে যাচাই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৯ · লাইব্রেরি ভ্রমণ — datetime, random, collections, osপরবর্তী পর্বডেটাবেসের তারিখ সামলানোর datetime আর standard library-র অন্য কাজের module.
- পর্ব ২৭ · CSV আর JSON — ডেটা আদান-প্রদানআগের পর্বফাইল-ভিত্তিক ফরম্যাট — কখন ফাইলই যথেষ্ট, কখন ডেটাবেস।
- পর্ব ৫২ · ORM — SQLAlchemy দিয়ে ডেটাবেসসামনেটেবিলকে class, সারিকে object — আজকের SQL-এর ওপরের স্তর।
- Python প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — ৮টি মডিউল, কোন পর্ব কখন।
- Java প্রোগ্রামিং · JDBC ও ডেটাবেসতুলনা করুনএকই SQL, PreparedStatement আর transaction — Java-র চোখে।