B-tree vs GIN vs GiST इंडेक्स: एक व्यावहारिक PostgreSQL गाइड
B-tree vs GIN vs GiST इंडेक्स: फ़िल्टर, सर्च, JSONB फ़ील्ड, जियो क्वेरीज और हाई-कार्डिनेलिटी कॉलम के लिए सही PostgreSQL इंडेक्स चुनने में मदद करने वाली निर्णय तालिका।

आप वास्तव में क्या चुन रहे हैं जब आप कोई इंडेक्स चुनते हैं
ज़्यादातर PostgreSQL इंडेक्स की समस्याएँ एक ही तरह शुरू होती हैं: एक लिस्ट व्यू 1,000 पंक्तियों पर तेज़ लगता है, फिर 1,000,000 पर धीमा पड़ता है। या एक सर्च बॉक्स जो टेस्ट में ठीक था, प्रोडक्शन में सेकंड-लंबा हो जाता है। जब ऐसा होता है, तो सवाल पूछने का झुकाव होता है, “कौन सा इंडेक्स सबसे अच्छा है?” एक बेहतर सवाल है: “यह स्क्रीन डेटाबेस से क्या काम करवाने को कह रही है?”
एक ही टेबल को अलग-अलग स्क्रीन विभिन्न तरीके से पढ़ सकती हैं, इसलिए अलग-अलग इंडेक्स प्रकारों की ज़रूरत पड़ती है। एक व्यू केवल status से फ़िल्टर करता है और created_at से सॉर्ट करता है। दूसरा फुल-टेक्स्ट सर्च करता है। तीसरा यह जांचता है कि JSON फ़ील्ड में कोई key मौजूद है या नहीं। चौथा मैप पर किसी पॉइंट के पास की आइटम ढूँढता है। ये सब अलग-अलग एक्सेस पैटर्न हैं, इसलिए एक ही इंडेक्स हर जगह काम नहीं करेगा।
इंडेक्स चुनते समय आप यही निर्धारित कर रहे होते हैं: ऐप डेटा को कैसे एक्सेस करता है। क्या आप ज्यादातर exact मैच, रेंज, और सॉर्टिंग कर रहे हैं? क्या आप डॉक्यूमेंट्स या एरेज़ के भीतर खोज रहे हैं? क्या आप पूछ रहे हैं “इस लोकेशन के पास क्या है” या “यह रेंज किससे ओवरलैप करती है”? इन उत्तरों के आधार पर B-tree, GIN, या GiST सही बैठेगा।
B-tree, GIN, और GiST को सामान्य भाषा में समझें
इंडेक्स चुनना ज़्यादा कॉलम टाइप के बारे में नहीं है, बल्कि यह इस बारे में है कि आपके क्वेरीज़ उनसे क्या करवा रहे हैं। PostgreSQL ऑपरेटरों (=, <, @>, @@ आदि) के आधार पर इंडेक्स चुनता है, न कि सिर्फ इसलिए कि कॉलम “text” है या “json” है। इसलिए वही फ़ील्ड अलग-अलग स्क्रीन पर अलग इंडेक्स चाह सकती है।
B-tree: ordered lookups के लिए तेज़
B-tree डिफ़ॉल्ट और सबसे सामान्य विकल्प है। यह तब अच्छा काम करता है जब आप exact वैल्यू से फ़िल्टर करते हैं, रेंज से फ़िल्टर करते हैं, या परिणामों को किसी ख़ास ऑर्डर में चाहिए होते हैं।
एक सामान्य उदाहरण है एक admin लिस्ट जो status से फ़िल्टर होती है और created_at से सॉर्ट होती है। (status, created_at) पर B-tree इंडेक्स फ़िल्टर और सॉर्ट दोनों में मदद कर सकता है। B-tree यूनिकनेस के लिए भी सामान्य उपकरण है।
GIN: जब हर रो में कई searchable keys हों
GIN उन सवालों के लिए बनाया गया है जो पूछते हैं “क्या इस रो में यह term/value मौजूद है?”, जहां एक रो कई keys से मैच कर सकती है। सामान्य उदाहरण हैं फुल-टेक्स्ट सर्च (डॉक्यूमेंट में शब्द मौजूद हैं) और JSONB/array membership (JSON में key/value मौजूद है)।
सोचिए एक कस्टमर रिकॉर्ड में JSONB preferences ऑब्जेक्ट है, और एक स्क्रीन उन यूज़र्स को फ़िल्टर करती है जिनकी preferences में { "newsletter": true } है। यह GIN-स्टाइल lookup है।
GiST: ranges, geo, और similarity के लिए लचीला
GiST एक जनरल फ्रेमवर्क है उन डेटा टाइप्स के लिए जो सरल ordering में नहीं फिट होते। यह ranges (overlaps, contains), geometric और geographic क्वेरीज़ (near, within), और कुछ similarity सर्च के लिए अच्छा है।
B-tree vs GIN vs GiST तय करते समय सबसे पहले यह लिखें कि आपके busiest स्क्रीन कौन से ऑपरेटर उपयोग कर रहे हैं। सही इंडेक्स आमतौर पर उसी के बाद स्पष्ट हो जाता है।
सामान्य स्क्रीन के लिए निर्णय तालिका (filters, search, JSON, geo)
ज़्यादातर ऐप्स को कुछ ही इंडेक्स पैटर्न चाहिए होते हैं। ट्रिक यह है कि स्क्रीन के व्यवहार को उन ऑपरेटरों से मिलाना जो आपके क्वेरीज़ इस्तेमाल करते हैं।
| Screen pattern | Typical query shape | Best index type | Example operator(s) |
|---|---|---|---|
| Simple filters (status, tenant_id, email) | Many rows, narrow down with equality | B-tree | = IN (...) |
| Date/number range filter | Time window or min/max | B-tree | >= <= BETWEEN |
| Sort + pagination (feed, admin list) | Filter then ORDER BY ... LIMIT | B-tree (often composite) | ORDER BY created_at DESC |
| High-cardinality column (user_id, order_id) | Very selective lookups | B-tree | = |
| Full-text search box | Search text across a field | GIN | @@ on tsvector |
| “Contains” text search | Substring match like “%term%” | Usually none (or special trigram setup) | LIKE '%term%' |
| JSONB contains (tags, flags, properties) | Match JSON shape or key/value | GIN on jsonb | @> |
| JSONB one key equality | Filter by one JSON key a lot | Targeted B-tree on expression | (data->>'plan') = 'pro' |
| Geo proximity / within radius | “Near me” and map views | GiST (PostGIS geometry/geography) | ST_DWithin(...) <-> |
| Ranges, overlap (schedules, pricing bands) | Interval overlap checks | GiST (range types) | && |
| Low selectivity filter (boolean, tiny enums) | Most rows match anyway | Index often helps little | is_active = true |
दो अलग-अलग इंडेक्स coexist कर सकते हैं जब endpoints अलग हों। उदाहरण के लिए, एक admin लिस्ट को (tenant_id, created_at) पर B-tree चाहिए हो सकता है तेज़ सॉर्टिंग के लिए, जबकि एक सर्च पेज को @@ के लिए GIN इंडेक्स चाहिए। सिर्फ़ तब दोनों रखें जब दोनों क्वेरी शैप बार-बार उपयोग होते हों।
अगर आप अनिश्चित हैं, तो पहले ऑपरेटर देखें। इंडेक्स तब मदद करते हैं जब डेटाबेस उन्हें बड़ी टेबल के हिस्सों को स्किप करने के लिए उपयोग कर सके।
Filters और sorting: जहाँ B-tree ज़्यादातर जीतता है
दैनिक स्क्रीन के लिए B-tree अक्सर वही भरोसेमंद विकल्प है जो काम करता है। अगर आपका क्वेरी “किसी कॉलम के बराबर की पंक्तियाँ निकालो, शायद सॉर्ट करो, फिर पेज दिखाओ” जैसा दिखता है, तो शुरू करने के लिए B-tree सामान्यतः पहला विकल्प है।
Equality फ़िल्टर्स क्लासिक केस हैं। कॉलम जैसे status, user_id, account_id, type, या tenant_id अक्सर dashboards और admin panels में दिखाई देते हैं। B-tree इंडेक्स सीधे मिलते-जुलते मानों पर जा सकता है।
Range फ़िल्टर्स भी B-tree के अनुकूल हैं। जब आप समय या संख्या रेंज से फ़िल्टर करते हैं, तो ordered structure मदद करता है: created_at >= ..., price BETWEEN ..., id > ...। अगर आपकी UI “Last 7 days” या “$50 to $100” ऑफर करती है, तो B-tree वही कर रहा है जिसकी आपको ज़रूरत है।
Sorting और pagination वो जगह हैं जहाँ B-tree सबसे ज़्यादा काम बचाता है। अगर इंडेक्स ऑर्डर आपके ORDER BY से मेल खाता है, PostgreSQL अक्सर rows पहले से सॉर्ट करके वापस कर सकता है बजाय बड़े सेट को मेमोरी में सॉर्ट करने के।
-- A common screen: "My open tickets, newest first"
CREATE INDEX tickets_user_status_created_idx
ON tickets (user_id, status, created_at DESC);
Composite indexes एक सरल नियम का पालन करते हैं: PostgreSQL केवल इंडेक्स के leading भाग का ही कुशलतापूर्वक उपयोग कर सकता है—सोचें “left to right।” (user_id, status, created_at) के साथ, वो क्वेरी जो user_id से फ़िल्टर करती है (और ऑप्शनलली status) लाभ उठाती है। सिर्फ status से फ़िल्टर करने वाली क्वेरी आमतौर पर फायदा नहीं पाती।
Partial indexes एक मजबूत सुधार हैं जब आपकी स्क्रीन केवल डेटा के एक हिस्से की परवाह करती है। आम भाग होते हैं “सिर्फ active rows,” “not soft-deleted,” या “recent activity।” वे इंडेक्स को छोटा और तेज़ रखते हैं।
High-cardinality कॉलम और अतिरिक्त इंडेक्स की लागत
High-cardinality कॉलमों में बहुत से यूनिक वैल्यूज़ होते हैं, जैसे user_id, order_id, email, या सेकंड तक रिज़ॉल्यूशन वाला created_at। इंडेक्स यहाँ फ़ायदेमंद होते हैं क्योंकि फ़िल्टर जल्दी से टेबल का छोटा हिस्सा ढूँढ देता है।
Low-cardinality कॉलम इसके विपरीत हैं: booleans और छोटे enums जैसे is_active, status IN ('open','closed'), या plan IN ('free','pro')। इन पर इंडेक्स अक्सर निराश करते हैं क्योंकि हर वैल्यू बड़ी संख्या में पंक्तियों से मेल खाती है। PostgreSQL सही तरीके से sequential scan चुन सकता है क्योंकि इंडेक्स से कूदना फिर भी कई टेबल पेज पढ़ना पड़ता है।
एक और सूक्ष्म लागत rows को fetch करने की है। भले ही इंडेक्स मिलते-गंभीड़ IDs जल्दी ढूँढ ले, डेटाबेस को बाकी कॉलम्स के लिए टेबल विज़िट करनी पड़ सकती है। अगर आपका क्वेरी केवल कुछ फील्ड्स चाहता है, तो covering index मदद कर सकता है, लेकिन वह इंडेक्स को बड़ा और मेंटेनेंस के लिहाज़ से महंगा बना देता है।
हर अतिरिक्त इंडेक्स का एक write price होता है। Inserts को हर इंडेक्स में लिखना पड़ता है। Updates जो indexed कॉलम बदलते हैं उन्हें भी अपडेट करना होता है। “बस हां, रख लेते हैं” वाले इंडेक्स पूरे ऐप को धीमा कर सकते हैं, सिर्फ़ एक स्क्रीन नहीं।
व्यावहारिक मार्गदर्शन:
- हर busy टेबल के लिए 1-2 काम आती हुई (workhorse) इंडेक्स से शुरू करें, असली फ़िल्टर्स और sorts के आधार पर।
WHEREऔरORDER BYमें उपयोग होने वाले high-cardinality कॉलमों को प्राथमिकता दें।- Booleans और छोटे enums इंडेक्स करते समय सावधान रहें जब तक वे किसी और selective कॉलम के साथ न जुड़ें।
- नया इंडेक्स केवल तब जोड़ें जब आप नाम ले कर बता सकें कि कौन सा सही क्वेरी तेज़ होगा।
उदाहरण: assignee_id (high-cardinality) से फ़िल्टर करने वाली support ticket list को इंडेक्स से फ़ायदा होगा, जबकि सिर्फ़ is_archived = false अक्सर नहीं।
Search स्क्रीन: full-text, prefixes, और “contains”
सर्च बॉक्स साधारण दिखते हैं, लेकिन यूज़र्स से बहुत उम्मीदें होती हैं: कई शब्द, अलग शब्द रूप, और सामान्य रैंकिंग। PostgreSQL में यह अक्सर full-text search होता है: आप tsvector में टेक्स्ट स्टोर करते हैं (prepared text) और tsquery से क्वेरी करते हैं (यूज़र के टाइप किए शब्दों को terms में पार्स करके)।
फुल-टेक्स्ट सर्च के लिए GIN सामान्य डिफ़ॉल्ट है क्योंकि यह “क्या यह डॉक्यूमेंट इन terms को contain करता है?” वाले सवालों का तेज़ जवाब देता है। ट्रेडऑफ heavier writes है: insert और update महंगे पड़ते हैं।
GiST भी फुल-टेक्स्ट सर्च के लिए काम कर सकता है। यह अक्सर छोटा और अपडेट में सस्ता होता है, पर पढ़ने में आमतौर पर GIN से धीमा। अगर आपका डेटा लगातार बदलता है (उदाहरण के लिए event-like टेबल्स), तो यह read-write बैलेंस मायने रखता है।
Prefix search फुल-टेक्स्ट नहीं है
Prefix search का मतलब “starts with” है, जैसे ईमेल प्रीफ़िक्स से यूज़र्स खोजना। यह फुल-टेक्स्ट का काम नहीं है। Prefix पैटर्न्स के लिए B-tree इंडेक्स मदद कर सकता है (अक्सर सही operator class के साथ) क्योंकि यह strings के ordering से मेल खाता है।
“Contains” सर्च जैसे ILIKE '%error%' में आम तौर पर B-tree मदद नहीं कर पाता। यहाँ trigram indexing या दूसरा सर्च तरीका लागू होता है।
जब यूज़र्स filters के साथ text search चाहते हैं
अधिकांश असली स्क्रीन सर्च को filters के साथ मिलाती हैं: status, assignee, date range, tenant, आदि। व्यावहारिक सेटअप होता है:
tsvectorकॉलम के लिए GIN (या कभी-कभी GiST) इंडेक्स।- सबसे selective फ़िल्टर्स के लिए B-tree इंडेक्स (उदा.,
account_id,status,created_at)। - “मिनिमम रखें” का नियम क्योंकि ज़्यादा इंडेक्स writes को धीमा कर देते हैं।
उदाहरण: एक support tickets स्क्रीन जो “refund delayed” खोजती है और status = 'open' तथा किसी account_id पर फ़िल्टर करती है। फुल-टेक्स्ट प्रासंगिक रो देता है, जबकि B-tree PostgreSQL को सही account और status जल्दी से नॅरोज़ करने में मदद करता है।
JSONB फील्ड्स: GIN और targeted B-tree के बीच चुनना
JSONB फ्लेक्सिबिलिटी देता है, पर अगर आप इसे सामान्य कॉलम की तरह treat करते हैं तो यह स्लो क्वेरीज़ में बदल सकता है। मूल निर्णय सरल है: क्या आप “JSON में कहीं भी खोज” कर रहे हैं, या कुछ खास paths पर बार-बार फ़िल्टर करते हैं?
Containment क्वेरीज़ जैसे metadata @> '{"plan":"pro"}' के लिए GIN इंडेक्स आमतौर पर पहला विकल्प है। यह “क्या डॉक्यूमेंट इस shape को contain करता है?” के लिए बनाया गया है और key-existence checks (?, ?|, ?&) भी सपोर्ट करता है।
अगर आपका ऐप ज़्यादातर एक या दो JSON paths पर फ़िल्टर करता है, तो targeted B-tree expression index अक्सर तेज़ और छोटा होता है। यह extracted वैल्यूज़ पर sorting या numeric comparisons में भी मदद करता है।
-- Broad support for containment and key checks
CREATE INDEX ON customers USING GIN (metadata);
-- Targeted filters and sorting on one JSON path
CREATE INDEX ON customers ((metadata->>'plan'));
CREATE INDEX ON events (((payload->>'amount')::numeric));
एक अच्छा नियम:
- जब यूज़र्स कई keys, tags, या nested structures खोजते हैं तो GIN उपयोग करें।
- जब आप कुछ स्थिर paths पर बार-बार फ़िल्टर करते/सॉर्ट करते हों तो B-tree expression इंडेक्स उपयोग करें।
- असली स्क्रीन पर दिखने वाली चीज़ों को ही इंडेक्स करें, हर चीज़ को नहीं।
- अगर प्रदर्शन कुछ JSON keys पर निर्भर करता है जिन्हें आप हमेशा use करते हैं, तो उन्हें वास्तविक कॉलम में प्रमोट करने पर विचार करें।
उदाहरण: एक support स्क्रीन metadata->>'priority' से फ़िल्टर करती है और created_at से सॉर्ट करती है। JSON priority path और सामान्य created_at कॉलम दोनों को इंडेक्स करें। जब तक यूज़र्स tags या nested attributes भी नहीं खोजते, तब तक broad GIN छोड़ दें।
Geo और range क्वेरीज़: जहाँ GiST सबसे अच्छा बैठता है
Geo और range स्क्रीन वे जगह हैं जहाँ GiST अक्सर स्पष्ट रूप से उपयुक्त होता है। GiST उन सवालों के लिए बनाया गया है जो “यह चीज़ ओवरलैप करती है?”, “यह इस में है?” या “यह किसी पॉइंट के पास है?” जैसे होते हैं, न कि “यह मान बराबर है?” जैसे सादे सवाल।
Geo डेटा आमतौर पर पॉइंट्स (स्टोर लोकेशन), लाइन्स (रूट), या पॉलीगन (डिलिवरी जोन) होते हैं। सामान्य स्क्रीन “मेरे पास की स्टोर्स,” “10 किमी के भीतर की जॉब्स,” “यह मैप बॉक्स के भीतर कौन से आइटम हैं,” या “क्या यह पता हमारी सेवा एरिया के भीतर है?” जैसी क्वेरीज करती हैं। GiST इंडेक्स (आम तौर पर PostGIS geometry/geography के माध्यम से) इन स्पेशल ऑपरेटरों को तेज़ कर देता है ताकि डेटाबेस ज़्यादातर रो को स्किप कर सके बजाय हर आकार जांचने के।
Ranges भी इसी तरह हैं। PostgreSQL में daterange और int4range जैसे range types हैं, और सामान्य सवाल overlap होता है: “क्या यह बुकिंग किसी मौजूदा बुकिंग से टकराती है?” या “इस हफ़्ते के दौरान किस सब्सक्रिप्शन सक्रिय थे?” GiST overlap और containment ऑपरेटरों को कुशलता से सपोर्ट करता है, इसलिए यह कैलेंडर, शेड्यूलिंग, और अवेलेबिलिटी चेक में आम है।
B-tree का रोल geo-जैसे स्क्रीन पर भी रह सकता है। कई पन्ने पहले tenant, status, या time से फ़िल्टर करते हैं, फिर स्पेशल कंडीशन लागू करते हैं, फिर सॉर्ट करते हैं। उदाहरण: “सिर्फ हमारी कंपनी की डिलिवरीज़, पिछले 7 दिनों की, सबसे नज़दीकी पहले।” GiST स्पेशल पार्ट को संभालेगा, पर selective filters और sorting में B-tree मदद करेगा।
इंडेक्स चुनने का चरण-दर-चरण तरीका
इंडेक्स चुनाव ज्यादातर ऑपरेटर के बारे में है, न कि कॉलम के नाम के बारे में। वही कॉलम अलग-अलग इंडेक्स चाह सकता है इस बात पर कि आप =, >, LIKE 'prefix%', full-text search, JSON containment, या geo distance किसे उपयोग कर रहे हैं।
क्वेरी को चेकलिस्ट की तरह पढ़ें: WHERE तय करता है कौन सी पंक्तियाँ योग्य हैं, JOIN टेबल्स को कैसे जोड़ता है, ORDER BY आउटपुट ऑर्डर तय करता है, और LIMIT यह तय करता है कि आपको कितनी पंक्तियाँ वास्तव में चाहिए। सबसे अच्छा इंडेक्स अक्सर वही होता है जो आपको पहले 20 rows जल्दी से ढूँढने में मदद करे।
एक सरल प्रक्रिया जो ज़्यादातर ऐप स्क्रीन के लिए काम करती है:
- exact ऑपरेटर्स लिखें जो आपकी स्क्रीन उपयोग करती है (उदा.:
status =,created_at >=,name ILIKE,meta @>,ST_DWithin). - सबसे selective फ़िल्टर या default sort से मिलते हुए इंडेक्स से शुरू करें। अगर स्क्रीन
created_at DESCसे सॉर्ट करती है, वहाँ से शुरू करें। - composite इंडेक्स तभी जोड़ें जब आप बार-बार वही filters एक साथ देखें। equality कॉलम पहले रखें, फिर range कॉलम, फिर sort key।
- जब आप हमेशा एक subset को फ़िल्टर करते हों तो partial index का उपयोग करें (उदा.: सिर्फ़
status = 'open')। जब आप computed वैल्यू पर क्वेरी करते हों तो expression index उपयोग करें (उदा.: case-insensitive lookup के लिएlower(email))। EXPLAIN ANALYZEसे validate करें। उसे रखें अगर वह execution time और पढ़ी गई rows को बहुत घटा देता है।
ठोस उदाहरण: एक support dashboard status से फ़िल्टर करता है और newest से सॉर्ट करता है। (status, created_at DESC) पर B-tree एक अच्छा पहला प्रयास है। अगर वही स्क्रीन JSONB flag जैसे meta @> '{"vip": true}' पर भी फ़िल्टर करती है, तो वह अलग ऑपरेटर है और आमतौर पर अलग JSON-केंद्रित इंडेक्स चाहिए।
सामान्य गलतियाँ जो समय बर्बाद कर देती हैं (और writes धीमा कर देती हैं)
एक सामान्य कारण निराशा का यह है कि आप सही इंडेक्स प्रकार चुनते हैं पर गलत ऑपरेटर के लिए। PostgreSQL केवल तब इंडेक्स उपयोग कर सकता है जब क्वेरी उसी चीज़ के अनुरूप हो जिस पर इंडेक्स बनाया गया था। अगर आपका ऐप ILIKE '%term%' उपयोग करता है, तो उस टेक्स्ट कॉलम पर साधारण B-tree बेकार रहेगा और आपको टेबल स्कैन ही करना पड़ेगा।
एक और जाल है विशाल multi-column इंडेक्स बनाना “बस केस के लिए।” वे सुरक्षित दिखते हैं, पर मेंटेन करने में महंगे होते हैं और अक्सर असली क्वेरी पैटर्न से मेल नहीं खाते। अगर leftmost कॉलम फ़िल्टर में उपयोग नहीं हो रहा, तो इंडेक्स का बाकी हिस्सा मदद नहीं कर पाता।
Low-selectivity कॉलमों पर ज्यादा इंडेक्स बनाना भी आसान गलती है। boolean जैसे is_active या status जैसे कॉलम पर साधारण B-tree अक्सर बेकार रहता है जब तक आप उसे partial न बनायें।
JSONB की अपनी खास समस्याएँ हैं। एक broad GIN इंडेक्स फ्लेक्सिबिलिटी देता है, पर बहुत सारे JSONB path checks expression index से तेज़ हो सकते हैं। अगर आपकी स्क्रीन हमेशा payload->>'customer_id' से फ़िल्टर करती है, तो उस expression को इंडेक्स करना पूरे डॉक्यूमेंट को इंडेक्स करने से छोटा और तेज़ होगा।
आख़िर में, हर अतिरिक्त इंडेक्स writes पर बोझ डालता है। अक्सर अपडेट होने वाली टेबल्स (tickets या orders जैसे) पर हर insert और update को हर इंडेक्स अपडेट करना पड़ता है।
इंडेक्स जोड़ने से पहले रुककर जाँचें:
- क्या इंडेक्स आपके क्वेरी के exact ऑपरेटर से मेल खाता है?
- क्या आप एक बड़े multi-column इंडेक्स को एक या दो फोकस्ड इंडेक्स से बदल सकते हैं?
- क्या यह partial इंडेक्स होना चाहिए ताकि low-selectivity का नॉइज़ हटे?
- JSONB के लिए क्या expression index स्क्रीन के लिए बेहतर रहेगा?
- क्या टेबल write-heavy इतनी है कि इंडेक्स लागत पढ़ने के लाभ से ज़्यादा है?
एक इंडेक्स जोड़ने (या रखने) से पहले त्वरित जाँचें
नया इंडेक्स बनाने से पहले, यह स्पष्ट करें कि ऐप वास्तव में क्या करता है। “अच्छा लगे” वाला इंडेक्स अक्सर writes को धीमा करने और स्टोरेज बढ़ाने के अलावा ज्यादा फायदा नहीं देता।
अपने टॉप 3 स्क्रीन (या API endpoints) से शुरू करें और उनके exact WHERE और ORDER BY पैटर्न लिखें (direction और NULL handling सहित)। कई “इंडेक्स समस्याएँ” वास्तव में “अस्पष्ट क्वेरी समस्याएँ” होती हैं, खासकर जब लोग B-tree vs GIN vs GiST पर बहस करते हैं बिना ऑपरेटर नाम बताए।
एक सरल चेकलिस्ट:
- 3 असली स्क्रीन चुनें और उनका exact
WHEREऔरORDER BYपैटर्न लिखें। - ऑपरेटर टाइप की पुष्टि करें: equality (
=), range (>,BETWEEN), prefix, contains, overlap, या distance। - हर सामान्य स्क्रीन पैटर्न के लिए एक इंडेक्स चुनें, टेस्ट करें, और सिर्फ़ वही रखें जो समय या पढ़े गए पृष्ठों (rows/pages) को मापन योग्य रूप से घटाए।
- अगर टेबल write-heavy है, तो सख़्ती बरतें: अतिरिक्त इंडेक्स write लागत को गुणा कर देते हैं और vacuum दबाव बढ़ा सकते हैं।
- फीचर बदलने के बाद पुन: जाँच करें। नया फ़िल्टर, नया default sort, या “starts with” से “contains” पर स्विच करने से पुराना इंडेक्स बेकार हो सकता है।
उदाहरण: एक डैशबोर्ड नया default sort last_activity DESC जोड़ता है। अगर आपने केवल status इंडेक्स किया था, तो फिल्टर तेज़ रह सकता है, पर सॉर्ट अब अतिरिक्त काम करती है।
उदाहरण: असली ऐप स्क्रीन को सही इंडेक्स से मैप करना
निर्णय तालिका तब ही मदद करती है जब आप उसे असली स्क्रीन से मैप कर सकें। यहाँ तीन सामान्य स्क्रीन हैं और वे किस इंडेक्स के साथ मेल खाती हैं।
| Screen | Typical query pattern | Index that usually fits | Why |
|---|---|---|---|
| Admin list: filters + sort + free-text search | status = 'open' plus created_at sort, plus search in title/notes | B-tree on (status, created_at) and GIN on a tsvector | Filters + sorting के लिए B-tree. Full-text search के लिए आमतौर पर GIN। |
| Customer profile: JSON preferences + flags | prefs->>'theme' = 'dark' or a flag exists | GIN on the JSONB column for flexible key lookups, or targeted B-tree on expressions for 1-2 hot keys | चुनें इस आधार पर कि आप कई keys खोजते हैं या कुछ ही स्थिर paths। |
| Nearby locations: distance + category filter | Places within X km, filtered by category_id | GiST on geometry/geography and B-tree on category_id | GiST distance/within संभालता है। B-tree सामान्य फ़िल्टर्स के लिए। |
इसे लागू करने का व्यावहारिक तरीका UI से शुरू करना है:
- हर कंट्रोल जो परिणामों को घटाता है (filters) को सूचीबद्ध करें।
- डिफ़ॉल्ट sort order नोट करें।
- सर्च व्यवहार के बारे में स्पष्ट रहें (full-text vs starts-with vs contains)।
- “विशेष” फील्ड्स (JSONB, geo, ranges) को अलग बताएं।
अगले कदम: इंडेक्सिंग को अपनी बिल्ड प्रक्रिया का हिस्सा बनाएं
अच्छे इंडेक्स आपकी स्क्रीन का अनुसरण करते हैं: वे फ़िल्टर जो लोग क्लिक करते हैं, वे सॉर्ट जो वे अपेक्षित करते हैं, और जो सर्च बॉक्स वे वास्तव में उपयोग करते हैं। डेवलपमेंट के दौरान इंडेक्सिंग को एक आदत बनाएं और आप बाद में ज़्यादातर प्रदर्शन आश्चर्य से बचेंगे।
इसे दोहराने योग्य रखें: एक स्क्रीन की 1-3 क्वेरीज़ की पहचान करें, सबसे छोटा इंडेक्स जोड़ें जो उन्हें मेल खाए, रियलिस्टिक डेटा के साथ टेस्ट करें, फिर जो वजन नहीं लाता उसे हटा दें।
अगर आप internal tool या customer portal बना रहे हैं, तो इंडेक्स ज़रूरतों को जल्दी योजना में शामिल करें क्योंकि ये ऐप्स अक्सर और फ़िल्टर और लिस्ट स्क्रीन जोड़कर बढ़ते हैं। अगर आप AppMaster (appmaster.io) के साथ बना रहे हैं, तो प्रत्येक स्क्रीन के फ़िल्टर और सॉर्ट कॉन्फ़िगरेशन को एक ठोस क्वेरी पैटर्न समझ कर रखें, और फिर केवल उन्हीं इंडेक्स को जोड़ें जो उन वास्तविक क्लिकों का समर्थन करते हैं।
सामान्य प्रश्न
Start by writing down what your busiest screens actually do in SQL terms: the WHERE operators, the ORDER BY, and the LIMIT. B-tree usually fits equality, ranges, and sorting; GIN fits “contains term/value” checks like full-text and JSONB containment; GiST fits overlap, distance, and “near/within” style queries.
A B-tree index is best when you filter by exact values, filter by ranges, or need results returned in a specific order. It’s the usual choice for admin lists, dashboards, and pagination where the query is “filter, sort, limit.”
Use GIN when each row can match many keys or terms and your query asks “does this row contain X?” It’s the common default for full-text search (@@ on tsvector) and JSONB/array containment like @> or key-existence checks.
GiST is a good fit for data that isn’t naturally ordered, where queries are about proximity, overlap, or containment in a geometric or range sense. Common cases are PostGIS “near me/within radius” queries and PostgreSQL range types where you check overlaps.
If your query filters and sorts, put the equality filters first, then any range filter, then the sort column. For example, (user_id, status, created_at DESC) works well when you always filter by user_id and status and show newest first; it won’t help much if you only filter by status.
A partial index is worth it when a screen always looks at a subset of rows, like “only open tickets” or “not soft-deleted.” It keeps the index smaller and faster, and it avoids paying index cost for rows that the screen never touches.
A plain index on a boolean or tiny enum often disappoints because each value matches a large part of the table, so PostgreSQL may prefer a sequential scan. It can still help when combined with a selective column (like tenant_id) or when made partial to match the exact slice you query.
Use a GIN index on the whole JSONB column when you need flexible containment and key checks across many different keys. Use targeted B-tree expression indexes when you repeatedly filter or sort by a few stable JSON paths, like (metadata->>'plan') or a numeric cast of a JSON value.
For “starts with” searches like email LIKE 'abc%', a B-tree index can help because it aligns with string ordering. For “contains” searches like ILIKE '%abc%', a normal B-tree usually won’t be used; you’ll need a different approach (often trigram indexing) or a different search design.
Create the smallest index that matches a specific, high-traffic query pattern, then validate with EXPLAIN ANALYZE and realistic data sizes. If you’re building screens in AppMaster, treat each list’s filters, default sort, and search behavior as the query contract, then add only the indexes that directly support those patterns to avoid unnecessary write slowdown.


