{"id":13878419,"url":"https://github.com/planetscale/fast_page","last_synced_at":"2025-12-24T10:29:45.476Z","repository":{"id":56799985,"uuid":"522710053","full_name":"planetscale/fast_page","owner":"planetscale","description":"Blazing fast pagination for ActiveRecord with deferred joins ⚡️","archived":false,"fork":false,"pushed_at":"2023-07-18T20:04:23.000Z","size":62,"stargazers_count":291,"open_issues_count":3,"forks_count":6,"subscribers_count":18,"default_branch":"main","last_synced_at":"2024-04-14T09:48:57.508Z","etag":null,"topics":["activerecord","kaminari","mysql","pagination","pagy"],"latest_commit_sha":null,"homepage":"https://planetscale.com/blog/fastpage-faster-offset-pagination-for-rails-apps","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/planetscale.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-08T21:17:58.000Z","updated_at":"2024-08-06T08:47:05.642Z","dependencies_parsed_at":"2024-08-06T08:47:00.955Z","dependency_job_id":"dc60d68b-740b-4980-8e0f-f172400e679a","html_url":"https://github.com/planetscale/fast_page","commit_stats":{"total_commits":42,"total_committers":5,"mean_commits":8.4,"dds":"0.23809523809523814","last_synced_commit":"44efd52aff2282aa8e583f3b7f1cada3c1f616ea"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/planetscale%2Ffast_page","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/planetscale%2Ffast_page/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/planetscale%2Ffast_page/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/planetscale%2Ffast_page/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/planetscale","download_url":"https://codeload.github.com/planetscale/fast_page/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226138849,"owners_count":17579496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","kaminari","mysql","pagination","pagy"],"created_at":"2024-08-06T08:01:49.077Z","updated_at":"2025-12-24T10:29:45.471Z","avatar_url":"https://github.com/planetscale.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"\u003cimg width=\"1280\" alt=\"FastPage by PlanetScale\" src=\"https://user-images.githubusercontent.com/6104/184650870-3c75026a-61bf-4a7b-8662-66ca910fdb94.png\"\u003e\n\n**`FastPage` applies the MySQL \"deferred join\" optimization to your ActiveRecord offset/limit queries.⚡️**\n\n[![See on RubyGems](https://badge.fury.io/rb/fast_page.svg)](https://badge.fury.io/rb/fast_page)\n\n## Usage\n\nAdd `fast_page` to your Gemfile.\n\n```ruby\ngem 'fast_page'\n```\n\nYou can then use the `fast_page` method on any ActiveRecord::Relation that is using offset/limit.\n\n### Example\nHere is a slow pagination query:\n```ruby\nPost.all.order(created_at: :desc).limit(25).offset(100)\n# Post Load (1228.7ms)  SELECT `posts`.* FROM `posts` ORDER BY `posts`.`created_at` DESC LIMIT 25 OFFSET 100\n```\n\nAdd `.fast_page` to your slow pagination query. It breaks it up into two, much faster queries.\n```ruby\nPost.all.order(created_at: :desc).limit(25).offset(100).fast_page\n# Post Pluck (456.9ms)  SELECT `posts`.`id` FROM `posts` ORDER BY `posts`.`created_at` DESC LIMIT 25 OFFSET 100 \n# Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (1271528, 1271527, 1271526, 1271525, 1271524, 1271523, 1271522, 1271521, 1271520, 1271519, 1271518, 1271517, 1271516, 1271515, 1271514, 1271512, 1271513, 1271511, 1271510, 1271509, 1271508, 1271507, 1271506, 1271505, 1271504) ORDER BY `posts`.`created_at` DESC\n```\n\n## Benchmarks\nWe wanted to see just how much faster using the deferred join could be. We took a table with about ~1 million records in it and benchmarked the standard ActiveRecord offset/limit query vs the query with FastPage.\n\nHere is the query:\n```ruby\nAuditLogEvent.page(num).per(100).where(owner: org).order(created_at: :desc)\n```\n\nBoth `owner` and `created_at` are indexed.\n\n\u003cimg width=\"781\" alt=\"Graph of FastPage vs standard ActiveRecord performance\" src=\"https://user-images.githubusercontent.com/6104/184651344-9e3044a9-fda1-4f09-aa8b-58647063b282.png\"\u003e\n\nAs you can see in the chart above, it's significantly faster the further into the table we paginate.\n\n## Compatible pagination libraries\n`FastPage` has been tested and works with these existing popular pagination gems. If you try it with any other gems, please let us know!\n\n### Kaminari\nAdd `.fast_page` to the end of your existing [Kaminari](https://github.com/kaminari/kaminari) pagination queries.\n\n```ruby\nPost.all.page(5).per(25).fast_page\n```\n\n### Pagy\nIn any controller that you want to use `fast_page`, add the following method. This will modify the query [Pagy](https://github.com/ddnexus/pagy) uses when retrieving the records.\n\n```ruby\ndef pagy_get_items(collection, pagy)\n  collection.offset(pagy.offset).limit(pagy.items).fast_page\nend\n```\n\n\n## How this works\n\nThe most common form of pagination is implemented using LIMIT and OFFSET.\n\nIn this example, each page returns 50 blog posts. For the first page, we grab the first 50 posts. On the 2nd page we grab 100 posts and throw away the first 50. As the `OFFSET` increases, each additional page becomes more expensive for the database to serve.\n\n```sql\n-- Page 1\nSELECT * FROM posts ORDER BY created_at DESC LIMIT 50;\n-- Page 2\nSELECT * FROM posts ORDER BY created_at DESC LIMIT 50 OFFSET 50;\n-- Page 3\nSELECT * FROM posts ORDER BY created_at DESC LIMIT 50 OFFSET 100;\n```\n\nThis method of pagination works well until you have a large number of records. The later pages become very expensive to serve. Because of this, applications will often have to limit the maximum number of pages they allow users to view or swap to cursor based pagination.\n\n### Deferred join technique\n\n[High Performance MySQL](https://learning.oreilly.com/library/view/high-performance-mysql/9781492080503/) recommends using a \"deferred join\" to increase the efficiency of LIMIT/OFFSET pagination for large tables.\n\n```sql\nSELECT * FROM posts \nINNER JOIN(select id from posts ORDER BY created_at DESC LIMIT 50 OFFSET 10000) \nAS lim USING(id);\n```\n\nNotice that we first select the ID of all the rows we want to show, then the data for those rows. This technique works \"because it lets the server examine as little data as possible in an index without accessing rows.\"\n\nThe FastPage gem makes it easy to apply this optimization to any `ActiveRecord::Relation` using offset/limit.\n\nTo learn more on how this works, check out this blog post: [Efficient Pagination Using Deferred Joins](https://aaronfrancis.com/2022/efficient-pagination-using-deferred-joins)\n\n## When should I use this?\n`fast_page` works best on pagination queries that include an `ORDER BY`. It becomes more effective as the page number increases. You should test it on your application's data to see how it improves your query times.\n\nWe have only tested `fast_page` with MySQL. It likely does not produce the same results for other databases. If you test it, please let us know!\n\nBecause `fast_page` runs 2 queries instead of 1, it is very likely a bit slower for early pages. The benefits begin as the user gets into deeper pages. It's worth testing to see at which page your application gets faster from using `fast_page` and only applying to your queries then.\n\n```ruby\nposts = Post.all.page(params[:page]).per(25)\n# Use fast page after page 5, improves query performance\nposts = posts.fast_page if params[:page] \u003e 5\n```\n\n## Thank you :heart:\nThis gem was inspired by [Hammerstone's `fast-paginate` for Laravel](https://github.com/hammerstonedev/fast-paginate) and [@aarondfrancis](https://github.com/aarondfrancis)'s excellent blog post: [Efficient Pagination Using Deferred Joins](https://aaronfrancis.com/2022/efficient-pagination-using-deferred-joins). We were so impressed with the results, we had to bring this to Rails as well.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/planetscale/fast_page. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/planetscale/fast_page/blob/main/CODE_OF_CONDUCT.md).\n\n## License\n\nThe gem is available as open source under the terms of the [Apache-2.0 license](https://github.com/planetscale/fast_page/blob/main/LICENSE).\n\n## Code of Conduct\n\nEveryone interacting in the FastPage project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/planetscale/fast_page/blob/main/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplanetscale%2Ffast_page","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fplanetscale%2Ffast_page","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplanetscale%2Ffast_page/lists"}