PostgreSQL JSONB in Laravel: Indexes and Queries | 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 Overhead        On this page       1. [  PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Overhead ](#postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-overhead)
2. [  When JSONB Is the Right Tool ](#when-jsonb-is-the-right-tool)
3. [  GIN Indexes: The Only Index That Matters for Containment ](#gin-indexes-the-only-index-that-matters-for-containment)
4. [  Querying JSONB Through Eloquent ](#querying-jsonb-through-eloquent)
5. [  Typed Casts: Keep Arrays Out of Your Domain ](#typed-casts-keep-arrays-out-of-your-domain)
6. [  Avoiding the -&gt; Operator in SELECT at Scale ](#avoiding-the-code-gtcode-operator-in-select-at-scale)
7. [  Key Takeaways ](#key-takeaways)

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

  #laravel   #postgresql   #jsonb   #eloquent   #performance  

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

     28 Jul 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 Overhead  ](#postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-overhead)
2. [  02   When JSONB Is the Right Tool  ](#when-jsonb-is-the-right-tool)
3. [  03   GIN Indexes: The Only Index That Matters for Containment  ](#gin-indexes-the-only-index-that-matters-for-containment)
4. [  04   Querying JSONB Through Eloquent  ](#querying-jsonb-through-eloquent)
5. [  05   Typed Casts: Keep Arrays Out of Your Domain  ](#typed-casts-keep-arrays-out-of-your-domain)
6. [  06   Avoiding the -&gt; Operator in SELECT at Scale  ](#avoiding-the-code-gtcode-operator-in-select-at-scale)
7. [  07   Key Takeaways  ](#key-takeaways)

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

JSONB is one of PostgreSQL's most powerful features, but it is also one of the most abused. Teams reach for it to avoid migrations, end up with unindexed blobs, and wonder why their dashboards time out. This article covers the practical mechanics: when to use JSONB, how to index it correctly, how to query it through Eloquent without raw SQL sprawl, and how to wrap it in a typed cast so your domain layer never sees an array.

---

### When JSONB Is the Right Tool

JSONB earns its place when the shape of data genuinely varies per row — feature flags per tenant, metadata from third-party webhooks, or user-defined attributes on a product. It is **not** a substitute for a proper relational schema when the keys are known and stable.

Rule of thumb: if you find yourself writing `WHERE data->>'status' = 'active'` on every query, that column belongs in its own typed column with a B-tree index.

---

### GIN Indexes: The Only Index That Matters for Containment

A plain B-tree index on a JSONB column is useless for containment queries (`@>`). You need a GIN index.

```sql
CREATE INDEX idx_products_attributes_gin
    ON products USING GIN (attributes);

```

In a Laravel migration:

```php
$table->jsonb('attributes')->nullable();
DB::statement(
    'CREATE INDEX idx_products_attributes_gin ON products USING GIN (attributes)'
);

```

For queries on a **specific key path**, a functional GIN index is cheaper:

```sql
CREATE INDEX idx_products_attributes_tags
    ON products USING GIN ((attributes->'tags'));

```

This index is smaller and faster when you only ever query `attributes->'tags'`.

---

### Querying JSONB Through Eloquent

Laravel's query builder exposes `whereJsonContains`, `whereJsonLength`, and raw path expressions. Use them instead of raw SQL strings scattered through your codebase.

```php
// Containment: find products tagged 'wireless'
Product::whereJsonContains('attributes->tags', 'wireless')->get();

// Path equality
Product::where('attributes->color', 'red')->get();

// Nested path
Product::where('attributes->dimensions->unit', 'cm')->get();

// Array length guard
Product::whereJsonLength('attributes->tags', '>', 2)->get();

```

`whereJsonContains` compiles to the `@>` containment operator, which **will** use your GIN index. The `->` path operator compiles to `->>` (text cast) for equality, which can use a functional B-tree index if you create one:

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

```

---

### Typed Casts: Keep Arrays Out of Your Domain

Returning raw PHP arrays from a JSONB column leaks implementation details everywhere. A custom Eloquent cast converts the column to a value object on read and back to JSON on write.

```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
    {
        $data = json_decode($value ?? '{}', true);
        return new ProductAttributes(
            tags: $data['tags'] ?? [],
            color: $data['color'] ?? null,
            dimensions: isset($data['dimensions'])
                ? Dimensions::from($data['dimensions'])
                : null,
        );
    }

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

```

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

```

Now `$product->attributes` is always a `ProductAttributes` object. PHPStan can analyse it, your IDE autocompletes it, and tests can assert on typed properties instead of array keys.

---

### Avoiding the `->` Operator in SELECT at Scale

Extract frequently-read JSONB keys into generated columns so PostgreSQL materialises them:

```sql
ALTER TABLE products
    ADD COLUMN color TEXT
    GENERATED ALWAYS AS (attributes->>'color') STORED;

CREATE INDEX idx_products_color_gen ON products (color);

```

Laravel sees `color` as a normal column. Queries are faster, and you keep the flexibility of JSONB for the rest of the payload.

---

### Key Takeaways

- Use a **GIN index** for containment (`@>`) queries; a functional B-tree for single-key equality.
- `whereJsonContains` in Eloquent maps to `@>` and will hit your GIN index.
- Wrap JSONB columns in a **custom cast** to expose typed value objects, not raw arrays.
- For hot query paths on a single key, add a **generated stored column** and index that instead.
- JSONB is not a schema escape hatch — use it only where the document shape genuinely varies.

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

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

  3 questions  

     Q01  Does `whereJsonContains` in Laravel actually use a GIN index?        Yes, provided you have a GIN index on the JSONB column or the specific key path. `whereJsonContains` compiles to the `@&gt;` containment operator, which PostgreSQL's GIN index is designed to accelerate. Verify with EXPLAIN ANALYZE to confirm index usage. 

      Q02  Should I store everything in JSONB to avoid migrations?        No. JSONB is appropriate when the document shape varies per row. Stable, well-known attributes belong in typed columns with proper indexes. Overusing JSONB trades short-term migration convenience for long-term query complexity and performance problems. 

      Q03  Can I use a custom Eloquent cast with JSONB and still run whereJsonContains queries?        Yes. The cast only affects PHP-side hydration and serialisation. The database still stores raw JSONB, so all PostgreSQL operators and Laravel's query builder methods work normally regardless of what cast you apply to the model. 

  Continue reading

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

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

 [ ![MySQL Query Profiling in Laravel: EXPLAIN ANALYZE, Slow Query Log, and Index Hints](https://cdn.msaied.com/481/8bfc417cb94b3e2868428e065fe4b166.png) laravel mysql performance 

### MySQL Query Profiling in Laravel: EXPLAIN ANALYZE, Slow Query Log, and Index Hints

Stop guessing why a query is slow. Learn how to use EXPLAIN ANALYZE, Laravel's slow query log listener, and in...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/mysql-query-profiling-in-laravel-explain-analyze-slow-query-log-and-index-hints) [ ![Laravel Custom Eloquent Relations: Building Polymorphic and Composite-Key Relations](https://cdn.msaied.com/480/c7d2c0d0fd1823460fbdb138f77b573f.png) laravel eloquent database 

### Laravel Custom Eloquent Relations: Building Polymorphic and Composite-Key Relations

Eloquent's built-in relations cover 90% of cases, but composite-key joins and non-standard polymorphic pivots...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-custom-eloquent-relations-building-polymorphic-and-composite-key-relations) [ ![Laravel 12: New Features, Helpers, and Practical Upgrade Notes](https://cdn.msaied.com/478/117fb4792b77da2d5ae106048c5e70a7.png) laravel laravel-12 upgrade 

### Laravel 12: New Features, Helpers, and Practical Upgrade Notes

Laravel 12 ships with streamlined application scaffolding, first-party starter kits built on React/Vue/Livewir...

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

 28 Jul 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-12-new-features-helpers-and-practical-upgrade-notes-1) 

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