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

Join Algorithms — nested loop, hash, merge

Join Algorithms

একই join তিনভাবে করা যায় আর তিনভাবের খরচ দশগুণে দশগুণ আলাদা — nested loop, hash, merge-এর গণিত, আর PostgreSQL-এ প্রতিটাকে জোর করে চালিয়ে মাপার পদ্ধতি।

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

  • Naive, block-nested আর index-nested loop join-এর পার্থক্য buffer count B দিয়ে IO সূত্রে হিসাব করে দেখাতে পারবেন — কোন parameter বদলালে কোনটা জেতে
  • Hash join-এর build-probe দুই phase আর memory overflow-এ grace-hash partitioning (আর recursive hashing)-এর কাজকর্ম trace করতে পারবেন
  • Sort-merge join কখন pre-sorted input-এ সবাইকে হারায়, duplicate-এর equi-zone handling কীভাবে করে — উদাহরণ দিয়ে বলতে পারবেন
  • Data size, memory আর sortedness দেখে তিন algorithm-এর মধ্যে সঠিকটা বাছাইয়ের decision tree নিজে আঁকতে পারবেন — inequality join-এর ব্যতিক্রম সহ
  • PostgreSQL-এ enable_* switches আর work_mem দিয়ে planner-কে জোর করে প্রতিটা join চালিয়ে EXPLAIN ANALYZE timing তুলনা করতে পারবেন

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

আগে এটা বুঝি

গত লেসনে আমরা দেখলাম SQL text কীভাবে plan tree হয়ে executor-এ পৌঁছায়। সেই tree-র node-গুলোর মধ্যে একটা জাত সবসময় সবচেয়ে বেশি সময় খায়: join। Production-এর প্রায় প্রতিটা ধীর query-র ভেতরে একটা mis-chosen join node থাকে।

সংজ্ঞা মৌলিক: relational algebra-র join definition matching tuples-এর pair — কিন্তু হিসাব করার পথ অনেক, আর পথের পার্থক্য সহজেই হাজার গুণ। একই ২-table join এক algorithm-এ ০.১ সেকেন্ড, আরেকটায় ৯০ সেকেন্ড — data এক, hardware এক, শুধু strategy আলাদা।

আজকের তিন নায়ক আর তাদের রাজত্ব:

  • Nested loop family — brute force থেকে buffer-aware block version, তারপর index-এর জাদু
  • Hash join — ছোট side দিয়ে hash table, বড় side stream; memory ফুরালে grace partitioning
  • Sort-merge join — sorted input-এর রাজা, duplicate-এর ধৈর্যশীল handler

শেষ পর্যন্ত PostgreSQL-এ প্রতিটাকে জোর করে চালিয়ে নিজের হাতে timing মাপবেন — planner-এর decision আর বাস্তব clock-এর মিল নিজের চোখে।

মূল ধারণা

Nested loop — ভিত্তি থেকে পরিশীলন

Naive: বাইরের table-এর প্রতিটা tuple-এর জন্য ভেতরের table-এর সব tuple ঘেঁটে মেলান — <Term id="asymptotic-notation">asymptotic notation</Term>-এর ভাষায় O(T_R · T_S) tuple-pair। Page-স্তরে আরও খারাপ: প্রতিটা outer page-এ পুরো inner table আবার পড়া — disk বারবার ঘুরবে।

Block-nested loop: buffer pool-এর জ্ঞান কাজে লাগাও। B-টা buffer frame থাকলে B−2 টায় outer-এর block, ১-এ inner-এর এক page, ১-এ output। এবার inner scan হয় outer-এর প্রতি page-এ না, প্রতি chunk-এ একবার:

IO(R ⋈ S) = P_R + ⌈P_R / (B − 2)⌉ × P_S

Index-nested loop: ভেতরের column-এ index থাকলে ঘেঁটে দেখার দরকারই নেই — প্রতিটা outer tuple-এ শুধু index lookup (গড় C_index page):

IO ≈ P_R + T_R × C_index

