Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ৮/১৯রিসার্চ-লেভেল১ ঘণ্টা ৫ মিনিট

Query Optimizer — cost model আর plan search

Query Optimization

Optimizer একটা search engine: কোটি সম্ভাব্য plan-এর ভেতর থেকে cost model-এর চোখ দিয়ে একটা বাছে — System-R থেকে geqo পর্যন্ত সেই খোঁজের architecture।

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

  • Optimization-কে search problem হিসেবে frame করে বলতে পারবেন — plan space কত বড়, আর equivalence rules (commutativity, associativity, selection pushdown, transitive predicates) কীভাবে space ছোট করে
  • Cost model-এর component-গুলো — seq_page_cost, random_page_cost, cpu_tuple_cost — এর মানে আর SSD-যুগে random_page_cost কমানোর logic ব্যাখ্যা করতে পারবেন
  • Startup cost বনাম total cost-এর পার্থক্য দেখিয়ে LIMIT কেন কখনো মোট খরচ বেশি plan-ই বাছে তা উদাহরণসহ দেখাতে পারবেন
  • System-R dynamic programming bottom-up join enumeration হাতে trace করতে পারবেন, interesting orders ধারণা সহ — আর wide join-এ greedy/geqo fallback কেন দরকার
  • PostgreSQL-এ random_page_cost flip, geqo_threshold, join_collapse_limit experiment করে planner-এর decision নিজের হাতে ঘুরিয়ে দেখাতে পারবেন

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

আগে এটা বুঝি

আজ পর্যন্ত আমরা executor-এর অস্ত্রাগার ঘুরেছি — scan, aggregate, তিন join algorithm। কিন্তু প্রশ্নটা ঝুলেই ছিল: কে ঠিক করে কোন অস্ত্র কখন? উত্তর: planner, বা তার সম্মানিত নামে — query optimizer।

এটা database-এর সবচেয়ে গর্বিত আর সবচেয়ে অভিশপ্ত component। গর্ব: 1979-এর Selinger-এর design আজও স্বীকৃতি পায়। অভিশাপ: production-এর অর্ধেক “database slow” complaint-এর শিকড় এই module-এর একটা ভুল অনুমানে।

মূল insight একটাই: optimizer উত্তর compute করে না, সিদ্ধান্ত নেয় — কোটি সম্ভাব্য plan-এর মধ্যে থেকে একটার অনুমান-করা খরচ সবচেয়ে কম। algorithms module-এর ভাষায় এটা pure search problem: state = partial plan, move = একটা transformation, evaluation = <Term id="asymptotic-notation">asymptotic notation</Term> না, concrete cost number।

আজকের map:

  • Plan space কত বিশাল, equivalence rules কীভাবে সেটা লাগাম দেয়
  • Cost model খোলা: page cost, CPU cost, startup-vs-total
  • System-R <Term id="dynamic-programming">dynamic programming</Term> — bottom-up enumeration আর interesting orders
  • Wide join-এ <Term id="greedy-algorithm">greedy</Term> আর genetic algorithm (geqo)
  • Experiment: random_page_cost flip করে planner-কে উল্টে দেওয়া

মূল ধারণা

Search problem — plan space কত বড়?

n-table join-এর ভিন্ন ভিন্ন binary-tree shape সংখ্যা Catalan number-এ বাড়ে — ১০ table-এ ৪৮৬২ bushy tree, ২০-তে ৬৫ কোটির ওপরে। তার ওপর প্রতিটা relation-এর access path (seq/index/bitmap) × প্রতিটা edge-এ join algorithm (NL/hash/merge)। গুণফল astronomical — exhaustive search অসম্ভব।

