Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ৫/১৯কঠিন৪৫ মিনিট

Hash, Bitmap, GiST, GIN, BRIN — এক index-এর জুগলবন্দি না

Hash, Bitmap, GiST, GIN, BRIN: Picking Beyond B-Tree

B-tree সব খায় না — equality-তে hash, low-cardinality AND/OR-এ bitmap scan, geometry-KNN-এ GiST, jsonb/full-text-এ GIN inverted index, append-only timestamp-এ KB-size BRIN; শেষে কোন query-shape-এ কোনটা — flowchart।

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

  • পাঁচটা specialized index-এর mechanics আঁকতে পারবেন — hash bucket, bitmap scan-এর page-bitmap AND/OR, GiST-এর predicate-tree, GIN-এর posting list, BRIN-এর block-range min/max
  • প্রতিটা index কোন query-shape-এ জেতে বলতে পারবেন — equality-only hash, low-cardinality analytics bitmap, geometry/KNN GiST, containment GIN, correlated append-only BRIN
  • Bitmap Heap Scan-এর exact/lossy mode আর Rows Removed by Index Recheck পড়ে work_mem trade-off ব্যাখ্যা করতে পারবেন
  • GIN-এর fastupdate pending list আর BRIN-এর correlation dependency — দুই নীরব ফাঁদ চিনে production সিদ্ধান্ত এড়াতে পারবেন

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

আগে এটা বুঝি

nbtree লেসনে দেখেছি B+ tree-র অমোঘ গণিত। কিন্তু মনে রাখবেন ওটার ভিত যা: key-দের total order। এই ভিত্তিটাই কিছু query-তে উল্টে যায় — geometry-তে ‘order’ নেই, jsonb-র ভেতরে key নেই, আর append-only log-এ order-এর দাম চুকিয়ে দেওয়া অপচয়। PostgreSQL তাই পাঁচ access method দেয়; আজ B-tree-র বাইরের চারজন + একটা execution-trick (bitmap scan) — আর শেষে কোন shape-এ কোনটা, এক flowchart-এ।

মূল ধারণা

Hash index — equality-only revival

Hash table-এর database-birth: key → hash → bucket। Lookup O(১)-কাছাকাছি, তবে শুধু equality — hash সংরক্ষণ করে না order, তাই range/sort/prefix অসম্ভব। ইতিহাস মজার: PG-র পুরনো hash index WAL-logged ছিল না (crash- unsafe!) — v10 (2017) full rewrite, এখন reliable। তবু জেতে খুব সীমিত ক্ষেত্রে: লম্বা random string key (session_token) — hash fixed-size entry বানায়, B-tree-র চেয়ে ছোট-দ্রুত। Default choice? প্রায় কখনোই না।

Bitmap scan — এক execution-trick

PostgreSQL-এ ‘bitmap index’ আলাদা storage structure না — scan strategy: প্রতিটা যোগ্য index একটা heap-page bitmap বানায় (bit i = page i-তে match), তারপর bitwise AND/OR/NOT — শেষ bitmap-এর set-page-গুলো একবার ঢিলে heap-read। Multi-condition OR-এ জাদু: (a OR b)-তে দুই index-এর union আগে, heap-touch পরে — random-fetch-এর ঝড় থেকে বাঁচে। Low-cardinality analytics (status, region-জাতীয়) + multiple predicate-এ king। (Oracle/SQL Server-এর stored bitmap index আলাদা প্রাণী — warehouse-দের জিনিস; PG-তে নেই।)

Bitmap Heap Scan — চার ধাপ
  1. প্রতিটা indexনিজের qualifying heap-page-গুলোর bitmap বানায়
  2. Bitwise combineAND/OR/NOT — CPU word-parallel, µs-scale
  3. Heap read onceset-page গুলো physical order-এ — random-fetch নেই
  4. Rechecklossy bitmap হলে per-row predicate যাচাই

GiST — framework, এক index না