ছোট outer + indexed inner (orders.customer_id-র উপর index) — এই combination-এ অন্য কোনো algorithm প্রতিযোগিতাই করতে পারে না।

Hash join — build আর probe

Equality join (a.k = b.k) হলে pair-test-ই লাগে না — <Term id="hash-function">hash function</Term>-ই address বলে দেয়:

  1. Build: ছোট side-এর প্রতিটা tuple <Term id="hash-table">hash table</Term>-এ key-ভিত্তিক bucket-এ ঢোকান — memory-তে পুরোটা আটকে গেলে best case
  2. Probe: বড় side stream করুন — প্রতিটা tuple hash করে bucket lookup; match মিললে output

খরচ: দুই side একবারই পড়া + hash table-এর CPU — IO-তে সর্বনিম্ন সম্ভাব্য। শর্ত একটাই: build side memory-তে আটকাতে হবে।

Memory ফুরালে — grace hash: দুই side-কেই একই hash function দিয়ে k-টা partition-এ ভাগ করে disk-এ লিখুন। Key মিলতে হলে দুই tuple-এর একই partition-এ পড়া বাধ্যতামূলক — তাই partition i ↔ partition i join করলেই সব match। Partition-ও বড় হয়ে গেলে? Recursive hashing — সেই partition-কে আবার ভিন্ন seed-এ ভাঙা। PostgreSQL-এর ভাষায় EXPLAIN-এ Hash Batches: 8 — ঠিক এই ভাগটা।

Sort-merge join — সাজানো জগতের বিবাহ

দুই side-ই join key-তে sorted হলে একবার সমান্তরাল walk-ই যথেষ্ট: দুই pointer, ছোটটা advance। Sortedness আসে তিনভাবে — explicit sort node, অথবা index scan যা আগে থেকেই sorted order দেয় (তৃতীয়টাই merge-এর আসল জয়: sort খরচ শূন্য)।

Duplicate-এর সমস্যা: এক key-র left-run × right-run পুরোটা output — একে বলে equi-zone; right pointer zone শেষে ফিরে পড়তে হয়। Extreme skew-তে (সব tuple এক key) merge হারে, nested-loop-ও হারে — hash-ও এক bucket-এ ডুবে যায়; skew-ই সব algorithm-এর common enemy।

Decision table — কে কখন

অবস্থাWinnerকারণ
ছোট outer, indexed innerIndex NLপ্রতি outer tuple-এ সস্তা lookup
বড় দুই table, equality, memory ঠিকHash joinদুই side একবারই পড়া
Input already sorted (index/order)Sort-mergesort খরচ শূন্য, streaming
Memory ছোট, data বিশালMerge বা batched hashদুটোই disk-friendly
Inequality join (a.x range vs b.y)শুধু nested loop familyhash/merge equality-only

ভেতরে কী ঘটছে

PostgreSQL-এর হাতে-কলমে বিবরণ

enable_* switches নিষেধ না — ভয় দেখানো। SET enable_hashjoin = off planner-কে বলে দেয় ওই path-এর cost-এ বিশাল penalty জুড়ে নিতে; অন্য কোনো বৈধ path না থাকলে সে-ই আবার hash join-ই বেছে নেবে। Point query-তে (unique index + equality) সব switch off করলেও nested loop-ই ফেরে — alternative নেই। তবু tuning/experiment-এ এরাই steering wheel।

work_mem আর Hash Batches-এর গণিত। Hash node build table-এর জন্য বাজেট পায় work_mem — মনে রাখুন, per node, per operation। Build size বাজেট ছাড়ালে batches:

Batches ≈ ⌈ build_size / work_mem ⌉   (power-of-two-তে rounded)

প্রতিটা batch temp file-এ; probe-এর tuple নিজের batch-এ route হয়। EXPLAIN (ANALYZE, BUFFERS)-এ temp written=/temp read= spill-এর সাক্ষ্য। Parallel query-তে subtlety: প্রতিটা worker নিজের work_mem — মোট memory ভেবে বসে থাকলে চমক অবধারিত।