তাই strategy তিন স্তরে:

  1. Equivalence rules — কোন transformations ফলাফল না বদলে plan বদলায়; এরাই বৈধ চালের তালিকা:
    • Join commutativity: A ⋈ B ≡ B ⋈ A (কোন side outer/build?)
    • Join associativity: (A ⋈ B) ⋈ C ≡ A ⋈ (B ⋈ C) — join order-এর মূল স্বাধীনতা
    • Selection pushdown: σ যত নিচে সম্ভব — ছোট intermediate আগে বানাও
    • Transitive predicates: a.id = b.id AND a.id = 42 ⇒ b.id = 42 — এক filter দুই জায়গায় নামে
  2. Pruning — সব বৈধ plan explore করা হয় না; DP প্রতি subset-এ শুধু cheapest রাখে (interesting orders বাদে)
  3. Heuristic fallback — subset বড় হলে greedy/genetic; perfect না, সময়মতো উত্তর দেওয়াই লক্ষ্য

Cost model — অনুমানের গণিত

PostgreSQL-এর cost unit arbitrary (default: এক sequential page read = 1.0):

ConstantDefaultমানে
seq_page_cost1.0পরপর page read-এর baseline
random_page_cost4.0এলোমেলো page read — spin-disk seek-এর legacy
cpu_tuple_cost0.01এক tuple process করার CPU
cpu_index_tuple_cost0.005index entry পড়া
cpu_operator_cost0.0025এক operator/function call

Plan cost = Σ (estimated pages × page-weight + estimated tuples × cpu-weight) — প্রতিটা node নিজের child-দের cost গুলে উঠে আসে। random_page_cost এখানেই drama: NVMe-তে random আর sequential read-এর gap ৪× না, ১.x× — তাই SSD system-এ 1.1 করা common tuning। Planner-এর চোখে পৃথিবীটা এই চারটা সংখ্যা দিয়েই তৈরি।

Startup vs total — LIMIT-এর প্রিয় ফাঁদ

EXPLAIN-এর cost=a..b-তে a = startup (প্রথম row দিতে খরচ), b = total (সব row)। Sort-এর startup বিশাল (সব input শেষ না হলে একটাও row দেয় না); index scan-এর startup প্রায় শূন্য।

LIMIT 10 চাইলে planner জানে — total-ই দেখার দরকার নেই, প্রথম ১০ row-ই জীবন। তখন low-startup index plan জিতে যায়, এমনকি full-result cost-এ সে sort-এর চেয়ে দামি হলেও। Cost-এর এই দুই-মাত্রা model-ই cursor/pagination workload-এ অনেক “অদ্ভুত” plan-এর ব্যাখ্যা।

System-R DP — bottom-up enumeration

Selinger-এর 1979 scheme, আজও PG-র হৃৎপিণ্ড:

level 1:  প্রতিটা relation-এর best access path
level 2:  প্রতি pair — দুই level-1 plan-এর সব বৈধ join-এর cheapest
level 3:  pair + singleton …
…
level n:  পূর্ণ join-এর cheapest plan

Subsets-এর ওপর DP: best(S) = S-subset-দুইভাগে-ভাঙা সব উপায়ের minimum। Complexity exponential কিন্তু pruning-এ সামলানো — আর বিনিময়ে optimal join order guarantee (model সঠিক হলে)।

Interesting orders: শুধু cheapest plan না রেখে, দামি কিন্তু useful ordering-ও রাখে — GROUP BY dept-এর জন্য dept-sorted intermediate পরে sort বাঁচায়। প্রতি subset-এ একাধিক “Pareto-optimal” candidate — DP-র memory এখানেই খরচ হয়।

Left-deep বনাম bushy: System-R শুধু left-deep (pipeline-friendly, এক side সর্বদা base relation) ঘুরত; আধুনিক PG সব partition-split (bushy সহ) দেখে — যতক্ষণ না geqo দখল নেয়।

Wide join — greedy আর genetics

DP-র exponential curve ৯–১০ table-এ practicality-র সীমা ছোঁয়। PG-র সমাধান: geqo_threshold (default 12) পার হলে Genetic Query Optimizer — plan-গুলো chromosome, crossover-mutation দিয়ে search; optimal-এর guarantee নেই, কিন্তু সময় bounded। মাঝামাঝি নিয়ন্ত্রণে join_collapse_limit — এর ওপরের relation সংখ্যায় planner explicit join order না ভেঙে দেয়।

Subquery unnesting আর CTE-র ইতিহাস

Rewrite phase-এর সবচেয়ে দামি কাজ subquery handling: correlated EXISTS-কে semi-join-এ নামানো, scalar subquery-কে join-এ। আর CTE (WITH) — version 12-র আগে PG এটাকে optimization fence হিসেবে সর্বদা materialize করত: planner CTE-র ভেতরে pushdown করত না। v12 থেকে নিয়ম: single-reference, side-effect-free CTE inline হয় — যতক্ষণ না আপনি AS MATERIALIZED লিখে fence চেয়ে নেন (recursive বা multiple-reference CTE তবু fence-ই)। পুরনো tuning blog-এর অর্ধেক advice v12-এর পরে বাতিল — history মনে রাখুন।

ভেতরে কী ঘটছে

Planner-এর প্রকৃত হাঁটা

standard_planner-এর path: rewrite-হওয়া Query tree → make_one_rel (base access paths) → standard_join_search (subset-DP) → cheapest path → Plan tree। DP-র ভেতরে প্রতিটা subset-এ:

for each valid split (S₁, S₂) of subset S:
    for each join algorithm applicable to (S₁, S₂):
        cost = best(S₁).cost + best(S₂).cost + join_cost(sizes, algo)
        keep cheapest (per interesting-order class)

join_collapse_limit-এর আসল অর্থ: FROM clause-এ ৮-table explicit join লিখলে এবং limit=7 হলে planner প্রথম ৭টার order free ঘুরায়, বাকিগুলো লেখা ক্রমেই আটকে যায় — মানে আপনার parenthesization তখন অর্থবহ। Tuning tool হিসেবে এটাই তার কাজ।

Plan instability-র anatomy। Autovacuum রাত ৩টায় ANALYZE চালাল — histogram বদলাল — সকালে production plan flip — dashboard ডুবল। Cost model deterministic কিন্তু তার input (statistics) variable। Prepared statement-এ আরেক স্তর: প্রথম ৫ execution custom plan (actual parameter values দিয়ে), তারপর generic plan-এ switch — মাঝে মাঝে সেই switch-ই performance cliff; plan_cache_mode দিয়ে control।

Model-এর সীমা। Cost formula-গুলো linear-ish approximation; cache effect, correlation, skew-এর জটিলতা ধরে না। Model আর reality-র gap-ই cardinality estimation-এর দায় — পরের লেসনের পুরো বিষয়।

উদাহরণ

হাতে-কলমে DP — তিন relation

Data: A = 1,000 rows; B = 1,000; C = 1,000,000। Pairwise selectivity: sel(A⋈B) = 0.99 (খারাপ — প্রায় cartesian!), sel(B⋈C) = 0.000000001 (উৎকৃষ্ট key), sel(A⋈C) = 0.99। Cost model সরল: প্রতিটা join node তার দুই input scan করে — node-cost = |left| + |right|; tree-cost = সব node-cost-এর যোগফল। Result size = |L|·|R|·sel।

Level 1:   best({A}) = 1,000     best({B}) = 1,000     best({C}) = 1,000,000

Level 2:   best({A,B}):  (A⋈B) cost = 2000,      size = 990,000
           best({B,C}):  (B⋈C) cost = 1,001,000, size = 1        ← দামি কিন্তু ফল এক-row!
           best({A,C}):  (A⋈C) cost = 1,001,000, size = 990,000

Level 3 — প্রতিটা split দেখুন:
  ({A,B}) ⋈ C : cost(AB)=2000 + (990,000 + 1,000,000) = 1,992,000   size = 990
  ({B,C}) ⋈ A : cost(BC)=1,001,000 + (1 + 1,000)      = 1,002,001   ← OPTIMAL!
  ({A,C}) ⋈ B : cost(AC)=1,001,000 + (990,000,000 + 1,000) = 991,002,000

DP-র winner: আগে দামি B⋈C, তারপর ছোট result-টা A-র সাথে — total 1,002,001।

