সরাসরি পর্বে যান
পর্ব ৫৫ · ৬৭-এর মধ্যে · মডিউল ৯

Data warehouse — OLAP, star schema আর ETL

Data Warehousing — OLTP vs OLAP, Dimensional Modeling, Star Schema, Slowly Changing Dimensions, OLAP Operations and ETL

Taught in Bangla, this lesson covers data warehousing: OLTP versus OLAP, why a separate warehouse and Inmon versus Kimball, dimensional modeling with grain, facts and dimensions, building star and snowflake schemas live from the course shop database, answering business questions with GROUP BY, ROLLUP and window functions, OLAP operations, slowly changing dimensions, ETL, ELT and incremental loads, and column stores and lakehouses.

এই পর্বে যা শিখবেন

  • OLTP আর OLAP-এর পার্থক্য, আলাদা warehouse-এর তিন কারণ, আর Inmon ও Kimball-এর দুই পথ
  • grain ঘোষণা করে fact আর dimension বেছে নেওয়া; star বনাম snowflake schema
  • db-shop থেকে লাইভ fact_sales, dim_date (recursive CTE), আর ROLLUP ও window function দিয়ে ব্যবসার প্রশ্নের উত্তর
  • OLAP-এর পাঁচ অপারেশন, additive আর semi-additive measure, আর SCD type 1, 2, 3
  • ETL বনাম ELT, quality check, watermark, column store, MPP, lakehouse আর BI টুলের মানচিত্র

১ · OLTP আর OLAP — একই ডেটা, দুই ধরনের কাজ

এতদিন যে ডেটাবেস দেখেছেন, তার কাজ প্রতিদিনের লেনদেন: একটি অর্ডার ঢোকানো, একটি স্টক কমানো, একজন গ্রাহকের ঠিকানা দেখা। এই ধরনের কাজের নাম OLTP (online transaction processing). প্রতিটি কোয়েরি অল্প কয়েকটি সারি ছোঁয়, primary key বা index ধরে, আর মিলিসেকেন্ডে শেষ হয়। কিন্তু দোকানের মালিক মাসের শেষে জানতে চান: “গত ছয় মাসে কোন শহরে কোন শ্রেণির বিক্রি সবচেয়ে বেড়েছে?” এই প্রশ্ন কয়েকটি সারি নয়, প্রায় সব সারি পড়ে আর যোগ করে। এই কাজের নাম OLAP (online analytical processing).

একটি বড় মুদি দোকানের কথা ভাবুন। ক্যাশ কাউন্টারে প্রতি মিনিটে দশজন খদ্দের — প্রতিটি বিক্রি দ্রুত খাতায় তুলতে হবে, লাইন যেন না জমে। এটি OLTP. রাতে দোকান বন্ধের পর হিসাবরক্ষক পুরো মাসের খাতা নিয়ে বসেন, পাতার পর পাতা যোগ করেন। এটি OLAP. দিনের ভিড়ে হিসাবরক্ষক যদি কাউন্টারের খাতাটিই টেনে নিয়ে যোগ করতে বসেন, খদ্দেরের লাইন থেমে যাবে। তাই বড় দোকান হিসাবের জন্য খাতার একটি আলাদা কপি রাখে — সেটিই data warehouse.

বৈশিষ্ট্যOLTP (চালু সিস্টেম)OLAP (warehouse)
কোয়েরির ধরনkey ধরে কয়েকটি সারি পড়া বা লেখা; INSERT, UPDATE বেশিবিশাল scan, JOIN আর GROUP BY; প্রায় শুধু পড়া
প্রতি কোয়েরিতে সারিএক থেকে কয়েকশোলাখ থেকে শতকোটি
মোট ডেটাবর্তমান অবস্থা, তুলনায় ছোটবহু বছরের ইতিহাস, বহু উৎস — অনেক বড়
ব্যবহারকারীহাজারো গ্রাহক আর অ্যাপ, একসাথেঅল্প কিছু বিশ্লেষক, dashboard আর রিপোর্ট
প্রত্যাশিত সময়মিলিসেকেন্ডসেকেন্ড থেকে মিনিট চলে
ডেটা কতটা তাজাএই মুহূর্তেরসাধারণত কয়েক মিনিট থেকে এক দিন পুরোনো
নকশাnormalized (3NF, পর্ব ৩৬) — লেখা সঠিক রাখতেইচ্ছাকৃত denormalized (star schema, পর্ব ৩৮) — পড়া সহজ ও দ্রুত রাখতে
storageসাধারণত row storeসাধারণত column store

এই পর্বের লাইভ SQL চলে দোকানের ডেটাবেসে (db-shop): customers (customer_id, name, city, phone, joined_on — ৮ জন), categories (category_id, name — ৪টি শ্রেণি), products (product_id, name, category_id, price, stock — ১০টি পণ্য), orders (order_id, customer_id, order_date, status — ১০টি অর্ডার, ২০২৫-এর সেপ্টেম্বর আর অক্টোবরে; status হলো pending, shipped, delivered বা cancelled) আর order_items (order_id, product_id, quantity, unit_price — ১৬টি লাইন; unit_price বিক্রির মুহূর্তের দাম)। এটিই আমাদের “চালু সিস্টেম”; এর থেকে আমরা একটি ছোট warehouse বানাব। প্রতিবার চালালে ডেটাবেস নতুন করে তৈরি হয়, আর একটি এডিটরের স্টেটমেন্টগুলো একটি সংযোগে পরপর চলে — কোনোটিতে ত্রুটি হলে সেখানেই থামে।

২ · আলাদা warehouse কেন — আর Inmon বনাম Kimball

চালু ডেটাবেসের একটি read replica-তেই তো রিপোর্ট চালানো যায় (পর্ব ৫২)? ছোট দোকানে প্রায়ই সেটাই যথেষ্ট। কিন্তু প্রতিষ্ঠান বড় হলে তিনটি কারণে আলাদা warehouse লাগে:

১. চালু সিস্টেমকে আঘাত না করা

একটি বিশাল GROUP BY ডিস্ক, CPU আর buffer pool (পর্ব ৩৯) দখল করে; লম্বা কোয়েরি MVCC-র পুরোনো version আটকে রাখে (পর্ব ৪৭)। warehouse-এ সেই চাপ আলাদা যন্ত্রে যায়।

২. বহু উৎস মেলানো

বিক্রি এক ডেটাবেসে, পেমেন্ট আরেকটিতে, কুরিয়ারের তথ্য একটি CSV-তে, বিজ্ঞাপনের খরচ একটি ওয়েবসাইটের API-তে। “কোন বিজ্ঞাপনে কত লাভ” — উত্তর দিতে সব এক জায়গায়, একই নাম আর একই key-তে আনতে হয়।

৩. ইতিহাস রাখা

চালু সিস্টেম বর্তমান অবস্থা রাখে: গ্রাহক শহর বদলালে পুরোনো শহর মুছে যায়, পুরোনো অর্ডার আর্কাইভ হয়। warehouse ইচ্ছে করে বছরের পর বছরের ইতিহাস রাখে — অংশ ৭-এ দেখবেন কীভাবে।

Bill Inmon-এর একটি পরিচিত সংজ্ঞায় warehouse হলো subject-oriented, integrated, time-variant, non-volatile ডেটার সংগ্রহ, যা ব্যবস্থাপনার সিদ্ধান্তে সাহায্য করে — অর্থাৎ বিষয় ধরে সাজানো (বিক্রি, গ্রাহক), বহু উৎস মিলিয়ে একরূপ, সময়ের সাথে ইতিহাসসহ, আর একবার বসানো ডেটা সাধারণত বদলানো হয় না। এই লক্ষ্যে পৌঁছানোর দুটি বিখ্যাত পথ আছে:

দিকInmon (ওপর থেকে নিচে)Kimball (নিচ থেকে ওপরে)
কেন্দ্রপুরো প্রতিষ্ঠানের একটি normalized (প্রায় 3NF) warehouseব্যবসার প্রক্রিয়া ধরে dimensional মডেল (star schema)
রিপোর্ট কোথা থেকেকেন্দ্র থেকে বানানো বিভাগভিত্তিক data martসরাসরি star schema থেকে; mart-গুলো মিলে warehouse
সংহতি কীভাবেকেন্দ্রীয় normalized মডেলেভাগ করা conformed dimension (একই dim_date, dim_product)
শক্তিএকটিই সংহত সত্য; উৎস বদলালে কেন্দ্র স্থির থাকেদ্রুত প্রথম ফল; ব্যবসার লোক সহজে বোঝেন
দুর্বলতাশুরুতে বড় বিনিয়োগ, ফল দেখতে দেরিconformed dimension-এ শৃঙ্খলা না থাকলে mart-গুলো একে অন্যের সাথে মেলে না

দুই পদ্ধতিই আজও ব্যবহৃত, আর বাস্তবে অনেক দল মিশিয়ে চলে: কাঁচা ডেটা একটি সংহত স্তরে, আর বিশ্লেষকের সামনে star schema. Data Vault-এর মতো তৃতীয় পদ্ধতিও আছে। “কোনটি সঠিক” — এই প্রশ্নের চেয়ে জরুরি হলো, আপনার দলের আকার আর ডেটার উৎস কোন পথে মানায় (প্রশ্ন ০১)।

ইতিহাস এক নজরে। ১৯৮০-এর দশকের শেষে IBM-এর Barry Devlin আর Paul Murphy “business data warehouse”-এর ধারণা প্রকাশ করেন। Bill Inmon-এর Building the Data Warehouse (১৯৯২) আর Ralph Kimball-এর The Data Warehouse Toolkit (১৯৯৬) দুই ধারার মূল বই। “OLAP” শব্দটি ১৯৯৩ সালে E. F. Codd ও সহলেখকদের একটি প্রতিবেদন থেকে প্রচলিত হয় — সেই Codd, যিনি ১৯৭০ সালে relational model দিয়েছিলেন। ১৯৯৬ সালে Jim Gray ও সহকর্মীরা “data cube” অপারেটর প্রস্তাব করেন, যা থেকে SQL:1999-এ ROLLUP, CUBE আর GROUPING SETS আসে।

৩ · Dimensional modeling — আগে grain, তারপর fact আর dimension

Dimensional modeling-এ টেবিল দুই রকম। Fact টেবিল-এর প্রতিটি সারি একটি মাপা ঘটনা — একটি বিক্রি, একটি পেমেন্ট, একটি ট্রেনের টিকিট — আর তাতে থাকে সংখ্যা, যাদের বলে measure (পরিমাণ, টাকা)। Dimension টেবিল ঘটনার প্রেক্ষাপট দেয়: কখন (তারিখ), কে (গ্রাহক), কী (পণ্য), কোথায় (শাখা, শহর)। রিপোর্টের ভাষায়, dimension দিয়ে “অনুযায়ী” ভাগ করেন (“শহর অনুযায়ী”), আর measure যোগ করেন।