Merge-এর শর্ত। PG merge join-এ দুই input-এই নির্দিষ্ট sort ordering লাগে; না থাকলে plan-এ Sort node ঢোকে — merge-এর সুবিধা তখন sort-এর ঋণে ডুবে যায়। B-tree index scan ordering free দেয় — indexed column-এর merge প্রায়ই হঠাৎ জেতে এজন্যই।

Parallel variants। Parallel Seq Scan-এর ওপর Parallel Hash Join (shared hash table — Parallel Hash node) আর parallel-aware nested loop; বড় analytics plan-এ এদের নাম দেখলে চমকাবেন না।

উদাহরণ

পূর্ণ worked example — সংখ্যা দিয়ে

R = customers: 10,000 tuple, 100 page। S = orders: 500,000 tuple, 5,000 page। Usable buffers B = 52। Inner column-এ index (lookup গড় 3 page)।

১. Naive NL (page-level):   P_R + P_R×P_S        = 100 + 100×5000  = 500,100 reads
২. Block-NL (chunk=50):     P_R + ⌈100/50⌉×P_S   = 100 + 2×5000    = 10,100 reads   ← ৪৯× কম
৩. Index-NL:                P_R + T_R×C_idx      = 100 + 10000×3   = 30,100 reads
৪. Hash join:               P_R + P_S            = 100 + 5000      =  5,100 reads (+CPU)
৫. Sort-merge:              sort(R)+sort(S)+P_R+P_S — sorting-ই dominant term

Reading: বড় unsorted join-এ hash champion। কিন্তু outer 50-tuple হলে? Index-NL: 100 + 50×3 = 250 — প্রায় শূন্য। Outer-এর size formula ঘুরিয়ে দেয়। আর orders যদি (customer_id) index থেকে আসে (sorted free), merge হঠাৎ competitive। একটা worked example-ই সার: absolute সেরা algorithm নেই — parameter space-এ প্রতিটার নিজের রাজ্য।

ছবিতে

Hash join lifecycle + decision ladder

             BUILD PHASE                        PROBE PHASE
             ────────────                       ────────────
smaller side                          larger side (streamed)
 ─────────────┐                              │
              ▼                              ▼
        ┌───────────┐    h(k)         ┌────────────┐
        │hash table │◀───buckets──────│ per-tuple  │
        │(memory)   │    ┌────────┐   │ h(k)+lookup│
        └───────────┘    │ match? ├───┴─────┬──────┘
                         │ yes→out│         │ no→skip
                         └────────┘

memory-তে ধরা না গেলে (grace):
both sides ──▶ h₁(k) mod k ──▶ P₀..P₄ (disk)
                                │
                  প্রতি partition-pair-এ আবার build+probe
                  (partition-ও বড় হলে recursive: h₂ seed)
Build-probe lifecycle — memory-তে আটকালে দুই pass; না আটকালে partition হয়ে disk ঘুরে আসে।

planner-এর মাথার ভেতরের সিঁড়িটা ঠিক এরকম:

Join algorithm বাছাই-এর সিঁড়ি
  1. equality predicate আছে?না হলে সরাসরি nested loop family — আর প্রশ্ন নেই
  2. outer ছোট + inner indexed?হ্যাঁ হলে index-NL প্রায় অপ্রতিরোধ্য
  3. input sorted (index/ORDER BY)?হ্যাঁ হলে merge candidate — sort খরচ শূন্য
  4. build side memory-তে?হ্যাঁ → in-memory hash; না → batches/grace partitioning
  5. cost model final comparesurvivors-দের estimated cost মিলিয়ে winner — পরের লেসনের বিষয়

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

EXPERIMENT

planner-কে জোর করে তিন join চালান, work_mem কমিয়ে spill দেখুন

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

ধাপ ১ — dataset:

