এই পর্বে যা শিখবেন
- INSERT-এর পাঁচটি রূপ — positional, কলামের নামসহ, একসাথে অনেক সারি, INSERT … SELECT আর DEFAULT VALUES
- স্বয়ংক্রিয় id আর সেটি ফেরত পাওয়া —
last_insert_rowid(),RETURNING, MySQL-এরLAST_INSERT_ID() - UPDATE দিয়ে এক বা অনেক সারি বদলানো, হিসাব করা মান আর অন্য টেবিল থেকে মান আনা
- WHERE ছাড়া UPDATE/DELETE-এর বিপদ, transaction দিয়ে নিরাপদ অভ্যাস, আর DELETE বনাম TRUNCATE বনাম DROP
- Constraint ভাঙলে কী হয়, UPSERT, soft delete আর বিপুল ডেটা একবারে লোড করা
১ · ডেটা বদলানোর তিনটি ক্রিয়া
মুদি দোকানের বাকির খাতা ভাবুন। নতুন খদ্দের এলে দোকানদার নতুন একটি লাইন লেখেন। কেউ আংশিক টাকা শোধ করলে পুরোনো অঙ্ক কেটে নতুন অঙ্ক লেখেন। কেউ পুরো শোধ করলে লাইনটিই কেটে দেন। খাতার সব কাজ এই তিনটি: লেখা, বদলানো, কাটা. SQL-এ এদের নাম INSERT, UPDATE আর DELETE.
পর্ব ১২-এ দেখেছেন, SQL-এর স্টেটমেন্টগুলো কয়েকটি দলে পড়ে। টেবিল বানানো বা বদলানো হলো DDL (পর্ব ১৩-এর CREATE TABLE). টেবিলের ভেতরের ডেটা নিয়ে কাজ হলো DML — Data Manipulation Language. SELECT ডেটা পড়ে, আর এই পর্বের তিনটি স্টেটমেন্ট ডেটা বদলায়. পড়ার ভুলে বড়জোর ভুল রিপোর্ট আসে। বদলানোর ভুলে ডেটাই নষ্ট হয়। তাই এই পর্বের অর্ধেক কথা “কীভাবে লিখবেন” নিয়ে, বাকি অর্ধেক “কীভাবে নিরাপদে লিখবেন” নিয়ে।
ব্যাংকের ক্যাশিয়ার ভুল অঙ্ক লিখে ফেললে খাতার পাতা ছিঁড়ে ফেলেন না। লাল কালিতে সংশোধনী লেখেন, আর দিনের শেষে হিসাব না মিললে পুরো দিনের লেনদেন আবার মিলিয়ে দেখেন। ডেটাবেসে এই সাবধানতার যন্ত্রগুলো হলো constraint (ভুল মান ঢুকতে না দেওয়া), transaction (কয়েকটি পরিবর্তন একসাথে পাকা করা বা একসাথে বাতিল করা) আর backup.
এই পর্বের সব লাইভ SQL চলে কোর্সের দোকানের ডেটাবেসে (db-shop, পর্ব ১১-এ যার ডিজাইন দেখেছেন)। পাঁচটি টেবিল: customers (customer_id, name, city, phone, joined_on — ৮ জন গ্রাহক), categories (category_id, name — ৪টি শ্রেণি: Electronics, Books, Clothing, Grocery), products (product_id, name, category_id, price, stock — ১০টি পণ্য, id 101–110), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার, id 1001–1010) আর order_items (order_id, product_id, quantity, unit_price — ১৬টি অর্ডার-লাইন)। products-এ আছে CHECK (price > 0), CHECK (stock >= 0) আর stock-এর DEFAULT 0. প্রতিবার “চালান” চাপলে ডেটাবেস নতুন করে তৈরি হয়, তাই নির্ভয়ে মুছুন, বদলান। প্রতিটি INSERT/UPDATE/DELETE-এর পর রানার দেখায় “কয়টি সারি বদলেছে” — এই সংখ্যাটি সবসময় খেয়াল করবেন, এটিই আপনার প্রথম যাচাই।
২ · INSERT — নতুন সারি ঢোকানোর পাঁচটি রূপ
রূপ ১ — শুধু ক্রম ধরে (positional): কেন ঝুঁকিপূর্ণ
সবচেয়ে ছোট লেখা হলো কলামের নাম না দিয়ে শুধু মানগুলো দেওয়া: INSERT INTO customers VALUES (9, 'Sadia Islam', 'Barishal', NULL, '2025-10-20');. DBMS মানগুলো টেবিলের কলাম-ক্রমে বসায় — প্রথম মান customer_id-এ, দ্বিতীয়টি name-এ, এভাবে। db-shop-এর নমুনা ডেটা এভাবেই লেখা, কারণ সেটি একবারের স্ক্রিপ্ট। কিন্তু অ্যাপের কোডে এই positional INSERT একটি টাইম বোমা:
- টেবিলে কলাম যোগ হলেই ভাঙে। পরে কেউ customers-এ
emailকলাম যোগ করলেন (পর্ব ২৩-এর ALTER TABLE). এখন কলাম ৬টি, আপনার INSERT-এ মান ৫টি — ত্রুটি:table customers has 6 columns but 5 values were supplied. - আরও খারাপ: নীরবে ভুল ঘরে মান। যদি কেউ টেবিলটি নতুন করে বানানোর সময় city আর phone-এর ক্রম উল্টে দেন, দুটিই TEXT — কোনো ত্রুটি আসবে না, শুধু সব নতুন গ্রাহকের শহরের ঘরে ফোন নম্বর বসবে। এমন ভুল ধরা পড়ে মাস পরে, রিপোর্টে।
- পড়া কঠিন।
VALUES (9, 'Sadia', 'Barishal', NULL, …)দেখে বোঝা যায় না NULL-টি কোন কলামের।
রূপ ২ ও ৩ — কলামের নামসহ, আর একসাথে অনেক সারি
নিরাপদ রূপ হলো টেবিলের নামের পরে বন্ধনীতে কলামের তালিকা দেওয়া। তখন মানগুলো আপনার লেখা ক্রমে বসে, টেবিলের ক্রমে নয়। যে কলাম তালিকায় নেই, সেটি পায় তার DEFAULT মান, DEFAULT না থাকলে NULL — আর সেই কলামে NOT NULL থাকলে ত্রুটি। একই স্টেটমেন্টে কমা দিয়ে কয়েকটি বন্ধনী লিখলে কয়েকটি সারি একসাথে ঢোকে (multi-row VALUES; SQLite-এ 3.7.11 থেকে, ২০১২ সালে)।
-- কলামের নাম লিখে: customer_id আর phone বাদ
INSERT INTO customers (name, city, joined_on)
VALUES ('Sadia Islam', 'Barishal', '2025-10-20');
-- এক স্টেটমেন্টে দুটি সারি; stock না দিলে DEFAULT 0
INSERT INTO products (product_id, name, category_id, price) VALUES
(111, 'Notebook (pack)', 2, 180),
(112, 'Lungi', 3, 650);
SELECT customer_id, name, city, phone FROM customers WHERE customer_id > 7;
SELECT * FROM products WHERE product_id > 110;প্রথম INSERT-এর পর “✓ INSERT — ১টি সারি বদলেছে”, দ্বিতীয়টির পর “২টি সারি বদলেছে” — একটি স্টেটমেন্ট, দুটি সারি। প্রথম SELECT দেখায় 8 | Mitu Rani | Dhaka | 01411000008 আর নতুন 9 | Sadia Islam | Barishal | NULL: customer_id আমরা দিইনি, SQLite নিজেই 9 বসিয়েছে (অংশ ৩); phone-এর DEFAULT নেই, তাই NULL. দ্বিতীয় SELECT-এ 111 | Notebook (pack) | 2 | 180 | 0 আর 112 | Lungi | 3 | 650 | 0 — stock বাদ দেওয়ায় DEFAULT 0 বসেছে। price কলামটি REAL, তবু এই রানার পূর্ণ সংখ্যাকে 180 হিসেবেই দেখায়; Python-এর sqlite3-এ একই মান 180.0 আকারে আসে। এখন কেউ customers-এ নতুন কলাম যোগ করলেও এই INSERT দুটি আগের মতোই চলবে।
রূপ ৪ ও ৫ — INSERT … SELECT আর DEFAULT VALUES
VALUES-এর জায়গায় একটি SELECT বসালে তার ফলের প্রতিটি সারি নতুন টেবিলে ঢোকে — INSERT INTO … SELECT …. পুরোনো অর্ডার archive টেবিলে সরানো, একটি রিপোর্ট-টেবিল ভরা, বা এক টেবিল থেকে অন্য টেবিলে ডেটা নেওয়ার এটিই সবচেয়ে দ্রুত পথ, কারণ ডেটা ডেটাবেসের ভেতরেই থাকে, অ্যাপে আসা-যাওয়া করে না। SELECT-এর কলাম সংখ্যা আর ক্রম INSERT-এর কলাম-তালিকার সাথে মিলতে হবে। আর যদি টেবিলের প্রতিটি কলামের DEFAULT থাকে, INSERT INTO t DEFAULT VALUES লিখে একটিও মান না দিয়ে সারি ঢোকানো যায়।
নিচের এডিটর db-shop-এর সাথে দুটি ছোট টেবিল যোগ করে: order_archive (order_id, customer_id, order_date, archived_on — শেষেরটির DEFAULT '2025-12-31') আর visit_log (visit_id, page DEFAULT 'home', hits DEFAULT 1).
-- সেপ্টেম্বর ২০২৫-এর delivered অর্ডারগুলো archive-এ কপি
INSERT INTO order_archive (order_id, customer_id, order_date)
SELECT order_id, customer_id, order_date
FROM orders
WHERE status = 'delivered' AND order_date < '2025-10-01';
SELECT * FROM order_archive;
-- কোনো মানই দিলাম না: সব কলাম DEFAULT পাবে
INSERT INTO visit_log DEFAULT VALUES;
SELECT * FROM visit_log;INSERT … SELECT-এর পর “৩টি সারি বদলেছে”। archive-এ তিনটি সারি: 1001 | 1 | 2025-09-01 | 2025-12-31, 1002 | 2 | 2025-09-03 | 2025-12-31 আর 1005 | 4 | 2025-09-15 | 2025-12-31. অক্টোবরের delivered অর্ডার (1007, 1010) WHERE-এ বাদ পড়েছে; archived_on আমরা দিইনি, তাই DEFAULT বসেছে। এখানে INSERT শুধু কপি করেছে — orders-এ সারিগুলো এখনো আছে। আসল “সরানো”-তে এরপর একই WHERE দিয়ে DELETE লাগে, আর দুটি কাজ এক transaction-এ (অংশ ৫)। শেষ SELECT-এ 1 | home | 1: visit_id স্বয়ংক্রিয়, বাকি দুটি DEFAULT.
৩ · স্বয়ংক্রিয় id — নতুন সারির নম্বর ফেরত পাওয়া
পর্ব ৬-এ দেখেছেন, ভালো primary key প্রায়ই একটি surrogate key — ডেটাবেস নিজে যে নম্বর দেয়। SQLite-এ INTEGER PRIMARY KEY কলামে মান না দিলে সে বসায় “এখন পর্যন্ত সবচেয়ে বড় id + 1”. MySQL-এ একই কাজ করে AUTO_INCREMENT, PostgreSQL-এ GENERATED … AS IDENTITY বা পুরোনো SERIAL (পর্ব ১৩)। এই সব ব্যবস্থাকে একসাথে বলে auto-increment.
কিন্তু একটি ব্যবহারিক সমস্যা আছে। নতুন অর্ডার ঢোকানোর পরই তার অর্ডার-লাইনগুলো ঢোকাতে হবে, আর সেখানে নতুন order_id লাগবে। নম্বরটি তো ডেটাবেস দিয়েছে — আপনি জানবেন কীভাবে? “সবচেয়ে বড় order_id কত” জিজ্ঞেস করা ভুল উত্তর: একই মুহূর্তে আরেকজন গ্রাহক অর্ডার দিলে আপনি তাঁর নম্বর পেয়ে যেতে পারেন। সঠিক পথ দুটি: last_insert_rowid() — এই সংযোগে শেষ যে সারি ঢুকেছে তার id; আর RETURNING — INSERT নিজেই নতুন সারির কলামগুলো ফেরত দেয়, যেন একটি SELECT.
-- order_id দিলাম না: SQLite নিজেই সবচেয়ে বড় id + 1 বসাবে
INSERT INTO orders (customer_id, order_date, status)
VALUES (4, '2025-10-20', 'pending');
SELECT last_insert_rowid() AS new_id;
-- RETURNING: INSERT নিজেই নতুন সারি ফেরত দেয়
INSERT INTO orders (customer_id, order_date, status)
VALUES (5, '2025-10-21', 'pending')
RETURNING order_id, customer_id, status;প্রথম INSERT “১টি সারি বদলেছে”, তারপর SELECT দেখায় new_id = 1011 — আগের সবচেয়ে বড় 1010, তাই 1011. দ্বিতীয় INSERT কোনো “বদলেছে” বার্তা দেয়নি, দিয়েছে একটি ফল-টেবিল: 1012 | 5 | pending. RETURNING থাকলে INSERT একটি কোয়েরির মতো আচরণ করে, তাই রানার তার সারিগুলো দেখায়। RETURNING-এর সুবিধা হলো এক যাত্রায় দুই কাজ, আর DEFAULT বা trigger-এ বসানো মানও (যেমন তৈরির সময়) সাথে সাথে ফেরত আসে। SQLite-এ RETURNING এসেছে 3.35.0 (২০২১) থেকে; UPDATE আর DELETE-এর সাথেও চলে — DELETE … RETURNING দিয়ে দেখা যায় ঠিক কী মুছল।
অন্য DBMS-এ একই কাজ (চালানো যায় না, শুধু তুলনার জন্য):
INSERT INTO orders (customer_id, order_date, status)
VALUES (4, '2025-10-20', 'pending')
RETURNING order_id; -- PostgreSQL 8.2 থেকে; UPDATE/DELETE-এও চলেINSERT INTO orders (customer_id, order_date, status)
VALUES (4, '2025-10-20', 'pending');
SELECT LAST_INSERT_ID(); -- এই সংযোগের শেষ AUTO_INCREMENT মান
-- MySQL-এ RETURNING নেই (MariaDB-তে INSERT/DELETE … RETURNING আছে)
-- SQL Server: INSERT … OUTPUT inserted.order_id VALUES (…)
-- Oracle: INSERT … RETURNING order_id INTO :new_idএকাধিক সারি ঢোকালে কোন id? একটি স্টেটমেন্টে দুটি অর্ডার ঢোকালে SQLite-এর last_insert_rowid() দেয় শেষ সারির id (এখানে পরীক্ষা করলে 1012), কিন্তু MySQL-এর LAST_INSERT_ID() দেয় প্রথম সারির id. দুটির একটিও “সবগুলো id” দেয় না। সব id লাগলে RETURNING (যেখানে আছে) সবচেয়ে নির্ভরযোগ্য। আর মনে রাখুন, auto-increment-এর নম্বরে ফাঁক থাকতে পারে: MySQL আর PostgreSQL-এ বাতিল হওয়া transaction বা ব্যর্থ INSERT-এ নেওয়া নম্বর ফেরত যায় না। উল্টো দিকে SQLite-এ (AUTOINCREMENT শব্দটি না লিখলে) সবচেয়ে বড় id-র সারি মুছে গেলে সেই নম্বর আবার ব্যবহার হতে পারে। তাই order_id-কে “অর্ডারের ক্রমিক সংখ্যা” ধরে হিসাব করবেন না।
একটি পূর্ণ অর্ডার — কয়েকটি স্টেটমেন্ট, একটি কাজ
বাস্তবে “অর্ডার দেওয়া” একটি স্টেটমেন্ট নয়: orders-এ একটি সারি, order_items-এ প্রতিটি পণ্যের একটি সারি, আর products-এ স্টক কমানো। মাঝপথে বিদ্যুৎ গেলে (লোডশেডিং!) অর্ধেক অর্ডার থেকে যেতে পারে — অর্ডার আছে কিন্তু লাইন নেই, বা স্টক কমেছে কিন্তু অর্ডার নেই। তাই পুরোটি BEGIN আর COMMIT-এর মাঝে রাখা হয়: হয় সব পাকা হবে, নয়তো কিছুই না। Transaction-এর পুরো তত্ত্ব পর্ব ৪৪-এ; এখানে শুধু ব্যবহার দেখুন। অ্যাপে 1011 সংখ্যাটি আসত RETURNING থেকে; এখানে পড়ার সুবিধার জন্য সরাসরি লেখা।
-- Nusrat (4) কিনছেন: ২টি DSA Book (104), ১টি Rice 5kg (108)
BEGIN;
INSERT INTO orders (order_id, customer_id, order_date, status)
VALUES (1011, 4, '2025-10-20', 'pending');
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1011, 104, 2, 650),
(1011, 108, 1, 420);
UPDATE products SET stock = stock - 2 WHERE product_id = 104;
UPDATE products SET stock = stock - 1 WHERE product_id = 108;
COMMIT;
SELECT * FROM order_items WHERE order_id = 1011;
SELECT product_id, name, stock FROM products WHERE product_id IN (104, 108);বার্তাগুলো ক্রমে: “✓ BEGIN”, INSERT ১টি, INSERT ২টি, UPDATE ১টি, UPDATE ১টি, “✓ COMMIT”. তারপর অর্ডার-লাইন দুটি: 1011 | 104 | 2 | 650 আর 1011 | 108 | 1 | 420; স্টক এখন 104 | DSA Book | 23 (25 − 2) আর 108 | Rice 5kg | 99 (100 − 1). খেয়াল করুন, order_items-এ unit_price আলাদা করে রাখা হয়েছে — পরে পণ্যের দাম বাড়লেও এই অর্ডারের দাম বদলাবে না (পর্ব ১১-এর ডিজাইন)। এখন একটি পরীক্ষা করুন: শেষ UPDATE-এ stock - 1-কে stock - 500 করে চালান। CHECK ভাঙবে, আর বাস্তব অ্যাপ তখন ROLLBACK করে পুরো অর্ডার বাতিল করত।
৪ · UPDATE — WHERE দিয়ে ঠিক সারিগুলো বদলানো
UPDATE-এর গঠন তিন অংশের: কোন টেবিল (UPDATE products), কোন কলামে কী মান (SET stock = stock + 25), আর কোন সারিতে (WHERE product_id = 103). WHERE যে সারিগুলোর জন্য সত্য, শুধু সেগুলো বদলায়। WHERE-এ primary key দিলে একটি সারি; অন্য শর্ত দিলে যতগুলো মেলে, সবগুলো। SET-এ নতুন মান সরাসরি লেখা যায়, আবার পুরোনো মান থেকে হিসাব করেও লেখা যায় — price = price * 1.10 মানে “এই সারির বর্তমান দামের ১.১ গুণ”। নিচের ডায়াগ্রামে দেখুন DBMS ধাপে ধাপে কী করে:
| এই ধাপের কাজ | |
| WHERE মিলেছে | |
| বদলানো সারি |
- স্টেটমেন্ট আসছে।
UPDATE products SET price = ROUND(price * 1.10, 2) WHERE category_id = (SELECT category_id FROM categories WHERE name = 'Books'). টেবিলে এখানে চারটি পণ্য দেখানো হলো; Books শ্রেণির দুটি বই আছে। - আগে subquery. WHERE-এর ভেতরের subquery একবার চলে: categories-এ name = 'Books' এমন সারির category_id = 2. এখন শর্তটি দাঁড়াল
category_id = 2. (Subquery বিস্তারিত পর্ব ২১-এ।) - সারি খোঁজা। DBMS প্রতিটি সারিতে শর্তটি যাচাই করে: 101 (category 1) না, 104 (2) হ্যাঁ, 105 (2) হ্যাঁ, 108 (4) না। category_id-তে index থাকলে সব সারি না পড়ে সরাসরি মেলানো সারিতে যেত (পর্ব ৪০)।
- নতুন মান হিসাব — পুরোনো মান থেকে। প্রতিটি মেলানো সারিতে SET-এর ডান দিক সেই সারির পুরোনো মান দিয়ে হিসাব হয়: 650 × 1.10 = 715, 550 × 1.10 = 605. ROUND না দিলে 715.0000000000001 আসত — ভগ্নাংশ দশমিক বাইনারিতে হুবহু রাখা যায় না।
- নিয়ম যাচাই আর লেখা। নতুন সারিগুলো CHECK (price > 0) মানে কি না দেখা হয় — মানে। পুরোনো মান journal/log-এ রেখে তারপর নতুন মান লেখা হয়, যাতে দরকারে ফিরিয়ে আনা যায় (পর্ব ৪৮)।
- ফল: ২টি সারি বদলেছে। 101 আর 108 একটুও ছোঁয়া হয়নি। DBMS জানায় “২টি সারি”, আর এই সংখ্যাটিই আপনার যাচাই: Books-এ যতগুলো বই আশা করেছিলেন, ততগুলোই কি বদলেছে?
এবার নিজে চালান। প্রথম UPDATE একটি সারির (primary key দিয়ে), দ্বিতীয়টি একটি শ্রেণির সব সারির:
-- নতুন চালান এল: Power Bank (103)-এ ২৫টি
UPDATE products SET stock = stock + 25 WHERE product_id = 103;
-- Books শ্রেণির সব বইয়ের দাম ১০% বাড়ল
UPDATE products
SET price = ROUND(price * 1.10, 2)
WHERE category_id = (SELECT category_id FROM categories WHERE name = 'Books');
SELECT product_id, name, price, stock FROM products
WHERE product_id IN (103, 104, 105);প্রথম UPDATE “১টি সারি বদলেছে”, দ্বিতীয়টি “২টি সারি বদলেছে”. ফল: 103 | Power Bank | 1800 | 25, 104 | DSA Book | 715 | 25, 105 | SQL Book | 605 | 30. Power Bank-এর স্টক 0 থেকে 25 হয়েছে, দাম অপরিবর্তিত; বই দুটির দাম বেড়েছে, স্টক অপরিবর্তিত — SET-এ যে কলাম নেই, সেটি ছোঁয়া হয় না। stock = stock + 25 লেখার আরেকটি সুবিধা: “আগে স্টক পড়ি, অ্যাপে ২৫ যোগ করি, তারপর লিখি” করলে একই সময়ে দুজন কর্মী চালান তুললে একজনের যোগ হারিয়ে যেতে পারে (lost update, পর্ব ৪৭)। হিসাবটি UPDATE-এর ভেতরে থাকলে DBMS সারিটি lock করে একবারে পড়ে-লেখে।
SET-এর সব হিসাব পুরোনো মান দিয়ে: SET price = price * 2, stock = price লিখলে SQL standard অনুযায়ী stock পায় পুরোনো price — ডান দিকের সব মান বদলের আগের সারি থেকে আসে। SQLite আর PostgreSQL তাই করে (DSA Book-এ চালালে price 1300, stock 650). ব্যতিক্রম MySQL: এক-টেবিলের UPDATE-এ সে assignment বাঁ থেকে ডানে চালায়, ফলে stock পায় নতুন price (1300). Dialect বদলালে এমন জায়গায় ফল নীরবে বদলায় — পর্ব ৩১-এ এমন আরও পার্থক্য।
অন্য টেবিল থেকে মান এনে UPDATE
কখনো নতুন মানটি অন্য টেবিলে থাকে। ধরুন অর্ডার 1009 (Rahim Uddin-এর, pending) বাতিল হলো: তাতে ১টি Mobile Phone (101) আর ১টি Smart Watch (110) ছিল, সেগুলো স্টকে ফেরত যাবে। প্রতিটি পণ্যের কতটি ফেরত যাবে, তা লেখা order_items-এ। দুটি পথ আছে। পুরোনো, সব DBMS-এ চলে এমন পথ হলো SET-এ একটি correlated subquery: SET stock = stock + (SELECT quantity FROM order_items WHERE order_id = 1009 AND product_id = products.product_id), সাথে WHERE-এ product_id IN (SELECT product_id FROM order_items WHERE order_id = 1009) — WHERE ভুলে গেলে অন্য সব পণ্যে subquery-র ফল NULL হতো, আর stock-এ NULL বসানোর চেষ্টা NOT NULL-এ আটকে পুরো স্টেটমেন্ট ব্যর্থ হতো। নতুন, পরিষ্কার পথ হলো UPDATE … FROM: অন্য টেবিলটিকে join-এর মতো জুড়ে দেওয়া। এটি PostgreSQL-এ বহুদিনের, আর SQLite-এ এসেছে 3.33.0 (২০২০) থেকে; MySQL-এ লেখা হয় UPDATE products JOIN order_items … SET ….
-- অর্ডার 1009 (pending) বাতিল: পণ্যগুলো স্টকে ফেরত
BEGIN;
UPDATE products
SET stock = products.stock + oi.quantity
FROM order_items AS oi
WHERE oi.product_id = products.product_id
AND oi.order_id = 1009;
UPDATE orders SET status = 'cancelled' WHERE order_id = 1009;
COMMIT;
SELECT product_id, name, stock FROM products WHERE product_id IN (101, 110);
SELECT * FROM orders WHERE order_id = 1009;প্রথম UPDATE “২টি সারি বদলেছে” (দুটি পণ্য), দ্বিতীয়টি “১টি”. ফল: 101 | Mobile Phone | 13 (12 + 1), 110 | Smart Watch | 6 (5 + 1), আর 1009 | 1 | 2025-10-08 | cancelled. FROM-এর order_items-এর সাথে যে পণ্যগুলো জোড়া লেগেছে, শুধু সেগুলো বদলেছে — বাকি ৮টি পণ্য অক্ষত। একটি সতর্কতা: FROM-এর join-এ একটি লক্ষ্য-সারির সাথে একাধিক সারি মিললে UPDATE সেগুলোর যেকোনো একটি ব্যবহার করে, যোগ করে না। এখানে (order_id, product_id) primary key বলে একটি পণ্য একটি অর্ডারে একবারই আছে, তাই নিরাপদ; অনেক অর্ডারের মোট ফেরত হিসাব করতে হলে আগে GROUP BY দিয়ে যোগফল বানিয়ে তারপর জুড়তে হয়। স্টক ফেরত আর status বদল একসাথে হওয়া উচিত, তাই BEGIN … COMMIT.
আরও গভীরে: “UPDATE মানে ঘরের মান মুছে নতুন লেখা” — সব DBMS-এ ভেতরে এমন নয়। PostgreSQL-এর MVCC-তে UPDATE আসলে পুরোনো সারিটিকে “মৃত” চিহ্ন দিয়ে একটি নতুন সংস্করণ লেখে; পুরোনোটি থাকে যতক্ষণ কোনো চলমান transaction সেটি দেখতে পারে, পরে VACUUM জায়গাটি ফেরত নেয়। তাই সেখানে ঘন ঘন UPDATE হওয়া টেবিল ফুলে ওঠে (bloat). MySQL-এর InnoDB সারিটি জায়গাতেই বদলায় আর পুরোনো মান রাখে undo log-এ; SQLite পুরো page বদলায়, পুরোনো page যায় rollback journal বা WAL-এ। একই SQL, ভেতরে তিন রকম কৌশল — পর্ব ৪৭ আর ৪৮-এ বিস্তারিত।
৫ · WHERE ভুলে গেলে — সবচেয়ে বড় বিপদ আর নিরাপদ অভ্যাস
SQL-এ WHERE ঐচ্ছিক। UPDATE products SET price = 100; সম্পূর্ণ বৈধ স্টেটমেন্ট: সব পণ্যের দাম ১০০ টাকা। DELETE FROM order_items;-ও বৈধ: সব অর্ডার-লাইন উধাও। DBMS কোনো প্রশ্ন করে না, কারণ SQL সারির সেট নিয়ে কাজ করে, আর শর্তহীন সেট মানে পুরো টেবিল। ডেটাবেস নিয়ে যাঁরা কাজ করেন, তাঁদের প্রায় সবার এই ভুলের অন্তত একটি গল্প আছে — প্রায়ই একটি লম্বা স্টেটমেন্ট লিখে শেষ লাইনের WHERE-টি সিলেক্ট না করেই “চালান” চাপা।
রক্ষাকবচ হলো transaction. Autocommit অবস্থায় (ডিফল্টে) প্রতিটি স্টেটমেন্ট চলার সাথে সাথেই পাকা হয়ে যায়। কিন্তু আগে BEGIN লিখলে পরের পরিবর্তনগুলো “খসড়া” থাকে, যতক্ষণ না আপনি COMMIT (পাকা) বা ROLLBACK (সব বাতিল) বলেন। নিচে দেখুন একটি ভুল DELETE কীভাবে ফেরানো যায়:
| transaction | |
| আপনার চোখে সারি | |
| অন্য সংযোগের চোখে | |
| journal-এ পুরোনো ডেটা |
- শুরুর অবস্থা। order_items-এ ১৬টি অর্ডার-লাইন। আপনি একটি বাতিল অর্ডারের লাইন মুছতে চান, কিন্তু অভ্যাসবশত আগে transaction খুলবেন।
- BEGIN. Transaction শুরু। এখনো কিছু বদলায়নি; এখন থেকে আপনার প্রতিটি পরিবর্তন খসড়া, যতক্ষণ না COMMIT হয়।
- ভুল DELETE.
DELETE FROM order_items;— WHERE লিখতে ভুলে গেছেন! ১৬টি সারিই মুছল। কিন্তু DBMS মোছার আগে পুরোনো ডেটা journal বা log-এ রেখে দিয়েছে, আর অন্য সংযোগগুলো এখনো আগের ১৬টি সারিই দেখছে (পর্ব ৪৭-এর isolation). - যাচাই — ভুল ধরা পড়ল।
SELECT COUNT(*)বলছে 0. আপনি মুছতে চেয়েছিলেন ১টি লাইন, মুছেছে ১৬টি। “কয়টি সারি বদলেছে” বার্তাতেও 16 এসেছিল — এই সংখ্যাটি দেখা ছিল প্রথম সংকেত। - ROLLBACK. DBMS journal থেকে পুরোনো অবস্থা ফিরিয়ে আনে আর transaction শেষ করে। আবার ১৬টি সারি — যেন কিছুই ঘটেনি। অন্য কেউ কখনো মুছে যাওয়া অবস্থাটি দেখেননি।
- তুলনা: BEGIN ছাড়া। Autocommit-এ DELETE চলার সাথে সাথেই পাকা হতো। তখন ফেরানোর উপায় শুধু backup — আর backup-এর পরের সব নতুন অর্ডার হাতে মেলাতে হতো (পর্ব ৫২)। BEGIN লিখতে খরচ এক লাইন।
BEGIN;
DELETE FROM order_items; -- ভুল! WHERE ভুলে গেছি
SELECT COUNT(*) AS lines_left FROM order_items;
ROLLBACK; -- সব ফিরিয়ে নিন
SELECT COUNT(*) AS lines_left FROM order_items;DELETE-এর পর “১৬টি সারি বদলেছে”, প্রথম COUNT-এ 0, তারপর “✓ ROLLBACK”, আর দ্বিতীয় COUNT-এ আবার 16. ঠিক ডায়াগ্রামের মতো। এখন ROLLBACK-কে COMMIT করে চালিয়ে দেখুন — দ্বিতীয় COUNT-ও 0 হবে, আর বাস্তব ডেটাবেসে সেটিই হতো চূড়ান্ত।
নিরাপদ অভ্যাসের তালিকা
- আগে SELECT, পরে বদল।
DELETE FROM orders WHERE status = 'cancelled'লেখার আগে চালানSELECT * FROM orders WHERE status = 'cancelled'. যে সারিগুলো দেখলেন, ঠিক সেগুলোই বদলাবে। তারপর শুধু প্রথম শব্দটি বদলান — WHERE একই থাকবে। - সংখ্যা মিলিয়ে নিন। SELECT-এ ১টি সারি এসেছিল, UPDATE বলছে “৩৪০টি সারি বদলেছে”? তাহলে থামুন, ROLLBACK.
- হাতে চালানো প্রতিটি বদল BEGIN দিয়ে শুরু। যাচাই করে তবেই COMMIT.
- সম্ভব হলে primary key দিয়ে WHERE. নাম বা শহর দিয়ে নয় — “Rahim” নামে কতজন আছেন, কে জানে।
- Production ডেটাবেসে সরাসরি না লেখা। আগে একটি কপিতে চালান; নিয়মিত backup রাখুন আর তা থেকে ফেরানো পরীক্ষা করুন।
MySQL-এ এর জন্য একটি বিল্ট-ইন সুরক্ষাও আছে: safe-updates mode. চালু থাকলে key কলামের শর্ত বা LIMIT ছাড়া UPDATE/DELETE সে চালায়ই না। MySQL Workbench-এ এটি ডিফল্টে চালু থাকে, আর mysql কমান্ড-লাইন ক্লায়েন্টে --safe-updates দিয়ে চালু করা যায় — যার একটি মজার বিকল্প নামও আছে: --i-am-a-dummy.
SET SQL_SAFE_UPDATES = 1;
DELETE FROM order_items;
-- ERROR 1175 (HY000): You are using safe update mode and you tried to
-- update a table without a WHERE that uses a KEY column.
DELETE FROM order_items WHERE order_id = 1004; -- key কলামে শর্ত: চলবে৬ · DELETE, TRUNCATE আর DROP — তিন রকম “মোছা”
তিনটি স্টেটমেন্টই কিছু একটা মোছে, কিন্তু মাত্রা আলাদা। খাতার উপমায়: DELETE হলো কিছু লাইন কেটে দেওয়া, TRUNCATE হলো খাতার সব লেখা পাতা ছিঁড়ে ফেলে মলাট রেখে দেওয়া, আর DROP TABLE হলো পুরো খাতাটাই ফেলে দেওয়া — মলাট, ঘরের দাগ, সব।
| বিষয় | DELETE | TRUNCATE TABLE | DROP TABLE |
|---|---|---|---|
| কী মোছে | WHERE-এ মেলা সারি (WHERE না দিলে সব) | সব সারি, একবারে | সারি + টেবিলের গঠন, index, constraint, trigger |
| পরে টেবিল থাকে? | হ্যাঁ | হ্যাঁ, খালি | না |
| দল | DML | সাধারণত DDL হিসেবে গণ্য | DDL |
| গতি | সারি ধরে ধরে, প্রতিটি log-এ | খুব দ্রুত — পুরো data page ছেড়ে দেয় | খুব দ্রুত |
| ROLLBACK করা যায়? | হ্যাঁ, সবখানে | PostgreSQL, SQL Server: হ্যাঁ; MySQL, Oracle: না (নিজেই commit করে) | PostgreSQL, SQL Server, SQLite: হ্যাঁ; MySQL, Oracle: না |
| DELETE trigger চলে? | হ্যাঁ | না | না |
| auto-increment কাউন্টার | যেমন ছিল | MySQL, SQL Server-এ শুরুতে ফেরে; PostgreSQL-এ শুধু RESTART IDENTITY দিলে | টেবিলের সাথে চলে যায় |
| অন্য টেবিল foreign key দিয়ে ধরে থাকলে | প্রতিটি সারিতে FK নিয়ম খাটে | সাধারণত নিষেধ (PostgreSQL-এ CASCADE লিখলে child টেবিলও খালি) | নিষেধ, বা constraint-সহ CASCADE |
SQLite-এ TRUNCATE নামে কোনো স্টেটমেন্ট নেই। তবে WHERE ছাড়া DELETE FROM t-কে সে নিজেই দ্রুত পথে চালায় (“truncate optimization”), যদি টেবিলে trigger না থাকে আর foreign key-র কারণে সারি ধরে যাচাই না লাগে। TRUNCATE-এর আসল ব্যবহার হলো staging টেবিল — প্রতি রাতে বাইরের ফাইল থেকে ডেটা এনে রাখার অস্থায়ী টেবিল — খালি করা, যেখানে লাখ লাখ সারি এক এক করে log করার কোনো মানে নেই।
TRUNCATE TABLE staging_sales; -- সব সারি, খুব দ্রুত
TRUNCATE TABLE staging_sales RESTART IDENTITY; -- id কাউন্টারও 1 থেকে
TRUNCATE TABLE orders CASCADE; -- orders-কে FK দিয়ে ধরে রাখা টেবিলও খালি — সাবধান!
DROP TABLE IF EXISTS staging_sales; -- টেবিলটিই নেই হয়ে যায়৭ · Constraint ভাঙলে — statement atomicity আর foreign key
পর্ব ৬-এ দেখেছেন, INSERT বা UPDATE কোনো constraint ভাঙলে DBMS পুরো স্টেটমেন্ট ফিরিয়ে দেয়। এখানে “পুরো” শব্দটি গুরুত্বপূর্ণ। ১০০ সারির একটি INSERT-এর ১০০তম সারি CHECK ভাঙলে আগের ৯৯টিও ঢোকে না; ৫০টি সারি বদলানো একটি UPDATE-এর ৩০তম সারিতে সমস্যা হলে আগের ২৯টির বদলও ফিরিয়ে নেওয়া হয়। একে বলে statement atomicity — প্রতিটি স্টেটমেন্ট নিজেই একটি ছোট, অবিভাজ্য কাজ (পর্ব ৪৪-এর atomicity-র ক্ষুদ্র রূপ)। নিচে তৃতীয় সারির দাম 0, যা CHECK (price > 0) ভাঙে:
SELECT COUNT(*) AS before FROM products;
-- তিনটি সারি: প্রথম দুটি ঠিক, তৃতীয়টির দাম 0
INSERT INTO products (product_id, name, category_id, price, stock) VALUES
(111, 'Notebook (pack)', 2, 180, 50),
(112, 'Lungi', 3, 650, 20),
(113, 'Free Sample', 4, 0, 10);প্রথমে before = 10, তারপর ত্রুটি: CHECK constraint failed: price > 0. এই রানার প্রথম ত্রুটিতে থেমে যায়, তাই পরের কোনো SELECT চলত না। প্রমাণ করতে হলে লাগে এমন কিছু, যা ত্রুটি ধরে নিয়ে তারপরও কাজ চালিয়ে যায় — যেমন একটি অ্যাপ। নিচের Python প্রোগ্রাম ঠিক তাই করে: ত্রুটি ধরে (try/except) তারপর সারি গোনে। সাথে দেখায় অ্যাপের ভেতর থেকে নতুন id পাওয়ার উপায় cursor.lastrowid, যা ভেতরে last_insert_rowid()-ই ব্যবহার করে।
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE products (product_id INTEGER PRIMARY KEY,"
" name TEXT NOT NULL, price REAL NOT NULL CHECK (price > 0))")
con.execute("INSERT INTO products VALUES (101, 'Mobile Phone', 15000)")
con.commit()
rows = [(111, "Notebook (pack)", 180), (112, "Lungi", 650),
(113, "Free Sample", 0)] # last row breaks CHECK
try:
con.execute("INSERT INTO products VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)",
[v for r in rows for v in r])
except sqlite3.IntegrityError as e:
print("error:", e)
count = con.execute("SELECT COUNT(*) FROM products").fetchone()[0]
print("rows after failed INSERT:", count) # still 1
cur = con.execute("INSERT INTO products (name, price) VALUES (?, ?)", ("Lungi", 650))
print("new id from cursor.lastrowid:", cur.lastrowid)
con.commit()আউটপুট তিন লাইন: error: CHECK constraint failed: price > 0, rows after failed INSERT: 1 আর new id from cursor.lastrowid: 102. টেবিলে আগে ছিল শুধু Mobile Phone; ব্যর্থ INSERT-এর প্রথম দুটি ঠিকঠাক সারিও ঢোকেনি, তাই গণনা এখনো 1. মানগুলো ? placeholder দিয়ে আলাদা পাঠানো হয়েছে, স্ট্রিং জোড়া দিয়ে নয় — এটিই SQL injection ঠেকানোর নিয়ম (পর্ব ৪৯ ও ৫১)। শেষ INSERT-এ id না দেওয়ায় SQLite দিয়েছে 101 + 1 = 102, আর অ্যাপ তা পেয়েছে lastrowid থেকে।
Transaction-এর ভেতরে ত্রুটি হলে DBMS-গুলো আলাদা আচরণ করে। SQLite আর MySQL-এর InnoDB সাধারণত শুধু ব্যর্থ স্টেটমেন্টটি ফেরায়; transaction খোলা থাকে, আগের সফল স্টেটমেন্টগুলো খসড়ায় থাকে, আর আপনি ঠিক করেন COMMIT না ROLLBACK. PostgreSQL কঠোর: একটি ত্রুটি হলেই পুরো transaction “aborted” — এরপর প্রতিটি স্টেটমেন্টে current transaction is aborted, commands ignored until end of transaction block, ROLLBACK ছাড়া উপায় নেই (SAVEPOINT দিয়ে আংশিক ফেরানো যায়)। আর MySQL-এর পুরোনো MyISAM-এর মতো transaction-হীন engine-এ বহু-সারির INSERT মাঝপথে ব্যর্থ হলে আগের সারিগুলো থেকেই যায় — statement atomicity সেখানে নেই।
Foreign key আর DELETE — RESTRICT বনাম CASCADE ফিরে দেখা
DELETE-এর সবচেয়ে সাধারণ ত্রুটি আসে foreign key থেকে। order_items-এর product_id → products. যে পণ্য কোনো অর্ডারে আছে, সেটি মুছলে অর্ডার-লাইনগুলো “অনাথ” হবে, তাই ডিফল্ট নিয়ম (NO ACTION) মোছা আটকায়। Power Bank (103) কোনো অর্ডারে নেই, DSA Book (104) আছে দুটি অর্ডারে (1002 আর 1005):
-- Power Bank (103) কোনো অর্ডারে নেই: মুছবে
DELETE FROM products WHERE product_id = 103;
-- DSA Book (104) দুটি অর্ডারে আছে: মুছবে না
DELETE FROM products WHERE product_id = 104;প্রথম DELETE “১টি সারি বদলেছে”, দ্বিতীয়টিতে FOREIGN KEY constraint failed. এটি ভালো খবর: বিক্রি হয়ে যাওয়া পণ্যের রেকর্ড কেউ ভুল করে মুছতে পারবে না। পণ্যটি আর বিক্রি না করতে চাইলে মোছার বদলে একটি is_active বা deleted_at কলাম দিন (অংশ ৯)। পর্ব ৬-এর সারাংশ মনে করুন: ON DELETE RESTRICT/NO ACTION আটকায়; CASCADE child সারিগুলোও মুছে দেয় (order_items → orders-এ স্বাভাবিক, orders → customers-এ বিপজ্জনক); SET NULL child-এর কলাম খালি করে। এগুলো পরে যোগ বা বদল করার নিয়ম পর্ব ২৩-এ।
৮ · UPSERT — “থাকলে বদলানো, না থাকলে ঢোকানো”
গুদামে মাসিক স্টক-গণনা হলো। কর্মীরা একটি তালিকা দিলেন: কোন পণ্য কতটি এল। কিছু পণ্য আগেই products-এ আছে (স্টক বাড়াতে হবে), কিছু একদম নতুন (ঢোকাতে হবে)। সহজ চিন্তা: “আগে SELECT করে দেখি আছে কি না, তারপর INSERT বা UPDATE”. কিন্তু SELECT আর INSERT-এর মাঝের মুহূর্তে আরেকটি প্রক্রিয়া একই পণ্য ঢুকিয়ে দিলে আপনার INSERT duplicate key-তে ব্যর্থ হবে, বা দুজনেই UPDATE ভেবে একজনের যোগ হারাবে। সমাধান হলো একটি স্টেটমেন্টে দুটি কাজ — UPSERT (UPdate + inSERT). SQLite (3.24.0, ২০১৮ থেকে) আর PostgreSQL (9.5, ২০১৬ থেকে)-এ লেখা হয় INSERT … ON CONFLICT (কলাম) DO UPDATE SET … বা DO NOTHING.
| এখন দেখছি | |
| key আগে আছে? | |
| কাজ |
- স্টেটমেন্ট। দুটি সারি আসছে: (102, Headphone, 15টি) আর (111, Notebook, 50টি)। নির্দেশ: product_id-তে সংঘাত হলে বিদ্যমান স্টকের সাথে নতুন সংখ্যা যোগ করা।
excludedমানে “যে সারিটি ঢোকানোর চেষ্টা হয়েছিল”। - সারি ১: key খোঁজা। DBMS product_id-র unique index-এ 102 খোঁজে — পাওয়া গেল, Headphone আগেই আছে, স্টক 40. সাধারণ INSERT হলে এখানেই duplicate key ত্রুটি হতো।
- সংঘাত → DO UPDATE. নতুন সারি ঢোকে না; বিদ্যমান সারিতে SET চলে:
products.stock + excluded.stock= 40 + 15 = 55. নাম বা দাম ছোঁয়া হয়নি, কারণ SET-এ শুধু stock. - সারি ২: সংঘাত নেই → INSERT. 111 index-এ নেই, তাই সারিটি সাধারণ INSERT-এর মতো ঢোকে, স্টক 50.
- তুলনা: DO NOTHING আর কিছু না লিখলে।
DO NOTHINGহলে 102-এর সারিটি নীরবে বাদ পড়ত, 40-ই থাকত; 111 ঢুকত। আর ON CONFLICT একদম না লিখলে 102-এ ত্রুটি হতো, আর statement atomicity-র কারণে 111-ও ঢুকত না। - ফল। একটি স্টেটমেন্ট, “২টি সারি বদলেছে” — একটি বদল, একটি নতুন। খোঁজা আর লেখা DBMS-এর ভেতরে একসাথে, lock ধরে হয়, তাই দুটি প্রক্রিয়া একসাথে চালালেও duplicate হয় না বা কারও যোগ হারায় না।
-- স্টক-গণনার তালিকা: আছে এমন পণ্যে যোগ, নতুন পণ্য ঢোকাও
INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 15),
(111, 'Notebook (pack)', 2, 180, 50)
ON CONFLICT (product_id) DO UPDATE
SET stock = products.stock + excluded.stock;
-- শ্রেণি আগে থাকলে কিছুই কোরো না
INSERT INTO categories (name) VALUES ('Books'), ('Toys')
ON CONFLICT (name) DO NOTHING;
SELECT product_id, name, stock FROM products WHERE product_id IN (102, 111);
SELECT * FROM categories;প্রথম UPSERT “২টি সারি বদলেছে”, দ্বিতীয়টি “১টি” — Books আগেই ছিল (UNIQUE name), তাই বাদ; শুধু Toys ঢুকেছে। ফল: 102 | Headphone | 55, 111 | Notebook (pack) | 50, আর categories-এ এখন ৫টি শ্রেণি, Toys পেয়েছে 5. ON CONFLICT-এর বন্ধনীতে যে কলাম, তাতে অবশ্যই PRIMARY KEY বা UNIQUE থাকতে হবে — না থাকলে DBMS বুঝবে না “সংঘাত” কী। DO UPDATE-এর পরে একটি WHERE-ও দেওয়া যায়, যেমন “শুধু নতুন গণনা পুরোনোর চেয়ে সাম্প্রতিক হলে বদলাও” (অনুশীলন ৩)।
অন্য DBMS-এ UPSERT আর MERGE
INSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 15)
ON CONFLICT (product_id) DO UPDATE
SET stock = products.stock + EXCLUDED.stock
RETURNING product_id, stock; -- SQLite-এর মতোই, সাথে RETURNINGINSERT INTO products (product_id, name, category_id, price, stock)
VALUES (102, 'Headphone', 1, 1200, 15) AS new
ON DUPLICATE KEY UPDATE stock = products.stock + new.stock;
-- পুরোনো লেখা: stock = stock + VALUES(stock) (8.0.20 থেকে deprecated)
-- যেকোনো PRIMARY KEY বা UNIQUE সংঘাতেই চলে — কলাম বেছে দেওয়া যায় না
-- কিছু না করতে: INSERT IGNORE (তবে এটি অন্য ত্রুটিও চেপে যায়)MERGE INTO products AS p
USING stock_count AS s -- আরেকটি টেবিল বা কোয়েরি
ON p.product_id = s.product_id
WHEN MATCHED THEN
UPDATE SET stock = p.stock + s.qty
WHEN NOT MATCHED THEN
INSERT (product_id, name, category_id, price, stock)
VALUES (s.product_id, s.name, s.category_id, s.price, s.qty);MERGE এসেছে SQL:2003 standard-এ। এটি পুরো একটি “উৎস” টেবিলকে লক্ষ্য টেবিলের সাথে মিলিয়ে প্রতিটি সারির জন্য UPDATE, INSERT বা (WHEN MATCHED … THEN DELETE দিয়ে) DELETE চালায় — ডেটা warehouse-এ রাতের লোডে খুব প্রচলিত (পর্ব ৫৫)। Oracle (9i থেকে) আর SQL Server (2008 থেকে)-এ বহুদিন আছে, PostgreSQL-এ এসেছে 15 (২০২২) থেকে; SQLite আর MySQL-এ নেই। একটি সতর্কতা: MERGE-এর মিল খোঁজা সাধারণ join-এর মতো, তাই একসাথে দুটি সেশন চালালে দুজনেই “মিল নেই” দেখে INSERT করে duplicate key ত্রুটি পেতে পারে। PostgreSQL-এর ডকুমেন্টেশনও একসাথে চলা INSERT-এর ঝুঁকি থাকলে বিকল্প হিসেবে ON CONFLICT বিবেচনা করতে বলে।
SQLite/MySQL-এর REPLACE UPSERT নয়। INSERT OR REPLACE (বা MySQL-এর REPLACE INTO) সংঘাত হলে পুরোনো সারিটি মুছে নতুন সারি ঢোকায়। ফল: যে কলাম আপনি দেননি, সেগুলো পুরোনো মান হারিয়ে DEFAULT পায়; DELETE-এর পরিণতিও ঘটতে পারে — যেমন ON DELETE CASCADE থাকলে child সারিগুলো মুছে যাওয়া। স্টক বা গণনা বাড়াতে চাইলে সবসময় ON CONFLICT … DO UPDATE.
৯ · Soft delete, hard delete আর বিপুল ডেটা লোড
এ পর্যন্ত DELETE মানে সারিটি সত্যিই চলে যাওয়া — একে বলে hard delete. অনেক সিস্টেমে কিন্তু সারি প্রায় কখনো মোছা হয় না। গ্রাহক অ্যাকাউন্ট বন্ধ করলেন, পণ্য বিক্রি বন্ধ হলো, কর্মী চাকরি ছাড়লেন — তবু তাঁদের পুরোনো অর্ডার, বিল, হাজিরার রেকর্ড অর্থপূর্ণ, আর হিসাব-নিরীক্ষার জন্য প্রায়ই রাখা বাধ্যতামূলক। তখন মোছার বদলে একটি চিহ্ন দেওয়া হয়: soft delete. NID ডেটাবেসের কথা ভাবুন — একজন নাগরিক মারা গেলে তাঁর রেকর্ড মুছে ফেলা হয় না, অবস্থা বদলানো হয়; পুরোনো দলিল আর উত্তরাধিকারের জন্য রেকর্ডটি দরকার।
ALTER TABLE customers ADD COLUMN deleted_at TEXT; -- NULL = সক্রিয়
-- “মোছা” আসলে একটি UPDATE
UPDATE customers SET deleted_at = '2025-10-20' WHERE customer_id = 5;
-- অ্যাপ সবসময় সক্রিয়দের দেখে — একটি view সহজ করে (পর্ব ২৪)
CREATE VIEW active_customers AS
SELECT * FROM customers WHERE deleted_at IS NULL;
-- সক্রিয়দের মধ্যেই ফোন নম্বর অনন্য: partial unique index
CREATE UNIQUE INDEX one_active_phone ON customers(phone)
WHERE deleted_at IS NULL;| বিষয় | Hard delete (DELETE) | Soft delete (deleted_at) |
|---|---|---|
| ফেরানো যায়? | শুধু backup থেকে | এক লাইনের UPDATE-এ |
| পুরোনো রেকর্ড আর foreign key | RESTRICT আটকায় বা CASCADE মুছে দেয় | সব সম্পর্ক অক্ষত |
| কোয়েরি লেখা | সরল | প্রতিটিতে deleted_at IS NULL ভুলে যাওয়ার ঝুঁকি |
| UNIQUE | সাধারণ | মুছে যাওয়া সারিও জায়গা দখল করে — partial index লাগে |
| গোপনীয়তা আইন | ডেটা সত্যিই চলে যায় | ব্যক্তিগত তথ্য থেকে যায় — আলাদা করে মুছতে বা anonymize করতে হয় |
| আকার | টেবিল ছোট থাকে | টেবিল শুধু বাড়ে; পুরোনো সারি archive-এ সরাতে হয় |
বিপুল ডেটা একবারে লোড করা
একটি CSV ফাইলে দশ লাখ বিক্রির রেকর্ড এল। দশ লাখ আলাদা INSERT চালানো সম্ভব, কিন্তু ধীর: প্রতিটি স্টেটমেন্ট আলাদা করে parse হয়, আর autocommit-এ প্রতিটি আলাদা transaction — প্রতিবার ডিস্কে পাকা করে লেখা (fsync). প্রথম উন্নতি সহজ: সব INSERT একটি BEGIN … COMMIT-এর ভেতরে, আর multi-row VALUES. আরও দ্রুত হলো প্রতিটি DBMS-এর নিজস্ব bulk load ব্যবস্থা, যা ফাইল সরাসরি টেবিলে ঢালে। কোনোটিই এই পাতায় চালানো যায় না:
COPY sales (sale_id, product_id, qty, sold_on)
FROM '/data/sales.csv' WITH (FORMAT csv, HEADER true); -- সার্ভারের ফাইল
-- psql থেকে নিজের কম্পিউটারের ফাইল: \copy sales FROM 'sales.csv' CSV HEADERLOAD DATA LOCAL INFILE 'sales.csv' INTO TABLE sales
FIELDS TERMINATED BY ',' IGNORE 1 LINES; -- সার্ভার ও ক্লায়েন্টে local_infile চালু থাকতে হয়sqlite3 shop.db
sqlite> .mode csv
sqlite> .import --skip 1 sales.csv salesবড় লোডের আগে অভিজ্ঞরা তিনটি কাজ করেন: আগে একটি staging টেবিলে লোড করে সেখানে duplicate আর ভুল মান খোঁজেন, তারপর INSERT … SELECT বা UPSERT দিয়ে মূল টেবিলে আনেন; দরকারে বড় লোডের সময় কিছু index পরে বানান (প্রতিটি সারিতে index হালনাগাদ ব্যয়বহুল, পর্ব ৪০); আর লোডের পর সারি গুনে ফাইলের লাইনের সংখ্যার সাথে মেলান।
INSERT, UPDATE আর DELETE SQL-এর প্রায় জন্ম থেকেই আছে — ১৯৭০-এর দশকে IBM-এর System R প্রকল্পের SEQUEL ভাষায়, আর ১৯৮৬ সালের প্রথম SQL standard (SQL-86)-এ। কিন্তু “ঢোকানো বা বদলানো” কাজটির standard রূপ, MERGE, আসতে লেগেছে আরও প্রায় দুই দশক — SQL:2003-এ। তার আগেই বিভিন্ন পণ্য নিজের পথ বানিয়ে ফেলেছিল: MySQL-এর REPLACE আর ON DUPLICATE KEY UPDATE, পরে PostgreSQL 9.5 (২০১৬)-এর ON CONFLICT, যা SQLite প্রায় হুবহু গ্রহণ করে 3.24.0 (২০১৮)-এ। RETURNING-এর গল্পও একই রকম: PostgreSQL-এ ২০০৬ সাল থেকে, SQLite-এ ২০২১ সালে, আর MySQL-এ এখনো নেই। তাই একই কাজ আজও প্রতিটি DBMS-এ একটু আলাদা করে লিখতে হয়।
১০ · AI-এর যুগে ডেটা বদলানো
AI-কে “দাম ১০% বাড়িয়ে দাও” বা “পুরোনো অর্ডার মুছে দাও” বললে সে সাথে সাথে একটি UPDATE বা DELETE লিখে দেয়, আর বেশিরভাগ সময় সেটি চলে. বিপদ এখানেই: SELECT ভুল হলে ভুল রিপোর্ট আসে, কিন্তু UPDATE বা DELETE ভুল হলে ডেটাই বদলে যায়। AI প্রায়ই যা ভুল করে:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
WHERE-এ নাম বা শহর দিয়ে শর্ত (WHERE name = 'Rahim') | একই নামের সবাই বদলে যায় | আগে একই WHERE দিয়ে SELECT চালিয়ে সারি গুনুন |
নেই এমন কলাম (products.quantity, is_deleted) | ত্রুটি, বা ভুল কলামে বদল | আসল schema পাশে রেখে মিলিয়ে নিন |
NULL ভুলে যাওয়া: WHERE city <> 'Dhaka' | city NULL এমন সারি বাদ পড়ে (পর্ব ১৬) | NULL আছে এমন নমুনা সারিতে পরীক্ষা |
| SET-এ correlated subquery, কিন্তু WHERE নেই | মিল না পাওয়া সারিতে NULL বসানোর চেষ্টা | “কয়টি সারি বদলেছে” আশার সাথে মেলান |
| কয়েকটি সম্পর্কিত বদল, কিন্তু BEGIN/COMMIT নেই | মাঝপথে ব্যর্থ হলে অর্ধেক কাজ পাকা | প্রতিটি “একটি কাজ” এক transaction-এ কি না দেখুন |
| “SELECT করে দেখি, তারপর INSERT” ধরনের UPSERT | একসাথে দুই অনুরোধে duplicate বা হারানো যোগ | ON CONFLICT বা DBMS-এর নিজস্ব UPSERT চান |
| MySQL-এর সিনট্যাক্স SQLite/PostgreSQL-এ (বা উল্টো) | চলে না, বা ভিন্ন ফল | প্রম্পটে DBMS আর সংস্করণ লিখে দিন |
AI-কে বদলের কোয়েরি চাইলে আগে “পরীক্ষার” কোয়েরি আর নিরাপত্তার কাঠামোও চেয়ে নিন:
AI-এর দেওয়া UPDATE/DELETE চালানোর আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- DELETE, TRUNCATE আর DROP-এর পার্থক্য? — DELETE শর্ত মেনে সারি মোছে (DML, rollback যোগ্য); TRUNCATE সব সারি দ্রুত মোছে, টেবিল রাখে; DROP টেবিলটিই মুছে দেয়।
- WHERE ছাড়া UPDATE কী করে? — টেবিলের প্রতিটি সারি বদলায়; তাই আগে SELECT আর transaction.
- UPSERT কী, আর SELECT-তারপর-INSERT কেন যথেষ্ট নয়? — থাকলে বদলানো, না থাকলে ঢোকানো; দুই ধাপের পথে মাঝখানে অন্য সেশন ঢুকে race condition তৈরি করে।
- একটি বহু-সারির INSERT-এর একটি সারি constraint ভাঙলে? — Statement atomicity: কোনো সারিই ঢোকে না (transaction-সমর্থিত engine-এ)।
- নতুন সারির auto-increment id কীভাবে পাবেন? — RETURNING (PostgreSQL, SQLite), LAST_INSERT_ID() (MySQL), last_insert_rowid() (SQLite) — MAX(id) নয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১WHERE ছাড়া UPDATE/DELETE এত বিপজ্জনক — তাহলে SQL কেন এটিকে নিষেধ করেনি? আর ভুল হয়ে গেলে বাস্তবে কী কী রক্ষাকবচ কাজে আসে?
SQL-এর মূল ধারণা সেট-ভিত্তিক: একটি স্টেটমেন্ট একটি শর্ত দিয়ে সারির একটি সেট বেছে নেয়, তারপর সেই সেটের ওপর কাজ করে। শর্ত না থাকা মানে সবচেয়ে বড় সেট — পুরো টেবিল। এটি ভাষার যুক্তিতে সামঞ্জস্যপূর্ণ, আর এর বৈধ ব্যবহারও আছে: একটি অস্থায়ী টেবিল খালি করা, নতুন কলামে সব সারিতে একটি প্রাথমিক মান বসানো। তাই ভাষা নিজে নিষেধ করেনি; সুরক্ষার দায় দেওয়া হয়েছে টুল আর অভ্যাসের ওপর।
টুলের দিকে সবচেয়ে পরিচিত উদাহরণ MySQL-এর safe-updates mode: key কলামের শর্ত বা LIMIT ছাড়া UPDATE/DELETE সে চালায় না, MySQL Workbench-এ এটি ডিফল্টে চালু, আর কমান্ড-লাইনের বিকল্পটির নামই --i-am-a-dummy — নির্মাতারা মেনে নিয়েছেন যে অভিজ্ঞ মানুষও এই ভুল করেন। অনেক ডেটাবেস GUI টুলও WHERE ছাড়া বদলের আগে নিশ্চিতকরণ চায়, আর PostgreSQL-এর জন্য এমন সুরক্ষার extension আছে।
ভুল হয়ে গেলে রক্ষাকবচ স্তরে স্তরে। প্রথম স্তর transaction: BEGIN দিয়ে শুরু করলে ROLLBACK এক শব্দের কাজ। দ্বিতীয় স্তর backup আর point-in-time recovery: PostgreSQL-এর WAL বা MySQL-এর binary log সংরক্ষণ থাকলে ভুলের ঠিক আগের মুহূর্ত পর্যন্ত ডেটাবেস ফেরানো যায় (পর্ব ৫২)। তৃতীয় স্তর delayed replica — কিছু প্রতিষ্ঠান ইচ্ছে করে এক ঘণ্টা পিছিয়ে চলা একটি কপি রাখে, যাতে ভুল ধরা পড়ার আগে সেটিও নষ্ট না হয়।
কিন্তু backup থাকলেই হয় না, তা থেকে ফেরানো পরীক্ষা করতে হয়। ২০১৭ সালের জানুয়ারিতে GitLab.com-এ একজন প্রকৌশলী ভুল সার্ভারে production ডেটাবেসের ডেটা-ফোল্ডার মুছে ফেলেন (এটি SQL DELETE নয়, ফাইল মোছা)। পরে দেখা গেল পাঁচ রকম backup ও replication ব্যবস্থা থাকলেও কোনোটিই ভরসাযোগ্যভাবে কাজ করছিল না; শেষে কয়েক ঘণ্টা আগের একটি কপি থেকে ফেরাতে হয়, আর সেই কয়েক ঘণ্টার ডেটা হারায়। প্রতিষ্ঠানটি পুরো ঘটনা প্রকাশ্যে লিখেছিল, আর সেটি আজও “backup পরীক্ষা করুন” শিক্ষার পরিচিত উদাহরণ।
মূল উপলব্ধি: ভাষা আপনাকে পুরো টেবিল বদলাতে দেবে — সুরক্ষা আসে অভ্যাস (আগে SELECT, তারপর BEGIN), টুল (safe-updates) আর পরীক্ষিত backup থেকে।
প্র ০২Soft delete শুনতে নিরাপদ — কিছুই হারায় না। তাহলে অনেক অভিজ্ঞ ডিজাইনার একে “ঝামেলার উৎস” বলেন কেন? কখন hard delete-ই ঠিক?
Soft delete-এর সুবিধা স্পষ্ট: ভুল “মোছা” এক লাইনের UPDATE-এ ফেরানো যায়, পুরোনো অর্ডার আর বিলের foreign key অক্ষত থাকে, আর অডিটে দেখানো যায় কে কবে নিষ্ক্রিয় হলেন। কিন্তু খরচগুলো ধীরে ধীরে চোখে পড়ে।
- প্রতিটি কোয়েরিতে একটি শর্ত মনে রাখা। রিপোর্ট, সার্চ, JOIN — সবখানে
deleted_at IS NULL. একটি জায়গায় ভুলে গেলে নিষ্ক্রিয় গ্রাহক আবার “সক্রিয় গ্রাহক” গণনায় ঢুকে পড়েন। View বা ORM-এর ডিফল্ট ফিল্টার এটি কমায়, পুরো দূর করে না। - UNIQUE ভেঙে যায়। একজন গ্রাহক অ্যাকাউন্ট বন্ধ করে একই ফোন নম্বরে আবার খুলতে চাইলেন — পুরোনো সারিটি এখনো নম্বরটি দখল করে আছে। সমাধান partial unique index (PostgreSQL আর SQLite-এ আছে) বা কৃত্রিম কৌশল।
- Foreign key-র অর্থ দুর্বল হয়। DBMS জানে না “মুছে যাওয়া” গ্রাহকের নামে নতুন অর্ডার নেওয়া উচিত নয় — সেই নিয়ম আলাদা করে লিখতে হয় (trigger বা অ্যাপে)।
- গোপনীয়তা। ইউরোপের GDPR (২০১৮ থেকে কার্যকর) “মুছে ফেলার অধিকার” দেয়; এমন আইনে শুধু একটি চিহ্ন দিয়ে ব্যক্তিগত তথ্য রেখে দেওয়া যথেষ্ট নাও হতে পারে। প্রচলিত সমাধান হলো সংখ্যাগত রেকর্ড রেখে নাম, ফোন, ঠিকানা মুছে বা বদলে দেওয়া (anonymize).
- আকার আর গতি। টেবিল শুধু বাড়ে; সক্রিয় সারি অল্প হলেও index-এ সব থাকে।
তাই ব্যবহারিক নিয়মটি এমন: যে ডেটার ইতিহাস ব্যবসা বা আইনের কাছে মূল্যবান (গ্রাহক, পণ্য, লেনদেন, কর্মী), সেখানে soft delete বা status কলাম; যে ডেটা নিছক অস্থায়ী (লগইন সেশন, কার্টের খসড়া, মেয়াদোত্তীর্ণ OTP), সেখানে hard delete, প্রায়ই নির্ধারিত সময় পর পর একটি পরিষ্কারের job দিয়ে। অনেক দল তৃতীয় পথও নেয়: মোছার মুহূর্তে সারিটি একটি archive বা history টেবিলে কপি করে (trigger দিয়ে, পর্ব ২৯) তারপর hard delete — মূল টেবিল পরিষ্কার, ইতিহাসও থাকে।
মূল উপলব্ধি: Soft delete “না মোছা” নয়, “মোছার অর্থ নিজে বাস্তবায়ন করা” — তাই প্রতিটি কোয়েরি, UNIQUE আর গোপনীয়তার দায় এখন আপনার।
প্র ০৩“আগে SELECT করে দেখি আছে কি না, তারপর INSERT বা UPDATE” — একজন ব্যবহারকারীর জন্য এটি ঠিক কাজ করে। একসাথে অনেক ব্যবহারকারী এলে কেন ভাঙে, আর ON CONFLICT কীভাবে সমস্যাটি সমাধান করে?
ধরুন ট্রেনের টিকিট বুকিংয়ে একটি টেবিল seat_holds(seat_no PRIMARY KEY, passenger). দুজন যাত্রী একই মুহূর্তে আসন 23 চাইলেন। দুটি সার্ভার-প্রক্রিয়া প্রায় একসাথে চালাল: “SELECT … WHERE seat_no = 23”. দুজনেই দেখল সারি নেই। দুজনেই INSERT চালাল। Primary key থাকায় একটি INSERT ব্যর্থ হবে — ভালো, ডেটা রক্ষা পেল; কিন্তু অ্যাপটি এই ত্রুটির জন্য প্রস্তুত না থাকলে যাত্রী দেখবেন “সার্ভার ত্রুটি”. আর যদি টেবিলে primary key না থাকত, দুজনের নামেই একই আসন বুক হয়ে যেত। একে বলে check-then-act race condition: যাচাই আর কাজের মাঝের ফাঁকে পৃথিবী বদলে যায়।
UPDATE-এর দিকেও একই সমস্যা। দুই প্রক্রিয়া স্টক পড়ল 40, দুজনেই নিজের হিসাবে 15 যোগ করে লিখল 55 — আসলে হওয়ার কথা ছিল 70. এটিই lost update (পর্ব ৪৭)।
সমাধানের তিনটি পথ আছে। এক, সবচেয়ে কঠোর isolation (SERIALIZABLE) — DBMS এমন সংঘাত ধরে একটি transaction বাতিল করে, অ্যাপকে আবার চেষ্টা করতে হয়। দুই, SELECT … FOR UPDATE দিয়ে সারি lock করা — কিন্তু যে সারি এখনো নেই, তাকে lock করা কঠিন। তিন, যেটি সবচেয়ে সরল: সিদ্ধান্তটিই DBMS-কে দিয়ে দেওয়া। INSERT … ON CONFLICT DO UPDATE-এ DBMS unique index-এ খোঁজে আর সেখানেই lock ধরে ঢোকায় বা বদলায়; এই দুটি ধাপের মাঝে অন্য কেউ ঢুকতে পারে না। PostgreSQL-এর ডকুমেন্টেশন একে বলে “atomic INSERT or UPDATE outcome” — একসাথে অনেক সেশন চালালেও প্রতিটি সারির জন্য হয় insert, নয়তো update নিশ্চিত।
PostgreSQL-এ এই সুবিধাটি আসতে বহু বছর লেগেছিল; সমস্যাটি ঠিকঠাক সমাধান করা এতটাই সূক্ষ্ম যে ডেভেলপার সম্প্রদায়ে দীর্ঘ আলোচনার পর 9.5 (২০১৬)-এ ON CONFLICT আসে। আর এজন্যই MERGE-কে UPSERT-এর বিকল্প ভাবলে সাবধান থাকতে হয়: MERGE-এর মিল খোঁজা সাধারণ join, তাই একসাথে চালালে duplicate key ত্রুটি হতে পারে; SQL Server-এ অনেকে এজন্য MERGE-এর সাথে HOLDLOCK ধরনের lock hint দেন।
মূল উপলব্ধি: দুই ধাপের “দেখি, তারপর করি” একসাথে চলা অনুরোধে ভাঙে; সমাধান হলো যাচাই আর কাজকে একটি atomic স্টেটমেন্টে বাঁধা — আর তার ভিত্তি একটি UNIQUE constraint.
প্র ০৪TRUNCATE এত দ্রুত কেন, যেখানে DELETE ধীর? আর MySQL বা Oracle-এ TRUNCATE ROLLBACK করা যায় না, কিন্তু PostgreSQL-এ যায় — এই পার্থক্যের কারণ কী?
DELETE সারি ধরে ধরে কাজ করে। প্রতিটি সারির জন্য DBMS তাকে খুঁজে বের করে, foreign key যাচাই করে, DELETE trigger থাকলে চালায়, প্রতিটি index থেকে সেই সারির entry সরায়, আর ফেরানোর জন্য log-এ লেখে। দশ লাখ সারি মানে দশ লাখবার এই কাজ, আর log-এর আকারও বিশাল। কিছু DBMS-এ মোছার পরও data page-গুলো টেবিলের দখলে থাকে; PostgreSQL-এ মুছে যাওয়া সারির জায়গা VACUUM না হওয়া পর্যন্ত ফেরে না।
TRUNCATE পুরো অন্য কৌশল নেয়। সে সারি দেখেই না: টেবিলের ডেটা রাখার ফাইল বা page-গুলো একবারে ছেড়ে দিয়ে একটি নতুন খালি সংরক্ষণস্থল দেয়। log-এ শুধু লেখা থাকে “এই টেবিলের সংরক্ষণস্থল বদলানো হলো”, প্রতিটি সারি নয়। এজন্য সারির সংখ্যা যতই হোক, সময় প্রায় একই। দাম হলো সূক্ষ্ম নিয়ন্ত্রণ হারানো — WHERE নেই, DELETE trigger চলে না, আর অন্য টেবিল foreign key দিয়ে ধরে থাকলে সারি ধরে যাচাই সম্ভব নয় বলে সাধারণত TRUNCATE-ই নিষেধ।
ROLLBACK-এর পার্থক্য আসে DDL কীভাবে বাস্তবায়িত, তা থেকে। Oracle আর MySQL-এ DDL স্টেটমেন্ট (CREATE, ALTER, DROP, TRUNCATE) চলার আগে ও পরে নিজে থেকেই commit করে — একে বলে implicit commit. তাই সেখানে transaction-এর ভেতরে TRUNCATE লিখলে আগের খসড়া কাজগুলোও পাকা হয়ে যায়, আর TRUNCATE ফেরানো যায় না। PostgreSQL-এর catalog নিজেই সাধারণ টেবিলে MVCC মেনে রাখা, তাই DDL-ও সাধারণ transaction-এর অংশ: পুরোনো ফাইলটি commit পর্যন্ত মুছে ফেলা হয় না, ROLLBACK করলে পুরোনোটিই ফিরে আসে। SQL Server-এও TRUNCATE transaction-এর ভেতরে ফেরানো যায়, কারণ সেখানে page ছেড়ে দেওয়ার ঘটনাটি log করা হয়। SQLite-ও transaction-এর ভেতরে DROP TABLE ফেরাতে পারে।
ব্যবহারিক ফল: migration স্ক্রিপ্ট লেখার সময় MySQL-এ “সব এক transaction-এ, ব্যর্থ হলে ফেরাব” পরিকল্পনা DDL থাকলে কাজ করে না; PostgreSQL-এ করে। আর staging টেবিল খালি করার মতো কাজে TRUNCATE আদর্শ, কিন্তু আসল ব্যবসার টেবিলে কখনো নয়।
মূল উপলব্ধি: DELETE সারি নিয়ে কাজ করে, TRUNCATE সংরক্ষণস্থল নিয়ে — একটি সূক্ষ্ম আর ফেরানো যায়, অন্যটি দ্রুত কিন্তু ভোঁতা; আর ফেরানো যাবে কি না, তা নির্ভর করে DBMS DDL-কে transaction-এর অংশ ভাবে কি না।
প্র ০৫একটি বহু-সারির INSERT-এর একটি সারি ব্যর্থ হলে পুরোটা বাতিল — কিন্তু ১০ লাখ সারির আমদানিতে একটি ভুল সারির জন্য সব ফেলে দেওয়া কি বাস্তবসম্মত? ভালো সারিগুলো রেখে খারাপগুলো আলাদা করবেন কীভাবে?
Statement atomicity-র কারণটি আগে মনে রাখুন: একটি স্টেটমেন্ট অর্ধেক হলে আপনি জানবেন না কোন সারি ঢুকেছে আর কোনটি নয়, আর আবার চালালে duplicate হবে। “সব বা কিছুই না” তাই সবচেয়ে নিরাপদ ডিফল্ট। কিন্তু আমদানির ক্ষেত্রে আসলেই একটি খারাপ সারির জন্য সব ফেলে দেওয়া অপচয়। পেশাদার পথগুলো:
- Staging টেবিল: আগে সব সারি একটি constraint-হীন (বা শিথিল) staging টেবিলে লোড করুন। তারপর SQL দিয়েই খারাপ সারি খুঁজুন — দাম শূন্য বা ঋণাত্মক, অস্তিত্বহীন category, duplicate id — সেগুলো একটি rejects টেবিলে সরান, আর শুধু ভালোগুলো INSERT … SELECT দিয়ে মূল টেবিলে আনুন। ব্যর্থতার কারণসহ rejects রিপোর্ট ব্যবসার মানুষের কাছে যায়।
- ছোট ব্যাচ: দশ লাখ সারিকে, ধরুন, এক হাজারের ব্যাচে ভাগ করুন; প্রতিটি ব্যাচ আলাদা transaction. একটি ব্যাচ ব্যর্থ হলে শুধু সেটির সারিগুলো আলাদা করে পরীক্ষা করুন।
- DBMS-এর নিজস্ব সুবিধা: SQLite-এ
INSERT OR IGNOREUNIQUE, NOT NULL, CHECK আর primary key সংঘাতের সারি নীরবে বাদ দেয় (foreign key-র নয়);OR FAILত্রুটির আগের সারিগুলো রেখে থামে। PostgreSQL-এON CONFLICT DO NOTHINGশুধু unique সংঘাত বাদ দেয়; সাম্প্রতিক সংস্করণে (17) COPY-তে ফরম্যাট-ভুল সারি এড়িয়ে যাওয়ার বিকল্প এসেছে। MySQL-এরINSERT IGNOREঅনেক ত্রুটিকে সতর্কবার্তায় নামিয়ে দেয় — এমনকি ভুল মানকে কাছাকাছি একটি মানে বদলে ঢুকিয়ে দিতে পারে, তাই সাবধান।
“নীরবে বাদ দাও” বিকল্পগুলোর বিপদ হলো, আপনি জানবেন না কী বাদ পড়ল। তাই নিয়ম: বাদ দিলে গুনুন — ফাইলের সারির সংখ্যা, ঢোকা সারির সংখ্যা, আর বাদ পড়ার সংখ্যা যোগ করলে যেন মেলে। ব্যাংকের দিনের শেষের হিসাব মেলানোর মতোই।
আরেকটি দিক হলো idempotency: আমদানি মাঝপথে বন্ধ হলে আবার চালালে যেন duplicate না হয়। প্রতিটি সারির একটি স্থির natural key (যেমন বাইরের সিস্টেমের লেনদেন নম্বর) রেখে UPSERT দিয়ে লোড করলে একই ফাইল দুবার চালালেও ফল একই থাকে।
মূল উপলব্ধি: Atomicity ডিফল্ট হিসেবে ঠিক; ভালো-খারাপ আলাদা করার কাজটি ইচ্ছাকৃতভাবে, staging আর গণনা দিয়ে করুন — “নীরবে বাদ দাও”-কে কখনো যাচাই ছাড়া বিশ্বাস করবেন না।
অনুশীলন
উপরের যেকোনো db-shop এডিটরে কোয়েরিগুলো চালিয়ে উত্তর মিলিয়ে নিতে পারেন।
-
কোয়েরি লিখুন: Electronics শ্রেণির যেসব পণ্যের stock ১০-এর কম, সেগুলোর দাম ৫% বাড়াতে হবে, দুই দশমিকে রাউন্ড করে। নিরাপদ অভ্যাস মেনে আগে যাচাইয়ের SELECT, তারপর UPDATE লিখুন। কোন পণ্যগুলো বদলাবে, নতুন দাম কত?
SELECT product_id, name, price, stock FROM products WHERE category_id = (SELECT category_id FROM categories WHERE name = 'Electronics') AND stock < 10; BEGIN; UPDATE products SET price = ROUND(price * 1.05, 2) WHERE category_id = (SELECT category_id FROM categories WHERE name = 'Electronics') AND stock < 10; COMMIT;SELECT দুটি সারি দেখায়:
103 | Power Bank | 1800 | 0আর110 | Smart Watch | 3500 | 5. Mobile Phone (stock 12) আর Headphone (40) বাদ। UPDATE বলবে “২টি সারি বদলেছে” — SELECT-এর সাথে মিলেছে, তাই COMMIT. নতুন দাম 1890 আর 3675. WHERE হুবহু কপি করা হয়েছে, শুধু প্রথম অংশ বদলেছে। -
ফল আগে বলুন (trace): টেবিল
t(id INTEGER PRIMARY KEY, v INTEGER NOT NULL CHECK (v >= 0))-এ তিনটি সারি: (1, 5), (2, 3), (3, 0). ক্রমে চালানো হলো:BEGIN;UPDATE t SET v = v - 1 WHERE id IN (1, 2);DELETE FROM t WHERE id = 3;ROLLBACK;UPDATE t SET v = v + 10 WHERE id = 3;UPDATE t SET v = v - 4;শেষ স্টেটমেন্টটি কী জানাবে, আর শেষে টেবিলে কী থাকবে?ROLLBACK প্রথম UPDATE আর DELETE দুটিই ফিরিয়ে নেয় — টেবিল আবার (1, 5), (2, 3), (3, 0). পরের UPDATE autocommit-এ চলে: id 3 হয় 10. শেষ UPDATE-এ WHERE নেই, তাই তিনটি সারিই 4 কমবে: 5 → 1, 3 → −1, 10 → 6. দ্বিতীয় সারি CHECK (v >= 0) ভাঙে, তাই ত্রুটি
CHECK constraint failed: v >= 0, আর statement atomicity-র কারণে প্রথম আর তৃতীয় সারির বদলও হয় না। শেষে: (1, 5), (2, 3), (3, 10). -
UPSERT-এর ফল বলুন: টেবিল
stock_count(product_id PRIMARY KEY, qty, counted_on)-এ আছে (1, 10, '2025-10-01') আর (2, 5, '2025-10-01'). চালানো হলো:INSERT INTO stock_count VALUES (1, 7, '2025-10-20'), (3, 4, '2025-10-20'), (2, 9, '2025-09-30') ON CONFLICT (product_id) DO UPDATE SET qty = excluded.qty, counted_on = excluded.counted_on WHERE excluded.counted_on > stock_count.counted_on;কয়টি সারি বদলেছে, আর শেষে টেবিলে কী?সারি 1: সংঘাত; নতুন তারিখ 2025-10-20 পুরোনো 2025-10-01-এর পরে, তাই বদল — (1, 7, '2025-10-20'). সারি 3: সংঘাত নেই, নতুন INSERT. সারি 2: সংঘাত, কিন্তু নতুন তারিখ 2025-09-30 পুরোনোর আগে — DO UPDATE-এর WHERE মিথ্যা, তাই কিছুই হয় না, পুরোনো গণনা থাকে। মোট ২টি সারি বদলেছে; শেষে (1, 7, '2025-10-20'), (2, 5, '2025-10-01'), (3, 4, '2025-10-20'). দেরিতে আসা পুরোনো গণনা নতুনকে মুছে দিতে পারল না — বাস্তব সিস্টেমে এই WHERE খুব দরকারি।
-
ডিজাইন করুন: দোকানটি ঠিক করল গ্রাহক অ্যাকাউন্ট বন্ধ করলে hard delete হবে না। customers টেবিলে কী বদলাবেন, “অ্যাকাউন্ট বন্ধ” কোন স্টেটমেন্ট হবে, phone-এর UNIQUE-এর কী হবে, আর orders → customers foreign key-তে কোন ON DELETE নিয়ম রাখবেন?
- কলাম:
deleted_at TEXT(NULL = সক্রিয়); চাইলেdeleted_reason. - অ্যাকাউন্ট বন্ধ:
UPDATE customers SET deleted_at = '2025-10-20' WHERE customer_id = ?;— primary key দিয়ে, BEGIN … COMMIT-এ। - UNIQUE: সাধারণ
phone UNIQUE-এর বদলে partial indexCREATE UNIQUE INDEX … ON customers(phone) WHERE deleted_at IS NULL, যাতে বন্ধ অ্যাকাউন্টের নম্বরে নতুন অ্যাকাউন্ট খোলা যায়। - Foreign key: orders → customers-এ RESTRICT/NO ACTION রাখুন — hard delete আর হওয়ার কথা নয়, কেউ ভুল করে চেষ্টা করলে DBMS আটকাবে। CASCADE কখনো নয়।
- বাকি কাজ:
active_customersview; নতুন অর্ডার নেওয়ার আগে গ্রাহক সক্রিয় কি না যাচাই; গ্রাহক ব্যক্তিগত তথ্য মুছতে বললে নাম-ফোন anonymize.
- কলাম:
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমProduction ডেটাবেস থেকে কিছু সারি নিরাপদে মোছার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- যে WHERE দিয়ে মুছবেন, সেটি দিয়ে SELECT চালিয়ে সারিগুলো দেখা ও গোনা
- BEGIN দিয়ে transaction শুরু করা
- হুবহু একই WHERE দিয়ে DELETE চালানো
- “কয়টি সারি বদলেছে” SELECT-এর গণনার সাথে মেলানো
- মিললে COMMIT, না মিললে ROLLBACK
products টেবিলে ১০টি সারি। UPDATE products SET stock = 0; চালালে কী হবে?
- ত্রুটি আসবে, কারণ UPDATE-এ WHERE বাধ্যতামূলক
- ১০টি সারির সবগুলোর stock 0 হবে
- শুধু প্রথম সারির stock 0 হবে
- stock-এ NULL বসবে
একটি টেবিলে CHECK (price > 0) আছে। একটি INSERT স্টেটমেন্ট VALUES-এ ৫টি সারি দেয়, যার চতুর্থটির price 0. Transaction-সমর্থিত DBMS-এ (যেমন PostgreSQL বা SQLite) কয়টি সারি ঢুকবে?
- ৩টি — ত্রুটির আগের সারিগুলো
- ৪টি — শুধু খারাপ সারিটি বাদ
- ০টি — পুরো স্টেটমেন্ট বাতিল
- ৫টি — CHECK শুধু সতর্কবার্তা দেয়
MySQL-এ TRUNCATE TABLE আর WHERE ছাড়া DELETE-এর পার্থক্য সম্পর্কে কোনটি সঠিক?
- TRUNCATE ROLLBACK করা যায় না, DELETE trigger চালায় না, আর AUTO_INCREMENT শুরুতে ফেরায়
- TRUNCATE সারি ধরে ধরে মোছে বলে DELETE-এর চেয়ে ধীর
- TRUNCATE-এ WHERE দিয়ে কিছু সারি বেছে মোছা যায়
- TRUNCATE টেবিলের গঠনও মুছে দেয়, DELETE দেয় না
SQLite বা PostgreSQL-এ INSERT … ON CONFLICT (product_id) DO UPDATE SET stock = products.stock + excluded.stock-এ excluded কী বোঝায়?
- টেবিলে আগে থেকে থাকা সারি
- মুছে ফেলা সারি রাখার একটি আলাদা টেবিল
- যে সারিগুলো DO NOTHING দিয়ে বাদ পড়েছে
- যে নতুন সারিটি ঢোকানোর চেষ্টা হয়েছিল কিন্তু সংঘাতে আটকেছে
একটি অ্যাপ orders টেবিলে নতুন অর্ডার ঢুকিয়ে তার স্বয়ংক্রিয় order_id জানতে চায়। একসাথে অনেক ব্যবহারকারী অর্ডার দিচ্ছেন। কোন পথটি ভুল?
- PostgreSQL বা SQLite-এ
INSERT … RETURNING order_id - INSERT-এর পর
SELECT MAX(order_id) FROM orders - MySQL-এ INSERT-এর পর
SELECT LAST_INSERT_ID() - Python sqlite3-এ
cursor.lastrowid
order_items.product_id হলো REFERENCES products(product_id), কোনো ON DELETE নিয়ম লেখা নেই। যে পণ্য দুটি অর্ডারে আছে, সেটি DELETE FROM products WHERE product_id = 104 দিয়ে মুছলে কী হবে?
- পণ্য আর তার দুটি অর্ডার-লাইন মুছে যাবে
- পণ্য মুছবে, অর্ডার-লাইনের product_id NULL হবে
- foreign key ত্রুটি দিয়ে DELETE বাতিল হবে
- পণ্য মুছবে, অর্ডার-লাইনগুলো অনাথ হয়ে থেকে যাবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- INSERT-এ সবসময় কলামের নাম; অনেক সারি একসাথে multi-row VALUES বা
INSERT … SELECTদিয়ে; বাদ দেওয়া কলাম পায় DEFAULT বা NULL. - নতুন id নিন RETURNING / last_insert_rowid() / LAST_INSERT_ID() দিয়ে — কখনো MAX(id) দিয়ে নয়।
- WHERE ছাড়া UPDATE/DELETE = পুরো টেবিল. আগে একই WHERE-এ SELECT, তারপর BEGIN, গণনা মিলিয়ে COMMIT বা ROLLBACK.
- একটি স্টেটমেন্ট পুরো হয় বা একটুও না; foreign key-তে আটকানো DELETE ভালো খবর। DELETE সারি মোছে, TRUNCATE সব সারি দ্রুত, DROP পুরো টেবিল।
- UPSERT =
ON CONFLICT DO UPDATE/DO NOTHING(MySQL-এ ON DUPLICATE KEY UPDATE, standard-এ MERGE); ইতিহাস রাখতে soft delete, অস্থায়ী ডেটায় hard delete.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৫ · SELECT — বাছাই, ফিল্টার আর সাজানোপরবর্তী পর্বডেটা পড়ার মূল স্টেটমেন্ট, ধাপে ধাপে।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- Python · SQLite ডেটাবেসহাতে-কলমেনিজের Python অ্যাপ থেকে INSERT, UPDATE আর transaction — placeholder দিয়ে।
- Python · SQLAlchemy ORMপাশের ট্র্যাকORM ভেতরে কোন INSERT/UPDATE চালায়, আর session কীভাবে commit করে।
- DSA · B-treeগভীরে যানUPDATE/DELETE যে index দিয়ে সারি খোঁজে, আর UPSERT যে index-এ সংঘাত ধরে।