77

Currently when I need to run a query that will be used w/ paging I do it something like this:

//Setup query (Typically much more complex)
var q = ctx.People.Where(p=>p.Name.StartsWith("A"));

//Get total result count prior to sorting
int total = q.Count();       

//Apply sort to query
q = q.OrderBy(p => p.Name);  

q.Select(p => new PersonResult
{
   Name = p.Name
}.Skip(skipRows).Take(pageSize).ToArray();

This works, but I wondered if it is possible to improve this to be more efficient while still using linq? I couldn't think of a way to combine the count w/ the data retrieval in a single trip to the DB w/o using a stored proc.

Rudey
  • 4,717
  • 4
  • 42
  • 84
C.J.
  • 6,789
  • 7
  • 36
  • 45
  • 1
    Take a look at [How should I expose the total record count and IEnumable collection of paged records from my service layer method?](http://stackoverflow.com/questions/6417886/how-should-i-expose-the-total-record-count-and-ienumable-collection-of-paged-reco/6418761#6418761) – Eranga Oct 14 '11 at 13:54
  • Interesting, but it seems to be the same as what I'm doing. It makes 2 distinct calls to the database. One for total count and another for the data page. – C.J. Oct 14 '11 at 14:10
  • 3
    EF does not have future queries like nHibernate. It will take 2 trips to database. – Eranga Oct 14 '11 at 14:13
  • Jeff Ogata's answer shows technical possibility to do 1 call, but it's better to have simple design with readable code and have 2 calls. Do not do premature optimization. – Michael Freidgeim Mar 10 '17 at 21:57
  • It worked in .net core 2.2 when I updated the version .net core to 3.1 It didn't work. would you please help me? – sajjad Feb 17 '20 at 10:47

4 Answers4

95

The following query will get the count and page results in one trip to the database, but if you check the SQL in LINQPad, you'll see that it's not very pretty. I can only imagine what it would look like for a more complex query.

var query = ctx.People.Where (p => p.Name.StartsWith("A"));

var page = query.OrderBy (p => p.Name)
                .Select (p => new PersonResult { Name = p.Name } )          
                .Skip(skipRows).Take(pageSize)
                .GroupBy (p => new { Total = query.Count() })
                .First();

int total = page.Key.Total;
var people = page.Select(p => p);

For a simple query like this, you could probably use either method (2 trips to the database, or using GroupBy to do it in 1 trip) and not notice much difference. For anything complex, I think a stored procedure would be the best solution.

Jeff Ogata
  • 56,645
  • 19
  • 114
  • 127
  • 20
    It will throw an error if the table has no records. To solve it, just replace .First() with .FirstOrDefault() and remember to check if the result is not null. – Jone Polvora Nov 16 '13 at 21:21
  • I also have same problem because I currently used DAPPER and it has query multiple option to retrieve multiple queries in single call.adrift solution is admirable witch I already think it was not possible in EF. many thanks adrift. – Prageeth godage Sep 21 '14 at 01:17
  • Beautifully done! I needed this for a paging grid of data, as I'm sure the other users did, and I would've never thought of this on my own, so thanks so much! You have my up-vote! – Jamie Nordmeyer Aug 05 '15 at 04:37
  • BAM! Good one! Just perfect for a simple query like I'm using – CodeHacker Mar 22 '16 at 16:06
  • 3
    It's a clever trick, but to have code maintainable it's better to have simple design with 2 calls – Michael Freidgeim Mar 10 '17 at 21:42
  • Note this also doesn't work correctly if you skip past the entire row set It would need to be changed to .FirstOrDefault() or .ToList() instead of .First() – Matt Vukomanovic Aug 04 '17 at 12:31
  • 4
    This probably never performs better than just doing two queries. My answer improves a little bit on this, but unfortunately the decreased performance still isn't worth it. – Rudey Jun 26 '18 at 15:58
  • I liked the look of this, but even with `FirstOrDefault` it doesn't give you the total if you have records but skip them all (i.e. with a page number that is too high). – moreginger Sep 25 '19 at 08:40
21

Jeff Ogata's answer can be optimized a little bit.

var results = query.OrderBy(p => p.Name)
                   .Select(p => new
                   {
                       Person = new PersonResult { Name = p.Name },
                       TotalCount = query.Count()
                   })          
                   .Skip(skipRows).Take(pageSize)
                   .ToArray(); // query is executed once, here

var totalCount = results.First().TotalCount;
var people = results.Select(r => r.Person).ToArray();

This does pretty much the same thing except it won't bother the database with an unnecessary GROUP BY. When you are not certain your query will contain at least one result, and don't want it to ever throw an exception, you can get totalCount in the following (albeit less cleaner) way:

var totalCount = results.FirstOrDefault()?.TotalCount ?? query.Count();

And the async variant:

var totalCount = await results.FirstOrDefaultAsync()?.TotalCount ?? await query.CountAsync();
Rudey
  • 4,717
  • 4
  • 42
  • 84
  • won't i query the count for each item ? – Jonathan ANTOINE Jul 09 '18 at 14:04
  • No. The database engine will optimize the query and only perform the count once. – Rudey Jul 09 '18 at 20:30
  • 1
    @JonathaANTOINE if you're using EFCore >= 1.1.x then yes it will. – SimonGates Jan 17 '19 at 16:08
  • 3
    Just tested this on EFCore 2.21. Only produces one query when written as above. However, if instead of specifying each field, you do a Person = p, it will produce a count for each row. – Brian McCord Apr 22 '19 at 19:18
  • what if we're not in Core at all? will this work in EF 6.x? – Matt Feb 12 '20 at 18:46
  • 1
    the 'totalCount" has a limitation, will give ZERO after row finished, which is incorrect. I have improvised the last a bit `code` var totalCount = results.FirstOrDefault()?.TotalCount ?? await query.CountAsync(); – Deepak Shaw Jul 30 '20 at 13:43
  • @DeepakShaw good point, I've edited my answer to include your fix. – Rudey Jul 31 '20 at 07:58
  • 2
    Has anyone tried this in .net 6 (ef core 6) because it looks like this will produce 2 queries now. – sommmen Apr 08 '22 at 10:35
  • @sommmen, yes it is producing 2 queries. One for count and another one for the actual data. However I observed that the time lag is almost negligible compared to one query. – Pradeep Kumar Sep 21 '22 at 06:52
10

Important Note for People using EF Core >= 1.1.x && < 3.0.0:

At the time I was looking for solution to this and this page is/was Rank 1 for the google term "EF Core Paging Total Count".

Having checked the SQL profiler I have found EF generates a SELECT COUNT(*) for every row that is returned. I have tired every solution provided on this page.

This was tested using EF Core 2.1.4 & SQL Server 2014. In the end I had to perform them as two separate queries like so. Which, for me at least, isn't the end of the world.

var query = _db.Foo.AsQueryable(); // Add Where Filters Here.


var resultsTask = query.OrderBy(p => p.ID).Skip(request.Offset).Take(request.Limit).ToArrayAsync();
var countTask = query.CountAsync();

await Task.WhenAll(resultsTask, countTask);

return new Result()
{
    TotalCount = await countTask,
    Data = await resultsTask,
    Limit = request.Limit,
    Offset = request.Offset             
};

It looks like the EF Core team are aware of this:

https://github.com/aspnet/EntityFrameworkCore/issues/13739 https://github.com/aspnet/EntityFrameworkCore/issues/11186

SimonGates
  • 5,961
  • 4
  • 40
  • 52
  • 1
    It seems that it has been solved at EF Core 3.0.0 according to this: https://github.com/aspnet/EntityFrameworkCore/issues/11186#issuecomment-499290221 – MÇT Aug 04 '19 at 10:47
  • 6
    This is evil. You should never run parallel operations on the same DB context instance. https://learn.microsoft.com/en-us/ef/core/querying/async – Владимiръ Apr 26 '20 at 18:31
5

I suggest making two queries for the first page, one for the total count and one for the first page or results.

Cache the total count for use as you move beyond the first page.

Bryan
  • 5,065
  • 10
  • 51
  • 68
  • 7
    Caching the total can cause inconsistency, if number of records changed between the first and subsequent page calls – Michael Freidgeim Mar 10 '17 at 21:27
  • 1
    it can, but often it doesn't matter, especially if there are many ages of results. When I needed the count and results together a single query was too slow and hard to read compared with two queries. – Bryan Mar 14 '17 at 12:39
  • 2
    Just make sure your cached total count is cached specifically for any where clause. If your first query is `ctx.People.Where (p => p.Name.StartsWith("A"))`, you don't want to reuse the total count on the next query `ctx.People.Where (p => p.Name.StartsWith("B"))` – xr280xr Jun 23 '17 at 20:38