Files
supabase-postgres-best-prac…/skills/supabase/references/db-schema-jsonb.md

2.4 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Structured Columns Over JSONB When Possible MEDIUM Improves query performance, type safety, and data integrity jsonb, json, schema-design, performance

Use Structured Columns Over JSONB When Possible

JSONB is flexible but should not replace proper schema design. Use structured columns for known fields, JSONB for truly dynamic data.

Incorrect:

-- Everything in JSONB - loses type safety and performance
create table users (
  id uuid primary key,
  data jsonb  -- contains email, name, role, etc.
);

-- Querying is verbose and slow without indexes
select data ->> 'email' from users
where data ->> 'role' = 'admin';

Correct:

-- Structured columns for known fields
create table users (
  id uuid primary key,
  email text not null,
  name text,
  role text check (role in ('admin', 'user', 'guest')),
  -- JSONB only for truly flexible data
  preferences jsonb default '{}'
);

-- Fast, type-safe queries
select email from users where role = 'admin';

When JSONB is Appropriate

  • Webhook payloads
  • User-defined fields
  • API responses to cache
  • Rapid prototyping (migrate to columns later)

Indexing JSONB

-- GIN index for containment queries
create index idx_users_preferences on users using gin(preferences);

-- Query using containment operator
select * from users
where preferences @> '{"theme": "dark"}';

Validate JSONB with pg_jsonschema

create extension if not exists pg_jsonschema with schema extensions;

alter table users
add constraint check_preferences check (
  jsonb_matches_schema(
    '{
      "type": "object",
      "properties": {
        "theme": {"type": "string", "enum": ["light", "dark"]},
        "notifications": {"type": "boolean"}
      }
    }',
    preferences
  )
);

Querying JSONB

// supabase-js
const { data } = await supabase
  .from("users")
  .select("email, preferences->theme")
  .eq("preferences->>notifications", "true");

// containment queries
const { data: darkUsers } = await supabase
  .from("users")
  .select("*")
  .contains("preferences", { theme: "dark" });

// contained-by query
const { data: subset } = await supabase
  .from("users")
  .select("*")
  .containedBy("preferences", { theme: "dark", notifications: true });