এবার <Term id="greedy-algorithm">greedy</Term> — “প্রতি ধাপে ছোট দুই group জোড়া” heuristic: প্রথম ধাপেই A,B জোড়া (ছোট দুই input!) → 990-row intermediate না, 990,000-row monster! পরের ধাপে C জুড়লে total = 1,992,000 — optimal-এর প্রায় দ্বিগুণ।

এটাই counterexample culture: local choice-এর মোহনীয়তা বনাম global search-এর শৃঙ্খলা। Build section-এ এই হিসাবটা code-এ প্রমাণ করবেন।

ছবিতে

Subset lattice — DP যে জগতে হাঁটে

                        {A,B,C,D}          ← চূড়ান্ত উত্তর
                       /   |    |   \
                 {A,B,C} {A,B,D} {A,C,D} {B,C,D}   ← level 3
                  / \  \   /  \   ...
             {A,B} {A,C} {A,D} {B,C} {B,D} {C,D}   ← level 2
               |     |     |     |     |     |
              {A}   {B}   {C}   {D}              ← level 1: access paths

split rule: প্রতিটা subset S-এর জন্য সব (S₁,S₂) partition,
S₁∩S₂=∅, S₁∪S₂=S — বাঁ দিকের প্রতিটা নোড থেকে দুই-ভাগের সব পথ।
৪-relation join-এর subset lattice — নিচ থেকে উপরে DP-র ভর্তি-পথ; প্রতি node-এ শুধু cheapest (আর interesting-order) plan বেঁচে থাকে।

Optimizer pipeline-এর পুরো পথটা:

Rewritten tree থেকে chosen plan — planner-এর কারখানা
  1. Base pathsপ্রতিটা relation-এর জন্য seq/bitmap/index candidates + cost
  2. Subset DPbottom-up enumeration, প্রতি subset-এ cheapest + interesting orders
  3. Algorithm selectionপ্রতি join-edge-এ NL/hash/merge cost তুলনা
  4. Top-level choicessort/hash agg, window, LIMIT-এর startup-aware pruning
  5. Path → Planwinning path tree-কে executable Plan tree-তে নামানো

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

EXPERIMENT

random_page_cost flip, geqo_threshold, join_collapse_limit — planner-কে জোর করে ঘোরানো

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

ধাপ ১ — borderline dataset (flip দেখার মূল কৌশল):

CREATE TABLE events (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  kind int NOT NULL,
  payload text
);
INSERT INTO events (kind, payload)
SELECT g % 20, md5(g::text)
FROM generate_series(1, 400000) g;
CREATE INDEX events_kind_idx ON events(kind);
ANALYZE events;

kind-এর ৫%-selectivity ইচ্ছাকৃত — random_page_cost=4-এ seq-vs-index প্রায় tie। এবার flip:

SET random_page_cost = 4.0;
EXPLAIN SELECT * FROM events WHERE kind = 7;

SET random_page_cost = 1.1;          -- SSD-যুগের মান
EXPLAIN SELECT * FROM events WHERE kind = 7;
rpc=4.0:  Bitmap Heap Scan / কখনো Seq Scan      cost=... (borderline)
rpc=1.1:  Index Scan using events_kind_idx       cost=...  ← random read সস্তা মনে হলেই index জেতে

Plan-এর cost line-এর পরিবর্তনটা খাতায় লিখুন — একই data, একই query, শুধু এক constant। Production SSD-তে ALTER SYSTEM SET random_page_cost = 1.1 ঠিক এই কারণেই করা হয়।

ধাপ ২ — startup vs total আর LIMIT:

RESET random_page_cost;
EXPLAIN (COSTS ON) SELECT id FROM events ORDER BY id LIMIT 3;
EXPLAIN (COSTS ON) SELECT id FROM events ORDER BY id;

LIMIT-ছাড়া plan-এর total আর LIMIT-সহ plan-এর total তুলনা করুন — LIMIT-এ planner low-startup index path-ই বেছে নেয়; দুই cost-এর গল্পটা লিখে ফেলুন।

