GROUP BY, HAVING ও সংখ্যান
এই পাঠে যা শিখবেন
- ৫টি core aggregate function — কোনটি কী করে, NULL-এ কীভাবে আচরণ করে
- GROUP BY — একাধিক column-এ grouping
- WHERE বনাম HAVING — কখন কোনটি
- Daraz analytics-এর বাস্তব scenario — top category, revenue trend, churn metric
১ · Aggregate function-এর মৌলিক ৫টি
Aggregate functionAggregate Functionএকটি function যা একাধিক row-কে summarize করে একটি single value-তে রূপান্তর করে। SUM, AVG, COUNT সবচেয়ে common। এর বিপরীত — scalar function (একটি input → একটি output)। = "অনেক ডেটা → একটি সারসংক্ষেপ"। SQL-এর ৫টি core:
COUNT(*) — সব row গণনা।
COUNT(column) — non-NULL row গণনা।
SUM(column) — যোগফল।
AVG(column) — গড়।
MIN/MAX(column) — সর্বনিম্ন / সর্বোচ্চ।
-- সব core aggregate একসাথে
SELECT
COUNT(*) AS total_orders,
COUNT(DISTINCT customer_id) AS unique_customers,
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order_value,
MIN(total_amount) AS smallest_order,
MAX(total_amount) AS largest_order
FROM orders
WHERE order_date >= '2024-01-01';
AVG(amount)-এ যদি ১০টি row-এ ৩টি NULL থাকে — denominator ৭, ১০ নয়। COUNT(*) সব row count, COUNT(column) non-NULL only।
২ · GROUP BY — subset-এ aggregate
"মোট revenue" নয়, "প্রতিটি district-এর revenue" — এটিই GROUP BY।
-- প্রতিটি জেলায় গ্রাহক সংখ্যা ও মোট কেনাকাটা
SELECT
c.district,
COUNT(DISTINCT c.customer_id) AS customers,
SUM(o.total_amount) AS total_revenue,
AVG(o.total_amount) AS avg_order
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.district
ORDER BY total_revenue DESC;
৩ · Multiple column GROUP BY
একাধিক dimension-এ — যেমন "প্রতিটি জেলা × প্রতিটি ক্যাটেগরি":
-- জেলা × ক্যাটেগরি — প্রতি combination-এ revenue
SELECT
c.district,
p.category,
COUNT(*) AS order_count,
SUM(o.total_amount) AS revenue
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN products p ON o.product_id = p.product_id
GROUP BY c.district, p.category
ORDER BY c.district, revenue DESC;
৪ · WHERE vs HAVING — সবচেয়ে important পার্থক্য
WHERE aggregate-এর আগে চলে — individual row filter। HAVING aggregate-এর পরে চলে — group filter।
-- ভুল: aggregate WHERE-এ
-- SELECT district, SUM(amount) FROM orders
-- WHERE SUM(amount) > 100000 -- ❌ ERROR
-- GROUP BY district;
-- সঠিক: HAVING use
SELECT
c.district,
SUM(o.total_amount) AS revenue
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2024-01-01' -- row filter (date)
GROUP BY c.district
HAVING SUM(o.total_amount) > 1000000 -- group filter (revenue)
ORDER BY revenue DESC;
এই query-তে WHERE পুরোনো order বাদ দেয় (২০২৪-এর আগে), GROUP BY district-এ aggregate করে, HAVING ১০ লক্ষ-এর কম revenue-এর জেলা বাদ দেয়।
৫ · DISTINCT-এর ভূমিকা
COUNT(DISTINCT column) — duplicate বাদ দিয়ে গণনা। বিশেষত "unique customer", "unique product" — analytics-এ অপরিহার্য।
-- COUNT-এর ৩ ধরনের ব্যবহার
SELECT
COUNT(*) AS total_rows,
COUNT(customer_id) AS non_null_customers,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders;
-- ক্যাটেগরি-ভিত্তিক unique buyer
SELECT
p.category,
COUNT(DISTINCT o.customer_id) AS unique_buyers,
COUNT(*) AS total_orders
FROM orders o
JOIN products p ON o.product_id = p.product_id
GROUP BY p.category;
৬ · CASE WHEN — conditional aggregate
একটি single query-তে multiple condition-ভিত্তিক metric — analytics-এর সবচেয়ে underrated trick।
-- বিভিন্ন status-এর order count
SELECT
DATE_TRUNC('month', order_date) AS month,
COUNT(*) AS total_orders,
COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled,
SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END) AS revenue,
AVG(CASE WHEN total_amount > 5000 THEN total_amount END) AS high_value_avg
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;
এই pattern — conditional aggregation — pandas-এর pivot বা Excel pivot-table-এর SQL equivalent।
৭ · GROUP BY-এর "common bug"
SELECT-এ যে non-aggregate column আছে — সেটি GROUP BY-তে থাকতে হবে। MySQL ছাড়া অন্য DB error দেয়; MySQL silent ভুল ফলাফল।
SELECT district, name, SUM(amount) FROM orders GROUP BY district; — name GROUP BY-তে নেই, কিন্তু non-aggregate। PostgreSQL/Oracle error; MySQL random একটি name দেখাবে। সবসময় explicit GROUP BY রাখুন।
৮ · ROLLUP, CUBE — multi-level aggregate
Advanced GROUP BY — সাব-totals automatically।
-- ROLLUP — district-level + grand total
SELECT
district,
SUM(total_amount) AS revenue
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
GROUP BY ROLLUP(district);
-- output: প্রতি district + একটি grand total row (district = NULL)
-- CUBE — সব combination
SELECT
district, category,
SUM(total_amount) AS revenue
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN products p ON o.product_id = p.product_id
GROUP BY CUBE(district, category);
-- output: প্রতি combo + per-district + per-category + grand total
৯ · বাস্তব analytics — সাধারণ pattern
- Top-N per group: "প্রতিটি ক্যাটেগরির top-৩ পণ্য" — Window function (পরের পাঠ)।
- Cohort analysis: "যারা ২০২৪ জানুয়ারিতে join করেছিলেন তাদের ৬-মাস retention"।
- Funnel: visit → cart → checkout → payment — প্রতি step-এ count।
- Revenue split: new customer vs repeat customer revenue।
- Heatmap data: day-of-week × hour-of-day পেমেন্ট heatmap।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ "AVG-এর সাথে গড় বুঝে নেবেন না।" — একজন senior data scientist বলেছেন। AVG-এর কোন কোন pitfall আছে যেগুলো বাস্তব analysis-এ ভুল conclusion-এ পৌঁছে দেয়?
"Average" — পরিসংখ্যানের সবচেয়ে অপব্যবহৃত metric।
(১) Outlier sensitivity:
- ৯৯ জনের আয় ৩০,০০০ + ১ জনের আয় ১ কোটি → AVG ১,২৯,৭০০ — বাস্তব representative নয়।
- AVG income reporting-এ — distortion বিশাল।
- Median (middle value) সাধারণত better।
(২) Simpson's paradox:
- প্রতি subgroup-এ A > B, কিন্তু overall B > A — counterintuitive।
- Hospital A ও B — A সব patient-এ better recovery rate, কিন্তু overall B better — কারণ A বেশি serious case নেয়।
(৩) AVG of ratios vs ratio of AVGs:
- "Average conversion rate" — তিন store-এ 5%, 10%, 50% → AVG 21.67%।
- কিন্তু সঠিক "overall conversion" = total_conversion / total_visit — হয়তো 8%।
- Weighted average দরকার, simple AVG ভুল।
(৪) NULL exclusion:
- "AVG response time" — যেসব ticket এখনো response পায়নি (NULL) — counted হয় না।
- Apparent AVG ভাল লাগে; reality খারাপ।
(৫) Categorical mean nonsense:
- "Average district code" বা "average category" — অর্থহীন।
- Numeric encoding-এর ফাঁদ।
(৬) Time aggregation distortion:
- Hourly AVG → daily AVG-এ pattern হারায়।
- Peak hour signal smoothed out।
(৭) Skewed distribution:
- Income, transaction amount — log-normal distribution।
- AVG > median সাধারণত — "typical" বলতে median বেশি honest।
Better metrics depending on context:
- Median: robust to outlier।
- Percentile: P50, P90, P99 — distribution shape।
- Trimmed mean: top & bottom 5% drop, then AVG।
- Geometric mean: growth rate-এ।
- Weighted average: different group size থাকলে।
Bangladesh examples:
- "Average income in Dhaka" — ৬০,০০০ BDT, কিন্তু median ৩০,০০০। AVG misleading।
- "Average bKash transaction" — small daily transactions + occasional huge ones।
- "Average rider rating in Pathao" — 4.8/5 (most ratings 5)। median = 5; AVG smaller picture।
Best practice — always show distribution:
- Histogram, box plot, percentile।
- Single AVG number-এ সিদ্ধান্ত avoid।
- "Mean ± std" + median + P90 — comprehensive picture।
মূল উপলব্ধি: AVG-এ blind trust danger। যেকোনো AVG report-এ — "distribution shape কী, outlier আছে কি?" — এই প্রশ্ন reflexive করে নিন। Senior analyst-রা AVG দিয়ে বিচার করেন না, distribution দিয়ে।
প্র ০২ একটি e-commerce-এ "প্রতিটি ক্যাটেগরির top-৩ best-selling পণ্য" বের করতে হবে। এটি pure GROUP BY দিয়ে কেন কঠিন — এবং সাধারণত কীভাবে solve করা হয়?
Top-N per group — interview question hall of fame।
কেন GROUP BY যথেষ্ট নয়:
- GROUP BY collapse করে — প্রতি group থেকে এক row।
- "Top-৩" — প্রতি group থেকে ৩টি row লাগবে।
- Aggregate function single value return — list নয়।
Approach 1 — correlated subquery (slow but understandable):
SELECT category, product_name, sales
FROM products p1
WHERE 3 > (
SELECT COUNT(*)
FROM products p2
WHERE p2.category = p1.category
AND p2.sales > p1.sales
)
ORDER BY category, sales DESC;
Performance বাজে — প্রতিটি row-এ subquery।
Approach 2 — JOIN with rank (older DB):
SELECT p.category, p.product_name, p.sales
FROM products p
INNER JOIN products p2
ON p.category = p2.category
AND p2.sales >= p.sales
GROUP BY p.category, p.product_name, p.sales
HAVING COUNT(*) <= 3
ORDER BY p.category, p.sales DESC;
Self-JOIN, expensive। Quadratic complexity।
Approach 3 — window function (modern, preferred):
SELECT category, product_name, sales
FROM (
SELECT
category,
product_name,
sales,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
) ranked
WHERE rn <= 3
ORDER BY category, sales DESC;
Single pass, O(n log n) sort — দ্রুত ও clean।
Tie handling — ROW_NUMBER vs RANK vs DENSE_RANK:
- ROW_NUMBER: 1, 2, 3 — duplicates arbitrary ordered।
- RANK: 1, 2, 2, 4 — same value same rank, gap।
- DENSE_RANK: 1, 2, 2, 3 — same value same rank, no gap।
ব্যবহার অনুযায়ী:
- "Top-3 strict" → ROW_NUMBER।
- "All top-3 including ties" → RANK ≤ 3।
PostgreSQL extension — LATERAL JOIN:
SELECT c.category, p.product_name, p.sales
FROM (SELECT DISTINCT category FROM products) c
CROSS JOIN LATERAL (
SELECT product_name, sales
FROM products
WHERE category = c.category
ORDER BY sales DESC
LIMIT 3
) p;
Per-group LIMIT — সবচেয়ে readable, performance ভাল।
Real-world scaling:
- ১ লক্ষ category × ১০ লক্ষ product → window function partition হাজার million row।
- Pre-aggregation দরকার — daily snapshot table।
- Specialized OLAP database (ClickHouse, Snowflake) এ optimal।
মূল উপলব্ধি: Top-N per group — SQL-এর "rite of passage"। Window function না জানলে এই প্রশ্ন painful। আজকের data analyst-এর জন্য window function essential — পরের পাঠের topic।
প্র ০৩ "Cohort analysis" customer retention measure-এর সবচেয়ে important method। SQL-এ এটি কীভাবে implement করবেন — কী কী aggregation pattern লাগে?
Cohort analysis — Daraz, bKash, Pathao সব analytics team-এর basic competency।
Cohort-এর ধারণা:
- "যারা একই সময়ে join করেছেন তাদের একসাথে track।"
- প্রতিটি cohort-এর retention pattern আলাদা — marketing campaign, product change-এর effect বের হয়।
Step 1 — Cohort assignment:
SELECT
customer_id,
DATE_TRUNC('month', MIN(order_date)) AS cohort_month
FROM orders
GROUP BY customer_id;
-- প্রতিটি customer-এর "first order month" → cohort
Step 2 — Activity tracking:
WITH cohort AS (
SELECT customer_id,
DATE_TRUNC('month', MIN(order_date)) AS cohort_month
FROM orders
GROUP BY customer_id
),
activity AS (
SELECT
c.cohort_month,
DATE_TRUNC('month', o.order_date) AS activity_month,
COUNT(DISTINCT o.customer_id) AS active_customers
FROM orders o
JOIN cohort c ON o.customer_id = c.customer_id
GROUP BY c.cohort_month, DATE_TRUNC('month', o.order_date)
)
SELECT
cohort_month,
activity_month,
active_customers,
EXTRACT(MONTH FROM AGE(activity_month, cohort_month)) AS months_after
FROM activity
ORDER BY cohort_month, activity_month;
Step 3 — Retention rate:
WITH cohort_size AS (
SELECT cohort_month, COUNT(*) AS size
FROM cohort
GROUP BY cohort_month
)
SELECT
a.cohort_month,
a.months_after,
a.active_customers,
cs.size,
ROUND(100.0 * a.active_customers / cs.size, 2) AS retention_pct
FROM activity_with_lag a
JOIN cohort_size cs ON cs.cohort_month = a.cohort_month;
Output (heatmap-ready):
cohort_month m0 m1 m2 m3
2024-01 100% 45% 32% 25%
2024-02 100% 52% 38% --
2024-03 100% 48% -- --
Insights from cohort:
- Newer cohort retention better → product improving।
- Specific month sharp drop → bug, competitor event, seasonality।
- Cohort size varying → marketing effect।
Aggregation patterns used:
- COUNT DISTINCT — unique customer।
- GROUP BY multi-column — cohort × activity month।
- JOIN to cohort dimension।
- Date arithmetic — DATE_TRUNC, AGE।
- CTE chain — readability।
Bangladesh business-এ application:
- bKash agent retention — কোন intake batch-এর agent best survival?
- Pathao rider — different city-এর rider lifetime ভিন্ন কিনা।
- Daraz customer — discount campaign-এর cohort retention impact।
- Banking — different month-এ open হওয়া account-এর dormancy rate।
Pitfalls:
- "Active" definition — order? login? page view? — clearly define।
- Right-censoring — recent cohort data কম থাকে।
- Cohort size variation — small cohort percentage misleading।
- Survivor bias — যারা churn করেছেন pure data-তে invisible।
মূল উপলব্ধি: Cohort analysis pure SQL-এ ১০০% করা যায় — তবে CTE, date function, multi-level GROUP BY মিলে। এই pattern শিখলে retention, A/B test analysis, marketing attribution — সব unlock।
প্র ০৪ "GROUP BY-তে SELECT-এ যা আছে সব column-ই দিতে হবে" — এই rule-এর exception কোথায়? Modern SQL-এ এই rule কীভাবে evolve করেছে?
SQL standard-এর সবচেয়ে controversial rule-এর একটি।
Strict rule (SQL standard):
- SELECT-এ যে column non-aggregate, সেটি GROUP BY-তে থাকতে হবে।
- কারণ — group-এ multiple value হলে SQL জানে না কোনটি দেখাবে।
MySQL-এর "feature" (legacy):
- MySQL ৫.৭ আগে — non-grouped column allow, arbitrary value।
- "Loose mode" — কোনো warning নেই, ভুল ফলাফল।
- ৫.৭+ ONLY_FULL_GROUP_BY default mode — strict।
"Functional dependency" exception:
- যদি একটি column অন্য column-এর সাথে functionally dependent (যেমন primary key) — তবে non-grouped allowable।
-- valid because customer_id determines name uniquely SELECT customer_id, name, COUNT(*) FROM customers GROUP BY customer_id;- PostgreSQL ৯.১+ এই smart detection support করে।
ANY_VALUE/MIN/MAX trick:
SELECT
customer_id,
MIN(name) AS name, -- explicit pick
COUNT(*)
FROM customers
GROUP BY customer_id;
Why strict matters:
- Bug — group-এ multiple value-এ silent ভুল।
- Portability — MySQL → PostgreSQL migration-এ break।
- Predictability — SQL behavior consistent।
Modern alternatives — window function:
-- GROUP BY-এর বদলে window function
SELECT
customer_id,
name, -- কোনো aggregation issue নেই
SUM(amount) OVER (PARTITION BY customer_id) AS total_spent
FROM orders
JOIN customers USING (customer_id);
Aggregate + non-aggregate-এর preferred pattern:
- Aggregate alone CTE-তে।
- JOIN back to dimension table।
- Non-aggregate column conventionally আনা।
WITH agg AS (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
)
SELECT c.customer_id, c.name, c.email, a.order_count
FROM customers c
JOIN agg a ON c.customer_id = a.customer_id;
Performance considerations:
- Window function-এ partition cost আছে।
- GROUP BY-এ less memory if proper aggregate।
- Query optimizer-এর choice — EXPLAIN দেখতে হবে।
Best practice:
- Strict mode সবসময় ON রাখুন।
- Functional dependency-এ rely না করে explicit GROUP BY।
- Window function modern alternative।
- Code review-এ "implicit grouping" red flag।
মূল উপলব্ধি: "GROUP BY rule" SQL-এর rigor-এর symbol। এর exception understand করা advanced। কিন্তু production-এ — strict mode + explicit grouping + window function — তিনটি combine করে modern data engineer।
অনুশীলন
-
Top categories: ২০২৪-এ বিক্রিত প্রতিটি ক্যাটেগরির total revenue, order count, ও unique buyer count। Revenue অনুযায়ী top ১০।
SELECT p.category, COUNT(o.order_id) AS orders, COUNT(DISTINCT o.customer_id) AS buyers, SUM(o.total_amount) AS revenue, AVG(o.total_amount) AS avg_order_value FROM orders o JOIN products p ON o.product_id = p.product_id WHERE o.order_date >= '2024-01-01' AND o.order_date < '2025-01-01' GROUP BY p.category ORDER BY revenue DESC LIMIT 10;Pattern: WHERE date filter (row-level), GROUP BY category, multiple aggregate, ORDER BY top-N।
-
HAVING-এর use: এমন গ্রাহক বের করুন যাদের total ৫টির বেশি অর্ডার আছে এবং মোট খরচ ১০,০০০ টাকার বেশি।
SELECT c.customer_id, c.name, COUNT(o.order_id) AS order_count, SUM(o.total_amount) AS total_spent FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name HAVING COUNT(o.order_id) > 5 AND SUM(o.total_amount) > 10000 ORDER BY total_spent DESC;Note: HAVING-এ aggregate function direct ব্যবহার করা যায়। SELECT-এর alias
order_countHAVING-এ use করা DB-ভেদে variable — safe path: full expression। -
Conditional aggregate: প্রতিটি মাসের total order, completed order count, cancelled order count, ও completed-এর revenue।
SELECT DATE_TRUNC('month', order_date) AS month, COUNT(*) AS total, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed, SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled, SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END) AS revenue, ROUND(100.0 * SUM(CASE WHEN status='cancelled' THEN 1 ELSE 0 END) / COUNT(*), 2) AS cancel_rate_pct FROM orders WHERE order_date >= '2024-01-01' GROUP BY DATE_TRUNC('month', order_date) ORDER BY month;Tip: Conditional aggregation — pivot-table style — analytics-এর "swiss-army knife"। PostgreSQL-এ এটি লেখার আরও clean way:
COUNT(*) FILTER (WHERE status = 'completed')।
আরও পড়ুন
- পাঠ ০৬ · Window function ও CTE পরবর্তী পাঠ Aggregate-এর advanced version — SQL-এর সবচেয়ে শক্তিশালী feature।
- পাঠ ০৪ · JOIN আগের পাঠ Aggregate-এর আগে JOIN বুঝতে হবে — fan-out trap।
- পাঠ ০৮ · Descriptive statistics এই পাঠের সাথে সম্পর্কিত AVG-এর বাইরে — median, percentile, distribution।
- সব AI Courses দেখুন ABCL TECH সব AI কোর্স।