Skip to content

PHP Tutorial Database CRUD

Pagination Basics

Lesson 276 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

Showing every record on one page is slow and hard to use. Pagination splits a list into pages, such as 10 products per page.

The idea

Two numbers decide what to show:

  • $perPage: how many rows per page.
  • $page: the current page (1, 2, 3 …).

The database needs LIMIT $perPage OFFSET $offset, where:

Text
PLAIN
offset = (page - 1) * perPage
PageOffsetRows shown (10 per page)
101 to 10
21011 to 20
32021 to 30

A pagination helper

index.php
PHP
<?php
function paginate(int $total, int $page, int $perPage): array
{
    $pages = max(1, (int) ceil($total / $perPage));
    $page = min(max(1, $page), $pages);

    return [
        "page" => $page,
        "pages" => $pages,
        "offset" => ($page - 1) * $perPage,
        "perPage" => $perPage,
        "hasPrev" => $page > 1,
        "hasNext" => $page < $pages,
    ];
}

print_r(paginate(47, 3, 10));
Output
OUTPUT
Array
(
    [page] => 3
    [pages] => 5
    [offset] => 20
    [perPage] => 10
    [hasPrev] => 1
    [hasNext] => 1
)

The function also clamps the page number: a visitor who asks for page 999 gets the last page, and page -4 gets page 1.

index.php
PHP
<?php
function paginate(int $total, int $page, int $perPage): array
{
    $pages = max(1, (int) ceil($total / $perPage));
    $page = min(max(1, $page), $pages);

    return ["page" => $page, "pages" => $pages, "offset" => ($page - 1) * $perPage];
}

echo paginate(47, 999, 10)["page"] . " " . paginate(47, -4, 10)["page"] . " " . paginate(0, 1, 10)["pages"];
Output
OUTPUT
5 1 1

Query one page

index.php
PHP
<?php
function paginate(int $total, int $page, int $perPage): array
{
    $pages = max(1, (int) ceil($total / $perPage));
    $page = min(max(1, $page), $pages);

    return ["page" => $page, "pages" => $pages, "offset" => ($page - 1) * $perPage];
}

$pdo = require __DIR__ . "/db.php";

$perPage = 2;
$total = (int) $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();

$p = paginate($total, (int) ($_GET["page"] ?? 2), $perPage);

$statement = $pdo->prepare("SELECT name FROM products ORDER BY id LIMIT :limit OFFSET :offset");
$statement->bindValue(":limit", $perPage, PDO::PARAM_INT);
$statement->bindValue(":offset", $p["offset"], PDO::PARAM_INT);
$statement->execute();

echo "Page {$p['page']} of {$p['pages']}: ";
echo implode(", ", $statement->fetchAll(PDO::FETCH_COLUMN));
Output
OUTPUT
Page 2 of 3: Bag, Marker

Bind LIMIT and OFFSET as integers (PDO::PARAM_INT).

Always use ORDER BY

Without ORDER BY, the database may return rows in any order, so pages could repeat or skip rows. Use a stable order, ideally ending with a unique column such as id.

index.php
PHP
<nav aria-label="Pagination">
  <?php if ($p["hasPrev"]): ?>
    <a href="?<?= h(http_build_query([...$_GET, "page" => $p["page"] - 1])) ?>">Previous</a>
  <?php endif; ?>

  Page <?= $p["page"] ?> of <?= $p["pages"] ?>

  <?php if ($p["hasNext"]): ?>
    <a href="?<?= h(http_build_query([...$_GET, "page" => $p["page"] + 1])) ?>">Next</a>
  <?php endif; ?>
</nav>

[...$_GET, "page" => 3] keeps the other query values (search, filter, sort) when you change the page. http_build_query() encodes them safely.

A performance note

OFFSET with a big number is slow, because the database still counts through all the skipped rows. For huge tables, keyset pagination (WHERE id > :last_id ORDER BY id LIMIT 10) is faster. For most sites, LIMIT/OFFSET is fine.

lightbulb
Show a sensible range
Do not print 500 page links. Show first, last and a few pages around the current one.