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।
আগে এটা বুঝি
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-তে নেই।)
- প্রতিটা indexনিজের qualifying heap-page-গুলোর bitmap বানায়
- Bitwise combineAND/OR/NOT — CPU word-parallel, µs-scale
- Heap read onceset-page গুলো physical order-এ — random-fetch নেই
- 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 shape | Index | কারণ |
|---|---|---|
token = ? long random strings | Hash | order-বিহীন, ছোট entry |
region=? AND status IN (...) analytics | Bitmap scan | cheap AND/OR over page-bits |
geom && box, ORDER BY geom <-> pt | GiST | bbox predicate + distance-order |
body @> '{"tags":[..]}', full-text @@ | GIN | element→ctid inversion |
ts BETWEEN ? AND ? append-only | BRIN | ১ MB-range prune, KB-footprint |
| সাধারণ equality/range/sort mix | B-tree | default — সব-চাকবি |
ভেতরে কী ঘটছে
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 লাগবে না 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নিজে চালিয়ে দেখুন
jsonb-GIN speedup, GiST overlap, BRIN-vs-btree footprint, forced Bitmap Heap Scan
ধাপ ০ — 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-এর জন্ম চোখে দেখা যায়।
নিজে বানান
BitmapIndex — postings big-int, AND/OR/NOT algebra, selectivity-estimate audit
- build(column): value → postings int (bit i = row i) — one pass
- AND/OR = &, |; NOT = full-mask xor — CPU-তে word-parallel
- count() popcount; rows() lowest-set-bit decode
- optimizer-style estimate: |A|·|B|/n (independence assumption)
- audit: estimate vs brute-force ground truth error%
- 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।
2Bitmap Heap Scan-এ Heap Blocks: exact=3100 lossy=812, Rows Removed by Index Recheck: 40211 — কী ঘটছে, কোন knob ঠিক করবেন?
যুক্তি
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 = ? একা? কেন দুটোই না?
প্রয়োগ
(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 খুঁজবে।
4Append-only events table-এ ingested_at BRIN রাখলেন; পরে backfill job
পুরনো random date-এর লাখ row ঢুকল। কী ভাঙে, কী করবেন?
যুক্তি
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-র সঙ্গী।
5Table: 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?
ডিজাইন
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-এর নিজেরটা।
আরও পড়ুন
- PostgreSQL Documentation, Chapter 11 — Index Types — The PostgreSQL Global Development Group · B-tree/Hash/GiSP/GIN/BRIN — পাঁচ access method-এর প্রামাণ্য তুলনা
- "Generalized Search Trees for Database Systems" (VLDB, 1995) — Joseph M. Hellerstein, Jeffrey F. Naughton, Alex Pfeffer · GiST-র মূল পেপার — R-tree-সহ যেকোনো search tree-র framework
- PostgreSQL Documentation — BRIN Indexes — The PostgreSQL Global Development Group · Block-range summary, opclasses, autosummarize — BRIN-এর প্রামাণ্য উৎস