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

ALTER TABLE, constraint আর referential action

ALTER TABLE, Constraints and Referential Actions — Evolving a Live Schema Safely

Taught in Bangla, this lesson shows how to evolve a live schema with ALTER TABLE: adding columns with DEFAULT and NOT NULL, renaming and dropping columns, SQLite's limited ALTER and its official 12-step table rebuild, finding rule-breaking rows before adding constraints, deferrable constraints for circular foreign keys, and choosing CASCADE, SET NULL, SET DEFAULT, RESTRICT or NO ACTION, plus safe production migrations, all with live SQL editors.

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

  • ADD COLUMN, RENAME COLUMN, RENAME TABLE আর DROP COLUMN — কী করে, কোথায় আটকায়
  • PostgreSQL/MySQL-এর পূর্ণ ALTER বনাম SQLite-এর সীমিত ALTER, আর SQLite-এর ১২ ধাপের rebuild হাতে-কলমে
  • নিয়ম ভাঙা পুরোনো ডেটায় নতুন constraint বসানো, constraint-এর নাম দেওয়া আর মোছা
  • Deferrable constraint দিয়ে বৃত্তাকার (circular) foreign key-তে ডেটা ঢোকানো
  • CASCADE, SET NULL, SET DEFAULT, RESTRICT আর NO ACTION — পার্থক্য, শিকল-বিপদ আর বাছাইয়ের নিয়ম; production-এ নিরাপদ schema বদল

১ · Schema কেন বদলায় — নতুন প্রয়োজন, পুরোনো টেবিল

পর্ব ১৩-এ আপনি CREATE TABLE দিয়ে টেবিল বানাতে শিখেছেন, আর পর্ব ৬-এ দেখেছেন key আর constraint কী। কিন্তু বাস্তবে টেবিল একবার বানিয়ে চিরকাল একই থাকে না। দোকান চালু হওয়ার ছয় মাস পর মালিক বললেন: “অর্ডারের রসিদ এখন থেকে গ্রাহকের email-এ যাবে।” customers টেবিলে email কলাম নেই। আবার নতুন টেবিল বানিয়ে আটজন গ্রাহককে নতুন করে ঢোকাবেন? না — এমন কাজের জন্যই আছে ALTER TABLE: চালু টেবিলের নকশা (schema) বদলানোর DDL স্টেটমেন্ট, যাতে ভেতরের ডেটা অক্ষত থাকে।

মুদি দোকানের বাকির খাতা ভাবুন। এতদিন কলাম ছিল তিনটি: নাম, ঠিকানা, টাকা। এখন দোকানদার bKash নম্বরও লিখে রাখতে চান। তিনি নতুন খাতা কেনেন না — পুরোনো খাতার পাতায় ডান পাশে একটি নতুন কলাম টানেন। পুরোনো লাইনগুলোতে ঘরটি আপাতত ফাঁকা থাকে; গ্রাহক এলে একে একে লিখে নেন। কলাম টানা সহজ, কিন্তু “প্রতিটি লাইনে নম্বর থাকতেই হবে” — এই নিয়ম আজই চালু করলে পুরোনো ফাঁকা ঘরগুলো নিয়ম ভাঙবে। ALTER TABLE-এর সব জটিলতা এই একটি ছবিতেই আছে।

Schema বদলের কারণ নানা রকম: নতুন ফিচার (email, ডেলিভারি চার্জ), নতুন আইন বা নীতি (ভ্যাটের হার আলাদা কলামে), ভুল ডিজাইন শোধরানো (ফোন নম্বর INTEGER থেকে TEXT), পারফরম্যান্স (index বা derived কলাম), আর নতুন ব্যবসার নিয়ম (CHECK, foreign key). প্রতিটি বদলের তিনটি প্রশ্ন আছে: পুরোনো সারির কী হবে? যারা এই টেবিল ব্যবহার করে (view, কোয়েরি, অ্যাপ) তারা ভাঙবে কি? আর বদলের সময় টেবিল কতক্ষণ আটকে থাকবে? এই পর্ব এই তিন প্রশ্নের উত্তর খোঁজে।

লাইভ SQL চলে দোকানের ডেটাবেসে (db-shop): customers (customer_id, name, city, phone, joined_on — ৮ জন), categories (category_id, name — ৪টি শ্রেণি), products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার) আর order_items (order_id, product_id, quantity, unit_price — ১৬টি লাইন)। কয়েকটি এডিটর নিজের ছোট parent/child টেবিল বানিয়ে নেয়; প্রতিটির “নমুনা ডেটা” বাক্সে schema দেখতে পাবেন। প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়, foreign key চালু থাকে।

২ · ADD COLUMN — নতুন কলাম, DEFAULT আর NOT NULL

সবচেয়ে সাধারণ বদল হলো কলাম যোগ। গঠন সব DBMS-এ প্রায় একই: ALTER TABLE টেবিল ADD COLUMN কলাম টাইপ [constraint];. নতুন কলামটি টেবিলের শেষে বসে, আর পুরোনো প্রতিটি সারিতে তার মান হয় DEFAULT — DEFAULT না দিলে NULL. চলুন দোকানের email-প্রয়োজনটি মেটাই:

-- নতুন প্রয়োজন: রসিদ যাবে email-এ
ALTER TABLE customers ADD COLUMN email TEXT;

-- পুরোনো ৮টি সারিতে email এখন NULL; তিনজনেরটা জানা গেল
UPDATE customers SET email = 'rahim@example.com'  WHERE customer_id = 1;
UPDATE customers SET email = 'karima@example.com' WHERE customer_id = 2;
UPDATE customers SET email = 'nusrat@example.com' WHERE customer_id = 4;

SELECT customer_id, name, email FROM customers;
SELECT COUNT(*) AS total, COUNT(email) AS with_email FROM customers;

প্রথমে “✓ ALTER TABLE”, তারপর তিনটি “✓ UPDATE — ১টি সারি বদলেছে”। প্রথম SELECT-এ ৮টি সারি: Rahim Uddin, Karima Begum আর Nusrat Jahan-এর পাশে তাঁদের email, বাকি পাঁচজনের (Sabbir, Tanvir, Farhana, Imran, Mitu) পাশে NULL. শেষ SELECT দেয় 8 | 3 — COUNT(email) শুধু non-NULL মান গোনে। পুরোনো সারিতে নতুন তথ্য ভরার এই কাজটিকে বলে backfill. বাস্তবে এটি একবারে না হয়ে ধীরে ধীরে হয় — গ্রাহক লগইন করলে email চাওয়া, কল সেন্টার থেকে সংগ্রহ। তাই নতুন কলাম প্রায় সবসময় শুরুতে NULL-যোগ্য থাকে।

NOT NULL কলাম যোগ করতে চাইলে

ধরুন নিয়ম হলো “প্রতিটি গ্রাহক হয় সক্রিয়, নয় নিষ্ক্রিয়” — ঘরটি কখনো ফাঁকা থাকবে না। নতুন কলামে NOT NULL দিলে প্রশ্ন ওঠে: পুরোনো ৮টি সারিতে কী বসবে? DEFAULT থাকলে উত্তর সহজ, না থাকলে DBMS কলামটি যোগ করতেই অস্বীকার করে:

-- DEFAULT আছে: পুরোনো সারিগুলো সেই মান পায়
ALTER TABLE customers ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1;
SELECT customer_id, name, is_active FROM customers WHERE customer_id <= 3;

-- DEFAULT ছাড়া NOT NULL: পুরোনো ৮টি সারিতে কী বসবে?
ALTER TABLE customers ADD COLUMN district TEXT NOT NULL;

প্রথম ALTER সফল; SELECT-এ তিনটি সারি — 1 | Rahim Uddin | 1, 2 | Karima Begum | 1, 3 | Sabbir Hasan | 1. দ্বিতীয় ALTER-এ ত্রুটি: Cannot add a NOT NULL column with default value NULL. যুক্তিটি পরিষ্কার: DEFAULT না থাকলে পুরোনো সারিগুলো NULL পেত, আর NOT NULL ঠিক সেটিই নিষেধ করে। সমাধান দুটি: একটি অর্থপূর্ণ DEFAULT দিন, অথবা তিন ধাপে যান — (১) NULL-যোগ্য কলাম যোগ, (২) backfill, (৩) তারপর NOT NULL বসানো। তৃতীয় ধাপ PostgreSQL-এ ALTER COLUMN district SET NOT NULL, কিন্তু SQLite-এ এমন স্টেটমেন্ট নেই — সেখানে লাগবে অংশ ৪-এর rebuild.

SQLite-এর ADD COLUMN-এ আরও কিছু নিয়ম আছে: নতুন কলাম PRIMARY KEY বা UNIQUE হতে পারে না; DEFAULT হিসেবে CURRENT_TIMESTAMP-এর মতো সময়-নির্ভর মান চলে না; আর foreign key চালু থাকলে REFERENCES-ওয়ালা নতুন কলামের DEFAULT NULL হতে হয় (নইলে ত্রুটি Cannot add a REFERENCES column with non-NULL default value). SQLite 3.37.0 (২০২১) থেকে নতুন কলামের CHECK পুরোনো সারিগুলোর ওপরও যাচাই হয়।

DEFAULT বেছে নিন সাবধানে। is_active DEFAULT 1 বলে দিচ্ছে “সব পুরোনো গ্রাহক সক্রিয়” — এটি একটি দাবি, আর দাবিটি ভুল হলে ডেটা নীরবে মিথ্যা বলবে। একইভাবে email DEFAULT '' দিলে “email জানা নেই” আর “email ফাঁকা” মিশে যায়। অজানা মানের জন্য NULL-ই সৎ উত্তর; DEFAULT দিন তখনই, যখন সেটি সত্যিই সব পুরোনো সারির জন্য সত্য।

৩ · নাম বদল আর কলাম মোছা — RENAME আর DROP COLUMN

কখনো নাম ভুল থাকে: phone আসলে শুধু মোবাইল নম্বর রাখে, দল চায় নাম হোক mobile. কখনো পুরো টেবিলের নাম বদলায়: categories থেকে product_categories. দুটির জন্য স্টেটমেন্ট: ALTER TABLE t RENAME COLUMN a TO b (RENAME COLUMN) আর ALTER TABLE t RENAME TO t2. বড় প্রশ্ন: যারা পুরোনো নামে এই টেবিল ব্যবহার করছিল — একটি view, অন্য টেবিলের foreign key — তাদের কী হবে?

-- পুরোনো নাম phone ব্যবহার করে একটি view
CREATE VIEW dhaka_customers AS
  SELECT customer_id, name, phone FROM customers WHERE city = 'Dhaka';

ALTER TABLE customers RENAME COLUMN phone TO mobile;
ALTER TABLE categories RENAME TO product_categories;

-- view আর products-এর foreign key কি নতুন নাম জানে?
SELECT * FROM dhaka_customers;
SELECT name, sql FROM sqlite_schema
WHERE name IN ('dhaka_customers', 'products');