GiST (Generalized Search Tree) বলে দেয় tree-র node-এ কী রাখলে ‘predicate’ হয়: প্রতিটা child-subtree-র জন্য এক assertion (“এই box-এর ভেতরে সব point”)। R-tree geometry: rectangle ধরে bounding-box hierarchy — overlap-query &&, containment @>, আর KNN operator <->: distance- order-এ nearest neighbor, ORDER BY geom <-> point LIMIT 5। Consistency হয়তো conservative (bbox overlap কিন্তু ভেতরের shape না) — তাই lossless recheck। Exclusion constraint, full-text (পুরনো path), trigram fuzzy — সব GiST-এর সন্তান।

GIN — inverted index

GIN (Generalized Inverted Index) উল্টে-দিকের খেলা: value-element → posting list of ctids। jsonb-র প্রতিটা key/value, array-র প্রতিটা element, tsvector-র প্রতিটা lexeme — সবাই আলাদা entry। Containment body @> '{"tags":["vegan"]}' = element-lookup + intersection। দাম: update ভারী — প্রতিটা insert অনেক posting-list touch করে। ঔষধ fastupdate: নতুন entries আগে ছোট pending list-এ জমে, threshold ছুঁলে bulk-move — write দ্রুত, কিন্তু কেউ pending-flush-এর বিল পরিশোধ করবে (vacuum/পরবর্তী insert)।

BRIN — দরিদ্রের বিশাল-index

BRIN (Block Range Index) প্রতি ~১২৮ page (=১ MB) heap-range-এ শুধু min/max রাখে। ১০ লাখ row-র timestamp-B-tree ≈ ২১৪ MB; BRIN ≈ কয়েক KB। শর্ত একটাই: physical order ≈ value order — append-only log-এ timestamp naturally sorted, perfect। Query মানে range-summary prune: “এই hour-এর data কোন MB-range-গুলোতে?” — মিলেছে পড়ো, না-মিলে skip। Update এলে order ভাঙে — summary ভুল হয়, autosummarize/vacuum ঠিক করে, তবু ছড়ানো update-এ BRIN-এর জাদু মরে যায়।

Choice table — কোন shape, কোন index

Query shapeIndexকারণ
token = ? long random stringsHashorder-বিহীন, ছোট entry
region=? AND status IN (...) analyticsBitmap scancheap AND/OR over page-bits
geom && box, ORDER BY geom <-> ptGiSTbbox predicate + distance-order
body @> '{"tags":[..]}', full-text @@GINelement→ctid inversion
ts BETWEEN ? AND ? append-onlyBRIN১ MB-range prune, KB-footprint
সাধারণ equality/range/sort mixB-treedefault — সব-চাকবি

ভেতরে কী ঘটছে

Bitmap Heap Scan-এর ভেতরটা

Bitmap Heap Scan on events  (rows=42000)
  Recheck Cond: (status = 'new')
  Heap Blocks: exact=3100 lossy=812     ← work_mem ভরলে lossy!
  ->  Bitmap Index Scan on idx_status   ← page-bitmap তৈরি

Exact bitmap = bit-per-page; work_mem ছোট হলে bitmap lossy হয় — page-number-এর বদলে range-chunk bit — মিললেই page পড়ে recheck: প্রতিটা row আসল predicate-এ যাচাই। Rows Removed by Index Recheck বড় হলেই সংকেত: work_mem বাড়ান বা partial-index ভাবুন।

GiST KNN — priority walk

ORDER BY loc <-> point LIMIT 5: root-এর children distance-order-এ priority-queue-তে, সবচেয়ে কাছের subtree pop → children push… first 5 leaf- hit-ই উত্তর — দূরের subtree কখনো খোলাই হয় না। R-tree-র pruning + PQ-র মিলন।

GIN pending list-এর হিসাব

gin_pending_list_limit default 4 MB। Bulk-load-এ fastupdate=on রাখলে insert ঝরঝরে, কিন্তু ৪ MB ছুঁলে যে transaction flush করে সে-ই বিল দেয় — p99-spike-এর গোপন উৎস। Heavy-write GIN-এ: bulk-path-এ fastupdate = off + পরে এক gin_clean_pending_list()।

BRIN-র গণিত

pages_per_range=128 → ১০⁷-row table ≈ ৪৪k heap-pages ÷ ১২৮ ≈ ৩৫০ range × ~৪০ B summary ≈ ১৪ KB — B-tree-র ১৫,০০০ ভাগের ১। Planner BRIN বাছে যখন pg_stats.correlation প্রায় ±১ — append-only-তে স্বাভাবিক, shuffled import-এ শূন্য (তখন BRIN অন্ধ, planner জানে না কোন range বাদ দেবে)।

