Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ৯/১৯অ্যাডভান্সড৫৫ মিনিট

Statistics ও Cardinality Estimation — optimizer-এর চোখ

Statistics and Cardinality Estimation

Optimizer-এর সব জ্ঞানের উৎস ক'শো page-এর একটা sample — histogram, MCV, n_distinct, correlation কীভাবে সেই sample থেকে জন্ম নেয় আর কখন প্রতারণা করে।

এই লেসন শেষে আপনি পারবেন

  • pg_stats view-এর প্রতিটা field — null_frac, n_distinct, most_common_vals/freqs, histogram_bounds, correlation — পড়ে বলতে পারবেন planner কোনটা দিয়ে কোন estimate বানায়
  • Equi-depth histogram-এর mechanics ব্যাখ্যা করে range predicate-এর selectivity interpolation হাতে হিসাব করতে পারবেন, আর MCV-বহির্ভূত equality-র uniform-rest formula প্রয়োগ করতে পারবেন
  • ANALYZE-র sampling economics ব্যাখ্যা করতে পারবেন — default target 100 মানে প্রায় ৩০,০০০ row-এর sample, আর statistics_target বাড়ালে কী দামে কী লাভ
  • Correlated column-এর estimation trap ধরিয়ে দেখাতে পারবেন — selectivity multiply হয়ে কীভাবे rows=1 বনাম actual=লক্ষ হয় — তারপর CREATE STATISTICS দিয়ে ঠিক করতে পারবেন
  • Estimation error-র multiplicative compounding গণিত দিয়ে দেখিয়ে বলতে পারবেন একটা ভুল input কীভাবে পুরো join tree-র plan choice poison করে

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

গত লেসনে আমরা optimizer-এর search machinery দেখলাম — DP, cost model, geqo। কিন্তু ইঞ্জিনটা যত পরিশীলিতই হোক, তার প্রতিটা সিদ্ধান্তের ভেতরে একটা অনুমান: “এই filter-এ মোটামুটি কত row টিকবে?” সংখ্যাটার নাম cardinality estimate, আর পুরো optimizer-ই কার্যত ওই একটা সংখ্যার জিম্মাদার।

ভয়ের কথাটা এখানে: database আপনার data সম্পর্কে সব জানে না — জানে একটা ছোট sample, কয়েক হাজার row। ওই অভাবী sample থেকেই সে histogram আঁকে, MCV list বানায়, join-এর ফল অনুমান করে। বেশিরভাগ সময় অনুমান চমৎকার; মাঝে মাঝে চরম প্রতারণা — আর প্রতারিত optimizer হাজার-গুণ খারাপ plan বেছে নেয় পূর্ণ আত্মবিশ্বাসে।

আজকের ভ্রমণ:

  • ANALYZE ঠিক কী কী জমায় — pg_stats-এর পাঁচ field খোলা
  • Equi-depth histogram আর MCV দিয়ে selectivity-র গণিত
  • Correlation stat — physical clustering কীভাবে index-vs-seq ঘুরায়
  • Error compounding: কেন ১০% ভুলও join-chain-এ বিপর্যয়
  • Extended statistics — correlated column-এর ফাঁদ থেকে মুক্তি

মূল ধারণা

ANALYZE কী জমায়

ANALYZE প্রতিটা table-এর একটা random page-sample নেয় আর প্রতি column-এর জন্য এই তথ্য জমায় (দেখা যায় pg_stats view-তে):

Fieldমানে
null_fracকত ভগ্নাংশ NULL
n_distinctdistinct value-র অনুমিত সংখ্যা (-1 = row-scaled ratio)
most_common_vals / freqsসবচেয়ে frequent value আর তাদের ভগ্নাংশ (MCV list)
histogram_boundsnon-MCV value-গুলোর equi-depth bucket-সীমা
correlationphysical storage order আর logical column order-এর মিল

Default statistics_target = 100 — sample size ≈ 300 × target = ৩০,০০০ row। Target বাড়ালে (per-column ALTER TABLE ... ALTER COLUMN ... SET STATISTICS) sample ও MCV/histogram precision বাড়ে — দাম: ANALYZE-র সময় আর pg_statistic-এর জায়গা।

Histogram — equi-depth বুনোট