view-এর ফলে তিনটি সারি — 1 | Rahim Uddin | 01711000001, 3 | Sabbir Hasan | 01911000003, 8 | Mitu Rani | 01411000008 — আর কলামের শিরোনাম এখন mobile! sqlite_schema (SQLite-এর system catalog, যেখানে প্রতিটি টেবিল আর view-এর CREATE লেখা থাকে) দেখাচ্ছে, SQLite view-এর সংজ্ঞা নিজেই বদলে লিখেছে: SELECT customer_id, name, mobile FROM customers …. products-এর সংজ্ঞাতেও এখন category_id INTEGER REFERENCES "product_categories"(category_id). অর্থাৎ ডেটাবেসের ভেতরের নির্ভরতাগুলো নতুন নাম পেয়েছে (SQLite 3.25/3.26 থেকে এটি হয়)। কিন্তু ডেটাবেসের বাইরের কোড — অ্যাপের Python বা PHP ফাইলে লেখা SELECT phone … — কেউ বদলাবে না; সেগুলো পরের বার চলামাত্র no such column: phone দিয়ে ভাঙবে। চালু সিস্টেমে নাম বদলের নিরাপদ পথ অংশ ৯-এ।

DROP COLUMN — মোছা সহজ, ফেরানো কঠিন

DROP COLUMN কলামটি তার সব ডেটাসহ মুছে দেয়। PostgreSQL আর MySQL বহু বছর ধরে এটি পারে; SQLite পেয়েছে ৩.৩৫.০ সংস্করণে (২০২১), কয়েকটি শর্তসহ: কলামটি PRIMARY KEY বা তার অংশ হতে পারবে না, UNIQUE হতে পারবে না, কোনো index-এ থাকতে পারবে না, অন্য কলামের CHECK-এ, generated column-এ, trigger-এ বা view-এ ব্যবহৃত হতে পারবে না। এক কথায় — যার ওপর আর কিছু নির্ভর করে, তা সরাসরি মোছা যায় না।

-- joined_on কোনো key, index বা constraint-এ নেই: মোছা যায়
ALTER TABLE customers DROP COLUMN joined_on;
SELECT * FROM customers WHERE customer_id <= 2;

-- phone-এ UNIQUE আছে: SQLite সরাসরি মুছতে দেবে না
ALTER TABLE customers DROP COLUMN phone;

প্রথম DROP সফল; SELECT-এ এখন চারটি কলাম — 1 | Rahim Uddin | Dhaka | 01711000001 আর 2 | Karima Begum | Chattogram | 01811000002. joined_on চিরতরে গেছে; backup ছাড়া এই তারিখগুলো আর ফেরানো যাবে না। দ্বিতীয়টিতে ত্রুটি: cannot drop UNIQUE column: "phone" — UNIQUE-এর পেছনে একটি index আছে, যা কলামটির ওপর নির্ভর করে। একই কারণে view-এ ব্যবহৃত কলাম মুছতে গেলে আসে error in view … after drop column: no such column. এমন কলাম মুছতে হলে আগে নির্ভরতা (view, index) সরাতে হয়, বা অংশ ৪-এর rebuild করতে হয়।

ALTER-এর ইতিহাস এক নজরে। ADD/DROP COLUMN আর ADD/DROP CONSTRAINT সহ ALTER TABLE-এর মূল রূপ, এবং DEFERRABLE constraint — দুটিই SQL-92 standard-এর অংশ। SQLite শুরু থেকেই ছোট আর সহজ থাকার নীতিতে চলেছে: ২০০৫ সালের দিকে এতে এসেছিল শুধু RENAME TO আর ADD COLUMN. তারপর ১৩ বছরেরও বেশি অপেক্ষা — RENAME COLUMN এল ৩.২৫.০-এ (২০১৮), DROP COLUMN ৩.৩৫.০-এ (২০২১)। এর মধ্যে বাকি সব বদলের জন্য SQLite-এর ডকুমেন্টেশন একটি হাতে-করা পদ্ধতি দিয়ে রেখেছে, যা আজও প্রযোজ্য।

৪ · SQLite-এর সীমিত ALTER আর ১২ ধাপের rebuild

PostgreSQL আর MySQL-এ ALTER TABLE প্রায় সবকিছু পারে: কলামের টাইপ বদল, NOT NULL বসানো-তোলা, DEFAULT বদল, constraint যোগ-বাদ। নিচের ব্লকগুলো তুলনার জন্য — এই পাতায় চালানো যায় না:

ALTER TABLE customers ALTER COLUMN phone TYPE varchar(20);
ALTER TABLE customers ALTER COLUMN city SET NOT NULL;
ALTER TABLE customers ALTER COLUMN city DROP NOT NULL;
ALTER TABLE customers ALTER COLUMN is_active SET DEFAULT true;
ALTER TABLE products  ADD CONSTRAINT products_stock_max CHECK (stock <= 10000);
ALTER TABLE products  DROP CONSTRAINT products_stock_max;
ALTER TABLE orders    ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id) REFERENCES customers (customer_id) ON DELETE RESTRICT;
ALTER TABLE customers MODIFY COLUMN phone VARCHAR(20) NULL;
ALTER TABLE customers MODIFY COLUMN city VARCHAR(50) NOT NULL;  -- strict mode-এ NULL থাকলে ব্যর্থ
ALTER TABLE customers RENAME COLUMN phone TO mobile;            -- 8.0 থেকে
ALTER TABLE products  ADD CONSTRAINT products_stock_max CHECK (stock <= 10000);
ALTER TABLE products  DROP CHECK products_stock_max;            -- 8.0.16 থেকে
ALTER TABLE orders    DROP FOREIGN KEY orders_ibfk_1;           -- MySQL-এর নিজের দেওয়া নাম

SQLite-এ এগুলোর কোনোটিই নেই। SQLite-এর ALTER TABLE ঠিক চারটি কাজ পারে: RENAME TO, RENAME COLUMN, ADD COLUMN আর DROP COLUMN. ALTER TABLE customers ADD CONSTRAINT … লিখলে আসে near "CONSTRAINT": syntax error, আর ALTER COLUMN লিখলে near "ALTER": syntax error. কারণটি SQLite-এর গঠনে: সে প্রতিটি টেবিলের schema রাখে মূল CREATE TABLE লেখাটি হিসেবেই, আর ডিস্কের প্রতিটি সারি সেই লেখার কলাম-ক্রম ধরে সাজানো। লেখাটি বদলানো যায়, কিন্তু টাইপ বা constraint বদলালে প্রতিটি সারিও যাচাই বা নতুন করে লিখতে হতে পারে — এই সাধারণ কাজটি SQLite নিজের ভেতরে রাখেনি।

তার বদলে SQLite-এর অফিশিয়াল ডকুমেন্টেশন একটি ১২ ধাপের পদ্ধতি দেয়। মূল ভাবনা সহজ: নতুন নকশায় একটি টেবিল বানানো, পুরোনো ডেটা কপি করা, পুরোনোটি মোছা, নতুনটির নাম বদলে পুরোনো নাম দেওয়া — সব একটি transaction-এর ভেতরে, যাতে মাঝপথে কিছু ভাঙলে পুরোটাই ফিরে যায়। নিচের ডায়াগ্রামে customers টেবিলে city-র জন্য একটি CHECK বসানোর পুরো পথটি দেখুন:

sqlite_schema আর সংযোগের অবস্থালাল = এই ধাপে যা বদলাল

            
অবস্থা
অফিশিয়াল ধাপ
foreign key যাচাই
transaction
  1. শুরু। customers-এ ১০ জন গ্রাহক (db-shop-এর আটজন আর অংশ ৫-এর আমদানি করা দুজন), city-তে কোনো নিয়ম নেই। orders টেবিলের foreign key customers-এর দিকে আঙুল তুলে আছে। লক্ষ্য: city-তে একটি CHECK বসানো, যা SQLite-এর ALTER দিয়ে সম্ভব নয়।
  2. ধাপ ১–৩: প্রস্তুতি। foreign key যাচাই বন্ধ (নইলে পুরোনো টেবিল মোছার সময় orders আটকাবে বা cascade হবে), transaction শুরু, আর SELECT sql FROM sqlite_schema WHERE tbl_name = 'customers' দিয়ে customers-এর index, trigger আর view-এর সংজ্ঞা লিখে রাখা — পরে আবার বানাতে হবে। PRAGMA foreign_keys transaction-এর ভেতরে কাজ করে না, তাই এটি BEGIN-এর আগে।
  3. ধাপ ৪: নতুন নকশা। CREATE TABLE customers_new (…) — আগের সব কলাম, সাথে নতুন CHECK. টেবিলটি এখনো খালি।
  4. ধাপ ৫: ডেটা কপি। INSERT INTO customers_new SELECT * FROM customers. এই মুহূর্তেই নতুন নিয়ম পুরোনো ডেটার ওপর পরীক্ষা হয় — কোনো সারি CHECK ভাঙলে এখানেই ত্রুটি আসবে আর কিছুই বদলাবে না। এখানে ১০টি সারিই পার হয়েছে।
  5. ধাপ ৬–৭: মোছা আর নাম বদল। পুরোনো customers মুছে customers_new-এর নাম customers. orders-এর সংজ্ঞায় লেখা ছিল REFERENCES customers — নাম ধরে — তাই সেটি এখন নিজে থেকেই নতুন টেবিলকে বোঝায়। ক্রমটি জরুরি: আগে পুরোনোটিকে customers_old নাম দিলে SQLite orders-এর foreign key-ও customers_old-এ বদলে লিখত!
  6. ধাপ ৮–১০: পুনর্গঠন আর যাচাই। ধাপ ৩-এ লিখে রাখা index আর trigger আবার বানানো, প্রভাবিত view মুছে নতুন করে বানানো, আর PRAGMA foreign_key_check চালানো — এটি সব foreign key-ভাঙা সারির তালিকা দেয়। কোনো সারি নেই মানে সব সম্পর্ক অক্ষত।
  7. ধাপ ১১–১২: শেষ। COMMIT — এই মুহূর্তে অন্য সংযোগগুলো একবারে নতুন টেবিল দেখতে পায়, মাঝের অবস্থা কেউ দেখে না। তারপর foreign key যাচাই আবার চালু। একই পদ্ধতিতে কলামের টাইপ বদল, NOT NULL বসানো, foreign key যোগ বা কলামের ক্রম বদল — সব করা যায়।

আরও গভীরে: উপরের ধাপ ৬–৭-এর সতর্কতা নিজে যাচাই করা হয়েছে। SQLite 3.26 থেকে ALTER TABLE … RENAME TO অন্য টেবিলের foreign key, trigger আর view-এর লেখাও নতুন নামে বদলে দেয় (PRAGMA legacy_alter_table = ON না থাকলে)। তাই customers-কে customers_old নাম দিলে orders-এর সংজ্ঞা হয়ে যায় REFERENCES "customers_old"(customer_id); পরে customers_old মুছলে orders এমন টেবিলের দিকে আঙুল তোলে, যা আর নেই। আর কোনো view যদি পুরোনো টেবিলের ওপর থাকে, ধাপ ৭-এর rename ব্যর্থ হয় error in view v: no such table: main.customers দিয়ে — কারণ rename-এর সময় SQLite পুরো schema নতুন করে parse করে। এজন্যই অফিশিয়াল ধাপ ৯ বলে প্রভাবিত view আগে মুছে পরে বানাতে।

