Constraints, Defaults & Domains

Constraint, default ও domain — schema-কেই data এর পাহারাদার বানান

Read: ~32 min Medium 14 practice problems Live SQL runner

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.

Constraint ছাড়া database মানে — এমন একজন cashier যিনি negative টাকা withdraw করতে দেন, যে account নেই সেখানে deposit করেন, এবং একই NID দিয়ে দু'বার account খুলে দেন। Constraint হলো schema-র সেই নিয়ম, যা বলে "এই অবস্থা ঘটাই অসম্ভব"। ফলে bug ঢোকার আগেই database তা প্রত্যাখ্যান করে।

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.

Mantra (নীতি)
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

ConstraintWhat it forbidsবাংলায়
NOT NULLNULL in this columnএই কলামে NULL রাখা যাবে না
UNIQUETwo rows with the same valueএকই মান দুটি row-তে থাকবে না
PRIMARY KEYBoth NULL and duplicates (UNIQUE + NOT NULL)NULL ও duplicate দুটোই নিষেধ
FOREIGN KEYA reference to a row that doesn't existযে row নেই তার দিকে রেফারেন্স দেওয়া
CHECK (...)Any condition you write evaluating to FALSEআপনি যে শর্ত লিখবেন, সেটি ভঙ্গ করা
পাঁচটি constraint-ই database-এর "নিয়মের কাঠামো"। NOT NULL, UNIQUE এবং PRIMARY KEY একটি কলামের ভেতরের নিয়ম দেখে; FOREIGN KEY দুটি table-এর সম্পর্ক ঠিক রাখে; আর CHECK আপনাকে নিজের শর্ত যোগ করার সুযোগ দেয়।
All five in one CREATE TABLE — runnable

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.

all_constraints.sql
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 > ০, ইত্যাদি।
checks_in_action.sql
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.

Common pitfall
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-এর মতো জিনিসে এটি অপূর্ব।
defaults.sql
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:

ActionBehaviourবাংলায়
CASCADEDelete/update the children tooসন্তান row-গুলোও একইসাথে delete/update হবে
SET NULLSet the child's FK column to NULLFK কলামে NULL বসে যাবে
RESTRICTRefuse the delete/update, immediatelydelete বাতিল হবে, transaction-এর মাঝপথেই
NO ACTIONRefuse — but the check is deferred to end of statementRESTRICT-এর মতোই, কিন্তু চেক হবে statement-এর শেষে
CASCADE ভয়ংকর শক্তিশালী — সাবধানে ব্যবহার করুন। একজন user delete করলে যদি তার সব order, address, review একসাথে মুছে যায় — সেটি ভালো কি খারাপ, depend করে আপনার business-এর ওপর। অনেক ক্ষেত্রে SET NULL বা RESTRICT বেশি নিরাপদ।
Live demo — 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.

cascade_demo.sql
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;
CASCADE DELETE parent children alsodeleted SET NULL DELETE parent child.fk = NULLchild kept RESTRICT DELETE parent refusedimmediately NO ACTION DELETE parent refusedat statement end Figure 22.1 — চারটি foreign-key action — কে কখন কী করে।

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.

Generated column মানে এমন একটি কলাম, যার মান অন্য কলাম থেকে formula-র মাধ্যমে বের হয়। আপনি এতে insert করেন না — database নিজেই হিসাব করে। যেমন, price * qty থেকে total বানানো যায়।
generated_columns.sql
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.

Domain হলো একটি custom data type যেটি আপনি নিজে define করেন — যেমন bd_phone: text, length 11, "01" দিয়ে শুরু। তারপর সেটি ১০টি table-এ ব্যবহার করতে পারেন। PostgreSQL-এ CREATE DOMAIN statement আছে; SQLite-এ নেই — তাই SQLite-এ আমরা প্রতিটি কলামে আলাদা CHECK লিখি।
Postgres syntax (এটি SQLite-এ চলবে না)
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 workaround — same idea, different syntax
domain_workaround.sql
-- 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 (শব্দকোষ)

TermMeaningবাংলায়
ConstraintA schema-level rule that data must obey.Schema-তে লেখা একটি নিয়ম, যা ডেটা মানতে বাধ্য।
NOT NULLColumn may not be NULL.NULL রাখা যাবে না।
UNIQUENo two rows share the value.মান unique হতে হবে।
PRIMARY KEYUNIQUE + NOT NULL — the row's identity.একটি row-এর পরিচিতি।
FOREIGN KEYReference to another table's PK.অন্য table-এর PK-কে রেফারেন্স।
CHECKCustom boolean rule.আপনার লেখা শর্ত।
DEFAULTValue used when none is supplied.কিছু না দিলে যে মান বসবে।
CASCADEChildren follow parent on delete/update.Parent মুছলে সন্তানরাও মুছবে।
Generated columnValue derived by formula from other columns.অন্য কলাম থেকে formula দিয়ে বের হওয়া কলাম।
DomainReusable named type+CHECK (Postgres feature).Postgres-এর reusable type।