MCV-তে না থাকা value-গুলোর জগতে planner রাখে equi-depth histogram: bucket-সীমাগুলো এমনভাবে কাটা যেন প্রতি bucket-এ sample-এর প্রায় সমান সংখ্যক row পড়ে — value-range সমান না, frequency সমান। Skewed data-তে এটাই বুদ্ধিমান: dense এলাকা সরু bucket-এ, sparse এলাকা চওড়া bucket-এ।

Range query-র selectivity: যেসব bucket overlap করে সেগুলোর ভগ্নাংশ-যোগফল, boundary bucket-এ linear interpolation:

histogram_bounds = [10, 20, 35, 80, 400]     -- ৪টা bucket, প্রতিটায় ~25%
WHERE amount BETWEEN 30 AND 60:
  bucket [20,35):  ৫/১৫ অংশ = 0.167 × 25%
  bucket [35,80):  পুরোটা        = 25%
  bucket [80,400): ০             = 0
  estimate = 0.167×0.25 + 0.25 ≈ 29% of non-MCV rows

MCV আর uniform-rest অনুমান

Equality (status = 'active'): value MCV list-এ থাকলে সরাসরি তার stored freq — সবচেয়ে নির্ভুল পথ। না থাকলে বাকি জগৎকে uniform ধরা হয়:

sel = (1 − Σ most_common_freqs) / (n_distinct − |MCV|)

অর্থাৎ MCV-রা যা খেয়েছে বাকি ভগ্নাংশটা বাকি distinct value-দের মধ্যে সমান ভাগ। Skew যত তীব্র, MCV list তত বড় দরকার — নাহলে heavy tail uniform অনুমানে ডুবে যায়।

n_distinct — inflation-এর ফাঁদ

n_distinct অনুমান করা হয় sample-এর distinct count থেকে extrapolate করে। Correlated key-তে এটা ভয়ংকরভাবে বাড়িয়ে দেখাতে পারে: (city, street) জোড়া প্রায় unique, কিন্তু আলাদা আলাদা column-এর distribution সম্পূর্ণ আলাদা গল্প — planner জোড়া-জ্ঞান রাখেই না, যতক্ষণ না আপনি extended statistics দেন। Manual override-ও আছে: ALTER TABLE t ALTER COLUMN c SET (n_distinct = -0.4) (negative = ratio-of-rows)।

Correlation — physical clustering-এর ছাপ

Index scan-এর খরচ নির্ভর করে target rows কতটা ছড়ানো — একই page-এ পাশাপাশি থাকলে sequential-ish, সারা table-এ ছড়ালে প্রতি row-এ random page। correlation stat (-1..+1) ঠিক সেই মিল মাপে: insert-time-ordered timestamp column-এ ≈ 1 (index scan প্রায় free), random hash column-এ ≈ 0 (planner seq scan-ই পছন্দ করবে)। Table যদি কখনো physical order-এ reorganize করতে চান — সেটা CLUSTER কমান্ড, আর stat বদলাতে আবার ANALYZE।

Error compounding — ভুলের সুদ

Join chain-এ estimate-রা গুণ হয়ে চলে: প্রতিটা ধাপে ১০%-করে under-estimate হলে ৫ join পরে:

0.9⁵ ≈ 0.59  — অর্ধেকেরও কম!

আর এই ভুল সংখ্যাই পরের join-এর algorithm বাছে — under-estimate মানে planner nested-loop ভাববে “ছোট, lookup-ই যথেষ্ট”, বাস্তবে লক্ষ row-এর ঢেউ। একটা bad input-ই পুরো plan poison করে — এজন্যই cardinality estimation database performance-এর সবচেয়ে গুরুত্বপূর্ণ অনুমান-বিজ্ঞান।

ভেতরে কী ঘটছে

Sampling internals — কেন page-ভিত্তিক

ANALYZE random tuple না, random pages নেয় — প্রতিটা sampled page-এর সব live tuple পড়ে। কারণ IO economics: আলাদা আলাদা tuple-এ jump করা মানেই random page read-এর ঝড়; page-granularity-তে ৩০,০০০ row-এর জন্য মোটামুটি ৩০,০০০/rows-per-page ≈ কয়েকশো page। এজন্যই ANALYZE production-এ সস্তা — কিন্তু page-clustered data-তে (একই block-এ একই value-র ঝাঁক) sample bias-এর সম্ভাবনা এখান থেকেই আসে।

