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

Laravel Pagination: Offset- und Cursor-Seitennavigation

Tausende Datensätze auf einem einzigen Listenbildschirm zu rendern, würgt sowohl den Browser als auch die Datenbank ab. Genau hier kommt Laravel Pagination ins Spiel: Sie teilt Abfrageergebnisse in Seiten auf und holt bei jeder Anfrage nur den Ausschnitt, den du brauchst. Laravel erledigt das mit drei verschiedenen Methoden, und welche du wählst, beeinflusst direkt sowohl die Performance als auch die Benutzererfahrung. In diesem Artikel erkläre ich den Unterschied zwischen offset-basierter und cursor-basierter Seitennavigation, wann du welche einsetzt und wie du sie mit echtem, funktionierendem Code umsetzt.

Drei Pagination-Methoden: paginate, simplePaginate, cursorPaginate

Eloquent und der Query Builder liefern drei fertige Methoden mit. Der Unterschied liegt darin, wie viele Informationen sie dem Nutzer zeigen und wie viel Last sie auf die Datenbank legen:

  • paginate() — Klassische Offset-Pagination. Sie zählt auch die Gesamtzahl der Datensätze, sodass du nummerierte Seitenlinks wie „1 2 3 … 50“ und die Gesamtseitenzahl anzeigen kannst.
  • simplePaginate() — Ebenfalls offset-basiert, zählt aber das Gesamtergebnis nicht. Sie erzeugt nur „Zurück / Weiter“-Buttons; ohne die Zählabfrage ist sie schneller.
  • cursorPaginate() — Cursor-basierte Seitennavigation. Statt eines Offsets bezieht sie sich auf die Spaltenwerte der letzten Zeile; bei sehr großen Tabellen ist das die schnellste und konsistenteste Methode.

So funktioniert Offset-Pagination

Der Offset-Ansatz nutzt in SQL LIMIT und OFFSET. Bei Seiten zu 20 erzeugt Laravel beim Aufruf von Seite 5 LIMIT 20 OFFSET 80. Es ist die gängigste und einfachste Methode:

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

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

Auf der Blade-Seite renderst du die Links in einer einzigen Zeile. Die Tailwind- oder Bootstrap-Ansicht kannst du mit php artisan vendor:publish --tag=laravel-pagination anpassen:

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

{{ $posts->links() }}

Der Vorteil: Nutzer können direkt zu jeder Seite springen und sehen, wie viele Seiten es insgesamt gibt. Der Nachteil zeigt sich beim Skalieren.

Die zwei großen Probleme von Offset

Mit wachsender Datenbank verursacht Offset zwei Kopfschmerzen:

  • Langsamer werdende Abfragen: Wenn du OFFSET 100000 angibst, muss die Datenbank die 100.000 Zeilen, die sie überspringen will, trotzdem durchgehen. Tiefe Seiten werden merklich langsamer.
  • Verschiebende Datensätze: Wird ein neuer Datensatz eingefügt, während du auf Seite 2 bist, rutscht jede Zeile um eins nach unten. Auf der nächsten Seite siehst du einen bereits gesehenen Datensatz erneut oder überspringst einen komplett.

Da Listen meist „von neu nach alt“ sortiert sind und ständig neue Inhalte hinzukommen, tritt dieses Verschiebungsproblem häufiger auf, als man denkt.

Cursor-Pagination: die richtige Wahl für große Tabellen

Cursor-Pagination nutzt einen Referenzpunkt statt eines Offsets. Sie sagt „gib mir die Zeilen nach dieser“ und fährt mit einer WHERE-Klausel fort, sodass keine übersprungenen Zeilen durchlaufen werden. Laravel verschlüsselt den Cursorwert und legt ihn in die URL:

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

Die im Hintergrund erzeugte Abfrage sieht ungefähr so aus; kein Offset, nur ein indizierter Vergleich:

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