CREATE TABLE cust_small (id int PRIMARY KEY, tier text);
INSERT INTO cust_small
SELECT g, CASE WHEN g % 9 = 0 THEN 'gold' ELSE 'basic' END
FROM generate_series(1, 2000) g;

CREATE TABLE orders_big (id int, cust_id int, amount numeric);
INSERT INTO orders_big
SELECT g, (random() * 1999)::int + 1, round((random() * 500)::numeric, 2)
FROM generate_series(1, 300000) g;

ANALYZE cust_small; ANALYZE orders_big;

ধাপ ২ — তিনবার, প্রতিবার একটা করে force করে:

-- A) nested loop:
SET enable_hashjoin = off; SET enable_mergejoin = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM cust_small c JOIN orders_big o ON o.cust_id = c.id;

-- B) hash join:
SET enable_nestloop = on; SET enable_mergejoin = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM cust_small c JOIN orders_big o ON o.cust_id = c.id;

-- C) merge join:
SET enable_mergejoin = on; SET enable_hashjoin = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM cust_small c JOIN orders_big o ON o.cust_id = c.id;

প্রতিটা plan-এর root-এর কাছের join node-এর নাম verify করুন (Nested Loop / Hash Join / Merge Join)। C-তে দুই side-এ Sort node এসেছে কি না দেখুন — cust_small-এর PK index হয়তো এক side-কে free ordering দিয়ে দিয়েছে। টেবিলটা আপনার মেশিনের সংখ্যায় ভরুন:

| algorithm   | execution ms | notes                          |
|-------------|--------------|--------------------------------|
| nested loop |          ___ | loops=? rows-per-loop=?        |
| hash join   |          ___ | Batches=1                      |
| merge join  |          ___ | Sort nodes present? extra mem? |

ধাপ ৩ — work_mem কমিয়ে spill:

RESET enable_hashjoin; RESET enable_mergejoin;
SET work_mem = '64kB';                     -- ইচ্ছাকৃত দরিদ্র বাজেট
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM cust_small c JOIN orders_big o ON o.cust_id = c.id;
->  Hash Join  ...
      Hash Cond: (o.cust_id = c.id)
      ->  Hash
            Buckets: 2048  Batches: 16  Memory Usage: 40kB   ← আগে Batches: 1!
            Buffers: ... temp read=210 temp written=208      ← disk-এ গেছে!

Batches: 16, temp read/written non-zero — grace partitioning live। এবার SET work_mem = '4MB' — Batches নামবে, temp blocks শূন্যের দিকে, time কমবে। তিনটা work_mem value-র সংখ্যা পাশাপাশি রাখুন — optimizer লেসনের cost model ঠিক এই behavior predict করবে।

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

একই data-তে তিন join algorithm-এর execution time স্পষ্টভাবে আলাদা হয়, আর work_mem কমালে Hash Batches 1 থেকে বেড়ে disk spill (temp blocks) দেখায় — memory constraint-ই batching-এর কারণ, theory আর EXPLAIN হুবহু মেলে।

নিজে বানান

BUILD IT

তিন join engine + benchmark matrix — নিজের হাতে ছোট query processor

Python · ●●●○○
  1. lists-of-dicts input-এ nested_loop, hash_join, sort_merge_join তিনটা function
  2. Duplicate key সব engine-এ ঠিক handle হয় — merge-এ equi-zone logic
  3. dict-based ground truth বানিয়ে তিনটার output cross-check
  4. Benchmark sizes {100×100k, 10k×10k} — runtime matrix প্রিন্ট
  5. Naive nested loop-এ pair-test cap — ১০⁸ টানার আগে skip

লক্ষ্য: theory-র তিন algorithm production-grade না হলেও semantics-complete হোক — duplicate, empty side, সব edge ঠিক। Benchmark-এ তাদের পার্থক্য নিজের চোখে।

import random
import time

random.seed(42)

# ---------- engines ----------

def nested_loop_join(left, right, lk, rk):
    out = []
    for l in left:                        # প্রতিটা left tuple-এ
        for r in right:                   # পুরো right scan
            if l[lk] == r[rk]:
                out.append((l, r))
    return out


