PostgreSQL JSONB vs Array Column
When you need to store a list of values in a single column, PostgreSQL gives you two options: a native array (text[]) or a jsonb column. They overlap in capability but serve different needs.
The Scenario
You're adding a profile_links field to a User model. Each user can have multiple URLs pointing to their portfolio, LinkedIn, personal site, and so on. The question: text[] or jsonb?
text[] Array
Use an array column when the data is a flat list of the same type and the structure won't change.
# migration
add_column :users, :profile_links, :text, array: true, default: []
add_index :users, :profile_links, using: :gin
Querying is straightforward:
# find users who have a specific URL
User.where("'https://example.com/portfolio' = ANY(profile_links)")
# find users who have any of these URLs
User.where("profile_links && ARRAY[?]::text[]", ["https://linkedin.com/in/darr", "https://darr.dev"])
Array columns are simpler, use less storage, and have slightly faster reads. The GIN index makes containment queries efficient.
The limitation: every element must be the same type, and you can't attach metadata to individual items.
jsonb
Use jsonb when each entry is a structured object, or when you expect to add fields to each item later.
# migration
add_column :users, :profile_links, :jsonb, default: []
add_index :users, :profile_links, using: :gin
Each link can carry metadata:
user.profile_links = [
{ url: "https://linkedin.com/in/darr", label: "LinkedIn", pinned: true },
{ url: "https://darr.dev", label: "Portfolio" }
]
Querying with containment:
# find users with a specific URL
User.where("profile_links @> '[{\"url\": \"https://darr.dev\"}]'")
# find users with any pinned link
User.where("profile_links @> '[{\"pinned\": true}]'")
jsonb supports GIN indexing and a richer set of operators, but the query syntax is more verbose and write overhead is slightly higher due to binary serialization.
Which One to Pick
Use text[] if you're storing plain URLs with no per-link metadata. The query syntax is cleaner and you're not over-engineering.
Use jsonb if you have a concrete requirement to store label, created_at, or other attributes alongside each URL. Migrating from text[] to jsonb later is doable but requires a data transformation, so it's worth making the call upfront if the requirement is clear.
If the list can grow large (hundreds of items per user), consider a separate links table instead. Storing large arrays in a single column can cause performance issues with PostgreSQL's TOAST storage.