0Pricing
PostgreSQL Performance & Query Optimization · 课时

JSONB 运算符与包含查询

使用 GIN 索引确实能够加速的包含运算符和路径运算符。

JSONB 运算符与包含查询 是 CoddyKit 上的免费 PostgreSQL Performance & Query Optimization 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PostgreSQL Performance & Query Optimization 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PostgreSQL Performance & Query Optimization 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Operator Choice Decides Index Use

In PostgreSQL, a column of type jsonb can be searched many different ways, but not every operator can use an index. Performance here is almost entirely about choosing operators that a GIN index can accelerate.

  • A GIN index (Generalized Inverted Index) stores the keys and values inside your JSON documents so lookups skip the full table.
  • The two operators that matter most are containment (@>) and key existence (?, ?|, ?&).

This lesson teaches exactly which operators those are, and how to write queries that stay index-friendly.

The Containment Operator @>

The containment operator @> asks: does the left JSONB contain the right JSONB? The right side is a fragment, and Postgres checks that every key/value in it appears in the left document.

  • '{"a":1,"b":2}' @> '{"a":1}' is true.
  • '{"a":1}' @> '{"a":1,"b":2}' is false (the right side has more).

This is the workhorse for filtering rows: WHERE data @> '{"status":"active"}' finds every row whose JSON includes that pair.

SELECT '{"a":1,"b":2}'::jsonb @> '{"a":1}'::jsonb AS contains_a,
       '{"a":1}'::jsonb @> '{"a":1,"b":2}'::jsonb AS contains_both;

Building a GIN Index for Containment

A plain GIN index on a jsonb column supports both containment and key-existence operators. This is the index you reach for first.

  • The default jsonb_ops operator class indexes every key and value.
  • It accelerates @>, ?, ?|, and ?&.

Create it once, and containment filters that previously scanned the whole table become bitmap index scans.

CREATE INDEX idx_events_data
  ON events
  USING GIN (data);

Containment Filters in WHERE

Once the GIN index exists, write the filter as a containment check so the planner can use it. Matching a nested fragment works too, because containment is recursive.

  • Top-level match: data @> '{"status":"active"}'.
  • Nested match: data @> '{"user":{"plan":"pro"}}'.

Notice we pass a JSON object literal on the right, not a column reference or function call. That literal shape is what makes the query index-eligible.

SELECT id, created_at
FROM events
WHERE data @> '{"user":{"plan":"pro"}}'
ORDER BY created_at DESC
LIMIT 50;

Key Existence Operators ? ?| ?&

Sometimes you only care whether a key is present, regardless of its value. The existence operators handle this and are also GIN-accelerated.

  • data ? 'email' — true if the top-level key email exists.
  • data ?| array['phone','email'] — true if any of these keys exist.
  • data ?& array['phone','email'] — true if all of these keys exist.

Important: ? checks top-level keys only, and for arrays it checks whether the string is an element.

SELECT '{"email":"x@y.z","phone":"123"}'::jsonb ? 'email'        AS has_email,
       '{"email":"x@y.z"}'::jsonb ?| array['phone','email']      AS has_any,
       '{"email":"x@y.z"}'::jsonb ?& array['phone','email']      AS has_all;

The Trap: Path Extraction Operators -> and ->>

The extraction operators look convenient but are not accelerated by a standard GIN index:

  • data -> 'status' returns the value as jsonb.
  • data ->> 'status' returns the value as text.

A query like WHERE data ->> 'status' = 'active' forces a sequential scan on a plain GIN index, because the index does not index extracted scalar comparisons. Prefer the containment form data @> '{"status":"active"}' instead.

-- Slow on a plain GIN index (seq scan):
SELECT * FROM events WHERE data ->> 'status' = 'active';

-- Fast equivalent (uses GIN):
SELECT * FROM events WHERE data @> '{"status":"active"}';

Rescuing ->> with an Expression Index

If you genuinely need range or pattern comparisons on one field, a B-tree expression index on the extracted text is the right tool — not GIN.

  • Index the exact expression you query.
  • Then comparisons like =, <, >, and BETWEEN can use it.