Wichtige Regeln:

  • Sortierung ist Pflicht und muss stabil sein: Das orderBy-Feld muss eindeutig sein (zum Beispiel id). Sortierst du nach einem Feld mit doppelten Werten, füge eine zweite Spalte als Tiebreaker hinzu: orderBy('created_at')->orderBy('id').
  • Kein Sprung zu einer beliebigen Seite: Ein Cursor bewegt sich nur vor-/rückwärts; einen „Seite 8“-Link kannst du nicht anbieten. Ideal für Infinite Scroll und „Mehr laden“-Buttons.
  • Keine Gesamtzahl: Wie simplePaginate zählt er nicht, und genau deshalb ist er schnell.

Wann welche Methode?

Die Entscheidung ist eigentlich einfach und klärt sich mit zwei Fragen: Muss der Nutzer zu nummerierten Seiten springen können, und wie groß ist die Tabelle?

  • Admin-Panels, Suchergebnisse: Seitenzahlen und Gesamtwerte werden erwartet → paginate().
  • Mobile Streams, Social Feeds, „Mehr laden“: vorwärts gerichtetes Infinite Scroll → cursorPaginate().
  • Einfache Listen, sehr große Tabelle, aber keine Nummern nötig: simplePaginate() oder Cursor.

Faustregel: Bei Tabellen, die Hunderttausende Zeilen erreichen, wechsle nach Möglichkeit zu Cursor; ist die nummerierte Navigation jedoch wirklich vom Produkt gefordert, bleib bei Offset und stütze deine Abfragen mit Indizes.

API- und Performance-Tipps

Schreibst du eine JSON-API, reicht es, den Paginator direkt zurückzugeben; Laravel erzeugt die Felder data, links und meta automatisch. Es funktioniert auch mit API Resources:

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

Ein paar praktische Empfehlungen:

  • Lege einen Datenbank-Index auf die Spalten an, nach denen du sortierst und filterst; die Geschwindigkeit von Cursor hängt davon ab.
  • Wähle nur die Spalten aus, die du brauchst: select('id', 'title', 'created_at').
  • Lade verknüpfte Daten per Eager Loading mit with(), damit du nicht in N+1-Abfragen pro Seite explodierst.
  • Kommt die Seitengröße aus Nutzereingaben, setze eine Obergrenze (z. B. max. 100).

Häufige Fragen

Kann ich die Gesamtseitenzahl mit Cursor-Pagination anzeigen?

Nein. Da Cursor (und simplePaginate) keine Zählabfrage ausführt, kennt er weder die Gesamtzahl der Datensätze noch die der Seiten. Brauchst du den Gesamtwert unbedingt, nutze das offset-basierte paginate() oder führe die Zählung als separate, gecachte Abfrage aus.

Kann ich die Ausgabe von cursorPaginate in nummerierte Seitenlinks umwandeln?

Nein. Ein Cursor kann sich naturgemäß nur zu benachbarten Seiten (zurück/weiter) bewegen; ein Sprung zu einer beliebigen Seite ist nicht möglich. Ist nummerierte Navigation Pflicht, musst du bei der Offset-Pagination bleiben.

Warum wird Offset bei sehr großen Tabellen langsam?

Der Befehl OFFSET N weist die Datenbank an, die ersten N Zeilen zu finden und wegzuwerfen. Je größer N, desto mehr Zeilen müssen übersprungen werden, also steigt die Arbeitslast. Cursor hingegen startet mit einer indizierten WHERE-Klausel direkt an der richtigen Stelle und bleibt so bei konstanter Geschwindigkeit.

Deine Pagination-Architektur richtig aufzusetzen ist die Grundlage einer Anwendung, die mitskaliert. Wenn du über die Umstellung von Offset auf Cursor in deinem Projekt oder die Optimierung langsamer Listenabfragen sprechen möchtest, nimm Kontakt mit mir auf.

Bu kategorideki tüm yazılar →

Devamı için