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

Key আর integrity constraint — super, candidate, primary আর foreign

Keys and Integrity Constraints — Superkey, Candidate, Primary & Foreign Keys, Referential Integrity

Taught in Bangla, this lesson explains why keys exist, then covers superkeys and candidate keys, primary, alternate and composite keys, natural versus surrogate keys such as NID or auto ids, foreign keys and referential integrity, the full family of integrity constraints, and what happens when a rule is broken: rejection and referential actions like CASCADE, all tried in live SQL.

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

  • Superkey, candidate key, primary key, alternate key আর composite key — সংজ্ঞা, পার্থক্য আর হাতে-কলমে খুঁজে বের করা
  • Natural key (NID, ফোন নম্বর) বনাম surrogate key (স্বয়ংক্রিয় id) — কখন কোনটি, আর কেন
  • Foreign key আর referential integrity — টেবিলগুলো কীভাবে একে অপরকে “বিশ্বাস” করে
  • সব ধরনের integrity constraint — domain, entity, referential, UNIQUE, NOT NULL, CHECK আর ব্যবসার নিয়ম
  • নিয়ম ভাঙলে কী হয়, আর ON DELETE / ON UPDATE-এর CASCADE, SET NULL, RESTRICT, NO ACTION

১ · Key কেন লাগে — ঠিক একজনকে চেনা

ধরুন একটি মুদি দোকানের বাকির খাতা। দোকানদার লিখে রাখেন: “রহিম — ৫৬০ টাকা”। কিছুদিন পর দেখা গেল পাড়ায় রহিম নামে তিনজন। কোন রহিমের কাছে টাকা পাওনা? খাতা আর বলতে পারছে না। দোকানদার তখন নামের পাশে লিখতে শুরু করলেন “রহিম (মসজিদের পাশের বাসা)”। এটিই key-এর আদি রূপ — এমন একটি তথ্য, যা দিয়ে একজন মানুষকে ঠিক একজন হিসেবে চেনা যায়।

আমাদের চারপাশে এমন “চেনার নম্বর” অনেক। জাতীয় পরিচয়পত্রের NID নম্বর একজন নাগরিককে চেনায়। বিশ্ববিদ্যালয়ের রোল বা student ID একজন শিক্ষার্থীকে চেনায়। bKash-এ টাকা পাঠাতে আপনি নাম লেখেন না, লেখেন মোবাইল নম্বর — কারণ নম্বরটি একটি নির্দিষ্ট অ্যাকাউন্টকেই বোঝায়। ট্রেনের টিকিটে থাকে PNR বা টিকিট নম্বর, আর লাইব্রেরির বইয়ে থাকে accession নম্বর।

পর্ব ৫-এ দেখেছেন, relational model-এ ডেটা থাকে relation বা টেবিলে, প্রতিটি সারি একটি tuple, আর প্রতিটি কলাম একটি attribute. Relation গণিতের অর্থে একটি সেট, তাই তাত্ত্বিকভাবে দুটি হুবহু একই সারি থাকতে পারে না। কিন্তু শুধু “হুবহু একই নয়” যথেষ্ট নয়। আমাদের দরকার এমন একটি ছোট অংশ, যার মান দেখেই বলা যায় কোন সারির কথা হচ্ছে। সেই অংশটিই key.

বিশ্ববিদ্যালয়ের ভর্তি রেজিস্টারের কথা ভাবুন। পরীক্ষার হলে শিক্ষক নাম ডাকেন না, ডাকেন রোল নম্বর। কারণ “নুসরাত” নামে পাঁচজন থাকতে পারেন, কিন্তু রোল 2201 একজনই। ফল প্রকাশ, ফি জমা, লাইব্রেরি কার্ড — সব কাজ ওই একটি নম্বর ধরে হয়। ডেটাবেসে key ঠিক এই কাজটিই করে: “কোন সারি?” প্রশ্নের নিশ্চিত উত্তর।

Key না থাকলে কী কী গোলমাল হয়, তা তালিকা করে দেখা যাক:

  • Update কাকে ছোঁবে, জানা যায় না। “রহিমের ঠিকানা বদলান” বললে তিন রহিমেরই ঠিকানা বদলে যেতে পারে।
  • একই মানুষ দুবার ঢুকে পড়ে। একজন গ্রাহক দুবার নিবন্ধন করলে দুটি অ্যাকাউন্ট, দুটি বাকির হিসাব — কোনটি সঠিক, কেউ জানে না।
  • অন্য টেবিল থেকে আঙুল তোলা যায় না। একটি অর্ডার “কার”, সেটি লিখতে হলে গ্রাহককে নিশ্চিতভাবে চেনাতে হবে। এই আঙুল তোলাই foreign key, যা অংশ ৫-এ দেখবেন।

এই পর্বের সব লাইভ SQL চলে একটি ছোট দোকানের ডেটাবেসে (db-shop). এতে পাঁচটি টেবিল: customers (customer_id, name, city, phone, joined_on — ৮ জন গ্রাহক), categories (৪টি পণ্য-শ্রেণি), 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 — কোন অর্ডারে কোন পণ্য কতটি)। প্রতিটি এডিটর নিচের “নমুনা ডেটা” বাক্সে পুরো টেবিলগুলো দেখায়। প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়, তাই নির্ভয়ে ভাঙচুর করুন।

২ · Superkey আর candidate key — কোন কলামগুলো সারি চেনায়

এবার সংজ্ঞাগুলো নির্দিষ্ট করি। ধরুন একটি relation schema R, যার attribute-গুলোর সেট হলো {A₁, A₂, …, Aₙ}. R-এর যেকোনো বৈধ অবস্থায় (অর্থাৎ ব্যবসার নিয়ম মেনে যত রকম ডেটা আসতে পারে, তার প্রতিটিতে) কোনো দুটি আলাদা সারির মান যদি একটি attribute-সেট K-তে কখনো হুবহু না মেলে, তাহলে K একটি superkey.

সংজ্ঞা

Superkey: K ⊆ R এমন, যাতে R-এর যেকোনো বৈধ instance r-এ, যেকোনো দুটি সারি t₁ ≠ t₂ হলে t₁[K] ≠ t₂[K].

Candidate key: এমন superkey K, যার কোনো প্রকৃত উপসেট (K থেকে অন্তত একটি attribute বাদ দিলে যা থাকে) superkey নয়। অর্থাৎ minimal superkey — একটি attribute-ও বাড়তি নেই।

দুটি শব্দ খেয়াল করুন। প্রথমত “যেকোনো বৈধ instance” — key হলো schema-র ধর্ম, আজকের ডেটার নয়। দ্বিতীয়ত “minimal” মানে সবচেয়ে কম সংখ্যক attribute নয়, বরং “এর থেকে কিছু বাদ দেওয়া যায় না”। একটি টেবিলের একটি candidate key-তে ১টি attribute আর অন্যটিতে ৩টি থাকতে পারে, দুটিই minimal. নিচের ডায়াগ্রামে একটি ছোট টেবিলে একটির পর একটি attribute-সেট যাচাই করে দেখুন:

students (ছোট নমুনা)সবুজ = এই ধাপে যে কলাম দেখছি

            
যাচাইয়ের ফল
যে সেট দেখছি
সব সারিতে মান আলাদা?
superkey?
candidate key?
  1. টেবিলটি দেখুন। চারজন শিক্ষার্থী, চারটি কলাম: roll, name, phone, dept. নিয়ম হলো: রোল নম্বর বিশ্ববিদ্যালয় একবারই দেয়, আর একটি ফোন নম্বর একজনের নামেই নিবন্ধিত। প্রশ্ন: কোন কোন কলামের সেট দিয়ে প্রতিটি সারিকে নিশ্চিতভাবে আলাদা করা যায়?
  2. {name} — না। roll 101 আর 103 দুজনের নামই Rahim. শুধু নাম দিয়ে “Rahim-এর ফোন নম্বর দিন” বললে কোন Rahim, তা বলা যায় না। তাই {name} superkey নয়।
  3. {dept} — না। CSE দুবার (101, 104), EEE দুবার (102, 103). একটি বিভাগে অনেক শিক্ষার্থী থাকবেই, তাই এটি কখনোই key হতে পারে না।
  4. {name, dept} — ফাঁদ। চারটি জোড়া (Rahim, CSE), (Karima, EEE), (Rahim, EEE), (Nusrat, CSE) — এই ডেটায় সবই আলাদা! কিন্তু কাল CSE-তে আরেকজন Rahim ভর্তি হতেই পারেন। Key ঠিক হয় নিয়ম দিয়ে, আজকের ডেটা দিয়ে নয়। তাই এটি superkey নয়।
  5. {roll} — candidate key. সব রোল আলাদা, আর নিয়মেও রোল কখনো দুবার দেওয়া হয় না। এটি একটিমাত্র attribute, এর থেকে কিছু বাদ দেওয়ার উপায় নেই — তাই এটি minimal, অর্থাৎ candidate key.
  6. {roll, name} — superkey, কিন্তু candidate নয়। roll আছে বলে সব সারি আলাদা হবেই, তাই এটি superkey. কিন্তু name বাদ দিলেও {roll} একাই কাজ করে — name এখানে বাড়তি। বাড়তি কিছু থাকলে সেটি minimal নয়।
  7. {phone} — আরেকটি candidate key. নিয়ম অনুযায়ী এক নম্বর এক জনের, তাই {phone}-ও minimal superkey. এখন দুটি candidate key: {roll} আর {phone}. একটিকে primary key বানানো হবে, অন্যটি হবে alternate key — কোনটি কেন, সেটি অংশ ৩-এ দেখবেন।

দুটি সহজ কিন্তু জরুরি সত্য

সত্য ১ — superkey-র যেকোনো বড় সেটও superkey. K superkey হলে আর K ⊆ K′ হলে, দুটি সারি K′-তে মিলতে হলে আগে K-তে মিলতে হবে — যা অসম্ভব। তাই K′-ও superkey. এজন্যই {roll} superkey হওয়ায় {roll, name}, {roll, dept}, {roll, name, phone, dept} সবই superkey.

সত্য ২ — প্রতিটি relation-এর অন্তত একটি candidate key আছে. প্রমাণ: relation একটি সেট, তাই দুটি সারি সব attribute-এ মিলতে পারে না — অর্থাৎ সব attribute-এর সেট R নিজেই একটি superkey. এখন R থেকে একটি একটি করে attribute বাদ দিতে থাকুন, যতক্ষণ বাকি অংশ superkey থাকে। Attribute সংখ্যা সসীম, তাই এই প্রক্রিয়া কোথাও থামবেই, আর যেখানে থামে সেখানে আর কিছু বাদ দেওয়া যায় না — সেটিই একটি minimal superkey, অর্থাৎ candidate key. ∎

এই দুই সত্য থেকে superkey গোনার একটি সুন্দর কৌশল আসে। ধরুন R-এর ৪টি attribute A, B, C, D আর একমাত্র candidate key {A}. তাহলে superkey হলো ঠিক সেই সেটগুলো যাতে A আছে। বাকি তিনটির প্রতিটি থাকতেও পারে, না-ও পারে — তাই 2³ = ৮টি superkey. সাধারণভাবে n টি attribute আর একটিমাত্র একক-attribute candidate key থাকলে superkey 2n−1টি। একাধিক candidate key থাকলে inclusion–exclusion লাগে — অনুশীলন ১-এ করবেন।

