Query Processing — SQL text থেকে result rows
Query Processing Pipeline
আপনার SQL text database-এর ভেতরে ছোট্ট একটা program হয়ে যায় — parse, rewrite, plan, execute-এর চার-ধাপের যাত্রা, আর সেই যাত্রার X-ray হলো EXPLAIN।
আগে এটা বুঝি
আগের লেসনগুলোতে আমরা database-এর ভিত দেখেছি — slotted page-এ tuple কীভাবে বসে, buffer pool কীভাবে disk আর memory-র মাঝে দালালি করে। কিন্তু একটা প্রশ্ন এখনো ঝুলে আছে: আপনি SELECT ... FROM ... WHERE লিখলেন, আর মুহূর্তেই rows ফিরে এলো। মাঝের অন্ধকার কক্ষে ঠিক কী ঘটল?
উত্তরটা চমকপ্রদ: আপনার SQL text আসলে একটা program, আর database সেটাকে একটা compiler-এর মতোই সামলায়। Tokenize, parse, semantic analysis, optimization, execution — compilers module-এর সেই pipeline-এর প্রতিধ্বনি এখানেও শোনা যায় (informal করে বললে: SQL source code, plan tree তার machine code)।
আজকের journey:
- পাঁচ ধাপের pipeline — প্রতিটা ধাপ ঠিক কী input নেয়, কী output দেয়
- Raw parse tree বনাম analyzed Query tree — কেন দুইটা আলাদা জিনিস
- Rewrite phase: view expansion, constant folding, predicate pushdown preview
- Executor-এর তিন দর্শন — volcano, vectorized, materialization
EXPLAIN— পুরো pipeline-টার X-ray মেশিন, লাইনে-লাইনে পড়া শেখা
মূল ধারণা
পাঁচ ধাপের pipeline
PostgreSQL-এ একটা statement-এর যাত্রা:
| ধাপ | Input | Output | দায়িত্ব |
|---|---|---|---|
| Lex + Parse | SQL text | raw parse tree | grammar check, token থেকে tree |
| Analyze | raw parse tree | Query tree | name resolution, type coercion |
| Rewrite | Query tree | rewritten Query tree | rules apply — views, constraints |
| Plan | rewritten tree | Plan tree | access path বাছাই, join order |
| Execute | Plan tree | tuples | buffer pool থেকে rows pull |
compilers module-এর pipeline-overview লেসনের সাথে মেলান: lexer+parser ↔ scanner/parser, analyze ↔ semantic analysis, planner ↔ optimizer+codegen, executor ↔ interpreter/runtime। পার্থক্য একটাই — SQL-এর “machine” হলো storage engine, আর প্রতিটা statement-ই fresh compile হয় (prepared statement cache ছাড়া)।
Raw parse tree vs Query tree — কেন দুইটা
Parser-এর কাজ শুধু grammar: SELECT name FROM users WHERE id = 5 বাক্যটা ঠিক shape-এর কি না। ফল হলো raw parse tree — column আর table শুধু string, id আসলে কোন relation-এর কলাম সে জানে না।
Analyze stage (PostgreSQL-এ parse_analyze) এসে জাগলিক জগৎটা বসায়:
- Name resolution:
users,name,id— প্রতিটাকে catalog (pg_class,pg_attribute) খুঁজে OID-তে bind করা। টাইপো করলেcolumn "nmae" does not existerror এখানেই আসে, parser-এ না। - Type coercion:
'2024-01-01' = placed_at— string আর date-এর তুলনা; analyzer implicit cast ঢোকায়, operator family ঠিক করে।WHERE id = 'abc'-এরinvalid input syntaxও এই ধাপের ফসল।
Rewrite phase — rule-ভিত্তিক রূপান্তর
Query tree এসে rewriter-এর টেবিলে, যে নিয়মগুলো অর্থ অক্ষুণ্ণ রেখে tree বদলায়:
- View expansion: View মানে stored query না — একটা rewrite rule।
SELECT * FROM active_usersদিলে rewriter ভেতরের query tree-টা সরাসরি গেঁথে দেয়; আলাদা করে view “execute” হয় না। - Constant folding:
WHERE amount > 500*2→amount > 1000; expression-এর ভেতরের immutable হিসাব এখনই হয়ে যায়। - Predicate pushdown preview:
WHEREcondition-গুলো scan node-এর যত কাছে নেওয়া যায় তত কম tuple উপরে ওঠে — planner-কে সাহায্যের প্রথম প্যাঁচ। - Outer-join simplification:
LEFT JOIN t ON true-এর মতো degenerate case গুলো inner join বা plain scan-এ নেমে আসে; অপ্রয়োজনীয় outer side মুছে যায়।
Executor-এর তিন দর্শন
Plan tree হাতে পেয়ে executor চালায় — কিন্তু কীভাবে data flow করবে, সেটা তিন স্কুল:
| Model | Unit | Strength | Weakness |
|---|---|---|---|
| Volcano / iterator | ১ tuple প্রতি next() call | generic, sparse-result friendly, LIMIT short-circuit | virtual call overhead প্রতি tuple-এ |
| Vectorized | batch (হাজার tuple) | CPU cache + SIMD ভালো খায় — analytics-এ ঝড় | complex operator-এ batch bookkeeping |
| Materialization | পুরো input একসাথে | সরল, sort/hash-এ natural | memory খায়, intermediate বড় হলে বিপদ |
PostgreSQL default-এ volcano — ExecInitNode দিয়ে tree init, ExecProcNode দিয়ে root-এ next() টানা। তবে আধুনিক PG একটা বাঁক নিয়েছে: যখন এক expression প্রায় লক্ষ tuple-এর ওপর চলবে (jit_above_cost), LLVM দিয়ে সেই expression loop-টাই compile হয়ে যায় — JIT expression compilation। Vectorization-এর পূর্ণ স্বাদ তাই না, কিন্তু per-tuple overhead-এর কিছুটা দাম কমানো।
ভেতরে কী ঘটছে
Parser-এর ভেতরে — bison grammar
PostgreSQL-এর parser হাতে লেখা না — gram.y নামের এক bison grammar file থেকে generate হয়, কয়েক লক্ষ state। আপনি যখন SELECT DISTINCT ON (...) লেখেন, আসলে একটা LALR parser সেই production rule গুনে tree বানাচ্ছে। এজন্যই SQL-এর অদ্ভুত syntax (যেমন GROUP BY-এর আগে HAVING লিখতে না পারা) ঠিক grammar-এর স্বভাব — language design না, yacc constraint।
Query tree-র নাড়ি-ধমনি
Analyzed Query tree-র প্রধান অঙ্গ-প্রত্যঙ্গ (C struct Query):
Query {
commandType: SELECT/INSERT/UPDATE/DELETE
rtable: range table — কোন কোন relation এসেছে (OID সহ)
targetList: কোন expression return হবে
jointree: FROM clause-এর boolean-algebra রূপ
qual: WHERE condition (implicit AND list)
groupClause: GROUP BY expressions
sortClause: ORDER BY
}Planner এই struct-টাকে Path tree দিয়ে cost করে, শেষে Plan tree-তে নামায় — পরের লেসনে সেই ভেতরটা।
Volcano executor — হাতে লেখা skeleton
প্রতিটা plan node একটা object তিনটা method সহ:
class Node:
def open(self): pass # children open + own state reset
def next(self):
while True:
row = self.child.next() # pull model — child টানা হয়
if row is None: return None # exhaustion
if self.passes(row): # own predicate
return self.project(row) # own projection
def close(self): pass # resource release
class SeqScan(Node):
def __init__(self, table, cond):
self.pages, self.i = table.pages(), 0
self.cond = cond
def next(self):
while self.i < len(self.pages): # page-by-page
row = self.pages[self.i].tuple_at(self.cursor)
self.advance()
if row and self.cond(row): return row
return NoneRoot-এ next() ডাকলে চাই পুরো subtree নিজে নিজে সাজে — উপরের node কিছুই জানে না নিচে কী ঘটছে। <Term id="recursion">recursion</Term> না থাকলেও এটা তার চেতনা: ছোট সরল interface, গভীর composite tree।
LIMIT-এর জাদু এখানেই: root যখন ১০-তম tuple পেয়ে থামে, নিচের Seq Scan মাঝপথেই থেমে যায় — কাজ নষ্ট হয় না।
উদাহরণ
একটা query-র পূর্ণ জীবন
Schema ছোট, প্রশ্নটা বাস্তব — কোন customer কত item অর্ডার করেছে, শুধু বড় order-এর জন্য:
SELECT c.name, SUM(i.qty)
FROM items i
JOIN orders o ON i.order_id = o.id
JOIN customers c ON o.customer_id = c.id
WHERE o.amount > 500
GROUP BY c.name;ধাপে ধাপে কী হয়:
১. Parse: পাঁচটা clause চেনা tree-তে বসে; c.name এখনো দুইটা string।
২. Analyze: items, orders, customers OID-resolve; alias binding; SUM(qty)-র overload ঠিক; o.amount > 500 numeric comparison-এ cast।
৩. Rewrite: এখানে কিছুই বদলাল না (view নেই) — কিন্তু big_orders view দিয়ে লেখা হলে ঠিক এই মুহূর্তে সেটা খুলে গেঁথে যেত।
৪. Plan: নিচের মতো একটা tree দাঁড়ায় (cost সংখ্যা মেশিনভেদে):
HashAggregate (cost=8123.10..8123.20 rows=400)
-> Hash Join (cost=1200.00..6900.00 rows=98000) c ⟕ o
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders Filter: (amount > 500)
-> Hash
-> Seq Scan on customers৫. Execute: Volcano pull — HashAggregate টানছে, Hash Join probe করছে, buffer pool page আনছে; SUM accumulate হয়ে client-এ একটা result set।
লক্ষ্য করুন: WHERE টা orders-এর নিজের node-এ নেমে গেছে — pushdown-এর হাতে-কলমে রূপ। items এখনো tree-তে নেই কেন ভাবছেন? সহজ রাখতে উদাহরণে two-table দেখালাম; পরের experiment-এ পূর্ণ three-table version নিজেই পড়বেন।
ছবিতে
Pipeline map — compiler-এর পাশে
SQL text ──▶ ┌─────────────┐ raw parse tree
│ Lex + Parse │───────────────────┐
└─────────────┘ ▼
source code ─▶ Lexer/Parser ══▶ AST ┌────────────┐
(compilers) │ Analyze │ Query tree
│ (semantic) │───────────┐
└────────────┘ ▼
┌───────────────┐
AST ═════════▶ Sema + IRGen ══▶ typed IR │ Rewrite │
(compilers) │ (rule engine) │
└───────┬───────┘
▼
typed IR ───▶ Optimizer ══▶ optimized IR ┌───────────────┐
(compilers) │ Planner │ Plan tree
└───────┬───────┘
▼
optimized IR ▶ Codegen ══▶ machine code ┌───────────────┐
(compilers) │ Executor │ tuples
└───────────────┘Executor-এর গাছটাও ঠিক plan tree-রই ছায়া:
- HashAggregateroot — GROUP BY key-তে hash bucket ধরে SUM জমায়
- Hash Join (inner)probe side: orders-এর filtered tuples টেনে hash table-এ খোঁজে
- Seq Scan orders + Filterpage-by-page read — amount threshold পার করেনি এমন tuple ফেলে দেয়, buffer pool-এর কাছ থেকে
- Hash build ← Seq Scan customersbuild side: ছোট table-এর tuple দিয়ে hash table তৈরি
নিজে চালিয়ে দেখুন
EXPLAIN VERBOSE লাইনে-লাইনে dissect + auto_explain + view rewrite
ধাপ ১ — three-table dataset বানান:
CREATE TABLE customers (id serial PRIMARY KEY, name text);
CREATE TABLE orders (
id serial PRIMARY KEY,
customer_id int REFERENCES customers(id),
amount numeric(10,2)
);
CREATE TABLE items (
id serial PRIMARY KEY,
order_id int REFERENCES orders(id),
qty int
);
INSERT INTO customers (name)
SELECT 'cust_' || g FROM generate_series(1, 2000) g;
INSERT INTO orders (customer_id, amount)
SELECT (random()*1999)::int + 1, round((random()*2000)::numeric, 2)
FROM generate_series(1, 60000);
INSERT INTO items (order_id, qty)
SELECT (random()*59999)::int + 1, (random()*5)::int + 1
FROM generate_series(1, 150000);
ANALYZE customers; ANALYZE orders; ANALYZE items;ধাপ ২ — VERBOSE output dissect করুন:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT c.name, SUM(i.qty)
FROM items i JOIN orders o ON i.order_id = o.id
JOIN customers c ON o.customer_id = c.id
WHERE o.amount > 500
GROUP BY c.name;Output-এর একটা representative slice, annotation সহ:
Finalize GroupAggregate (cost=.. rows=..) (actual time=95.0 rows=2000 loops=1)
Output: c.name, sum(i.qty) ← VERBOSE-এর অবদান: node-এর output columns
-> Sort ...
-> Hash Join (actual rows=45000 loops=1)
Hash Cond: (o.customer_id = c.id) ← join algorithm-এর সুইচবোর্ড
-> Seq Scan on public.orders o
Output: o.id, o.customer_id, o.amount
Filter: (o.amount > '500'::numeric)
Rows Removed by Filter: 15123 ← filter-এর শিকার — waste-এর মাপ
Buffers: shared hit=520 read=13 ← buffer pool lesson live!
-> Hash
Buckets: 4096 Batches: 1 Memory Usage: 180kB
-> Seq Scan on public.customers c (actual rows=2000 loops=1)তিনটা reading-rule: (১) rows= estimate, (actual rows=N) সত্যি — gap বড় হলে statistics suspect (পরের লেসনের বিষয়)। (২) actual rows per-loop average — loops=1000 হলে মোট = rows × loops। (৩) Rows Removed by Filter বড় মানে index-এর দরকার হতে পারে।
ধাপ ৩ — auto_explain দিয়ে ধীর query log করুন:
LOAD 'auto_explain';
SET auto_explain.log_min_duration = '0'; -- সব লগ করো (production-এ '500ms')
SET auto_explain.log_analyze = true;
SET auto_explain.log_buffers = true;
-- এখন থেকে session-এর প্রতিটা statement server log-এ নিজের plan লিখে যাবে
SELECT count(*) FROM items i JOIN orders o ON i.order_id = o.id;Log পড়ুন: Docker-এ docker logs pg-lab 2>&1 | grep -A 15 'Query Text'। Cluster-wide চাইলে shared_preload_libraries = 'auto_explain' restart সহ — production slow-query hunting-এর স্ট্যান্ডার্ড অস্ত্র।
ধাপ ৪ — view rewrite চোখে দেখুন:
CREATE VIEW big_orders AS
SELECT * FROM orders WHERE amount > 1000;
EXPLAIN SELECT customer_id FROM big_orders WHERE customer_id = 42;Index Only Scan using ?? on orders -- view-এর WHERE + আপনার WHERE মিশে গেছে!
Index Cond: ((customer_id = 42) AND (amount > '1000'))View-এর condition আর আপনার condition এক node-এ গলে গেছে — কোথাও আলাদা “view execution” নেই। এটাই rewrite phase-এর view expansion, plan-এর ভাষায় সাক্ষ্য।
EXPLAIN output-এর প্রতিটা field plan tree-র এক-একটা node-এর প্রতিচ্ছবি — cost/rows/loops/buffers পড়েই executor-এর আসল আচরণ reconstruct করা যায়, আর view আসলে rewrite-time-এ inline হয় তা plan-এই চোখে পড়ে।
নিজে বানান
PlanTree pretty-printer — JSON plan থেকে indented tree + estimate warning
- EXPLAIN (FORMAT JSON)-এর সরলীকৃত subset ধরে node class model করুন
- Recursive render — indent guides (├─ └─ │) দিয়ে tree আঁকুন
- Node-এর extras (Filter, Hash Cond, Sort Key...) আলাদা লাইনে ছাপুন
- Estimated-vs-actual ratio হিসাব করে ১০× পার হলে ⚠ flag দিন
- loops গুণ ধরে total-এ তুলনা — per-loop average-এর ফাঁদ এড়ান
লক্ষ্য: EXPLAIN (FORMAT JSON) যা দেয় তার সরল রূপ consume করে terminal-এ মানবপাঠ্য tree — ঠিক যেভাবে আপনি psql-এ text output পড়েন, কিন্তু estimate-drift warning সহ।
import json
WARN_X = 10.0
def _pad(prefix, last):
return prefix + (' ' if last else '│ ')
def fmt_head(n):
label = n['Node Type']
if 'Relation Name' in n:
label += f" on {n['Relation Name']}"
if n.get('Alias') != n.get('Relation Name') and n.get('Alias'):
label += f" as {n['Alias']}"
if 'Join Type' in n:
label += f" ({n['Join Type']})"
su, tc = n['Startup Cost'], n['Total Cost']
head = f"{label} (cost={su:.2f}..{tc:.2f} rows={n['Plan Rows']} width={n['Plan Width']})"
if 'Actual Rows' in n:
head += f" (actual rows={n['Actual Rows']} loops={n.get('Actual Loops', 1)})"
return head
def fmt_extras(n):
out = []
for k in ('Sort Key', 'Group Key', 'Hash Cond', 'Join Filter',
'Index Cond', 'Filter', 'Strategy'):
if k in n:
out.append(f"{k}: {n[k]}")
return out
def render(n, prefix='', last=True, lines=None):
lines = lines if lines is not None else []
branch = '└─ ' if last else '├─ '
lines.append(f"{prefix}{branch}{fmt_head(n)}")
pad = _pad(prefix, last)
for e in fmt_extras(n):
lines.append(f"{pad} {e}")
if 'Actual Rows' in n:
loops = n.get('Actual Loops', 1)
est_t, act_t = n['Plan Rows'] * loops, n['Actual Rows'] * loops
hi, lo = max(est_t, act_t), max(min(est_t, act_t), 1)
if hi / lo >= WARN_X:
lines.append(f"{pad} ⚠ estimate off by {hi/lo:.0f}x "
f"(planned {n['Plan Rows']}, actual {n['Actual Rows']}, loops={loops})")
kids = n.get('Plans', [])
for i, k in enumerate(kids):
render(k, pad, i == len(kids) - 1, lines)
return lines
def print_plan(doc):
root = doc['Plan'] if isinstance(doc, dict) and 'Plan' in doc else doc
for line in render(root, '', True):
print(line)
# ---- sample plans (EXPLAIN FORMAT JSON-এর সরলীকৃত রূপ) ----
HASH_AGG_PLAN = {
'Node Type': 'Finalize GroupAggregate', 'Startup Cost': 9000, 'Total Cost': 9500,
'Plan Rows': 40, 'Plan Width': 24, 'Actual Rows': 2000, 'Actual Loops': 1,
'Group Key': ['c.name'],
'Plans': [{
'Node Type': 'Gather Merge', 'Startup Cost': 8500, 'Total Cost': 9400,
'Plan Rows': 60, 'Plan Width': 24, 'Actual Rows': 4000, 'Actual Loops': 1,
'Plans': [{
'Node Type': 'Partial HashAggregate', 'Startup Cost': 7400, 'Total Cost': 7600,
'Plan Rows': 30, 'Plan Width': 24, 'Actual Rows': 2000, 'Actual Loops': 2,
'Group Key': ['c.name'],
'Plans': [{
'Node Type': 'Hash Join', 'Startup Cost': 300, 'Total Cost': 6800,
'Plan Rows': 98000, 'Plan Width': 32, 'Actual Rows': 45000, 'Actual Loops': 2,
'Hash Cond': '(o.customer_id = c.id)',
'Plans': [
{'Node Type': 'Seq Scan', 'Relation Name': 'orders', 'Alias': 'o',
'Startup Cost': 0, 'Total Cost': 3200, 'Plan Rows': 30000, 'Plan Width': 16,
'Actual Rows': 44877, 'Actual Loops': 2, 'Filter': "(amount > '500'::numeric)"},
{'Node Type': 'Hash', 'Startup Cost': 250, 'Total Cost': 280,
'Plan Rows': 2000, 'Plan Width': 16, 'Actual Rows': 2000, 'Actual Loops': 2,
'Plans': [
{'Node Type': 'Seq Scan', 'Relation Name': 'customers', 'Alias': 'c',
'Startup Cost': 0, 'Total Cost': 25, 'Plan Rows': 2000, 'Plan Width': 16,
'Actual Rows': 2000, 'Actual Loops': 2}]}]}]}]}]}
if __name__ == '__main__':
print_plan(HASH_AGG_PLAN)প্রত্যাশিত output (renderer-এর হিসাব অনুযায়ী):
└─ Finalize GroupAggregate (cost=9000.00..9500.00 rows=40 width=24) (actual rows=2000 loops=1)
Group Key: ['c.name']
⚠ estimate off by 50x (planned 40, actual 2000, loops=1)
└─ Gather Merge (cost=8500.00..9400.00 rows=60 width=24) (actual rows=4000 loops=1)
⚠ estimate off by 67x (planned 60, actual 4000, loops=1)
└─ Partial HashAggregate (cost=7400.00..7600.00 rows=30 width=24) (actual rows=2000 loops=2)
Group Key: ['c.name']
⚠ estimate off by 67x (planned 30, actual 2000, loops=2)
└─ Hash Join (cost=300.00..6800.00 rows=98000 width=32) (actual rows=45000 loops=2)
Hash Cond: (o.customer_id = c.id)
├─ Seq Scan on orders as o (cost=0.00..3200.00 rows=30000 width=16) (actual rows=44877 loops=2)
│ Filter: (amount > '500'::numeric)
└─ Hash (cost=250.00..280.00 rows=2000 width=16) (actual rows=2000 loops=2)
└─ Seq Scan on customers as c (cost=0.00..25.00 rows=2000 width=16) (actual rows=2000 loops=2)⚠ লাইনগুলোই এই tool-এর প্রাণ — estimate drift চেনে না মানুষ, চেনে সংখ্যার অনুপাত।
নিজে বাড়ান:
EXPLAIN (FORMAT JSON)-এর আসল output paste করে সরাসরি চালান —json.load-এর পরে একই renderer কাজ করবে- Nested Loop + Index Scan-সহ second plan sample যোগ করে দুই plan পাশাপাশি diff করুন
- ANSI color যোগ করুন — warning লাল, scans নীল, joins হলুদ
বাস্তব সিস্টেমে
বাস্তব জগতে query processing
Production monitoring। auto_explain + log aggregation-ই slow-query triage-এর প্রথম স্তর; pganalyze, pgMustard-এর মতো tool সেই plan-গুলো পড়ে suggestion দেয় — মূলত আজ যা শিখলেন তারই automated রূপ।
ORM debugging। Django/Prisma/Hibernate যে SQL বানায় তার N+1 বা monster-join ধরার একমাত্র নির্ভরযোগ্য পথ EXPLAIN ANALYZE — ORM-এর abstraction ভেতরে ঠিক এই pipeline-ই চালায়।
Vectorized engines। DuckDB, ClickHouse, Snowflake-এর analytic path vectorized/batch execution — MonetDB/X100 paper (2005) থেকে শুরু; OLAP workload-এ per-tuple volcano overhead-ই সেখানে প্রথম শিকার। PostgreSQL-এর JIT সেই ব্যথার আধা-প্রতিকার।
Plan caching। Prepared statement-এ parse-analyze-plan একবারই; extended protocol (PQexecParams) বারবার execute — high-QPS service-এ latency budget-এর বড় অংশ এখান থেকে বাঁচে।
যে ভুলগুলো সবাই করে
“EXPLAIN লিখলেই query চলে যায়, তাই EXPLAIN দেখা ব্যয়বহুল।”
শুধু EXPLAIN কিছুই execute করে না — শুধু plan দেখায়, মিলিসেকেন্ডে। Execute হয় EXPLAIN ANALYZE দিলে — তখন সত্যিই সব data scan হয়। আর সতর্ক: UPDATE/DELETE-এর সামনে EXPLAIN ANALYZE দিলে data সত্যিই বদলে যায় — transaction wrap করে ROLLBACK করুন।
“EXPLAIN-এর cost number মিলিসেকেন্ড — cost=100 মানে ১০০ms।”
Cost একটা arbitrary unit-এর relative score: seq_page_cost=1 ধরে সব কিছু calibrate। একই query দুই machine-এ ভিন্ন cost, একই machine-এ দুই plan-এর তুলনাতেই cost-এর একমাত্র বৈধ ব্যবহার। Actual time দেখতে ANALYZE option — cost কখনো clock না।
“View slow, কারণ প্রতিবার stored query আলাদা করে চলে।”
View কোনো executable entity না — rewrite phase-এ তার tree সরাসরি গেঁথে যায় (experiment ধাপ ৪-এর প্রমাণ)। Optimizer গেঁথে-যাওয়া অংশটাকেও পূর্ণ স্বাধীনতায় optimize করে। ধীর view মানে ভেতরের query-ই ধীর — আলাদা penalty নেই। ব্যতিক্রম: materialized view, সেটা আসলে stored table।
“Column-এর বানান ভুল হলে parser error দেয় — তাহলে parsing-ই schema-aware।”
Parser শুধু grammar দেখে — SELECT nosuchcol FROM t perfectly valid sentence, তাই parser pass করে। Error আসে analyze stage-এ, catalog lookup ব্যর্থ হলে (column "nosuchcol" does not exist)। Error message-এর tone-ই বলে দেয় কোন ধাপে আটকালেন — এটা debugging-এর shortcut।
বুঝেছেন কি না দেখুন
1পাঁচ-ধাপের pipeline-এ প্রতিটা ধাপের input আর output এক লাইনে বলুন।
স্মরণ
Text → raw parse tree (lex+parse); raw tree → Query tree (analyze: name resolution + coercion); Query tree → rewritten Query tree (rewrite rules); rewritten tree → Plan tree (planner); Plan tree → tuples (executor)।
2EXPLAIN ANALYZE-এ এক node দেখাচ্ছে (actual rows=3 loops=1000)। ঐ node মোট কত row produce করেছে? শুধু rows=3 পড়ে কী ভুল conclusion হতো?
প্রয়োগ
(actual rows=3 loops=1000)। ঐ node মোট কত row produce করেছে? শুধু rows=3 পড়ে কী ভুল conclusion হতো?মোট = 3 × 1000 = 3000। শুধু 3 পড়লে মনে হতো index lookup প্রায় ফাঁপা — অথচ প্রতিটা outer tuple-এ গড়ে ৩-row ফলে ৩০০০ row উপরে গেছে; nested-loop-এর ভেতরের node পড়তে loops-গুণ বাধ্যতামূলক।
3column "nmae" does not exist আর syntax error at or near ")" — দুটো কোন কোন ধাপে জন্ম? কেন দুই ধাপ আলাদা রাখা হয়েছে?
যুক্তি
column "nmae" does not exist আর syntax error at or near ")" — দুটো কোন কোন ধাপে জন্ম? কেন দুই ধাপ আলাদা রাখা হয়েছে?প্রথমটা analyze (catalog binding ব্যর্থ), দ্বিতীয়টা parse (grammar violation)। আলাদা রাখার কারণ: grammar শুধু language-এর ব্যাপার, context-free; name/type context-dependent — catalog দরকার। Separation মানে parser স্বয়ংসম্পূর্ণ testable, আর extension (custom type/operator) শুধু analyze/planner layer ছুঁয়ে যোগ হয়।
4Volcano model LIMIT-এ দুর্দান্ত কিন্তু full-scan aggregate-এ ধীর কেন? PostgreSQL তবু কেন volcano রেখেছে?
যুক্তি
Volcano-র প্রতি-tuple virtual-call overhead constant, কিন্তু aggregate-এ কোটি tuple পেরোলে সেই constant কোটিবার — measurable। LIMIT-এ আবার early-stop লাভ দেয়। PG-র পছন্দ: generic plan node interface (FDW, custom scan, parallel workers সব এই contract-এ), sparse OLTP result-এ খরচ নগণ্য — trade-off সচেতন।
5WHERE created_at > now() - interval '7 days' — constant folding এখানে কি হবে? now()-কে কেন fold করা বিপজ্জনক হতো?
প্রয়োগ
WHERE created_at > now() - interval '7 days' — constant folding এখানে কি হবে? now()-কে কেন fold করা বিপজ্জনক হতো?হবে না — now() stable, immutable না; এক statement-এর ভেতরে constant, statement-এর মাঝে variable। Fold করলে prepared statement cache-এ পুরনো timestamp আটকে থাকত — প্রতিটা execution ভুল সময়সীমা ধরত। Folding শুধু pure function-এ (যেমন '5'::int * 2)।
6আপনার report query-তে view-in-view-in-view — তিন স্তরের nesting। Debugging-এ কোন tool দিয়ে বুঝবেন rewriter ঠিক কী বানাল, আর কোন signal বলবে pushdown কাজ করেনি?
ডিজাইন
EXPLAIN (VERBOSE) — প্রতিটা node-এর Output list আর merged conditions দেখায়; গভীরতম base relation-এ আপনার outermost WHERE-এর predicates নেমে এসেছে কি না সেটাই pushdown-এর সাক্ষ্য। Signal of failure: উপরের node-এ বড় Filter আর নিচে Rows Removed by Filter-এর স্তূপ — অর্থাৎ বেশি tuple উঠে এসে পরে ফেলা হচ্ছে; non-pushable construct (volatile function, window) খুঁজুন।
এরপর কী
এরপর কী
Plan tree-র node-গুলোর মধ্যে সবচেয়ে দামি — join nodes। আজ আমরা Hash Join, Nested Loop-এর নাম দেখেছি; পরের লেসনে তিনটা join algorithm ভেতর-থেকে খুলব:
- Nested loop: naive O(N·M) থেকে block-nested — buffer-sized chunk-এর IO গণিত
- Index-nested loop: ছোট outer + indexed inner-এর জাদু
- Hash join: build + probe, আর memory ছোট হলে grace-hash partitioning
- Sort-merge: pre-sorted input-এর রাজত্ব, duplicate handling সহ
- PostgreSQL-এ
enable_*switches আরwork_memদিয়ে প্রতিটাকে জোর করে চালিয়ে timing মাপা
আজ EXPLAIN পড়া শিখলেন; পরের লেসনে সেই দক্ষতাই join algorithm-এর microscope হবে।
আরও পড়ুন
- PostgreSQL Documentation — Performance Tips: Using EXPLAIN — PostgreSQL Global Development Group · EXPLAIN output-এর প্রতিটা field-এর official ব্যাখ্যা — cost, rows, loops, buffers
- PostgreSQL Documentation — Overview of PostgreSQL Internals: The Query Processor Stage — PostgreSQL Global Development Group · Parse → analyze → rewrite → plan → execute pipeline-এর সংক্ষিপ্ত প্রামাণ্য map
- Architecture of a Database System, Section 4 — Query Processing — J. M. Hellerstein, M. Stonebraker, J. Hamilton · Iterator model আর plan representation-এর classic treatment — এই লেসনের মেরুদণ্ড