-
-
Notifications
You must be signed in to change notification settings - Fork 521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cursor Based Pagination #300
Comments
Thank you for raising this. Yes this knowledge is helpful. There isn't too many things SeaORM need to do to support cursor based pagination, because you can: let mut finder = fruit::Entity::find()
.filter(fruit::Column::key.gt(cursor))
.order_by_asc(fruit::Column::key);
finder.query().limit(10);
let result = finder.all().await; I could think of adding a finder.first(10).await; which probably could be a nice bit of sugar. |
Thanks for that and do agree that it's mostly simple. Just a question about that example, doesn't Edit: Can just do the following: sea_orm::QuerySelect::query(&mut finder).limit(10); |
Yes, you are right. It kinda broke the fluent interface. But it shouldn't bother you, because no additional Anyway, I think adding |
I think the implementation of |
Cursor pagination is actually freaking hard to get right specially when you add ordering on arbitrary columns. I wrote a whole internal framework for Caido using sea-query and from memory there are around 10-12 different edge cases you need to consider when you do the full package: before, after, first, last, order (asc/desc). I actually need to write a blog post on that at some point but it is not trivial. |
Just created this PR for the |
Thank you for the PR. I think Sytten's suggestion sounds good
|
For the simple base cases (that is without special ordering & assuming an ID that is sortable):
Happy to help if you want to add ordering in the mix but it becomes exponentially more difficult even with one parameter. |
I imagine the API to be... let mut cursor = fruit::Entity::find().cursor();
let res = cursor
.after(1) // Filter by primary key > 1
.first(100) // Sort by primary key in descending order
.all(db);
let res = cursor
.before((10, 12)) // Filter by composite primary key < (10, 12)
.last(5) // Sort by composite primary key in ascending order
.all(db); To keep things simple, here I only allow primary key to be used as cursor and sort by primary key only. |
Hey everyone, please check #754 |
I'm confused about how composite key pagination works. For example lets say you have a table like:
Now you want to build an API that allows sorting by category. Category isn't unique so in order to page you need to combine with the ID column into a composite key. Lets query the first 3 results: SELECT * FROM example
ORDER BY category ASC, id ASC
LIMIT 3 You get back results:
Now we want to get the next page, according to the current implementation this creates a query like: SELECT * FROM example
WHERE category > 'A' AND id > 6
ORDER BY category ASC, id ASC
LIMIT 3 But this is surely wrong - we would end up only getting two more results, missing out many inbetween
Should we not actually have a query like: SELECT * FROM example
WHERE (category = 'A' and id > 6) OR category > 'A'
ORDER BY category ASC, id ASC
LIMIT 3 which produces:
|
Thank you for the detailed report with example. Can you open a new issue? Also, what do you think would be the case as in composite key composed of 3 columns? I imagine by induction the first two columns would be "= or >" , or will there be more complications? |
Composite key become exponentially worse, I suggest taking a look at the algorithm prisma uses for cursor pagination in their rust engine. It supports quite a bit of things. |
@Sytten I am not an expert on this topic... but I was under the impression the performance was acceptable if used with a composite index? https://www.postgresql.org/docs/current/indexes-multicolumn.html |
Not harder in terms of computation on the database side, mostly in terms of implementing a generic algorithm for it. Even more so if you want to order said collection. |
It would be fantastic if SeaORM could support pagination in an efficient manner, namely by using a combination of
first
/after
orlast
/before
count/cursor combinations.I see that the current implementation uses
OFFSET
andLIMIT
to do pagination, which will work just fine for small tables, but quickly becomes an issue as the size of the table grows. Imagine that anOFFSET
grows very large, then this becomes necessarily a lot of work for the database as the entire ordered result set containingOFFSET
+LIMIT
rows needs to be computed thenOFFSET
rows skipped over.This topic has been written about many, many times.
A more efficient way of tackling this problem would be to use a cursor based pagination where the database can leverage its indexes to produce the ordered result set starting from the cursor. e.g.
SELECT id, name FROM table WHERE table.id >= $after LIMIT $first;
.This feels like it might be a necessary approach for users wanting to use this ORM at anything resembling scale and would greatly assist implementation for systems that de facto use cursor based pagination (i.e., graphQL).
It's unlikely I'll be able to contribute a PR to implement this unfortunately, but figured I'd raise the issue for anyone else.
The text was updated successfully, but these errors were encountered: