PostgreSQL JSONB in Laravel: Indexes &amp; Eloquent Casts | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain

 PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain
==============================================================================

 JSONB columns unlock flexible schemas in PostgreSQL, but raw queries get ugly fast. Learn how to index, query, and cast JSONB in Laravel with clean Eloquent patterns that stay performant at scale.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 10 Aug 2026 · Updated 10 Aug 2026 · 4 min read

ShareCopy linkCopied

 ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain](https://cdn.msaied.com/532/0c1c122849d3f997950ffca44076f86c.png) 

  On this page +1. [Why JSONB Belongs in Your Laravel Stack](#why-jsonb-belongs-in-your-laravel-stack)
2. [Indexing JSONB Correctly](#indexing-jsonb-correctly)
3. [GIN for Containment Queries](#gin-for-containment-queries)
4. [Expression Index for a Specific Path](#expression-index-for-a-specific-path)
5. [Querying JSONB in Eloquent](#querying-jsonb-in-eloquent)
6. [A Reusable Scope](#a-reusable-scope)
7. [Custom Eloquent Cast for Typed JSONB](#custom-eloquent-cast-for-typed-jsonb)
8. [Updating Nested Keys Without Overwriting](#updating-nested-keys-without-overwriting)
9. [Takeaways](#takeaways)

 Why JSONB Belongs in Your Laravel Stack
---------------------------------------

PostgreSQL's `jsonb` type is not a document-store escape hatch — it is a first-class column type with binary storage, deduplication, and indexable paths. Used correctly it eliminates entire pivot tables and EAV nightmares. Used naively it becomes an unindexed black hole that kills query plans.

This article covers the three layers you need to get right: **indexing strategy**, **query builder patterns**, and **Eloquent casts**.

---

Indexing JSONB Correctly
------------------------

### GIN for Containment Queries

The default GIN index covers the `@>` (contains) and `?` (key exists) operators — the two you will use most.

```sql
CREATE INDEX idx_users_meta_gin ON users USING GIN (meta);

```

In a migration:

```php
$table->jsonb('meta')->nullable();
DB::statement('CREATE INDEX idx_users_meta_gin ON users USING GIN (meta)');

```

### Expression Index for a Specific Path

When you always filter on `meta->>'plan'`, a targeted B-tree expression index is cheaper than a full GIN index:

```sql
CREATE INDEX idx_users_meta_plan
  ON users ((meta->>'plan'));

```

This index is used by `WHERE meta->>'plan' = 'pro'` and nothing else — tight and fast.

---

Querying JSONB in Eloquent
--------------------------

Laravel's query builder has no native JSONB operator support, but `whereRaw` and `->` / `->>` operators are readable enough:

```php
// Containment: users whose meta contains {"plan": "pro"}
User::whereRaw("meta @> ?::jsonb", [json_encode(['plan' => 'pro'])])->get();

// Text extraction: uses expression index above
User::whereRaw("meta->>'plan' = ?", ['pro'])->get();

// Key existence
User::whereRaw("meta \? ?", ['onboarded'])->get();

```

### A Reusable Scope

Wrap the noise in a query scope so call sites stay clean:

```php
// app/Models/Concerns/HasJsonbMeta.php
trait HasJsonbMeta
{
    public function scopeWhereMetaContains(
        Builder $query,
        array $subset,
        string $column = 'meta'
    ): Builder {
        return $query->whereRaw(
            "{$column} @> ?::jsonb",
            [json_encode($subset)]
        );
    }

    public function scopeWhereMetaPath(
        Builder $query,
        string $path,
        mixed $value,
        string $column = 'meta'
    ): Builder {
        return $query->whereRaw(
            "{$column}->>'$path' = ?",
            [(string) $value]
        );
    }
}

```

Usage:

```php
User::whereMetaContains(['plan' => 'pro', 'trial' => false])->paginate();
User::whereMetaPath('plan', 'pro')->whereMetaPath('locale', 'en')->get();

```

---

Custom Eloquent Cast for Typed JSONB
------------------------------------

Storing arbitrary arrays is fine for prototypes. In production, cast to a typed DTO so you get IDE completion and validation at the boundary.

```php
// app/Casts/UserMetaCast.php
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class UserMetaCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): UserMeta
    {
        $data = json_decode($value ?? '{}', true);
        return UserMeta::fromArray($data);
    }

    public function set($model, string $key, $value, array $attributes): string
    {
        if ($value instanceof UserMeta) {
            return json_encode($value->toArray());
        }
        return json_encode($value);
    }
}

```

```php
// app/Data/UserMeta.php
readonly class UserMeta
{
    public function __construct(
        public string $plan = 'free',
        public string $locale = 'en',
        public bool $trial = false,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            plan: $data['plan'] ?? 'free',
            locale: $data['locale'] ?? 'en',
            trial: $data['trial'] ?? false,
        );
    }

    public function toArray(): array
    {
        return ['plan' => $this->plan, 'locale' => $this->locale, 'trial' => $this->trial];
    }
}

```

Register on the model:

```php
protected $casts = [
    'meta' => UserMetaCast::class,
];

```

Now `$user->meta->plan` is typed, and saving is automatic.

---

Updating Nested Keys Without Overwriting
----------------------------------------

Avoid loading the full row just to change one key. Use PostgreSQL's `jsonb_set`:

```php
DB::table('users')
    ->where('id', $userId)
    ->update([
        'meta' => DB::raw("jsonb_set(meta, '{plan}', '\"enterprise\"')"),
    ]);

```

This is an atomic server-side update — no race condition, no full-row read.

---

Takeaways
---------

- Use a **GIN index** for containment/key-existence queries; use an **expression B-tree index** when filtering a single known path.
- Prefer `@>` with `::jsonb` cast over `->>` string comparisons when you need multi-key containment — one operator, one index scan.
- Wrap raw JSONB operators in **query scopes** or **macro helpers** to keep Eloquent call sites readable.
- Cast JSONB columns to **typed readonly DTOs** rather than plain arrays; you get validation, IDE support, and serialization in one place.
- Use `jsonb_set` for surgical key updates instead of read-modify-write cycles in PHP.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [postgresql](https://www.msaied.com/public/articles?search=postgresql)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [jsonb](https://www.msaied.com/public/articles?search=jsonb)

 Frequently asked questions 
---------------------------

  Does Laravel's built-in `array` cast work with JSONB columns?Yes, but it casts to a plain PHP array with no type safety. For production code, a custom cast backed by a typed readonly DTO gives you IDE completion, validation, and a clean serialization contract.

   When should I choose a GIN index over an expression B-tree index on a JSONB column?Use GIN when you query multiple paths or use containment (`@&gt;`) and key-existence (`?`) operators. Use an expression B-tree index when you always filter on one specific path with equality — it is smaller and faster for that single access pattern.

   Can I use Eloquent's `where` method directly on JSONB paths?Laravel's `where('meta-&gt;plan', 'pro')` syntax works for MySQL JSON columns but does not translate to PostgreSQL JSONB operators. Use `whereRaw` with `-&gt;&gt;` or `@&gt;` operators, ideally wrapped in a reusable query scope.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleContextual Binding and Method Injection in Laravel's Service Container](https://www.msaied.com/public/articles/contextual-binding-and-method-injection-in-laravels-service-container-2) [Next articleLaravel artisan dev: Run Server, Queue, Logs, and Vite in One Command](https://www.msaied.com/public/articles/laravel-artisan-dev-run-server-queue-logs-and-vite-in-one-command)  

   On this page
-------------

1. [Why JSONB Belongs in Your Laravel Stack](#why-jsonb-belongs-in-your-laravel-stack)
2. [Indexing JSONB Correctly](#indexing-jsonb-correctly)
3. [GIN for Containment Queries](#gin-for-containment-queries)
4. [Expression Index for a Specific Path](#expression-index-for-a-specific-path)
5. [Querying JSONB in Eloquent](#querying-jsonb-in-eloquent)
6. [A Reusable Scope](#a-reusable-scope)
7. [Custom Eloquent Cast for Typed JSONB](#custom-eloquent-cast-for-typed-jsonb)
8. [Updating Nested Keys Without Overwriting](#updating-nested-keys-without-overwriting)
9. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
