এই পর্বে যা শিখবেন
- Optimizer-এর তিনটি অংশ — plan space, cost model আর cardinality estimation
- Query tree-তে heuristic rewrite, subquery decorrelation আর view-এ predicate pushdown — লাইভ EXPLAIN QUERY PLAN-এ
- Left-deep বনাম bushy plan, n! গণনা আর Selinger-এর dynamic programming, ধাপে ধাপে
- Statistics, histogram আর selectivity-র সূত্র — নিজের হাতে অনুমান বনাম আসল সারি মিলিয়ে
- ANALYZE আর sqlite_stat1, plan বদল, plan cache, GEQO আর hint — কখন আর কতটা সাবধানে
১ · Optimizer-এর কাজ — একই উত্তরের অনেক পথ
পর্ব ৪২-এ আপনি দেখেছেন একটি কোয়েরি চালানোর যন্ত্রপাতি: full scan আর index scan, nested loop, sort-merge আর hash join, বাইরের sort. কিন্তু একটি কোয়েরিতে কোন যন্ত্রটি কোথায় লাগবে, কোন টেবিল আগে পড়া হবে — সেই সিদ্ধান্ত নেয় DBMS-এর query optimizer. SQL একটি declarative ভাষা: আপনি বলেন কী চান, কীভাবে আনতে হবে বলেন না। সেই “কীভাবে” লেখা থাকে একটি query plan-এ।
মিরপুর থেকে মতিঝিল যেতে হবে। মেট্রো রেল, বাস, রিকশা আর হাঁটা মিলিয়ে পথ অনেক — সবগুলোই শেষে মতিঝিলে পৌঁছায়, অর্থাৎ “উত্তর” একই। আপনি পথ বাছেন অনুমানে: এই সময়ে রাস্তায় কেমন জ্যাম, স্টেশনে কত ভিড়। অনুমান ঠিক হলে চমৎকার যাত্রা; ভুল হলে — ধরুন হঠাৎ মিছিলে রাস্তা বন্ধ — সবচেয়ে “দ্রুত” পথেই দুই ঘণ্টা আটকে থাকা। Optimizer ঠিক এই কাজ করে: অনেক সমতুল্য পথ, প্রতিটির খরচের অনুমান, আর সবচেয়ে সস্তা অনুমানের পথটি বাছাই।
যেকোনো optimizer-কে তিনটি প্রশ্নের উত্তর দিতে হয়:
১. কোন কোন plan সম্ভব?
Plan space: পর্ব ৩২–৩৩-এর equivalence rule দিয়ে একই ফলের সব relational algebra রূপ, সাথে প্রতিটি অপারেটরের অ্যালগরিদম আর প্রতিটি টেবিলের access path.
২. একটি plan কতটা দামি?
Cost model: কত page পড়া, কত CPU কাজ, কত মেমরি — পর্ব ৪২-এর সূত্রগুলো দিয়ে। প্রতিটি সূত্রের ইনপুট হলো সারির সংখ্যা।
৩. প্রতিটি ধাপে কত সারি?
Cardinality estimation: কোয়েরি না চালিয়েই অনুমান — এই শর্তে কত সারি টিকবে, এই join-এ কত সারি বেরোবে। সবচেয়ে কঠিন, আর ভুলের প্রধান উৎস।
একটি সূক্ষ্ম কথা মনে রাখুন: optimizer “সবচেয়ে ভালো plan” খোঁজে না, খোঁজে সবচেয়ে কম আনুমানিক খরচের plan. দুটি এক নয়। আর পুরো খোঁজটি হতে হবে কোয়েরি চালানোর চেয়ে অনেক কম সময়ে — এক মিলিসেকেন্ডের কোয়েরির জন্য এক সেকেন্ড ধরে plan খোঁজা অর্থহীন। তাই বাস্তব optimizer পুরো plan space দেখে না; নিয়ম দিয়ে ছাঁটে, আর বাকিটা চতুরভাবে খোঁজে।
এই পর্বের লাইভ SQL চলে দুটি তৈরি-করা টেবিলে, যা প্রতিটি এডিটর একটি recursive CTE দিয়ে (পর্ব ২৫) নতুন করে বানায় — নমুনা ডেটার বাক্সে পুরো স্ক্রিপ্ট দেখতে পাবেন। cust-এ ২,০০০ গ্রাহক: id, city (৭টি শহর — Dhaka অর্ধেক, Chattogram ২০%, Gazipur ১০%, বাকি চারটি ৫% করে), division (বিভাগ; শহরের সাথে বাঁধা — Gazipur ঢাকা বিভাগে) আর phone (প্রতি দশজনে একজনের NULL). ord-এ ২০,০০০ অর্ডার: id, cust_id, status (delivered ৮৫%, pending ১০%, cancelled ৪%, returned ১%) আর amount (৭০% অর্ডার ১,০০০ টাকার নিচে, অল্প কিছু ৫০ হাজার পর্যন্ত)। প্রথম ২০ জন গ্রাহক বিশেষ: প্রত্যেকের প্রায় ২৬০টি অর্ডার। ডেটাটি ইচ্ছে করে অসমান — বাস্তব ডেটার মতোই।
২ · Heuristic rewrite — নিয়ম দিয়ে গাছ বদলানো
Optimizer-এর প্রথম ধাপ cost হিসাব নয়, কিছু নিয়ম। এই heuristic rewrite-গুলো প্রায় সব ক্ষেত্রেই লাভজনক, তাই খরচ না মেপেই করা হয়। Parser SQL থেকে একটি প্রাথমিক query tree বানায়: নিচে টেবিলগুলোর cartesian product, ওপরে WHERE-এর সব শর্ত একসাথে, তারও ওপরে SELECT-এর কলাম। তারপর পর্ব ৩২–৩৩-এর equivalence rule দিয়ে গাছটি ধাপে ধাপে বদলায়। প্রশ্নটি: Sylhet-এর যে গ্রাহকদের কোনো অর্ডার ফেরত এসেছে, তাঁদের ফোন নম্বর।
SELECT c.phone
FROM cust c, ord o
WHERE c.id = o.cust_id AND c.city = 'Sylhet' AND o.status = 'returned';| join-এ ঢোকে | |
| সবচেয়ে বড় মধ্যবর্তী ফল | |
| প্রতি সারিতে কলাম | |
| শেষ ফল |
- প্রাথমিক গাছ। SQL হুবহু অনুবাদ করলে আগে দুই টেবিলের cartesian product — ২,০০০ × ২০,০০০ = ৪ কোটি জোড়া — তারপর শর্ত। ফল মাত্র ৪২টি সারি, অথচ পথে ৪ কোটি।
- Cascade. AND-যুক্ত একটি selection তিনটি আলাদা selection-এ ভাঙা যায় — পর্ব ৩২-এর প্রথম equivalence. নিজে কিছু বাঁচায় না, কিন্তু প্রতিটি শর্তকে আলাদাভাবে সরানোর সুযোগ দেয়।
- Selection pushdown. city-র শর্তে শুধু cust-এর কলাম, তাই সেটি cust-এর ঠিক ওপরে নামে (১০০ সারি); status-এর শর্ত ord-এর ওপরে (২০০ সারি)। এখন cartesian product হলেও ২০,০০০ জোড়া — দুই হাজার গুণ কম।
- Join তৈরি। দুই টেবিলের কলাম মেলানোর শর্তটি × -এর ঠিক ওপরে বসলে দুটি মিলে একটি θ-join. join অ্যালগরিদম (পর্ব ৪২) ২০,০০০ জোড়া বানায় না — সরাসরি ৪২টি মেলা সারি দেয়। সবচেয়ে বড় মধ্যবর্তী ফল এখন ২০০।
- Projection pushdown. ওপরে লাগে শুধু phone; join-এ লাগে id আর cust_id. বাকি কলাম আগেই ফেলে দিলে প্রতিটি সারি সরু হয় — মেমরিতে বেশি সারি ধরে, sort বা hash-এ কম কাজ।
- নিয়মের সীমা। নিয়ম বলে না কোন টেবিল দিয়ে শুরু, কোন index, কোন join অ্যালগরিদম। এগুলো নির্ভর করে কোন শর্ত কত সারি রাখে তার ওপর — সেখানে আসে cost-based optimization.
এই নিয়মগুলোর পেছনের যুক্তি সরল: প্রতিটি অপারেটরের খরচ তার ইনপুটের আকারের সাথে বাড়ে, আর selection কখনো সারি বাড়ায় না। তাই selection যত আগে, পরের সব অপারেটরের ইনপুট তত ছোট বা সমান। তবে “প্রায় সবসময়” — ব্যতিক্রমও আছে। শর্তটি নিজেই দামি হলে (ধরুন প্রতিটি সারিতে একটি ধীর user-defined function, পর্ব ২৮), আগে join করে সারি কমিয়ে তারপর শর্ত চালানো সস্তা হতে পারে। তাই আধুনিক optimizer এমন শর্তকেও cost দিয়ে বিচার করে।
আরও তিনটি বড় rewrite
- View merging: পর্ব ২৪-এ দেখেছেন, view-এর নামের জায়গায় তার SELECT বসে (view expansion), আর সম্ভব হলে বাইরের কোয়েরির সাথে এক হয়ে যায় — তখন ওপরের সব নিয়ম view-এর ভেতরেও খাটে।
- View-এ predicate pushdown: view-টি GROUP BY-ওয়ালা হলে পুরোপুরি মেশানো যায় না, কিন্তু GROUP BY-এর কলামের ওপর বাইরের শর্তটি ভেতরে ঠেলে দেওয়া যায় — সব দলের যোগফল না করে শুধু দরকারি দলটির।
- Subquery unnesting (decorrelation): পর্ব ২১-এর correlated subquery বাইরের প্রতিটি সারিতে একবার চলে। optimizer তাকে join বা semi-join-এ বদলাতে পারলে পুরো খোঁজটি একবারে হয়।
আসল SQLite-এ দেখুন। EXPLAIN QUERY PLAN কোয়েরি না চালিয়ে plan দেখায়: ফলের detail কলামে প্রতিটি লাইন একটি loop, ওপরের লাইন বাইরের loop. (notused কলামটি উপেক্ষা করুন।) এই এডিটরে ord.cust_id-তে একটি index আগে থেকেই আছে:
CREATE VIEW cust_total AS
SELECT cust_id, COUNT(*) AS n, SUM(amount) AS total
FROM ord GROUP BY cust_id;
EXPLAIN QUERY PLAN SELECT * FROM cust_total WHERE cust_id = 7;
SELECT * FROM cust_total WHERE cust_id = 7;
CREATE VIEW sylhet_orders AS
SELECT o.id, o.cust_id, o.amount
FROM ord o JOIN cust c ON c.id = o.cust_id
WHERE c.city = 'Sylhet';
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM sylhet_orders WHERE cust_id = 18;
SELECT COUNT(*) FROM sylhet_orders WHERE cust_id = 18;প্রথম plan: CO-ROUTINE cust_total-এর ভেতরে SEARCH ord USING INDEX ord_cust (cust_id=?) — বাইরের cust_id = 7 শর্তটি view-এর GROUP BY-এর ভেতরে ঢুকে গেছে, তাই SQLite ১,৫০৫টি দলের যোগফল না করে শুধু একজনের ২৬০টি সারি পড়ে; ফল 7 | 260 | 960520. দ্বিতীয় view-টি পুরো মিশে গেছে (কোনো CO-ROUTINE নেই), আর plan-এ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) আর SEARCH o USING COVERING INDEX ord_cust (cust_id=?). লক্ষ করুন, আপনি c.id-তে কোনো শর্ত দেননি। optimizer নিজে যুক্তি করেছে: o.cust_id = 18 আর c.id = o.cust_id, তাই c.id = 18 — একে বলে transitive predicate. ফল 260: গ্রাহক 18 Sylhet-এর।
এবার decorrelation. প্রশ্ন: কতগুলো অর্ডারের টাকা সেই গ্রাহকের নিজের গড় অর্ডারের চেয়ে বেশি? correlated রূপে প্রতিটি অর্ডারের জন্য সেই গ্রাহকের গড় আবার হিসাব হয়; হাতে করা unnesting-এ সব গড় একবারে, তারপর join:
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM ord o
WHERE o.amount > (SELECT AVG(o2.amount) FROM ord o2 WHERE o2.cust_id = o.cust_id);
SELECT COUNT(*) FROM ord o
WHERE o.amount > (SELECT AVG(o2.amount) FROM ord o2 WHERE o2.cust_id = o.cust_id);
EXPLAIN QUERY PLAN
WITH a AS (SELECT cust_id, AVG(amount) AS avg_amt FROM ord GROUP BY cust_id)
SELECT COUNT(*) FROM ord o JOIN a ON a.cust_id = o.cust_id WHERE o.amount > a.avg_amt;
WITH a AS (SELECT cust_id, AVG(amount) AS avg_amt FROM ord GROUP BY cust_id)
SELECT COUNT(*) FROM ord o JOIN a ON a.cust_id = o.cust_id WHERE o.amount > a.avg_amt;প্রথম plan: SCAN o, তার নিচে CORRELATED SCALAR SUBQUERY 1 আর SEARCH o2 USING INDEX ord_cust (cust_id=?) — অর্থাৎ ২০,০০০টি অর্ডারের প্রতিটির জন্য একবার index-খোঁজ আর গড় হিসাব; একই গ্রাহকের ২৬০টি অর্ডারে একই গড় ২৬০ বার। SQLite এই subquery-কে নিজে unnest করেনি। দ্বিতীয় plan: CO-ROUTINE a একবার পুরো টেবিল index-এর ক্রমে পড়ে সব গড় বানায় (SCAN ord USING INDEX ord_cust), তারপর SCAN a আর SEARCH o USING INDEX ord_cust (cust_id=?). দুটির ফলই 3993 — rewrite-টি সঠিক। চালিয়ে দেখুন, প্রথমটি লক্ষণীয়ভাবে ধীর (আমাদের পরীক্ষায় কয়েক দশ গুণ)। PostgreSQL EXISTS আর IN-কে নিজেই semi-join-এ বদলায়; SQL Server আর Oracle আরও অনেক ধরনের subquery unnest করে। কোন DBMS কতটা করে তা জানা না থাকলে, plan দেখে নিশ্চিত হন।
৩ · Plan space — join order-এর বিস্ফোরণ
নিয়মের পর বড় সিদ্ধান্ত: join order. Inner join commutative আর associative (পর্ব ৩৩), তাই চারটি টেবিল A, B, C, D যেকোনো ক্রমে আর যেকোনো বন্ধনীতে join করলে ফল একই — কিন্তু মধ্যবর্তী ফলের আকার আকাশ-পাতাল আলাদা হতে পারে। গাছের আকার দুই রকম:
Left-deep
Left-deep tree: প্রতিটি join-এর ডান দিকে একটি মূল টেবিল, যেমন ((A ⋈ B) ⋈ C) ⋈ D. আগের ফল সরাসরি পরের join-এ বয়ে যায় (pipelining), আর ডান দিকের টেবিলে index থাকলে index nested loop চলে।
Bushy
Bushy tree: দুই দিকেই join-এর ফল থাকতে পারে, যেমন (A ⋈ B) ⋈ (C ⋈ D). কখনো অনেক সস্তা, কিন্তু খোঁজার জায়গা বহুগুণ বড়, আর মধ্যবর্তী ফল জমিয়ে রাখতে হয়।
কতগুলো ক্রম? Left-deep গাছ ঠিক করে দেয় টেবিলগুলোর একটি সারি — কোনটি প্রথম, কোনটি দ্বিতীয় … — তাই n টেবিলে n! টি left-deep গাছ: প্রথম জায়গায় n টি পছন্দ, দ্বিতীয়তে n − 1, এভাবে। Bushy গাছ গুনতে দুটি জিনিস লাগে: n টি পাতাওয়ালা পূর্ণ binary গাছের আকার কতভাবে হয় (Catalan সংখ্যা Cn−1), আর প্রতিটি আকারে পাতায় টেবিল বসানোর n! উপায়। গুণফল n! · Cn−1 = (2n − 2)! / (n − 1)!.
| টেবিল (n) | left-deep (n!) | bushy ((2n−2)!/(n−1)!) |
|---|---|---|
| ৩ | ৬ | ১২ |
| ৪ | ২৪ | ১২০ |
| ৬ | ৭২০ | ৩০,২৪০ |
| ১০ | ৩৬,২৮,৮০০ | প্রায় ১,৭৬৪ কোটি |
| ১৫ | প্রায় ১.৩ লাখ কোটি | তারও প্রায় ২৭ লাখ গুণ |
প্রতিটি গাছে প্রতিটি join-এর তিন-চারটি অ্যালগরিদম আর প্রতিটি টেবিলের কয়েকটি access path যোগ করলে সংখ্যাটি আরও বহুগুণ। দশ টেবিলের একটি রিপোর্ট-কোয়েরি বাস্তবে বিরল নয়। তাই দুটি কৌশল প্রায় সর্বজনীন: cross product এড়ানো — যে দুটি টেবিলের মধ্যে join শর্ত নেই, তাদের আগে জোড়া না লাগানো — আর একই উপসমস্যা বারবার না সমাধান করা, যা নিয়ে পরের অংশ।
৪ · System R-এর dynamic programming — Selinger-এর পদ্ধতি
ইতিহাস এক নজরে। ১৯৭০-এর দশকে IBM-এর San Jose গবেষণাগারে System R প্রমাণ করতে চেয়েছিল, declarative SQL-ও হাতে লেখা navigational কোডের কাছাকাছি গতিতে চলতে পারে। ১৯৭৯ সালে Patricia Selinger ও সহকর্মীরা SIGMOD সম্মেলনে প্রকাশ করেন “Access Path Selection in a Relational Database Management System”. সেই প্রবন্ধের তিনটি ধারণা — statistics থেকে selectivity, cost দিয়ে plan তুলনা, আর dynamic programming দিয়ে join order — আজও প্রায় সব বড় relational DBMS-এর optimizer-এর ভিত্তি।
মূল পর্যবেক্ষণটি principle of optimality: ধরুন {A, B, C, D}-এর সেরা left-deep plan শেষে D join করে। তাহলে তার ভেতরের {A, B, C}-এর অংশটিও {A, B, C}-এর সেরা plan হতেই হবে — নইলে সেই অংশ সস্তাটি দিয়ে বদলালে পুরো plan আরও সস্তা হতো। তাই প্রতিটি টেবিল-উপসেটের সেরা plan একবার হিসাব করে জমিয়ে রাখা যায়, আর বড় উপসেট গড়া যায় ছোটগুলো থেকে। এটিই dynamic programming — DSA কোর্সের DP-র ভিত্তি পর্বের সেই একই চিন্তা।
for each table R:
best[{R}] = সবচেয়ে সস্তা access path (scan বা index)
for size = 2 .. n:
for each subset S with |S| = size:
for each table R in S: # R হবে শেষে join হওয়া টেবিল
if S − {R} আর R-এর মধ্যে join শর্ত নেই: continue
p = join(best[S − {R}], R, সবচেয়ে সস্তা join অ্যালগরিদম)
if cost(p) < cost(best[S]): best[S] = p
return best[{all tables}]খরচ: উপসেট আছে 2ⁿ − 1টি, প্রতিটিতে সর্বোচ্চ n টি “শেষ টেবিল” চেষ্টা — তাই O(n · 2ⁿ) টি plan তুলনা, n! নয়। n = 10-এ n! প্রায় ৩৬ লাখ, অথচ n · 2ⁿ মাত্র ১০,২৪০. Bushy plan-ও ধরলে প্রতিটি উপসেটকে দুই ভাগে ভাঙার সব উপায় দেখতে হয়, মোট কাজ O(3ⁿ)। এবার চারটি টেবিলে হাতে চালান। ধরা যাক একটি বড় দোকান (সংখ্যাগুলো হিসাবের জন্য ধরে নেওয়া): customers-এর মধ্যে Sylhet-এর ১০০ জন (C′), orders ২০,০০০ (O), order_items ৬০,০০০ (I), আর products-এর মধ্যে Books শ্রেণির ১০টি (P′)। join শর্তগুলো একটি শিকল: C′–O–I–P′. সরল cost model: সব মধ্যবর্তী join-ফলের আকারের যোগফল (যত কম সারি পথে তৈরি হয়, তত সস্তা)। join-এর আকার অংশ ৬-এর সূত্রে।
| স্তর | |
| এই স্তরে জমা best[] | |
| তুলনা করা plan | |
| সেরা cost |
- ইনপুট। heuristic আগেই প্রতিটি টেবিলের নিজের শর্ত নিচে নামিয়েছে: Sylhet-এর ১০০ গ্রাহক, Books-এর ১০টি পণ্য। join শর্ত শুধু পাশাপাশি টেবিলের মধ্যে — C′–O, O–I, I–P′.
- স্তর ১. প্রতিটি টেবিল একা। এখানে join নেই, তাই আমাদের সরল model-এ খরচ ০; বাস্তবে এই স্তরেই প্রতিটি টেবিলের সেরা access path বাছা হয়।
- স্তর ২. join শর্ত আছে এমন তিনটি জোড়া। আকার |R|·|S|/max(V): {C′,O} ১,০০০, {O,I} ৬০,০০০, {I,P′} ৬,০০০. বাকি তিনটি জোড়ায় শর্ত নেই — cross product — তাই System R এদের দেখেই না।
- স্তর ৩. {C′,O,I}-এর দুটি পথ: আগে {C′,O} তারপর I (১,০০০ + ৩,০০০ = ৪,০০০), অথবা আগে {O,I} তারপর C′ (৬৩,০০০)। প্রথমটি জমা থাকে, দ্বিতীয়টি চিরতরে বাদ — principle of optimality. {O,I,P′}-এ জেতে আগে {I,P′}: ১২,০০০.
- স্তর ৪. পুরো কোয়েরির দুটি প্রার্থী, প্রতিটি আগের স্তরের জমানো সেরা plan থেকে: ৪,৩০০ বনাম ১২,৩০০. বিজয়ী ((C′ ⋈ O) ⋈ I) ⋈ P′ — ছোট টেবিল থেকে শুরু করে শিকল ধরে এগোনো।
- লাভ কতটা। সবচেয়ে খারাপ বৈধ left-deep ক্রম ৬৬,৩০০ — ১৫ গুণ। bushy গাছটি (৭,৩০০) এখানে জেতে না। আর ORDER BY order_id থাকলে order_id-ক্রমের একটি plan আলাদা করে রাখা হতো, যদিও সেটি একটু দামি — পরে sort বাঁচাতে পারে।
শেষ ধাপের কথাটির নাম interesting order, আর এটি Selinger-এর প্রবন্ধের সবচেয়ে সূক্ষ্ম অবদান। কোনো plan-এর ফল যদি এমন কলামে সাজানো থাকে, যা পরে কাজে লাগবে (ORDER BY, GROUP BY, বা পরের merge join-এর কলাম), তাহলে সেটি একটু দামি হলেও বাদ দেওয়া যায় না — কারণ সস্তা plan-টির পরে আলাদা sort লাগতে পারে। তাই DP প্রতিটি উপসেটে একটি নয়, প্রতিটি interesting order-এর জন্য একটি সেরা plan রাখে। principle of optimality তখন খাটে “একই উপসেট আর একই ক্রমের” plan-গুলোর মধ্যে।
৫ · Cardinality estimation — statistics আর histogram
DP-র প্রতিটি সংখ্যা — ১,০০০, ৩,০০০, ৩০০ — আসলে অনুমান। কোয়েরি না চালিয়ে DBMS এগুলো পায় আগে থেকে জমানো statistics থেকে, যা ANALYZE কমান্ড (বা স্বয়ংক্রিয় একটি কাজ) টেবিল পড়ে, প্রায়ই একটি নমুনা থেকে, বানায় আর system catalog-এ রাখে। প্রচলিত statistics:
- সারির সংখ্যা |R| আর page-এর সংখ্যা — scan-এর খরচের জন্য।
- Distinct মানের সংখ্যা V(R, a) — n_distinct; equality আর join-এর অনুমানের ভিত্তি।
- NULL-এর ভাগ —
a = 5বাa IS NULL-এর অনুমানে লাগে। - Most common values (MCV) — সবচেয়ে প্রচলিত কয়েকটি মান আর তাদের ভাগ।
- Histogram — বাকি মানগুলো কোন পরিসরে কতটা ঘন।
নিজের হাতে PostgreSQL-এর pg_stats-এর মতো পরিসংখ্যান বানান:
-- আকার, distinct মান, NULL-এর ভাগ
SELECT COUNT(*) AS n_rows, COUNT(DISTINCT city) AS v_city,
COUNT(DISTINCT division) AS v_division,
ROUND(AVG(phone IS NULL), 2) AS null_frac_phone
FROM cust;
-- status-এর MCV তালিকা: মান আর ভাগ
SELECT status, COUNT(*) AS cnt, ROUND(COUNT(*) / 20000.0, 3) AS freq
FROM ord GROUP BY status ORDER BY cnt DESC;
-- cust_id: কয়টি আলাদা মান, গড়ে আর সর্বোচ্চ কয়টি সারি
SELECT COUNT(DISTINCT cust_id) AS v_cust,
ROUND(COUNT(*) * 1.0 / COUNT(DISTINCT cust_id), 1) AS avg_rows,
(SELECT MAX(c) FROM (SELECT COUNT(*) AS c FROM ord GROUP BY cust_id)) AS max_rows
FROM ord;প্রথম ফল 2000 | 7 | 6 | 0.1: ৭টি শহর, ৬টি বিভাগ (Gazipur আলাদা শহর, কিন্তু ঢাকা বিভাগে), আর ১০% ফোন NULL. AVG(phone IS NULL) কৌশলটি কাজ করে কারণ SQLite-এ শর্তের মান 1 বা 0. MCV তালিকা: delivered | 17000 | 0.85, pending | 2000 | 0.1, cancelled | 800 | 0.04, returned | 200 | 0.01. শেষ ফল 1505 | 13.3 | 261: গড়ে একজন গ্রাহকের ১৩টির মতো অর্ডার, কিন্তু কারও ২৬১টি — গড় দিয়ে এই skew ধরা যায় না। শুধু V জানলে optimizer ভাবত, প্রতিটি status-এ ২০,০০০ / ৪ = ৫,০০০ সারি; MCV জানলে জানে returned মাত্র ২০০.
Equi-width বনাম equi-depth histogram
amount-এর মতো সংখ্যার কলামে হাজারো আলাদা মান, সবার জন্য MCV রাখা যায় না। তখন histogram: মানের পরিসরকে কয়েকটি bucket-এ ভাগ করে প্রতিটির সারি গোনা। দুই রকম ভাগ:
Equi-width
Equi-width: প্রতিটি bucket সমান চওড়া (যেমন প্রতিটি ৫,০০০ টাকার পরিসর)। বানানো সহজ, কিন্তু ডেটা একদিকে জমা থাকলে প্রায় সব সারি এক bucket-এ পড়ে, বাকিগুলো প্রায় খালি।
Equi-depth
Equi-depth (equi-height): প্রতিটি bucket-এ সমান সংখ্যক সারি; ঘন জায়গায় bucket সরু, পাতলা জায়গায় চওড়া। PostgreSQL-এর histogram_bounds এই ধরনের।
-- equi-depth: ১০টি bucket, প্রতিটিতে সমান সারি (NTILE, পর্ব ২৬)
SELECT b AS bucket, COUNT(*) AS rows_in, MIN(amount) AS lo, MAX(amount) AS hi
FROM (SELECT amount, NTILE(10) OVER (ORDER BY amount) AS b FROM ord)
GROUP BY b ORDER BY b;
-- equi-width: ১০টি সমান চওড়া bucket (100 থেকে 49994)
SELECT (amount - 100) * 10 / 49895 AS bucket, COUNT(*) AS rows_in
FROM ord GROUP BY bucket ORDER BY bucket;
-- আসল উত্তর
SELECT COUNT(*) AS actual FROM ord WHERE amount < 1000;Equi-depth-এর প্রতিটি bucket-এ 2000 সারি, সীমানা: 100–228, 228–357, 357–485, 485–614, 614–743, 743–871, 871–1011, 1013–3005, 3005–5029, 5065–49994. প্রথম সাতটি bucket মাত্র ৯০০ টাকার পরিসরে, শেষটি ৪৫ হাজার টাকা চওড়া — ডেটার ঘনত্ব নিজেই ধরা পড়েছে। Equi-width-এ bucket 0-তেই 18002 সারি, বাকি নয়টিতে ২১৪ থেকে ২৩১ করে। আসল উত্তর 13990. এই দুই histogram থেকে অনুমান কত হয়, নিচের ডায়াগ্রামে ধাপে ধাপে।
| পদ্ধতি | |
| অনুমান | |
| আসল |
- শুধু min আর max. statistics-এ histogram না থাকলে optimizer ধরে নেয়, মানগুলো পুরো পরিসরে সমানভাবে ছড়ানো। ৯০০ টাকার পরিসর মোট পরিসরের ১.৮% — তাই ৩৬১ সারি। অথচ ডেটার ৭০% ঠিক ওই জায়গায়।
- Equi-width. প্রথম bucket-এর ভেতরে ১৮,০০২ সারি, আর ১,০০০ সেই bucket-এর প্রথম ১৮%. bucket-এর ভেতরে সমান ছড়ানো ধরে ৩,২৪৭ — আগের চেয়ে ভালো, তবু চার গুণের বেশি কম। সমস্যা একই: যে bucket-এ সব সারি, তার ভেতরের খবর নেই।
- Equi-depth, পূর্ণ bucket. প্রথম ছয়টি bucket-এর সর্বোচ্চ মান ৮৭১, যা ১,০০০-এর কম — এদের সব সারি নিশ্চিতভাবে শর্ত মানে: ৬ × ২,০০০ = ১২,০০০.
- Equi-depth, আংশিক bucket. সপ্তম bucket ৮৭১ থেকে ১,০১১; ১,০০০ তার ৯২% পর্যন্ত। শুধু এই সরু bucket-এর ভেতরে সমান ছড়ানো ধরতে হয় — ভুলের জায়গা ছোট। মোট ১৩,৮৪৩.
- তুলনা। আসল ১৩,৯৯০. uniform অনুমান ৩৯ গুণ কম, equi-width ৪.৩ গুণ কম, equi-depth মাত্র ১% কম। একই সংখ্যক bucket, ভিন্ন ভাগ — ঘন জায়গায় বেশি bucket মানে বেশি নির্ভুলতা।
- উল্টো দিক। amount > 40000-এ uniform অনুমান ৪,০০৬ — আসলের (৪৩৪) ৯ গুণ বেশি। equi-depth-এর শেষ চওড়া bucket থেকে ৪৪৫. কম অনুমান আর বেশি অনুমান দুটিই বিপজ্জনক, তবে ভিন্নভাবে (অংশ ৭)।
বাস্তব সিস্টেমে এগুলো DBMS নিজে বানায়। PostgreSQL-এ default_statistics_target সাধারণত ১০০ — সর্বোচ্চ ১০০টি MCV আর ১০০ bucket-এর histogram, আর ANALYZE একটি নমুনা পড়ে (target-এর ৩০০ গুণ সারি)। MySQL 8.0 থেকে histogram আছে, তবে হাতে বানাতে হয়। তুলনার জন্য (এখানে চালানো যায় না):
-- PostgreSQL
ANALYZE ord;
SELECT attname, null_frac, n_distinct, most_common_vals,
most_common_freqs, histogram_bounds
FROM pg_stats WHERE tablename = 'ord';
ALTER TABLE ord ALTER COLUMN amount SET STATISTICS 500; -- বেশি bucket
-- MySQL 8.0+
ANALYZE TABLE ord UPDATE HISTOGRAM ON amount WITH 64 BUCKETS;৬ · Selectivity-র সূত্র — হাতে হিসাব
একটি শর্তের selectivity হলো সে মোট সারির কত ভগ্নাংশ রাখে; অনুমিত সারি = |R| × sel. System R-এর সময় থেকে প্রচলিত সূত্রগুলো:
| শর্ত | সূত্র | ধরে নেওয়া কথা |
|---|---|---|
a = c | 1 / V(R, a); c MCV-তে থাকলে তার ভাগ | সব মান সমান ঘন (uniformity) |
a < c | (c − min) / (max − min), বা histogram থেকে | পরিসরে সমান ছড়ানো |
a IS NULL | NULL-এর ভাগ | — |
NOT p | 1 − sel(p) | (NULL বাদ দিয়ে) |
p AND q | sel(p) × sel(q) | স্বাধীনতা |
p OR q | sel(p) + sel(q) − sel(p) × sel(q) | inclusion–exclusion + স্বাধীনতা |
R.a = S.a (join) | |R| · |S| / max(V(R,a), V(S,a)) | ছোট মান-সেট বড়টির ভেতরে (containment) |
Join-এর সূত্রটি কোথা থেকে? ধরুন V(R,a) ≤ V(S,a), আর ধরে নেওয়া হলো R-এর প্রতিটি a-মান S-এও আছে (মান-সেটের containment). R-এর একটি সারি নিন; তার a-মানটি S-এর V(S,a)টি মানের একটি, আর uniformity ধরলে S-এর প্রতিটি মানের |S| / V(S,a)টি সারি। তাই R-এর প্রতিটি সারি গড়ে |S| / V(S,a)টি সারির সাথে মেলে, মোট |R| · |S| / V(S,a). V(S,a) দুইয়ের বড়টি, তাই হর-এ max. বিশেষ ক্ষেত্র: a যদি S-এর primary key আর R-এর foreign key হয়, তাহলে V(S,a) = |S|, আর সূত্র দেয় ঠিক |R| — প্রতিটি অর্ডার ঠিক একজন গ্রাহকের সাথে মেলে। System R-এর মূল প্রবন্ধে statistics না থাকলে কিছু নির্দিষ্ট মানও ধরা হতো: equality-তে ১/১০, তুলনায় ১/৩, BETWEEN-এ ১/৪.
এবার সূত্র বনাম আসল সারি, আমাদের ডেটায়। ভাগগুলো আগের এডিটরের statistics থেকে: Dhaka ০.৫, ঢাকা বিভাগ ০.৬, Gazipur ০.১, চট্টগ্রাম বিভাগ ০.২, phone NULL ০.১, returned ০.০১:
SELECT 2000 / 7 AS est_by_1_over_V,
SUM(city = 'Sylhet') AS sylhet, SUM(city = 'Dhaka') AS dhaka FROM cust;
SELECT 2000 * 0.5 * 0.6 AS est_and, COUNT(*) AS actual
FROM cust WHERE city = 'Dhaka' AND division = 'Dhaka';
SELECT 2000 * 0.1 * 0.2 AS est_and, COUNT(*) AS actual
FROM cust WHERE city = 'Gazipur' AND division = 'Chattogram';
SELECT ROUND(2000 * (0.5 + 0.1 - 0.5 * 0.1)) AS est_or, COUNT(*) AS actual
FROM cust WHERE city = 'Dhaka' OR phone IS NULL;
SELECT ROUND(100 * 20000.0 / 1505) AS est_join, COUNT(*) AS actual
FROM cust c JOIN ord o ON o.cust_id = c.id WHERE c.city = 'Sylhet';
SELECT ROUND(100 * 20000.0 / 1505 * 0.01) AS est_3, COUNT(*) AS actual
FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';Equality: 285 | 100 | 1000 — 1/V প্রতিটি শহরে ২৮৫ (integer ভাগ) ধরে, অথচ Sylhet-এ ১০০, Dhaka-য় ১,০০০; MCV ছাড়া দুটিই ভুল, উল্টো দিকে। AND, correlated কলাম: 600 | 1000 — Dhaka শহরের সবাই ঢাকা বিভাগে, তাই দ্বিতীয় শর্ত কিছুই বাদ দেয় না; স্বাধীনতা ধরে নেওয়ায় কম অনুমান। 40 | 0 — Gazipur কখনো চট্টগ্রাম বিভাগে নয়, তবু সূত্র ৪০টি সারি দেখে। OR: 1100 | 1100 — শহর আর ফোন-NULL সত্যিই স্বাধীন, তাই সূত্র নিখুঁত (ROUND না দিলে দশমিকের হিসাবে 1099.9999999999998 দেখাত)। Join: 1329 | 1250 — |C′| = ১০০, |ord| = ২০,০০০, max(V) = max(১০০, ১,৫০৫) = ১,৫০৫; প্রায় মিলেছে। Join + আরেকটি শর্ত: 13 | 42 — তিন গুণের বেশি কম। কারণ ২০০টি returned অর্ডার মাত্র ৪৭ জন গ্রাহকের — ফেরত দেওয়া কয়েকজনের অভ্যাস, আর তা শহরের সাথে স্বাধীন নয়।
৭ · অনুমান কোথায় ভুল হয় — আর ভুল কীভাবে গুণ হয়
প্রতিটি সূত্রের পেছনে একটি “ধরে নেওয়া” আছে, আর বাস্তব ডেটা প্রায়ই সেটি ভাঙে। পাঁচটি পরিচিত উৎস:
Correlated কলাম
শহর আর বিভাগ, গাড়ির মডেল আর নির্মাতা, পোস্টকোড আর জেলা — AND-এ গুণ করলে অনুমান অনেক কম আসে।
Skew
কয়েকটি মানে বেশিরভাগ সারি (জনপ্রিয় পণ্য, বড় মার্চেন্ট)। গড় ধরে নেওয়া 1/V বিরল মানে বেশি, প্রচলিত মানে কম অনুমান দেয়।
পুরোনো statistics
শেষ ANALYZE-এর পর লাখো সারি ঢুকেছে; optimizer এখনো ছোট টেবিল ভাবছে। বিশেষ বিপদ: নতুন তারিখের সারি, যা histogram-এর max-এর বাইরে।
ফাংশন আর হিসাব
WHERE UPPER(city) = 'DHAKA' বা amount * 1.15 > 5000: কলামের statistics এখানে সরাসরি খাটে না, optimizer একটি নির্দিষ্ট আন্দাজি ভাগ ধরে।
Parameter
WHERE status = ?: plan বানানোর সময় মান অজানা, অথচ 'delivered' আর 'returned'-এ সারি ৮৫ গুণ আলাদা (অংশ ৯)।
ভুল কেন গুণ হয়? join-এর অনুমান = বাঁ দিকের অনুমান × ডান দিকের অনুমান × join selectivity. ধরুন প্রতিটি শর্তের অনুমান সঠিকের f গুণ (f = ৩ মানে তিন গুণ বেশি বা কম)। দুটি শর্তের পর ভুল f², আর k টি এমন ধাপের পর fk পর্যন্ত — f = ৩ আর k = ৫ হলে ২৪৩ গুণ। সব ভুল একদিকে যায় না, তবু ১৯৯১ সালে Ioannidis আর Christodoulakis দেখান, খারাপ ক্ষেত্রে join-এর সংখ্যার সাথে ভুল সূচকীয় হারে বাড়ে। ২০১৫ সালে Leis ও সহকর্মীদের “How Good Are Query Optimizers, Really?” প্রবন্ধ IMDb-র আসল ডেটায় কয়েকটি বড় DBMS পরীক্ষা করে দেখায়, বহু-join কোয়েরিতে সবাই প্রায়ই অনেক কম অনুমান করে, আর plan-এর গুণমানে cost model-এর চেয়ে cardinality-র ভুলের প্রভাব অনেক বেশি।
কম অনুমান সবচেয়ে বিপজ্জনক। optimizer যদি ভাবে একটি ধাপে ১০টি সারি আসবে, সে nested loop বেছে নেয় — ১০ বার ভেতরের টেবিল খোঁজা সস্তা। আসলে ১০ লাখ সারি এলে সেই একই plan ১০ লাখ বার খোঁজে। বেশি অনুমানের দাম সাধারণত সীমিত (দরকারের চেয়ে বড় hash table), কিন্তু কম অনুমানের দাম কোয়েরিকে মিলিসেকেন্ড থেকে ঘণ্টায় নিয়ে যেতে পারে।
প্রতিকারের একটি পথ বাড়তি statistics. PostgreSQL 10 থেকে extended statistics দিয়ে কয়েকটি কলামের সম্পর্ক জানানো যায় (নিচের mcv ধরনটি এসেছে PostgreSQL 12-এ); SQL Server আর Oracle-এ বহু-কলামের statistics-এর নিজস্ব ব্যবস্থা আছে:
CREATE STATISTICS cust_city_div (dependencies, ndistinct, mcv)
ON city, division FROM cust;
ANALYZE cust; -- এখন city = 'Dhaka' AND division = 'Dhaka'-এ প্রায় ১,০০০৮ · হাতে-কলমে — index, ANALYZE আর plan-এর বদল
SQLite-এর optimizer-ও cost-based. তার statistics থাকে sqlite_stat1 টেবিলে, যা ANALYZE বানায়। প্রথমে দেখুন সেখানে কী থাকে:
CREATE INDEX ord_status ON ord(status);
CREATE INDEX ord_cust ON ord(cust_id);
CREATE INDEX cust_city ON cust(city);
ANALYZE;
SELECT * FROM sqlite_stat1;তিনটি সারি: ord | ord_cust | 20000 14, ord | ord_status | 20000 5000, cust | cust_city | 2000 286. stat কলামের প্রথম সংখ্যা index-এ কয়টি সারি, দ্বিতীয়টি index-এর প্রথম কলামের একটি মানে গড়ে কয়টি সারি — অর্থাৎ |R| / V, ওপরের দিকে গোল করা: ২০,০০০ / ৪ = ৫,০০০; ২০,০০০ / ১,৫০৫ ≈ ১৩.৩ → ১৪; ২,০০০ / ৭ ≈ ২৮৬. এটিই অংশ ৬-এর 1/V সূত্র। কোনো MCV নেই, histogram নেই — পাতার এই SQLite SQLITE_ENABLE_STAT4 ছাড়া তৈরি, যা থাকলে sqlite_stat4-এ নমুনা মানও জমা হতো। তাই এই SQLite জানে না যে returned বিরল।
এবার একটি join-এর plan কীভাবে বদলায়, index যোগ করতে করতে। প্রশ্নটি অংশ ২-এর: Sylhet-এর গ্রাহকদের returned অর্ডার। (এখানে ANALYZE নেই।)
-- ক. কোনো index নেই
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';
-- খ. city আর join-কলামে index
CREATE INDEX cust_city ON cust(city);
CREATE INDEX ord_cust ON ord(cust_id);
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';
-- গ. status-এও index
CREATE INDEX ord_status ON ord(status);
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';ক: SCAN o তারপর SEARCH c USING INTEGER PRIMARY KEY (rowid=?) — বাইরের loop ord-এর সব ২০,০০০ সারি, প্রতিটিতে primary key দিয়ে গ্রাহক খোঁজা। c.id primary key, তাই cust ভেতরে বসাই স্বাভাবিক। খ: SEARCH c USING COVERING INDEX cust_city (city=?) তারপর SEARCH o USING INDEX ord_cust (cust_id=?) — join order উল্টে গেছে: এখন বাইরে Sylhet-এর গ্রাহক, ভেতরে তাঁদের অর্ডার। গ: SEARCH o USING INDEX ord_status (status=?) তারপর SEARCH c USING INTEGER PRIMARY KEY (rowid=?) — আবার উল্টো, এবার returned অর্ডার দিয়ে শুরু। statistics না থাকায় SQLite ধরে নেয় প্রতিটি index-equality অল্প কয়েকটি সারি দেয়; তিনটি plan-ই সেই আন্দাজের ওপর তৈরি।
এবার তিনটি index-ই আছে। ANALYZE চালালে কী হয়, আর কোন পথে আসলে কত সারি ছুঁতে হয়:
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';
ANALYZE;
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';
-- দুই পথে কত সারি পড়তে হয়?
SELECT (SELECT COUNT(*) FROM ord o JOIN cust c ON c.id = o.cust_id
WHERE c.city = 'Sylhet') AS via_city,
(SELECT COUNT(*) FROM ord WHERE status = 'returned') AS via_status;
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM ord WHERE status = 'delivered' AND amount > 40000;ANALYZE-এর আগে plan শুরু হয় ord_status দিয়ে; পরে SEARCH c USING COVERING INDEX cust_city (city=?) আর SEARCH o USING INDEX ord_cust (cust_id=?). statistics পেয়ে SQLite খারাপ plan বেছেছে! কারণ sqlite_stat1 বলে, একটি status-এ গড়ে ৫,০০০ সারি, আর একটি শহরে ২৮৬ জন × ১৪টি অর্ডার। আসল কাজ 1250 | 200: city-পথে ১,২৫০টি অর্ডার পড়ে status মেলাতে হয়, status-পথে মাত্র ২০০টি — ছয় গুণের বেশি পার্থক্য। গড় skew লুকিয়েছে। শেষ plan উল্টো দিকের ভুল: SEARCH ord USING INDEX ord_status (status=?) — SQLite ভাবছে 'delivered'-এ ৫,০০০ সারি, আসলে ১৭,০০০; এত সারির জন্য index দিয়ে একটি একটি করে টেবিলে যাওয়ার চেয়ে পুরো টেবিল একবার পড়া সাধারণত সস্তা। MCV থাকলে (PostgreSQL, বা STAT4-ওয়ালা SQLite) দুটি ভুলই এড়ানো যেত।
Statistics তাজা রাখা আর plan-এর কারণ পড়া
- PostgreSQL: autovacuum নিজে থেকে ANALYZE চালায়, যখন শেষবারের পর যথেষ্ট সারি বদলায় (মোটামুটি টেবিলের ১০% আর একটি ছোট নির্দিষ্ট সংখ্যা)। বড় bulk load বা নতুন টেবিলের পর নিজে
ANALYZEচালান — নইলে পরের কয়েকটি কোয়েরি পুরোনো ছবিতে plan পায়। - MySQL/InnoDB: persistent statistics নিজে হালনাগাদ হয়; বড় পরিবর্তনের পর
ANALYZE TABLE. - SQLite: statistics কখনো নিজে হয় না। SQLite-এর নির্দেশিকা অনুযায়ী ডেটাবেস বন্ধের আগে বা মাঝে মাঝে
PRAGMA optimizeচালানো ভালো, যা দরকার মনে হলে ANALYZE করে।
plan কেন বাছা হলো, তা পড়ার মূল কৌশল: প্রতিটি ধাপে আনুমানিক সারি বনাম আসল সারি মেলানো। PostgreSQL-এর EXPLAIN ANALYZE কোয়েরি সত্যিই চালায়, আর প্রতিটি গিঁটে দুটিই দেখায়। নিচে আকৃতিটি (সংখ্যাগুলো ব্যাখ্যার জন্য, কোনো আসল রানের নয়):
Nested Loop (cost=… rows=13 …) (actual time=… rows=42 loops=1)
-> Index Scan using cust_city on cust c (… rows=100 …) (actual … rows=100 loops=1)
-> Index Scan using ord_cust on ord o (… rows=1 …) (actual … rows=0 loops=100)
Filter: (status = 'returned')
প্রথম rows= অনুমান, actual … rows= আসল (প্রতি loop-এ গড়)। খোঁজার নিয়ম: নিচ থেকে ওপরে যান, আর প্রথম যে গিঁটে অনুমান আর আসলের মধ্যে ১০ গুণ বা তার বেশি ফারাক, সেখানেই সাধারণত ভুল plan-এর শিকড়। ওপরের গিঁটগুলো সেই ভুলের ওপর দাঁড়িয়ে।
৯ · অনেক join, plan cache আর hint
অনেক টেবিল: randomized আর greedy খোঁজ
O(n · 2ⁿ)-ও অসীম নয়: ২০টি টেবিলে ২ কোটির বেশি plan. তাই প্রতিটি DBMS একটি সীমার পর পুরো DP ছেড়ে দেয়। PostgreSQL-এ FROM-এর আইটেম geqo_threshold-এর (সাধারণত ১২) সমান বা বেশি হলে GEQO চালু হয় — genetic algorithm-এর মতো: কয়েকটি এলোমেলো join order-এর একটি “জনসংখ্যা”, ভালোগুলো মিলিয়ে নতুন ক্রম, কয়েক প্রজন্ম পর সেরাটি। ফল ভালো, কিন্তু নিশ্চিতভাবে সেরা নয়। join_collapse_limit (সাধারণত ৮) ঠিক করে, লেখা JOIN-ক্রম কতটা নতুন করে সাজানো হবে। MySQL-এর optimizer greedy খোঁজ ব্যবহার করে, যার গভীরতা optimizer_search_depth দিয়ে বদলানো যায়।
Plan cache আর parameter-এর ফাঁদ
প্রতিবার plan বানানো দামি, তাই অ্যাপ prepared statement (পর্ব ৪৯) ব্যবহার করলে DBMS একটি plan জমা রাখে (plan cache). সমস্যা: WHERE status = ?-এর জন্য একটি plan, অথচ 'returned'-এ index চমৎকার আর 'delivered'-এ সর্বনাশ। SQL Server প্রথম ডাকের মান দেখে plan বানায় আর জমা রাখে — parameter sniffing; প্রথম ডাকটি বিরল মানের হলে পরের সব সাধারণ মানের ডাক ধীর। প্রতিকার আছে: SQL Server-এ OPTION (RECOMPILE) বা OPTIMIZE FOR, আর SQL Server 2022-এ parameter-sensitive plan optimization, যা একই কোয়েরির কয়েকটি plan রাখতে পারে। PostgreSQL প্রথম পাঁচবার প্রতিটি মানের জন্য আলাদা custom plan বানায়, তারপর একটি generic plan-এ যায় যদি সেটি গড়ে খুব খারাপ না হয়; plan_cache_mode দিয়ে এটি বদলানো যায়।
Adaptive execution
অনুমান যখন অনিশ্চিত, সিদ্ধান্তটি কিছুটা চালানোর সময় পর্যন্ত পিছিয়ে দেওয়া যায় — adaptive execution. Oracle 12c-র adaptive plan প্রথম কিছু সারি দেখে nested loop আর hash join-এর মধ্যে বেছে নেয়; SQL Server 2017-এর adaptive join একই রকম। Spark SQL-এর adaptive query execution প্রতিটি পর্যায়ের আসল আকার দেখে বাকি plan বদলায়।
Hint আর plan forcing — শেষ উপায়
Hint মানে optimizer-কে বলে দেওয়া — এই index ব্যবহার করা, এই ক্রমে join করা। প্রতিটি DBMS-এর নিজস্ব রূপ (তুলনার জন্য, এখানে চালানো যায় না):
-- MySQL 8.0
SELECT /*+ JOIN_ORDER(o, c) INDEX(o ord_status) */ COUNT(*)
FROM cust c JOIN ord o ON o.cust_id = c.id WHERE … ;
-- Oracle
SELECT /*+ LEADING(o c) USE_NL(c) INDEX(o ord_status) */ COUNT(*) FROM … ;
-- SQL Server
SELECT COUNT(*) FROM ord o WITH (INDEX(ord_status)) JOIN cust c ON …
OPTION (FORCE ORDER);
-- PostgreSQL: মূল সংস্করণে hint নেই; pg_hint_plan extension দিয়ে
/*+ Leading(o c) IndexScan(o ord_status) */ SELECT COUNT(*) FROM … ;SQLite-এ hint-এর কাজ করে তিনটি জিনিস: likelihood(X, p) — শর্ত X কত ভাগ সারিতে সত্য, তা বলে দেওয়া; likely(X) মানে p = 0.9375, unlikely(X) মানে p = 0.0625; আর CROSS JOIN লিখলে বাঁ দিকের টেবিল বাইরের loop-এ থাকতে বাধ্য। অংশ ৮-এর ভুল plan-টি দিয়ে পরীক্ষা করুন — এই এডিটরে তিনটি index আর ANALYZE আগে থেকেই করা:
-- ক. কোনো hint নেই
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND o.status = 'returned';
-- খ. unlikely(): শর্তটি 6.25% সারিতে সত্য
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND unlikely(o.status = 'returned');
-- গ. likelihood(): আসল ভাগ 1%
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM cust c JOIN ord o ON o.cust_id = c.id
WHERE c.city = 'Sylhet' AND likelihood(o.status = 'returned', 0.01);
-- ঘ. CROSS JOIN: ord বাইরে থাকতে বাধ্য
EXPLAIN QUERY PLAN SELECT COUNT(*) FROM ord o CROSS JOIN cust c
WHERE o.cust_id = c.id AND c.city = 'Sylhet' AND o.status = 'returned';ক আর খ একই plan: SEARCH c USING COVERING INDEX cust_city (city=?), তারপর SEARCH o USING INDEX ord_cust (cust_id=?). unlikely() optimizer-এর অনুমান কমিয়েছে, কিন্তু ২০,০০০ × ০.০৬২৫ = ১,২৫০ সারির অনুমান SQLite-এর হিসাবে city-পথকে হারাতে যথেষ্ট ছিল না — “hint দিলেই plan বদলায়” ধরে নেবেন না, পরীক্ষা করুন। গ: SEARCH o USING INDEX ord_status (status=?) তারপর SEARCH c USING INTEGER PRIMARY KEY (rowid=?) — আসল ভাগ বলে দেওয়ায় plan ঠিক হয়েছে। ঘ: একই ক্রম, সাথে BLOOM FILTER ON c (id=?) — একটি ছোট Bloom filter, যা অমিল সারির খোঁজ আগেই বাদ দেয়। hint শুধু পথ বদলায়, উত্তর নয় — যেকোনো রূপ থেকে EXPLAIN QUERY PLAN সরিয়ে চালালে ফল একই, ৪২.
Hint হলো আজকের ডেটার জন্য একটি স্থায়ী সিদ্ধান্ত। ডেটা বদলায়, index বদলায়, DBMS-এর নতুন সংস্করণ আসে — hint চুপচাপ থেকে যায়। তাই আগে statistics ঠিক করুন (ANALYZE, বাড়তি statistics, কোয়েরি নতুন করে লেখা); hint দিলে কারণটি পাশে মন্তব্যে লিখুন, আর নিয়মিত যাচাই করুন hint ছাড়া plan এখনো খারাপ কি না। কোয়েরি না বদলে plan স্থির রাখার ব্যবস্থাও আছে — SQL Server-এর Query Store-এ plan forcing, Oracle-এর SQL plan baseline — একই সতর্কতায়।
আরও গভীরে: Selinger-এর bottom-up DP-র পাশে আরেকটি বড় ধারা Goetz Graefe-র Volcano (১৯৯৩) আর Cascades (১৯৯৫) কাঠামো: top-down খোঁজ, memoization, আর rule দিয়ে সমতুল্য রূপের একটি “memo” গড়ে তোলা; SQL Server-এর optimizer এই ধারার বলে পরিচিত। Join-graph-এর গঠন ধরে শুধু সংযুক্ত উপসেট গোনা আরও দক্ষ DP অ্যালগরিদম (যেমন Moerkotte আর Neumann-এর DPccp, ২০০৬) গবেষণায় প্রতিষ্ঠিত। আর সাম্প্রতিক গবেষণা machine learning দিয়ে cardinality অনুমান বা plan বাছাই শেখানোর চেষ্টা করছে — আশাব্যঞ্জক, তবে বড় DBMS-এর মূল optimizer-এ এখনো সীমিত।
১০ · AI-এর যুগে query optimization
“এই কোয়েরি দ্রুত করো” — AI সহজেই নতুন index, hint বা নতুন করে লেখা কোয়েরি দেয়। কিন্তু আপনার ডেটার বণ্টন, statistics আর আসল plan সে দেখেনি। সাধারণ ভুল:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| plan না দেখেই index বা hint-এর পরামর্শ | অপ্রয়োজনীয় index, লেখা ধীর | আগে আর পরে EXPLAIN (ANALYZE) মিলিয়ে দেখুন |
| “JOIN সবসময় subquery-র চেয়ে দ্রুত” — সাধারণীকরণ | ভুল rewrite, কখনো ফলও বদলায় (duplicate) | দুই রূপের ফল EXCEPT দিয়ে মেলান, সারি গুনুন |
| IN-কে JOIN-এ বদলানো, DISTINCT ছাড়া | এক-থেকে-বহু সম্পর্কে সারি গুণ হয় | COUNT(*) আগে-পরে তুলনা করুন |
| statistics-এর কথা না তোলা | আসল কারণ (পুরোনো stats, skew) থেকে যায় | ANALYZE চালিয়ে আবার plan দেখুন |
| অন্য DBMS-এর hint-সিনট্যাক্স (Oracle-এর hint PostgreSQL-এ) | মন্তব্য হিসেবে নীরবে উপেক্ষিত | plan সত্যিই বদলেছে কি না দেখুন |
| অনুমিত কলাম বা index-এর নাম | ভুল বা অচল কোড | schema আর index-তালিকা নিজে দিন |
AI-কে অনুমানের উপাদান দিন — schema, index, statistics আর আসল plan:
AI-এর দেওয়া optimization চালু করার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Heuristic আর cost-based optimization-এর পার্থক্য? — প্রথমটি নিয়ম দিয়ে প্রায় সবসময় লাভজনক rewrite (selection আগে), দ্বিতীয়টি statistics থেকে খরচ অনুমান করে plan তুলনা।
- n টেবিলে কতগুলো left-deep join order? — n!; bushy ধরলে (2n − 2)! / (n − 1)!.
- System R কীভাবে join order খোঁজে? — dynamic programming: প্রতিটি উপসেটের সেরা plan (প্রতিটি interesting order-এর জন্য), ছোট উপসেট থেকে বড়; cross product এড়িয়ে।
- Join size কীভাবে অনুমান করবেন? — |R| · |S| / max(V(R,a), V(S,a)); foreign key join-এ ফল ≈ foreign key-ওয়ালা টেবিলের আকার।
- Optimizer ভুল plan কেন বাছে? — cardinality-র ভুল: correlated কলাম, skew, পুরোনো statistics, ফাংশন, parameter; join-এ ভুল গুণ হয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Selinger-এর ১৯৭৯ সালের প্রবন্ধের পর চার দশকের বেশি কেটেছে, হার্ডওয়্যার পুরোপুরি বদলেছে। তবু বড় DBMS-গুলোর optimizer এখনো মূলত সেই কাঠামোয় চলে কেন? কোথায় কাঠামোটি দুর্বল?
System R-এর প্রবন্ধটি আসলে তিনটি আলাদা ধারণাকে একসাথে জুড়েছিল, আর তিনটিই হার্ডওয়্যার-নিরপেক্ষ। প্রথমটি খরচকে সংখ্যায় প্রকাশ করা: page পড়া আর CPU কাজের একটি ভারযুক্ত যোগফল। ডিস্ক থেকে SSD বা মেমরিতে গেলে ভারগুলো বদলায় — PostgreSQL-এর random_page_cost-এর মতো সেটিং ঠিক এই কাজ করে — কিন্তু “plan-এর একটি দাম আছে, সবচেয়ে কম দামেরটি বাছো” কাঠামো একই থাকে।
দ্বিতীয়টি statistics থেকে selectivity. ১/V, range-এর ভগ্নাংশ, AND-এ গুণ — এই সূত্রগুলো সরল, দ্রুত আর ব্যাখ্যাযোগ্য। পরে MCV, ভালো histogram আর বহু-কলামের statistics যোগ হয়েছে, কিন্তু ভিত্তি সেই একই। তৃতীয়টি dynamic programming আর interesting order: n!-এর খোঁজকে O(n · 2ⁿ)-এ নামানো, আর এমন plan বাঁচিয়ে রাখা যার সাজানো ফল পরে sort বাঁচায়। এটি একটি গাণিতিক কৌশল, যন্ত্রের ওপর নির্ভর করে না।
দুর্বলতাও স্পষ্ট। এক, principle of optimality cost model-এর একটি অনুমানের ওপর দাঁড়িয়ে: একটি উপ-plan-এর খরচ তার চারপাশের plan-এর ওপর নির্ভর করে না। বাস্তবে buffer pool-এ কী আছে, কয়টি কোয়েরি একসাথে চলছে, সমান্তরাল কাজ — এসব সেই অনুমান ভাঙে। দুই, পুরো ভবন দাঁড়িয়ে cardinality-র ওপর, যা সূত্রগুলোর স্বাধীনতা আর uniformity-র অনুমান থেকে আসে; DP নিখুঁতভাবে একটি ভুল সংখ্যার সেরা উত্তর খুঁজে দেয়। তিন, এটি কম্পাইল-সময়ের সিদ্ধান্ত — একবার plan, তারপর অন্ধভাবে চালানো। Adaptive execution, বিভিন্ন DBMS-এর চালানোর সময়ের প্রতিক্রিয়া আর learned optimizer-এর গবেষণা ঠিক এই তিন জায়গায় কাজ করছে।
তবু বিকল্পগুলো সাধারণত কাঠামোটি ফেলে দেয় না, তার ওপর স্তর যোগ করে — কারণ ব্যাখ্যাযোগ্যতা, পূর্বানুমানযোগ্যতা আর গতিতে এর সমকক্ষ এখনো বিরল।
মূল উপলব্ধি: System R টিকে আছে কারণ এর ধারণাগুলো যন্ত্রের নয়, গণিতের। দুর্বলতাও গণিতের: যে সংখ্যা দিয়ে সে হিসাব করে, তা নিজেই অনুমান।
প্র ০২গবেষণায় বারবার দেখা গেছে, plan-এর গুণমানে cost model-এর নিখুঁততার চেয়ে cardinality-র অনুমান অনেক বেশি গুরুত্বপূর্ণ। কেন এমন হয়, আর optimizer-এর নকশাকারীরা এ থেকে কী শিক্ষা নিয়েছেন?
একটি উদাহরণ দিয়ে ভাবুন। ধরুন cost model ২০% ভুল — hash join-কে বাস্তবের চেয়ে একটু সস্তা ভাবে। এতে দুটি কাছাকাছি plan-এর মধ্যে মাঝে মাঝে ভুলটি বাছা হয়, কিন্তু দুটিই কাছাকাছি, তাই ক্ষতি কম। এবার ধরুন একটি join-এর ফল ১০ সারি ভাবা হলো, আসলে ১০ লাখ। তখন plan-এর পুরো আকারই বদলে যায়: nested loop বাছা হয়, পরের join-গুলো সেই ভুল ছোট ফলের ওপর সাজানো হয়, মেমরি কম বরাদ্দ হয়। ২০% আর এক লাখ গুণ — দুই ভুলের মাপই আলাদা জগতের।
দ্বিতীয় কারণ গুণ হওয়া। অংশ ৭-এ দেখেছেন, প্রতিটি join-এ বাঁ আর ডান দিকের অনুমান গুণ হয়, তাই ভুলও গুণ হয়। Leis ও সহকর্মীদের ২০১৫ সালের পরীক্ষায় IMDb-র আসল, correlated ডেটায় কয়েকটি বড় DBMS পাঁচ-ছয়টি join-এর পর প্রায়ই কয়েক হাজার গুণ বা তারও বেশি কম অনুমান করেছিল। একই প্রবন্ধে তাঁরা দেখান, সঠিক cardinality দিলে খুব সরল cost model-ও প্রায় সেরা plan খুঁজে দেয়।
শিক্ষাগুলো কয়েকটি স্তরে। এক, ভালো statistics: MCV, histogram, বহু-কলামের statistics, আর join-এর জন্য নমুনা। দুই, robustness: শুধু সবচেয়ে সস্তা অনুমানের plan নয়, এমন plan বাছা যা অনুমান ভুল হলেও বিপর্যয় ঘটায় না — যেমন অনিশ্চিত হলে nested loop-এর বদলে hash join, যার খারাপ ক্ষেত্রও সীমিত। তিন, চালানোর সময় যাচাই: আসল সারি অনুমানের চেয়ে অনেক বেশি হলে plan বদলানো (adaptive join). চার, শেখা: IBM DB2-এর LEO প্রকল্পের মতো (২০০১) আগের রানের আসল সংখ্যা দিয়ে পরের অনুমান শোধরানো।
মূল উপলব্ধি: optimizer একটি অনুমানের ওপর হিসাবের যন্ত্র; ইনপুট ভুল হলে নিখুঁত হিসাবও ভুল উত্তর দেয়। তাই উন্নতির বড় অংশ এসেছে অনুমান ভালো করা আর অনুমান ভুল হলে টিকে থাকার কৌশল থেকে।
প্র ০৩Oracle-এ শত শত hint আছে, অথচ PostgreSQL-এর মূল সংস্করণ বহু বছর ধরে hint যোগ করতে অস্বীকার করেছে। দুই দর্শনের যুক্তি কী, আর একটি দল কোনটির দিকে ঝুঁকবে?
Hint-এর পক্ষের যুক্তি ব্যবহারিক। কোটি টাকার লেনদেনের একটি রাতের batch হঠাৎ ধীর হলো, কারণ optimizer একটি নতুন plan বেছেছে। DBA-র হাতে তখন সময় নেই statistics-এর গভীরে যাওয়ার; একটি hint দিয়ে পুরোনো plan ফিরিয়ে আনা মানে সকালে ব্যাংক খোলা। Oracle-এর দীর্ঘ ইতিহাসে বিশাল এন্টারপ্রাইজ গ্রাহকদের এই চাহিদা থেকেই hint আর পরে SQL plan baseline-এর মতো plan স্থির রাখার ব্যবস্থা এসেছে। MySQL আর SQL Server-ও একই পথে।
PostgreSQL সম্প্রদায়ের যুক্তি দীর্ঘমেয়াদি। তাদের উইকিতে এই আলোচনা লেখা আছে, মূল কথাগুলো মোটামুটি এরকম: hint কোয়েরির ভেতরে একটি স্থায়ী সিদ্ধান্ত, যা ডেটা বদলালে পুরোনো হয়ে যায়; hint দিয়ে সমস্যা চাপা দিলে optimizer-এর আসল দুর্বলতা সম্প্রদায়ের কাছে রিপোর্টই হয় না, তাই optimizer উন্নত হয় না; আর নতুন সংস্করণের ভালো plan থেকে hint-ওয়ালা কোয়েরি বঞ্চিত থাকে। তার বদলে তারা ভালো statistics (extended statistics), কিছু planner সেটিং আর CTE-র MATERIALIZED-এর মতো স্পষ্ট নিয়ন্ত্রণ দিয়েছে। যাঁদের তবু দরকার, তাঁদের জন্য pg_hint_plan extension আছে — মূল কোডের বাইরে।
দুই দর্শনই একটি সত্য মানে: optimizer কখনো কখনো ভুল করবে। পার্থক্য হলো ভুলের প্রতিকার কোথায় — কোয়েরিতে, না optimizer-এর ইনপুটে। একটি দলের জন্য বাস্তব পথ প্রায়ই মাঝামাঝি: প্রথমে কারণ খুঁজুন (EXPLAIN ANALYZE, statistics), ইনপুট ঠিক করুন; জরুরি অবস্থায় hint বা plan forcing দিন, কিন্তু তাকে একটি “অস্থায়ী সেলাই” হিসেবে ট্র্যাক করুন — কারণ, তারিখ আর কবে আবার যাচাই হবে সহ। নিয়ন্ত্রিত, কম বদলানো রিপোর্টিং কোয়েরিতে hint কম ঝুঁকির; দ্রুত বদলানো OLTP ডেটায় বেশি।
মূল উপলব্ধি: hint আজকের সমস্যা মেটায়, আগামী দিনের সমস্যা লুকায়। ভালো দল hint ব্যবহার করে, কিন্তু কখনো ভুলে যায় না যে সেটি সেখানে আছে।
প্র ০৪একটি মোবাইল ওয়ালেট অ্যাপে “একজন মার্চেন্টের আজকের লেনদেন” কোয়েরিটি prepared statement-এ চলে। বেশিরভাগ মার্চেন্টের দিনে কয়েকটি লেনদেন, কিন্তু কয়েকটি বড় চেইন দোকানের হাজারো। সোমবার সকালে সার্ভার রিস্টার্টের পর হঠাৎ সব মার্চেন্টের পাতা ধীর। কী ঘটে থাকতে পারে?
সবচেয়ে সম্ভাব্য গল্পটি plan cache আর skew-এর মিলন। রিস্টার্টে plan cache খালি হয়। সকালে প্রথম যে অনুরোধটি এলো, ধরুন সেটি একটি বড় চেইন দোকানের — merchant_id-এর মানটি হাজারো সারির। SQL Server-এর মতো sniffing-ভিত্তিক সিস্টেমে optimizer এই মান দেখে plan বানায়: হাজারো সারি, তাই হয়তো পুরো দিনের partition scan আর hash aggregate. plan-টি জমা হয়। এরপর ছোট মার্চেন্টদের প্রতিটি অনুরোধ — যাদের দরকার ছিল একটি index seek আর পাঁচটি সারি — একই ভারী plan পায়। উল্টোটাও ঘটে: প্রথম ডাক ছোট মার্চেন্টের হলে nested loop plan জমা হয়, আর বড় দোকানের পাতা হাজারো index-খোঁজে আটকে যায়।
যাচাইয়ের পথ: plan cache থেকে কোয়েরিটির জমা plan আর তার “compiled parameter value” দেখা (SQL Server-এ এটি plan-এর XML-এ থাকে), আর একই কোয়েরি ছোট আর বড় মান দিয়ে আলাদা চালিয়ে plan তুলনা।
প্রতিকার কয়েক স্তরের। দ্রুততম: কোয়েরিটির plan cache থেকে মুছে নতুন করে compile করানো — কিন্তু পরের রিস্টার্টে আবার লটারি। নির্দিষ্ট কোয়েরিতে OPTION (RECOMPILE): প্রতিবার নতুন plan, compile-এর খরচ দিয়ে; দিনে লাখো ডাকে সেটি নিজেই ভার। OPTIMIZE FOR দিয়ে একটি প্রতিনিধিত্বমূলক মান ঠিক করে দেওয়া। SQL Server 2022-এর parameter-sensitive plan optimization একই কোয়েরির কয়েকটি plan রাখতে পারে, মানের পরিসর অনুযায়ী। নকশায় সমাধান: বড় চেইন দোকানগুলোর জন্য আলাদা কোয়েরি-পথ। PostgreSQL-এ একই ঝুঁকি generic plan-এ; প্রথম পাঁচবার custom plan বানানোর নিয়মটি অংশত এই সমস্যা ধরতেই।
মূল উপলব্ধি: plan cache ধরে নেয়, একটি কোয়েরির জন্য একটি plan যথেষ্ট। skewed ডেটায় এই অনুমান ভাঙে — তখন “কোন মান দিয়ে plan বানানো হয়েছিল” প্রশ্নটিই রহস্যের চাবি।
প্র ০৫“অনুমান না করে কোয়েরি চালাতে চালাতেই শিখলে কেমন হয়?” — এই চিন্তা থেকে adaptive execution আর learned optimizer-এর গবেষণা এসেছে। এগুলোর মূল ধারণা কী, আর প্রচলিত optimizer এগুলোকে পুরোপুরি জায়গা দেয়নি কেন?
সবচেয়ে মৌলিক রূপটি এসেছিল ২০০০ সালে Avnur আর Hellerstein-এর “Eddies” প্রবন্ধে: plan-কে একবারে স্থির না করে, প্রতিটি সারিকে চালানোর সময় অপারেটরগুলোর মধ্যে পাঠানো, আর কোন অপারেটর বেশি সারি বাদ দিচ্ছে তা দেখে ক্রম ক্রমাগত বদলানো। ধারণাটি সুন্দর, কিন্তু প্রতিটি সারির জন্য সিদ্ধান্ত নেওয়ার খরচ আর জটিলতা বাণিজ্যিক DBMS-এ সাধারণ হয়নি।
বাস্তবে যা জায়গা পেয়েছে, তা সীমিত adaptivity: কয়েকটি নির্দিষ্ট “সিদ্ধান্ত-বিন্দু”। Oracle-এর adaptive plan আর SQL Server-এর adaptive join একটি join-এর জন্য দুটি বিকল্প তৈরি রাখে, প্রথম কিছু সারি গুনে বেছে নেয়। Spark-এর adaptive execution প্রতিটি পর্যায় শেষে আসল আকার দেখে পরের পর্যায় বদলায় — বিতরণ করা সিস্টেমে পর্যায়ের সীমানা স্বাভাবিকভাবেই থাকে, তাই সেখানে এটি সহজ।
অন্য ধারাটি শেখা। IBM-এর LEO (২০০১) আগের রানের আসল সারি দিয়ে পরের অনুমান শোধরাত। সাম্প্রতিক গবেষণায় neural network দিয়ে cardinality অনুমান, আর পুরো plan বাছাই শেখানোর চেষ্টা হয়েছে — যেমন Neo (২০১৯), আর Bao (২০২১), যা পুরোনো optimizer-কে না সরিয়ে শুধু কোন hint-সেট দেওয়া হবে তা শেখে। ফল আশাব্যঞ্জক, বিশেষত বারবার চলা কোয়েরিতে।
পুরোপুরি জায়গা না পাওয়ার কারণগুলো প্রকৌশলের। এক, পূর্বানুমানযোগ্যতা: ব্যাংক বা হাসপাতালের সিস্টেমে কাল রাতে যে কোয়েরি ১ সেকেন্ডে চলেছে, আজ মডেল বদলে ১০ মিনিট নিলে তা অগ্রহণযোগ্য। দুই, ব্যাখ্যা: DBA-কে বলতে হয় plan কেন বদলাল। তিন, নতুন কোয়েরি আর নতুন ডেটায় মডেল দুর্বল, আর প্রশিক্ষণের খরচ আছে। তাই প্রবণতা হলো প্রচলিত optimizer-এর পাশে শেখা অংশ যোগ করা, তার জায়গায় নয়।
মূল উপলব্ধি: ভালো অনুমান আর দ্রুত শোধরানো — দুটি পরিপূরক কৌশল। বাস্তব সিস্টেম দ্বিতীয়টি নেয় সাবধানে, কারণ ডেটাবেসে “গড়ে দ্রুত” যথেষ্ট নয়, “আজও কালকের মতো” হওয়াও জরুরি।
অনুশীলন
একটি কোয়েরি লেখা, একটি DP হাতে চালানো, একটি plan আগে বলা, একটি অনুমানের হিসাব।
-
কোয়েরি লিখুন: এই পর্বের ord টেবিলে (২০,০০০ সারি; status-এর ভাগ: pending ০.১; amount ≥ 5000 মোট ২,০০৩টি সারিতে) একটি কোয়েরি লিখুন, যা
status = 'pending' AND amount >= 5000-এর জন্য স্বাধীনতা ধরে নেওয়া অনুমান আর আসল সারি পাশাপাশি দেখায় — ভাগ দুটিও কোয়েরির ভেতরেই হিসাব করে। অংশ ৬-এর এডিটরে লিখে চালাতে পারেন।SELECT ROUND(COUNT(*) * AVG(status = 'pending') * AVG(amount >= 5000)) AS est, SUM(status = 'pending' AND amount >= 5000) AS actual FROM ord;এক scan-এই সব:
AVG(status = 'pending')= ০.১,AVG(amount >= 5000)≈ ০.১০০১৫; অনুমান ২০,০০০ × ০.১ × ০.১০০১৫ ≈ ২০০, আসল ২০২. এখানে দুটি কলাম প্রায় স্বাধীন, তাই গুণের সূত্র ভালো কাজ করে — অংশ ৬-এর city আর division-এর মতো correlated জোড়ায় করে না। এক টেবিলের ওপর এমন “অনুমান বনাম আসল” কোয়েরি correlation খোঁজার দ্রুত হাতিয়ার। -
হাতে চালান (trace): তিনটি টেবিল A (১,০০০ সারি), B (১০,০০০), C (১০০)। join শর্ত A–B আর B–C. V(B, a_id) = ১,০০০, V(A, a_id) = ১,০০০; V(B, c_id) = ১০০, V(C, c_id) = ১০০. cost = মধ্যবর্তী join-ফলের আকারের যোগফল (শেষ ফলসহ)। System R-এর DP চালিয়ে সেরা left-deep plan আর তার cost বের করুন।
স্তর ২: {A,B} = ১,০০০ × ১০,০০০ / ১,০০০ = ১০,০০০; {B,C} = ১০,০০০ × ১০০ / ১০০ = ১০,০০০; {A,C}-এ join শর্ত নেই — বাদ। স্তর ৩: পুরো ফল = ১০,০০০ × ১০০ / ১০০ = ১০,০০০ (বা ১০,০০০ × ১,০০০ / ১,০০০ — একই)। প্রার্থী: best[{A,B}] ⋈ C = ১০,০০০ + ১০,০০০ = ২০,০০০; best[{B,C}] ⋈ A = ১০,০০০ + ১০,০০০ = ২০,০০০. টাই — দুটিই সেরা, cost ২০,০০০.
শিক্ষা: দুটি foreign key join-এ প্রতিটি ধাপের ফল B-এর আকারই থাকে, তাই এই সরল model-এ ক্রমে পার্থক্য নেই। বাস্তবে পার্থক্য আসবে access path আর অ্যালগরিদম থেকে — যেমন ছোট C দিয়ে hash table বানানো সস্তা। আর A বা C-তে একটি বাছাই-করা শর্ত থাকলে সেটি দিয়ে শুরু করা স্পষ্টভাবে জিতত।
-
Plan আগে বলুন: একটি SQLite ডেটাবেসে ANALYZE-এর পর sqlite_stat1-এ আছে:
ord | ord_status | 20000 5000আরord | ord_cust | 20000 14. কোয়েরি:SELECT COUNT(*) FROM ord WHERE status = 'returned' AND cust_id = 18. (ক) কোন index বাছা হবে? (খ) ধরুন আসলে returned সারি ২০০টি আর cust_id = 18-এর ২৬০টি — বাছাইটি কি ঠিক?(ক)
ord_cust— stat1 অনুযায়ী একটি cust_id-তে গড়ে ১৪টি সারি, একটি status-এ ৫,০০০; ১৪ অনেক কম। আসল SQLite-এ এই অবস্থায় plan আসেSEARCH ord USING INDEX ord_cust (cust_id=?).(খ) না, তবে ক্ষতি সামান্য। গ্রাহক 18 গড়পড়তা নন — তাঁর ২৬০টি অর্ডার, গড়ের ১৯ গুণ; আর returned মাত্র ২০০টি। আসলে status-পথে কম সারি (২০০ বনাম ২৬০)। দুটি পথেই skew-এর কারণে অনুমান ভুল — একটিতে কম, একটিতে বেশি — আর দুটি ভুল মিলে প্রায় সমান দুটি পথের মধ্যে একটি বাছা হয়েছে। আসল সংখ্যা জানতে STAT4 বা MCV লাগত।
-
অনুমান হিসাব করুন: employees টেবিলে ১০,০০০ সারি। dept-এর ২০টি আলাদা মান, MCV তালিকায় 'Sales' ০.৩০. salary 20,000 থেকে 1,20,000; equi-depth histogram-এ ১০টি bucket, তার একটি [40,000, 50,000]. (ক)
dept = 'Sales'; (খ)dept = 'HR'(MCV-তে নেই; MCV-তে মোট ৫টি মান, তাদের মোট ভাগ ০.৬০); (গ)salary BETWEEN 40000 AND 45000; (ঘ) (ক) OR (গ) — প্রতিটির অনুমিত সারি কত?- (ক) MCV থেকে সরাসরি: ০.৩০ × ১০,০০০ = ৩,০০০.
- (খ) বাকি ভাগ ১ − ০.৬০ = ০.৪০, বাকি মান ২০ − ৫ = ১৫টি; প্রতিটি ০.৪০ / ১৫ ≈ ০.০২৬৭ → প্রায় ২৬৭. (শুধু 1/V ধরলে ৫০০ — MCV বাদ দিয়ে হিসাব করাই PostgreSQL-এর পদ্ধতি।)
- (গ) একটি bucket-এ ১০,০০০ / ১০ = ১,০০০ সারি; পরিসরটি bucket-এর অর্ধেক → প্রায় ৫০০.
- (ঘ) ০.৩০ + ০.০৫ − ০.৩০ × ০.০৫ = ০.৩৩৫ → প্রায় ৩,৩৫০ — স্বাধীনতা ধরে নিয়ে। Sales-এর বেতন যদি অন্য বিভাগ থেকে আলাদা ধাঁচের হয়, এই সংখ্যা ভুল হবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি SQL কোয়েরি থেকে plan বাছাই পর্যন্ত optimizer-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- SQL parse করে প্রাথমিক query tree বানানো
- View merging আর subquery unnesting
- Selection আর projection নিচে নামানো
- Statistics থেকে প্রতিটি ধাপের সারি অনুমান
- Dynamic programming দিয়ে join order আর অ্যালগরিদম তুলনা
- সবচেয়ে কম আনুমানিক খরচের plan চালানো
পাঁচটি টেবিলের inner join-এ কতগুলো আলাদা left-deep join order সম্ভব (cross product-সহ সব ক্রম গুনে)?
- ২৫
- ৩২
- ১২০
- ১,৬৮০
একটি টেবিলে ১০,০০০ সারি, আর city কলামে ৫০টি আলাদা মান। কোনো MCV বা histogram নেই। WHERE city = 'Khulna'-এ optimizer প্রচলিত সূত্রে কত সারি অনুমান করবে?
- ৫০
- ২০০
- ১,০০০
- ৫,০০০
R-এ ১,০০০ সারি, S-এ ৫,০০০ সারি; V(R, a) = ১০০, V(S, a) = ৫০০. R ⋈ S ON R.a = S.a-এর আকারের প্রচলিত অনুমান কত?
- ১,০০০
- ৫০,০০০
- ৫০,০০,০০০
- ১০,০০০
customers টেবিলে city আর division কলাম; প্রতিটি শহর ঠিক একটি বিভাগে। city = 'Dhaka' AND division = 'Dhaka'-এ optimizer যদি দুই selectivity গুণ করে, সাধারণত কী ঘটে?
- অনুমান আসলের চেয়ে কম হয়
- অনুমান আসলের চেয়ে বেশি হয়
- অনুমান নিখুঁত হয়, কারণ গুণের সূত্র সবসময় ঠিক
- কোয়েরিটি ত্রুটি দেয়
amount কলামের ৭০% মান 100–1000-এর মধ্যে, বাকিগুলো 50,000 পর্যন্ত ছড়ানো। amount < 800-এর ভালো অনুমানের জন্য একই সংখ্যক bucket-এর কোন histogram সাধারণত ভালো?
- Equi-width, কারণ প্রতিটি bucket সমান চওড়া
- Equi-depth, কারণ ঘন অংশে bucket সরু হয়
- কোনো histogram নয়, min আর max-ই যথেষ্ট
- শুধু MCV তালিকা
একটি prepared statement WHERE merchant_id = ? প্রথমবার একটি খুব বড় মার্চেন্টের id দিয়ে চলল, আর DBMS সেই মান দেখে plan বানিয়ে জমা রাখল। পরে ছোট মার্চেন্টদের পাতা ধীর। এই ঘটনার নাম আর মূল কারণ কী?
- Deadlock — দুটি অনুরোধ একে অপরের অপেক্ষায়
- SQL injection — parameter-এর মান কোয়েরিতে ঢুকে গেছে
- Parameter sniffing — প্রথম মানের জন্য বানানো plan সব মানে ব্যবহার
- Stale statistics — ANALYZE অনেক দিন চলেনি
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Optimizer সবচেয়ে কম আনুমানিক খরচের plan বাছে। তিন অংশ: plan space, cost model, cardinality estimation — আর তৃতীয়টিই ভুলের প্রধান উৎস।
- আগে নিয়ম: selection আর projection নিচে, σ আর × মিলে ⋈, view merging, view-এ predicate pushdown, subquery unnesting — প্রায় সবসময় লাভ; তবু plan দেখে নিশ্চিত হন।
- Join order: left-deep n!, bushy (2n − 2)!/(n − 1)!. System R-এর DP প্রতিটি উপসেটের (আর interesting order-এর) সেরা plan জমিয়ে O(n · 2ⁿ)-এ খোঁজে; অনেক টেবিলে GEQO বা greedy.
- সূত্র: equality 1/V বা MCV, range histogram থেকে, AND-এ গুণ, OR-এ p + q − pq, join-এ |R| · |S| / max(V). correlated কলাম, skew, পুরোনো stats, ফাংশন আর parameter এগুলো ভাঙে; join-এ ভুল গুণ হয়।
- বাস্তবে: ANALYZE দিয়ে statistics তাজা রাখুন, EXPLAIN-এ আনুমানিক বনাম আসল সারি মেলান, plan cache-এর parameter-ফাঁদ চিনুন; hint শেষ উপায়, কারণসহ লিখে রাখা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৪ · Transaction আর ACIDপরবর্তী পর্বমডিউল ৭ শুরু — একসাথে অনেক লেখা আর crash.
- পর্ব ৪২ · Query processing — sort আর join অ্যালগরিদমফিরে দেখুনplan-এর প্রতিটি অপারেটর আর তার খরচ।
- পর্ব ৩৩ · Relational algebra — join আর divisionভিত্তিjoin-এর commutativity আর associativity — rewrite-এর অনুমতিপত্র।
- পর্ব ২৪ · View আর materialized viewসম্পর্কিতview expansion আর predicate pushdown.
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- DSA · Dynamic programming-এর ভিত্তিসম্পর্কিত কোর্সoptimal substructure আর memoization — join enumeration-এর গণিত।