def hash_join(left, right, lk, rk):
    ht = {}                               # build: right side
    for r in right:
        ht.setdefault(r[rk], []).append(r)
    out = []                              # probe: stream left
    for l in left:
        out.extend((l, m) for m in ht.get(l[lk], ()))
    return out


def sort_merge_join(left, right, lk, rk):
    L = sorted(left, key=lambda t: t[lk])
    R = sorted(right, key=lambda t: t[rk])
    out, i, j = [], 0, 0
    while i < len(L) and j < len(R):
        lv, rv = L[i][lk], R[j][rk]
        if lv < rv:
            i += 1
        elif lv > rv:
            j += 1
        else:                             # equi-zone: পুরো run × পুরো run
            i2 = i
            while i2 < len(L) and L[i2][lk] == lv:
                i2 += 1
            j2 = j
            while j2 < len(R) and R[j2][rk] == lv:
                j2 += 1
            out.extend((a, b) for a in L[i:i2] for b in R[j:j2])
            i, j = i2, j2
    return out


def ground_truth(left, right, lk, rk):
    idx = {}
    for r in right:
        idx.setdefault(r[rk], []).append(r)
    return [(l, m) for l in left for m in idx.get(l[lk], ())]

# ---------- data ----------

KEY_POOL = 5000                           # ইচ্ছাকৃত ছোট → duplicates guaranteed

def make_rows(n):
    return [{'k': random.randrange(KEY_POOL), 'p': f'r{i}'}
            for i in range(n)]

# ---------- correctness ----------

L, R = make_rows(300), make_rows(400)

def sig(rows):
    return sorted((a['p'], b['p']) for a, b in rows)

gt = sig(ground_truth(L, R, 'k', 'k'))
for name, fn in [('nested', nested_loop_join),
                 ('hash', hash_join),
                 ('merge', sort_merge_join)]:
    assert sig(fn(L, R, 'k', 'k')) == gt, f'{name} mismatch!'
print('correctness: ৩টা engine-ই ground truth multiset-এর সাথে মিলেছে')

# ---------- benchmark matrix ----------

ALGOS = [('nested', nested_loop_join), ('hash', hash_join), ('merge', sort_merge_join)]
MAX_PAIR_TESTS = 2 * 10 ** 7             # naive-এর জন্য safety cap

print(f"\n{'size':>13} | {'nested':>26} | {'hash':>10} | {'merge':>10}")
print('-' * 68)
for nl, nr in [(100, 100_000), (10_000, 10_000)]:
    left, right = make_rows(nl), make_rows(nr)
    cells = []
    for name, fn in ALGOS:
        if name == 'nested' and nl * nr > MAX_PAIR_TESTS:
            cells.append(f'skip (est {nl * nr:,} tests)')
            continue
        t0 = time.perf_counter()
        got = fn(left, right, 'k', 'k')
        dt = time.perf_counter() - t0
        cells.append(f'{dt * 1000:>9.1f} ms')
    print(f'{f"{nl}x{nr}":>13} | {cells[0]:>26} | {cells[1]:>10} | {cells[2]:>10}')

সাধারণ ফলাফল (laptop, Python 3.12 — সংখ্যা machine-ভেদে বদলাবে, pattern দেখুন):

correctness: ৩টা engine-ই ground truth multiset-এর সাথে মিলেছে

       size |                     nested |       hash |      merge
--------------------------------------------------------------------
   100x100000 |                   790.0 ms |     30.4 ms |   108.2 ms
  10000x10000 | skip (est 100,000,000 tests) |    16.6 ms |    31.9 ms

তিনটা reading: (১) hash-এর build cost amortize হয় — বড় probe-এ flat। (২) merge-এর খরচ মূলত sorting; input pre-sorted হলে সে সবাইকে ছাড়ায়। (৩) nested-এর quadratic curve ১০⁸ pair-test-এ minutes-এ ডুবত — planner বড় unindexed join-এ তাকে কেন বাছে না, benchmark-ই উত্তর।

