Bootcamp i FastAPI-backendudvikling · Lektion

Cursor- kontra offset-pagination i stor skala

Implementér keyset-/cursor-pagination til stabil og effektiv visning af store datasæt i hyppig forandring.

Lektion 2 af 413 trin

Cursor- kontra offset-pagination i stor skala er en gratis Bootcamp i FastAPI-backendudvikling-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Bootcamp i FastAPI-backendudvikling, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.

Hvorfor pagineringsstrategien er vigtig

Når en liste-endpoint returnerer tusinder eller millioner af rækker, skal du inddele resultaterne i sider. De to dominerende strategier er offset-paginering (LIMIT/OFFSET eller ?page=3) og cursor-/keyset-paginering (?after=<token>).

  • Offset er enkel og understøtter spring til vilkårlige sider.
  • Cursor er stabil og hurtig på store datasæt, der ændres ofte.

Denne lektion viser, hvorfor offset-løsningen ikke skalerer, og hvordan du implementerer keyset-paginering korrekt i en FastAPI-tjeneste.

Sådan fungerer offset-paginering

Offset-paginering beder databasen om at springe N rækker over og returnere den næste del med sidens størrelse. En forespørgsel efter side 3 med sidestørrelsen 20 bliver til OFFSET 40 LIMIT 20.

Endpointen er meget enkel at skrive og lader klienter springe direkte til en vilkårlig side. Her er en minimal simulering i hukommelsen af, hvordan offset-udsnit fungerer.

def offset_page(rows, page, size):
    start = (page - 1) * size
    end = start + size
    return rows[start:end]

items = [f"item-{i}" for i in range(1, 101)]
print("page 1:", offset_page(items, 1, 5))
print("page 3:", offset_page(items, 3, 5))
print("page 20:", offset_page(items, 20, 5))

De skjulte omkostninger ved OFFSET

Databaser springer ikke på magisk vis til række 1.000.000. For at opfylde OFFSET 1000000 LIMIT 20 skal motoren gennemgå og kassere den første million rækker, før den returnerer 20. Jo dybere siden ligger, jo langsommere bliver forespørgslen.

  • Side 1 er øjeblikkelig; side 50.000 kan tage flere sekunder.
  • Arbejdet vokser lineært med offset-værdien (O(offset)).
  • Indekser hjælper med sorteringen, men kan ikke springe de kasserede rækker over gratis.

Derfor er dyb offset-paginering en almindelig årsag til langsomme liste-endpoints og pludselige belastningsspidser på databasen.

Problemet med forskydning

Offset-sider beregnes ud fra et mål i bevægelse. Hvis rækker indsættes eller slettes mellem forespørgsler, forskydes offset-værdierne under klienten.

  • En ny række indsættes øverst, mens brugeren læser side 1.
  • På side 2 vises det sidste element fra side 1 igen (duplikat).
  • Eller en sletning får et element til at blive sprunget helt over.

På travle feeds og dashboards giver det manglende og gentagne elementer — uacceptabelt ved uendelig rulning.

rows = list(range(1, 11))  # ids 1..10, newest last
size = 3

page1 = rows[0:3]            # [1, 2, 3]
rows.insert(0, 0)           # a new row 0 arrives at the top
page2 = rows[3:6]           # shifted by the insert
print("page1:", page1)
print("page2:", page2)
print("id 3 repeated?", 3 in page2)

Keyset- (cursor-)paginering

Keyset-paginering tæller ikke rækker, der skal springes over. I stedet husker den den sidst viste nøgle og beder om rækker, der ligger strengt efter den: WHERE id < :last_id ORDER BY id DESC LIMIT :size.

  • Forespørgslen bruger et indeksslag i stedet for en gennemgang — konstant tidsforbrug uanset dybden.
  • Indsættelser og sletninger før cursoren forskyder ikke vinduet, så der opstår ingen duplikater eller oversprungne elementer.

Ulempen er, at du kun kan gå til næste eller forrige side i forhold til en cursor — du kan ikke springe til »side 4.217«.

Keyset-logik i almindelig Python

Før du arbejder med SQL, er det nyttigt at se keyset-reglen isoleret. Givet en sorteret liste og det sidste id fra den forrige side skal du returnere den næste del af elementer, hvis id ligger under denne cursor.

Bemærk, at arbejdet kun afhænger af sidestørrelsen, ikke af hvor langt nede vi er.

def keyset_page(rows, after_id, size):
    # rows sorted by id DESC; return items strictly after the cursor
    result = [r for r in rows if r["id"] < after_id]
    return result[:size]

rows = [{"id": i, "name": f"u{i}"} for i in range(10, 0, -1)]
first = rows[:3]
print("first page:", [r["id"] for r in first])
cursor = first[-1]["id"]
next_page = keyset_page(rows, cursor, 3)
print("next page:", [r["id"] for r in next_page])

Design af en stabil sorteringsnøgle

Keyset-paginering kræver en total ordning — sorteringsnøglen skal være unik. Det er usikkert kun at paginere efter created_at, fordi mange rækker kan have samme tidsstempel; rækker ved grænsen kan gå tabt eller blive gentaget.

  • Brug en strengt voksende nøgle som primærnøglen id, eller
  • brug en sammensat nøgle som (created_at, id), så ligheder afgøres af det unikke id.

Medtag altid den unikke kolonne som sidste afgørende faktor, og sørg for, at der findes et matchende indeks i samme kolonnerækkefølge.

Kodning af cursoren som et uigennemsigtigt token

