22

Is there a way to get the row count of a complex Linq query and millions of records without hitting the db twice or writing 2 separate queries??

I might have my own suggestion. Write a stored procedure, but I'm good with MySQL not MSSQL.

Any better suggestions would be great. Also, if anyone knows if Microsoft is working on adding this feature to the entity framework.

Jason Foglia
  • 2,414
  • 3
  • 27
  • 48
  • 3
    I quite confident you cannot get the number of rows in your query without either hitting the database or writing a query separate from the one that actually returns those rows. – Jonathan Wood Apr 13 '12 at 17:54
  • 1
    When you use .Count() in EF it does not select all rows, it only executes a `select count() from table` sql statement - so while you do need 2 queries, one of them is very cheap. – JK. Apr 16 '12 at 23:02
  • @JK `select count()` is not cheap at all! Actually it has almost the same complexity as actual fetching of data, the only difference is instead of fetching rows it only counts them. But it still have to perform all scans, etc. – Vladimir Perevalov Apr 17 '12 at 09:15
  • If I have a complex query that just counts the results, which could be 10,000+, another that would just grabbed 20 of those results. Will Count() tax the entire process? – Jason Foglia Apr 18 '12 at 15:53
  • Just to throw this out there. I tested the Count then Results execution time in milliseconds and found this: Total Records:1,324,224, Count time AVG: 125, 20 items - Results time AVG: 2850 – Jason Foglia Apr 18 '12 at 17:41
  • [MSSQL solution](http://stackoverflow.com/questions/109232/what-is-the-best-way-to-paginate-results-in-sql-server) – Ernest Jan 18 '17 at 19:22

6 Answers6

17

I'd suggest using the Take() function. This can be used to specify the number of records to take from a linq query or List. For example

List<customers> _customers = (from a in db.customers select a).ToList();
var _dataToWebPage = _customers.Take(50);

I use a similar technique in an MVC app where I write the _customers list to the session and then use this list for further pagination queries when the user clicks on page 2, 3 etc. This saves multiple database hits. However if your list is very large then writing it too the session is probably not a good idea.

For pagination you can use the Skip() and Take() function together. For example to get page 2 of the data :

var _dataToWebPage = _customers.Skip(50).Take(50);
Tim Newton
  • 817
  • 1
  • 11
  • 24
  • 5
    Thanks @TimNewton. Of course, but if I have a million records or better yet a thousand records with very large data columns you run into an out of memory exception. – Jason Foglia Apr 16 '12 at 23:31
  • Jason, you could consider writing the primary keys to a List in the session rather than the entire object, then retrieving the details from the database each time you need to redisplay the data using the primary keys from the list? This still requires multiple database reads though. I dont think you can get away without multiple db reads if you dataset is so large. – Tim Newton Apr 17 '12 at 20:29
  • 1
    you might be right about not being able to make one db read for this instance in c# and using Linq. I know this can be done, its just that I want the ability to use Linq for that it's strongly typed and goes with the rest of my clean code. I have pagination working perfectly on smaller tables that don't hold large data sets or large data. – Jason Foglia Apr 17 '12 at 21:06
  • 63
    calling `ToList()` before `Take(50)` means you pull every record into your application. And then, after making the server do all that work, you ignore all but 50. Call `Take(50)` before `ToList()` – Don Cheadle Mar 02 '16 at 21:40
9

I was recently inspired by (copied from) this Code Project article Entity Framework pagination

The code:

public async Task<IList<SavedSearch>> FindAllSavedSearches(int page, int limit)
{
    if (page == 0)
        page = 1;

    if (limit == 0)
        limit = int.MaxValue;

    var skip = (page - 1) * limit;

    var savedSearches = _databaseContext.SavedSearches.Skip(skip).Take(limit).Include(x => x.Parameters);
    return await savedSearches.ToArrayAsync();
}

I'm not experienced with Entity Framework and I've not tested it for performance, so use with caution :)

jk1990
  • 341
  • 3
  • 12
2