ডেটা key প্রমাণ করতে পারে না, শুধু খণ্ডন করতে পারে। একটি কলামে আজ কোনো duplicate নেই — এর মানে শুধু “এই মুহূর্তে আলাদা”। ধাপ ৪-এর {name, dept}-এর মতো কাল একটি নতুন সারি এসেই সেটি ভেঙে দিতে পারে। আর একটিও duplicate পেলে নিশ্চিত — সেটি key নয়। Key ঠিক করতে হয় ব্যবসার নিয়ম জেনে: “বিশ্ববিদ্যালয় কি কখনো একই রোল দুজনকে দেয়?”

SQL দিয়ে duplicate খোঁজা — candidate key-র প্রাথমিক পরীক্ষা

তবু বাস্তবে একটি পুরোনো টেবিল হাতে পেলে প্রথম কাজ হলো দেখা, কোন কলাম বা কলামের জোড়ায় duplicate আছে। এর জন্য GROUP BY দিয়ে একই মানের সারিগুলো এক দলে আনুন, তারপর HAVING COUNT(*) > 1 দিয়ে শুধু সেই দলগুলো রাখুন যেখানে একাধিক সারি। (GROUP BY আর HAVING বিস্তারিত পর্ব ১৮-এ; এখানে শুধু এই একটি ব্যবহার দেখুন।) কোনো সারি না এলে এই ডেটায় duplicate নেই।

-- কোন কলাম (বা কলামের জোড়া) এই ডেটায় duplicate-মুক্ত?
SELECT city, COUNT(*) AS n
FROM customers GROUP BY city HAVING COUNT(*) > 1;

SELECT name, COUNT(*) AS n
FROM customers GROUP BY name HAVING COUNT(*) > 1;

SELECT order_id, COUNT(*) AS n
FROM order_items GROUP BY order_id HAVING COUNT(*) > 1;

SELECT order_id, product_id, COUNT(*) AS n
FROM order_items GROUP BY order_id, product_id HAVING COUNT(*) > 1;

চারটি প্রশ্নের চারটি ফল। প্রথমটিতে একটি সারি: Dhaka | 3 — তিনজন গ্রাহক ঢাকার, তাই city কখনোই key নয়। দ্বিতীয়টিতে “(কোনো সারি নেই)” — এই ৮ জনের নাম সব আলাদা, কিন্তু তা থেকে name-কে key বলা যাবে না; সারা দেশে একই নামের মানুষ অগণিত। তৃতীয়টিতে ৬টি সারি — 1001, 1002, 1003, 1005, 1007 আর 1009, প্রতিটির পাশে n = 2. একটি অর্ডারে একাধিক পণ্য থাকে, তাই order_items-এ order_id একা key নয়। চতুর্থটিতে “(কোনো সারি নেই)” — (order_id, product_id) জোড়া এই ডেটায় অনন্য, আর নিয়মেও তাই হওয়ার কথা (একই অর্ডারে একই পণ্য দুই লাইনে না লিখে quantity বাড়ানো হয়)। এই জোড়াটিই order_items-এর আসল key — অংশ ৩-এ দেখবেন।

NULL-এর বেলায় সাবধান: GROUP BY সব NULL-কে এক দলে রাখে। customers-এ Imran Hossain-এর city NULL; সেখানে মাত্র একজন বলে উপরের ফলে আসেনি। কিন্তু key খোঁজার সময় NULL থাকা কলাম এমনিতেই সন্দেহজনক — primary key-তে NULL চলে না (অংশ ৬)। NULL-এর পুরো যুক্তি পর্ব ১৬-এ।

৩ · Primary key, alternate key আর composite key

একটি টেবিলে একাধিক candidate key থাকতে পারে — students-এর {roll} আর {phone}, বা customers-এর {customer_id} আর {phone}. ডিজাইনার এদের মধ্যে একটিকে বেছে নেন primary key হিসেবে: এটিই হবে সারির “অফিসিয়াল ঠিকানা”, অন্য টেবিল এটি দিয়েই এই সারির দিকে আঙুল তুলবে। বাকি candidate key-গুলো হয় alternate key (বা secondary key); SQL-এ এদের ওপর UNIQUE (আর সাধারণত NOT NULL) বসিয়ে অনন্যতা রক্ষা করা হয়।

ভালো primary key-র তিন গুণ

স্থির (stable)

মান কখনো বদলাবে না। Primary key বদলালে যত টেবিল এটির দিকে আঙুল তুলেছে, সবখানে বদলাতে হয়।

রোল নম্বর: ভালো · ফোন নম্বর: ঝুঁকিপূর্ণ

ছোট (short)

প্রতিটি foreign key আর প্রতিটি index-এ এটি কপি হয়। ৪ বা ৮ বাইটের পূর্ণসংখ্যা লম্বা লেখার চেয়ে তুলনা আর জমা রাখায় সস্তা।

INTEGER · BIGINT

কখনো NULL নয়

যে সারির primary key নেই, সেটিকে চেনার উপায় নেই। তাই প্রতিটি সারিতে, প্রথম দিন থেকেই, মান থাকতে হবে।

entity integrity

এই তিন গুণে customers-এর customer_id জেতে: পূর্ণসংখ্যা, দোকান নিজে দেয়, কখনো বদলায় না, প্রত্যেক গ্রাহকের প্রথম দিন থেকেই থাকে। phone হারে — Tanvir Ahmed-এর ফোন নম্বর এখনো জানা নেই (NULL), আর নম্বর বদলাতেও পারে। তবু phone-এ UNIQUE আছে, কারণ দুজন গ্রাহকের একই নম্বর থাকা উচিত নয়। এখন দেখুন DBMS কীভাবে primary key রক্ষা করে — customer_id 3 আগেই Sabbir Hasan-এর:

-- নতুন গ্রাহক, নতুন id: ঢুকবে
INSERT INTO customers VALUES (9, 'Sadia Islam', 'Barishal', '01711000009', '2025-10-20');

-- id 3 আগেই Sabbir Hasan-এর: বাতিল হবে
INSERT INTO customers VALUES (3, 'Rakib Hasan', 'Cumilla', '01711000010', '2025-10-21');

প্রথম INSERT-এর পর আসে “✓ INSERT — ১টি সারি বদলেছে”। দ্বিতীয়টিতে ত্রুটি: UNIQUE constraint failed: customers.customer_id. SQLite primary key-র লঙ্ঘনকেও “UNIQUE constraint” বলে জানায়, কারণ primary key আসলে অনন্যতারই একটি নিয়ম (INTEGER PRIMARY KEY-এ সেটি সারির rowid, অন্য primary key-তে একটি অনন্য index). Rakib-এর সারি ঢোকেনি, Sabbir-এর সারিও অক্ষত — DBMS নিজের থেকে কারও তথ্য মুছে বা বদলে দেয় না, শুধু নতুন অনুরোধটি ফিরিয়ে দেয়। PostgreSQL-এ একই ভুলে বার্তা হয় duplicate key value violates unique constraint "customers_pkey", MySQL 8-এ Duplicate entry '3' for key 'customers.PRIMARY' (পুরোনো সংস্করণে শুধু 'PRIMARY').

Alternate key-ও একইভাবে রক্ষা পায়, তবে একটি পার্থক্য আছে: UNIQUE কলামে NULL থাকতে পারে, আর বেশিরভাগ DBMS-এ একাধিক NULL একে অপরের “সমান” ধরা হয় না। নিচে Sadia-র ফোন নম্বর জানা নেই, Tanvir-এরও নেই — দুটি NULL পাশাপাশি থাকতে পারে কি না, আর Rahim Uddin-এর নম্বর দিয়ে নতুন গ্রাহক খোলা যায় কি না, দেখুন:

-- ফোন নম্বর অজানা (NULL): UNIQUE কলামে আরেকটি NULL চলে
INSERT INTO customers VALUES (9, 'Sadia Islam', 'Barishal', NULL, '2025-10-20');
SELECT customer_id, name, phone FROM customers WHERE phone IS NULL;

-- কিন্তু 01711000001 আগেই Rahim Uddin-এর
INSERT INTO customers VALUES (10, 'Rakib Hasan', 'Cumilla', '01711000001', '2025-10-21');

SELECT-এর ফলে দুটি সারি: 5 | Tanvir Ahmed | NULL আর 9 | Sadia Islam | NULL. দুটি NULL একসাথে চলেছে, কারণ SQL-এর চোখে NULL মানে “অজানা”, আর দুটি অজানা মান সমান কি না বলা যায় না — তাই একে duplicate ধরা হয় না। তৃতীয় স্টেটমেন্টে ত্রুটি: UNIQUE constraint failed: customers.phone. একটি সতর্কতা: SQLite, PostgreSQL, MySQL আর Oracle একাধিক NULL মেনে নেয়, কিন্তু SQL Server-এর UNIQUE constraint একটির বেশি NULL মানে না। PostgreSQL 15 থেকে চাইলে UNIQUE NULLS NOT DISTINCT লিখে SQL Server-এর মতো আচরণ পাওয়া যায়।

Composite key — যখন একটি কলামে হয় না

কখনো কোনো একক কলামই সারি চেনাতে পারে না। order_items-এ order_id একা পারে না (একটি অর্ডারে অনেক পণ্য), product_id একা পারে না (একটি পণ্য অনেক অর্ডারে)। কিন্তু (order_id, product_id) জোড়া পারে: “অর্ডার 1001-এর Headphone লাইন” একটিই। একাধিক attribute মিলে তৈরি key-কে বলে composite key. SQL-এ এটি কলামের পাশে নয়, টেবিলের শেষে আলাদা করে লিখতে হয়: PRIMARY KEY (order_id, product_id).

বিশ্ববিদ্যালয়ের ডেটাবেসে (db-uni, পরের পর্বগুলোতে ব্যবহার হবে) enrollments টেবিলের key আরও বড়: (student_id, course_id, term). শুধু (student_id, course_id) হলে একজন শিক্ষার্থী F পেয়ে পরের term-এ একই কোর্স আবার নিতে পারতেন না — অথচ নিয়ম তা অনুমতি দেয়। এখানে ব্যবসার নিয়মই key-র আকার ঠিক করে দিচ্ছে।

-- অর্ডার 1006-এ এখন শুধু Smart Watch (110) আছে
-- একই অর্ডার, নতুন পণ্য: জোড়া (1006, 102) নতুন — ঢুকবে
INSERT INTO order_items VALUES (1006, 102, 1, 1200);

-- একই অর্ডার, একই পণ্য: জোড়া (1006, 110) আগেই আছে — বাতিল
INSERT INTO order_items VALUES (1006, 110, 2, 3500);

প্রথমটি “✓ INSERT — ১টি সারি বদলেছে”: order_id 1006 আগেও ছিল, product_id 102-ও অন্য অর্ডারে ছিল, কিন্তু জোড়াটি নতুন। দ্বিতীয়টিতে UNIQUE constraint failed: order_items.order_id, order_items.product_id — বার্তাতেই দুটি কলামের নাম, কারণ লঙ্ঘন হয়েছে জোড়ার। Composite key-তে প্রতিটি কলাম আলাদাভাবে বারবার আসতে পারে; শুধু পুরো সমন্বয়টি অনন্য হতে হয়।

Composite key-র কলামগুলোর ক্রম তত্ত্বে গুরুত্বহীন — {order_id, product_id} আর {product_id, order_id} একই সেট। কিন্তু বাস্তবে DBMS key-র জন্য যে index বানায়, সেখানে ক্রম গুরুত্বপূর্ণ: (order_id, product_id) index “অর্ডার 1001-এর সব লাইন” খুঁজতে দ্রুত, কিন্তু “পণ্য 102 কোন কোন অর্ডারে” খুঁজতে নয়। এটি পর্ব ৪১-এর composite index-এর বিষয়।