উদাহরণ

পাঁচ query, পাঁচ বিচার

Q1 session_token = 'a94f...' (৬৪-char random)      → Hash
   btree: ৬৪-byte entry, deep fanout-loss; hash: ৮-byte digest

Q2 WHERE region='asia' AND status IN ('new','paid') → Bitmap scan
   ৩ btree→bitmap, OR-union, AND — heap once

Q3 ORDER BY geog <-> ST_MakePoint(lon,lat) LIMIT 5  → GiST KNN
   priority-walk, কোন sort node নেই

Q4 WHERE body @> '{"tags":["vegan"]}'               → GIN
   'tags'+'vegan' posting-list intersect

Q5 WHERE ingested_at > now() - '10 minutes'         → BRIN
   last-MB-ranges only; index size ~KB

প্রতিটার বিপরীতে ভুল pick: Q1-এ btree (ঠিক চলে, মোটা), Q2-তে দুটো আলাদা Index Scan (double heap-random), Q4-তে seq scan (২০০ ms→২ ms হারানো), Q5-তে full B-tree (২১৪ MB বেতন, ১৪ KB-র কাজ)।

ছবিতে

             predicate-এর রূপ?
                  │
   ┌──────────────┼───────────────────┐
   ▼              ▼                   ▼
 equality-only   range/sort/mix     element/containment?
   │              │                    │
   ▼              ▼            ┌───────┴────────┐
 key লম্বা         B-TREE        ▼                ▼
 random string?  (default)   jsonb/array/     geometry/range/
   │                          full-text?       neighborhood?
   ▼ HASH                        ▼ GIN            ▼ GiST (&&,<->)

 আর append-only time/data-log?
   └─► value↘physical order? ─yes─► BRIN (KB-class)
                              └no──► B-tree

 একাধিক low-cardinality condition (AND/OR)?
   └─► existing btree-দের BITMAP SCAN combine — নতুন index লাগবে না
Query-shape → index-type decision flow (PG-র পাঁচ + bitmap trick):
 GIN: docs(id, body jsonb)                      BRIN: ticks(ts)
 doc 1: {"tags":["a","b"]}                      range [pg 0–127]:
 doc 2: {"tags":["b"]}                            ts_min … ts_max
 doc 3: {"tags":["c"]}                          range [pg 128–255]:
                                                  ts_min … ts_max
 inverted:                                      ...(≈৩৫০ range, ~১৪ KB)
  "a" → {ctid(0,1)}
  "b" → {ctid(0,1),(0,2)}
  "c" → {ctid(0,3)}
 @> {"tags":["b"]} → read ctids {(0,1),(0,2)} directly
GIN-এর inversion আর BRIN-এর summary — দুই extreme footprint:

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

EXPERIMENT

jsonb-GIN speedup, GiST overlap, BRIN-vs-btree footprint, forced Bitmap Heap Scan

PostgreSQL 14+ (psql; Docker caveat নিচে)· ২৫ মিনিট

ধাপ ০ — server: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16, তারপর docker exec -it pg-lab psql -U postgres; \timing on।

ধাপ ১ — jsonb @> : GIN বনাম না-GIN

CREATE TABLE docs AS
SELECT g AS id, jsonb_build_object(
         'user', g % 1000,
         'tags', (ARRAY['alpha','beta','gamma','delta','epsilon'])
                 [1 + g % 5 : 2 + g % 3]) AS body
FROM generate_series(1, 200000) g;

SELECT count(*) FROM docs WHERE body @> '{"tags":["gamma"]}';  -- seq scan

CREATE INDEX docs_gin ON docs USING gin (body jsonb_path_ops);
SELECT count(*) FROM docs WHERE body @> '{"tags":["gamma"]}';  -- bitmap+gin

মোটামুটি: ~২৫০ ms → ~২ ms। EXPLAIN-এ দেখুন Bitmap Index Scan on docs_gin। (jsonb_path_ops = শুধু path-hash, ছোট-দ্রুত; default jsonb_ops extra operators দেয়।)

ধাপ ২ — tstzrange overlap: GiST &&

