69

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,480
  • 3
  • 40
  • 78
C.J.
  • 6,621
  • 7
  • 34
  • 44
  • 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

89

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
  • 54,819
  • 18
  • 110
  • 126
  • 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
  • 1
    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
  • 1
    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
16

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();
Rudey
  • 4,480
  • 3
  • 40
  • 78
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,773
  • 4
  • 36
  • 51
  • 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
  • 2
    This is evil. You should never run parallel operations on the same DB context instance. https://docs.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
  • 4,657
  • 7
  • 48
  • 66
  • 5
    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