The query's expression must match the indexed expression character for character, or the planner ignores the index.

CREATE INDEX idx_events_status
  ON events ((data ->> 'status'));

-- Now this can use the B-tree index:
SELECT * FROM events WHERE (data ->> 'status') = 'active';

jsonb_path_ops: Smaller, Faster, Containment-Only

The alternative operator class jsonb_path_ops indexes hashed root-to-leaf paths instead of every key.

  • It produces a smaller index and is typically faster for @> queries.
  • Trade-off: it supports only containment (@>), not the existence operators ?, ?|, ?&.

Choose jsonb_path_ops when your workload is dominated by containment filtering and you never need key-existence searches.

CREATE INDEX idx_events_data_path
  ON events
  USING GIN (data jsonb_path_ops);

Containment Against Arrays

Containment also matches inside JSON arrays, which makes it ideal for tag-style data. To ask "does this array contain a value," wrap the value in an array on the right side.

  • '["a","b","c"]' @> '["b"]' is true.
  • For a tagged document: data @> '{"tags":["urgent"]}' finds rows whose tags array includes urgent.

This stays fully index-eligible on a GIN index, so tag filtering scales well.

SELECT '["a","b","c"]'::jsonb @> '["b"]'::jsonb   AS has_b,
       '{"tags":["urgent","billing"]}'::jsonb
         @> '{"tags":["urgent"]}'::jsonb            AS is_urgent;

Verify With EXPLAIN

Never assume the index is used — confirm it. Run EXPLAIN and look for a Bitmap Index Scan on your GIN index. A Seq Scan means your operator or expression defeated the index.

  • Good sign: Bitmap Index Scan on idx_events_data.
  • Bad sign: Seq Scan on events with a JSON filter.

Use EXPLAIN (ANALYZE, BUFFERS) to also see real timing and how many pages were read.

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM events
WHERE data @> '{"status":"active"}';

Putting It Together: A Decision Rule

Use this quick rule when writing a JSONB filter:

  • Matching key/value or nested fragment? Use @> with a GIN index.
  • Only checking a key is present? Use ?/?|/?& with default jsonb_ops GIN.
  • Containment-only workload, want the smallest index? Use jsonb_path_ops GIN.
  • Range or pattern on one scalar field? Use a B-tree expression index on ->>.

Avoid ->> equality filters without a matching expression index — they trigger sequential scans.

Quick Check

You have a default jsonb_ops GIN index on events.data. Which WHERE clause can use that index?

Recap

You learned which JSONB operators actually benefit from indexing:

  • @> (containment) is the primary GIN-accelerated filter, including nested objects and arrays.
  • ?, ?|, ?& (key existence) are GIN-accelerated, but only with the default jsonb_ops class, and check top-level keys.
  • jsonb_path_ops gives a smaller, faster containment-only index.
  • -> and ->> extraction filters do NOT use a plain GIN index; rewrite as @> or add a B-tree expression index.
  • Always confirm with EXPLAIN that you get a Bitmap Index Scan, not a Seq Scan.

常见问题解答

「JSONB 运算符与包含查询」课时是免费的吗?

是的 — 「JSONB 运算符与包含查询」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PostgreSQL Performance & Query Optimization 课程的其余内容,请升级到 CoddyKit PRO。 PostgreSQL Performance & Query Optimization 课程共包含 4 节课。

「JSONB 运算符与包含查询」这节课中我会学到什么?

使用 GIN 索引确实能够加速的包含运算符和路径运算符。 你通过在浏览器中直接运行的动手代码来练习 PostgreSQL Performance & Query Optimization,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PostgreSQL Performance & Query Optimization 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PostgreSQL Performance & Query Optimization 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「JSONB 运算符与包含查询」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PostgreSQL Performance & Query Optimization 课中编写并运行代码吗?

能。每节 PostgreSQL Performance & Query Optimization 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. JSONB 运算符与包含查询
  2. JSONB 上的 GIN 索引与表达式索引对比
  3. 使用 JSONPath 查询 JSONB
  4. 何时应将 JSONB 规范化为普通列
← 返回 PostgreSQL Performance & Query Optimization