Logical → Physical Schema Mapping

Logical থেকে physical schema

Read: ~36 min Medium 10 practice problems Live SQLite runner

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.

ER diagram এঁকেছেন, 3NF-এ এনেছেন। এবার বাস্তব database তৈরি করতে হবে — সঠিক type বেছে নিতে হবে, CREATE TABLE লিখতে হবে, এমন নাম রাখতে হবে যা ১০ বছর পরও বোঝা যায়, এবং একটি migration script লিখতে হবে যা production-এ নিরাপদে চালানো যায়।

This module is the practical bridge between abstract design and the real CREATE statements that engineers run in production.

2. ER → Relational Mapping Rules

Strong entities

A regular entity becomes a table. Its key attribute becomes the primary key.

1:1 relationships

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.

1:1 সম্পর্ক: দুটি entity-কে একটি টেবিলে মিশিয়ে দিন (যদি একপাশ সবসময় থাকে), অথবা আলাদা টেবিলে রেখে FK কলামে UNIQUE দিন।
one-to-one.sql
-- 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;
1:N relationships

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.

one-to-many.sql
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;
M:N relationships

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).

many-to-many.sql
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;
Weak entities

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).

weak-entity.sql
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;
ISA / inheritance

"A teacher IS-A person; a student IS-A person." Three mapping options:

StrategyWhat you createWhen to pick
Single tableOne persons table with all columns + kind column.Subtypes share most columns, queries are mostly polymorphic.
Class-per-tablepersons, teachers, students, joined by id.Subtypes have very different columns; you want strict NOT NULL on subtype-specific fields.
Subtype-onlyNo parent table; teachers and students independently.Subtypes never need to be queried together.
ISA-র জন্য তিনটি কৌশল — সব subtype একটি টেবিলে, প্রতি subtype-এর জন্য আলাদা টেবিল (parent-এর সাথে join), অথবা parent ছাড়াই subtype-গুলো আলাদা। বেশিরভাগ ক্ষেত্রে class-per-table সবচেয়ে কম bug-prone।

3. Choosing Data Types — Cheap Now, Expensive Later

The wrong type is almost free to choose and very expensive to change. Pick deliberately.

DomainRight typeWhy
Surrogate ID, < 2 billion rowsINTEGER / INT (4 bytes)Cheap, fast joins, plenty of room.
Surrogate ID, ≥ 2 billion rowsBIGINT (8 bytes)Logs, events, IoT — overflow risk is real.
UUID / external IDUUID (Postgres) or TEXT (SQLite)Globally unique, distributed-friendly.
Short string, max knownVARCHAR(n)Postgres treats it the same as TEXT internally; in MySQL, n matters.
Free-form textTEXTDescription, comments, blog body.
MoneyNUMERIC(19, 4) / DECIMALNever FLOAT. Floats lie about decimals.
BooleanBOOLEAN (Postgres) / INTEGER 0/1 (SQLite)Don't use 'Y'/'N' strings.
TimestampTIMESTAMPTZ (Postgres) / TEXT ISO 8601 (SQLite)Always store with timezone.
Phone numberTEXTLeading zeros and "+" matter.
Three traps. (1) 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: id or {table_singular}_id consistently.
  • Foreign key: {referenced_table_singular}_id.
  • Boolean: prefix with is_ or has_.
  • Timestamps: created_at, updated_at, deleted_at.
  • Indexes: idx_{table}_{cols}.

⚠️ Avoid (এড়িয়ে চলুন)

  • tbl_, tbl_customers — already a table.
  • Mixed case: OrderItems in some DBs is case-sensitive.
  • Reserved words: order, user, type.
  • Bangla/abbreviations: cus_phn, oms_num.
  • Inconsistent IDs: id here, customer_no there.
Naming-এ একটি নিয়ম মেনে চলুন: snake_case, plural-table / singular-column, প্রতিটি FK = referenced_singular_id, প্রতিটি timestamp = created_at / updated_at। সংক্ষেপ এড়ান, reserved word এড়ান, এবং দশ বছর পর কোডটি যেন আপনিই পড়তে পারেন।

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.

Production database প্রতিদিন পরিবর্তিত হয়। প্রতিটি পরিবর্তনকে version-controlled, ordered SQL ফাইল হিসেবে সংরক্ষণ করার জন্যই migration tool।
Two qualities every migration must have
  1. Idempotent — running it twice is the same as running it once. Use IF NOT EXISTS / IF EXISTS liberally.
  2. 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.)
V0023__add_customer_loyalty.sql (up)
-- 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;
U0023__remove_customer_loyalty.sql (down)
-- 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

