Partial &amp; Covering Indexes in Laravel + PostgreSQL | 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 Partial and Covering Indexes in Laravel Migrations and Query Plans        On this page       1. [  Why Generic Indexes Leave Performance on the Table ](#why-generic-indexes-leave-performance-on-the-table)
2. [  Partial Indexes: Index Only What You Query ](#partial-indexes-index-only-what-you-query)
3. [  Covering Indexes: Eliminate Heap Fetches ](#covering-indexes-eliminate-heap-fetches)
4. [  Reading EXPLAIN ANALYZE Output ](#reading-explain-analyze-output)
5. [  Combining Both: Partial Covering Index ](#combining-both-partial-covering-index)
6. [  Practical Takeaways ](#practical-takeaways)

  ![PostgreSQL Partial and Covering Indexes in Laravel Migrations and Query Plans](https://cdn.msaied.com/373/a6b1671af111801768bc65898128a6e4.png)

  #postgresql   #laravel   #performance   #database  

 PostgreSQL Partial and Covering Indexes in Laravel Migrations and Query Plans 
===============================================================================

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

       Table of contents

1. [  01   Why Generic Indexes Leave Performance on the Table  ](#why-generic-indexes-leave-performance-on-the-table)
2. [  02   Partial Indexes: Index Only What You Query  ](#partial-indexes-index-only-what-you-query)
3. [  03   Covering Indexes: Eliminate Heap Fetches  ](#covering-indexes-eliminate-heap-fetches)
4. [  04   Reading EXPLAIN ANALYZE Output  ](#reading-explain-analyze-output)
5. [  05   Combining Both: Partial Covering Index  ](#combining-both-partial-covering-index)
6. [  06   Practical Takeaways  ](#practical-takeaways)

 Why Generic Indexes Leave Performance on the Table
--------------------------------------------------

Most Laravel applications add indexes reactively — a slow query appears, someone runs `Schema::table('orders', fn($t) => $t->index('status'))`, and the problem is patched. That works until the table grows and the planner decides a sequential scan is cheaper because the index covers too many rows.

Two PostgreSQL features fix this at the schema level: **partial indexes** (index only the rows you actually query) and **covering indexes** (embed payload columns so the planner never touches the heap). Neither requires a custom database driver — just raw index definitions in your migrations.

---

Partial Indexes: Index Only What You Query
------------------------------------------

A partial index carries a `WHERE` clause. If your application only ever queries `orders` where `status = 'pending'`, index only those rows.

```php
// database/migrations/2024_06_01_000001_add_partial_index_to_orders.php
public function up(): void
{
    DB::statement(
        "CREATE INDEX idx_orders_pending_created
         ON orders (created_at DESC)
         WHERE status = 'pending'"
    );
}

public function down(): void
{
    DB::statement('DROP INDEX IF EXISTS idx_orders_pending_created');
}

```

The index is tiny — it only contains rows where `status = 'pending'`. The planner will use it when your query includes the same predicate:

```php
// Planner sees the WHERE clause matches the partial index condition
Order::where('status', 'pending')
     ->orderByDesc('created_at')
     ->limit(50)
     ->get();

```

If you omit `->where('status', 'pending')`, PostgreSQL will not use this index. That is intentional — the index is meaningless for other statuses.

---

Covering Indexes: Eliminate Heap Fetches
----------------------------------------

A covering index uses `INCLUDE` to attach non-key columns. The planner can satisfy the entire query from the index leaf pages without a heap fetch (an **Index Only Scan**).

```php
DB::statement(
    "CREATE INDEX idx_users_email_covering
     ON users (email)
     INCLUDE (id, name, created_at)"
);

```

Now this query never touches the `users` heap:

```php
User::select('id', 'name', 'created_at')
    ->where('email', $email)
    ->first();

```

The `INCLUDE` columns are not part of the B-tree key, so they add no ordering overhead and do not bloat the index structure as aggressively as adding them to the key would.

---

Reading EXPLAIN ANALYZE Output
------------------------------

Always verify with `EXPLAIN (ANALYZE, BUFFERS)`:

```php
$sql = User::select('id', 'name', 'created_at')
    ->where('email', 'alice@example.com')
    ->toRawSql();

$plan = DB::select("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) {$sql}");
foreach ($plan as $row) {
    echo $row->{'QUERY PLAN'} . "\n";
}

```

Key lines to look for:

- **Index Only Scan** — the covering index is working; heap fetches are zero.
- **Index Scan** — the index is used but heap rows are still fetched (add `INCLUDE` columns).
- **Bitmap Heap Scan** — common for range queries; acceptable but check `Heap Blocks: exact=0` for visibility map wins.
- **Seq Scan** — the planner chose a full scan; check row estimates and `work_mem`.

```yaml
Index Only Scan using idx_users_email_covering on users
  (cost=0.43..8.45 rows=1 width=52)
  (actual time=0.021..0.022 rows=1 loops=1)
  Index Cond: (email = 'alice@example.com')
  Heap Fetches: 0

```

`Heap Fetches: 0` confirms the visibility map is current and no heap I/O occurred.

---

Combining Both: Partial Covering Index
--------------------------------------

You can combine both techniques:

```php
DB::statement(
    "CREATE INDEX idx_invoices_unpaid_covering
     ON invoices (due_date ASC)
     INCLUDE (id, customer_id, total_cents)
     WHERE paid_at IS NULL"
);

```

This index is small (only unpaid invoices), ordered for due-date queries, and carries the columns needed for a dashboard list — all without touching the heap.

---

Practical Takeaways
-------------------

- Use partial indexes when a query always filters on a low-cardinality column with a fixed value (`status`, `type`, boolean flags).
- Use `INCLUDE` to turn an Index Scan into an Index Only Scan for read-heavy list queries.
- Combine both for high-traffic, narrow-predicate queries like dashboards or queue polling.
- Always confirm with `EXPLAIN (ANALYZE, BUFFERS)` — the planner can surprise you.
- Run `VACUUM ANALYZE` after bulk loads; stale statistics cause the planner to ignore valid indexes.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-partial-and-covering-indexes-in-laravel-migrations-and-query-plans&text=PostgreSQL+Partial+and+Covering+Indexes+in+Laravel+Migrations+and+Query+Plans) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-partial-and-covering-indexes-in-laravel-migrations-and-query-plans) 

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

  3 questions  

     Q01  Will Laravel's Schema builder create partial or covering indexes natively?        Not yet. The Blueprint API has no `partial()` or `include()` methods. You must use `DB::statement()` with raw DDL inside your migration's `up()` and `down()` methods, which is perfectly safe and version-controlled. 

      Q02  When should I prefer a partial index over a composite index?        Use a partial index when one column in your composite key is always a fixed value in queries (e.g., `status = 'pending'`). Moving that fixed predicate into the index `WHERE` clause shrinks the index significantly and can make the difference between an index scan and a sequential scan on large tables. 

      Q03  Does INCLUDE affect write performance?        Yes, but less than adding columns to the index key. INCLUDE columns are stored only in leaf pages, not in internal B-tree nodes, so the structural overhead is lower. For write-heavy tables, benchmark with realistic insert/update loads before committing. 

  Continue reading

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

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

 [ ![Advanced Authorization in Laravel: Gates, Policies, and Response-Based Access Control](https://cdn.msaied.com/571/e2c97418f4d543aac16e77c5dfd1055a.png) laravel authorization security 

### Advanced Authorization in Laravel: Gates, Policies, and Response-Based Access Control

Go beyond simple boolean gates. Learn how Laravel's response-based authorization lets you return rich denial r...

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

 20 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/advanced-authorization-in-laravel-gates-policies-and-response-based-access-control-4) [ ![Queue::forward(): Reroute Laravel Queues in One Place](https://cdn.msaied.com/572/c3ded57b390d88d1ceb9bd8570729835.png) Laravel Queues Laravel 13.26 

### Queue::forward(): Reroute Laravel Queues in One Place

Laravel 13.26 introduces Queue::forward(), letting you redirect any queue to a different name, connection, or...

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

 19 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/queueforward-reroute-laravel-queues-in-one-place) [ ![Laravel Tackle: Run an AI Coding Agent Inside Your Laravel Application](https://cdn.msaied.com/573/8f6b08a47a4bf04ae26b9f03d8e2e697.png) Laravel AI Artisan 

### Laravel Tackle: Run an AI Coding Agent Inside Your Laravel Application

Laravel Tackle brings an AI coding agent directly into your app as Artisan commands. It can read routes, query...

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

 19 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-tackle-run-an-ai-coding-agent-inside-your-laravel-application) 

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