সেশন ০৭

Database Indexes - Database এর দ্রুত Data খুঁজে পাবার রহস্য

এই সেশনের লক্ষ্যঃ একটা single database এর ভেতরে গিয়ে দেখবো এটা কীভাবে একটা row খুঁজে বের করে। Index আসলে কী (একটা B-tree), কেন এটা read fast করে কিন্তু write এর cost বাড়ায়, composite index আর left-prefix rule, আর কখন index কাজে আসে না। এগুলো বোঝা।
Database Indexes - Database এর দ্রুত Data খুঁজে পাবার রহস্য
Database Indexes - Database এর দ্রুত Data খুঁজে পাবার রহস্য

০০ যে প্রশ্নগুলোর উত্তর খুঁজবো

আগের সব সেশন ছিলো database এর আশেপাশের জিনিসপত্র নিয়ে। এই সেশনে আমরা database এর ভেতরে একটু ঢুঁ মারবো। প্রতিটা প্রশ্নের উত্তর জানা হয়ে গেলে আপনি বুঝবেন একটা query কীভাবে full scan থেকে binary search এ পরিণত হয়।

  • Database এর কাছে একটা row চাইলে, database কীভাবে সেই row টা খুঁজে পায়?
  • বড় table এ একটা row খুঁজে বের করা তো slow। lookup fast করবো কীভাবে?
  • Index আসলে কী? (কোন data structure ইউজ হয়?)
  • Index যদি read fast করে, তাহলে প্রতিটা column index করি না কেন?
  • একাধিক column এ filter query করবার জন্য কীভাবে index করবো?
  • কখন index কাজে আসে না, বরং উল্টো ক্ষতি করে?
  • Full picture: index আগের সব কিছুর সাথে কীভাবে কানেক্টেড?

০১ Database এর কাছে একটা row চাইলে, database কীভাবে সেই row টা খুঁজে পায়?

ধরুন আপনার একটা users table আছে, আর সেই table এ ১ কোটি row জমা আছে। এবার আপনি query লিখলেনঃ

SELECT * FROM users WHERE email = 'roy@example.com';

Table টা একটা বিশাল খাতার মতো। Row গুলো insertion order এ store করা থাকে (user 1, user 2, user 3…), email অনুযায়ী সাজানো থাকে না। তাই roy@example.com email টা খুঁজতে database একে একে প্রতিটা row চেক করেঃ

Row 1: alice@example.com - নো ম্যাচ।
Row 2: bob@@example.com? - নো ম্যাচ।।
...
Row 7,234,891: roy@@example.com? - ম্যাচ।

অর্থাৎ match না পাওয়া পর্যন্ত database একটার পর একটা row পড়তেই থাকে। এটাকে বলে full table scan; m সংখ্যক row এর জন্য এর complexity O(m)।

শুধু একটা সাধারণ lookup এর জন্যই database প্রতি সেকেন্ডে কোটি কোটি comparison করে। এতে system overloaded হয়ে যায়।

প্রধান সমস্যা

কোনো সাহায্য ছাড়া একটা row খোঁজা মানে শুরু থেকে এক এক করে scan করা। ছোট table এ এতে কোনো অসুবিধা নেই, কিন্তু মিলিয়ন বা বিলিয়ন row হলে database ভয়ংকর রকম slow হয়ে যায়। আর এটা ঘটে প্রতিটা query তে

০২ বড় table এ একটা row খুঁজে বের করা তো slow। lookup fast করবো কীভাবে?

Phone book এর analogy টা দেখি। একটা কাগজের phone book এ লাখ লাখ নাম থাকে। একটা নাম খুঁজতে গেলে আপনি নিশ্চয়ই page 1 থেকে পড়া শুরু করবেন না। আপনি বইটা মাঝখানে খুলবেন, একটা নাম দেখবেন, তারপর ঠিক করবেন আপনার খোঁজা নামটা এটার আগে নাকি পরে। প্রতি ধাপে বাকি page এর অর্ধেক বাদ পড়ে যায়। এটাকেই বলে binary search