Kimball-এর চার ধাপের নকশা-পদ্ধতি: ১) ব্যবসার প্রক্রিয়া বাছুন; ২) grain ঘোষণা করুন — fact-এর এক সারি ঠিক কী; ৩) dimension বাছুন; ৪) measure বাছুন। ক্রমটি গুরুত্বপূর্ণ: grain আগে না ঠিক করলে কোন dimension বসবে আর কোন সংখ্যা যোগ করা নিরাপদ, কিছুই ঠিক হয় না। মিশ্র grain-এর টেবিল — কিছু সারি অর্ডার, কিছু সারি অর্ডার-লাইন — যোগফল চুপচাপ দ্বিগুণ করে। নিচের stepper-এ db-shop-এর বিক্রির জন্য তিনটি grain যাচাই করুন:

প্রক্রিয়া: বিক্রি (বাতিল অর্ডার 1004 বাদ)তিনটি প্রশ্ন: ক. মাস × শ্রেণি · খ. শহরে সেরা গ্রাহক · গ. পণ্য কত পিস

            
হিসাব
বিবেচনায়
fact-এর সারি
উত্তর মেলে
  1. প্রক্রিয়া আর উৎস। বিক্রির প্রক্রিয়া দুই টেবিলে: orders আর order_items. বাতিল অর্ডার বাদ দিলে ৯টি অর্ডার, ১৫টি লাইন। অর্ডার 1005-এ দুটি ভিন্ন শ্রেণির পণ্য — এটিই পরীক্ষার উদাহরণ।
  2. বিকল্প A: অর্ডার। এক সারি = এক অর্ডার, ৯টি সারি। শহর ধরে সেরা গ্রাহক বের করা যায়, কিন্তু 1005-এর 5050 টাকার কতটা Clothing আর কতটা Books, তা আর বলা যায় না।
  3. বিকল্প B: দিন × পণ্য। আগে থেকে যোগ করা summary — বড় দোকানে সারি অনেক কমে (এই ছোট ডেটায় কাকতালীয়ভাবে ১৫টিই)। কিন্তু গ্রাহক মুছে গেছে, তাই প্রশ্ন খ অসম্ভব।
  4. বিকল্প C: অর্ডার-লাইন। উৎসের সবচেয়ে সূক্ষ্ম স্তর। A আর B দুটিই এ থেকে GROUP BY করে পাওয়া যায়, উল্টোটা কখনো নয় — যোগ করা তথ্য আর ভাঙা যায় না। তিনটি প্রশ্নেরই উত্তর মেলে।
  5. ঘোষণা আর তার ফল। grain ঠিক হলে বাকিটা প্রায় নিজে থেকে আসে: প্রতিটি লাইনের কখন, কে, কী — তিনটি dimension; সংখ্যা quantity আর amount. order_id fact-এই থাকে, কারণ তার বলার মতো আলাদা কোনো বৈশিষ্ট্য নেই — একে বলে degenerate dimension.
মূল নিয়ম

সম্ভব হলে সবচেয়ে সূক্ষ্ম (atomic) grain বেছে নিন, আর একটি fact টেবিলে কখনো দুই grain মেশাবেন না। সূক্ষ্ম ডেটা থেকে যেকোনো সারাংশ বানানো যায়, সারাংশ থেকে সূক্ষ্ম ডেটা কখনো নয়। গতির জন্য সারাংশ দরকার হলে সেটি আলাদা একটি aggregate টেবিল, নিজের ঘোষিত grain-সহ।

৪ · Star schema আর snowflake schema — db-shop থেকে লাইভ বানানো

Star schema-এ মাঝে fact টেবিল, চারপাশে dimension — প্রতিটি dimension fact থেকে ঠিক এক ধাপ দূরে। dimension ইচ্ছে করে denormalized: পণ্যের dimension-এ শ্রেণির নাম সরাসরি একটি কলাম (পর্ব ৩৮)। dimension আবার normalized করে ভাঙলে — পণ্য থেকে শ্রেণি, তারিখ থেকে মাস আলাদা টেবিলে — নকশার নাম snowflake schema:

Star schema বনাম snowflake schema

Same fact_sales; on the right, dimensions are normalized into extra tables

dim_date dim_customer dim_product fact_salesqty · amount star: প্রতিটি dimension এক ধাপে dim_month dim_date dim_customer dim_product dim_city dim_category fact_salesqty · amount snowflake: বেগুনি টেবিলে যেতে দুই ধাপ
Star-এ “শ্রেণি অনুযায়ী বিক্রি” একটি JOIN; snowflake-এ দুটি। বিনিময়ে snowflake-এ শ্রেণির নাম এক জায়গায় থাকে।

কোনটি ভালো? Kimball-ধারা সাধারণত star পছন্দ করে: কোয়েরি সহজ, BI টুল সহজে বোঝে, আর dimension fact-এর তুলনায় এত ছোট যে কপি করা নামের জায়গা প্রায় নগণ্য। snowflake কাজে আসে যখন একটি dimension বিশাল আর তার একটি অংশ বারবার বদলায়, বা একই উপ-টেবিল বহু dimension ভাগ করে। এবার db-shop থেকে আসল star বানানো যাক। CREATE TABLE AS SELECT একটি SELECT-এর ফল থেকে সরাসরি টেবিল বানায় — ছোট ETL-এর জন্য যথেষ্ট:

CREATE TABLE dim_product AS
SELECT p.product_id AS product_key, p.name AS product_name, c.name AS category
FROM products p JOIN categories c ON c.category_id = p.category_id;
CREATE TABLE dim_customer AS
SELECT customer_id AS customer_key, name, COALESCE(city, 'Unknown') AS city
FROM customers;
CREATE TABLE fact_sales AS                    -- grain: একটি অর্ডার-লাইন
SELECT i.order_id,
       CAST(strftime('%Y%m%d', o.order_date) AS INTEGER) AS date_key,
       o.customer_id AS customer_key, i.product_id AS product_key,
       i.quantity, i.quantity * i.unit_price AS amount
FROM orders o JOIN order_items i ON i.order_id = o.order_id
WHERE o.status <> 'cancelled';
SELECT COUNT(*) AS lines, COUNT(DISTINCT order_id) AS orders, SUM(amount) AS revenue
FROM fact_sales;

তিনটি টেবিল তৈরি (✓), তারপর 15 | 9 | 52320 — ১৫টি লাইন, ৯টি অর্ডার, মোট ৫২,৩২০ টাকা। লক্ষ করুন তিনটি ETL-সিদ্ধান্ত, যা এখন লিখিত নিয়ম: বাতিল অর্ডার বাদ; Imran-এর অজানা শহর NULL-এর বদলে 'Unknown' — নইলে GROUP BY-তে একটি নামহীন দল আসে আর BI টুলে ফিল্টার করা কঠিন হয়; আর date_key একটি YYYYMMDD সংখ্যা, যা মানুষও পড়তে পারে। এখানে সহজ রাখতে উৎসের id-ই key; বাস্তবে dimension-এর নিজস্ব surrogate key থাকে, আর উৎসের id থাকে business key হিসেবে — কেন, তা অংশ ৭-এ স্পষ্ট হবে।

Date dimension — recursive CTE দিয়ে প্রতিটি দিনের একটি সারি

“শুক্র-শনিবারে বিক্রি কেমন?”, “ঈদের সপ্তাহে কত?” — এমন প্রশ্নে প্রতিবার তারিখ-ফাংশন লেখার বদলে warehouse-এ একটি date dimension থাকে: প্রতিটি দিনের একটি সারি, সাথে মাস, ত্রৈমাসিক, সাপ্তাহিক ছুটি কি না, সরকারি ছুটি কি না। দিনগুলো বানানো যায় পর্ব ২৫-এর recursive CTE দিয়ে। বাংলাদেশে সাপ্তাহিক ছুটি সাধারণত শুক্র আর শনিবার; SQLite-এর strftime('%w')-এ রবিবার 0, তাই শুক্র 5, শনি 6:

CREATE TABLE dim_date AS
WITH RECURSIVE d(day) AS (
  SELECT '2025-09-01'
  UNION ALL
  SELECT DATE(day, '+1 day') FROM d WHERE day < '2025-10-31'
)
SELECT CAST(strftime('%Y%m%d', day) AS INTEGER) AS date_key,
       day AS full_date,
       strftime('%Y-%m', day) AS month,
       CASE WHEN strftime('%w', day) IN ('5', '6') THEN 1 ELSE 0 END AS is_weekend
FROM d;
SELECT COUNT(*) AS days, MIN(full_date) AS first_day, MAX(full_date) AS last_day,
       SUM(is_weekend) AS weekend_days
FROM dim_date;
SELECT * FROM dim_date WHERE full_date BETWEEN '2025-09-04' AND '2025-09-07';

প্রথম ফল 61 | 2025-09-01 | 2025-10-31 | 17 — সেপ্টেম্বরের ৩০ আর অক্টোবরের ৩১, মোট ৬১ দিন, যার ১৭টি শুক্র বা শনিবার। দ্বিতীয় ফলে চারটি দিন: 09-04 (বৃহস্পতি) is_weekend 0, 09-05 আর 09-06 (শুক্র, শনি) 1, 09-07 (রবি) 0. ISO ফরম্যাটের তারিখ লেখা হিসেবেও ঠিক ক্রমে সাজে, তাই day < '2025-10-31' তুলনাটি নিরাপদ। বাস্তবে এই টেবিল কয়েক দশকের জন্য একবার বানানো হয় — ৫০ বছরেও মাত্র ১৮ হাজারের মতো সারি — আর সরকারি ছুটির তালিকা আলাদা করে বসানো হয়, কারণ ঈদের তারিখ চাঁদ দেখার ওপর নির্ভর করে, কোনো সূত্রে আসে না।

৫ · ব্যবসার প্রশ্নের উত্তর — GROUP BY, ROLLUP আর window

এখন থেকে প্রতিটি এডিটরে উপরের তিনটি dimension আর fact_sales আগে থেকেই বানানো থাকে (নমুনা ডেটার বাক্সে দেখতে পাবেন)। প্রথম প্রশ্ন: মাস × শ্রেণি অনুযায়ী বিক্রি। star schema-র প্রতিটি রিপোর্ট একই আকারের — fact থেকে দরকারি dimension-এ এক ধাপের JOIN, তারপর GROUP BY (পর্ব ১৮):

SELECT d.month, p.category,
       SUM(f.quantity) AS units, SUM(f.amount) AS revenue