৫ · পুরোনো ডেটায় নতুন constraint — আগে নিয়মভঙ্গকারী খুঁজুন

নতুন নিয়ম: customers-এর city হবে বাংলাদেশের আটটি বিভাগের নামের একটি, ঠিক এই বানানে — Dhaka, Chattogram, Rajshahi, Khulna, Sylhet, Barishal, Rangpur, Mymensingh (অজানা হলে NULL চলবে)। কারণ রিপোর্টে “Ctg” আর “Chattogram” আলাদা শহর হিসেবে গোনা হচ্ছিল। এই এডিটরে db-shop-এর সাথে দুজন নতুন গ্রাহক যোগ হয়েছেন, যাঁদের তথ্য একটি Excel ফাইল থেকে আমদানি করা: (9, 'Sadia Islam', 'Ctg', …) আর (10, 'Rakib Hasan', 'dhaka', …).

যেকোনো DBMS-এ constraint যোগের সময় সেটি পুরোনো সব সারিতে যাচাই হয়; একটি সারিও ভাঙলে পুরো কাজ ব্যর্থ। তাই পেশাদার নিয়ম: constraint বসানোর আগে একটি SELECT দিয়ে দেখুন, কারা নিয়ম ভাঙছে। CHECK শুধু তখনই আটকায়, যখন শর্ত FALSE হয় (NULL হলে UNKNOWN, যা পার পায় — পর্ব ১৬), তাই violator খোঁজার শর্ত হলো WHERE NOT (শর্ত), বা এখানে city NOT IN (…):

-- ১. নিয়ম বসানোর আগে: কোন সারিগুলো নিয়ম ভাঙছে?
SELECT customer_id, name, city FROM customers
WHERE city NOT IN ('Dhaka', 'Chattogram', 'Rajshahi', 'Khulna',
                   'Sylhet', 'Barishal', 'Rangpur', 'Mymensingh');

-- ২. না শুধরেই rebuild শুরু করলে: ধাপ ৫-এর কপি ব্যর্থ
CREATE TABLE customers_new (
  customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL,
  city TEXT CONSTRAINT customers_city_chk CHECK (city IN ('Dhaka', 'Chattogram',
       'Rajshahi', 'Khulna', 'Sylhet', 'Barishal', 'Rangpur', 'Mymensingh')),
  phone TEXT UNIQUE, joined_on TEXT NOT NULL);
INSERT INTO customers_new SELECT * FROM customers;

SELECT দুটি সারি দেয়: 9 | Sadia Islam | Ctg আর 10 | Rakib Hasan | dhaka. Imran Hossain (7)-এর city NULL হলেও তালিকায় নেই — NULL NOT IN (…)-এর ফল UNKNOWN, আর CHECK-ও তাঁকে আটকাবে না। তারপর “✓ CREATE TABLE”, আর INSERT-এ ত্রুটি: CHECK constraint failed: customers_city_chk. খেয়াল করুন, বার্তায় এসেছে constraint-এর নাম — কারণ আমরা CONSTRAINT customers_city_chk লিখে নাম দিয়েছি। নাম না দিলে SQLite পুরো শর্তের লেখাটি দেখাত। PostgreSQL-এ একই কাজ ALTER TABLE customers ADD CONSTRAINT customers_city_chk CHECK (…), আর ব্যর্থ হলে বার্তা check constraint "customers_city_chk" of relation "customers" is violated by some row.

এখন ডেটা শোধরান — Sadia চট্টগ্রামের, Rakib ঢাকার — তারপর পুরো ১২ ধাপের rebuild চালান। এবার প্রতিটি ধাপ পার হওয়ার কথা:

UPDATE customers SET city = 'Chattogram' WHERE city = 'Ctg';
UPDATE customers SET city = 'Dhaka' WHERE city = 'dhaka';
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE customers_new (
  customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL,
  city TEXT CONSTRAINT customers_city_chk CHECK (city IN ('Dhaka', 'Chattogram',
       'Rajshahi', 'Khulna', 'Sylhet', 'Barishal', 'Rangpur', 'Mymensingh')),
  phone TEXT UNIQUE, joined_on TEXT NOT NULL);
INSERT INTO customers_new SELECT * FROM customers;
DROP TABLE customers;
ALTER TABLE customers_new RENAME TO customers;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;
SELECT city, COUNT(*) AS n FROM customers GROUP BY city;

দুটি UPDATE-এ “১টি সারি বদলেছে”, তারপর একে একে ✓ চিহ্ন; INSERT-এ “১০টি সারি বদলেছে”। PRAGMA foreign_key_check-এর ফল “(কোনো সারি নেই)” — orders-এর ১০টি অর্ডারই নতুন customers-এ তাদের গ্রাহক খুঁজে পেয়েছে। শেষ SELECT-এ ছয়টি দল: NULL | 1 (Imran), Chattogram | 2, Dhaka | 4, Khulna | 1, Rajshahi | 1, Sylhet | 1 — “Ctg” আর “dhaka” আর আলাদা দল নয়। এখন নিয়মটি পাহারায়: এডিটরের শেষে UPDATE customers SET city = 'Comilla' WHERE customer_id = 3; যোগ করে চালান — আসবে CHECK constraint failed: customers_city_chk.

Constraint-এর নাম — কেন নিজে দেবেন

নাম না দিলে DBMS নিজে একটি নাম বানায়: PostgreSQL-এ products_price_check, orders_customer_id_fkey, customers_pkey; MySQL-এ products_chk_1, orders_ibfk_1. এই নামগুলো DBMS-ভেদে আলাদা, আর একই migration দুটি সার্ভারে আলাদা ক্রমে চললে আলাদা নামও পেতে পারে। অথচ constraint মুছতে বা বদলাতে নামটিই লাগে: ALTER TABLE products DROP CONSTRAINT products_price_check. তাই পেশাদার দলগুলো একটি নামকরণ-রীতি মানে, যেমন <টেবিল>_<কলাম>_chk, <টেবিল>_<কলাম>_fk. নাম থাকলে ত্রুটি-বার্তাও পড়া যায়, যেমন উপরে দেখলেন। SQLite-এ নাম দেওয়া যায়, কিন্তু নাম ধরে constraint মোছার স্টেটমেন্ট নেই — সেখানেও rebuild.

বড় টেবিলে “আগে নিয়ম, পরে পুরোনো ডেটা”: কোটি সারির টেবিলে সব violator শোধরাতে সপ্তাহ লাগতে পারে, অথচ নতুন ভুল ডেটা ঢোকা আজই বন্ধ করা দরকার। PostgreSQL-এ ADD CONSTRAINT … NOT VALID লিখলে নিয়ম শুধু নতুন আর বদলানো সারিতে খাটে; পুরোনোগুলো শোধরানো শেষে VALIDATE CONSTRAINT দিয়ে পূর্ণ যাচাই হয়, যা হালকা lock নিয়ে চলে। SQL Server-এ সমতুল্য হলো WITH NOCHECK, Oracle-এ ENABLE NOVALIDATE. ভাবনার প্রশ্ন ১-এ এর লাভ-ক্ষতি দেখবেন।

৬ · Deferrable constraint — যাচাই commit-এর সময়

সাধারণত constraint যাচাই হয় প্রতিটি স্টেটমেন্টের সাথে সাথে। কিন্তু কিছু বৈধ কাজ মাঝপথে কিছুক্ষণের জন্য নিয়ম ভাঙে। ক্লাসিক উদাহরণ বৃত্তাকার সম্পর্ক: প্রতিটি বিভাগের একজন প্রধান থাকবেন, যিনি একজন কর্মী (departments.head_id → employees), আর প্রতিটি কর্মী কোনো একটি বিভাগে (employees.dept_id → departments) — দুটিই NOT NULL. নতুন অফিস খুললে প্রথম বিভাগ আর তার প্রধান — কাকে আগে ঢোকাবেন? বিভাগ আগে ঢোকালে প্রধান নেই; প্রধান আগে ঢোকালে বিভাগ নেই। ডিম আগে না মুরগি!

সমাধান deferrable constraint: DEFERRABLE INITIALLY DEFERRED লিখলে যাচাই পিছিয়ে যায় transaction-এর শেষে, COMMIT-এর মুহূর্তে। মাঝখানে নিয়ম ভাঙা চলে; COMMIT-এর সময় সব ঠিক থাকলেই হলো। SQLite-এ শুধু foreign key-ই deferrable হতে পারে; PostgreSQL-এ foreign key, UNIQUE, PRIMARY KEY আর EXCLUDE পারে (CHECK আর NOT NULL পারে না); Oracle-ও সমর্থন করে; MySQL আর SQL Server করে না।

-- বিভাগ আর তার প্রধান: একসাথে, একটি transaction-এ
BEGIN;
INSERT INTO departments VALUES (1, 'Accounts', 10);  -- কর্মী 10 এখনো নেই
INSERT INTO employees   VALUES (10, 'Rina Das', 1);
COMMIT;
SELECT d.name AS department, e.name AS head
FROM departments d JOIN employees e ON e.emp_id = d.head_id;

-- যাচাই হয় COMMIT-এ: অস্তিত্বহীন প্রধান 99
BEGIN;
INSERT INTO departments VALUES (2, 'Sales', 99);
COMMIT;

প্রথম transaction-এ প্রথম INSERT-এর পর এক মুহূর্তের জন্য Accounts-এর প্রধান 10 অস্তিত্বহীন — তবু “✓ INSERT — ১টি সারি বদলেছে”, কারণ যাচাই পিছানো। Rina ঢোকার পর COMMIT-এর সময় দুটি সম্পর্কই ঠিক, তাই ✓. SELECT দেয় Accounts | Rina Das. দ্বিতীয় transaction-এ INSERT-ও ✓ পায় (সারিটি ঢুকেছে!), কিন্তু COMMIT-এ ত্রুটি: FOREIGN KEY constraint failed — প্রধান 99 কখনো এল না। ত্রুটিটি কোন স্টেটমেন্টে এল, খেয়াল করুন: INSERT নয়, COMMIT. অ্যাপের কোডকে তাই COMMIT-এর ত্রুটিও ধরতে হয়। SQLite-এ COMMIT এভাবে ব্যর্থ হলে transaction খোলা থাকে — ডেটা ঠিক করে আবার COMMIT, বা ROLLBACK. (BEGIN ছাড়া প্রতিটি স্টেটমেন্ট নিজেই একটি transaction; তখন deferred যাচাই হয় সেই স্টেটমেন্টের শেষেই।)