CREATE TABLE bookings AS
SELECT g AS id, (g % 50)::int AS room,
       tstzrange(timestamptz '2026-08-25 09:00'
                   - ((g * 7) % 600) * interval '1 minute',
                 timestamptz '2026-08-25 09:00'
                   - ((g * 7) % 600) * interval '1 minute'
                   + (10 + (g * 13) % 80) * interval '1 minute') AS during
FROM generate_series(1, 200000) g;

SELECT count(*) FROM bookings
WHERE during && '[2026-08-25 08:00, 2026-08-25 09:00)'::tstzrange;

CREATE INDEX bookings_gist ON bookings USING gist (during);
-- একই query আবার

মোটামুটি: ~১৮০ ms → ~৩ ms। একই GiST index exclusion-constraint-এও কাজ করে — double-booking রোধ!

ধাপ ৩ — BRIN vs B-tree: ১০M timestamp

CREATE TABLE ticks AS
SELECT g, timestamptz '2026-01-01' + g * interval '1 second' AS ts
FROM generate_series(1, 10000000) g;

\timing on
CREATE INDEX ticks_btree ON ticks USING btree (ts);
CREATE INDEX ticks_brin  ON ticks USING brin  (ts);

SELECT pg_size_pretty(pg_relation_size('ticks_btree')) AS btree,
       pg_size_pretty(pg_relation_size('ticks_brin'))  AS brin;

মোটামুটি: btree ~২৫ s ও 214 MB; brin ~১ s ও ~120 KB — প্রায় ২০০০× footprint-ফারাক, identical range-query answer।

ধাপ ৪ — Bitmap Heap Scan জোর করে, তারপর lossy

CREATE INDEX bookings_room ON bookings (room);
SET enable_indexscan = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM bookings
WHERE during && '[2026-08-25 08:00, 2026-08-25 09:00)'::tstzrange
   OR room = 7;

SET work_mem = '64kB';      -- bitmap ফুলে lossy হবে
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM bookings WHERE during && '[2026-08-25 08:30, 2026-08-25 09:00)'::tstzrange;
RESET ALL;

প্রথম plan-এ খুঁজুন: BitmapOr → দুটো Bitmap Index Scan (gist + btree) → Bitmap Heap Scan, Heap Blocks: exact=...। দ্বিতীয়টায় দেখুন exact=... lossy=... আর Rows Removed by Index Recheck: ... — work_mem- এর দাম, row-level recheck-এ পরিশোধিত।

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

Containment query-তে GIN ২০০ ms→২ ms-কাছে; BRIN timestamp-index ২১৪ MB-র কাজ ১৪ KB-এ করে; enable_indexscan=off দিয়ে BitmapOr-plan আর work_mem কমিয়ে lossy-recheck-এর জন্ম চোখে দেখা যায়।

নিজে বানান

BUILD IT

BitmapIndex — postings big-int, AND/OR/NOT algebra, selectivity-estimate audit

Python · ●●○○○
  1. build(column): value → postings int (bit i = row i) — one pass
  2. AND/OR = &, |; NOT = full-mask xor — CPU-তে word-parallel
  3. count() popcount; rows() lowest-set-bit decode
  4. optimizer-style estimate: |A|·|B|/n (independence assumption)
  5. audit: estimate vs brute-force ground truth error%
  6. microbench: bitmap AND vs row-scan over 50k rows
import random, time


class BitmapIndex:
    """value → postings-bitmap (Python big-int); AND/OR/NOT algebra"""

    def __init__(self):
        self.postings = {}                     # value → int (bit i = row i)

    def build(self, column):
        for rid, val in enumerate(column):
            self.postings[val] = self.postings.get(val, 0) | (1 << rid)

    def eq(self, val):
        return self.postings.get(val, 0)

    @staticmethod
    def count(bits):
        return bin(bits).count('1')


