Taképarametry stránkování, pagination params, page parametryPokročilý
Definice
Stránkovací parametry jsou hodnoty v URL nebo v těle požadavku, kterými klient určuje, jakou část dlouhého seznamu záznamů má server vrátit. Typicky jde o dvojice page a limit, offset a limit nebo cursor a limit. Server podle nich odřízne výsledky dotazu a spolu s daty vrací informace o dalších stránkách.
Než se na to spolehnete: Odkaz na stránku Stripe o stránkování a na Google Search Central je uveden podle kanonické struktury dokumentace; pokud by konkrétní cesta neodpovídala, nahraďte kořenem dokumentace. Doporučení k indexaci stránkovaných výpisů se v čase mění (rel=next/prev už Google nepoužívá), text proto zůstává u obecných principů.
Proč seznamy vůbec dělíme na stránky
Výpis produktů, log událostí nebo tabulka objednávek může mít stovky tisíc řádků. Poslat je v jedné odpovědi znamená zatížit databázi, síť i prohlížeč. Stránkovací parametry dávají klientovi řízený způsob, jak si vyžádat jen kousek: nejčastěji jako součást query stringu, tedy ?page=3&limit=20. Server má díky nim tvrdý horní strop na velikost odpovědi a může ho vynutit i tehdy, když si klient řekne o milion záznamů.
Tři rodiny parametrů a jejich chování
Offset a limit
Offsetové stránkování používá dvojici offset (kolik záznamů přeskočit) a limit (kolik jich vrátit). Varianta page a per_page je totéž, jen se offset dopočítá jako (page - 1) * per_page. Databáze musí projít a zahodit všechny přeskočené řádky, takže OFFSET 100000 je znatelně pomalejší než OFFSET 0. Zároveň platí, že když mezi načtením stránky 2 a 3 někdo vloží nový záznam, jeden řádek se zobrazí dvakrát a jiný se přeskočí.
Cursor a limit
Kurzorové stránkování posílá místo pozice ukazatel na poslední viděný záznam: ?after=eyJpZCI6MTIzfQ&limit=20. Server dotaz přeloží na podmínku typu WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 20, což využije index a je stejně rychlé na první i tisící stránce. Cenou je, že klient nemůže skočit na stránku 47 a obvykle nezná celkový počet.
Rozsahové a časové parametry
Některá API stránkují podle času (since, until) nebo podle hlavičky Range. Tento přístup se hodí u proudů událostí, kde klient dohání to, co zmeškal.
Co s parametry dělá SEO
Každá kombinace stránkovacích parametrů vytváří samostatnou URL. Pokud výpis nabízí i řazení a filtry, počet adres roste kombinatoricky a vyhledávač prochází tisíce téměř shodných stránek. Řešením je držet jednu závaznou podobu URL, nepovolené kombinace neindexovat a u variant, které jsou jen jiným pohledem na stejný obsah, nastavit kanonickou URL. Stránka 2 výpisu ale kanonickou verzí stránky 1 není: má vlastní obsah a měla by být indexovatelná samostatně.
Validace na straně serveru
Stránkovací parametry přicházejí od uživatele, takže s nimi zacházej jako s každým jiným vstupem. Praktické minimum: limit ořízni na maximum (běžně 100), zápornou nebo nečíselnou hodnotu nahraď výchozí, název sloupce pro řazení validuj proti seznamu povolených hodnot a nikdy ho nevkládej do SQL řetězcem. Bez stropu na limit stačí jediný požadavek s limit=10000000, aby vyčerpal paměť procesu.
Co vracet klientovi
Odpověď by kromě dat měla obsahovat metadata: použitý limit, kurzor na další stránku nebo příznak has_more. Celkový počet záznamů je u velkých tabulek drahý (COUNT(*) projde celou množinu), proto ho řada API vrací jen na vyžádání nebo vůbec. Přehledné je i doplnit odkazy na další a předchozí stránku přímo do odpovědi, aby klient nemusel skládat URL sám.
Příklady z praxe
Offsetový výpis v REST API
E-shop vypisuje objednávky přes endpoint /api/orders. Klient pošle page a limit, server hodnoty ověří, ořízne limit na 100 a dopočítá offset. V odpovědi vrací kromě položek i celkový počet, protože tabulka objednávek jednoho zákazníka je malá a COUNT je zde levný.
const page = Math.max(1, Number(req.query.page) || 1); const limit = Math.min(100, Number(req.query.limit) || 20); const rows = await db.query( 'SELECT id, total, created_at FROM orders WHERE customer_id = $1 ORDER BY id DESC LIMIT $2 OFFSET $3', [customerId, limit, (page - 1) * limit] );Kurzor u nekonečného scrollu
Feed příspěvků v mobilní aplikaci se donačítá při scrollování a nová data přibývají každou minutu. Offsetové stránkování by způsobilo duplicity, proto server vrací cursor sestavený z dvojice (created_at, id) posledního záznamu. Další požadavek ho pošle zpět jako parametr after a dotaz sáhne rovnou na index.
SELECT id, body, created_at FROM posts WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC LIMIT 20;
Časté omyly
- MýtusStránkování je jen LIMIT a OFFSET, na tom se nedá nic pokazit.
- Ve skutečnostiOffsetové stránkování nad měnícími se daty vynechává a duplikuje záznamy a s rostoucím offsetem lineárně zpomaluje, protože databáze musí přeskakované řádky přesto načíst. U velkých nebo živých seznamů je kurzorové stránkování spolehlivější.
- MýtusStránky 2 a dál by měly mít kanonickou URL na stránku 1.
- Ve skutečnostiStránka 2 obsahuje jiné položky, takže kanonizace na stránku 1 vede k tomu, že se její obsah přestane indexovat. Každá stránka výpisu má být kanonická sama na sebe; kanonizovat se mají jen adresy lišící se nepodstatnými parametry.
- MýtusKdyž klient pošle limit=100000, je to jeho problém.
- Ve skutečnostiNeomezený limit je vektor pro odepření služby: jediný požadavek dokáže vyčerpat paměť aplikace i spojení do databáze. Server musí mít vlastní tvrdý strop nezávisle na tom, co klient požaduje.
Časté dotazy
- Jak pojmenovat stránkovací parametry v REST API?
- Stránkovací parametry se v praxi nejčastěji pojmenovávají page a limit, offset a limit nebo cursor a limit. Konkrétní volba je méně důležitá než konzistence napříč celým API: pokud jeden endpoint používá per_page a druhý pageSize, integrace se zbytečně komplikuje. Vyplatí se držet malá písmena a jedno oddělovací schéma, doplnit rozumnou výchozí hodnotu limitu a v dokumentaci uvést i maximum, které server akceptuje. U kurzorů je zvykem hodnotu považovat za neprůhlednou, tedy klientovi zakázat její vlastní skládání.
- Proč je vysoký offset v databázi pomalý?
- Vysoký offset je pomalý proto, že klauzule OFFSET nepřeskočí řádky zdarma. Databáze musí odpovídající záznamy najít, seřadit a teprve pak zahodit, takže dotaz s OFFSET 500000 udělá zhruba stejnou práci jako dotaz vracející půl milionu řádků. Náklady rostou lineárně s hloubkou stránky. Kurzorové stránkování tento problém obchází podmínkou na hodnoty posledního viděného záznamu, kterou index dokáže vyhodnotit přímo, takže doba odpovědi zůstává stejná na první i na tisící stránce.
- Mají mít stránkovací parametry vliv na indexaci vyhledávači?
- Stránkovací parametry mají na indexaci zásadní vliv, protože každá kombinace tvoří samostatnou URL. Doporučený postup je nechat jednotlivé stránky výpisu indexovatelné, ale zamezit vzniku duplicit z nepodstatných parametrů, například řazení nebo tracking kódů. Pomáhá pevné pořadí parametrů v odkazech, kanonická URL u ekvivalentních variant a odkazy na další a předchozí stránku v HTML, aby robot výpis prošel celý. Stránky za rozumnou hloubkou se často z procházení vyřazují, protože jejich obsah je pro vyhledávání málo hodnotný.
- Jak stránkovat, když uživatel potřebuje skákat na konkrétní stránku?
- Skoky na konkrétní stránku vyžadují offsetové stránkování, protože kurzor zná jen pozici posledního načteného záznamu. Kompromisem je nabídnout číslované stránky jen do určité hloubky, například do padesáté stránky, a hlubší navigaci nahradit filtry, fulltextem nebo řazením. Uživatel, který hledá konkrétní záznam na stránce 300, ho stejně najde rychleji vyhledáním. Druhou možností je předpočítat hranice stránek do pomocné tabulky, což se vyplatí u dat, která se mění zřídka.
- Patří stránkovací parametry do URL, nebo do těla požadavku?
- Stránkovací parametry patří u operací čtení do query stringu URL. Odpověď zůstane cacheovatelná, adresa jde sdílet a zalogovat a chování odpovídá tomu, že GET nemá mít tělo s významem. Do těla se stránkování posouvá jen tam, kde je samotný dotaz složitý a posílá se metodou POST, typicky u vyhledávání s mnoha filtry nebo u GraphQL. I v takovém případě je vhodné argumenty pojmenovat stejně jako v REST části API.
Zdroje
- URL Standard(otevře se v novém okně)
- LIMIT and OFFSET(otevře se v novém okně)
- cursor.skip()(otevře se v novém okně)
- Pagination(otevře se v novém okně)
- Google Search Central: URL structure best practices(otevře se v novém okně)