ধাপ ৩ — geqo আর collapse limit (১২-table):

-- ১২টা ছোট table, chain-join
DO $$ BEGIN
  FOR i IN 1..12 LOOP
    EXECUTE format('CREATE TABLE t%s AS SELECT g AS id FROM generate_series(1,100) g', i);
  END LOOP; END $$;

SET geqo_threshold = 2;      -- force GEQO (min value 2)
EXPLAIN ANALYZE SELECT count(*)
FROM t1 NATURAL JOIN t2 NATURAL JOIN t3 NATURAL JOIN t4
     NATURAL JOIN t5 NATURAL JOIN t6 NATURAL JOIN t7 NATURAL JOIN t8
     NATURAL JOIN t9 NATURAL JOIN t10 NATURAL JOIN t11 NATURAL JOIN t12;

SET geqo_threshold = 13;     -- force DP
EXPLAIN ANALYZE SELECT count(*) /* একই query */;

দুই run-এর Planning Time footer তুলনা করুন — GEQO planning দ্রুত কিন্তু plan quality lottery। তারপর:

SET geqo_threshold = 13;
SET join_collapse_limit = 1;   -- লেখা order রক্ষা
EXPLAIN SELECT ... /* একই ৮-table join */ ;

Collapse limit=1-এ planner আর স্বাধীনভাবে reorder করতে পারে না — plan shape লেখা sequence-এর দাস। এই switch তিনটাই আপনার emergency toolkit: plan ভুল মনে হলে আগে cost constant, তারপর collapse limit, শেষ অস্ত্র geqo off।

এটা কী প্রমাণ করে

একটা cost constant বদলে plan উল্টে যায় (seq ↔ index), বড় join-এ geqo planning-time বদলায়, আর join_collapse_limit লেখা order রক্ষা করে — cost model-ই planner-এর steering, তা direct observation-এ প্রমাণিত।

নিজে বানান

BUILD IT

Join-order DP enumerator — optimal খোঁজা আর greedy-র counterexample প্রমাণ

Python · ●●●○○
  1. Relations-এর size + pairwise selectivity function define করুন
  2. Subset-bitmask DP — প্রতি subset-এ best (cost, size) রাখুন, সব partition split try
  3. Bushy naturally আসবে — partition split মানেই দুই দিকেই composite allowed
  4. Top-3 distinct total cost + তাদের tree description প্রিন্ট করুন
  5. Greedy (smallest-pair-first) চালিয়ে skewed case-এ DP-র সাথে পার্থক্য প্রমাণ করুন

লক্ষ্য: System-R-এর core loop হাতে লেখা — ছয় relation পর্যন্ত exhaustive, আর Selinger-এর insight-এর সবচেয়ে ছোট সম্পূর্ণ প্রমাণ।

from itertools import combinations

# ---------- input model ----------

SIZE = {'A': 1_000, 'B': 1_000, 'C': 1_000_000}

SEL = {frozenset({'A', 'B'}): 0.99,        # খারাপ জোড়া — প্রায় cartesian
       frozenset({'B', 'C'}): 1e-9,        # চমৎকার key join → ফল এক-row
       frozenset({'A', 'C'}): 0.99}
DEFAULT_SEL = 0.05                          # অজানা pair-দের অনুমান

def cross_sel(left_grp, right_grp):
    """দুই group-এর মধ্যে জানা সবচেয়ে শক্ত link — conservative estimate"""
    known = [s for p in ((a, b) for a in left_grp for b in right_grp)
             if (s := SEL.get(frozenset(p))) is not None]
    return min(known) if known else DEFAULT_SEL

def out_size(l_size, r_size, l_grp, r_grp):
    return max(1, round(l_size * r_size * cross_sel(l_grp, r_grp)))

def join_node_cost(l_size, r_size):
    return l_size + r_size      # node = দুই input-এর scan; leaf-এর নিজের cost ০

# ---------- System-R style DP over bitmasks ----------

NAMES = sorted(SIZE)
FULL = (1 << len(NAMES)) - 1

