The Relational Model & Basic Queries
Table, row, column, type — আর যে SELECT statement দিয়ে ডেটা আবার টেনে বের করা হয়।
গল্পে বুঝি
ধরুন আপনি একটা স্কুলের অফিসে বসে আছেন। কেরানি ইবনে সিনা সাহেবের টেবিলে দুটো আলাদা রেজিস্টার খাতা। একটা “ছাত্র রেজিস্টার” — প্রতি পাতায় এক লাইনে একজন ছাত্রের তথ্য: রোল নম্বর, নাম, বয়স, কোন ক্লাসে পড়ে তার ক্লাস-আইডি। প্রতিটা লাইনের বাঁয়ে বসানো রোল নম্বরটাই ওই ছাত্রকে আলাদা করে চেনায় — আল-খোয়ারিজমি আর ফাতিমা আল-ফিহরির নাম এক হলেও রোল আলাদা, তাই কখনো গুলিয়ে যায় না। আরেকটা খাতা “ক্লাস রেজিস্টার” — এক লাইনে এক ক্লাস: ক্লাস-আইডি, ক্লাসের নাম, ঘরের নম্বর।
এখন হেডস্যার এসে বললেন, “ক্লাস নাইনের সব ছাত্রের একটা লিস্ট দাও।” ইবনে সিনা সাহেব ছাত্র রেজিস্টারটা খুলে উপর থেকে নিচে চোখ বোলান, আর শুধু যাদের ক্লাস-আইডি নাইনের সাথে মেলে তাদের লাইনগুলোই আলাদা কাগজে টুকে নেন — বাকিদের বাদ। কারো ক্লাসের ঘর কোথায় জানতে হলে ছাত্রের ক্লাস-আইডি দিয়ে ক্লাস রেজিস্টারে গিয়ে মিলিয়ে দেখেন। দুই খাতা এই আইডি দিয়েই একে অপরের সাথে বাঁধা।
গল্পের প্রতিটা রেজিস্টার খাতা আসলে একটা table, খাতার এক-একটা লাইন হলো একটা row, আর রোল/নাম/বয়সের মতো ঘরগুলো হলো column। রোল নম্বরটা প্রতিটা row-কে unique করে চেনায় — এটাই key। দুই খাতাকে ক্লাস-আইডি দিয়ে জোড়া লাগানোটাই দুই table-এর মধ্যে relation। আর “শুধু ক্লাস নাইনের লাইনগুলো টুকে নাও” — এটাই SELECT ... WHERE: পুরো table থেকে শর্তে মেলা row-গুলো বেছে নেওয়া। বাস্তবে একটা database ঠিক এভাবেই কাজ করে, শুধু ইবনে সিনা সাহেবের হাতে চোখ বোলানোর বদলে কাজটা মিলিসেকেন্ডে সেরে ফেলে — স্কুলের হাজার হাজার ছাত্রের হাজিরা বা রেজাল্ট সিস্টেম এই একই ধারণার উপর দাঁড়িয়ে।
The Relational Model
একটা relational database ডেটা রাখে table-এ (আনুষ্ঠানিকভাবে, relation)। একটা table হলো একটা grid:
- একটা row (বা tuple) হলো একটা একক record — একজন user, একটা order, একটা event।
- একটা column (বা attribute) প্রতিটা row-এর একটা property বর্ণনা করে — একটা নাম, একটা দাম, একটা created timestamp।
- প্রতিটা column-এর একটা data type থাকে, যা ঠিক করে দেয় সেটা কী কী value ধরে রাখতে পারবে।
এই মডেলের শুরু Edgar Codd-এর ১৯৭০ সালের পেপার থেকে, আর এর শক্তি আসে একটা সহজ ধারণা থেকে: ডেটা মানে শুধু row-এর কিছু set, আর আপনি বর্ণনা করেন কী চান, কীভাবে আনতে হবে সেটা নয়। “কীভাবে”-টা database নিজেই বের করে নেয়।
users নামের একটা table দেখতে এমন হতে পারে:
| id | age | created_at | |
|---|---|---|---|
| 1 | lubna@example.com | 36 | 2026-01-04 09:12:00 |
| 2 | nusayba@example.com | 41 | 2026-02-18 14:30:00 |
| 3 | harun@example.com | 2026-03-01 08:00:00 |
লক্ষ্য করুন row 3-এর কোনো age নেই — ওই খালি cell-টা একটা NULL, যেটায় আমরা আবার ফিরব।
Data Types
সঠিক type বেছে নেওয়া গুরুত্বপূর্ণ: এটা storage size নিয়ন্ত্রণ করে, কোন operation বৈধ তা ঠিক করে, আর database কীভাবে value sort ও compare করবে সেটা নির্ধারণ করে। PostgreSQL-এর কমন কিছু type:
| Type | যার জন্য |
|---|---|
integer / bigint | পূর্ণসংখ্যা, ID, count |
numeric(p, s) | নির্ভুল দশমিক — টাকাপয়সা, এখানে কখনো float নয় |
text / varchar(n) | String |
boolean | True/false flag |
date / timestamptz | তারিখ আর timezone-সচেতন timestamp |
uuid | Globally unique identifier |
jsonb | Semi-structured document |
টাকাপয়সা কখনো floating point-এ রাখবেন না। float আর double 0.1-এর মতো value নির্ভুলভাবে ধরে রাখতে পারে না, তাই total এক সেন্টের ভগ্নাংশ পরিমাণে সরে যায়। যেখানে নির্ভুলতা জরুরি সেখানে numeric (যাকে decimal-ও বলে) ব্যবহার করুন।
Creating a Table (DDL)
যেসব statement structure সংজ্ঞায়িত করে — CREATE, ALTER, DROP — সেগুলোকে বলা হয় DDL (Data Definition Language)।
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
age integer,
created_at timestamptz NOT NULL DEFAULT now()
); একটু ভেঙে দেখা যাক:
PRIMARY KEYid-কে প্রতিটা row-এর unique identifier হিসেবে চিহ্নিত করে; এটা স্বয়ংক্রিয়ভাবেNOT NULLআর indexed।GENERATED ALWAYS AS IDENTITYPostgres-কে দিয়ে আপনার হয়ে ধারাবাহিক id বসিয়ে দেয়।NOT NULLওই column-এ missing value নিষিদ্ধ করে।UNIQUEদুটো row-কে একই email শেয়ার করতে দেয় না।DEFAULT now()আপনি না দিলেcreated_atস্বয়ংক্রিয়ভাবে ভরে দেয়।
Row insert করতে DML (Data Manipulation Language) ব্যবহার হয়:
INSERT INTO users (email, age) VALUES
('lubna@example.com', 36),
('nusayba@example.com', 41),
('harun@example.com', NULL); SELECT: Reading Data
SELECT statement হলো আপনার প্রশ্ন করার উপায়। এর মূল রূপ:
SELECT email, age
FROM users
WHERE age > 30
ORDER BY age DESC
LIMIT 10; প্রতিটা clause একটা করে কাজ করে:
SELECT— কোন column ফেরত দেওয়া হবে।SELECT *সবগুলো ফেরত দেয় (ইন্টারঅ্যাক্টিভভাবে সুবিধাজনক, তবে application code-এ column-গুলো স্পষ্ট করে লিখুন, যাতে পরে একটা column যোগ করলে সেটা আপনাকে চমকে না দেয়)।FROM— কোন table থেকে পড়া হবে।WHERE— একটা filter; শুধু যেসব row-তে শর্তটা সত্য, সেগুলোই ফেরত আসে।ORDER BY— sort করার ক্রম,ASC(default) বাDESC।LIMIT— কতগুলো row ফেরত আসবে তার সীমা।
Filtering with WHERE
WHERE-এর শর্তগুলো AND, OR আর NOT দিয়ে comparison জোড়া লাগায়:
SELECT * FROM users
WHERE age >= 18 AND age < 65;
SELECT * FROM users
WHERE email LIKE '%@example.com';
SELECT * FROM users
WHERE age IN (36, 41, 50);
SELECT * FROM users
WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31'; যখন আপনি একটা column-কে একটা literal-এর সাথে তুলনা করেন — যেমন, “৩০-এর কম বয়সী সবাইকে দাও” — সেটা prose-এ হয় WHERE age < 30, কিন্তু একটা code block-এর ভেতরে আপনি সাধারণভাবে শুধু age < 30 লিখবেন। LIKE pattern matching করে, যেখানে % মানে “যেকোনো ক্রমের অক্ষর”।
DISTINCT
DISTINCT রেজাল্ট থেকে ডুপ্লিকেট row সরিয়ে দেয়:
SELECT DISTINCT age FROM users ORDER BY age; এটা প্রতিটা আলাদা age একবার করে ফেরত দেয়। DISTINCT পুরো selected row-এর ওপর প্রযোজ্য, তাই SELECT DISTINCT age, email দুটো column-এর সমন্বয়ের ওপর দেখে deduplicate করে।
NULL Semantics
NULL মানে “অজানা” বা “অনুপস্থিত” — এটা zero নয়, আর empty string-ও নয়। শুরুতে এটা প্রায় সবাইকে বিভ্রান্ত করে, কারণ comparison-এ NULL কোনো value-এর মতো আচরণ করে না।
NULL-এর সাথে যেকোনো comparison-এর ফল NULL (যাকে “সত্য নয়” হিসেবে ধরা হয়), তাই:
SELECT * FROM users WHERE age = NULL; -- returns ZERO rows, always
SELECT * FROM users WHERE age <> 25; -- excludes NULL-age rows! null পরীক্ষা করতে হলে আপনাকে বিশেষ IS operator ব্যবহার করতেই হবে:
SELECT * FROM users WHERE age IS NULL;
SELECT * FROM users WHERE age IS NOT NULL; arithmetic-এও NULL ছোঁয়াচে। 5 + NULL হলো NULL, আর 'hi' || NULL হলো NULL। কোনো column null হতে পারে এমন হলে fallback value বসাতে COALESCE(age, 0) ব্যবহার করুন — এটা প্রথম non-null argument-টা ফেরত দেয়।
একটা বাস্তব ফলাফল: আপনি যদি চান “যাদের age 25 নয় এমন সবাই, সহ যাদের age অজানা”, তাহলে আপনাকে স্পষ্ট হতে হবে:
SELECT * FROM users WHERE age <> 25 OR age IS NULL; Recap
এখন আপনার ভিত্তিটা তৈরি: table typed row ধরে রাখে, DDL সেগুলো সংজ্ঞায়িত করে, আর WHERE / ORDER BY / LIMIT সহ SELECT সেগুলো আবার পড়ে। NULL-এর নিয়মগুলো মাথায় রাখুন — অন্য যেকোনো feature-এর চেয়ে এগুলোই বেশি সূক্ষ্ম bug-এর উৎস। এরপর আমরা aggregation দিয়ে অনেকগুলো row-কে একক উত্তরে সারসংক্ষেপ করব।