if __name__ == '__main__':
    N = 50_000
    random.seed(7)
    region = [random.choice(('asia', 'eu', 'us', 'sa', 'af')) for _ in range(N)]
    status = [random.choice(('new', 'paid', 'ship')) for _ in range(N)]
    ri, si = BitmapIndex(), BitmapIndex()
    t0 = time.perf_counter(); ri.build(region); si.build(status)
    print(f'build 2 indexes over {N:,} rows: {(time.perf_counter() - t0) * 1000:.1f} ms')

    q_and = ri.eq('asia') & si.eq('new')               # AND — word-parallel
    brute = sum(r == 'asia' and s == 'new' for r, s in zip(region, status))
    assert BitmapIndex.count(q_and) == brute
    est = BitmapIndex.count(ri.eq('asia')) * BitmapIndex.count(si.eq('new')) / N
    print(f"asia AND new : exact={brute}  independence-estimate={est:.0f}"
          f"  ({abs(est - brute) / brute * 100:.1f}% error)")

    q_or = ri.eq('asia') | ri.eq('eu')                 # OR
    assert BitmapIndex.count(q_or) == region.count('asia') + region.count('eu')
    q_not = ((1 << N) - 1) ^ si.eq('new')              # NOT — full-mask xor
    assert BitmapIndex.count(q_not) == sum(x != 'new' for x in status)
    print(f"OR asia|eu = {BitmapIndex.count(q_or):,}   "
          f"NOT new = {BitmapIndex.count(q_not):,}")

    t0 = time.perf_counter()
    for _ in range(200):
        BitmapIndex.count(ri.eq('us') & si.eq('paid'))
    bm_us = (time.perf_counter() - t0) / 200 * 1e6
    t0 = time.perf_counter()
    for _ in range(200):
        sum(r == 'us' and s == 'paid' for r, s in zip(region, status))
    bf_ms = (time.perf_counter() - t0) / 200 * 1e3
    print(f"AND-query: bitmap {bm_us:.1f} µs vs row-scan {bf_ms:.2f} ms")

প্রত্যাশিত output (timing machine-ভেদে):

build 2 indexes over 50,000 rows: 52.8 ms
asia AND new : exact=3317  independence-estimate=3347  (0.9% error)
OR asia|eu = 20,052   NOT new = 33,249
AND-query: bitmap 132 µs vs row-scan 3.63 ms

তিনটা পাঠ: ৫০k-row AND-এর উত্তর µs-এ (row-scan-এর ~৩০× দ্রুত); independence- estimate এখানে প্রায় নিখুঁত কারণ columns সত্যিই independent — বাস্তবে correlated columns-এ error দশ-শতাংশ ছাড়ায় (optimizer-এর misestimate-এর উৎস); আর bitmap-approach জেতে যখন predicate-গুলো আগে থেকে indexed — নইলে build-cost একাই scan-কে হারায়।

নিজে বাড়ান: (১) run-length encode (roaring-bitmap ভাবনা) — sparse data-তে memory কত বাঁচে মাপুন; (২) third column যোগ করে correlated pair বানান (status=factory-dependent) — estimate-error বাড়ে দেখুন; (৩) top-k combination search — কোন AND-combination সবচেয়ে selective?

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

Production-এ পাঁচজন

  • GiST: PostGIS-র প্রাণ (geom gist), exclusion constraints (double- booking রোধ), pg_trgm fuzzy-search — %similar% LIKE-ও index-এ!
  • GIN: jsonb-heavy app-দের default; full-text tsvector; RUM extension posting-list-ordered variant — score-sort-ও index থেকে।
  • BRIN: time-series/log ingestion (TimescaleDB-দের পাড়ার প্রতিবেশী); huge append-only fact-table-এ বিলিয়ন-row index KB-তে।
  • Hash: rare — API-token lookup জাতীয় niche; বেশিরভাগ ‘hash-লাগবে’ ভাবনা আসলে covering-btree-তেই মেটে।
  • Bitmap scan: nightly-report-এর প্রিয়; OLTP hot path-এ বিরল — bitmap-build cost latency-sensitive request-এ মানানসই না।

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

“Hash index দ্রুত — সব equality-তে hash-ই বানানো উচিত।”

B-tree equality-ও cache-hot-এ sub-µs; hash-এর লাভ শুধু entry-size আর fanout — লম্বা random key ছাড়া মাপা যায় না, আর range/sort/uniqueness- flexibility সব হারায়। Default B-tree, hash niche।

“Bitmap scan মানে database-এ bitmap index আছে।”