Selectivity functions — operator ভেদে আলাদা বই

Planner-এর type system-এ প্রতিটা operator-এর নিজের selectivity function: scalar equality ওই uniform-rest formula, scalar range interpolation, LIKE-এর জন্য pattern-এর প্রথম অক্ষর-ভিত্তিক heuristic, join selectivity আবার দুই side-এর n_distinct/MCV মিলিয়ে। এই function-গুলোর গুচ্ছ-ই “optimizer-এর চোখ” — চোখ দুর্বল হলে শরীর (plan) কুঁকড়ে যায়।

Autovacuum আর stats lifecycle

Stats বাসি হয়: ১০% row বদলালেই autovacuum-এর analyze trigger। কিন্তু trigger-ও delayed, আর বড় bulk-load-এর পরে নিজে হাতে ANALYZE t না দিলে planner পুরনো ছবিতেই plan করবে — “load করার পরে সব query slow” complaint-এর এক-লাইনা diagnosis। দেখভালের tool: last_analyze, last_autoanalyze in pg_stat_user_tables।

উদাহরণ

একটা skewed column-এর পূর্ণ পাঠ

Table: payments(status text, amount numeric) — ৯৯% row active, বাকি ১% ছড়ানো আরও ৯টা value-তে। ANALYZE-র পরে pg_stats বলে দেবে:

most_common_vals     = {active, failed, pending, refunded, ...}
most_common_freqs    = {0.99,  0.004,  0.003,   0.002, ...}   ← যোগফল ≈ 0.999
n_distinct           = 10
histogram_bounds     = {failed, pending, queued, refunded, stale, ...}

এবার planner-এর হিসাব:

WHERE status = 'active'   → MCV hit!  sel = 0.99            ← প্রায় নিখুঁত
WHERE status = 'expired'  → MCV miss: (1−0.999)/(10−5) = 0.0002
                            actual হয়তো 0.001 — ভুল, কিন্তু ছোট ভুল
WHERE status IS NOT NULL  → sel = 1 − null_frac

লক্ষণীয়: প্রথম estimate-টা প্রায় perfect কারণ MCV ঠিক skew-এর দিকেই তীর ধরে। তৃতীয়টা দেখায় null_frac-ও একটা first-class selectivity input। এই একই কলামে ORDER BY amount-এর index scan আলোচনায় ফিরবে correlation-এর অধ্যায়ে।

ছবিতে

Sample থেকে plan — পুরো নালী

value ──────────────────────────────────────────────▶
        ▂▃▅▇███▇▅▃▂▁                    ▁▁▂▁▁        ← data density
       ├────┼──┼─┼┼──┼────────┼───────┼───┼──┤
bounds: 10  20 35 80      400      900  970  999
        │ 25%│25%│  25%    │  25%  │          ← প্রতি bucket-এ সমান row
window [30,60): bucket২-এর ⅓ + bucket৩-এর পুরো = 8.3% + 25% ≈ 33%
Equi-depth histogram-এর shape — dense এলাকায় সরু bucket, sparse-এ চওড়া; query window-এর পড়া bucket-ভগ্নাংশই selectivity।

আর পুরো pipeline-টা এক নজরে:

Tuple থেকে plan decision — estimation নালী
  1. Random page sampleANALYZE — ৩০০×target rows, page-ভিত্তিক নমুনা
  2. Per-column statsMCV/freqs, equi-depth bounds, n_distinct, correlation
  3. Selectivity functionsoperator ভেদে estimate — equality/range/LIKE/join
  4. Cardinality propagationjoin/filter chain ধরে গুণ — এখানেই compounding
  5. Cost + plan choiceestimated rows → algorithm/access-path সিদ্ধান্ত

নিজে চালিয়ে দেখুন

EXPERIMENT

skewed column-এ pg_stats পাঠ, optimizer-ফাঁদ, আর extended statistics-এর নিরাময়

psql (Docker: docker run --name pg-lab -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:16)· ২৫ মিনিট

ধাপ ১ — ইচ্ছাকৃত skewed dataset:

CREATE TABLE visits (
  id bigint GENERATED ALWAYS AS IDENTITY,
  city text NOT NULL,
  zip text NOT NULL,
  status text NOT NULL
);
-- zip = f(city) — পূর্ণ correlated! Dhaka↔1219 সর্বদা একসাথে
INSERT INTO visits (city, zip, status)
SELECT
  (ARRAY['Dhaka','Chattogram','Khulna','Rajshahi','Sylhet'])[1 + (g % 5)],
  CASE WHEN g % 5 = 0 THEN '1219' WHEN g % 5 = 1 THEN '4000'
       WHEN g % 5 = 2 THEN '9100' WHEN g % 5 = 3 THEN '6000'
       ELSE '3100' END,
  CASE WHEN g % 100 > 2 THEN 'ok' ELSE 'flagged' END
FROM generate_series(1, 500000) g;

ANALYZE visits;
SELECT * FROM pg_stats WHERE tablename = 'visits'\x

\x expanded mode-এ প্রতিটা field পড়ুন: most_common_freqs-এ ‘ok’-এর ভগ্নাংশ ≈ 0.97, histogram_bounds-এ city-গুলোর ক্রম, correlation ≈ ধনাত্মক কিছু (insert order-এর ছাপ)।

ধাপ ২ — optimizer-ফাঁদ পাতুন:

EXPLAIN ANALYZE SELECT count(*)
FROM visits WHERE city = 'Dhaka' AND zip = '1219';
->  Seq Scan on visits (actual rows=100000 loops=1)
      Filter: ((city = 'Dhaka') AND (zip = '1219'))
      Rows Removed by Filter: 400000
Planning estimate: rows=20000   ← 0.2 × 0.2 = 0.04 × 500000
Actual:            rows=100000  ← ৫ গুণ off — দুই column আসলে এক তথ্য!

Planner দুই filter-কে independent ভেবে 0.2 × 0.2 = 0.04 ধরেছে; বাস্তবে দুটো এক মুদ্রার দুই পিঠ — 0.2-ই সত্যি। এখন scale-up ভাবুন: এই estimate-ই পরের join-এ hash-vs-nested ঠিক করবে।

ধাপ ৩ — extended statistics দিয়ে চোখ ফেরান:

CREATE STATISTICS visits_city_zip (dependencies)
  ON city, zip FROM visits;
ANALYZE visits;

EXPLAIN ANALYZE SELECT count(*)
FROM visits WHERE city = 'Dhaka' AND zip = '1219';
-- Planning rows now ≈ 100000 — dependency-তে correlation ধরা পড়েছে!

SELECT * FROM pg_stats_ext WHERE statistics_name = 'visits_city_zip'\x
-- dependencies kind-এ functional-dependency coefficient দেখবেন:
-- "zip প্রায় সবসময় city থেকে derive হয়" — planner তখন দ্বিতীয় filter-এর
-- ভগ্নাংশ আর স্বাধীনভাবে গুণ করে না। ndistinct kind যোগ করতে চাইলে:
-- CREATE STATISTICS ... (dependencies, ndistinct)
এটা কী প্রমাণ করে

Correlated column-pair-এ planner selectivity গুণ করে ভয়ংকর under-estimate করে (rows=1 বনাম actual লক্ষাধিক), আর CREATE STATISTICS (dependencies) যোগ করলে estimate সংশোধিত হয় — statistics-ই optimizer-এর চোখ, অন্ধ হলে plan অন্ধ।

নিজে বানান

BUILD IT

EquiDepthHistogram + MCV estimator — sampling-এর সূদ নিজের চোখে

Python · ●●●○○
  1. Power-law skewed numeric stream বানান (paretovariate)
  2. EquiDepthHistogram: sorted sample থেকে quantile edges, range-selectivity interpolation
  3. MCV estimator: top-k counter + uniform-rest fallback
  4. Brute-force exact answer full data-তে — ground truth
  5. Sample sizes {0.1%, 1%, 10%}-এ error% টেবিল — sampling law প্রমাণ

লক্ষ্য: ANALYZE যা করে তার হ্রাসপ্রাপ্ত প্রতিরূপ — ছোট sample থেকে estimator বানিয়ে full-data truth-র সাথে মেপে দেখা।

import random
from collections import Counter

random.seed(7)

# ---------- synthetic skewed data ----------

N = 200_000

def make_data(n):
    # jittered Pareto: ঘন নিম্নমান, দীর্ঘ tail, কোনো duplicate-edge trap নেই
    return [min(999_999,
                int((random.paretovariate(1.15) - 1 + random.random()) * 30_000))
            for _ in range(n)]