FROM fact_sales f
JOIN dim_date d    ON d.date_key = f.date_key
JOIN dim_product p ON p.product_key = f.product_key
GROUP BY d.month, p.category
ORDER BY d.month, revenue DESC;

৭টি সারি। সেপ্টেম্বর: Electronics | 4 | 20900, Clothing | 2 | 4400, Books | 4 | 2400, Grocery | 5 | 1780; অক্টোবর: Electronics | 3 | 19150, Grocery | 9 | 3140, Books | 1 | 550. অক্টোবরে Clothing-এর কোনো সারিই নেই — বিক্রি শূন্য হলে GROUP BY সারি বানায় না। শূন্যসহ পূর্ণ গ্রিড চাইলে dim_date × dim_product-এর CROSS JOIN থেকে fact-এ LEFT JOIN করতে হয়। লক্ষ করুন, পর্ব ৩৮-এ শহর × শ্রেণির একই ধরনের প্রশ্নে normalized টেবিলে পাঁচটি টেবিল, চারটি JOIN আর বাতিলের শর্ত লেগেছিল; এখানে দুটি JOIN, আর বাতিলের নিয়ম ETL-এ একবারই লেখা।

রিপোর্টে প্রায়ই প্রতি মাসের উপ-মোট আর সর্বমোট লাগে। PostgreSQL, SQL Server আর Oracle-এ GROUP BY ROLLUP (month, category) একবারে (month, category), (month) আর () — তিন স্তরের ফল দেয় (ROLLUP, পর্ব ১৮)। MySQL-এ লেখা হয় GROUP BY month, category WITH ROLLUP. SQLite-এ ROLLUP নেই, তাই একই ফল তিনটি GROUP BY-এর UNION ALL দিয়ে:

WITH s AS (
  SELECT d.month, p.category, f.amount
  FROM fact_sales f
  JOIN dim_date d    ON d.date_key = f.date_key
  JOIN dim_product p ON p.product_key = f.product_key
), r AS (
  SELECT month, category, SUM(amount) AS revenue FROM s GROUP BY month, category
  UNION ALL
  SELECT month, NULL, SUM(amount) FROM s GROUP BY month
  UNION ALL
  SELECT NULL, NULL, SUM(amount) FROM s
)
SELECT * FROM r
ORDER BY month IS NULL, month, category IS NULL, category;

১০টি সারি: সেপ্টেম্বরের চারটি শ্রেণির পরে 2025-09 | NULL | 29480, অক্টোবরের তিনটির পরে 2025-10 | NULL | 22840, আর শেষে NULL | NULL | 52320. ROLLUP-ও ঠিক এভাবে উপ-মোটের সারিতে NULL বসায়। ঝুঁকি: আসল ডেটায় category নিজেই NULL হলে কোনটি উপ-মোট আর কোনটি “অজানা শ্রেণি”, বোঝা যায় না — ROLLUP-এর সাথে তাই GROUPING(category) ফাংশন থাকে, যা উপ-মোটের সারিতে 1 দেয়। এজন্যও dimension-এ NULL না রেখে 'Unknown' রাখা ভালো। ORDER BY month IS NULL কৌশলটি উপ-মোটের সারিকে দলের শেষে পাঠায়; UNION-এর পরে সরাসরি এমন expression দিয়ে সাজানো SQLite-এ চলে না, তাই বাইরের SELECT.

দ্বিতীয় প্রশ্ন: প্রতিটি শহরের সেরা গ্রাহক, আর মোট বিক্রিতে প্রত্যেকের ভাগ। এখানে লাগে পর্ব ২৬-এর window function — GROUP BY-এর পর প্রতিটি দলের সারি না হারিয়ে র‍্যাংক আর শতাংশ:

WITH spent AS (
  SELECT c.city, c.name, SUM(f.amount) AS total
  FROM fact_sales f
  JOIN dim_customer c ON c.customer_key = f.customer_key
  GROUP BY c.city, c.name
)
SELECT city, name, total,
       RANK() OVER (PARTITION BY city ORDER BY total DESC) AS rank_in_city,
       ROUND(100.0 * total / SUM(total) OVER (), 1) AS pct_of_all
FROM spent
ORDER BY total DESC;

৬টি সারি: Dhaka | Rahim Uddin | 37180 | 1 | 71.1, Rajshahi | Nusrat Jahan | 5050 | 1 | 9.7, Chattogram | Karima Begum | 5000 | 1 | 9.6, Sylhet | Farhana Akter | 3500 | 1 | 6.7, Unknown | Imran Hossain | 1040 | 1 | 2, আর Dhaka | Mitu Rani | 550 | 2 | 1.1. ঢাকায় দুজন, তাই শুধু সেখানে র‍্যাংক 2 আছে। SUM(total) OVER ()-এর খালি বন্ধনী মানে পুরো ফল একটি window — সর্বমোট ৫২,৩২০, প্রতিটি সারির পাশে। একজন গ্রাহক মোট বিক্রির ৭১ শতাংশ — ছোট দোকানের ডেটায় এটি একটি ব্যবসায়িক ঝুঁকির সংকেত, আর warehouse-এর কাজই এমন প্রশ্ন সহজ করা। Sabbir আর Tanvir নেই: একজনের একমাত্র অর্ডার বাতিল, অন্যজনের কোনো অর্ডারই নেই।

৬ · OLAP অপারেশন আর measure-এর তিন জাত

বিশ্লেষক ডেটাকে একটি cube হিসেবে ভাবেন: প্রতিটি dimension একটি অক্ষ, প্রতিটি ঘরে একটি measure. আমাদের cube-এর তিন অক্ষ মাস, শ্রেণি আর শহর; ঘরে বিক্রির টাকা। এর ওপর পাঁচটি প্রচলিত অপারেশন — প্রতিটি আসলে একটি GROUP BY বা WHERE-এর বদল:

cube: মাস × শ্রেণি × শহর · ঘরে amount (টাকা)সংখ্যাগুলো db-shop-এর fact_sales থেকে

            
অবস্থা
অপারেশন
দেখানো ঘর
মোট টাকা
  1. Cube-এর একটি তল। শহর অক্ষ যোগ করে ফেললে মাস × শ্রেণির ৮টি ঘর থাকে। অক্টোবরে Clothing-এর বিক্রি নেই (—)। সব ঘর মিলে ৫২,৩২০ টাকা।
  2. Roll-up. শ্রেণির অক্ষে ওপরের স্তরে উঠে যোগ: প্রতি মাসের একটি সংখ্যা। ঘর ৮ থেকে ২, মোট একই — ৫২,৩২০. Roll-up মানে GROUP BY থেকে একটি কলাম সরানো।
  3. Drill-down. উল্টো দিকে: অক্টোবরের ২২,৮৪০ ভেঙে দিনে দিনে। চারটি দিনের যোগ আবার ২২,৮৪০ — drill-down সম্ভব কারণ fact-এর grain দিনের চেয়েও সূক্ষ্ম।
  4. Slice. শহর অক্ষে একটি মান স্থির: শুধু Dhaka. cube থেকে একটি তল কাটা পড়ে, আর বাকি দুই অক্ষ থাকে। ঢাকার মোট ৩৭,৭৩০ (Rahim ৩৭,১৮০ + Mitu ৫৫০)।
  5. Dice. কয়েকটি অক্ষে মানের সেট: দুই শহর আর দুই শ্রেণি — একটি ছোট উপ-cube. মোট ১৭,৪০০ + ১,৭৮০ + ১৯,১৫০ + ২,১০০ = ৪০,৪৩০ (slice আর dice দুটিই আসলে WHERE).
  6. Pivot. ডেটা একই, শুধু অক্ষ ঘোরানো: শ্রেণি সারিতে, মাস কলামে। SQL-এ এটি পর্ব ২৭-এর pivot প্যাটার্ন — নিচের এডিটরে চালিয়ে দেখুন।
SELECT p.category,
       SUM(CASE WHEN d.month = '2025-09' THEN f.amount ELSE 0 END) AS sep_2025,
       SUM(CASE WHEN d.month = '2025-10' THEN f.amount ELSE 0 END) AS oct_2025
FROM fact_sales f
JOIN dim_date d     ON d.date_key = f.date_key
JOIN dim_product p  ON p.product_key = f.product_key
JOIN dim_customer c ON c.customer_key = f.customer_key
WHERE c.city IN ('Dhaka', 'Chattogram')                 -- dice
  AND p.category IN ('Electronics', 'Grocery')
GROUP BY p.category
ORDER BY p.category;

দুটি সারি: Electronics | 17400 | 19150 আর Grocery | 1780 | 2100 — stepper-এর dice আর pivot ধাপের সংখ্যাই। WHERE অংশটি dice, CASE অংশটি pivot. WHERE থেকে category শর্তটি মুছে চালালে Books সারিও আসে (1750 | 550 — Karima-র সেপ্টেম্বরের বই আর Mitu-র অক্টোবরের SQL Book). মাসগুলো কোয়েরিতে হাতে লেখা — নতুন মাস এলে কোয়েরি বদলাতে হয়; তাই বেশির ভাগ BI টুল pivot নিজেরাই করে, SQL থেকে লম্বা আকারের ফল নিয়ে।

সব সংখ্যা যোগ করা যায় না — additive, semi-additive আর non-additive

Roll-up মানে যোগ। কিন্তু যোগ সব measure-এ অর্থবহ নয়:

Additive

যেকোনো dimension ধরে যোগ করা যায়: বিক্রির টাকা, পরিমাণ। দিন যোগ করে মাস, শহর যোগ করে দেশ — সব ঠিক।

Semi-additive

কিছু অক্ষে যোগ চলে, সময়ে নয়। bKash-এর মতো ওয়ালেটে দিনশেষের ব্যালান্স: একই দিনে সব গ্রাহকের যোগ ঠিক, কিন্তু একজনের ৩০ দিনের ব্যালান্স যোগ করা অর্থহীন। সময়ে নিতে হয় শেষ দিনের মান বা গড়। গুদামের দিনশেষের স্টকও তাই।

Non-additive

কোনো অক্ষেই যোগ চলে না: লাভের শতাংশ, গড় দাম, আলাদা গ্রাহকের সংখ্যা। শতাংশ যোগ বা গড়ের গড় ভুল; লব আর হর আলাদা রেখে শেষে ভাগ করুন।

একটি উদাহরণে non-additive-এর ফাঁদ: সেপ্টেম্বরে আলাদা ক্রেতা ৪ জন, অক্টোবরে ৪ জন — দুই মাসে কি ৮ জন? না; Rahim আর Karima দুই মাসেই কিনেছেন, তাই আসলে ৬ জন। COUNT(DISTINCT) দিনের বা মাসের স্তরে জমিয়ে রাখলে ওপরের স্তরে যোগ করা যায় না। যে fact টেবিলে প্রতিদিন প্রতিটি অ্যাকাউন্টের ব্যালান্সের একটি সারি থাকে, তাকে বলে periodic snapshot fact — আমাদের fact_sales-এর মতো প্রতিটি ঘটনার একটি সারি (transaction fact) থেকে তার grain আলাদা।

