PostgreSQL JSONB in Laravel: Indexes, Queries &amp; Casts | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Bloat        On this page       1. [  PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Bloat ](#postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-bloat)
2. [  When JSONB Makes Sense ](#when-jsonb-makes-sense)
3. [  Migration: Column and Index ](#migration-column-and-index)
4. [  Querying with Eloquent ](#querying-with-eloquent)
5. [  Custom Eloquent Cast for Typed JSONB ](#custom-eloquent-cast-for-typed-jsonb)
6. [  Updating Nested Keys Without Overwriting the Column ](#updating-nested-keys-without-overwriting-the-column)
7. [  Key Takeaways ](#key-takeaways)

  ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Bloat](https://cdn.msaied.com/527/ccaba2b97bd9c80bd87c9e4886481cb6.png)

  #laravel   #postgresql   #eloquent   #jsonb   #performance  

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

     9 Aug 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Bloat  ](#postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-bloat)
2. [  02   When JSONB Makes Sense  ](#when-jsonb-makes-sense)
3. [  03   Migration: Column and Index  ](#migration-column-and-index)
4. [  04   Querying with Eloquent  ](#querying-with-eloquent)
5. [  05   Custom Eloquent Cast for Typed JSONB  ](#custom-eloquent-cast-for-typed-jsonb)
6. [  06   Updating Nested Keys Without Overwriting the Column  ](#updating-nested-keys-without-overwriting-the-column)
7. [  07   Key Takeaways  ](#key-takeaways)

 PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Bloat
------------------------------------------------------------------------------

JSONB is one of PostgreSQL's most powerful features, but it is also one of the most misused. Developers reach for it to avoid schema migrations, then discover their queries are doing sequential scans across millions of rows. This article covers the indexing strategies, Eloquent query methods, and custom casts that keep JSONB practical in production Laravel applications.

---

### When JSONB Makes Sense

JSONB is a good fit when:

- The shape of data varies per row (e.g., product attributes, feature flags per tenant, webhook payloads).
- You need to query *into* the structure, not just store and retrieve it.
- You want to avoid a separate EAV table with its own join overhead.

It is a poor fit when every row shares the same keys and you query those keys frequently — that is a relational schema waiting to happen.

---

### Migration: Column and Index

```php
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->jsonb('attributes')->default('{}');
    $table->timestamps();
});

// Add a GIN index so containment queries use an index scan
DB::statement(
    'CREATE INDEX products_attributes_gin ON products USING GIN (attributes)'
);

```

For queries that filter on a *specific key path* rather than containment, a functional B-tree index is cheaper:

```sql
CREATE INDEX products_attributes_color
    ON products ((attributes->>'color'));

```

Run `EXPLAIN (ANALYZE, BUFFERS)` after inserting representative data to confirm the planner picks your index.

---

### Querying with Eloquent

Laravel's `whereJsonContains` maps directly to the `@>` containment operator, which the GIN index covers:

```php
// Products where attributes contain {"color": "red"}
Product::whereJsonContains('attributes->color', 'red')->get();

// Multiple values — generates @> for each
Product::whereJsonContains('attributes->tags', ['sale', 'new'])->get();

```

For range or comparison queries on a JSON key, drop to a raw expression so PostgreSQL can use the functional index:

```php
Product::whereRaw("(attributes->>'price')::numeric > ?", [100])->get();

```

Avoid `->whereJsonLength()` on large datasets unless you have a matching expression index — it forces a sequential scan.

---

### Custom Eloquent Cast for Typed JSONB

Storing raw arrays is fine for prototypes, but a typed value object prevents silent key-name bugs and gives IDE autocompletion.

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

class ProductAttributesCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ProductAttributes
    {
        return ProductAttributes::fromArray(json_decode($value, true) ?? []);
    }

    public function set($model, string $key, $value, array $attributes): string
    {
        $data = $value instanceof ProductAttributes ? $value->toArray() : $value;
        return json_encode($data);
    }
}

```

```php
// app/ValueObjects/ProductAttributes.php
readonly class ProductAttributes
{
    public function __construct(
        public readonly string $color = '',
        public readonly float  $price = 0.0,
        public readonly array  $tags  = [],
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            color: $data['color'] ?? '',
            price: (float) ($data['price'] ?? 0),
            tags:  $data['tags']  ?? [],
        );
    }

    public function toArray(): array
    {
        return ['color' => $this->color, 'price' => $this->price, 'tags' => $this->tags];
    }
}

```

```php
// Product model
protected $casts = [
    'attributes' => ProductAttributesCast::class,
];

// Usage
$product->attributes->color; // typed, no magic strings

```

---

### Updating Nested Keys Without Overwriting the Column

PostgreSQL's `jsonb_set` lets you patch a single key atomically:

```php
DB::table('products')
    ->where('id', $product->id)
    ->update([
        'attributes' => DB::raw(
            "jsonb_set(attributes, '{price}', '149.99'::jsonb)"
        ),
    ]);

```

This avoids a read-modify-write cycle and prevents race conditions under concurrent updates.

---

### Key Takeaways

- Use a **GIN index** for containment (`@>`) queries; use a **functional B-tree index** for single-key comparisons.
- `whereJsonContains` is index-friendly; `whereRaw` with a cast is needed for numeric comparisons.
- Wrap JSONB columns in a **typed value object + custom cast** to eliminate magic strings and enable static analysis.
- `jsonb_set` for partial updates avoids read-modify-write races.
- Always verify index usage with `EXPLAIN (ANALYZE, BUFFERS)` on production-sized data before deploying.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-bloat&text=PostgreSQL+JSONB+in+Laravel%3A+Indexing%2C+Querying%2C+and+Casting+Without+the+Bloat) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-bloat) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does whereJsonContains in Laravel use a PostgreSQL GIN index?        Yes. whereJsonContains generates a containment query using the @&gt; operator, which a GIN index on the JSONB column covers. Always verify with EXPLAIN ANALYZE on realistic data volumes. 

      Q02  Should I use JSONB or a separate table for variable product attributes?        Use JSONB when attribute keys vary significantly per product and you need containment or existence queries. If every product shares the same attributes and you query them individually with ranges or joins, a relational table with proper indexes will outperform JSONB. 

      Q03  Can I use PHP 8.2+ readonly classes as Eloquent JSONB casts?        Yes. Implement CastsAttributes, deserialize into a readonly class in get(), and serialize back to a JSON string in set(). The model stores a plain string in the database while your application code works with a fully typed value object. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos](https://cdn.msaied.com/526/bc43aae3afe723f9a29f47820735edf5.png) laravel postgresql jsonb 

### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos

JSONB columns unlock flexible schemas, but without the right indexes and Eloquent integration they become a pe...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 9 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-chaos-2) [ ![Filament v4 Schema-Based Forms: Practical Patterns for the Unified Schema API](https://cdn.msaied.com/525/44fb6fe80b4b2439c1b1d9124976c67d.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms: Practical Patterns for the Unified Schema API

Filament v4 replaces scattered form/infolist definitions with a single Schema API. This post walks through rea...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 8 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-practical-patterns-for-the-unified-schema-api) [ ![Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/524/bffe5038d4150b93f86c783df9f73d28.png) laravel design-patterns architecture 

### Laravel Pipeline Pattern: Building Custom Pipelines Beyond Middleware

The Pipeline pattern in Laravel is far more powerful than middleware alone. Learn how to compose reusable, tes...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 8 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-pipeline-pattern-building-custom-pipelines-beyond-middleware-3) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