def splits(mask):
    """mask-এর সব (ছোট,বড়) partition — প্রতিটা ঠিক একবার"""
    sub = (mask - 1) & mask
    while sub:
        other = mask ^ sub
        if sub < other:
            yield sub, other
        sub = (sub - 1) & mask

def grp_of(mask):
    return frozenset(NAMES[i] for i in range(len(NAMES)) if mask >> i & 1)

def dp():
    best = {}                               # mask -> (cost, size, desc)
    finals = []                             # FULL mask-এর প্রতিটা candidate
    for i, n in enumerate(NAMES):
        best[1 << i] = (0, SIZE[n], n)
    for mask in range(1, FULL + 1):
        if bin(mask).count('1') < 2:
            continue
        for m1, m2 in splits(mask):
            c1, z1, d1 = best[m1]
            c2, z2, d2 = best[m2]
            cost = c1 + c2 + join_node_cost(z1, z2)
            desc = f'({d1}⋈{d2})'
            if mask == FULL:
                finals.append((cost, desc))
            if mask not in best or cost < best[mask][0]:
                best[mask] = (cost, out_size(z1, z2, grp_of(m1), grp_of(m2)),
                              desc)
    top3 = sorted({c: d for c, d in finals}.items())[:3]
    return best[FULL], top3

# ---------- greedy baseline: প্রতি ধাপে ছোট দুই group ----------
# (selectivity পুরো উপেক্ষা — ঐতিহাসিক naive heuristic)

def greedy():
    groups = {frozenset(n): (SIZE[n], n) for n in NAMES}
    total = 0
    while len(groups) > 1:
        cand = [(v1[0] + v2[0], g1, g2)
                for (g1, v1), (g2, v2)
                in combinations(list(groups.items()), 2)]
        _, g1, g2 = min(cand)
        z1, d1 = groups.pop(g1)
        z2, d2 = groups.pop(g2)
        total += join_node_cost(z1, z2)
        groups[g1 | g2] = (out_size(z1, z2, g1, g2), f'({d1}⋈{d2})')
    size, desc = groups[frozenset(NAMES)]
    return total, size, desc

if __name__ == '__main__':
    (c, s, d), top3 = dp()
    gc, gs, gd = greedy()
    print(f'DP optimal : cost={c:>12,}  final-size={s:>9,}  {d}')
    print('top-3 distinct plan costs:')
    for rank, (cc, dd) in enumerate(top3, 1):
        print(f'  #{rank} cost={cc:>12,}  {dd}')
    print(f'greedy     : cost={gc:>12,}  final-size={gs:>9,}  {gd}')
    print(f'greedy খরচ DP-র {gc / c:.2f}x — local choice-এর দাম')

প্রত্যাশিত output (skew-crafted data):

DP optimal : cost=   1,002,001  final-size=      990  (A⋈(B⋈C))
top-3 distinct plan costs:
  #1 cost=   1,002,001  (A⋈(B⋈C))
  #2 cost=   1,992,000  ((A⋈B)⋈C)
  #3 cost= 991,002,000  (B⋈(A⋈C))
greedy     : cost=   1,992,000  final-size=      990  (C⋈(A⋈B))
greedy খরচ DP-র 1.99x — local choice-এর দাম

Greedy প্রথম ধাপেই A,B জুড়েছে — ছোট দুই group, মোহনীয় local view! Result ৯.৯ লক্ষ-row intermediate, তার ওপর C-র পূর্ণ scan। DP জানত B⋈C-র দামি প্রথম ধাপ পরে এক-row-র ফসল দেয়, আর শেষ join তখন প্রায় free। Bushy tree-ও naturally এসেছে — split দুই side-ই composite হতে পারে।

নিজে বাড়ান:

  1. Interesting orders: প্রতি subset-এ (cheapest, dept-sorted-cheapest) দুই candidate রাখুন — GROUP BY reuse simulate করুন
  2. Left-deep-only mode toggle — bushy বনাম left-deep-এর optimal-gap মাপুন
  3. Relation ৭-৮-এ DP runtime মাপুন — exponential wall নিজের stopwatch-এ

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

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