PG-তে bitmap এক execution-strategy — যেকোনো btree/gist/gin output থেকে page-bitmap বানিয়ে AND/OR করে। Stored per-value bitmap index (Oracle-style) PG core-এ নেই।

“jsonb-তে GIN বানালেই সব jsonb query দ্রুত হবে।”

GIN খাটে containment/existence/@>-জাতীয় pattern-এ; ভেতরের numeric range ((body->>'price')::numeric > 100) চায় expression-B-tree। আর heavy-update table-এ GIN-এর pending-list/write-bill উল্টো ব্যথা দেয়।

“Timestamp table-তে B-tree-র বদলে সবসময় BRIN — তো ছোট!”

BRIN শুধু correlation-এ বাঁচে; shuffled backfill, ঘন update বা non-monotonic value-তে prune-power শূন্য — planner-ই তখন BRIN ফেলে দেয়। Append-only pattern প্রমাণ হলে তবেই BRIN।

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

1

পাঁচ PG access method আর একটা করে signature query-shape?

স্মরণ

B-tree: equality/range/sort general। Hash: pure equality। GiST: geometric/ overlap/KNN predicate। GIN: element-containment inversion (jsonb/array/ tsvector)। BRIN: block-range min/max — correlated append-only। Bonus strategy: bitmap scan = multi-condition page-bit algebra।

2

Bitmap Heap Scan-এ Heap Blocks: exact=3100 lossy=812, Rows Removed by Index Recheck: 40211 — কী ঘটছে, কোন knob ঠিক করবেন?

যুক্তি

work_mem-এ bitmap ধরে না → chunked lossy encoding; set-page মানে ‘maybe’ — প্রতিটা row recheck হয়, ৪০k ফেলা। Knob: work_mem বাড়ান (exact-এ ফেরত), বা predicate narrower/partial-index — recheck-work নিজেই কমে।

3

(a) hash index-এ a > 5? (a,b) btree-এ b = ? একা? কেন দুটোই না?

প্রয়োগ

Hash: hash(a>5) বলা যায় না — hash order destroy করে, buckets unordered। (b)-alone: leftmost-prefix ভাঙে — b-রা a-ভাঙা ছড়ানো, descent-entry-ই নেই। দুটোতেই planner Seq Scan/bitmap-alternative খুঁজবে।

4

Append-only events table-এ ingested_at BRIN রাখলেন; পরে backfill job পুরনো random date-এর লাখ row ঢুকল। কী ভাঙে, কী করবেন?

যুক্তি

Range-summary আর physical layout আলাদা হতে থাকে — প্রতিটা range-এর min/max পুরো টেবিল-span করে, prune-power শূন্য, planner BRIN ছেড়ে দেয়। Backfill-এর পরে BRIN summarize (autosummarize/vacuum); ভবিষ্যতে backfill আলাদা partition-এ — partition-key pruning BRIN-র সঙ্গী।

5

Table: listings(id, geo geography(point), attrs jsonb, posted_at timestamptz, city text)। Queries: (A) map-viewport + nearest-5, (B) attrs @> '{"amenities":["wifi"]}', (C) city='Dhaka' AND posted_at DESC feed, (D) nightly city×amenity counts। কোন index-set?

ডিজাইন

(A) GiST(geo) — && viewport + <-> KNN। (B) GIN(attrs jsonb_path_ops)। (C) B-tree (city, posted_at DESC) — leftmost-prefix + ORDER BY ফ্রি। (D) কোনো নতুন index নয় — bitmap scan A/B/C-র index- output combine করে; রাতের batch-এ latency-tail সস্তা। চার query, তিন index + এক strategy।

এরপর কী

এরপর কী

Index-রা এখন arsenal: B-tree/hash/GiST/GIN/BRIN + bitmap-combine। কিন্তু কে ঠিক করে কোন query-তে কোনটা চলবে? Planner — cost model, statistics, plan-search। পরের লেসন Query Processing: SQL text → parser → planner-র cost-ভিত্তিক plan-search (volcano executor-সহ) → EXPLAIN-এর সেই লুকানো ভাষা পাঠোদ্ধার। আজকের flowchart ছিল আপনার মাথার planner — পরের লেসনে দেখব database-এর নিজেরটা।

আরও পড়ুন