The common way to show millions of records is simply not to display all pages. Think of it: if you have millions of records, say 20 or even 100 items per page, then you'll have tens of thousands of pages. It does not make sense to show them all. You can simply load the current page and provide a link to the next page, that's it. Or you may load say 100-500 records, but still show only one page and use the loaded records information to generate page links for first several pages (so know for sure how many next pages are available).

Vladimir Perevalov
  • 4,059
  • 18
  • 22
  • 1
    You are right, I would not want to show hundreds or thousands of links or even make someone step through that many pages. I guess the trouble I'm having is that I want to write just one query. For instance: MySQL allowed you to use SQL_CALC_FOUND_ROWS within a query and then another query to pull just that result. Very efficient!!! Made Pagination wonderful! Thanks @VladimirPerevalov for your thoughts! – Jason Foglia Apr 13 '12 at 18:46
  • AFAIK there is no such support in LINQ neither in MS SQL Server. Actually there are many things that MySql does and MS SQL does not. E.g. `SELECT BETWEEN ...` is also very effective for pagination. – Vladimir Perevalov Apr 13 '12 at 18:49
  • you can also provide a text box "Go To:" and a button to jump to a specific page directly. –  Apr 13 '12 at 18:51
  • 3
    Actually, it is quite common to show some sort of pager controls that indicate the total number of pages/items. – Jonathan Wood Apr 13 '12 at 18:59
  • 1
    @Jonathan Wood look at results from search engines for example. They show only ~10 pages. And an `approximate` number of total results (but is quite the other question). – Vladimir Perevalov Apr 13 '12 at 19:00
  • @Vladimir, yes some sites do that. In the case of Google, there could literally be over a billion results. I guess if you expect millions of results, then I'd tend to agree. But for smaller amounts, most sites do indicate the total row count and allow me to easily jump to the last one. – Jonathan Wood Apr 13 '12 at 19:03
  • @Jonathan Wood I agree, this depends more on the actual data and how it is generally used. I'm there are examples, where user really need to quickly navigate over all of those millions (or even thousands) of records. – Vladimir Perevalov Apr 13 '12 at 19:07
2

It is so easy on SQL Server.

You can write this query:

select count() over(), table.* from table

The count () over() will return the count of the total rows in the result, so you don't need to run two queries. Remember that you should run raw SQL on your context or use Dapper, which returns the result as a view model.

RoastBeast
  • 1,059
  • 2
  • 22
  • 38
2

If you need a quick solution you can use XPagedList https://github.com/dncuug/X.PagedList. XPagedList is a library that enables you to easily take an IEnumerable/IQueryable, chop it up into "pages", and grab a specific "page" by an index. For example

var products = await _context.Products.ToPagedListAsync(pageNumber, pageSize)
Chitova263
  • 719
  • 4
  • 13
0

I created a nuget library that does pagination for you. https://github.com/wdunn001/EntityFrameworkPaginateCore

add nuget to project

Install-Package EntityFrameworkPaginateCore add

using EntityFrameworkPaginateCore; to you provider

Has 1 method and 2 overloads for that method overloads allow sorting and filtering. use the sort object and the filter objects

public async Task<Page<Example>> GetPaginatedExample(
            int pageSize = 10, 
            int currentPage = 1, 
            string searchText = "", 
            int sortBy = 2
            )
        {
            var filters = new Filters<Example>();
                filters.Add(!string.IsNullOrEmpty(searchText), x => x.Title.Contains(searchText));

            var sorts = new Sorts<Example>();
            sorts.Add(sortBy == 1, x => x.ExampleId);
            sorts.Add(sortBy == 2, x => x.Edited);
            sorts.Add(sortBy == 3, x => x.Title);

            try
            {
                return await _Context.EfExample.Select(e => _mapper.Map<Example>(e)).PaginateAsync(currentPage, pageSize, sorts, filters);
            }
            catch (Exception ex)
            {
                throw new KeyNotFoundException(ex.Message);
            }
        }
Vinez
  • 560
  • 2
  • 11