-- PostgreSQL: DEFERRABLE INITIALLY IMMEDIATE = সাধারণত সঙ্গে সঙ্গে যাচাই,
-- কিন্তু দরকার হলে একটি transaction-এর জন্য পিছানো যায়
ALTER TABLE departments ADD CONSTRAINT departments_head_fk
  FOREIGN KEY (head_id) REFERENCES employees (emp_id)
  DEFERRABLE INITIALLY IMMEDIATE;

BEGIN;
SET CONSTRAINTS departments_head_fk DEFERRED;
-- ... বৃত্তাকার INSERT ...
COMMIT;

Deferral-এর আরও দুটি পরিচিত কাজ: (১) একটি UNIQUE কলামে দুটি মান অদলবদল — ক্লাসের রোল 5 আর 6 অদলবদল করলে মাঝপথে দুজনেরই রোল 6 হয়ে যায়; deferrable UNIQUE হলে এটি একটি transaction-এ চলে। (২) বড় ডেটা আমদানি, যেখানে child-এর ফাইল parent-এর ফাইলের আগে আসে। SQLite-এ সব foreign key একটি transaction-এর জন্য পিছাতে আছে PRAGMA defer_foreign_keys = ON.

৭ · Referential action গভীরে — CASCADE থেকে NO ACTION

পর্ব ৬-এ referential action-এর পরিচয় পেয়েছেন: parent সারি মুছলে (ON DELETE) বা তার key বদলালে (ON UPDATE) child সারির কী হবে। পাঁচটি বিকল্প: CASCADE (child-ও মোছে বা বদলায়), SET NULL (child-এর foreign key NULL), SET DEFAULT (child-এর foreign key তার DEFAULT মান পায়), RESTRICT আর NO ACTION (দুটিই child থাকলে আটকায়)। নিচের ডায়াগ্রামে একই ছোট টেবিলে একটি একই DELETE পাঁচটি নিয়মে কী করে, পাশাপাশি দেখুন:

categories (parent) · products (child)লাল = এই নিয়মে যা ঘটল

            
ফল
নিয়ম
Books মুছল?
products-এ সারি
যাচাই কখন
  1. শুরু। তিনটি শ্রেণি (0 Uncategorized, 1 Electronics, 2 Books) আর তিনটি পণ্য; DSA Book আর SQL Book দুটিই Books (2)-এর। আসছে DELETE FROM categories WHERE category_id = 2. Books-এর দিকে দুটি child আঙুল তুলে আছে — নিয়ম অনুযায়ী ফল আলাদা।
  2. CASCADE — child-ও যায়। Books মুছল, সাথে দুটি বই-পণ্যও। products-এ থাকল শুধু Headphone. শ্রেণির মতো “লেবেল” মুছলে আসল পণ্য হারানো এখানে প্রায় নিশ্চিতভাবে ভুল।
  3. SET NULL — child থাকে, সম্পর্ক ছিঁড়ে যায়। Books মুছল; দুটি বই রইল, কিন্তু category_id এখন NULL — “শ্রেণিহীন”। শর্ত: child কলামে NOT NULL থাকা চলবে না।
  4. SET DEFAULT — child যায় ডিফল্ট parent-এ। কলামের DEFAULT 0, তাই দুটি বই চলে গেল Uncategorized (0)-এ। শর্ত: ডিফল্ট মানের parent সারিটিও থাকতে হবে, নইলে foreign key ভাঙবে।
  5. RESTRICT — সঙ্গে সঙ্গে না। child আছে দেখামাত্র DELETE বাতিল; কিছুই বদলায়নি। deferred হলেও RESTRICT অপেক্ষা করে না।
  6. NO ACTION — শেষে যাচাই। সাধারণ ক্ষেত্রে ফল RESTRICT-এর মতোই: DELETE বাতিল। পার্থক্য সময়ে — যাচাই হয় স্টেটমেন্টের শেষে, আর constraint deferrable হলে COMMIT-এ। তাই মাঝখানে কেউ একই key-তে নতুন parent ঢুকিয়ে দিলে NO ACTION মেনে নেয়, RESTRICT নেয় না।

এবার আসল SQLite-এ। এই এডিটরের ছোট schema: categories(category_id, name), products(product_id, name, category_id) — ON DELETE SET NULL — আর banners(banner_id, title, category_id), অর্থাৎ ওয়েবসাইটের প্রথম পাতায় একটি শ্রেণির বিজ্ঞাপন-ব্যানার — ON DELETE CASCADE. শ্রেণি না থাকলে তার ব্যানারের কোনো অর্থ নেই, কিন্তু পণ্যের আছে:

-- Books (2) শ্রেণি বন্ধ: পণ্য থাকবে, ব্যানার যাবে
DELETE FROM categories WHERE category_id = 2;

SELECT * FROM products;
SELECT * FROM banners;

“✓ DELETE — ১টি সারি বদলেছে” (cascade আর SET NULL-এর সারিগুলো এই সংখ্যায় গোনা হয় না)। products-এ চারটি সারিই আছে: 102 | Headphone | 1, 104 | DSA Book | NULL, 105 | SQL Book | NULL, 108 | Rice 5kg | 3. banners-এ শুধু 3 | Gadget Week | 1 — Books-এর দুটি ব্যানার CASCADE-এ মুছে গেছে। একটি DELETE, দুটি child টেবিল, দুই রকম আচরণ — কারণ প্রতিটি সম্পর্কের প্রশ্ন আলাদা: “parent ছাড়া child-এর কি নিজের অর্থ আছে?”

SET DEFAULT-এর একটি ফাঁদ আছে: ডিফল্ট মানটিও একটি foreign key মান, তাই সেটিরও parent থাকতে হয়। এই এডিটরে products.category_id-এর DEFAULT 0 (Uncategorized), নিয়ম ON DELETE SET DEFAULT:

-- Books (2) মুছলে বইগুলো যায় ডিফল্ট শ্রেণি 0-তে
DELETE FROM categories WHERE category_id = 2;
SELECT p.product_id, p.name, c.name AS category
FROM products p JOIN categories c ON c.category_id = p.category_id;

-- এবার ডিফল্ট শ্রেণি 0 নিজেই মুছলে?
DELETE FROM categories WHERE category_id = 0;

প্রথম DELETE সফল; JOIN-এর ফল: 102 | Headphone | Electronics, 104 | DSA Book | Uncategorized, 105 | SQL Book | Uncategorized. দ্বিতীয় DELETE-এ ত্রুটি FOREIGN KEY constraint failed: শ্রেণি 0 মুছলে SET DEFAULT বইগুলোকে আবার 0 দিতে চায় — কিন্তু 0 আর নেই। তাই “Uncategorized” ধরনের ডিফল্ট parent সারিকে কখনো মোছা যায় না, এমন ব্যবস্থা রাখতে হয়। একটি সতর্কতা: MySQL-এর InnoDB SET DEFAULT লেখা parse করে, কিন্তু এমন টেবিল বানাতেই দেয় না; Oracle-এ ON DELETE-এ শুধু CASCADE আর SET NULL আছে।

RESTRICT বনাম NO ACTION — পার্থক্য শুধু সময়ে

SQL standard অনুযায়ী RESTRICT যাচাই করে সঙ্গে সঙ্গে, প্রতিটি সারি বদলের মুহূর্তে; NO ACTION যাচাই করে স্টেটমেন্টের শেষে, আর constraint deferred হলে COMMIT-এ। পার্থক্যটি চোখে পড়ে তখনই, যখন মাঝখানে কিছু অবস্থাটি শুধরে দেয়। এই এডিটরে দুটি child: orders (ON DELETE NO ACTION) আর payments (ON DELETE RESTRICT), দুটিই DEFERRABLE INITIALLY DEFERRED. Rahim (1)-এর একটি অর্ডার, Karima (2)-এর একটি পেমেন্ট আছে। কাজ: গ্রাহকের সারি মুছে সংশোধিত তথ্যসহ আবার ঢোকানো — যা আমদানি-স্ক্রিপ্ট আর REPLACE INTO ভেতরে ভেতরে করে:

-- Rahim-এর অর্ডার আছে (NO ACTION): মুছে আবার ঢোকানো চলে
BEGIN;
DELETE FROM customers WHERE id = 1;
INSERT INTO customers VALUES (1, 'Rahim Uddin');
COMMIT;
SELECT * FROM customers;

-- Karima-র পেমেন্ট আছে (RESTRICT): deferred হলেও সঙ্গে সঙ্গে আটকায়
BEGIN;
DELETE FROM customers WHERE id = 2;

প্রথম transaction পুরোপুরি সফল: DELETE-এর পর অর্ডার 1001 এক মুহূর্তের জন্য অনাথ, কিন্তু NO ACTION অপেক্ষা করেছে; COMMIT-এর সময় নতুন Rahim (1) হাজির, তাই সব ঠিক। SELECT: 1 | Rahim Uddin, 2 | Karima. দ্বিতীয় transaction-এ DELETE-এই ত্রুটি FOREIGN KEY constraint failed — RESTRICT কোনো সুযোগ দেয় না। PostgreSQL আর SQLite এই পার্থক্য মানে; MySQL-এ InnoDB সবসময় সঙ্গে সঙ্গে যাচাই করে, তাই সেখানে RESTRICT আর NO ACTION হুবহু এক। SQL Server-এ RESTRICT শব্দটিই নেই, ডিফল্ট NO ACTION.

৮ · Cascade-এর শিকল আর সম্পর্ক বুঝে action বাছাই

CASCADE এক স্তরে থামে না: child নিজে আরেকটি টেবিলের parent হলে ঢেউ সেখানেও যায়। ধরুন কেউ “সুবিধার জন্য” db-shop-এর সব foreign key-তে CASCADE দিয়েছেন — products.category_id → categories আর order_items.product_id → products. এখন অ্যাডমিন প্যানেলে কেউ Electronics শ্রেণিটি “পুরোনো” ভেবে মুছে দিলেন:

categories → products → order_items (সব CASCADE)লাল = মোছার জন্য চিহ্নিত

            
হিসাব
মোছার জন্য চিহ্নিত
ফাঁকা হয়ে যাওয়া অর্ডার
  1. একটি নিরীহ-দেখতে DELETE. লক্ষ্য শুধু একটি শ্রেণি। ডেটাবেসে ৪টি শ্রেণি, ১০টি পণ্য আর ১০টি অর্ডারের ১৬টি লাইন।
  2. স্তর ১: শ্রেণি। Electronics (1) মোছার জন্য চিহ্নিত। এখন DBMS দেখে, এর দিকে কে কে আঙুল তুলে আছে।
  3. স্তর ২: পণ্য। products.category_id-এ CASCADE, তাই Electronics-এর চারটি পণ্য — Mobile Phone, Headphone, Power Bank, Smart Watch — চিহ্নিত। মোট ৫টি।
  4. স্তর ৩: অর্ডার-লাইন। order_items.product_id-এও CASCADE, তাই ওই চারটি পণ্যের যত বিক্রির লাইন — চারটি অর্ডারে ৬টি লাইন — সব চিহ্নিত। মোট ১১টি।
  5. ফল: হিসাবের খাতা ছেঁড়া। ১১টি সারি গেল, অথচ SQLite-এর বার্তা “১টি সারি বদলেছে”। অর্ডার 1001, 1006 আর 1009 এখন লাইনহীন — রসিদের মোট শূন্য, অথচ গ্রাহক টাকা দিয়েছেন। মাসিক বিক্রির রিপোর্ট, ভ্যাটের হিসাব, ফেরতের দাবি — সব ভুল। কোনো ত্রুটি-বার্তা আসেনি বলে ভুলটি হয়তো সপ্তাহ পরে ধরা পড়বে।
  6. সঠিক নকশা। products.category_id-এ SET NULL (শ্রেণি একটি লেবেল মাত্র), order_items.product_id-এ RESTRICT (বিক্রি হওয়া পণ্য মোছা যাবে না — দরকার হলে “বিক্রি বন্ধ” চিহ্ন)। একই DELETE এখন শুধু শ্রেণিটি মোছে; পণ্যগুলো শ্রেণিহীন হয়, আর বিক্রির ১৬টি লাইনই অক্ষত।

প্রতিটি সম্পর্কের জন্য একটি প্রশ্ন

action বাছাইয়ের সবচেয়ে কাজের প্রশ্ন: “parent না থাকলে child-এর কি নিজের অর্থ, ইতিহাস বা আইনি মূল্য আছে?” উত্তর থেকে নিয়ম আসে:

সম্পর্ক (child → parent)ON DELETEকেন
orders → customersRESTRICTঅর্ডার হিসাবের রেকর্ড; গ্রাহক চলে গেলেও বিক্রি ঘটেছিল। গ্রাহক “মুছতে” হলে soft delete বা তথ্য গোপন করা
order_items → ordersCASCADEঅর্ডার-লাইন অর্ডারের অংশ; অর্ডার ছাড়া এর কোনো অর্থ নেই (ভুল করে তৈরি ড্রাফট অর্ডার মুছতে সুবিধা)
order_items → productsRESTRICTবিক্রি হওয়া পণ্য মোছা মানে ইতিহাস মোছা; বিক্রি বন্ধ করুন, মুছবেন না
products → categoriesSET NULLশ্রেণি একটি লেবেল; শ্রেণি উঠে গেলেও পণ্য থাকে
banners → categoriesCASCADEশ্রেণির ব্যানার শ্রেণি ছাড়া অর্থহীন
instructors.mentor_id → instructorsSET NULLmentor অবসরে গেলে শিক্ষক থাকেন, mentor ঘর ফাঁকা

ON UPDATE-এর ক্ষেত্রে বাস্তব পরামর্শ আরও সহজ: surrogate primary key (পর্ব ৬) কখনো বদলায় না, তাই ON UPDATE-এর প্রশ্নই ওঠে না; ডিফল্ট NO ACTION রাখুন। ON UPDATE CASCADE লাগছে মানে key-টি সম্ভবত স্থির নয় — ডিজাইন আবার ভাবুন। আর একটি ব্যবহারিক কথা: CASCADE বা SET NULL চালাতে DBMS-কে child টেবিলে foreign key কলাম ধরে খুঁজতে হয়; সেখানে index না থাকলে প্রতিটি parent মোছায় পুরো child টেবিল পড়া হয় (পর্ব ৪১)।

৯ · Production-এ schema বদল — lock, বড় টেবিল আর migration

এই পাতার ১০ সারির টেবিলে ALTER চোখের পলকে শেষ। কিন্তু একটি চালু ই-কমার্স সাইটের orders টেবিলে কয়েক কোটি সারি থাকতে পারে, আর প্রতি সেকেন্ডে নতুন অর্ডার আসে। সেখানে ALTER-এর দুটি খরচ: সময় — কিছু বদলে DBMS পুরো টেবিল নতুন করে লেখে (table rewrite) বা প্রতিটি সারি যাচাই করে; আর lock — সেই সময়ে টেবিলে লেখা, কখনো পড়াও, আটকে থাকে। PostgreSQL-এ বেশিরভাগ ALTER TABLE সবচেয়ে কড়া lock (ACCESS EXCLUSIVE) নেয়। এমন টেবিলে একটি ঘণ্টা-ব্যাপী ALTER মানে ঘণ্টাখানেক কোনো অর্ডার নেওয়া যাবে না — ঈদের আগের রাতে যা দুঃস্বপ্ন।

বদলসাধারণত খরচমন্তব্য
NULL-যোগ্য কলাম যোগশুধু metadata (দ্রুত)প্রায় সব আধুনিক DBMS-এ
ধ্রুব DEFAULT-সহ কলাম যোগPostgreSQL 11 থেকে দ্রুত; MySQL 8.0-তে ALGORITHM=INSTANTআগে পুরো টেবিল নতুন করে লেখা হতো
কলামের টাইপ বদলপ্রায়ই table rewriteকিছু ব্যতিক্রম, যেমন PostgreSQL-এ varchar-এর সীমা বাড়ানো
NOT NULL / CHECK / FK যোগপুরো টেবিল স্ক্যান করে যাচাইPostgreSQL-এ NOT VALID + VALIDATE দিয়ে ভাগ করা যায়
index যোগপুরো টেবিল পড়া আর সাজানোPostgreSQL-এ CREATE INDEX CONCURRENTLY; MySQL-এ online DDL

যে বদল DBMS নিজে “online” করতে পারে না, তার জন্য বড় MySQL ব্যবহারকারীরা online schema change টুল ব্যবহার করেন — পরিচিত উদাহরণ pt-online-schema-change (Percona Toolkit-এর অংশ) আর gh-ost (GitHub-এর তৈরি, open source). দুটির মূল কৌশল একই — একটি ছায়া-টেবিল:

orders (ধরা যাক ৫ কোটি সারি)লাল = এই ধাপের কাজ

            
অবস্থা
অ্যাপ লিখতে পারছে?
নতুন টেবিলে ডেটা
  1. সমস্যা। যে ALTER পুরো টেবিল নতুন করে লেখে, বিশাল টেবিলে তা অনেকক্ষণ চলে, আর সেই সময়ে অ্যাপের লেখা আটকে থাকতে পারে। ব্যবসা থামিয়ে রাখা চলে না।
  2. ছায়া-টেবিল। টুল নতুন নকশায় একটি খালি টেবিল বানায়। আসল orders-এ এখনো কিছু বদলায়নি, অ্যাপ স্বাভাবিক।
  3. টুকরো টুকরো কপি। পুরোনো সারিগুলো ছোট ছোট দলে কপি হয়, প্রতিটি দল আলাদা ছোট transaction-এ। সার্ভারে চাপ বেশি হলে টুল নিজেই গতি কমায়।
  4. চলমান পরিবর্তন ধরা। কপির সময়ও নতুন অর্ডার আসছে, পুরোনো বদলাচ্ছে। pt-online-schema-change আসল টেবিলে trigger বসিয়ে প্রতিটি পরিবর্তন ছায়া-টেবিলে পাঠায়; gh-ost trigger ছাড়াই MySQL-এর replication log (binlog) পড়ে একই কাজ করে।
  5. Cut-over. দুই টেবিল মিলে গেলে একটি atomic RENAME-এ নাম অদলবদল। অ্যাপ এই কয়েক মুহূর্ত থামে, তারপর না জেনেই নতুন নকশার টেবিলে লেখে।
  6. পরিষ্কার। পুরোনো টেবিল কিছুদিন রাখা হয়, যাতে সমস্যা হলে ফেরা যায়। একটি সীমাবদ্ধতা: foreign key-ওয়ালা টেবিলে এই টুলগুলো সীমিতভাবে কাজ করে বা করে না — কেন, ভাবনার প্রশ্ন ৩-এ।

Lock-এর লাইন — ছোট ALTER-ও বিপদ ঘটাতে পারে

PostgreSQL-এ একটি সূক্ষ্ম ফাঁদ: দ্রুত ALTER-ও (যেমন NULL-যোগ্য কলাম যোগ) ACCESS EXCLUSIVE lock চায়। কোনো দীর্ঘ রিপোর্ট-কোয়েরি টেবিলটি পড়তে থাকলে ALTER অপেক্ষা করে — আর ALTER-এর পেছনে লাইনে দাঁড়িয়ে যায় অ্যাপের সব নতুন কোয়েরি, এমনকি সাধারণ SELECT-ও। এক মিলিসেকেন্ডের কাজ সাইট কয়েক মিনিট বন্ধ করে দিতে পারে। প্রচলিত প্রতিকার: ALTER-এর আগে SET lock_timeout = '2s' — lock না পেলে ALTER নিজে সরে যায়, পরে আবার চেষ্টা।

Migration — schema বদল কোড হিসেবে

পেশাদার দলে কেউ production সার্ভারে হাতে ALTER লেখেন না। প্রতিটি schema বদল একটি migration ফাইল — ক্রমিক নম্বরসহ, version control-এ (যেমন git) রাখা, code review হয়, আর development, test ও production-এ একই ক্রমে চলে। ডেটাবেসের একটি ছোট টেবিল মনে রাখে কোন migration চলেছে। Flyway, Liquibase, Alembic (Python), Rails বা Laravel-এর migration এই ধারণার পরিচিত রূপ — বিস্তারিত পর্ব ৫০-এ।

-- migration 0007: গ্রাহকের email (up)
ALTER TABLE customers ADD COLUMN email TEXT;
CREATE UNIQUE INDEX customers_email_uq ON customers (email);

-- ফেরার পথ (down) — আলাদা ফাইলে বা একই ফাইলের নিচে
-- DROP INDEX customers_email_uq;
-- ALTER TABLE customers DROP COLUMN email;

চালু অ্যাপে নাম বদল বা কলাম মোছার নিরাপদ কৌশল expand–contract (বা parallel change): প্রথমে বাড়ান — নতুন কলাম যোগ, অ্যাপ দুই কলামেই লেখে, পুরোনো ডেটা backfill; তারপর পড়া নতুন কলাম থেকে; সব অ্যাপ-সংস্করণ নতুন কলামে চলে গেলে তবে সংকোচন — পুরোনো কলাম মোছা। প্রতিটি ধাপ আলাদা migration, আর প্রতিটির পরে অ্যাপের পুরোনো ও নতুন দুই সংস্করণই চলতে পারে।

১০ · AI-এর যুগে schema বদল