৪ · Natural key বনাম surrogate key — NID, ফোন নম্বর নাকি স্বয়ংক্রিয় id

Primary key বাছাইয়ের সবচেয়ে পুরোনো বিতর্ক এটি। Natural key হলো বাস্তব জগতে আগে থেকেই থাকা একটি পরিচয় — NID নম্বর, ফোন নম্বর, বইয়ের ISBN, গাড়ির রেজিস্ট্রেশন নম্বর। Surrogate key হলো ডেটাবেস নিজে যে নম্বর বানায়, যার বাস্তব কোনো অর্থ নেই — 1, 2, 3… বা একটি এলোমেলো UUID. আমাদের customers.customer_id একটি surrogate key; দোকানের বাইরে এর কোনো মানে নেই।

বিষয়Natural key (NID, phone, ISBN)Surrogate key (স্বয়ংক্রিয় id)
অর্থবাস্তবে অর্থ আছে, মানুষ চেনেঅর্থহীন, শুধু ডেটাবেসের ভেতরে
স্থিরতাবাইরের নিয়মে বদলাতে পারেএকবার দেওয়া হলে কখনো বদলায় না
প্রথম দিনেই জানা?সবসময় নয় (গ্রাহক ফোন নম্বর দেননি)হ্যাঁ, সারি ঢোকার মুহূর্তেই
আকারপ্রায়ই লম্বা লেখা (১০–১৭ অঙ্ক)৪ বা ৮ বাইটের পূর্ণসংখ্যা
duplicate ঠেকানোনিজেই ঠেকায়ঠেকায় না — আলাদা UNIQUE লাগে
গোপনীয়তাURL বা log-এ গেলে ব্যক্তিগত তথ্য ফাঁসকম ঝুঁকি (তবে ক্রমিক id অনুমান করা যায়)

ফোন নম্বর কেন খারাপ primary key

bKash বা Nagad অ্যাকাউন্টের কথা ভাবলে মনে হয় ফোন নম্বরই তো পরিচয়। কিন্তু ডেটাবেসের ভেতরে নম্বরটিকে primary key বানালে তিনটি সমস্যা আসে:

  • মানুষ নম্বর বদলান। হারানো সিম, নতুন চাকরির অফিস-নম্বর, বিদেশ যাওয়া। নম্বর primary key হলে অর্ডার, পেমেন্ট, রিভিউ — যত টেবিলে সেটি foreign key হিসেবে কপি হয়েছে, সবখানে বদলাতে হবে।
  • নম্বর হাতবদল হয়। মোবাইল অপারেটর দীর্ঘদিন বন্ধ থাকা নম্বর একসময় নতুন গ্রাহককে দিতে পারে। তখন পুরোনো মালিকের ইতিহাস নতুন মালিকের ঘাড়ে চাপার ঝুঁকি।
  • নম্বরের “অর্থ” বদলায়। বাংলাদেশে ২০১৮ সাল থেকে নম্বর অক্ষুণ্ণ রেখে অপারেটর বদলানো যায় (MNP). ফলে 017 মানেই একটি নির্দিষ্ট অপারেটর — এই পুরোনো ধারণার ওপর ভিত্তি করে লেখা কোড ভুল হয়ে যায়।

NID-ও পুরোপুরি নিরাপদ নয়: পুরোনো আর নতুন (স্মার্ট) কার্ডে নম্বরের দৈর্ঘ্য আলাদা হতে পারে, শিশুদের NID নেই, বিদেশি গ্রাহকের নেই, আর NID নম্বর URL-এ ঘুরে বেড়ানো গোপনীয়তার জন্য বিপজ্জনক। বইয়ের ISBN একটি ভালো শিক্ষা: ২০০৭ সালের ১ জানুয়ারি থেকে ISBN ১০ অঙ্ক থেকে ১৩ অঙ্কে বদলে যায়। যেসব লাইব্রেরি-সফটওয়্যারে ISBN ছিল primary key আর কলামটি ছিল ১০ অক্ষরের, তাদের সবাইকে schema বদলাতে হয়েছিল।

প্রচলিত পেশাদার সমাধান: surrogate primary key + natural key-তে UNIQUE
customer_id INTEGER PRIMARY KEY, phone TEXT UNIQUE

এভাবে দুই দিকের সুবিধাই মেলে: ভেতরের সম্পর্কগুলো স্থির, ছোট id দিয়ে জোড়া, আর বাইরের পরিচয়ের duplicate ঠেকায় UNIQUE. ফোন নম্বর বদলালে শুধু customers-এর একটি ঘর বদলায়। তবে মনে রাখুন, surrogate key একা duplicate ঠেকায় না — UNIQUE না দিলে একই মানুষ দুটি id-তে দুবার ঢুকে পড়বেন, আর DBMS কিছুই বলবে না।

স্বয়ংক্রিয় id — বিভিন্ন DBMS-এ

Surrogate key সাধারণত DBMS নিজেই দেয়, কিন্তু লেখার ধরন একেক সিস্টেমে একেক রকম। SQLite-এ INTEGER PRIMARY KEY কলামে মান না দিলে (বা NULL দিলে) সে নিজেই পরের নম্বর বসায়। নিচের ব্লকগুলো চালানো যায় না — শুধু তুলনার জন্য:

CREATE TABLE customers (
  customer_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,  -- SQL standard (PostgreSQL 10+)
  -- পুরোনো লেখা: customer_id SERIAL PRIMARY KEY
  name  TEXT NOT NULL,
  phone TEXT UNIQUE
);
CREATE TABLE customers (
  customer_id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(100) NOT NULL,
  phone VARCHAR(15) UNIQUE
) ENGINE = InnoDB;

SQL Server-এ লেখা হয় IDENTITY(1,1), Oracle 12c থেকে GENERATED AS IDENTITY (তার আগে sequence আর trigger). একাধিক সার্ভারে আলাদাভাবে id বানাতে হলে, বা id অনুমান করা ঠেকাতে চাইলে, অনেকে UUID ব্যবহার করেন — ১২৮ বিটের এলোমেলো মান। এর দাম: ১৬ বাইট, আর পুরোপুরি এলোমেলো UUID নতুন সারিকে index-এর যেখানে-সেখানে ঢোকায়, যা B+ tree-র জন্য ব্যয়বহুল (পর্ব ৪০)। সময়-ক্রমিক UUIDv7 এই সমস্যা অনেকটা কমায়।

৫ · Foreign key আর referential integrity — টেবিল জোড়ার চুক্তি

orders টেবিলে গ্রাহকের নাম-ঠিকানা আবার লেখা হয় না, লেখা হয় শুধু customer_id. এই কলামটি customers-এর primary key-র দিকে আঙুল তুলে বলে, “এই অর্ডার ওই গ্রাহকের”। এমন কলামকে বলে foreign key: একটি টেবিলের attribute (বা attribute-সেট), যার প্রতিটি মান অন্য (বা একই) টেবিলের একটি candidate key-তে — প্রায় সবসময় primary key-তে — থাকতেই হবে। যে টেবিল আঙুল তোলে সেটি child বা referencing টেবিল (orders), যার দিকে তোলা হয় সেটি parent বা referenced টেবিল (customers).

Referential integrity

Child টেবিলের প্রতিটি সারির foreign key-র মান হয় parent টেবিলের কোনো সারির key-র সাথে হুবহু মিলবে, নয়তো (যদি অনুমতি থাকে) পুরোপুরি NULL হবে। কোনো foreign key এমন কিছুর দিকে আঙুল তুলতে পারবে না, যা নেই।

ট্রেনের টিকিট বুকিং ভাবুন। প্রতিটি টিকিটে ট্রেন নম্বর লেখা — যেমন একটি আন্তঃনগর ট্রেনের নম্বর। এমন নম্বরের টিকিট বিক্রি করা যায় না, যে নম্বরের কোনো ট্রেন নেই; যাত্রী স্টেশনে এসে দেখবেন ট্রেনই নেই। আবার কোনো ট্রেন তালিকা থেকে তুলে দিতে চাইলে আগে সেই ট্রেনের বিক্রি হওয়া টিকিটগুলোর কী হবে, তা ঠিক করতে হবে — ফেরত, অন্য ট্রেনে বদল, নাকি তুলে দেওয়া আটকে রাখা। Foreign key-র দুই দিকের নিয়ম ঠিক এই দুটি।

নিচের ডায়াগ্রামে দেখুন, একটি নতুন অর্ডার ঢোকার সময় DBMS ধাপে ধাপে কী যাচাই করে:

orders-এ নতুন সারিসবুজ = এই ধাপে যা দেখা হচ্ছে

            
অবস্থা
স্টেটমেন্টের ফল
orders-এ সারি
  1. একটি অর্ডার আসছে। অ্যাপ পাঠাল INSERT INTO orders VALUES (1012, 99, '2025-10-20', 'pending'). orders টেবিলে এখন ১০টি অর্ডার (1001–1010), আর customers-এ ৮ জন গ্রাহক, যাঁদের customer_id 1 থেকে 8.
  2. নিজের টেবিলের নিয়ম আগে। order_id 1012 আগে নেই — primary key ঠিক। কোনো NOT NULL কলাম খালি নেই। status-এর মান 'pending', যা CHECK-এর তালিকায় আছে। এ পর্যন্ত সব ঠিক।
  3. Foreign key যাচাই। orders.customer_id একটি foreign key — এর মান customers-এর primary key-তে থাকতেই হবে। DBMS customers-এর primary key index-এ 99 খুঁজছে। Index থাকায় এটি পুরো টেবিল পড়া নয়, কয়েকটি ধাপের খোঁজ।
  4. পাওয়া যায়নি — পুরো স্টেটমেন্ট বাতিল। 99 নম্বর গ্রাহক নেই। এমন অর্ডার ঢুকলে সেটি হতো “অনাথ” (orphan) অর্ডার — কার কাছে মাল পাঠাবেন, কেউ জানে না। SQLite জানায় FOREIGN KEY constraint failed; orders-এ এখনো ১০টি সারি, কিছুই বদলায়নি।
  5. তুলনা: বৈধ অর্ডার। একই অর্ডার customer_id 3 দিয়ে পাঠালে 3 পাওয়া যায় (Sabbir Hasan), সব যাচাই পার হয়, সারিটি ঢোকে — এখন ১১টি অর্ডার। নিয়মটি সহজ: সন্তান (child) সারির foreign key-র মান হয় অভিভাবক (parent) টেবিলে থাকবে, নয়তো NULL হবে।

এবার নিজে চালিয়ে দেখুন। প্রথমটি বৈধ গ্রাহকের (Sabbir Hasan, id 3) অর্ডার, দ্বিতীয়টি এমন গ্রাহকের, যিনি নেই:

-- গ্রাহক 3 আছেন: অর্ডার ঢুকবে
INSERT INTO orders VALUES (1011, 3, '2025-10-20', 'pending');

-- গ্রাহক 99 নেই: foreign key ভাঙবে
INSERT INTO orders VALUES (1012, 99, '2025-10-20', 'pending');

প্রথমটিতে “✓ INSERT — ১টি সারি বদলেছে”, দ্বিতীয়টিতে FOREIGN KEY constraint failed. SQLite-এর বার্তা সংক্ষিপ্ত — কোন কলাম, কোন মান, তা বলে না। PostgreSQL অনেক স্পষ্ট: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey", সাথে Key (customer_id)=(99) is not present in table "customers". দ্বিতীয় দিকটিও আছে: শুধু child-এ ভুল মান ঢোকানো নয়, parent থেকে এমন সারি মুছে ফেলাও নিয়ম ভাঙে, যার দিকে কেউ আঙুল তুলে আছে।