৭ · Slowly changing dimension — গ্রাহক শহর বদলালে ইতিহাসের কী হয়

Rahim Uddin ২০২৫-এর ৫ অক্টোবর Dhaka থেকে Chattogram-এ চলে গেলেন। চালু সিস্টেমে customers.city সোজা UPDATE হবে — সেটিই ঠিক, কারণ কুরিয়ারকে নতুন ঠিকানা লাগবে। কিন্তু warehouse-এ প্রশ্ন ওঠে: সেপ্টেম্বরের “ঢাকার বিক্রি”-তে Rahim-এর কেনাকাটা থাকবে, না Chattogram-এ সরে যাবে? যে dimension-এর মান মাঝে মাঝে বদলায়, তার নাম slowly changing dimension (SCD), আর বদল রাখার তিনটি প্রচলিত নিয়ম:

  • Type 1 — মুছে লেখা: dim_customer-এর city সোজা বদলান। সহজ, কিন্তু ইতিহাস হারায়: Rahim-এর সব পুরোনো বিক্রি এখন Chattogram-এর। নামের বানান ঠিক করার মতো ভুল-সংশোধনে এটিই ঠিক।
  • Type 2 — নতুন সারি: পুরোনো সারি বন্ধ করুন (valid_to, is_current = 0), নতুন surrogate key-সহ নতুন সারি যোগ করুন। পুরোনো fact পুরোনো সারির key ধরে থাকে, তাই ইতিহাস অটুট।
  • Type 3 — আগের মানের কলাম: city-র পাশে previous_city. শুধু একটি আগের মান রাখা যায়; “পুরোনো আর নতুন অঞ্চল-ভাগে পাশাপাশি তুলনা”-র মতো নির্দিষ্ট কাজে।

Type 2-ই সবচেয়ে প্রচলিত, আর এখানেই surrogate key অপরিহার্য: একই customer_id (business key) এখন দুটি সারিতে, তাই সারি চেনাতে আলাদা key লাগে। ধাপে ধাপে দেখুন:

dim_customer (শুধু Rahim, customer_id = 1)sk = surrogate key · cur = is_current

            
অবস্থা
Rahim-এর সারি
current সারি
Dhaka-র বিক্রি
  1. শুরুর অবস্থা। Rahim-এর একটি সারি, sk 1, যোগদানের দিন থেকে “চিরকাল” (9999-12-31) পর্যন্ত বৈধ। তাঁর সেপ্টেম্বরের দুটি অর্ডার fact-এ sk 1 ধরে আছে।
  2. বদল ধরা। রাতের ETL উৎস থেকে customers-এর কপি staging-এ আনে আর current সারির সাথে মেলায়। city বদলেছে, আর নকশায় city একটি type 2 কলাম — তাই নতুন version লাগবে।
  3. পুরোনো সারি বন্ধ। valid_to হলো বদলের আগের দিন, is_current 0. লক্ষ করুন, এই মুহূর্তে Rahim-এর কোনো current সারি নেই — তাই এই UPDATE আর পরের INSERT একটি transaction-এ হওয়া জরুরি (পর্ব ৪৪)।
  4. নতুন সারি। নতুন surrogate key 9, একই business key 1, শহর Chattogram, বৈধ ২০২৫-১০-০৫ থেকে। এখন Rahim-এর দুটি সারি, যাদের সময়-পরিসর একটুও ওভারল্যাপ করে না।
  5. Fact লোডে key খোঁজা। নতুন অর্ডার 1009-এর তারিখ কোন সারির পরিসরে পড়ে? sk 9-এর। এভাবে খোঁজাকে বলে point-in-time join. পুরোনো fact সারি বদলায় না — warehouse-এর non-volatile নীতি।
  6. ফল। type 2-এ ঢাকায় ১৯,৭৩০ (Rahim-এর সেপ্টেম্বরের ১৯,১৮০ + Mitu-র ৫৫০), Chattogram-এ ২৩,০০০ (Karima ৫,০০০ + Rahim-এর অক্টোবরের ১৮,০০০)। type 1 হলে ঢাকায় মাত্র ৫৫০ — ইতিহাস চুপচাপ বদলে যেত।

এখন নিজে চালান। এই এডিটরে dim_customer আগে থেকেই type 2 আকারে বানানো: customer_sk (INTEGER PRIMARY KEY, তাই SQLite নিজে বাড়ায়), customer_id, name, city, valid_from (যোগদানের দিন), valid_to ('9999-12-31') আর is_current (1), ৮ জনের ৮টি সারি:

-- 2025-10-05: Rahim (customer_id 1) Dhaka থেকে Chattogram-এ গেলেন
BEGIN;
UPDATE dim_customer SET valid_to = '2025-10-04', is_current = 0
WHERE customer_id = 1 AND is_current = 1;
INSERT INTO dim_customer (customer_id, name, city, valid_from, valid_to, is_current)
VALUES (1, 'Rahim Uddin', 'Chattogram', '2025-10-05', '9999-12-31', 1);
COMMIT;
SELECT customer_sk, customer_id, city, valid_from, valid_to, is_current
FROM dim_customer WHERE customer_id = 1;
SELECT COUNT(*) AS rows_total, SUM(is_current) AS current_rows FROM dim_customer;

UPDATE আর INSERT দুটিই “১টি সারি বদলেছে”; তারপর Rahim-এর দুটি সারি: 1 | 1 | Dhaka | 2025-01-15 | 2025-10-04 | 0 আর 9 | 1 | Chattogram | 2025-10-05 | 9999-12-31 | 1. শেষ ফল 9 | 8 — টেবিলে ৯টি সারি, কিন্তু current মাত্র ৮টি, প্রতি গ্রাহকে একটি। এই “প্রতি business key-তে ঠিক একটি current সারি” নিয়মটি একটি quality check হিসেবে প্রতি লোডের পর যাচাই করা উচিত। '9999-12-31'-এর বদলে NULL রাখাও প্রচলিত, কিন্তু তখন BETWEEN-এর কোয়েরিতে আলাদা করে valid_to IS NULL লিখতে হয়।

এবার fact লোড করার সময় point-in-time join দিয়ে প্রতিটি লাইনের সঠিক customer_sk খোঁজা, আর শহর অনুযায়ী বিক্রি। এই এডিটরে উপরের বদলটি আগেই হয়ে আছে:

CREATE TABLE fact_sales AS
SELECT i.order_id, o.order_date, d.customer_sk,
       i.quantity * i.unit_price AS amount
FROM orders o
JOIN order_items i  ON i.order_id = o.order_id
JOIN dim_customer d ON d.customer_id = o.customer_id
                   AND o.order_date BETWEEN d.valid_from AND d.valid_to
WHERE o.status <> 'cancelled';
SELECT d.city, SUM(f.amount) AS revenue
FROM fact_sales f JOIN dim_customer d ON d.customer_sk = f.customer_sk
GROUP BY d.city ORDER BY revenue DESC;

ফল: Chattogram | 23000, Dhaka | 19730, Rajshahi | 5050, Sylhet | 3500, Unknown | 1040 — stepper-এর শেষ ধাপের type 2 কলাম। রিপোর্টের কোয়েরিতে কোনো তারিখের শর্তই নেই: সময়ের হিসাব লোডের সময় একবার হয়ে গেছে, fact-এ সঠিক key বসে আছে। একটি পরীক্ষা করুন: JOIN থেকে AND o.order_date BETWEEN … লাইনটি মুছে চালান। তখন Rahim-এর প্রতিটি লাইন তাঁর দুটি dimension সারির সাথেই মেলে — fact-এ লাইন দ্বিগুণ, Rahim-এর বিক্রি দুই শহরেই পুরোটা। type 2 dimension-এ business key দিয়ে JOIN করার এটিই ক্লাসিক ভুল।

৮ · ETL, ELT আর incremental load — ডেটা warehouse-এ আসে কীভাবে

পর্ব ৩৮-এ দেখেছেন, যেকোনো কপির দুটি প্রশ্ন: কীভাবে হালনাগাদ থাকবে, আর ভুল হলে কীভাবে ধরব। warehouse পুরোটাই একটি বিশাল কপি, তাই উত্তরটি একটি নিয়মিত pipeline: ETL — Extract (উৎস থেকে তোলা), Transform (পরিষ্কার, মেলানো, key খোঁজা), Load (warehouse-এ বসানো)। পুরোনো ধারায় রূপান্তর হতো আলাদা একটি ETL সার্ভারে। cloud warehouse আসার পর প্রচলিত হয়েছে ELT: কাঁচা ডেটা আগে warehouse-এর একটি স্তরে বসে, তারপর রূপান্তর হয় warehouse-এর ভেতরেই, SQL দিয়ে — কারণ সেখানে গণনার শক্তি সহজে বাড়ানো যায়, আর কাঁচা কপি থাকলে পরে নিয়ম বদলে আবার চালানো যায়।

একটি সাধারণ ELT pipeline-এর স্তর

  1. উৎসOLTP, CSV, APIচালু সিস্টেম
  2. Stagingstg_ordersকাঁচা কপি, যাচাই
  3. Transformkey, নিয়ম, SCDSQL বা টুল
  4. Star schemafact + dimBI আর রিপোর্ট

প্রতিদিন সব ডেটা নতুন করে আনা (full load) ছোট টেবিলে চলে, বড় টেবিলে নয়। Incremental load শুধু নতুন বা বদলানো সারি আনে। সবচেয়ে সরল উপায় watermark: শেষবার কোন সময় পর্যন্ত আনা হয়েছে, একটি ছোট টেবিলে লিখে রাখা। এই এডিটরে fact_sales-এ ৩০ সেপ্টেম্বর পর্যন্ত লোড হয়ে আছে, আর etl_watermark টেবিলে (source, last_loaded) = ('orders', '2025-09-30'):

SELECT COUNT(*) AS lines_before, SUM(amount) AS revenue_before FROM fact_sales;
INSERT INTO fact_sales
SELECT i.order_id, CAST(strftime('%Y%m%d', o.order_date) AS INTEGER),
       o.customer_id, i.product_id, i.quantity, i.quantity * i.unit_price
FROM orders o JOIN order_items i ON i.order_id = o.order_id
WHERE o.status <> 'cancelled'
  AND o.order_date > (SELECT last_loaded FROM etl_watermark WHERE source = 'orders');
