CertGrid CertGrid
Hands-on Lab·PostgreSQL

PostgreSQL JSON and Array Columns

`jsonb` and `text[]` are real column types with real operators - containment, extraction, membership, expansion - so semi-structured data stays queryable in SQL rather than becoming a string the database cannot see into.

Schemas and Data Guide 10 of 47 Intermediate

Written against the versions above. Use `jsonb`, not `json`. `json` stores the original text and reparses on every access; `jsonb` stores a parsed binary form, supports indexing and is what every operator here needs.

Every command on this page ran on db-a01.
Server NameIP AddressOSRolesCPURAMHDD
db-a01192.168.0.81Ubuntu 26.04 LTSPrimary / Source / Replica Set Member 12 Core4 GB50 GB

Before you start

  1. Query inside a JSON document

    Three jsonb features in one statement.

    ->> extracts a value as text; -> would return jsonb. The distinction matters when comparing - profile->>'tier' = 'gold' compares text, and profile->'tier' = 'gold' is a type error.

    (profile->>'seats')::int casts explicitly, because JSON extraction yields text. Sorting without the cast would order 9 before 3 as strings.

    @> is containment - *does this document contain this fragment* - and it is the operator that matters most, because it is the one a GIN index can accelerate. profile->>'tier' = 'gold' reads naturally and cannot use a GIN index; profile @> '{"tier":"gold"}' can.

    Two gold customers, ordered by seats.

    bash Example session
    psql -d appdb -c "SELECT name, profile->>'tier' AS tier, (profile->>'seats')::int AS seats FROM customers WHERE profile @> '{\"tier\":\"gold\"}' ORDER BY seats DESC"     name      | tier | seats---------------+------+------- Zoe Washburne | gold |     9 Ada Lovelace  | gold |     3(2 rows)psql -d appdb -c "SELECT name, tags FROM customers WHERE 'vip' = ANY(tags)"     name     |   tags--------------+---------- Ada Lovelace | {vip,eu}(1 row)

    Expected resultTwo gold-tier rows sorted by seats, then the row tagged vip.

    Success conditionYou can filter on JSON contents without extracting them in application code.

  2. Query and expand an array column

    'vip' = ANY(tags) is array membership, and it reads as an English sentence.

    Then unnest does the opposite: it expands each array element into its own row, so an array column can be grouped and counted like a join table. Four tags across three customers, one row each.

    That is the honest trade with array columns. They avoid a join table for genuinely list-like data - tags, labels, permissions - and they are indexable with GIN. But there is no foreign key from an array element, so nothing guarantees a tag is spelled the same way twice. Use them where the values are opaque strings you do not need referential integrity on, and a real table where you do.

    bash Example session
    psql -d appdb -c "SELECT unnest(tags) AS tag, count(*) FROM customers GROUP BY 1 ORDER BY 2 DESC, 1" tag  | count------+------- apac |     1 eu   |     1 us   |     1 vip  |     1(4 rows)

    Expected resultThe vip row, then four tags with counts.

    Success conditionYou can treat an array column as rows when you need to.

Troubleshooting

Official sources