DATA = make_data(N)

# ---------- estimators ----------

class EquiDepthHistogram:
    """equi-depth: প্রতি bucket-এ sample-এর ~সমান সংখ্যক value"""

    def __init__(self, sample, buckets=20):
        s = sorted(sample)
        self.buckets = buckets
        self.edges = [s[round(k * (len(s) - 1) / buckets)]
                      for k in range(buckets + 1)]

    def range_selectivity(self, lo, hi):
        hits = 0.0
        for k in range(self.buckets):
            blo, bhi = self.edges[k], self.edges[k + 1]
            width = max(bhi - blo, 1)
            overlap = min(hi, bhi) - max(lo, blo)
            if overlap > 0:
                hits += min(overlap, width) / width
        return hits / self.buckets


class MCVEstimator:
    """value-র বদলে stripe (v // 10000)-এ MCV — repeated discrete unit"""

    def __init__(self, sample, k=10):
        stripes = [v // 10_000 for v in sample]
        self.total = len(stripes)
        self.mcv = Counter(stripes).most_common(k)
        self.freq = {s: c / self.total for s, c in self.mcv}
        self.rest_mass = max(0.0, 1.0 - sum(self.freq.values()))
        self.n_other = max(1, len(set(stripes)) - len(self.mcv))

    def equality_selectivity(self, stripe):
        return self.freq.get(stripe, self.rest_mass / self.n_other)

# ---------- ground truth (full data brute force) ----------

def exact_range(lo, hi):
    return sum(1 for x in DATA if lo <= x <= hi) / N

def exact_eq(stripe):
    return sum(1 for x in DATA if x // 10_000 == stripe) / N

# ---------- experiment: sample-size ladder ----------

QUERIES_R = [(2000, 6000), (0, 90000), (150000, 160000), (400000, 700000)]
QUERIES_E = [0, 1, 2]

def evaluate(frac):
    m = max(int(N * frac), 50)
    sample = random.sample(DATA, m)
    hist = EquiDepthHistogram(sample)
    mcv = MCVEstimator(sample)
    errs_r = [abs(hist.range_selectivity(lo, hi) - exact_range(lo, hi))
              / max(exact_range(lo, hi), 1e-4) for lo, hi in QUERIES_R]
    errs_e = [abs(mcv.equality_selectivity(s) - exact_eq(s))
              / max(exact_eq(s), 1e-4) for s in QUERIES_E]
    return m, 100 * sum(errs_r) / len(errs_r), 100 * sum(errs_e) / len(errs_e)

print(f"{'sample':>7} | {'rows':>7} | {'range err%':>10} | {'equality err%':>13}")
print('-' * 47)
for frac in (0.001, 0.01, 0.10):
    m, er, ee = evaluate(frac)
    print(f'{frac:>6.1%} | {m:>7,} | {er:>9.1f}% | {ee:>12.1f}%')

সাধারণ ফলাফল (seed=7 — আপনার সংখ্যা কাছাকাছি হবে):

 sample |    rows | range err% | equality err%
-----------------------------------------------
   0.1% |     200 |       34.4% |         24.7%
   1.0% |   2,000 |       19.6% |          7.8%
  10.0% |  20,000 |       10.3% |          3.1%

Reading: sample ১০০ গুণ বাড়ায় error উভয়ই ~৩× নেমেছে — sampling error ∝ 1/sqrt(n)-এর বাস্তব ছায়া। Range-এর error তুলনামূলক বেশি থাকে কারণ interpolation long-tail bucket-এ সবচেয়ে কাঁপে; equality দ্রুত ঠিক হয় কারণ ঘন stripe-গুলো তাড়াতাড়ি MCV list-এ ঢোকে। Production lesson: ANALYZE কোটি-row table-এও মাত্র ৩০,০০০ row দেখে — তবু যথেষ্ট, কারণ planner perfect estimate না, সঠিক order-এর estimate চায়।

নিজে বাড়ান:

  1. Buckets সংখ্যা sweep করুন 50 — ছোট sample-এ বেশি bucket কি সাহায্য করে না ক্ষতি করে?
  2. Uniform histogram (equi-width) variant লিখে skewed data-তে তুলনা করুন — equi-depth কেন standard তার প্রমাণ
  3. Join-selectivity mini-sim: দুই independent column-এর estimated sel গুণ করে exact join-size-এর সাথে error compounding দেখুন

বাস্তব সিস্টেমে

বাস্তব জগতে statistics engineering

Bulk-load discipline। ETL-এর পরে script-এ ANALYZE step থাকা একটা সংস্কৃতি — নইলে রাতের load-এর পরে সকালের dashboard পুরনো ছবি নিয়ে plan করে। Data warehouse migration checklist-এর অবিচ্ছেদ্য লাইন।

Oracle/SQL Server-এ এগিয়ে যাওয়া। SQL Server-এ automatic plan correction আর runtime feedback (CE feedback), Oracle-এ adaptive plans — classic PG-র absent-feedback philosophy-র industrial বিকল্প। Trade-off এখনো তর্কাস্পদ।

HyperLogLog আর sketches। Distinct-count estimation-এর modern sketch family (HLL) streaming systems-এ standard — ANALYZE-র n_distinct problem-এরই scalable উত্তর; approximate-but-bounded-error দর্শন।

Extended stats adoption। Multi-column filter-heavy schemas-এ (tenant_id + status জাতীয়) CREATE STATISTICS এখন performance review-র checklist item — ৫ লাইনের SQL, বহু ঘণ্টার firefighting বাঁচায়।

যে ভুলগুলো সবাই করে

“ANALYZE পুরো table scan করে — তাই বড় table-এ চালানো ব্যয়বহুল।”

ANALYZE random page-sample নেয় — ৩০,০০০ row-এর জন্য কয়েকশো page, কোটি-row table-এও সেকেন্ডের অঙ্কে। ব্যয়বহুল মনে হওয়ার আসল কারণ হলে সেটা lock contention বা ভুলly tuned maintenance window — scan cost না।

“Estimate ভুল হলে database bug — ঠিক করা উচিত সবসময়।”

Estimate মানেই probabilistic model-এর output — ছোট ভুল স্বাভাবিক, এমনকি সুস্থ লক্ষণ। সমস্যা তখনই যখন ভুল বড় এবং plan-choice বদলে দেয়। Rows=47 বনাম actual=61 — নিশ্চিন্তে উপেক্ষা করুন; rows=1 বনাম actual=100000 — তখনই তদন্ত।

“statistics_target বাড়ালেই slow query ঠিক হবে।”

Target বাড়ায় sample precision — কিন্তু সমস্যা যদি correlation/independence-অনুমানে হয় (correlated columns), বড় sample-ও সেই structural ভুল বহন করে। সেখানে ওষুধ extended statistics; target শুধু heavy-skew MCV truncation-এর ক্ষেত্রেই জাদু দেখায়।

“VACUUM আর ANALYZE একই জিনিস — autovacuum চললে statistics fresh।”

VACUUM dead-tuple space ফেরায়; ANALYZE statistics লেখে — দুই আলাদা কাজ, এক wrapper-এ (VACUUM ANALYZE)। Autovacuum দুটোই schedule করে কিন্তু আলাদা threshold-এ — তাই “autovacuum চলেছে” মানেই stats fresh না; last_analyze দেখুন, assumption না।

বুঝেছেন কি না দেখুন

1

pg_stats-এর পাঁচটা প্রধান field আর প্রতিটার এক-লাইনা কাজ বলুন।

স্মরণ

null_frac — NULL-ভগ্নাংশ estimate; n_distinct — distinct-value অনুমান (join/group-by sizing); most_common_vals/freqs — skew-এর সরাসরি সাক্ষ্য, equality-র নিখুঁত path; histogram_bounds — non-MCV range-এর equi-depth map; correlation — physical clustering, index-cost model-এর ইনপুট।

2

Σ most_common_freqs = 0.95, |MCV| = 8, n_distinct = 108। Non-MCV value-র equality selectivity কত?

প্রয়োগ

(1 − 0.95) / (108 − 8) = 0.05/100 = 0.0005। Uniform-rest অনুমান — বাকি ৫% mass ধরে ১০০টা remaining distinct value-এ সমান ভাগ। Actual skewed হলে এখানেই ভুল জন্মায়।

3

Equi-width-এর বদলে equi-depth histogram কেন? Skewed data-তে equi-width ঠিক কোথায় অন্ধ হয়?

যুক্তি

Skewed data-র বেশিরভাগ row জমে ছোট value-window-এ — equi-width-এ ওই window একটাই mega-bucket হয়ে যায়, ভেতরের distribution completely invisible; বাকি শত bucket ফাঁপা। Equi-depth সমান-frequency কাটে — dense এলাকা zoom করে, sparse এলাকা coarse — প্রতি bucket-এর information content সমান থাকে।

4

৪-join query-তে প্রতিটা intermediate estimate ২০%-করে over-estimate। Plan choice-এর ওপর প্রভাব কোন দিকে ঝুঁকে বেশি — আর কেন?

যুক্তি

Over-estimate মানে planner intermediate বড় ভাবে — hash/merge-এর দিকে ঝোঁকে, memory-budget ভয়ে batching ধরে, আর nested-loop-কে “too many iterations” ভেবে ফেলে দেয়। Under-estimate-এর বিপর্যয় (nested-loop explosion) তীব্র হলেও over-estimate-এর ব্যয় মূলত হারানো opportunity — hash-এর extra build খরচ। দুই দিকেই ক্ষতি, ভিন্ন চেহারায়।

5

WHERE tenant_id = 42 AND status = 'archived' — প্রতিটা tenant-এ archived-ভাগ আসলে ৫%, কিন্তু planner rows=total×s₁×s₂ দিচ্ছে। কোন অনুমান ভাঙছে, remedy কী?

প্রয়োগ

Independence অনুমান ভাঙছে — tenant আর status correlated (নির্দিষ্ট tenant-এর archived share গlobally আলাদা)। Remedy: CREATE STATISTICS (dependencies) ON tenant_id, status FROM t; ANALYZE; — functional dependency ধরলে planner দ্বিতীয় filter-কে স্বাধীন গুণন থেকে বাদ দেয়। Extreme case-এ partial index-ও ওষুধ।

6

কোটি-row event table-এ নতুন rare event-type যোগ হলো (share 0.5%)। Planner তা এখনো দেখছেই না — estimate কেন ব্যর্থ, আর design-level তিনটা সমাধান?

ডিজাইন

ব্যর্থতার কারণ: ৩০k sample-এ 0.5%-এর প্রত্যাশিত নমুনা ≈ ১৫০ row — দেখার কথা; কিন্তু নতুন value MCV-তে জায়গা পায়নি হলে uniform-rest-এ ডুবে যায়, আর stats বাসি হলে কিছুই না। Solutions: (১) forced ANALYZE + statistics_target বাড়িয়ে MCV depth; (২) ওই value-র জন্য partial index + query hint-free rewrite; (৩) hot/cold partitioning — rare-type আলাদা table/partition, নিজের stats সহ। Design moral: rare-class analytics-এ statistics-কে contract হিসেবে সামলাতে হয়।

এরপর কী

এরপর কী

Storage engine, executor, optimizer, statistics — database-এর “মেধা” দেখা শেষ। এবার সেই প্রশ্ন, যেটা module-এর শুরু থেকে ঝুলছিল: বিদ্যুৎ চলে গেলে আমার committed transaction টিকে থাকে কীভাবে?

পরের লেসনে transactions আর ACID:

  • Atomicity-র promise — all-or-nothing আসলে কীভাবে implement হয়
  • Consistency, Isolation, Durability — চার letter-এর চার আলাদা প্রকৌশল-সমস্যা
  • Commit মুহূর্তে crash হলে ঠিক কী থাকে disk-এ
  • PostgreSQL-এর ভাষায়: commit sequence-এর প্রথম ঝলক

আজকের চোখ (statistics) এখন থেকে durability-র স্মৃতি (WAL) পর্যন্ত — পুরো engine একসাথে দেখা শুরু হবে।

আরও পড়ুন

  • PostgreSQL Documentation — Statistics Used by the Planner — PostgreSQL Global Development Group · pg_stats-এর প্রতিটা field আর row estimation examples-এর প্রামাণ্য ব্যাখ্যা
  • PostgreSQL Documentation — Extended Statistics — PostgreSQL Global Development Group · CREATE STATISTICS-এর dependencies/ndistinct kinds-এর official reference
  • Query Optimization (ACM Computing Surveys, 1996) — Yannis Ioannidis · Selectivity estimation-এর সমস্যা আর histogram family-র survey — ক্লাসিক রেফারেন্স