Constraints, Defaults & Domains
Constraint, default ও domain — schema-কেই data এর পাহারাদার বানান
1. Why Constraints Exist
A database without constraints is like a bank teller who lets you withdraw negative money, deposit into a non-existent account, or open two accounts with the same NID. Constraints are the schema's way of saying "these states are physically impossible". They turn a class of bugs into a category of errors that the database refuses before the bad data ever lands.
In this module we walk through every standard constraint — NOT NULL, UNIQUE,
PRIMARY KEY, FOREIGN KEY, CHECK — plus DEFAULT, the
referential actions (CASCADE, SET NULL, RESTRICT,
NO ACTION), generated columns, and PostgreSQL's domains.
Make the wrong state impossible to represent. If a column should never be NULL — declare it
NOT NULL. If two rows must never share an email — declare it UNIQUE. The schema
should be the first line of defence, not the last.
2. The Five Standard Constraints
| Constraint | What it forbids | বাংলায় |
|---|---|---|
NOT NULL | NULL in this column | এই কলামে NULL রাখা যাবে না |
UNIQUE | Two rows with the same value | একই মান দুটি row-তে থাকবে না |
PRIMARY KEY | Both NULL and duplicates (UNIQUE + NOT NULL) | NULL ও duplicate দুটোই নিষেধ |
FOREIGN KEY | A reference to a row that doesn't exist | যে row নেই তার দিকে রেফারেন্স দেওয়া |
CHECK (...) | Any condition you write evaluating to FALSE | আপনি যে শর্ত লিখবেন, সেটি ভঙ্গ করা |
NOT NULL, UNIQUE এবং
PRIMARY KEY একটি কলামের ভেতরের নিয়ম দেখে; FOREIGN KEY দুটি table-এর সম্পর্ক
ঠিক রাখে; আর CHECK আপনাকে নিজের শর্ত যোগ করার সুযোগ দেয়।
The schema below is a tiny student-registration database. It demonstrates every constraint in one place —
and the INSERT statements that follow show which ones the database accepts and which ones it
rejects.
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER CHECK (age BETWEEN 15 AND 100),
dept_id INTEGER REFERENCES departments(id)
);
INSERT INTO departments(id,name) VALUES(1,'CSE'),(2,'EEE');
-- Allowed:
INSERT INTO students(id,name,email,age,dept_id)
VALUES(1,'Arif','arif@du.ac.bd',21,1);
-- Rejected — duplicate email (UNIQUE):
-- INSERT INTO students VALUES(2,'Imran','arif@du.ac.bd',22,1);
-- Rejected — age out of range (CHECK):
-- INSERT INTO students VALUES(3,'Tiny','t@x.bd',9,1);
-- Rejected — dept_id 99 does not exist (FOREIGN KEY):
-- INSERT INTO students VALUES(4,'Ghost','g@x.bd',22,99);
SELECT * FROM students;
Uncomment any of the rejected inserts and re-run — SQLite will refuse with a clear error. The schema, not the application, has the final say.
3. CHECK — Custom Rules
CHECK is the most flexible constraint. Anything you can write as a SQL boolean expression
can be a CHECK. Below are real examples we use in production schemas at Bangladeshi
e-commerce companies.
CHECK হলো সবচেয়ে নমনীয় constraint। আপনি যা চান সেটাই একটি boolean expression-এ রূপান্তর করে
CHECK দিয়ে enforce করতে পারেন — Bangladesh-এর mobile number ১১ digit, GPA ০ থেকে ৪ এর মধ্যে,
order quantity > ০, ইত্যাদি।
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
qty INTEGER CHECK (qty > 0),
price INTEGER CHECK (price >= 0),
discount INTEGER CHECK (discount BETWEEN 0 AND 100),
status TEXT CHECK (status IN ('pending','paid','shipped','cancelled')),
phone TEXT CHECK (length(phone) = 11 AND phone LIKE '01%')
);
INSERT INTO orders VALUES(1,2,450,10,'paid','01711234567');
SELECT * FROM orders;
Notice the last CHECK: it enforces both the length (11 digits) and the prefix ("01") of a
Bangladeshi mobile number. A bad number will never reach disk. Try editing the insert to put
'12345' as the phone — the engine refuses.
CHECK conditions that compare against another table are not supported in standard SQL.
Use a foreign key for that, or — if you really must — a trigger. SQLite will silently accept some such
CHECKs and not enforce them at write time.
4. DEFAULT — Sensible Fallbacks
DEFAULT tells the database what value to use when an INSERT omits a column.
It is not a constraint exactly — nothing forbids you to override it — but it removes a huge class of
"oh, the user forgot to set created_at" bugs.
DEFAULT মানে — যদি app কলামটির মান না দেয়, database নিজে থেকে এই মানটি ব্যবহার করবে।
created_at, status, country code-এর মতো জিনিসে এটি অপূর্ব।
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
view_count INTEGER NOT NULL DEFAULT 0,
country TEXT NOT NULL DEFAULT 'BD',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Insert with only the title — every other column gets its default:
INSERT INTO posts(title) VALUES('My first blog post');
INSERT INTO posts(title,status) VALUES('Launch announcement','published');
SELECT id, title, status, view_count, country, created_at
FROM posts;
5. Foreign-Key Actions — ON DELETE & ON UPDATE
When the parent row of a foreign key is deleted (or its key is updated), what should happen to the children? SQL gives you four choices:
| Action | Behaviour | বাংলায় |
|---|---|---|
CASCADE | Delete/update the children too | সন্তান row-গুলোও একইসাথে delete/update হবে |
SET NULL | Set the child's FK column to NULL | FK কলামে NULL বসে যাবে |
RESTRICT | Refuse the delete/update, immediately | delete বাতিল হবে, transaction-এর মাঝপথেই |
NO ACTION | Refuse — but the check is deferred to end of statement | RESTRICT-এর মতোই, কিন্তু চেক হবে statement-এর শেষে |
CASCADE ভয়ংকর শক্তিশালী — সাবধানে ব্যবহার করুন। একজন user delete করলে যদি তার সব order, address,
review একসাথে মুছে যায় — সেটি ভালো কি খারাপ, depend করে আপনার business-এর ওপর। অনেক ক্ষেত্রে
SET NULL বা RESTRICT বেশি নিরাপদ।
CASCADE in action
SQLite needs PRAGMA foreign_keys = ON to enforce foreign keys (it is OFF by default for legacy
reasons). The block below turns it on, then deletes a department and watches its students disappear.
PRAGMA foreign_keys = ON;
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT,
dept_id INTEGER REFERENCES departments(id)
ON DELETE CASCADE
);
INSERT INTO departments VALUES(1,'CSE'),(2,'EEE');
INSERT INTO students VALUES(1,'Arif',1),(2,'Nusrat',1),(3,'Rahim',2);
-- Drop the CSE department; its two students cascade away:
DELETE FROM departments WHERE id = 1;
SELECT * FROM students;
6. Generated / Computed Columns
A generated column is a column whose value is derived from other columns by an expression. You never insert into it — the database computes it. SQLite supports both virtual (computed on read) and stored (computed on write, persisted) generated columns.
price * qty থেকে total বানানো যায়।
CREATE TABLE order_items (
id INTEGER PRIMARY KEY,
price INTEGER NOT NULL,
qty INTEGER NOT NULL CHECK (qty > 0),
total INTEGER GENERATED ALWAYS AS (price * qty) VIRTUAL
);
INSERT INTO order_items(price,qty) VALUES(450,3),(120,5);
SELECT id, price, qty, total FROM order_items;
VIRTUAL means "compute on every read" — no extra disk, but the expression is evaluated each
time. STORED means "compute on write, store the value" — fast reads, slightly bigger table.
For simple multiplications use VIRTUAL; for expensive expressions you query often, prefer
STORED.
7. Domains — Reusable Type + Constraint Bundles
A domain is a named, reusable bundle of "type + constraints". SQL standard, supported by
PostgreSQL: define email_addr once with a length and a regex CHECK; use it in five tables.
SQLite does not have CREATE DOMAIN, so this section is conceptual — but the
idea is important for production schema design.
bd_phone: text, length 11,
"01" দিয়ে শুরু। তারপর সেটি ১০টি table-এ ব্যবহার করতে পারেন। PostgreSQL-এ CREATE DOMAIN
statement আছে; SQLite-এ নেই — তাই SQLite-এ আমরা প্রতিটি কলামে আলাদা CHECK লিখি।
CREATE DOMAIN bd_phone AS TEXT CHECK (length(VALUE) = 11 AND VALUE LIKE '01%'); CREATE TABLE customers (id SERIAL, phone bd_phone NOT NULL); CREATE TABLE riders (id SERIAL, phone bd_phone NOT NULL);
-- SQLite has no CREATE DOMAIN. Repeat the CHECK in each table:
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
phone TEXT NOT NULL CHECK (length(phone) = 11 AND phone LIKE '01%')
);
CREATE TABLE riders (
id INTEGER PRIMARY KEY,
phone TEXT NOT NULL CHECK (length(phone) = 11 AND phone LIKE '01%')
);
INSERT INTO customers(id,phone) VALUES(1,'01711234567');
SELECT * FROM customers;
8. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| Constraint | A schema-level rule that data must obey. | Schema-তে লেখা একটি নিয়ম, যা ডেটা মানতে বাধ্য। |
| NOT NULL | Column may not be NULL. | NULL রাখা যাবে না। |
| UNIQUE | No two rows share the value. | মান unique হতে হবে। |
| PRIMARY KEY | UNIQUE + NOT NULL — the row's identity. | একটি row-এর পরিচিতি। |
| FOREIGN KEY | Reference to another table's PK. | অন্য table-এর PK-কে রেফারেন্স। |
| CHECK | Custom boolean rule. | আপনার লেখা শর্ত। |
| DEFAULT | Value used when none is supplied. | কিছু না দিলে যে মান বসবে। |
| CASCADE | Children follow parent on delete/update. | Parent মুছলে সন্তানরাও মুছবে। |
| Generated column | Value derived by formula from other columns. | অন্য কলাম থেকে formula দিয়ে বের হওয়া কলাম। |
| Domain | Reusable named type+CHECK (Postgres feature). | Postgres-এর reusable type। |
9. Practice Problems
Each answer is runnable SQLite SQL. Try first, then expand.
-
Create a
userstable whereemailis unique and required, andnameis required.এমন একটি users table বানান যেখানে email unique ও required এবং name required।✨ Show Answer
ans1.sqlCREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE ); INSERT INTO users(name,email) VALUES('Arif','a@x.bd'); SELECT * FROM users; -
Add a CHECK so that
price > 0for all products.products.price > 0 — এই CHECK যোগ করুন।✨ Show Answer
ans2.sqlCREATE TABLE products( id INTEGER PRIMARY KEY, name TEXT, price INTEGER CHECK(price > 0) ); INSERT INTO products VALUES(1,'Walton TV',45000); SELECT * FROM products; -
Define an
orderstable whosestatuscan only be one ofpending,paid,shipped,cancelled.orders.status শুধু এই চারটি মানের একটি হতে পারবে — এমন CHECK লিখুন।✨ Show Answer
ans3.sqlCREATE TABLE orders( id INTEGER PRIMARY KEY, status TEXT NOT NULL CHECK(status IN ('pending','paid','shipped','cancelled')) ); INSERT INTO orders VALUES(1,'pending'); SELECT * FROM orders; -
Create a
poststable wherecreated_atdefaults to the current timestamp andview_countdefaults to 0.created_at default = CURRENT_TIMESTAMP, view_count default = 0।✨ Show Answer
ans4.sqlCREATE TABLE posts( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, view_count INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); INSERT INTO posts(title) VALUES('Hello'); SELECT * FROM posts; -
Set up an
orders→customersforeign key withON DELETE CASCADE. Then delete a customer and verify the orders are gone.customers delete হলে তার orders-ও মুছবে — CASCADE দিয়ে দেখান।✨ Show Answer
ans5.sqlPRAGMA foreign_keys = ON; CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE orders( id INTEGER PRIMARY KEY, cust_id INTEGER REFERENCES customers(id) ON DELETE CASCADE ); INSERT INTO customers VALUES(1,'Arif'); INSERT INTO orders VALUES(10,1),(11,1); DELETE FROM customers WHERE id = 1; SELECT * FROM orders; -
Same as above, but use
ON DELETE SET NULL.এবার SET NULL দিয়ে করুন।✨ Show Answer
ans6.sqlPRAGMA foreign_keys = ON; CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE orders( id INTEGER PRIMARY KEY, cust_id INTEGER REFERENCES customers(id) ON DELETE SET NULL ); INSERT INTO customers VALUES(1,'Arif'); INSERT INTO orders VALUES(10,1); DELETE FROM customers WHERE id = 1; SELECT * FROM orders; -
Define a
library_bookstable where (title,author) pair is unique together (composite UNIQUE).title ও author একসাথে unique — composite UNIQUE লিখুন।✨ Show Answer
ans7.sqlCREATE TABLE library_books( id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL, UNIQUE(title, author) ); INSERT INTO library_books(title,author) VALUES('Padma Nadir Majhi','Manik B.'); SELECT * FROM library_books; -
Use a generated column to compute
full_namefromfirstandlast.first ও last থেকে full_name = first||' '||last — generated column বানান।✨ Show Answer
ans8.sqlCREATE TABLE people( id INTEGER PRIMARY KEY, first TEXT, last TEXT, full_name TEXT GENERATED ALWAYS AS (first || ' ' || last) VIRTUAL ); INSERT INTO people(first,last) VALUES('Arif','Hossain'); SELECT * FROM people; -
In one paragraph, explain when to use
CASCADEvsRESTRICT.CASCADE আর RESTRICT — কখন কোনটি? এক অনুচ্ছেদে।✨ Show Answer
Answer: Use
CASCADEwhen the child rows have no meaning without their parent — like a blog post's tags or a cart's line items. UseRESTRICT(orNO ACTION) when the child has independent value and you want a human to decide first — like orders that belong to a customer (you almost never want a strayDELETE FROM customersto also delete a year of order history).CASCADE তখনই ব্যবহার করুন যখন parent ছাড়া child-এর কোনো অর্থ নেই (যেমন একটি post-এর tag-গুলো)। কিন্তু order, payment-এর মতো গুরুত্বপূর্ণ child-এর জন্য RESTRICT রাখাই নিরাপদ — যেন অসাবধানতাবশত পুরো history মুছে না যায়।
-
Why does SQLite require
PRAGMA foreign_keys = ON?SQLite-এ এই PRAGMA কেন দরকার?✨ Show Answer
Answer: Foreign-key enforcement was added to SQLite later (3.6.19, 2009). To stay compatible with old applications, the SQLite team made it OFF by default. Every modern app should turn it on right after opening a connection:
PRAGMA foreign_keys = ON;.SQLite-এ FK enforcement পরে যোগ হয়েছিল; পুরাতন app যেন না ভাঙে তাই default OFF। আধুনিক app-এ connection খুলেই
PRAGMA foreign_keys = ONদিতে হবে। -
Add a CHECK that ensures a Bangladeshi NID column has exactly 10, 13 or 17 digits.NID ১০, ১৩, বা ১৭ digit হবে — CHECK দিয়ে enforce করুন।
✨ Show Answer
ans11.sqlCREATE TABLE citizens( id INTEGER PRIMARY KEY, nid TEXT NOT NULL CHECK (length(nid) IN (10,13,17)) ); INSERT INTO citizens(nid) VALUES('1234567890'); SELECT * FROM citizens; -
Why is
NULLdifferent from "empty"? How doesUNIQUEtreat multiple NULL rows in SQLite?NULL আর "ফাঁকা" এক জিনিস নয় — UNIQUE কলামে একাধিক NULL কি সম্ভব?✨ Show Answer
Answer: NULL means "unknown / not applicable" — it is not equal to anything, not even another NULL. SQLite (and the SQL standard) allows multiple NULLs in a UNIQUE column, because each NULL is "unknown" and unknowns are not duplicates of each other. If you really want only one NULL, you need an extra constraint (or just
NOT NULL).NULL মানে "অজানা" — দুটি NULL সমান নয়, কারণ অজানা কি অজানা সমান হতে পারে? তাই SQLite-এ UNIQUE কলামে একাধিক NULL row রাখা যায়। চাইলে
NOT NULLযোগ করুন। -
Demonstrate that an
INSERTviolating a CHECK actually fails — show the error path.CHECK ভঙ্গ হলে INSERT fail করে — দেখান।✨ Show Answer
ans13.sqlCREATE TABLE marks(id INTEGER PRIMARY KEY, score INTEGER CHECK(score BETWEEN 0 AND 100)); INSERT INTO marks(score) VALUES(85); -- The next line will fail with: CHECK constraint failed: marks INSERT INTO marks(score) VALUES(150); SELECT * FROM marks; -
Define a
transactionstable with aSTOREDgenerated columnnet=amount - fee.net = amount - fee — STORED generated column বানান।✨ Show Answer
ans14.sqlCREATE TABLE transactions( id INTEGER PRIMARY KEY, amount INTEGER NOT NULL, fee INTEGER NOT NULL DEFAULT 0, net INTEGER GENERATED ALWAYS AS (amount - fee) STORED ); INSERT INTO transactions(amount,fee) VALUES(1000,15),(500,5); SELECT * FROM transactions;
Summary — Module 22
Constraints make wrong data physically impossible. NOT NULL, UNIQUE,
PRIMARY KEY, FOREIGN KEY, and CHECK together cover the vast majority
of integrity rules. DEFAULT removes "forgot-to-set" bugs. Foreign-key actions
(CASCADE, SET NULL, RESTRICT, NO ACTION) tell the
engine what to do when parents disappear. Generated columns push computed values into the schema, and
domains (Postgres) bundle types + checks for reuse. Always: let the schema be the first line of
defence.