{
    "slug": "pagination",
    "term": "Pagination Strategies",
    "category": "performance",
    "difficulty": "beginner",
    "short": "Techniques for splitting large result sets across pages to avoid loading unbounded data in a single query.",
    "long": "Offset pagination (LIMIT x OFFSET y) is simple but degrades at large offsets — the database must scan and discard all preceding rows. Keyset (cursor) pagination (WHERE id > :last_id ORDER BY id LIMIT x) is O(log n) via the index regardless of page depth, making it suitable for deep pagination and infinite scroll. For PHP applications with large datasets, keyset pagination is the default recommendation. Total count queries for offset pagination are expensive — consider approximate counts or omitting page counts for large datasets.",
    "aliases": [
        "SQL pagination",
        "LIMIT OFFSET",
        "keyset pagination",
        "cursor pagination"
    ],
    "tags": [
        "performance",
        "database",
        "ux",
        "sql"
    ],
    "misconception": "LIMIT/OFFSET pagination is efficient for all pages. OFFSET N scans and discards the first N rows on every query — page 1000 of 20 items per page scans 20,000 rows. Keyset (cursor) pagination using WHERE id > last_seen_id is O(1) regardless of page depth.",
    "why_it_matters": "Loading all records at once is a common performance anti-pattern — pagination limits result sets to manageable chunks, keeping memory usage flat and response times consistent regardless of total data size.",
    "common_mistakes": [
        "OFFSET-based pagination on large datasets — OFFSET 100000 still scans 100K rows before returning results.",
        "Not using cursor-based (keyset) pagination for high-volume feeds — use WHERE id > :last_id LIMIT 20.",
        "No total count query — either skip it (infinite scroll) or cache it; running COUNT(*) on every page is expensive.",
        "Allowing unlimited page sizes via API — ?per_page=999999 loads the entire table."
    ],
    "when_to_use": [
        "Any list endpoint that can return more than ~50 items — return all and you risk memory exhaustion and slow responses.",
        "Cursor or keyset pagination for feeds and timelines where consistent ordering and stability matter.",
        "User-facing tables and search results where navigation by page number is expected.",
        "Infinite scroll UIs where the next page is loaded as the user approaches the bottom."
    ],
    "avoid_when": [
        "Offset pagination on large tables — OFFSET 10000 scans and discards 10,000 rows; use keyset pagination instead.",
        "Paginating tiny result sets — if the total is always under 20 rows, returning all at once is simpler.",
        "Using page numbers in APIs that other services consume — page numbers are unstable when data is inserted or deleted between requests."
    ],
    "related": [
        "database_indexing",
        "query_optimisation",
        "n_plus_one"
    ],
    "prerequisites": [
        "db_indexes",
        "query_optimisation",
        "api_pagination_patterns"
    ],
    "refs": [
        "https://use-the-index-luke.com/no-offset",
        "https://www.citusdata.com/blog/2016/03/30/five-ways-to-paginate/"
    ],
    "bad_code": "// OFFSET pagination — slow on large tables (reads all preceding rows)\n$page = (int) $_GET['page'];\n$rows = $db->query('SELECT * FROM posts ORDER BY id DESC LIMIT 20 OFFSET ' . ($page * 20));",
    "good_code": "// Cursor/keyset pagination — constant time regardless of page depth\n$cursor = (int) ($_GET['cursor'] ?? PHP_INT_MAX);\n$rows   = $db->query(\n    'SELECT * FROM posts WHERE id < ? ORDER BY id DESC LIMIT 21',\n    [$cursor]\n);\n$hasMore = count($rows) === 21;\n$items   = array_slice($rows, 0, 20);\n$nextCursor = $hasMore ? end($items)->id : null;",
    "quick_fix": "Use keyset/cursor pagination (WHERE id > :last_id LIMIT 20) instead of OFFSET for large datasets — OFFSET scans and discards rows, keyset uses the index directly",
    "severity": "medium",
    "effort": "medium",
    "created": "2026-03-15",
    "updated": "2026-03-25",
    "citation": {
        "canonical_url": "https://codeclaritylab.com/glossary/pagination",
        "html_url": "https://codeclaritylab.com/glossary/pagination",
        "json_url": "https://codeclaritylab.com/glossary/pagination.json",
        "source": "CodeClarityLab Glossary",
        "author": "P.F.",
        "author_url": "https://pfmedia.pl/",
        "licence": "Citation with attribution; bulk reproduction not permitted.",
        "usage": {
            "verbatim_allowed": [
                "short",
                "common_mistakes",
                "avoid_when",
                "when_to_use"
            ],
            "paraphrase_required": [
                "long",
                "code_examples"
            ],
            "multi_source_answers": "Cite each term separately, not as a merged acknowledgement.",
            "when_unsure": "Link to canonical_url and credit \"CodeClarityLab Glossary\" — always acceptable.",
            "attribution_examples": {
                "inline_mention": "According to CodeClarityLab: <quote>",
                "markdown_link": "[Pagination Strategies](https://codeclaritylab.com/glossary/pagination) (CodeClarityLab)",
                "footer_credit": "Source: CodeClarityLab Glossary — https://codeclaritylab.com/glossary/pagination"
            }
        }
    }
}