অধ্যায় 5 · পাইপলাইন ও AI
ওয়্যারহাউস, লেক, স্ট্রিম আর স্কেল
- পৃষ্ঠা 20 / 22
- 16 মিনিট পড়া
শপের ডেটাবেস বানানো হয়েছে শপ চালানোর জন্য: একটা অর্ডার ঢোকানো, একটা স্টকের সংখ্যা বদলানো, একজন গ্রাহককে খোঁজা — মিনিটে হাজার হাজার ছোট ট্রানজ্যাকশন। অ্যানালিস্ট বা ML ইঞ্জিনিয়ারের প্রশ্ন আলাদা: "তিন বছরের মাস আর ক্যাটাগরিভিত্তিক আয়", "গত কোয়ার্টারে প্রত্যেক ব্যবহারকারীর প্রতিটা ক্লিক, ট্রেনিং ডেটা হিসেবে"। এসব প্রশ্ন লাখ লাখ সারি স্ক্যান করে, আর শপকে ধীর করে দেবে। তাই কোম্পানিগুলো ডেটা কপি করে নেয় বড় মাপে পড়ার জন্য বানানো সিস্টেমে: ওয়্যারহাউস, লেক আর লেকহাউস, যেগুলোতে ডেটা আসে ব্যাচে বা স্ট্রিম হয়ে।
এই পাতায় শপের ডেটা থেকে একটা ছোট স্টার স্কিমা বানিয়ে কোয়েরি করবেন, আসল PostgreSQL সার্ভারে একটা টেবিল পার্টিশন করবেন, তারপর বাকি জগৎটার একটা মানচিত্র দেখবেন — ডেটা আর ML প্ল্যাটফর্মের যেকোনো চাকরিতে যে শব্দগুলো শুনবেন।
যা শিখবেন
- OLTP বনাম OLAP, আর অ্যানালিটিক্যাল ডেটা কেন আলাদা সিস্টেমে থাকে
- ফ্যাক্ট আর ডাইমেনশন টেবিল, স্টার বনাম স্নোফ্লেক স্কিমা, ডেটা মার্ট — শপের ডেটায় বানিয়ে কোয়েরি করে
- ধীরে বদলানো ডাইমেনশন (slowly changing dimension): টাইপ ১ (ওভাররাইট) বনাম টাইপ ২ (ইতিহাস রাখা), পয়েন্ট-ইন-টাইম জয়েনসহ
- ব্যাচ বনাম স্ট্রিমিং, Kafka, Spark, লেক বনাম ওয়্যারহাউস বনাম লেকহাউস, আর ক্লাউড প্ল্যাটফর্ম
- স্কেলিং: পার্টিশনিং (PostgreSQL-এ চালিয়ে), শার্ডিং, রেপ্লিকেশন, ভার্টিক্যাল বনাম হরাইজন্টাল স্কেলিং, গভর্ন্যান্স, স্কিমা বিবর্তন আর খরচ
OLTP বনাম OLAP
| OLTP (অপারেশনাল) | OLAP (অ্যানালিটিক্যাল) | |
|---|---|---|
| উদ্দেশ্য | ব্যবসা চালানো: অর্ডার, পেমেন্ট, লগইন | ব্যবসা বোঝা: রিপোর্ট, ড্যাশবোর্ড, ML ট্রেনিং ডেটা |
| সাধারণ কোয়েরি | কি দিয়ে একটা সারি: WHERE order_id = 10 | লাখ লাখ সারির অ্যাগ্রিগেট: GROUP BY month, category |
| লেখা | অনেক ছোট INSERT আর UPDATE, ট্রানজ্যাকশন | বাল্ক লোড (ঘণ্টায়, দিনে) বা স্ট্রিমে যোগ হওয়া |
| স্কিমা | নরমালাইজড (3NF): পুনরাবৃত্তি যতটা সম্ভব কম | ডিনরমালাইজড (স্টার): কম জয়েন, কোয়েরি সহজ |
| স্টোরেজ | সারি-ভিত্তিক (row-oriented) | কলাম-ভিত্তিক (column-oriented), কমপ্রেস করা |
| উদাহরণ | PostgreSQL, MySQL, SQL Server | BigQuery, Snowflake, Redshift, Databricks SQL, ClickHouse, DuckDB |
শপের টেবিলগুলো OLTP: এগুলো ডেটাবেস ডিজাইন: কি, ER ডায়াগ্রাম আর নরমালাইজেশন-এর নরমালাইজেশনের নিয়ম মেনে চলে। ডেটা ওয়্যারহাউস হলো OLAP দিক: অনেক সিস্টেম (শপ, পেমেন্ট, সাপোর্ট, মার্কেটিং) থেকে আসা ডেটার একটা কেন্দ্রীয়, পরিষ্কার, ঐতিহাসিক কপি, যা লোড করে ETL ও ELT: নির্ভরযোগ্য ডেটা পাইপলাইন বানানো-র পাইপলাইনগুলো।
ফ্যাক্ট আর ডাইমেনশন টেবিল
ওয়্যারহাউস সাজানো হয় ব্যবসার ঘটনা (event) ঘিরে। একটা ফ্যাক্ট টেবিল ঘোষিত একটা গ্রেইন-এ ("প্রতি অর্ডার লাইনে একটা সারি") প্রতিটা ঘটনার একটা সারি রাখে, যোগ করার মতো সংখ্যাসহ (মেজার: পরিমাণ, আয়), আর থাকে ডাইমেনশন টেবিলের কি — যেগুলো বলে কে, কী, কখন আর কোথায়। আঁকলে একটা তারা (star) হয়:
dim_date
(date_key, year, quarter,
year_month, is_weekend)
│
dim_customer ───── fact_sales ───── dim_product
(customer_key, (date_key, customer_key, (product_key, product_id,
customer_id, product_key, order_id, product_name, category,
city, valid_from, order_status, quantity, list_price)
valid_to) revenue, discount)স্নোফ্লেক স্কিমায় ডাইমেনশনগুলো আরও নরমালাইজ করা হয় — dim_product তখন আলাদা একটা dim_category-কে দেখাবে। এতে সামান্য জায়গা বাঁচে, কিন্তু প্রতিটা কোয়েরিতে একটা জয়েন বাড়ে; বেশিরভাগ টিম স্টারই পছন্দ করে। ডেটা মার্ট হলো একটা টিমের জন্য বানানো বিষয়ভিত্তিক ছোট অংশ (সেলস মার্ট, মার্কেটিং মার্ট)।
ডাইমেনশনগুলো বানানো
তারিখের ডাইমেনশন কপি করা হয় না, জেনারেট করা হয়: প্রতিদিনের জন্য একটা সারি, যেসব বৈশিষ্ট্য দিয়ে মানুষ গ্রুপ করে সেগুলোসহ। সাপ্তাহিক ছুটিটা খেয়াল করুন: বাংলাদেশে সেটা শুক্র আর শনিবার (%w = 5 আর 6)। ডাইমেনশন টেবিল পায় নিজস্ব সারোগেট কি; উৎসের ন্যাচারাল কি পাশে রাখা থাকে:
CREATE TABLE dim_date (
date_key INTEGER PRIMARY KEY, -- 20260412
full_date TEXT NOT NULL,
year INTEGER NOT NULL,
quarter INTEGER NOT NULL,
year_month TEXT NOT NULL,
is_weekend INTEGER NOT NULL
);
INSERT INTO dim_date
WITH RECURSIVE d(day) AS (
SELECT '2026-01-01'
UNION ALL
SELECT date(day, '+1 day') FROM d WHERE day < '2026-06-30'
)
SELECT CAST(strftime('%Y%m%d', day) AS INTEGER),
day,
CAST(strftime('%Y', day) AS INTEGER),
(CAST(strftime('%m', day) AS INTEGER) + 2) / 3,
strftime('%Y-%m', day),
CASE WHEN strftime('%w', day) IN ('5', '6') THEN 1 ELSE 0 END
FROM d;
CREATE TABLE dim_product (
product_key INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL, -- শপের ন্যাচারাল কি
product_name TEXT NOT NULL,
category TEXT NOT NULL,
list_price INTEGER NOT NULL
);
INSERT INTO dim_product (product_id, product_name, category, list_price)
SELECT p.product_id, p.name, COALESCE(c.name, 'Uncategorised'), p.price
FROM products AS p
LEFT JOIN categories AS c ON c.category_id = p.category_id
ORDER BY p.product_id;
CREATE TABLE dim_customer (
customer_key INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
name TEXT NOT NULL,
city TEXT NOT NULL,
valid_from TEXT NOT NULL,
valid_to TEXT NOT NULL DEFAULT '9999-12-31',
is_current INTEGER NOT NULL DEFAULT 1
);
INSERT INTO dim_customer (customer_id, name, city, valid_from)
SELECT customer_id, name, COALESCE(city, 'Unknown'), joined_on
FROM customers
ORDER BY customer_id;
SELECT (SELECT COUNT(*) FROM dim_date) AS days,
(SELECT SUM(is_weekend) FROM dim_date) AS weekend_days,
(SELECT COUNT(*) FROM dim_product) AS products,
(SELECT COUNT(*) FROM dim_customer) AS customers;+------+--------------+----------+-----------+
| days | weekend_days | products | customers |
+------+--------------+----------+-----------+
| 181 | 52 | 11 | 8 |
+------+--------------+----------+-----------+ওয়্যারহাউসে ডাইমেনশনের NULL-এর জায়গায় স্পষ্ট লেবেল ('Unknown', 'Uncategorised') বসানো হয়, যাতে রিপোর্টে সেগুলো বাদ না পড়ে আলাদা দলে দেখা যায়।
ধীরে বদলানো ডাইমেনশন (SCD)
২০২৬-এর ১ এপ্রিল নাদিয়া (গ্রাহক ১) ঢাকা থেকে চট্টগ্রামে চলে যান। তাঁর জানুয়ারির অর্ডার কি ঢাকার আয় হিসেবে গুনবেন, নাকি চট্টগ্রামের?
| টাইপ | কী করে | ফলাফল | কখন ব্যবহার করবেন |
|---|---|---|---|
| টাইপ ১ | বৈশিষ্ট্যটা ওভাররাইট করে | ইতিহাস নতুন করে লেখা হয়: তাঁর সব বিক্রি চট্টগ্রামে চলে যায় | সংশোধন (ভুল বানানের নাম), যে বৈশিষ্ট্য সময় ধরে কেউ বিশ্লেষণ করে না |
| টাইপ ২ | পুরোনো সারি বন্ধ করে, নতুন সারোগেট কি দিয়ে নতুন সারি যোগ করে | প্রতিটা বিক্রি সেই সময়ের সত্যি শহরটাই রাখে | যা কিছু ইতিহাস ধরে বিশ্লেষণ করেন বা ML ফিচার হিসেবে ব্যবহার করেন |
টাইপ ২ মানে দুটো স্টেটমেন্ট:
UPDATE dim_customer
SET valid_to = '2026-04-01', is_current = 0
WHERE customer_id = 1 AND is_current = 1;
INSERT INTO dim_customer (customer_id, name, city, valid_from)
VALUES (1, 'Nadia Rahman', 'Chattogram', '2026-04-01');
SELECT customer_key, customer_id, city, valid_from, valid_to, is_current
FROM dim_customer
WHERE customer_id = 1
ORDER BY customer_key;+--------------+-------------+------------+------------+------------+------------+
| customer_key | customer_id | city | valid_from | valid_to | is_current |
+--------------+-------------+------------+------------+------------+------------+
| 1 | 1 | Dhaka | 2025-11-03 | 2026-04-01 | 0 |
| 9 | 1 | Chattogram | 2026-04-01 | 9999-12-31 | 1 |
+--------------+-------------+------------+------------+------------+------------+বৈধতার পরিসরটা অর্ধ-খোলা: valid_from ভেতরে, valid_to বাইরে। এটা হুবহু SQL ও pandas দিয়ে ML-এর উপযোগী ডেটা তৈরি-র পয়েন্ট-ইন-টাইম ধারণা: টাইপ ২ ডাইমেনশন দিয়ে আবার বানানো যায় "সেই তারিখে এই গ্রাহক সম্পর্কে আমরা কী জানতাম?" — লিকমুক্ত ফিচারের ঠিক এটাই দরকার।
ফ্যাক্ট টেবিল লোড করা
প্রতিটা অর্ডার লাইন হয় একটা ফ্যাক্ট সারি। গ্রাহকের কি খুঁজে পাওয়া যায় একটা পয়েন্ট-ইন-টাইম জয়েন দিয়ে: ডাইমেনশনের সেই সারি, যার বৈধতার পরিসরে অর্ডারের তারিখটা পড়ে। ডিসকাউন্ট এখানে একবারই হিসাব হয়, যাতে কোনো রিপোর্টকে লজিকটা আবার লিখতে না হয়:
CREATE TABLE fact_sales (
date_key INTEGER NOT NULL,
customer_key INTEGER NOT NULL,
product_key INTEGER NOT NULL,
order_id INTEGER NOT NULL, -- "ডিজেনারেট" ডাইমেনশন: এর নিজের কোনো টেবিল নেই
order_status TEXT NOT NULL,
quantity INTEGER NOT NULL,
revenue INTEGER NOT NULL,
discount INTEGER NOT NULL,
FOREIGN KEY (date_key) REFERENCES dim_date (date_key),
FOREIGN KEY (customer_key) REFERENCES dim_customer (customer_key),
FOREIGN KEY (product_key) REFERENCES dim_product (product_key)
);
INSERT INTO fact_sales
SELECT CAST(strftime('%Y%m%d', o.order_date) AS INTEGER),
dc.customer_key,
dp.product_key,
o.order_id,
o.status,
oi.quantity,
oi.quantity * oi.unit_price,
oi.quantity * (dp.list_price - oi.unit_price)
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN dim_customer AS dc
ON dc.customer_id = o.customer_id
AND o.order_date >= dc.valid_from AND o.order_date < dc.valid_to
JOIN dim_product AS dp ON dp.product_id = oi.product_id;
SELECT COUNT(*) AS fact_rows, SUM(revenue) AS revenue, SUM(discount) AS discount
FROM fact_sales;+-----------+---------+----------+
| fact_rows | revenue | discount |
+-----------+---------+----------+
| 20 | 103550 | 450 |
+-----------+---------+----------+২০টা অর্ডার লাইনের জন্য ২০টা ফ্যাক্ট সারি: গ্রেইন ঠিক আছে। ৪৫০ টাকার ডিসকাউন্টটা অর্ডার ৯-এর কিবোর্ড, ৪৫০০-র বদলে ৪০৫০-এ বিক্রি।
স্টারে কোয়েরি
প্রতিটা স্টার কোয়েরির একই আকার: ফ্যাক্ট থেকে শুরু, দরকারি ডাইমেনশন জয়েন, তাদের বৈশিষ্ট্য দিয়ে ফিল্টার আর গ্রুপ, মেজারগুলো যোগ।
SELECT d.quarter, p.category, SUM(f.quantity) AS units, SUM(f.revenue) AS revenue
FROM fact_sales AS f
JOIN dim_date AS d ON d.date_key = f.date_key
JOIN dim_product AS p ON p.product_key = f.product_key
WHERE f.order_status <> 'cancelled'
GROUP BY d.quarter, p.category
ORDER BY d.quarter, revenue DESC;
SELECT c.city AS city_at_sale, SUM(f.revenue) AS revenue
FROM fact_sales AS f
JOIN dim_customer AS c ON c.customer_key = f.customer_key
WHERE f.order_status <> 'cancelled'
GROUP BY c.city
ORDER BY revenue DESC, city_at_sale;+---------+-------------+-------+---------+
| quarter | category | units | revenue |
+---------+-------------+-------+---------+
| 1 | Courses | 2 | 18000 |
| 1 | Electronics | 4 | 7200 |
| 1 | Books | 4 | 6100 |
| 1 | Accessories | 2 | 3700 |
| 2 | Electronics | 4 | 24350 |
| 2 | Courses | 2 | 18000 |
| 2 | Accessories | 3 | 5200 |
| 2 | Books | 1 | 2500 |
+---------+-------------+-------+---------+
+--------------+---------+
| city_at_sale | revenue |
+--------------+---------+
| Chattogram | 41000 |
| Dhaka | 20100 |
| Khulna | 19950 |
| Unknown | 4000 |
+--------------+---------+নাদিয়ার এপ্রিলের অর্ডার (অর্ডার ১০, ১৮,৫০০ টাকা) গোনা হয়েছে চট্টগ্রামে; জানুয়ারি আর ফেব্রুয়ারির অর্ডার থেকে গেছে ঢাকায়। এর বদলে বর্তমান শহর ধরে রিপোর্ট চাইলে (টাইপ ১-এর আচরণ), ন্যাচারাল কি ধরে is_current = 1 সারিতে জয়েন করুন। টাইপ ২ দুটো প্রশ্নেরই উত্তর দিতে পারে; টাইপ ১ শুধু দ্বিতীয়টার।
ব্যাচ, স্ট্রিম আর ডেটা কোথায় থাকে
অ্যানালিটিক্যাল দিকে ডেটা কীভাবে পৌঁছায়, সেটা আরেকটা বড় ডিজাইনের সিদ্ধান্ত:
অ্যাপ / ডেটাবেস ──CDC──┐
ক্লিক ইভেন্ট ───────────┼──▶ Kafka (ইভেন্ট লগ) ──▶ স্ট্রিম প্রসেসিং ──▶ রিয়েল-টাইম ফিচার, অ্যালার্ট
IoT, লগ ────────────────┘ │ (Flink, Spark Structured Streaming)
▼
ডেটা লেক (ফাইল: S3 / GCS / ADLS-এ Parquet)
│ ব্যাচ জব (Spark, dbt, SQL)
▼
ওয়্যারহাউস / লেকহাউস টেবিল ──▶ BI ড্যাশবোর্ড, ML ট্রেনিং সেট- ব্যাচ একটা নির্দিষ্ট অংশ সূচি ধরে প্রসেস করে ("গত রাতের অর্ডার")। সহজ, সস্তা, কয়েক মিনিট থেকে কয়েক ঘণ্টার দেরি। স্ট্রিমিং প্রতিটা ইভেন্ট আসামাত্র প্রসেস করে; রিয়েল-টাইম মানে মিলিসেকেন্ড থেকে সেকেন্ড, নিয়ার-রিয়েল-টাইম মানে সেকেন্ড থেকে মিনিট (মাইক্রো-ব্যাচ)। স্ট্রিমিং বাছুন শুধু তখন, যখন কেউ সত্যিই টাটকা ডেটা দেখে কাজ করে: জালিয়াতি যাচাই, লাইভ রেকমেন্ডেশন, একটা LLM অ্যাপ নজরে রাখা।
- Apache Kafka হলো ইভেন্টের একটা টেকসই, পার্টিশন করা, শুধু-যোগ-করা (append-only) লগ। একটা টপিক কয়েকটা পার্টিশনে ভাগ হয়ে বিভিন্ন ব্রোকারে (সার্ভারে) থাকে; প্রোডিউসার সেগুলোর শেষে ইভেন্ট যোগ করে, আর কনজিউমার নিজের গতিতে পড়ে, কোথা পর্যন্ত পড়েছে (offset) মনে রেখে। একই কনজিউমার গ্রুপের কনজিউমাররা টপিকের পার্টিশনগুলো ভাগ করে সমান্তরালে কাজ করে, আর আলাদা আলাদা গ্রুপ প্রত্যেকে পুরো স্ট্রিমটা স্বাধীনভাবে পড়ে। ইভেন্ট একটা নির্দিষ্ট সময় (retention) পর্যন্ত থাকে, তাই নতুন কোনো কনজিউমার পুরোনো ইতিহাসও আবার পড়তে পারে। CDC টুল (যেমন Debezium) ডেটাবেসের প্রতিটা পরিবর্তনকে একটা Kafka ইভেন্টে বদলে দেয়।
- Apache Spark ডেটাকে পার্টিশনে ভাগ করে একটা ক্লাস্টারে সমান্তরালে প্রসেস করে, DataFrame API আর Spark SQL দিয়ে। ডেটা যখন আর এক মেশিনে আঁটে না, তখন এটা ব্যবহার করুন। তার নিচে DuckDB — প্রসেসের ভেতরেই চলা একটা অ্যানালিটিক্যাল ডেটাবেস — ল্যাপটপেই Parquet আর CSV ফাইলে চমৎকার গতিতে SQL চালায়।
# ল্যাপটপে DuckDB: Parquet ফাইলের একটা ফোল্ডারে সরাসরি SQL
duckdb -c "SELECT year_month, SUM(revenue) FROM 'sales/*.parquet' GROUP BY 1 ORDER BY 1"| ডেটা লেক | ডেটা ওয়্যারহাউস | লেকহাউস | |
|---|---|---|---|
| যা রাখে | যেকোনো ধরনের কাঁচা ফাইল (JSON, ছবি, Parquet) | পরিষ্কার, মডেল করা টেবিল | খোলা ফরম্যাটের ফাইল হিসেবে টেবিল (Delta Lake, Apache Iceberg, Hudi) |
| স্কিমা | পড়ার সময় (on read) | লেখার সময় (on write) | লেখার সময়, বিবর্তনসহ |
| শক্তি | সস্তা, নমনীয়, যেকোনো ডেটা (ML-এর জন্য দারুণ) | দ্রুত SQL, গভর্ন্যান্স, BI | দুটোই: সস্তা স্টোরেজে ACID টেবিল, SQL আর Spark/ML দুই দিক থেকেই ব্যবহারযোগ্য |
| ঝুঁকি | ক্যাটালগ না থাকলে "ডেটা জলাভূমি" (data swamp) | খরচ, অসংগঠিত ডেটার জন্য কম উপযোগী | চলমান অংশ বেশি |
বড় ক্লাউড প্ল্যাটফর্মগুলো, ধারণাগতভাবে: BigQuery (Google, সার্ভারলেস, স্ক্যান করা বাইট বা রিজার্ভ করা স্লট ধরে দাম), Snowflake (AWS, Azure বা Google Cloud-এ চলে; স্টোরেজ আলাদা, আর কম্পিউটের জন্য আলাদা "ভার্চুয়াল ওয়্যারহাউস", যা ছোট-বড়, চালু-বন্ধ করা যায়, আর যতক্ষণ চলে ততক্ষণের সেকেন্ড ধরে বিল হয়), Amazon Redshift (AWS, ক্লাস্টার বা সার্ভারলেস), Databricks (Spark আর Delta Lake-এর ওপর লেকহাউস, ML-এ শক্তিশালী)। সবাই SQL বোঝে, ডায়ালেক্টে কিছু পার্থক্যসহ; সবাই কলাম ধরে ডেটা রাখে আর (মূলত) স্টোরেজ থেকে কম্পিউট আলাদা রাখে, তাই ডেটা না সরিয়েই কম্পিউট বাড়ানো-কমানো বা থামানো যায়।
ডেটাবেস স্কেল করা
পার্টিশনিং: PostgreSQL-এ চালিয়ে দেখুন
পার্টিশনিং একটা বড় টেবিলকে একটা কি (key) ধরে, সাধারণত তারিখ, একই সার্ভারের ভেতরে কয়েকটা টুকরোয় ভাগ করে (নিচের শার্ডিং ডেটা ছড়িয়ে দেয় একাধিক সার্ভারে)। যে কোয়েরি সেই কি দিয়ে ফিল্টার করে, সে বাকি টুকরোগুলো বাদ দেয় (পার্টিশন প্রুনিং), আর পুরোনো একটা পুরো মাস মুছে ফেলা মুহূর্তের কাজ। PostgreSQL নিজেই এটা সমর্থন করে:
-- PostgreSQL
CREATE TABLE sales_events (
event_id INTEGER NOT NULL,
event_date DATE NOT NULL,
customer_id INTEGER NOT NULL,
amount INTEGER NOT NULL
) PARTITION BY RANGE (event_date);
CREATE TABLE sales_events_2026q1 PARTITION OF sales_events
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE sales_events_2026q2 PARTITION OF sales_events
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
INSERT INTO sales_events
SELECT p.payment_id, p.paid_on, o.customer_id, p.amount
FROM payments AS p
JOIN orders AS o ON o.order_id = p.order_id;
SELECT tableoid::regclass::text AS partition, COUNT(*) AS events, SUM(amount) AS amount
FROM sales_events
GROUP BY 1
ORDER BY 1;
EXPLAIN (COSTS OFF)
SELECT SUM(amount) FROM sales_events WHERE event_date >= '2026-05-01';+---------------------+--------+--------+
| partition | events | amount |
+---------------------+--------+--------+
| sales_events_2026q1 | 8 | 35000 |
| sales_events_2026q2 | 5 | 35050 |
+---------------------+--------+--------+
+----------------------------------------------------+
| QUERY PLAN |
+----------------------------------------------------+
| Aggregate |
| -> Seq Scan on sales_events_2026q2 sales_events |
| Filter: (event_date >= '2026-05-01'::date) |
+----------------------------------------------------+সারিগুলো নিজে থেকেই ঠিক পার্টিশনে গেছে, আর প্ল্যান শুধু sales_events_2026q2 ছুঁয়েছে। ওয়্যারহাউসও তা-ই করে: BigQuery-তে পার্টিশন কলাম দিয়ে ফিল্টার করা কোয়েরির বিল হয় শুধু যে পার্টিশনগুলো পড়েছে তার।
স্কেল করার অন্য উপায়গুলো
| কৌশল | এটা কী | বিনিময়ে যা দিতে হয় |
|---|---|---|
| ভার্টিক্যাল স্কেলিং | বড় মেশিন: বেশি CPU, RAM, দ্রুত ডিস্ক | সবচেয়ে সহজ; কিন্তু একটা সীমা আছে, আর ওই একটা মেশিন বিকল হলেই সব বন্ধ (single point of failure) |
| রিড রেপ্লিকা (রেপ্লিকেশন) | প্রাইমারিকে অনুসরণ করা কপি; পড়া আর অ্যানালিটিক্স সেখানে যায় | রেপ্লিকা একটু পিছিয়ে থাকে (অ্যাসিনক্রোনাস): সদ্য লেখা সারি হয়তো এখনো আসেনি |
| হাই অ্যাভেইলেবিলিটি | প্রাইমারি বন্ধ হলে স্ট্যান্ডবাই রেপ্লিকা নিজে থেকে প্রাইমারি হয়ে যায় | সিনক্রোনাস রেপ্লিকেশন বেশি নিরাপদ, কিন্তু প্রতিটা commit ধীর করে |
| শার্ডিং (হরাইজন্টাল স্কেলিং) | একটা কি ধরে (যেমন customer_id) সারিগুলো অনেক সার্ভারে ভাগ | শার্ড পেরিয়ে জয়েন আর ট্রানজ্যাকশন কঠিন; কি সাবধানে বাছুন |
| কলামার স্টোরেজ | প্রতিটা কলাম একসাথে, কমপ্রেস করে রাখা | কয়েকটা কলামের অ্যাগ্রিগেটে অনেক কম ডেটা পড়তে হয়; একটা সারির update ধীর |
| ক্যাশিং, আগে থেকে অ্যাগ্রিগেট | ম্যাটেরিয়ালাইজড ভিউ, সামারি টেবিল | পড়া দ্রুত; ডেটা ততটাই টাটকা যতটা শেষ রিফ্রেশ |
গভর্ন্যান্স, স্কিমা বিবর্তন আর খরচ
- গভর্ন্যান্স আর ক্যাটালগ। একটা ক্যাটালগ (Unity Catalog, AWS Glue, DataHub, OpenMetadata) লিখে রাখে কোন কোন টেবিল আছে, মালিক কে, কলামের মানে কী, কোথা থেকে এসেছে (lineage) আর কে পড়তে পারবে। PII কলাম চিহ্নিত করুন, যাতে অ্যানালিস্টদের কাছে সেগুলো মাস্ক করা থাকে আর অনুমতি ছাড়া ট্রেনিং ডেটায় না ঢোকে।
- স্কিমা বিবর্তন। উৎস বদলায়: কলাম যোগ হয়, নাম বা টাইপ বদলায়। যোগ করার মতো পরিবর্তন পছন্দ করুন (নতুন nullable কলাম), মডেল ভার্সন করুন, আর অপ্রত্যাশিত পরিবর্তনে পাইপলাইন যেন চুপচাপ NULL লোড না করে জোরে ব্যর্থ হয়। Iceberg আর Delta-র মতো ফরম্যাট স্কিমার ভার্সন রাখে আর "টাইম ট্রাভেল" দিয়ে টেবিলটা আগের অবস্থায় পড়তে দেয় — ML ট্রেনিং সেট হুবহু আবার বানাতে কাজের।
- খরচ আর পারফরম্যান্স। ক্লাউডে টাকা দেন স্ক্যান করা বাইট আর কম্পিউটের সময়ের জন্য। শুধু দরকারি কলাম আনুন (কলাম স্টোরেজে
SELECT *-এর সত্যিই দাম আছে), পার্টিশন কলাম দিয়ে ফিল্টার করুন, প্রচলিত ফিল্টার ধরে ক্লাস্টার করুন, ব্যস্ত ড্যাশবোর্ডের জন্য আগে থেকে অ্যাগ্রিগেট করুন, আর বসে থাকা (idle) কম্পিউট বন্ধ রাখুন।
সাধারণ ভুল
- প্রোডাকশন ডেটাবেসে ভারী অ্যানালিটিক্স চালানো। একটা বড় রিপোর্ট সবার চেকআউট ধীর করে দেয়। সমাধান: হালকা রিপোর্টের জন্য রিড রেপ্লিকা, বাকি সবকিছুর জন্য ওয়্যারহাউস।
- গ্রেইন ঘোষণা না করা। একই ফ্যাক্ট টেবিলে অর্ডার-স্তর আর লাইন-স্তরের সারি মেশালে দুবার গোনা হয় (অনেক টেবিল জয়েন, আর ডুপ্লিকেট সারির ফাঁদ-এর ফ্যান-আউট ফাঁদ)। সমাধান: গ্রেইন লিখে রাখুন — "প্রতি অর্ডার লাইনে একটা সারি" — আর উৎসের সাথে
COUNT(*)মিলিয়ে নিন। - যে বৈশিষ্ট্যের ওপর ইতিহাস নির্ভর করে তা ওভাররাইট করা। শহরে টাইপ ১ করলে গত বছরের আয় এই বছরের শহরে চলে যায়, আর পুরোনো ML ফিচারে আজকের মান ঢুকে পড়ে। সমাধান:
valid_from/valid_to-সহ টাইপ ২, আর তারিখ ধরে জয়েন। - আধুনিক শোনায় বলে স্ট্রিমিং। দিনে একবার দেখা ড্যাশবোর্ডের জন্য Kafka ক্লাস্টার মানে কোনো লাভ ছাড়াই খরচ আর জটিলতা। সমাধান: ব্যাচ দিয়ে শুরু করুন; কোনো সিদ্ধান্তের জন্য টাটকা ডেটা লাগলে তবেই স্ট্রিম।
- যে কলাম দিয়ে ফিল্টার করেন না, তা দিয়ে পার্টিশন। প্রুনিং কখনো হয় না, আর প্রতিটা কোয়েরি প্রতিটা পার্টিশন খোলে। সমাধান: বেশিরভাগ
WHERE-এ যে কলাম থাকে (সাধারণত ইভেন্টের তারিখ), তা দিয়ে পার্টিশন করুন; পার্টিশন যেন খুব ছোট বা খুব বড় না হয়।
নিজে চেষ্টা করুন
- সহজ: স্টার স্কিমা দিয়ে সাপ্তাহিক ছুটির দিন বনাম কাজের দিনের বিক্রির আয় দেখান (বাতিল নয় এমন অর্ডার)।
- মাঝারি: গ্রাহকদের বর্তমান শহর ধরে আয়ের রিপোর্ট বানান (টাইপ ১-এর দৃষ্টিভঙ্গি): প্রতিটা ফ্যাক্টের গ্রাহক সারিকে একই
customer_id-এর বর্তমান সারির সাথে জয়েন করুন। - কঠিন: PostgreSQL-এ ২০২৬-এর তৃতীয় কোয়ার্টারের জন্য একটা পার্টিশন যোগ করুন,
2026-08-15তারিখের একটা ইভেন্ট ঢোকান, আর প্রতি পার্টিশনে সারির সংখ্যা দেখান।2027-01-05তারিখের ইভেন্ট ঢোকালে কী হয়?
উত্তর
-- ১.
SELECT CASE d.is_weekend WHEN 1 THEN 'weekend' ELSE 'weekday' END AS day_type,
COUNT(DISTINCT f.order_id) AS orders,
SUM(f.revenue) AS revenue
FROM fact_sales AS f
JOIN dim_date AS d ON d.date_key = f.date_key
WHERE f.order_status <> 'cancelled'
GROUP BY day_type
ORDER BY day_type;
-- ২.
SELECT cur.city AS current_city, SUM(f.revenue) AS revenue
FROM fact_sales AS f
JOIN dim_customer AS c ON c.customer_key = f.customer_key
JOIN dim_customer AS cur ON cur.customer_id = c.customer_id AND cur.is_current = 1
WHERE f.order_status <> 'cancelled'
GROUP BY cur.city
ORDER BY revenue DESC, current_city;-- PostgreSQL
-- ৩. কোনো পার্টিশনে না পড়া ইভেন্ট ব্যর্থ হয়: "no partition of relation ... found for row"।
CREATE TABLE sales_events_2026q3 PARTITION OF sales_events
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
INSERT INTO sales_events VALUES (100, '2026-08-15', 3, 2500);
SELECT tableoid::regclass::text AS partition, COUNT(*) AS events
FROM sales_events
GROUP BY 1
ORDER BY 1;
INSERT INTO sales_events VALUES (101, '2027-01-05', 3, 900);+---------------------+--------+
| partition | events |
+---------------------+--------+
| sales_events_2026q1 | 8 |
| sales_events_2026q2 | 5 |
| sales_events_2026q3 | 1 |
+---------------------+--------+
Error: no partition of relation "sales_events" found for rowসারসংক্ষেপ
- OLTP সিস্টেম ছোট, নরমালাইজড ট্রানজ্যাকশনে ব্যবসা চালায়; OLAP সিস্টেম ইতিহাসের ওপর বড় প্রশ্নের উত্তর দেয়, সাধারণত আলাদা একটা কলাম-ভিত্তিক ওয়্যারহাউসে।
- স্টার স্কিমা হলো ঘোষিত গ্রেইনের একটা ফ্যাক্ট টেবিল, চারপাশে সারোগেট কিসহ ডাইমেনশন টেবিল; কোয়েরি জয়েন করে, বৈশিষ্ট্য দিয়ে ফিল্টার করে আর মেজার যোগ করে।
- টাইপ ১ ডাইমেনশন ওভাররাইট করে; টাইপ ২ বৈধতার পরিসর আর পয়েন্ট-ইন-টাইম জয়েন দিয়ে ইতিহাস রাখে — যে ধারণা ML ফিচারকে লিকমুক্ত রাখে, সেটাই।
- ব্যাচই ডিফল্ট; স্ট্রিমিং (Kafka, Flink, Spark Structured Streaming) সেই সিদ্ধান্তের জন্য যাতে টাটকা ডেটা লাগে। লেকে থাকে কাঁচা ফাইল, ওয়্যারহাউসে মডেল করা টেবিল, লেকহাউস দুটোকে মেলায়।
- পার্টিশনিং (প্রুনিং), রেপ্লিকা, শার্ডিং আর কলামার স্টোরেজ দিয়ে স্কেল করুন; ক্যাটালগ, সাবধানী স্কিমা বিবর্তন আর স্ক্যান করা বাইটে নজর রেখে শৃঙ্খলা বজায় রাখুন।
এরপর: SQL ইন্টারভিউ ও সমস্যা সমাধানের অনুশীলন পুরো টিউটোরিয়ালকে একসাথে নিয়ে আসে শপের ডেটার ওপর ধাপে ধাপে কঠিন হওয়া সমস্যায়, প্রতিটার সমাধান আর তার পেছনের যুক্তিসহ।