নিজে বাড়ান:

  1. Pre-sorted input দিয়ে merge মাপুন — sorted() call বাদ দিয়ে timing তুলনা
  2. Grace-hash simulation: hash_join-এ max-memory-budget + partition-recurse যোগ করুন
  3. Skew test: KEY_POOL = 10 করে equi-zone explosion দেখুন — merge-এর zone আর hash-এর bucket দুটোই ফুলে যাবে

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

বাস্তব জগতে join algorithms

MySQL-এর ইতিহাস-পাঠ। বহু বছর MySQL-এ শুধু nested-loop variants — hash join 8.0.18-তে (2019) এলো। এক algorithm-এর একচ্ছত্রতা optimizer-কে কতটা অন্ধ করে, decade-long সেই গল্প।

Spark আর distributed world। Spark SQL-এ shuffle hash vs sort-merge choice ঠিক আজকের decision table; shuffle = grace partitioning-এর network রূপ। Skew hint (SKEW JOIN) আসলে equi-zone explosion-এর প্রতিষেধ।

OLTP বনাম OLAP split। Point-lookup-heavy workload-এ (auth, session store) index-nested loop দিনে লক্ষবার চলে; analytics-এ উল্টো — hash king, nested loop প্রায় নিষিদ্ধ। এক database, দুই জগৎ, দুই বিজয়ী।

DuckDB-র single-machine বিপ্লব। Vectorized hash join + perfect-hash specialization laptop-এ billion-row join — algorithm engineering-এর ceiling কোথায়, তার প্রমাণ।

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

“enable_hashjoin = off দিলে hash join আর কখনো চলবে না।”

Off মানে অত্যন্ত অপছন্দের — cost-এ বিশাল penalty, কিন্তু path বৈধই থাকে। Alternative না থাকলে planner ফিরে এসেই hash বেছে নেবে। Switches steering wheel, brake না।

“Hash join সবসময় fastest — nested loop legacy technology।”

ছোট outer + selective index-এ index-NL milliseconds-এ শেষ, যেখানে hash-এর build phase নিজেই বড় side-এর পূর্ণ scan চায়। OLTP point query-তে nested loop রাজা, full analytics-এ hash। Absolute champion নেই — parameter-ই রাজা।

“Merge join করতে SQL-এ ORDER BY লিখতেই হয়।”

ORDER BY result-এর ব্যাপার, join-input-এর না। Planner চায় input physically sorted — B-tree index scan সেটা free দেয়, না পেলে নিজেই Sort node বসায়। Query text-এ ORDER BY ছাড়াও merge join সম্পূর্ণ বৈধ ও common।

“work_mem বড় করে রাখলে সব hash join দ্রুত হবে।”

work_mem per hash node, per operation, parallel-এ per worker — allocate হয়। ১০০ concurrent connection × ক’টা join node × 256MB: মোট RAM সহজেই শেষ, swap-এ গেলে পুরো DB ঝুলে যায়। Batching-এর disk spill ধীর কিন্তু নিরাপদ; OOM দ্রুত কিন্তু মারাত্মক।

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

1

Block-nested loop-এর IO সূত্রটা লিখুন — B বাড়লে ঠিক কোন ভাগ কমে?

স্মরণ

IO = P_R + ceil(P_R / (B−2)) × P_S। B বাড়লে chunk বড় হয় → ceil(P_R/(B−2)) ভাগটা (inner re-scan সংখ্যা) কমে। B যথেষ্ট (outer পুরোটা ধরার মতো) হলে scan মাত্র ১ — নিম্নসীমা।

2

Outer 80 page, inner 2000 page, buffers B = 22। Block-NL-এ inner কতবার scan, মোট IO কত?

প্রয়োগ

Chunk = B−2 = 20 page → scans = ceil(80/20) = 4। IO = 80 + 4×2000 = 8,080 reads। Outer-এর page count scan সংখ্যা ঠিক করে; inner-এর size শুধু গুণফলে ঢোকে।