-- Tanvir Ahmed (5)-এর কোনো অর্ডার নেই: মোছা যাবে
DELETE FROM customers WHERE customer_id = 5;

-- Rahim Uddin (1)-এর তিনটি অর্ডার আছে: মোছা যাবে না
DELETE FROM customers WHERE customer_id = 1;

প্রথম DELETE “✓ DELETE — ১টি সারি বদলেছে”, কারণ Tanvir-এর দিকে কোনো অর্ডার আঙুল তোলেনি। দ্বিতীয়টিতে আবার FOREIGN KEY constraint failed: অর্ডার 1001, 1003 আর 1009 Rahim-এর। তিনি মুছে গেলে ওই তিনটি অর্ডার অনাথ হতো। orders.customer_id-এ কোনো referential action লেখা নেই, তাই ডিফল্ট নিয়ম NO ACTION খাটে — মানে “এমন মোছা চলবে না”। অন্য নিয়মগুলো অংশ ৭-এ।

Foreign key-র আরও কয়েকটি বৈশিষ্ট্য

  • NULL হতে পারে, যদি নিষেধ না থাকে। products.category_id-এ NOT NULL নেই, তাই শ্রেণিহীন পণ্য রাখা যায়। orders.customer_id-এ NOT NULL আছে — গ্রাহক ছাড়া অর্ডার অর্থহীন। NULL foreign key কোনো কিছুর দিকে আঙুল তোলে না, তাই যাচাইও হয় না।
  • একই টেবিলের দিকেও আঙুল তোলা যায়। db-uni-র instructors টেবিলে mentor_id → instructors(instructor_id): একজন শিক্ষকের mentor আরেকজন শিক্ষক। একে self-referencing foreign key বলে; সবচেয়ে সিনিয়রের mentor_id NULL.
  • Composite-ও হতে পারে। parent-এর key composite হলে foreign key-ও সেই কয়টি কলাম মিলে হয়, একই ক্রমে।
  • নাম আর টাইপ মিলতে হবে না, অর্থ মিলতে হবে। কলামের নাম আলাদা হতে পারে (mentor_id → instructor_id), কিন্তু domain একই হওয়া চাই।

SQLite-এ foreign key ডিফল্টভাবে বন্ধ! পুরোনো অ্যাপের সাথে সামঞ্জস্য রাখতে SQLite প্রতিটি নতুন সংযোগে foreign key যাচাই বন্ধ রাখে; চালু করতে হয় PRAGMA foreign_keys = ON; দিয়ে, প্রতিটি সংযোগে আলাদা করে। এই পাতার রানার নিজেই এটি চালু করে নেয়, তাই উপরের ত্রুটিগুলো দেখতে পেয়েছেন। নিজের Python বা Android অ্যাপে এটি ভুলে গেলে REFERENCES লেখা থাকলেও অনাথ সারি চুপচাপ ঢুকে যাবে। PostgreSQL, SQL Server, Oracle আর MySQL-এর InnoDB engine ডিফল্টভাবেই যাচাই করে (বিশেষ সেটিং দিয়ে সাময়িক বন্ধ করা গেলেও); কিন্তু MySQL-এর পুরোনো MyISAM engine foreign key ঘোষণা মেনে নিলেও যাচাই করে না।

৬ · Integrity constraint-এর পুরো পরিবার

Key আর foreign key হলো integrity constraint-এর দুটি সদস্য মাত্র। Integrity constraint মানে ডেটার ওপর এমন একটি শর্ত, যা প্রতিটি বৈধ অবস্থায় সত্য থাকতে হবে; DBMS প্রতিটি INSERT, UPDATE আর DELETE-এর সময় শর্তগুলো যাচাই করে। কাজটি অ্যাপের কোডে না রেখে ডেটাবেসে রাখার সুবিধা: ওয়েবসাইট, মোবাইল অ্যাপ, অফিসের Excel-আমদানি স্ক্রিপ্ট — যে পথেই ডেটা আসুক, পাহারাদার একজনই।

Constraintকী নিশ্চিত করেSQL-এdb-shop-এ উদাহরণ
Domain constraintপ্রতিটি মান তার কলামের domain (অনুমোদিত মানের সেট) থেকে আসবেডেটা টাইপ, CHECK, DEFAULTquantity পূর্ণসংখ্যা আর ধনাত্মক
Entity integrityPrimary key-র কোনো অংশ NULL হবে নাPRIMARY KEYcustomer_id সবসময় আছে
Key / UNIQUECandidate key-তে duplicate নেইPRIMARY KEY, UNIQUEphone, categories.name
Referential integrityForeign key-র মান parent-এ আছে বা NULLREFERENCES, FOREIGN KEYorders.customer_id
NOT NULLঘরটি খালি থাকবে নাNOT NULLcustomers.name
CHECKএকটি সারির ভেতরের যেকোনো শর্তCHECK (…)price > 0, stock >= 0, status-এর তালিকা
ব্যবসার নিয়ম (semantic)একাধিক সারি বা টেবিল জুড়ে শর্তtrigger, transaction, অ্যাপের কোডস্টকের বেশি অর্ডার নেওয়া যাবে না

Entity integrity আর NOT NULL

Entity integrity-র যুক্তি সরল: primary key দিয়েই একটি সারিকে চেনা হয়। key-র কোনো অংশ NULL হলে সারিটি “অচেনা”, আর দুটি NULL-key সারি একই বস্তু কি না বলাই যায় না। তাই primary key-র প্রতিটি কলাম অবশ্যই মানসহ। অন্য কলামে NULL চলবে কি না, তা ঠিক করে NOT NULL. customers-এ name NOT NULL — নামহীন গ্রাহক চলবে না; কিন্তু phone আর city NULL হতে পারে।

-- phone না দিলে NULL বসে: phone-এ NOT NULL নেই, তাই চলবে
INSERT INTO customers (customer_id, name, city, joined_on)
VALUES (9, 'Sadia Islam', 'Barishal', '2025-10-20');

-- name NOT NULL: নাম ছাড়া গ্রাহক চলবে না
INSERT INTO customers (customer_id, name, city, joined_on)
VALUES (10, NULL, 'Dhaka', '2025-10-21');

প্রথমটি “✓ INSERT — ১টি সারি বদলেছে” — বাদ দেওয়া phone কলামে NULL বসেছে। দ্বিতীয়টিতে NOT NULL constraint failed: customers.name. খেয়াল করুন, ফাঁকা লেখা '' আর NULL এক নয়: '' দিলে NOT NULL আটকাত না, কারণ সেটি একটি মান (দৈর্ঘ্য শূন্য)। ফাঁকা নামও আটকাতে চাইলে লাগবে CHECK (length(name) > 0) ধরনের শর্ত। (ব্যতিক্রম Oracle: সেখানে ''-কে NULL ধরা হয়।)

SQLite-এর একটি পুরোনো বাগ, যা এখন “বৈশিষ্ট্য”: SQL standard বলে primary key কলাম স্বয়ংক্রিয়ভাবে NOT NULL. কিন্তু SQLite-এ INTEGER PRIMARY KEY ছাড়া অন্য টাইপের primary key-তে (যেমন db-uni-র course_id TEXT PRIMARY KEY) NULL ঢোকানো যায় — এবং একাধিক NULL-ও! পুরোনো সংস্করণের বাগ ঠিক করলে বহু অ্যাপ ভেঙে যেত বলে SQLite এটি রেখে দিয়েছে। প্রতিকার: SQLite-এ primary key কলামে নিজে NOT NULL লিখুন, বা STRICT / WITHOUT ROWID টেবিল ব্যবহার করুন। PostgreSQL, MySQL, SQL Server আর Oracle এমন NULL সবসময় ফিরিয়ে দেয়।

Domain আর CHECK

Domain constraint বলে প্রতিটি মান তার কলামের অনুমোদিত সেট থেকে আসবে। সবচেয়ে মোটা দাগের domain হলো ডেটা টাইপ — INTEGER কলামে “আটশো” লেখা চলবে না (SQLite-এ এখানে একটু শিথিলতা আছে, পর্ব ১৩-এ দেখবেন)। সূক্ষ্ম domain লেখা হয় CHECK দিয়ে: দাম ধনাত্মক, স্টক ঋণাত্মক নয়, অর্ডারের status চারটি শব্দের একটি, CGPA ০ থেকে ৪-এর মধ্যে। CHECK-এর শর্ত মিথ্যা হলে সারিটি বাতিল; শর্ত সত্য বা UNKNOWN (কোনো মান NULL হলে) হলে সারিটি ঢোকে — এই সূক্ষ্মতা পর্ব ১৬-এ।

-- Headphone (102)-এর স্টক 40: দুটি বিক্রি, থাকে 38
UPDATE products SET stock = stock - 2 WHERE product_id = 102;

-- Power Bank (103)-এর স্টক 0: একটি বিক্রি করলে -1 হবে
UPDATE products SET stock = stock - 1 WHERE product_id = 103;

প্রথম UPDATE “✓ UPDATE — ১টি সারি বদলেছে”। দ্বিতীয়টিতে CHECK constraint failed: stock >= 0 — products টেবিলে লেখা আছে CHECK (stock >= 0), আর 0 − 1 = −1 সেই শর্ত ভাঙে। স্টকে না থাকা পাওয়ার ব্যাংক বিক্রির ভুল এভাবে ডেটাবেসেই আটকে গেল, অ্যাপের কোডে যাচাই থাকুক বা না থাকুক। ইতিহাসের একটি সতর্কতা: MySQL দীর্ঘদিন CHECK লেখা মেনে নিত কিন্তু যাচাই করত না; যাচাই শুরু হয় MySQL 8.0.16 (২০১৯) থেকে। পুরোনো সংস্করণে এই UPDATE নীরবে −1 বসিয়ে দিত।

যা CHECK পারে না — ব্যবসার নিয়ম

CHECK শুধু একটি সারির ভেতরের কলাম দেখতে পারে। কিন্তু অনেক নিয়ম একাধিক সারি বা টেবিল জুড়ে:

  • “একটি অর্ডারের পণ্যের quantity সেই পণ্যের stock-এর বেশি হবে না” — order_items আর products দুটি টেবিল লাগে।
  • “একজন গ্রাহকের একসাথে ৫টির বেশি pending অর্ডার থাকবে না” — একই টেবিলের অনেক সারি গুনতে হয়।
  • “একজন শিক্ষকের বেতন তাঁর mentor-এর বেতনের বেশি হবে না” — একই টেবিলের দুটি আলাদা সারি তুলনা।
  • “bKash-এ দিনে নির্দিষ্ট অঙ্কের বেশি পাঠানো যাবে না” — দিনের সব লেনদেন যোগ করতে হয়।

SQL standard এমন নিয়মের জন্য CREATE ASSERTION নামে একটি ব্যবস্থা রেখেছে, কিন্তু প্রধান DBMS-গুলোর কোনোটিই এটি বাস্তবায়ন করেনি — প্রতিটি পরিবর্তনে পুরো ডেটাবেস জুড়ে শর্ত যাচাই অত্যন্ত ব্যয়বহুল। বাস্তবে এগুলো রক্ষা হয় তিনভাবে: trigger (পরিবর্তনের মুহূর্তে DBMS নিজে একটি কোড চালায় — পর্ব ২৯), transaction-এর ভেতরে যাচাই করে তারপর লেখা (পর্ব ৪৪), আর অ্যাপের কোড। শেষেরটি সবচেয়ে দুর্বল, কারণ অন্য পথে আসা ডেটা পাহারা পায় না, আর একসাথে দুটি অনুরোধ এলে দুজনই “স্টক আছে” দেখে ফেলতে পারে।