9. Practice Problems

Each answer is runnable SQLite SQL. Try first, then expand.

প্রতিটি প্রশ্নের উত্তর SQLite-এ চলার মতো লেখা। আগে নিজে চেষ্টা করুন, তারপর Show Answer খুলে মিলিয়ে নিন।
  1. Create a users table where email is unique and required, and name is required.
    এমন একটি users table বানান যেখানে email unique ও required এবং name required।
    ✨ Show Answer
    ans1.sql
    CREATE 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;
  2. Add a CHECK so that price > 0 for all products.
    products.price > 0 — এই CHECK যোগ করুন।
    ✨ Show Answer
    ans2.sql
    CREATE TABLE products(
        id INTEGER PRIMARY KEY,
        name TEXT,
        price INTEGER CHECK(price > 0)
    );
    INSERT INTO products VALUES(1,'Walton TV',45000);
    SELECT * FROM products;
  3. Define an orders table whose status can only be one of pending, paid, shipped, cancelled.
    orders.status শুধু এই চারটি মানের একটি হতে পারবে — এমন CHECK লিখুন।
    ✨ Show Answer
    ans3.sql
    CREATE 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;
  4. Create a posts table where created_at defaults to the current timestamp and view_count defaults to 0.
    created_at default = CURRENT_TIMESTAMP, view_count default = 0।
    ✨ Show Answer
    ans4.sql
    CREATE 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;
  5. Set up an orders → customers foreign key with ON DELETE CASCADE. Then delete a customer and verify the orders are gone.
    customers delete হলে তার orders-ও মুছবে — CASCADE দিয়ে দেখান।
    ✨ Show Answer
    ans5.sql
    PRAGMA 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;
  6. Same as above, but use ON DELETE SET NULL.
    এবার SET NULL দিয়ে করুন।
    ✨ Show Answer
    ans6.sql
    PRAGMA 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;
  7. Define a library_books table where (title, author) pair is unique together (composite UNIQUE).
    title ও author একসাথে unique — composite UNIQUE লিখুন।
    ✨ Show Answer
    ans7.sql
    CREATE 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;
  8. Use a generated column to compute full_name from first and last.
    first ও last থেকে full_name = first||' '||last — generated column বানান।
    ✨ Show Answer
    ans8.sql
    CREATE 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;
  9. In one paragraph, explain when to use CASCADE vs RESTRICT.
    CASCADE আর RESTRICT — কখন কোনটি? এক অনুচ্ছেদে।
    ✨ Show Answer

    Answer: Use CASCADE when the child rows have no meaning without their parent — like a blog post's tags or a cart's line items. Use RESTRICT (or NO 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 stray DELETE FROM customers to also delete a year of order history).

    CASCADE তখনই ব্যবহার করুন যখন parent ছাড়া child-এর কোনো অর্থ নেই (যেমন একটি post-এর tag-গুলো)। কিন্তু order, payment-এর মতো গুরুত্বপূর্ণ child-এর জন্য RESTRICT রাখাই নিরাপদ — যেন অসাবধানতাবশত পুরো history মুছে না যায়।

  10. 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 দিতে হবে।

  11. Add a CHECK that ensures a Bangladeshi NID column has exactly 10, 13 or 17 digits.
    NID ১০, ১৩, বা ১৭ digit হবে — CHECK দিয়ে enforce করুন।
    ✨ Show Answer
    ans11.sql
    CREATE 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;
  12. Why is NULL different from "empty"? How does UNIQUE treat 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 যোগ করুন।

  13. Demonstrate that an INSERT violating a CHECK actually fails — show the error path.
    CHECK ভঙ্গ হলে INSERT fail করে — দেখান।
    ✨ Show Answer
    ans13.sql
    CREATE 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;
  14. Define a transactions table with a STORED generated column net = amount - fee.
    net = amount - fee — STORED generated column বানান।
    ✨ Show Answer
    ans14.sql
    CREATE 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.

Constraint-গুলো ব্যবহার করুন — schema-কেই data-এর প্রথম পাহারাদার বানান। NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK — পাঁচটি constraint মিলে অধিকাংশ integrity issue ঠেকিয়ে দেয়। DEFAULT, generated column, এবং foreign-key action আপনাকে আরও সূক্ষ্ম নিয়ন্ত্রণ দেয়। আর domain (Postgres-এ) হলো নিজস্ব type বানানোর উপায়।

Next Module → Indexes — query speed-এর সবচেয়ে বড় lever।