PHP Search, Filter, Pagination and SQL LIMIT/OFFSET Debugger
Find whether your PHP search or pagination problem is caused by the keyword pattern, prepared statement, count query, LIMIT, OFFSET, page calculation, missing filters, unsafe sorting, JOIN duplicates or lost query parameters.
Use this tool when: search returns no records, page 2 repeats page 1, rows are skipped, the last page is empty, filters disappear after navigation, total pages are incorrect, JOINs create duplicate records or an AJAX “Load More” button repeatedly loads the same results.
Describe Your Search or Pagination Problem
Complete the settings you know. The optional SQL and PHP code fields improve the diagnosis but are not required.
Likely failing layer
Most Likely Root Causes
Fix in This Order
Search and SQL Checks
Pagination and URL Checks
Stage-by-Stage Test Matrix
| Stage | Assessment | Exact test |
|---|
Personalized PHP and SQL Templates
These templates use sanitized example identifiers. Match the real connection variable, table names, filter rules, authorization conditions and database indexes before placing them in a project.
PDO search and pagination template
MySQLi search and pagination template
Filter-preserving pagination links
Safe ORDER BY allowlist
AJAX Load More pattern
Security and Data-Safety Warnings
Search and Pagination Test Cases
Viva-Ready Explanation
Copyable Debugging Report
How PHP Search and Pagination Work Together
Search and pagination are two parts of the same database operation. The search decides which records belong in the result set. Pagination decides which small section of that filtered result set should appear on the current page.
A reliable implementation normally runs two related queries. The count
query calculates how many filtered records exist. The data query uses
the same filters, adds a stable order and retrieves one page through
LIMIT and OFFSET.
Input stage
PHP reads and validates the keyword, filters, page number, per-page value and selected sort option.
Filter stage
PHP builds one controlled WHERE condition and prepares the corresponding parameter values.
Count stage
A filtered count determines the number of available records and pages.
Offset stage
The current page and page size produce the zero-based SQL offset.
Data stage
The data query uses matching filters, a stable order, LIMIT and OFFSET.
Navigation stage
Pagination links preserve the keyword, filters and sorting choice.
The Correct LIMIT and OFFSET Calculation
User-facing page numbers usually begin at one, while SQL offsets begin at zero. Therefore, page one begins at offset zero. Page two begins after one complete page of records.
$offset = ($currentPage - 1) * $recordsPerPage;
| Page | Records per page | OFFSET | Returned positions |
|---|---|---|---|
| 1 | 10 | 0 | Rows 1–10 |
| 2 | 10 | 10 | Rows 11–20 |
| 3 | 10 | 20 | Rows 21–30 |
Using $currentPage * $recordsPerPage skips the first
group because page one starts at offset ten. Failing to apply the
calculated offset causes every page to return the same first records.
Validate the Page Number Before SQL
A page number comes from the browser and must not be trusted. It may be missing, zero, negative, decimal text or extremely large.
$currentPage = filter_input(
INPUT_GET,
"page",
FILTER_VALIDATE_INT
);
$currentPage = max(1, $currentPage ?: 1);
After the count query calculates the total pages, clamp the requested value again. A request for page 500 should not silently produce a confusing empty page when only five pages exist.
$totalPages = max(
1,
(int) ceil($totalRecords / $recordsPerPage)
);
$currentPage = min($currentPage, $totalPages);
Why Page 2 Repeats Page 1
Repeated pages usually mean the offset was calculated but never added to the executed SQL, the offset variable remains zero, or the page parameter is read from the wrong request key.
Inspect these values together:
var_dump([
"page" => $currentPage,
"per_page" => $recordsPerPage,
"offset" => $offset
]);
Also inspect the IDs returned on each page. Page one and page two should not contain the same complete ID sequence when the order and dataset remain unchanged.
Why Search and Filters Disappear on Page 2
A page link such as ?page=2 replaces the existing query
string. If the active keyword, status, category or sort option is not
included, PHP receives only the page number and rebuilds an unfiltered
query.
Build links from all active parameters:
$query = http_build_query([
"search" => $search,
"status" => $status,
"sort" => $sort,
"page" => $pageNumber
]);
$url = "records.php?" . $query;
This also handles spaces and special URL characters more safely than manually joining raw values with ampersands.
The Count Query Must Match the Data Query
The total-page calculation is only correct when the count query represents the same filtered dataset as the data query.
These conditions must match:
- Search keyword and searched columns
- Status, category and date filters
- User ownership or role restrictions
- Relevant JOIN conditions
- Soft-delete conditions
- Tenant or organization restrictions
Counting every row in the table while displaying only active search results creates too many pagination links and empty final pages.
SELECT COUNT(*)
FROM products
WHERE name LIKE ?
AND status = ?;
SELECT id, name, status
FROM products
WHERE name LIKE ?
AND status = ?
ORDER BY id DESC
LIMIT ? OFFSET ?;
Use Prepared Statements for Search Values
Do not concatenate a browser-provided keyword directly into SQL. Create the wildcard pattern as a value and bind that value to a placeholder.
$search = trim($_GET["search"] ?? "");
$pattern = "%" . $search . "%";
$stmt = $pdo->prepare(
"SELECT id, name
FROM products
WHERE name LIKE :search"
);
$stmt->execute([
"search" => $pattern
]);
For a multiple-column search, bind the pattern separately for each placeholder rather than inserting the original keyword into the SQL string.
Empty Search and LIKE %%
Adding percent signs around an empty keyword creates
LIKE '%%', which normally matches every non-null value.
This may be correct when an empty search should show all records, but
it is a bug when the page is supposed to require a keyword.
Choose the behavior deliberately:
- Show all records when search is empty.
- Return no records until a keyword is entered.
- Display a validation message requiring a keyword.
Do not let an accidental empty pattern decide the product behavior.
Use a Safe Sorting Allowlist
Prepared-statement placeholders represent data values. They do not safely replace SQL column names or keywords in an ORDER BY clause. Therefore, user-selected sorting should map to server-controlled SQL.
$allowedSorts = [
"newest" => "created_at DESC, id DESC",
"oldest" => "created_at ASC, id ASC",
"name" => "name ASC, id ASC"
];
$orderBy = $allowedSorts[$sort]
?? $allowedSorts["newest"];
The unique ID used as a secondary order makes pagination more stable when multiple records share the same name or date.
Why JOINs Produce Duplicate Records
A parent row is repeated when it matches several rows in a joined child table. For example, one product with three tags may appear three times after joining the product-tag relationship.
Possible solutions depend on the intended output:
- Correct an accidental JOIN condition.
- Use
DISTINCTwhen the selected parent columns are identical. - Group by the parent record when calculating aggregates.
- Load child collections in a separate query.
- Count distinct parent IDs in the count query.
Do not add DISTINCT blindly. First identify why one parent record matches multiple child rows and whether those relationships are meaningful.
Stable Ordering Prevents Missing and Repeated Rows
Pagination without ORDER BY does not define a reliable record order. Ordering only by a non-unique value, such as status or creation date, can also leave tied rows without a stable position.
ORDER BY created_at DESC, id DESC
Even with a stable order, new inserts and deletions between page requests can shift offset-based pagination. For ordinary student projects, stable offset pagination is usually acceptable. Larger real-time systems may use cursor-based pagination instead.
AJAX Load More Must Advance One State
A Load More feature must maintain one clear page or offset value. It should not increment both before and after a request, and it should not reset to page one when the search term changes unexpectedly.
The response should tell the browser:
- Which page was returned
- How many rows were returned
- Whether another page exists
- The normalized active filters
Disable the button while a request is pending to avoid two concurrent requests loading the same page.
Safe Debugging Order
- Run the filtered data query without LIMIT and confirm the results.
- Run the filtered count query and compare its total.
- Validate the page and records-per-page values.
- Calculate and display the expected offset.
- Add stable ORDER BY, LIMIT and OFFSET.
- Compare returned IDs between page one and page two.
- Preserve all active query parameters in navigation links.
- Test sorting, search and filters together.
- Inspect JOINs when IDs are duplicated.
- Test empty, invalid and out-of-range page requests.
Continue Your CodeZips PHP Project Workflow
Technical References
Frequently Asked Questions
Why does page 2 show the same records as page 1?
The calculated offset may not be included in the executed SQL, the
offset may remain zero, or PHP may read the wrong page parameter.
Use ($currentPage - 1) * $recordsPerPage.
Why does my search disappear when I click page 2?
The pagination link probably includes only the page parameter. Include the current keyword, status, category and sorting values in every page URL.
Why is my total number of pages incorrect?
The count query may not use the same WHERE and JOIN conditions as the query that loads displayed records.
Why does an empty search return every row?
Adding percent signs around an empty string creates a pattern such
as %%. Decide explicitly whether empty search should
show all rows, none or a validation message.
Can I bind an ORDER BY column using a placeholder?
Prepared placeholders represent data values, not SQL identifiers. Map the requested sort option to a server-controlled allowlist.
Why does a JOIN duplicate the same record?
One parent record may match several child rows. Inspect the relationship before choosing DISTINCT, GROUP BY, an aggregate query or a separate child query.
Should pagination start at page zero or page one?
User-facing pages normally start at one, while SQL OFFSET starts at zero. Page one therefore uses offset zero.
Why does sorting change which records appear on each page?
Pagination needs a stable ORDER BY. Add a unique secondary column such as the record ID when the main sort column can contain ties.
Does this tool execute my SQL query?
No. It analyzes the selected settings and visible code patterns locally in the browser. It does not connect to MySQL.
Final Takeaway
Reliable search pagination requires one consistent filtered dataset. Validate the keyword and page number, use prepared values, run a matching count query, calculate a zero-based offset, apply stable sorting and preserve every active filter in page links. Debug these stages separately instead of changing LIMIT values randomly.

