PostgreSQL Window Functions in Laravel | 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 Window Functions in Laravel: Ranking, Running Totals, and Gap Detection        On this page       1. [  Why Window Functions Belong in Your Laravel Toolkit ](#why-window-functions-belong-in-your-laravel-toolkit)
2. [  ROW\_NUMBER for Per-Partition Ranking ](#row-number-for-per-partition-ranking)
3. [  Running Totals with SUM OVER ](#running-totals-with-sum-over)
4. [  Gap Detection with LAG ](#gap-detection-with-lag)
5. [  Wrapping Results in Eloquent Models ](#wrapping-results-in-eloquent-models)
6. [  Query Plan Sanity Check ](#query-plan-sanity-check)
7. [  Key Takeaways ](#key-takeaways)

  ![PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://cdn.msaied.com/546/f045f6411aa801b18d8a06d0518d540a.png)

  #laravel   #postgresql   #sql   #performance  

 PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection 
====================================================================================

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

       Table of contents

1. [  01   Why Window Functions Belong in Your Laravel Toolkit  ](#why-window-functions-belong-in-your-laravel-toolkit)
2. [  02   ROW\_NUMBER for Per-Partition Ranking  ](#row-number-for-per-partition-ranking)
3. [  03   Running Totals with SUM OVER  ](#running-totals-with-sum-over)
4. [  04   Gap Detection with LAG  ](#gap-detection-with-lag)
5. [  05   Wrapping Results in Eloquent Models  ](#wrapping-results-in-eloquent-models)
6. [  06   Query Plan Sanity Check  ](#query-plan-sanity-check)
7. [  07   Key Takeaways  ](#key-takeaways)

 Why Window Functions Belong in Your Laravel Toolkit
---------------------------------------------------

Window functions execute across a *set* of rows related to the current row without collapsing them into a single output row the way `GROUP BY` does. That distinction matters: you keep every row while still computing aggregates, ranks, or offsets across a logical partition. Doing the same work in PHP means loading thousands of rows into memory and iterating — a trade-off you should rarely accept.

PostgreSQL has supported window functions since version 8.4. Laravel's query builder does not have a dedicated API for them, but `selectRaw`, `DB::raw`, and subquery wrapping give you everything you need.

---

ROW\_NUMBER for Per-Partition Ranking
-------------------------------------

Imagine a `orders` table and you want the most recent order per customer without a correlated subquery.

```php
$ranked = DB::table('orders')
    ->selectRaw(
        'id, customer_id, total, created_at,
         ROW_NUMBER() OVER (
             PARTITION BY customer_id
             ORDER BY created_at DESC
         ) AS rn'
    );

$latest = DB::query()
    ->fromSub($ranked, 'ranked')
    ->where('rn', 1)
    ->get();

```

The inner query assigns a rank; the outer query filters to rank 1. PostgreSQL executes this as a single pass with a window sort — far cheaper than a `MAX` self-join on large tables.

---

Running Totals with SUM OVER
----------------------------

A running total is the canonical window function example, but it comes up constantly in financial dashboards and audit trails.

```php
$ledger = DB::table('transactions')
    ->where('account_id', $accountId)
    ->selectRaw(
        'id, amount, created_at,
         SUM(amount) OVER (
             PARTITION BY account_id
             ORDER BY created_at
             ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
         ) AS running_balance'
    )
    ->orderBy('created_at')
    ->get();

```

The `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` frame clause is explicit about what "running" means. Omitting it relies on the default frame, which changes when you add `ORDER BY` — being explicit prevents subtle bugs.

---

Gap Detection with LAG
----------------------

`LAG` and `LEAD` access the previous or next row's value without a self-join. This is useful for detecting gaps in sequential data — invoice numbers, ticket IDs, or scheduled slots.

```php
$gaps = DB::query()->fromSub(
    DB::table('invoices')
        ->selectRaw(
            'invoice_number,
             LAG(invoice_number) OVER (ORDER BY invoice_number) AS prev_number'
        ),
    'lagged'
)
->whereRaw('invoice_number  prev_number + 1')
->whereNotNull('prev_number')
->get();

```

Each row in `lagged` carries the previous invoice number. The outer filter surfaces any row where the sequence is broken. A PHP loop doing the same work would require the entire result set in memory first.

---

Wrapping Results in Eloquent Models
-----------------------------------

You can hydrate Eloquent models from raw window-function queries using `hydrate`:

```php
$rows = DB::select(
    'SELECT *, RANK() OVER (ORDER BY score DESC) AS rank
     FROM leaderboard_entries
     WHERE season_id = ?',
    [$seasonId]
);

$entries = LeaderboardEntry::hydrate($rows);
// $entries[0]->rank is accessible as a dynamic attribute

```

The extra columns (`rank` here) become accessible as dynamic properties. They will not be persisted if you call `save()`, but they are perfect for read-heavy display logic.

---

Query Plan Sanity Check
-----------------------

Always verify the plan when adding window functions to hot paths:

```sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, SUM(amount) OVER (PARTITION BY account_id ORDER BY created_at)
FROM transactions
WHERE account_id = 42;

```

Look for `WindowAgg` in the plan. If you see a sequential scan on a large table, a partial index on `(account_id, created_at)` will typically convert it to an `Index Scan` and eliminate the sort step entirely.

---

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

- Use `selectRaw` or `DB::raw` to embed window functions; no special query builder API is needed.
- Always specify the frame clause (`ROWS BETWEEN ...`) to avoid frame-default surprises.
- Wrap window queries in a subquery (`fromSub`) when you need to filter on the computed column.
- `LAG`/`LEAD` replace self-joins for sequential comparisons — cleaner SQL, better plans.
- Hydrate Eloquent models from raw results to keep presentation logic in the model layer.
- Verify execution plans and add partial covering indexes on partition + order columns for hot queries.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-1&text=PostgreSQL+Window+Functions+in+Laravel%3A+Ranking%2C+Running+Totals%2C+and+Gap+Detection) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection-1) 

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

  3 questions  

     Q01  Can I use window functions with Eloquent scopes?        Not directly inside a scope, but you can wrap an Eloquent query as a subquery using `DB::query()-&gt;fromSub(YourModel::query(), 'sub')-&gt;selectRaw(...)` and still benefit from scopes applied to the inner builder. 

      Q02  Do window functions work with Laravel's pagination?        Standard `paginate()` wraps your query in a COUNT subquery, which can conflict with window function aliases. Use `simplePaginate` or manual LIMIT/OFFSET on a subquery that already contains the window computation. 

      Q03  Will these queries work on MySQL too?        MySQL 8.0+ supports most window functions with the same syntax. However, frame clause support and optimizer behaviour differ. If you target both engines, test EXPLAIN output on each separately. 

  Continue reading

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

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

 [ ![Filament v3 Custom Field Plugins: Building Reusable Inputs with Full Form Integration](https://cdn.msaied.com/547/a61037a8f397f843359f1438d70c8bc5.png) filament laravel livewire 

### Filament v3 Custom Field Plugins: Building Reusable Inputs with Full Form Integration

Learn how to build a production-ready Filament v3 custom field plugin — covering the Field contract, state hyd...

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

 14 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v3-custom-field-plugins-building-reusable-inputs-with-full-form-integration) [ ![Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without the Ceremony](https://cdn.msaied.com/545/14148532753288225b142923e6704a4d.png) laravel event-sourcing ddd 

### Event Sourcing in Laravel: Aggregates, Projectors, and Reactors Without the Ceremony

Event sourcing sounds academic until you need a full audit trail or time-travel debugging in production. This...

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

 13 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/event-sourcing-in-laravel-aggregates-projectors-and-reactors-without-the-ceremony) [ ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/543/97ef3abac42d00989679f44916e2efd5.png) laravel database postgresql 

### Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

Learn how Laravel's database layer handles read/write splitting, when sticky reads save you from replication l...

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

 13 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel-6) 

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