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 Chaos        On this page       1. [  Why JSONB Deserves More Than a json Column ](#why-jsonb-deserves-more-than-a-codejsoncode-column)
2. [  Choosing the Right GIN Index ](#choosing-the-right-gin-index)
3. [  Querying JSONB in Eloquent ](#querying-jsonb-in-eloquent)
4. [  Containment — hits the GIN index ](#containment-hits-the-gin-index)
5. [  Path extraction — hits the functional B-tree index ](#path-extraction-hits-the-functional-b-tree-index)
6. [  Encapsulate in a scope to avoid raw SQL leaking everywhere ](#encapsulate-in-a-scope-to-avoid-raw-sql-leaking-everywhere)
7. [  Type-Safe JSONB with a Custom Eloquent Cast ](#type-safe-jsonb-with-a-custom-eloquent-cast)
8. [  Avoiding the Silent Performance Trap ](#avoiding-the-silent-performance-trap)
9. [  Key Takeaways ](#key-takeaways)

  ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos](https://cdn.msaied.com/169/5efebaf19646869da7c6064340c7d09f.png)

  #laravel   #postgresql   #eloquent   #performance  

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

     14 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

  9 sections  

1. [  01   Why JSONB Deserves More Than a json Column  ](#why-jsonb-deserves-more-than-a-codejsoncode-column)
2. [  02   Choosing the Right GIN Index  ](#choosing-the-right-gin-index)
3. [  03   Querying JSONB in Eloquent  ](#querying-jsonb-in-eloquent)
4. [  04   Containment — hits the GIN index  ](#containment-hits-the-gin-index)
5. [  05   Path extraction — hits the functional B-tree index  ](#path-extraction-hits-the-functional-b-tree-index)
6. [  06   Encapsulate in a scope to avoid raw SQL leaking everywhere  ](#encapsulate-in-a-scope-to-avoid-raw-sql-leaking-everywhere)
7. [  07   Type-Safe JSONB with a Custom Eloquent Cast  ](#type-safe-jsonb-with-a-custom-eloquent-cast)
8. [  08   Avoiding the Silent Performance Trap  ](#avoiding-the-silent-performance-trap)
9. [  09   Key Takeaways  ](#key-takeaways)

       Why JSONB Deserves More Than a `json` Column
--------------------------------------------

PostgreSQL's `jsonb` type stores JSON in a decomposed binary format, enabling indexing and operator-based querying that the plain `json` type simply cannot match. Laravel's `json` cast gets you to the finish line for simple storage, but the moment you need to filter or sort on nested keys at scale, you need to think about what PostgreSQL is actually doing under the hood.

---

Choosing the Right GIN Index
----------------------------

A generic GIN index on a `jsonb` column supports containment (`@>`) and existence (`?`) operators:

```sql
CREATE INDEX idx_orders_meta ON orders USING GIN (meta);

```

For deep path queries (`meta->'shipping'->>'country'`), a functional B-tree index is often faster:

```sql
CREATE INDEX idx_orders_shipping_country
  ON orders ((meta->'shipping'->>'country'));

```

In a Laravel migration:

```php
Schema::table('orders', function (Blueprint $table) {
    // GIN for containment queries
    $table->rawIndex('meta', 'idx_orders_meta_gin', 'gin');

    // Functional B-tree for a known path
    DB::statement(
        "CREATE INDEX idx_orders_shipping_country "
        . "ON orders ((meta->'shipping'->>'country'))"
    );
});

```

> **Rule of thumb:** GIN when you query arbitrary keys; functional B-tree when you always query the same path.

---

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

Laravel's query builder exposes `whereJsonContains`, `whereJsonLength`, and raw expressions. Use them deliberately.

### Containment — hits the GIN index

```php
// Find orders where meta contains a specific shipping country
Order::whereJsonContains('meta->shipping->country', 'DE')->get();

```

### Path extraction — hits the functional B-tree index

```php
Order::whereRaw("meta->'shipping'->>'country' = ?", ['DE'])->get();

```

### Encapsulate in a scope to avoid raw SQL leaking everywhere

```php
// app/Models/Order.php
public function scopeShippingCountry(Builder $query, string $country): Builder
{
    return $query->whereRaw(
        "meta->'shipping'->>'country' = ?",
        [$country]
    );
}

// Usage
Order::shippingCountry('DE')->paginate();

```

---

Type-Safe JSONB with a Custom Eloquent Cast
-------------------------------------------

A plain `array` cast gives you an untyped array. A custom cast backed by a DTO gives you autocomplete, validation, and a clear contract.

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

class ShippingMetaCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes): ShippingMeta
    {
        $data = json_decode($value ?? '{}', true);
        return new ShippingMeta(
            country: $data['country'] ?? '',
            postalCode: $data['postal_code'] ?? '',
            carrier: $data['carrier'] ?? null,
        );
    }

    public function set($model, string $key, $value, array $attributes): string
    {
        if ($value instanceof ShippingMeta) {
            return json_encode([
                'country'     => $value->country,
                'postal_code' => $value->postalCode,
                'carrier'     => $value->carrier,
            ]);
        }
        return json_encode($value);
    }
}

```

```php
// app/Data/ShippingMeta.php
readonly class ShippingMeta
{
    public function __construct(
        public string $country,
        public string $postalCode,
        public ?string $carrier,
    ) {}
}

```

```php
// app/Models/Order.php
protected $casts = [
    'meta' => ShippingMetaCast::class,
];

// Now fully typed:
$order->meta->country; // string

```

---

Avoiding the Silent Performance Trap
------------------------------------

The most common mistake: storing deeply nested, frequently queried data in JSONB and then filtering with `LIKE` on a cast text value. PostgreSQL cannot use any index for that.

Run `EXPLAIN (ANALYZE, BUFFERS)` on any JSONB query before shipping:

```sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE meta->'shipping'->>'country' = 'DE';

```

Look for `Bitmap Index Scan` or `Index Scan` on your functional index. If you see `Seq Scan`, your index is missing or the planner is ignoring it — check column statistics with `ANALYZE orders`.

---

Key Takeaways
-------------

- Use **GIN** for containment/existence queries; use **functional B-tree** for fixed-path equality filters.
- Wrap raw JSONB expressions in **Eloquent scopes** to keep models readable and testable.
- Replace the generic `array` cast with a **typed custom cast** backed by a readonly DTO.
- Always verify index usage with `EXPLAIN (ANALYZE, BUFFERS)` — never assume the planner will do what you expect.
- `ANALYZE` your table after bulk inserts so the planner has fresh statistics for JSONB columns.

 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-chaos&text=PostgreSQL+JSONB+in+Laravel%3A+Indexing%2C+Querying%2C+and+Casting+Without+the+Chaos) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-chaos) 

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

  3 questions  

     Q01  When should I use JSONB instead of dedicated columns in PostgreSQL?        Use JSONB for genuinely variable or schema-less attributes — feature flags, third-party webhook payloads, or per-tenant configuration. If you query the same field in every WHERE clause, a dedicated typed column with a standard B-tree index will almost always outperform JSONB. 

      Q02  Does `whereJsonContains` in Laravel use the GIN index automatically?        Yes, if a GIN index exists on the column. `whereJsonContains` compiles to the `@&gt;` containment operator, which PostgreSQL's GIN index supports natively. Verify with EXPLAIN that the planner chooses an index scan rather than a sequential scan. 

      Q03  Can I use a custom JSONB cast alongside Filament form fields?        Yes. Filament reads and writes through Eloquent, so your custom cast is transparent. You will need to map the DTO's properties to individual form fields using `afterStateHydrated` and `dehydrateStateUsing` callbacks, or flatten the DTO to an array before passing it to the form schema. 

  Continue reading

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

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

 [ ![Eloquent Custom Casts: Encapsulating Value Objects Without the Bloat](https://cdn.msaied.com/174/2c75896ee4182bb2f66e2c93bed18796.png) laravel eloquent ddd 

### Eloquent Custom Casts: Encapsulating Value Objects Without the Bloat

Custom Eloquent casts let you bind rich value objects directly to model attributes. This article shows how to...

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

 14 Jun 2026     1 min read  

  Read    

 ](https://msaied.com/articles/eloquent-custom-casts-encapsulating-value-objects-without-the-bloat) [ ![Filament v4 Schema-Based Forms: Unified Schema API in Practice](https://cdn.msaied.com/173/6e0d9faa9137cb296e37831c3645e7ba.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms: Unified Schema API in Practice

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 

 14 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-unified-schema-api-in-practice) [ ![The Pipeline Pattern in Laravel: Building Custom Pipelines Beyond Middleware](https://cdn.msaied.com/172/a5dea5e84a6eab22d5d7a76869aaecb4.png) laravel design-patterns pipeline 

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

Laravel's Pipeline class is far more than the engine behind HTTP middleware. Learn how to compose reusable, te...

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

 14 Jun 2026     3 min read  

  Read    

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

   [  ![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)