UPDATE etl_watermark SET last_loaded = (SELECT MAX(order_date) FROM orders)
WHERE source = 'orders';
SELECT COUNT(*) AS lines_after, SUM(amount) AS revenue_after,
       (SELECT last_loaded FROM etl_watermark) AS watermark
FROM fact_sales;

আগে 9 | 29480 (সেপ্টেম্বরের ৯টি লাইন); INSERT আনে অক্টোবরের লাইনগুলো — “৬টি সারি বদলেছে”; watermark-এর UPDATE “১টি সারি বদলেছে”; শেষে 15 | 52320 | 2025-10-09 — পূর্ণ লোডের সমান। কিন্তু এই সরল নকশার চারটি ফাঁক আছে, বাস্তবে সবকটিই ঘটে: (১) পুরোনো অর্ডারের status পরে বদলালে (pending থেকে cancelled) order_date-এর watermark তা কখনো দেখে না — তাই watermark রাখা হয় একটি updated_at কলামে, order_date-এ নয়; (২) উৎসে মোছা সারি কোনো timestamp-এ ধরা পড়ে না; (৩) লোডের মুহূর্তে কোনো লেনদেন তখনো commit না হলে, পরে commit হয়েও তার সারির সময় watermark-এর আগে পড়ে যেতে পারে, আর সেটি আর কখনো আসে না — তাই অনেকে একটু পিছিয়ে আবার আনেন, আর লোডকে idempotent রাখেন (একই সারি দুবার এলে upsert বা MERGE, দ্বিগুণ নয়); (৪) এডিটরটি দুবার চালালে কিছু হয় না, কিন্তু watermark-এর UPDATE বাদ দিয়ে দুবার চালালে লাইন দ্বিগুণ হতো। এসব ফাঁক বন্ধের শক্ত উপায় change data capture (CDC): ডেটাবেসের নিজের log (WAL বা binlog, পর্ব ৪৮, ৫২) পড়ে প্রতিটি INSERT, UPDATE আর DELETE commit-এর ক্রমে পাওয়া; Debezium-এর মতো টুল এ কাজে পরিচিত।

লোড শেষ হলেই কাজ শেষ নয়। প্রতিটি লোডের পর স্বয়ংক্রিয় data quality check চলে, আর কোনোটি ব্যর্থ হলে রিপোর্ট আটকে সতর্কবার্তা যায়। এই এডিটরে একটি দুর্ঘটনা সাজানো আছে: অর্ডার 1010-এর লাইনটি ভুল করে দুবার লোড হয়েছে (যেন একটি batch দুবার চলেছে):

SELECT 'grain-এ duplicate' AS check_name, COUNT(*) AS problems
FROM (SELECT order_id, product_key FROM fact_sales
      GROUP BY order_id, product_key HAVING COUNT(*) > 1)
UNION ALL
SELECT 'অচেনা product_key', COUNT(*)
FROM fact_sales f LEFT JOIN dim_product p ON p.product_key = f.product_key
WHERE p.product_key IS NULL
UNION ALL
SELECT 'amount শূন্য বা ঋণাত্মক', COUNT(*) FROM fact_sales WHERE amount <= 0
UNION ALL
SELECT 'উৎসের সাথে টাকার পার্থক্য',
       (SELECT SUM(amount) FROM fact_sales)
     - (SELECT SUM(i.quantity * i.unit_price) FROM order_items i
        JOIN orders o ON o.order_id = i.order_id WHERE o.status <> 'cancelled');

চারটি সারি: grain-এ duplicate | 1, অচেনা product_key | 0, amount শূন্য বা ঋণাত্মক | 0, উৎসের সাথে টাকার পার্থক্য | 1040. দুটি পরীক্ষা একই দুর্ঘটনা ধরেছে: grain-এর নিয়ম (একটি অর্ডার আর একটি পণ্যে একটিই লাইন) ভেঙেছে, আর warehouse-এর মোট উৎসের চেয়ে ঠিক ১,০৪০ টাকা বেশি — 1010-এর লাইনের দাম। grain ঘোষণা তাই শুধু নকশার কথা নয়, একটি পরীক্ষাযোগ্য নিয়ম। দ্বিতীয় পরীক্ষা reconciliation-এর কাজ করে; LEFT JOIN আর IS NULL দিয়ে অনাথ key খোঁজা পর্ব ২০-এর anti join. বাস্তবে আরও থাকে: কোনো লোডে সারি-সংখ্যা হঠাৎ অর্ধেক হলো কি না, শেষ লোড কত ঘণ্টা আগে (freshness), প্রয়োজনীয় কলামে NULL আছে কি না।

৯ · Column store, MPP, cloud warehouse আর lakehouse

বিশ্লেষণের কোয়েরি সাধারণত চওড়া টেবিলের অল্প কয়েকটি কলাম পড়ে, কিন্তু প্রায় সব সারি। পর্ব ৩৯-এর row store-এ একটি page-এ পুরো সারিগুলো পাশাপাশি থাকে, তাই দুটি কলাম পড়তেও পুরো সারি ডিস্ক থেকে আনতে হয়। ধরুন fact-এর প্রতিটি সারি ২০০ বাইট, আর কোয়েরি লাগে ৮ বাইটের দুটি কলাম: row store পড়ে ২০০ বাইট, column store পড়ে ১৬ বাইট — compression-এর আগেই কাজের মাত্র ৮ শতাংশ। column store-এ প্রতিটি কলাম আলাদা ধারায় থাকে, আর একই ধরনের মান পাশাপাশি থাকায় compression খুব ভালো কাজ করে:

  • Dictionary encoding: category কলামে মাত্র ৪টি আলাদা মান; প্রতিটি সারিতে পুরো লেখার বদলে একটি ছোট সংখ্যা (৪টি মানে ২ বিটই যথেষ্ট), সাথে একটি অভিধান।
  • Run-length encoding: date_key ধরে সাজানো fact-এ একই তারিখ হাজারো বার পরপর — রাখা হয় শুধু (মান, কতবার)।
  • Min/max সারাংশ: প্রতিটি খণ্ডের সর্বনিম্ন আর সর্বোচ্চ মান রাখা থাকে; “অক্টোবরের বিক্রি” কোয়েরি যে খণ্ডের পরিসরে অক্টোবর নেই, তা পড়েই না।
  • Vectorized execution: একটি একটি সারি না ধরে হাজারখানেক মানের ব্যাচে কাজ — CPU-র cache আর SIMD নির্দেশ ভালোভাবে কাজে লাগে।

দাম আছে: একটি সারি ঢোকাতে বা বদলাতে বহু কলাম-ধারায় লিখতে হয়, তাই column store একটি একটি সারির OLTP কাজে দুর্বল, বড় ব্যাচের লোডে ভালো। এজন্যই চালু সিস্টেম row store, warehouse column store.

ডেটা এক যন্ত্রে না ধরলে MPP: টেবিলের সারি অনেক সার্ভারে ভাগ করা (পর্ব ৫৪-এর partitioning), প্রতিটি সার্ভার নিজের অংশে GROUP BY চালিয়ে আংশিক যোগফল বানায়, তারপর আংশিক ফলগুলো জোড়া লাগে। এখানে measure-এর জাত আবার কাজে আসে: SUM-এর SUM সঠিক; AVG-এর জন্য প্রতিটি অংশ (sum, count) পাঠায়, গড়ের গড় নয়; COUNT(DISTINCT) সবচেয়ে কঠিন — নির্ভুল উত্তরে মানগুলো এক জায়গায় আনতে হয়, তাই অনেক সিস্টেমে আনুমানিক ফাংশন (যেমন HyperLogLog-ভিত্তিক) থাকে।

নামকী ধরনেরসাধারণ পরিচয় (সংস্করণভেদে বদলায়)
Snowflakecloud warehouse (সেবা)storage আর গণনা আলাদা, গণনার যন্ত্র আলাদাভাবে বাড়ানো-কমানো যায়
Google BigQuerycloud warehouse (সেবা)serverless; সাধারণত পড়া ডেটার পরিমাণ বা বরাদ্দ ক্ষমতা ধরে খরচ
Amazon Redshiftcloud warehouse (সেবা)MPP cluster হিসেবে শুরু, এখন serverless বিকল্পও আছে
ClickHouseopen source column storeদ্রুত aggregation-এর জন্য পরিচিত; নিজে চালানো বা সেবা হিসেবে
DuckDBin-process বিশ্লেষণী ডেটাবেসSQLite-এর মতো অ্যাপের ভেতরে চলে, কিন্তু column-ভিত্তিক; ল্যাপটপে Parquet বা CSV-তে সরাসরি SQL
Parquetফাইল ফরম্যাটকলাম ধরে সাজানো, compression-সহ খোলা ফরম্যাট
Data lakeস্থাপত্যসস্তা object storage-এ কাঁচা ফাইল; কাঠামো ঠিক হয় পড়ার সময়
Iceberg, Delta Lakeopen table formatParquet ফাইলের সংগ্রহকে snapshot আর atomic লেখাসহ একটি টেবিল বানায়
Lakehouseস্থাপত্যlake-এর ফাইলের ওপর warehouse-এর মতো SQL আর transaction

কোনটি “সেরা”, তার সর্বজনীন উত্তর নেই — খরচের মডেল, দলের দক্ষতা আর ডেটার আকার অনুযায়ী বদলায়, আর এই বাজার দ্রুত বদলাচ্ছে। শেষ ধাপে মানুষ ডেটা দেখেন BI টুল দিয়ে — Microsoft Power BI, বা open source Metabase আর Apache Superset — যারা warehouse-এ SQL পাঠিয়ে চার্ট আর dashboard বানায়। আর ELT-এর রূপান্তর-ধাপ আজকাল প্রায়ই লেখা হয় dbt-এ: প্রতিটি ধাপ একটি SELECT ফাইল, টুলটি নির্ভরতার ক্রমে চালায়, আর “এই কলাম unique”, “এই key dim-এ আছে” — অংশ ৮-এর পরীক্ষাগুলো ঘোষণা করে লেখা যায়। এই কাজের নতুন নাম analytics engineering: সফটওয়্যারের মতো version control, code review আর test — কিন্তু SQL-এ।

আরও গভীরে: column store-এর আধুনিক রূপের পেছনে গবেষণার একটি ধারা আছে — নেদারল্যান্ডসের CWI-এর MonetDB, আর ২০০৫ সালের C-Store প্রবন্ধ (Stonebraker ও সহলেখক), যা থেকে বাণিজ্যিক Vertica আসে। একটি সূক্ষ্ম কৌশল হলো compressed ডেটার ওপর সরাসরি কাজ: RLE-তে (মান 20251008, 5000 বার) থাকলে SUM-এর জন্য ৫০০০টি সারি না খুলে একবার গুণ করলেই হয়। আর একই টেবিলের row আর column দুই রূপ একসাথে রেখে OLTP আর OLAP একই সিস্টেমে চালানোর চেষ্টার নাম HTAP — SQL Server-এর columnstore index বা Oracle-এর In-Memory column store এই দিকের উদাহরণ।