জাদুকরী যে property টা binary search কে সম্ভব করেছে সেটা হলোঃ phone book টা sorted। Binary search শুধু sorted data তেই কাজ করে। নাম গুলো random order এ থাকলে একটা নাম দেখে পরের নামটা কোথায় আছে সেটা বোঝার কোনো উপায় থাকতো না।

Binary search এ Speed এর পার্থক্যটা কিন্তু বিশাল

১ কোটি row এর মধ্য থেকে একটা নাম খুঁজতেঃ

Data দ্বিগুণ হলে binary search এ মাত্র একটা step বাড়ে, কিন্তু full scan এর কাজ দ্বিগুণ হয়ে যায়। scaling এর সময় এই ছোট্ট পার্থক্যটাই ঠিক করে দেয় কোন system টিকে থাকবে আর কোনটা ভেঙে পড়বে।

Insight কি পেলাম?

Sorting fast search কে unlock করে। Data sorted রাখুন, binary-search ব্যবহার করুন, search complexity O(m) থেকে O(log m) এ নামিয়ে ফেলুন।

একটা catch আছে

একটা table কে physically একসাথে শুধু একভাবেই sort করা যায়। Email দিয়ে sort করলে email order টা থাকে; কিন্তু তারপর username দিয়ে sort করলে email order টা নষ্ট হয়ে যায়। শুধু শেষ sort টাই অ্যাপ্লাই থাকে। অথচ আমরা প্রায়ই অনেক column এ search করি (email, username, phone)। সব column অনুযায়ী একটা table কে একসাথে sorted রাখা যায় না।

০৩ Index আসলে কী? (কোন data structure ইউজ হয়?)

বুদ্ধিমানের কাজ হলো পুরো table কে sort না করা। বরং একটা ছোট, আলাদা, sorted structure বানানো, যেটাতে শুধু search column টা থাকে আর সেই সাথে full row এর জন্য একটা pointer থাকে।

TABLE (insertion order, unsorted):
Row 1: id:1, email: roy@example.com
Row 2: id:2, email: alice@example.com
Row 3: id:3, email: bob@example.com

INDEX on email (sorted, আলাদা structure):
alice@example.com  →  Row 2
bob@example.com    →  Row 3
roy@example.com    →  Row 1

Search WHERE email = 'roy@example.com':

  1. Sorted index এ binary-search করুন; প্রায় ২৪ step এ roy@example.com পাওয়া যায়।
  2. Entry টা Row 1 এর দিকে point করে।
  3. সরাসরি table এর Row 1 এ চলে যান।

Table কে physically sort না করেই আপনি binary-search speed পেয়ে গেলেন। আর index যেহেতু আলাদা, তাই অনেকগুলো index বানানো যায়, প্রতিটা search প্রয়োজনের জন্য একটা করেঃ

INDEX on email    →  row এর দিকে pointer
INDEX on username →  row এর দিকে pointer
INDEX on phone    →  row এর দিকে pointer
Index কী

Index হলো এক (বা একাধিক) column এর একটা আলাদা, sorted copy, যেটাতে full row এর দিকে pointer থাকে। Table কে sort না করেই এটা একটা column binary-search করতে দেয়, আর আপনি চাইলে অনেকগুলো index রাখতে পারেন।

এটা আসলে একটা B-tree

আসল database গুলো index কে একটা flat sorted list হিসেবে রাখে না, রাখে একটা B-tree (B+ tree) হিসেবে (একটা বিশাল flat list এর মাঝখানে কিছু insert করতে গেলে পুরোটা shift করতে হতো)। Intuition টা হলো: এটা একটা sorted tree, যেটার উপর থেকে নিচের দিকে হেঁটে নামলে প্রতি step এ range টা সরু হয়ে আসে।

bob@ | roy@ root node alice@ < bob bob@ · carol@ bob … roy roy@ · zoe@ > roy
একটা B-tree: root থেকে নিচে হেঁটে নামুন, প্রতি step এ range সরু হয়। roy@ খুঁজতে root দেখে ডান দিকের leaf এ নামুন, কয়েক step এ পৌঁছে যায়।

