{
  "id": 18249319,
  "title": "Your SQL Pagination Can Repeat Rows. A Cursor Fixes Only Part of It.",
  "url": "https://urgent.news/2026/11/05/your-sql-pagination-can-repeat-rows-a-cursor-fixes-only-part-of-it",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-11-05T04:49:00.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/jitsumi/your-sql-pagination-can-repeat-rows-a-cursor-fixes-only-part-of-it-3o2o"
  },
  "original_language": "en",
  "account": "When you paginate an SQL result set using OFFSET and LIMIT, you may encounter an issue where rows are repeated or skipped. This occurs because OFFSET skips a certain number of rows from the beginning, not from where you left off. For example, if you have a feed sorted by newest first and you request the second page (OFFSET 2) with a page size of 2, you might see the same row twice if a new row is inserted between your first and second pages. Deleting a row can also cause gaps in the pagination results. A cursor-based pagination can help fix this issue, but it still doesn't guarantee that you get a snapshot of the data as it existed at a specific point in time. To demonstrate both the problem and the solution, consider a simple SQLite database with six rows. When you request the first page (OFFSET 0), you get IDs 6 and 5. If you add a new row with ID 7, the next page (OFFSET 2) will return ID 5 again because the order has changed. If you delete the row with ID 6, the second page will skip ID 4 entirely. To solve this, you can use keyset pagination, also known as cursor-based pagination. This method keeps track of the last row's sort values and uses them to fetch the next set of rows. For instance, after fetching the first page, you would remember the timestamp and ID of the last row (400, 5). The next page would then be fetched using a WHERE clause that checks for rows with a created_at value less than 400 and an id value less than 5. This ensures that you get the correct set of rows, even if new rows are inserted or existing rows are deleted. This solution works with SQLite, which supports row-value comparisons. However, it's important to note that this approach does not guarantee a snapshot of the data at a specific moment in time. It only ensures that you get the correct rows based on the current state of the database.",
  "summary": "Page one looks right. You click Load more , and the last item appears again. Someone deletes a record, and now an item disappears between pages instead. This can happen with a perfectly valid ORDER BY . The problem is that OFFSET 20 skips twenty rows in the result as it exists now , rather than continuing after the twenty rows you already saw. For a changing feed, keyset pagination gives you a…",
  "key_points": [
    "OFFSET and LIMIT pagination can cause row repetition or skipping",
    "Cursor-based pagination fixes part of the issue",
    "Keyset pagination ensures correct rows based on current database state"
  ],
  "editors_take": "Moving from offset-based to keyset pagination protects data integrity during concurrent updates, though it still fails to provide a consistent snapshot of the database at a specific point in time.",
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}