{"id":1207,"date":"2018-01-11T11:56:34","date_gmt":"2018-01-11T19:56:34","guid":{"rendered":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/"},"modified":"2018-01-11T11:56:34","modified_gmt":"2018-01-11T19:56:34","slug":"offset-keyset-pagination-n1ql-query-couchbase","status":"publish","type":"post","link":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/","title":{"rendered":"Database Pagination: Using OFFSET and Keyset in N1QL"},"content":{"rendered":"\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4429\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2-300x128-1.png\" alt=\"\" width=\"516\" height=\"220\"><\/p>\n\n\n\n<p><span>Read the pagination background in my previous article: <\/span><a href=\"https:\/\/www.couchbase.com\/blog\/optimizing-database-pagination-using-couchbase-n1ql\/\"><span>https:\/\/www.couchbase.com\/blog\/optimizing-database-pagination-using-couchbase-n1ql\/<\/span><\/a><\/p>\n\n\n\n<p><span>Pagination is the task of dividing the potential result into pages and retrieving the required pages, one by one on demand. \u00a0Using OFFSET and LIMIT is the easy way to write pagination into database queries. Together, OFFSET and LIMIT, make the pagination clause of the SELECT statement. Pagination is a common application job and its implementation has a major impact on the customer experience. Let\u2019s look at the issues and solutions with Couchbase N1QL in detail.<\/span><\/p>\n\n\n\n<p><span>Markus Winand from <\/span><a href=\"https:\/\/use-the-index-luke.com\/\"><span>https:\/\/use-the-index-luke.com\/<\/span><\/a><span> argues it may not be the most efficient. He also suggests, when possible, using keyset pagination approach instead of OFFSET pagination. For this article, I\u2019m going to use the example queries modeled on his article, <\/span><a href=\"https:\/\/use-the-index-luke.com\/no-offset\"><span>https:\/\/use-the-index-luke.com\/no-offset<\/span><\/a><span> to show what OFFSET optimizations we\u2019ve done, when and how to exploit the keyset pagination in N1QL. <\/span><\/p>\n\n\n\n<p><span>Since Couchbase ships with a travel-sample dataset, we\u2019ll use it to write our pagination queries.<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>GET THE FIRST PAGE<\/span><\/li>\n\n<\/ol>\n\n\n<p>[crayon theme=&#8221;github&#8221; scroll=&#8221;true&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;plsql&#8221; decode=&#8221;true&#8221;]SELECT *<br \/>\nFROM `travel-sample`<br \/>\nWHERE type = \u2018hotel\u2019<br \/>\nORDER BY country, city<br \/>\nOFFSET 0<br \/>\nLIMIT 10;<\/p>\n<p>CREATE INDEX ixtopic ON `travel-sample`(type);<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><span>For this query, using the index ixtopic, the query engine executes in a simple way. The query engine gets all the qualified keys from the index, then gets all the documents, sorts based on the ORDER BY clause and then drops the OFFSET number of documents (in this case zero) and projects LIMIT (in this case 10) number of documents.<\/span><\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4430\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/Screen-Shot-2018-01-09-at-10.34.54-AM-300x169-1.png\" alt=\"\" width=\"744\" height=\"419\"><\/p>\n\n\n\n<p><span>Here\u2019s the plan showing the index and the spans.<\/span><\/p>\n\n\n\n<p>\u00a0<\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n\n\n\n<p><span>N1QL chooses the index ixtype and pushes the filter (type = \u201chotel\u201d) to the index scan. To implement the ORDER BY clause, it fetches all the documents. In the sort phase, we recognize LIMIT 10 clause and make the sort efficient by keeping only the top 10 items.<\/span><\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]    {<br \/>\n       &#8220;#operator&#8221;: &#8220;Order&#8221;,<br \/>\n       &#8220;limit&#8221;: &#8220;10&#8221;,<br \/>\n       &#8220;sort_terms&#8221;: [<br \/>\n         {<br \/>\n           &#8220;expr&#8221;: &#8220;(`travel-sample`.`country`)&#8221;<br \/>\n         },<br \/>\n         {<br \/>\n           &#8220;expr&#8221;: &#8220;(`travel-sample`.`city`)&#8221;<br \/>\n         }<br \/>\n       ]<br \/>\n     },<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><span>Let\u2019s look at the index scan operator efficiency with this index:<\/span><\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]               &#8220;#operator&#8221;: &#8220;IndexScan2&#8221;,<br \/>\n               &#8220;#stats&#8221;: {<br \/>\n                 &#8220;#itemsOut&#8221;: 917,<br \/>\n                 &#8220;#phaseSwitches&#8221;: 3671,<br \/>\n                 &#8220;execTime&#8221;: &#8220;2.646892ms&#8221;,<br \/>\n                 &#8220;kernTime&#8221;: &#8220;31.095431ms&#8221;,<br \/>\n                 &#8220;servTime&#8221;: &#8220;19.781593ms&#8221;<br \/>\n               },<br \/>\n\u2026<br \/>\n               &#8220;#operator&#8221;: &#8220;Fetch&#8221;,<br \/>\n               &#8220;#stats&#8221;: {<br \/>\n                 &#8220;#itemsIn&#8221;: 917,<br \/>\n                 &#8220;#itemsOut&#8221;: 917,<br \/>\n                 &#8220;#phaseSwitches&#8221;: 3787,<br \/>\n                 &#8220;execTime&#8221;: &#8220;3.43324ms&#8221;,<br \/>\n                 &#8220;kernTime&#8221;: &#8220;20.847541ms&#8221;,<br \/>\n                 &#8220;servTime&#8221;: &#8220;69.875698ms&#8221;<br \/>\n               },<br \/>\n\u2026<br \/>\n           &#8220;#operator&#8221;: &#8220;Order&#8221;,<br \/>\n           &#8220;#stats&#8221;: {<br \/>\n             &#8220;#itemsIn&#8221;: 917,<br \/>\n             &#8220;#itemsOut&#8221;: 10,<br \/>\n             &#8220;#phaseSwitches&#8221;: 1849,<br \/>\n             &#8220;execTime&#8221;: &#8220;6.519061ms&#8221;,<br \/>\n             &#8220;kernTime&#8221;: &#8220;88.307572ms&#8221;<br \/>\n           },[\/crayon]<\/p>\n\n\n\n<p><span>There were 917 documentkeys returned by the indexer. The query engine fetched 917 documents. Then the sort (order) operator sorted them and returned the 10 items.<\/span><\/p>\n\n\n\n<p><span>2. GET THE SECOND PAGE<\/span><\/p>\n\n\n<p>[crayon whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;plsql&#8221; decode=&#8221;true&#8221;]SELECT *<br \/>\nFROM `travel-sample`<br \/>\nWHERE type = \u2018hotel\u2019<br \/>\nORDER BY country, city<br \/>\nOFFSET 10<br \/>\nLIMIT 10;<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><span>In this case, everything is same as the query1 except:<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>Sort (ORDER BY) operator will return 20 documents (offset + limit).<\/span><\/li>\n\n\n<li><span>The new OFFSET operator will execute after the Order operator and drop the first 10 rows.<\/span><\/li>\n\n<\/ol>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]         &#8220;#operator&#8221;: &#8220;Order&#8221;,<br \/>\n           &#8220;#stats&#8221;: {<br \/>\n             &#8220;#itemsIn&#8221;: 917,<br \/>\n             &#8220;#itemsOut&#8221;: 20,<br \/>\n             &#8220;#phaseSwitches&#8221;: 1859,<br \/>\n             &#8220;execTime&#8221;: &#8220;6.485904ms&#8221;,<br \/>\n             &#8220;kernTime&#8221;: &#8220;65.92484ms&#8221;<br \/>\n           },<br \/>\n   {<br \/>\n           &#8220;#operator&#8221;: &#8220;Offset&#8221;,<br \/>\n           &#8220;#stats&#8221;: {<br \/>\n             &#8220;#itemsOut&#8221;: 10,<br \/>\n             &#8220;#phaseSwitches&#8221;: 32,<br \/>\n             &#8220;execTime&#8221;: &#8220;5.071503ms&#8221;,<br \/>\n             &#8220;kernTime&#8221;: &#8220;701ns&#8221;,<br \/>\n             &#8220;state&#8221;: &#8220;running&#8221;<br \/>\n           },<br \/>\n           &#8220;expr&#8221;: &#8220;10&#8221;,<br \/>\n           &#8220;#time_normal&#8221;: &#8220;00:00.0050&#8221;,<br \/>\n           &#8220;#time_absolute&#8221;: 0.005071503<br \/>\n         },<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><span>As the OFFSET increases, the number of documents scanned by the sort will increase as well, consuming more memory and CPU.<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span> Let\u2019s improve the performance by covering the predicate and sort keys with a single index.<\/span><\/li>\n\n<\/ol>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;plsql&#8221; decode=&#8221;true&#8221;]CREATE INDEX ixtypectcy ON `travel-sample`(type, country, city);<br \/>\nLet\u2019s execute our recent query again:<br \/>\nSELECT *<br \/>\nFROM `travel-sample`<br \/>\nWHERE type = \u2018hotel\u2019<br \/>\nORDER BY country, city<br \/>\nOFFSET 10<br \/>\nLIMIT 10;<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4435\" src=\"https:\/\/www.couchbase.com\/wp-content\/uploads\/sites\/5\/2026\/05\/Screen-Shot-2018-01-09-at-2.38.36-PM-300x179-1.png\" alt=\"\" width=\"641\" height=\"383\"><\/p>\n\n\n\n<p><span>It uses the right index and creates the right filters (spans) for the index scan.Explain includes the following.<\/span><\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]       {<br \/>\n           &#8220;#operator&#8221;: &#8220;IndexScan2&#8221;,<br \/>\n           &#8220;index&#8221;: &#8220;ixtypectcy&#8221;,<br \/>\n           &#8220;index_id&#8221;: &#8220;2a2ed6573354e21&#8221;,<br \/>\n           &#8220;index_projection&#8221;: {<br \/>\n             &#8220;primary_key&#8221;: true<br \/>\n           },<br \/>\n           &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n           &#8220;limit&#8221;: &#8220;10&#8221;,<br \/>\n           &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n           &#8220;offset&#8221;: &#8220;10&#8221;,<br \/>\n           &#8220;spans&#8221;: [<br \/>\n             {<br \/>\n               &#8220;exact&#8221;: true,<br \/>\n               &#8220;range&#8221;: [<br \/>\n                 {<br \/>\n                   &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                   &#8220;inclusion&#8221;: 3,<br \/>\n                   &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                 }<br \/>\n               ]<br \/>\n             }<br \/>\n           ],<br \/>\n[\/crayon]<\/p>\n\n\n\n<p><span>Let\u2019s look at the index scan operator efficiency with this index:<\/span><\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]               &#8220;#operator&#8221;: &#8220;IndexScan2&#8221;,<br \/>\n               &#8220;#stats&#8221;: {<br \/>\n                 &#8220;#itemsOut&#8221;: 10,<br \/>\n                 &#8220;#phaseSwitches&#8221;: 43,<br \/>\n                 &#8220;execTime&#8221;: &#8220;41.786\u00b5s&#8221;,<br \/>\n                 &#8220;kernTime&#8221;: &#8220;11.15\u00b5s&#8221;,<br \/>\n                 &#8220;servTime&#8221;: &#8220;855.759\u00b5s&#8221;<br \/>\n               },<br \/>\n[\/crayon]<\/p>\n\n\n\n<p>\u00a0<\/p>\n\n\n\n<p><span>Only the required 10 document keys are returned from the index scan! \u00a0The N1QL optimizer evaluates both the WHERE clause and the pagination (OFFSET, LIMIT) clause. The query optimizer decides to pushdown the pagination clause to the indexer based on the order by clause of the query and the index key order. \u00a0<\/span><\/p>\n\n\n\n<p><span>In this case, query predicate is: type = \u2018hotel\u2019<\/span><\/p>\n\n\n\n<p><span>Order by clause is: ORDER BY country, city<\/span><\/p>\n\n\n\n<p><span>Index key order is: (type, country, city)<\/span><\/p>\n\n\n\n<p><span>Just by simple comparison, order by clause is not exactly the same as index key order. However, the leading key of the index (type) has an equality predicate (type = \u2018hotel\u2019). Therefore, the optimizer knows the projected document keys will be in the sorted order of (country and city).<\/span><\/p>\n\n\n\n<p><span>**and the ORDER BY clause to see if it can pushdown the pagination parameters to the index scan. \u00a0In this case, there is a perfect match.<\/span><\/p>\n\n\n\n<p><span>The optimizer pushes both OFFSET and LIMIT in pagination to index scan and the index scan returns only the 10 document keys required no matter the OFFSET! Therefore, fetch only needs to fetch the 10 documents. Indexer does have to go through the index scan to evaluate the keys.<\/span><\/p>\n\n\n\n<p><span>This query executed in about 6.81 milliseconds in my environment. Let\u2019s paginate to subsequent pages to see the performance:<\/span><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<tbody>\n<tr>\n<td><span>OFFSET<\/span><\/td>\n<td><span>LIMIT<\/span><\/td>\n<td><span>Response Time<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>10<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>6.81 ms<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>20<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>7.17 ms<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>100<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>7.02 ms<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>400<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>9.54 ms<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>800<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>9.08 ms<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p><span>If you want to paginate in the descending order, change the query and index to the following. The collation of the key in the index should match the collation in the query. In this case, both country and city are in DESCending order.<\/span><\/p>\n\n\n<p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;plsql&#8221; decode=&#8221;true&#8221;]DROP INDEX `travel-sample`.ixtypectcy;<\/p>\n<p>CREATE INDEX ixtypectcy ON `travel-sample`(type, country DESC, city DESC);<\/p>\n<p>SELECT country, city<br \/>\nFROM `travel-sample`<br \/>\nWHERE type = &#8220;hotel&#8221;<br \/>\nORDER BY country DESC, city DESC<br \/>\nOFFSET 10<br \/>\nLIMIT 10;[\/crayon]<\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n0<\/p>\n\n\n\n<p>\u00a0<\/p>\n\n\n\n<p><span>Let\u2019s now look at a query that\u2019s not covered by the index. The query we saw earlier, we decide to retrieve the name of the hotel along with country and city. The name isn\u2019t in the index.<\/span><\/p>\n\n\n\n<p>\u00a0<\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n1<\/p>\n\n\n\n<p><span>In this case, again, the index has the data to cover the WHERE clause and the ORDER BY clause. The index scan will apply the predicate. The index order matches query order (note: there is only equality predicate on leading key. So, country and city are in order automatically). Index skips of the OFFSET number of qualified keys and returns the number of keys specified by the LIMIT clause. The engine retrieves the documents in order to maintain the order specified by ORDER BY clause.<\/span><\/p>\n\n\n\n<p><span>All these little optimizations together ensure the query performance is consistent whether OFFSET is 10, 100 or 1000. The only overhead between higher OFFSET is the index scan skipping over the additional items. Index key evaluation and skipping is significantly faster than getting all the documents and sorting.<\/span><\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n2<\/p>\n\n\n\n<p><span>Let\u2019s consider a longer running query with a potential of returning up to 24K documents.<\/span><\/p>\n\n\n\n<p><span class=\"theme:github lang:plsql highlight:0 decode:true crayon-inline \">create index idxtypeaiaid ON `travel-sample`(type, airline, airlineid) <\/span><\/p>\n\n\n\n<p><span>Even when the pagination query predicates and order by is completely covered by the index, there is a limited number of cases where the pagination LIMIT and OFFSET can be pushed to the index scan in order to make this very efficient.<\/span><\/p>\n\n\n\n<p><span>Here are general rules and requirements for pushing down OFFSET and LIMIT to the indexer. \u00a0Note these rules are not exhaustive and complete but give you a sense of when this optimization is done.<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>The query scan has to be on a single keyspace (Single reference in the FROM clause without JOINs.<\/span>\n<ol>\n<li><span>FROM `travel-sample` t<\/span><\/li>\n<li><span>FROM `travel-sample` hotel INNER JOIN `travel-sample` landmarks ON KEYS hotel.lmid<\/span><\/li>\n<\/ol>\n<\/li>\n\n<\/ol>\n\n\n\n<p><span>Case (a) qualifies, and (b) does not.<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>All the predicates in the query are pushed down to index scan (spans).<\/span>\n<ol>\n<li><span>We cannot push the\u00a0predicates (col LIKE \u201c%xyz\u201d) to index scan. Therefore the pagination cannot be pushed down as well.<\/span><\/li>\n<\/ol>\n<\/li>\n\n\n<li><span>All the predicates should be represented by a single span. If we have to do multiple index scans, post index scan processing (e.g. union scan, distinct scan in the explain), we cannot have the indexer evaluate the pagination. The query will execute without pushing the pagination clause to the index scan. Here are examples of predicate that generate single span.<\/span>\n<ol>\n<li><span>E.g. (a between 4 and 8 and a &lt;&gt; 12)<\/span><\/li>\n<li><span>(a = 5 OR a = 6)<\/span><\/li>\n<li><span>(a IN [4, 6, 9]) <\/span><\/li>\n<\/ol>\n<\/li>\n\n\n<li><span>All the predicates have to be exact.<\/span>\n<ol>\n<li><span>Exact predicates: (a = 5), (a between 9.8 and 9.99)<\/span><\/li>\n<li><span>Inexact predicates: (a &gt; 5 and a &gt; $param)<\/span><\/li>\n<\/ol>\n<\/li>\n\n\n<li><span>The query cannot contain aggregation, grouping, having clauses. These do the document reduction after the After the should not eliminate any documents after the index scan (no post index scan filtering\/reduction)<\/span><\/li>\n\n\n<li><span>The ORDER BY keys and the order of the keys should match the index key and index key order.<\/span>\n<ol>\n<li><span>CREATE INDEX i1 ON t(a, b, c);<\/span><\/li>\n<li><span>SELECT * FROM t WHERE a = 1 ORDER BY a, b, c;<\/span><\/li>\n<li><span>SELECT * FROM t WHERE a = 1 ORDER BY b, c;<\/span><\/li>\n<li><span>SELECT * FROM t WHERE a &gt; 3 ORDER BY b, c<\/span><\/li>\n<li><span>SELECT * FROM t WHERE a &gt; 3 ORDER BY a, b, c<\/span><\/li>\n<li><span>SELECT * FROM t WHERE (a LIKE \u201c%xyz\u201d) ORDER BY a, b, c OFFSET 10 LIMIT 5;<\/span><\/li>\n<li>\n<\/ol>\n<\/li>\n\n<\/ol>\n\n\n\n<p><span>The case (2) is a perfect match of the order.<\/span><\/p>\n\n\n\n<p><span>The case (3) doesn\u2019t match perfectly, but the leading key has an equality filter. So, we know the results from the scan will be in the order of (b, c). \u00a0Hence the pagination pushdown to the index is possible.<\/span><\/p>\n\n\n\n<p><span>For the case (4), the leading key has a range predicate (a &gt; 3) and therefore, t pagination pushdown is not possible.<\/span><\/p>\n\n\n\n<p><span>For the case (5), there is a perfect match of the ORDER BY clause to the index keys. Therefore, we can push down the pagination to the indexer, even if there is range predicate on the leading key.<\/span><\/p>\n\n\n\n<p><span>For the case (f), we can exploit the index order, but cannot push down the OFFSET and LIMIT because the complete predicate cannot be evaluated by the index scan. Since we get the results in the required order, after the OFFSET and LIMIT number of documents are projected, we terminate the index scan.<\/span><\/p>\n\n\n\n<p><span>For simplicity, I\u2019ve used one leading key. The statements are generalized and applicable to multiple leading keys.<\/span><\/p>\n\n\n\n<p><span>Obviously, this optimizer logic is complex! See additional information in the appendix on the requirements.<\/span><\/p>\n\n\n\n<p><b>OFFSET overhead:<\/b><\/p>\n\n\n\n<p><span>The initial travel-sample has about 900 documents with (type = \u201chotel\u201d). For experimentation, I simply increase the amount of data by inserting the same data again few times using the following query:<\/span><\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n3<\/p>\n\n\n\n<p><span>Now, let\u2019s try this query:<\/span><\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n4<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<tbody>\n<tr>\n<td><span>OFFSET<\/span><\/td>\n<td><span>LIMIT<\/span><\/td>\n<td><span>Response Time<\/span><\/td>\n<\/tr>\n<tr>\n<td><span>38000<\/span><\/td>\n<td><span>10<\/span><\/td>\n<td><span>28.97 ms<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p><span>As the OFFSET increases, the amount of time spent by the indexer to traverse through the index items increases. This affects both latency and throughput. \u00a0<\/span><\/p>\n\n\n\n<p><span>Can we improve this? Keyset pagination comes to the rescue. All the rules we mentioned for pagination optimization apply here as well. In addition, one of the predicates has to be UNIQUE so that we can clearly navigate from one page to another without duplicate documents appearing in multiple pages. With Couchbase, we\u2019re in luck. Each document in a bucket has a unique document key. It\u2019s referenced by META().id in the N1QL query.<\/span><\/p>\n\n\n\n<p><span>The basic insight is, customers typically want the NEXT page instead of some random page from the start of the resultset. So, when you fetch each page in order, remember the last set (HIGHEST\/LOWEST depending on the ORDER BY clause). Then, use that information to set the predicates and position the index for the next scan. This will avoid the wasted key processing during OFFSET.<\/span><\/p>\n\n\n\n<p><span>Let\u2019s look at an example:<\/span><\/p>\n\n\n\n<p><span>CREATE INDEX ixtypectcy ON `travel-sample`<\/span><span>\u00a0(type, country, city, META().id);<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>GET The FIRST PAGE. \u00a0Note that I\u2019ve added META().id to the index, projection and ORDER BY clause so it guarantees UNIQUE value in each of them.<\/span><\/li>\n\n<\/ol>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n5<\/p>\n\n\n\n<p>Results:<\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n6<\/p>\n\n\n\n<p><span>This query ran in 5.14 milliseconds.<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span> Now, construct the query for next page WITHOUT using OFFSET:<\/span><\/li>\n\n<\/ol>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n7<\/p>\n\n\n\n<p><span>How did we construct this query?<\/span><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><span>Reuse all the predicates in the WHERE clause. (type = \u201chotel\u201d).<\/span><\/li>\n\n\n<li><span>Now, take the keys in the ORDER BY and construct the additional predicates. So, simply take the highest value returned by the last query and \u00a0add the new predicates. \u00a0\u00a0Don\u2019t add the document key yet. \u00a0In this case, we\u2019re sorting in the ascending order. So, use the Greatherthan-OR-Equal-To operator.<\/span><\/li>\n\n<\/ol>\n\n\n\n<p>In this case, it is: (country &gt;= \u201cFrance\u201d AND city &gt;= \u201cAvignon\u201d).<\/p>\n\n\n\n<p><span>Now, to ensure unique value, we add the META().id predicate.<\/span><\/p>\n\n\n\n<p><span>(META().id &gt; &#8220;038c8a13-e1e7-4848-80ec-8819ff923602&#8221;)<\/span><\/p>\n\n\n\n<p><span>This executes in 5 milliseconds with the following plan. As you go through the subsequent pages, simply repeat the steps to construct the next query and avoid the OFFSET. You can continue to go forever with similar response time!<\/span><\/p>\n\n\n\n<p><p>[crayon theme=&#8221;github&#8221; whitespace-before=&#8221;1&#8243; whitespace-after=&#8221;1&#8243; lang=&#8221;js&#8221; decode=&#8221;true&#8221;]          {<br \/>\n            &#8220;index&#8221;: &#8220;ixtype&#8221;,<br \/>\n            &#8220;index_id&#8221;: &#8220;8630f5f7e05ee113&#8221;,<br \/>\n            &#8220;index_projection&#8221;: {<br \/>\n              &#8220;primary_key&#8221;: true<br \/>\n            },<br \/>\n            &#8220;keyspace&#8221;: &#8220;travel-sample&#8221;,<br \/>\n            &#8220;namespace&#8221;: &#8220;default&#8221;,<br \/>\n            &#8220;spans&#8221;: [<br \/>\n              {<br \/>\n                &#8220;exact&#8221;: true,<br \/>\n                &#8220;range&#8221;: [<br \/>\n                  {<br \/>\n                    &#8220;high&#8221;: &#8220;&#8221;hotel&#8221;&#8221;,<br \/>\n                    &#8220;inclusion&#8221;: 3,<br \/>\n                    &#8220;low&#8221;: &#8220;&#8221;hotel&#8221;&#8221;<br \/>\n                  }<\/p>\n<p>[\/crayon]<\/p>\n8<\/p>\n\n\n\n<p><span><strong>SUMMARY:<\/strong> <\/span><\/p>\n\n\n\n<p><span>You just learned how pagination works. You also learned how the Couchbase N1QL optimizer exploits the index to improve the efficiency of query pagination performance. \u00a0You can use keyset pagination techniques to improve the performance of OFFSET further using the technique written.<\/span><b><\/b><\/p>\n\n\n\n<p><b>References:<\/b><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.couchbase.com\/blog\/optimizing-database-pagination-using-couchbase-n1ql\/\">https:\/\/www.couchbase.com\/blog\/optimizing-database-pagination-using-couchbase-n1ql\/<\/a><\/li>\n\n\n<li><a href=\"https:\/\/www.slideshare.net\/MarkusWinand\/p2d2-pagination-done-the-postgresql-way?qid=237cb981-6e99-46e8-9197-96eacfa0bb86&amp;v=&amp;b=&amp;from_search=3\"><span>https:\/\/www.slideshare.net\/MarkusWinand\/p2d2-pagination-done-the-postgresql-way<\/span><\/a><\/li>\n\n\n<li><a href=\"https:\/\/use-the-index-luke.com\/sql\/partial-results\/fetch-next-page\"><span>https:\/\/use-the-index-luke.com\/sql\/partial-results\/fetch-next-page<\/span><\/a><\/li>\n\n\n<li><a href=\"https:\/\/blog.jooq.org\/2013\/10\/26\/faster-sql-paging-with-jooq-using-the-seek-method\/\"><span>https:\/\/blog.jooq.org\/2013\/10\/26\/faster-sql-paging-with-jooq-using-the-seek-method\/<\/span><\/a><\/li>\n\n<\/ol>\n\n\n\n<p><b>Appendix: <\/b><span>Optimize the query with order, offset, limit to exploit index ordering<\/span><\/p>\n\n\n\n<p><span>Query ORDER BY evaluation can exploit the Index order with the indexer returning results in the required order and the query does not have to modify the order (e.g. does grouping,join). Whenever the order of record changes we need to use sort data to satisfy the ORDER BY clause.<\/span><\/p>\n\n\n\n<p>\u00a0<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><span>Index order will not be used when the from clause contains aggregates.<\/span><\/li>\n\n\n<li><span>Index order will not be used when GROUP BY clause present.<\/span><\/li>\n\n\n<li><span>Index order will not be used when JOINs, NEST, or UNNEST are used.<\/span><\/li>\n\n\n<li><span>Index order will not be used when DISTINCT is present.<\/span><\/li>\n\n\n<li><span>Index order will not be used when SET operators are used. This includes UNION, UNION ALL, INTERSECT, EXCEPT, EXCEPTALL.<\/span><\/li>\n\n\n<li><span>Index order will not be used when Primary Scan is involved.<\/span><\/li>\n\n\n<li><span>Index order will not be used when intersect or union scan is involved.<\/span><\/li>\n\n\n<li><span>Index order will not be used when Array Indexes are involved.<\/span><\/li>\n\n\n<li><span>Index order will not be used when order collation (ASC, DESC) doesn&#8217;t match with index collation.<\/span><\/li>\n\n\n<li><span>Index order will not be used when any order term is not matched with index keys.<\/span><\/li>\n\n\n<li><span>If Query exploits the Index order Order operator will not present in the EXPLAIN of the query.<\/span><\/li>\n\n<\/ul>\n\n\n\n<p><span>OFFSET and LIMIT can be pushed to IndexScan <\/span><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><span>when the ORDER BY is present and is able to exploit the index order<\/span><\/li>\n\n\n<li><span>When query ORDER BY is not present<\/span><\/li>\n\n\n<li><span>All the predicates are pushed to indexer as part of the spans<\/span><\/li>\n\n\n<li><span>Pushed spans values are exact <\/span><\/li>\n\n\n<li><span>Based on spans Indexer doesn&#8217;t generate false positives<\/span><\/li>\n\n\n<li><span>Query does not further reduce index scan results.<\/span><\/li>\n\n\n<li><span>If the OFFSET is pushed to indexer &#8220;offset&#8221; will appear in IndexScan section of the EXPLAIN and Offset Operator will not present at the end of the EXPLAIN.<\/span><\/li>\n\n\n<li><span>If the limit is pushed to indexer &#8220;limit&#8221; will appear in IndexScan section of the EXPLAIN. <\/span><\/li>\n\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Read the pagination background in my previous article: https:\/\/www.couchbase.com\/blog\/optimizing-database-pagination-using-couchbase-n1ql\/ Pagination is the task of dividing the potential result into pages and retrieving the required pages, one by one on demand. \u00a0Using OFFSET and LIMIT is the easy way to write pagination into database queries. Together, OFFSET and LIMIT, make the pagination clause of the SELECT [&hellip;]<\/p>\n","protected":false},"author":55,"featured_media":1206,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[127,136,17,18],"tags":[289,192,30,290],"ppma_author":[291],"class_list":["post-1207","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-application-design","category-best-practices-and-tutorials","category-performance","category-n1ql-query","tag-application","tag-indexing","tag-json","tag-pagination"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.6 (Yoast SEO v27.6) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>OFFSET and Keyset Pagination in N1QL Query | Couchbase<\/title>\n<meta name=\"description\" content=\"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Database Pagination: Using OFFSET and Keyset in N1QL\" \/>\n<meta property=\"og:description\" content=\"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/\" \/>\n<meta property=\"og:site_name\" content=\"The Couchbase Blog\" \/>\n<meta property=\"article:published_time\" content=\"2018-01-11T19:56:34+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"853\" \/>\n\t<meta property=\"og:image:height\" content=\"363\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Keshav Murthy\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@rkeshavmurthy\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Keshav Murthy\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/\"},\"author\":{\"name\":\"Keshav Murthy\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/person\\\/c261644262bf98e146372fe647682636\"},\"headline\":\"Database Pagination: Using OFFSET and Keyset in N1QL\",\"datePublished\":\"2018-01-11T19:56:34+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/\"},\"wordCount\":2900,\"commentCount\":1,\"publisher\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/arrows2.png\",\"keywords\":[\"application\",\"Indexing\",\"JSON\",\"pagination\"],\"articleSection\":[\"Application Design\",\"Best Practices and Tutorials\",\"High Performance\",\"SQL++ \\\/ N1QL Query\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/\",\"name\":\"OFFSET and Keyset Pagination in N1QL Query | Couchbase\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/arrows2.png\",\"datePublished\":\"2018-01-11T19:56:34+00:00\",\"description\":\"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/arrows2.png\",\"contentUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/05\\\/arrows2.png\",\"width\":853,\"height\":363},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/offset-keyset-pagination-n1ql-query-couchbase\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Database Pagination: Using OFFSET and Keyset in N1QL\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\",\"name\":\"The Couchbase Blog\",\"description\":\"Couchbase, the NoSQL Database\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#organization\",\"name\":\"The Couchbase Blog\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/06\\\/logo.svg\",\"contentUrl\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/wp-content\\\/uploads\\\/sites\\\/5\\\/2026\\\/06\\\/logo.svg\",\"width\":\"1024\",\"height\":\"1024\",\"caption\":\"The Couchbase Blog\"},\"image\":{\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/#\\\/schema\\\/person\\\/c261644262bf98e146372fe647682636\",\"name\":\"Keshav Murthy\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g4e51d72fc07c662aa791316deafffac4\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g\",\"caption\":\"Keshav Murthy\"},\"description\":\"Keshav Murthy is a Vice President at Couchbase R&amp;D. Previously, he was at MapR, IBM, Informix, Sybase, with more than 20 years of experience in database design &amp; development. He lead the SQL and NoSQL R&amp;D team at IBM Informix. He has received two President's Club awards at Couchbase, two Outstanding Technical Achievement Awards at IBM. Keshav has a bachelor's degree in Computer Science and Engineering from the University of Mysore, India, and has received twenty four US patents.\",\"sameAs\":[\"https:\\\/\\\/blog.planetnosql.com\\\/\",\"https:\\\/\\\/x.com\\\/rkeshavmurthy\"],\"url\":\"https:\\\/\\\/www.couchbase.com\\\/blog\\\/author\\\/keshav-murthy\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"OFFSET and Keyset Pagination in N1QL Query | Couchbase","description":"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/","og_locale":"en_US","og_type":"article","og_title":"Database Pagination: Using OFFSET and Keyset in N1QL","og_description":"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.","og_url":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/","og_site_name":"The Couchbase Blog","article_published_time":"2018-01-11T19:56:34+00:00","og_image":[{"width":853,"height":363,"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png","type":"image\/png"}],"author":"Keshav Murthy","twitter_card":"summary_large_image","twitter_creator":"@rkeshavmurthy","twitter_misc":{"Written by":"Keshav Murthy","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#article","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/"},"author":{"name":"Keshav Murthy","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/c261644262bf98e146372fe647682636"},"headline":"Database Pagination: Using OFFSET and Keyset in N1QL","datePublished":"2018-01-11T19:56:34+00:00","mainEntityOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/"},"wordCount":2900,"commentCount":1,"publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png","keywords":["application","Indexing","JSON","pagination"],"articleSection":["Application Design","Best Practices and Tutorials","High Performance","SQL++ \/ N1QL Query"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/","url":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/","name":"OFFSET and Keyset Pagination in N1QL Query | Couchbase","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#primaryimage"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png","datePublished":"2018-01-11T19:56:34+00:00","description":"Pagination is a common application and has a major impact on customer experience. Here are detailed issues and solutions with Couchbase N1QL.","breadcrumb":{"@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#primaryimage","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/05\/arrows2.png","width":853,"height":363},{"@type":"BreadcrumbList","@id":"https:\/\/www.couchbase.com\/blog\/offset-keyset-pagination-n1ql-query-couchbase\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.couchbase.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Database Pagination: Using OFFSET and Keyset in N1QL"}]},{"@type":"WebSite","@id":"https:\/\/www.couchbase.com\/blog\/#website","url":"https:\/\/www.couchbase.com\/blog\/","name":"The Couchbase Blog","description":"Couchbase, the NoSQL Database","publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.couchbase.com\/blog\/#organization","name":"The Couchbase Blog","url":"https:\/\/www.couchbase.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/06\/logo.svg","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/5\/2026\/06\/logo.svg","width":"1024","height":"1024","caption":"The Couchbase Blog"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/c261644262bf98e146372fe647682636","name":"Keshav Murthy","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g4e51d72fc07c662aa791316deafffac4","url":"https:\/\/secure.gravatar.com\/avatar\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/af74df754db27152971d0aed2f323ead5a1f9fe5afd0209af91e12e784451224?s=96&d=mm&r=g","caption":"Keshav Murthy"},"description":"Keshav Murthy is a Vice President at Couchbase R&amp;D. Previously, he was at MapR, IBM, Informix, Sybase, with more than 20 years of experience in database design &amp; development. He lead the SQL and NoSQL R&amp;D team at IBM Informix. He has received two President's Club awards at Couchbase, two Outstanding Technical Achievement Awards at IBM. Keshav has a bachelor's degree in Computer Science and Engineering from the University of Mysore, India, and has received twenty four US patents.","sameAs":["https:\/\/blog.planetnosql.com\/","https:\/\/x.com\/rkeshavmurthy"],"url":"https:\/\/www.couchbase.com\/blog\/author\/keshav-murthy\/"}]}},"acf":[],"authors":[{"term_id":291,"user_id":55,"is_guest":0,"slug":"keshav-murthy","display_name":"Keshav Murthy","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/1207","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/users\/55"}],"replies":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/comments?post=1207"}],"version-history":[{"count":0,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/1207\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media\/1206"}],"wp:attachment":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media?parent=1207"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/categories?post=1207"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/tags?post=1207"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=1207"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}