user1106897
user1106897

Reputation: 77

Pagination and Entity Framework

In my mobile app, I try to retrieve data from a table of my SQL Server database. I'm using EF and I try to use pagination for better performance. I need to retrieve data from the last element of the table. so if the table has 20 rows, I need, for page 0, IDs 20, 19, 18, 17, 16 then for page 1 IDs 15, 14, 13, 12, 11 and so on...

The problem is this: what if, while user "A" is downloading data from table, user "B" add row? If user "A" get Page 0 (so IDs 20, 19, 18, 17, 16), and user "B" in the same moment add row (so ID 21), with classic query, user "A" for page 1 will get IDs 16, 15, 14, 13, 12... so another time ID 16

My code is very simple:

int RecordsForPagination = 5; 
var list = _context.NameTable
                   .Where(my_condition)
                   .OrderByDescending(my_condition_for ordering)
                   .Skip (RecordsForPagination * Page)
                   .Take (RecordsForPagination)
                   .ToList();

Of course Page is int that come from the frontend.

How can I solve the problem?

I found a solution but I don't know if it's the perfect one. I could use

.SkipWhile(x => x.ID >= LastID) 

instead

.Skip (RecordsForPagination * Page)

and of course LastID always is sent from the frontend.

Do you think that performance is always good with this code? Is there a better solution?

Upvotes: 1

Views: 778

Answers (1)

Chris Schaller
Chris Schaller

Reputation: 16689

The performance impacts will depend greatly on your SQL Index implementation and the order by clause. But its not so much a question about performance as it is about getting the expected results.

Stack Overflow is a great example where there is a volume of activity such that when you get to the end of any page, the next page may contain records from the page you just viewed because the underlying recordset has changed (more posts have been added)

I bring this up because in a live system it is generally accepted and in some cases an expected behaviour. As developers we appreciate the added overheads of trying to maintain a single result set and recognise that there is usually much lower value in trying to prevent what looks like duplications as you iterate the pages.

It is often enough to explain to users why this occurs, in many cases they will accept it

If it was important to you to maintain the place in the original result set, then you should constrain the query with a Where clause, but you'll need to retrieve either the Id or the timestamp in the original query. In your case you are attempting to use LastID, but to get the last ID would require a separate query on it's own because the orderby clause will affect it.

You can't really use .SkipWhile(x => x.ID >= LastID) for this, because skip is a sequential process that is affected by the order and is dis-engaged the first instance that the expression evaluates to false, so if your order is not based on Id, your skip while might result is skipping no records at all.

int RecordsForPagination = 5; 
int? MaxId = null;
...
var query = _context.NameTable.Where(my_condition);
// We need the Id to constraint the original search
if (!MaxId.HasValue)
    MaxId = query.Max(x => x.ID);

var list = query.Where(x => x.ID <= MaxId)
                .OrderByDescending(my_condition_for ordering)
                .Skip(RecordsForPagination * Page)
                .Take(RecordsForPagination);
                .ToList();

It is generally simpler to filter by a point in time as this is known from the client without a round trip to the DB, but depending on the implementation the filtering on dates can be less efficient.

Upvotes: 2

Related Questions