aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Webontwikkeling

Laravel Pagination: offset- en cursorpaginatie uitgelegd

Duizenden records op één lijstscherm tonen verstikt zowel de browser als de database. Precies daar komt Laravel pagination om de hoek kijken: het splitst queryresultaten op in pagina's en haalt bij elk verzoek alleen het stuk op dat je nodig hebt. Laravel doet dit met drie verschillende methodes, en welke je kiest heeft directe invloed op zowel de prestaties als de gebruikerservaring. In dit artikel leg ik het verschil uit tussen offset-gebaseerde en cursor-gebaseerde paginering, wanneer je welke gebruikt, en hoe je ze implementeert met echte, werkende code.

Drie pagineringsmethodes: paginate, simplePaginate, cursorPaginate

Eloquent en de query builder bieden drie kant-en-klare methodes. Het verschil zit in hoeveel informatie ze de gebruiker tonen en hoeveel belasting ze op de database leggen:

  • paginate() — Klassieke offset-paginering. Het telt ook het totale aantal records, zodat je genummerde paginalinks als "1 2 3 … 50" en het totale aantal pagina's kunt tonen.
  • simplePaginate() — Nog steeds offset-gebaseerd, maar telt het totaal niet. Het produceert alleen "Vorige / Volgende"-knoppen; zonder de telquery is het sneller.
  • cursorPaginate() — Cursor-gebaseerde paginering. In plaats van een offset verwijst het naar de kolomwaarden van de laatste rij; op zeer grote tabellen is dit de snelste en meest consistente methode.

Hoe offset-paginering werkt

De offset-aanpak gebruikt LIMIT en OFFSET in SQL. Bij pagina's van 20 produceert Laravel LIMIT 20 OFFSET 80 wanneer je pagina 5 opvraagt. Het is de meest gangbare en eenvoudigste methode:

// Controller
public function index()
{
    $posts = \App\Models\Post::query()
        ->where('published', true)
        ->orderByDesc('created_at')
        ->paginate(20);

    return view('posts.index', compact('posts'));
}

Aan de Blade-kant render je de links in één regel. Je kunt de Tailwind- of Bootstrap-weergave aanpassen met php artisan vendor:publish --tag=laravel-pagination:

<div class="posts">
    @foreach ($posts as $post)
        <article>{{ $post->title }}</article>
    @endforeach
</div>

{{ $posts->links() }}

Het voordeel: gebruikers kunnen direct naar elke pagina springen en zien hoeveel pagina's er in totaal zijn. Het nadeel verschijnt naarmate je opschaalt.

De twee grote problemen met offset

Naarmate de database groeit, veroorzaakt offset twee kopzorgen:

  • Vertragende queries: wanneer je OFFSET 100000 zegt, moet de database de 100.000 rijen die het gaat overslaan tóch doorlopen. Diepe pagina's worden merkbaar trager.
  • Verschuivende records: als er een nieuw record wordt toegevoegd terwijl je op pagina 2 zit, schuift elke rij één plek naar beneden. Op de volgende pagina zie je een record dat je al zag, of sla je er een volledig over.

Omdat lijsten meestal van "nieuw naar oud" zijn gesorteerd en er voortdurend nieuwe content bijkomt, gebeurt deze verschuiving vaker dan je zou denken.

Cursor-paginering: de juiste keuze voor grote tabellen

Cursor-paginering gebruikt een referentiepunt in plaats van een offset. Het zegt "geef me de rijen ná deze" en gaat verder met een WHERE-clausule, dus er worden geen overgeslagen rijen doorlopen. Laravel versleutelt de cursorwaarde en plaatst die in de URL:

$posts = \App\Models\Post::query()
    ->where('published', true)
    ->orderByDesc('id')
    ->cursorPaginate(20);

De query die op de achtergrond wordt gegenereerd ziet er ruwweg zo uit; geen offset, alleen een geïndexeerde vergelijking:

select * from posts
where published = 1 and id < 480
order by id desc
limit 21;

Belangrijke regels:

  • Sorteren is verplicht en moet stabiel zijn: het orderBy-veld moet uniek zijn (bijvoorbeeld id). Sorteer je op een veld met dubbele waarden, voeg dan een tweede kolom toe als tiebreaker: orderBy('created_at')->orderBy('id').
  • Geen sprong naar een willekeurige pagina: een cursor beweegt alleen voor-/achteruit; je kunt geen "pagina 8"-link aanbieden. Het is ideaal voor infinite scroll en "Meer laden"-knoppen.
  • Geen totaalaantal: net als simplePaginate telt het niet, en daarom is het snel.

Wanneer welke gebruiken

De beslissing is eigenlijk simpel en wordt duidelijk met twee vragen: moet de gebruiker naar genummerde pagina's kunnen springen, en hoe groot is de tabel?

  • Beheerpanelen, zoekresultaten: paginanummers en totalen worden verwacht → paginate().
  • Mobiele streams, sociale feeds, "Meer laden": voorwaartse infinite scroll → cursorPaginate().
  • Eenvoudige lijsten, zeer grote tabel maar geen nummers nodig: simplePaginate() of cursor.

Vuistregel: stap bij tabellen die honderdduizenden rijen bereiken waar mogelijk over op cursor; maar als genummerde navigatie echt vereist is door het product, blijf dan bij offset en ondersteun je queries met indexen.

API- en prestatietips

Schrijf je een JSON-API, dan volstaat het om de paginator direct terug te geven; Laravel genereert automatisch de velden data, links en meta. Het werkt ook met API Resources:

return \App\Http\Resources\PostResource::collection(
    Post::where('published', true)->cursorPaginate(20)
);

Een paar praktische aanbevelingen:

  • Voeg een database-index toe op de kolommen waarop je sorteert en filtert; de snelheid van cursor hangt daarvan af.
  • Selecteer alleen de kolommen die je nodig hebt: select('id', 'title', 'created_at').
  • Laad gerelateerde data eager met with() zodat je niet in N+1-query's per pagina ontploft.
  • Komt de paginagrootte uit gebruikersinvoer, stel dan een bovengrens in (bijv. max 100).

Veelgestelde vragen

Kan ik het totale aantal pagina's tonen met cursor-paginering?

Nee. Omdat cursor (en simplePaginate) geen telquery uitvoert, kent het noch het totale aantal records noch het aantal pagina's. Heb je het totaal echt nodig, gebruik dan het offset-gebaseerde paginate(), of voer de telling uit als een aparte, gecachete query.

Kan ik de output van cursorPaginate omzetten naar genummerde paginalinks?

Nee. Een cursor kan van nature alleen naar aangrenzende pagina's (vorige/volgende) bewegen; springen naar een willekeurige pagina kan niet. Is genummerde navigatie verplicht, dan moet je bij offset-paginering blijven.

Waarom wordt offset trager op zeer grote tabellen?

Het commando OFFSET N vertelt de database de eerste N rijen te vinden en weg te gooien. Naarmate N groeit, groeit ook het aantal over te slaan rijen, dus neemt de werklast toe. Cursor begint daarentegen direct op het juiste punt met een geïndexeerde WHERE-clausule en blijft zo op constante snelheid.

Je paginering-architectuur goed opzetten is de basis van een applicatie die meeschaalt. Wil je praten over de overstap van offset naar cursor in je project, of het optimaliseren van trage lijstquery's, neem dan contact met me op.

Bu kategorideki tüm yazılar →

Devamı için