১০ · AI-এর যুগে warehouse আর বিশ্লেষণের SQL

“গত মাসের শহরভিত্তিক বিক্রি দাও” — AI সেকেন্ডে কোয়েরি লেখে, আর সেটি চলেও। কিন্তু বিশ্লেষণের ভুল ত্রুটি-বার্তা দেয় না, শুধু একটি ভুল সংখ্যা দেয়, যা একটি সভায় সিদ্ধান্ত হয়ে যায়। সাধারণ ভুলগুলো:

AI-এর ভুলফলকীভাবে ধরবেন
grain না জেনে order-স্তরের সংখ্যা line-স্তরে JOIN করে যোগfan-out-এ গুণিত টাকাJOIN-এর আগে আর পরে সারি-সংখ্যা আর মোট মেলান
type 2 dimension-এ business key দিয়ে JOINপ্রতিটি version-এ fact দ্বিগুণsurrogate key বা is_current দেখুন; মোট উৎসের সাথে মেলান
শতাংশ বা গড়ের গড়, COUNT(DISTINCT)-এর যোগnon-additive মাপ ভুললব আর হর আলাদা যোগ করে শেষে ভাগ
ব্যালান্স বা স্টক সময় ধরে SUMঅর্থহীন বিশাল সংখ্যাsemi-additive: শেষ দিনের মান বা গড়
অনুমিত কলাম বা টেবিলের নাম (revenue, sales_date)চলে না, বা ভুল কলামschema আর কলামের অর্থ নিজে দিন
বাতিল অর্ডার, NULL শহর, শূন্য-বিক্রির মাস ভুলে যাওয়াবেশি বা বাদ পড়া সারিপ্রান্তিক ক্ষেত্রসহ নমুনায় হাতে মিলিয়ে দেখুন

AI-কে schema, grain আর ব্যবসার নিয়ম দিন, আর যাচাইয়ের কোয়েরিও চান:

আমার warehouse PostgreSQL 16-এ, star schema: fact_sales(order_id, date_key, customer_sk, product_key, quantity, amount) — grain: একটি অর্ডারের একটি লাইন, বাতিল অর্ডার নেই। dim_customer(customer_sk, customer_id, name, city, valid_from, valid_to, is_current) — SCD type 2. dim_date(date_key, full_date, month, is_weekend), dim_product(product_key, product_name, category). প্রশ্ন: ২০২৫-এর প্রতিটি মাসে, বিক্রির সময়ের শহর অনুযায়ী মোট বিক্রি আর আলাদা ক্রেতার সংখ্যা, মাসের উপ-মোটসহ। ১. কোয়েরি দাও; ROLLUP আর GROUPING() ব্যবহার করো। ২. কেন customer_sk দিয়ে JOIN করছ, customer_id দিয়ে নয় — এক লাইনে বলো। ৩. উপ-মোটে আলাদা ক্রেতা কেন শহরগুলোর যোগফল নয়, ব্যাখ্যা করো। ৪. ফল যাচাইয়ের জন্য একটি কোয়েরি দাও, যা মোট টাকা fact_sales-এর সরাসরি SUM-এর সাথে মেলায়। ৫. কোনো কলামের নাম অনুমান করবে না — না জানলে জিজ্ঞেস করো।

AI-এর লেখা বিশ্লেষণের কোয়েরি রিপোর্টে বসানোর আগে — চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • OLTP আর OLAP-এর পার্থক্য? — ছোট, ঘন ঘন লেনদেন বনাম বড় বিশ্লেষণী পড়া; normalized বনাম dimensional; row store বনাম column store; বর্তমান বনাম ইতিহাস।
  • Star আর snowflake schema-র পার্থক্য? — star-এ dimension denormalized, fact থেকে এক ধাপ; snowflake-এ dimension normalized, JOIN বেশি, redundancy কম।
  • Grain কী, কেন আগে ঠিক করতে হয়? — fact-এর এক সারির অর্থ; এটি ঠিক করে কোন dimension বসবে আর কোন যোগফল সঠিক।
  • SCD type 1, 2, 3? — মুছে লেখা; valid_from/valid_to আর surrogate key-সহ নতুন সারি; আগের মানের একটি কলাম।
  • ETL আর ELT-এর পার্থক্য? — রূপান্তর লোডের আগে আলাদা যন্ত্রে, বনাম কাঁচা ডেটা লোডের পর warehouse-এর ভেতরে SQL-এ।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Inmon আর Kimball-এর বিতর্ক ১৯৯০-এর দশক জুড়ে চলেছে। আজ কি একটি পদ্ধতি “জিতেছে”, নাকি দুটিই অন্য রূপে টিকে আছে?

বিতর্কের মূল প্রশ্নটি ছিল সংহতি কোথায় হবে। Inmon বলেছিলেন, আগে পুরো প্রতিষ্ঠানের একটি normalized, সংহত মডেল — গ্রাহক, পণ্য, হিসাব — সবার জন্য একটি সত্য; তারপর তা থেকে বিভাগের data mart. Kimball বলেছিলেন, ব্যবসার একটি প্রক্রিয়া ধরে দ্রুত একটি star schema দিন, আর সংহতি আনুন conformed dimension দিয়ে: বিক্রি আর স্টক দুই mart যদি একই dim_product আর dim_date ব্যবহার করে, তবে তারা স্বাভাবিকভাবেই একে অন্যের সাথে মেলে। Kimball এর জন্য একটি “bus matrix” দিয়েছিলেন — সারিতে প্রক্রিয়া, কলামে dimension.

বাস্তবে কেউ পুরোপুরি জেতেনি। Kimball-এর ভাষা বিশ্লেষণের জগতে প্রায় সর্বজনীন হয়েছে: fact, dimension, grain, SCD — BI টুল, dbt-এর মতো টুলের দলিল আর চাকরির ইন্টারভিউ সবখানে এই শব্দ। কিন্তু Inmon-এর মূল ধারণা — কাঁচা, সংহত, বিষয়ভিত্তিক একটি কেন্দ্রীয় স্তর — নতুন নামে ফিরে এসেছে। আধুনিক ELT-এ প্রায়ই তিনটি স্তর দেখা যায়: কাঁচা কপি, পরিষ্কার ও সংহত মডেল, আর বিশ্লেষকের জন্য star schema বা চওড়া টেবিল। lakehouse-এর জগতে একে কেউ কেউ bronze, silver, gold বলেন। Data Vault (Dan Linstedt) আরেকটি পথ দেয়, যেখানে কেন্দ্রীয় স্তরে hub, link আর satellite — উৎস বদলালেও ইতিহাস অটুট রাখতে।

আর ক্ষেত্রটিও বদলেছে। ১৯৯০-এর দশকে storage আর গণনা দামি ছিল, তাই নকশার প্রতিটি ভুল শোধরানো কঠিন ছিল। আজ cloud-এ কাঁচা ডেটা রেখে নিয়ম বদলে আবার চালানো তুলনামূলক সস্তা, তাই “আগে সব মডেল ঠিক করো” পদ্ধতির ঝুঁকি কমেছে, আবার “দ্রুত mart বানাও” পদ্ধতির বিশৃঙ্খলাও সহজে বাড়ে।

মূল উপলব্ধি: বিতর্কটি দুটি সত্যকে সামনে এনেছিল — সংহতি লাগে, আর ব্যবহারকারীর কাছে সহজ মডেল লাগে। আধুনিক নকশা দুটিকেই আলাদা স্তরে রাখে।

প্র ০২Relational ডেটাবেসের প্রথম কয়েক দশক প্রায় সবই row store ছিল। তাহলে ২০০০-এর দশকের পর থেকে বিশ্লেষণের প্রায় সব নতুন ইঞ্জিন column store হলো কেন? কলাম ধরে রাখার ধারণা কি নতুন?

ধারণাটি নতুন নয়। ১৯৭০-এর দশকেই কলাম ধরে ফাইল সাজানোর গবেষণা ছিল, আর ১৯৯০-এর দশকে Sybase IQ-এর মতো বাণিজ্যিক column-ভিত্তিক পণ্য ছিল। নেদারল্যান্ডসের CWI-এ MonetDB-র কাজ ১৯৯০-এর দশকেই শুরু হয়। কিন্তু তখন বাজারের প্রধান কাজ ছিল OLTP, আর বড় বিক্রেতাদের ইঞ্জিন সেই কাজের জন্য row store-এ গড়া ছিল; বিশ্লেষণও সেই ইঞ্জিনেই চলত।

পরিবর্তন এসেছে কয়েকটি শক্তি একসাথে মিলে। এক, ডেটার আকার: ওয়েবের click, লেনদেন আর log-এ বিশ্লেষণের টেবিল শতকোটি সারিতে পৌঁছাল, যেখানে প্রতিটি কোয়েরি অল্প কলামে পুরো scan. তখন ২০০ বাইটের সারি থেকে ১৬ বাইট পড়ার পার্থক্য ঘণ্টা আর মিনিটের পার্থক্য। দুই, হার্ডওয়্যার: CPU-র গতি মেমোরি আর ডিস্কের চেয়ে অনেক দ্রুত বেড়েছে, তাই compressed ডেটা ডিস্ক থেকে কম পড়ে CPU-তে খোলা লাভজনক হলো — আর column-এ একই ধরনের মান পাশাপাশি থাকায় compression অনেক ভালো। তিন, গবেষণা: ২০০৫ সালের C-Store প্রবন্ধ দেখাল যে বিশ্লেষণের কাজে column store বড় ব্যবধানে এগিয়ে থাকতে পারে, আর সেখান থেকে Vertica আসে। MonetDB/X100-এর কাজ থেকে vectorized execution-এর ধারণা ছড়ায়।

এর পর প্রায় প্রতিটি বিশ্লেষণী ইঞ্জিন — cloud warehouse, ClickHouse, DuckDB — আর Parquet-এর মতো ফাইল ফরম্যাট কলাম-ভিত্তিক। প্রচলিত ডেটাবেসগুলোও column বিকল্প যোগ করেছে, যেমন SQL Server-এর columnstore index. তবে OLTP-তে row store-ই রয়ে গেছে, কারণ একটি অর্ডার ঢোকানো মানে একটি সারির সব কলাম একসাথে লেখা — column store-এ সেটি বহু জায়গায় লেখা।

