Introduction
Beyond simple extraction, JSONB supports containment and existence operators that test whether a document contains a specific structure or keys. These operators are uniquely powerful because they work with GIN indexes, making them the fastest way to filter JSONB documents in large tables.
Key Concepts
- Containment (
@>): Tests whether the left JSONB value contains the right JSONB value as a subset, including nested structures. - Contained by (
<@): The reverse of containment — tests whether the left value is contained within the right value. - Key existence (
?): Tests whether a specific key (or string element in an array) exists in the top level of a JSONB object. - Any key exists (
?|): Tests whether any key in the given array exists in the JSONB value. - All keys exist (
?&): Tests whether all keys in the given array exist in the JSONB value.
Real World Context
Containment queries are the backbone of document-style filtering in PostgreSQL. When an e-commerce site lets users filter products by brand, color, and specs, the backend often uses @> with a GIN index for sub-millisecond lookups across millions of products. Key existence operators are used to find documents with optional fields, such as checking which users have set up two-factor authentication.
Deep Dive
Containment Operators
The @> operator checks whether the left value contains the right value:
sql-- @> : Contains (left contains right) SELECT '{"a": 1, "b": 2}'::jsonb @> '{"a": 1}'::jsonb; -- true SELECT '{"a": 1}'::jsonb @> '{"a": 1, "b": 2}'::jsonb; -- false -- <@ : Is contained by SELECT '{"a": 1}'::jsonb <@ '{"a": 1, "b": 2}'::jsonb; -- true -- Array containment SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb; -- true SELECT '[1, 2, 3]'::jsonb @> '[1, 4]'::jsonb; -- false
Containment checks nested structures recursively, so you can match deeply nested values in a single operator.
Existence Operators
Existence operators check for the presence of keys:
sql-- ? : Key exists SELECT '{"a": 1, "b": 2}'::jsonb ? 'a'; -- true SELECT '{"a": 1, "b": 2}'::jsonb ? 'c'; -- false -- ?| : Any key exists SELECT '{"a": 1, "b": 2}'::jsonb ?| array['c', 'b']; -- true -- ?& : All keys exist SELECT '{"a": 1, "b": 2}'::jsonb ?& array['a', 'b']; -- true SELECT '{"a": 1, "b": 2}'::jsonb ?& array['a', 'c']; -- false
These operators only check top-level keys. To check nested keys, extract the nested object first.
Practical Queries
Here are common patterns you will use in production:
sql-- Find products with specific brand SELECT * FROM products WHERE attributes @> '{"brand": "Apple"}'; -- Find users with admin role SELECT * FROM users WHERE profile->'roles' @> '["admin"]'::jsonb; -- Find documents with required fields SELECT * FROM documents WHERE metadata ?& array['author', 'created_at', 'version']; -- Find any matching tag SELECT * FROM articles WHERE tags ?| array['postgresql', 'database', 'sql'];
All of these queries can use GIN indexes, making them very fast even on large datasets.
Common Pitfalls
- Expecting containment to check nested keys directly —
'{"a": {"b": 1}}' @> '{"b": 1}'is false because@>checks at the same nesting level. You need'{"a": {"b": 1}}'::jsonb @> '{"a": {"b": 1}}'::jsonb. - Using ? with parameterized queries in some ORMs — The
?operator conflicts with parameter placeholders in many ORMs and drivers. Use the function formjsonb_exists(column, key)or escape the operator. - Forgetting that existence operators check top-level only —
'{"a": {"b": 1}}'::jsonb ? 'b'is false becausebis not a top-level key.
Best Practices
- Use @> with GIN indexes for document filtering — Containment queries are the most index-friendly JSONB operations and should be your default filtering pattern.
- Prefer ?& for required-fields validation — Rather than multiple
?checks, use?& array['key1', 'key2']for cleaner and slightly faster queries. - Create GIN indexes on columns used with containment — Without a GIN index, containment operators still do a sequential scan.
Summary
@>tests whether a JSONB value contains another as a subset (works recursively on nested structures).?,?|, and?&test for key existence at the top level.- All containment and existence operators are supported by GIN indexes.
- Use
@>as your primary filtering pattern for JSONB document queries. - Existence operators only check top-level keys, not nested ones.
Code Examples
-- Find products matching multiple criteria
SELECT * FROM products
WHERE attributes @> '{
"brand": "Dell",
"specs": {"ram": 16}
}'::jsonb;
-- Find users with specific permissions
SELECT * FROM users
WHERE permissions @> '["read", "write"]'::jsonb;
-- Check for optional fields
SELECT
id,
data->>'name' AS name,
CASE WHEN data ? 'email' THEN data->>'email' ELSE 'N/A' END AS email
FROM profiles;