Key property গুলো:

কেউ যখন বলে “database টা index ব্যবহার করে”, তখন প্রায় সবসময় সে একটা B-tree index এর কথাই বোঝায়। Mental model: একটা sorted tree, যেটা ধরে নিচে নেমে দ্রুত জিনিস খুঁজে পান।

০৪ প্রতিটা operation এ table আর index এ কী হয়

এটাই পুরো mechanical picture। Setup: একটা users table, যার email এর উপর একটা index আছে।

TABLE (insertion order):                INDEX on email (sorted):
Row 1: id:1, email: roy@example.com     alice@example.com  →  Row 2
Row 2: id:2, email: alice@example.com   bob@example.com    →  Row 3
Row 3: id:3, email: bob@example.com     roy@example.com    →  Row 1

READ (SELECT): সুবিধাটা

Query: SELECT * FROM users WHERE email = 'roy@example.com'

Index ছাড়া (full scan): worst case এ সব m টা row scan করতে হয়। Cost O(m), slow।

Index সহ (B-tree binary search): index এ roy@example.com binary-search করুন (~২৪ step), তারপর pointer ধরে সরাসরি Row 1 এ চলে যান। Cost O(log m), fast। এটাই পুরো সুবিধা।

(Non-indexed column এ read করলে কোনো সুবিধা নেই; সেটা আবার full scan এ ফিরে যায়। Index শুধু যে column টা cover করে, সেই column এর read এ help করে।)

INSERT a new row

id:4, email: carol@example.com insert করুন।

Step 1 - table এ row লেখা (শেষে, insertion order):
Row 4: carol@example.com     ← নতুন row

Step 2 - index এ সঠিক SORTED position এ insert:
alice@example.com  →  Row 2
bob@example.com    →  Row 3
carol@example.com  →  Row 4     ← sorted জায়গায় বসলো, শেষে না
roy@example.com    →  Row 1

Cost: ১ টা table write আর প্রতি index এ ১ টা B-tree insert (জায়গা খুঁজতে ~log m)।

UPDATE an indexed column

Row 3 এর email bob@example.com থেকে zoe@example.com এ বদলান। Index এ value এর sorted position টা বদলে যায়, তাই এটা আসলে হয়ে দাঁড়ায় পুরোনো entry delete আর নতুন entry insert:

Step 1 - table এ value বদলান: Row 3: zoe@example.com

Step 2 - index থেকে পুরোনো entry delete (bob@example.com চলে গেলো)
Step 3 - নতুন sorted position এ insert:
alice@example.com  →  Row 2
carol@example.com  →  Row 4
roy@example.com    →  Row 1
zoe@example.com    →  Row 3     ← নতুন sorted position

Indexed column update করা মানে delete + insert। এটাই সবচেয়ে expensive operation।

UPDATE a non-indexed column

Row 1 এর last_login_time বদলান। last_login_time indexed না, তাই email index এ কোনো হাত পড়ে না। Cost: শুধু table write। Index এ কোনো কাজ নেই।

DELETE a row

Row 2 (alice@example.com) delete করুন। Table থেকে row টা সরান, আর index থেকে তার entry টাও সরান। Cost: ১ টা table delete আর প্রতি index এ ১ টা B-tree delete।

পুরো operations table

OperationTableIndex (প্রতি index)Cost
READ on indexed columnpointer ধরে row এ jumpB-tree binary searchO(log m), fast (সুবিধাটা)
READ on non-indexed columnfull table scanindex unusedO(m), slow
INSERTrow যোগsorted position এ ১ টা inserttable write + n × O(log m)
UPDATE indexed columnvalue বদল১ delete + ১ insert (position সরে)সবচেয়ে expensive
UPDATE non-indexed columnvalue বদলকিছু নাcheap
DELETErow সরানো১ deletetable delete + n × O(log m)
Pattern টা

প্রতিটা write যেসব index এর কোনো changed column cover করে, সেগুলোর সবটায় গিয়ে ছড়ায়। INSERT সব index এ ছড়ায়। DELETE সব index এ ছড়ায়। UPDATE শুধু changed column এর index এ ছড়ায়, আর সেখানে double cost (delete + insert), কারণ sorted position টা সরে যায়।

০৫ Index যদি read fast করে, তাহলে প্রতিটা column index করি না কেন?

কারণ index free না। n টা index সহ একটা row insert করার cost মোটামুটিঃ

১ (table write) + n × log(m)

এখানে n হলো index সংখ্যা, আর m হলো table এর row সংখ্যা। প্রতিটা relevant write এ প্রতিটা index কে sorted রাখতে হয়।

আরো indexপ্রভাব
Read (indexed column এ)Faster (full scan এর বদলে binary search)
Write (insert/update/delete)Slower (প্রতিটা index maintain করতে হয়)
StorageMore (প্রতিটা index একটা আলাদা structure)
নিয়মটা

যে column এ search করেন সেগুলোই index করুন, প্রতিটা column না। প্রতিটা index আসলে একটা বাজি: “এটা যে read fast করছে, সেটা যে write slow করছে তার চেয়ে বেশি মূল্যবান”। Access pattern অনুযায়ী index করুন।

Guideline গুলো:

উদাহরণ: Logs Table

একটা logs table এ সেকেন্ডে ৫০,০০০ insert হয়, কিন্তু search হয় সপ্তাহে একবার।

সবচেয়ে common ভুল

বাস্তবে সবচেয়ে common ভুল হলো actual query না দেখে অনুমান করে index column বেছে নেওয়া। যে query গুলো আপনি আসলে চালান, সেগুলোর জন্যই index করুন।

০৬ একাধিক column এ filter query করবার জন্য কীভাবে index করবো?

একটা composite index (compound বা multi-column) হলো দুই বা তার বেশি column একসাথে নিয়ে বানানো একটা index:

INDEX on (last_name, first_name)

এটা প্রথম column দিয়ে sort করে, tie হলে দ্বিতীয় দিয়ে, তারপর তৃতীয় দিয়ে; ঠিক একটা phone book এর মতো যেটা (last_name, first_name) দিয়ে sorted:

Rahman, Abdul
Rahman, Fatima
Rahman, Karim      ← আগে last name দিয়ে group...
Rahman, Zara          ...তারপর প্রতিটা last name এর ভেতরে first name দিয়ে sort
Roy, Amit

Left-prefix rule (যেখানে সবাই আটকায়)

একটা (A, B, C) composite index ওই query গুলোতেই help করে, যেগুলো একটা continuous left prefix এ filter করে: A, বা A+B, বা A+B+C। যেসব query প্রথম column টা skip করে, সেগুলোতে এটা help করে না।

(last_name, first_name) index দিয়ে:

কেন

Index টা আগে প্রথম column দিয়ে sorted। দ্বিতীয় column টা শুধু প্রথম column এর group গুলোর ভেতরে sorted। প্রথমটা skip করলে দ্বিতীয় column এর order এর আর কোনো অর্থ থাকে না।

দুইটা failure mode: গুরুত্বপূর্ণ পার্থক্য

(country, city, age) দিয়ে উদাহরণ:

প্রথম column হলো gatekeeper

প্রথম column ছাড়া index টা একদমই ব্যবহার করা যায় না, query তে পরের যত column ই থাকুক না কেন। এই কারণেই (department, role, salary) index এর বিপরীতে (role, salary) দিয়ে filter করলে কোনো help পাওয়া যায় না; department টা missing।

Composite index design

Column গুলো access pattern অনুযায়ী order করুন: যে column আপনি সবসময় filter করেন সেটা আগে, আর যেটা মাঝে মাঝে অতিরিক্ত filter করেন সেটা পরে।

উদাহরণ: ধরুন query গুলো হলো “customer X এর সব order” আর “customer X এর order একটা date range এ”। তাহলে index হবে (customer_id, order_date)। customer_id দুই query তেই ব্যবহৃত left prefix। (এটা “সব customer এর একটা date এর সব order” এ help করতো না; সেটা শুধু date এ filter করে, যেটা left prefix না।)

Covering index (যে term টা জানা দরকার)

একটা index এ যদি query এর দরকারি সব column থাকে, তাহলে database table এ না গিয়েই পুরো উত্তর index থেকেই দিয়ে দিতে পারে। (last_name, first_name) index এর বিপরীতে SELECT first_name WHERE last_name='Rahman' query তে দুইটা column ই index এ আছে, তাই table এ jump করার দরকার পড়ে না। এটাকে বলে covering index (“query এর সব column cover করে”)। Row lookup skip করে বলে এটা faster।

০৭ কখন index কাজে আসে না, বরং উল্টো ক্ষতি করে?

Index help করে না যখন:

Index ক্ষতি করে যখন:

পরিণত instinct

আগে actual query দেখুন, তারপর সেগুলোর সাথে match করে index করুন। যে index এর বিপরীতে আপনি query করেন না, সেটা শুধুই cost, কোনো benefit নেই।

০৮ Full picture: index আগের সব কিছুর সাথে কীভাবে কানেক্টেড?

সেশন ১ থেকে ৬ এর সব কিছু ছিলো database এর চারপাশে। Index থাকে একটা single database এর ভেতরে, আর প্রতিটা query কে fast করে।

Load Balancer S2 · request ছড়ায় Cache S4 · পারলে DB একদম এড়ায় Shard Router S5 · সঠিক shard বাছে এক Shard / Database একটা single database INDEXES S7 · এই DB এর ভেতরে row দ্রুত খুঁজে পায় Table Data raw rows
একটা read প্রথমে cache এ যায় (S4); miss হলে database এ পৌঁছায় (হয়তো এক shard, S5), যেখানে index lookup কে full scan থেকে binary search এ পরিণত করে। Index হলো DB এর ভেতরের last-mile speedup।

Connection গুলো

Decision framework
  1. Actual query দেখুন; যে column এ filter / search / join করেন সেগুলো index করুন।
  2. Read-heavy table: বেশি index লাভজনক।
  3. Write-heavy table: যত কম index তত ভালো।
  4. Multi-column filter: composite index, left-prefix rule অনুযায়ী order করা।
  5. প্রায়ই বদলায় এমন column: দুইবার ভাবুন (indexed-column update double cost)।

০৯ মগজে প্রেশার দিন

আগে নিজে ভাবুন। তারপর দেখুন: সবাই যেভাবে ভাবে, কেন সেটা অসম্পূর্ণ, আর আসল insight টা কোথায়।

প্রশ্ন ১: Index read fast করে কিন্তু write slow করে কেন?
উত্তর দেখুন আগে নিজে ভাবুন

সবাই যেভাবে ভাবে

“Index search কে O(log n) করে। Write slow হয় কারণ নতুন row টাকে সঠিক জায়গায় বসাতে হয়। অনেক index থাকলে cost index সংখ্যার সাথে বাড়ে।”

Direction টা ঠিক আছে, কিন্তু একটা জায়গা সবাই miss করে।

যে জায়গাটা tighten করা দরকার

শুধু insert না; insert, delete, আর indexed-column update সবগুলোই index maintain করে। Read হলো sorted structure এ একটা O(log n) binary search। Write slow হয় কারণ প্রতিটা নতুন row কে প্রতিটা index এর sorted position এ বসাতে হয়, আর সেই cost index সংখ্যার সাথে বাড়তে থাকে।

সঠিক ভাষায়

Indexed column update সবচেয়ে expensive: এটা delete + insert, কারণ value এর sorted position টা সরে যায়। তাই প্রায়ই বদলায় এমন column index করা সবচেয়ে বেশি ক্ষতিকর।