“customers টেবিলে email যোগ করে UNIQUE বানাও” — AI কয়েক সেকেন্ডে ALTER লিখে দেয়। সমস্যা হলো, ALTER-এর ভুল প্রায়ই চুপচাপ থাকে, আর production-এ একবার চললে ফেরানো কঠিন। AI-এর সাধারণ ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
SQLite-এ ADD CONSTRAINT বা ALTER COLUMN লিখেছেsyntax errorআপনার DBMS আর সংস্করণ প্রম্পটে বলুন; ছোট কপিতে চালান
rebuild-এ আগে পুরোনো টেবিলের নাম বদলেছে (X → old_X)অন্য টেবিলের FK old_X-এ চলে যায়শেষে PRAGMA foreign_key_check আর sqlite_schema দেখুন
DEFAULT ছাড়া NOT NULL কলাম, বা মনগড়া DEFAULTALTER ব্যর্থ, বা পুরোনো সারিতে মিথ্যা মানপ্রতিটি DEFAULT পুরোনো সারির জন্য সত্য কি না জিজ্ঞেস করুন
violator না খুঁজে সরাসরি constraintproduction-এ migration মাঝপথে ব্যর্থআগে WHERE NOT (শর্ত) দিয়ে গুনুন
সব foreign key-তে ON DELETE CASCADEএকটি DELETE-এ ইতিহাস উধাওপ্রতিটি সম্পর্কের জন্য অংশ ৮-এর প্রশ্নটি করুন
বড় টেবিলে lock আর সময়ের কথা বলেনিসাইট বন্ধসারির সংখ্যা জানিয়ে জিজ্ঞেস করুন: table rewrite হবে কি?
নেই এমন কলাম বা constraint-এর নাম (যেমন customers_email_key)DROP CONSTRAINT ব্যর্থcatalog থেকে আসল নাম দেখে নিন

AI-কে শুধু ALTER চাইবেন না — পুরো migration পরিকল্পনা চান, ঝুঁকিসহ:

আমার ডেটাবেস SQLite 3.45 (পরে PostgreSQL 16-এ যাবে). customers টেবিল: customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT, phone TEXT UNIQUE, joined_on TEXT NOT NULL; orders.customer_id এটিকে REFERENCES করে। প্রায় ২ লাখ সারি। আমি city-তে একটি CHECK বসাতে চাই (আটটি বিভাগের নাম), আর পরে city NOT NULL করতে চাই। ১. আগে নিয়মভঙ্গকারী সারি খোঁজার SELECT দাও (NULL কীভাবে গোনা হচ্ছে, স্পষ্ট বলো)। ২. SQLite-এর জন্য অফিশিয়াল ১২ ধাপের rebuild স্ক্রিপ্ট দাও — foreign_keys বন্ধ, transaction, new_X তৈরি করে X মোছা, তারপর rename, শেষে foreign_key_check. ৩. PostgreSQL-এর জন্য NOT VALID + VALIDATE CONSTRAINT দিয়ে কম-lock পদ্ধতি দাও, lock_timeout সহ। ৪. প্রতিটি ধাপ ব্যর্থ হলে কীভাবে ফিরব (down migration) বলো। ৫. কোনো কলাম, টেবিল বা constraint-এর নাম অনুমান করবে না — না জানলে জিজ্ঞেস করো।

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

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

  • DDL আর DML-এর পার্থক্য, ALTER কোনটি? — DDL schema বদলায় (CREATE, ALTER, DROP), DML ডেটা বদলায় (INSERT, UPDATE, DELETE); ALTER হলো DDL.
  • ডেটা-ভরা টেবিলে NOT NULL কলাম যোগ করবেন কীভাবে? — DEFAULT দিয়ে, অথবা NULL-যোগ্য কলাম → backfill → NOT NULL.
  • RESTRICT আর NO ACTION-এর পার্থক্য? — RESTRICT সঙ্গে সঙ্গে যাচাই করে; NO ACTION স্টেটমেন্টের শেষে বা (deferred হলে) COMMIT-এ।
  • ON DELETE CASCADE কোথায় ঠিক, কোথায় বিপজ্জনক? — অংশ-সম্পর্কে ঠিক (অর্ডার-লাইন → অর্ডার); হিসাবের রেকর্ডে বিপজ্জনক (অর্ডার → গ্রাহক)।
  • Deferrable constraint কখন লাগে? — বৃত্তাকার foreign key, UNIQUE মান অদলবদল, বা ক্রমহীন বড় আমদানিতে।

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

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

প্র ০১কোটি সারির টেবিলে নতুন নিয়ম আজই চালু করতে হবে, কিন্তু পুরোনো ডেটার হাজারখানেক সারি নিয়ম ভাঙে, আর সেগুলো শোধরাতে সপ্তাহ লাগবে। “আগে নিয়ম, পরে পুরোনো ডেটা” — এটি কি নিরাপদ?

সাধারণ ADD CONSTRAINT এখানে কাজ করবে না: DBMS পুরো টেবিল যাচাই করবে, প্রথম violator পেয়েই ব্যর্থ হবে, আর কোটি সারি স্ক্যানের সময় টেবিলে কড়া lock থাকবে। অপেক্ষা করলে আরেক সমস্যা — শোধরানোর সপ্তাহে প্রতিদিন নতুন ভুল ডেটা ঢুকছে, আর পরিষ্কারের কাজ শেষই হচ্ছে না। বালতি খালি করার সময় কল খোলা রাখার মতো।

বড় DBMS-গুলোর উত্তর দুই ধাপে ভাগ করা। PostgreSQL-এ ADD CONSTRAINT … NOT VALID constraint-টি catalog-এ বসিয়ে দেয়, পুরোনো সারি না দেখে — তাই কাজটি প্রায় তাৎক্ষণিক। এখন থেকে প্রতিটি নতুন INSERT আর UPDATE যাচাই হয়: কল বন্ধ। তারপর ধীরে ধীরে পুরোনো violator শোধরানো, আর শেষে VALIDATE CONSTRAINT, যা পুরো টেবিল স্ক্যান করে কিন্তু এমন lock নেয় যাতে সাধারণ পড়া-লেখা চলতে থাকে। SQL Server-এ WITH NOCHECK আর Oracle-এ ENABLE NOVALIDATE একই ধারণা।

দাম কী? যতক্ষণ যাচাই হয়নি, constraint-টি একটি প্রতিশ্রুতি মাত্র, প্রমাণ নয়। কোয়েরি optimizer অনেক সময় যাচাই-করা constraint থেকে সিদ্ধান্ত নেয় (যেমন “এই শর্ত সবসময় সত্য, তাই এই অংশ বাদ”); অযাচাইকৃত constraint-এর ওপর সে ভরসা করে না। SQL Server এমন constraint-কে “untrusted” চিহ্ন দেয়, আর পরে WITH CHECK CHECK CONSTRAINT দিয়ে আবার যাচাই না করলে চিহ্নটি থেকে যায় — অনেক ডেটাবেসে বছরের পর বছর ধরে এমন ভুলে-যাওয়া untrusted constraint পাওয়া যায়। আরও একটি সূক্ষ্মতা: পুরোনো violator সারিতে অন্য কোনো কলাম UPDATE করলেও নতুন নিয়ম খাটে, তাই সেই UPDATE হঠাৎ ব্যর্থ হতে পারে — অ্যাপ দলকে আগে জানিয়ে রাখা দরকার।

SQLite-এ এমন কিছু নেই; তবে ছোট ডেটাবেসে পুরো rebuild-ই কয়েক সেকেন্ডের, তাই সেখানে আগে শোধরানো, পরে নিয়ম — সহজ পথ।

মূল উপলব্ধি: নতুন নিয়মের দুটি কাজ — নতুন ভুল ঠেকানো আর পুরোনো ভুল খুঁজে পাওয়া। বড় টেবিলে এ দুটিকে আলাদা ধাপে ভাগ করুন, কিন্তু দ্বিতীয় ধাপ শেষ করতে ভুলবেন না।

প্র ০২PostgreSQL 11-এর আগে ADD COLUMN … DEFAULT 0 বড় টেবিলে ঘণ্টা লাগাতে পারত, ১১ থেকে তা প্রায় তাৎক্ষণিক। কী বদলাল? আর কোন DEFAULT এখনো ধীর?

আগের সংস্করণে DEFAULT-সহ কলাম যোগ মানে ছিল: প্রতিটি পুরোনো সারিতে নতুন মানটি আসলেই লিখে দেওয়া, অর্থাৎ পুরো টেবিল নতুন করে লেখা — ACCESS EXCLUSIVE lock ধরে রেখে। কয়েকশো GB-র টেবিলে সেটি ঘণ্টার কাজ, আর পুরো সময় টেবিল অচল। তাই অভিজ্ঞ দলগুলো কৌশল করত: আগে DEFAULT ছাড়া কলাম যোগ (যা শুধু catalog বদলায়, দ্রুত), তারপর ব্যাচে ব্যাচে UPDATE দিয়ে backfill, শেষে DEFAULT বসানো।

PostgreSQL 11 (২০১৮) একটি চতুর পর্যবেক্ষণ কাজে লাগাল: DEFAULT যদি ধ্রুব হয় (যেমন 0 বা 'active'), তাহলে পুরোনো সারিগুলোর জন্য মানটি একবার catalog-এ লিখে রাখলেই চলে। প্রতিটি সারি ডিস্কে লেখা থাকে তার নিজের কলাম-সংখ্যা নিয়ে; কোনো সারি পড়ার সময় যদি দেখা যায় তাতে নতুন কলামটি নেই, তবে catalog থেকে সেই “অনুপস্থিত মান” বসিয়ে দেওয়া হয়। নতুন বা বদলানো সারিতে মানটি আসলেই লেখা হয়। ফলে ADD COLUMN শুধু metadata-র কাজ — টেবিল যত বড়ই হোক।

কিন্তু DEFAULT যদি volatile হয় — প্রতিবার আলাদা মান দেয়, যেমন random() বা clock_timestamp() — তাহলে প্রতিটি পুরোনো সারির মান আলাদা হতে হবে, একটি মান catalog-এ রাখা যায় না। তখন এখনো পুরো table rewrite. (now() একটি transaction-এর ভেতরে স্থির, তাই সেটি দ্রুত পথেই যায়।) MySQL 8.0-ও প্রায় একই সময়ে ALGORITHM=INSTANT দিয়ে কলাম যোগ metadata-তে নামিয়ে আনে, আর পরের সংস্করণগুলোতে এর পরিধি বেড়েছে।

শিক্ষাটি বৃহত্তর: একই SQL স্টেটমেন্টের খরচ DBMS আর সংস্করণভেদে আকাশ-পাতাল। তাই “এই ALTER কি table rewrite করবে?” প্রশ্নটির উত্তর ডকুমেন্টেশনে নিজের সংস্করণ মিলিয়ে দেখতে হয়, আর বড় টেবিলে আগে কপিতে মেপে নিতে হয়।

মূল উপলব্ধি: দ্রুত ALTER-এর রহস্য হলো ডেটা না ছুঁয়ে শুধু metadata বদলানো। যে বদল প্রতিটি সারির আলাদা কাজ দাবি করে, তা কোনো কৌশলেই তাৎক্ষণিক হয় না।