Eksponér aldrig rå interne nøgler direkte. Pak dem ind i et uigennemsigtigt, URL-sikkert token (typisk base64). Klienter behandler det som en sort boks og sender det blot tilbage, så du senere kan ændre formatet på den underliggende nøgle uden at bryde kontrakten.

For sammensatte nøgler skal alle dele kodes samlet i tokenet.

import base64, json

def encode_cursor(created_at, last_id):
    raw = json.dumps({"ts": created_at, "id": last_id}).encode()
    return base64.urlsafe_b64encode(raw).decode()

def decode_cursor(token):
    raw = base64.urlsafe_b64decode(token.encode())
    return json.loads(raw)

token = encode_cursor("2026-06-10T12:00:00Z", 8842)
print("cursor:", token)
print("decoded:", decode_cursor(token))

Et FastAPI-cursor-endpoint

Kobl det nu på FastAPI. Endpointen accepterer en valgfri cursor og en limit, afkoder cursoren til et last_id og henter én side. Et almindeligt trick er at hente limit + 1 rækker for at afgøre, om der findes en næste side.

Dette er framework-kode, der afhænger af en databasesession, så den er illustrerende og ikke selvstændig.

from fastapi import FastAPI, Query, Depends
from sqlalchemy import select

app = FastAPI()

@app.get("/users")
async def list_users(
    cursor: str | None = Query(default=None),
    limit: int = Query(default=20, le=100),
    db=Depends(get_session),
):
    last_id = decode_cursor(cursor)["id"] if cursor else None
    stmt = select(User).order_by(User.id.desc()).limit(limit + 1)
    if last_id is not None:
        stmt = stmt.where(User.id < last_id)
    rows = (await db.execute(stmt)).scalars().all()
    has_more = len(rows) > limit
    rows = rows[:limit]
    next_cursor = encode_cursor(rows[-1].id) if has_more else None
    return {"items": rows, "next_cursor": next_cursor}

Udformning af svarkontrakten

En ren cursor-API returnerer siden samt metadata om navigation — aldrig et samlet sidetal (beregningen ville genindføre en fuld gennemgang). En typisk indpakning:

  • items: den aktuelle side med poster.
  • next_cursor: tokenet til den følgende side eller null, når listen er udtømt.
  • eventuelt has_more: et praktisk boolesk flag.

Klienter gentager processen ved at sende den returnerede next_cursor tilbage som ?cursor=, indtil den er null.

def build_page(rows, limit, get_id):
    has_more = len(rows) > limit
    page = rows[:limit]
    next_cursor = get_id(page[-1]) if has_more and page else None
    return {"items": page, "next_cursor": next_cursor, "has_more": has_more}

fetched = list(range(20, 9, -1))  # asked for 10, got 11
result = build_page(fetched, limit=10, get_id=lambda x: x)
print("items:", result["items"])
print("next_cursor:", result["next_cursor"])
print("has_more:", result["has_more"])

Sammensat keyset og indeksering i SQL

Ved sortering efter aktualitet skal du sammenligne tuplen (created_at, id), så ligheder aldrig bryder kontrakten. I Postgres kan du bruge sammenligning af rækkeværdier:

  • WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT :n
  • Understøt det med CREATE INDEX ON items (created_at DESC, id DESC).

Indekset lader planlæggeren gå direkte til cursor-positionen, så ydeevnen er stabil, uanset om brugeren er på side 1 eller side 10.000.

Hurtigt tjek

Test din forståelse af, hvornår du skal vælge hver strategi.

Opsummering: Cursor kontra offset i stor skala

Du har lært at vælge og opbygge den rigtige pagineringsstrategi:

  • Offset er enkel og understøtter spring mellem sider, men er langsom med O(offset) på dybe sider og forskydes (duplikater/oversprungne elementer), når data ændres.
  • Keyset/cursor søger ud fra den sidst viste nøgle, hvilket giver konstant tidsforbrug på dybe sider og stabile resultater på travle datasæt.
  • Brug en unik total ordning — en primærnøgle eller en sammensat (created_at, id) — og understøt den med et matchende indeks.
  • Eksponér et uigennemsigtigt cursor-token, og returnér { items, next_cursor } i stedet for sidetal og totaler.

Tommelfingerregel: vælg offset til små tabeller i administrationsstil, hvor der er behov for at springe mellem sider; vælg cursor til store, ofte ændrede lister med uendelig rulning.

Gratis at komme i gang

Lær Bootcamp i FastAPI-backendudvikling med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
21
Lektioner
84

Ofte stillede spørgsmål

Er lektionen “Cursor- kontra offset-pagination i stor skala” gratis?

Ja — alle 3 lektioner i læringssporet Bootcamp i FastAPI-backendudvikling, inklusive “Cursor- kontra offset-pagination i stor skala”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Cursor- kontra offset-pagination i stor skala”?

Implementér keyset-/cursor-pagination til stabil og effektiv visning af store datasæt i hyppig forandring. Du øver dig i Bootcamp i FastAPI-backendudvikling med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Bootcamp i FastAPI-backendudvikling?

Der kræves ingen tidligere erfaring. Bootcamp i FastAPI-backendudvikling på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “Cursor- kontra offset-pagination i stor skala”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Bootcamp i FastAPI-backendudvikling-lektion?

Ja. Alle Bootcamp i FastAPI-backendudvikling-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Versionering via URL, headers og medietype
  2. Cursor- kontra offset-pagination i stor skala
  3. Dynamiske filtre og sorteringsparametre
  4. Design af stabile response envelopes
← Tilbage til Bootcamp i FastAPI-backendudvikling