ToolEcosystemHow it tracks stateStyle
FlywayJava / generic JDBCflyway_schema_history tablePlain V0001__name.sql files. Boring and reliable.
LiquibaseJava / generic JDBCdatabasechangelog tableXML / YAML / JSON / SQL changesets. Heavy but powerful.
AlembicPython (SQLAlchemy)alembic_version tablePython migration scripts with autogenerate from ORM.
Knex / SequelizeNode.jsmigrations tableJavaScript migration files.
Goose / sqlx-migrateGoversions tableLightweight, plain SQL or Go code.
Rails migrationsRubyschema_migrationsRuby DSL.
পছন্দের tool আপনার stack অনুযায়ী — Java-তে Flyway, Python-এ Alembic, Node-এ Knex। আসল কথা হলো — সব schema পরিবর্তন version-controlled থাকবে, manually কোনো production database-এ 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).

V0001__library.sql
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 (শব্দকোষ)

TermMeaningবাংলায়
Logical schemaThe abstract design — entities, relationships, attributes.বিমূর্ত ডিজাইন।
Physical schemaActual CREATE TABLE statements with types and indexes.প্রকৃত CREATE TABLE।
Junction tableThe table that resolves an M:N relationship.M:N সম্পর্কের জন্য মধ্যবর্তী টেবিল।
Weak entityAn entity that needs another entity's key to be identified.অন্য entity-র key ছাড়া identify করা যায় না।
ISA hierarchySub-types of a more general type (Person → Student / Teacher).শ্রেণিবদ্ধ ধারণা — Person থেকে Student, Teacher।
Idempotent migrationRunning it twice has the same effect as once.দ্বিতীয়বার চালানো নিরাপদ।
Reversible migrationHas an explicit "down" undo script.পূর্বাবস্থায় ফেরানো যায়।

9. Practice Problems

Ten problems covering ER mapping, type choice and migrations.

ER mapping, type পছন্দ এবং migration নিয়ে ১০টি প্রশ্ন।
  1. 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.sql
    CREATE 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;
  2. Build an M:N schema for "students take many courses; courses have many students" plus a relationship attribute grade.
    student-course M:N + grade attribute সহ schema বানান।
    ✨ Show Answer (উত্তর দেখুন)
    ans2.sql
    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),
        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;
  3. "Why is FLOAT wrong for storing money?" — answer with a SQL demonstration.
    টাকা-পয়সার জন্য FLOAT কেন ভুল — SQL দিয়ে দেখান।
    ✨ Show Answer (উত্তর দেখুন)
    ans3.sql
    SELECT 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.

  4. Map a weak entity "ticket" that exists only in the context of a "concert".
    "concert" ছাড়া অর্থহীন একটি "ticket" weak entity-কে map করুন।
    ✨ Show Answer (উত্তর দেখুন)
    ans4.sql
    CREATE 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;
  5. Implement an ISA hierarchy "Person → Student / Teacher" with the class-per-table strategy.
    class-per-table পদ্ধতিতে Person → Student / Teacher schema বানান।
    ✨ Show Answer (উত্তর দেখুন)
    ans5.sql
    CREATE 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;
  6. Spot all naming and type problems: CREATE TABLE Order (ID FLOAT, CustEmail VarChar(20), AmtTk FLOAT, dt DATE);
    উপরের CREATE statement-এ কী কী সমস্যা?
    ✨ Show Answer (উত্তর দেখুন)

    Problems: (1) Order is a reserved word; use orders. (2) MixedCase + plural mismatch with the rest of the schema. (3) ID FLOAT — IDs should be INTEGER or BIGINT; never floats. (4) VARCHAR(20) is too short for an email — use VARCHAR(254) or TEXT. (5) AmtTk FLOAT — money must be NUMERIC or integer paisa. (6) dt is a meaningless name; use created_at TIMESTAMPTZ.

  7. Write an idempotent migration that adds a deleted_at column for soft delete on a users table.
    users টেবিলে soft-delete-এর জন্য idempotent migration লিখুন।
    ✨ Show Answer (উত্তর দেখুন)
    ans7.sql
    CREATE 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';
  8. 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 (or INTEGER for small systems).
    • price_paisa — INTEGER; or NUMERIC(12, 2) if you must use decimals.
    • email — VARCHAR(254) / TEXT.
    • last_login_at — TIMESTAMPTZ in Postgres, ISO-8601 TEXT in SQLite.
    • is_active — BOOLEAN (Postgres) or INTEGER 0/1 (SQLite).
  9. 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.

  10. 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 — orders and products — connected by an M:N relationship "contains" with attribute qty. 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. The order_items table 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.

ER design থেকে বাস্তব database-এ পৌঁছানোর পথ মূলত কয়েকটি নিয়ম মেনে চলার বিষয় — strong entity = table, 1:N-এ FK "many"-পাশে, M:N-এ junction table, weak entity-তে composite key, এবং ISA-র জন্য তিনটি কৌশল। সঙ্গে সঠিক type, সঙ্গত naming এবং version-controlled migration থাকলে schema দশ বছরও টিকে যাবে।

Next Module → Transactions ও ACID — সব-অথবা-কিছুই-নয়, বিশ্বাসের ভিত্তি।