পাঠ ০৫ · ৩০-এর মধ্যে · মডিউল ১

GROUP BY, HAVING ও সংখ্যান

Aggregations — COUNT, SUM, AVG, GROUP BY, HAVING
৭ মিনিট পড়া মাঝারি · Intermediate Analytics

এই পাঠে যা শিখবেন

  • ৫টি 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:

Five core aggregates

COUNT(*) — সব row গণনা।
COUNT(column) — non-NULL row গণনা।
SUM(column) — যোগফল।
AVG(column) — গড়।
MIN/MAX(column) — সর্বনিম্ন / সর্বোচ্চ।

SQL
-- সব 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';

    
NULL behavior: aggregate function NULL ignore করে। তাই AVG(amount)-এ যদি ১০টি row-এ ৩টি NULL থাকে — denominator ৭, ১০ নয়। COUNT(*) সব row count, COUNT(column) non-NULL only।

২ · GROUP BY — subset-এ aggregate

"মোট revenue" নয়, "প্রতিটি district-এর revenue" — এটিই GROUP BY।

SQL
-- প্রতিটি জেলায় গ্রাহক সংখ্যা ও মোট কেনাকাটা
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;

    
ভাবুন bKash-এর ১ কোটি transaction। GROUP BY-হীন SUM = "মোট কত টাকা"। GROUP BY agent_district দিলে — "প্রতিটি জেলায় কত টাকা"। দু'টি ভিন্ন level-এর insight — সিদ্ধান্ত-এ ভিন্ন কাজে লাগে।

৩ · Multiple column GROUP BY

একাধিক dimension-এ — যেমন "প্রতিটি জেলা × প্রতিটি ক্যাটেগরি":

SQL
-- জেলা × ক্যাটেগরি — প্রতি 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;

    
Result row count = (district-এর unique count × category-এর unique count) — তবে শুধু সেই combination যা ডেটায় present।

৪ · WHERE vs HAVING — সবচেয়ে important পার্থক্য

WHERE aggregate-এর আগে চলে — individual row filter। HAVING aggregate-এর পরে চলে — group filter।

SQL
-- ভুল: 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-এর জেলা বাদ দেয়।

SQL aggregate query-এর pipeline WHERE → GROUP BY → HAVING ১ · FROM all rows ২ · WHERE row filter ৩ · GROUP BY bucket rows ৪ · HAVING group filter ৫ · SELECT output cols ৬ · ORDER BY ৭ · LIMIT মনে রাখার নিয়ম WHERE → individual rows HAVING → groups (after GROUP BY) aggregate function (SUM, AVG, COUNT) WHERE-এ থাকতে পারবে না — HAVING-এ যাবে।
SQL aggregate execution pipeline — WHERE row-level, HAVING group-level filter।

৫ · DISTINCT-এর ভূমিকা

COUNT(DISTINCT column) — duplicate বাদ দিয়ে গণনা। বিশেষত "unique customer", "unique product" — analytics-এ অপরিহার্য।

SQL
-- 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।

SQL
-- বিভিন্ন 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।

SQL
-- 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:

  1. Aggregate alone CTE-তে।
  2. JOIN back to dimension table।
  3. 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।

অনুশীলন

  1. 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।

  2. 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_count HAVING-এ use করা DB-ভেদে variable — safe path: full expression।

  3. 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')।

আরও পড়ুন

SQL practice site: LeetCode SQL ও HackerRank SQL — ৩০-মিনিট ডেলি অভ্যাস।
পূর্ববর্তী পাঠ
পাঠ ০৪ · JOIN