Choose a PostgreSQL JSONB index from the operator and expression your query actually uses. A GIN index on the whole document, a path-specific expression index and a B-tree on an extracted scalar serve different query shapes. First verify that the stored value is the intended object or array; an index cannot correct a JSON string that merely contains serialized object text.
Inventory paths and operators
For a synthetic questionnaire table, list the frequent predicates before choosing an index. Tag containment might use metadata @> with an object containing an array. A status filter might use metadata->>'status' = 'ready'. A key-existence check uses another operator. Record tenant scope, expected selectivity, field types and how missing keys should behave. The same document column can justify different access paths without making one universal index the right answer.
PostgreSQL's JSON documentation distinguishes jsonb_ops from jsonb_path_ops. The latter supports containment and certain JSON-path operations but not key-existence operators. Expression indexes can target a commonly queried path. Use the operator-class documentation for the installed server version and verify the expression the planner actually sees.
PostgreSQL: JSONB operators, GIN classes and expression indexes
Two separate candidate indexes
The following examples are proposed SQL for a disposable dataset. They are alternatives to evaluate, not instructions to install every index on every table. A broad document GIN index supports a flexible containment surface; an extracted scalar B-tree can be narrower for a stable status filter. Include the tenant in the reviewed query and evaluate whether the access path fits that scope.
CREATE INDEX fixture_metadata_gin ON fixture_answers
USING GIN (metadata jsonb_path_ops);
-- Candidate for a distinct equality query:
CREATE INDEX fixture_status_btree ON fixture_answers
(workspace_id, (metadata->>'status'));
-- Different query shapes:
WHERE metadata @> '{"tags":["review"]}'::jsonb
WHERE workspace_id = $1 AND metadata->>'status' = $2Do not assume the whole-document index covers every operator applied to a nested expression. A query on metadata->'tags' can require an index on that expression or a rewritten containment predicate. Keep query semantics constant while comparing plans. Rewriting a condition just to make an index usable can change missing-key, null or heterogeneous-value behavior if the equivalence was never established.
| Predicate | Candidate | Case to verify |
|---|---|---|
| Whole-document containment | GIN class supporting @> | Nested array semantics |
| Key exists | jsonb_ops or appropriate expression | Missing versus null |
| Extracted status equality | B-tree on expression | Type and collation |
| Malformed encoded container | Data repair first | jsonb_typeof string |
Data shape comes before plan shape
Populate a synthetic fixture with an object, an absent status key, explicit JSON null, a numeric status and a string containing JSON. Record the expected result for each predicate. Extracting text can blur distinctions that matter to the product; validate the schema or add explicit type checks where needed. Inspect jsonb_typeof rather than relying on how a value looks when printed by a database console.
If a custom driver codec serializes Python values, pass the value type the codec expects. Pre-serializing a dictionary can turn the stored value into a JSON string. Repairing historical shapes is a separate migration with its own audit and rollback plan. An index added before that review can make a malformed dataset faster to query incorrectly without resolving the underlying contract.
Compare reads and writes on representative data
Capture EXPLAIN output and safe actual execution for each candidate under realistic tenant sizes and filters. Measure index size, insertion/update cost and maintenance behavior. A containment query matching most rows may reasonably choose a sequential scan. Do not call that a failed index merely because the object exists in the catalogue. Planner choice depends on statistics, selectivity and competing access paths.
No PostgreSQL index experiment was executed here. The query inventory and heterogeneous fixture matrix are proposed review artifacts. A useful recommendation names the operator, indexed expression and measured tradeoff. If the status field becomes central to constraints, reporting and frequent updates, compare a typed relational column as well; JSONB flexibility does not require every important attribute to remain an unvalidated nested value.
Firestore or PostgreSQL? Start with the product’s data.
Building UltraFit360: the app, coach portal and shared backend
Bring one API or data workflow and a sanitized failure example for a bounded implementation assessment.
Which PostgreSQL index should you use for JSONB?
Match the operator and indexed expression: whole-document GIN, path-specific GIN or scalar B-tree, after validating the stored JSONB shape.
Does jsonb_path_ops support every JSONB operator?
No. Its documented operator set differs from jsonb_ops, including lack of key-existence support.
Will a JSONB index fix double-encoded values?
No. Validate and repair the stored type separately; an encoded object string is still a JSONB string.