Enterprise engines। SQL Server-এ Cascades-style framework, Greenplum-এ Orca, Calcite-ভিত্তিক বহু engine — সবাই একই সমস্যা: plan space, cost, pruning। Volcano/Cascades-এর rule-engine abstraction ঠিক আজকের equivalence rules-এর industrial রূপ।

Cloud scale-এ plan regression। Snowflake/Databricks-এ query history দেখে plan-drift alert — কারণ statistics বদলালেই plan বদলায়। PostgreSQL-এ সেই কাজ manual: pg_stat_statements + auto_explain diff।

Hint-এর দর্শন। Oracle-এর hint হাজার; PG-র “fix the model” দর্শন বহু outage-এ ভালো প্রমাণিত — কিন্তু migration project-গুলোতে pg_hint_plan জীবনরক্ষক। দুই philosophy-ই বৈধ trade-off।

Machine learning optimizers। Bao, Lero-জাতীয় research: learned cost models আর plan competition — classic DP skeleton অটুট, শুধু evaluator smarter হচ্ছে।

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

“Optimizer সবসময় সবচেয়ে দ্রুত plan বাছে।”

সে বাছে cost model-এর চোখে estimably cheapest plan — model-এর input ভুল (statistics বাসি, skew অদৃশ্য) হলে সে নিশ্চিন্তে ভুল plan বাছে, পূর্ণ confidence-এ। Guarantee-টা model-এর সাপেক্ষে, clock-এর সাপেক্ষে না।

“Cost কম plan সবসময় দ্রুত চলে — তাই cost তুলনাই যথেষ্ট benchmark।”

Cost একটা linear approximation — cache locality, parallelism, JIT effects এর বাইরে। দুই plan-এর cost difference ২০% হলে actual time উল্টোও হতে পারে। Decision-এর জন্য cost, verification-এর জন্য EXPLAIN ANALYZE-এর actual time — দুটোই লাগবে।

“GEQO আরও smart — তাই বড় join-এ better plan দেয়।”

GEQO হলো speed-এর জন্য quality-র আপস: genetic search bounded সময়ে decent plan দেয়, optimal-এর কোনো দাবি নেই — সাধারণত DP-র চেয়ে খারাপ plan, দ্রুত planning। Threshold কমিয়ে GEQO “enable” করা optimization upgrade না, বরং জরুরি ব্যবস্থা।

“CTE সবসময় materialize হয় — তাই প্রতিবার একই ফল পাওয়া যায়।”

PG 12-র পর থেকে single-reference CTE inline হয় — volatile function থাকলে বারবার execute হতে পারে! Deterministic snapshot চাইলে স্পষ্টভাবে WITH x AS MATERIALIZED (...) লিখতে হবে। পুরনো behavior-এর ওপর লেখা tuning advice আজ ভুল-প্রমাণ।

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

1

চারটা equivalence rule-এর নাম আর এক-লাইনে প্রত্যেকটার লাভ বলুন।

স্মরণ

Commutativity (build/probe side swap), associativity (join order freedom), selection pushdown (intermediate আগে ছোট), transitive predicates (এক filter দুই leaf-এ নামে) — চারটাই ফল অক্ষুণ্ণ রেখে plan space-এ নতুন নতুন region খোলে।

2

random_page_cost 4.0 → 1.1 করলে ঠিক কোন গণিত বদলায় যে index plan হঠাৎ জিতে যায়?

প্রয়োগ

Index plan-এর cost-এ random page read-গুলোর weight 4→1.1-এ নামে — যেখানে seq scan পুরোটাই weight-1 pages। Borderline selectivity-তে index-এর অনুমিত খরচ প্রায় অর্ধেক — তাই tie ভেঙে index জেতে। Data বদলায়নি, পৃথিবীর model বদলেছে।

3

LIMIT 10 কেন মোট cost বেশি এমন plan বাছতে পারে? Startup-total distinction ছাড়া এই সিদ্ধান্ত অসম্ভব কেন?