প্র ০৩Online schema change টুলগুলো foreign key-ওয়ালা টেবিলে কেন ঝামেলায় পড়ে? আর trigger-ভিত্তিক বনাম log-ভিত্তিক পদ্ধতির পার্থক্য কী?

মনে করুন ছায়া-টেবিল কৌশল: নতুন টেবিল _orders_new, কপি, তারপর নাম অদলবদল। এখন order_items-এর foreign key আসল orders-এর দিকে আঙুল তুলে আছে। MySQL-এ টেবিলের নাম বদলালে তার দিকে তোলা foreign key-ও সেই টেবিলের সাথেই যায় — অর্থাৎ cut-over-এর পর order_items-এর foreign key পুরোনো টেবিল _orders_old-কে বোঝাবে, নতুন orders-কে নয়। ঠিক এই পর্বের অংশ ৪-এর SQLite ফাঁদের মতো! প্রতিকারে টুলগুলোকে foreign key মুছে আবার বানাতে হয়, যা নিজেই বড় টেবিলে ভারী কাজ, অথবা কিছু সময় যাচাই বন্ধ রাখতে হয়। আর ছায়া-টেবিলে নিজের foreign key থাকলে কপির ক্রম আর CASCADE-এর আচরণ জটিল হয়ে যায়। এজন্য gh-ost-এর মতো টুল foreign key-ওয়ালা টেবিল সমর্থনই করে না, আর pt-online-schema-change বিশেষ বিকল্পসহ সীমিতভাবে করে। এটি বড় MySQL ব্যবহারকারীদের foreign key এড়িয়ে চলার একটি বাস্তব কারণ (পর্ব ৬-এর ভাবনার প্রশ্ন ৩ মনে করুন)।

দ্বিতীয় প্রশ্ন। trigger-ভিত্তিক পদ্ধতিতে আসল টেবিলে INSERT, UPDATE আর DELETE trigger বসানো হয়, যারা প্রতিটি পরিবর্তন সাথে সাথে ছায়া-টেবিলেও করে। সুবিধা: সহজ, একই transaction-এ। অসুবিধা: প্রতিটি অ্যাপ-লেখার খরচ প্রায় দ্বিগুণ, trigger থামানো বা গতি কমানো যায় না, আর ব্যস্ত টেবিলে lock-প্রতিযোগিতা বাড়ে। log-ভিত্তিক পদ্ধতি (gh-ost, ২০১৬ সালে GitHub প্রকাশ করে) trigger ছাড়াই replication log পড়ে পরিবর্তনগুলো নিজের গতিতে ছায়া-টেবিলে প্রয়োগ করে। ফলে আসল টেবিলের ওপর বাড়তি চাপ কম, কাজ থামানো-চালানো যায়, আর সার্ভার ব্যস্ত হলে পিছিয়ে থাকা যায়। দাম: replication log-এর নির্দিষ্ট format লাগে, আর cut-over-এর মুহূর্তটি সামলানো জটিল।

আজ MySQL-এর INSTANT ও INPLACE online DDL অনেক বদল নিজেই করে, তাই এসব টুল লাগে মূলত সত্যিকারের table rewrite-এ।

মূল উপলব্ধি: foreign key টেবিলের পরিচয়ের সাথে বাঁধা, আর ছায়া-টেবিল কৌশল ঠিক সেই পরিচয় অদলবদল করে — তাই দুটির সংঘর্ষ অনিবার্য। কোন কৌশল বেছে নেবেন, তা নির্ভর করে টেবিলের আকার, লেখার চাপ আর foreign key-র ওপর।

প্র ০৪RESTRICT আর NO ACTION প্রায় সব সময় একই ফল দেয়। তাহলে SQL standard দুটি আলাদা নিয়ম রাখল কেন, আর deferrable constraint অনেক DBMS-এ নেই কেন?

পার্থক্যটি বুঝতে প্রশ্ন করুন: “নিয়ম ভাঙা হয়েছে” — এটি কখন বিচার করব? একটি স্টেটমেন্ট অনেক সারি বদলায়, একটি transaction অনেক স্টেটমেন্ট চালায়। মাঝের অবস্থা অস্থায়ী; ব্যবহারকারীর কাছে শুধু শেষের অবস্থাই অর্থপূর্ণ। NO ACTION এই দর্শন মানে: শেষে সব ঠিক থাকলেই হলো, আর constraint deferrable হলে “শেষ” মানে COMMIT. RESTRICT ইচ্ছে করেই কড়া: parent-এর যে সারির দিকে child আঙুল তুলে আছে, তাকে মুহূর্তের জন্যও মোছা বা বদলানো যাবে না। এই কড়াকড়ি কাজে লাগে যখন trigger বা অন্য কোনো প্রক্রিয়া মাঝের অবস্থা দেখে সিদ্ধান্ত নেয় — RESTRICT নিশ্চিত করে অনাথ সারি কখনো, এক মুহূর্তের জন্যও, দেখা যাবে না।

Deferral-এর বাস্তব প্রয়োজন তিনটি: বৃত্তাকার foreign key (বিভাগ ↔ প্রধান), UNIQUE মান অদলবদল (দুটি সিটের নম্বর বা দুটি রোল নম্বর বিনিময়), আর এমন আমদানি যেখানে ফাইলের ক্রম আপনার হাতে নেই। এগুলো ছাড়া কোড লিখতে হয় কৌশল করে — প্রথমে NULL বসিয়ে পরে UPDATE, বা একটি অস্থায়ী মান (−1) দিয়ে অদলবদল।

তবু অনেক DBMS deferral রাখেনি, কারণ খরচ আছে। পিছিয়ে রাখা যাচাইয়ের তালিকা commit পর্যন্ত মনে রাখতে হয়; বড় transaction-এ তালিকাটি বিশাল হতে পারে। ত্রুটি আসে COMMIT-এ, যখন কোন স্টেটমেন্ট দায়ী তা বের করা কঠিন। আর UNIQUE deferrable হলে DBMS সেই index দিয়ে কিছু optimization করতে পারে না — PostgreSQL-এ deferrable UNIQUE-কে foreign key-র parent হিসেবেও ব্যবহার করা যায় না। MySQL-এর InnoDB তাই সব যাচাই সারি ধরে সঙ্গে সঙ্গে করে, আর সেজন্যই সেখানে RESTRICT ও NO ACTION অভিন্ন। SQLite মাঝামাঝি পথ নিয়েছে: শুধু foreign key deferrable, কারণ সেখানেই প্রয়োজনটি সবচেয়ে বেশি।

মূল উপলব্ধি: integrity মানে শেষ অবস্থার সঠিকতা, কিন্তু “শেষ” কোথায় — সারি, স্টেটমেন্ট নাকি transaction — সেটি একটি নকশার সিদ্ধান্ত। RESTRICT আর NO ACTION সেই সিদ্ধান্তের দুটি নাম।

প্র ০৫একটি চালু অ্যাপে customers.phone-এর নাম mobile করতে হবে। এক লাইনের RENAME COLUMN চালালে সমস্যা কী, আর পেশাদার দল কীভাবে করে?

ডেটাবেসের ভেতরে RENAME নিখুঁত — অংশ ৩-এ দেখেছেন, view আর foreign key নিজে থেকে নতুন নাম পায়। সমস্যা ডেটাবেসের বাইরে। একটি চালু সিস্টেমে একাধিক জিনিস একই সাথে এই কলাম পড়ে: ওয়েব সার্ভারের কয়েকটি কপি, মোবাইল অ্যাপের API, রাতের রিপোর্ট-স্ক্রিপ্ট, data analyst-এর dashboard. RENAME চলার মুহূর্তে এরা সবাই যদি একই সাথে নতুন কোডে না যায় — আর বাস্তবে যায় না — তাহলে যার কোডে এখনো phone লেখা, সে column "phone" does not exist পেতে থাকবে। নতুন কোড আগে deploy করলে উল্টো সমস্যা: mobile তখনো নেই।

সমাধান expand–contract, ধাপে ধাপে, প্রতিটি আলাদা migration ফাইলে: (১) mobile কলাম যোগ। (২) অ্যাপের নতুন সংস্করণ দুই কলামেই লেখে, পড়ে phone থেকে। (৩) পুরোনো সারি backfill — ব্যাচে ব্যাচে UPDATE … SET mobile = phone, যাতে বড় lock না হয়। (৪) অ্যাপ এখন পড়ে mobile থেকে। (৫) সব পাঠক নতুন কলামে গেছে নিশ্চিত হলে লেখা বন্ধ phone-এ। (৬) শেষে phone মোছা। প্রতিটি ধাপের পর পিছিয়ে যাওয়া যায়, আর কোনো মুহূর্তে পুরোনো বা নতুন কোনো সংস্করণ ভাঙে না। দাম — একটি কাজ ছয় ধাপে, কয়েক দিনে। অনেক দল তাই সিদ্ধান্ত নেয়, শুধু সৌন্দর্যের জন্য নাম বদলের দরকার নেই। কেউ কেউ মাঝপথে একটি view দিয়ে পুরোনো নামটি কিছুদিন বাঁচিয়ে রাখে।

এই শৃঙ্খলা সম্ভব হয় migration-কে কোড হিসেবে রাখলে: প্রতিটি ধাপ git-এ, review-সহ, test সার্ভারে আগে চলে, আর ডেটাবেস নিজেই মনে রাখে কোনটি চলেছে। Rails-এর মতো framework ২০০০-এর দশকের মাঝামাঝি থেকে এই ধারণা জনপ্রিয় করে; আজ Flyway, Liquibase, Alembic, Laravel — সব জায়গায় একই ধারণা। পর্ব ৫০-এ ORM আর migration-এর সুবিধা ও ফাঁদ বিস্তারিত দেখবেন।