প্রশ্ন ২: একটা orders table। Common query: (ক) customer X এর সব order, (খ) customer X এর order একটা date range এ। একটা index দিলে কোন column, কোন order এ?
উত্তর দেখুন আগে নিজে ভাবুন

সঠিক choice আর কেন

Composite index (customer_id, order_date)। customer_id দুই query তেই ব্যবহৃত left prefix; আর order_date query (খ) এর জন্য সেটাকে extend করে।

উল্টে দিলে কী হতো

(order_date, customer_id) করলে query (ক) ভেঙে যেতো; “customer X এর সব order” তে কোনো date নেই, তাই প্রথম column missing, index অকেজো। তাই access pattern অনুযায়ী order করাই সঠিক।

মূল pattern

যে column সবসময় filter করেন সেটা আগে রাখুন। Left-prefix rule ঠিক করে index কোন query accelerate করবে।

প্রশ্ন ৩: "Read slow, তাই প্রতিটা column এ একটা index দাও।" পুশব্যাক করুন।
উত্তর দেখুন আগে নিজে ভাবুন

সবাই যেভাবে ভাবে

“Index এর insert, update, delete cost আছে। n টা column index করলে cost n গুণ বাড়ে। শুধু যে column এ read frequent সেগুলোই index করুন।”

Key point গুলো ঠিক, কিন্তু আরো দুইটা argument যোগ করা যায়।

আরো ধারালো পুশব্যাক

প্রতিটা column index করা storage নষ্ট করে (প্রতিটা index আলাদা structure), আর বেশিরভাগ index কখনো ব্যবহারই হবে না। একটা index শুধু সেই query তেই help করে যেটা ওই column এ filter করে। তাই unsearched column index করা মানে শুধু write tax আর storage, zero benefit।

সোজা কথা

আগে actual query দেখুন, তারপর সেগুলোর সাথে match করে index করুন। Read-heavy হলে index, আর write-heavy হলে কম index আর শুধু priority column। Fit হলে composite index ভাবুন।

১০ যে টার্মগুলো শিখলাম

টার্মমানে
Full table scanএকটা match খুঁজতে এক এক করে প্রতিটা row পড়া। O(m)।
Binary searchএকটা sorted range প্রতি step এ অর্ধেক করে value খোঁজা। O(log m)। শুধু sorted data তে কাজ করে।
Indexএকটা column এর আলাদা sorted structure, যাতে full row এর দিকে pointer থাকে।
B-tree (B+ tree)Database যে sorted tree structure index এর জন্য ব্যবহার করে; fast lookup আর efficient insert/delete।
Composite indexএকাধিক column একসাথে নিয়ে index, আগে প্রথম column দিয়ে sorted, তারপর দ্বিতীয়, …।
Left-prefix ruleComposite index শুধু তার column গুলোর একটা continuous left prefix এ help করে (A, A+B, A+B+C)।
Partial helpএকটা middle column skip করা; gap এর আগের prefix টা তখনো কাজ করে।
No help (প্রথম column missing)প্রথম column skip করা; entry point নেই, index অকেজো।
Covering indexযে index এ query এর দরকারি সব column থাকে, তাই table lookup লাগে না।
Cardinalityএকটা column এ distinct value এর সংখ্যা (high cardinality হলে ভালো index candidate)।

১১ সর্ট সামারী

  1. Index হলো একটা আলাদা sorted structure (B-tree), যেটা একটা column এর read কে full scan O(m) থেকে binary search O(log m) এ নিয়ে আসে; database এর ভেতরে এটাই সবচেয়ে বড় read-performance lever।
  2. Index free না: প্রতিটা insert, delete, আর indexed-column update প্রতিটা relevant index কে sorted রাখে, তাই প্রতিটা column না, শুধু যে column এ আসলে query করেন সেগুলোই index করুন।
  3. Multi-column filter এর জন্য left-prefix rule অনুযায়ী order করা composite index ব্যবহার করুন; এটা একটা continuous left prefix এ help করে (A, A+B, A+B+C), middle column skip করলে partial, আর প্রথম column skip করলে একদমই না।

১২ মনে রাখুন