যুক্তি

Client ১০ row-ই চায় — প্রথম row দেওয়ার খরচ (startup) তখনই দেখার বিষয়। Index scan startup≈0; sort-এর startup = পূর্ণ input। এক-মাত্রা total-cost model-এ দুই plan-এর এই তফাত invisible — LIMIT-এর optimization-এর জন্য দুই-মাত্রা cost essential।

4

Interesting orders DP-র complexity বাড়ায় কেন? কোন query pattern-এ এর লাভ সর্বোচ্চ?

যুক্তি

প্রতি subset-এ এক না, একাধিক Pareto candidate (cheapest, প্রতিটা useful ordering-এর সস্তাতম) রাখতে হয় — state space গুণিতক। লাভ সর্বোচ্চ যেখানে downstream-এ ORDER BY/GROUP BY/merge-join ordering লাগবে — sort node পুরোটাই বাদ, খরচ বহুগুণ কম।

5

join_collapse_limit = 1 আর geqo_threshold = 2 — দুটোই “planner-এর স্বাধীনতা কমায়”? পার্থক্য কোথায়?

প্রয়োগ

Collapse-limit কমালে planner আপনার লেখা order-ই মানে — নিজের সিদ্ধান্ত ত্যাগ করে determinism দেয়। Geqo-threshold কমালে উল্টো planner আরও বেশি query-তে heuristic search-এ যায় — স্বাধীনতা তখনও আছে, শুধু exhaustive guarantee থাকে না। একটা control আপনার হাতে, অন্যটা algorithm-এর হাতে।

6

Production-এ রাতারাতি এক report query ধীর হয়ে গেল; কোড অপরিবর্তিত। Optimizer-এর দৃষ্টিতে তিনটা suspect আর প্রতিটার যাচাই-পদ্ধতি?

ডিজাইন

Suspect ১: statistics বদলেছে (autovacuum ANALYZE) — যাচাই: পুরনো/নতুন plan diff (auto_explain log), প্রয়োজনে সেই সময়ের pg_stats। Suspect ২: prepared statement generic-plan switch — যাচাই: plan_cache_mode=force_custom_plan দিয়ে test। Suspect ৩: cost GUC change (config reload/migration) — যাচাই: SHOW values আর git-history of config। তিনটাই model-এর input বদলায়, query নিজে না।

এরপর কী

এরপর কী

আজ দেখলাম optimizer কীভাবে সিদ্ধান্ত নেয় — কিন্তু তার প্রতিটা সিদ্ধান্তের পেছনে একটা অনুমান লুকানো: “এই filter-এ ৫% tuple টিকবে”, “এই join-এ ১০০ row হবে”। সেই অনুমান বানায় কে? ভুল হলে কী বিপদ?

পরের লেসনে statistics আর cardinality estimation:

  • Histogram (equi-depth bucket), MCV list, n_distinct — pg_stats-এর প্রতিটা field খোলা
  • Correlation stat — physical clustering কীভাবে index-vs-seq সিদ্ধান্ত ঘুরায়
  • Estimation error-র multiplicative compounding — একটা ভুল input কীভাবে পুরো plan poison করে
  • Extended statistics — correlated column-এর ফাঁদ থেকে মুক্তি

আজকের flip-experiment-এ দেখা plan-turn আসলে statistics-এরই দূরবর্তী প্রভাব — পরের লেসনে সেই উৎসমুখে যাত্রা।

আরও পড়ুন

  • Access Path Selection in a Relational Database Management System (SIGMOD 1979) — Patricia Selinger et al., IBM San Jose · System-R optimizer-র original paper — dynamic programming join enumeration-এর জন্মসনদ
  • PostgreSQL Documentation — Genetic Query Optimizer (GEQO) — PostgreSQL Global Development Group · geqo_threshold আর genetic search-এর official semantics
  • PostgreSQL Documentation — Query Planning: Planner Cost Constants — PostgreSQL Global Development Group · seq_page_cost/random_page_cost/cpu_* constant-দের প্রামাণ্য ব্যাখ্যা