SQL concepts: SELECT · ORDER BY · LIMIT
AJAX pagination can request one page of records as JSON and update the table without reloading the full document. The browser sends a page number, while PHP validates it, calculates the offset, and returns both rows and navigation metadata.
Demo of AJAX paging scriptKeep the client contract small: send only the requested page number. URLSearchParams handles URL encoding, and the response is parsed as JSON.
async function loadPage(page) {
const params = new URLSearchParams({ page: String(page) });
const response = await fetch(`php_paging-ajaxck.php?${params}`);
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Unable to load records');
}
renderRows(payload.data);
nextButton.disabled = !payload.hasNext;
previousButton.disabled = !payload.hasPrevious;
}
Client-side values cannot be trusted. Accept only integers greater than or equal to 1. Invalid page values should return an HTTP 400 response instead of being used in SQL.
<?php
$pageRaw = $_GET['page'] ?? '1';
if (filter_var(
$pageRaw,
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]]
) === false) {
http_response_code(400);
echo json_encode(['error' => 'Invalid page number']);
exit;
}
$page = (int) $pageRaw;
?>
Use COUNT(*) to obtain the total number of records. Avoid relying on rowCount() for SELECT queries because its behavior is driver-dependent.
$limit = 10;
$totalRecords = (int) $dbo->query('SELECT COUNT(*) FROM student')->fetchColumn();
$totalPages = max(1, (int) ceil($totalRecords / $limit));
$page = min($page, $totalPages);
$offset = ($page - 1) * $limit;
An explicit ORDER BY keeps paging stable, and integer binding keeps request-derived values out of the SQL string.
$stmt = $dbo->prepare(
'SELECT id, name, class AS myclass, mark FROM student ORDER BY id LIMIT :limit OFFSET :offset'
);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'data' => $rows,
'page' => $page,
'totalRecords' => $totalRecords,
'totalPages' => $totalPages,
'hasPrevious' => $page > 1,
'hasNext' => $page < $totalPages,
]);
Database values should be placed into cells with textContent rather than concatenated into an HTML string. This keeps record data from being interpreted as markup.
rows.forEach((record) => {
const td = document.createElement('td');
td.textContent = String(record.name ?? '');
});
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.
| arifxxx | 13-06-2014 |
| this is one of the best tutorial what i've know | |
| subbu | 26-02-2017 |
| how to implement or add one more button that retrieve all records at once? can you provide the code for that? thanks. | |
| smo1234 | 28-02-2017 |
| Your query reads like this $query=" SELECT * FROM student limit $eu, $limit "; If you remove limit part and make it like this. $query=" SELECT * FROM student "; You can display all the records. With the button you can pass a variable like this <a href=page_name.php?display_status=dispaly_all>Show all</a> Collect display_status like this at the top of the page. $display_all=$_GET['display_all']; if($dispaly_all=='display_all'){ $q='SELECT * FROM student'; }else{ $q='SELECT * FROM student limit $eu,$limit'; } | |
| Php Developer | 01-06-2018 |
| Nice work! This is massively helpful to me. It just made my work easier. It saved lot of time. Thanks. | |