3

Hash join-এ build side কেন ছোট side? বড় side build করলে ঠিক কী খারাপ হয়?

যুক্তি

Hash table পুরোটা memory-তে থাকতে হয় — ছোট side বেছে footprint ও spill probability কমে। বড় side build করলে table ফুলে batches/temp বাড়ে, অথচ probe (ছোট side) তবু পুরো পড়তে হয় — দুই দিকেই ক্ষতি, লাভ শূন্য।

4

EXPLAIN-এ Hash Join-এর ভেতরে Batches: 64, Memory Usage: 30MB — মানে কী? Batches কমানোর তিনটা পথ?

যুক্তি

Build side work_mem (30MB) ছাড়িয়েছে — ৬৪ partition-এ ভাগ হয়ে disk-এ spill করছে (temp files)। পথ: (১) work_mem বাড়ানো, (২) build side ছোট করা — filter/pushdown early reduce, (৩) statistics ঠিক করা — ভুল estimate-এ planner build-side মাপও ভুল ধরে (ANALYZE)।

5

employees e JOIN contracts s ON s.end_date greater-than e.hire_date — planner কোন algorithm-গুলোর মধ্যে আটকে যাবে, কেন?

প্রয়োগ

শুধু nested loop family। Hash bucket এক specific key-র ঠিকানা — range predicate বহু bucket জুড়ে থাকে, O(1) lookup অর্থহীন। Merge sorted-equality pairing ধরে চলে। Range-range matching-এর কোনো linear-time trick নেই — block/index-NL-ই একমাত্র বৈধ পথ।

6

Report query-তে Merge Join + দুইটা Sort node, Sort Method: external merge Disk: 1.2GB, runtime ৪০s। Diagnosis আর তিন remedy?

ডিজাইন

Diagnosis: কোনো input physically sorted না — planner sort করে merge করছে, আর sort দুটো memory-র বাইরে disk-এ। Remedy ১: এক side-এ join-key-র B-tree index → free ordering, Sort node গায়েব। Remedy ২: work_mem বাড়িয়ে in-memory sort (per-op budget মনে রেখে)। Remedy ৩: merge-এর সুবিধা এখানে নেই — enable_mergejoin=off দিয়ে hash-path test। মূল শিক্ষা: external sort silent disk tax।

এরপর কী

এরপর কী

আজ দেখলাম তিন algorithm কীভাবে চলে — বড় প্রশ্নটা খোলাই রইল: planner কীভাবে বাছে? আর multi-way join-এ (৫-table!) কোন জোড়া আগে — order-এর সংখ্যা কোটি ছাড়ায়।

পরের লেসনে query optimizer-এর মস্তিষ্ক:

  • Optimization = search problem — plan space কত বড়, equivalence rules কীভাবে সেটা কাটে
  • Cost model-এর ভেতরে: seq_page_cost, random_page_cost, cpu_tuple_cost — startup-vs-total আর LIMIT-এর connection
  • System-R dynamic programming bottom-up enumeration — wide join-এ greedy + genetic algorithm (geqo)
  • random_page_cost flip করে plan উল্টে দেখা — নিজের হাতে

আজকের timing টেবিল সংরক্ষণ করুন — পরের লেসনের cost formulas সেই সংখ্যাগুলোই predict করতে শেখাবে।

আরও পড়ুন

  • Database Systems: The Complete Book, Chapter 15 — Query Execution Algorithms — Hector Garcia-Molina, Jeffrey Ullman, Jennifer Widom · Nested-loop, hash আর sort-based join-এর IO সূত্র — এই লেসনের B-buffer গণিতের উৎস
  • PostgreSQL Documentation — Query Planning (Planner Cost Constants) — PostgreSQL Global Development Group · work_mem ও enable_* GUC-দের official semantics
  • Database Internals, Part 1 — Query Execution: Join Algorithms — Alex Petrov · Grace hash আর hybrid hash join-এর modern treatment