Skip to content

Entities, attributes, relationships

SQL-এর আগে, ORM-এর আগে, box আর line আঁকুন। ER-চিন্তা হলো বিশ মিনিট যা এক বছরের refactoring বাঁচায়।

Data modeling-এ সবচেয়ে সস্তা debugging tool হলো একটা পেন্সিল। CREATE TABLE লেখার আগে একটা sketch আঁকুন — জিনিসের জন্য box, তাদের মধ্যে line, দিকের জন্য arrowhead, cardinality-র জন্য শব্দ। বিশ মিনিট sketching এমন দ্ব্যর্থতা বের করে আনে যা দুই মাসের code ঠিক করতে পারে না।

এই অধ্যায় হলো entity আর relationship-এ ভাবার জন্য যে ছোট vocabulary দরকার, সাথে relationship-এর চারটা shape যা বাস্তব model-এর ৯৫% cover করে।

বাস্তব জগতের উপমা

বাস্তব জীবনে একটা social network — মানুষ (entities) যারা “একসাথে কাজ করে” বা “বিবাহিত” এমন relationship দিয়ে যুক্ত।

গল্পে বুঝি

কর্ডোবার একটা স্কুলের অফিসে ফাতিমা একটা মোটা খাতা খুলে বসেছেন। খাতার প্রথম অংশে তিনি লিখছেন প্রতিটা ছাত্রের কথা — কার নাম খোয়ারিজমি, রোল কত, বয়স কত। আরেকটা অংশে লিখছেন স্কুলের ক্লাসগুলোর কথা — কোন ক্লাস, কোন ঘরে বসে। দুটো আলাদা তালিকা, দুই রকমের “জিনিস” যা অফিস আলাদা করে মনে রাখতে চায়।

কিন্তু আসল কাজ শুরু হয় তৃতীয় খাতায়। সেখানে ফাতিমা লিখে রাখছেন কে কোন ক্লাসে “ভর্তি” — সিনা গণিতেও আছে, দর্শনেও আছে; আবার গণিত ক্লাসে সিনার পাশে আরও তিরিশজন ছাত্র। এক ক্লাসে অনেক ছাত্র, আবার এক ছাত্র অনেক ক্লাসে। তাই ছাত্র আর ক্লাসকে এক তালিকায় গুঁজে দেওয়া যায় না; ওই “ভর্তি” সম্পর্কটাই আলাদা করে লিখে রাখতে হয়।

এই তিন খাতাই আসলে data model। ছাত্র আর ক্লাস হলো দুটো entity — অফিস যে “জিনিস” নিয়ে জানে। প্রতিটা ছাত্রের নাম, রোল আর বয়স হলো তার attribute (property)। “ভর্তি” হলো ছাত্র আর ক্লাসের মধ্যেকার relationship। আর যেহেতু এক ক্লাসে অনেক ছাত্র (one-to-many) এবং এক ছাত্র অনেক ক্লাসে (মিলিয়ে many-to-many), সেটাই এই relationship-এর cardinality। বাস্তবে ঠিক এভাবেই একটা school management system কাজ করে — students আর classes দুটো table, আর মাঝে একটা enrollment table যা কে কোন ক্লাসে আছে তা ধরে রাখে।

Vocabulary

Entity — system যে জিনিসটা সম্পর্কে জানে। User, Order, Listing, Comment। SQL-এ, সাধারণত একটা table।

Attribute — একটা entity-র একটা property। email, name, created_at। SQL-এ, একটা column।

Relationship — দুটো entity কীভাবে সম্পর্কিত। এক user-এর অনেক order. একটা listing এক seller-এর. SQL-এ, foreign key (নয়তো একটা join table)।

Cardinality — এক দিকের কতগুলো অন্য দিকের কতগুলোর সাথে সম্পর্কিত। চারটা shape:

Cardinalityউদাহরণ
1 : 1একটা user-এর ঠিক একটা user_settings row থাকে
1 : Nএকটা user-এর অনেক orders; প্রতিটা order-এর এক user
N : 11 : N-এর উল্টো
N : Mএকটা user অনেক listings favorite করে; প্রতিটা listing অনেক users favorite করে

এটাই পুরো vocabulary। পাঁচটা term।

এটা আঁকা

দুটো ব্যবহারিক notation।

ER diagram (formal). ভেতরে attribute সহ box; মাঝে cardinality-র জন্য crow’s-foot symbol সহ line।

Mermaid (in-code). Markdown-বান্ধব:

erDiagram
  USER ||--o{ ORDER : places
  USER ||--o{ LISTING : sells
  LISTING ||--o{ ORDER : "ordered as"
  USER }o--o{ LISTING : favorites

পড়ুন: ||--o{ হলো one-to-many; }o--o{ হলো many-to-many। Arrow-গুলো গুরুত্বপূর্ণ নয় — প্রতি প্রান্তের symbol-ই আসল।

Whiteboard sketch (সবচেয়ে দ্রুত). শুধু box, line, আর “1” বা “N” label। কোনো tooling লাগে না। ৮০% design আলোচনার জন্য, আপনি আসলে এটাই ব্যবহার করেন।

একটা বাছুন আর ধারাবাহিকভাবে ব্যবহার করুন। Sketch হলো আপনার জন্য আর যাদের সাথে design করছেন তাদের জন্য। এটা deliverable নয়; এটা একটা চিন্তার সহায়ক।

Entity চেনা — পরীক্ষা

কোনো কিছু entity নাকি attribute তা জানার দুটো উপায়।

পরীক্ষা ১: এর কি নিজস্ব জীবন আছে? একজন user log in করে, email পায়, তার settings আছে। স্বাধীন অস্তিত্ব → entity। একজন user-এর birthday-র settings নেই বা email পায় না; এটা নিছক user-এর সাথে সংযুক্ত একটা তারিখ → attribute।

পরীক্ষা ২: এর কি নিজস্ব children থাকবে? একটা address-কে user-এর একটা attribute মনে হয়। কিন্তু যদি এক user-এর একাধিক address থাকে (shipping, billing)? এখন address-এর নিজস্ব children আছে — এটা একটা entity (addresses table) user_id foreign key সহ।

সন্দেহ হলে, attribute হিসেবে শুরু করুন। পরে entity-তে উন্নীত করা একটা এক-ধাপের migration; একটা entity-কে attribute-এ নামানো কদাচিৎ সঠিক পদক্ষেপ।

চারটা shape

1 : 1 — কম ব্যবহার করা হয়

CREATE TABLE users (id BIGSERIAL PRIMARY KEY, ...);
CREATE TABLE user_settings (
  user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  notifications_enabled BOOLEAN NOT NULL DEFAULT true,
  ...
);

user_settings.user_id একই সাথে primary key এবং foreign key। প্রতিটা user-এর user_settings-এ ঠিক একটা row। কেন ভাগ করবেন:

  • Performance / row width। ৩০ column-এর একটা users table যার একটা হলো একটা 50KB JSON blob যা কদাচিৎ query হয় — blob-টাকে আলাদা table-এ ভাগ করুন।
  • Optionality। Settings শুধু user settings page খোলার পরই থাকে। একটা row থাকা/না-থাকা state-এর সংকেত দেয়।
  • আলাদা access pattern। Settings প্রতি session-এ একবার পড়া হয়; user identity প্রতিটা request-এ পড়া হয়।

বেশিরভাগ 1:1 relationship একটা লক্ষণ যে আপনার শুধু parent table-এ column যোগ করা উচিত। তিনটা কারণের একটা খাটলেই কেবল আলাদা table-এর দিকে হাত বাড়ান।

1 : N — কাজের ঘোড়া

CREATE TABLE users (id BIGSERIAL PRIMARY KEY, ...);
CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  buyer_id BIGINT NOT NULL REFERENCES users(id),
  ...
);

Foreign key থাকে “many” দিকে। এক user, অনেক order।

সিদ্ধান্তের বিন্দুগুলো:

  • ON DELETE আচরণ। ইচ্ছাকৃতভাবে বাছুন (অধ্যায় ৬ এটা cover করে)।
  • Indexed? হ্যাঁ — foreign key সবসময় index করুন। CREATE INDEX ON orders(buyer_id);। এটা ছাড়া, “user 42-র সব order খোঁজো” হলো একটা full table scan।
  • Required নাকি optional? NOT NULL বলে প্রতিটা order-এর একটা buyer থাকতেই হবে। Nullable বলে কিছু order buyer-হীন (“guest checkout”)। স্পষ্ট থাকুন।

N : 1 — একই shape, ভিন্ন প্রশ্ন

N : 1 হলো নিছক 1 : N many দিক থেকে দেখা। “একটা order এক user-এর” বনাম “এক user-এর অনেক order” একই relationship।

এই framing যে প্রশ্ন সামনে আনে: child থেকে parent কীভাবে পাই? Foreign key দিয়ে একটা SQL JOIN। সেটা যদি একটা hot path হয়, তাহলে foreign key index-ই একটা 1ms query আর একটা 1s query-র মধ্যে পার্থক্য।

N : M — একটা join table লাগে

“Many-to-many”-কে একটা একক foreign key হিসেবে প্রকাশ করার কোনো উপায় নেই। আপনার একটা তৃতীয় table লাগবে।

CREATE TABLE users (id BIGSERIAL PRIMARY KEY, ...);
CREATE TABLE listings (id BIGSERIAL PRIMARY KEY, ...);

CREATE TABLE user_listing_favorites (
  user_id     BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  listing_id  BIGINT NOT NULL REFERENCES listings(id) ON DELETE CASCADE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (user_id, listing_id)
);

Join table:

  • দুটো foreign key-এর composite primary key নিশ্চিত করে কোনো duplicate favorite নেই।
  • দুই দিকেই ON DELETE CASCADE — user বা listing যেটাই delete হোক, favorite মুছে যায়। সাধারণত সঠিক পছন্দ।
  • প্রায়ই নিজের attribute পায়। created_at স্বাভাবিক। কখনও একটা note field। Join table আঠা হিসেবে শুরু হয়ে নিজের একটা entity-তে বড় হয়।

দুটো foreign key-ই index করুন (Postgres কেবল composite PK-র প্রথম column auto-index করে):

CREATE INDEX ON user_listing_favorites(listing_id);

এই index ছাড়া, “এই listing কে favorite করেছে?” হলো একটা full scan।

N:M table সবসময় স্পষ্টভাবে নাম দিন। user_listing_favorites users_listings-এর চেয়ে ভালো পড়ায়। Verb অন্তর্ভুক্ত করলে intent পরিষ্কার হয় আর একই জোড়ার একাধিক relationship থাকলে (“favorites” vs “blocks” vs “follows”) name conflict এড়ানো যায়।

যখন relationship-এর নিজস্ব attribute দরকার

কখনও relationship নিজেই এমন property রাখে যা কোনো entity-তেই মানায় না।

CREATE TABLE org_memberships (
  user_id     BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  org_id      BIGINT NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
  role        TEXT NOT NULL CHECK (role IN ('owner','admin','member','guest')),
  joined_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  invited_by  BIGINT REFERENCES users(id),
  PRIMARY KEY (user_id, org_id)
);

role users-এ মানায় না (এক user-এর ভিন্ন org-এ ভিন্ন role থাকতে পারে) বা orgs-এ মানায় না (এক org-এর অনেক role, প্রতি member-এ একটা)। এটা মানায় membership-এ — relationship-এর নিজেই।

Join table যখন একটা আসল entity-র মতো মনে হতে শুরু করে, তখন একে নিজের একটা surrogate key দিন (অধ্যায় ৩) আর বাড়তে দিন:

CREATE TABLE org_memberships (
  id          BIGSERIAL PRIMARY KEY,
  user_id     BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  org_id      BIGINT NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
  role        TEXT NOT NULL,
  joined_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (user_id, org_id)
);

এখন membership-এর নিজের ID আছে, আর unique constraint “কোনো duplicate নেই” বজায় রাখে। দুটো রূপই বৈধ; দ্বিতীয়টা বেশি নমনীয়।

Polymorphic relationship — সাধারণত একটা গন্ধ

একটা সাধারণ প্রলোভন:

CREATE TABLE comments (
  id BIGSERIAL PRIMARY KEY,
  commentable_type TEXT NOT NULL,  -- 'post', 'photo', 'video'
  commentable_id   BIGINT NOT NULL,
  body TEXT NOT NULL
);

“একটা comment একটা post, একটা photo, বা একটা video-তে যুক্ত হতে পারে।” দেখতে সুন্দর। তিনটা সমস্যা আছে:

  1. কোনো foreign key constraint নেই। commentable_type-এর ওপর নির্ভর করে commentable_id আলাদা table-কে reference করে। Row-টা আছে কিনা DB enforce করতে পারে না। Orphan comment জমতে থাকে।
  2. Query করা কঠিন। “সব comment-করা item”-এর জন্য একাধিক table জুড়ে একটা UNION লাগে, তারপর comments-এ back-join।
  3. Index-গুলো বেঢপ। একটা (commentable_type, commentable_id) index কাজ করে কিন্তু কিছু optimizer সচেতনতা হারায়।

আরও ভালো: প্রতি parent type-এ একটা comment table, নয়তো explicit nullable FK সহ একটা table:

CREATE TABLE comments (
  id BIGSERIAL PRIMARY KEY,
  body TEXT NOT NULL,
  post_id  BIGINT REFERENCES posts(id) ON DELETE CASCADE,
  photo_id BIGINT REFERENCES photos(id) ON DELETE CASCADE,
  video_id BIGINT REFERENCES videos(id) ON DELETE CASCADE,
  CHECK (
    (post_id IS NOT NULL)::int +
    (photo_id IS NOT NULL)::int +
    (video_id IS NOT NULL)::int = 1
  )
);

তিনটা nullable FK; CHECK নিশ্চিত করে ঠিক একটা set আছে। আসল foreign key, আসল cascade আচরণ। সামান্য বেশি column; সঠিক।

খুব dynamic system-এর জন্য (runtime-এ অনেক parent type যোগ হয়), কঠোর app-level check সহ একটা single-table polymorphic design-ই একমাত্র option হতে পারে। কিন্তু একে default হিসেবে এড়িয়ে চলুন।

Self-referencing relationship

এক user আরেক user-কে follow করে। একটা folder অন্য folder ধারণ করে। একটা reply একটা parent comment-এর। কৌশল: foreign key একই table-কে নির্দেশ করে।

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  ...
);

CREATE TABLE follows (
  follower_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  followee_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (follower_id, followee_id),
  CHECK (follower_id <> followee_id)
);

একই table-কে reference করা দুটো column; CHECK self-follow ঠেকায়।

Tree structure-এর জন্য — subcategory সহ category, comment thread — একটা parent_id self-FK হলো সরল shape:

CREATE TABLE comments (
  id        BIGSERIAL PRIMARY KEY,
  parent_id BIGINT REFERENCES comments(id) ON DELETE CASCADE,
  ...
);

এটা একটা ক্লাসিক adjacency list। কয়েক হাজার node পর্যন্ত tree-র জন্য ঠিক আছে। গভীরতর বা চওড়া tree path-based বা recursive-CTE pattern থেকে উপকার পায়, যা advanced data-modeling উপাদানে cover করা হয়েছে।

যে modeling টিপস কাজে দেয়

Relationship পরিষ্কারভাবে নাম দিন। একটা listing যখন এক “seller”-এর, তখন seller_id user_id-র চেয়ে ভালো পড়ায়। একজন user যদি buyer আর seller দুটোই হতে পারে, তাহলে দুই নামই intent জানায়।

একই table-এ দুটো FK-র আলাদা নাম দরকার। একটা order-এ buyer_id আর seller_id (দুটোই → users.id) হলো standard shape। user_id পুনর্ব্যবহার করবেন না।

FK-কে দুই প্রান্তেই denormalize করবেন না। একটা সাধারণ ফাঁদ: users-এ latest_order_id রাখা যাতে orders table query করতে না হয়। এখন আপনার দুটো source of truth আছে যেগুলো সরে যায়। শুধু তখনই denormalize করুন যখন আপনি একটা আসল read-pattern লাভ মাপেন (অধ্যায় ৫)।

Cardinality সৎ রাখুন। আপনি যদি একটা relationship-কে 1:1 চিহ্নিত করেন কিন্তু পরে 1:N দরকার হয়, migration কষ্টকর। অস্পষ্ট ক্ষেত্রে default হিসেবে 1:N (বাড়তি table) নিলে আপনি নমনীয়তা পান।

সারসংক্ষেপ

  • পাঁচটা term: entity, attribute, relationship, cardinality, primary key।
  • চারটা shape: 1:1, 1:N, N:1, N:M। SQL লেখার আগে এগুলো sketch করুন।
  • 1:1 বিরল আর সাধারণত ভুল — আসল কারণ না থাকলে এক table-এ মিলিয়ে ফেলুন।
  • 1:N হলো কাজের ঘোড়া। Foreign key many দিকে। সবসময় indexed।
  • N:M-এ join table লাগে; composite PK; দুই column-ই index করুন।
  • Attribute বাড়ানো join table নিজের ID সহ আসল entity হয়ে যায়।
  • Polymorphic FK সাধারণত একটা গন্ধ — একাধিক typed FK পছন্দ করুন।
  • Self-reference একটা CHECK দূরত্বে ভাঙা থেকে — id = parent_id-এর বিরুদ্ধে পাহারা দিন।
  • Naming আর explicit cardinality আপনাকে সবচেয়ে বেশি পুনরায় কাজ থেকে বাঁচায়।

পরবর্তী: Keys — এমন identifier বাছা যা সময়ের সাথে টিকে যায়।