৭ · নিয়ম ভাঙলে কী হয় — reject আর referential action

এ পর্যন্ত সব লঙ্ঘনে একই ঘটনা ঘটেছে: DBMS পুরো স্টেটমেন্টটি বাতিল (reject) করে, ডেটাবেস আগের অবস্থায় থাকে, আর অ্যাপ একটি ত্রুটি পায়। একটি UPDATE যদি ১০০টি সারি বদলাতে গিয়ে ৫০তম সারিতে CHECK ভাঙে, আগের ৪৯টির পরিবর্তনও ফিরিয়ে নেওয়া হয় — স্টেটমেন্ট হয় পুরোপুরি হবে, নয়তো একটুও নয়। অ্যাপের কাজ তখন ত্রুটি ধরে ব্যবহারকারীকে বোধগম্য বার্তা দেখানো: “এই ফোন নম্বরে আগেই অ্যাকাউন্ট আছে”।

Foreign key-র parent দিকে কিন্তু “না” ছাড়াও অন্য উত্তর দেওয়া যায়। Parent সারি মুছলে বা তার key বদলালে child সারিগুলোর কী হবে, তা foreign key ঘোষণার সময় বলে দেওয়া যায় — এগুলোকে বলে referential action:

নিয়মParent মুছলে (ON DELETE)Parent-এর key বদলালে (ON UPDATE)কোথায় মানায়
NO ACTION (ডিফল্ট)child থাকলে বাতিলchild থাকলে বাতিলনিরাপদ ডিফল্ট
RESTRICTchild থাকলে সঙ্গে সঙ্গে বাতিলএকইলেনদেন, হিসাবের রেকর্ড
CASCADEchild-ও মুছে যায়child-এর foreign key-ও বদলায়অর্ডার → অর্ডার-লাইন (অংশ যা পুরো ছাড়া অর্থহীন)
SET NULLchild-এর foreign key NULL হয়একইশিক্ষার্থী → বিভাগ (বিভাগ উঠে গেলেও শিক্ষার্থী থাকেন)
SET DEFAULTchild-এর foreign key ডিফল্ট মান পায়একইকম ব্যবহৃত; ডিফল্ট মানটিও parent-এ থাকতে হয়

RESTRICT আর NO ACTION-এর সূক্ষ্ম পার্থক্য: দুটিই child থাকলে আটকায়। RESTRICT যাচাই করে সঙ্গে সঙ্গে; NO ACTION যাচাই করে স্টেটমেন্ট শেষে — আর constraint-টি DEFERRABLE হলে transaction-এর শেষে (commit-এর সময়)। তাই NO ACTION-এ একই transaction-এ আগে parent মুছে পরে নতুন parent ঢুকিয়ে দিলে চলে, RESTRICT-এ চলে না। MySQL-এ দুটি একই আচরণ করে; PostgreSQL আর SQLite-এ পার্থক্যটি আছে। Oracle-এ ON UPDATE নিয়মই নেই, আর ON DELETE-এ শুধু CASCADE ও SET NULL.

তিনটি টেবিল (ছোট নমুনা)সবুজ = মোছার জন্য চিহ্নিত

            
হিসাব
সারি (customers · orders · items)
মোছা হবে
  1. শুরুর অবস্থা। দুজন গ্রাহক, তিনটি অর্ডার, চারটি অর্ডার-লাইন। orders.customer_id আর order_items.order_id — দুটি foreign key-ই ON DELETE CASCADE দিয়ে ঘোষণা করা। এবার আসছে DELETE FROM customers WHERE id = 1.
  2. অভিভাবক সারি চিহ্নিত। Rahim (id 1) মোছা হবে। কিন্তু তাঁর দিকে আঙুল তুলে আছে orders-এর দুটি সারি। শুধু Rahim মুছলে ওই দুটি অর্ডার অনাথ হয়ে যাবে — referential integrity ভাঙবে। DBMS তাই foreign key-র নিয়ম দেখে: CASCADE.
  3. প্রথম ঢেউ: orders. CASCADE মানে “অভিভাবক গেলে সন্তানও যাবে।” customer_id = 1 এমন অর্ডার 1001 আর 1003 মোছার জন্য চিহ্নিত হলো। 1002 (Karima-র) অক্ষত।
  4. দ্বিতীয় ঢেউ: order_items. এখন 1001 আর 1003 নিজেরাই অভিভাবক — order_items-এর তিনটি লাইন তাদের দিকে আঙুল তুলে আছে। সেখানেও CASCADE, তাই ওই তিনটিও চিহ্নিত। ঢেউটি এভাবে যতগুলো স্তর আছে ততদূর যায়।
  5. ফল। একটি DELETE স্টেটমেন্টে মোট ১ + ২ + ৩ = ৬টি সারি মুছল। পুরো কাজটি atomic — মাঝপথে কোনো সমস্যা হলে কিছুই মুছত না। খেয়াল করুন, SQLite-এর “কয়টি সারি বদলেছে” হিসাবে শুধু customers-এর ১টি সারি গোনা হয়; cascade-এ মোছা সারি সেই সংখ্যায় আসে না।
  6. তুলনা: RESTRICT বা NO ACTION. নিয়ম যদি CASCADE না হয়ে RESTRICT বা NO ACTION (ডিফল্ট) হতো, DBMS ধাপ ২-তেই দেখত Rahim-এর অর্ডার আছে, আর পুরো DELETE বাতিল করত — FOREIGN KEY constraint failed. কোন নিয়ম কখন ঠিক, সেটি ব্যবসার প্রশ্ন; পর্ব ২৩-এ আরও বিস্তারিত দেখবেন।

এবার একই ছোট উদাহরণটি আসল SQLite-এ চালান। এই এডিটর db-shop নয়, নিজের একটি ছোট schema বানিয়ে নেয় — customers(id, name), orders(id, customer_id) আর order_items(order_id, product), দুটি foreign key-ই ON DELETE CASCADE. শুরুর ডেটা ঠিক উপরের ডায়াগ্রামের মতো; “নমুনা ডেটা” বাক্সে schema দেখতে পাবেন।

-- Rahim (1)-কে মুছলে তাঁর অর্ডার আর অর্ডার-লাইনও যাবে
DELETE FROM customers WHERE id = 1;

SELECT * FROM customers;
SELECT * FROM orders;
SELECT * FROM order_items;

DELETE-এর পর বার্তা “✓ DELETE — ১টি সারি বদলেছে” — অথচ আসলে ৬টি সারি মুছেছে! SQLite-এর পরিবর্তন-গণনা শুধু সরাসরি লক্ষ্যের সারি গোনে, foreign key action-এ মোছা সারি নয়। তিনটি SELECT-এর ফল: customers-এ শুধু 2 | Karima, orders-এ শুধু 1002 | 2, order_items-এ শুধু 1002 | DSA Book. CASCADE দুই স্তর নেমে গেছে। এখানে এটি সুবিধাজনক, কিন্তু ভাবুন বাস্তব দোকানে: একজন গ্রাহকের অ্যাকাউন্ট মুছলে তাঁর সব বিক্রির রেকর্ড উধাও — হিসাবরক্ষণ, ভ্যাট আর অডিটের জন্য যা মারাত্মক। তাই orders → customers-এ CASCADE সাধারণত ভুল পছন্দ; order_items → orders-এ সেটি স্বাভাবিক।

এখন SET NULL আর ON UPDATE CASCADE. এই এডিটরের ছোট schema: departments(dept_id, name) আর students(student_id, name, dept_id), যেখানে students.dept_id-এ লেখা ON DELETE SET NULL ON UPDATE CASCADE. CSE বিভাগের কোড 1 থেকে 10 করা হবে, আর English বিভাগ (4) বন্ধ হবে:

-- CSE-র কোড 1 থেকে 10: শিক্ষার্থীর dept_id-ও বদলাবে
UPDATE departments SET dept_id = 10 WHERE dept_id = 1;

-- English (4) বন্ধ: Jui থাকবেন, কিন্তু বিভাগ NULL
DELETE FROM departments WHERE dept_id = 4;

SELECT * FROM students;

দুটি পরিবর্তন-বার্তার পর students-এর ফল: 2101 | Arif | 10, 2103 | Chayon | 2, 2401 | Jui | NULL. Arif-এর dept_id নিজে থেকে 1 থেকে 10 হয়েছে (ON UPDATE CASCADE), আর Jui-এর সারি মোছেনি, শুধু বিভাগের ঘর খালি হয়েছে (ON DELETE SET NULL). সতর্কতা: SET NULL শুধু তখনই কাজ করে, যখন child কলামে NOT NULL নেই। orders.customer_id-এর মতো NOT NULL কলামে SET NULL দিলে parent মোছার চেষ্টা NOT NULL constraint failed দিয়ে বাতিল হবে। আর ভালো ডিজাইনে primary key বদলানোর দরকারই পড়ে না — ON UPDATE CASCADE লাগছে মানে key-টি সম্ভবত “স্থির” নয় (অংশ ৪)। Constraint যোগ-বাদ দেওয়া আর এই নিয়মগুলোর আরও বিস্তারিত পর্ব ২৩-এ।

আরও গভীরে: Foreign key যাচাই আসলে index-এর খোঁজ। Child-এ সারি ঢোকানোর সময় DBMS parent-এর primary key index খোঁজে — এটি সবসময় থাকে। কিন্তু parent মুছলে বা key বদলালে খুঁজতে হয় child-এর foreign key কলামে, আর সেখানে index না থাকলে প্রতিবার পুরো child টেবিল পড়তে হয়: একটি গ্রাহক মুছতে কোটি অর্ডারের টেবিল স্ক্যান। MySQL InnoDB foreign key কলামে index না থাকলে নিজেই বানিয়ে নেয়; PostgreSQL আর SQLite বানায় না, তাই সেখানে নিজে CREATE INDEX করা একটি পরিচিত টিউনিং পরামর্শ (পর্ব ৪১)। PostgreSQL ভেতরে foreign key যাচাই করে সিস্টেমের নিজস্ব trigger দিয়েই, আর যাচাইয়ের সময় parent সারিতে একটি হালকা shared lock নেয়, যাতে যাচাই আর মোছার মাঝখানে অন্য কেউ parent-টি মুছে ফেলতে না পারে।

৮ · ইতিহাস — Codd-এর integrity নিয়ম থেকে আজকের SQL

১৯৭০ সালে IBM-এর গবেষক Edgar F. Codd তাঁর বিখ্যাত প্রবন্ধ “A Relational Model of Data for Large Shared Data Banks”-এ relational model-এর প্রস্তাব দেন। সেখানেই primary key আর foreign key ধারণা দুটি আসে: প্রতিটি relation-এর একটি key তার সারিগুলো চেনায়, আর অন্য relation-এর key-র মান বহন করে টেবিলগুলো যুক্ত হয় — pointer বা ডিস্কের ঠিকানা দিয়ে নয়, শুধু মান দিয়ে। পরে Codd দুটি সাধারণ নিয়ম স্পষ্ট করে লেখেন, যা আজ পাঠ্যবইয়ে “Codd-এর integrity rules” নামে পরিচিত: entity integrity (primary key-র কোনো অংশ NULL নয়) আর referential integrity (প্রতিটি non-NULL foreign key-র মান কোনো বিদ্যমান key-র সাথে মেলে)। এগুলোর সুপরিচিত রূপ পাওয়া যায় ১৯৭৯ সালের তাঁর RM/T প্রবন্ধে, যেখানে তিনি system-এর দেওয়া স্থায়ী পরিচয় “surrogate”-এর পক্ষেও যুক্তি দেন।

