Logical → Physical Schema Mapping
Logical থেকে physical schema
1. From Diagram to Database
You drew an ER diagram. You normalized to 3NF. Now you need to actually build the database — pick types, write CREATE TABLEs, choose names that survive the next ten years, and ship a migration script that you can run safely in production tomorrow morning.
This module is the practical bridge between abstract design and the real CREATE statements that engineers run in production.
2. ER → Relational Mapping Rules
A regular entity becomes a table. Its key attribute becomes the primary key.
Two options. Either merge both entities into one table (best when one side is always present) or
keep them separate and put the foreign key on the side that more often references the other.
Make the FK column UNIQUE to enforce 1:1.
UNIQUE দিন।
-- 1:1 — every employee has at most one company laptop.
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE laptops (
laptop_id INTEGER PRIMARY KEY,
model TEXT,
emp_id INTEGER UNIQUE REFERENCES employees(emp_id)
);
INSERT INTO employees VALUES (1, 'Arif');
INSERT INTO laptops VALUES (100, 'ThinkPad T14', 1);
SELECT e.name, l.model FROM employees e LEFT JOIN laptops l ON l.emp_id = e.emp_id;
The single most common case. Put the foreign key on the "many" side. A customer has many orders →
orders.customer_id references customers.customer_id.
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(customer_id),
total INTEGER
);
INSERT INTO customers VALUES (1, 'Arif'), (2, 'Sadia');
INSERT INTO orders VALUES (100, 1, 1500), (101, 1, 600), (102, 2, 2300);
SELECT c.name, COUNT(o.order_id) AS orders, SUM(o.total) AS spent
FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.name;
Create a third table — a junction table — whose primary key is the pair of foreign keys.
A student takes many courses; a course has many students → enrollments(student_id,
course_id).
CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE courses (course_id INTEGER PRIMARY KEY, title TEXT);
CREATE TABLE enrollments (
student_id INTEGER REFERENCES students(student_id),
course_id INTEGER REFERENCES courses(course_id),
enrolled_at TEXT DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id)
);
INSERT INTO students VALUES (1, 'Arif'), (2, 'Sadia');
INSERT INTO courses VALUES (101, 'DBMS'), (102, 'OS');
INSERT INTO enrollments(student_id, course_id) VALUES (1, 101), (1, 102), (2, 101);
SELECT * FROM enrollments;
A weak entity has no key of its own; it borrows from its owner. Example: an apartment building's units — unit "3A" only makes sense in the context of building 7. The unit's primary key is the composite (building_id, unit_no).
CREATE TABLE buildings (
building_id INTEGER PRIMARY KEY,
address TEXT
);
CREATE TABLE units (
building_id INTEGER,
unit_no TEXT,
floor INTEGER,
PRIMARY KEY (building_id, unit_no),
FOREIGN KEY (building_id) REFERENCES buildings(building_id) ON DELETE CASCADE
);
INSERT INTO buildings VALUES (7, '14 Road, Dhanmondi');
INSERT INTO units VALUES (7, '3A', 3), (7, '3B', 3);
SELECT * FROM units;
"A teacher IS-A person; a student IS-A person." Three mapping options:
| Strategy | What you create | When to pick |
|---|---|---|
| Single table | One persons table with all columns + kind column. | Subtypes share most columns, queries are mostly polymorphic. |
| Class-per-table | persons, teachers, students, joined by id. | Subtypes have very different columns; you want strict NOT NULL on subtype-specific fields. |
| Subtype-only | No parent table; teachers and students independently. | Subtypes never need to be queried together. |
3. Choosing Data Types — Cheap Now, Expensive Later
The wrong type is almost free to choose and very expensive to change. Pick deliberately.
| Domain | Right type | Why |
|---|---|---|
| Surrogate ID, < 2 billion rows | INTEGER / INT (4 bytes) | Cheap, fast joins, plenty of room. |
| Surrogate ID, ≥ 2 billion rows | BIGINT (8 bytes) | Logs, events, IoT — overflow risk is real. |
| UUID / external ID | UUID (Postgres) or TEXT (SQLite) | Globally unique, distributed-friendly. |
| Short string, max known | VARCHAR(n) | Postgres treats it the same as TEXT internally; in MySQL, n matters. |
| Free-form text | TEXT | Description, comments, blog body. |
| Money | NUMERIC(19, 4) / DECIMAL | Never FLOAT. Floats lie about decimals. |
| Boolean | BOOLEAN (Postgres) / INTEGER 0/1 (SQLite) | Don't use 'Y'/'N' strings. |
| Timestamp | TIMESTAMPTZ (Postgres) / TEXT ISO 8601 (SQLite) | Always store with timezone. |
| Phone number | TEXT | Leading zeros and "+" matter. |
FLOAT/DOUBLE for money. (2)
INT for "phone_number" — drops leading zeros. (3) Naive TIMESTAMP
without timezone — every Bangladesh-Bahrain debug session ends in tears.
তিনটি ফাঁদ: টাকা-পয়সার জন্য FLOAT, ফোন নম্বরের জন্য INT, এবং timezone ছাড়া TIMESTAMP — তিনটিই বহু সিস্টেমকে ভোগায়।
4. Naming Conventions — Words Matter
Names live longer than the people who picked them. A consistent, boring convention is more valuable than a clever one.
✅ Recommended (সঠিক)
- Tables:
snake_case, plural —customers,order_items. - Columns:
snake_case, singular —customer_id,created_at. - Primary key:
idor{table_singular}_idconsistently. - Foreign key:
{referenced_table_singular}_id. - Boolean: prefix with
is_orhas_. - Timestamps:
created_at,updated_at,deleted_at. - Indexes:
idx_{table}_{cols}.
⚠️ Avoid (এড়িয়ে চলুন)
tbl_,tbl_customers— already a table.- Mixed case:
OrderItemsin some DBs is case-sensitive. - Reserved words:
order,user,type. - Bangla/abbreviations:
cus_phn,oms_num. - Inconsistent IDs:
idhere,customer_nothere.
5. Migration Scripts — Idempotent and Reversible
Production databases evolve. The team adds a column on Tuesday, renames it on Wednesday, and removes a defunct table on Thursday. Migration tools manage these changes as ordered, version-controlled SQL files.
- Idempotent — running it twice is the same as running it once. Use
IF NOT EXISTS/IF EXISTSliberally. - Reversible — every "up" migration ships with a "down" that undoes it. (For destructive changes, the down might be just "restore from backup" — write that explicitly.)
-- Up migration: add a loyalty tier column. Idempotent.
CREATE TABLE IF NOT EXISTS customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- ALTER is idempotent if we check whether the column already exists.
-- (In real Flyway, the 'V' filename guarantees one-time apply.)
ALTER TABLE customers ADD COLUMN loyalty_tier TEXT DEFAULT 'bronze';
INSERT INTO customers(customer_id, name) VALUES (1, 'Arif'), (2, 'Sadia');
SELECT * FROM customers;
-- Down migration: drop the column.
-- SQLite < 3.35 cannot DROP COLUMN; use the rebuild trick.
CREATE TABLE customers_new AS
SELECT customer_id, name FROM customers;
DROP TABLE customers;
ALTER TABLE customers_new RENAME TO customers;
SELECT * FROM customers;
6. Migration Tools You Will Actually Use
| Tool | Ecosystem | How it tracks state | Style |
|---|---|---|---|
| Flyway | Java / generic JDBC | flyway_schema_history table | Plain V0001__name.sql files. Boring and reliable. |
| Liquibase | Java / generic JDBC | databasechangelog table | XML / YAML / JSON / SQL changesets. Heavy but powerful. |
| Alembic | Python (SQLAlchemy) | alembic_version table | Python migration scripts with autogenerate from ORM. |
| Knex / Sequelize | Node.js | migrations table | JavaScript migration files. |
| Goose / sqlx-migrate | Go | versions table | Lightweight, plain SQL or Go code. |
| Rails migrations | Ruby | schema_migrations | Ruby DSL. |
ALTER TABLE চালানো হবে না।
7. End-to-End — A Library Schema
Tying it all together: a library system with members (entity), books (entity), loans (relationship + extra attributes), and authors (M:N with books).
CREATE TABLE IF NOT EXISTS members (
member_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
phone TEXT UNIQUE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS authors (
author_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS books (
book_id INTEGER PRIMARY KEY,
isbn TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
total_copies INTEGER NOT NULL DEFAULT 1 CHECK(total_copies > 0)
);
-- M:N junction
CREATE TABLE IF NOT EXISTS book_authors (
book_id INTEGER REFERENCES books(book_id),
author_id INTEGER REFERENCES authors(author_id),
PRIMARY KEY (book_id, author_id)
);
-- 1:N from members and books, with relationship attributes.
CREATE TABLE IF NOT EXISTS loans (
loan_id INTEGER PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES members(member_id),
book_id INTEGER NOT NULL REFERENCES books(book_id),
loaned_at TEXT DEFAULT CURRENT_TIMESTAMP,
due_date TEXT NOT NULL,
returned_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_loans_member ON loans(member_id);
CREATE INDEX IF NOT EXISTS idx_loans_open ON loans(returned_at) WHERE returned_at IS NULL;
INSERT INTO members(member_id, name, phone) VALUES (1, 'Arif', '017xxxxxxxx');
INSERT INTO authors(author_id, name) VALUES (1, 'Humayun Ahmed');
INSERT INTO books(book_id, isbn, title) VALUES (1, '978-1', 'Misir Ali');
INSERT INTO book_authors VALUES (1, 1);
INSERT INTO loans(loan_id, member_id, book_id, due_date) VALUES (1, 1, 1, '2026-06-10');
SELECT m.name AS member, b.title, l.due_date
FROM loans l
JOIN members m ON m.member_id = l.member_id
JOIN books b ON b.book_id = l.book_id;
8. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| Logical schema | The abstract design — entities, relationships, attributes. | বিমূর্ত ডিজাইন। |
| Physical schema | Actual CREATE TABLE statements with types and indexes. | প্রকৃত CREATE TABLE। |
| Junction table | The table that resolves an M:N relationship. | M:N সম্পর্কের জন্য মধ্যবর্তী টেবিল। |
| Weak entity | An entity that needs another entity's key to be identified. | অন্য entity-র key ছাড়া identify করা যায় না। |
| ISA hierarchy | Sub-types of a more general type (Person → Student / Teacher). | শ্রেণিবদ্ধ ধারণা — Person থেকে Student, Teacher। |
| Idempotent migration | Running it twice has the same effect as once. | দ্বিতীয়বার চালানো নিরাপদ। |
| Reversible migration | Has an explicit "down" undo script. | পূর্বাবস্থায় ফেরানো যায়। |
9. Practice Problems
Ten problems covering ER mapping, type choice and migrations.
-
Map the relationship "Each Pathao rider owns at most one motorbike" to a 1:1 schema."প্রতিটি Pathao rider সর্বোচ্চ একটি motorbike-এর মালিক" — 1:1 schema বানান।
✨ Show Answer (উত্তর দেখুন)
ans1.sqlCREATE TABLE riders (rider_id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE motorbikes ( bike_id INTEGER PRIMARY KEY, plate TEXT, rider_id INTEGER UNIQUE REFERENCES riders(rider_id) ); INSERT INTO riders VALUES (1,'Arif'); INSERT INTO motorbikes VALUES (10,'DHA-1234',1); SELECT * FROM motorbikes; -
Build an M:N schema for "students take many courses; courses have many students" plus a relationship attribute
grade.student-course M:N +gradeattribute সহ schema বানান।✨ Show Answer (উত্তর দেখুন)
ans2.sqlCREATE TABLE students(student_id INTEGER PRIMARY KEY, name TEXT); CREATE TABLE courses (course_id INTEGER PRIMARY KEY, title TEXT); CREATE TABLE enrollments ( student_id INTEGER REFERENCES students(student_id), course_id INTEGER REFERENCES courses(course_id), grade TEXT, PRIMARY KEY(student_id, course_id) ); INSERT INTO students VALUES(1,'Arif'); INSERT INTO courses VALUES(101,'DBMS'); INSERT INTO enrollments VALUES(1,101,'A'); SELECT * FROM enrollments; -
"Why is
FLOATwrong for storing money?" — answer with a SQL demonstration.টাকা-পয়সার জন্য FLOAT কেন ভুল — SQL দিয়ে দেখান।✨ Show Answer (উত্তর দেখুন)
ans3.sqlSELECT 0.1 + 0.2 AS floaty, (100 + 200) / 1000.0 AS normalized_paisa; -- 0.30000000000000004 vs 0.3 — store integer paisa, not floats.Solution: store amounts as integer paisa (
amount_paisa INTEGER) and divide only at presentation time. -
Map a weak entity "ticket" that exists only in the context of a "concert"."concert" ছাড়া অর্থহীন একটি "ticket" weak entity-কে map করুন।
✨ Show Answer (উত্তর দেখুন)
ans4.sqlCREATE TABLE concerts (concert_id INTEGER PRIMARY KEY, artist TEXT); CREATE TABLE tickets ( concert_id INTEGER, seat_no TEXT, price INTEGER, PRIMARY KEY(concert_id, seat_no), FOREIGN KEY(concert_id) REFERENCES concerts(concert_id) ON DELETE CASCADE ); INSERT INTO concerts VALUES(1,'James'); INSERT INTO tickets VALUES(1,'A1',1500),(1,'A2',1500); SELECT * FROM tickets; -
Implement an ISA hierarchy "Person → Student / Teacher" with the class-per-table strategy.class-per-table পদ্ধতিতে Person → Student / Teacher schema বানান।
✨ Show Answer (উত্তর দেখুন)
ans5.sqlCREATE TABLE persons ( person_id INTEGER PRIMARY KEY, name TEXT NOT NULL, phone TEXT ); CREATE TABLE students ( person_id INTEGER PRIMARY KEY REFERENCES persons(person_id), student_no TEXT UNIQUE NOT NULL, cgpa REAL ); CREATE TABLE teachers ( person_id INTEGER PRIMARY KEY REFERENCES persons(person_id), salary INTEGER ); INSERT INTO persons VALUES(1,'Arif','017'),(2,'Karim','018'); INSERT INTO students VALUES(1,'NSU-2021',3.7); INSERT INTO teachers VALUES(2,75000); SELECT p.name, s.student_no, t.salary FROM persons p LEFT JOIN students s ON s.person_id = p.person_id LEFT JOIN teachers t ON t.person_id = p.person_id; -
Spot all naming and type problems:
CREATE TABLE Order (ID FLOAT, CustEmail VarChar(20), AmtTk FLOAT, dt DATE);উপরের CREATE statement-এ কী কী সমস্যা?✨ Show Answer (উত্তর দেখুন)
Problems: (1)
Orderis a reserved word; useorders. (2) MixedCase + plural mismatch with the rest of the schema. (3)ID FLOAT— IDs should beINTEGERorBIGINT; never floats. (4)VARCHAR(20)is too short for an email — useVARCHAR(254)orTEXT. (5)AmtTk FLOAT— money must beNUMERICor integer paisa. (6)dtis a meaningless name; usecreated_at TIMESTAMPTZ. -
Write an idempotent migration that adds a
deleted_atcolumn for soft delete on auserstable.usersটেবিলে soft-delete-এর জন্য idempotent migration লিখুন।✨ Show Answer (উত্তর দেখুন)
ans7.sqlCREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, email TEXT UNIQUE NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); ALTER TABLE users ADD COLUMN deleted_at TEXT; CREATE INDEX IF NOT EXISTS idx_users_active ON users(user_id) WHERE deleted_at IS NULL; SELECT name FROM sqlite_master WHERE type='index'; -
Pick the right type for each: order_id, product price in BDT, customer email, login timestamp, "is active" flag.প্রতিটি কলামের জন্য সঠিক type বেছে নিন।
✨ Show Answer (উত্তর দেখুন)
Answer:
order_id—BIGINT(orINTEGERfor small systems).price_paisa—INTEGER; orNUMERIC(12, 2)if you must use decimals.email—VARCHAR(254)/TEXT.last_login_at—TIMESTAMPTZin Postgres, ISO-8601TEXTin SQLite.is_active—BOOLEAN(Postgres) orINTEGER0/1 (SQLite).
-
Why might Flyway-style "V0001__name.sql" filenames be safer than "ALTER TABLE in a chat message"?"chat-এ ALTER TABLE পাঠানোর" চেয়ে Flyway filename কেন বেশি নিরাপদ?
✨ Show Answer (উত্তর দেখুন)
Answer: (1) Migrations are version-controlled — every environment runs the same script, in the same order. (2) Flyway records what it has already run; rerunning is a no-op. (3) Code review catches bad SQL before it touches production. (4) New environments (staging, hot-fix branch) can be rebuilt from the same migration history. A chat message has none of these properties.
-
Reverse-engineer the ER diagram implied by this SQL:
CREATE TABLE order_items(order_id INTEGER REFERENCES orders(order_id), product_id INTEGER REFERENCES products(product_id), qty INTEGER, PRIMARY KEY(order_id, product_id));উপরের SQL থেকে ER diagram বের করুন।✨ Show Answer (উত্তর দেখুন)
Answer: Two strong entities —
ordersandproducts— connected by an M:N relationship "contains" with attributeqty. The composite primary key on(order_id, product_id)tells us each pair appears at most once per order, which is the natural meaning of an order line. Theorder_itemstable is a junction table.
Summary — Module 29
Translating an ER design into a real database is a series of routine, learnable rules: strong entities become tables, 1:N puts the FK on the "many" side, M:N spawns a junction table, weak entities take a composite key, and ISA hierarchies have three known mapping strategies. Pair this with disciplined type choice, boring naming and version-controlled migrations, and your schema will outlive several rewrites of the application above it.