মূল উপলব্ধি: storage-এর বিন্যাস কাজের ধরন মেনে চলে। কোয়েরি সারি ধরে কাজ করলে row, কলাম ধরে কাজ করলে column — আর হার্ডওয়্যারের অনুপাত বদলালে পুরোনো ধারণাও নতুন করে জেতে।

প্র ০৩SCD type 2 ইতিহাস রাখে, কিন্তু সমস্যা কি সব মেটায়? ধরুন একটি পুরোনো অর্ডারের তথ্য দেরিতে এল, বা জানা গেল গ্রাহক আসলে দুই সপ্তাহ আগেই শহর বদলেছিলেন। তখন কী হয়?

Type 2 একটি নির্দিষ্ট প্রশ্নের উত্তর দেয়: “ঘটনার মুহূর্তে dimension-এর মান কী ছিল?” কিন্তু সময়ের দুটি আলাদা অর্থ আছে। একটি হলো বাস্তবে কখন সত্য ছিল — Rahim কবে আসলে Chattogram-এ গেলেন (valid time). অন্যটি হলো ডেটাবেস কখন সেটি জানল — ETL কবে বদলটি দেখল (transaction বা system time). সাধারণ type 2-এ valid_from সাধারণত লোডের দিন, তাই দুটি অর্থ মিশে যায়।

দেরিতে আসা fact সহজ: অর্ডারের তারিখ ধরে point-in-time join করলে পুরোনো সারির key-ই পাওয়া যায়, শুধু লোডারকে “আজকের current সারি” না নিয়ে তারিখ মেলাতে হবে। দেরিতে আসা dimension-বদল কঠিন: যদি জানা যায় Rahim ২১ সেপ্টেম্বর থেকেই Chattogram-এ, তবে সারিগুলোর পরিসর বদলাতে হবে, আর ২১ সেপ্টেম্বরের পরের যেসব fact-এ sk 1 বসেছে, সেগুলোকে sk 9-এ সরাতে হবে। তার মানে warehouse-এর “non-volatile” নীতি ভেঙে ইতিহাস নতুন করে লেখা — আর গত মাসের প্রকাশিত রিপোর্টের সংখ্যা বদলে যাবে।

এ জন্য SQL:2011 standard-এ দুই ধরনের সময় আলাদা রাখার ধারণা (application-time আর system-versioned টেবিল) এসেছে, যাকে একসাথে বলে bitemporal; SQL Server-এর temporal table আর MariaDB-র system-versioned টেবিল এর কিছু অংশ দেয়। তখন প্রশ্ন করা যায়, “১ অক্টোবরে আমরা যা জানতাম, সেই অনুযায়ী সেপ্টেম্বরের ঢাকার বিক্রি কত ছিল?” — নিরীক্ষা আর আইনি হিসাবে এটি জরুরি।

আরেকটি টান: ব্যক্তিগত তথ্য। type 2 গ্রাহকের প্রতিটি পুরোনো ঠিকানা চিরকাল রাখে, অথচ গোপনীয়তার নিয়ম প্রায়ই দরকার শেষ হলে মুছে ফেলতে বলে। তাই অনেক নকশায় ব্যক্তিগত কলাম type 1 রাখা হয়, বা আলাদা টেবিলে সরিয়ে মুছে ফেলার ব্যবস্থা থাকে।

মূল উপলব্ধি: type 2 “তখন কী সত্য ছিল” রাখে, “আমরা কখন জানলাম” নয়। দুটিই দরকার হলে bitemporal নকশা লাগে — আর ইতিহাস রাখার সিদ্ধান্তে গোপনীয়তার দায়ও আসে।

প্র ০৪ETL থেকে ELT-এ সরে যাওয়াকে অনেকে প্রযুক্তির স্বাভাবিক অগ্রগতি বলেন। এই পরিবর্তন কেন এল, আর এর লুকানো ঝুঁকি কী?

পুরোনো warehouse-এ গণনার ক্ষমতা ছিল সীমিত আর দামি — একটি বড় যন্ত্র, বছরের পর বছরের লাইসেন্স। তাই রূপান্তরের ভারী কাজ warehouse-এর বাইরে, আলাদা ETL সার্ভারে করা হতো, আর warehouse-এ আসত শুধু পরিষ্কার, চূড়ান্ত ডেটা। কাঁচা ডেটা প্রায়ই রাখা হতো না; নিয়মে ভুল ধরা পড়লে উৎস থেকে আবার তুলতে হতো, যা অনেক সময় আর সম্ভবই ছিল না।

cloud warehouse এই হিসাব উল্টে দিল। storage সস্তা হলো, আর গণনা প্রয়োজনমতো বাড়ানো-কমানো যায়, storage থেকে আলাদা। তখন সহজ পথ হলো কাঁচা ডেটা যেমন আছে তেমন বসানো, আর রূপান্তর warehouse-এর ভেতরে SQL-এ — যে ভাষা বিশ্লেষকেরা আগে থেকেই জানেন। dbt-এর মতো টুল (২০১০-এর দশকের মাঝামাঝি থেকে) এই SQL-কে version control, test আর ডকুমেন্টেশনসহ সাজাতে শেখাল। লাভ স্পষ্ট: নিয়ম বদলালে কাঁচা স্তর থেকে আবার চালানো যায়, আর কোন সংখ্যা কোথা থেকে এল, SQL পড়েই বোঝা যায়।

ঝুঁকিও আছে। এক, খরচ: “চালাতে সস্তা” মানে বিনামূল্যে নয়; শত শত মডেল প্রতি ঘণ্টায় পুরো টেবিল নতুন করে বানালে মাসের বিল চমকে দিতে পারে। incremental মডেল আর partition এখানে জরুরি। দুই, নিয়ন্ত্রণ: কাঁচা ডেটা warehouse-এ বসলে সবাই তা ছুঁতে পারেন — ফোন নম্বর আর NID-এর মতো ব্যক্তিগত তথ্য না ঢেকে বসানো বড় নিরাপত্তা-ঝুঁকি (পর্ব ৫১)। তিন, বিশৃঙ্খলা: সহজে মডেল বানানো যায় বলে একই “মাসিক বিক্রি”-র পাঁচটি ভিন্ন সংজ্ঞা জন্মায়। conformed dimension আর লিখিত metric সংজ্ঞার শৃঙ্খলা তাই আগের চেয়েও জরুরি।

মূল উপলব্ধি: ELT রূপান্তরের জায়গা বদলেছে, দায়িত্ব নয়। কাঁচা ডেটা রাখার স্বাধীনতার সাথে আসে খরচ, গোপনীয়তা আর সংজ্ঞার শৃঙ্খলার দায়।

প্র ০৫ঢাকার একটি মাঝারি অনলাইন দোকান — কয়েক লাখ অর্ডার, তিনজনের প্রযুক্তি দল। মালিক বললেন, “আমাদের একটি cloud data warehouse লাগবে।” আপনি কী পরামর্শ দেবেন?

প্রথম প্রশ্ন প্রযুক্তি নয়, প্রয়োজন: কোন প্রশ্নের উত্তর এখন পাওয়া যাচ্ছে না, আর কতটা তাজা উত্তর লাগে? কয়েক লাখ অর্ডার আধুনিক মাপে ছোট ডেটা। একটি PostgreSQL বা MySQL-এর read replica (পর্ব ৫২), কয়েকটি summary টেবিল বা materialized view (পর্ব ২৪), আর Metabase-এর মতো একটি open source BI টুল দিয়েই অনেক দূর যাওয়া যায় — খরচ প্রায় শুধু একটি বাড়তি সার্ভার। রিপোর্টের ভারী কোয়েরি replica-তে চললে চালু সিস্টেম নিরাপদ।

আলাদা warehouse-এর দিকে যাওয়ার সংকেতগুলো স্পষ্ট: ডেটা আসছে বহু উৎস থেকে (দোকান, পেমেন্ট গেটওয়ে, কুরিয়ার, ফেসবুক বিজ্ঞাপন) আর হাতে Excel-এ মেলানো হচ্ছে; ইতিহাস লাগছে, যা চালু সিস্টেম রাখে না; রিপোর্টের কোয়েরি replica-তেও মিনিট নিচ্ছে; বা একাধিক দল একই সংখ্যার ভিন্ন সংজ্ঞা দিচ্ছে। তখনও শুরু করা যায় ছোট থেকে: একটি আলাদা PostgreSQL-এ star schema, রাতে একটি ETL স্ক্রিপ্ট, বা ল্যাপটপে DuckDB দিয়ে Parquet ফাইলে বিশ্লেষণ।

cloud warehouse তখন যুক্তিযুক্ত, যখন ডেটা সত্যিই বড় বা দ্রুত বাড়ছে, আর দলের হাতে খরচ নজরে রাখার সময় আছে। ভুলে যাবেন না: বাংলাদেশ থেকে বৈদেশিক মুদ্রায় মাসিক বিল, ডেটা দেশের বাইরে রাখার নীতিগত প্রশ্ন, আর ব্যবহারভিত্তিক বিলে একটি ভুল কোয়েরির খরচ — এগুলো স্থানীয়ভাবে ভাবতে হয়। প্রযুক্তির চেয়েও বেশি জরুরি তিনটি অভ্যাস, যা যেকোনো যন্ত্রে লাগে: grain ঘোষণা, লিখিত metric সংজ্ঞা, আর প্রতি লোডের পর quality check.

মূল উপলব্ধি: warehouse একটি সমস্যার সমাধান, মর্যাদার প্রতীক নয়। ছোট থেকে শুরু করুন, dimensional নকশার শৃঙ্খলা আগে আনুন — যন্ত্র পরে বদলানো যায়।

অনুশীলন