প্রথম দিকের SQL পণ্যে কিন্তু এসব ছিল না — key আর foreign key যাচাইয়ের দায় ছিল অ্যাপের ঘাড়ে। SQL standard-এ PRIMARY KEY, FOREIGN KEY আর CHECK আসে ১৯৮৯ সালের সংশোধনে (SQL-89-এর integrity সংযোজন), আর ON DELETE / ON UPDATE action পূর্ণ রূপ পায় SQL-92-এ। ১৯৮৫ সালে Codd তাঁর বিখ্যাত ১২টি নিয়মের দশম নিয়মে (“integrity independence”) দাবি করেন: integrity constraint ডেটাবেসের catalog-এ সংরক্ষিত থাকবে, অ্যাপের কোডে নয়। তিন দশকেরও বেশি পরে MySQL-এ CHECK যাচাই চালু হওয়া (২০১৯) বা SQLite-এর ডিফল্টভাবে বন্ধ foreign key দেখায়, সেই দাবি পুরোপুরি পূরণ হতে কত সময় লেগেছে।

৯ · AI-এর যুগে key আর constraint

AI-কে “একটা ই-কমার্স ডেটাবেসের schema বানিয়ে দাও” বললে কয়েক সেকেন্ডে সুন্দর CREATE TABLE চলে আসে। সমস্যা হলো, constraint-এর ভুল চোখে পড়ে না — schema চলে, ডেটা ঢোকে, ভুলটা ধরা পড়ে মাস ছয়েক পরে, যখন অনাথ অর্ডার আর duplicate গ্রাহক জমে গেছে। AI প্রায়ই যা করে:

AI-এর ভুলফলকীভাবে ধরবেন
surrogate id দিয়েছে, কিন্তু email/phone-এ UNIQUE দেয়নিএকই গ্রাহক দুই id-তেএকই phone দিয়ে দুবার INSERT করে দেখুন
foreign key কলাম রেখেছে, REFERENCES লেখেনিঅনাথ সারি চুপচাপ ঢোকেঅস্তিত্বহীন parent-এর id দিয়ে INSERT
সবখানে ON DELETE CASCADEএকটি DELETE-এ হিসাবের ইতিহাস উধাওপ্রতিটি FK-র জন্য প্রশ্ন: parent গেলে child-এর কী হওয়া উচিত?
SET NULL দিয়েছে NOT NULL কলামেparent মোছাই যায় নাparent মুছে দেখুন
SQLite কোডে PRAGMA foreign_keys = ON নেইসব FK অকার্যকরFK-ভাঙা INSERT চালিয়ে দেখুন ত্রুটি আসে কি না
নেই এমন কলামের নাম (যেমন customer.email)কোয়েরি চলেই না, বা ভুল টেবিলে চলেআসল schema পাশে রেখে মিলিয়ে নিন
ব্যবসার নিয়ম শুধু অ্যাপের কোডেঅন্য পথে আসা ডেটা পাহারাহীনকোন নিয়ম CHECK/FK-তে যায়, কোনটি trigger-এ — তালিকা করুন

AI-কে schema চাইলে শুধু “বানিয়ে দাও” না বলে key আর constraint-এর যুক্তিও চেয়ে নিন। তাহলে প্রতিটি সিদ্ধান্ত আপনি যাচাই করতে পারবেন:

আমি একটি ছোট অনলাইন দোকানের জন্য ডেটাবেস বানাচ্ছি (SQLite দিয়ে শুরু, পরে PostgreSQL). টেবিল: customers, products, orders, order_items. প্রতিটি টেবিলের জন্য: ১. সব candidate key-র তালিকা দাও, আর কোনটিকে primary key বানাচ্ছ ও কেন (natural না surrogate) বলো। ২. প্রতিটি alternate key-তে UNIQUE আর NOT NULL লাগবে কি না বলো। ৩. প্রতিটি foreign key-র জন্য ON DELETE-এ কোন নিয়ম (CASCADE / SET NULL / RESTRICT) দিচ্ছ, আর কেন — হিসাবরক্ষণের রেকর্ড যেন হারিয়ে না যায়। ৪. CHECK দিয়ে কোন কোন নিয়ম লেখা যায়, আর কোন নিয়ম CHECK দিয়ে লেখা যায় না (trigger বা অ্যাপে লাগবে) — আলাদা করে দেখাও। ৫. শেষে প্রতিটি constraint ভাঙার একটি করে INSERT/DELETE দাও, যাতে আমি নিজে চালিয়ে দেখতে পারি ত্রুটি আসে।

AI-এর দেওয়া schema হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

  • Superkey আর candidate key-র পার্থক্য? — Candidate key হলো minimal superkey: এর থেকে কোনো attribute বাদ দিলে আর সারি আলাদা করা যায় না।
  • Primary key আর UNIQUE-এর পার্থক্য? — Primary key টেবিলে একটিই আর NULL নিষেধ (entity integrity); UNIQUE একাধিক থাকতে পারে আর সাধারণত NULL চলে।
  • Foreign key কি NULL হতে পারে? Duplicate? — দুটিই পারে; একজন গ্রাহকের অনেক অর্ডার, আর NULL কোনো কিছুর দিকে আঙুল তোলে না।
  • R(A, B, C, D)-এ candidate key {A} আর {B, C} হলে superkey কয়টি? — ৮ + ৪ − ২ = ১০টি (inclusion–exclusion).
  • ON DELETE CASCADE আর SET NULL কখন? — Child অংশটি parent ছাড়া অর্থহীন হলে CASCADE (অর্ডার-লাইন), child স্বাধীনভাবে টেকে এমন হলে SET NULL (বিভাগহীন শিক্ষার্থী)।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোয়েরি-লেখক থেকে ডেটাবেস-ডিজাইনার বানাবে।

প্র ০১একটি মোবাইল ওয়ালেট কোম্পানি গ্রাহক-টেবিলের primary key হিসেবে ফোন নম্বর রাখতে চায় — “আমাদের ব্যবসাই তো নম্বর-ভিত্তিক”। আপনি কী পরামর্শ দেবেন?

যুক্তিটি আকর্ষণীয়, কারণ গ্রাহক নিজেও নম্বর দিয়েই নিজেকে চেনান। কিন্তু primary key-র কাজ ব্যবহারকারীর কাছে পরিচয় দেওয়া নয়, ডেটাবেসের ভেতরে সম্পর্কগুলো স্থিরভাবে ধরে রাখা। একটি ওয়ালেট সিস্টেমে গ্রাহকের key কপি হয় লেনদেন, KYC নথি, অভিযোগ, এজেন্ট-রেকর্ড, ঝুঁকি-বিশ্লেষণ — কয়েক ডজন টেবিলে, কোটি কোটি সারিতে।

এখন ভাবুন নম্বর বদলের ঘটনা। গ্রাহক সিম হারিয়ে নতুন নম্বর নিলেন, বা অফিস তাঁকে নতুন নম্বর দিল। নম্বর primary key হলে প্রতিটি টেবিলে সেই গ্রাহকের সব সারি বদলাতে হবে — ON UPDATE CASCADE থাকলেও এটি একটি বিশাল, lock-ভারী কাজ, আর কোনো একটি টেবিলে (ধরুন পুরোনো archive-এ) foreign key না থাকলে ইতিহাস ছিঁড়ে যাবে। আরও খারাপ ঘটনা হলো নম্বর হাতবদল: অপারেটর দীর্ঘদিন বন্ধ নম্বর নতুন কাউকে দিলে, নম্বর-ভিত্তিক key পুরোনো মালিকের লেনদেন-ইতিহাস নতুন মালিকের সাথে জুড়ে দেওয়ার ঝুঁকি তৈরি করে — যা আর্থিক ও আইনি দুই দিক থেকেই গুরুতর।

এই শিক্ষা নতুন নয়। ২০০৭ সালে ISBN ১০ থেকে ১৩ অঙ্কে যাওয়ায় বহু লাইব্রেরি ও বইয়ের দোকানের সফটওয়্যারকে schema বদলাতে হয়েছিল, কারণ তারা ধরে নিয়েছিল “ISBN কখনো বদলাবে না”। Codd নিজেও ১৯৭৯ সালে যুক্তি দেন, বাস্তব জগতের নাম-নম্বর বদলায়, তাই system-এর দেওয়া স্থায়ী surrogate দরকার।

পরামর্শ তাই: customer_id BIGINT surrogate primary key; phone-এ UNIQUE NOT NULL, যাতে এক নম্বরে দুটি সক্রিয় অ্যাকাউন্ট না হয়; আর নম্বর বদলের ইতিহাস আলাদা টেবিলে (customer_id, phone, কবে থেকে, কবে পর্যন্ত)। লগইন আর টাকা পাঠানো চলবে নম্বর দিয়েই — UNIQUE index-এর খোঁজ primary key-র মতোই দ্রুত। ব্যবহারকারীর অভিজ্ঞতা একটুও বদলায় না, শুধু ভেতরের সম্পর্ক স্থির হয়।

মূল উপলব্ধি: “মানুষ যা দিয়ে চেনে” আর “ডেটাবেস যা দিয়ে জোড়ে” — দুটি আলাদা প্রশ্ন। প্রথমটির উত্তর natural key + UNIQUE, দ্বিতীয়টির উত্তর এমন একটি মান যা কখনো বদলায় না।

প্র ০২Primary key-তে NULL নিষেধ, কিন্তু foreign key আর UNIQUE কলামে NULL চলে — এই অসমতার যুক্তি কী? আর UNIQUE কলামে কয়টি NULL থাকতে পারে, সে বিষয়ে DBMS-গুলো একমত নয় কেন?

শুরু করা যাক NULL-এর অর্থ দিয়ে। SQL-এ NULL মানে “মান অজানা বা প্রযোজ্য নয়”। Primary key-র একমাত্র কাজ সারি চেনানো; যার key অজানা, তাকে চেনানো যায় না। দুটি সারির key-ই NULL হলে তারা এক বস্তু না দুই, বলার উপায় নেই। এজন্যই Codd-এর entity integrity নিয়ম primary key-র প্রতিটি অংশকে মানসহ চায় — composite key-র একটি কলামও NULL হতে পারে না।

Foreign key-র কাজ আলাদা: সে একটি সম্পর্ক জানায়। আর “সম্পর্ক এখনো নেই” বা “জানা নেই” একটি বৈধ বাস্তব অবস্থা। শ্রেণি ঠিক হয়নি এমন পণ্য, mentor নেই এমন সিনিয়র শিক্ষক, বিভাগ বেছে নেননি এমন নতুন শিক্ষার্থী — এদের foreign key NULL রাখা অর্থপূর্ণ। NULL কোনো কিছুর দিকে আঙুল তোলে না, তাই কোনো সারির অস্তিত্বও দাবি করে না; referential integrity ভাঙে না। সম্পর্কটি বাধ্যতামূলক হলে (অর্ডারের গ্রাহক) ডিজাইনার আলাদা করে NOT NULL লেখেন।

UNIQUE-এর ক্ষেত্রে প্রশ্নটি হলো: দুটি NULL কি “একই মান”? SQL-এর তিন-মানের যুক্তিতে NULL = NULL-এর ফল TRUE নয়, UNKNOWN. সেই যুক্তিতে দুটি NULL duplicate নয়, তাই SQLite, PostgreSQL, MySQL আর Oracle একাধিক NULL মেনে নেয় (Oracle-এ composite UNIQUE-এর ক্ষেত্রে কিছু সূক্ষ্মতা আছে)। SQL Server কিন্তু UNIQUE constraint-এ NULL-কে অন্য মানের মতোই ধরে, তাই একটির বেশি NULL চলে না; একাধিক NULL চাইলে সেখানে WHERE phone IS NOT NULL শর্তসহ filtered unique index লাগে। আবার PostgreSQL 15 উল্টো দিকের সুবিধা যোগ করেছে: UNIQUE NULLS NOT DISTINCT লিখলে একটির বেশি NULL চলবে না। মতভেদের কারণ, standard-এর পুরোনো লেখা দুই রকম ব্যাখ্যার সুযোগ রেখেছিল, আর প্রতিটি পণ্য নিজের ব্যবহারকারীদের ভাঙবে না বলে নিজের ব্যাখ্যায় স্থির থেকেছে।

বাস্তব ফল: একটি schema SQL Server থেকে PostgreSQL-এ (বা উল্টো) সরালে UNIQUE কলামের আচরণ নীরবে বদলে যেতে পারে। Migration-এর পরীক্ষায় তাই “দুটি NULL ঢোকান” একটি আবশ্যিক test case.

মূল উপলব্ধি: NULL কোনো মান নয়, মানের অনুপস্থিতি। যে constraint “চেনানো”-র কাজ করে, সে অনুপস্থিতি সহ্য করতে পারে না; যে constraint শুধু “সংঘাত ঠেকায়”, সে অনুপস্থিতিকে সংঘাত মনে করে না — আর এই সীমানায় DBMS-গুলোর আচরণ আলাদা, তাই নিজে পরীক্ষা করে নিন।

প্র ০৩কিছু বড় ওয়েব কোম্পানির ডেটাবেসে foreign key constraint ইচ্ছে করেই রাখা হয় না, যাচাই হয় অ্যাপের কোডে। এটি কি অবহেলা, নাকি যুক্তিসংগত trade-off?

দুটোই হতে পারে — নির্ভর করে পরিস্থিতির ওপর। আগে দেখি কেন কেউ foreign key বাদ দেন।

  • Sharding: খুব বড় সিস্টেমে ডেটা অনেক সার্ভারে ভাগ করা থাকে (পর্ব ৫৪)। গ্রাহক এক সার্ভারে, তাঁর অর্ডার আরেকটিতে থাকলে কোনো একক DBMS দুই সার্ভার জুড়ে foreign key যাচাই করতে পারে না। কিছু sharding ব্যবস্থা দীর্ঘদিন foreign key-কে সীমিতভাবে বা একেবারেই সমর্থন করেনি।
  • লেখার খরচ আর lock: প্রতিটি child INSERT-এ parent-এর খোঁজ আর parent সারিতে lock — খুব বেশি লেখার চাপে বা একটি “জনপ্রিয়” parent (ধরুন একটি বড় merchant) থাকলে এটি প্রতিযোগিতা বাড়ায়।
  • Online schema change: চালু অবস্থায় বিশাল টেবিল বদলানোর কিছু জনপ্রিয় টুল foreign key-ওয়ালা টেবিলে কাজ করতে পারে না বা সীমিতভাবে পারে।
  • বড় আমদানি আর মোছা: কোটি সারি লোড বা পরিষ্কারের সময় ক্রম মানতে হয়, cascade নিয়ন্ত্রণ করা কঠিন।

দাম কিন্তু চড়া। যাচাই অ্যাপে গেলে প্রতিটি পথ — মূল অ্যাপ, অ্যাডমিন প্যানেল, পুরোনো স্ক্রিপ্ট, হাতে লেখা হটফিক্স — নিজে নিয়ম মানবে, এমন আশা করতে হয়। দুটি অনুরোধ একসাথে এলে (একজন গ্রাহক মুছছে, আরেকজন সেই গ্রাহকের অর্ডার ঢোকাচ্ছে) অ্যাপের “আগে দেখে পরে লিখি” যাচাই race condition-এ হারে; DBMS-এর foreign key lock দিয়ে ঠিক এটিই ঠেকায়। ফল: ধীরে ধীরে অনাথ সারি জমে, রিপোর্টে JOIN করলে হিসাব মেলে না, আর একদিন কাউকে বসে পরিষ্কারের স্ক্রিপ্ট লিখতে হয়।

তাই যাঁরা foreign key বাদ দেন, তাঁরা সাধারণত বিকল্প পাহারা রাখেন: নিয়মিত “অনাথ খোঁজার” কোয়েরি (LEFT JOIN … WHERE parent.id IS NULL, পর্ব ২০-এর anti join), soft delete (মোছার বদলে deleted_at চিহ্ন), আর ডেটার মালিকানা এক সার্ভিসে সীমিত রাখা।

একটি মাঝারি আকারের অ্যাপের জন্য — যা এক সার্ভারে চলে, যার লেখার চাপ কয়েক হাজার প্রতি সেকেন্ডের নিচে — এই যুক্তিগুলো প্রায় খাটেই না। সেখানে foreign key বাদ দেওয়া অবহেলা।

মূল উপলব্ধি: ডেটাবেসের constraint হলো সবচেয়ে সস্তা আর নির্ভরযোগ্য পাহারাদার। তাকে ছাঁটাই করা যায়, কিন্তু শুধু তখনই যখন স্কেলের প্রয়োজন প্রমাণিত, আর বিকল্প পাহারা তৈরি।

প্র ০৪ON DELETE CASCADE লিখে দিলে কাজ সহজ হয় — তাহলে অভিজ্ঞ ডিজাইনাররা হিসাব-সংক্রান্ত টেবিলে এটিকে ভয় পান কেন? গ্রাহক “মুছতে” হলে তাহলে কী করবেন?

CASCADE-এর শক্তিই তার বিপদ: একটি ছোট স্টেটমেন্ট অনেক স্তর নেমে গিয়ে অগণিত সারি মুছে দিতে পারে, আর SQLite-এর মতো সিস্টেমে “১টি সারি বদলেছে” দেখে তা বোঝাও যায় না। অ্যাডমিন প্যানেলে ভুল করে একজন বড় পাইকারি গ্রাহক মুছলেন — সাথে মুছে গেল বছরের পর বছরের অর্ডার, অর্ডার-লাইন, পেমেন্ট, ফেরতের রেকর্ড। Backup থেকে ফেরানো যায়, কিন্তু ততক্ষণে মাঝের নতুন লেনদেনগুলো জোড়া লাগানো এক দুঃস্বপ্ন।

হিসাব-সংক্রান্ত ডেটার আরেকটি দিক আইনি। বিক্রির রেকর্ড, ভ্যাট চালান, ব্যাংক লেনদেন — এগুলো নির্দিষ্ট সময় সংরক্ষণের বাধ্যবাধকতা থাকে, আর অডিটে দেখাতে হয় কোন দিন কী বিক্রি হয়েছিল। গ্রাহক চলে গেছেন বলে তাঁর কেনাকাটার ইতিহাস মুছে ফেলা সেখানে অগ্রহণযোগ্য। এ কারণে ব্যাংকিং ও হিসাবরক্ষণ সফটওয়্যারে লেনদেনের রেকর্ড সাধারণত কখনো মোছা হয় না; ভুল হলে উল্টো লেনদেন (reversal) দিয়ে শোধরানো হয় — পুরোনো খাতার লাল কালির সংশোধনের মতো।

তাহলে গ্রাহক মুছতে চাইলে কী করবেন? প্রচলিত পথগুলো:

  • RESTRICT / NO ACTION রাখুন orders → customers-এ, যাতে ভুল মোছা ধরা পড়ে।
  • Soft delete: customers-এ is_active বা deleted_at কলাম; অ্যাপ নিষ্ক্রিয় গ্রাহক দেখায় না, কিন্তু সম্পর্কগুলো অক্ষত।
  • গোপনীয়তার জন্য anonymize: গ্রাহক তথ্য মুছে দিতে বললে নাম, ফোন, ঠিকানা মুছে বা বদলে দিন, কিন্তু customer_id আর অর্ডারের সংখ্যাগত রেকর্ড রাখুন। ইউরোপের GDPR-এর মতো আইনে “মুছে ফেলার অধিকার” আর হিসাব রাখার বাধ্যবাধকতার মধ্যে সমন্বয় এভাবেই করা হয়।
  • CASCADE রাখুন শুধু “অংশ”-এ: order_items → orders, বা একটি পোস্টের ড্রাফট-সংস্করণ → পোস্ট — যেখানে child-এর parent ছাড়া কোনো স্বাধীন অর্থ নেই।

মূল উপলব্ধি: প্রতিটি foreign key-র ON DELETE নিয়ম একটি ব্যবসায়িক সিদ্ধান্ত, প্রযুক্তিগত সুবিধা নয়। প্রশ্নটি হলো “parent না থাকলে child-এর কি কোনো অর্থ বা আইনি মূল্য আছে?” — হ্যাঁ হলে CASCADE নয়।

প্র ০৫একটি relation-এ সর্বোচ্চ কয়টি candidate key থাকতে পারে? আর শুধু ডেটা দেখে candidate key খুঁজে বের করা কতটা কঠিন?

প্রথমে একটি পর্যবেক্ষণ: দুটি আলাদা candidate key কখনো একটি আরেকটির উপসেট হতে পারে না। K₁ ⊂ K₂ হলে K₂-র একটি প্রকৃত উপসেট (K₁) superkey হয়ে যায়, ফলে K₂ minimal নয় — বিরোধ। অর্থাৎ candidate key-গুলোর সংগ্রহ গণিতের ভাষায় একটি antichain: কোনো সদস্য অন্যটির ভেতরে নেই।

১৯২৮ সালে গণিতবিদ Emanuel Sperner প্রমাণ করেন, n উপাদানের একটি সেটের উপসেটগুলোর মধ্যে সবচেয়ে বড় antichain-এর আকার C(n, ⌊n/2⌋) — অর্থাৎ ঠিক অর্ধেক আকারের সব উপসেট। তাই n টি attribute-এর relation-এ candidate key সর্বোচ্চ C(n, ⌊n/2⌋)টি, আর এই সীমা পৌঁছানো যায়: এমন নিয়ম ভাবুন যেখানে যেকোনো ⌊n/2⌋টি attribute একসাথে সারি চেনায় কিন্তু তার কম পারে না। n = ৪-এ সীমা C(4, 2) = ৬; n = ২০-এ ১,৮৪,৭৫৬. অর্থাৎ candidate key-র সংখ্যা attribute-এর সংখ্যার সাথে exponential হারে বাড়তে পারে, তাই “সব candidate key তালিকা করা” সমস্যাটির উত্তরই কখনো কখনো বিশাল।

এবার ডেটা থেকে খোঁজা। একটি নির্দিষ্ট সেট K-র জন্য duplicate আছে কি না দেখা সহজ — GROUP BY K HAVING COUNT(*) > 1, যা সর্ট বা hash দিয়ে প্রায় O(m log m) সময়ে হয় (m = সারির সংখ্যা)। কিন্তু সবচেয়ে ছোট unique সেট খোঁজা কঠিন: এটি তত্ত্বে NP-hard বলে পরিচিত, আর ডেটা প্রোফাইলিং নিয়ে গবেষণায় (যেমন unique column combination খোঁজার অ্যালগরিদমে) নানা ছাঁটাই-কৌশল ব্যবহার হয়। আর সবচেয়ে বড় কথা — ডেটা থেকে পাওয়া সেটগুলো শুধু “এই instance-এ unique”; সেগুলো নিয়ম কি না, তা বলতে পারে কেবল ব্যবসা জানা মানুষ।

তত্ত্বে সঠিক পথটি হলো functional dependency থেকে key বের করা: কোন attribute-গুলো জানলে বাকি সব নির্ধারিত হয়ে যায় (attribute closure). এটি পর্ব ৩৫-এর বিষয়, আর normalization-এর ভিত্তি।

মূল উপলব্ধি: Candidate key-রা পরস্পর অতুলনীয় বলে তাদের সংখ্যা সর্বোচ্চ C(n, ⌊n/2⌋) — Sperner-এর উপপাদ্য। আর ডেটা শুধু প্রার্থী দেখায়; key ঘোষণা করে নিয়ম, আর নিয়ম লেখা হয় functional dependency-র ভাষায়।

অনুশীলন

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

  1. Superkey গুনুন (হাতে): R(A, B, C, D)-এর candidate key ঠিক দুটি: {A} আর {B, C}. R-এর মোট superkey কয়টি? কোনগুলো, তালিকা করুন।

    Superkey মানে এমন সেট, যাতে অন্তত একটি candidate key পুরোপুরি আছে। A আছে এমন সেট: বাকি B, C, D-র প্রতিটি থাকতে বা না থাকতে পারে, তাই 2³ = ৮টি। B আর C দুটিই আছে এমন সেট: A আর D ইচ্ছামতো, তাই 2² = ৪টি। দুটিতেই গোনা হয়েছে এমন সেট (A, B, C তিনটিই আছে): D ইচ্ছামতো, 2¹ = ২টি। Inclusion–exclusion: ৮ + ৪ − ২ = ১০টি.

    তালিকা: {A}, {A,B}, {A,C}, {A,D}, {A,B,C}, {A,B,D}, {A,C,D}, {A,B,C,D}, {B,C}, {B,C,D}. গুনে দেখুন — ১০টি।

  2. কোয়েরি লিখুন: orders টেবিলে (order_id, customer_id, order_date, status) একজন সহকর্মী দাবি করছেন, (customer_id, order_date) জোড়াও একটি candidate key. db-shop-এ এই দাবি পরীক্ষার কোয়েরি লিখুন (উপরের প্রথম এডিটরে চালাতে পারবেন)। ফল কী আসে, আর তাতে দাবিটি প্রমাণ হয় কি?
    SELECT customer_id, order_date, COUNT(*) AS n
    FROM orders
    GROUP BY customer_id, order_date
    HAVING COUNT(*) > 1;

    ফল: “(কোনো সারি নেই)”। Rahim (1)-এর তিনটি অর্ডারই আলাদা দিনে (2025-09-01, 09-10, 10-08), Karima (2)-এর দুটিও (09-03, 10-01). কিন্তু এতে দাবি প্রমাণ হয় না — একজন গ্রাহক একই দিনে সকালে আর বিকেলে দুটি অর্ডার দিতেই পারেন, নিয়মে কোনো বাধা নেই। ডেটা এখানে শুধু “এখনো খণ্ডন হয়নি” বলছে। (customer_id, order_date) তাই candidate key নয়; order_id-ই একমাত্র candidate key.

  3. ফল আগে বলুন (trace): তিনটি টেবিল: customers (1 Rahim, 2 Karima); orders (1001 → 1, 1002 → 2, 1003 → 1), যেখানে orders.customer_id REFERENCES customers ON DELETE RESTRICT; order_items ((1001, Mobile Phone), (1001, Headphone), (1002, DSA Book), (1003, Rice 5kg)), যেখানে order_items.order_id REFERENCES orders ON DELETE CASCADE. ক্রমানুসারে চালানো হলো: (ক) DELETE FROM orders WHERE id = 1001; (খ) DELETE FROM customers WHERE id = 2; (গ) DELETE FROM customers WHERE id = 1; প্রতিটির পর কী হয়, আর শেষে কোন টেবিলে কয়টি সারি?

    (ক) অর্ডার 1001 মোছে; CASCADE-এ তার দুটি লাইন (Mobile Phone, Headphone)-ও যায়। এখন orders: 1002, 1003; order_items: (1002, DSA Book), (1003, Rice 5kg).

    (খ) Karima (2)-এর অর্ডার 1002 আছে, আর orders → customers-এ RESTRICT, তাই FOREIGN KEY constraint failed — কিছুই মোছে না।

    (গ) Rahim (1)-এর অর্ডার 1003 এখনো আছে — RESTRICT আবার আটকায়, কিছুই মোছে না।

    শেষে: customers ২টি, orders ২টি (1002, 1003), order_items ২টি। শিক্ষা: CASCADE নিচে নামে, কিন্তু উপরের স্তরে RESTRICT থাকলে সেই পথে কোনো ঢেউ শুরুই হয় না। চাইলে উপরের CASCADE এডিটরে schema বদলে নিজে মিলিয়ে নিন।

  4. ডিজাইন করুন: একটি মোবাইল ওয়ালেটের transfers টেবিল: কে পাঠাল (sender), কে পেল (receiver), কত টাকা, কখন, আর অপারেটরের দেওয়া একটি transaction ID (TrxID). Primary key কী হবে, কোন কোন candidate/alternate key, কোন foreign key আর ON DELETE নিয়ম, কোন CHECK — আর কোন নিয়ম CHECK দিয়ে লেখা যাবে না?
    • Primary key: surrogate transfer_id BIGINT. TrxID একটি natural candidate key — trx_id TEXT NOT NULL UNIQUE (alternate key). ভেতরের সম্পর্ক ছোট পূর্ণসংখ্যায়, বাইরের পরিচয় UNIQUE দিয়ে রক্ষিত।
    • Foreign key: sender_id আর receiver_id দুটিই REFERENCES accounts(account_id), দুটিই NOT NULL, আর ON DELETE RESTRICT — আর্থিক রেকর্ড কখনো cascade-এ মুছবে না; অ্যাকাউন্ট বন্ধ হলে soft delete.
    • CHECK: CHECK (amount > 0), CHECK (sender_id <> receiver_id) — দুটিই একটি সারির ভেতরের শর্ত, তাই CHECK-এ চলে।
    • CHECK-এ যায় না: “পাঠানোর পর sender-এর ব্যালান্স ঋণাত্মক হবে না” (accounts টেবিল লাগে) আর “দিনে সর্বোচ্চ সীমা” (একই দিনের সব সারি যোগ)। এগুলো transaction-এর ভেতরে যাচাই করে তারপর লেখা (পর্ব ৪৪), বা trigger (পর্ব ২৯) দিয়ে রক্ষা করতে হয় — শুধু অ্যাপের কোডে রাখলে একসাথে আসা দুটি অনুরোধ দুজনই “ব্যালান্স আছে” দেখে ফেলতে পারে।

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

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

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

  1. ব্যবসার নিয়ম জেনে নেওয়া — কোন তথ্য কখনো দুবার হয় না
  2. যেসব attribute-সেট প্রতিটি সারিকে আলাদা করে, সেগুলো খোঁজা (superkey)
  3. বাড়তি attribute বাদ দিয়ে minimal সেটগুলো রাখা (candidate key)
  4. স্থির, ছোট, কখনো NULL নয় এমন একটিকে primary key করা
  5. বাকি candidate key-তে UNIQUE ও NOT NULL বসানো (alternate key)

একটি relation R(A, B, C)-এর একমাত্র candidate key হলো {A}. R-এর মোট superkey কয়টি?

  • ১টি
  • ৩টি
  • ৪টি
  • ৮টি

Entity integrity নিয়ম অনুযায়ী কোনটি কখনো NULL হতে পারে না?

  • যেকোনো foreign key কলাম
  • Primary key-র প্রতিটি কলাম
  • UNIQUE কলাম
  • টেবিলের সব কলাম

orders.customer_id কলামে NOT NULL আছে, আর এটি REFERENCES customers(customer_id) ON DELETE SET NULL. যাঁর অর্ডার আছে এমন একজন গ্রাহককে মুছতে গেলে কী হবে?

  • গ্রাহক মুছবে, অর্ডারগুলোর customer_id NULL হবে
  • গ্রাহক আর তাঁর সব অর্ডার মুছবে
  • গ্রাহক মুছবে, অর্ডারগুলো আগের customer_id নিয়েই থাকবে
  • ত্রুটি দিয়ে পুরো DELETE বাতিল হবে

একটি customers টেবিলে SELECT email, COUNT(*) FROM customers GROUP BY email HAVING COUNT(*) > 1; চালিয়ে কোনো সারি পাওয়া গেল না। এ থেকে কোন সিদ্ধান্তটি ঠিক?

  • এই মুহূর্তের ডেটায় email-এ duplicate নেই, কিন্তু email key কি না তা নিয়ম দেখে ঠিক করতে হবে
  • email নিশ্চিতভাবে একটি candidate key
  • email কখনো key হতে পারে না
  • কোয়েরিটি ভুল, কারণ HAVING-এ COUNT ব্যবহার করা যায় না

নিচের কোন নিয়মটি সাধারণ DBMS-এ একটি CHECK constraint দিয়ে লেখা যায় না?

  • পণ্যের দাম শূন্যের বেশি হবে
  • টাকা পাঠানোর লেনদেনে sender আর receiver একই হবে না
  • অর্ডারের quantity সেই পণ্যের বর্তমান stock-এর বেশি হবে না
  • অর্ডারের status চারটি নির্দিষ্ট শব্দের একটি হবে

ফোন নম্বরের বদলে স্বয়ংক্রিয় পূর্ণসংখ্যা id-কে (surrogate key) primary key করার প্রধান সুবিধা কোনটি?

  • এটি নিজেই একই গ্রাহকের দুবার নিবন্ধন ঠেকায়
  • বাস্তব জগতের তথ্য বদলালেও key বদলায় না, তাই অন্য টেবিলের সম্পর্ক স্থির থাকে
  • গ্রাহকেরা নিজের id সহজে মনে রাখতে পারেন
  • এতে foreign key-র আর দরকার হয় না

শব্দকোষ

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

মনে রাখুন

  1. Superkey সারি আলাদা করে; candidate key হলো minimal superkey; একটি candidate বেছে primary key, বাকিরা alternate key (UNIQUE).
  2. Key আসে নিয়ম থেকে; GROUP BY … HAVING COUNT(*) > 1 শুধু দেখায় কোনটি key নয়।
  3. ভালো primary key স্থির, ছোট, কখনো NULL নয় — তাই প্রায়ই surrogate id + natural key-তে UNIQUE.
  4. Foreign key-র মান parent-এ থাকবে বা NULL হবে (referential integrity); SQLite-এ PRAGMA foreign_keys = ON লাগে।
  5. লঙ্ঘনে পুরো স্টেটমেন্ট বাতিল; parent মুছলে কী হবে ঠিক করে NO ACTION / RESTRICT / CASCADE / SET NULL — হিসাবের রেকর্ডে CASCADE নয়।

পরের পর্বে: মডিউল ১ শেষ — আপনি এখন জানেন টেবিল, key আর constraint কী। কিন্তু কোন টেবিল বানাবেন, সেটি ঠিক হয় কীভাবে? পর্ব ০৭-এ শুরু হবে ডিজাইন: ER মডেলে entity, attribute আর relationship এঁকে বাস্তব জগৎকে ডেটাবেসে আনা।

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

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