Skip to main content

Command Palette

Search for a command to run...

DDIA: Chapter-3

Storage and Retrieval

Published
36 min read
DDIA: Chapter-3

যারা সবকিছু পরিপাটি করে রাখে, তারা মূলত খোঁজাখুঁজির কষ্ট করতে চায় না বলেই এমনটা করে। - German Proverb

মৌলিকভাবে একটি ডাটাবেসকে দুটি কাজ নিখুঁতভাবে করতে হয়: ১. ডেটা স্টোর করা: যখন আপনি ডাটাবেসকে কোনো তথ্য দেবেন, তখন সেটি যেন সুরক্ষিতভাবে জমা রাখে। ২. ডেটা ফেরত দেওয়া: যখন আপনি পরবর্তীতে সেই তথ্যটি চাইবেন, তখন যেন সেটি দ্রুত খুঁজে বের করে আপনাকে দিতে পারে।

অধ্যায় ২-এ আমরা ডাটা মডেল (Data Models) এবং কোয়েরি ল্যাঙ্গুয়েজ নিয়ে আলোচনা করেছি, যা মূলত একজন অ্যাপ্লিকেশন ডেভেলপারের দৃষ্টিভঙ্গি থেকে দেখা। কিন্তু এই অধ্যায়ে আমরা ডাটাবেসের Internal point of view থেকে দেখব—কীভাবে ডেটাগুলো পর্দার আড়ালে save থাকে এবং কীভাবে সেগুলো পুনরায় খুঁজে পাওয়া যায়।

একজন ডেভেলপার হিসেবে আপনার কেন এটি জানা প্রয়োজন?

হয়তো আপনি নিজে স্ক্র্যাচ থেকে কোনো স্টোরেজ ইঞ্জিন তৈরি করবেন না, তবুও এটি জানা জরুরি কারণ:

  • সঠিক ইঞ্জিন নির্বাচন: বাজারে অনেক ধরণের স্টোরেজ ইঞ্জিন আছে। আপনার অ্যাপ্লিকেশনের জন্য কোনটি সঠিক, তা বোঝার জন্য এর অভ্যন্তরীণ কার্যপদ্ধতি জানা প্রয়োজন।

  • পারফরম্যান্স টিউনিং: আপনার কাজের ধরন বা 'Workload' অনুযায়ী স্টোরেজ ইঞ্জিনকে অপ্টিমাইজ বা টিউন করতে হলে এটি কীভাবে কাজ করে তার একটি স্বচ্ছ ধারণা থাকতে হবে।

স্টোরেজ ইঞ্জিনের প্রধান দুটি ভাগ

বইটিতে স্টোরেজ ইঞ্জিনকে তাদের কাজের ধরনের ওপর ভিত্তি করে মূলত দুটি বড় ভাগে ভাগ করা হয়েছে:

১. Transactional Workloads (OLTP): যা সাধারণত আমাদের পরিচিত ট্র্যাডিশনাল রিলেশনাল ডাটাবেস বা অনেক NoSQL ডাটাবেসে ব্যবহৃত হয়। এর ভেতর আবার দুটি জনপ্রিয় মেকানিজম আছে: Log-structured storage engines: (যেমন: LSM-Trees) যেখানে নতুন ডেটা সবসময় ফাইলের শেষে অ্যাপেন্ড বা যুক্ত করা হয়। Page-oriented storage engines: (যেমন: B-trees) যেখানে ডেটাকে নির্দিষ্ট সাইজের পেজে ভাগ করে রাখা হয়।

২. Analytics (OLAP): যা মূলত অ্যানালিটিক্যাল কাজের জন্য ব্যবহৃত হয়। এখানে Column-Oriented Storage সম্পর্কে আলোচনা করা হয়েছে (যা বইয়ের পরবর্তী অংশে বিস্তারিত আসবে)।

Data Structures That Power Your Database

বিশ্বের সহজতম ডাটাবেস (The World’s Simplest Database)

একটি ডাটাবেস মূলত দুটি কাজ করে: আপনি যখন তাকে কোনো ডাটা দেন সে সেটা স্টোর (Store) করে, এবং পরবর্তীতে চাইলে সেই ডাটা আপনাকে খুঁজে (Retrieve) দেয়। লেখক এখানে দুটি Bash ফাংশন ব্যবহার করে একটি Key-Value স্টোর তৈরি করেছেন:

#!/bin/bash
db_set () {
    echo "$1,$2" >> database
}

db_get () {
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}
  • db_set : এটি একটি ফাইলে নতুন ডাটা অ্যাপেন্ড (Append) করে।

  • db_get : এটি ফাইলটি সার্চ করে কোনো নির্দিষ্ট কী (Key)-এর সর্বশেষ ভ্যালু বের করে আনে।

এটি কীভাবে কাজ করে? (Working Process)

এখানে স্টোরেজ ফরম্যাটটি খুবই সাধারণ একটি টেক্সট ফাইল, যেখানে প্রতিটি লাইনে কমা দিয়ে আলাদা করা Key-Value pair থাকে (যেমনটা CSV ফাইলে থাকে)।

আপনি যখনই কোনো ভ্যালু আপডেট করেন, এটি আগের ভ্যালু মুছে ফেলে না, বরং ফাইলের শেষে নতুন লাইন যোগ করে। তাই db_get ফাংশনটি tail -n 1 ব্যবহার করে যাতে ফাইলের সবচেয়ে শেষে থাকা (অর্থাৎ লেটেস্ট) ভ্যালুটি পাওয়া যায়।

উদাহরণ:

$ db_set 123456 '{"name":"London","attractions":["Big Ben","London Eye"]}'
$ db_set 42 '{"name":"San Francisco","attractions":["Golden Gate Bridge"]}'
$ db_get 42
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}

ফাইলের ভেতরে ডাটা এভাবে জমা হয়:

123456,{"name":"London","attractions":["Big Ben","London Eye"]}
42,{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
42,{"name":"San Francisco","attractions":["Exploratorium"]}

লগ (Log) এর ধারণা

বইটিতে Log শব্দটিকে একটি বিশেষ অর্থে ব্যবহার করা হয়েছে। সাধারণত আমরা লগ বলতে অ্যাপ্লিকেশনের টেক্সট লগকে বুঝি, কিন্তু ডাটাবেসের ক্ষেত্রে Log হলো একটি অ্যাপেন্ড-অনলি (Append-only) ডাটা ফাইল। এটি মানুষের পড়ার উপযোগী টেক্সট হতে পারে অথবা বাইনারি ফরম্যাটও হতে পারে। অনেক রিয়েল-ওয়ার্ল্ড ডাটাবেস ইন্টারনালি এই লগ প্রিন্সিপাল ব্যবহার করে।

পারফরম্যান্স বিশ্লেষণ (Performance Analysis)

  • Write Performance: db_set ফাংশনটির পারফরম্যান্স অত্যন্ত ভালো। কারণ ফাইলের শেষে কোনো কিছু যোগ করা (Appending) খুব সহজ এবং দ্রুত গতির অপারেশন।

  • Read Performance: db_get ফাংশনটির পারফরম্যান্স খুবই খারাপ। আপনার ডাটাবেসে যদি অনেক রেকর্ড থাকে, তবে প্রতিবার কোনো কিছু খুঁজতে ফাইলটির শুরু থেকে শেষ পর্যন্ত স্ক্যান করতে হয়। অ্যালগরিদমিক ভাষায় এর কস্ট হলো O(n)।

ইনডেক্সিং এবং ট্রেড-অফ (Indexing and Trade-off)

Read Performance বাড়ানোর জন্য আমাদের একটি আলাদা ডাটা স্ট্রাকচার প্রয়োজন, যাকে বলা হয় Index (ইনডেক্স)

  • মূল ধারণা: ইনডেক্স হলো মূল ডাটা থেকে তৈরি করা বাড়তি কিছু মেটাডেটা, যা আপনাকে কাঙ্ক্ষিত ডাটার লোকেশন খুঁজে পেতে সাহায্য করে (একটি সাইনপোস্টের মতো)।

  • ট্রেড-অফ (Trade-off): এটি স্টোরেজ সিস্টেমের একটি গুরুত্বপূর্ণ শিক্ষা। ইনডেক্স রিড কোয়েরি (Read query) দ্রুত করে, কিন্তু এটি রাইট অপারেশনকে (Write) স্লো করে দেয়। কারণ প্রতিবার ডাটা রাইট করার সময় ইনডেক্সকেও আপডেট করতে হয়।

সতর্কবার্তা: এই কারণেই ডাটাবেস সাধারণত সবকিছু ডিফল্টভাবে ইনডেক্স করে না। একজন ডেভেলপার বা অ্যাডমিনিস্ট্রেটরকে তার অ্যাপ্লিকেশনের কোয়েরি প্যাটার্ন বুঝে ম্যানুয়ালি ইনডেক্স বেছে নিতে হয়, যাতে অতিরিক্ত ওভারহেড ছাড়াই সর্বোচ্চ সুবিধা পাওয়া যায়।

Hash Indexes

সবচেয়ে সহজ ডাটাবেজ স্টোরেজ হলো একটি ফাইল যেখানে নতুন ডেটা শুধু শেষে যোগ (Append) করা হয়। কিন্তু ফাইল বড় হয়ে গেলে নির্দিষ্ট কোনো তথ্য খুঁজে বের করা কঠিন। এর সমাধানের জন্য মেমোরিতে (RAM) একটি Hash Map ব্যবহার করা হয়।

  • কীভাবে কাজ করে?: মেমোরিতে একটি Hash Map থাকবে যেখানে প্রতিটি Key-এর বিপরীতে ফাইলের একটি Byte Offset (ফাইলের ঠিক কোন জায়গায় ডেটাটি আছে তার ঠিকানা) জমা থাকে।

  • রিড অপারেশন: যখনই কোনো ভ্যালু দরকার হয়, Hash Map থেকে অফসেট দেখে সরাসরি ডিস্কের সেই লোকেশনে গিয়ে ডেটা পড়া হয়।

Bitcask: একটি বাস্তব উদাহরণ

এই মেকানিজমটি Bitcask স্টোরেজ ইঞ্জিনে (Riak-এর ডিফল্ট ইঞ্জিন) ব্যবহৃত হয়।

  • এটি তখন খুব ভালো কাজ করে যখন আপনার Keys সংখ্যায় কম (যাতে RAM-এ ধরে) কিন্তু Values অনেক বড় বা ঘনঘন আপডেট হয়।

  • উদাহরণ: একটি বিড়াল বা ক্যাট ভিডিওর URL হলো Key, আর সেটি কতবার প্লে হয়েছে তা হলো Value। এখানে ভিডিওর সংখ্যা সীমিত হতে পারে, কিন্তু প্লে কাউন্ট প্রতি সেকেন্ডে আপডেট হতে পারে।

বইয়ের এই চিত্রে দেখানো হয়েছে কীভাবে মেমোরির Hash Map ডিস্কের ফাইলের অফসেটকে পয়েন্ট করে।

ফাইল কমপ্যাকশন এবং মার্জিং (Compaction and Merging)

যেহেতু আমরা শুধু ফাইলের শেষে ডেটা অ্যাপেন্ড করছি, ফাইলটি সময়ের সাথে অনেক বড় হয়ে যাবে। এর সমাধান হলো:

  1. Segments: ফাইলটিকে নির্দিষ্ট সাইজ অনুযায়ী ছোট ছোট সেগমেন্টে ভাগ করা হয়। একটি সেগমেন্ট পূর্ণ হলে সেটি বন্ধ করে নতুন ফাইলে লেখা শুরু হয়।

  2. Compaction: পুরাতন সেগমেন্টগুলো থেকে ডুপ্লিকেট কী মুছে ফেলা হয় এবং শুধুমাত্র লেটেস্ট আপডেটটি রাখা হয়।

  3. Merging: ছোট ছোট একাধিক সেগমেন্টকে একসাথে মার্জ করে বড় এবং ক্লিন সেগমেন্ট তৈরি করা হয়। এটি ব্যাকগ্রাউন্ড থ্রেডে চলে, তাই ডাটাবেজের পারফরম্যান্সে প্রভাব ফেলে না।

এখানে দেখানো হয়েছে কীভাবে ডুপ্লিকেট কী বাদ দিয়ে শুধু লেটেস্ট ভ্যালু রাখা হয় (যেমন: ভিডিও প্লে কাউন্ট)।

এই চিত্রে সেগমেন্ট মার্জিং এবং কমপ্যাকশন একসাথে হওয়ার প্রক্রিয়া দেখানো হয়েছে।

বাস্তব ইমপ্লিমেন্টেশনের গুরুত্বপূর্ণ দিকগুলো

বাস্তবে এই সিস্টেমটি চালাতে আরও কিছু জিনিস খেয়াল রাখতে হয়:

  • Binary Format: CSV-এর চেয়ে বাইনারি ফরম্যাট দ্রুত।

  • Tombstones (Record Deletion): কোনো ডেটা ডিলিট করতে হলে সেখানে একটি বিশেষ চিহ্ন বা 'Tombstone' বসিয়ে দেওয়া হয়, যাতে মার্জিংয়ের সময় ওই Key-টি বাদ যায়।

  • Crash Recovery: ডাটাবেজ ক্র্যাশ করলে RAM-এর ইনডেক্স মুছে যায়। পুরো ফাইল পড়ে ইনডেক্স তৈরি করা সময়সাপেক্ষ, তাই Bitcask ইনডেক্সের একটি স্ন্যাপশট (Snapshot) ডিস্কে সেভ করে রাখে।

  • Concurrency: সাধারণত একজন রাইটার (Writer) থাকে, কিন্তু অনেক রিডার (Reader) একসাথে ডেটা পড়তে পারে কারণ ফাইলগুলো ইমিউটেবল (অপরিবর্তনশীল)।

কেন এই অ্যাপেন্ড-অনলি (Append-only) ডিজাইন ভালো?

১. স্পিড: ডিস্কে র‍্যান্ডম রাইটের চেয়ে সিকুয়েনশিয়াল রাইট (পর পর লেখা) অনেক বেশি দ্রুত (HDD এবং SSD উভয়ের জন্য)।

২. ক্র্যাশ রিকভারি: ডেটা ওভাররাইট না করায় ফাইল নষ্ট হওয়ার ঝুঁকি কম থাকে।

৩. ফ্রেগমেন্টেশন: সেগমেন্ট মার্জিংয়ের ফলে ডিস্ক স্পেস অগোছালো (Fragmented) হয় না।

লিমিটেশন বা সীমাবদ্ধতা (Limitations)

এত সুবিধা থাকা সত্ত্বেও হাশ ইনডেক্সের দুটি প্রধান সমস্যা আছে:

১. Memory Constraint: সব কী-কে অবশ্যই RAM-এ জায়গা হতে হবে। ডিস্কে Hash Map রাখা খুব কঠিন এবং স্লো।

২. Range Queries: আপনি যদি জানতে চান "kitty00000" থেকে "kitty99999" এর মধ্যে সব Key-এর ভ্যালু কী, তবে Hash Map সেটি করতে পারবে না। আপনাকে প্রতিটি Key আলাদা আলাদা খুঁজতে হবে।

SSTables and LSM-Trees

Log-Structured Storage থেকে Sorted String Table (SSTable) এবং সেখান থেকে LSM-Tree তৈরি করার ধারণা

SSTables (Sorted String Tables)

আমরা এই অংশে লগ-স্ট্রাকচারড স্টোরেজ সেগমেন্টের একটি উন্নত সংস্করণ নিয়ে আলোচনা করব। আগে আমরা দেখেছি সেগমেন্টগুলোতে ডেটা যেভাবে আসত সেভাবেই লেখা হতো। কিন্তু SSTable-এ একটি প্রধান শর্ত যোগ করা হয়েছে: প্রতিটি সেগমেন্ট ফাইলে কী-ভ্যালু (Key-Value) Pair গুলো তাদের ‘কী’ (Key) অনুযায়ী সাজানো (Sorted) থাকতে হবে।

SSTable-এর প্রধান সুবিধাগুলো:

১. সেগমেন্ট মার্জ করা সহজ ও খুব দক্ষতার সাথে করে (Merging Segments): ফাইলগুলো মেমোরির চেয়ে বড় হলেও সেগুলোকে মার্জ করা খুব সহজ। এটি অনেকটা Mergesort অ্যালগরিদমের মতো কাজ করে।

  • একাধিক ফাইল পাশাপাশি রেখে তাদের প্রথম 'Key' গুলো তুলনা করা হয়।

  • সবচেয়ে ছোট 'Key'-টি নতুন ফাইলে কপি করা হয় এবং এই প্রক্রিয়া চলতেই থাকে।

  • যদি একই 'Key' একাধিক সেগমেন্টে থাকে, তবে সবচেয়ে সাম্প্রতিক (Most Recent) সেগমেন্টের ভ্যালুটি রাখা হয় এবং পুরনো ভ্যালুগুলো বাদ দেওয়া হয়।

২. ইন-মেমোরি ইনডেক্স ছোট রাখা সম্ভব (Sparse Index): যেহেতু ফাইলটি সাজানো থাকে, তাই মেমোরিতে প্রতিটি 'Key'-এর অফসেট (Offset) রাখার প্রয়োজন নেই। একে বলা হয় Sparse Index

  • উদাহরণস্বরূপ, আপনার কাছে 'handbag' এবং 'handsome' এর অফসেট আছে। আপনি যদি 'handiwork' খুঁজতে চান, তবে আপনি নিশ্চিতভাবেই জানেন যে এটি এই দুটির মাঝখানে থাকবে।

  • এর ফলে মেমোরিতে খুব অল্প ডেটা রেখেও ডিস্কের বিশাল ডেটা দ্রুত খুঁজে পাওয়া যায়।

৩. ডেটা কম্প্রেশন (Compression): যেহেতু রিড রিকোয়েস্টের সময় একটি নির্দিষ্ট রেঞ্জ স্ক্যান করতে হয়, তাই ডেটাগুলোকে ছোট ছোট Block-এ ভাগ করে কম্প্রেস করা যায়। এতে ডিস্কের জায়গা বাঁচে এবং I/O ব্যান্ডউইথ কম খরচ হয়।

Constructing and Maintaining SSTables

ডেটা তো যেকোনো অর্ডারে আসতে পারে, তাহলে আমরা ডিস্কে সাজানো অবস্থায় লিখব কীভাবে? এর সমাধান হলো মেমোরি এবং ডিস্কের সমন্বয়:

  1. Memtable: যখন কোনো ডেটা রাইট করা হয়, তখন সেটি সরাসরি ডিস্কে না লিখে মেমোরিতে একটি ব্যালেন্সড ট্রি (যেমন: Red-Black Tree বা AVL Tree) স্ট্রাকচারে রাখা হয়। একে বলা হয় Memtable। এটি ডেটাকে মেমোরিতে সাজানো অবস্থায় রাখে।

  2. SSTable Flush: যখন Memtable একটি নির্দিষ্ট সাইজে পৌঁছায় (যেমন কয়েক মেগাবাইট), তখন এটিকে একটি SSTable ফাইল হিসেবে ডিস্কে লিখে দেওয়া হয়। যেহেতু মেমোরিতে এটি আগে থেকেই সাজানো ছিল, তাই ডিস্কে লেখার সময় এটি খুব দ্রুত সিকুয়েনশিয়াল রাইট হিসেবে সম্পন্ন হয়।

  3. Read Process: কোনো কিছু খোঁজার জন্য প্রথমে Memtable-এ দেখা হয়, তারপর ডিস্কের সবচেয়ে সাম্প্রতিক সেগমেন্টে, এরপর তার পরের পুরনো সেগমেন্টে—এভাবে চলতে থাকে।

  4. Compaction: ব্যাকগ্রাউন্ডে সময় সময় মার্জ এবং কমপ্যাকশন প্রসেস চলে যাতে ডুপ্লিকেট 'Key' মুছে ফেলা যায় এবং ফাইল সংখ্যা কমানো যায়।

ক্র্যাশ রিকভারি (Crash Recovery): মেমোরিতে ডেটা থাকা অবস্থায় যদি ডাটাবেস ক্র্যাশ করে, তবে ডেটা হারিয়ে যেতে পারে। এটি রোধ করতে ডিস্কে একটি আলাদা Append-only Log রাখা হয় যেখানে প্রতিটি রাইট তৎক্ষণাৎ লিখে রাখা হয়। ক্র্যাশ করলে এই লগ থেকে Memtable আবার তৈরি করা যায়। যখন Memtable ডিস্কে SSTable হিসেবে সেভ হয়ে যায়, তখন পুরনো লগটি ফেলে দেওয়া হয়।

LSM-Trees (Log-Structured Merge-Tree)

এই যে SSTables মার্জ এবং কমপ্যাক্ট করার পুরো সিস্টেম, একেই বলা হয় LSM-Tree

  • Who Use it?: LevelDB, RocksDB, Cassandra, HBase এবং এমনকি Lucene (Elasticsearch-এর ইনডেক্সিং ইঞ্জিন) এই পদ্ধতি ব্যবহার করে।

  • Lucene-এর ক্ষেত্রে: এটি ফুল-টেক্সট সার্চের জন্য ‘Term Dictionary’ সংরক্ষণে এই পদ্ধতি ব্যবহার করে, যেখানে ‘Key’ হলো শব্দ এবং ‘ভ্যালু’ হলো ডকুমেন্ট আইডি-র লিস্ট।

Performance Optimizations

LSM-Tree-কে আরও ফাস্ট করার জন্য কিছু টেকনিক ব্যবহার করা হয়:

  • Bloom Filters: যদি কোনো 'Key' ডাটাবেসে না থাকে, তবে LSM-Tree-তে সেটি খুঁজতে সবকটি সেগমেন্ট চেক করতে হয় যা সময়সাপেক্ষ। Bloom Filter হলো একটি মেমোরি-এফিসিয়েন্ট ডেটা স্ট্রাকচার যা আগেই বলে দিতে পারে যে ওই 'কী'-টি ডাটাবেসে নেই, ফলে ডিস্ক রিড করার প্রয়োজন হয় না।

  • Compaction Strategies:

    • Size-tiered compaction: ছোট এবং নতুন SSTable গুলোকে পুরনো বড় SSTable-এর সাথে মার্জ করা হয়। (HBase এটি ব্যবহার করে)।

    • Leveled compaction: ডেটাকে নির্দিষ্ট লেভেলে ভাগ করা হয়, যা কমপ্যাকশনকে আরও ধারাবাহিক করে এবং ডিস্ক স্পেস কম নেয়। (LevelDB এবং RocksDB এটি ব্যবহার করে)।

LSM-trees হাই রাইট থ্রুপুট (High write throughput) এবং রেঞ্জ কুয়েরি (Range queries) সাপোর্টের জন্য অত্যন্ত কার্যকর, এমনকি ডেটাসেট মেমোরির চেয়ে অনেক বড় হলেও।

B-Trees

ডাটাবেস ইনডেক্সিংয়ের জগতে সবচেয়ে জনপ্রিয় এবং বহুল ব্যবহৃত স্ট্রাকচার হলো B-tree। ১৯৭০ সালে এটি উদ্ভাবিত হলেও আজ পর্যন্ত এটি রিলেশনাল এবং অনেক নন-রিলেশনাল ডাটাবেসের স্ট্যান্ডার্ড হিসেবে টিকে আছে।

মূল ধারণা ও ডিজাইন ফিলোসফি

SSTables বা Log-structured ইনডেক্সের মতো B-trees ও কি-ভ্যালু (key-value) Pair কে সর্টেড বা সাজানো অবস্থায় রাখে, যা লুকআপ এবং রেঞ্জ কুয়েরিকে সহজ করে। তবে এদের কাজের ধরন আলাদা:

  • Log-structured indexes: ডাটাবেসকে বড় সাইজের ভেরিয়েবল মেমরি সেগমেন্টে ভাগ করে।

  • B-trees: ডাটাবেসকে নির্দিষ্ট সাইজের (Fixed-size) ব্লক বা Pages-এ ভাগ করে। সাধারণত এই পেজগুলো 4 KB সাইজের হয়। এই ডিজাইনটি হার্ডডিস্কের ফিজিক্যাল ব্লকের সাথে সামঞ্জস্যপূর্ণ।

ডাটা খোঁজার প্রক্রিয়া (Lookup)

B-tree-তে একটি পেজকে Root হিসেবে ধরা হয়। যেকোনো কি (key) খুঁজতে এখান থেকেই যাত্রা শুরু হয়। প্রতিটি পেজে কিছু কি এবং পাশের পেজগুলোর রেফারেন্স (pointer) থাকে।

  • প্রতিটি চাইল্ড পেজ একটি নির্দিষ্ট রেঞ্জের ডাটা ধারণ করে।

  • Branching Factor: একটি পেজ যতগুলো চাইল্ড পেজের রেফারেন্স রাখতে পারে, তাকে ব্রাঞ্চিং ফ্যাক্টর বলে (যেমন: ৫ বা ৬, তবে বাস্তবে এটি কয়েকশ হয়)। (Figure 3-6) অনুযায়ী, সেখানে Branching Factor হলো ৬ (six)।

আপডেট এবং নতুন ডাটা যোগ করা (Update and Insert)

  • Update: কোনো কি-এর ভ্যালু আপডেট করতে চাইলে প্রথমে সেই লিফ পেজ (Leaf page) খুঁজে বের করা হয় এবং নতুন ভ্যালু লিখে পেজটি ডিস্কে সেভ করা হয়।

  • Insertion: নতুন কি যোগ করার জন্য সেই নির্দিষ্ট রেঞ্জের পেজটি খুঁজে বের করা হয়। যদি পেজে পর্যাপ্ত জায়গা না থাকে, তবে পেজটি Split বা দুই ভাগে বিভক্ত হয়ে যায়। তখন প্যারেন্ট পেজকেও আপডেট করতে হয় যেন সে নতুন রেঞ্জ বুঝতে পারে।

এই মেকানিজমের কারণে B-tree সবসময় Balanced থাকে। n সংখ্যক কি-এর জন্য এর ডেপথ (Depth) সবসময় O(logn) হয়। সাধারণত ৪ লেভেলের একটি ৪ কিলোবাইট পেজের ট্রি (ব্রাঞ্চিং ফ্যাক্টর ৫০০ হলে) প্রায় ২৫৬ টেরাবাইট ডাটা স্টোর করতে পারে।

B-trees-কে নির্ভরযোগ্য করা (Making B-trees Reliable)

B-tree সরাসরি ডিস্কের পেজ ওভাররাইট (Overwrite) করে। এটি LSM-trees থেকে আলাদা, কারণ LSM-trees কখনো পুরনো ফাইল পরিবর্তন করে না, শুধু নতুন ফাইল অ্যাপেন্ড করে।

১. ক্র্যাশ রিকভারি ও WAL

পেজ ওভাররাইট করা ঝুঁকিপূর্ণ। ধরুন, একটি পেজ স্প্লিট করার সময় ডাটাবেস ক্র্যাশ করলো; এতে ইনডেক্স করাপ্টেড হতে পারে। এই সমস্যা এড়াতে B-tree Write-Ahead Log (WAL) বা redo log ব্যবহার করে।

  • যেকোনো পরিবর্তনের আগে তা একটি অ্যাপেন্ড-অনলি ফাইলে লিখে রাখা হয়।

  • ক্র্যাশ করার পর ডাটাবেস পুনরায় চালু হলে এই লগ দেখে ট্রি-কে আগের সঠিক অবস্থায় ফিরিয়ে আনা হয়।

২. কনকারেন্সি কন্ট্রোল (Concurrency Control)

একাধিক থ্রেড যখন একসাথে ট্রি অ্যাক্সেস করে, তখন ইনকনসিস্টেন্সি এড়াতে Latches (হালকা ওজনের লক) ব্যবহার করা হয়।

B-tree অপ্টিমাইজেশন (B-tree Optimizations)

দীর্ঘদিন ধরে ব্যবহারের ফলে B-tree-তে অনেক উন্নত পরিবর্তন আনা হয়েছে:

  • Copy-on-write: সরাসরি পেজ ওভাররাইট না করে নতুন লোকেশনে নতুন পেজ তৈরি করা হয়। এটি কনকারেন্সি কন্ট্রোলে সাহায্য করে (যেমন: LMDB)।

  • Key Compression: পেজের ভেতরে পুরো কি (key) স্টোর না করে তার সংক্ষিপ্ত রূপ রাখা হয়, যেন একটি পেজে বেশি কি জায়গা পায় এবং ব্রাঞ্চিং ফ্যাক্টর বাড়ে।

  • Sequential Layout: ডিস্ক সিঙ্ক (Disk seek) কমানোর জন্য চেষ্টা করা হয় যেন লিফ পেজগুলো ডিস্কে ক্রমানুসারে থাকে।

  • Sibling Pointers: প্রতিটি লিফ পেজ থেকে তার ডানে ও বামের পেজে সরাসরি যাওয়ার জন্য পয়েন্টার থাকে, ফলে রেঞ্জ স্ক্যান করার সময় বারবার প্যারেন্ট পেজে ফিরে যেতে হয় না।

Comparing B-Trees and LSM-Trees

ডেটাবেজ স্টোরেজ ইঞ্জিনের ক্ষেত্রে B-Trees এবং LSM-Trees এর মধ্যে তুলনা করার সময় একটি সাধারণ নিয়ম হলো: LSM-trees সাধারণত রাইট (Write) এর জন্য দ্রুততর, আর B-trees রিড (Read) এর জন্য দ্রুততর।

LSM-trees এ রিড স্লো হওয়ার কারণ হলো, একটি ডেটা খোঁজার জন্য একে বিভিন্ন লেভেলের ডেটা স্ট্রাকচার এবং একাধিক SSTables চেক করতে হয়। তবে পারফরম্যান্স সবসময় আপনার কাজের ধরন বা ওয়ার্কলোডের (Workload) ওপর নির্ভর করে।

Advantages of LSM-trees (LSM-trees এর সুবিধা)

১. Write Amplification কম হওয়া: B-tree তে যেকোনো ডেটা অন্তত দুইবার লেখা হয়: একবার Write-Ahead Log (WAL) এ এবং একবার ট্রি পেজে। এমনকি পেজের সামান্য কয়েক বাইট পরিবর্তন হলেও পুরো পেজটি পুনরায় লিখতে হয়। অন্যদিকে, LSM-trees ও কমপ্যাকশনের (Compaction) কারণে ডেটা বারবার লেখে। এই যে একবার ডেটা ইনপুট দিলে ডিস্কে কয়েকবার লেখা হয়, একে বলে Write Amplification। LSM-trees এ রাইট অ্যাম্প্লিফিকেশন তুলনামূলক কম হতে পারে এবং এটি সিকোয়েনশিয়াল ভাবে ডেটা লেখে (Random write এর বদলে), যা বিশেষ করে ম্যাগনেটিক হার্ড ড্রাইভে অনেক দ্রুত কাজ করে।

২. বেশি রাইট থ্রুপুট (Higher Write Throughput): LSM-trees খুব সহজে উচ্চমাত্রার রাইট লোড সামলাতে পারে। এর কারণ এটি ডিস্কে সরাসরি ওভাররাইট না করে সিকোয়েনশিয়াল ভাবে SSTable ফাইল তৈরি করে।

৩. ভালো কম্প্রেশন এবং কম জায়গা দখল: B-tree তে 'Fragmentation' এর কারণে ডিস্কের কিছু জায়গা অব্যবহৃত থেকে যায় (যখন পেজ স্প্লিট হয় বা কোনো রো পেজে ফিট হয় না)। LSM-trees পেজ-ওরিয়েন্টেড নয় এবং নিয়মিত কমপ্যাকশনের মাধ্যমে ফ্র্যাগমেন্টেশন দূর করে, তাই এটি ডিস্কে ডেটা অনেক বেশি কমপ্যাক্ট বা ঘনভাবে রাখতে পারে।

উপরের চিত্রটি বইতে নেই , বুঝার সুবিধার জন্য Byte Byte Go এর ব্লগ থেকে নেয়া

Downsides of LSM-trees (LSM-trees এর অসুবিধা)

১. কমপ্যাকশন ইন্টারফেয়ারেন্স (Compaction Interference): ব্যাকগ্রাউন্ডে যখন কমপ্যাকশন প্রসেস চলে, তখন সেটি ডিস্কের রিসোর্স (I/O bandwidth) ব্যবহার করে। এর ফলে অনেক সময় রিড বা রাইট রিকোয়েস্টকে অপেক্ষা করতে হয়। যদিও গড় রেসপন্স টাইম ঠিক থাকে, কিন্তু মাঝেমধ্যে কিছু কুয়েরি অনেক বেশি সময় নিতে পারে (High percentiles response time), যা B-trees এর তুলনায় কম প্রেডিক্টেবল।

২. কমপ্যাকশন বনাম ইনকামিং রাইট: যদি ডেটাবেজে রাইট করার গতি খুব বেশি হয় এবং কমপ্যাকশন সেই গতিতে কাজ করতে না পারে, তবে ডিস্কে আন-মার্জড (Unmerged) সেগমেন্ট ফাইল বাড়তে থাকে। এতে একসময় ডিস্ক ফুল হয়ে যেতে পারে এবং রিড অপারেশনও অনেক স্লো হয়ে যায় কারণ তখন অনেক বেশি ফাইল চেক করতে হয়।

৩. ট্রানজ্যাকশনাল সেমান্টিকস (Transactional Semantics): B-tree এর একটি বড় সুবিধা হলো—প্রতিটি কী (Key) ইনডেক্সের ঠিক একটি নির্দিষ্ট জায়গাতেই থাকে। অন্যদিকে, LSM-tree তে একই Key-এর একাধিক কপি বিভিন্ন সেগমেন্টে থাকতে পারে। একারণে যেসব ডেটাবেজ স্ট্রং আইসোলেশন বা ট্রানজ্যাকশন সাপোর্ট করে, তাদের জন্য B-tree সুবিধাজনক। কারণ B-tree তে সরাসরি কী-র রেঞ্জ লক (Lock) করা সহজ।

কখন কোনটি ব্যবহার করবেন?

  • B-trees: যদি আপনার অ্যাপ্লিকেশন রিড হেভি হয় এবং আপনার কনসিস্টেন্ট ও প্রেডিক্টেবল পারফরম্যান্স দরকার হয়। এটি রিলেশনাল ডেটাবেজের জন্য আদর্শ।

  • LSM-trees: যদি আপনার অ্যাপ্লিকেশন রাইট হেভি হয় এবং স্টোরেজ স্পেস সাশ্রয় করা জরুরি হয়। আধুনিক অনেক নো-এসকিউএল (NoSQL) ডেটাবেজে এটি জনপ্রিয় হচ্ছে।

Other Indexing Structures

এতক্ষণ আমরা শুধু Key-Value Index নিয়ে আলোচনা করেছি যা অনেকটা রিলেশনাল ডাটাবেজের Primary Key-র মতো। এটি একটি রেকর্ডকে ইউনিকভাবে (Uniquely) খুঁজে বের করতে সাহায্য করে। কিন্তু বাস্তব জীবনে আমাদের আরও অনেক ধরণের ইনডেক্স প্রয়োজন হয়।

সেকেন্ডারি ইনডেক্স (Secondary Indexes)

রিলেশনাল ডাটাবেজে CREATE INDEX কমান্ড দিয়ে আমরা একই টেবিলে একাধিক সেকেন্ডারি ইনডেক্স তৈরি করতে পারি।

ধরুন আমাদের কাছে একটি রিলেশনাল ডাটাবেজ টেবিল আছে যার নাম orders। এই টেবিলে বিভিন্ন ইউজারের অর্ডারের তথ্য রাখা হয়। টেবিলটির গঠন এমন:

iduser_idproduct
110Mobile
210Laptop
320Tablet

এখানে id হলো Primary Key, তাই এই কলামের উপর ডাটাবেজ নিজে থেকেই একটি Primary Index তৈরি করে। এই ইনডেক্সের কারণে ডাটাবেজ খুব দ্রুত নির্দিষ্ট id-এর রো খুঁজে পায়। কিন্তু বাস্তবে আমরা সব সময় id দিয়ে ডাটা খুঁজি না। অনেক সময় আমাদের দরকার হয় একটি নির্দিষ্ট user_id-এর সব অর্ডার বের করা।

যদি user_id কলামের উপর কোনো ইনডেক্স না থাকে, তাহলে ডাটাবেজকে পুরো orders টেবিলটি এক রো এক রো করে স্ক্যান করতে হয় এবং প্রতিটা রোর user_id মিলিয়ে দেখতে হয়। টেবিল ছোট হলে এতে সমস্যা নেই, কিন্তু টেবিলে যদি লক্ষ লক্ষ রো থাকে, তাহলে এই কাজটি খুব ধীর হয়ে যায়।

এই সমস্যার সমাধানের জন্য আমরা user_id কলামের উপর Secondary Index তৈরি করি। Secondary Index মানে হলো Primary Key ছাড়া অন্য কোনো কলামের উপর তৈরি করা ইনডেক্স। এখানে CREATE INDEX কমান্ড ব্যবহার করে একই টেবিলের উপর একাধিক Secondary Index তৈরি করা সম্ভব।

এখন যদি আমরা user_id এর উপর একটি Secondary Index তৈরি করি, তাহলে ডাটাবেজ আলাদা করে একটি ইনডেক্স স্ট্রাকচার রাখে। সেই ইনডেক্সে দেখা যায় যে user_id = 10 মানটি টেবিলের একাধিক রোতে আছে—যেমন id = 1 এবং id = 2। যেহেতু Secondary Index ইউনিক নয়, তাই একটি key-এর বিপরীতে একাধিক রো ম্যাপ করতে হয়।

এই জায়গায় ডাটাবেজ সাধারণত দুইভাবে কাজ করে। এক পদ্ধতিতে, user_id = 10 key-এর নিচে একটি তালিকা রাখা হয়, যেখানে id = 1 এবং id = 2—এই রো দুটির রেফারেন্স থাকে। ফলে ডাটাবেজ খুব সহজেই বুঝতে পারে যে এই user_id-এর সাথে কোন কোন রো যুক্ত আছে। অন্য পদ্ধতিতে, ইনডেক্সে সরাসরি user_id-এর সাথে Primary Key বা Row ID যোগ করে রাখা হয়, যেমন (10, 1) এবং (10, 2)। এতে করে প্রতিটি ইনডেক্স এন্ট্রি ইউনিক হয়ে যায় এবং ডাটাবেজ সেই ইউনিক কম্বিনেশন ব্যবহার করে রো খুঁজে বের করে।

সবশেষে বলা যায়, Secondary Index টেবিলের বাইরের কোনো আলাদা বিষয় নয়, বরং এটি সেই একই টেবিলের ডাটাকে ভিন্ন একটি কলামের মাধ্যমে দ্রুত খুঁজে পাওয়ার জন্য তৈরি করা একটি সহায়ক কাঠামো। টেবিলের ভিতরের ডাটাই ইনডেক্সের মাধ্যমে সাজানোভাবে রাখা হয়, যাতে সার্চ এবং JOIN অপারেশন অনেক বেশি কার্যকর ও দ্রুত হয়।

ইনডেক্সের ভেতরে ভ্যালু সংরক্ষণ (Storing Values Within the Index)

উপরে দেখানো orders টেবিলে অর্ডারের আসল ডাটা রাখা হয় এবং ডাটাগুলো সাধারণত কোনো নির্দিষ্ট ক্রমে সাজানো থাকে না। এই অসংগঠিতভাবে রাখা মূল ডাটার জায়গাটাকেই বলা হয় Heap File। এখন যখন আমরা কোনো ইনডেক্স ব্যবহার করে সার্চ করি, তখন আসলে আমরা ইনডেক্সে থাকা Key খুঁজি। কিন্তু সেই Key পাওয়ার পর ডাটাবেজকে জানতে হয়—এই Key-এর সাথে যুক্ত আসল ডাটা টেবিলের কোথায় আছে। এখানেই ইনডেক্সের Value গুরুত্বপূর্ণ হয়ে ওঠে।

সবচেয়ে সাধারণ ক্ষেত্রে, ইনডেক্সের Value হিসেবে থাকে Heap File Reference। এর মানে হলো, ইনডেক্সে শুধু Key আর একটি লোকেশন পয়েন্টার রাখা হয়, যা বলে দেয় টেবিলের কোন জায়গায় আসল Row-টি আছে। উদাহরণ হিসেবে বলা যায়, যদি user_id = 10 দিয়ে ইনডেক্সে খোঁজা হয়, তাহলে ইনডেক্স প্রথমে জানিয়ে দেয় id = 1 এবং id = 2 এই দুইটা রো দরকার, তারপর ডাটাবেজ Heap File-এ গিয়ে সেই রো দুটো তুলে আনে। এর বড় সুবিধা হলো, টেবিলের আসল ডাটা এক জায়গাতেই থাকে। আপনি যত খুশি Secondary Index বানান না কেন, ডাটা ডুপ্লিকেট হয় না—শুধু রেফারেন্স বাড়ে।

কিন্তু সব ডাটাবেজ সবসময় এই Heap File পদ্ধতি ব্যবহার করে না। কিছু ক্ষেত্রে ইনডেক্স নিজেই ডাটার আসল স্টোরেজ হয়ে যায়। একে বলা হয় Clustered Index। এখানে ইনডেক্সের ভেতরেই পুরো Row রাখা থাকে। MySQL-এর InnoDB ইঞ্জিনে Primary Key সবসময়ই একটি Clustered Index। এর মানে হলো, টেবিলের ডাটা বাস্তবে Primary Key-এর ক্রম অনুযায়ী সাজানো থাকে। ফলে যখন Primary Key দিয়ে ডাটা পড়া হয়, তখন ইনডেক্স পড়লেই সরাসরি পুরো Row পাওয়া যায়, আলাদা করে Heap File-এ যেতে হয় না। এই কারণে রিড পারফরম্যান্স খুব দ্রুত হয়।

তবে Clustered Index সব সমস্যার সমাধান নয়। কারণ যেহেতু ডাটাগুলো ইনডেক্সের ভেতরেই রাখা, তাই একই ডাটার একাধিক কপি তৈরি হওয়ার সম্ভাবনা থাকে, বিশেষ করে যদি একাধিক Clustered বা বড় ইনডেক্স রাখা হয়। এছাড়া Insert, Update বা Delete করার সময় ইনডেক্সের স্ট্রাকচার নতুন করে সাজাতে হয়, ফলে Write অপারেশনের খরচ বেড়ে যায় এবং ট্রানজ্যাকশন ম্যানেজ করা তুলনামূলক কঠিন হয়।

এই দুইয়ের মাঝামাঝি একটি ধারণা হলো Covering Index। এখানে পুরো Row ইনডেক্সে রাখা হয় না, আবার শুধু রেফারেন্সও না। বরং কুয়েরিতে যেসব কলাম দরকার, শুধু সেগুলোই ইনডেক্সের ভেতরে রাখা হয়। ধরুন আপনার কুয়েরি শুধু user_id এবং product কলাম চায়। যদি এই দুইটা কলাম ইনডেক্সে থাকে, তাহলে ডাটাবেজকে আর মূল টেবিলে যেতে হয় না—শুধু ইনডেক্স পড়েই কুয়েরির উত্তর দিয়ে দিতে পারে। তখন বলা হয়, “index covers the query”। এতে রিড অনেক দ্রুত হয়, কিন্তু Clustered Index-এর মতো অত বেশি ডাটা ডুপ্লিকেটও হয় না।

সবশেষে মনে রাখার বিষয় হলো, ইনডেক্সে যত বেশি ডাটা রাখা হবে, পড়া তত দ্রুত হবে, কিন্তু লেখার সময় তত বেশি ওভারহেড তৈরি হবে। তাই Heap File reference, Clustered Index, বা Covering Index—কোনটা ব্যবহার হবে, সেটা নির্ভর করে টেবিলের ব্যবহার কেমন, রিড বেশি নাকি রাইট বেশি, এবং ট্রানজ্যাকশনাল গ্যারান্টি কতটা গুরুত্বপূর্ণ তার উপর।

মাল্টি-কলাম ইনডেক্স (Multi-column Indexes)

এতক্ষণ আমরা শুধু একটি Key নিয়ে আলোচনা করেছি। কিন্তু যদি আমাদের একাধিক কলাম (যেমন: নাম এবং শহর) দিয়ে একসাথে সার্চ করতে হয়, তখন মাল্টি-কলাম ইনডেক্স লাগে। Example

SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079

AND longitude > -0.1162 AND longitude < -0.1004;
  • Concatenated Index: এটি সবথেকে সাধারণ পদ্ধতি। কয়েকটি কলামকে জোড়া লাগিয়ে একটি Key বানানো হয়।

    • উদাহরণ: ফোনবুক। যেখানে (Lastname, Firstname) ক্রমে নাম থাকে। আপনি "Lastname" দিয়ে সার্চ করতে পারবেন, কিন্তু শুধু "Firstname" দিয়ে সার্চ করলে এই ইনডেক্স কোনো কাজে আসবে না।
  • Multi-dimensional Indexes: এটি মূলত জিওস্পেশাল (Geospatial) ডাটার জন্য ব্যবহৃত হয় (যেমন: ম্যাপে অক্ষাংশ ও দ্রাঘিমাংশ দিয়ে রেস্টুরেন্ট খোঁজা)।

    • সাধারণ B-tree দিয়ে একই সাথে দুটি ডাইমেনশনে (Latitude and Longitude) রেঞ্জ কুয়েরি করা কঠিন।

    • সমাধান: এজন্য R-trees বা স্পেস-ফিলিং কার্ভ (Space-filling curve) ব্যবহার করা হয়। এটি ই-কমার্স সাইটে রঙের রেঞ্জ (Red, Green, Blue) বা আবহাওয়ার ডাটা (Date, Temperature) সার্চ করতেও ব্যবহৃত হয়।

ফুল-টেক্সট সার্চ এবং ফাজি ইনডেক্স (Full-text Search and Fuzzy Indexes)

এতক্ষণের ইনডেক্সগুলো ছিল একদম নিখুঁত (Exact) ডাটা খোঁজার জন্য। কিন্তু ইউজার যদি বানান ভুল করে (Misspelled words) তবে কী হবে? একে বলে Fuzzy Querying

  • Lucene: এটি একটি সার্চ ইঞ্জিন লাইব্রেরি যা Edit Distance (যেমন: একটি অক্ষর পরিবর্তন বা বাদ দেওয়া) ব্যবহার করে ভুল বানানের শব্দও খুঁজে পায়।

  • Levenshtein Automaton: Lucene এর ইন-মেমোরি ইনডেক্স একটি Finite State Automaton ব্যবহার করে যা কিউ-ওয়ার্ডের কাছাকাছি শব্দগুলো দ্রুত খুঁজে বের করতে পারে।

ইন-মেমোরি ডাটাবেজ (Keeping Everything in Memory)

এখন পর্যন্ত আলোচিত ডাটা স্ট্রাকচারগুলো ডিস্কের সীমাবদ্ধতা সামলানোর জন্য তৈরি করা হয়েছিল। ডিস্ক ধীর এবং জটিল হলেও আমরা ব্যবহার করি কারণ এটি ডাটা নষ্ট হয় না এবং RAM-এর তুলনায় সস্তা। কিন্তু RAM ধীরে ধীরে সস্তা হওয়ায় অনেক ডাটাসেট পুরোপুরি মেমরিতে রাখা সম্ভব হচ্ছে, যার ফলে in-memory database এর ধারণা এসেছে।

কিছু in-memory ডাটাবেজ শুধু ক্যাশ হিসেবে ব্যবহৃত হয় (যেমন Memcached), যেখানে ডাটা হারালেও সমস্যা নেই। আবার কিছু in-memory ডাটাবেজ ডিউরেবিলিটি (যেমন Redis) নিশ্চিত করে—লগ লেখা, snapshot নেওয়া বা replication-এর মাধ্যমে। যদিও তারা ডিস্কে লেখে, তবু পড়ার কাজ পুরোপুরি মেমরি থেকেই হয়।

In-memory ডাটাবেজ দ্রুত হয় শুধু ডিস্ক না পড়ার কারণে নয়, বরং ডিস্কে লেখার উপযোগী ফরম্যাটে ডাটা encode/decode করার ওভারহেড না থাকার কারণে। এছাড়া মেমরিতে থাকার কারণে Redis-এর মতো সিস্টেমে জটিল ডাটা স্ট্রাকচার সহজে সাপোর্ট করা যায়।

গবেষণায় দেখা গেছে, in-memory ডাটাবেজকে এমনভাবে ডিজাইন করা যায় যাতে মেমরি কম হলে কম ব্যবহৃত ডাটা ডিস্কে পাঠিয়ে দেওয়া যায় (anti-caching), তবুও ডিস্ক-কেন্দ্রিক আর্কিটেকচারের জটিলতা না ফিরিয়ে। ভবিষ্যতে non-volatile memory জনপ্রিয় হলে ডাটাবেজ ডিজাইনে আরও বড় পরিবর্তন আসতে পারে।

Transaction Processing or Analytics?

ডেটাবেস ব্যবহারের বিবর্তন এবং ব্যবহারের ধরনের ওপর ভিত্তি করে ডেটাবেস সিস্টেমকে দুটি প্রধান ভাগে ভাগ করতে পারি। নিচে তার বিস্তারিত আলোচনা করা হলো:

ট্রানজ্যাকশনের বিবর্তন (Evolution of Transaction)

শুরুর দিকে ডেটাবেস মূলত বাণিজ্যিক লেনদেনের (যেমন: বিক্রি, অর্ডার বা বেতন প্রদান) জন্য ব্যবহৃত হতো। যদিও বর্তমানে ডেটাবেসে অনেক অ-আর্থিক তথ্য (যেমন: ব্লগে কমেন্ট বা গেমের অ্যাকশন) জমা থাকে, তবুও 'Transaction' শব্দটি এখনও রয়ে গেছে। এটি মূলত কতগুলো রিড (Read) এবং রাইট (Write)-এর একটি যৌক্তিক ইউনিটকে বোঝায়।

\> বিশেষ দ্রষ্টব্য: বইতে উল্লেখ করা হয়েছে যে, ট্রানজ্যাকশন মানেই যে তাকে ACID প্রপার্টি মেনে চলতে হবে এমন নয়। ট্রানজ্যাকশন প্রসেসিং বলতে মূলত সেই সিস্টেমকে বোঝায় যেখানে ক্লায়েন্ট খুব দ্রুত (Low-latency) ডেটা রিড বা রাইট করতে পারে। এটি ব্যাচ প্রসেসিং (Batch Processing) থেকে আলাদা, যা সাধারণত দিনে একবার বা নির্দিষ্ট সময় পর পর চালানো হয়।

OLTP (Online Transaction Processing)

যখন কোনো অ্যাপ্লিকেশন ব্যবহারকারীর ইনপুটের ভিত্তিতে অল্প কিছু রেকর্ড খুঁজে বের করে (Key ব্যবহার করে) এবং ডেটা ইনসার্ট বা আপডেট করে, তখন সেই প্যাটার্নকে বলা হয় OLTP

  • উদাহরণ: অ্যাড্রেস বুক থেকে কারো কন্টাক্ট দেখা, ব্লগে কমেন্ট করা বা গেমে কোনো মুভ দেওয়া।

  • এটি ইন্টারঅ্যাক্টিভ এবং ব্যবহারকারী সরাসরি এর সাথে যুক্ত থাকে।

OLAP (Online Analytic Processing)

অন্যদিকে, ডেটা অ্যানালিটিক্সের ক্ষেত্রে ডেটাবেস ব্যবহারের ধরন সম্পূর্ণ আলাদা। এখানে ব্যবহারকারী কোনো একক রেকর্ড খোঁজে না, বরং লক্ষ লক্ষ রেকর্ড স্ক্যান করে কিছু নির্দিষ্ট কলামের ওপর ভিত্তি করে পরিসংখ্যান (Aggregate statistics) বের করে। একে বলা হয় OLAP

  • উদাহরণ: * জানুয়ারি মাসে প্রতিটি স্টোরে মোট কত আয় হয়েছে?

    • প্রমোশন চলাকালীন কলা বিক্রির হার স্বাভাবিকের চেয়ে কতটুকু বেড়েছে?
  • এগুলো সাধারণত বিজনেস অ্যানালিস্টরা ব্যবহার করেন এবং এর ওপর ভিত্তি করে ম্যানেজমেন্ট বড় সিদ্ধান্ত নেয় (Business Intelligence)।

OLTP বনাম OLAP-এর তুলনামূলক বৈশিষ্ট্য

বৈশিষ্ট্য (Property)OLTP (Transaction Processing)OLAP (Analytic Systems)
Main read patternঅল্প সংখ্যক রেকর্ড কি (Key) দিয়ে খোঁজা হয়অনেক রেকর্ডের ওপর অ্যাগ্রিগেশন (Sum, Count) করা হয়
Main write patternব্যবহারকারীর ইনপুট থেকে র‍্যান্ডম এবং দ্রুত রাইটএকসাথে অনেক ডেটা ইমপোর্ট (ETL) বা ইভেন্ট স্ট্রিম
Primarily used byEnd user , ওয়েব অ্যাপের মাধ্যমেইন্টারনাল অ্যানালিস্ট, সিদ্ধান্ত গ্রহণের জন্য
What data representsডেটার বর্তমান অবস্থা (Latest state)সময়ের সাথে ঘটে যাওয়া ইভেন্টের ইতিহাস (History)
Dataset sizeগিগাবাইট থেকে টেরাবাইট (GB to TB)টেরাবাইট থেকে পেটাবাইট (TB to PB)

ডেটা ওয়্যারহাউস (Data Warehouse)-এর প্রয়োজনীয়তা

শুরুর দিকে একই ডেটাবেস দিয়ে OLTP এবং OLAP উভয় কাজই চালানো হতো কারণ SQL ভাষাটি উভয় ক্ষেত্রে বেশ ফ্লেক্সিবল। কিন্তু ১৯৮০ থেকে ১৯৯০ এর দশকের শেষের দিকে কোম্পানিগুলো অ্যানালিটিক্সের জন্য আলাদা ডেটাবেস ব্যবহার শুরু করে। এই বিশেষ উদ্দেশ্যমূলক আলাদা ডেটাবেসকেই বলা হয় Data Warehouse

Data Warehousing

মূলত একটি প্রতিষ্ঠানের ট্রানজেকশনাল সিস্টেম (OLTP) এবং অ্যানালিটিক্যাল সিস্টেম (Data Warehouse) এর মধ্যে পার্থক্য এবং কেন একটি আলাদা ডাটা ওয়্যারহাউস প্রয়োজন, তা অনুসন্ধান করা যেতে পারে ।

OLTP বনাম ডাটা ওয়্যারহাউস (Data Warehousing) এর প্রয়োজনীয়তা

একটি বড় এন্টারপ্রাইজে সাধারণত কয়েক ডজন ভিন্ন ভিন্ন ট্রানজেকশনাল সিস্টেম থাকে (যেমন: ওয়েবসাইট, স্টোর চেকআউট সিস্টেম, ইনভেন্টরি ট্র্যাকিং, ইত্যাদি)। এগুলোকে বলা হয় OLTP (Online Transaction Processing)

  • OLTP এর সীমাবদ্ধতা: এই সিস্টেমগুলো মূলত কাস্টমারদের দ্রুত সার্ভিস দেওয়ার জন্য তৈরি। এগুলো সবসময় সচল (Highly Available) থাকতে হয় এবং খুব দ্রুত (Low Latency) রেসপন্স করতে হয়।

  • কেন সরাসরি কুয়েরি করা হয় না? যদি একজন বিজনেস অ্যানালিস্ট একটি বড় অ্যানালিটিক্যাল কুয়েরি (যা পুরো ডাটাবেস স্ক্যান করে) সরাসরি OLTP ডাটাবেসে চালায়, তবে সেটি সিস্টেমের পারফরম্যান্স মারাত্মকভাবে কমিয়ে দিতে পারে। এতে সাধারণ গ্রাহকদের ট্রানজেকশনে সমস্যা তৈরি হয়। একারণে ডাটাবেস অ্যাডমিনিস্ট্রেটররা সরাসরি OLTP-তে অ্যানালিটিক্যাল কুয়েরি চালাতে দিতে চান না।

ডাটা ওয়্যারহাউস (Data Warehouse) এবং ETL প্রক্রিয়া

Data Warehouse হলো একটি আলাদা ডাটাবেস, যেখানে প্রতিষ্ঠানের সমস্ত OLTP সিস্টেমের ডাটা একটি Read-only কপি হিসেবে রাখা হয়। এখানে অ্যানালিস্টরা মূল ট্রানজেকশন সিস্টেমের ক্ষতি না করেই যত খুশি কুয়েরি করতে পারেন।

ডাটা ওয়্যারহাউসে ডাটা নিয়ে আসার এই প্রক্রিয়াকে বলা হয় ETL (Extract–Transform–Load):

  1. Extract: বিভিন্ন OLTP ডাটাবেস থেকে ডাটা সংগ্রহ করা (পিরিওডিক ডাম্প বা কন্টিনিউয়াস স্ট্রিম এর মাধ্যমে)।

  2. Transform: ডাটাগুলোকে অ্যানালিসিসের উপযোগী ফরম্যাটে রূপান্তর করা এবং ক্লিন করা।

  3. Load: ডাটাগুলোকে ডাটা ওয়্যারহাউসে জমা করা।

ডাটা ওয়্যারহাউসের ব্যবহারিক প্রেক্ষাপট

  • বড় কোম্পানি: বড় কোম্পানিতে ডাটা ওয়্যারহাউস প্রায় অপরিহার্য, কারণ তাদের অনেকগুলো ভিন্ন ভিন্ন OLTP সিস্টেম থাকে।

  • ছোট কোম্পানি: ছোট কোম্পানিতে সাধারণত ডাটা ওয়্যারহাউস দেখা যায় না। কারণ তাদের ডাটা পরিমাণ কম থাকে যা সাধারণ SQL ডাটাবেস বা স্প্রেডশিটেই অ্যানালিসিস করা যায়।

সুবিধা: ডাটা ওয়্যারহাউসের প্রধান সুবিধা হলো এটি অ্যানালিটিক্যাল কুয়েরি প্যাটার্নের জন্য অপ্টিমাইজ করা যায়। OLTP-তে ব্যবহৃত ইনডেক্সিং অ্যালগরিদমগুলো অ্যানালিটিক্যাল কুয়েরির জন্য খুব একটা কার্যকর হয় না।

OLTP এবং ডাটা ওয়্যারহাউসের মধ্যে পার্থক্য (Divergence)

যদিও ডাটা ওয়্যারহাউস এবং রিলেশনাল OLTP ডাটাবেস উভয়ই SQL Interface ব্যবহার করে, কিন্তু পর্দার আড়ালে (Internals) তারা সম্পূর্ণ আলাদাভাবে কাজ করে।

  • SQL এবং গ্রাফিকাল টুলস: ডাটা ওয়্যারহাউসে SQL ব্যবহার করা হয় কারণ এটি অ্যানালিটিক্যাল কুয়েরির জন্য খুবই উপযুক্ত। বিভিন্ন গ্রাফিকাল টুল দিয়ে ডাটাকে Drill-down, Slicing এবং Dicing করা যায়।

  • ভিন্ন ভিন্ন ইঞ্জিন: মাইক্রোসফট এসকিউএল সার্ভার (MS SQL Server) বা স্যাপ হানা (SAP HANA) একই প্রোডাক্টের ভেতরে ট্রানজেকশন এবং ওয়্যারহাউজিং সাপোর্ট দিলেও, বাস্তবে তারা দুটি আলাদা স্টোরেজ এবং কুয়েরি ইঞ্জিন ব্যবহার করে।

কিছু জনপ্রিয় ডাটা ওয়্যারহাউস সিস্টেম:

কিছু বাণিজ্যিক এবং ওপেন সোর্স সিস্টেম:

  • Commercial: Teradata, Vertica, SAP HANA, Amazon RedShift (ParAccel-এর হোস্টেড ভার্সন)।

  • Open Source (SQL-on-Hadoop): Apache Hive, Spark SQL, Cloudera Impala, Facebook Presto, Apache Tajo, এবং Apache Drill। এগুলোর অনেকগুলোই গুগলের Dremel পেপারের আইডিয়া থেকে তৈরি।

Stars and Snowflakes: Schemas for Analytics

এনালিটিক্স বা ডেটা ওয়্যারহাউজের ক্ষেত্রে বহুল ব্যবহৃত ডেটা মডেলিং পদ্ধতি নিয়ে আলোচনা করা হবে এখন। ট্রানজ্যাকশন প্রসেসিং (OLTP) এর তুলনায় এনালিটিক্যাল সিস্টেমে ডেটা মডেলের বৈচিত্র্য কম থাকে এবং এখানে মূলত Star Schema বা Dimensional Modeling ব্যবহার করা হয়।

Fact Table (তথ্যের কেন্দ্রবিন্দু)

ডেটা ওয়্যারহাউজের মডেলে একদম কেন্দ্রে থাকে Fact Table

  • ইভেন্ট হিসেবে ডেটা: ফ্যাক্ট টেবিলের প্রতিটি রো (row) একটি নির্দিষ্ট সময়ে ঘটে যাওয়া কোনো ইভেন্ট বা ঘটনাকে নির্দেশ করে। যেমন: একজন কাস্টমারের একটি প্রোডাক্ট কেনা। যদি এটি কোনো ওয়েবসাইট হয়, তবে প্রতিটি ক্লিক বা পেজ ভিউ হবে একেকটি ফ্যাক্ট।

  • বিশাল আকার: প্রতিটি ছোট ঘটনাকে আলাদাভাবে রেকর্ড করা হয় যাতে ভবিষ্যতে বিশ্লেষণের সময় সর্বোচ্চ flexibility পাওয়া যায়। এর ফলে ফ্যাক্ট টেবিলগুলো বিশাল হয়; অ্যাপল বা ওয়ালমার্টের মতো বড় প্রতিষ্ঠানের ক্ষেত্রে এটি কয়েক পেটাবাইট (Petabytes) পর্যন্ত হতে পারে।

  • কলামের ধরন: ফ্যাক্ট টেবিলে মূলত দুই ধরনের কলাম থাকে:

    1. Attributes: যেমন পণ্যের বিক্রয়মূল্য (price) বা সাপ্লায়ারের খরচ (cost), যা দিয়ে প্রফিট মার্জিন ক্যালকুলেট করা যায়।

    2. Foreign Keys: এগুলো অন্যান্য ডাইমেনশন টেবিলের সাথে সংযোগ স্থাপন করে।

Dimension Tables (Context)

ফ্যাক্ট টেবিলের চারদিকে থাকা টেবিলগুলোকে বলা হয় Dimension Tables। ফ্যাক্ট যদি হয় একটি "ঘটনা", তবে ডাইমেনশনগুলো উত্তর দেয় সেই ঘটনার— কে, কী, কোথায়, কখন, কীভাবে এবং কেন (Who, What, Where, When, How, Why)

(এখানে কেন্দ্রে fact_sales টেবিল এবং চারদিকে বিভিন্ন ডাইমেনশন টেবিল যেমন dim_product , dim_date, dim_store ইত্যাদি থাকবে।)

  • উদাহরণ: dim_product টেবিলে প্রোডাক্টের SKU, বর্ণনা, ব্র্যান্ড, ক্যাটাগরি ইত্যাদি থাকে। fact_sales টেবিলে থাকা একটি ফরেন Key নির্দেশ করে ওই নির্দিষ্ট ট্রানজ্যাকশনে কোন প্রোডাক্টটি কেনা হয়েছে।

  • তারিখ এবং সময়: মজার ব্যাপার হলো, ডেটা ওয়্যারহাউজে তারিখ এবং সময়কেও আলাদা ডাইমেনশন টেবিল হিসেবে রাখা হয়। এতে ছুটির দিন (public holidays) বা বিশেষ ইভেন্টগুলো আলাদাভাবে ট্র্যাক করা যায়, যা সাধারণ ডেট-টাইম ফরম্যাটে সম্ভব নয়।

Star Schema vs. Snowflake Schema

  • Star Schema: যখন ফ্যাক্ট টেবিলটি মাঝখানে থাকে এবং ডাইমেনশন টেবিলগুলো তাকে ঘিরে থাকে, তখন এটি দেখতে একটি তারার (Star) মতো লাগে। একেই স্টার স্কিমা বলে। এটি এনালিস্টদের জন্য ব্যবহার করা খুব সহজ।

  • Snowflake Schema: এটি স্টার স্কিমার একটি ভ্যারিয়েশন। এখানে ডাইমেনশন টেবিলগুলোকে আরও ভেঙে ছোট ছোট সাব-ডাইমেনশনে ভাগ করা হয় (Normalization)। যেমন: dim_product টেবিলের ভেতরে সরাসরি 'Brand' এর নাম না লিখে 'Brand ID' রাখা এবং ব্র্যান্ডের জন্য আলাদা টেবিল তৈরি করা। এটি স্টার স্কিমার চেয়ে বেশি নরমালাইজড, তবে স্টার স্কিমা বেশি জনপ্রিয় কারণ এটি পরিচালনা করা সহজ।

টেবিলের গঠনগত বৈশিষ্ট্য

  • Wide Tables: ডেটা ওয়্যারহাউজের টেবিলগুলো সাধারণত অনেক Wide হয়। একটি ফ্যাক্ট টেবিলে ১০০ থেকে শুরু করে কয়েকশ কলাম পর্যন্ত থাকতে পারে।

  • মেটাডেটা: ডাইমেনশন টেবিলগুলোতেও প্রচুর তথ্য থাকে। যেমন একটি dim_store টেবিলে দোকানের আয়তন, সেখানে বেকারি আছে কি না, হাইওয়ে থেকে দূরত্ব কত—এই সব বিশ্লেষণধর্মী তথ্য সংরক্ষিত থাকে।

ফ্যাক্ট টেবিল = কী ঘটেছে (ঘটনা)। ডাইমেনশন টেবিল = ঘটনার বিস্তারিত তথ্য (কার দ্বারা, কোথায়, কখন)।

Column-Oriented Storage

যখন আমাদের ডাটাবেসে ট্রিলিয়ন ট্রিলিয়ন রো (rows) এবং পেটাবাইট (petabytes) স্কেলে ডাটা থাকে (বিশেষ করে Fact Tables এ), তখন সেগুলো স্টোর করা এবং কুয়েরি করা বেশ চ্যালেঞ্জিং হয়ে দাঁড়ায়। সাধারণত ডাটা ওয়্যারহাউস কুয়েরিতে সব কলামের প্রয়োজন হয় না, বরং নির্দিষ্ট ৪-৫টি কলাম নিয়ে কাজ করা হয়।

সমস্যা: Row-Oriented Storage (OLTP স্টাইল)

অধিকাংশ প্রথাগত ডাটাবেস (যেমন: MySQL, PostgreSQL) ডাটাকে Row-oriented পদ্ধতিতে সংরক্ষণ করে। এর মানে হলো একটি রো-এর সমস্ত ভ্যালু ডিস্কে পাশাপাশি থাকে।

সমস্যাটি যেখানে হয়: যদি আপনার fact_sales টেবিলে ১০০টি কলাম থাকে এবং আপনি শুধু ৩টি কলামের ডাটা (যেমন: তারিখ, প্রোডাক্ট আইডি এবং পরিমাণ) জানতে চান, তাহলেও ডাটাবেস ইঞ্জিনকে পুরো Row-টি (সব ১০০টি কলামসহ) মেমোরিতে লোড করতে হয়। এরপর সেগুলোকে ফিল্টার করে কাঙ্ক্ষিত কলামগুলো বের করতে হয়। এটি অনেক সময় সাপেক্ষ এবং মেমোরি অপচয় করে।

সমাধান: Column-Oriented Storage

কলাম-ওরিয়েন্টেড স্টোরেজের মূল ধারণাটি খুব সহজ: একটি Row-এর সব ভ্যালু একসাথে না রেখে, প্রতিটি কলামের সব ভ্যালু একসাথে আলাদা আলাদা ফাইলে স্টোর করা।

এর ফলে সুবিধা হলো, একটি কুয়েরিতে যদি শুধু ৩টি কলাম প্রয়োজন হয়, তবে ডাটাবেস ইঞ্জিন শুধুমাত্র ওই ৩টি কলামের ফাইলগুলোই রিড করবে। বাকি ৯৭টি কলামের ডাটা তাকে স্পর্শও করতে হবে না।

দ্রষ্টব্য: এটি রিলেশনাল ডাটা মডেলের জন্য বোঝা সহজ হলেও, এটি নন-রিলেশনাল ডাটার ক্ষেত্রেও প্রযোজ্য। যেমন: Parquet হলো একটি কলামার স্টোরেজ ফরম্যাট যা ডকুমেন্ট ডাটা মডেল Support করে।

প্র্যাকটিক্যাল উদাহরণ (SQL কুয়েরি)

ধরা যাক , আমাদের বিজনেস এনালিস্ট এখন একটি প্রশ্নের উত্তর জানতে চান - " মানুষ সপ্তাহের নির্দিষ্ট দিনে ফল নাকি ক্যান্ডি বেশি কেনে তা বের করতে হবে "

SELECT 
  dim_date.weekday, 
  dim_product.category, 
  SUM(fact_sales.quantity) AS quantity_sold 
FROM 
  fact_sales 
  JOIN dim_date ON fact_sales.date_key = dim_date.date_key 
  JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk 
WHERE 
  dim_date.year = 2013 
  AND dim_product.category IN ('Fresh fruit', 'Candy') 
GROUP BY 
  dim_date.weekday, 
  dim_product.category;

এই কুয়েরিটি fact_sales টেবিলের বিশাল ডাটা এক্সেস করলেও তার মাত্র ৩টি কলাম দরকার: date_key, product_sk, এবং quantity। কলাম-ওরিয়েন্টেড স্টোরেজ ব্যবহার করলে ডাটাবেস শুধু এই তিনটি কলামের ফাইলগুলোই প্রসেস করবে।

ডাটা কীভাবে রি-অ্যাসেম্বল (Reassemble) করা হয়?

আপনার মনে প্রশ্ন আসতে পারে, কলামগুলো আলাদা থাকলে আমরা পুরো একটি রো-এর ডাটা ফেরত পাবো কীভাবে? এর কৌশল হলো: প্রতিটি কলাম ফাইলে ডাটাগুলো একই ক্রমানুসারে (Order) রাখা হয়।

ধরা যাক, আপনাকে ২৩ নম্বর রো-এর ডাটা দেখতে হবে। আপনি প্রতিটি আলাদা কলাম ফাইল থেকে ২৩ নম্বর এন্ট্রিটি তুলে নিয়ে একসাথে জোড়া দিলেই পুরো ২৩ নম্বর রো-টি তৈরি হয়ে যাবে।

Column Compression

কীভাবে Column-oriented storage ব্যবহার করে ডেটাকে সংকুচিত (compress) করা যায়, যা ডিস্কের জায়গা বাঁচানোর পাশাপাশি কুয়েরি পারফরম্যান্স বহুগুণ বাড়িয়ে দেয়।

কেন কলাম কম্প্রেশন কার্যকর?

অ্যানালিটিক্যাল কুয়েরিতে সাধারণত নির্দিষ্ট কিছু কলামের ওপর অপারেশন চালানো হয়। কলাম-ওরিয়েন্টেড স্টোরেজে একই ধরনের ডেটা একসাথে থাকে, যা কম্প্রেশনের জন্য খুব উপযোগী। উদাহরণস্বরূপ, একটি কলামে যদি কোটি কোটি ডেটা থাকে কিন্তু ইউনিক ভ্যালু কম থাকে, তবে সেই রিপিটেটিভ (repetitive) ডেটাকে খুব সহজে ছোট করে ফেলা যায়।

Bitmap Encoding (বিটম্যাপ এনকোডিং)

ডেটা ওয়্যারহাউজে সবচেয়ে কার্যকর একটি কম্প্রেশন টেকনিক হলো Bitmap Encoding

  • পদ্ধতি: ধরুন একটি কলামে n সংখ্যক ইউনিক ভ্যালু আছে। আমরা এই কলামটিকে n টি আলাদা বিটম্যাপে ভাগ করতে পারি। প্রতিটি বিটম্যাপে প্রতিটি রো (row)-এর জন্য একটি করে বিট (০ অথবা ১) থাকবে। যদি ওই রো-তে নির্দিষ্ট ভ্যালুটি থাকে তবে বিট হবে ১, নাহলে ০।

  • Sparse Bitmaps & Run-length Encoding: যদি ইউনিক ভ্যালু অনেক বেশি হয়, তবে বিটম্যাপে শূন্যের (০) সংখ্যা অনেক বেড়ে যায় (যাকে sparse বলা হয়)। এই ক্ষেত্রে Run-length encoding ব্যবহার করে বিটম্যাপগুলোকে আরও ছোট বা কম্প্যাক্ট করা হয়।

বিটম্যাপ ইনডেক্সের সুবিধা (Bitwise Operations)

অ্যানালিটিক্যাল কুয়েরিগুলোতে এই বিটম্যাপ ইনডেক্সগুলো খুব দ্রুত কাজ করে। দুটি উদাহরণ দেওয়া যায়:

  • Example 1: WHERE product_sk IN (30, 68, 69) এখানে ডাটাবেস শুধু ৩০, ৬৮ এবং ৬৯ নম্বর প্রোডাক্টের তিনটি বিটম্যাপ লোড করবে এবং তাদের মধ্যে Bitwise OR অপারেশন চালাবে। এটি অত্যন্ত দ্রুত execute হয়।

  • Example 2: WHERE product_sk = 31 AND store_sk = 3 এখানে ডাটাবেস product_sk = 31 এবং store_sk = 3-এর বিটম্যাপ দুটি নিয়ে তাদের মধ্যে Bitwise AND অপারেশন চালাবে। যেহেতু প্রতিটি কলামে ডেটা একই অর্ডারে থাকে, তাই k-তম বিট সবসময় একই রো-কে নির্দেশ করে।

Column-oriented storage vs. Column families

এখানে একটি গুরুত্বপূর্ণ ভুল ধারণা ভেঙে দেওয়া হয়েছে। Cassandra এবং HBase-এ Column Families নামক কনসেপ্ট আছে যা Bigtable থেকে এসেছে।

  • সতর্কবার্তা: এদেরকে "Column-oriented" বলাটা Confusing। কারণ Column family-র ভেতরে প্রতিটি Row-এর সব কলাম একসাথেই স্টোর করা হয় এবং এখানে কলাম কম্প্রেশন ওইভাবে কাজ করে না। তাই Bigtable মডেলটি মূলত Row-oriented হিসেবেই গণ্য করা উচিত।

Memory Bandwidth and Vectorized Processing

অ্যানালিটিক্যাল ডাটাবেসে শুধু ডিস্ক থেকে মেমোরিতে ডেটা আনাই চ্যালেঞ্জ নয়, বরং মেমোরি থেকে CPU ক্যাশে ডেটা নেওয়া এবং প্রসেস করাটাও একটি বড় চ্যালেঞ্জ।

  • CPU Efficiency: কলাম-ওরিয়েন্টেড স্টোরেজ CPU সাইকেল সাশ্রয় করে। কুয়েরি ইঞ্জিন কলামের একটি বড় অংশ (chunk) সরাসরি CPU-এর L1 Cache-এ নিয়ে আসতে পারে।

  • Vectorized Processing: কমপ্রেসড ডেটার ওপর সরাসরি লুপ চালিয়ে প্রসেসিং করা হয় যেখানে কোনো ফাংশন কল করার প্রয়োজন পড়ে না। আধুনিক CPU-তে থাকা SIMD (Single-Instruction-Multi-Data) ইন্সট্রাকশন ব্যবহার করে একসাথে অনেকগুলো ডেটা প্রসেস করা যায়। একেই বলা হয় Vectorized Processing। বিটম্যাপের AND/OR অপারেশনগুলো এই পদ্ধতিতে সরাসরি কম্প্রেসড ডেটার ওপর চালানো সম্ভব।

Sort Order in Column Storage

সাধারণত একটি কলাম স্টোরে (Column Store) ডেটা কোন অর্ডারে বা ক্রমানুসারে জমা থাকবে, তা খুব একটা গুরুত্বপূর্ণ হয় না। ডেটা যেভাবে আসে (Insertion order) সেভাবেই 'কলাম ফাইলগুলোর' শেষে অ্যাপেন্ড (Append) করে দেওয়া সবচেয়ে সহজ উপায়। কিন্তু আমরা চাইলে এই ডেটাগুলোকে একটি নির্দিষ্ট ক্রমে বা Sort Order-এ সাজিয়ে রাখতে পারি (যেমনটা আমরা SSTables-এ দেখেছিলাম), যা ইনডেক্সিংয়ের মতো কাজ করে এবং কুয়েরি পারফরম্যান্স বাড়িয়ে দেয়।

পুরো রো (Row) ধরে সর্টিং

বইটিতে একটি অত্যন্ত গুরুত্বপূর্ণ সতর্কবার্তা দেওয়া হয়েছে: আপনি কখনই প্রতিটি কলামকে স্বাধীনভাবে বা আলাদা আলাদা ভাবে সর্ট করতে পারবেন না।

  • কেন? কারণ কলাম স্টোরেজে একটি রো (Row) Reconstruct বা পুনরায় তৈরি করার একমাত্র উপায় হলো কলামগুলোর ইনডেক্স জানা। যদি একটি কলামের ১০ নম্বর আইটেমটি একটি নির্দিষ্ট ইউজারের হয়, তবে অন্য সব কলামের ১০ নম্বর আইটেমগুলোও ওই একই ইউজারের হতে হবে।

  • সমাধান: সর্টিং করতে হবে পুরো রো ধরে। অর্থাৎ, একটি নির্দিষ্ট কলামের মানের ওপর ভিত্তি করে পুরো টেবিলটিকে সাজাতে হবে, যদিও ডেটাগুলো কলাম অনুযায়ী আলাদা ফাইলে জমা থাকছে।

সর্ট কি (Sort Key) নির্বাচন

ডেটাবেজ অ্যাডমিনিস্ট্রেটর তার অভিজ্ঞতা এবং কুয়েরির প্যাটার্ন দেখে ঠিক করেন কোন কলামের ওপর ভিত্তি করে সর্টিং হবে।

  • উদাহরণ: যদি দেখা যায় কুয়েরিগুলো প্রায়ই একটি নির্দিষ্ট সময়ের রেঞ্জ (Date Range) নিয়ে কাজ করে, তবে date_key-কে প্রথম Sort Key হিসেবে রাখা বুদ্ধিমানের কাজ। এতে কুয়েরি অপ্টিমাইজার পুরো টেবিল স্ক্যান না করে শুধু নির্দিষ্ট তারিখের অংশটুকু স্ক্যান করবে, যা অনেক দ্রুত।

  • সেকেন্ডারি সর্ট কি: একটি সর্ট কি-র মান যদি অনেকগুলো রো-তে একই হয় (যেমন একই তারিখে অনেকগুলো সেলস), তবে দ্বিতীয় আরেকটি কলাম (যেমন product_sk) দিয়ে সেগুলোকে সাজানো যায়। এতে একই তারিখের একই প্রোডাক্টের সেলসগুলো একসাথে জমা থাকবে, যা ফিল্টারিং বা গ্রুপিংয়ের জন্য সহায়ক।

দ্রষ্টব্য: এখানে Figure 3-10-এর কথা উল্লেখ করা হয়েছে যেখানে date_key এবং product_sk ব্যবহার করে সর্টিং দেখানো হয়েছে।

কম্প্রেশন বা ডেটা সংকোচন (Compression Efficiency)

সর্ট করা কলাম স্টোরেজের একটি বড় সুবিধা হলো এটি কলাম কম্প্রেশনে দারুণ সাহায্য করে।

  • Run-length encoding: যদি প্রথম সর্ট Key-তে অনেকগুলো একই মান বারবার থাকে, তবে সেগুলোকে খুব সহজে কম্প্রেস করা যায়। ধরুন, বিলিয়ন বিলিয়ন রো আছে কিন্তু তারিখ মাত্র কয়েকশ, তাহলে সর্ট করার পর একই তারিখের লাখ লাখ রো পরপর থাকবে। এই দীর্ঘ সিকুয়েন্সকে রান-লেংথ এনকোডিংয়ের মাধ্যমে মাত্র কয়েক কিলোবাইটে নামিয়ে আনা সম্ভব।

  • সীমাবদ্ধতা: প্রথম সর্ট Key-তে কম্প্রেশন সবচেয়ে ভালো হয়। দ্বিতীয় বা তৃতীয় সর্ট Key-র ক্ষেত্রে ডেটা কিছুটা এলোমেলো (Jumbled) থাকে, তাই সেখানে রিপিটেড ভ্যালুর সংখ্যা কমে যায় এবং কম্প্রেশন ততটা কার্যকর হয় না।

দ্রষ্টব্য: এখানে Figure 3-11-এর কথা বলা হয়েছে যেখানে বিটম্যাপে রান-লেংথ এনকোডিংয়ের ব্যবহার দেখানো হয়েছে।

মাল্টিপল সর্ট অর্ডার (Several Different Sort Orders)

C-Store এবং Vertica-এর মতো আধুনিক ডেটা ওয়্যারহাউস একটি চমৎকার বুদ্ধি ব্যবহার করে। যেহেতু ডেটা লস এড়ানোর জন্য আমাদের ডেটা একাধিক মেশিনে রেপ্লিকেট (Replicate) বা কপি করে রাখতে হয়, তাই সব কপিতে একই অর্ডারে ডেটা না রেখে ভিন্ন ভিন্ন কপিতে ভিন্ন ভিন্ন সর্ট অর্ডারে ডেটা রাখা যায়।

  • সুবিধা: কুয়েরি করার সময় অপ্টিমাইজার দেখে নেবে কোন সর্ট অর্ডারের কপিটি ওই কুয়েরির জন্য সবচেয়ে বেশি কার্যকর এবং সেটিই ব্যবহার করবে।

  • ইনডেক্সের সাথে পার্থক্য: রো-ওরিয়েন্টেড স্টোরে সেকেন্ডারি ইনডেক্স সাধারণত ডেটার লোকেশন বা পয়েন্টার (Pointer) ধারণ করে। কিন্তু কলাম স্টোরেজে সাধারণত কোনো পয়েন্টার থাকে না, সেখানে সরাসরি কলামের ভ্যালুগুলোই ভিন্ন ভিন্ন অর্ডারে সাজানো থাকে।

Writing to Column-Oriented Storage

আগের অংশে আমরা দেখেছি কীভাবে কলাম স্টোরেজ, কম্প্রেশন এবং সর্টিং রিড-কুয়েরি (Read query) দ্রুত করে। কিন্তু এই সুবিধাগুলোর একটি বড় নেতিবাচক দিক (Downside) আছে: এগুলো ডেটা Write বা আপডেট করাকে অনেক কঠিন করে দেয়।

কেন ডেটা আপডেট করা কঠিন?

  • Update-in-place অসম্ভব: B-trees যেভাবে সরাসরি ডিস্কের নির্দিষ্ট জায়গায় গিয়ে ডেটা আপডেট করতে পারে, কম্প্রেসড কলাম স্টোরেজে তা সম্ভব নয়।

  • পজিশনাল অ্যালাইনমেন্ট (Positional Alignment): কলাম স্টোরেজে একটি রো-এর বিভিন্ন কলামকে চেনা হয় তাদের পজিশন দিয়ে (যেমন: সব কলামের ১০ নম্বর আইটেম মিলে একটি Row)। এখন আপনি যদি সর্ট করা টেবিলের মাঝখানে একটি নতুন Row ইনসার্ট করতে চান, তবে আপনাকে সবকটি কলাম ফাইল নতুন করে লিখতে হবে যাতে সবার পজিশন ঠিক থাকে। এটি বিলিয়ন বিলিয়ন Row-এর ক্ষেত্রে অত্যন্ত সময়সাপেক্ষ।

সমাধান: LSM-trees এবং ইন-মেমোরি স্টোর

বইটিতে এই সমস্যার একটি চমৎকার সমাধান দেওয়া হয়েছে যা আমরা এই চ্যাপ্টারের শুরুতে LSM-trees পড়ার সময় জেনেছিলাম।

  • ধাপ ১ (Writes in Memory): সব নতুন রাইট (Insert/Update) সরাসরি ডিস্কে না পাঠিয়ে প্রথমে একটি ইন-মেমোরি স্টোরে (In-memory store) পাঠানো হয়। সেখানে ডেটাগুলো একটি সর্টেড স্ট্রাকচারে (যেমন: Memtable) জমা হয়।

  • ধাপ ২ (Row vs Column in Memory): ইন-মেমোরি স্টোরটি Row-ওরিয়েন্টেড না Column-ওরিয়েন্টেড, তা খুব একটা গুরুত্বপূর্ণ নয়।

  • ধাপ ৩ (Merging): যখন মেমোরিতে যথেষ্ট পরিমাণ ডেটা জমা হয়ে যায়, তখন সেগুলোকে ডিস্কের কলাম ফাইলগুলোর সাথে মার্জ (Merge) করা হয় এবং নতুন ফাইলে বাল্ক (Bulk) আকারে লেখা হয়। ডেটা ওয়্যারহাউস Vertica মূলত এই পদ্ধতিই অনুসরণ করে।

কুয়েরি করার পদ্ধতি

যখন কোনো এনালিস্ট কুয়েরি করেন, তখন ডেটাবেজ ইঞ্জিন দুটি জায়গা থেকে ডেটা সংগ্রহ করে:

  1. ডিস্কে থাকা পুরনো কলাম ফাইলগুলো।

  2. মেমোরিতে থাকা সাম্প্রতিক (Recent) রাইটগুলো।

কুয়েরি অপ্টিমাইজার এই দুই জায়গার ডেটা নিজে থেকেই কম্বাইন বা মার্জ করে ইউজারকে দেখায়। ফলে একজন ইউজারের কাছে মনে হয় ডেটা তাৎক্ষণিকভাবে আপডেট হয়ে গেছে, যদিও ব্যাকএন্ডে এটি ব্যাচ প্রসেসে মার্জ হচ্ছে।

Aggregation: Data Cubes and Materialized Views

কলাম স্টোরেজ এই কনসেপ্ট এর যে শুধু মাত্র ডেটা ওয়্যারহাউসে সবসময় ব্যাবহার করা হয় এমন নয়; প্রথাগত Row-ওরিয়েন্টেড ডেটাবেজও ব্যবহৃত হয়। তবে অ্যাড-হক (Ad hoc) অ্যানালিটিক্যাল কুয়েরির জন্য কলাম স্টোরেজ দ্রুততর হওয়ায় এটি বেশি জনপ্রিয়। ডেটা ওয়্যারহাউসের আরেকটি গুরুত্বপূর্ণ দিক হলো Materialized Aggregates

মেটেরিয়ালাইজড ভিউ (Materialized Views)

ডেটা ওয়্যারহাউস কুয়েরিতে প্রায়ই COUNT, SUM, AVG, MIN, বা MAX-এর মতো অ্যাগ্রিগেট ফাংশন ব্যবহার করা হয়। যদি একই ধরনের হিসাব বারবার প্রয়োজন হয়, তবে প্রতিবার র-ডেটা (Raw data) প্রসেস করা সময়ের অপচয়। এর সমাধান হলো এই হিসাবগুলোকে ক্যাশ (Cache) করে রাখা।

  • ভার্চুয়াল ভিউ বনাম মেটেরিয়ালাইজড ভিউ:

    • ভার্চুয়াল ভিউ: এটি কেবল একটি কুয়েরি লেখার শর্টকাট। যখন আপনি এটি রিড করেন, ইঞ্জিন ব্যাকএন্ডে আসল কুয়েরিটি চালায়।

    • মেটেরিয়ালাইজড ভিউ: এটি কুয়েরির রেজাল্টের একটি বাস্তব কপি যা ডিস্কে লেখা থাকে।

  • রাইট কস্ট (Write Cost): যখন মূল টেবিলের ডেটা পরিবর্তন হয়, তখন মেটেরিয়ালাইজড ভিউটিও আপডেট করতে হয় কারণ এটি ডেটার একটি ডিনর্মালাইজড কপি। এই অতিরিক্ত রাইট কস্টের কারণে এটি OLTP (যেখানে প্রচুর রাইট হয়) ডেটাবেজে কম ব্যবহৃত হয়, কিন্তু রিড-হেভি ডেটা ওয়্যারহাউসে এটি পারফরম্যান্স বাড়াতে কার্যকর হতে পারে।

ডেটা কিউব বা OLAP কিউব (Data Cube / OLAP Cube)

এটি মেটেরিয়ালাইজড ভিউয়ের একটি বিশেষ ধরণ। এটি মূলত বিভিন্ন ডাইমেনশন বা মাত্রা অনুযায়ী সাজানো একটি অ্যাগ্রিগেট গ্রিড।

এখানে Figure 3-12 লক্ষ্য করুন, যেখানে ডেটা কিউবের দুটি ডাইমেনশন (Date এবং Product) ব্যবহার করে ডেটা সামারি (Sum) দেখানো হয়েছে।

  • কিভাবে কাজ করে?: ধরুন, একটি টেবিলে কেবল দুটি ডাইমেনশন আছে—তারিখ (Date) এবং পণ্য (Product)। আপনি একটি টু-ডাইমেনশনাল টেবিল কল্পনা করতে পারেন যার এক অক্ষে তারিখ এবং অন্য অক্ষে পণ্য থাকবে। প্রতিটি সেলে (Cell) ওই নির্দিষ্ট তারিখে ওই নির্দিষ্ট পণ্যের মোট বিক্রির অর্থ (যেমন SUM(net_price)) জমা থাকবে।

    • এখন যদি আপনি কোনো একটি Row বা Column ধরে সব সেল যোগ করেন, তবে আপনি একটি ডাইমেনশন কমিয়ে সামারি পাবেন (যেমন: তারিখ যাই হোক না কেন, নির্দিষ্ট পণ্যের মোট কত বিক্রি হলো)।
  • হাইপারকিউব (Hypercube): বাস্তবে ডাইমেনশন অনেক বেশি হতে পারে (যেমন: তারিখ, পণ্য, স্টোর, প্রমোশন এবং কাস্টমার—Figure 3-9 অনুযায়ী ৫টি ডাইমেনশন)। পাঁচ-মাত্রার একটি কিউব কল্পনা করা কঠিন হলেও মূলনীতি একই থাকে। প্রতিটি সেল একটি নির্দিষ্ট কম্বিনেশনের (যেমন: নির্দিষ্ট তারিখে, নির্দিষ্ট স্টোরে, নির্দিষ্ট প্রমোশনে এই পণ্যের বিক্রি) মান ধারণ করে।

সুবিধা ও অসুবিধা (Advantages and Disadvantages)

  • সুবিধা: কিছু নির্দিষ্ট কুয়েরি অত্যন্ত দ্রুত হয় কারণ ফলাফলগুলো আগে থেকেই ক্যালকুলেট বা Precomputed করা থাকে। উদাহরণস্বরূপ, যদি জানতে চাওয়া হয় "গতকাল প্রতিটি স্টোরে মোট কত বিক্রি হয়েছে?", তবে বিলিয়ন রো স্ক্যান না করে সরাসরি কিউবের সংশ্লিষ্ট ডাইমেনশন থেকে মানটি নিয়ে আসা যায়।

  • অসুবিধা: ডেটা কিউবে র-ডেটা (Raw data) কুয়েরি করার মতো ফ্লেক্সিবিলিটি থাকে না।

    • উদাহরণ: আপনি যদি জানতে চান "১০০ ডলারের বেশি দামি পণ্যগুলো থেকে কত শতাংশ সেল এসেছে?", তবে কিউব থেকে তা জানা সম্ভব নয় যদি 'দাম' (Price) আপনার কিউবের কোনো ডাইমেনশন না হয়ে থাকে।

সিদ্ধান্ত: বেশিরভাগ ডেটা ওয়্যারহাউস তাই যতটা সম্ভব র-ডেটা রাখার চেষ্টা করে এবং ডেটা কিউবকে কেবল নির্দিষ্ট কিছু কুয়েরির পারফরম্যান্স বুস্ট করার জন্য ব্যবহার করে।

Summary

এই চ্যাপ্টারে আমরা ডেটাবেজের ভেতরে আসলে কী ঘটে—অর্থাৎ ডেটা কীভাবে জমা হয় এবং কুয়েরি করার সময় তা কীভাবে খুঁজে বের করা হয়, তার একদম গভীরে যাওয়ার চেষ্টা করেছি। হাই-লেভেলে স্টোরেজ ইঞ্জিনগুলোকে দুটি প্রধান ভাগে ভাগ করা যায়: OLTP এবং OLAP

১. OLTP বনাম OLAP (ব্যবহারের ধরন ও পার্থক্য)

বৈশিষ্ট্যOLTP (Transaction Processing)OLAP (Analytics)
ব্যবহারকারীসরাসরি এন্ড-ইউজার (End users)বিজনেস অ্যানালিস্ট (Business Analysts)
কুয়েরির ভলিউমঅনেক বেশি (High volume)তুলনামূলক কম (Lower volume)
ডেটার পরিমাণকুয়েরি প্রতি অল্প কিছু রেকর্ডকুয়েরি প্রতি লাখ লাখ বা কোটি কোটি রেকর্ড
প্রধান বাধা (Bottleneck)Disk Seek Time (ডেটা খুঁজে পেতে ডিস্কের মুভমেন্ট)Disk Bandwidth (কত দ্রুত প্রচুর ডেটা রিড করা যায়)
সমাধানইনডেক্সিং (Indexing)কলাম-ওরিয়েন্টেড স্টোরেজ

২. OLTP স্টোরেজ ইঞ্জিনের দুটি ঘরানা (Schools of Thought)

OLTP সিস্টেমে স্টোরেজ ইঞ্জিন সাধারণত দুটি দর্শনের ওপর ভিত্তি করে কাজ করে:

  • Log-structured School (লগ-স্ট্রাকচার্ড):

    • এই পদ্ধতিতে ডেটা সবসময় ফাইলের শেষে যোগ (Append) করা হয়। পুরনো ডেটা সরাসরি আপডেট না করে অপ্রাসঙ্গিক হিসেবে ফেলে দেওয়া হয় এবং নতুন ডেটা অ্যাপেন্ড করা হয়।

    • এটি Random-access writes-কে Sequential writes-এ রূপান্তর করে, যা হার্ড ড্রাইভ এবং SSD-তে রাইট পারফরম্যান্স অনেক বাড়িয়ে দেয়।

    • উদাহরণ: Bitcask, SSTables, LSM-trees, LevelDB, Cassandra, HBase, এবং Lucene।

  • Update-in-place School (আপডেট-ইন-প্লেস):

    • এখানে ডিস্ককে নির্দিষ্ট সাইজের কিছু পেজ (Fixed-size pages) হিসেবে ধরা হয়। কোনো ডেটা আপডেট করতে হলে সরাসরি ওই নির্দিষ্ট পেজে গিয়ে আগের ডেটার ওপর নতুন ডেটা লিখে (Overwrite) দেওয়া হয়।

    • B-trees হলো এই দর্শনের সবচেয়ে বড় উদাহরণ। প্রায় সব বড় রিলেশনাল ডেটাবেজ এটি ব্যবহার করে।

৩. এনালিটিক্যাল আর্কিটেকচার ও কলাম স্টোরেজ

OLTP থেকে সরে যখন আমরা ডেটা ওয়্যারহাউস বা এনালিটিক্যাল কাজের দিকে তাকাই, তখন দেখা যায় সেখানে ইনডেক্স খুব একটা কার্যকর নয়। কারণ এনালিটিক্যাল কুয়েরিতে সাধারণত পুরো টেবিল বা বিশাল সংখ্যক রো (Rows) স্ক্যান করার প্রয়োজন হয়।

  • এখানে মূল লক্ষ্য থাকে ডেটাকে কত টাইট বা কম্প্যাক্টভাবে (Compactly) জমা রাখা যায়, যাতে ডিস্ক থেকে কম ডেটা রিড করতে হয়।

  • কলাম-ওরিয়েন্টেড স্টোরেজ এই কাজে সবচেয়ে বেশি সাহায্য করে।

৪. একজন ডেভেলপার হিসেবে এই জ্ঞানের গুরুত্ব

আপনি যদি স্টোরেজ ইঞ্জিনের এই ইন্টারনাল বিষয়গুলো জানেন, তবে আপনি নিচের সুবিধাগুলো পাবেন:

  1. সঠিক টুল নির্বাচন: আপনার অ্যাপ্লিকেশনের জন্য কোন ডেটাবেজটি সবচেয়ে ভালো কাজ করবে, তা আপনি সহজেই বুঝতে পারবেন।

  2. প্যারামিটার টিউনিং: ডেটাবেজের কনফিগারেশনে কোনো ভ্যালু কমালে বা বাড়ালে তার প্রভাব সিস্টেমে কেমন পড়বে, তা আপনি কল্পনা করতে পারবেন।

  3. ডকুমেন্টেশন বোঝা: বিভিন্ন ডেটাবেজের টেকনিক্যাল ডকুমেন্টেশন পড়তে গেলে এখন আপনি সেখানকার টেকনিক্যাল টার্মগুলো (Vocabulary) এবং আইডিয়াগুলো সহজেই ধরতে পারবেন।

Designing Data-Intensive Applications: Bangla Summary

Part 1 of 3

এই ব্লগ সিরিজটি মার্টিন ক্লেপম্যানের কালজয়ী বই "Designing Data-Intensive Applications" (DDIA)-এর একটি বিস্তারিত বাংলা সারসংক্ষেপ।

Up next

DDIA: Chapter-2

Data Models and Query Languages