একটি কোয়েরি লেখা, একটি হাতে ফল বের করা, একটি শ্রেণিবিভাগ, একটি নকশা।

  1. কোয়েরি লিখুন: অংশ ৫-এর star schema-তে (fact_sales, dim_date-এ is_weekend) সাপ্তাহিক ছুটির দিন (শুক্র-শনি) আর কাজের দিনের তুলনা করুন: প্রতিটির জন্য অর্ডারের সংখ্যা আর মোট বিক্রি। (অংশ ৫-এর যেকোনো এডিটরে লিখে চালাতে পারেন।)
    SELECT CASE d.is_weekend WHEN 1 THEN 'weekend' ELSE 'weekday' END AS day_type,
           COUNT(DISTINCT f.order_id) AS orders, SUM(f.amount) AS revenue
    FROM fact_sales f JOIN dim_date d ON d.date_key = f.date_key
    GROUP BY d.is_weekend;

    ফল: weekday | 8 | 48820 আর weekend | 1 | 3500 — শুধু ২০ সেপ্টেম্বর (শনিবার) Farhana-র Smart Watch. অর্ডার গুনতে COUNT(DISTINCT f.order_id) লাগে, কারণ fact-এর grain লাইন; COUNT(*) লাইন গুনত (কাজের দিনে ১৪)। অর্ডার-সংখ্যা non-additive: দুই সারির ৮ আর ১ যোগ করে ৯ এখানে ঠিক, কারণ একটি অর্ডার একটিই দিনে — কিন্তু “আলাদা ক্রেতা” গুনলে যোগ ঠিক হতো না।

  2. ফল আগে বলুন (trace): Rahim নয়, এবার Karima Begum (customer_id 2, Chattogram, যোগদান 2025-02-03) ২০২৫-০৯-২০ তারিখে Sylhet-এ গেলেন, SCD type 2-এ। Karima-র অর্ডার: 1002 (2025-09-03, 1750 টাকা) আর 1007 (2025-10-01, 3250 টাকা)। Sylhet-এ আগে থেকে Farhana-র 3500. (ক) Karima-র dim_customer সারিগুলো লিখুন (নতুন sk 9 ধরে); (খ) point-in-time join দিয়ে শহর অনুযায়ী বিক্রিতে Chattogram আর Sylhet কত দেখাবে; (গ) type 1 হলে কত?

    (ক) 2 | Chattogram | 2025-02-03 | 2025-09-19 | 0 আর 9 | Sylhet | 2025-09-20 | 9999-12-31 | 1. (খ) 1002-এর তারিখ ০৯-০৩ প্রথম সারির পরিসরে → Chattogram 1750; 1007-এর ১০-০১ দ্বিতীয় সারিতে → Sylhet = 3500 + 3250 = 6750. (গ) type 1-এ Karima-র সব বিক্রি Sylhet-এ: Chattogram 0 (সারিই আসবে না), Sylhet 3500 + 5000 = 8500. লক্ষ করুন, (খ) আর (গ)-তে শহরগুলোর মধ্যে বিক্রির ভাগ বদলাল, কিন্তু দোকানের সর্বমোট ৫২,৩২০-ই থাকল — SCD শুধু বিক্রিকে কোন দলে রাখা হবে তা বদলায়, যোগফল নয়।

  3. শ্রেণিবিভাগ করুন: প্রতিটি measure additive, semi-additive না non-additive? সময় ধরে roll-up করতে কী নেবেন? (ক) বিক্রির পরিমাণ (পিস); (খ) গুদামে দিনশেষের স্টক; (গ) লাভের হার (%); (ঘ) একটি মোবাইল ওয়ালেটে দিনশেষের ব্যালান্স; (ঙ) দিনে আলাদা ক্রেতার সংখ্যা; (চ) ডেলিভারি চার্জ (টাকা)।
    • (ক) additive — যেকোনো অক্ষে SUM.
    • (খ) semi-additive — একই দিনে পণ্য বা গুদাম ধরে যোগ চলে; সময়ে শেষ দিনের মান বা গড়।
    • (গ) non-additive — লাভ আর বিক্রি আলাদা যোগ করে শেষে ভাগ; শতাংশের যোগ বা গড় নয়।
    • (ঘ) semi-additive — গ্রাহক ধরে যোগ চলে (একদিনে মোট জমা), সময়ে নয়।
    • (ঙ) non-additive — একই ক্রেতা বহু দিনে কিনতে পারেন; মাসের হিসাবে fact থেকে নতুন করে COUNT(DISTINCT).
    • (চ) additive — টাকার অঙ্ক, যেকোনো অক্ষে SUM; তবে এটি অর্ডারের স্তরের সংখ্যা, লাইন-grain-এর fact-এ বসালে প্রতিটি লাইনে কপি হয়ে যোগফল ফুলে যায় — আলাদা fact বা ভাগ করে বসাতে হয়।
  4. নকশা করুন: একটি রেলওয়ের অনলাইন টিকিট বুকিংয়ের জন্য star schema নকশা করুন। ব্যবসার প্রক্রিয়া, grain, অন্তত চারটি dimension আর তিনটি measure লিখুন। কোন dimension-এ SCD type 2 দরকার হতে পারে?

    প্রক্রিয়া: টিকিট বিক্রি। Grain: একটি বুকিংয়ের একটি আসন (একটি PNR-এ চারটি আসন = চারটি সারি) — সবচেয়ে সূক্ষ্ম, তাই আসন-শ্রেণি আর যাত্রীর ধরন ধরে ভাগ করা যায়। Dimension: dim_date দুটি ভূমিকায় (বুকিংয়ের দিন আর যাত্রার দিন — একই টেবিল দুবার JOIN), dim_train (নাম, ধরন), dim_route বা dim_station (উৎস, গন্তব্য, বিভাগ), dim_seat_class (শোভন, স্নিগ্ধা, এসি…), dim_channel (অ্যাপ, ওয়েব, কাউন্টার); PNR নম্বর degenerate dimension. Measure: ভাড়া (additive), সার্ভিস চার্জ (additive), আসনসংখ্যা = 1 (additive); “বুকিং থেকে যাত্রা পর্যন্ত দিন” গড় করা যায়, যোগ নয়। SCD type 2: dim_train (কোচ বা সময়সূচি বদল), dim_station-এর বিভাগ বা নাম — পুরোনো বিক্রি পুরোনো তথ্যের সাথে রাখতে। বাতিল টিকিট আলাদা fact (refund) বা ঋণাত্মক সারি হিসেবে রাখার সিদ্ধান্তও লিখে রাখুন। (রেলের আসল শ্রেণির নাম বা নিয়ম এখানে উদাহরণমাত্র।)

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি নতুন star schema বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ব্যবসার প্রক্রিয়া বাছাই করা (যেমন বিক্রি)
  2. grain ঘোষণা করা — fact-এর এক সারি কী
  3. dimension বাছাই করা (কখন, কে, কী, কোথায়)
  4. measure বাছাই করা (পরিমাণ, টাকা)
  5. ETL লিখে dimension আর fact লোড করা
  6. quality check — grain-এ duplicate আর উৎসের মোটের সাথে মিল

নিচের কোন কাজটি OLAP-এর বৈশিষ্ট্যপূর্ণ উদাহরণ?

  • একজন গ্রাহকের অর্ডারের অবস্থা order_id দিয়ে দেখা
  • নতুন অর্ডার ঢুকিয়ে স্টক কমানো
  • তিন বছরের বিক্রি শহর × শ্রেণি অনুযায়ী যোগ করে প্রবণতা দেখা
  • একজন গ্রাহকের ফোন নম্বর হালনাগাদ করা

একটি fact টেবিলে কিছু সারি একটি পুরো অর্ডারের মোট, আর কিছু সারি অর্ডারের একটি লাইন। এর মূল সমস্যা কী?

  • টেবিলে বেশি সারি থাকে, তাই কোয়েরি ধীর হয়
  • যোগফল ভুল হয়, কারণ একই বিক্রি দুই স্তরে দুবার গোনা যায় — grain মিশে গেছে
  • SQL এমন টেবিলে INSERT করতে দেয় না
  • dimension টেবিল বেশি লাগে

dim_customer-এ SCD type 2: একজন গ্রাহক শহর বদলানোয় তাঁর দুটি সারি (surrogate key 1 আর 9, একই customer_id 1). একটি রিপোর্ট fact_sales-কে ON d.customer_id = f.customer_id দিয়ে JOIN করে (fact-এ customer_id আর customer_sk দুটিই আছে)। ফল কী?

  • গ্রাহকের বিক্রি রিপোর্ট থেকে বাদ পড়ে
  • প্রতিটি বিক্রি স্বয়ংক্রিয়ভাবে বিক্রির সময়ের শহরে যায়
  • DBMS ত্রুটি দেয়, কারণ customer_id unique নয়
  • গ্রাহকের প্রতিটি বিক্রি দুবার আসে — দুই শহরেই পুরো টাকা

একটি মোবাইল ওয়ালেটের fact টেবিলে প্রতিদিন প্রতিটি অ্যাকাউন্টের দিনশেষের ব্যালান্সের একটি সারি। “অক্টোবরে একজন গ্রাহকের ব্যালান্স” রিপোর্টে কী নেওয়া উচিত?

  • মাসের শেষ দিনের ব্যালান্স, বা দরকার হলে দৈনিক ব্যালান্সের গড়
  • ৩১ দিনের ব্যালান্সের যোগফল
  • শুধু মাসের প্রথম দিনের ব্যালান্স
  • মাসে কতগুলো snapshot সারি আছে তার COUNT

মাস × শ্রেণি × শহরের একটি cube থেকে “শুধু Dhaka-র, মাস আর শ্রেণি অনুযায়ী বিক্রি” বের করা কোন OLAP অপারেশন?

  • Roll-up
  • Drill-down
  • Slice
  • Pivot

একটি incremental load WHERE order_date > watermark দিয়ে নতুন অর্ডার আনে। কোন পরিবর্তনটি এটি কখনো warehouse-এ আনবে না?

  • আজ ঢোকানো একটি নতুন অর্ডার
  • গত মাসের একটি অর্ডারের status আজ pending থেকে cancelled হওয়া
  • আজকের একটি অর্ডারের দুটি লাইন
  • একদিন লোড না চলায় মাঝের দিনে ঢোকানো নতুন অর্ডার

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. OLTP আর OLAP আলাদা কাজ: ছোট লেনদেন, normalized, row store বনাম বিশাল পড়া, dimensional, column store. warehouse চালু সিস্টেমকে রক্ষা করে, উৎস মেলায়, ইতিহাস রাখে।
  2. আগে grain: fact-এর এক সারি কী, তা ঘোষণা করুন; সম্ভব হলে সবচেয়ে সূক্ষ্ম, আর কখনো দুই grain এক টেবিলে নয়। তারপর dimension (কখন, কে, কী, কোথায়) আর measure.
  3. Star schema: denormalized dimension, fact থেকে এক ধাপ; snowflake-এ dimension normalized. ROLLUP উপ-মোট দেয় (SQLite-এ UNION ALL দিয়ে), window function র‍্যাংক আর ভাগ।
  4. Measure-এর জাত আর SCD: additive, semi-additive (ব্যালান্স), non-additive (অনুপাত, আলাদা-সংখ্যা)। SCD type 2 = পুরোনো সারি বন্ধ + নতুন surrogate key; fact-এ key খোঁজা হয় ঘটনার তারিখ ধরে।
  5. Pipeline: ETL বা ELT, staging, watermark বা CDC দিয়ে incremental, idempotent লোড, আর প্রতি লোডের পর quality check. নিচে column store, compression আর MPP.

পরের পর্বে: relational জগতের বাইরে — পর্ব ৫৬-এ NoSQL: key-value, document, wide-column আর graph ডেটাবেস, কোনটি কোন প্রশ্নের জন্য।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান