惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
GbyAI
GbyAI
P
Proofpoint News Feed
B
Blog
MyScale Blog
MyScale Blog
V
V2EX
B
Blog RSS Feed
Microsoft Security Blog
Microsoft Security Blog
量子位
Jina AI
Jina AI
博客园 - 叶小钗
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
罗磊的独立博客
L
LangChain Blog
I
InfoQ
云风的 BLOG
云风的 BLOG
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理
小众软件
小众软件
V
Visual Studio Blog
月光博客
月光博客
The Cloudflare Blog
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Postgres JSONB indexes: GIN vs BTREE on the same column
BaoDev Studi · 2026-05-25 · via DEV Community

BaoDev Studio

caught this in production last quarter and the answer is more boring than i expected: GIN and BTREE on the same JSONB column solve different problems, and the right choice depends on the SHAPE of your queries, not the size of the data.

heres the actual benchmark + when each one wins.

the setup

table: users with 2.3M rows. one column attributes JSONB. typical row contains:

{
  "plan": "pro",
  "country": "ID",
  "signup_source": "organic",
  "feature_flags": ["beta_search", "new_billing"],
  "preferences": {"theme": "dark", "lang": "id"}
}

Enter fullscreen mode Exit fullscreen mode

queries i run regularly:

  • Q1: WHERE attributes->>'plan' = 'pro' — find all pro users
  • Q2: WHERE attributes @> '{"feature_flags": ["beta_search"]}' — find users with a feature flag
  • Q3: WHERE attributes->>'country' = 'ID' AND attributes->>'plan' = 'pro' — pro users in indonesia
  • Q4: WHERE attributes ? 'preferences' — users who have the preferences key at all

four queries. different optimal indexes for each.

the three index options

-- option A: GIN on whole column
CREATE INDEX idx_attrs_gin ON users USING GIN (attributes);

-- option B: GIN with jsonb_path_ops (faster but only for @>)
CREATE INDEX idx_attrs_gin_path ON users USING GIN (attributes jsonb_path_ops);

-- option C: BTREE on extracted scalar
CREATE INDEX idx_attrs_plan ON users ((attributes->>'plan'));

Enter fullscreen mode Exit fullscreen mode

actual EXPLAIN ANALYZE results (2.3M rows)

Q1: WHERE attributes->>'plan' = 'pro'

  • no index: 480ms (seq scan)
  • GIN (option A): 220ms (bitmap scan, recheck) — GIN is generic, returns false positives, needs the recheck step
  • GIN path_ops (option B): doesn't work for ->> extraction. ignored.
  • BTREE on (attributes->>'plan') (option C): 8ms (index scan, no recheck) — winner by 27x

Q2: WHERE attributes @> '{"feature_flags": ["beta_search"]}'

  • no index: 510ms (seq scan)
  • GIN: 14ms (bitmap scan) — solid
  • GIN path_ops: 6ms — winner. path_ops loses other operators but is 2.3x faster for @> specifically because it stores hashed paths only.
  • BTREE on extracted: cant express this query. n/a.

Q3: WHERE plan='pro' AND country='ID' (compound on JSONB)

  • BTREE on plan only: 120ms (index on plan, then filter for country in heap)
  • BTREE composite on ((attributes->>'plan'), (attributes->>'country')): 3ms — winner.

if you find yourself running compound queries on JSONB scalars, the COMPOSITE BTREE on extracted columns beats every other option for that exact shape.

Q4: WHERE attributes ? 'preferences'

  • GIN (option A): 18ms — works because the ? operator is supported
  • GIN path_ops: doesnt work for the ? operator. path_ops only indexes @>.
  • BTREE on extracted: cant express. n/a.

the actual lesson

GIN with default ops is the safe default if you don't know your query shape yet. handles @>, ?, ?|, ?&. flexibility tax: ~2x slower than path_ops on @>, way slower than BTREE on ->>.

GIN with jsonb_path_ops is the specialist if you ONLY use @> (contains). most apps doing feature flag / array-contains queries fit here.

BTREE on extracted scalar (((attributes->>'X'))) is always the fastest for that specific scalar predicate. price: one BTREE index per accessed scalar field.

compound BTREE on multiple extracted scalars is the god mode for queries shaped like Q3. costs you write-amplification but reads are 40-100x faster than alternatives.

what i actually use in production

after benchmarking:

  • composite BTREE on the 2 high-cardinality scalars (plan, country) that show up in 80% of WHERE clauses → 3ms for the hot path
  • one GIN path_ops index for the @> feature_flags queries → 6ms for flag lookups
  • no general-purpose GIN. the ? operator queries (Q4) are rare; we accept seq scan for them.

trade-off: 2 BTREE + 1 GIN = 3 indexes on a single JSONB column. write amplification per row: ~30% slower INSERTs vs no indexes. acceptable because writes are 1% of our traffic.

the rule of thumb i give other engineers

if you know the query shape, use a BTREE on the extracted scalar. if you don't, use a default GIN. only add jsonb_path_ops if you've measured + you ONLY use @>.

GIN-on-everything is the lazy answer that costs you 20-50x on simple equality queries. and most JSONB queries in business code are simple equality queries.

one more thing

the BTREE on (attributes->>'plan') index works ONLY if your query writes the predicate as attributes->>'plan' = 'pro'. if you write (attributes->'plan')::text = '"pro"' (note the double quote inside the literal — JSONB vs text comparison), it won't use the index. lost an hour to this once. expression on the LEFT side of = must match the indexed expression exactly.