মূল উপলব্ধি: schema আর কোড একসাথে বদলায় না — মাঝে সবসময় একটি সময় থাকে, যখন দুই সংস্করণ পাশাপাশি চলে। নিরাপদ migration মানে সেই সময়ে দুটিকেই চলতে দেওয়া।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে ফল বের করা, দুটি ডিজাইন।

  1. কোয়েরি লিখুন: নতুন নিয়ম — customers-এর phone থাকলে সেটি ঠিক ১১ অঙ্কের আর '01' দিয়ে শুরু হবে। (ক) db-shop-এ violator খোঁজার SELECT লিখুন (উপরের যেকোনো db-shop এডিটরে চালাতে পারবেন) — ফল কী? Tanvir Ahmed (phone NULL) কি তালিকায় আসবেন? (খ) PostgreSQL-এ নিয়মটি নাম দিয়ে, পুরোনো ডেটা পরে যাচাই করার ব্যবস্থাসহ বসানোর ALTER লিখুন।
    SELECT customer_id, name, phone FROM customers
    WHERE NOT (length(phone) = 11 AND phone LIKE '01%');

    (ক) ফল “(কোনো সারি নেই)” — সাতটি নম্বরই ১১ অঙ্কের আর 01 দিয়ে শুরু। Tanvir আসবেন না: phone NULL হলে length(phone) = 11 UNKNOWN, NOT (UNKNOWN) = UNKNOWN, আর WHERE শুধু TRUE সারি রাখে। CHECK-ও তাঁকে আটকাবে না — যা এখানে ঠিক, কারণ phone ঐচ্ছিক।

    ALTER TABLE customers ADD CONSTRAINT customers_phone_chk
      CHECK (length(phone) = 11 AND phone LIKE '01%') NOT VALID;
    -- পুরোনো ডেটা শোধরানো শেষে:
    ALTER TABLE customers VALIDATE CONSTRAINT customers_phone_chk;
  2. ফল আগে বলুন (trace): authors (1, 2); books (10 → author 1, 11 → author 1, 12 → author 2), ON DELETE CASCADE; reviews (r1 → book 10, r2 → book 12), ON DELETE CASCADE; loans (l1 → book 11), ON DELETE RESTRICT. ক্রমানুসারে: (ক) DELETE FROM authors WHERE id = 2; (খ) DELETE FROM authors WHERE id = 1; প্রতিটির পরে কোন টেবিলে কী থাকে?

    (ক) author 2 মোছে → CASCADE-এ book 12 → CASCADE-এ review r2. মোট ৩টি সারি গেল (SQLite বলবে “১টি সারি বদলেছে”)। এখন authors: 1; books: 10, 11; reviews: r1; loans: l1.

    (খ) author 1 মুছলে CASCADE-এ book 10 আর 11 মোছার কথা; book 10-এর review r1-ও যেত। কিন্তু book 11-এর দিকে loans-এর l1 আঙুল তুলে আছে, আর সেখানে RESTRICT. তাই পুরো DELETE বাতিল — FOREIGN KEY constraint failed, এমনকি book 10 আর r1-ও অক্ষত। একটি স্টেটমেন্ট হয় পুরোটা হয়, নয়তো একটুও না: শিকলের যেকোনো জায়গায় একটি RESTRICT পুরো ঢেউ থামিয়ে দেয়।

  3. ডিজাইন করুন: বিশ্ববিদ্যালয়ের ডেটাবেসে (db-uni) এই সম্পর্কগুলোর ON DELETE নিয়ম ঠিক করুন, প্রতিটির পক্ষে এক লাইনের যুক্তিসহ: enrollments → students, enrollments → courses, students → departments, courses → departments, instructors.mentor_id → instructors.
    • enrollments → students: RESTRICT — enrollment-এ গ্রেড আছে, যা transcript-এর আইনি রেকর্ড। শিক্ষার্থী চলে গেলেও রেকর্ড থাকে; status কলাম দিয়ে “প্রাক্তন” চিহ্ন।
    • enrollments → courses: RESTRICT — একই কারণে; বন্ধ কোর্স “আর অফার হয় না” চিহ্ন পায়, মোছে না।
    • students → departments: RESTRICT (বা SET NULL, যদি নিয়মে বিভাগহীন শিক্ষার্থী চলে) — বিভাগ উঠে গেলে শিক্ষার্থীদের আগে অন্য বিভাগে সরাতে হবে, এটি সচেতন সিদ্ধান্ত।
    • courses → departments: RESTRICT — বিভাগ মুছলে কোর্সগুলো cascade-এ গেলে তাদের enrollment-ও আটকাবে বা হারাবে।
    • instructors.mentor_id → instructors: SET NULL — mentor চলে গেলে শিক্ষক থাকেন; mentor ঘর ফাঁকা। CASCADE হলে একজনের বিদায়ে তাঁর সব mentee-ও মুছে যেতেন!
  4. পরিকল্পনা করুন: একটি চালু PostgreSQL ডেটাবেসে orders টেবিলে (কয়েক কোটি সারি) একটি নতুন কলাম delivery_fee যোগ করতে হবে, যা NOT NULL, আর পুরোনো সব অর্ডারের জন্য 0 সঠিক মান। কোন স্টেটমেন্ট(গুলো) চালাবেন, আর কোন সতর্কতা নেবেন? মান যদি পুরোনো প্রতিটি অর্ডারের জন্য আলাদা হিসাব করতে হতো (ঠিকানা অনুযায়ী), তাহলে কী বদলাত?

    PostgreSQL 11 বা পরের সংস্করণে একটি স্টেটমেন্টই যথেষ্ট: ALTER TABLE orders ADD COLUMN delivery_fee numeric(10,2) NOT NULL DEFAULT 0; — DEFAULT ধ্রুব, তাই table rewrite নেই, কাজটি শুধু catalog-এ। সতর্কতা: আগে SET lock_timeout = '2s', যাতে দীর্ঘ কোয়েরির পেছনে আটকে অ্যাপের সব কোয়েরি লাইনে না দাঁড়ায়; ব্যর্থ হলে ব্যস্ততা কম এমন সময়ে আবার চেষ্টা। কাজটি একটি migration ফাইলে, আগে production-এর কপিতে পরীক্ষা করে।

    মান আলাদা হলে: (১) NULL-যোগ্য কলাম যোগ; (২) ব্যাচে ব্যাচে (ধরুন ১০ হাজার সারি করে) UPDATE দিয়ে backfill, প্রতিটি ব্যাচ আলাদা transaction; (৩) নতুন অর্ডারে অ্যাপ নিজেই মান লেখে; (৪) শেষে NOT NULL — বড় টেবিলে আগে CHECK (delivery_fee IS NOT NULL) NOT VALID → VALIDATE, তারপর SET NOT NULL (PostgreSQL 12 থেকে যাচাই-করা এমন CHECK থাকলে পুরো স্ক্যান লাগে না), শেষে CHECK-টি মোছা।

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

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

ক্রমSQLite-এ একটি টেবিলে নতুন CHECK বসানোর rebuild-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. PRAGMA foreign_keys = OFF, তারপর BEGIN
  2. নতুন নকশায় CREATE TABLE customers_new
  3. INSERT INTO customers_new SELECT … FROM customers
  4. DROP TABLE customers
  5. ALTER TABLE customers_new RENAME TO customers
  6. PRAGMA foreign_key_check, COMMIT, তারপর foreign_keys = ON

৮টি সারি আছে এমন একটি SQLite টেবিলে ALTER TABLE customers ADD COLUMN district TEXT NOT NULL; চালালে কী হবে?

  • কলাম যোগ হবে, পুরোনো ৮টি সারিতে NULL বসবে
  • ত্রুটি দিয়ে বাতিল হবে, কারণ পুরোনো সারিগুলোর জন্য কোনো DEFAULT নেই
  • কলাম যোগ হবে, পুরোনো সারিতে ফাঁকা লেখা '' বসবে
  • SQLite-এ ADD COLUMN নেই, তাই syntax error

একটি foreign key DEFERRABLE INITIALLY DEFERRED. একটি transaction-এ parent সারি মুছে একই key-তে নতুন parent সারি ঢোকানো হলো, তারপর COMMIT. ON DELETE RESTRICT আর ON DELETE NO ACTION-এ ফল কী?

  • দুটিতেই সফল
  • দুটিতেই ব্যর্থ
  • RESTRICT-এ DELETE-এই ব্যর্থ, NO ACTION-এ সফল
  • RESTRICT-এ সফল, NO ACTION-এ ব্যর্থ

products টেবিলে CHECK (price > 0) বসানোর আগে নিয়মভঙ্গকারী সারি খুঁজতে চান — অর্থাৎ ঠিক সেই সারিগুলো, যেগুলোর জন্য constraint যোগ ব্যর্থ হবে। কোন শর্তটি সঠিক?

  • WHERE NOT (price > 0)
  • WHERE price > 0
  • WHERE price IS NULL
  • WHERE price <= 0 OR price IS NULL

order_items (অর্ডারের প্রতিটি পণ্য-লাইন) টেবিলের order_id → orders foreign key-এর জন্য সাধারণত কোন ON DELETE নিয়মটি সবচেয়ে মানানসই?

  • SET NULL
  • SET DEFAULT
  • RESTRICT
  • CASCADE

SQLite-এ একটি বিদ্যমান টেবিলের কলামে নতুন CHECK constraint যোগ করার অফিশিয়াল উপায় কোনটি?

  • ALTER TABLE … ADD CONSTRAINT … CHECK (…)
  • নতুন নকশায় টেবিল বানিয়ে ডেটা কপি, পুরোনোটি মুছে নতুনটির নাম বদল — একটি transaction-এ
  • ALTER TABLE … ALTER COLUMN … ADD CHECK (…)
  • sqlite_schema টেবিলে CREATE লেখাটি সরাসরি UPDATE করা

departments.head_id → employees আর employees.dept_id → departments — দুটি foreign key-ই NOT NULL. প্রথম বিভাগ আর তার প্রধানকে ঢোকানোর সঠিক উপায় কোনটি?

  • foreign key দুটি DEFERRABLE INITIALLY DEFERRED করে, দুটি INSERT একটি transaction-এ
  • যেকোনো একটি আগে ঢুকিয়ে অন্যটি পরে, আলাদা স্টেটমেন্টে, deferral ছাড়া
  • head_id-তে আপাতত NULL দিয়ে পরে UPDATE
  • foreign key দুটি মুছে ফেলা, কারণ বৃত্তাকার সম্পর্ক অসম্ভব

শব্দকোষ

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

মনে রাখুন

  1. ALTER TABLE ডেটা না হারিয়ে schema বদলায়; নতুন NOT NULL কলামে পুরোনো সারির জন্য DEFAULT লাগে, আর DEFAULT সত্য হতে হবে।
  2. RENAME ডেটাবেসের ভেতরের view ও foreign key-তে নতুন নাম পৌঁছায়, কিন্তু অ্যাপের কোডে নয়; DROP COLUMN ফেরানো যায় না।
  3. SQLite-এর ALTER শুধু চারটি কাজ পারে; বাকি সব ১২ ধাপের rebuild — new_X বানানো, কপি, X মোছা, new_X-এর নাম X, foreign_key_check.
  4. নতুন constraint-এর আগে WHERE NOT (শর্ত) দিয়ে violator খুঁজুন; constraint-কে নিজে নাম দিন।
  5. RESTRICT সঙ্গে সঙ্গে, NO ACTION শেষে যাচাই করে; CASCADE শিকলে ছড়ায় — হিসাবের রেকর্ডে RESTRICT, অংশ-সম্পর্কে CASCADE, লেবেলে SET NULL.

পরের পর্বে: মডিউল ৩ শেষ — আপনি এখন টেবিল বানাতে, ডেটা বদলাতে, জটিল কোয়েরি লিখতে আর schema নিরাপদে বদলাতে পারেন। পর্ব ২৪-এ শুরু উন্নত SQL: view আর materialized view — একটি কোয়েরিকে নাম দিয়ে টেবিলের মতো ব্যবহার, আর নাম বদলের সময় পুরোনো নাম বাঁচিয়ে রাখার কৌশলও।

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

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